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. Spawn claude -p via runHeadlessClaude, parse stream-JSON, validate with Zod. All subprocesses set KB_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/&lt;log&gt;.md<br/>pending]
    end

    subgraph extract[Extract candidates]
        SS1[SessionStart] --> KB2[kb-proposal-drain.mjs<br/>async, claude -p]
        SL --> KB2
        KB2 --> SLD[_sessions/&lt;log&gt;.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/&lt;kind&gt;/&lt;slug&gt;.md)]
        KB3 --> PC[conflicts/&lt;id&gt;.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

  • computeNodesHash is content-addressed and mtime-independent.
  • generateIndex / generateGraph are pure functions of nodes/ plus an injected now.
  • slugify, deriveNodeId, ensureUniqueId are pure.
  • crypto.randomUUID() is the only randomness, scoped to run_id minting.

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 for src/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

This site uses Just the Docs, a documentation theme for Jekyll.