Files
reliquary/docs/workpad/designs/2026-09-18-adept-discovery.md
g4borg 9e6deea36e 📝 Add adept discovery design pipeline artifacts and harness investigation goal
Design, plan, fact-check report, and athanor implementation report for
the adept discovery and tiered knowledge folders feature. Also captures
the harness behavior investigation as an open goal.
2026-09-18 15:56:01 +02:00

25 KiB
Raw Permalink Blame History

Design: Adept discovery and tiered knowledge folders

Date: 2026-09-18 Touches: plugins/spire/agents/adept.md, plugins/spire/agents/igor.md, plugins/spire/skills/igor/SKILL.md, plugins/spire/skills/igor/references/journal-template.md, plugins/spire/skills/init/SKILL.md, plugins/spire/skills/workpad/SKILL.md, new skill plugins/spire/skills/adepts/ Builds on: designs/spire-project-override-and-init.md (which shipped the _template scaffolding this design replaces)

The problem

Three gaps, all downstream of the same fact — the adept folder is opaque from outside:

  1. No discovery. Igor is told to "check the workpad's adepts/ folder before spawning." In practice that means ls, then reading each CLAUDE.md to learn what a folder covers and whether anyone has been there lately. That is exactly the kind of inline work Igor is not supposed to do, and it scales linearly with the number of domains. Nothing records which domains were recently active.
  2. Monolithic CLAUDE.md. The current template puts scope, key files, conventions, and open questions in one file. Every adept appends to it; every successor loads all of it. Because the harness auto-loads a directory's CLAUDE.md when files beneath it are read, a fat one is paid for repeatedly. There is no tiering — a successor cannot tell what it must read from what it may read.
  3. Weak handoff. A successor "reads the CLAUDE.md and journals." Journals are append-only and grow without bound; the successor has no cheap way to know what the predecessor's last state was, what is in flight on a branch, or whether the code has moved since the predecessor last verified anything.

The design principle for all three: adepts write knowledge; tooling derives metadata. An adept should never maintain an index, a counter, or a timestamp by hand. Everything the roster needs is either declared once in frontmatter or derived from files that already exist (journal filenames, git).

Design

A. Domain folder layout

<workpad>/adepts/
  CLAUDE.md                 # directory conventions (init-scaffolded; rewritten by this design)
  _template/                # init-scaffolded starter; copied on founding
    CLAUDE.md
    handoff.md
  <domain-slug>/
    CLAUDE.md               # INDEX — frontmatter + tiered reading list. Short. Auto-loaded.
    handoff.md              # rolling state-of-the-domain for the successor. Overwritten, not appended.
    key-files.md            # solid knowledge docs — as many as the domain needs
    conventions.md
    architecture.md
    gotchas.md
    ...                     # any other doc, provided it is listed in CLAUDE.md
    journal/
      YYYY-MM-DD-HH-genesis.md
      YYYY-MM-DD-HH-<topic>.md

Changes from today:

  • Journals move into journal/ and drop the journal- infix (the folder says it). The domain root then reads as a knowledge base: index, handoff, docs. ls alone tells a successor what exists.
  • handoff.md is new and is the one file that is overwritten rather than appended.
  • Knowledge docs are plural. The names above are a suggested vocabulary, not a fixed schema — but any file in the domain root must appear in CLAUDE.md's reading list. If it is not in the index, it does not exist.

Where the two knowledge tiers from adept.md land:

Content Lives in Update rule
Solid knowledge (verified on main) key-files.md, conventions.md, architecture.md, gotchas.md, … Correct on contact; dated refinement sections when disputing a predecessor; curator compresses
Experiences (branch work, observations, dead ends) journal/ Append-only, one file per topic or session
Current state, open threads, in-flight branch notes handoff.md Overwritten by the living adept; written early, refreshed often
Scope, boundaries, what to read when CLAUDE.md Rarely changes; edited when docs are added or the domain splits

B. CLAUDE.md as index

Frontmatter carries the only hand-declared metadata. Everything temporal is derived (see D).

---
domain: Authentication
summary: Login flow, session tokens, the auth middleware chain, and the permission model.
covers:
  - src/auth/
  - src/middleware/auth*
  - tests/auth/
related: [api-gateway, user-profile]
status: active            # active | split | merged
---
# Authentication

Everything between "a request arrives with credentials" and "a request carries a
verified principal." Permission *checks* inside feature code are not this domain —
that is whoever owns the feature. Token *issuance* for service accounts is
api-gateway's.

## Must-read

Every genesis, before answering anything. Keep this list to three or four entries.

- `handoff.md` — where the previous adept left things. Always first.
- `key-files.md` — the files that matter, what each does, where the seams are.
- `conventions.md` — rules here that differ from the project-wide ones.

## Good-to-read-if

- `architecture.md` — if asked how the pieces fit, or where a new thing should go.
- `gotchas.md` — if asked why something behaves oddly, or before recommending any change to the middleware order.
- `journal/` — if `handoff.md` points at a specific entry, or a question touches a dead end noted there. Do not read the whole journal.

## Optional

- `history.md` — how the domain got this way. Read for "why" questions only.
- `../api-gateway/CLAUDE.md` — read only when a question crosses into token issuance.

## Open questions

Carried forward by predecessors. Try to resolve one before adding one.

- Why does `AdminService` bypass the standard middleware? (since 2026-08-30)

Rules the adept agent enforces:

  • Length. CLAUDE.md stays under ~60 lines. It is auto-loaded by the harness whenever an adept reads any file in the folder, so every line costs repeatedly. If it grows, something in it belongs in a doc.
  • Tiers are conditional, not ranked. "Good-to-read-if" entries state the condition. A successor answering a "why is this weird" question reads gotchas.md; one answering "where do I add X" reads architecture.md. Neither reads both at boot.
  • Docs split at ~200 lines. When a doc passes that, the adept splits it and updates the index in the same edit.
  • Siblings are introspected, not preloaded. related: names sibling slugs. The adept reads a sibling's CLAUDE.md (index only) when a consultation crosses a boundary, and may grep across ../*/journal/ when hunting a dead end. Nothing sibling-shaped is read at genesis.

C. handoff.md — the successor's first read

Rolling, overwritten, short (≤ ~40 lines). Written early in an adept's life and refreshed after every significant consultation — sessions end abruptly; an adept that writes its handoff "at the end" often never does.

---
updated: 2026-09-16-14
session: journals/2026-09-16-14-auth-refactor.md
branch: main
verified-at: 4886539            # HEAD when the solid docs were last checked against code
---
# Handoff — Authentication

## State
Middleware chain is documented and verified. Permission model doc is complete
except for the admin bypass (open question). Nothing in flight on main.

## In flight (experiences, not solid)
- Branch `feat/oauth-pkce` adds a third token type. Observed, journaled in
  `journal/2026-09-16-14-pkce-branch.md`, NOT in key-files.md until it merges.

## Open threads
- Admin bypass still unexplained. Explore agent found no comment, no commit message. Next: `git log -S AdminService --follow`.
- User asked whether refresh-token rotation is enforced; answer pending a test run.

## Successor: read first
1. This file.
2. `key-files.md` §Middleware chain — most likely to have drifted.
3. `journal/2026-09-16-14-pkce-branch.md` if the OAuth branch is still open.

## Drift check
    git log --oneline 4886539..HEAD -- src/auth/ src/middleware/auth* tests/auth/

verified-at is the mechanism that makes continuity cheap. The successor runs the drift check (paths come from covers:) and knows in one command whether any solid knowledge could be stale — and which files to re-verify before trusting key-files.md. Without it the successor either trusts blindly or re-reads the domain.

D. The roster — discovery without reading

A new skill, spire:adepts, shell-injects a derived roster at load time — same mechanism spire:project uses.

---
name: adepts
description: Lists the adept domains in the workpad — what each covers, when it was last active, its status. Loaded by Igor at boot; any agent may invoke it for a fresh listing.
allowed-tools: Bash(bash:*)
---

!`bash "${CLAUDE_SKILL_DIR}/roster.sh"`

<!-- Empty roster: no adepts/ folder yet, or /spire:init has not been run. -->

roster.sh (sketch — the planner decides the exact implementation):

#!/usr/bin/env bash
# Resolve workpad the same way spire:workpad does, minus the "ask" step.
WP="${WORKPAD_FOLDER:-$(just workpad-folder 2>/dev/null)}"
[ -z "$WP" ] && { [ -d docs/workpad ] && WP=docs/workpad || WP=.workpad; }
[ -d "$WP/adepts" ] || { echo "## Adept roster — none (no $WP/adepts/)"; exit 0; }

echo "## Adept roster — $WP/adepts/ ($(date +%F))"
for dir in "$WP"/adepts/*/; do
  slug=$(basename "$dir"); [ "$slug" = "_template" ] && continue
  fm=$(sed -n '2,/^---$/p' "$dir/CLAUDE.md" 2>/dev/null)
  summary=$(echo "$fm" | sed -n 's/^summary: *//p')
  status=$(echo  "$fm" | sed -n 's/^status: *//p')
  related=$(echo "$fm" | sed -n 's/^related: *//p')
  # Recency and session count derive from journal filenames — nothing to maintain.
  newest=$(ls "$dir"/journal/ "$dir" 2>/dev/null | grep -E '^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}' | sort | tail -1 | cut -c1-13)
  sessions=$(ls "$dir"/journal/ "$dir" 2>/dev/null | grep -Eo '^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}' | sort -u | wc -l)
  if [ -z "$summary" ]; then
    echo "- **$slug** — (unmigrated: no frontmatter) · last active ${newest:-never}"
  else
    echo "- **$slug** — $summary · last active ${newest:-never} · $sessions session(s) · ${status:-active}${related:+ · related: $related}"
  fi
done

Output Igor sees at boot:

## Adept roster — docs/workpad/adepts/ (2026-09-18)
- **auth** — Login flow, session tokens, the auth middleware chain, and the permission model. · last active 2026-09-16-14 · 4 session(s) · active · related: [api-gateway, user-profile]
- **billing** — Stripe integration and the invoice pipeline. · last active 2026-08-02-09 · 1 session(s) · active
- **legacy-importer** — (unmigrated: no frontmatter) · last active 2026-07-02-11

Properties:

  • Never stale. There is no index file to forget to update. Frontmatter is declared once; recency and session counts fall out of the journal naming convention that already exists.
  • No shared mutable file. Two adepts in parallel sessions each touch only their own folder.
  • Tolerates legacy. A folder without frontmatter still appears, flagged.
  • Cheap. One line per domain. Twenty domains is twenty lines. If a project ever has enough domains for this to matter, the roster can gain a --recent N mode; not needed now.
  • Available on demand. Adepts (and athanor) can Skill("spire:adepts") when a question crosses a boundary, instead of ls-ing and reading.

Fallback when skill shell execution is disabled: the skill body's comment tells Igor to dispatch a Haiku subagent with the one-liner bash <plugin>/skills/adepts/roster.sh. Igor never reads the folders itself.

Dormancy is derived, not declared: Igor reads "last active" and judges. status: exists only for the states an adept must declare because tooling cannot infer them — split (with successor slugs in related:) and merged (target in related:). A split or merged folder stays on disk; its CLAUDE.md shrinks to a pointer.

E. Flows

Igor boot

  1. spire:project loads project.md (unchanged).
  2. spire:adepts injects the roster (new — agents/igor.md adds it to skills:).
  3. Igor creates the session journal. The template gains an ## Adepts section; Igor seeds it with the roster rows relevant to the topic.
  4. Greet, ask what the user wants (unchanged).
  5. If the topic clearly matches a roster entry, Igor names it in the greeting — "there's an auth adept, last active two days ago; I'll wake it once I know what we're doing" — and spawns it as soon as the direction is clear. One flourish, then the dispatch.

Spawning — founding vs. successor

The brief changes shape depending on whether the slug is in the roster:

# Successor (slug in roster)
Agent(subagent_type: "adept", name: "auth-adept", model: "opus",
      prompt: "Your domain is Authentication. Your folder is docs/workpad/adepts/auth/ — you are a
               successor; it has a handoff.md. We are on branch main. Session journal:
               docs/workpad/journals/2026-09-18-10-oauth-pkce.md.
               Orient per your genesis protocol and be ready for consultation.")

# Founding (slug not in roster)
Agent(subagent_type: "adept", name: "billing-adept", model: "opus",
      prompt: "Your domain is Billing (Stripe integration, invoices). Your folder is
               docs/workpad/adepts/billing/ — it does not exist; you are the founding adept.
               We are on branch main. Session journal: <path>.
               Orient and be ready for consultation.")

Igor picks the slug from the roster when one fits strictly (the gravity-well rule from adept.md §4 stays; the roster just makes it possible to apply without reading folders). The session journal path goes in the brief so the adept's journal can link back.

Adept genesis — successor

1. Read adepts/CLAUDE.md                          (conventions, short)
2. Read <slug>/CLAUDE.md                          (index — auto-loaded anyway)
3. Read <slug>/handoff.md
4. Run the drift check from handoff.md            (git log <verified-at>..HEAD -- <covers>)
5. Read the Must-read docs
6. Read only the journal entries handoff.md names  (usually one or two)
7. If step 4 showed changes under a key path: verify those claims before anything else;
   correct on contact; note corrections in the genesis journal entry
8. Write journal/<now>-genesis.md — short for a successor: drift found, corrections made,
   orientation trace. Link the session journal.
9. Rewrite handoff.md: bump `updated`, `session`, `verified-at` (if step 7 verified), note "successor active"

Steps 1–6 are bounded: two short files, one short handoff, three or four docs, one or two journal entries. A successor never reads the whole journal.

Adept genesis — founding

As today (adept.md §Genesis), plus: copy _template/CLAUDE.md and _template/handoff.md; fill frontmatter (domain, summary, covers, related) during orientation; create the docs the orientation produces — at minimum key-files.md; write handoff.md before the first consultation, with verified-at = current HEAD. The genesis journal entry keeps its orientation-trace duty.

During consultation

Unchanged in spirit. Additions:

  • After a consultation that changes state (new open thread, a branch observation, a resolved question), refresh handoff.md in the same turn. It costs a few lines; it is the file that survives an abrupt end.
  • When a question crosses the domain boundary: Skill("spire:adepts") for the roster, read the sibling's CLAUDE.md index, and — if warranted — tell Igor "this is api-gateway's; consider waking that adept." Do not absorb the sibling's domain.
  • When adding a doc: add it to the index in the same edit.

Session wrap-up (Igor)

  1. SendMessage each live adept: "Session closing. Refresh handoff.md, write your closing journal entry, reply with one line of state." Wait via ListAgents, read the <result>s.
  2. Journal ## Adepts: final state per adept, path to its handoff.md.
  3. Close the journal (existing steps 3–4 of the lifecycle continue unchanged).

Compression mid-session

Already in adept.md: an adept whose context compresses wraps up and asks for a successor. With this design "wrap up" is concrete — refresh handoff.md, closing journal entry — and the successor's genesis is the bounded protocol above, so replacement is cheap enough to do without hesitation.

F. Journal template changes

references/journal-template.md gains one section between Log and Artifacts:

## Adepts

<!-- Roster rows relevant to this session at boot; then one line per adept as
     spawned/consulted/closed. Path to handoff.md on close. -->

- auth — successor (5th session) — spawned 10:12 — consulted 3× — closed 11:40, handoff refreshed

This is the "cross-session memory of what was useful": the next Igor grepping journals/ for auth — finds every session that used the adept and what it did. The roster gives recency; the journals give usefulness. Neither requires the adept to keep score.

G. adepts/CLAUDE.md (directory conventions) — rewritten

Init writes this; the content changes to describe the layout above. The conventions section becomes:

## Conventions

- **One domain per folder.** Overlap → pick the stricter scope. Broad names are gravity wells.
- **CLAUDE.md is an index, not a document.** Frontmatter + tiered reading list + open questions. Under ~60 lines. It is auto-loaded whenever anything in the folder is read — keep it cheap.
- **Every file in the domain root is listed in CLAUDE.md.** Unlisted files do not exist.
- **handoff.md is overwritten, never appended.** Write it early, refresh it after anything significant. `verified-at` is the commit the solid docs were last checked against.
- **journal/ is append-only.** One file per topic or session, `YYYY-MM-DD-HH-<topic>.md`. Recency and session counts are derived from these names — keep the prefix exact.
- **Solid knowledge goes in docs; experiences go in the journal.** Branch observations stay in the journal (and handoff.md's "In flight") until the code is on main.
- **Don't hoard.** Findings that belong in the project's docs or a workpad report go there; link from here.
- **Metadata is derived.** Never hand-maintain counts, dates, or an index file. Declare `domain`, `summary`, `covers`, `related`, `status` once in frontmatter; the roster does the rest.

H. Integration summary

Component Change
skills/adepts/SKILL.md + roster.sh New. Shell-injected roster; invocable on demand.
agents/igor.md Add spire:adepts to skills:.
skills/igor/SKILL.md §Adepts Replace "check the workpad's adepts/ folder" with "the roster is in context; spawn as successor when the slug exists." Add the founding/successor brief shapes. Add wrap-up step 1 (close-out message). Add shell-disabled fallback (Haiku runs roster.sh).
skills/igor/SKILL.md §Session start Step 1 seeds the journal's ## Adepts from the roster; step 2 may name a matching adept in the greeting.
skills/igor/references/journal-template.md Add ## Adepts section.
agents/adept.md Genesis splits into founding/successor protocols; folder structure updated; handoff.md duties added (write early, refresh often, drift check); index-maintenance rule; sibling introspection rule; doc-split threshold.
skills/init/SKILL.md Phase 4 Rewrite adepts/CLAUDE.md content (§G). _template/ gains handoff.md; _template/CLAUDE.md becomes the index template (§B with placeholders). Phase 7 commit list gains _template/handoff.md.
skills/workpad/SKILL.md Add adepts/ to the optional-folders list ("adept domain folders — see spire:adepts"). Today it is an unknown folder the skill says to ignore.
skills/igor/references/project.md Adepts: line points at the roster: "listed by spire:adepts at boot."
skills/curator/SKILL.md Scope note: knowledge docs in adept folders accrete refinement sections and are curator targets; handoff.md and journal/ are not.
agents/athanor.md No change. If athanor ever consults adepts, it invokes spire:adepts on demand.

Migration path

This repo

No adept folders exist under docs/workpad/. Migration here is template-only: update init, re-run /spire:init (templates are status: generated, so they regenerate).

Projects with existing adept folders

Lazy migration, done by the next adept spawned into each domain — not a bulk job. The roster flags unmigrated folders so Igor knows to include the instruction in the brief:

… Your folder is <workpad>/adepts/legacy-importer/ — it predates the tiered layout
(no frontmatter, journals at the root). Migrate it during genesis per adepts/CLAUDE.md,
then orient.

Migration steps the adept performs, in one commit-sized change:

  1. git mv each YYYY-MM-DD-HH-journal-<topic>.md → journal/YYYY-MM-DD-HH-<topic>.md.
  2. Split the monolithic CLAUDE.md: ## Key files → key-files.md; ## Conventions → conventions.md; remaining prose → architecture.md or gotchas.md by judgment. Preserve dated refinement sections in whichever doc their parent section landed.
  3. Write the new CLAUDE.md index: frontmatter (domain, summary, covers from the key-files table, related from any cross-references found), tiered list of the docs just created, carried-over open questions.
  4. Write handoff.md from the newest journal entry's "what's unclear" plus the open questions. verified-at = HEAD after the adept has verified key-files.md against current code — or, if it has not yet, omit the field and say so in ## State; the next successor will set it.
  5. Journal entry journal/<now>-migration.md: what moved where, what was verified, what was not.

If a project has many legacy domains and the user wants them done at once, this is a natural athanor job: one brief, one subagent per domain, same five steps.

Plugin update for existing installs

Re-run /spire:init. adepts/CLAUDE.md and _template/* regenerate if status: generated. If a project authored either, init skips it and the summary tells the user which files to merge by hand. roster.sh ships with the plugin, so discovery works immediately, including over unmigrated folders.

Open questions

For the planner:

  1. Journal relocation — worth the churn? Moving journals to journal/ is the one change that touches history in existing projects. Alternative: keep them flat with the journal- infix and have roster.sh match both. The clean ls is the argument for moving; zero migration is the argument against. This design says move; the planner may overrule.
  2. Doc vocabulary — fixed or free? key-files.md is the only doc this design requires. The rest are suggestions. A fixed vocabulary makes cross-domain grepping predictable; free naming fits odd domains better. Current answer: suggested vocabulary in _template/CLAUDE.md comments, free additions, index rule enforces discoverability either way.
  3. ## Adepts in the session journal — is grepping journals enough for "which adept was useful," or is there appetite for a counter? This design says grep is enough and counters are exactly the hand-maintained metadata the principle forbids.
  4. roster.sh location. Inside the skill folder (resolved via ${CLAUDE_SKILL_DIR}) is the obvious spot. If the planner prefers a just adepts recipe scaffolded by init, the skill can call that with the script as fallback — but that adds a Justfile dependency this design otherwise avoids.
  5. Parallel sessions in the same domain. Two Igors in two worktrees could each spawn an auth-adept; both would overwrite handoff.md. Worktrees mean separate files until merge, where handoff.md conflicts. Acceptable for now (rare; the conflict is short and human-resolvable), but the adept should mention its worktree in handoff.md's branch: so the loser of the merge knows what it is looking at.

For the fact-checker:

  1. Nested CLAUDE.md auto-loading. The design leans on the claim that the harness loads <dir>/CLAUDE.md when an agent reads files under <dir>. Confirm this holds for subagents and for files read via the Read tool (not just cwd). If it does not, the "keep CLAUDE.md short because it is auto-loaded" argument weakens — the index design still stands on its own, but the length pressure is less acute.
  2. Skill shell injection with a script. spire:project injects cat. Confirm !bash "${CLAUDE_SKILL_DIR}/roster.sh"`` works with allowed-tools: Bash(bash:*), that ${CLAUDE_SKILL_DIR} resolves for plugin-cached skills at agent-startup preload (not only on /skill invocation), and what the behaviour is under disableSkillShellExecution.
  3. Workpad resolution inside the script. roster.sh cannot ask the user. Confirm WORKPAD_FOLDER / just workpad-folder / docs/workpad / .workpad covers every configuration the spire:workpad skill supports — the external tickets-folder case (<tickets>/workpad/) may need .claude/spire/project.md to record the resolved path, which init already knows at scaffold time.
  4. Skill("spire:adepts") from a subagent. Confirm adepts can invoke a plugin skill on demand and receive the injected output, so sibling discovery does not fall back to ls.
  5. git log <sha>..HEAD -- <paths> with glob entries in covers:. src/middleware/auth* needs the shell to expand or git's pathspec to handle it; confirm which, and whether the handoff should store the literal command (as drafted) or just the paths.