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 matches
  • grep -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
                                                 ↘ CANCELLED

Every 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 run audit_session_start → work → audit_verify_chainthought_recordmerkle_finalizemerkle_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. ANALYZE cannot transition straight to APPLY; the packet must exist.
  • Verification is not optional. APPLY always transitions to VERIFY, even if APPLY failed — verification records the failure.
  • Cancel is terminal and recorded. A cancelled task still writes a thought_record describing the reason.
  • Retries re-enter GATHER. A VERIFY failure that the engine classifies as transient re-enters GATHER with the attempt counter bumped; a permanent failure goes to CANCELLED with reason verify_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 — “Only todo tasks are executable”) while data/ams.db remains 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 no src/gsd/ directory and no donor RETRY edge in Phase 0 — failure handling is via CANCELLED and 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_STATES tuple + TaskState derived union.
  • Pure, side-effect-free. detectMode(env) and capabilitiesFor(mode) have no eager side-effects, no process.*, 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.3 at the top.
  • Frozen singletons for static data. FULL_CAPS, READONLY_CAPS, etc. are module-scoped Object.freeze(...) records. The new module will freeze VALID_TRANSITIONS and TERMINAL_STATES similarly.
  • 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 on TaskState will 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.env mutation — 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 in config.test.ts lines 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 describe blocks by function name.
  • Error assertions test both the error type and the message content (e.g. toThrow(/Invalid COLIBRI_MODE/) plus toThrow(/full/)). The new test will assert InvalidTransitionError type + {from, to, taskId} message content.
  • 100% branch coverage verified in coverage/lcov-report/ per jest.config.ts collectCoverage: 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 at src/__tests__/*.test.ts, matching the packet deviation (spec said tests/domains/..., actual convention is src/__tests__/...).
  • collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/__tests__/**'] — any new src/domains/tasks/state-machine.ts is 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 .ts sources.

4b. .eslintrc.json

  • @typescript-eslint/no-explicit-any: warn — the state machine will not use any.
  • @typescript-eslint/consistent-type-imports: errortype-only imports must use import type. The new module has no type-only imports (it re-exports everything it defines), but the test file imports TaskState as 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 use any for 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.
  • noUncheckedIndexedAccess is relevant: VALID_TRANSITIONS[state] returns readonly TaskState[] | undefined under this flag. The implementation must handle undefined explicitly — either with a default (VALID_TRANSITIONS[state] ?? []) or with the compile-time guarantee that every TaskState has an entry (which exhaustive record typing provides).
  • ESM target — imports use .js extension per NodeNext resolution, even for TS sources.

4d. package.json scripts

  • npm run linteslint 'src/**/*.ts' — must pass.
  • npm testnode --experimental-vm-modules node_modules/jest/bin/jest.js — must pass with 100% branch coverage on state-machine.ts.
  • npm run buildtsc -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 in src/__tests__/ (except adding a new file alongside them).

Worktree scan:

  • src/startup.ts — does not exist in baseline 3ebbe419. 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

  1. noUncheckedIndexedAccess + record indexing. VALID_TRANSITIONS[from] returns readonly TaskState[] | undefined. The Record<TaskState, readonly TaskState[]> type narrows to readonly TaskState[] only if every TaskState is a key. Under noUncheckedIndexedAccess, 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.
  2. InvalidTransitionError.message format. 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).
  3. Type-level satisfies test. 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-time satisfies in the production file + runtime set-equality in the test).
  4. Immutability of task parameter. transition<T>(task: T, to: TaskState): T returns { ...task, state: to }. Spread is shallow. If the caller passes a task with a Date field 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.
  5. TypeScript satisfies vs as. The packet specifies as const on the tuple — that is a type assertion. satisfies is reserved for the compile-time test. No as outside as const.
  6. Agent-prompt file drift. docs/guides/implementation/task-prompts/p0.3-beta-task-pipeline.md still 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__/... vs tests/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).


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

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