.conductor/state.json. Every epic, every detour stack frame, every link, every tracker configuration — it’s all in that one JSON file. PROJECT.md is a generated view rendered from it and should never be hand-edited. Understanding what each file is for — and what the rules around touching them are — prevents the class of state corruption that’s hardest to debug: a file that looks valid but disagrees with the real state.
The .conductor/ directory
Three files live here. Each has a distinct purpose and a distinct write policy./pm:init and managed entirely by the PM engine after that. Only state.json is the authoritative source; the other two are logs and should never be edited directly.
Writing state — exit codes, the lock, and what the engine refuses to guess (0.44.0)
Read the exit code before you react. They mean different things and want different responses:
An unreadable
state.json is refused, never replaced — exit 11. Present but unparseable (conflict markers after a merge, truncation) or the wrong shape (top level not an object, epics or detourStack present and not an array, an epics element not an object): every verb that reads state refuses and writes nothing, and --force does not override it. Before this, such a file loaded as an empty record: add-epic over a conflicted three-epic file exited 0 and left only the new epic. ABSENT is still dormancy. The message names the reason and the remedies:
gate-guard blocks Edit/Write/NotebookEdit and, on Bash, only a member of its closed list of write shapes (exit 2) — but with a carve-out, since 0.46.0, that an affirmed Bash call carrying a command is allowed even over an unreadable record, so the remedies still run from the shell; brief injects only the warning; snapshot writes nothing (exit 11, never 2, which would block compaction); commit-nudge, on PostToolUse and PostToolUseFailure alike, writes nothing — the observation record included, so a commit that landed meanwhile is reported by the first run after the file is repaired (exit 2 when a commit has landed; 0 otherwise, without reading state). verify-state never loads the file, and activity reports the revision and the log’s on/off state as unknown.
Saves are serialised by a lock file. saveState() holds .conductor/state.json.lock from the revision check through the rename and read-back, and fsyncs the temp file before the rename. A live holder is waited for up to 2 s, then the save is refused with exit 9 (machine-specific values shown as <…>):
.conductor/state.json.lock.break and re-judge before removing anything, so none removes a lock it did not judge. --force bypasses only the revision comparison, never the lock. The residual the design accepts: a holder stalled past 30 s (a suspended laptop, a debugger) can be judged stale and, if it resumes in the instant before its rename, two writers write. This reverses 0.26.0’s decision against a lock file; the stale rule is the answer to that objection, and the revision guard alone lost updates under 16 parallel writers. Do not commit or hand-delete a fresh lock while a pm command is running.
state.json structure
The top-level fields of.conductor/state.json and what they mean:
A minimal real example of a
claude-code-lane epic with inline stories:
PROJECT.md
PROJECT.md is a generated Markdown view that renders the full conductor briefing. It is re-rendered by conductor.mjs render after every state change. Never edit it by hand — state.json always wins, and the next render will overwrite any manual changes.
The briefing shows:
- Active epic — title, lane icon, priority, and current status.
- Detour stack — each frame with its paused epic, reason, and spawned detour.
- NEXT UP queue — highest-priority
queuedepics, P0→P3, skipping any epic starved on an unresolveddepends-onlink (with the blocker named explicitly). - Per-lane counts — how many epics are in each lane, by status.
- Tracker sync line — any active-work epics missing
externalIdwhen a tracker is configured. Only honestly-computable drift is shown; PM never fabricates transition state it can’t see. - Hierarchy rollup — for parent epics, an
X/Y children archivedprogress indicator.
SessionStart hook re-injects the briefing via additionalContext every time a session opens or resumes after a compaction — this is how PM survives context loss without requiring you to re-read docs.
detours.log
.conductor/detours.log is an append-only trail of all detour events. One entry per line:
log-detour subcommand. Substantial pushes and pops are logged automatically. The commit hook — on PostToolUse and PostToolUseFailure since 0.45.0 — also auto-detects unlogged minimal detours from the commit’s shape — file count, commit prefix, whether an epic is active at all, and whether an active detour is already open — and logs them without requiring a manual /pm:detour --minimal invocation.
As of 0.45.0 the log’s own fields are escaped at write: the engine parses this file by line and tab, so a newline or a tab inside a detour reason or note can no longer start a row the engine never wrote. The same escaping runs on .conductor/honcho-memories.log, one line per entry.
0.45.0 stopped the hook logging work that was never a detour. A commit touching the active epic’s own artifacts (
openspec/changes/<id>/, its plan path, its spec path) writes no AUTO-DETOUR row, and a commit confined to a paused epic’s own artifacts writes no DETOUR-COMMIT row — decided from the paths, never from a subject prefix. A nested conductor no longer logs its own bookkeeping either: changed paths are compared from the conductor root, so a commit touching only projects/sub/.conductor/state.json and projects/sub/PROJECT.md is bookkeeping, while a mixed commit touching a file outside that root is still logged.detours.log is git-ignored while the PROJECT.md it renders is tracked, so a hand-removal leaves the false row in what gets committed. 0.45.0 added the inverse of the hook’s logging:
RETRACTED row covering every AUTO-DETOUR and DETOUR-COMMIT row of that commit, removes nothing, and re-renders PROJECT.md without them. <sha> is a commit sha, not a ref — HEAD is refused. It also refuses, naming why and writing nothing: no matching row, a MINIMAL-only row (a declaration, not retractable), an already-retracted commit, a missing or empty --reason, a too-short or ambiguous sha of a pruned commit, and a detached tree. There is no un-retract — re-declare with log-detour. See /pm:detour for the full rule.
Two corrections shipped in 0.32.0. The hook used to log a spurious minimal detour for a perfectly ordinary commit when no epic was active — there is no work to detour from in that case, so nothing is logged now. And the detection reads the repository rather than the command text, so a commit in any flag form is seen and a command that merely mentions
git commit is not.In 0.33.0 the trail stopped duplicating itself. A commit-derived row is refused when the trail already holds one for that
(sha, kind) — which mattered because the fallback rung has no memory of what it already logged. A MINIMAL detour is deliberately exempt: it records what the agent declared, not what git observed, so two of them at one commit are two real events. A - sha means “cannot tell” and is never treated as an identity.That duplication was half of why PROJECT.md was never clean — a bookkeeping commit appended a row, which changed the file, which produced another commit. The other half was a bookkeeping filter that guarded one branch of the hook and had never been applied to the identical branch beside it.CLAUDE.md rules block
PM writes and maintains a managed rules block in the project’sCLAUDE.md. This is how the conductor discipline — the detour protocols, the reconcile gate, the tracker sync instructions, the lane workflow rules — becomes part of every Claude Code session automatically, without you re-reading documentation.
The rules block is:
- Written by
/pm:initwhen a project is first set up. - Re-injected by the
SessionStarthook viaadditionalContexton every session open, resume, and compaction recovery, so the rules survive context loss. - Refreshable via
/pm:upgradeafter a plugin update — run it to pull in any new rules or behavioral changes from the latest version. - Idempotent — deleting the block from
CLAUDE.mdand re-running/pm:initor/pm:upgraderestores it safely. To permanently opt out, delete the block and do not run upgrade. - Located by whole marker LINES and written literally (0.44.0). Exactly one BEGIN line then one END line → replaced in place, every byte outside untouched, line endings following the file’s; no marker lines → appended; any other arrangement (an orphan marker, two blocks) → refused, exit 11, naming every marker’s line number, rules file untouched. A marker quoted in prose or inline code is ordinary text.
initandupgradecheck before their first write and write nothing — a refused upgrade never stampspmVersion.write-rules,set-trackerandset-review-moderefuse AT the block write, after their state save: delete the stray lines (shell, highest line number first), then runwrite-rulesandrender— NOT a re-run of the verb. The full refusal is on/pm:review-mode.

