# Admin Skills Loading & skill_list Redesign

## Problem Statement

1. **Agent only loads its own skills from nostr** — the admin's public skills (kind `31123`) are never fetched or cached at startup, so the agent cannot discover or adopt them.
2. **`skill_list` only shows adopted skills** — there is no way to see available-but-not-adopted skills, making skill management opaque.
3. **`created_at` displays in scientific notation** — Unix timestamps (~1.7e9) are stored as `double` via cJSON and printed in scientific notation (e.g., `1.7e+09`).

## Current Architecture

```mermaid
flowchart TD
    A[main.c startup] --> B[nostr_handler_subscribe_self_skills]
    B --> C[Filter: kinds 31123+31124+10123, authors=agent_pubkey]
    C --> D[on_self_skill_event callback]
    D --> E[self_skill_cache_upsert_event_locked]
    E --> F{pubkey == agent?}
    F -->|Yes| G[Cache in g_self_skill_events]
    F -->|No| H[Reject - BLOCKER]
    
    I[skill_list tool] --> J[nostr_handler_get_self_skill_events_json]
    J --> K[Filter: only kind 31123+31124]
    K --> L{In adoption list?}
    L -->|Yes| M[Include in output]
    L -->|No| N[Skip]
```

### Key Files

| File | Role |
|------|------|
| `src/nostr_handler.c:1831` | `nostr_handler_subscribe_self_skills()` — subscribes to agent's own skill events |
| `src/nostr_handler.c:646` | `self_skill_cache_upsert_event_locked()` — caches events, **rejects non-agent pubkeys** |
| `src/nostr_handler.c:2446` | `nostr_handler_get_self_skill_events_json()` — returns cached skill events |
| `src/tools/tool_skill.c:655` | `execute_skill_list()` — lists only adopted skills from cache |
| `src/tools/tool_skill.c:373` | `extract_skill_summary_local()` — builds summary JSON, has `created_at` bug |
| `src/agent.c:1503` | `refresh_adopted_skills_cache_if_needed()` — fetches skill content for context injection |

## Proposed Changes

### 1. Subscribe to Admin's Public Skills on Startup

**File: `src/nostr_handler.c`**

Modify `nostr_handler_subscribe_self_skills()` to also include the admin's pubkey in the authors filter for kind `31123` (public skills only — we should not fetch the admin's private skills or adoption list).

**Approach A — Expand the existing subscription filter:**

Add `g_cfg->admin.pubkey` to the `authors` array in the existing subscription. This is the simplest approach since the subscription already handles kinds `31123`, `31124`, `10123`.

However, this would also fetch the admin's `31124` (private) and `10123` (adoption list) events, which we don't need and can't decrypt. So we need **two subscriptions**:

**Approach B — Add a second subscription (recommended):**

Keep the existing self-skill subscription unchanged. Add a new subscription specifically for the admin's public skills:
- kinds: `[31123]` only
- authors: `[admin_pubkey]`
- limit: 100

This keeps concerns separated and avoids fetching admin private skills or adoption lists.

**New function:** `nostr_handler_subscribe_admin_skills()` or extend the existing function with a second subscription block.

### 2. Expand Skill Cache to Accept Admin Events

**File: `src/nostr_handler.c`**

The core blocker is in `self_skill_cache_upsert_event_locked()` at line 667:
```c
if (!g_cfg || strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) != 0) {
    return;
}
```

**Changes needed:**

1. **Rename the cache** from `g_self_skill_events` to `g_skill_events` (or keep the name but change semantics) to reflect it now holds both agent and admin skills.

2. **Relax the pubkey check** to accept events from either the agent's pubkey OR the admin's pubkey:
   ```c
   int is_agent = (strcmp(pubkey->valuestring, g_cfg->keys.public_key_hex) == 0);
   int is_admin = (g_cfg->admin.pubkey[0] != '\0' &&
                   strcmp(pubkey->valuestring, g_cfg->admin.pubkey) == 0);
   if (!is_agent && !is_admin) {
       return;
   }
   ```

3. **For admin events, only accept kind `31123`** (public skills) — reject `31124` and `10123` from admin:
   ```c
   if (is_admin && kind_val != 31123) {
       return;
   }
   ```

4. **Update `nostr_handler_get_self_skill_events_json()`** to return ALL cached skill events (agent + admin), not just agent's. The function name could be renamed to `nostr_handler_get_skill_events_json()` but to minimize churn, we can keep the name and update the semantics.

5. **Add a new accessor** `nostr_handler_get_all_skill_events_json()` that returns everything, while keeping the existing function for backward compatibility if needed. Or simply update the existing one since `skill_list` is the only consumer that needs the broader view.

### 3. Redesign `skill_list` Output

**File: `src/tools/tool_skill.c`**

Currently `execute_skill_list()` only shows adopted skills. The new design:

#### New Output Structure

```json
{
  "success": true,
  "summary": [
    {
      "d_tag": "spellcheck",
      "owner": "agent",
      "pubkey": "52a3e8...",
      "kind": 31123,
      "adopted": true
    },
    {
      "d_tag": "translate",
      "owner": "admin",
      "pubkey": "8fdd1c...",
      "kind": 31123,
      "adopted": false
    },
    {
      "d_tag": "code-review",
      "owner": "admin",
      "pubkey": "8fdd1c...",
      "kind": 31123,
      "adopted": true
    }
  ],
  "skills": [
    {
      "kind": 31123,
      "created_at": 1710412800,
      "id": "abc123...",
      "pubkey": "52a3e8...",
      "owner": "agent",
      "d_tag": "spellcheck",
      "scope": "public",
      "description": "Spelling and grammar checker",
      "adopted": true,
      "content_preview": "...",
      "content_length": 450,
      "content_truncated": false
    }
  ],
  "count": 3,
  "adopted_count": 2
}
```

#### Key Changes to `execute_skill_list()`

1. **Remove the adoption-only filter** — iterate ALL cached skill events (agent + admin), not just adopted ones.

2. **Add `adopted` boolean** to each skill summary by checking against the adoption list.

3. **Add `owner` field** — `"agent"` if pubkey matches agent, `"admin"` if pubkey matches admin, otherwise the raw pubkey.

4. **Add `summary` array** at the top — one compact line per skill with just `d_tag`, `owner`, `kind`, and `adopted` status.

5. **Keep `skills` array** with full details on each skill.

6. **Add `adopted_count`** alongside existing `count`.

7. **Optional `filter` parameter** — add an optional `adopted` boolean parameter to filter the list:
   - `{"adopted": true}` — show only adopted skills (current behavior)
   - `{"adopted": false}` — show only non-adopted skills
   - omitted — show all skills

### 4. Fix `created_at` Scientific Notation

**File: `src/tools/tool_skill.c`**

In `extract_skill_summary_local()` at line 389:
```c
if (created_at && cJSON_IsNumber(created_at)) {
    cJSON_AddNumberToObject(summary, "created_at", created_at->valuedouble);
}
```

The issue is that `cJSON_AddNumberToObject` stores the value as a `double`, and cJSON's printer uses `%g` format which switches to scientific notation for large numbers.

**Fix:** Cast to `long long` and use `cJSON_AddRaw` or format as a string-encoded integer. The cleanest approach with cJSON:

```c
if (created_at && cJSON_IsNumber(created_at)) {
    char ts_buf[32];
    snprintf(ts_buf, sizeof(ts_buf), "%lld", (long long)created_at->valuedouble);
    cJSON* ts_item = cJSON_CreateRaw(ts_buf);
    if (ts_item) {
        cJSON_AddItemToObject(summary, "created_at", ts_item);
    }
}
```

This outputs the timestamp as a raw integer literal in JSON (e.g., `1710412800`) instead of scientific notation.

**Apply the same fix** to the `kind` field if it also uses `cJSON_AddNumberToObject` — though `kind` values (31123, 31124, 10123) are small enough to not trigger scientific notation.

## Data Flow After Changes

```mermaid
flowchart TD
    A[main.c startup] --> B[nostr_handler_subscribe_self_skills]
    B --> C[Sub 1: kinds 31123+31124+10123, authors=agent]
    B --> D[Sub 2: kinds 31123, authors=admin]
    
    C --> E[on_self_skill_event]
    D --> F[on_admin_skill_event - or reuse same callback]
    
    E --> G[skill_cache_upsert_event_locked]
    F --> G
    
    G --> H{pubkey == agent OR admin?}
    H -->|Yes| I{admin? only kind 31123}
    H -->|No| J[Reject]
    I -->|OK| K[Cache in g_skill_events]
    
    L[skill_list tool] --> M[get_all_skill_events_json]
    M --> N[For each event]
    N --> O[Check adoption list]
    O --> P[Build summary + detail with adopted flag]
```

## Implementation Order

1. **Fix `created_at` scientific notation** — smallest, isolated change
2. **Expand cache to accept admin pubkey** — modify `self_skill_cache_upsert_event_locked()`
3. **Add admin skill subscription** — new subscription in `nostr_handler_subscribe_self_skills()` or new function
4. **Redesign `skill_list` output** — add summary section, show all skills with adoption status
5. **Update `nostr_handler.h`** — add any new function declarations
6. **Update tool schema** — add `adopted` filter parameter to `skill_list` schema in `tools_schema.c`
7. **Test** — verify with `--test-tool skill_list '{}'`

## Edge Cases

- **Admin pubkey not configured** — skip admin subscription, behave as before
- **Admin and agent publish skill with same d-tag** — both appear in the list, distinguished by `owner` field
- **Admin skill already adopted** — shows `adopted: true` in summary
- **Live updates** — admin skill events received after startup should also be cached (the subscription stays open)
