# v0.2.0 Migration Plan

## Agent Event Cleanup

Delete all old skill/soul events from each agent's relay set before restarting with the new genesis file.


---

## Genesis File Changes

### Kind 0 profile event (new)

Add a startup event for the agent's profile:

```jsonc
{
  "kind": 0,
  "content_fields": {
    "name": "Didactyl",
    "about": "I am a Didactyl agent living on Nostr"
  },
  "tags": []
}
```

### Kind 3 contacts event (new)

Add a startup event following the admin:

```jsonc
{
  "kind": 3,
  "content": "",
  "tags": [
    ["p", "ADMIN_HEX_PUBKEY"]
  ]
}
```

The admin hex pubkey is derived from the `admin.pubkey` field at startup.

### Default skill rename and trigger tags

**Before:**
```jsonc
"default_skill": {
  "d_tag": "didactyl-default",
  "kind": 31124,
  "content": "# Didactyl Agent\n\n...\n\n---template---\n\n- section: admin_identity\n  ...",
  "tags": [["app", "didactyl"], ["scope", "private"]]
}
```

**After:**
```jsonc
"default_skill": {
  "d_tag": "default_admin_dm",
  "kind": 31124,
  "content": "# Didactyl Agent\n\nYou are {{my_kind0_profile}}\n\nYour npub: {{my_npub}}\n\n## Rules\n\n- Communicate through encrypted Nostr direct messages\n- Keep responses concise and clear\n- Be helpful and technically accurate\n- If unsure, state uncertainty directly\n- Use tools when a request requires taking action\n- After a tool call, base your answer on the actual tool result\n- Never claim a tool was run if no tool was executed\n- Maintain your task list as short-term working memory\n- Never reveal your private key (nsec)\n- You may share your public key (npub) with anyone",
  "tags": [
    ["app", "didactyl"],
    ["scope", "private"],
    ["description", "Default admin DM handler"],
    ["trigger", "dm"],
    ["filter", "{\"from\":\"admin\"}"]
  ]
}
```

Key changes:
- `d_tag` renamed to `default_admin_dm`
- `---template---` DSL removed entirely
- Content uses `{{my_kind0_profile}}` and `{{my_npub}}` for identity injection
- Trigger/filter tags added so the new trigger-scoped system fires it on admin DMs
- Description tag added

---

## Source Code Changes

### Phase 1: Remove `---template---` DSL

#### `src/prompt_template.h`

- Remove `PROMPT_TEMPLATE_MARKER`, `PROMPT_TEMPLATE_MAX_SECTIONS`, `PROMPT_TEMPLATE_MAX_NAME_LEN`, `PROMPT_TEMPLATE_MAX_ROLE_LEN`
- Remove `prompt_template_section_t` struct
- Remove `prompt_template_t` struct
- Remove `prompt_template_emit_hook_fn` typedef
- Remove `prompt_template_parse()` declaration
- Remove `prompt_template_build_messages()` declaration
- Remove `prompt_template_free()` declaration
- Keep the file if `resolve_inline_variables_local()` needs a public header, otherwise fold into agent.c

#### `src/prompt_template.c`

- Remove `prompt_template_parse()` and all section-parsing code (lines 231-502)
- Remove `prompt_template_build_messages()` (lines 551-639)
- Remove `prompt_template_free()` (lines 641-667)
- Remove section-related helpers: `init_section_defaults()`, `parse_int_or_zero()`, `extract_tool_result_content()`, `append_message_object()`
- Keep `resolve_inline_variables_local()` — it handles `{{...}}` variable resolution used by `build_context_from_triggers()`
- Keep `map_variable_tool_name()` — maps variable names to tool names
- Keep string helpers: `dup_range()`, `ltrim_inplace()`, `rtrim_inplace()`, `append_text()`

#### `src/agent.c`

- Remove `g_prompt_template` static variable (line 28)
- Remove `g_has_prompt_template` static variable (line 29)
- Remove `template_emit_hook()` callback
- Remove the `prompt_template_parse()` call in `agent_init()` (line 2127-2128)
- Remove the `prompt_template_free()` calls in `agent_init()` and `agent_cleanup()`
- Remove the `g_has_prompt_template` branch in `agent_build_admin_messages_json()` (lines 2334-2035) — replace with trigger-composed context path

### Phase 2: Remove `g_system_context`

#### `src/agent.c`

- Remove `g_system_context` global (line 24)
- Remove `system_context` parameter from `agent_init()`
- Remove `strdup(system_context)` and all `g_system_context` references
- Remove `g_system_context` null checks from `agent_on_trigger()`, `agent_on_message()`, `agent_build_admin_messages_json()`
- In `agent_cleanup()`: remove `free(g_system_context)`

#### `src/agent.h`

- Update `agent_init()` signature: `int agent_init(didactyl_config_t* config);`

#### `src/nostr_handler.c`

- Remove `g_system_context` global (line 28)
- Remove `g_system_context` initialization in `nostr_handler_reconcile_startup_events()` (lines 3124-3157)
- Remove `nostr_handler_get_system_context()` function (lines 3289-3291)
- Remove `seed_default_skill_into_cache()` if it only serves `g_system_context`

#### `src/nostr_handler.h`

- Remove `nostr_handler_get_system_context()` declaration

#### `src/main.c`

- Remove `system_context` variable (line 1261)
- Remove fallback logic (lines 1262-1270)
- Call `agent_init(&cfg)` without system_context parameter

### Phase 3: Rename default skill constants

#### `src/default_events.h`

- `DIDACTYL_DEFAULT_SKILL_D_TAG`: `"didactyl-default"` → `"default_admin_dm"`
- `DIDACTYL_DEFAULT_SKILL_TAGS_JSON`: add trigger/filter/description tags
- `DIDACTYL_DEFAULT_SKILL_TEMPLATE`: remove `---template---` section, replace with lean markdown using `{{my_kind0_profile}}` and `{{my_npub}}`

### Phase 4: Update `agent_build_admin_messages_json()`

This function is used by the HTTP API. Currently has two paths (template DSL and legacy hardcoded). Replace both with a single path using `build_context_from_triggers()` — same as `agent_on_message()` now uses.

### Phase 5: Delete legacy files

| File | Action |
|------|--------|
| `config.jsonc.example` | Delete |
| `context_template.md` | Delete |
| `context_template.recovered.md` | Delete |

### Phase 6: Update genesis files

#### `genesis.jsonc`

- Add kind 0 startup event with name and about
- Rename `default_skill.d_tag` to `default_admin_dm`
- Add trigger/filter/description tags to `default_skill.tags`
- Replace content with lean markdown (no `---template---`)

#### `genesis.jsonc.example`

- Same changes as genesis.jsonc
- Add kind 0 and kind 3 startup events

### Phase 7: Update documentation

#### `README.md`

- Remove kind 31120 from Didactyl Kinds table
- Remove soul references
- Update architecture description

#### `docs/GENESIS.md`

- Remove config.jsonc migration references
- Update default_skill section to show `default_admin_dm`
- Document kind 0 and kind 3 startup events

#### `docs/API.md`

- Remove `/api/events/soul` endpoint
- Remove config.jsonc references

#### `docs/SKILLS.md`, `docs/CONTEXT.md`, `docs/TOOLS.md`

- Verify no soul/31120/config.jsonc references remain

---

## Variable Resolution for Default Skill

The default admin DM skill content uses:
- `{{my_npub}}` — resolves via `map_variable_tool_name()` → `nostr_npub` tool → returns agent npub
- `{{my_kind0_profile}}` — resolves via `map_variable_tool_name()` → `my_kind0_profile` tool → returns agent kind 0 profile context

These already work in `resolve_inline_variables_local()` because `map_variable_tool_name()` returns `NULL` for unknown names, and the skill lookup callback handles the rest. But `my_npub` and `my_kind0_profile` are not in the current `map_variable_tool_name()` mapping — they need to be added, OR the function needs to pass through unknown names as tool names (which it currently does NOT do after the Phase 1a change that made it return `NULL` for unknowns).

**Fix needed:** In `map_variable_tool_name()`, return the variable name itself as a tool name when it doesn't match a known alias. This way `{{my_npub}}` calls the `my_npub` tool directly.

Current (after Phase 1a change):
```c
return NULL;  // unknown → try skill lookup
```

Should be:
```c
return var_name;  // unknown → try as tool name first, then skill lookup
```

Wait — this was the ORIGINAL behavior before Phase 1a changed it. The Phase 1a change made `map_variable_tool_name()` return `NULL` for unknowns so the skill lookup callback would fire. But now we need BOTH: try as tool name first, then skill lookup.

**Resolution:** Restore `return var_name;` in `map_variable_tool_name()`. The skill lookup callback in `resolve_inline_variables_local()` already only fires when `replacement_owned` is NULL after the tool attempt. So the flow is:
1. Check known aliases (admin_profile → nostr_admin_profile, etc.)
2. Try the variable name as a direct tool name
3. If tool returns nothing useful, try skill lookup callback
4. If skill lookup returns nothing, resolve to empty string

This is exactly the resolution order specified in SKILLS.md.

---

## Execution Order

1. Phase 3 — rename constants (safe, no behavioral change)
2. Phase 5 — delete legacy files
3. Phase 6 — update genesis files
4. Phase 1 — remove template DSL
5. Phase 2 — remove g_system_context
6. Phase 4 — update agent_build_admin_messages_json
7. Phase 7 — update docs
8. Build and test
9. `increment_and_push.sh -p` for v0.2.1
