The work directory
Every artifact the pipeline produces, where it lives, what validates it, and what is safe to delete.
The work directory (--work) is where everything the pipeline produces lives. One per
repository you are documenting.
<work>/
phase1/
graph.json the call graph — everything downstream reads this
functions.csv every function, flat, for grepping or a spreadsheet
graph.dot Graphviz: dot -Tsvg graph.dot -o graph.svg
dropped-calls.json calls we could NOT resolve, categorized — not hidden
scan-coverage.json files we could NOT read or fully parse — not counted as covered
phase2/
cards/<rel>.json one card per source file, mirroring the source tree
cards/_coverage.json how many files got prose, and which did not
cards/_rejected/ replies that produced no usable card (capped at 20)
skeleton.yaml the stage spine
assignment.json file → stage
organization.yaml intra-stage groups + reading order
strategy.json which strategy produced the above
phase3/
narration.json stage and system prose
registers.json cross-stage state registers
cache/ content-hash caches for prose and registers
handbook/ the rendered output, once you run `render`
run-manifest.json model, phases, timings and token usage of the last good runThree properties worth relying on
Everything is schema-validated on read
Every artifact carries a version field and is validated with zod when it is loaded. A
corrupted or hand-edited artifact fails loudly and names itself:
handbook: error: work/myrepo/phase2/skeleton.yaml: stages.3.id: InvalidIt never propagates into a later phase.
Two deliberate exceptions, both about resilience rather than laxity:
- Cards — a single unparseable card file is skipped, not fatal. One foreign or half-synced JSON in the cards directory would otherwise crash resume, phases 2b/2c/3 and every model load.
languagesmetadata ingraph.json— optional, because there is no artifact migration mechanism here and every graph written before fidelity declarations existed must keep validating.unparsedFilesis optional for the same reason: absent means the analysis predates the record, not that nothing failed.
Every write is atomic
Write to a temp file, then rename. A crash mid-write never leaves a half-written artifact for the next run to choke on.
One run per work directory
generate and resync take the same re-entrant directory lock. A concurrent CLI run and
Studio job on the same artifacts would interleave writes; the second one is refused with
a clear message instead.
What is safe to do
| Action | Safe? | Note |
|---|---|---|
| Delete the whole work dir | ✅ | Nothing outside it was modified. Regenerate from scratch. |
| Commit it to git | ✅ | It is all text. Handy for reviewing what a regeneration changed. |
Delete phase3/cache/ | ✅ | Costs you a full re-narration next run. |
Delete phase2/cards/_rejected/ | ✅ | Diagnostics only. Cleared automatically at the start of a new cards pass. |
Hand-edit skeleton.yaml | ✅ | It is validated on read, and --skeleton exists precisely for this. |
Hand-edit graph.json | ⚠️ | It is generated. Re-run analyze instead. |
Delete phase2/strategy.json | ⚠️ | The next run falls back to file, which may not match the artifacts. |
| Share it publicly | ⚠️ | Cards quote and describe your source. Treat it like source. |
Reading it by hand
The graph is the interesting one:
# how big is this codebase, really
jq '.metadata | {files: (.scannedFiles|length), nInternalFunctions, nEdges}' phase1/graph.json
# the busiest functions — where a change is most likely to fan out
jq -r '.nodes | to_entries | map(select(.value.kind=="internal"))
| sort_by(-.value.nCallers) | .[:15]
| .[] | "\(.value.nCallers)\t\(.value.qualname)\t\(.value.file)"' phase1/graph.json
# what could not be resolved, by category
jq '.metadata.byCategory' phase1/dropped-calls.json
# which files the scan could not turn into facts, and why
jq '.metadata.byReason, .files' phase1/scan-coverage.json
# which files never got prose
jq '.missing' phase2/cards/_coverage.jsonfunctions.csv is there for the same reason — sometimes the fastest tool is a
spreadsheet.
The two coverage files answer different questions
_coverage.json answers "which files did the model fail to describe?".
scan-coverage.json answers the question underneath it: "which files did the parser
never manage to read in the first place?" — with a reason of unreadable, unparsable
or partial.
The first two contribute no facts, so they are also removed from
graph.json's scannedFiles: nothing downstream writes a card about a file nobody opened
and then counts it as described. partial files stay — tree-sitter recovered from the
syntax error and the functions it did find are real, merely incomplete.
An empty files array means every scanned file parsed cleanly. That is a claim; the
artifact simply being missing is not.
Where else things are written
Handbooks writes outside the work directory in exactly two places, both opt-in by the command you ran:
<source>/.handbook-patches/— created byapply, holds backups and their manifests. A.gitignoreis written into it automatically so backups never enter git.$HOME/.handbook-studio/— Studio's repository registry and its auto-created work dirs. Move it with--state-dir.