Running the daemon
Start, observe, pause, park, and stop the background build loop that drains a repo’s spec backlog. For an operator running the harness on one or more repos.
The daemon builds a spec only after its PR is merged onto the default branch. It reads .docs/plans and .docs/shipped from the committed base-branch tree, never from the working tree, so an unmerged spec/<slug> branch is invisible to it.
Prerequisites
| Requirement | Check |
|---|---|
ai-conductor on PATH | ai-conductor --help |
| GitHub CLI 2.73.0 or later, authenticated | gh --version reports 2.73.0 or later; gh auth status succeeds |
tmux installed (for daemon start and every management verb) | tmux -V |
| The repo registered | ai-conductor register <path> |
| A fresh install | bin/install --check exits 0 or 2 — the freshness gate accepts both |
| At least one merged spec on the default branch | .docs/plans/<slug>.md present on main |
Fix a blocked merged spec
Discovery reads only the default branch. When it cannot build a merged spec, it marks that spec blocked rather than dispatching it, writes the reason and remedy to .daemon/blocked.json, and logs one of these lines:
skip <slug>: merged spec cannot build — stories not approved (need "Status: Accepted", no DRAFT). Fix the spec on the default branch; logged once.
skip <slug>: merged specs cannot build — ADR .docs/decisions/<adr-file> is not approved (status "<found>" | no status declaration). Approve <adr-file> on the default branch. logged once this pass.
skip <slug>: merged spec cannot build — plan has no dependency tree ("## Task Dependency Graph" or "**Dependencies:**" lines). Fix the spec on the default branch; logged once.
skip <slug>: merged spec cannot build — missing or unparseable coherence artifact (.docs/coherence/<slug>.md) required for tier <tier>. Author it on the default branch; logged once.
The first rejects an unapproved stories artifact. Before a spec can land, each newly added ADR must be named adr-YYYY-MM-DD-lowercase-hyphenated-slug.md using a real calendar date; ADRs already on the default branch keep their legacy names. The second rejects a non-conforming ADR corpus: discovery scans every .docs/decisions/adr-*.md file on the default branch once per pass, and any merged spec is blocked while any ADR’s first declared status is not APPROVED or SUPERSEDED (including an ADR with no status declaration at all) — approving the offending ADR unblocks every spec the next pass, with no daemon restart. The third rejects a plan without a task dependency tree. The fourth applies only outside tier S: author a parseable .docs/coherence/<stem>.md on the default branch, or verify that the feature is correctly classified as tier S. For a structural parse failure, the BLOCKED remedy and skip line append Detail: line <n>: <message>; correct that row or table separator in the committed artifact. Missing, empty, and table-less artifacts have no source row, so they do not report a line number. Fix the indicated artifact on the default branch; the next discovery pass replaces the blocked snapshot, clearing a repaired spec without manual cleanup. Each reason is logged once per slug (the ADR reason once per pass for the whole corpus) through .daemon/warned/<slug>; the marker suppresses repeated poll warnings until the spec is fixed.
Run ai-conductor daemon status to read the persisted BLOCKED section. It lists each blocked slug, machine-readable reason, remedy, and the latest scan age without invoking Git or the network. An empty snapshot reports that no specs are blocked; no valid snapshot means the blocked state is unknown until discovery completes. The startup dashboard does not yet render blocked specs; dashboard treatment is tracked in #1332.
DECIDE artifacts are human-authored before merge. The daemon pre-seeds every DECIDE step (recording tier-skipped steps as skipped) and starts at BUILD; it never authors or reruns DECIDE work.
Start the daemon
ai-conductor daemon start
start refuses to launch on a stale install: it runs bin/install --check first, and on drift it either prompts to run bin/install --update (interactive) or throws (non-interactive). A stale install means newly added skills are unregistered and would fail silently mid-build.
On success it creates a tmux session and auto-attaches read-only. Detach with Ctrl-b d. You may invoke daemon management or a direct daemon run from any directory inside the repository: the daemon resolves the main repository root first, so runtime files always live in the root .daemon/ directory rather than in a nested package or linked worktree.
| Variant | Result |
|---|---|
ai-conductor daemon start on a TTY | Starts and attaches read-only |
ai-conductor daemon start -D | Prints daemon started (detached). Attach with 'conduct daemon connect'. |
ai-conductor daemon start with no TTY | Prints daemon started (no interactive terminal to attach to)… |
Exit 1 on any error, including a missing tmux.
Watch it work
ai-conductor daemon status
Sweeps the whole project registry and prints one badge line per repo — state, path, pid, since, engine version id, pause metadata, last log line and its mtime, and tmux session up/down. It then prints GATED: and BLOCKED sections plus an attribution-agreement line. BLOCKED reads the latest discovery snapshot and lists every blocked spec’s reason and remedy; it is not yet part of the startup dashboard (#1332). The nine state badges and what each one means are in cli reference; ● running is the one you want.
status exits 0 even when entries are stale or missing — those are reported, not errors. It exits 1 only when the registry itself is unreadable.
For the log:
ai-conductor daemon logs # this repo, whole file
ai-conductor daemon logs --lines 200 # last 200 lines
ai-conductor daemon logs --follow # stream new lines until Ctrl-C
ai-conductor daemon logs --all # every registered repo, with ==> path <== headers
--lines/-n, -f, and --repo=<path> all work but are absent from --help. --follow with --all prints --follow is not supported with --all; showing a static snapshot. and does not follow. A missing log prints (no daemon log yet for <path>).
Lines a feature run owns are tagged with its slug, so a serial drain of several features stays readable and greppable:
[daemon] holding daemon lock (pid 12345) for /home/you/code/my-project
[daemon][my-feature-254] ▶ start my-feature-254
[daemon][my-feature-254] · ▶ build
[daemon][my-feature-254] claude: done — 54 turns, 8m7s, $4.96
[daemon][my-feature-254] Implemented the parser and committed.
[daemon][my-feature-254] · build via claude (opus) ✓ — 54 turns, 8m7s, $4.96
Filter one feature’s narrative with ai-conductor daemon logs | grep '\[<slug>\]'. Untagged [daemon] lines are daemon-wide, not feature work. The exact shapes and the slug length bound are in artifacts.
Build-review rubric progress
Before publication, FINISH rechecks the current validation evidence. If one validator crashes during that final recheck, the daemon retries only that validator within its configured attempt budget; already passing validation siblings are not repeated. A retry never authorizes publication by itself: FINISH publishes only after fresh passing evidence exists. When the budget is exhausted, the feature HALTs and the publication coordinator is not invoked.
During build_review, the daemon log names each rubric branch and its full lap identifier. This lets you distinguish concurrent or retried review laps without reading .pipeline/events.jsonl:
· build_review [<lap-id>] <rubric> started
· build_review [<lap-id>] <rubric> cache hit
· build_review [<lap-id>] <rubric> PASS
· build_review [<lap-id>] <rubric> skipped: <reason>
· build_review [<lap-id>] <rubric> infrastructure failure: <reason> — <excerpt>
· build_review [<lap-id>] outer verdict: <effective> (raw: <raw>)
skipped is not a judged failure. An infrastructure failure is also distinct from a judged FAIL; it means the rubric could not run. The raw: suffix appears only when a deterministic policy changed the effective verdict. A projection-oversized infrastructure failure includes measured and limit byte counts and halts for an operator without consuming the shared mechanical-fault allowance. The optional excerpt, deterministic reason, and unresolved-marker count are included only when the event supplies them.
A rebase that changes the active feature’s stories or PRD reruns its PRD audit and coverage binding. Changes to its plan or coherence carrier rerun coverage binding. Document-only changes leave BUILD and aggregate test proof intact; unrelated features’ documents leave these reviews intact. Rebase events name the input paths responsible for each decision.
Protected-artifact rebaselines
The daemon distinguishes a stale pre-rebase seal from a genuine protected-artifact mutation:
Protected artifact rebaseline: trigger=proactive-rebase fromCommit=<old> toCommit=<new> paths=<paths>
Protected artifact rotation refused: condition=<condition> path=<path>
The first line is successful recovery: the engine proved the protected artifacts came from the base branch and rotated the seal. defensive-history-rewrite is the equivalent verification-time trigger for a seal stranded by an earlier rebase. The refusal line is a real guardrail failure; read its condition and path, then follow the stalled-feature runbook. Never delete or rewrite .pipeline/protected-artifact-seal.json by hand.
engine-append-unvouched means a recorded remediation-task heading exists, but the committed artifact is not an exact append of the base-tip or fingerprint-verified sealed content. The refusal also reports the operator-reseal and engine-append exit outcomes; review that content before deciding whether an approved amendment needs a reseal.
An approved plan or architecture amendment after first BUILD intentionally makes the existing seal baseline stale. Review the amendment, then reseal the approved paths with ai-conductor reseal before clearing the HALT (--clear-halt clears it in the same command). If the refusal occurs during REKICK before git starts, the HALT begins protected-artifact seal error; it is not a rebase conflict and must not be sent through git rebase --continue.
A ai-conductor reseal outcome also logs:
protected artifacts resealed: <paths>
protected artifact reseal refused [<path>] — <condition>
<path> on the refusal line is present only when the refusal condition names a specific artifact.
A related but distinct line covers remediation, not the seal itself:
↩ remediation gap <gapId> → plan — sealed artifact <artifact> — <task title|rationale>: "<directing clause>"
This fires when a remediation gap’s target — its task scope or, absent a **Files:** declaration, a directed reference in its rationale prose — names another feature’s sealed DECIDE artifact. The gap is redirected to the owning DECIDE step instead of routing to build; see gates. When available, the suffix names whether the directing text came from the task title or rationale and quotes the clause the engine read. The quote is whitespace-normalized and capped at 160 characters so the daemon event ledger remains line-oriented. Older redirect events without this evidence retain the shorter line.
Provider attribution and result summaries
Three line kinds tell you what a step actually did, which provider ran it, and what the feature cost in total — the per-step attribution matters because providers are routed per step (llm_provider top level plus per-step overrides; see configuration), so the provider executing a given step is not necessarily the repo default.
<provider>: done — <turns>, <duration>, <cost>followed by the agent’s own prose is the provider subprocess’s captured result. Non-interactive Claude--print --output-format stream-jsonstdout and Codexexec --jsonstdout are machine envelopes; the daemon summarizes the telemetry and prints the human-readable result text instead of teeing the raw single-line JSON blob. Output the daemon does not recognize as a machine envelope — prose, stderr, crash traces — is still logged verbatim, so no diagnostic detail is lost.· <step> via <provider> (<model>) ✓ — <turns>, <duration>, <cost>attributes the completed dispatch.grep ' via 'over the log answers “which provider ran this step” without inspecting process argv. A provider skipped from a cached availability result dispatches no process and is not logged. A candidate skipped because required setup is unavailable also dispatches no process, but its fallback prints⚠ PROVIDER FALLBACK; the line identifies the unavailable candidate and the next provider.· gate <step>: satisfiedor· gate <step>: unsatisfied — <reason>states the objective gate verdict. A satisfied line may include a reason; neither verdict line uses the provider-completion check glyph, so it is distinct from the preceding dispatch attribution.-
· finish: total usage — <dispatches>, <cost> (<n> cost-metered dispatches), <fresh> fresh + <cached> cached→<out> tok, <n> cost-unmetered (tokens counted, cost not), <n> unmeteredis logged once, when the feature’sfinishstep completes.<fresh>counts non-cached input tokens;<cached>counts prompt-cache reads and creation — the conversation an agentic dispatch resubmits on every internal tool call, billed at a fraction of fresh input (the+ <cached> cachedpart is omitted when no cache volume was tracked). Per-dispatch provider lines qualify the same way with a(N% cached)suffix. It is the sum of every dispatch that feature recorded in its own.pipeline/events.jsonl— so it spans the whole build, including steps run in earlier daemon dispatches, not just the session that happened to reachfinish. After finish usage is persisted, the engine refreshes the committed.docs/shipped/<slug>.md## Costblock from the same rollup and attempts one final push. Both operations are best-effort: a refresh failure can leave the local record at its pre-finish snapshot, and a push failure can leave the PR branch at that snapshot, without re-dispatchingfinish. A concurrent upstream commit is adopted only when its tree exactly matches the verified refresh; unrelated branch content still fails closed under the ordinary shipment-evidence gates.The
(<n> cost-metered dispatches)clause appears only when<cost>covers fewer recorded dispatches than the feature total. A non-zerocost-unmeteredcount means<cost>is a PARTIAL figure: those dispatches reported token counts that ARE in the token totals, but no dollars. That happens when a provider reports no cost of its own (codex, or a Pi message without a cost) and the model it ran has no entry in the committed.ai-conductor/rate-card.json— see the rate-card section of the configuration reference, and runai-conductor rate-card refreshto close the gap. The clause is omitted when every metered dispatch also carried a cost.Token figures appear when at least one dispatch was metered. Cost appears only when at least one of those dispatches was cost-metered; if all reported tokens but no price, the token and
cost-unmeteredsegments remain while the money figure is withheld. A build whose provider reported no usage prints its dispatch count and an explicit<n> unmeteredinstead of a fabricated$0.00— “never measured” must not read as “free”. Unreadable or missing event records are counted as unmetered for the same reason. The line is best-effort: a feature never fails to ship because its cost could not be computed.
daemon status does not yet carry the provider for a step that is still in flight (#1081).
To watch the session itself:
ai-conductor daemon connect # attach READ-ONLY
ai-conductor daemon connect --write # attach READ-WRITE (same as `debug`)
ai-conductor daemon debug # attach READ-WRITE
Ctrl-b d detaches from any of these.
If you’re already inside a tmux client (an interactive shell in a tmux pane), attaching directly hits tmux’s own nesting guard (sessions should be nested with care, unset $TMUX to force). Use --attach-into <target> to deliver the attach into an already-open pane elsewhere on the same tmux server instead of taking over the current process’s terminal:
ai-conductor daemon connect --write --attach-into mywindow:1.0
<target> is a tmux session, session:window, or session:window.pane string. This also works on daemon start.
Everything the daemon spawns — provider sessions (Claude, Codex) and the test or verification subprocesses it runs (full-suite, scoped-run, smoke, closeout) — starts with TMUX and TMUX_PANE scrubbed from its environment. A child that inherited them and ran a targetless tmux command (tmux respawn-pane -k, tmux new-session) would have tmux resolve the target to the daemon’s own pane and kill the daemon silently. A dispatched test or agent that genuinely needs tmux must name its target explicitly with -t; see environment.
If an enforcement script still cannot be restored, the build remains halted rather than dispatching without its attribution gate. The recheck after a repair is authoritative and strict: a script must exist as an executable regular file at the expected path, so a hook that restored non-executable or that resolves through a symlink still counts as not restored and halts the build rather than arming.
When the implementation PR is opened
The implementation PR is opened as a draft when the feature enters the SHIP phase — before the first SHIP step is dispatched — not at finish. The engine pushes the feature branch (a plain push; it never forces) and opens one draft PR against the discovered base branch, with a placeholder title and body.
This keeps the PR available for the whole ship tail while the implementation branch remains out of release-artifact maintenance. The serialized release-PR workflow owns pending CHANGELOG.md and VERSION changes after implementation PRs merge.
What the draft window does and does not mean:
- Nothing merges it. A draft PR cannot be merged, and the mergeable sweep excludes drafts from its autoresolve and CI-fix candidates, so no remediation runs against an in-flight build’s own PR.
- FINISH authors the prose, then flips it ready. The engine-owned publication coordinator re-observes the PR and dispatches one bounded title/body pass —
author_pr_prosewhile the body is still the engine-seeded placeholder,judge_pr_prosefor a quality verdict on prose that exists. The authoring pass is given the branch diff and the feature’s spec artifacts, so the judgment pass is never asked to grade a body nobody wrote. Only after prose is accepted does the coordinator write and push the shipped record and mark the PR ready for review. That order is deliberate: the shipped record is the daemon backlog’s dedup key, so committing it before the prose survived used to make a prose halt permanently un-redispatchable. When the judgment finds an authored body structurally incomplete, FINISH re-observes that exact revision and routes it back toauthor_pr_prosewith the judgment’s objection, rather than treating the author→judge lap as an ordinary retry. It re-enters FINISH after each verified transition, so a revision or retry resumes instead of replaying publication effects. - The placeholder body already has the right shape. It is the
/prbody template —## Why,## What Changed,## Testing, and theClosesreference — with each section explicitly marked “not yet authored”, so a reader landing on the PR mid-build, and the FINISH authoring pass filling it in, both see the section shape FINISH will demand. It carries no release metadata: choosing a release disposition is the pre-finishrelease-dispositionstep’s job. - The placeholder body is deliberately marked as one. It carries the engine’s body-floor marker, which is how FINISH knows deterministically that the body is unauthored and must be written before anything judges it; FINISH never records completion from placeholder or halt content. If the completion gate still observes a floored body on the recorded PR, it re-dispatches
finishfor a body rewrite — never/remediate, and never a re-openedbuild. The marker is provenance, not a verdict: it is an invisible HTML comment, so an authoring pass can rewrite every word around it and leave it in place. FINISH therefore classifies a marked body by its content — the intact “not yet authored” sections, the draft note, or free text no larger than the one description slot a floor can fill. Authored prose that kept the marker counts as authored, and an untouched floor still counts as a placeholder, without anyone having to instruct the provider to delete the marker. - It inherits the issue’s criticality. When the feature came from an intake issue, the engine copies that issue’s
priority: <band>labels onto the PR as it is adopted, so the PR list carries the same urgency the daemon dispatched on without anyone opening the linked issue. Only the criticality family is copied —size:and every other label stay on the issue. This is fail-open: no linked issue, an unreadable label list, or a rejected label write logs one[pr-criticality]line and changes nothing else. Re-entering SHIP re-applies the same labels, which GitHub accepts unchanged. - It is advisory. If the push is rejected or
ghis unauthenticated, the engine logs one loud[ship-draft-pr]line and the build continues; only the finish-time publish is load-bearing. - It is idempotent. Re-entering SHIP after a kickback, resume, or rework reuses the open PR — it never opens a second one and never re-drafts a PR that finish already marked ready.
- Self-host builds are included. The VERSION-approval and release-artifact gates still run before
finish. The coordinator also requires the configuredrelease-dispositionevidence to be a regular file written during the current feature run; missing, stale, malformed, or unreadable evidence stops before judgment, while a process restart alone does not invalidate it.
There is no configuration for this; the timing is fixed.
Provider preparation timeout and activity telemetry
daemon.log records step boundaries, provider activity, build progress, and verdict-freshness decisions. The deterministic BUILD verification path runs test_suite before build_review. For build_review, prd_audit, architecture_review_as_built, and preserved manual_test evidence, the freshness line names the step and artifact:
· build_review verdict build-review.json preserved — surface miss
· ✗ build_review verdict build-review.json invalidated — stale verdict rejected
· prd_audit verdict prd-audit.md rewritten — current
preserved means the code changed outside the gate’s judged surface, so the prior passing verdict remains valid. invalidated means the judged surface changed and the stale verdict was rejected; the gate must run again. For SHIP-tail gates, a stale verdict can mean that the report belongs to an earlier dispatch; the daemon treats it as no verdict, retries within the existing step budget, and never routes its findings. rewritten means the current judging attempt produced the artifact.
After a BUILD repair, test_suite still runs. A prior evidence file does not skip it. The suite verifier logs its settle decision after it evaluates current evidence:
· BUILD member test_suite settled: reuse (fingerprint-match)
reuse means the suite’s existing evidence remains valid; recompute means it derived fresh evidence. The basis is a closed diagnostic classification, never command output, credentials, or an absolute host path. The group join remains the authority that marks a member satisfied.
Before a provider process is spawned, its candidate resolution, session setup, and self-host preparation are bounded by provider_preparation_timeout_minutes (default 5; see configuration). The first expired preparation attempt is revoked and receives one replacement for that logical step. If the replacement also expires, the daemon writes a needs-human HALT headed Provider preparation exhausted.; it does not start another replacement. The daemon log and dashboard identify the preparing, running, recovering, or halted lifecycle phase with the attempt id and recovery count.
After spawn, activity is observation-only. While a step’s provider subprocess is running, the engine touches .pipeline/step-heartbeat in that feature’s worktree on every observed stdout/stderr activity boundary (throttled to at most once every few seconds — activity telemetry, not a transcript or termination control). The IN-PROGRESS dashboard the daemon prints on startup (and re-prints at key transitions) shows the current dispatch’s elapsed time, the latest validated aggregate test outcome, its current input/output token totals when live provider observation is available, and the heartbeat’s age when one exists:
IN-PROGRESS (1)
• my-feature [M] @build (working) (activity telemetry: 12s ago) (elapsed: 3m12s) (last test outcome: PASS) (children: 2) (tokens: 12 in / 34 out)
A feature with no (activity telemetry: … ago) suffix hasn’t produced its first activity pulse yet (a step that just started) — that’s distinct from a stale heartbeat, and is never rendered as if the step were stuck.
If no current-dispatch start event is available, elapsed time is omitted. If test-suite evidence is missing, malformed, or unreadable, the dashboard says (last test outcome: unavailable) rather than inferring a result.
(working) means the current dispatch has fresh activity telemetry; (waiting) means the current dispatch returned but its completion gate is still unmet. A waiting acceptance-specs step also names its RED-evidence state and the completion predicate’s unmet-condition reason:
IN-PROGRESS (1)
• my-feature [M] @acceptance_specs (waiting; RED: rejected; completion condition: acceptance specs RED run shows 0 failed — RED not established) (children: unknown) (tokens: unavailable)
completion condition: unavailable means the completion predicate did not supply a reason. The dashboard shows (children: N) only when the current live provider observation reports an active child count; otherwise it says (children: unknown), never inventing a count. It likewise shows (tokens: <input> in / <output> out) only from that current live observation, and says (tokens: unavailable) when no live token observation exists. These figures are current totals for the dispatch, not a finish-time usage rollup.
The heartbeat file is overwritten, never cleared, so a worktree keeps its last pulse after the step that wrote it ends. The dashboard ignores a heartbeat from another step or from before the current dispatch; a leftover heartbeat is treated as “no heartbeat yet.” Neither a missing nor a stale heartbeat terminates, retries, replaces, or completes a running provider.
step_heartbeat_stall_minutes is accepted only as a deprecated compatibility no-op. It grants no termination authority and is never used as provider_preparation_timeout_minutes; changing it has no effect on heartbeat telemetry or provider lifecycle behavior. See the configuration reference for its compatibility contract and the stalled-feature runbook for preparation-exhaustion recovery.
Pause and resume dispatch
A pause stops the daemon starting new work while leaving in-flight work and the daemon process alone. It is the right tool when you want the loop to go quiet without killing it.
ai-conductor daemon pause
ai-conductor daemon resume
pause prints daemon paused, or already paused when the marker exists. resume prints daemon resumed, or not paused. Both are listed in ai-conductor daemon --help.
The marker is .daemon/PAUSED. Its existence is authoritative; its JSON body is informational only. Reads fail closed — an unreadable marker counts as paused.
Park a feature before you touch its git state
Park first. Always. The daemon re-dispatches anything in its backlog and re-creates branches you delete, and its resume path re-kicks git errors with no backoff. Removing a worktree or branch under a live daemon produces a git worktree add failure loop, not a clean stop.
ai-conductor daemon park <slug>
You should see:
Parked '<slug>' — it will not be dispatched or re-kicked until unparked.
Marked for park: <repo>/.daemon/parked/<slug>
Park validates the slug: either .docs/plans/<slug>.md or .worktrees/<slug> must exist, otherwise it prints error: slug '<slug>' not found under <root> … and exits 1. It resolves the main repo root via git rev-parse --git-common-dir, so it works from the project root or from inside any worktree. Re-parking an already-parked slug is a no-op that reports when it was originally parked.
An operator park outranks everything: the re-kick sweep checks it first, ahead of the shipped-record dedup and the per-SHA guard, and preserves a pending .pipeline/REKICK sentinel rather than consuming it.
If the feature is already running, the daemon lets its current provider attempt finish, then declines every later attempt, including retries. In a parallel group, each started member may finish its current attempt but a parked member does not retry. The daemon records the outcomes and blocks the next serial step or parallel group. It does not create a HALT or interrupt a provider call. Interactive conduct runs are unchanged. The command reports whether work is still running, fully stopped, or unknown; see daemon park for the exact output.
To release:
ai-conductor daemon unpark <slug>
unpark resets the no-evidence attempt counter first and removes the park marker only after that succeeds — a failed reset deliberately leaves the marker in place for retry. You should see Unparked '<slug>' and reset no-evidence counter — normal dispatch and re-kick resume.
Known limitation.
ai-conductor daemon parkwith no slug does not print a park usage error. The park detector returns null without a slug,parkis a known sub-verb so the unknown-sub-verb guard passes it through, and the invocation falls all the way to the inline refusal, printingconduct: the inline SDLC pipeline now runs under the \inline` subcommand.` and exiting 1 — a message unrelated to parking. Always pass the slug. Tracked in #1012.
Parked-feature reconciliation
On startup and on every idle poll tick, the daemon classifies each parked slug: merged, orphan (its source issue is closed but the work never merged), normal, or unclassified (the check was unavailable). ai-conductor daemon status annotates the parked list accordingly — — orphan — needs manual review or — merged — ready to reconcile.
A slug counts as merged on either of two signals:
- A shipped record on the base branch.
.docs/shipped/<stem>.mdcommitted onorigin/mainis this harness’s definition of “the work shipped”, and it is what the daemon backlog dedups on. It is matched allowing for theYYYY-MM-DD-plan-date prefix, because park markers are keyed by the undated slug while records are keyed by the dated plan stem. This signal is durable: it still answers after the branch is deleted at merge, and after a squash or rebase merge leaves the branch tip outsideorigin/main. - Branch ancestry. Any local branch whose final path segment is the slug —
feat/,spec/,fix/,chore/, whatever prefix the author used — thatgit merge-base --is-ancestorproves is contained inorigin/main.
A missing branch, an unreadable origin/main, or a git failure yields unclassified and no action. It never reads as “not merged”.
The same sweep also considers every non-detached worktree registered directly under .worktrees/. It ignores nested worktrees and worktrees outside that directory. A registered candidate is retained without attempting removal when it is in flight, belongs to an engineer-* or resolve-* lifecycle, has an unreadable or present .pipeline/HALT, has an invalid slug, or the worktree listing cannot be read. This scan does not depend on the mergeable-watch registry.
By default (reconcile_parked_auto_cleanup is unset or true), a merged parked slug with a .docs/shipped/<slug>.md record on origin/main is reconciled automatically: its worktree is removed, any branch for it is deleted, and it is unparked. The record on origin/main is what settles completion here, so a worktree whose local .pipeline/conduct-state.json still reads mid-build — the normal state for anything built before feature_status existed, or for a finish that pushed and then died — does not block cleanup. The shipped record is never authority for the deletion itself; every local branch for the slug must first be proven to hold no commit that deleting it would drop, by either of two proofs:
- Ancestry.
git merge-base --is-ancestor <branch> origin/mainsucceeds (fast-forward or merge-commit merge). - Merged-PR head identity. A
MERGEDpull request for that branch reports the branch’s current tip as the commit it merged (gh pr list --head <branch> --state merged --json headRefOid). This covers the squash- and rebase-merge case, where the merge rewrites the commits and ancestry is structurally false forever even for a branch carrying nothing beyond what landed. One extra local commit moves the tip, the SHAs diverge, and this proof fails.
If neither proof holds for some branch, cleanup is refused and nothing is deleted, even though the slug still classifies merged. The reason distinguishes no merged-PR proof (no-merge-proof), commits the merged PR head does not contain, whether added after it or left behind by a rebased PR head (unmerged-commits), a branch whose tip is an ancestor of that head (branch-behind-merged-head), and evidence that Git or gh could not check (ancestry-check-failed). For unmerged-commits, daemon reconcile-parked prints up to ten SHA subject lines and an overflow count, so the operator can inspect what cleanup would drop. Once a proof holds, the branch is deleted with the safe git branch -d only; no force flag is ever used. git’s own -d merge check refuses a squash-merged branch whose tip is not an ancestor of the local default branch. That branch is left in place and the refusal is reported as branch-delete-failed, never escalated to -D.
Before any worktree is removed, the reconciler checks git status --porcelain inside it, both before and after the project teardown runs. Any output, or a status that cannot be read, refuses cleanup with dirty-worktree, so uncommitted or untracked work in a worktree is never deleted.
Worktree removal tolerates one more real-world shape. Some .worktrees/<slug> paths exist on disk without ever having been registered as git worktrees, and git worktree remove rejects those with “is not a working tree” rather than a missing-path error. The reconciler checks git worktree list --porcelain and, when the path is genuinely unregistered, deletes the leftover directory directly instead of refusing. A removal failure on a path git does own — locked, dirty, permissions — still refuses with worktree-remove-failed, and so does an unreadable worktree listing.
A merged parked slug with no shipped record yet is left parked and, when a merged PR can be found, gets an ST-916 record-repair PR requested on its behalf; it reconciles on a later tick once the record lands. A registered worktree follows the same proof checks, but a shipped record is required only for a feat/daemon-* branch; another branch with merge proof can be reclaimed without one.
Set reconcile_parked_auto_cleanup: false to disable automatic cleanup only for parked slugs and use reclaim_merged_worktrees: false to make registered-worktree reclamation report-only. The explicit command remains available for a parked slug:
ai-conductor daemon reconcile-parked <slug>
See daemon reconcile-parked for its exact output and refusal reasons. Reclaimed, retained, and failed registered-worktree outcomes are persisted in the daemon event ledger as worktree_reclaim_reclaimed, worktree_reclaim_retained, and worktree_reclaim_failed. The daemon log renders reclaimed outcomes, and a failed removal, branch deletion, or unpark as ✗ worktree reclaim failed. Any other helper refusal leaves the worktree intact and renders as ↷ worktree retained <slug> (<branch>; <reason>). It is shown once per slug until its branch or reason changes, although the ledger records it on every sweep. A worktree whose .pipeline/phase-active marker is more than seven days old is treated as abandoned rather than in flight; every merge proof and the dirty-worktree check still apply to it. An orphan classification is never auto-reconciled — it needs an operator to decide whether to park it, delete it, or resume it manually.
The cleanup sweep writes one aggregate line when its counts change instead of one line per parked slug. It includes refused=N and, when nonzero, a per-reason breakdown plus guidance for the dominant refusal. The startup dashboard runs an observational classification pass with cleanup disabled, so its aggregate always reports refused=0. A cleanup-sweep line joins its tag directly to the daemon prefix and explains the next action for every nonzero outcome, for example:
[daemon][parked-reconciliation] reconciled=0 deferred=1 orphaned=0 parked=7 refused=2 skipped=56; refusals: unmerged-commits=2; next: 1 deferred awaits shipped-record repair; 2 refusals requires resolving unmerged-commits; 7 parked remain parked; 56 skipped retry when merge/issue evidence is available
Project setup runs once per worktree
When the daemon prepares a worktree it runs the project’s bin/setup, and a successful run records a marker at <worktree>/.daemon/setup-ok.json. The marker names the bin/setup that ran (a content-and-mode fingerprint) and the base SHA the worktree was prepared against, plus the commit the worktree was actually at — provenance for reading the record, never part of the skip decision.
A later dispatch re-reads the marker and skips setup only when the recorded fingerprint and the recorded base SHA both still match what it resolves now. Everything else re-runs setup: no marker, a marker that cannot be read or is from an older format, an edited bin/setup, or a base moved by a rebase or re-kick — which is exactly when migrations and dependency changes arrive. Task commits made by the build move the worktree’s HEAD but not its base, so they never re-run setup. A failed setup leaves no marker, and the marker is deleted before each re-run, so a broken setup can never be skipped.
When setup fails, the daemon uses a bounded recovery ladder before it parks the feature. If the worktree is dirty, it first preserves the residue on wip/setup-quarantine-<slug>, resets to a clean HEAD, and retries setup once. A successful retry resumes the feature and leaves .pipeline/QUARANTINE as a notice for reviewing the preserved changes. The daemon starts its one provider repair session only when setup still fails at that clean HEAD; a successful dirty-tree retry never spends that repair attempt.
The repair session is accepted only after the daemon re-runs bin/setup and mechanically verifies the candidate state. It retains a verified uncommitted repair in an engine commit, accepts an existing repair commit when its history is forward from the original HEAD, and rejects rewritten history, setup drift, a still-failing setup, or an unpreserved candidate. Rejections park the feature; when preservation succeeds, the same quarantine branch holds the rejected attempt and the original HEAD is restored. A provider failure that leaves the worktree unchanged also parks, but does not create repair or quarantine state.
Triage verification never trusts the marker: all of its verification re-runs are forced, so they always execute bin/setup for real. A forced run that succeeds records a fresh marker — a project repaired by triage skips setup on its next dispatch instead of paying for it again.
Under the daemon each setup decision is emitted as a project_setup event, persisted in the feature’s .pipeline/events.jsonl and rendered into the daemon log, so the log says whether setup ran and what made it run. A completed or rejected repair also emits setup_repair; a rejected event names its reason and, when available, its quarantine branch and preserved paths.
The marker lives with the worktree and dies with it: a worktree recreated from its branch has no marker and re-provisions from scratch. To force a single re-run by hand, delete <worktree>/.daemon/setup-ok.json.
Project teardown hook
To clean project-owned resources before the daemon removes a feature worktree, add an executable bin/teardown to the project. The daemon runs it from the worktree immediately before an already authorized removal: post-ship reaping, ai-conductor daemon reclaim-worktree <slug>, or parked-feature reconciliation. It never runs for a retained worktree or before the removal safety proofs pass.
The hook inherits the daemon’s process environment, with CI=true and WORKTREE_NAMESPACE overlaid. The namespace is derived from the worktree directory name. Do not depend on .env or .pipeline/: reconciliation can clean a leftover directory where those files are absent. A missing hook is a silent no-op.
#!/usr/bin/env sh
set -eu
# Remove only resources namespaced for this feature.
./scripts/drop-preview-resources "$WORKTREE_NAMESPACE"
Keep the hook idempotent. Its output is summarized by default; set daemon_verbose: true to log each non-blank line. A non-zero exit, an unrunnable script, or a timeout is logged with the teardown: prefix and is contained: the daemon still attempts the already-authorized removal. This differs from a real Git removal failure. daemon reclaim-worktree returns an error instead of reporting removal, and parked reconciliation retains a registered worktree with worktree-remove-failed for recovery. The hook is bounded by teardown_timeout_seconds (default 120 seconds); that bound cannot be disabled. See configuration for value handling and environment variables for its process contract.
Per-dispatch hook
Before project preparation, every daemon dispatch ensures the worktree’s .memory/ path is linked to the canonical project memory store. A legacy directory containing entries is migrated at that point; an empty directory receives the canonical symlink through fresh setup, without a migration backup. The daemon logs the observed pre-setup state and whether the final path is canonical; setup or telemetry failure is reported as a non-canonical result and does not prevent project preparation or dispatch.
With OpenTelemetry enabled, conductor.memory.setup counts these observations by before and canonical, using the existing project, worker, and feature identity. Failure text remains in the event and daemon log rather than becoming a metric label.
An executable bin/dispatch-start runs at the end of worktree preparation on every daemon dispatch, including a redispatch whose successful bin/setup marker skips provisioning. It receives the same CI=true and WORKTREE_NAMESPACE environment as setup and teardown. The hook is optional and silent when absent. Its output is shown only with daemon_verbose: true; a non-zero exit, timeout, or unrunnable script is logged and contained so the dispatch proceeds. Configure its bounded timeout with dispatch_start_timeout_seconds (default 120 seconds); see configuration.
Retained worktrees
A feature’s worktree is not removed when its implementation PR opens. The mergeable sweep tears it down only after the PR reaches MERGED or CLOSED and a .docs/shipped/<slug>.md record is proven present on origin/main — the same signal parked-feature reconciliation uses to define “shipped”. Until then the worktree is retained on disk, one sweep tick at a time:
The daemon/auto finish session records and publishes the outcome but performs no worktree cleanup. Opening, updating, or marking the implementation PR ready therefore cannot bypass this sweep-owned gate. Interactive local-merge and explicitly confirmed discard outcomes use their separate, proof-gated cleanup paths.
MERGED, record not yet onorigin/main. Logged asretained <slug> — reason: record-not-yet-on-main, re-checked on the next tick. This is the normal window between merge and the shipped-record commit landing.CLOSEDwithout merging. Logged asretained <slug> (reclaimable) — reason: pr-closed-unmerged. The PR is pruned from the watch registry (there is nothing left to poll), but the worktree itself is left behind for inspection or manual recovery — it is never deleted automatically.- Record proven present. The sweep invokes cleanup and logs
reaped <slug> — reason: shipped-record-on-main. A failingbin/teardownis logged separately with theteardown:prefix and does not stop the removal attempt. Do not treat that hook log as proof that Git removal failed;daemon reclaim-worktreeand parked reconciliation surface their actual Git removal failures and retain the worktree for recovery.
ai-conductor daemon status’s startup dashboard groups every retained worktree under RETAINED WORKTREES (<n>). A retained row includes an evidence-derived reason and a remedy: line. pr-open-awaiting-main appears only after the daemon has verified that the ledger’s PR URL is still open; an unavailable, failed, or mismatched PR lookup reports pr-state-unknown instead. Legacy ships with no recorded URL report shipped-no-pr-reference. Closed-unmerged, unknown, and legacy retained rows name ai-conductor daemon reclaim-worktree <slug> as the available operator action; an open PR states that retention ends when the PR lands on main.
NEVER-STARTED (<n>) is separate from retained worktrees. It means the dashboard found no readable .pipeline/conduct-state.json; it remains dispatchable and needs no operator action. PARKED and a live HALTED marker take precedence over both groups: their rows state the reason and print the corresponding ai-conductor daemon unpark <slug> or HALT-clear remedy. A slug appears in only its highest-precedence dashboard group.
To remove a single retained worktree by hand — a closed-unmerged one you’ve decided not to resume, or one you want gone before its shipped record lands — use daemon reclaim-worktree:
ai-conductor daemon reclaim-worktree <slug>
It refuses a slug with a resume in progress, refuses anything but a single plain slug (no globs, no paths, no lists), and is a no-op when the worktree is already gone. It never touches the branch — both manual reclaim and the automatic sweep remove only the worktree. The automatic sweep never deletes the feature branch, and its reap gate is shipped-record presence on origin/main, not branch ancestry.
Operator safety rules
Each of these encodes a failure that has already corrupted daemon state.
- Park before you touch a feature’s git state. See the section above. Never unpark, then delete — that guarantees a re-dispatch race.
- Never bulk-delete worktrees or branches. Do not
rm -rfover a glob or a computed set, and never loop-delete branches. Enumerate every path explicitly, print the list, confirm it, then delete.mapfile/readarrayare bash-only and silently do nothing under zsh — a guard built on an unpopulated array deletes everything it was supposed to protect. - The branch is the source of truth; a worktree checkout is disposable. Removing
.worktrees/<slug>loses that worktree’s.pipeline/state — the task status and the evidence sidecar — which then produces falseno_task_progressstalls on work that is already committed. Recreate the worktree from its branch and recover the evidence rather than letting the build redo finished tasks. See worktree and evidence recovery. - A manual PR is not a harness finish. Opening a PR by hand tells the daemon nothing, so it re-dispatches the feature forever and parking is the only stopgap. Record the ship instead — see the next section.
- Dispatched sessions cannot run
ai-conductor. Every session the daemon dispatches carriesCONDUCT_DAEMON_SESSION=1, andai-conductorrefuses to run under it (exit 1) except for the session-sanctioned worker commands its skills mandate — so a maker session can never park, unpark, restart, or reseal the daemon that dispatched it. See the CLI reference.
Record a manual finish
ai-conductor shipped-record --slug <slug> --pr <url>
Use --pr local for a merge-local finish. This writes and commits .docs/shipped/<slug>.md on the current branch, hashing .docs/plans/<slug>.md and its stories file, so the merge atomically records the ship and the daemon’s backlog dedups it.
It is idempotent; identical content already committed produces no duplicate commit. The exit code proves nothing — the command exits 0 even when it wrote no record, so verify the file before you rely on it. See shipped-record reconciliation.
Restart after an engine change
ai-conductor daemon restart
Behavior depends on what the daemon is doing:
| Daemon state | Outcome |
|---|---|
| Idle | For the self-host harness, fast-forwards the installed main checkout and rebuilds/relinks it; then clears any stale lock, reconciles an orphaned process, and respawns. The outcome message is always printed |
| Paused | Counts as idle and follows the same refresh/rebuild/respawn path; the pause marker is never touched |
| Busy | Writes .daemon/RESTART-PENDING and returns at once. At the next idle boundary, a forced source refresh runs before rebuild/relink and respawn |
Single-repository restart never blocks or polls. Its self-host refresh fails closed: when the checkout is dirty, diverged, offline, or not on its default branch, restart reports the reason and does not rebuild or respawn from stale source. A degraded restart (fallback kill-and-recreate, which loses scrollback) is reported explicitly.
Fleet operations
pause, resume, and restart accept fleet selectors: --all, or one or more bare repo names after the verb. With a selector the verb iterates the project registry instead of acting on the current directory. None of these selectors appear in --help.
ai-conductor daemon pause --all
ai-conductor daemon resume <repo-a> <repo-b>
ai-conductor daemon restart --all
Each repo is handled in its own try/catch, so one failure never aborts the sweep. Per-repo restart outcomes are: paused → respawn, idle → respawn, busy → queued, stopped with no session → daemon started (was stopped), error → reported and the sweep continues.
Stop the daemon
ai-conductor daemon stop
Kills the tmux session. Exit 1 on error.
To halt one in-flight feature rather than the whole loop, see emergency stop a running feature.
Run the daemon in the foreground
Bare ai-conductor daemon runs the loop in the current terminal, with no tmux session and no supervisor. Use it for a bounded drain or for debugging.
ai-conductor daemon --continuous --max-items 3 --idle-poll 30
Three things shape the run itself. Every flag, its default, and its exact parsing behavior are in cli reference — several real flags are absent from --help, and integer flags fall back to their defaults silently rather than erroring.
- Choose the worker-pool width.
--concurrency <n>runs up toneligible features at once. Without the flag, the daemon usesdaemon_concurrencyfrom merged configuration; without either, it uses the serial default,1. The explicit flag wins. AtN=1, the daemon preserves the recorded serial dispatch and maintenance ordering. - Bound a
--continuousrun. With no--max-items,--max-cost,--max-runtime, or--max-idle-pollsit warns and then runs until youCtrl-Cit. Dispatching a feature restarts the consecutive empty-poll count used by--max-idle-polls. - Pass
--idle-pollexplicitly if the polling interval matters. Its effective default does not match its help text.
ai-conductor daemon --help (or -h) anywhere after daemon prints the daemon help and exits 0. That guard runs before every daemon dispatcher on purpose: without it, --help would be treated as an unknown flag and would launch a daemon run.
A typo’d sub-verb — anything outside status, logs, park, unpark, reconcile-parked, start, stop, restart, connect, debug, pause, resume — prints conduct daemon: unknown subcommand '<token>'. followed by the daemon help, and exits 1.
Finish-time mergeability
After publication, watched PRs have a separate recovery loop controlled by mergeable_autoresolve.enabled and ci_watch.enabled. Conflicts take precedence over CI repair. The harness repository explicitly enables both; see self-host open-PR recovery for its suite command and activation requirements. This does not enable automatic merging.
An autoresolve attempt whose rebase completes without conflicts still runs the preservation checks and configured suite before pushing with a lease. refreshed is reported only after that push succeeds; failed verification or a rejected push reports escalated.
At the daemon-only rebase step immediately before finish, the engine first checks whether the feature can merge cleanly with the current base. Textual cleanliness alone is not enough to skip the rebase — git merge-tree proves only that the two trees do not collide, never that this branch’s gates were graded against the base that will actually be merged into. A clean result is skippable only when both of these also hold:
- The base has not moved in code or this feature’s active review inputs. No code or test path, active story, PRD, plan, or coherence carrier differs between the branch’s merge-base and the base ref. If the base gained code after
build_reviewgraded the diff,test_suiteproved a tree, ormanual_testexercised behavior, those verdicts predate it. A changed active review input can also change the PRD-audit or coverage-binding result. In either case the engine rebases and lets delta-aware invalidation decide what to re-verify. Unrelated documentation still permits a skip. - The base is not a degraded fallback. When an
originremote exists but its default-branch discovery orgit fetchfailed, the engine compares against the LOCAL base branch, which in a daemon worktree can be arbitrarily far behind origin — a “clean” verdict against it means nothing, so the skip is refused. A repository with genuinely nooriginis not degraded: its local base is authoritative and remains skippable.
An uncomputable merge-base or base delta is likewise not skippable — the justification for the skip could not be established.
A skip records rebase_mergeable_skip and continues to finish without rewriting the feature branch or reopening downstream gate evidence. The skip line, the event and the gate verdict all name the exact ref, its sha, and whether it came from origin or a local branch — rebase skipped — cleanly mergeable with origin/main@c6839018bf47 (remote), no code/test changes on it since the merge-base — so a wrong skip is auditable from the log instead of requiring a source read.
A refused skip, a conflicting result, or an indeterminate result all keep the established rebase and recovery path: the engine attempts the real rebase, uses the bounded resolver when configured, and parks the feature if it cannot finish safely. A refused skip is not a conflict path — a textually clean branch rebases cleanly; it just re-verifies afterwards instead of shipping stale verdicts.
Merge-bearing feature branches
Every engine-started feature rebase — the finish-time rebase step, the re-kick rebase, and open-PR autoresolve — uses one replay rule:
- No merge in
<base>..HEAD: the engine runsgit rebase --autostash <base>unchanged. - Any merge in that range: the engine flattens the branch along its first parent. Ordinary commits replay as-is. A merge whose tree equals its first parent’s tree is ancestry-only and is dropped. Every other merge becomes one flattened merge commit with the merge’s tree, subject, and author, a single parent, and a
Flattened-merge: <merge sha>trailer. Side-lineage commits are not replayed individually, so merge resolutions are kept and duplicated commits never resurface as add/add conflicts.
Before anything moves, the engine proves that the flattened list rebuilds HEAD’s exact tree on the merge base, then dry-runs it against the target. A successful flatten records one rebase_merge_audit event; ordinary-commit conflicts then take the normal resolver path. The rebased history no longer shows the merges or side-lineage commits; evidence that cited them is translated to the commit that absorbed them.
The engine refuses, without mutating the branch, when:
- the dry run’s first conflict falls on a flattened merge commit. The feature parks with a HALT beginning
flattened rebase refused; autoresolve escalates withmerge-flatten-refused. - the proof fails before a content-bearing merge can be named. The feature parks with a HALT beginning
rebase did not start; autoresolve escalates withrebase-error.
Both refusals block finish and issue no push. For recovery, see a flattened rebase was refused.
How a halted feature resumes
When a feature halts, the daemon leaves .pipeline/HALT in its worktree and stops dispatching it. On a genuine advance of the base branch SHA, the re-kick sweep runs over every halted worktree and, per feature:
- Skips it entirely if it is operator-parked, already shipped, or already re-kicked at this SHA; the last condition reads the durable per-slug record in the main checkout’s
.daemon/rekicked/state directory. - Skips it, on every sweep regardless of SHA, if
.pipeline/HALT.classreadsneeds-humanorunclassified— onlymechanicalandlegacyare ordinarily retryable. See the classification table in stalled or stuck feature. - Aborts a paused rebase if one is mid-flight — a failed abort leaves the HALT marker intact rather than half-clearing it.
- Renames
.pipeline/HALTto.pipeline/HALT.cleared, preserving the reason. - Drops a
.pipeline/REKICKsentinel and records the triggering SHA in that durable per-slug record.
The sweep never dispatches directly; the cleared feature is re-dispatched on the next poll. That is why a git error left in a feature’s worktree gets retried without backoff, and why parking is the only reliable way to make a feature stay stopped.
Restarting the daemon no longer grants a fresh re-kick at an unchanged base SHA; clearing the halt or unparking the feature remains the sanctioned operator lever.
Budget-cap halts need an authorization
A budget-cap halt is the one exception to the ordinary needs-human retention rule. From the main repository checkout, inspect the feature and then authorize either more allowance or a fresh count:
ai-conductor kickback-budget inspect --feature <slug>
ai-conductor kickback-budget raise --feature <slug> --gate <gate> --by <positive-integer> --rationale "<why more allowance is justified>"
# or
ai-conductor kickback-budget reset --feature <slug> --gate <gate> --rationale "<why this count may restart>"
The command accepts only the live cap halt and generation it inspected, records the operator and rationale, and the daemon clears that matching halt on its next loop iteration. Do not delete the markers by hand. If the feature was already parked, unpark it after the authorization; an operator park deliberately prevents automatic consumption. See the kickback-budget CLI reference for its exact gate and terminal requirements.
For a plan-growth halt, use raise: it extends the feature’s plan-growth cap. reset only restarts a gate’s lap count and refuses plan-growth evidence.
DECIDE-entry halts need a grant
A needs-human HALT that begins DECIDE entry refused is not retryable. After deciding that the named DECIDE step should be authored, record a one-use grant from any directory inside that repository, then clear the halt:
ai-conductor decide-grant --slug <slug> --step <step> --reason "<why this authoring pass is approved>"
rm -f .worktrees/<slug>/.pipeline/HALT .worktrees/<slug>/.pipeline/HALT.class
The command resolves the main checkout and writes the grant to .daemon/grants/<slug>.json there — deliberately outside the feature worktree, so a build agent cannot authorize its own DECIDE entry by writing a file into .pipeline/. Outside a repository, it refuses without recording a grant. plan is never grantable: the command rejects it and the entry policy refuses it regardless, so a halt requesting a plan revision is driven by hand and then cleared.
The grant is scoped to the exact step and consumed immediately before its provider dispatch. Clearing the halt alone only makes the feature eligible to be checked again; with no matching grant, it halts again without entering DECIDE. Follow the full DECIDE-entry recovery procedure when the halt names an unknown target, missing artifact, or disputed routing.
Before any of this, the daemon runs a one-time startup migration (owned by whichever process holds the daemon lock) that stamps every pre-existing HALT still missing .pipeline/HALT.class as legacy, so a halt written before the sidecar existed is retryable like a mechanical one instead of silently stuck. A watermark at .daemon/migrations/halt-classification-v1 makes this run exactly once; a lock loser never runs it and never touches worktrees.
Post-rebase gate invalidation on resume
Honouring the REKICK sentinel always rebases the feature onto the advanced base before any gate resumes, even when it is cleanly mergeable. This play-forward path is intentionally different from normal finish: the pending gate must observe the advanced base in its worktree. When that rebase changes code, test paths, or an active review input, the downstream judged gates — test_suite, build_review, and (when they ran) manual_test, prd_audit, coverage_binding, architecture_review_as_built — are candidates for re-opening. Which ones actually re-open depends on the delta: each judged gate declares the surface its verdict depends on, and only a delta that lands inside that surface invalidates it.
| Gate | Depends on | Re-opened when the rebase delta touches |
|---|---|---|
test_suite | the whole tree | any code or test path, anywhere |
manual_test | all runtime source | any runtime source path, feature-owned or not |
build_review | the feature’s own code and tests | the feature’s own source or its own test files |
prd_audit | the feature’s own runtime source, active stories, and PRD | the feature’s own source, active story, or active PRD |
coverage_binding | the feature’s own runtime source, active stories, PRD, plan, and coherence carrier | the feature’s own source or one of those active review inputs |
architecture_review_as_built | the feature’s own runtime source | the feature’s own source |
build_review grades the feature’s own code and tests — currently only the opt-in testQuality rubric’s judgement of whether a changed test is insensitive to the behavior it claims to cover — so a delta made up entirely of foreign, main-side paths cannot change that grade and its verdict is preserved: a rebase that only merges in unrelated base work no longer pays for an LLM re-grade. Its own test files DO re-open it, since those are exactly what testQuality judges. test_suite deliberately stays maximally aggressive: it proves the exact tree, so any delta at all makes that proof stale, and it runs no model, so re-running it is cheap.
If aggregate verification then exposes a base-induced repair, the daemon records its sanitized failure identity in .pipeline/build-review-rebase-repairs.json. Entries accumulate across repeated rebases, but the retained testQuality rubric does not currently consume them as judgement context — see gates.
build is the exception. Its predicate re-derives mechanically from the rebased history — the union of Task: commit trailers with the .pipeline/task-status.json rows — so the daemon re-evaluates it against the new tree before deciding. If every plan task is still evidenced, the gate keeps a fresh satisfied: true verdict, a rebase_gate_reverified event records that dispatch was skipped, and no build agent re-runs finished work. Anything less — a plan task with no trailer, an unresolvable plan, or an error during the check — falls back to the ordinary kickback and the build step re-runs. This is fail-closed: the confirmation is itself a fresh evaluation of the rebased tree, never a carried-over verdict.
.pipeline/task-status.json is not the authority here. Nothing in the engine flips its rows to completed; the durable record of finished work is the Task: trailer on each commit, which is why losing or re-seeding that file does not by itself re-open a finished build.
Repair obligations keep a commit boundary for deciding which later Task: trailers can settle a repair. During the same rebase translation, the daemon rewrites that boundary when its pre-rebase commit has a patch-id match. If the boundary’s commit was absorbed upstream, it advances only to the earliest surviving later commit on the feature’s first-parent history, so the eligible evidence range can only narrow. Each rewrite is persisted as a repair_boundary_translated event with its direct or successor rule. A boundary without a safe successor remains unchanged and is named by its obligation id in the rebase_citation_residue event and .pipeline/rebase-residue.json; the ordinary repair-resolution path then continues to refuse that stale boundary rather than guessing.
Halt-PR presentation is cleared when the halt resolves
Escalating a halt marks the feature’s PR: draft status, the needs-remediation label, a body marker, and a halt comment. Every poll, the halt-PR reconciliation sweep re-reads the open PRs and re-applies any of those facets that drifted off.
The sweep also removes them. A marked PR whose head branch already carries the feature’s shipped record (.docs/shipped/<slug>.md, committed by /finish) has shipped, so the sweep undrafts it, removes the label, strips the body marker, and rewrites the halt comment to say the halt resolved. Being draft-and-labeled is evidence that the marking was applied, never that the halt still stands — without the shipped-record check a resolved feature stays drafted and labeled until a human clears it by hand. The check is fail-closed in both directions: it reads the committed branch tree (so a torn-down worktree cannot hide the record), and it refuses to guess a slug for any branch the daemon did not cut, so a hand-authored PR is never touched.
A reused halt PR is made presentable at resolution and at the dispatch boundary, not only at finish
The conductor opens the implementation PR as a draft at SHIP-phase entry, and that publisher adopts whatever OPEN PR already exists for the branch rather than opening a second one. A feature that halted earlier already has one: the needs-remediation placeholder the halt-PR reconciliation sweep opened. So the placeholder silently becomes the retained SHIP PR that every later ship step reads.
Two independent repairs keep that PR usable, at two different points in the run:
- Resolution-time presentation repair. Whatever resolves the retained SHIP PR’s identity — SHIP-phase adoption, the pre-finish snapshot, or the finish-time restore — repairs it first. It clears the
needs-remediationlabel and body marker, rewrites aneeds-remediation:title tofeat: <feature>, strips the halt banner from the body, and preserves the remediation narrative as a single PR comment. This is no longer bound to the SHIP-entrypublishedoutcome: any later consumer that resolves the same retained PR triggers the same repair, so a custom SHIP step scheduled ahead offinishnever reads a remediation placeholder regardless of which resolver reaches it first. - Dispatch-boundary halt-state clear. Before the first step of any run — not gated on phase — the conductor also runs a lighter clear that removes the
needs-remediationlabel and body marker and supersedes the halt comment, while preserving draft status. This reaches a feature that resumes intoBUILD, where the PR is resolved throughghdirectly rather than through the conductor’s own resolver, so a BUILD-phase halt no longer leaves the branch’s PR permanently occupied by the remediation placeholder. The clear re-reads the PR after removing the label and marker; if either is still present, or the read fails, the clear counts as partial and runs again at the next dispatch. Once verified, it rewrites the existing halt comment in place to say the halt resolved, and posts a new comment only when no halt comment is found.
Both repairs share the same properties:
- The PR stays a draft. Only
finishflips it ready-for-review, after the ship gates have run. Neither repair touches draft status. - Each repair is advisory and idempotent. Neither throws into the build loop, a PR with no halt signal costs one read and zero mutations, and repairing the same PR more than once leaves one repaired PR and one halt-history comment.
Before this, every repair was bound to the finish step, which runs last. Any SHIP step scheduled ahead of finish was handed the placeholder and could only refuse — a step that writes release metadata into the retained PR, for instance, would correctly decline to write it into a remediation placeholder and then fail its own completion artifact on every retry. A halt raised during BUILD was worse: the branch had no SHIP-phase entry to repair it at all, so its PR stayed a remediation placeholder until someone cleared it by hand.
Kickback-cap halt
The kickback budget is durable for each gate: it survives daemon re-dispatch while the feature’s tree hash and resolved-task count are unchanged. After the cap is exhausted, the daemon writes a HALT that names the gate, lap count, and most recent gate reason. build_review and the kickback-ping-pong guard classify that halt needs-human, so the re-kick sweep never clears it. test_suite’s cap halt stays mechanical and can still be cleared by the re-kick sweep on a base-branch advance.
Read the marker and fix the reported gate failure before resuming. To authorize a justified retry, use ai-conductor kickback-budget; do not clear the marker merely to retry the same unchanged loop.
The former terminal-less park caused by a passing stale BUILD-member verdict is retired. A repaired BUILD round re-verifies all non-skipped members, so it proceeds to its join or records an explicit halt for a real unresolved condition; operators should not park a feature merely to work around a missing terminal verdict.
Troubleshooting
status shows ⚠ session-up/process-dead. The tmux session outlived the daemon process. Run ai-conductor daemon restart, which reconciles the orphan (SIGTERM, then SIGKILL) and reclaims the lock before respawning.
The daemon reports that gh cannot satisfy version 2.73.0. Dispatch is paused before any feature is claimed. Upgrade or repair GitHub CLI, confirm gh --version reports 2.73.0 or later, then let the next poll resume dispatch. If finish-record reports an unsupported JSON field, it also refuses without writing a finish outcome; upgrade gh and rerun the command.
The daemon keeps re-dispatching a feature you already shipped by hand. You are missing the shipped record. Park it, then run ai-conductor shipped-record. See shipped-record reconciliation.
An intake command reports a corrupt ledger or a ledger.json.lease timeout. Do not bypass the dedup failure. Follow corrupt intake ledger or stuck ledger lease to inspect the quarantine copy, repair or replace the ledger, and safely clear an orphaned lease.
A feature the daemon itself finished is deduped from both sides: discovery skips it once the shipped record is on the base branch (post-merge) and once the record is committed on the feature’s own branch (feat/daemon-<slug>, pre-merge). The pre-merge half exists because a finish that records the ship but then reports failure would otherwise leave a completed feature eligible for re-dispatch, re-running finish and duplicating publication work while the original worktree remains retained.
The pre-merge half needs a second piece of evidence, because .docs/shipped/<slug>.md is committed by the mid-sequence write_shipped_record publication transition — on its own it proves one transition ran, not that the ship completed. So the pre-merge dedup skips a candidate only when FINISH recorded its outcome (.pipeline/finish-choice in the feature’s worktree) or the worktree is already gone; a retained worktree with no outcome record is re-dispatched and logged as re-dispatch <slug>: shipped record is on this feature's branch but FINISH recorded no outcome …. Without that, a FINISH that halted after writing the record was terminal: an operator could clear the HALT and discovery would still refuse the feature forever, and because the run never reported done it was never enrolled in the mergeable watch either, so nothing could reap it. The absent-worktree case still skips — there is nothing to resume, and re-dispatching it is the “path does not exist” loop the dedup was added to prevent. See a cleared FINISH halt resumes to a recorded ship.
A step fails with Cannot dispatch '<step>': its working directory … does not exist. The feature’s worktree was removed while the run was in flight. The engine refuses the dispatch before launching any provider, and the run halts immediately rather than retrying into the same absent path — previously this surfaced as an opaque provider error_during_execution blob and was retried and kicked back. Nothing is written back into the missing path: a stub there makes the next git worktree add fail 128. The branch holds the work; recover with worktree and evidence recovery.
The daemon is alive but nothing moves. Check for .daemon/PAUSED, a park marker under .daemon/parked/, and the GATED: section of ai-conductor daemon status. See stalled or stuck feature and daemon recovery.