Agents: SQLite store + type-defaults config refactor #48

Closed
opened 2026-04-18 14:59:24 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As a maintainer, I want config/agents.json to describe agent types (Forgejo user, token file, git identity, GPG key, default model, default container image) and a SQLite-backed agents table to describe instances (name, type, per-instance overrides), so that adding a second dev or reviewer is a row insert rather than a JSON edit + restart.

Context

Today config/agents.json has one top-level object per hardcoded name (boss, dev, reviewer), each with its complete config. The system has no concept of "agent type" vs. "agent instance" — there's exactly one of each.

This story introduces that split and adds the SQLite layer for instance overrides. First boot after this lands must be behaviour-identical to today: the migration seeds one <type>-default instance per type, each with no overrides, and routing falls through to type defaults.

Acceptance criteria

Config shape

  • config/agents.json rewritten as { types: { boss: { forgejo_user, token_file, git_name, git_email, branch_prefix, default_model, default_container_image, default_system_prompt, gpg_key_file? }, dev: {...}, reviewer: {...} }, webhook_secret_file, forgejo_url, forgejo_mcp_command }. No top-level agents object.
  • Loader validates every type block has the required fields; missing fields fail fast on startup.

SQLite layer

  • New module src/db.ts — thin wrapper around bun:sqlite, opens a file at ~/.local/state/claude-hooks/agents.db (or stateRoot()/agents.db to match sessions.json). Creates the agents table on first open.
  • Schema:
    CREATE TABLE IF NOT EXISTS agents (
      name           TEXT PRIMARY KEY,
      type           TEXT NOT NULL,
      model          TEXT,
      prompt_appendix TEXT,
      match_labels   TEXT,            -- JSON array of strings
      notes          TEXT,
      created_at     INTEGER NOT NULL,
      updated_at     INTEGER NOT NULL
    );
    CREATE INDEX IF NOT EXISTS agents_type_idx ON agents(type);
    
  • Export listAgents(), getAgent(name), listAgentsByType(type), createAgent({name, type, ...}), updateAgent(name, patch), deleteAgent(name).

Merged instance resolver

  • Export resolveAgent(name) -> Agent | null that merges the type defaults with the SQLite row. Precedence: SQLite row field wins when non-null; type defaults fill the rest. Returns a fully-populated Agent object (shape compatible with today's AgentConfig).
  • match_labels is parsed JSON → string[], defaults to [] when null.

Seed migration

  • On first startup (or when the agents table is empty), insert one row per type: { name: "<type>-default", type: "<type>", model: null, prompt_appendix: null, match_labels: null }. No overrides; the type default wins.
  • If config/agents.json is still in the old shape on upgrade, log a clear migration error pointing at a one-shot just migrate-agents-v2 recipe (or embed the rewrite in startup — pick one, document it).

Session key

  • sessionKey becomes <type>:<repo>:<issueOrPr> (was <name>:<repo>:<issueOrPr>). Pool members of the same type can resume each other's sessions.
  • Old session entries stay on disk and expire via the sweeper — no migration of historical sessions.

Tests

  • db.test.ts — CRUD round-trip, type lookup, JSON match_labels parse/serialize, default seed.
  • resolveAgent — type-default-only, override partial, override complete.
  • Config loader — valid, missing-required-field, old-shape error.

Out of scope

  • Pool scheduler (see A2).
  • Container reconciliation (see A5).
  • Dashboard UI (see A6).
  • Migrating the existing running instances — for the first boot after this lands, we'll restart the service once and accept a single generation of session-key miss.

References

  • Tracking issue: #47.
  • Existing config: config/agents.json.
  • Existing session key: src/sessions.ts:sessionKey.

Dependencies

  • Blocks: A2, A3, A4, A5, A6, A7.
  • Not blocked by: anything.
  • Branch off: main.
## User story As a **maintainer**, I want `config/agents.json` to describe agent **types** (Forgejo user, token file, git identity, GPG key, default model, default container image) and a SQLite-backed `agents` table to describe **instances** (name, type, per-instance overrides), so that adding a second `dev` or `reviewer` is a row insert rather than a JSON edit + restart. ## Context Today `config/agents.json` has one top-level object per hardcoded name (`boss`, `dev`, `reviewer`), each with its complete config. The system has no concept of "agent type" vs. "agent instance" — there's exactly one of each. This story introduces that split and adds the SQLite layer for instance overrides. First boot after this lands must be behaviour-identical to today: the migration seeds one `<type>-default` instance per type, each with no overrides, and routing falls through to type defaults. ## Acceptance criteria ### Config shape - [ ] `config/agents.json` rewritten as `{ types: { boss: { forgejo_user, token_file, git_name, git_email, branch_prefix, default_model, default_container_image, default_system_prompt, gpg_key_file? }, dev: {...}, reviewer: {...} }, webhook_secret_file, forgejo_url, forgejo_mcp_command }`. No top-level `agents` object. - [ ] Loader validates every type block has the required fields; missing fields fail fast on startup. ### SQLite layer - [ ] New module `src/db.ts` — thin wrapper around `bun:sqlite`, opens a file at `~/.local/state/claude-hooks/agents.db` (or `stateRoot()/agents.db` to match `sessions.json`). Creates the `agents` table on first open. - [ ] Schema: ```sql CREATE TABLE IF NOT EXISTS agents ( name TEXT PRIMARY KEY, type TEXT NOT NULL, model TEXT, prompt_appendix TEXT, match_labels TEXT, -- JSON array of strings notes TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS agents_type_idx ON agents(type); ``` - [ ] Export `listAgents()`, `getAgent(name)`, `listAgentsByType(type)`, `createAgent({name, type, ...})`, `updateAgent(name, patch)`, `deleteAgent(name)`. ### Merged instance resolver - [ ] Export `resolveAgent(name) -> Agent | null` that merges the type defaults with the SQLite row. Precedence: SQLite row field wins when non-null; type defaults fill the rest. Returns a fully-populated `Agent` object (shape compatible with today's `AgentConfig`). - [ ] `match_labels` is parsed JSON → `string[]`, defaults to `[]` when null. ### Seed migration - [ ] On first startup (or when the `agents` table is empty), insert one row per type: `{ name: "<type>-default", type: "<type>", model: null, prompt_appendix: null, match_labels: null }`. No overrides; the type default wins. - [ ] If `config/agents.json` is still in the old shape on upgrade, log a clear migration error pointing at a one-shot `just migrate-agents-v2` recipe (or embed the rewrite in startup — pick one, document it). ### Session key - [ ] `sessionKey` becomes `<type>:<repo>:<issueOrPr>` (was `<name>:<repo>:<issueOrPr>`). Pool members of the same type can resume each other's sessions. - [ ] Old session entries stay on disk and expire via the sweeper — no migration of historical sessions. ### Tests - [ ] `db.test.ts` — CRUD round-trip, type lookup, JSON `match_labels` parse/serialize, default seed. - [ ] `resolveAgent` — type-default-only, override partial, override complete. - [ ] Config loader — valid, missing-required-field, old-shape error. ## Out of scope - Pool scheduler (see **A2**). - Container reconciliation (see **A5**). - Dashboard UI (see **A6**). - Migrating the existing running instances — for the first boot after this lands, we'll restart the service once and accept a single generation of session-key miss. ## References - Tracking issue: #47. - Existing config: `config/agents.json`. - Existing session key: `src/sessions.ts:sessionKey`. ## Dependencies - **Blocks:** A2, A3, A4, A5, A6, A7. - **Not blocked by:** anything. - **Branch off:** `main`.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#48
No description provided.