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.
25 KiB
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:
- No discovery. Igor is told to "check the workpad's
adepts/folder before spawning." In practice that meansls, then reading eachCLAUDE.mdto 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. - 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.mdwhen 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. - 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 thejournal-infix (the folder says it). The domain root then reads as a knowledge base: index, handoff, docs.lsalone tells a successor what exists. handoff.mdis 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.mdstays 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" readsarchitecture.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'sCLAUDE.md(index only) when a consultation crosses a boundary, and maygrepacross../*/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 Nmode; not needed now. - Available on demand. Adepts (and athanor) can
Skill("spire:adepts")when a question crosses a boundary, instead ofls-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
spire:projectloadsproject.md(unchanged).spire:adeptsinjects the roster (new —agents/igor.mdadds it toskills:).- Igor creates the session journal. The template gains an
## Adeptssection; Igor seeds it with the roster rows relevant to the topic. - Greet, ask what the user wants (unchanged).
- 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.mdin 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'sCLAUDE.mdindex, 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)
SendMessageeach live adept: "Session closing. Refresh handoff.md, write your closing journal entry, reply with one line of state." Wait viaListAgents, read the<result>s.- Journal
## Adepts: final state per adept, path to itshandoff.md. - 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:
git mveachYYYY-MM-DD-HH-journal-<topic>.md→journal/YYYY-MM-DD-HH-<topic>.md.- Split the monolithic
CLAUDE.md:## Key files→key-files.md;## Conventions→conventions.md; remaining prose →architecture.mdorgotchas.mdby judgment. Preserve dated refinement sections in whichever doc their parent section landed. - Write the new
CLAUDE.mdindex: frontmatter (domain,summary,coversfrom the key-files table,relatedfrom any cross-references found), tiered list of the docs just created, carried-over open questions. - Write
handoff.mdfrom the newest journal entry's "what's unclear" plus the open questions.verified-at= HEAD after the adept has verifiedkey-files.mdagainst current code — or, if it has not yet, omit the field and say so in## State; the next successor will set it. - 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:
- Journal relocation — worth the churn? Moving journals to
journal/is the one change that touches history in existing projects. Alternative: keep them flat with thejournal-infix and haveroster.shmatch both. The cleanlsis the argument for moving; zero migration is the argument against. This design says move; the planner may overrule. - Doc vocabulary — fixed or free?
key-files.mdis 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.mdcomments, free additions, index rule enforces discoverability either way. ## Adeptsin 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.roster.shlocation. Inside the skill folder (resolved via${CLAUDE_SKILL_DIR}) is the obvious spot. If the planner prefers ajust adeptsrecipe scaffolded by init, the skill can call that with the script as fallback — but that adds a Justfile dependency this design otherwise avoids.- Parallel sessions in the same domain. Two Igors in two worktrees could each spawn an
auth-adept; both would overwritehandoff.md. Worktrees mean separate files until merge, wherehandoff.mdconflicts. Acceptable for now (rare; the conflict is short and human-resolvable), but the adept should mention its worktree inhandoff.md'sbranch:so the loser of the merge knows what it is looking at.
For the fact-checker:
- Nested
CLAUDE.mdauto-loading. The design leans on the claim that the harness loads<dir>/CLAUDE.mdwhen 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. - Skill shell injection with a script.
spire:projectinjectscat. Confirm!bash "${CLAUDE_SKILL_DIR}/roster.sh"`` works withallowed-tools: Bash(bash:*), that${CLAUDE_SKILL_DIR}resolves for plugin-cached skills at agent-startup preload (not only on/skillinvocation), and what the behaviour is underdisableSkillShellExecution. - Workpad resolution inside the script.
roster.shcannot ask the user. ConfirmWORKPAD_FOLDER/just workpad-folder/docs/workpad/.workpadcovers every configuration thespire:workpadskill supports — the external tickets-folder case (<tickets>/workpad/) may need.claude/spire/project.mdto record the resolved path, which init already knows at scaffold time. 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 tols.git log <sha>..HEAD -- <paths>with glob entries incovers:.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.