Artifacts and state files

Every file the harness writes, file by file: what creates it, what reads it, whether it is committed, and what breaks if it disappears. For operators diagnosing a stuck feature and for contributors adding a step or gate. The reasoning behind the design is in evidence model.

The four trees

Tree Contents Committed Scope
.docs/ artifacts — the durable spec record yes repo
.pipeline/ state — one run’s working evidence no (.gitignore) one worktree
.daemon/ daemon-scoped state no (.gitignore) the main checkout
.worktrees/ build and spec worktrees no (.gitignore) repo

.memory/ and .memory*.bak/ are also gitignored siblings. The .pipeline/, .daemon/, and .worktrees/ patterns are unanchored, so they match nested copies such as src/conductor/.pipeline/.

.worktrees/ is the only worktree creation target. .claude/worktrees/ is a legacy convention the engine reads (resume probe) and excludes (self-host fingerprint) but never writes.

Known limitation. Removing a worktree directory destroys its .pipeline/, including task-status.json and the task-evidence.json sidecar. The branch survives, so already-committed work is safe. Reconstructible run state — task-status.json, the session hooks, the git hooks — is rebuilt mechanically at the next build entry, and completions proven by Task: trailers are restored with it (see Reconstruction). Everything else, including conduct-state.json and every gate verdict, is genuinely gone. Park the feature before touching its git state, and recover the rest rather than letting the build redo finished tasks — see worktree and evidence recovery. Tracked in #497.

.docs/ — committed artifacts

Nineteen entries. Alphabetized; the four with no code reference are marked.

Entry Naming Written by Read by / gate role
architecture/ YYYY-MM-DD-<topic>.md, plus fixed system-context.md, containers.md, components.md, erd.md, and a sequences/ subdir architecture-diagram, architecture-review, bootstrap architecture_diagram completion glob; mermaid render check at land; non-Small artifacts require at least one fenced Mermaid block; protected-artifact seal
audit/ free-form manual no code reference
audits/ free-form JSON a one-off backfill shipment-audit.ts — one hardcoded path, nothing else
coherence/ <plan-stem>.md coherence-check skill (M and L tiers only) coherence_check completion glob; the land-time coherence validator
coherence-waivers/ <plan-stem>.md operator, hand-authored the land-time coherence waiver check. The directory appears when the first waiver is committed — see below
complexity/ <slug>.md, with an undated-stem fallback complexity step, composer loop parseComplexityTier reads a Tier: <S\|M\|L> line. Missing ⇒ the daemon defaults to M; other paths differ — see where the tier comes from. The land gate enforces tier agreement
conflicts/ YYYY-MM-DD-<slug>.md conflict-check skill conflict_check completion glob
decisions/ New ADRs: adr-YYYY-MM-DD-lowercase-hyphenated-slug.md with a real calendar date. Existing legacy adr-<topic>.md and NNN-<topic>.md files remain valid; also architecture-review-*.md, technical-assessment-*.md architecture-review, assess, bootstrap, prd, simplify, debugging, finish architecture_review and assess completion globs; the land gate requires every newly added ADR to use the canonical date-prefixed name, while legacy ADRs already on the default branch are exempt. The land gate and daemon discovery both scan every adr-*.md and reject one whose first declared status is not APPROVED or SUPERSEDED
halted/ <slug>.md the halt-marker writer An operator-readable halt record on the feature branch. It records status, slug, halt class, halting step, phase, branch, HEAD SHA, UTC halt time, the full HALT body, and whether the record may be ahead of the remote. It is written and committed for an operator-actionable (needs-human, plan-gap, or protected-artifact) halt off the default branch, then pushed best-effort; mechanical halts produce no record. Clearing the halt changes the record’s status to resolved while retaining its original details.
intake/ <plan-stem>.md intake skill parseIntakeSourceRef reads Source-Ref: owner/repo#N; Owner: <id> drives the daemon owner gate
manual-test-results.md loose file legacy no code reference — superseded by .pipeline/manual-test-results.md
observation/ free-form manual no code reference
phase7-daemon-validation.md loose file manual no code reference
plans/ YYYY-MM-DD-<slug>.md — the stem is the canonical feature key plan skill; the composer loop writes .docs/plans/<slug>.md at land plan completion glob; land requires every parsed task to have a Done when: block with 2–5 nonblank list checks and refuses 41 or more tasks without exactly one non-empty **Scope-exception:** declaration; seeds .pipeline/task-status.json; the build predicate parses ### Task <id> headings; protected-artifact seal
release-waivers/ <plan-stem>.md operator, hand-authored in the same diff the self-host release gate. Also the only .docs prefix always writable during BUILD
shipped/ <plan-stem>.md ai-conductor shipped-record daemon backlog dedup; the only input to ai-conductor kpi
specs/ YYYY-MM-DD-<slug>.md prd skill (product track only) prd completion glob; protected-artifact seal
stories/ YYYY-MM-DD-<slug>.md, plus epics/ and features/<name>/ subdirs stories skill stories completion glob; plan-coverage check; coherence rows; protected-artifact seal
track/ <slug>.md, with an undated-stem fallback explore skill parseTrack reads a Track: product\|technical line. Missing ⇒ defaults to product. Decides whether prd and prd_audit run. The file also carries a Scope boundary: line recording the operator-confirmed fix breadth; plan and stories read it as binding free-form text — no code parses it

Every entry above is committed.

Abandoned work leaves no repository artifact; its record is a closed issue — see abandoning a spec.

.docs/coherence-waivers/ is the one entry you may not find on disk. Git does not track empty directories, so the directory appears the first time a waiver is committed — nothing pre-creates it, and its absence is not a defect. The mechanism behind it is live: the land-time coherence gate calls evaluateCoherenceWaiver (src/conductor/src/engine/engineer/coherence-waiver.ts, invoked from coherence-validator.ts) on every run that reports gaps, and blocks unless a fresh, well-formed waiver covers every gap id. Write the first one as a plain file at .docs/coherence-waivers/<plan-stem>.md.

Naming

Artifacts are keyed by the plan stem: the plan file’s basename with only a trailing .md stripped. Interior dots survive, so .docs/plans/phase-9.3b-intake.md has the stem phase-9.3b-intake. That stem is the shared key across the daemon backlog, the interactive conduct path, and the land gate — and the filename of the matching complexity/, track/, intake/, coherence/, and shipped/ entries.

The undated-stem fallback

Exactly two entries get a relaxed second lookup, and only in the daemon’s backlog discovery: complexity/ and track/. When <stem>.md is absent and the stem carries a leading YYYY-MM-DD- date, the daemon retries under the date-stripped stem — but only when exactly one plan maps to that undated base. Two plans sharing one undated base refuse the fallback rather than guess between features.

The exact stem always wins. A marker that resolves to nothing after both attempts logs the paths it tried to daemon.log, once per slug, before the daemon applies its default — so the miss is visible in the log rather than hours later as gate behavior. The relaxed stem is a lookup key only; it never keys state, and no other artifact gets it.

Waiver grammar

Both waiver kinds share one two-line idiom:

Waives: <comma-separated names>

Rationale: <non-empty prose>

A release waiver’s names must be drawn from exactly four canonical breaking surfaces: bin/conduct CLI, skill symlink targets, hook wiring, settings.json schema. A coherence waiver’s names must be gap ids the validator reported for this change set. Both are fail-closed on freshness: the waiver must appear as an added or modified file in the base...HEAD diff, so a waiver merged by a prior feature can never satisfy a later one. See releases and gates.

Coherence mapping shape

An M/L plan whose current change set contains citable decisions in non-deleted land-accepted ADRs (APPROVED or SUPERSEDED) carries this table:

## Architecture Obligation Coverage

| Decision | Disposition | Task(s) | Evidence |
| --- | --- | --- | --- |
| adr-<stem>#D<n> | task | task-<id>[, ...] | <exact Done-when fragment> |
| adr-<stem>#D<n> | existing | none | <specific existing implementation evidence> |
| adr-<stem>#D<n> | no-change | none | <why no implementation change is required> |

The land gate derives the required decision ids from the ADRs rather than trusting the table. task rows must cite real plan tasks and quote an exact substring of one cited task’s Done when: block after whitespace normalization. existing and no-change rows cite no tasks and require non-empty evidence. Bookkeeping validation is mechanical; coherence-check judges whether the evidence semantically satisfies the decision.

A coherence artifact may contain one mapping table or several. Ordinary trailing Markdown tables and commentary are ignored. A later table is mapping content only when its first data row begins with a recognized mapping row class. If a recognized mapping row is stranded in an ignored prose-first table, the artifact is refused and the parser reports that row.

The five legacy row classes — outcome, fr, story, task, and adr — use five cells:

| Row class | Cited id(s) | Counterpart id(s) | Verdict | Notes |

An outcome row’s quote cell must be the staged intake bullet itself — the sanitized projection in .pipeline/intake-outcomes.md — and the land gate compares the two. Only presentation is normalized (a leading list marker, surrounding quotation marks, collapsed whitespace); any other difference is reported as an uncovered outcome naming the quote as the problem, so raw tracker text cannot reach the committed artifact through this cell.

The criterion row class uses six required cells because its subject is the exact criterion text and its coverage claim needs grounded task evidence plus a locality decision. A seventh, optional Correction cell is permitted only on a fail row:

| Row class | Criterion | Cited task id(s) | Verdict | Quote | Disposition | Correction |
| criterion | <exact extracted criterion> | task-<id>[, ...] | fail | <verbatim Done when span> | diff-local | plan |

Criterion text must match one extracted happy- or negative-path story criterion exactly. The verdict is one of covered, gap, or fail; the disposition is diff-local or outside-diff. A correction is either plan or architecture:adr-<stem>#D<n>; the latter must name a decision enumerated from a non-deleted ADR in the current change set. Unknown values, missing required cells, a correction on a non-fail row, an empty criterion, and an empty cited-task list make the row unparseable. The quote may span lines because comparison normalizes whitespace, but it must otherwise be an exact substring of at least one cited task’s Done when: block. A corrected fail row reports a waivable criterion:cannot-deliver-plan:<n> or criterion:cannot-deliver-architecture:<n> gap; an unresolved architecture decision additionally reports criterion:correction-unknown-decision:<n>. The land gate requires a one-to-one criterion set and accepts only covered plus diff-local without a waiver. Already-landed coherence artifacts with no criterion rows remain valid for daemon discovery and BUILD.

Write guards

Two independent mechanisms protect .docs/ during a run.

Phase write-guard. While a BUILD or SHIP step is dispatched, the engine stamps .pipeline/phase-active with allow: <prefix> lines and docs-guard.sh default-denies every other .docs/ write. The allowlist is .docs/release-waivers/ always, plus .docs/plans/ during the remediate step.

remediate holds the plan-write permission because it is the step that reasons about a blocking gate’s dispositions. build deliberately does not: a build agent rewriting its own plan to match what it implemented is the scope violation build_review exists to catch, and the repair routes to the plan step, which an autonomous run may not enter.

When the engine appends remediation tasks to the plan after a remediate round, it commits that amendment itself (chore(plan): record appended remediation tasks) — the appended heading is engine bookkeeping, not builder work, and leaving it uncommitted would fail the build step’s clean-tree completion check. Each appended task includes one valid Done when: block with two to three single-line, nonblank checks: the cited criterion, governing clause, rationale, or title as available, plus a check to re-run the gate. The engine also records every appended task id in .pipeline/engine-state.json (appendedRemediationTaskIds), and the build completion predicate refuses completion while any recorded id’s ### Task <id> heading is missing from the plan: deleting a remediation task never completes it. The guard disarms only when the engine-state file is absent (e.g. a recreated worktree), never on a plan edit.

The seal-rotation evaluator honors the same record: an authored plan whose divergence from either the base tip or the fingerprint-verified content at the seal baseline is exactly an append of the recorded ### Task rem-* blocks (anchor content a byte prefix of head, every suffix heading a recorded task id) is treated as the engine’s own amendment and rotates without an operator reseal — the protected_artifact_rebaseline event reports it under includedEngineAppendedPaths. The sealed baseline is an anchor only when its bytes reproduce the recorded fingerprint. Any other authored divergence (unrecorded ids, extra headings, prose before the first recorded heading, or a non-append edit) still refuses. A recorded remediation heading that cannot be vouched by either anchor reports engine-append-unvouched; other committed feature-authored divergence reports feature-authored:head-differs-from-base and requires ai-conductor reseal.

Protected-artifact seal. .pipeline/protected-artifact-seal.json fingerprints every file under .docs/architecture, .docs/decisions, .docs/plans, .docs/specs, and .docs/stories against a baseline commit:

interface ProtectedArtifactSeal {
  version: 2;
  baselineCommit: string;
  protectedArtifacts: { path: string; fingerprint: string }[];
  rebaselines: {
    fromCommit: string;
    toCommit: string;
    trigger: string;
    paths: string[];
  }[];
}

The seal is written at first BUILD entry. BUILD and SHIP agents cannot replace its baseline, but the engine rebaselines it after a proven history rewrite. A clean engine rebase rotates it after post-rebase evidence translation. Verification also rotates a seal stranded by an earlier rebase when its baseline is no longer an ancestor of HEAD. Both paths require every changed workspace artifact to equal the blob at HEAD. A HEAD blob that still differs from the base-branch tip only blocks rotation when this feature authored that divergence; a path git-provably not touched by this feature since the merge-base is excluded from the rotation instead (excludedBaseAheadPaths), and rotation proceeds for the rest. An indeterminate authorship probe still fails closed and refuses rotation. The engine records each rotation in rebaselines and logs the trigger, old and new commits, and paths.

An accepted-artifact correction belongs to DECIDE, before that first BUILD entry. For every protected artifact except stories, DECIDE writes the additive note beside the original assertion:

> **Amended YYYY-MM-DD by #NNN:** <what the assertion now says, and why>

The original assertion remains present, and no separate amendment artifact is created. Story artifacts under .docs/stories/ are the exception: DECIDE replaces the superseded assertion in place and leaves no amendment record — git history and the spec PR carry the correction’s provenance. Either way, this places the correction in the initial seal baseline. ai-conductor plan-protected-targets <plan-path> prevents a plan from assigning the same mutation to BUILD, and the land gate independently refuses a violating plan.

An operator-approved plan or architecture amendment committed after first BUILD leaves this baseline stale by design. Review the amendment, then reseal the reviewed paths with ai-conductor reseal before the feature is re-queued — it runs only from an interactive operator terminal, refuses the whole reseal if any unlisted protected path has also drifted, and records the old and new fingerprints, trigger, and rationale in both the seal’s rebaselines entry and the audit trail. If a resealed, changed path belongs to the current coverage-binding DECIDE set and its completed coverage evidence is still current, the engine marks that evidence invalidated, makes the gate unsatisfied with a decide-change origin, and re-runs coverage binding before BUILD resumes. Editing the JSON directly is never a valid reseal.

Verification also tolerates these cases without halting:

  • Own-feature amendment — a changed artifact whose filename stem names the current feature (a date prefix on either side is ignored). The engine logs a warning naming each amended path; the mutation is not blocked, but prd_audit’s scope-as-intent judgement grades it OVER_SCOPE unless the change stays within the PRD’s or stories’ stated intent.
  • Base-branch inheritance — a changed or newly appeared artifact whose current workspace content is byte-identical to that path as committed at the base branch tip (origin/<base>, falling back to the local <base>). This is the content the feature’s own rebase brought in, and the base branch already vouches for it.
  • Branch-untouched inheritance — the artifact no longer matches the base-branch tip (the base has moved again since this feature’s last rebase), but this feature never changed the path relative to the base (git diff <base>...HEAD -- <path> is empty) and the workspace copy still exactly equals HEAD’s blob. The feature is simply behind an older revision it already carried forward untouched; this is neither a local amendment nor an uncommitted edit, so it is tolerated the same as base-branch inheritance.

Everything else still halts BUILD/SHIP before dispatch: content that neither the base branch nor a fingerprint-verified sealed baseline vouches for, any addition neither contains, and any deletion either still retains. Tolerance requires the base branch name to be resolvable. When the seal baseline commit is no longer readable, verification omits the sealed-baseline anchor and decides from the base-tip proof alone; it does not fail merely because that older object is gone. An unavailable base ref, missing merge base, or failed inheritance probe still leaves the seal fully protected and reports Protected artifact provenance undeterminable: <path> with the specific cause (Missing base ref, No merge-base exists between HEAD and <base>, or Inheritance probe failed: git diff) and the recovery step (supply the base ref, or rebase onto the base branch to establish shared history). A genuine violation instead reports either Uncommitted protected artifact changed: <path> (the workspace differs from HEAD; restore from HEAD) or Protected artifact changed: <path> with a Feature-authored committed change cause (revert to the committed DECIDE content and route any actual amendment to DECIDE). Do not delete or hand-edit the seal to recover from a halt; follow the stalled-feature runbook.

A seal rejection raised before a re-kick rebase is reported as protected-artifact seal error. Because git has not started a rebase, its recovery procedure is review plus audited resealing—not conflict resolution or git rebase --continue.

Step to artifact map

STEP_ARTIFACT_CONTRACTS (src/conductor/src/engine/artifacts.ts) is the hand-authored source of truth for which step produces which file, and the lifecycle scope each pattern belongs to. STEP_ARTIFACT_GLOBS is a mechanically-derived compatibility projection — pattern strings only, no scope — kept for callers that still need a plain glob list. Source-ordered; all 26 step names appear.

Every pattern declares one lifecycle scope:

  • feature — the output belongs to the active plan/feature and must be associated with it before it counts. Scope is per-pattern, not per-step, so a step can pair a feature-primary report with a broader supplemental pattern (e.g. architecture_review’s ADR glob) without the second pattern being mislabeled feature-scoped.
  • repository — the declared corpus intentionally applies to the whole checkout; no feature filtering is applied.
  • run — stable worktree-local .pipeline evidence, where the step’s existing freshness or custom predicate remains the sole authority.
Step Glob(s) Scope
bootstrap (none) —
memory (none) —
assess .docs/decisions/technical-assessment-*.md repository
explore (none — advisory and ephemeral) —
prd .docs/specs/*.md feature
complexity (none) —
stories .docs/stories/**/*.md feature
conflict_check .docs/conflicts/*.md feature
plan .docs/plans/*.md feature
coherence_check .docs/coherence/*.md feature
architecture_diagram .docs/architecture/*.md repository
architecture_review .docs/decisions/architecture-review-*.md (feature), .docs/decisions/adr-*.md (repository) mixed
worktree (none) —
coverage_binding .pipeline/coverage-binding.json run
acceptance_specs 15 stack-convention test globs — spec/acceptance/**/*, spec/requests/**/*, spec/system/**/*, test/acceptance/**/*, test/**/*, tests/**/*, __tests__/**/*, and *.{test,spec}.{js,ts,jsx,tsx} — plus any acceptance_spec_globs the project declares repository
build .pipeline/task-status.json run
build_review .pipeline/build-review.json run
test_suite .pipeline/test-suite-evidence.json run
manual_test .pipeline/manual-test-results.md run
prd_audit .pipeline/prd-audit.md run
architecture_review_as_built .pipeline/architecture-review-as-built.md run
rebase (none — verdict computed from git state) —
finish (none) —
remediate (none — the engine reads .pipeline/remediation.json directly) —
attribution_verify (none — computed, not a file) —

Totals: 8 steps write into .docs/, 7 write into .pipeline/, acceptance_specs matches project test sources, and 10 produce no file artifact at all.

Feature-scoped resolution

resolveArtifactFiles(dir, step, context) is what generic completion (checkStepCompletion), the interactive artifact-review prompt, and dashboard status (getArtifactStatus, in both the terminal and create renderers) actually call — never findArtifactFiles directly. For each feature-scoped pattern it matches the raw glob, then narrows to the current feature by, in order: files in the worktree’s changed/untracked set, files whose name matches the active plan/feature identity under the pattern’s declared identity strategy (exact plan stem, or a normalized stem with date-prefix and step-name-prefix stripping), and — only when exactly one candidate remains after those checks fail to narrow it — a legacy singleton fallback. Several remaining candidates that cannot be associated with the current feature resolve to an ambiguous diagnostic (done: false with a reason naming the candidate count), never an alphabetical, newest-mtime, or first-match guess. repository- and run-scoped patterns are returned as-is, with run patterns still gated by their own custom completion predicate.

findArtifactFiles remains the low-level, policy-free pattern expander — the raw repository-wide corpus for callers that explicitly want it, and the primitive resolveArtifactFiles builds on internally. See adr-2026-07-28-feature-aware-artifact-resolution for the full design rationale.

The SHIP-tail verdict artifacts (manual_test, prd_audit, architecture_review_as_built, build_review, test_suite) live in gitignored .pipeline/ deliberately. They are regenerated every run; committing them caused date-stamp sprawl, rebase and merge conflicts, and dirty-tree HALTs at the finish-time rebase.

For step order, phase, tier-skip, and enforcement, see steps.

.pipeline/ — run state

One .pipeline/ per worktree, at the worktree root. Gitignored wholesale, so nothing here survives a worktree removal.

Core state

File Shape Writer Lifecycle If lost
conduct-state.json Backward-compatible ConductState formatted as 2-space JSON with a trailing newline; it includes per-step status plus feature_desc, complexity_tier, track, bootstrap_mode, run_started_at, session_started_at, last_step, pr_url, rebase_base_sha, worktree_dir, worktree_branch, feature_status, and artifact_approvals. filesystem-conduct-state-store.ts Created on the first state mutation. Mutations and invariant batches run under a worktree-local lease and persist by atomic replacement; ordinary mutations preserve unowned and omitted fields. Full replacement is reserved for reset/start-over. Missing ⇒ {}; empty or invalid ⇒ hard corrupted error. A legacy brainstorm status is migrated forward onto explore + prd on every load The feature restarts from step zero and every gate re-runs. pr_url, worktree_branch, and session_started_at are gone, so SHIP-tail freshness gates (which compare artifact mtimes to session_started_at) fail open
task-evidence.json { evidenceStamps: Record<string, EvidenceStamp>, noEvidenceAttempts, noEvidenceReasons?, migrationGrandfather, lastResolvedCount? }; each stamp is { sha, form, citedShas?, verdictAnchor?, testEvidence? } with form ∈ commit, trailer, evidence:satisfied-by, semantic-verified task-evidence.ts Read-modify-write per gate evaluation, written atomically via a same-directory temp file plus rename(2). Missing or corrupt ⇒ empty state, logged, never throws lastResolvedCount reads 0, so the progress delta degrades to “no progress” rather than crashing the tick; the no-evidence retry budget resets; completed tasks may be re-attempted
task-status.json { plan_ref?, tasks: [{ id, name?, status?, … }] }. Duplicate rows merge by status rank: completed/skipped 3, in_progress 2, pending 1. A row restored from git evidence also carries commit and restored_from: "task-trailer" task-seed.ts::seedTaskStatus Re-seeded from the plan on every build-gate evaluation and at the build preflight. Written atomically (same-directory temp plus rename(2)) and re-read afterwards — a reconstruction that does not land throws rather than reporting success Self-heals from the plan on the next evaluation, with completions intact: see reconstruction below
engine-state.json { activePlanPath?: string, repairObligations?: { version: 1, records, currentByPlan, admissionsByPlan }, … }. Each repair record binds its active plan, owning task ids, finding/authority/instruction, admission commit/tree/progress baseline, settlement state, and per-task open or resolved state. task-seed.ts, conductor.ts, repair-obligations.ts (serialized atomic updates) Written when the active plan is resolved and when existing-task remediation is admitted, settled, or closed. activePlanPath is recorded only by the interactive plan step, so a daemon-dispatched feature that enters at build has none; repair-scoped reads therefore resolve the plan through the full ladder (recorded path, then the slug-scoped convention) instead of keying on that field. An admitted open repair keeps its owner unresolved until current closure evidence or post-admission matching task evidence is accepted. Resolution falls back to stem match, then to a single plan on disk. With several plans and no match it returns nothing and the build gate fails closed rather than guessing. A malformed or incompatible present repair section also fails closed; it is never treated as legacy absence. So does an unresolvable plan while obligations exist — present obligations are never read as legacy absence.
kickback-ledger.json { version: 1, gates: Record<gate, { count, cumulative, laps?, treeHash, lastReason, priorVerdict, resolvedBefore, mechanicalFaults?, suiteInfrastructureRetries?, effectiveLimit?, effectiveLapCap?, adjustments?, pendingAdjustment?, capEvidence?, resumeAuthorization? }>, pendingRepair? }. pendingRepair ({ receiptId, charges: { prd_audit?, architecture_review_as_built?: { laps, growth } }, taskIds, prdAuditCriteria? }) is an appended remediation repair whose lap and growth are charged only when build dispatches; a malformed record makes the prd_audit, as-built, and growth allowances read as exhausted. build_review’s mechanicalFaults counts its infrastructure-failure allowance (cap MAX_MECHANICAL_FAULTS_BUILD_REVIEW, 3); effectiveLimit/effectiveLapCap, adjustment history, cap evidence, and a one-use resume authorization record a human-approved budget recovery. kickback-ledger.ts Every read-modify-write takes the feature-local kickback-ledger lease, then atomically replaces the file. A gate’s count survives daemon re-dispatch and advances toward its cap; an approved recovery changes only its named gate. Cleared entirely on a fresh feature session. A missing ledger is the empty base case. An unreadable ledger, or an unreadable gate entry, fails closed: the affected recovery or enforcement path refuses rather than inventing fresh allowance; healthy sibling gates remain readable when only one entry is malformed.
build-outcome.json { version: 1, records: BuildOutcomeRecord[] }. Each record carries outcome (moved/no-movement), terminalOutcome (done/failed/no-verdict), the kicking-back gate (or null), the gate verdict at entry, the rung (model, effort) dispatched at, both movement witnesses (treeBefore/treeAfter, headBefore/headAfter), and an optional bounded note (the same last-200-lines tail step_completed carries) plus an inferred or agent-declared category (disputes-gate/belongs-to-decide/silent-no-movement) build-outcome.ts, appended to at every build-step terminal outcome from conductor.ts Read when the conductor handles an active gate’s kickback to build: an identical no-movement cycle can halt instead of paying for a repeat dispatch (sameNoOpCycle) Missing, corrupt, or version-mismatched sidecars fail open to an empty record set (never throw, never block dispatch); a lost sidecar just means one already-observed no-op cycle may be repeated once before the guard has evidence again
build-dispute.json { category: 'disputes-gate' \| 'belongs-to-decide' \| 'silent-no-movement' } optional, hand- or agent-authored during a build step resolveBuildOutcomeCategory, preferred over the note-text inference when present and well-formed Absent, malformed, or shape-invalid content is ignored outright and the category is inferred from the build’s note text instead — this artifact is never required for any behavior in the feature

Reconstruction: what self-heals and what must not

.pipeline/ is gitignored and lives inside the worktree, so removing or recreating a worktree destroys all of it. Run state therefore splits in two, and the engine treats the halves oppositely.

Mechanically reconstructible — rebuilt at the point of use, never halted on. Anything derivable from the plan, from git, or from constants in the source:

Artifact Rebuilt from Where
.pipeline/ itself mkdir -p seedTaskStatus, writePhaseMarker
task-status.json The plan’s ### Task <id> headings, plus Task: <id> commit trailers on the branch for completion task-seed.ts::seedTaskStatus, called from the build completion predicate and the build preflight
session-hooks/*.sh Constants in git-hook-assets.ts worktree-prepare.ts::ensureSessionHooks, called from the build preflight
git-hooks/* The same constants worktree-prepare.ts
engine-state.json Plan-stem match, then a single plan on disk task-seed.ts
task-evidence.json Empty state; counters reset task-evidence.ts

When any plan task has no row in task-status.json — the file is missing, empty, or unparseable, or an interrupted write left it partial — the re-seed restores each missing task that carries a Task: <id> trailer on a commit on the branch as status: "completed", stamped with that commit and restored_from: "task-trailer". Missing tasks with no such trailer are written pending. This grants no new authority — resolveTaskIds already resolves those exact task ids from the same trailers for build-step routing (adr-2026-07-23-trailer-union-build-step-routing), and the per-task Done when: evidence check at task close and prd_audit’s criterion-level grading still re-judge the real work — it only stops a row-only reader from redoing finished, committed work. An existing row is never trailer-backfilled, so a row deliberately reverted to pending stays that way.

Immediately before each BUILD dispatch, the re-seed also reclaims stale in_progress rows left by an interrupted run such as daemon death. A row whose task has no branch Task: trailer resets to pending, so BUILD picks it up again; a row with one keeps its status. Every other re-seed leaves in_progress rows untouched.

Both reconstructions are filesystem-authoritative: after repairing, the engine re-reads the path and halts if the repair itself could not land. A repair outcome is never evidence on its own.

Irreplaceable — absence stays meaningful and is never fabricated. Nothing below is created by any repair path, because inventing one would let unearned work pass a gate or erase an operator’s decision:

HALT, HALT.class, HALT.cleared, QUARANTINE, REKICK, DONE, halt-user-input-required, finish-choice, version-approval, conduct-state.json, gates/*.json, protected-artifact-seal.json, and every verdict/evidence artifact in the two tables below (build-review.json, test-suite-evidence.json, acceptance-specs-red.json, manual-test-results.md, prd-audit.md, architecture-review-as-built.md, …). Losing one of these re-runs its step or restarts its phase; that cost is correct. events.jsonl, otel.jsonl, and the audit trail are append-only history — also never reconstructed, because a fabricated history is worse than none.

Gate verdicts

.pipeline/gates/<step>.json, one per step:

interface GateVerdict {
  satisfied: boolean;
  reason?: string;
  checkedAt: number;                              // epoch ms
  kickback?: { from: StepName; evidence: string };
}

The loop owns objective verdicts — it recomputes them from on-disk evidence after each step rather than trusting an agent’s self-report. The only agent-authored writes are kickback invalidations, and those must carry evidence.

Verdict and evidence artifacts

Agent-authored, engine-validated. Alphabetized.

| File | Shape | Writer | | — | — | — | | acceptance-specs-red.json | { command, targetSpecs[], executed, passed, failed, skipped, errors, failingTests[], ranAt, intentRationale, exception?, summary? }. Validation hard-fails on errors > 0, skipped > 0, executed < 1, or failed < 1 unless a recorded exception ({ kind: 'remediation', reason, attribution }) waives separate RED proof — a RED phase must actually fail, or the waiver must be attributable | acceptance-red-runner.ts | | architecture-review-as-built.md | Markdown with a Verdict: <value> line | as-built review step | | architecture-review-as-built-code-stamp.json | The HEAD sha the review was formed against, plus the engine-stamped runId for its latest dispatch | engine | | assessment/ | Assessment outputs | assess skill | | attribution-memo.json | Fast-lane attribution memo | attribution-lane.ts | | attribution-verdict.json | { schema?, anchor?: { head?, residue?[] }, results? } | attribution-verdict.ts | | audit-trail/ | Per-task review.json, rework-N.json, commit.txt, summary.json, plus events.jsonl and a WRITE-FAILED marker | audit-trail.ts, pipeline skill | | bootstrap-detection.json, bootstrap-inventory.md | Stack detection output | bootstrap skill | | build-review.json | { verdict: 'PASS'\|'FAIL', reasons?, findings?, rubric: { testQuality }, codeStamp? }. A rubric.<item>: true means that item failed. A missing or malformed item fails closed; an unknown item (such as a retired rubric — tautology, scope, rootCause, completeness, wiring) is ignored, so a verdict written before the rubric consolidation still parses. | build_review step | | build-review-regrade.json | Per-feature-session regrade counter; bounds stale-mirage regrade to once per session | build-review-disposition.ts | | build-review-work-order.json | { version: 'v1', domain: 'build_review', feature, effectId, cases[], attemptedCaseIds? }. The effect-bound, feature-local ordered BUILD work from a post-join adjudication; BUILD records attemptedCaseIds before dispatch so restart recovery and repeat detection remain durable. | build-review adjudication coordinator | | build-stall-question.md | Free-form stall question surfaced to the operator | task-progress.ts | | coverage-binding.json | { version: 1, slug, runId, status, entries[], sliceMembership? }, where status is disabled, done, failed, partial, refused, or invalidated; entries bind engine-stamped digests either to a criterion verdict and cited task checks or to a DECIDE amendment’s plan-obligation verdict. When a plan declares a valid ## Slices manifest, sliceMembership records { taskSlices, titles }: each non-remediation task id maps to its positive slice position, and titles preserves the ordered slice titles. partial checkpoints accepted batches, and invalidated retains the prior entries as a re-judgement baseline; neither is completion evidence. | coverage_binding step | | documentation-delivery.json | { version: 1, branch, prUrl, sourceRef } with strict source-ref and PR-URL regexes and a staleness check | documentation-delivery.ts | | fr-coverage.md | Product-track FR-to-spec coverage table | writing-system-tests skill | | intake-outcomes.md | Staged intake outcomes | engineer/outcome-staging.ts | | manual-test-results.md | Per-story PASS/WARN/FAIL rows. The recorder stamps attempts containing exact WARN cells with <!-- manual-test:warning -->; WARN is visible but non-blocking. The gate fails on any FAIL row in the latest attempt, and on an mtime older than session start | manual-test skill | | manual-test-fail-evidence.json | Failure detail for the above | engine | | manual-test-code-stamp.json | The HEAD sha the manual test was formed against when available, plus the engine-stamped runId for its latest dispatch | engine | | per-task-floor.json | Per-task commit-floor telemetry | step-runners.ts | | prd-audit.md | A ## Verdict Table with one graded row per story acceptance criterion: Criterion, Grade (PASS|FIXABLE|PLAN_GAP|OVER_SCOPE), Plan task (required for FIXABLE; any grade may cite one or a comma-separated list, and every cited id must be declared by the active plan — a FIXABLE row must cite exactly one, because its repair is appended under that single parent task), FR, Intent relation (required for OVER_SCOPE: within|outside-harmless|outside-visible), Evidence. The grade is read from the verdict cell, not from anywhere else in the row. Every Criterion key must be an active story criterion id, each on exactly one row. A finding that owns no criterion (typically an unplanned change) is a unique NC.<n> OVER_SCOPE row in ## Findings without an owning criterion, with Finding, Grade, Intent relation, and Evidence. For a visible NC widening, its offer/decision binds immutable original evidence; a later rewording is classified only through a fresh durable relationship, never summary similarity. Invalid or duplicate rows are rejected individually, but any rejected row blocks the audit with its diagnostic. | prd-audit skill | | prd-audit-code-stamp.json | The HEAD sha the audit was formed against, plus the engine-stamped runId for its latest dispatch | engine | | protected-artifact-seal.json | See above | protected-artifact-seal.ts | | rebase-residue.json | [{ sha, citingTaskIds[], reason }] — citations a rebase could not translate | rebase-translate.ts | | rebase-rewrites.json | Pre-to-post rebase sha map, merged transitively; atomic temp plus rename | rebase-translate.ts | | remediation.json | Legacy remediation output is per-gap dispositions and tasks. Post-join build_review adjudication writes the additive strict { mode: 'case-v1', domain: 'build_review', sourceOutcomes, cases } form; the engine validates it before any case or effect state changes. Its source outcomes include refuted; a refute case binds an existing attempted action case, has high confidence, and carries a claim plus assertion verdicts with path/excerpt evidence. | remediate skill | | remediation-cases.json | Version 2 feature-local case state. It retains the version-one build_review cases, effects, and suppressions, and adds a distinct prd_widening collection with engine-stamped case IDs, immutable original source snapshots, current source links, and validated relationships. PRD records carry no autonomous action/effect or operator authority. A resolved build-review refute case retains its refutation record and any residual deferral effect. Reads and writes validate the feature identity and replace atomically under the conductor lease; malformed or unsupported history remains intact and halts with named recovery. | build-review adjudication coordinator | | summary.json | At least { tasks_completed: number }; read tolerantly — missing or corrupt reads as 0 | pipeline skill | | test-failures.md | Failure detail consumed by the remediation flow | remediate flow | | test-suite-environment.key | Environment fingerprint for suite evidence | full-suite-fingerprint.ts | | test-suite-evidence.json | Scalar and scoped verification use version 4. Ordered test_suite.commands aggregate verification uses version 5 and records plannedEntryCount plus ordered per-entry results; FAIL also records failedEntryIndex, so unexecuted entries are distinguishable from successful ones. PASS requires every planned entry to succeed. Both forms include the fingerprint, provenance, timing, diagnostics, and one of nine failure reasons; diagnostics truncate at 16384 bytes. | full-suite-evidence.ts | | version-signal.json | { verdict, level, files, classifiedAt } — the PATCH auto-pass audit | self-host/version-gate.ts | In a post-repair BUILD-verification round, test_suite reuses only matching content fingerprints; the current verifier result, not a file left on disk, decides whether it is satisfied.

All of these are ephemeral. Losing one re-runs its step; none of them is the durable record of anything.

Sentinel markers

Existence is the signal. Alphabetized.

Marker Written by Effect
.memory-count-at-start session-start-context.sh Baseline for the stop-time memory-delta reminder
.task-status.lock pre-dispatch.sh (mkdir lock) Serializes concurrent task-status.json row flips
DONE conductor on convergence Paired with the loop_converged event
HALT halt-marker.ts::writeHaltMarker, best-effort — write failures are swallowed The daemon treats it as a full stop: it never advances, opens a PR, or merges past it. The first non-empty body line is the reason the dashboard shows
HALT.class the same writer, always, plus the daemon’s startup migration for halts predating it needs-human, kickback-cap, mechanical, legacy, protected-artifact, or over-scope. A matching one-use kickback-budget authorization is the only automatic path that clears an eligible needs-human or kickback-cap budget halt. An over-scope halt contains an operator-authored fenced over-scope-decisions block; its pending entries are inert until explicitly changed to accept or refuse with a rationale, and a refusal is retained as an explicit revision target rather than erased. protected-artifact identifies a genuine protected DECIDE-artifact violation; legacy is stamped once by the daemon’s startup migration for a HALT it finds still unclassified; missing or unrecognized content reads as unclassified and never throws. Written atomically (temp file plus rename) after removing any stale sidecar, so a reader never observes a class from a prior HALT paired with a newer body
HALT.cleared the re-kick sweep Records halt lifecycle closure; pairs with the halt_cleared event
accepted-widenings.json conductor Version 2 durable operator authority: immutable source/case/offer references, engine decision ID, accept or refuse, rationale, resolved operator, and engine-ordered revision/supersession. Valid legacy rows are preserved as attributed source snapshots in original order; malformed or unsupported history never reads as an empty decision set.
QUARANTINE setup triage The feature is quarantined from dispatch
REKICK the re-kick sweep Body is literally rekick
conduct-session-id step runners Durable conductor run identity. It survives daemon restart and redispatch; provider attempts use separate fresh IDs and do not rewrite it
current-task ai-conductor task Per-task stamp; the source of the prepare-commit-msg auto-stamp. Stale stamps are cleared during seeding
dispatch-count pre-dispatch.sh One line per dispatch. Crossing the unattributed threshold emits unattributed_dispatch
finish-choice FINISH publication coordinator through ai-conductor finish-record Final publication outcome; subject to the session freshness check. Interactive intent is acquired before this marker exists
halt-user-input-required pipeline skill on a user-requested exit The build predicate returns not-done while it exists
phase-active phase-marker.ts::writePhaseMarker Line-oriented on purpose so bash hooks can read it without a parser: step: <name>, phase: <BUILD\|SHIP>, written: <ISO-8601>, then zero or more allow: <prefix> lines. Removed idempotently on step exit
rate-limit-hit rate-limit-wait.sh Line 1 epoch, line 2 wait seconds parsed from the Claude StopFailure payload; consumed by the legacy bash CLI’s rate-limit retry path
review-required-<step> review skills Existence means “found issues”. Observed for prd_audit, architecture_review, and conflict_check; architecture_review_as_built instead uses its typed verdict
tdd-phase nothing in the engine or any skill Opt-in trigger for both TDD gates. Dormant by default
version-approval operator Records the approved VERSION bump for the self-host approval gate

To clear a halt safely, use the procedure in stalled or stuck feature — deleting HALT by hand leaves HALT.class behind.

Generated assets and logs

Path Contents Notes
session-hooks/ pre-dispatch.sh, docs-guard.sh Written mode 0755 during worktree preparation; retired no-op assets are removed; see settings and hooks
git-hooks/ prepare-commit-msg, commit-msg Wired via the worktree-local core.hooksPath
events.jsonl The run event log Append-only, no rotation — see below
pipeline-events.jsonl Pipeline-owned closeout timing events Separate single-writer ledger — see below
composer-events.jsonl Compose-loop-owned events (land_gate_rejected), written at the target repository root Separate single-writer ledger, same schema as events.jsonl; merged by the run report
audit-trail/events.jsonl A separate ledger with a different shape See below
otel.jsonl OTLP-JSON, one batch per line Default file-transport target. Off unless the otel: config block is present. For daemon runs, each feature writes its own worktree .pipeline/otel.jsonl. Append-only, unbounded
conduct.log Session narrative Written only by the legacy bash CLI; ai-conductor never writes it. Read by rate-limit-wait.sh
progress.log Batch-boundary narrative Appended by the pipeline skill

Known limitation. No engine code reads .pipeline/fr-coverage.md, .pipeline/otel.jsonl, .pipeline/audit-trail/events.jsonl, .pipeline/audit-trail/WRITE-FAILED, .pipeline/bootstrap-detection.json, or .pipeline/bootstrap-inventory.md. They are written every run and consumed only by skill prose, by external OTLP tooling, or by nobody. Do not treat their presence as evidence that anything acted on them. Tracked in #1008.

.daemon/

Daemon-scoped state at the main checkout root. Gitignored. Sixteen paths.

Path Contents Notes
PAUSED { pausedAt, pausedBy? } Existence is authoritative; the body is informational only. Fail-closed — any read error other than “not found” is treated as paused
RESTART-PENDING { requestedAt, requestedBy?, blockingSlug? } Consumed once at the next daemon boot; a re-request refreshes rather than duplicating
RESTART_PENDING { reason, fromIdentity, targetIdentity, at } Stale-engine restart marker; this is a separate restart pipeline from RESTART-PENDING
RESTART_PENDING.suppression Suppression record for the stale-engine restart marker above Derived from RESTART_PENDING with the .suppression suffix
attribution-accuracy.jsonl Append-only accuracy ledger attribution_divergence events are observational only — they never revoke a stamp or write a halt marker
daemon.log The active daemon log Rotated at open time when it exceeds 1 MB
daemon.log.1 The rotated log Overwritten by each rotation
daemon.pid pid, uuid, engine dir O_EXCL pidfile. Fleet-wide GC cross-checks every pidfile; any read error other than “not found” aborts GC with zero deletions
gated.json Owner-gate snapshot —
last-base-sha Fast-forward tracking —
mergeable-watch.jsonl Append-only mergeable sweep ledger —
migrations/halt-classification-v1 complete\n once written One-time watermark. The daemon stamps any pre-existing HALT still missing .pipeline/HALT.class as legacy before touching worktrees, then writes this file so the sweep never repeats. A lock loser never runs it
otel-spool/ traces/ and metrics/ OTLP/HTTP protobuf batches (<epoch-ms>-<pid>-<hex>-<items>.pb), plus lease.json ({ pid, uuid, heartbeatAt }) Durable OTel spool. Directories are mode 0700. Each batch is fsynced, then renamed, so a final .pb is always complete; .tmp files are ignored. The lease heartbeats every 10 s; a successor reclaims it after 60 s without a heartbeat or when its pid is dead. Bounded by otel.spool.max_bytes
parked/<slug> Per-slug operator park Resolved against the main repo root via git rev-parse --git-common-dir, so a worktree and its main checkout share one park namespace
processed/<slug> {"status":"shipped","prUrl":…} Legacy plain-text shipped still parses
warned/<slug> Per-slug warn-once record —

Daemon procedures live in running the daemon; recovery lives in daemon recovery.

Git machinery

Worktree and branch names

Actor Worktree path Branch
Daemon build <projectRoot>/.worktrees/<slug> feat/daemon-<slug>
Composer loop (spec authoring) <canonicalPath>/.worktrees/engineer-<slug> spec/<slug>, then -2, -3… on collision
Interactive worktree step <projectRoot>/.worktrees/<slug> feature/<slug>, then -2… on collision
Autoresolve <repoCwd>/.worktrees/resolve-<slug> (checks out the conflicting ref)
Setup triage quarantine — wip/setup-quarantine-<slug>
Shipment reconciliation — shipment-repair/<prNumber>/<slug>

Five branch-name templates in total. The daemon slug is always the plan-file stem, enumerated from the base branch tree, never from the working tree. The engineer and interactive slugs come from slugifying the idea or feature description: lowercase, spaces to -, strip anything outside [a-z0-9-], collapse runs of -, trim trailing -, truncate to 50 characters.

ensureWorktree has three outcomes — reused (already registered, no git mutation), attached (git worktree add <path> <branch>), and created (git worktree add -b <branch> <path> <base>). The base is resolved lazily, only in the create case.

On a halt or error the daemon deliberately leaves the worktree in place for the operator. Only the legacy interactive cleanup path also deletes the branch.

Commit trailers

Daemon worktrees may contain .pipeline/co-author, a hook input written during preparation. It is not telemetry: it supplies the configured bot’s Co-authored-by: trailer to build commits and is removed when no bot identity is available. The root checkout and operator-created worktrees never receive this hook input.

The engine’s Task/Evidence trailer parser recognizes exactly two keys: Task: and Evidence:. There is no Owner: commit trailer (Owner: is a line inside .docs/intake/<slug>.md) or Shipped-Record: trailer. Co-authored-by: is hook input only; it is not interpreted as task or evidence telemetry.

Task: <id> is auto-stamped by prepare-commit-msg from .pipeline/current-task, but only when no explicit trailer is already present. A leading T before a digit is folded, so Task: T3 and Task: 3 are the same id.

Task: trailers are partly telemetry and partly load-bearing. The distinction matters:

Consumer Gate or telemetry?
commit-msg presence check Telemetry. A commit with no Task: trailer is never rejected
commit-msg format check Gate. A trailer that is supplied and uses the task-N form, or names an id absent from task-status.json, blocks the commit with exit 1
Per-task commit floor Telemetry. Purely additive: it never feeds the build grader, never changes success, never triggers a kickback
derive-feedback commit-evidence check Telemetry. Advisory only; never writes task-status.json or the evidence sidecar
Build-completion predicate Gate. Task ids resolved from Task: trailers are unioned with task-status.json rows; any plan task id in neither set returns not-done
Build stall and halt breaker Gate. The resolved-task count drives the no_task_progress stall verdict

A task evidenced only by a Task: trailer, whose task-status.json row was never flipped, therefore satisfies the build gate. That union is deliberate — it fixed a false halt at 100% real completion. Final completion authority rests with each task’s Done when: evidence, shown true before the task counts as complete, and — once behavior ships — prd_audit’s criterion-level grading against the stories’ acceptance criteria, rather than trusting any self-report.

Evidence: satisfied-by <sha> / Evidence: skipped <reason> is telemetry only. The values are extracted by commit-msg and never acted on. This is distinct from the EvidenceStamp records in .pipeline/task-evidence.json, whose form field can be trailer or evidence:satisfied-by.

commit-msg skips all trailer machinery for merge commits, --amend, rebase replay, and any commit made with CONDUCT_ENGINE_COMMIT=1.

Shipped records

A shipped record is the durable, committed fact that a feature shipped. It lives at .docs/shipped/<plan-stem>.md, committed on the implementation branch, so the merge that lands the work also lands the record.

---
slug: <slug>
spec_hash: <sha256-hex>
pr: <pr url | local>
shipped: <YYYY-MM-DD>
engine_version: <engine-version-id | dev>
---

engine_version is the engine build that shipped the feature — the same id ai-conductor daemon status prints as version:<id>, resolved from the running engine’s own module path (dev for an unpublished source checkout). The line is emitted only when a value is supplied, so records written before this field existed, backfill proposals, and repair writes stay byte-identical to the four-field form. ai-conductor kpi reports an unstamped record as engine=unknown, keeping unattributed ships visible.

spec_hash is SHA-256 over the trimmed plan bytes, a 0x00 separator, and the trimmed stories bytes. Only trailing newline runs are trimmed; interior bytes are never modified and CRLF is deliberately not normalized. Changing that computation is a breaking change to persisted identity.

renderShippedRecordWithCost appends a ## Cost block after the closing fence (input, output, cache_read, cache_creation, cost_usd, dispatches, retries, halts, unmetered, and a providers: sub-block when non-empty). Appending is safe because the parser stops at the closing ---. Its dispatch ledger treats invoked provider_attempt events as authoritative, deduplicates their matching successful step_completed events, and retains an unmatched completion as a legacy fallback only when it carries provider evidence (token usage, a provider, or a model). Provider-free step completions are not dispatches. The OTel dispatch counters use this same projection. After each terminal step, the engine emits a non-persisted feature_cost_snapshot projection carrying cumulative whole-feature, per-step cost, and per-step token dimensions. feature_usage_total at closeout carries the resulting whole-feature values to the same OTel conductor.feature.cost gauge, so the last successful snapshot, the closeout gauge, and a shipped record produced from the same event ledger have the same cost_usd value.

A separate ## Time block follows, computed and rendered independently of Cost so a timing failure never blocks either section. state is one of measured, partial, or unavailable. measured carries active_ms (the union of all active step/group execution intervals), provider_active_ms (the portion of active time actually spent inside a provider process), and no_provider_active_ms (active_ms - provider_active_ms, engine/code time). partial carries active_ms only when active time itself was trustworthy but provider or completeness evidence was not. When the rollup identifies the downgrade route, partial also carries reason: empty-active-union, active-evidence-incomplete, open-executions:<ids>, provider-outside-active-union, or provider-evidence-incomplete; records shipped before the reason field existed simply omit that line. unavailable carries no fields. Timing is derived from .pipeline/events.jsonl at ship time and is never a fabricated zero — missing or incomplete evidence downgrades the state instead. Records written before this section existed simply have no ## Time block, and ai-conductor kpi reports those as time=unavailable.

ai-conductor shipped-record --slug <plan-stem> --pr <pr-url-or-local> writes it; both flags are required and re-running with identical content is a no-op. Its exit code cannot be used to detect success — it exits 0 even when it wrote nothing. Recording a ship and verifying it landed is in shipped-record reconciliation.

The daemon never writes shipped records. A daemon-side write would land on the main checkout’s base branch, never be pushed, and wedge the fast-forward advance.

Backlog dedup reads records from the base branch tree, not the working tree, so an uncommitted record is invisible by construction. Two passes run: stem match, then content-hash match against spec_hash for a renamed spec. A malformed record still dedups by stem, just not by hash.

Reconciliation for a merged PR whose record is missing or wrong is in shipped-record reconciliation.

Observability outputs

.pipeline/events.jsonl

One JSON object per line: a ConductorEvent spread plus a writer-stamped ISO-8601 ts. Append-only — no rotation, no truncation, no size cap. Path is <pipelineDir>/events.jsonl for an interactive run and <worktreePath>/.pipeline/events.jsonl per feature under the daemon. Gitignored, never committed.

ConductorEvent is a discriminated union. EventPersister writes the event types marked persist: true in event-sinks.ts; common persisted types include:

land_gate_rejected, contained_live_checkout_drift, self_host_containment_verdict, self_host_boundary_fingerprint, containment_check_unresolved, operator_rewind, setup_repair, project_setup, build_review_rubric_started, build_review_rubric_prompt, build_review_rubric_result, build_review_rubric_skipped, build_review_cache_hit, build_review_cache_discarded, build_review_rubric_infrastructure_failure, build_review_outer_verdict, build_review_stale_aggregate, build_review_disposition_version_invalidated, build_review_mechanical_allowance_exhausted, remediation_adjudication_started, remediation_adjudication_completed, remediation_adjudication_failed, remediation_case_reconciled, remediation_case_refuted, remediation_effect_reserved, remediation_effect_applied, remediation_effect_failed, remediation_semantic_repeat_halt, step_started, deprecated_step, step_completed, step_failed, step_refused, provider_discovery, provider_attempt, provider_stream_progress, scratch_cleanup_reclaimed, scratch_cleanup_retained, scratch_cleanup_failed, feature_usage_total, provider_fallback, session_policy, step_retry, checkpoint_reached, recovery_needed, gate_blocked, tier_skip, config_skip, navigation_back, rate_limit, provider_suppressed, session_reset, credentials_park, operator_park_boundary, credentials_park_progress, finish_publication_transition, finish_publication_blocked, finish_publication_disposition, feature_complete, dashboard_refresh, protected_artifact_rebaseline, protected_artifact_rebaseline_refused, auto_heal, remediation_sealed_artifact_redirect, remediation_disposition_rejected, verdict_freshness, build_review_repair_context, mode_skip, build_stall, build_progress, build_no_progress, build_active_stall, renderer_error, when_skip, parallel_started, parallel_completed, parallel_failure, gate_verdict, test_suite_verification, build_member_evidence_reused, build_member_evidence_recomputed, kickback, loop_halt, over_scope_decision, halt_marker_write_failed, halt_record_written, halt_record_write_failed, halt_record_push_failed, shipment_evidence_refused, step_status_write_refused, rebase_changed, rebase_gate_preserved, rebase_gate_invalidated, rebase_merge_audit, rebase_conflict_halt, unattributed_progress, attribution_divergence, acceptance_red, rebase_citation_residue, and rebase_supersession_verdict.

rebase_merge_audit records a flattened replay of a merge-bearing feature branch: the shas of flattened merges, the shas of dropped ancestry-only merges, and the count of side-lineage commits not replayed individually. When a flattened merge blocks the replay, rebase_conflict_halt carries an optional mergeAudit field naming that merge, its parents, its flattened sha, and the conflicting paths. See merge-bearing feature branches.

rebase_supersession_verdict records a successful mergeable-sweep test-only judgement: its choice, rationale, and declared superseded replay SHAs. rebase_citation_residue records the corresponding preservation-guard excusals. Both persist in the affected feature worktree’s event ledger; neither is emitted by a finish-time or re-kick rebase, where supersession judgement is unavailable.

provider_discovery records the installed built-in provider ids and every missing built-in id with one of four reasons: not-found, not-executable, version-failed, or timeout. It is emitted once when a provider-dispatching command boots; non-dispatching commands do not run discovery.

step_retry and step_failed carry an optional providerExit object when the failed provider attempt exited without a classifiable result (not a missing binary, auth, rate-limit, model-unavailable, or unresolved-command failure). This includes parallel group members. Its fields, all optional, are processErrorCode (EACCES, EAGAIN, ENOENT, EPERM, or UNKNOWN), exitCode (non-negative integer; omitted for a signal termination), signal (SIGABRT, SIGALRM, SIGHUP, SIGINT, SIGKILL, SIGPIPE, SIGQUIT, SIGTERM, or UNKNOWN), stdoutBytes, and stderrBytes. No raw output is retained.

rate_limit records the bounded wait and, for usage exhaustion, the provider and retry deadline. The daemon also writes provider_suppressed to its own event ledger. On restart it restores only a still-valid bounded suppression, so later feature dispatches do not re-invoke the exhausted provider.

contained_live_checkout_drift and self_host_containment_verdict are the containment boundary’s closure events (live-containment.ts): the drift event names a concurrent operator’s live-checkout change once a dispatch is proven contained, and the verdict event records whether that proof succeeded for each completed self-host dispatch. Both render to the terminal and daemon log and persist to this file; see live_containment and the live-boundary runbook.

self_host_boundary_fingerprint records the live-boundary fingerprint cost before each self-host provider candidate starts: one elapsedMs and fileCount measurement for live checkout and one for provider state. It renders those measurements to the terminal and daemon log and persists to this file; it does not change the boundary’s exclusions or enforcement behavior.

land_gate_rejected records a failed engineer land attempt with its closed gate identifier, bounded reason, project, worktree path, and optional source reference. The command writes it to the target repository’s .pipeline/composer-events.jsonl (a compose-loop-owned sibling ledger, per adr-2026-08-08 D2), rather than the disposable per-idea worktree ledger, so rejection history remains available after that worktree is removed. The run report merges this ledger with events.jsonl by timestamp and renders per-gate counts and latest reasons.

The remediation adjudication events are the durable lifecycle trace for post-join build_review handling. They identify the lap, case, and effect where applicable; they persist only to .pipeline/events.jsonl, not the daemon log, audit trail, or telemetry export.

step_status_write_refused records a non-fatal state invariant: a step already marked skipped cannot be restaged as stale. It carries the status field, expected skipped, requested stale, and the write intent. The conductor keeps the on-disk and in-memory value as skipped and continues the run; operators can use this event to identify a caller that bypassed the normal skip-preserving restage filter.

gate_verdict records step, satisfied, and an optional reason. The serial path emits it after its gate check; a validation-group join emits it for each member that returned a passing dispatch outcome and received a computed verdict, including an accepted-risk-adjusted prd_audit result. The ledger is historical observability, not the authority to resume: the engine still recomputes the on-disk gate evidence before it admits a step.

build_review_disposition_accepted and build_review_disposition_refused are declared persist: false deliberately: they are written by the external build-review CLI to its own pipeline-owned ledger and tailed onto the live bus, so re-persisting them here would duplicate the same occurrence.

The BUILD-member settle events carry only a member, decision, and closed basis classification: build_member_evidence_reused is reuse with fingerprint-match; build_member_evidence_recomputed is recompute with recorded-head-versus-current-head, fingerprint-mismatch, or fresh-evidence-required. They make reuse observable without becoming a second validity authority; the BUILD group join still decides round satisfaction.

test_suite_verification records the resolved verification mode and, when applicable, the closed drift-budget verdict. A scoped evaluation that has no test selectors records the explicit scoped-empty-selection-aggregate basis, proving the aggregate verifier ran rather than a silent empty scoped command. Older records without these additive fields remain parseable.

A build step’s step_completed event carries two build-only witnesses, treeBefore/treeAfter (absent on every other step and on events from an older engine). Both the daemon-log renderer and the interactive renderer use them to annotate the line with the tree movement observed for that step — (tree abc1234..def5678) when it moved, (tree abc1234 unchanged) when it did not, or (tree unknown) when the hash could not be determined. This is a rendering-only annotation of the same witness pair persisted in .pipeline/build-outcome.json; the two fields do not report the separate HEAD-commit witness (adr-2026-07-23-commit-movement-liveness-floor), which can legitimately disagree on the same turn (an empty commit moves HEAD without moving the tree).

session_policy records when the fail-closed supportsSessionResume capability seam suppresses a would-be session resume. All built-in providers declare the capability false, so this is diagnostic evidence of a prevented resume rather than a path to a later resumed invocation. It is emitted by the same session-capability contract described in Per-step session capability contract.

Halt occurrences are consumed by cost-rollup.halts, the shipped record’s ## Cost block, ai-conductor kpi, and the engineer-loop signal assembler. ai-conductor inline --report renders kickback tables but not halt tables.

Known limitation. The other 26 event types — including gate_verdict, loop_converged, auto_park, zero_work_product, unattributed_dispatch, halt_cleared, ci_failed, and every remaining rebase_* variant not listed above — are emitted for real but never persisted, because the emitter dispatches only to handlers registered for that exact type. loop_halt, halt_marker_write_failed, and rebase_conflict_halt are persisted, so cost-rollup.halts, shipped records’ ## Cost blocks, ai-conductor kpi, and the engineer-loop signal assembler can consume real halt occurrences. .pipeline/HALT remains the durable park signal and the daemon log remains a useful immediate diagnostic. kickback is likewise persisted, and --report renders kickback tables but not halt tables.

build_progress events carry an additional tickReason (task-delta | head-moved | heartbeat) and an explicit headMoved boolean, letting a reader distinguish “HEAD did not move” from the older heartbeat ticks that hard-coded an absent commitCount.

build_progress and build_no_progress events carry an activity classification: quiet, active-committing, or active-not-committing. build_active_stall records an active build that reached build_progress.active_stall_minutes without task or HEAD movement: minutes, resolved, total, optional lastCommitAt/lastActivityAt, and the configured action (warn | end_attempt). A build_stall event with reason active_stall records an attempt the end_attempt policy ended. See build_progress.

The pipeline_closeout event type is declared on ConductorEvent but is never written here — EVENT_SINKS.pipeline_closeout sets persist: false, so it stays a single-writer event confined to .pipeline/pipeline-events.jsonl (below). It is rendered (render: true) when a running build step’s CloseoutEventTail re-emits it onto the live bus, so it still reaches the daemon log, the terminal UI, and the OTel visualizer without a second writer to this file.

pipeline_tail_diagnostic records a recoverable tailing problem in events.jsonl. Its reason is malformed-line when a completed source-ledger line cannot be parsed, or poll-failed when a background read fails; it includes the relative source-ledger path and, for a malformed line, its byte byteOffset. The event is rendered and persisted, but is not audited or sent to OTel. It never includes the corrupt line’s contents or an exception message.

.pipeline/pipeline-events.jsonl

The pipeline’s own closeout timing ledger, written by ai-conductor closeout-event — see ai-conductor closeout-event. One JSON pipeline_closeout ConductorEvent per line: obligation (one of evaluator, simplify, architecture-diagram, memory, summary), startedAt/endedAt epoch milliseconds, and a pipeline-stamped ts. Append-only, gitignored, never committed.

This is a deliberate second single-writer ledger, not a duplicate of events.jsonl: the pipeline process that runs closeout obligations may have no conductor or daemon attached (an inline run), so it cannot depend on EventPersister to observe its own completions. When a build step is running under the engine, CloseoutEventTail tails this file incrementally (skipping partial trailing lines) and re-emits each valid complete record onto the live bus exactly once. A malformed complete line is consumed once so later valid records remain observable; the tail emits one pipeline_tail_diagnostic instead of replaying or exposing the line. An inline run produces the same records with no tail and no re-emission. computeBuildTailRollup (see ai-conductor build-tail) merges this ledger with events.jsonl by ts to decompose a build window into task execution, remediation, and closeout segments.

.pipeline/audit-trail/events.jsonl

A separate ledger with a different shape. Do not confuse it with the run event log.

type AuditRecord = {
  origin: StepName | 'operator'; phase?: Phase; event: string; reason?: string;
  cause?: string; attempt?: number; at: number; kickback_outcome?: string;
  paths?: { path: string; priorFingerprint: string; newFingerprint: string }[];
  condition?: string; path?: string; fromCommit?: string; toCommit?: string;
};

at is epoch milliseconds, not the ISO ts used by events.jsonl, and event is a derived string, not a raw event type. phase is omitted for an operator-origin record — an interactive reseal runs outside any step’s phase. It subscribes to fifteen source events (gate_verdict, step_retry, kickback, loop_halt, step_completed, halt_cleared, protected_artifact_reseal, protected_artifact_reseal_refused, halt_marker_write_failed, halt_record_written, halt_record_write_failed, halt_record_push_failed, shipment_evidence_refused, remediation_sealed_artifact_redirect, verdict_freshness) and emits fourteen strings (gate_pass, gate_fail, retry, kickback, intervention, halt_cleared, reseal, reseal_refused, halt_marker_write_failed, halt_record_written, halt_record_write_failed, halt_record_push_failed, shipment_evidence_refused, verdict_freshness). remediation_sealed_artifact_redirect is subscribed but intentionally emits no audit record. A write failure drops a WRITE-FAILED marker beside it and, for ai-conductor reseal specifically, fails the reseal itself — its writer is constructed fail-closed, unlike every step-attributed writer, because a reseal whose audit record was lost must not be treated as complete. No TypeScript reader exists.

Daemon logs

Active log at <repo>/.daemon/daemon.log, rotated to daemon.log.1. Each line is <ISO-8601 timestamp> <line>; timestamps appear only in the file, not on the live tmux console.

Rotation has a 1 MB cap applied only at open time, so a long-running daemon never rotates mid-run.

The log carries every daemon log() line, rendered inner-loop events, console.warn and console.error tee’d with [warn]/[error] prefixes and ANSI stripped, and the startup dashboard snapshot — which deliberately omits the PROCESSED group the console version shows with --completed.

Line shapes

Every daemon line carries the [daemon] prefix. A line a feature owns carries its slug tag immediately after, with no space between the two:

Owner Shape
The daemon itself — discovery, sweeps, lock, restart [daemon] <message>
One feature run — lifecycle records, rendered loop events, provider warnings, subprocess diagnostics [daemon][<slug>] <message>

The slug is bounded to 24 display characters: a longer one is cut to 23 and closed with …. A multi-line message is split first, so every physical line gets its own prefix, tag, and timestamp rather than only the first.

Feature-owned tagging is attribution, not routing — both shapes land in the same file and on the same console. Untagged lines are the ones emitted directly on the daemon-wide bus; an event forwarded from a feature’s own bus renders once, tagged.

Lifecycle transitions are deduplicated per slug across both sinks: a repeated ▶ start or an ■ done repeating the recorded outcome is dropped, and ↻ resume always prints with (was: <status>) appended. A line counts as a transition only when the glyph opens the message, behind at most a bracketed tag and ANSI codes — so a line that merely quotes another feature’s lifecycle text cannot suppress that feature’s real transition.

Read it with ai-conductor daemon logs; flags are in cli.

Known limitation. daemon.log.1 is produced by rotation but no CLI path ever opens it — both the tail and follow primitives only open the active log. Rotated history is reachable only by reading the file directly. Tracked in #1008.

ai-conductor kpi

Input: <cwd>/.docs/shipped/*.md and nothing else. It re-parses the committed ## Cost and ## Time markdown blocks with regexes; it does not read events.jsonl, .pipeline/, or otel.jsonl.

Parsed fields (KpiCostFields): input, output, cacheRead, cacheCreation, costUsd, dispatches, retries, halts, unmeteredCount, unmeteredDurationMs, and costUnmetered (from the top-level cost_unmetered field). Each providers: entry is also parsed with its input, output, cache, cost, dispatch, and cost_unmetered fields.

Parsed timing fields (KpiTimeFields, mirrors TimingRollup): state (measured, partial, or unavailable), plus activeMs, providerActiveMs, and noProviderActiveMs when present. Parsing is independent of Cost. A partial block also parses its optional reason downgrade route; records shipped before the reason field existed simply omit it. A missing, malformed, or absent ## Time block never affects Cost output and is reported as state: unavailable.

Output: a plain-text report on stdout. No file, no JSON. Per feature it prints input, output, tokens (their sum), cache fields, dispatch fields, duration_ms, and cost_usd, suffixed [PARTIAL — unmetered dispatches present] when any dispatch went unmetered or [COST-PARTIAL — cost-unmetered dispatches present] when tokens are metered but cost is unknown. Each provider prints its input, output, tokens, cost, cost_unmetered, and dispatch count; a provider with cost-unmetered dispatches prints cost_usd=unavailable. A record with no parsable ## Cost block prints no Cost data available (skipped). The aggregate line prints the counted feature count, total tokens with an input/output breakdown, and total cost_usd to four decimal places; features with any unmetered dispatch are excluded from the aggregate, while cost-unmetered features still contribute tokens but not cost. If no feature has metered cost, the aggregate cost is unavailable. An empty or missing directory prints No shipped features yet — .docs/shipped/ is empty or does not exist.

Each feature row also appends a time= suffix: time=measured active_ms=<n> provider_active_ms=<n> no_provider_active_ms=<n> when measured, time=partial (with active_ms=<n> when active time alone was trustworthy and reason=<route> when the downgrade route is known), or time=unavailable. The aggregate line adds timing measured=<n> partial=<n> unavailable=<n> counts, plus avg_active_ms, avg_provider_active_ms, and avg_no_provider_active_ms averaged only over measured features — partial and unavailable rows never contribute to or fabricate an average.

The command takes zero flags — anything after kpi is ignored — and always exits 0.


This site uses Just the Docs, a documentation theme for Jekyll.