State Machine

September 11, 2026 · View on GitHub

This chapter is the canonical reference for AI-DLC's state machines, the audit-event taxonomy, and the rule that connects them — every state transition has exactly one tool-owned emitter. Keeping this chapter's tables in sync with the code is enforced by the drift test at tests/integration/t48-audit-event-emitters.test.ts. If the doc and the code disagree, t48 fails.

Three nested state machines drive AI-DLC: workflow, phase, and stage. A fourth, independent stream records session events emitted by Claude Code hooks. These four streams share the intent's audit trail (the audit/ shard dir under its record dir, <record>/ = aidlc/spaces/<active-space>/intents/<YYMMDD>-<label>/) but are owned by different code paths, so it's easiest to read them as separate concerns and remember that their timelines interleave.

North-star invariant: TypeScript owns deterministic bookkeeping; the LLM owns judgment. Every audit emission originates in a tool or hook, keeping LLM prose out of the emit path. If you're reading an MD file and see aidlc-audit.ts append <EVENT> as a prose instruction, that is a bug.

Authority binding: a query never writes, and a guard never deletes evidence. Authority binds to content and attempt, never to issuance identity or event order. The "Authority invariants" section states both rules and names what they cover.

Audit-first atomicity: tools emit their audit entries before mutating state. If audit emission fails, the tool throws before touching state — so audit.md and the state file never disagree. The "Audit-first atomicity" section near the end of this chapter spells out the failure modes, plus the two exceptions: audit-of-intent (WORKTREE_*, AUDIT_*, MERGE_DISPATCH_INVOKED) and the audit-last DocumentKB catalog events, whose artifact is derived and rebuildable.


Why three state machines

A workflow completes by passing through phases; a phase completes by passing through its in-scope stages; a stage completes when its approval gate closes. Each layer owns a distinct decision:

  • Workflow — is the overall job running, or done?
  • Phase — is this lifecycle phase in progress, verified, or skipped because the scope excluded it?
  • Stage — is the stage being worked on, waiting on the user, being revised after rejection, or complete?

Flattening them into one state field conflates those decisions. Separating them means /aidlc --status can answer "what's blocking this workflow?" in one read: workflow Running, phase Active, stage [?] → "awaiting your approval on <stage>".


Workflow machine

stateDiagram-v2
    [*] --> Running : WORKFLOW_STARTED
    Running --> Completed : WORKFLOW_COMPLETED
    Running --> Archived : WORKFLOW_ARCHIVED
    Archived --> Running : WORKFLOW_UNARCHIVED
    Completed --> [*]

Status values: Running, Completed, Archived.

A workflow starts when the first intent is created (aidlc-utility intent-create, auto-invoked on the first /aidlc or via /aidlc-init) and ends when the last in-scope stage's approval gate closes. There is no Paused status and no Waiting for Approval status — approval is a stage-level concern, pause has no UX.

A workflow's Running state persists across Claude Code sessions. You start a workflow on Monday, stop the session, resume on Tuesday — the workflow is still Running; the session ended and a new one started.

TransitionTriggerEmitter
[*] -> Runningaidlc-utility intent-createtools/aidlc-utility.ts
Running -> CompletedFinal stage outcome reported through aidlc-orchestrate.ts reporttools/aidlc-state.ts (internal emitter)
Running -> Archivedaidlc-utility intent archive <name> (human decision; refused for completed intents, live Bolt worktrees, and claimed team Units)tools/aidlc-utility.ts
Archived -> Runningaidlc-utility intent unarchive <name>tools/aidlc-utility.ts

Phase machine

stateDiagram-v2
    [*] --> Pending
    Pending --> Active : PHASE_STARTED
    Pending --> Skipped : PHASE_SKIPPED
    Active --> Verified : PHASE_COMPLETED + PHASE_VERIFIED
    Verified --> [*]
    Skipped --> [*]
    note right of Verified
        At a phase boundary, advance
        emits PHASE_COMPLETED +
        PHASE_VERIFIED + PHASE_STARTED
        (next phase) in one transaction.
    end note

Status values: Pending, Active, Verified, Skipped.

Phase state is tracked in the ## Phase Progress section of aidlc-state.md. Intent creation seeds the section: Initialization lands Verified (creation completes every init stage before handing off), the first post-init stage's phase lands Active, and each later phase lands Skipped when the scope leaves it without EXECUTE stages (one PHASE_SKIPPED audit row each) or Pending otherwise. Phase completion fires both PHASE_COMPLETED and PHASE_VERIFIED at the phase boundary, then PHASE_STARTED for the next one, and the rows flip in the same state write. The section is display-only: routing reads Lifecycle Phase and the Stage Progress checkboxes, and /aidlc --status recomputes its phase block live.

TransitionTriggerEmitter
seed (Verified/Active/Pending/Skipped)aidlc-utility intent-createtools/aidlc-utility.ts
Active -> VerifiedStage completion/skip reported through aidlc-orchestrate.ts at a phase boundary; forward aidlc-jump executetools/aidlc-state.ts (internal emitter), tools/aidlc-jump.ts
Pending -> Active (boundary)Engine routes after a reported outcome, or aidlc-jump executetools/aidlc-state.ts (internal emitter), tools/aidlc-jump.ts
Pending -> Skipped (jumped over)forward aidlc-jump execute past a whole phasetools/aidlc-jump.ts
Verified/Active -> Pending resetbackward aidlc-jump execute (reset phases with EXECUTE stages)tools/aidlc-jump.ts
Pending <-> Skipped re-derivationaidlc-utility scope-change / recompose (not-yet-reached rows only)tools/aidlc-utility.ts

At the init→post-init hand-off, aidlc-utility intent-create itself emits PHASE_COMPLETED + PHASE_VERIFIED + PHASE_STARTED + STAGE_STARTED after the final init stage so the audit trail captures the transition instead of going silent between creation and the first advance.


Stage machine

stateDiagram-v2
    state "[ ] Pending" as Pending
    state "[-] Active" as Active
    state "[?] AwaitingApproval" as Awaiting
    state "[R] Revising" as Revising
    state "[x] Completed" as Completed
    state "[S] Skipped" as Skipped

    [*] --> Pending
    Pending --> Active : STAGE_STARTED
    Active --> Awaiting : STAGE_AWAITING_APPROVAL
    Awaiting --> Completed : GATE_APPROVED + STAGE_COMPLETED
    Awaiting --> Revising : GATE_REJECTED + STAGE_REVISING
    Revising --> Awaiting : STAGE_AWAITING_APPROVAL
    Pending --> Skipped : STAGE_SKIPPED
    Active --> Skipped : STAGE_SKIPPED
    Revising --> Skipped : STAGE_SKIPPED
    Completed --> [*]
    Skipped --> [*]

Checkbox legend (in aidlc-state.md):

CheckboxStateMeaning
[ ]PendingNot started
[-]ActiveIn progress
[?]AwaitingApprovalStage work done, gate open — user is the blocker
[R]RevisingUser rejected the gate — stage is being revised before re-entry
[x]CompletedApproved and done
[S]SkippedExcluded by scope, skipped via jump, or cut mid-flight

[?] and [R] disambiguate two situations that would otherwise both look like [-]. On resume, [R] tells the conductor to present the prior artifact and feedback before re-entering the gate, instead of re-executing the stage from scratch.

TransitionTriggerEmitter
Pending → ActiveEngine routes after the previous reported outcometools/aidlc-state.ts (internal emitter)
Active → AwaitingApprovalaidlc-orchestrate.ts report --stage <slug> --result awaiting-approval; reviewer-bearing stages require a fresh terminal receipt before gate openingtools/aidlc-state.ts (internal emitter)
AwaitingApproval → Completedaidlc-orchestrate.ts report --stage <slug> --result approved --user-input "<exact choice>"tools/aidlc-state.ts (internal emitter)
AwaitingApproval → Revisingaidlc-orchestrate.ts report --stage <slug> --result rejected --user-input <text>tools/aidlc-state.ts (internal emitter)
Active → RevisingThe same rejected report when gate-open recovery is neededtools/aidlc-state.ts (internal emitter)
Revising → AwaitingApprovalaidlc-orchestrate.ts report --stage <slug> --result revised; reviewer-bearing stages require a fresh post-rejection terminal receipt before gate re-entrytools/aidlc-state.ts (internal emitter)
{Active,Revising} → Skippedaidlc-orchestrate.ts report --stage <slug> --result skipped --reason <text>tools/aidlc-state.ts (internal routed-skip emitter)
Pending → SkippedScope composition or aidlc-jump executetools/aidlc-utility.ts, tools/aidlc-jump.ts

The approved report owns the full post-gate transition: it emits GATE_APPROVED + STAGE_COMPLETED, then routes to the next in-scope stage, emitting STAGE_STARTED plus any PHASE_* events at boundaries. On the final in-scope stage it emits PHASE_COMPLETED + PHASE_VERIFIED + WORKFLOW_COMPLETED and sets Status=Completed. The conductor does not call state lifecycle verbs before or after reporting.

Routed skip. report --result skipped is accepted only on the main workflow with an explicit nonblank --stage and --reason, when the named stage is declared execution: CONDITIONAL, equals Current Stage, and is Active or Revising. It runs before artifact, per-unit, and ensemble-evidence guards because a justified skip owes no completion evidence. The engine invokes the internal skip transition with its routing marker: the transaction preserves [S], emits exactly one STAGE_SKIPPED, never emits STAGE_COMPLETED, and either starts the next stage (including boundary events) or completes the workflow. If onward routing fails, recovery leaves the skipped marker and cursor at the same stage so the route can be retried without duplicating the skip event. report --single --result skipped is rejected.

Artifact guard (issue #366). Every report outcome that marks a stage [x] runs a deterministic artifact check before completing it, so a stage cannot be marked complete without evidence of work on disk. A stage that declares produces[] must have at least one of those artifacts present under the active intent's record dir or its per-unit Construction directories. A codekb stage is stricter: every registered repository directory must contain the full declared produces[] set; single/unrecorded intents use the one resolved codekb directory. workspace_requires: true also requires source-work evidence outside aidlc/ and the harness dir. A failure writes nothing. Optional outputs do not participate. For produces_kinds, units whose kind prunes the required set to zero owe no artifact; any applicable unit remains strict. Bypass with AIDLC_SKIP_ARTIFACT_GUARD=1. The same switch also bypasses the review logger's required-output existence check; without it, a stage-level review of a per-Unit stage requires every authoritative Unit's applicable required outputs. When no authored Unit DAG exists but the current attempt has merged Bolt rows, the stage-level review fingerprint instead enumerates exactly those merged Units. A Bolt merge is proven, not asserted: aidlc-bolt complete --merge emits BOLT_COMPLETED before the state and audit merges run, so a slug-backed (worktree) attempt counts as merged only once a matching later AUDIT_MERGED receipt confirms the merge sequence landed on main; a name-only, non-worktree Bolt's BOLT_COMPLETED remains terminal. A completion still awaiting merge evidence stays on the open path, a slugless completion cannot close a slug-backed attempt, and a later fragment-cleanup BOLT_FAILED cannot erase a confirmed merge. AUDIT_MERGED itself carries no reviewer authority: a same-second row in another audit shard never makes a review request, verdict, or receipt ambiguous. An open Bolt remains on the ordinary stage-level fallback path because its Unit artifacts still live outside the main tree. Merging that Bolt changes the fingerprint domain and intentionally invalidates an earlier stage-level receipt until its exact pending ordinal is rebound with --retry-pending. Per-Unit receipt filtering still recognizes both open and merged Units. A Unit may belong to both sets when an older attempt merged while a newer attempt remains open; merged membership drives gate demand and stage-level fingerprinting, while the union drives receipt filtering.

Reviewer gate guard (issue #551). A reviewer-bearing stage cannot enter AwaitingApproval through gate-start or revise until its configured reviewer has a fresh terminal REVIEW_COMPLETED receipt. The same receipt remains mandatory on all four completion paths. Re-reporting an already-open gate re-runs these guards without writing a duplicate transition. A rejection reported directly from Active moves to Revising without fabricating a STAGE_AWAITING_APPROVAL row. Synthetic transition tests that deliberately isolate another guard may set AIDLC_SKIP_REVIEWER_GATE_GUARD=1; this bypass applies only to gate opening, never to approve, advance, finalize, or complete-workflow. The adjacent summary-confirmation test bypass is AIDLC_SKIP_SUMMARY_CONFIRMATION_GUARD=1.

Ensemble evidence gate. On a mob or subagent-with-supports stage, the report path refuses awaiting-approval, revised, and approved while a declared support agent's contribution file (<stage>/contributions/<agent-slug>.md) is missing or lacks its **Collaborator:** identity-marker first line — the deterministic proof the ensemble actually convened. A settled autonomous swarm is exempt (its per-unit convergence ledger is the evidence); report --single checks stage-level evidence only. On mode: pipeline, the same report outcomes and every direct completing transition require an ordered, current-attempt PIPELINE_LINK_COMPLETED receipt for every lead/support link. Multi-repo reverse engineering requires a complete chain per scanned repo; a current-attempt repo-scoped ARTIFACT_REUSED row with Decision=keep exempts a reused repo. Isolated rows carry Workflow: single-stage:<slug> and are accepted only while the complete graph-declared Reverse Engineering artifact set remains valid and the store remains CURRENT; while modify/redo rows do not. A rejection, jump, or later stage start resets the main-workflow evidence, and isolated --single link rows never satisfy it. Bypass with AIDLC_DISABLE_ENSEMBLE_EVIDENCE=1, intended only for recovering a legitimately-run stage whose contribution files or in-flight link receipts were lost.

Source freshness and per-unit attribution (#629/#646/#662). On a workspace_requires stage, every terminal review still carries the workspace- global Source Fingerprint; the newest modern binding is normally the outer post-review-mutation boundary on all four completion routes. Per-unit receipts add Unit Source Fingerprint, which binds the raw bytes of the unit's strict source-manifest.json and the current content of every exact/directory claim. Receipts are evaluated newest-first, so a newer validated claimant may shield an older receipt for an intentional shared path. An uncovered edit, deletion, or new path in an exact/directory claim invalidates only the owning unit and enters that unit's one bounded stale-receipt recovery.

WORKFLOW_STARTED, STAGE_JUMPED, and a workspace_requires STAGE_STARTED record content-addressed source-listing baselines. After every applicable unit has fresh modern evidence, completion compares baseline to the current listing and refuses any changed application-source path outside the fresh claims union. Unit-major Construction always uses the workflow/jump boundary because source work can precede its late STAGE_STARTED. Equal-second cross-shard rows that would decide a boundary or newest claimant fail closed instead of trusting shard filename order.

A rejection resets review and run-floor accounting but never replaces that completion baseline: otherwise any unclaimed path present at rejection would be grandfathered into the next attempt. When the prior attempt has a validated SWARM_SOURCE_MERGED chain, GATE_REJECTED carries only its final Prior Accepted Source Fingerprint. The next attempt's first source merge must start from that aggregate, while completion continues to compare against the original stage-entry baseline.

There is one narrowly bounded reconciliation of the global boundary: if an unclaimed baseline change — addition, modification, or deletion — is fully reverted, completion may proceed only when the effective baseline snapshot is present and valid, every applicable unit still has a fresh modern unit binding, and the baseline-to-current delta contains zero unclaimed paths. This proves that the transient unclaimed change is gone. Any ordinary post-review edit, stale or legacy unit binding, missing evidence, or remaining unclaimed delta still takes the normal global-first refusal path.

The fingerprint and canonical per-path listing come from one bounded filesystem walk, independent of repository metadata and Git executable availability. Ordinary and ignored application bytes, external source-symlink targets, and workspace-roof files remain bound. Framework state, exact sensor caches, VCS metadata, dependency/cache directories or symlinks, and unregistered build/, coverage/, dist/, logs/, target/, and tmp/ directories or symlinks remain outside the source boundary.

Real source beneath a conditional generated-output directory, including binary or extensionless source, can be declared in root .aidlc-source-paths.json:

{"version":1,"paths":["dist/worker.js","build/source"]}

Registered paths are content-bound regardless of encoding and are included in the canonical listing and autonomous swarm Source Commit. Absolute, traversing, framework, sensor-cache, and dependency/cache paths are rejected. Missing registered repositories contribute an explicit marker; unreadable, unstable, over-budget, or malformed boundaries remain unbindable and fail closed.

Migration is deliberate: a pre-upgrade workflow with no baseline skips the unclaimed check, and a fieldless per-unit receipt retains the #629 global policy. A present but unbindable, missing, or corrupt modern baseline/unit snapshot fails closed. AIDLC_SKIP_SOURCE_FRESHNESS=1 bypasses both global and per-unit checks; missing/invalid-manifest receipts explicitly record Unit Source Binding Bypass: true, so the switch must be present again at completion. In a modern Bolt, finalize also verifies the attested base-to- worktree footprint is a subset of the reviewed manifest claims before the settled-swarm stage-level exemption applies.

Swarm footprint verification and immutable Source Commit creation apply the same boundary. Clean-filter raw-byte replacement is restricted to exact filesystem-included regular paths, so excluded generated or framework files cannot re-enter after shaping. New-submodule recovery shares one 30-second cumulative deadline and a 32-proof cap across the entire finalize call, in addition to the per-command, ref-count, refspec-size, recursion, and materialized-checkout bounds. AIDLC_SKIP_SOURCE_FRESHNESS=1 disables the check; a bypassed finalize records Source Freshness Bypass: true, and merge must repeat the same switch.

Gate-revision backstop. If the conductor revises an artifact at an open gate without first reporting rejection, the approved report reconciles the missing GATE_REJECTED + STAGE_REVISING pair before completion when audit evidence proves a post-gate human turn followed by an artifact write. The backfilled rows carry Recovered: true; reviewer writes before the human turn do not count. Reviewer-bearing stages persist [R] after that recovered rejection and require a fresh review plus the normal revised report before the gate can reopen. Bypass with AIDLC_SKIP_REVISION_BACKSTOP=1.

Archive (issue #980). aidlc-utility intent archive <name> [--reason <text>] retires an in-flight intent the team will not finish. Under the workspace lock it emits WORKFLOW_ARCHIVED into that intent's own audit shard first, then flips the state file's Status to Archived and the intents.json row to archived; the record dir, its artifacts, and its audit shards are never moved or deleted. A subsequent next on that record (a stale per-user cursor or session binding) emits a terminal done naming intent unarchive, so retired stages never resume by accident; park refuses an archived workflow the same way it refuses a completed one. Archiving is refused for a completed intent (already terminal), an intent with Bolt worktrees still in flight, and a team-owned intent with claimed Units. intent unarchive <name> reverses the two field writes and emits WORKFLOW_UNARCHIVED. The default intent listing hides archived rows (--all shows them; --json always carries every row), the creation gate ignores them, and the lone-record fallback never resolves one implicitly.

Park (issue #365/#367). aidlc-orchestrate park writes a Parked / Parked At Stage runtime marker (via aidlc-state.ts park, which emits WORKFLOW_PARKED) without advancing any stage; a subsequent plain next re-emits a terminal parked directive and the Stop hook lets the turn end, so a long workflow can pause across sessions instead of rubber-stamping the remaining stages to reach done. /aidlc --resume clears the marker (unpark emits WORKFLOW_UNPARKED) before continuing. An unattended autonomous Construction run (Construction Autonomy Mode: autonomous) refuses to park: both the tool and the Stop hook's parked allow decline under autonomous mode, so the loop keeps moving with no human to resume it.

Revision loop

report awaiting-approval  →  [?] AwaitingApproval
          ↘ report rejected  →  [R] Revising  (Revision Count += 1)
                   ↓ report revised
                   [?] AwaitingApproval
                   ↘ report approved  →  [x] Completed

Revision Count lives in the state file and increments on each rejected report. The conductor uses this to detect the revision-loop escape hatch (default is 3 cycles before offering to skip).

When a revision changes a produces[] artifact on a stage whose directive carries a reviewer, the conductor re-runs the stage-protocol-reviewer.md §12a step before reporting revised (stage-protocol Part 0). The engine verifies the fresh terminal receipt before accepting the revised report and re-opening the gate.


Session stream (hook-owned, independent)

Session events are emitted by Claude Code hooks, not by AI-DLC tools. A session is a single Claude Code conversation; a workflow is a long-lived directory state. The relationship is many-to-many — one workflow can span multiple sessions, one session can touch multiple workflows — so the streams are independent by design.

EventEmitterTrigger
SESSION_STARTEDhooks/aidlc-session-start.tsSessionStart with source=startup or clear
SESSION_RESUMEDhooks/aidlc-session-start.tsSessionStart with source=resume
SESSION_COMPACTEDhooks/aidlc-validate-state.tsPreCompact — fires at compaction time so it's captured reliably
SESSION_ENDEDhooks/aidlc-session-end.tsSessionEnd

Session hooks check for the active intent's aidlc-state.md (under aidlc/spaces/<space>/intents/<YYMMDD>-<label>/) before emitting. If no such file exists (no active AI-DLC workflow in the cwd), the hook exits silently without writing to any audit log. Session events exist to annotate an active workflow's timeline — a session in a directory with no workflow has nothing to annotate.

Compaction awareness

aidlc-state.ts resume scans the audit tail for the latest SESSION_COMPACTED. If no stage activity (STAGE_STARTED, STAGE_COMPLETED, GATE_APPROVED, SESSION_RESUMED, RECOVERY_COMPLETED) follows it, resume returns compaction_pending: true and the conductor surfaces a three-option prompt (continue / review / restart) before proceeding. RECOVERY_COMPLETED is emitted by acknowledge-compaction once the user picks an option, satisfying the activity gate so subsequent compactions detect a fresh boundary.


Audit event taxonomy

98 events, grouped below into 20 categories (the canonical audit-format.md registry splits the same 98 into 24 - the grouping is presentational, the event set is the invariant). Every event has exactly one tool or hook emitter, except for events pre-registered for an upcoming release whose Emitter cell reads Reserved (v0.4.0 PR N), Reserved (v0.5.0 PR N), or Reserved (v0.6.0 PR N) - these are skipped by the drift test's forward check until the consumer PR ships the emitter. The drift test tests/integration/t48-audit-event-emitters.test.ts enforces forward/reverse/tertiary/pairing/MD-MD consistency between this chapter's tables and the code.

Workflow lifecycle

EventEmitterNotes
WORKFLOW_STARTEDtools/aidlc-utility.tsMandatory first event on every intent creation
WORKFLOW_COMPLETEDtools/aidlc-state.ts
WORKFLOW_PARKEDtools/aidlc-state.tspark - workflow parked mid-flow for a later session; no stage advanced
WORKFLOW_UNPARKEDtools/aidlc-state.tsunpark - park marker cleared on explicit --resume re-entry
WORKFLOW_ARCHIVEDtools/aidlc-utility.tsintent archive <name> - intent retired to Archived / archived; record and audit shards preserved; written to that intent's own shard
WORKFLOW_UNARCHIVEDtools/aidlc-utility.tsintent unarchive <name> - archived intent returned to Running / in-flight

Phase lifecycle

EventEmitterNotes
PHASE_STARTEDtools/aidlc-utility.ts, tools/aidlc-state.ts, tools/aidlc-jump.tsFirst fire in init; subsequent fires at stage-tool phase boundaries
PHASE_COMPLETEDtools/aidlc-utility.ts, tools/aidlc-state.ts, tools/aidlc-jump.tsPaired with PHASE_VERIFIED at every boundary
PHASE_VERIFIEDtools/aidlc-utility.ts, tools/aidlc-state.ts, tools/aidlc-jump.tsAlways paired with PHASE_COMPLETED
PHASE_SKIPPEDtools/aidlc-utility.tsOne per scope-excluded phase, emitted at intent creation

Stage lifecycle

EventEmitterNotes
STAGE_STARTEDtools/aidlc-state.ts, tools/aidlc-utility.ts, tools/aidlc-jump.tsInternal route marks [ ][-]
STAGE_AWAITING_APPROVALtools/aidlc-state.tsInternal emitter for report --result awaiting-approval / revised; recovered rows carry Recovered=true; an authorized blocking-sensor override records sensor ids, optional detail paths, and evaluation reasons
STAGE_COMPLETEDtools/aidlc-state.ts, tools/aidlc-utility.tsInternal emitter for a completed/approved report; never paired with a skipped report
STAGE_REVISINGtools/aidlc-state.tsInternal emitter paired with GATE_REJECTED after a rejected report
STAGE_SKIPPEDtools/aidlc-state.ts, tools/aidlc-jump.tsExactly one per [S] transition; the main-workflow report path routes onward atomically
STAGE_JUMPEDtools/aidlc-jump.tsRecords the destination slug on --stage/--phase jump. Backward jumps also bind the concrete changed upstream artifact paths and the downstream artifact/review paths invalidated by the reset.

Gate decisions

EventEmitterNotes
GATE_APPROVEDtools/aidlc-state.ts, tools/aidlc-unit.ts gate--user-input captures the exact choice. On reviewer-backed gates, the same atomic row stores content-addressed Accepted risk dispositions for every current open finding. Unit merge gates also bind Pinned OID, Attempt Generation, Strategy, and Target branch.
GATE_REJECTEDtools/aidlc-state.ts, tools/aidlc-unit.ts gate--feedback captures the rejection reason. Explicit --reject-finding <review-artifact>#R-NN=<reason> values store content-addressed Rejected: <reason> dispositions; generic revision feedback does not reject a finding. Unit merge gates bind the same pinned transaction fields.

Under Unit Ownership: team, these rows additionally carry Unit, Gate Scope, and Gate Stages. Per-stage approval settles only that (stage, Unit); unit-end approval settles the Unit chain. A Unit-tagged rejection floors lifecycle and review receipts only for that Unit (all Gate Stages for unit-end); legacy Unit-less rows retain stage-global behavior.

User interaction

EventEmitterNotes
DECISION_RECORDEDtools/aidlc-log.tsFires before a non-gate AskUserQuestion so options are captured
QUESTION_ANSWEREDtools/aidlc-log.tsFires after a non-gate question response; approval choices are lifecycle events owned by report
SUMMARY_CONFIRMATION_RECORDEDtools/aidlc-log.tsHuman-backed consolidated-summary receipt; new rows carry Hash Scope: confirmed-content-v1, which preserves the canonical order of the preamble and all visible Q and feedback sections, including follow-up questions after an assumption decision. Exactly one post-summary Assumption Confirmation section and its contents are excluded; a same-named pre-summary section remains hashed. Any other visible Markdown or raw-HTML heading after the summary fails closed. Stage-specific pre-summary headings remain valid. Unscoped receipts retain legacy whole-file verification and need reconfirmation after an allowed append. A Looks correct receipt also carries Summary Authorization Id, the authorization the confirmation minted (a digest of the attempt, stage, Unit, workflow, questions path, confirmed content, and choice); the same id becomes the scope's active authorization under <record>/.aidlc-summary-authorization/, and a Request changes reply withdraws it. Reserved from public audit append.
PLAN_APPROVAL_RECORDEDtools/aidlc-log.tsHuman-backed Code Generation plan receipt. The authority it records binds to intent, stage or Unit target, stage attempt (run floor), content fingerprint (the projected plan and instructions plus the Testing Contract hash), prompt (the questions file with answers blanked), and session response, never to the identity of the directive that presented the question nor to row order. The row also carries the directive epoch and the raw questions-file digest (Questions SHA-256) as provenance; both are recorded and never compared, so a note appended to the questions file after approval leaves the decision standing while a change to the prompt the human saw retires it. Protected runtime state is the authority; this row is provenance only.
PLAN_APPROVAL_OVERRIDDENtools/aidlc-log.tsThe human-only break-glass exit for Plan Approval. Fires only when the human typed Override Plan Approval: <reason> as a prompt (the human-turn hook records that typed text under the session; a picked option never does), the conductor ran answer --checkpoint plan-approval --override "<reason>" with the same reason, and the normal receipt path refused. Carries Reason, Failed Checks (what the normal path refused), Session, Unit or stage-level, and Fingerprint; the paired PLAN_APPROVAL_RECORDED row carries Override: yes. The receipt it accompanies binds to plan content and stage attempt only, so no later check compares its source. The typed request is single-use and the conductor never proposes or initiates it
REVIEW_REQUESTEDtools/aidlc-log.tsFires when the conductor dispatches the reviewer defined by stage-protocol-reviewer.md §12a. The stage's required review_artifact scalar names the Markdown output the review is about; plugin-added outputs and produces ordering cannot change it. A new --unit request must name a member of the authoritative DAG or a Unit proven by a matching open or merge-confirmed tool-owned Bolt attempt in the current no-DAG attempt; a completion still awaiting its AUDIT_MERGED merge evidence, like a historyless Unit, refuses. A single stable file-identity snapshot records every declared artifact exactly as dispatched (Artifact Fingerprint) and the request-time workspace source for workspace_requires stages; the row mints a Request Id that the completion row and the review record echo. The command's JSON returns requestId and reviewFile, the slot under <record>/.aidlc-reviews/ where the reviewer writes its review; the request opens that slot, removing a draft an earlier incomplete dispatch of the same iteration left. --retry-pending re-issues one unmatched request with the original binding and request id when the artifacts and source still match; a request recorded before request ids or source binding gains them through that one retry, marked Upgrade: legacy-request. Rows written under the retired appendix protocol also carry Review Appendix Artifact, Review Appendix Offset, Review Appendix Prior Digest, Review Appendix Prior Length, and Review Challenge; they stay readable and a completion of such a request echoes them unchanged.
REVIEW_COMPLETEDtools/aidlc-log.tsFires only after a matching positive-iteration request. One coherent artifact snapshot must reproduce the requested bytes exactly: the reviewer writes no artifact, so Request Fingerprint and Artifact Fingerprint are the same identity. The review is read from the request's review file (or --review-file), validated with Bun's Markdown parser (one rendered Verdict matching --verdict, one Reviewer, one Iteration, no later Markdown or raw-HTML H1/H2; literal examples in fenced/inline code and HTML comments carry no authority, while list/blockquote/table containers cannot mint ownership), and written as the review record <record>/.aidlc-reviews/<stage>/stage/<attempt>/<iteration>.json or <record>/.aidlc-reviews/<stage>/units/<unit>/<attempt>/<iteration>.json in the same locked transaction; the row names it (Review Record) and pins its bytes (Review Record Digest), and echoes the Request Id. Request-time and completion source fingerprints use the same Git-independent bounded filesystem identity and must match. Deprecated for this release cycle: a verdict is still accepted from a terminal ## Review section a reviewer appended to review_artifact after the request (the bytes before it are the requested bytes and the request saw no appendix); that validated section is copied into the review record. A retried incomplete review may record NOT-READY with an empty review record. A malformed row is ignored and does not consume its pending request.
PIPELINE_LINK_COMPLETEDtools/aidlc-log.tsFires after one declared pipeline link returns. Carries Stage, Link, and Position k/N; multi-repo chains also carry Repo, and isolated runs carry Workflow=single-stage:<slug>. The tool refuses undeclared, duplicate, or out-of-order links within that receipt scope. Main-workflow gate-start, approval, advance, finalize, and workflow completion ignore isolated rows and require every scanned-repo current-attempt link receipt.

Unit lifecycle (inline per-unit Construction stages)

EventEmitterNotes
UNIT_STARTEDtools/aidlc-state.tsunit start — requires the exact stage/Unit pair currently routed by the engine, a safe Unit identifier from the authoritative DAG (including safe legacy spellings), and no other open Unit
UNIT_PAUSEDtools/aidlc-state.tsunit pause — requires --reason and --next-action; the engine routes the paused unit first and hard-stops until an explicit resume
UNIT_RESUMEDtools/aidlc-state.tsunit resume — only the currently-paused unit can resume
UNIT_COMPLETEDtools/aidlc-state.tsSerial unit complete verifies the active unit's required artifacts. Wave unit complete --wave instead verifies the engine still exposes that entry as build-complete/review-settled, copies new Unit diary entries into the parent diary with deterministic markers, binds the receipt to the final artifact fingerprint, then commits without opening a single-active checkpoint. All lifecycle rows carry an exact boundary-event/timestamp/ordinal Run floor (or a fail-closed cross-shard ambiguity token); receipt mode stays enabled across attempts, so stale, changed, ambiguous, reopened, or not-yet-fanned-in Units block the gate until they complete again.
UNIT_MERGEDtools/aidlc-state.tsMain landed the pinned candidate content, received the team's audit shard, and folded this Unit's derived row. Fields bind the row to Unit, owner, pinned candidate OID, merge commit OID, and attempt generation.

Team-owned unit-major runs add a derived ## Unit Progress table to state. The engine rewrites it from these receipts, artifacts, reviews, Unit gate rows, and UNIT_MERGED receipts on each next; it is not an authority and manual cells are ignored. Once a pinned merge transaction or UNIT_MERGED receipt exists, a merged column prevents the per-unit block from settling until every merge-bound row has landed. Claims alone retain the increment-2 projection without this column.

Scope and configuration

EventEmitterNotes
SCOPE_DETECTEDtools/aidlc-utility.tsdetect-scope subcommand; Source field records provenance (freeform / keyword / env / cli)
SCOPE_CHANGEDtools/aidlc-utility.tsscope-change subcommand on active workflow
PLUGIN_SELECTION_CHANGEDtools/aidlc-utility.tsselect-plugins set-mode; fields: Previous Selection, New Selection
DEPTH_CHANGEDtools/aidlc-utility.tsconfig set depth <value> / config-change --depth
TEST_STRATEGY_CHANGEDtools/aidlc-utility.tsconfig set test-strategy <value> / config-change --test-strategy
UNIT_OWNERSHIP_SETtools/aidlc-state.ts`set-unit-ownership team
UNIT_GATE_RHYTHM_SETtools/aidlc-state.ts`set-unit-gate-rhythm per-stage
REVIEW_CLASS_CHANGEDtools/aidlc-utility.tsconfig set review <value> / config-change --review / a combined scope-change --review set or cleared the per-run review override
RECOMPOSEDtools/aidlc-utility.tsrecompose subcommand - the adaptive composer's in-flight plan re-shape (pending-stage suffix flips under the audit lock)
CHANGE_CONTROL_SETtools/aidlc-lib.tsThe intent's Change Control value moved: the change-control <strict|relaxed> verb (behind /aidlc --change-control and the plain-chat request), a scope-change carrying a scope-supplied value to the new scope's default, or a governed checkpoint observing a memory-layer edit. Fields: Old Value, New Value, Source (you, scope <name>, <layer>.md)
CHANGE_ACCEPTEDtools/aidlc-lib.tsA governed checkpoint (plan-approval source drift, review-receipt content change, summary-confirmation authorization) accepted an input change under relaxed and continued. Fields: Stage, optional Unit, Checkpoint, Changed, Recorded, Current, Details (the one line the human hears). One row per distinct change; the same values never produce a second row

Change Control decides the consequence of an input change after a human approval or confirmation (strict reopens the approval with the existing remedy, relaxed records the change and continues); it never removes a gate, never alters a reviewer's verdict, and never deletes evidence. The value is read only where such a change is met (the three governed checkpoints), by the change-control verb, by intent-create, and by /aidlc --status; a check that meets no change reads nothing. An invalid memory Mode: value is a validation error naming the file and the two allowed values at each of those reads, so a stage whose inputs did not change is not stopped by it.

Artifacts

EventEmitterNotes
ARTIFACT_CREATEDhooks/aidlc-write-audit-log.tsWrite to net-new path, distinguished from UPDATED via mtimeMs == birthtimeMs stat check. Carries Summary Authorization Id when the written stage and Unit have an active summary confirmation, so completion can ask whether the output descends from the current confirmation
ARTIFACT_UPDATEDhooks/aidlc-write-audit-log.tsEdit tool or Write overwriting existing file. Same Summary Authorization Id stamp as ARTIFACT_CREATED
ARTIFACT_REUSEDtools/aidlc-state.tsreuse-artifact subcommand — keep/modify/redo decisions; optional Repo scopes evidence to one registered repo, optional --single binds it to the open synthetic attempt, but only keep with a complete authoritative artifact set and still-CURRENT isolated Reverse Engineering store grants that pipeline exemption

Construction Bolts

EventEmitterNotes
BOLT_STARTEDtools/aidlc-bolt.tsAccepts CSV bolt names for parallel batches; a modern --worktree row propagates the immutable Base commit and content-addressed raw-aware Base Source Listing attested at worktree creation
BOLT_COMPLETEDtools/aidlc-bolt.tsPaired with a prior BOLT_STARTED
BOLT_FAILEDtools/aidlc-bolt.ts (fail + abort)--succeeded-siblings captures parallel-batch survivors; abort adds Reason: aborted field for sub-classification
AUTONOMY_MODE_SETtools/aidlc-bolt.tsAtomically updates Construction Autonomy Mode field; validates field exists first (audit-first)

Session

EventEmitterNotes
SESSION_STARTEDhooks/aidlc-session-start.tssource=startup or clear
SESSION_RESUMEDhooks/aidlc-session-start.tssource=resume
SESSION_COMPACTEDhooks/aidlc-validate-state.tsEmitted at PreCompact (not at next SessionStart) to avoid duplication
SESSION_ENDEDhooks/aidlc-session-end.tsIncludes Reason field from Claude Code
HUMAN_TURNhooks/aidlc-record-human-turn.ts (+ per-harness prompt-submit adapters)One per observed prompt-submit or answered-widget seam unless the driver declares AIDLC_UNATTENDED=1; the approval/interview gate requires one since the last gate resolution. This is presence/freshness evidence, not an authenticated transcript or proof that later caller-supplied decision text was authored by the human.
SUBAGENT_COMPLETEDhooks/aidlc-log-subagent.tsRecords subagent completion via SubagentStop hook
REVIEWER_SCOPE_BLOCKEDhooks/aidlc-reviewer-scope.tsA per-unit reviewer's tool call refused for reaching into sibling units' construction/ paths (the reviewer-module read-scope bound); one row per refusal
REVIEW_FREEZE_BLOCKEDhooks/aidlc-review-freeze.tsA file-tool or shell produces[] write refused because it would invalidate a fresh terminal review receipt before the gate (READY or terminal NOT-READY under the effective class); one row per refusal
PLAN_APPROVAL_BLOCKEDhooks/aidlc-plan-approval-guard.tsA code-generation developer-agent dispatch or workspace mutation refused because the active unit or zero-Unit stage target lacked a current fingerprinted plan, test instructions, Testing Contract, explicit approval, or matching worker-brief marker; one row per refusal
GUARD_DISABLEDhooks/aidlc-plan-approval-guard.tsA tool call passed the Plan Approval guard because its deterministic off-switch environment variable was set while a workflow existed. Carries Guard (plan-approval-guard) and Tool; one row per streak, appended only when the newest row in the active shard is not already this event for the same guard

Diagnostics and workspace

EventEmitterNotes
HEALTH_CHECKEDtools/aidlc-utility.ts--doctor run
WORKSPACE_SCAFFOLDEDtools/aidlc-utility.tsNet-new directory tree created by init
WORKSPACE_SCANNEDtools/aidlc-utility.tsBrownfield workspace detection complete
WORKSPACE_INITIALISEDtools/aidlc-utility.tsState file materialized

Documents

The DocumentKB is space-level, so all three land in one space-level shard even for an intent-scoped document — the intent UUID is a field on the event, not the shard selector.

That shard is spaces/<space>/intents/audit/, not spaces/<space>/audit/. The intents/ segment is inherited from intentsDir(), which is where every shard in a space lives; the space-level shard is a sibling of the per-intent record dirs rather than a directory one level up. An earlier version of this line documented the shorter path, which does not exist on disk — measured by onboarding a document and finding the written shard.

Workflow-authority readers enumerate only the resolved intent's shards. Consumers that need space-level provenance request it explicitly; --doctor --export does so and reads the space shard before the resolved intent shards, keeping document events visible without widening lifecycle authority beyond the intent ledger.

All three ship with tools/aidlc-knowledge.ts (DocumentKB S1). Emitting verbs per event are listed in each row below — onboard, sync, associate, dissociate, rebind, and summarize all emit.

EventEmitterNotes
DOCUMENT_INDEXEDtools/aidlc-knowledge.tsFrom onboard and from sync's fresh-document branch: a customer document entered the DocumentKB for the first time Audit-last (see "Audit-last for derived catalogs"): emitted only after every catalog write succeeds.
DOCUMENT_UPDATEDtools/aidlc-knowledge.tsFrom associate, dissociate, rebind, summarize (Change: summarized), onboard's edited-row branch, and sync's moved/changed/retried branches: a new revision, re-extraction, move, summary publication, or intent-association change. A normal no-op emits nothing. An idempotent retry may emit Change: audit-repair or the missing association delta when it detects that a prior audit-last call committed the catalog but failed before provenance; this records the already-committed state rather than a new user mutation. Audit-last (see "Audit-last for derived catalogs"): emitted only after every catalog write succeeds.
DOCUMENT_REMOVEDtools/aidlc-knowledge.tsFrom sync: the original is gone, so the row is tombstoned and extracted content deleted. The metadata.json tombstone is kept, so a later index rebuild does not resurrect the row as absent Audit-last (see "Audit-last for derived catalogs"): emitted only after every catalog write succeeds.

All three land in the space-level audit shard even when the document is scoped to an intent: a document outlives any intent, and its scope can move later, so filing its provenance under whichever intent happened to be active would split one document's history across shards and make it unreconstructible.

Error and recovery

EventEmitterTrigger
ERROR_LOGGEDtools/aidlc-lib.ts (via emitError from every tool's error())Any tool CLI that calls error(msg) to exit non-zero; best-effort — no-op if no workflow in cwd, guarded against recursion
RECOVERY_COMPLETEDtools/aidlc-state.ts`acknowledge-compaction --choice <continue

Worktree

Pre-registered for v0.4.0; the three WORKTREE_* rows ship with aidlc-worktree.ts (milestone 7); STATE_* lands in milestone 9 (state fork/merge); AUDIT_* lands in milestone 10 (audit fork/merge). t48 forward check skips rows whose Emitter cell still reads Reserved.

EventEmitterTrigger
WORKTREE_CREATEDtools/aidlc-worktree.tsAudit-first per-Bolt creation records the immutable Base commit, Base Source Listing, and portable creating-repo selector (Repo, - for root); private worktree metadata also binds the canonical Git common-dir. Swarm prepare additionally stamps intent/Unit/batch/stage/floor provenance (subcommand: create)
WORKTREE_MERGEDtools/aidlc-worktree.tsBolt's worktree merged back to main on gate approval (subcommand: merge)
WORKTREE_DISCARDEDtools/aidlc-worktree.tsAborted Bolt's worktree explicitly removed (subcommand: discard)
STATE_FORKEDtools/aidlc-state.tsState file forked to worktree on Bolt start (subcommand: fork)
STATE_MERGEDtools/aidlc-state.tsWorktree's state merged back to main on gate approval; alphabetical-slug tiebreak as defence-in-depth (subcommand: merge)
AUDIT_FORKEDtools/aidlc-audit.ts (audit-fork)Audit log forked to worktree on Bolt start; audit-of-intent — emit precedes the byte-copy
AUDIT_MERGEDtools/aidlc-audit.ts (audit-merge)Worktree's audit entries appended to main audit on gate approval; per-Bolt entry order preserved, cross-Bolt order reflects merge-completion order. Under the same lock, before any row lands, the review records named by the delta's REVIEW_COMPLETED rows are carried into the main intent record: only for a completion that descends from a REVIEW_REQUESTED row in the same delta, read without following symlinks, refused when hardlinked, oversize, or not hashing to the pinned digest, and never overwriting a record already present with different bytes. A merge that cannot carry a record refuses.

Practices

Pre-registered for v0.4.0; emitters land in milestone 8 (stage 2.2 practices-discovery) and milestone 13 (Construction orchestrator runtime).

EventEmitterTrigger
PRACTICES_DISCOVEREDtools/aidlc-state.ts practices-event --type discoveredGreenfield or brownfield lead draft + three spokes + human interview + lead integration completed; drafts await affirmation
PRACTICES_AFFIRMEDtools/aidlc-state.ts practices-promoteTeam approved practices; content promoted from the intent's inception/practices-discovery/ to aidlc/spaces/<active-space>/memory/team.md and project.md
PRACTICES_OVERRIDEtools/aidlc-state.ts practices-promote (write-failure path) and tools/aidlc-state.ts practices-event --type override (bolt-plan-marker-conflict path)Either promotion failed and the stage remains awaiting approval, or the active-space walking-skeleton stance overrode the current Bolt's marker
PRACTICES_SECTION_EMPTYtools/aidlc-state.ts practices-event --type emptyConductor read a practices section that returned empty; advisory-only, falls back to org defaults

Merge dispatch

Pre-registered for v0.4.0 in milestone 1; emitters land in milestone 13 via the new aidlc-bolt dispatch-event subcommand. The conductor brackets each aidlc-pipeline-deploy-agent dispatch — pre-call INVOKED, post-call RETURNED on successful YAML parse, FALLBACK on timeout / malformed-YAML / low-confidence.

EventEmitterTrigger
MERGE_DISPATCH_INVOKEDtools/aidlc-bolt.ts dispatch-event --event MERGE_DISPATCH_INVOKEDConductor dispatched aidlc-pipeline-deploy-agent via Task to determine merge strategy from team practices prose
MERGE_DISPATCH_RETURNEDtools/aidlc-bolt.ts dispatch-event --event MERGE_DISPATCH_RETURNEDAgent returned parsed YAML with strategy, target branch, confidence, and notes
MERGE_DISPATCH_FALLBACKtools/aidlc-bolt.ts dispatch-event --event MERGE_DISPATCH_FALLBACKAgent timed out or returned malformed YAML; conductor fell back to org defaults — critical observability hook

Sensors

The sensor dispatcher emits the four SENSOR_* events and doctor emits the paired-coverage GUARDRAIL_LOADED row. Write-fired sensors dispatch from PostToolUse on matching paths. Gate-fired sensors dispatch once per existing declared deliverable before initial, revised, or approve-backstop recovered gate entry. A blocking binding proceeds only on a verified pass; findings, unavailable execution, malformed/mismatched verdicts, and budget overruns refuse. An override requires the logged offered choice, a human turn, the exact answer receipt, and matching --user-input; autonomous mode cannot override. Explicit and discovered artifact paths are canonically confined to the stage produce directories. Blocking declarations on write-fired sensors remain advisory in this release.

EventEmitterTrigger
SENSOR_FIREDtools/aidlc-sensor.ts fireDispatcher invoked a sensor against a stage output from a matching Write/Edit or gate-boundary dispatch
SENSOR_PASSEDtools/aidlc-sensor.ts fireSensor completed and reported no findings (also covers tool-unavailable and script-error fall-through; Note field discriminates)
SENSOR_FAILEDtools/aidlc-sensor.ts fireSensor completed and reported findings; detail file written at <record>/.aidlc-sensors/<stage-slug>/<sensor-id>-<fire-id>.md (in the intent's record dir)
SENSOR_BUDGET_OVERRIDEtools/aidlc-sensor.ts fireSensor exceeded its configured cap (registry / binding / depth-derived per the three-layer cap model) and was terminated or skipped
GUARDRAIL_LOADEDtools/aidlc-utility.tsGuardrail loader resolved the scope-hierarchical guardrail set for the active workflow (org → project → phase → stage); doctor's paired-coverage check reads from this event

Learning loop

Pre-registered for v0.5.0 in milestone 4; MEMORY_EMPTY emitter lands in milestone 8 (aidlc-runtime.ts compile). The §13 Learnings Ritual writes a per-stage memory.md during execution; on stage approval, the runtime-graph compile reads memory.md and emits MEMORY_EMPTY for any stage with zero non-blank entries under the four standard headings. milestone 12's learning-gate tool (aidlc-learnings.ts persist) emits RULE_LEARNED when a kept learning lands as a dated practice entry in aidlc/spaces/<active-space>/memory/{project,team}.md, and SENSOR_PROPOSED when a learning installs a sensor binding (manifest + originating stage sensors: frontmatter). Doctor reads these rows for diary-discipline observability.

EventEmitterTrigger
MEMORY_EMPTYtools/aidlc-runtime.tsStage approval's runtime-graph compile found memory.md missing or with zero non-blank entries under §13's four headings
RULE_LEARNEDtools/aidlc-learnings.tsThe learning gate persisted a kept learning as a dated practice entry to aidlc/spaces/<active-space>/memory/{project,team}.md
SENSOR_PROPOSEDtools/aidlc-learnings.tsThe learning gate scaffolded a project-tier sensor manifest and bound it to the originating stage's sensors: frontmatter

Swarm

The swarm taxonomy has seven events. Six emit from the stateless referee aidlc-swarm.ts: prepare captures the exact stage-attempt token, stamps it into worktree creation metadata, and forks the batch; finalize requires that token to remain current, re-verifies every claimed Unit, snapshots its exact declared record artifacts plus bound source manifest, merges those records and AIDLC metadata, and emits convergence/failure, baton, and batch rows. SWARM_SOURCE_MERGED emits later from aidlc-worktree.ts merge, after the immutable reviewed application source lands in main. It correlates durable worktree provenance with the exact current Bolt, batch, stage, and run floor, then links the main checkout from the stage baseline, the prior attempt's accepted rejection fingerprint, or the previous current-attempt aggregate. Selector-free merges recover the creating intent from durable creation authority; explicit mismatches name the required --space/--intent recovery, and authority-path comparison canonicalizes filesystem aliases. Pre-binding fieldless convergence retains historical branch-merge behavior; modern convergence does not advance routing until its source-merge authority exists. The check subcommand remains advisory and emits nothing. The conductor handles invoke-swarm as an orthogonal directive kind beside the stage mode enum; it does not activate the reserved agent-team mode.

EventEmitterTrigger
SWARM_STARTEDtools/aidlc-swarm.tsSwarm referee prepare captured the exact attempt, the full attempt-bound Unit obligation set, and forked one batch of dependency-linked Units
SWARM_UNIT_CONVERGEDtools/aidlc-swarm.tsA swarm Unit re-verified green and untampered, copied its exact declared record artifacts plus bound source-manifest.json into the main record, and merged its AIDLC metadata back. Unless the row explicitly carries Source Freshness Bypass: true, finalize also verified the configured post-Bolt reviewer receipt, current Source Fingerprint and Unit Source Fingerprint, and the attested raw-aware base-to-worktree footprint against reviewed manifest claims before recording the immutable Source Commit. A bypass row omits those freshness guarantees and requires AIDLC_SKIP_SOURCE_FRESHNESS=1 again at source merge.
SWARM_SOURCE_MERGEDtools/aidlc-worktree.tsThe exact current-attempt immutable reviewed source landed in the creating repository and extended the aggregate source fingerprint chain. The row carries that immutable Source Commit plus the portable Repo selector; settled completion requires it to match the Unit's latest convergence, requires one row per converged Unit, and verifies the final main checkout.
SWARM_UNIT_FAILEDtools/aidlc-swarm.tsA swarm Unit failed the finalize re-verify (not claimed, claimed-but-red, tampered, or missing its configured reviewer receipt)
SWARM_BATON_RETURNEDtools/aidlc-swarm.tsA swarm Unit returned the baton to the conductor for orchestrator-mediated coordination
SWARM_COMPLETEDtools/aidlc-swarm.tsAll Units in the batch finished (converged or failed); batch closed
SWARM_DEGRADEDtools/aidlc-swarm.tsAIDLC_USE_SWARM=1 was requested but the Workflow tool was unavailable; the conductor ran the subagent floor

Commit provenance

One enrichment event. aidlc attest anchor records that a commit was observed to land reviewed source claims — one row per involved intent per (commit, repo), deduplicated on re-anchor and skipped when a SWARM_SOURCE_MERGED receipt already binds the same (commit, repo). aidlc attest resolve never reads these rows: attribution is a pure function of committed content, so an unanchored commit resolves identically. See the commit provenance chapter.

EventEmitterTrigger
SOURCE_COMMITTEDtools/aidlc-attest.ts (runAnchor — the explicit anchor verb, or the opt-in session-start sweep under AIDLC_SESSION_ANCHOR=1)A commit's changed paths were attributed to reviewed units by an anchor invocation (or by the opt-in session-start sweep)

Every event in the taxonomy is either backed by a real emitter or marked Reserved (v0.4.0 PR N) / Reserved (v0.5.0 PR N) / Reserved (v0.6.0 PR N) for a pre-registered upcoming consumer. The drift test enforces both halves — the Reserved early-skip applies only while the cell literally contains "Reserved"; consumer PRs replace it with the real emitter file path in the same commit they ship the emit call.


Audit-first atomicity

State-mutating commands emit their audit entries before mutating the state file — with two documented exceptions: the audit-of-intent group below (audit first, side-effect second, for outcomes that cannot be checked before emission) and the DocumentKB catalog events (audit last — see "Audit-last for derived catalogs"). Two consequences:

  1. If audit emission fails (lock timeout, disk error, invalid event type), the tool throws before touching state. The state stays at its previous value; audit.md stays clean.
  2. If state writing fails after audit emission, the audit has an "intent" entry but the state didn't move. The drift is visible and diagnosable; --doctor surfaces it.

The case test("65: approve is audit-first ...") in tests/unit/t17.test.ts proves this for approve: chmod'ing audit.md to read-only forces an audit failure and asserts the state file stays at [?] (not [x]). The same invariant holds for gate-start, reject, revise, skip, advance, complete-workflow, reuse-artifact, aidlc-bolt.ts set-autonomy, and aidlc-state.ts fork / aidlc-state.ts merge (the v0.4.0 milestone 9 state fork/merge subcommands — see tests/unit/t76.test.ts for the equivalent chmod-the-lock-dir Part A and chmod-the-target-after-emit Part B proofs).

State fork/merge are deliberately NOT in the audit-of-intent exception below: re-reading and re-writing a state file is idempotent (unlike git worktree add, which leaves the worktree present after a kill-9 between emit and git), so the strict invariant applies cleanly. A failed state write after a successful audit emit becomes a phantom STATE_FORKED row that doctor (v0.4.0 milestone 15) reconciles against the worktree's record-dir aidlc-state.md existence.

Audit-last for derived catalogs (DOCUMENT_INDEXED, DOCUMENT_UPDATED, DOCUMENT_REMOVED)

The DocumentKB events invert the ordering: aidlc-knowledge.ts collects them during a commit and emits them only after index.json, every metadata.json, and every content.md write has succeeded. This is the one place in the framework where audit follows state, and it is a deliberate consequence of the catalog being derived.

Workflow state is authoritative — nothing can rebuild aidlc-state.md, so an audit row recorded ahead of a failed write leaves a phantom entry that --doctor can reconcile against the state file, and that diagnosable drift is the better trade. The DocumentKB catalog is the opposite: it is reconstructible from disk, because sync rebuilds a lost index.json from the surviving per-document metadata.json records, tombstones included. So the two failure modes are not symmetric here:

  • Audit before state (rejected): a DOCUMENT_UPDATED row asserting a revision the catalog never took. Every later reader of the ledger — --doctor, an export, an agent citing provenance — is misled by a change that did not happen, and no rebuild removes the false row.
  • Audit after state (chosen): a committed catalog change with no ledger row. The catalog itself remains authoritative; an idempotent retry rewrites any missing derived metadata and emits a repair row that describes the already-committed source/digest/scope. The missing row is therefore recoverable without inventing a state transition that did not occur.

A missing entry understates what happened; a phantom entry asserts something untrue. For a derived artifact that can be rebuilt, understating is the safer failure. The same reasoning does not extend to any authoritative state file, which is why this exception is scoped to these three events and not generalised.

Audit-of-intent semantics (WORKTREE_*, AUDIT_*, and merge-dispatch MERGE_DISPATCH_INVOKED)

Audit-of-intent semantics apply to side-effects whose outcome cannot be checked before emission — including disk operations (worktree creation / removal, audit byte-copy) and LLM Task dispatch (aidlc-pipeline-deploy-agent). The emitting tool writes the audit entry first, then performs the side-effect. If the side-effect fails after the emit, the tool calls emitError with the slug embedded in the message ([slug=<slug>]); the audit-fork / audit-merge handlers additionally tag failures with [fork-emitted:<timestamp>] so --doctor (v0.4.0 milestone 15) can distinguish "intent recorded, side-effect never landed" from earlier failure modes. For MERGE_DISPATCH_INVOKED, doctor reconciliation matches orphan INVOKED rows to a missing MERGE_DISPATCH_RETURNED or MERGE_DISPATCH_FALLBACK partner via slug + timestamp window (no correlation tag needed because the LLM Task call has no disk artifact to sequence against). appendAuditEntry records an ERROR_LOGGED entry on disk-side-effect failure; doctor reconciles audit drift at observation time.

Event groupEmitterSide-effect that follows the emit
WORKTREE_CREATED, WORKTREE_MERGED, WORKTREE_DISCARDEDtools/aidlc-worktree.tsgit worktree add, git merge + cleanup, git worktree remove + branch delete
AUDIT_FORKED, AUDIT_MERGEDtools/aidlc-audit.tsmkdir -p + copyFileSync of main audit; appendFileSync of worktree-audit delta to main audit
MERGE_DISPATCH_INVOKEDtools/aidlc-bolt.ts dispatch-eventTask(aidlc-pipeline-deploy-agent, ...) LLM dispatch — the side-effect is the LLM call itself; success is observed via the matching MERGE_DISPATCH_RETURNED or MERGE_DISPATCH_FALLBACK post-call emit

This is a deliberate departure from the strict audit-first invariant for stage transitions, motivated by the kill-9 / OS-crash window where neither the rollback emit nor ERROR_LOGGED can be guaranteed. The pattern is bounded to the events listed above. STATE_FORKED / STATE_MERGED (milestone 9) deliberately do NOT take this exception — see the previous section for the strict-first rationale (state writes are idempotent, so a failed write surfaces as recoverable drift instead of unrecoverable orphan state). MERGE_DISPATCH_RETURNED / MERGE_DISPATCH_FALLBACK are post-call emits (audit-of-result, not intent — strict-first) and don't take the exception. All other state-mutating commands stay strict-first per the section above.

SWARM_SOURCE_MERGED is post-result authority, not an audit-of-intent row. If the Git merge commit lands but this row cannot be appended, the tool preserves the worktree and returns [merge-succeeded:<sha>] with a non-retryable remedy. Rerunning would merge source twice and is forbidden; restart the stage attempt, or use AIDLC_SKIP_SOURCE_FRESHNESS=1 only after explicit human approval. If the row did land and only later cleanup failed, rerunning the same aidlc-worktree merge detects that authority and performs cleanup only; it never reapplies source or emits a second authority row.

Authority invariants

Two rules govern every artifact the engine reads as authorization: Plan Approval challenges, responses and receipts, reviewer receipts, gate and Unit lifecycle receipts, and the active-directive marker.

A query never writes, and a guard never deletes evidence. next, the Stop hook's next probe, the unit start route check, /aidlc --status, --doctor and team-board are queries. The two engine observers (the Stop probe and the route check) write nothing at all, and a typed barrier at the durable write primitives makes an observer that reaches one fail loudly rather than corrupt authority. A guard's only move is to refuse: it does not clear a receipt, a challenge, or a marker to express a refusal. Only an explicit human decision (Request Changes) withdraws a recorded decision, and only the owning tool retires one.

Authority binds to content and attempt, never to issuance identity or event order. A receipt is valid when its target, its content fingerprint, and its stage attempt match the live state. It does not matter which directive presented the question, how many times that directive was re-issued, what revision the marker was on, or which audit shard was written first. Two counter-examples this chapter used to describe, both removed: rotating an authority epoch when a directive was re-issued, so re-asking the engine what to do invalidated an approval nobody had changed; and clearing the Plan Approval runtime directory from a publication path, so a query destroyed the evidence it came to read.

A field the engine ignores when comparing state is a deliberate classification, not an oversight: see the cache-layer projection the active directive binds to, and the contributor question in 11-contributing.md.

Guard admission and recovery asks

A guard that refuses returns a typed refusal, not a sentence the conductor has to interpret: the code, the blocked action, the invariant it protects, one sentence for the human, and the remedies that are executable from the current lifecycle state (in-progress, awaiting-approval, revising, completed, pending, skipped; for a team Unit, the Unit's own gate status). Every remedy carries a closed op from GUARD_REMEDY_OPS in aidlc-lib.ts (present-approval-gate, request-review, start-recovery-review, apply-repairs-then-request, record-verdict, retry-pending, request-changes, redo-jump, restore-or-jump, restart-stage, change-scope, restore-scope, abort-bolt, repair-source-boundary, reconfirm-summary, unset-unattended). Routing decisions compare op and never the remedy sentence; the directive contract refuses an unknown op.

One predicate per guard, shared. The chain of guards for a lifecycle action is listed once (admitStageAction in aidlc-state.ts) and called by both the enforcing handler and the router: report and next run it on the same state snapshot before spawning the state tool, so the router and the tool cannot disagree about a refusal. The attempt as a guard sees it (budget, the single recovery slot, the pending review, and whether summary, review, and source evidence still cover the current bytes) is built in one place, guardAttemptState, from the shared attempt reducer.

One rule for first occurrence, at both sites. A refusal renders as a guard-recovery ask the first time it happens. The router emits it as the directive; an enforcing tool prints the human sentence and then the same ask as the last line of its refusal, which the router parses back into the directive it would have emitted itself. The .aidlc-guard-refusals/ record beside the other gitignored runtime files counts repetitions of one guard state (stage, Unit, lifecycle state, attempt fields, the latest session/workflow/jump/rejection boundary, and the resource fingerprints); it carries no authority, and an observer reads it without writing. A refusal with no executable remedy is still a question: a terminal ask with an empty remedy list that names the situation, and past the repetition cap the guard-state signature for escalation. Nothing here ever emits an error directive or counts silently.

The human's selection survives the re-ask. A guard-recovery ask is published as an active-directive marker (kind: "ask", ask_type: "guard-recovery"). The marker carries remedies, the offered op/action entries in display order. The human-turn hook records the human's remedy selection on it (delivery: consumed, guard_recovery_response.status: awaiting-feedback) together with the response's selection_sha256 and selected_op; selected_op is null when the selection is unmatched or ambiguous. Their later feedback changes the response to status: ready. A repeated next that derives the same ask for an unchanged state, gate, and ordered remedy op/action entries returns the ask without rewriting the marker, and the Stop hook releases the turn on the ask, so the conductor is never told to re-present a question the human already answered. reject is allowed only after selected_op records request-changes and matching human feedback arrives: --feedback must be that human's own words, compared whitespace-normalized. A paraphrase is refused, and a selection alone is refused with "ask what should change". Legacy consumed responses without selected_op are refused rather than treated as authorization. The ask names the blocked target, so it may carry a Unit; the reject names the gate the report path allows, which is --unit <name> under Unit Ownership: team and the stage alone under solo ownership (where --unit is refused). The binding follows that same rule: team compares stage and Unit, solo compares the stage. The gate's "Request Changes" choice is matched tolerant of case, an option prefix, quotes, and trailing punctuation; Plan Approval keeps its exact labels because those are the anti-forgery binding.

Forbidden patterns

Don't emit audit events from LLM prose. The following anti-patterns are the reason this refactor exists:

  • bun .claude/tools/aidlc-audit.ts append WORKFLOW_STARTED ... as a step in SKILL.md — replaced by the tool emitting it internally
  • **Event**: STAGE_COMPLETED markdown block written by a stage file — events only come from appendAuditEntry in a tool or hook
  • Freeform ## Artifact Update sections written by hooks — replaced by canonical ARTIFACT_CREATED / ARTIFACT_UPDATED

The public CLI enforces the sharpest slice of this mechanically: append / append-batch refuse the authority-bearing receipts the engine's guards read as authorization evidence (STAGE_COMPLETED, HUMAN_TURN, GATE_APPROVED, GATE_REJECTED, QUESTION_ANSWERED, PLAN_APPROVAL_RECORDED, REVIEW_REQUESTED, REVIEW_COMPLETED, PIPELINE_LINK_COMPLETED, ARTIFACT_REUSED, SWARM_STARTED, SWARM_UNIT_CONVERGED, AUTONOMY_MODE_SET, UNIT_OWNERSHIP_SET, UNIT_GATE_RHYTHM_SET, UNIT_STARTED, UNIT_PAUSED, UNIT_RESUMED, UNIT_COMPLETED, UNIT_MERGED, the three DOCUMENT_* provenance events, and the commit-provenance anchor SOURCE_COMMITTED — the CLI_PROTECTED_EVENT_TYPES set in aidlc-audit.ts), every field name must match a strict printable single-line label grammar (and Event remains reserved), values have line terminators escaped, and append-raw refuses taxonomy event lines or line-breaking headings. The structured renderer exclusively owns Timestamp and Event, so every block it writes contains exactly one of each; free-form append-raw blocks sit outside that guarantee (they carry the emitter's **Timestamp**: line, no **Event**: line, and a verbatim body). Timestamp remains accepted by generic --field parsing for compatibility, but a supplied value is intentionally ignored; park/unpark and other owning tools do not pass it. Historical shards are not rewritten: block-aware readers need no migration, while flat readers must split on --- and use the first emitter-owned timestamp in each block or deduplicate older duplicate timestamp fields. Owning tools and hooks emit through the library import (appendAuditEntry), which the floor does not touch. Test fixtures that simulate owning emitters set AIDLC_ALLOW_DIRECT_AUDIT_EVENTS=1.

The drift test at tests/integration/t48-audit-event-emitters.test.ts catches drift between this chapter's tables and the code: every event in the tables must have a matching appendAuditEntry(..., "EVENT", ...) call in the declared emitter file, and every emission call site in the codebase must appear in the tables. The test also guards against deleted events being resurrected and against pairing invariants (e.g., handleApprove must emit both GATE_APPROVED and STAGE_COMPLETED).


Same-commit rule

When you change state-machine behavior, update both the code and this chapter in the same commit. The rule catches itself via the drift test, but the cost of fixing drift after the fact (chasing down who owns which event across three files) is much higher than updating one table.

Specifically:

  • Adding an event → add to VALID_EVENT_TYPES in aidlc-audit.ts, add the emitter, add to the appropriate table above.
  • Deleting an event → remove from VALID_EVENT_TYPES, remove emitter, remove the row here, grep the codebase for any stale prose or tests.
  • Renaming an emitter file → update the Emitter column in every table row that points at it.
  • Adding an input to a fingerprint, an epoch, or a receipt identity → name the human-visible change that input detects, in the table row here and in the Authority Policy checklist in 11-contributing.md. An input no human action changes does not belong in an identity.

Known limitations

  • Multi-project sessions. Claude Code doesn't fire a hook on cd within a session, so if a user runs /aidlc in project A and then cds to project B, the session hooks won't re-fire against B's audit.md. Session events may not perfectly reflect every workspace switch. This is a Claude Code limitation, not an AI-DLC design flaw.

  • Orchestrator — how /aidlc --status, session check, and the resume path consume state-machine signals.
  • Stage Protocol — the stage-level behavioral contract, including the approval-gate UX that drives [?] / [R] transitions.
  • Hooks and Tools — hook lifecycle, CLI tool reference, and the audit-event catalog.
  • Testing — how the drift test works and when to run it.

Stage result validity projection

A completed checkbox records execution history. It does not prove that the result still matches the runtime artifact instances captured at completion. Execution state and result validity are therefore separate concepts.

Each main-workflow STAGE_COMPLETED event may carry a schema-3 Validation Basis. Runtime resolution remains concrete and instance-aware: the active Bolt DAG expands per-unit artifacts, produces_kinds filters unit kinds, and the artifact-vocabulary filename mapping resolves collision-safe names such as build-test-results -> test-results.md.

The audit receipt remains compact because the current projection is stage-level, not Unit-level. For each receipt-recorded canonical artifact it records the producer, required flag, instance/present counts, a structure hash over resolved path/unit/kind tuples, and a content hash over the corresponding file states.

Before normal next routing, the orchestrator recomputes each tracked basis. A mismatch projects the completed stage as stale. Downstream propagation uses artifact inputs actually recorded by completed consumers, not every possible static optional consumes declaration. A missing optional input therefore creates no edge; if it later appears, the consumer's own aggregate basis changes and becomes stale.

The basis is captured when STAGE_COMPLETED is reported. It does not prove which bytes the stage read while executing. "Observed dependency" means an input recorded in the completion receipt; changes before capture become the baseline, while later changes can be detected.

Schema-2 and earlier receipts remain untracked until normal re-completion. Schema 2 cannot distinguish its former zero-instance resolution from the stage-level zero-Unit resolution, so treating it as advisory avoids reporting unchanged in-flight workflows as stale after an upgrade.

requires_stage is not treated as an invalidation edge because the current v2 schema uses it for both semantic dependency and ordering. An explicit edge kind would be required before it can safely participate in validity propagation.

The projection remains read-only and advisory. next keeps its normal directive kind and adds a machine-readable stage_validity field for stale, revalidation, or unavailable results. Untracked-only histories appear in /aidlc --status rather than every next. The suggested recovery is /aidlc --stage <earliest-affected-stage>, but this release does not enforce it. Schema-1, receipt-less, and capture-failed histories remain untracked/fail-open until a normal re-completion writes schema 2. The scope is AI-DLC Markdown artifact validity; source-code, Git-tree, CI, deployment, and external-system validity require separate ownership and observation contracts.

Receipt lookup starts at the latest WORKFLOW_STARTED event in the selected intent audit. Historical ledgers from releases that supported forced re-init may contain a new boundary from a forced re-init in those releases; completions before that boundary read as untracked and fail open until their stages complete again.

Artifact resolution follows the approved workflow plan for per-Unit stages. When Units Generation is skipped, including express and recomposed zero-Unit plans, validity resolves one stage-level artifact instance under <record>/construction/<stage>/ and does not inspect a Bolt DAG or stale per-Unit directories. When Units Generation executes, normal Bolt DAG expansion and the legacy no-DAG directory fallback remain unchanged. Missing or ambiguous plan state makes receipt capture or inspection unavailable with a non-blocking warning rather than reporting false drift.