Shipped record reconciliation
Land the missing .docs/shipped/<slug>.md for work that already shipped, so the daemon stops re-dispatching it. For operators who finished a feature by hand.
Not sure this is the right runbook? Run
/daemon-triage— it gathers the evidence read-only, classifies the failure, and routes to the one runbook that owns it.
A manual PR is not a harness finish. Opening and merging a PR yourself tells the daemon nothing. The only thing that records a ship is a committed shipped record on the base branch.
Symptom
- The daemon keeps dispatching a feature whose implementation is already merged.
ai-conductor daemon park <slug>is the only thing holding it back — and you have to keep it parked forever.- The startup dashboard lists the slug under ELIGIBLE even though its PR shows as merged.
ai-conductor shipment-evidence --pr <url>exits 1 with ashipped-record-*code.
Parking is a stopgap, not a finish. A parked feature is one you can never unpark, and its park marker is indistinguishable from a deliberate operational hold.
Diagnosis
1. Is the record on the base branch?
The dedup check reads shipped records only from the base-branch tree — via git ls-tree <base-branch>:.docs/shipped and git show <base-branch>:<path>. A record that exists in your working tree, or on an unmerged branch, is invisible by construction.
git ls-tree --name-only <base-branch>:.docs/shipped | grep "<slug>"
git show <base-branch>:.docs/shipped/<slug>.md
<slug> is the plan file stem: .docs/plans/<slug>.md → <slug>.
2. Is the record well-formed?
A valid record starts with a frontmatter block, exactly:
---
slug: <slug>
spec_hash: <sha256-hex>
pr: <pr url | local>
shipped: <YYYY-MM-DD>
---
The parser requires line 0 to be exactly ---, a closing ---, and non-empty slug and spec_hash. Anything else parses as malformed. A malformed record still dedups by stem — it just cannot dedup a renamed spec by content hash.
3. Ask the verifier
ai-conductor shipment-evidence --pr <implementation-pr-url>
| Output | Meaning | Exit |
|---|---|---|
shipped-record: valid <path> | Record present and bound to this PR. | 0 |
shipped-record: not applicable (<class>) | This PR is not an implementation PR for a plan. | 0 |
shipped-record: <refusal-code> | Named defect — e.g. shipped-record-missing, shipped-record-not-in-candidate, shipped-record-incomplete, shipped-record-hash-mismatch. | 1 |
The evaluator deliberately refuses to infer a successful ship from merge state alone. A merged PR with no record is a defect, not a ship.
4. Check the daemon’s local ledger
cat .daemon/processed/<slug>
{"status":"shipped","prUrl":"…"} means the daemon has already recorded the ship locally. This cache is a fast path, not the authority — the base-branch record is. A missing cache entry alongside a present base-branch record is harmless; the daemon repairs the cache the first time the dedup fires.
Recovery
Pick the branch that matches where the implementation is. Every one of them ends in ai-conductor shipped-record, so read this first — it applies to all of them.
Known limitation — do not trust the exit code.
ai-conductor shipped-recordis degrade-never-block by contract: any failure (unreadable plan, ambiguous candidates, filesystem error, git error) prints one warning —shipped-record write failed — dedup degraded to local cache for <slug>: <reason>— and exits 0. A clean exit does not mean a record was written, and dedup silently falls back to the local.daemon/processed/cache, which a later daemon on another checkout does not see. Always verify withgit cat-file -e(see Verification). Tracked in #1023.
The implementation branch has not merged yet
This is the normal path and the cheapest one. Run the command in the implementation worktree, on the implementation branch, before the final push:
cd .worktrees/<slug>
ai-conductor shipped-record --slug <slug> --pr <implementation-pr-url>
What it changes:
- Resolves the plan identity —
.docs/plans/<slug>.mdexactly, else exactly one.docs/plans/YYYY-MM-DD-<slug>.mdcandidate. Two candidates isambiguous; zero ismissing. It never guesses. - Hashes the plan bytes plus its stories file (the plan’s
**Stories:**reference first, else.docs/stories/<slug>.md) intospec_hash. - Renders the record, appending a Cost block when a cost rollup is available:
input,output,cache_read,cache_creation,cost_usd,dispatches,retries,halts,unmetered, and a per-provider breakdown; then appends a## Timeblock computed independently from.pipeline/events.jsonl. Its field contract and compatibility behavior are in the shipped-record reference. A missing or corrupt event ledger never blocks the Cost block or the commit. - Writes
.docs/shipped/<slug>.md,git adds it, and commitsshipped record: <slug>with--no-verify— but only if the staged diff is non-empty. Re-running with identical content prints✓ shipped record already committed: <path>and creates no duplicate commit.
Use --pr local instead of a URL for a merge-local finish. Never run it for a keep or discard finish — those did not ship.
Known limitation. The Cost block’s
halts:field counts persistedloop_haltevents from.pipeline/events.jsonl; it is written into every committed shipped record and re-read byai-conductor kpi. Use it as the ledger-derived count of recorded halt occurrences..pipeline/HALTremains the durable current park signal, and the daemon log provides immediate diagnostic detail. The per-provider breakdown is written but never surfaced:ai-conductor kpihas no parser for theproviders:lines.
Then push, and merge the PR. The record rides in with the code, so the merge lands the implementation and the shipped fact atomically:
git push
The implementation PR is already merged
Do not commit a record onto the base branch by hand. Use the reconciler, which branches off main, writes only the record, and never mutates main:
export GITHUB_REPOSITORY=<owner>/<repo>
ai-conductor shipment-evidence reconcile --pr <implementation-pr-url> --shipped <YYYY-MM-DD>
What it changes: creates shipment-repair/<pr-number>/<slug> off main, commits exactly one file (the write is asserted to be a .docs/shipped/*.md path — anything else throws), opens or finds the matching repair PR, re-verifies the evidence at the immutable repair head, and posts a shipped-record commit status carrying the verdict. The injected publisher surface offers no merge, approve, or auto-merge operation — a human merges the repair PR.
--shipped must match YYYY-MM-DD. GITHUB_REPOSITORY is required; without it the run throws GITHUB_REPOSITORY is required for repair publication.
| Output | Meaning | Exit |
|---|---|---|
shipped-record: aligned | Nothing to repair. | 0 |
shipped-record: repair-published | Repair branch and PR exist; merge it. | 0 |
shipped-record: unresolved | The defect is not one of the repairable record codes. | 1 |
Blast radius: none on main — the repair only ever exists on its own branch until you merge it. Confirm: the repair PR exists and its head carries a shipped-record commit status of success.
Hold the line while you fix it
Park the slug so the daemon stops burning dispatches during the repair, and unpark once the record is merged:
ai-conductor daemon park <slug>
# …land the record…
ai-conductor daemon unpark <slug>
Ordering rules are in emergency stop a running feature.
If the implementation PR is already merged (the case this section is under), landing the record is usually enough on its own: the daemon’s parked-feature reconciliation sweep detects the newly-committed record on origin/main on its next idle tick and removes the worktree, deletes the branch, and unparks the slug automatically — daemon unpark is then unnecessary. The record alone is enough; the sweep no longer needs the branch to still exist. If the branch does still exist and carries commits that cleanup cannot prove safe to delete, the sweep refuses it with a specific reason; an unmerged-commits refusal lists the commits that would be dropped. Merge or otherwise resolve the listed work, and the next tick reconciles the slug. Run ai-conductor daemon reconcile-parked <slug> to do the same thing immediately instead of waiting for the next tick, or if reconcile_parked_auto_cleanup is set to false.
The spec was renamed after it shipped
You do not need a second record. The daemon runs two dedup passes: a stem match, then a content-hash match that compares the candidate’s freshly computed spec_hash against every committed record’s. A renamed spec whose plan and stories bytes are unchanged still dedups, and the local cache is repaired under the new slug.
If the plan or stories content changed, the hash no longer matches and the candidate is a genuinely different spec — it will be dispatched, correctly.
Verification
Verify the record, then verify that the dedup actually took. The first without the second is the mistake this runbook exists to prevent.
The record is committed
git cat-file -e "HEAD:.docs/shipped/<slug>.md" && echo "record committed"
This is the check the finish flow itself trusts, precisely because the command’s exit code is not trustworthy. Run it in the worktree whose branch you committed on.
The record is on the base branch
git show <base-branch>:.docs/shipped/<slug>.md
Until the PR merges, this fails — and until it succeeds, the daemon cannot see the record at all.
The record parses and binds to the PR
ai-conductor shipment-evidence --pr <implementation-pr-url>
Expect shipped-record: valid <path> and exit 0.
The dedup actually fired
Two independent signals, both on the next daemon poll after the merge:
- The daemon log carries the skip line:
ai-conductor daemon logs --lines 200 | grep "shipped dedup"Expect
skip <slug>: shipped dedup — implementation already merged (base-branch shipped record found); not re-dispatching.— or, for a renamed spec,skip <slug>: shipped dedup — shipped under '<stem>', candidate '<slug>' matches by content (spec_hash); not re-dispatching.This message is logged once and suppressed thereafter by a
.daemon/warned/<slug>marker. Its absence from a later log is not evidence of failure — use signal 2. - The local processed cache was repaired:
cat .daemon/processed/<slug>Expect
{"status":"shipped","prUrl":"<url>"}. The daemon writes this when the base-branch dedup fires, so its presence proves the dedup ran.
The slug leaves the eligible set
ai-conductor daemon logs --lines 60
In the startup dashboard the slug must appear under PROCESSED — not ELIGIBLE, and not PARKED. If it is still PARKED, you have not unparked it, and the park is masking whether the dedup works. Unpark and re-check.
Verify the shipped record’s halts: count against the persisted loop_halt occurrences in that feature’s .pipeline/events.jsonl. A count of 0 means no loop_halt occurrence was recorded; use .pipeline/HALT and the daemon log for the current park state and diagnostic detail.
Related: stalled or stuck feature when the feature never reached a ship at all, and daemon recovery when the re-dispatch loop is a daemon fault rather than a missing record. Artifact-by-artifact detail is in artifacts.