Architecture
Layout
src/
├── cli.ts # Commander entry
├── commands/ # User-facing CLI implementations
├── hooks/ # Compiled-to-.mjs hook scripts
│ ├── kb-capture.ts # capture (Stop/SessionEnd/PreCompact)
│ ├── kb-proposal-drain.ts # extraction (SessionStart, async)
│ └── kb-session-start.ts # consume (SessionStart, sync)
├── lib/ # Reusable building blocks
├── adapters/ # Adapter interface
└── templates-source/ # Files copied into consumer repos
tsup builds dist/cli.js (CLI binary) and dist/hooks/*.mjs (one bundle per hook). The prepare script copies templates-source/ to templates/ and drops compiled hooks into templates/claude/hooks/. The npm package ships dist/ and templates/.
Two CLI shapes
- Deterministic:
init,doctor,status,node add,index rebuild. No LLM. - LLM-invoking:
curate,bootstrap-incremental. Spawnclaude -pviarunHeadlessClaude, parse stream-JSON, validate with Zod. All subprocesses setKB_BUILDER_INTERNAL=1.
Each of the three claude -p subprocess sites reads its own { name, effort } config object before spawning: proposalModel (the proposal drain hook), curatorModel (the curate CLI), and bootstrapModel (the bootstrap-incremental CLI). When the object is set, runHeadlessClaude appends --model <name> and --effort <effort>; when it is absent, neither flag is passed and the user’s claude CLI default applies. The /kb-bootstrap skill is agent-driven (its sub-agent runs via the Task tool, not claude -p); it honors bootstrapModel.name on a best-effort basis but ignores bootstrapModel.effort because the Task tool exposes no effort parameter.
Pipelines
flowchart TB
subgraph capture[Capture]
H1[Stop / SessionEnd / PreCompact] --> KB1[kb-capture.mjs<br/>sync, secretlint redact]
KB1 --> SL[_sessions/<log>.md<br/>pending]
end
subgraph extract[Extract candidates]
SS1[SessionStart] --> KB2[kb-proposal-drain.mjs<br/>async, claude -p]
SL --> KB2
KB2 --> SLD[_sessions/<log>.md<br/>done + candidates]
end
subgraph curate[Curate]
UC[/kb-curate or curate CLI/] --> KB3[curate command<br/>claude -p]
SLD --> KB3
KB3 --> NODES[(nodes/<kind>/<slug>.md)]
KB3 --> PC[conflicts/<id>.md]
KB3 --> IDX[INDEX.md / GRAPH.md<br/>regenerated end-of-run]
end
subgraph review[Review]
NODES --> RV[git diff<br/>git commit / git restore]
PC --> SK[/kb-curate skill<br/>resolves with user/]
SK --> NODES
RV --> COMMIT[(committed nodes)]
end
subgraph consume[Consume]
SS2[SessionStart] --> KB4[kb-session-start.mjs<br/>sync]
IDX --> KB4
KB4 --> CTX[additionalContext → harness]
end
State files
| File | Owner | Purpose |
|---|---|---|
_sessions/<log>.md | capture, extract, curate | Per-session checkpoint. Filename is YYYYMMDD-HHmm-<sessionId>.md; re-firing the hook for the same session_id overwrites in place. |
_logs/{proposal,curator,bootstrap-incremental}/*.jsonl | LLM pipelines | Stream-JSON traces. Gitignored. |
nodes/{practice,map}/ | curator, node-add, bootstrap, human reviewer | Canonical knowledge. Reviewed via git diff and accepted via git commit. |
INDEX.md / GRAPH.md | curator, index rebuild (incl. --stage for opt-in pre-commit hooks) | Deterministic outputs derived from nodes/. Regenerated by the curator at end-of-run; consumers may also wire index rebuild --stage into their own pre-commit hook. |
.state/installed-version | init | Package version + selected harnesses. Committed. |
.state/state.json | drain, curator, bootstrap, consume | Lock + last_nudged_at. Gitignored. |
.state/bootstrap-state.json | bootstrap | Doc SHA-256 cache. Gitignored. |
conflicts/<run-id>-<n>.md | curator (write), kb-curate skill (resolve), status (read) | Curator-detected contradictions, one markdown file per conflict. Frontmatter carries status: pending; resolution is via git restore (Reject / Accept-after-apply) or git commit (Keep as record). |
.config/prompts/* | init | Local prompt overrides. Committed. |
Locking
state.json holds one lock at a time (name, pid, acquired_at, ttl_ms). 30-min TTL; stale locks are reclaimed.
proposal-drain: prevents concurrent SessionStart drains racing on the queue.curator: prevents duplicate proposals from concurrent curate runs.bootstrap-incremental: same, for bootstrap.
Consume doesn’t lock.
Determinism contract
computeNodesHashis content-addressed and mtime-independent.generateIndex/generateGraphare pure functions ofnodes/plus an injectednow.slugify,deriveNodeId,ensureUniqueIdare pure.crypto.randomUUID()is the only randomness, scoped torun_idminting.
Tests rely on this. See tests/lib/index-gen.test.ts for golden-file comparisons.
Adapter interface
src/adapters/types.ts:
interface Adapter {
name: string;
hookInstallPath(): string;
skillInstallPath(): string;
writeHookConfig(repoRoot: string, hooks: HookSpec[]): Promise<void>;
readTranscript(hookInput: unknown): Promise<RoleTaggedTranscript>;
runHeadless<T>(promptBody: string, stdin: string, schema: ZodSchema<T>, opts?: HeadlessOpts): Promise<T>;
renderSkill(spec: SkillSpec): string;
}
Adding an adapter: implement the methods, dispatch from init.ts.
Testing
- Unit + integration (
npm test) - pure-function tests forsrc/lib/, plus pipeline integration tests against a fake runner. CLI integration tests build the package and run the binary in a temp-dir sandbox. ~10s. - Manual - see Manual test plan.
Where to extend
| Goal | Path |
|---|---|
| Change extraction | templates-source/prompts/proposal-extract.md |
| Change curator | templates-source/prompts/curator.md (logic in src/lib/curate.ts) |
| Change bootstrap | templates-source/prompts/bootstrap-incremental.md or skill body |
| New CLI subcommand | src/commands/<name>.ts + wire in src/cli.ts + doc in cli-reference.md |
| New hook | src/hooks/<name>.ts + tsup.config.ts + register in init.ts |
| New state file | Schema in src/lib/schemas.ts; add to gitignore block |
| New adapter | Implement src/adapters/types.ts; dispatch from init.ts |