P0.3.1 — Step 1 Audit
Inventory of the worktree against the task spec for P0.3.1 β Task Pipeline State Machine (first task in the β Task Pipeline group). Scope: canonical FSM spec reconstruction, adjacent code the new module must mirror, and what is absent.
Baseline: worktree E:/AMS/.worktrees/claude/p0-3-1-task-state-machine/ at commit 3ebbe419 (P0.2.2 SQLite init landed).
§1. Surface being added
Target: src/domains/tasks/state-machine.ts — a new module that does not yet exist. Companion test: src/__tests__/task-state-machine.test.ts (packet-level deviation from the spec’s tests/domains/tasks/state-machine.test.ts because Jest roots is ['<rootDir>/src']).
A worktree scan confirms absence:
ls src/domains/tasks/state-machine.ts→ “No such file or directory”ls src/domains/→ “No such file or directory”grep -rn "TaskState\|VALID_TRANSITIONS\|InvalidTransitionError\|canTransition" src/→ zero matchesgrep -rn "INIT.*GATHER\|state-machine" src/→ zero matches
This is a greenfield module; the task authors the full surface in a single PR. The entire src/domains/ directory tree will be created by this task (β is the first domain to land code).
§2. Canonical FSM spec — verbatim reconstruction
The single source of truth for the β FSM is docs/colibri-system.md §6.3. The full text of §6.3 (lines 194–217) reproduced verbatim:
6.3 Task level (hours)
A task is the unit executors operate on. Every task moves through the β Task Pipeline 7-state FSM:
INIT → GATHER → ANALYZE → PLAN → APPLY → VERIFY → DONE ↘ CANCELLEDEvery task executor runs the 5-step chain: audit → contract → packet → implement → verify. Every task has a writeback contract:
task_update(status=done, progress=100)+thought_record(...). Proof-grade tasks additionally runaudit_session_start→ work →audit_verify_chain→thought_record→merkle_finalize→merkle_root.
The colibri-system.md diagram shows a linear forward path plus a terminal cancel side-branch. It does not enumerate backward edges. The next layer down — docs/3-world/execution/task-pipeline.md — is the deeper spec that provides transition rules. Lines 35–40 of that spec state:
Transition rules
- No skipping.
ANALYZEcannot transition straight toAPPLY; the packet must exist.- Verification is not optional.
APPLYalways transitions toVERIFY, even if APPLY failed — verification records the failure.- Cancel is terminal and recorded. A cancelled task still writes a
thought_recorddescribing the reason.- Retries re-enter GATHER. A
VERIFYfailure that the engine classifies as transient re-entersGATHERwith the attempt counter bumped; a permanent failure goes toCANCELLEDwith reasonverify_permanent_fail.
And lines 22–23:
Eight states. Seven forward transitions plus one terminal cancel. There is no “PAUSED” and no “RETRY” as first-class states — retries re-enter GATHER with a new attempt counter.
State semantics table (task-pipeline.md lines 24–33) describes INIT (task accepted), GATHER (context collected), ANALYZE (problem framed), PLAN (packet written), APPLY (packet executed), VERIFY (acceptance criteria checked), DONE (writeback complete, terminal), CANCELLED (explicit abandon with reason, terminal).
Also cross-referenced: docs/5-time/task.md lines 49–58, which defines the same FSM and lays out the 1:1 mapping between the 5-step executor chain and the FSM’s forward states:
| 5-step chain stage | β state |
|---|---|
| 1. Audit | GATHER |
| 2. Contract | ANALYZE |
| 3. Packet | PLAN |
| 4. Implement | APPLY |
| 5. Verify | VERIFY |
INIT (task accepted) and DONE (writeback complete) bookend the chain. CANCELLED is the one off-ramp.
§2a. Derived transition map
From the three canonical sources above (colibri-system.md §6.3 · task-pipeline.md §”Transition rules” · task.md §”The β state machine”), the adjacency map is:
| From | To | Kind |
|---|---|---|
INIT |
GATHER |
Forward |
GATHER |
ANALYZE |
Forward |
ANALYZE |
PLAN |
Forward |
PLAN |
APPLY |
Forward |
APPLY |
VERIFY |
Forward (mandatory, even on APPLY failure) |
VERIFY |
DONE |
Forward (acceptance criteria pass) |
VERIFY |
GATHER |
Backward / retry (transient failure re-enters GATHER) |
INIT |
CANCELLED |
Cancel (reachable from every non-terminal state) |
GATHER |
CANCELLED |
Cancel |
ANALYZE |
CANCELLED |
Cancel |
PLAN |
CANCELLED |
Cancel |
APPLY |
CANCELLED |
Cancel |
VERIFY |
CANCELLED |
Cancel (including verify_permanent_fail) |
DONE |
— | Terminal. No outgoing edges. |
CANCELLED |
— | Terminal. No outgoing edges. |
Edge count: 13 (6 forward + 1 retry + 6 cancel).
§2b. Heritage note — donor kanban vocabulary
The AMS donor (pre-R53) used a kanban-style lifecycle: backlog | todo | in_progress | blocked | review | done | cancelled. That vocabulary is explicitly called out as heritage only by task-breakdown.md line 170:
Heritage note: The AMS donor task store used a kanban-style lifecycle (
backlog | todo | in_progress | blocked | review | done | cancelled). That vocabulary survives at the PM-facing level (see CLAUDE.md §5 — “Onlytodotasks are executable”) whiledata/ams.dbremains the task store during Phase 0 bootstrap. The β execution FSM inside Colibri is the canonical INIT..DONE pipeline above, not the donor lifecycle. Mapping between the two belongs to ν Integrations, not β.
The agent-prompt file docs/guides/implementation/task-prompts/p0.3-beta-task-pipeline.md lines 41–43 + 74 still reference the donor kanban states. This is stale relative to the R73 unification. The implementation follows colibri-system.md §6.3 (canonical), not the agent-prompt file. Resolution is recorded in the contract.
§2c. Donor RETRY / PAUSED states — explicitly excluded
docs/reference/extractions/beta-task-pipeline-extraction.md (the donor extraction) describes RETRY as a first-class state with edges VERIFY → RETRY, APPLY → RETRY, RETRY → GATHER. Line 12 of that file explicitly states:
Phase 0 Colibri β is an 8-state FSM (
INIT → GATHER → ANALYZE → PLAN → APPLY → VERIFY → DONE+CANCELLED) defined in../../concepts/β-task-pipeline.md. There is nosrc/gsd/directory and no donor RETRY edge in Phase 0 — failure handling is viaCANCELLEDand re-enqueue.
And task-pipeline.md line 22 confirms “no PAUSED and no RETRY as first-class states.”
Decision: no RETRY state, no PAUSED state. Transient VERIFY failure is modeled by the backward edge VERIFY → GATHER. Permanent VERIFY failure is modeled by VERIFY → CANCELLED.
§3. Adjacent code that the new module must integrate with
3a. src/modes.ts (186 lines — P0.4.1)
The packet explicitly names this as the pattern to mirror. Relevant idioms:
- Tuple + derived type pattern.
export const RUNTIME_MODES = ['FULL', 'READONLY', 'TEST', 'MINIMAL'] as const+export type RuntimeMode = (typeof RUNTIME_MODES)[number]. Keeps value list and type in lockstep. The new module uses the identical pattern:TASK_STATEStuple +TaskStatederived union. - Pure, side-effect-free.
detectMode(env)andcapabilitiesFor(mode)have no eager side-effects, noprocess.*, no logging. The new module is held to the same standard — every function is pure. - Top-of-file JSDoc block. Purpose + canonical doc reference. The new module will cite
docs/colibri-system.md §6.3at the top. - Frozen singletons for static data.
FULL_CAPS,READONLY_CAPS, etc. are module-scopedObject.freeze(...)records. The new module will freezeVALID_TRANSITIONSandTERMINAL_STATESsimilarly. - Exhaustive switch with
noImplicitReturns.capabilitiesFor’s switch is exhaustive over the closed union. The state machine does not use a switch (its lookup is a record), but any helper that branches onTaskStatewill follow the same exhaustiveness discipline. - No zod for small closed sets. modes.ts uses a hand-written type guard rather than a zod schema. The new module follows suit — the FSM surface is pure TypeScript plus a stdlib
Set.
3b. src/__tests__/modes.test.ts (277 lines — P0.4.1)
Establishes the test style for pure-factory modules:
- No
process.envmutation — env (for detectMode) passed explicitly per test. The state machine’s analogue is that task objects are passed explicitly; no shared state. - No
jest.isolateModulesAsync— the zod v3 locale-cache bug under ts-jest ESM is documented inconfig.test.tslines 10–14. The new module has no zod dependency, so this is a non-issue, but the no-isolate posture keeps the style uniform. - Grouped
describeblocks by function name. - Error assertions test both the error type and the message content (e.g.
toThrow(/Invalid COLIBRI_MODE/)plustoThrow(/full/)). The new test will assertInvalidTransitionErrortype +{from, to, taskId}message content. - 100% branch coverage verified in
coverage/lcov-report/perjest.config.tscollectCoverage: true.
3c. src/config.ts + src/__tests__/config.test.ts (P0.1.4)
Out of scope — config.ts is a runtime env reader with an eager config export and a pure loadConfig(env) factory. The state machine has no env dependency. Relevant only for style (frozen return types, Readonly<...> annotations).
3d. src/server.ts + src/db/* (P0.2.1, P0.2.2)
Explicitly not imported. The state machine is pure domain logic. It does not touch the database, does not emit events, does not log, does not call the MCP server. Callers (future P0.3.2 repository.ts, future P0.3.4 task_update tool) are responsible for persistence and event emission.
3e. src/__tests__/db-init.test.ts, src/__tests__/server.test.ts
Existing tests in src/__tests__/. The new test file (src/__tests__/task-state-machine.test.ts) must not import from them or interfere with their fixtures. Independence is trivial — the state machine has no shared state to pollute.
§4. Tooling posture
4a. jest.config.ts
roots: ['<rootDir>/src']— tests live atsrc/__tests__/*.test.ts, matching the packet deviation (spec saidtests/domains/..., actual convention issrc/__tests__/...).collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/__tests__/**']— any newsrc/domains/tasks/state-machine.tsis automatically included in coverage.ts-jest/presets/default-esm— ESM + TypeScript.moduleNameMapper: { '^(\\.{1,2}/.*)\\.js$': '$1' }— ESM import specifiers ('./foo.js') are rewritten to resolve.tssources.
4b. .eslintrc.json
@typescript-eslint/no-explicit-any: warn— the state machine will not useany.@typescript-eslint/consistent-type-imports: error—type-only imports must useimport type. The new module has no type-only imports (it re-exports everything it defines), but the test file importsTaskStateas a type.eqeqeq: always,curly: all— strict equality and always-braces. Trivial compliance.- Test-file override:
parserOptions.project: null,no-console: off,no-explicit-any: off. Tests may useanyfor exception catch parameters if needed; the state machine tests will not need this.
4c. tsconfig.json
strict: true,noImplicitAny: true,strictNullChecks: true,noImplicitReturns: true,noFallthroughCasesInSwitch: true,noUncheckedIndexedAccess: true,exactOptionalPropertyTypes: true.noUncheckedIndexedAccessis relevant:VALID_TRANSITIONS[state]returnsreadonly TaskState[] | undefinedunder this flag. The implementation must handleundefinedexplicitly — either with a default (VALID_TRANSITIONS[state] ?? []) or with the compile-time guarantee that everyTaskStatehas an entry (which exhaustive record typing provides).- ESM target — imports use
.jsextension per NodeNext resolution, even for TS sources.
4d. package.json scripts
npm run lint→eslint 'src/**/*.ts'— must pass.npm test→node --experimental-vm-modules node_modules/jest/bin/jest.js— must pass with 100% branch coverage onstate-machine.ts.npm run build→tsc -p tsconfig.json— must pass (type-checks production sources only, excludes tests).
§5. Acceptance criteria mapping
Every line from the task prompt’s “Acceptance criteria” block:
| # | Criterion | Audit note |
|---|---|---|
| 1 | All 8 states defined exactly as INIT, GATHER, ANALYZE, PLAN, APPLY, VERIFY, DONE, CANCELLED |
Matches colibri-system.md §6.3 verbatim |
| 2 | canTransition returns true for every valid edge |
13 valid edges per §2a |
| 3 | canTransition returns false for every invalid combination |
8×8 = 64 ordered pairs; 13 valid → 51 invalid (including self-loops). At least one per state tested. |
| 4 | transition returns a new object with updated state (original unchanged) |
Immutable — return { ...task, state: to } |
| 5 | transition throws InvalidTransitionError with {from, to, taskId} populated |
Error class exposes all three as readonly fields; message includes all three |
| 6 | Any transition from DONE throws |
DONE outgoing = ∅; every attempted edge from DONE throws |
| 7 | Any transition from CANCELLED throws |
CANCELLED outgoing = ∅; every attempted edge from CANCELLED throws |
| 8 | CANCELLED reachable from each of 6 non-terminal states |
6 assertions; all present in §2a |
| 9 | InvalidTransitionError.message includes all three of {from, to, taskId} |
Formatted message: e.g. "Invalid task transition for task <taskId>: <from> → <to>" |
| 10 | Type-level TaskState is exactly the 8-state union (compile-only satisfies) |
TASK_STATES satisfies readonly TaskState[] plus a const-array literal checked for length and content |
Coverage target: 100% stmt / 100% branch / 100% func / 100% line on src/domains/tasks/state-machine.ts.
§6. Parallel-lock compliance (non-negotiable)
The parallel-batch lock lists:
- OWN (allowed to create/edit):
src/domains/tasks/state-machine.ts,src/__tests__/task-state-machine.test.ts, four chain docs. - MUST NOT TOUCH:
package.json,package-lock.json, Jest/ESLint/tsconfig/CI configs,src/config.ts,src/modes.ts,src/server.ts,src/startup.ts,src/db/*,src/domains/skills/*,src/domains/trail/*, existing tests insrc/__tests__/(except adding a new file alongside them).
Worktree scan:
src/startup.ts— does not exist in baseline3ebbe419. P0.2.3 (in another parallel batch) creates it. Not touching.src/domains/skills/— does not exist. P0.6.1 (in another parallel batch) creates it. Not touching.src/domains/trail/— does not exist. P0.7.1 (in another parallel batch) creates it. Not touching.
The new module has zero imports from any of the above paths. Lock is satisfiable by construction.
§7. Risks / notes for the contract
noUncheckedIndexedAccess+ record indexing.VALID_TRANSITIONS[from]returnsreadonly TaskState[] | undefined. TheRecord<TaskState, readonly TaskState[]>type narrows toreadonly TaskState[]only if everyTaskStateis a key. UndernoUncheckedIndexedAccess, TS still treats the result as possibly undefined. Resolution: use.includes()on the result with a nullish-coalescing?? []to keep the logic tight and the compiler happy, or assert membership via the record’s exhaustive key literal. Packet will specify the exact pattern.InvalidTransitionError.messageformat. The acceptance criterion only requires{from, to, taskId}all appear in the message. Concrete format is a packet decision. Proposal:Invalid task transition for task <taskId>: <from> → <to>. Must be deterministic (tests match on substring).- Type-level
satisfiestest. Three possibilities:- Assertion at compile time only:
const _ = TASK_STATES satisfies readonly TaskState[]— silent if typecheck passes. - Runtime tuple-length assertion:
expect(TASK_STATES.length).toBe(8)— catches accidental additions/removals. - Runtime set-equality assertion: compare
new Set(TASK_STATES)to the hand-written expected set. Packet specifies all three are present (compile-timesatisfiesin the production file + runtime set-equality in the test).
- Assertion at compile time only:
- Immutability of
taskparameter.transition<T>(task: T, to: TaskState): Treturns{ ...task, state: to }. Spread is shallow. If the caller passes a task with aDatefield or nested object, those references are shared — that is accepted (the state machine does not own deep-copy semantics; callers who need deep freeze handle it themselves). The contract must state this explicitly. - TypeScript
satisfiesvsas. The packet specifiesas conston the tuple — that is a type assertion.satisfiesis reserved for the compile-time test. Noasoutsideas const. - Agent-prompt file drift.
docs/guides/implementation/task-prompts/p0.3-beta-task-pipeline.mdstill lists the donor kanban states. It is out of scope for this task (touching it violates the parallel lock even if it were desirable). Flag for a future doc PR; record in the verification writeback.
§8. Audit summary
- Greenfield module. Nothing to refactor, only to author.
- Canonical spec is unambiguous at the level of states (8 states). Transition map is derivable without ambiguity once the three canonical sources (colibri-system.md §6.3 · task-pipeline.md §”Transition rules” · task.md §”The β state machine”) are read together — the one retry edge is
VERIFY → GATHER. - One packet-level spec deviation (test path
src/__tests__/...vstests/domains/...) — already Sigma-approved. - Heritage prompt file drift flagged but out of scope.
- Parallel lock satisfied by construction — zero imports from locked paths.
- Coverage target 100% branch achievable — 13 valid + 51 invalid ordered pairs, finite and enumerable.
Proceed to Step 2 (Contract).