Skip to main content
PM’s source of truth lives in .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.
The directory is created by /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:
Never hand-repair it by guessing which side of a conflict to keep — that is the user’s call. Meanwhile the hooks never write over it: 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 <…>):
Stale rule: a lock whose mtime is more than 30 s in the past (or in the future), or whose holder is confirmed dead on this host in this pid namespace, is broken by the next save; breakers are serialised by .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 queued epics, P0→P3, skipping any epic starved on an unresolved depends-on link (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 externalId when 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 archived progress indicator.
The 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:
Every minimal detour is logged here by the 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.
A wrong automatic row is corrected with a verb, never by editing this file. 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:
It appends one 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.
This log is never modified by hand. If you need to audit what happened during a session or trace when a particular change was introduced as a side-effect of a detour, this is the file to read.

CLAUDE.md rules block

PM writes and maintains a managed rules block in the project’s CLAUDE.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:init when a project is first set up.
  • Re-injected by the SessionStart hook via additionalContext on every session open, resume, and compaction recovery, so the rules survive context loss.
  • Refreshable via /pm:upgrade after a plugin update — run it to pull in any new rules or behavioral changes from the latest version.
  • Idempotent — deleting the block from CLAUDE.md and re-running /pm:init or /pm:upgrade restores 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. init and upgrade check before their first write and write nothing — a refused upgrade never stamps pmVersion. write-rules, set-tracker and set-review-mode refuse AT the block write, after their state save: delete the stray lines (shell, highest line number first), then run write-rules and render — NOT a re-run of the verb. The full refusal is on /pm:review-mode.
CLAUDE_PROJECT_DIR decides which repository every path below belongs to. When it is set, it overrides the working directory — which is correct and load-bearing for hooks and cross-repo dispatch, and silently catastrophic when the value is stale. As of 0.33.0 the engine warns, naming both repositories and how to undo it, when the variable resolves elsewhere and the directory you are standing in has a conductor of its own. It warns and never refuses: the defect was that a redirect looked identical in the output, not that redirects happen.
Never hand-edit state.json directly. Use PM commands. Bypassing the engine means render timestamps, active-epic invariants, and cross-link consistency can silently diverge. If you must recover from a bad state, use git checkout .conductor/state.json to restore the last known-good version — PM’s state changes are committed regularly, so git history is always the recovery path.
conductor.mjs verify-state compares state.json’s revision and mtime against the engine’s last recorded write — the stamp the last render wrote, or the lastSave every engine save adds to it, whichever is later (0.50.0). A hand-edit advances neither, so it fails loudly when the file changed at the same revision as that write, when the revision went backwards, or when it moved past any revision the engine recorded — a mechanical check for accidental hand-edits. Saves by verbs that do not re-render (a claim, set-activity-log) exit 0 with a note that PROJECT.md may be stale. It cannot see a hand-edit followed by an engine save, which re-baselines over it. Run it any time you suspect the file has drifted, or wire it into your own pre-commit checks.