Schemas
Every YAML frontmatter and JSON state file is validated by a Zod schema at read time. The schemas in src/lib/schemas.ts are the source of truth; when this page disagrees with the code, the code wins.
All shapes carry schema_version: 1. A schema mismatch is a parse failure; the file is silently dropped.
Node (nodes/{practice,map}/<slug>.md)
---
schema_version: 1
id: practice-prefer-constructor-injection # <kind>-<slug>
title: "..."
kind: practice | map
tags: [string, ...]
derived_from:
- 20260510-1014-session-abc.md
relates_to: [string, ...]
depends_on: [string, ...]
confidence: low | medium | high
summary: "≤140 char summary, used in INDEX.md"
---
Validated by NodeFrontmatterSchema. Git history is the timeline of record; the frontmatter carries no separate timestamps.
Field rationale
schema_version: integer schema marker. A mismatch is a parse failure.id: stable identifier in the form<kind>-<slug>. Used byrelates_to,depends_on,derived_from, andtarget_node_idon curator actions.title: human-readable label rendered inINDEX.md.kind:practice(how we build) ormap(what exists). Drives directory placement undernodes/<kind>/and theINDEX.mdsection the node lands in.tags: free-form labels used by the## By topicsection inINDEX.md.derived_from: list of sources (session log filename, repo-relative doc path, or absolute path).doctor --verboselists dangling refs; the consume path silently ignores them.relates_to: loose cross-references, rendered inGRAPH.md. Not enforced.depends_on: strict cross-references, rendered inGRAPH.md. Not enforced.confidence:low,medium, orhigh. Curator default:mediumfor implicit sources,highwhen stated explicitly with rationale.summary: ≤140-character one-liner injected viaINDEX.md.
Two kinds
- Practice: how we build. Imperative guidance.
- Map: what exists. Named entities (modules, services, vocabulary).
The proposal prompt splits combined statements: “use bravo_analytics.dispatcher, our event-tracking service” becomes one practice (use the dispatcher) and one map (what the dispatcher is).
Conflict resolution
When the curator emits a contradict action, the /kb-curate skill walks each pending file under .ai/knowledge-base/conflicts/ with the user. The menu is three-way and git-driven:
| Choice | On-disk effect | Side effects |
|---|---|---|
| Accept | Skill rewrites nodes/<kind>/<target_node_id>.md from the proposed body. | Contributor git restores the conflict file to discard it. |
| Reject | None. The existing node file is untouched. | Contributor git restores the conflict file to discard it. |
| Keep as record | None to the node tree. | Contributor git commits the conflict file; it stays in .ai/knowledge-base/conflicts/ as durable history for future curate runs to read. |
Conflict files (conflicts/<run-id>-<n>.md)
The curator records contradict actions as one markdown file per conflict under .ai/knowledge-base/conflicts/, instead of writing conflicting nodes to disk. The file shape is set inline by the curate wrapper (src/lib/curate.ts); there is no Zod schema for the file (it is reviewed and resolved by humans, not parsed for state).
---
id: <run-id>-<n>
status: pending
detected_at: <ISO>
run_id: <curator run-id>
candidate_origin: <session_id>:<practice|map>:<index>
target_node_id: practice-foo
proposed_kind: practice | map
proposed_title: "..."
---
## Rationale
<curator's free-text rationale>
## Proposed node
<the proposed node body as the curator would have written it to nodes/>
The /kb-curate skill reads every file with status: pending after the curator subprocess exits, walks each with the user, and lets the user advance the file via git restore (Reject and Accept-after-applying) or git commit (Keep as record). ai-knowledge-base status reports the pending count.
Curator failure reports
runCurate returns a failures: FailureReport[] array alongside conflicts. Failures cover two cases the curator must not paper over:
reason: "add_collision": anaddaction targets a node that already exists on disk.reason: "modify_missing_target": amodifyaction’starget_node_iddoesn’t resolve to an existing file.
Failures are reported in CLI output and not persisted; rerun the curator after fixing the underlying issue.
Session log (_sessions/<YYYYMMDD-HHmm-id>.md)
schema_version: 1
session_id: <claude-code-session-id>
captured_by: stop | session_end | pre_compact | manual
captured_at: 2026-05-11T10:00:00Z
transcript_hash: sha256:<hex>
proposal_status: pending | done | failed | skipped
proposal_completed_at: <ISO> | null
proposal_error: <string> | null
proposal_log: _logs/proposal/<id>__<ts>.jsonl | null
secret_scan_status: clean | redacted | blocked | skipped
topics: [string, ...]
proposals:
practice: [<ProposalCandidate>, ...]
map: [<ProposalCandidate>, ...]
curator_processed_at: 2026-05-11T11:00:00Z # set after curate
curator_run_id: <UUID>
Validated by SessionLogFrontmatterSchema.
Proposal candidate
kind: practice | map
tags: [string, ...]
title: <string>
summary: <≤140 chars>
body: <markdown>
confidence: low | medium | high
supports_existing_node: <node-id> | null
contradicts_existing_node: <node-id> | null
Validated by ProposalCandidateSchema. Top-level: ProposalOutputSchema = { practice: [...], map: [...] }.
Bootstrap candidate
Superset of the proposal candidate with derived_from. supports_existing_node and contradicts_existing_node are always null in bootstrap output. Validated by BootstrapCandidateSchema.
Curator action
action: add | modify | contradict | drop
candidate_origin: "<session_id>:<practice|map>:<index>"
target_node_id: <node-id> | null
proposed_node: <CuratorProposedNode> | null # null only for drop
rationale: <free-text>
suggested_resolution: supersede | keep_both | reject | null
Validated by CuratorOutputSchema (array of actions).
INDEX.md / GRAPH.md frontmatter
schema_version: 1
nodes_hash: sha256:<hex>
node_count: 47
Validated by IndexFrontmatterSchema / GraphFrontmatterSchema.
nodes_hash algorithm
Deterministic, mtime-independent:
- Walk all
.mdfiles undernodes/. - For each, compute
sha256(contents). - Build strings:
<relative-path>\t<sha256-hex>. - Sort lexicographically.
- Join with
\n. nodes_hash = sha256(joined).
Defined in computeNodesHash (src/lib/nodes.ts).
State files
.state/state.json
{
"schema_version": 1,
"lock": { "name": "...", "pid": 12345, "acquired_at": "...", "ttl_ms": 1800000 },
"last_nudged_at": "2026-05-11T10:00:00Z"
}
lock is null when no lock is held. Validated by StateFileSchema. Gitignored.
.state/bootstrap-state.json
Records the SHA-256 of every doc the bootstrap pipelines have processed. Hash hits are skipped on re-runs.
{
"schema_version": 1,
"last_full_bootstrap_at": "2026-05-10T14:30:00Z",
"last_incremental_at": "2026-05-15T09:12:00Z",
"docs": {
"docs/architecture/auth.md": {
"content_sha256": "abc123...",
"last_processed_at": "2026-05-10T14:32:00Z",
"produced_nodes": [
"practice/practice-auth-flow.md",
"map/map-auth-module.md"
]
}
}
}
| Field | Meaning |
|---|---|
last_full_bootstrap_at | Last /kb-bootstrap run. Never set by the CLI. |
last_incremental_at | Last bootstrap-incremental non-dry-run that processed ≥1 doc. |
docs[].content_sha256 | SHA-256 of file contents at processing time. |
docs[].last_processed_at | Timestamp of last processing. Not updated on hash hits. |
docs[].produced_nodes | <kind>/<filename>.md paths (relative to nodes/) written from this doc. Informational. |
Lifecycle:
- First run: file is created with
docs: {}. - Hash hit: doc is skipped;
last_processed_atis not updated. - Hash miss: doc is queued. On success, the entry is overwritten. On failure, the entry is left untouched so a re-run retries.
--dry-run: file is read, never written.- Force re-bootstrap: delete the file.
A malformed file is treated as missing. Validated by BootstrapStateSchema. Gitignored.