@dtlabs/ai-toolkit 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/bin/cli.js +851 -9
- package/docs/task-executor-bootstrap.md +195 -0
- package/docs/task-executor-cli.md +319 -0
- package/docs/workflow-control-plane.md +95 -0
- package/lib/agent-registry.js +1018 -0
- package/lib/execution-ledger.js +283 -7
- package/lib/task-executor/claude-process.js +563 -0
- package/lib/task-executor/git.js +1362 -0
- package/lib/task-executor/index.js +3729 -0
- package/lib/task-executor/ownership.js +590 -0
- package/lib/task-executor/plan.js +725 -0
- package/lib/task-executor/store.js +1164 -0
- package/lib/workflow-artifacts.js +102 -0
- package/lib/workflow-control.js +219 -0
- package/lib/workflow-runner.js +39 -0
- package/package.json +1 -1
- package/src/claude/agents/{concurrency-safety-assessment.md → gaia-concurrency-safety-assessment.md} +1 -1
- package/src/claude/agents/{define-feature.md → gaia-define-feature.md} +1 -1
- package/src/claude/agents/{dependency-injection-refactoring.md → gaia-dependency-injection-refactoring.md} +1 -1
- package/src/claude/agents/{dependency-supply-chain-security.md → gaia-dependency-supply-chain-security.md} +1 -1
- package/src/claude/agents/{developer-backend.md → gaia-developer-backend.md} +1 -1
- package/src/claude/agents/{developer-frontend.md → gaia-developer-frontend.md} +1 -1
- package/src/claude/agents/{developer-testing.md → gaia-developer-testing.md} +1 -1
- package/src/claude/agents/{domain-model-refactoring.md → gaia-domain-model-refactoring.md} +1 -1
- package/src/claude/agents/{generate-requirements.md → gaia-generate-requirements.md} +1 -1
- package/src/claude/agents/{generate-tech-spec.md → gaia-generate-tech-spec.md} +1 -1
- package/src/claude/agents/{generate-work-breakdown.md → gaia-generate-work-breakdown.md} +5 -5
- package/src/claude/agents/{generic-software-assessment.md → gaia-generic-software-assessment.md} +1 -1
- package/src/claude/agents/{god-class-decomposition.md → gaia-god-class-decomposition.md} +1 -1
- package/src/claude/agents/{init-agents-md.md → gaia-init-agents-md.md} +1 -1
- package/src/claude/agents/{install-toolkit.md → gaia-install-toolkit.md} +1 -1
- package/src/claude/agents/{intervention-documentation-standard.md → gaia-intervention-documentation-standard.md} +10 -10
- package/src/claude/agents/{layered-architecture-assessment.md → gaia-layered-architecture-assessment.md} +1 -1
- package/src/claude/agents/{review-solution.md → gaia-review-solution.md} +1 -1
- package/src/claude/agents/{security-hardening.md → gaia-security-hardening.md} +1 -1
- package/src/claude/agents/{validate-feature-docs.md → gaia-validate-feature-docs.md} +2 -2
- package/src/claude/agents/{validate-work-breakdown-semantic.md → gaia-validate-work-breakdown-semantic.md} +1 -1
- package/src/claude/commands/next-task.md +4 -4
- package/src/claude/skills/assess-codebase/SKILL.md +9 -103
- package/src/claude/skills/define-feature/SKILL.md +2 -2
- package/src/claude/skills/hi-gaia/SKILL.md +1 -1
- package/src/claude/skills/implement-feature/SKILL.md +131 -38
- package/src/claude/skills/init-agents/SKILL.md +2 -2
- package/src/claude/skills/install-toolkit/SKILL.md +2 -2
- package/src/claude/workflows/am-phase1.js +43 -294
- package/src/claude/workflows/am-phase2.js +10 -164
- package/src/claude/workflows/pm-phase1.js +45 -287
- package/src/claude/workflows/pm-phase2.js +51 -511
- package/src/claude/workflows/pm-phase3.js +5 -767
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Task Executor Bootstrap
|
|
2
|
+
|
|
3
|
+
> Companion to [`docs/task-executor-cli.md`](task-executor-cli.md), which documents the CLI
|
|
4
|
+
> contract (commands, flags, exit codes, JSON schema) in detail and is not repeated here. This
|
|
5
|
+
> document covers the **operational** side: how a real operator/coordinator starts a run after
|
|
6
|
+
> Gates 1/2, the Windows-only qualification that gates whether `start` will run at all, and the
|
|
7
|
+
> persistence caveats an operator must know about before trusting a run's state — all pulled
|
|
8
|
+
> from FTR-018's own approved artifacts and test findings, not restated from memory.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. The supervised delivery procedure after Gates 1/2
|
|
13
|
+
|
|
14
|
+
Once a feature's Requirements, Tech-Spec and Work Breakdown are approved (Gate 1 and Gate 2 both
|
|
15
|
+
recorded in `{PREFIX}-Approvals.md`), the `implement-feature` skill's **Step 6 — Launch the task
|
|
16
|
+
executor (Implementation Phase)** (`src/claude/skills/implement-feature/SKILL.md`) takes over.
|
|
17
|
+
|
|
18
|
+
The key behavioral change from the retired `pm-phase3` model, as stated in Step 6 itself:
|
|
19
|
+
|
|
20
|
+
- Implementation is **no longer dispatched as a subagent**. There is no `pm-phase3` workflow, no
|
|
21
|
+
Tier-1 dispatch guard, and no `pm-phase3:dispatch` ledger entry for this step.
|
|
22
|
+
- Once Gate 2 is approved, the skill's job is limited to **presenting or launching one explicit
|
|
23
|
+
`task-executor` start command** — the executor process itself then owns the repo, plans,
|
|
24
|
+
executes, checkpoints and resumes tasks deterministically, independent of the chat.
|
|
25
|
+
- **No subagent orchestrates the run, and no assistant or agent involvement is required for each
|
|
26
|
+
task transition once the run has started.** A single durable command launch is enough, and the
|
|
27
|
+
run may keep going, checkpointing and resuming, even after the launching conversation ends.
|
|
28
|
+
|
|
29
|
+
Concretely (Step 6a), the skill builds:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
ai-toolkit executor start \
|
|
33
|
+
--project . \
|
|
34
|
+
--feature <path-to-feature.md> \
|
|
35
|
+
--max-concurrency 1 \
|
|
36
|
+
--claude-path <resolved-claude-cli-path> \
|
|
37
|
+
--task-timeout-ms <task-timeout-ms, e.g. 900000> \
|
|
38
|
+
--agent-budget-usd <per-agent budget in USD, e.g. 5.00>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If the host's ordinary terminal capability can launch a durable external command, the skill
|
|
42
|
+
launches it directly so the process persists independently of the chat. If not, it shows the
|
|
43
|
+
exact command for the user to run themselves in a terminal — it does **not** fake a durable
|
|
44
|
+
launch by running it as a blocking in-chat command.
|
|
45
|
+
|
|
46
|
+
In both cases, the `run-id` printed by `start` must be captured — it is required for every
|
|
47
|
+
subsequent `status`, `diagnose`, `stop`, `reconcile`, `resume`, and `replan` call; there is no
|
|
48
|
+
implicit last-run selection.
|
|
49
|
+
|
|
50
|
+
**The run may outlive the launching conversation.** The skill explicitly does not block on the
|
|
51
|
+
run reaching a terminal state; it proceeds to its own Step 7 with whatever the latest status
|
|
52
|
+
reports at that point. To check on a run later — from the same conversation, a new one, or no
|
|
53
|
+
conversation at all — the operator uses the read-only `status` subcommand documented in
|
|
54
|
+
`docs/task-executor-cli.md`:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
ai-toolkit executor status --project <path> --run-id <uuid>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Note, per the CLI reference, that `status` (like `diagnose`) is currently a stub that always
|
|
61
|
+
throws `NOT_IMPLEMENTED` (exit 1) — the command surface and its intended read-only summary shape
|
|
62
|
+
(task status counts, run status, total cost from the ledger) are defined, but the real body has
|
|
63
|
+
not been built yet (attributed there to `US-06-TASK-BE-01`). Until it lands, checking on a
|
|
64
|
+
running or finished executor run means reading the ledger and per-run state directly, or using
|
|
65
|
+
`reconcile`/`resume`, both of which are fully implemented and report each task's classification.
|
|
66
|
+
|
|
67
|
+
For the full flag reference, exit-code table, and JSON result schemas of every subcommand
|
|
68
|
+
(`start`, `status`, `diagnose`, `stop`, `reconcile`, `resume`, `replan`), see
|
|
69
|
+
`docs/task-executor-cli.md` — this document intentionally does not restate that contract.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 2. Windows-only E-02 qualification
|
|
74
|
+
|
|
75
|
+
The executor's process-supervision guarantees — tree-kill of in-flight attempts, tag-based
|
|
76
|
+
worker-liveness scanning, and immediate-stop confirmation — are proven for **Windows only**. This
|
|
77
|
+
is FTR-018's Gate-1 binding constraint #3 (`FTR-018-Approvals.md`):
|
|
78
|
+
|
|
79
|
+
> **E-02 is Windows-only qualified.** Other platforms require dedicated proofs before their
|
|
80
|
+
> execution is enabled; no implicit cross-platform guarantee.
|
|
81
|
+
|
|
82
|
+
This is not a paper constraint: `execute()` in `lib/task-executor/index.js` checks
|
|
83
|
+
`process.platform !== 'win32'` as its **very first validation step** — before any other argument
|
|
84
|
+
validation, before deriving the execution root, before touching the plan snapshot, and before
|
|
85
|
+
acquiring the ownership lease — and throws `PLATFORM_NOT_QUALIFIED` (CLI exit code 7) on any
|
|
86
|
+
non-Windows platform (US-08-TASK-BE-05, behaviorally tested by US-08-TASK-TEST-02's
|
|
87
|
+
`platform-guard.test.js`, which asserts no agent dispatch and no worktree creation occur on a
|
|
88
|
+
simulated non-win32 platform).
|
|
89
|
+
|
|
90
|
+
In other words: there is **no implicit cross-platform guarantee, and no partial or best-effort
|
|
91
|
+
execution on other platforms**. On any platform other than Windows, `start` refuses to run at
|
|
92
|
+
all rather than degrading silently.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 3. Persistence caveats
|
|
97
|
+
|
|
98
|
+
These are the real, already-confirmed caveats and limitations discovered during this feature's
|
|
99
|
+
own delivery. Each one is cited to its source rather than paraphrased from memory.
|
|
100
|
+
|
|
101
|
+
### a. E-01 residual: worktree persistence not re-reproduced in the isolated proof run
|
|
102
|
+
|
|
103
|
+
Per `FTR-018-Approvals.md`, Gate 1 section (OQ-01a / E-01 sub-point), the 2026-09-28 binding
|
|
104
|
+
closure of the agent-provenance proof carries an explicit residual:
|
|
105
|
+
|
|
106
|
+
> **Residual:** persistence not re-reproduced in this run (Write permission-denied); proven
|
|
107
|
+
> separately by the original Test B.
|
|
108
|
+
|
|
109
|
+
That is: the isolated 2026-09-28 proof run that closed the *definition-binding* question (verified
|
|
110
|
+
agent-hash → loaded definition) did **not** itself re-demonstrate worktree persistence, because
|
|
111
|
+
the Write tool was permission-denied in that isolated cwd. Persistence is proven by a separate,
|
|
112
|
+
earlier proof (the original Test B referenced in the same evidence record), not by this run. An
|
|
113
|
+
operator relying only on the E-01 dossier for "the whole chain is proven end-to-end in one run"
|
|
114
|
+
should know that persistence and definition-binding were proven in two separate runs, not one.
|
|
115
|
+
|
|
116
|
+
### b. Windows filesystem-race flake on atomic write (operational, not a code defect)
|
|
117
|
+
|
|
118
|
+
`store.js`'s state persistence uses a temp-write-then-rename atomic-write protocol
|
|
119
|
+
(`_atomicWriteFileSync` in `lib/task-executor/store.js`: open a same-directory exclusive temp
|
|
120
|
+
file, write, `fsyncSync`, close, then `fs.renameSync(tmpPath, filePath)`). This is the same
|
|
121
|
+
atomic-write discipline the executor's own production state persistence (`writeState`) relies on.
|
|
122
|
+
|
|
123
|
+
**Operational note:** under heavy concurrent I/O load on Windows (e.g. many parallel Jest workers
|
|
124
|
+
writing state files simultaneously), this rename step can occasionally hit a transient
|
|
125
|
+
filesystem error (EPERM / rename race) that is environment-load-dependent rather than
|
|
126
|
+
deterministic — it has not reproduced when run in isolation, and is not a defect in the
|
|
127
|
+
atomic-write logic itself. Anyone operating or testing the executor under high Windows I/O
|
|
128
|
+
contention should expect this as a known environmental characteristic, not treat an occasional
|
|
129
|
+
transient failure of this shape as evidence of a code bug.
|
|
130
|
+
|
|
131
|
+
### c. `resume()`/`reconcile()` only inspect and report — no automatic continuation
|
|
132
|
+
|
|
133
|
+
Confirmed by US-07-TASK-TEST-01 (`tests/task-executor/sequential.test.js`, commit `25b2663`):
|
|
134
|
+
|
|
135
|
+
> Finding, confirmed and NOT worked around: resume()/reconcile() only inspect and report - there
|
|
136
|
+
> is currently no way to actually continue/finish an interrupted run (execute() always mints a
|
|
137
|
+
> fresh runId and resets all tasks to pending). This is a known, explicit architecture gap, not a
|
|
138
|
+
> bug - documented in the test file for any future work that wants to add real continuation.
|
|
139
|
+
|
|
140
|
+
**Operator implication:** "resuming" a run today means safely inspecting what has already been
|
|
141
|
+
durably completed and getting an evidence-based classification per task — it does **not** mean
|
|
142
|
+
the interrupted run will automatically finish its remaining tasks. `execute()` always starts a
|
|
143
|
+
brand-new run (a fresh `runId`, all tasks reset to pending); there is no run loop wired into
|
|
144
|
+
`resume()`.
|
|
145
|
+
|
|
146
|
+
### d. Dormant concurrency gap in `dispatchTaskAttempt` (must be resolved before N>1 wiring)
|
|
147
|
+
|
|
148
|
+
Confirmed by US-08-TASK-TEST-01 (`tests/task-executor/parallel.test.js`, commit `2f06883`),
|
|
149
|
+
Finding B:
|
|
150
|
+
|
|
151
|
+
> a real, currently-dormant concurrency gap: two concurrent dispatchTaskAttempt calls requesting
|
|
152
|
+
> the IDENTICAL, not-yet-existing attemptNumber for the same task both dispatch real subprocesses
|
|
153
|
+
> - neither is rejected, only one attempt record persists, and its processIdentity is
|
|
154
|
+
> byte-identical for both calls (derived only from runId/taskId/attemptNumber), so persisted state
|
|
155
|
+
> cannot even reveal two real processes ran. Currently unreachable via any real production entry
|
|
156
|
+
> point (execute()'s only caller dispatches one attempt at a time sequentially, and no N>1
|
|
157
|
+
> orchestrator exists), but flagged prominently for whoever eventually wires real N>1 dispatch.
|
|
158
|
+
|
|
159
|
+
**Operator implication:** this race cannot fire today because `execute()` is strictly
|
|
160
|
+
sequential (`maxConcurrency=1` only; see docs/task-executor-cli.md — any other value is
|
|
161
|
+
rejected as `UNSUPPORTED_CONCURRENCY`, never silently downgraded) and no orchestrator calls
|
|
162
|
+
`dispatchTaskAttempt` concurrently. It is a **constraint that must be resolved before any future
|
|
163
|
+
N>1 wiring work** lands, not something safe to rely on or ignore once that work begins.
|
|
164
|
+
|
|
165
|
+
### e. Verification receipts already on disk are not consulted on resume (efficiency gap)
|
|
166
|
+
|
|
167
|
+
Confirmed by US-09-TASK-TEST-01 (`tests/task-executor/fault-injection.test.js`, commit `e601e3d`),
|
|
168
|
+
Finding 1:
|
|
169
|
+
|
|
170
|
+
> there is no distinct durable state for "verification passed, review not yet dispatched" - a
|
|
171
|
+
> crash in that exact window leaves reconcile() unable to tell it apart from "verification never
|
|
172
|
+
> ran", so a resume may safely but redundantly re-run verification commands whose receipts are
|
|
173
|
+
> already sitting on disk. Not an AC-10 violation (nothing is ever wrongly marked complete) - a
|
|
174
|
+
> design/efficiency observation for a future decision, not fixed here.
|
|
175
|
+
|
|
176
|
+
**Operator implication:** a resume after a crash in that narrow window is always *safe* (nothing
|
|
177
|
+
is ever falsely marked complete), but may be redundant — it can re-run verification commands,
|
|
178
|
+
including potentially expensive ones, that already passed and whose receipts already exist on
|
|
179
|
+
disk.
|
|
180
|
+
|
|
181
|
+
### f. N>1 parallel-execution primitives exist and are tested, but are not wired into `execute()`
|
|
182
|
+
|
|
183
|
+
`createTaskWorktree`, `createSlotPool` (US-08-TASK-BE-01/02), `integrateAttempt`
|
|
184
|
+
(`lib/task-executor/git.js`, US-08-TASK-BE-03) and `registerIntegratedSHA`
|
|
185
|
+
(`lib/task-executor/store.js`, US-08-TASK-BE-04) are all real, already-implemented functions, and
|
|
186
|
+
US-08-TASK-TEST-01's `parallel.test.js` exercises them together end-to-end for N=2/3 scenarios.
|
|
187
|
+
But as that same commit's message states plainly:
|
|
188
|
+
|
|
189
|
+
> there is no real "execute() with maxConcurrency=N" path yet (execute() still rejects anything
|
|
190
|
+
> but N=1), so this proves the standalone primitives compose correctly rather than exercising a
|
|
191
|
+
> not-yet-existing orchestrator.
|
|
192
|
+
|
|
193
|
+
**Operator implication:** these primitives are proven to compose correctly in isolation, but
|
|
194
|
+
`execute()` remains N=1 / sequential only. Do not expect `--max-concurrency` values other than
|
|
195
|
+
`1` to do anything but fail fast with `UNSUPPORTED_CONCURRENCY` today.
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Task Executor CLI
|
|
2
|
+
|
|
3
|
+
> Documents the **real, currently-implemented** behavior of `ai-toolkit executor <subcommand>`
|
|
4
|
+
> (`bin/cli.js`'s `handleExecutorCommand`, wired to `lib/task-executor/index.js`), part of
|
|
5
|
+
> FTR-018 (deterministic task execution, checkpoints and resume).
|
|
6
|
+
>
|
|
7
|
+
> This file describes what the code actually does today, not the aspirational CLI contract in
|
|
8
|
+
> `FTR-018-Tech-Spec.md` section 10. Every discrepancy between the two is called out explicitly
|
|
9
|
+
> below rather than silently reconciled.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Invocation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
ai-toolkit executor <subcommand> [flags]
|
|
17
|
+
# or, inside this repo:
|
|
18
|
+
node bin/cli.js executor <subcommand> [flags]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`--project <path>` is accepted by every subcommand and defaults to `process.cwd()` when omitted.
|
|
22
|
+
All other flags are per-subcommand (see below). Any flag not explicitly recognized for a
|
|
23
|
+
subcommand is silently ignored — there is no generic flag-forwarding path, so nothing reaches a
|
|
24
|
+
filesystem path or lock primitive except the whitelisted flags read via `getFlag()`/`hasFlag()`.
|
|
25
|
+
|
|
26
|
+
**Discrepancy vs. Tech-Spec:** section 10 documents `--format json or text (default)` as a
|
|
27
|
+
common flag. The real CLI has no `--format` flag at all — `handleExecutorCommand` always emits
|
|
28
|
+
JSON (`JSON.stringify(result, null, 2)`) on success and always emits JSON on error. There is no
|
|
29
|
+
text-mode output in the current implementation.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
|
|
35
|
+
### `start`
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
ai-toolkit executor start --project <path> --feature <path> [--max-concurrency N] [--claude-path <path>] [--task-timeout-ms <ms>] [--agent-budget-usd <amount>]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
| Flag | Required | Default | Notes |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| `--project` | no | `process.cwd()` | Repo root; also resolves the common Git directory |
|
|
44
|
+
| `--feature` | **yes** | — | Path to the feature's `feature.md`; the CLI usage-errors (exit 2, no JSON) if omitted |
|
|
45
|
+
| `--max-concurrency` | no | `1` | Forwarded to `execute()` as a Number; only `1` is currently supported (see below) |
|
|
46
|
+
| `--claude-path` | no | — | Absolute path to the qualified Claude CLI executable |
|
|
47
|
+
| `--task-timeout-ms` | no | — | Forwarded as a Number |
|
|
48
|
+
| `--agent-budget-usd` | no | — | Forwarded as a Number; persisted into run config, not itself enforced |
|
|
49
|
+
|
|
50
|
+
Backs `lib/task-executor/index.js`'s `execute()`. Validates gates/config, acquires the
|
|
51
|
+
repo-wide execution lease for a brand-new run, builds an immutable plan snapshot from the
|
|
52
|
+
feature's own `{PREFIX}-Work-Breakdown.md/.csv`, and drives a sequential (slot count 1)
|
|
53
|
+
dispatch → verify → review → checkpoint loop to completion or block.
|
|
54
|
+
|
|
55
|
+
Only `maxConcurrency=1` is implemented — any other requested value is **rejected**
|
|
56
|
+
(`UNSUPPORTED_CONCURRENCY`), never silently downgraded. `execute()` also refuses to run at all
|
|
57
|
+
on any platform other than `win32` (see "Platform qualification" below), before touching any
|
|
58
|
+
other validation.
|
|
59
|
+
|
|
60
|
+
### `status`
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
ai-toolkit executor status --project <path> --run-id <uuid>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`--run-id` is required (usage error, exit 2, if omitted). Read-only summary of a run: task
|
|
67
|
+
status counts, current run status, and total cost from the ledger.
|
|
68
|
+
|
|
69
|
+
**Not implemented.** `status()` in `lib/task-executor/index.js` is currently a stub that always
|
|
70
|
+
throws `NOT_IMPLEMENTED` (attributed to US-06-TASK-BE-01, which has not built its real body
|
|
71
|
+
yet). Calling `ai-toolkit executor status` today always fails with exit code 1 and an error
|
|
72
|
+
JSON body — it does not return the summary shape documented in its own JSDoc until that task is
|
|
73
|
+
implemented.
|
|
74
|
+
|
|
75
|
+
### `diagnose`
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
ai-toolkit executor diagnose --project <path> --run-id <uuid>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`--run-id` is required. Intended as a read-only detailed evidence dump (per-task/attempt
|
|
82
|
+
evidence, ownership/lease state, repair suggestions).
|
|
83
|
+
|
|
84
|
+
**Not implemented.** Same situation as `status`: `diagnose()` is a stub that always throws
|
|
85
|
+
`NOT_IMPLEMENTED` (attributed to US-06-TASK-BE-02). Calling it today always exits 1 with an
|
|
86
|
+
error JSON body.
|
|
87
|
+
|
|
88
|
+
### `stop`
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
ai-toolkit executor stop --project <path> --run-id <uuid> [--mode graceful|immediate]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Flag | Required | Default | Notes |
|
|
95
|
+
|---|---|---|---|
|
|
96
|
+
| `--run-id` | **yes** | — | |
|
|
97
|
+
| `--mode` | no | `'graceful'` | Only `'graceful'` or `'immediate'` are accepted; anything else throws `STOP_VALIDATION_ERROR` |
|
|
98
|
+
|
|
99
|
+
Persists a stop request and acknowledges receipt only — it never itself claims the run has
|
|
100
|
+
stopped.
|
|
101
|
+
|
|
102
|
+
- `graceful` (default): persists the request; the already-running `execute()` loop is the only
|
|
103
|
+
thing that actually enforces the block, at its next natural iteration boundary.
|
|
104
|
+
- `immediate`: additionally attempts real termination of every in-flight attempt (stage
|
|
105
|
+
`dispatching`/`running` with a recorded process tag), via the same Windows-only tagged-process
|
|
106
|
+
tree-kill technique used elsewhere in this module, then polls for confirmed termination.
|
|
107
|
+
|
|
108
|
+
Idempotent: if the run's current status is already `stopping` or settled
|
|
109
|
+
(`completed`/`blocked`/`superseded`/`paused`), this call is a pure no-op and reports the
|
|
110
|
+
originally-recorded mode.
|
|
111
|
+
|
|
112
|
+
### `reconcile`
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
ai-toolkit executor reconcile --run-id <uuid> [--project <path>]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`--run-id` is required. `--project` resolves the execution root and the current-branch task ref
|
|
119
|
+
automatically (via `git rev-parse --abbrev-ref HEAD` in the given `--project`, or its default,
|
|
120
|
+
the current directory) — there is no separate flag for these; the CLI derives them itself.
|
|
121
|
+
|
|
122
|
+
Reconciles persisted state and evidence for a run and classifies every task's current attempt.
|
|
123
|
+
**Only repairs proven gaps — it never dispatches new work and never creates new integrations.**
|
|
124
|
+
The only two repairs it can apply are: registering a commit SHA when a checkpoint-prepared
|
|
125
|
+
attempt's tip exactly matches its own persisted checkpoint intent, and finalizing a task
|
|
126
|
+
checkpoint when a committed attempt's registered SHA is confirmed reachable but the task's
|
|
127
|
+
status was not yet advanced. Refuses to proceed (`COMPETING_COORDINATOR_LIVE` /
|
|
128
|
+
`COMPETING_COORDINATOR_LIVENESS_UNKNOWN`) if a different, live-or-unknown-liveness coordinator
|
|
129
|
+
holds the repo-wide execution lease.
|
|
130
|
+
|
|
131
|
+
### `resume`
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
ai-toolkit executor resume --run-id <uuid> [--project <path>]
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Same flags/derivation as `reconcile` (in fact the CLI dispatches both `reconcile` and `resume`
|
|
138
|
+
through the identical flag-parsing branch, selecting the target function by subcommand name).
|
|
139
|
+
Enforces the same "no live competing coordinator" precondition, then calls `reconcile()` to
|
|
140
|
+
apply its repairs, then reports each task's current status plus the next-safe-action
|
|
141
|
+
classification `reconcile()` already computed. Does **not** dispatch new work itself — there is
|
|
142
|
+
no run loop wired into `resume()` yet (the real dispatch loop lives in `execute()`); its scope
|
|
143
|
+
ends at "reconcile, then report what is safe to continue."
|
|
144
|
+
|
|
145
|
+
### `replan`
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
ai-toolkit executor replan --run-id <uuid> --feature <path> [--project <path>] [--task-mapping-json '<json>']
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
| Flag | Required | Default | Notes |
|
|
152
|
+
|---|---|---|---|
|
|
153
|
+
| `--run-id` | **yes** | — | The original run being replanned |
|
|
154
|
+
| `--feature` | **yes** | — | Path to the **successor** feature's `feature.md` (the approved successor plan) |
|
|
155
|
+
| `--project` | no | `process.cwd()` | Resolves execution root + current-branch task ref, same as `reconcile`/`resume` |
|
|
156
|
+
| `--task-mapping-json` | no | `'[]'` (empty array) | JSON array of `{oldTaskId, newTaskId}` objects |
|
|
157
|
+
|
|
158
|
+
**Discrepancy vs. Tech-Spec:** the table in section 10 lists only `--run-id`/`--feature` for
|
|
159
|
+
`replan`. The real `replan()` in `lib/task-executor/index.js` requires `args.taskMapping` to be
|
|
160
|
+
an array (throwing `DISPATCH_VALIDATION_ERROR` otherwise) and never invents one itself. The CLI
|
|
161
|
+
therefore added `--task-mapping-json` as a necessary extension beyond the literal table, using
|
|
162
|
+
the same `--*-json` structured-input convention the `ledger` subcommand already established
|
|
163
|
+
(`--metadata-json`). This is documented here as an intentional, flagged gap — not silently
|
|
164
|
+
papered over — per the implementing agent's own note at the flag's parse site in `bin/cli.js`.
|
|
165
|
+
Omitting the flag defaults to an empty mapping (a valid "no task carries forward" array), never
|
|
166
|
+
a rejection.
|
|
167
|
+
|
|
168
|
+
Grants **no approval itself** — the successor's Gate 2 approval must already exist on disk
|
|
169
|
+
(`## Gate 2` heading in its `Approvals.md`, produced entirely outside the executor by the normal
|
|
170
|
+
`implement-feature` gate protocol). Does not dispatch, start, or touch the successor run in any
|
|
171
|
+
way. Validates the caller-supplied old→new task mapping against both the original run's recorded
|
|
172
|
+
tasks and the successor's plan snapshot, computes carry-completion eligibility per pair
|
|
173
|
+
(read-only), refuses if any original-run attempt's worker liveness is not confirmed dead
|
|
174
|
+
(`LIVE_WORKER_BLOCKS_REPLAN`), then records the mapping as a durable intent and marks the
|
|
175
|
+
original run `superseded`. Never deletes or rewrites any task/attempt evidence.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Exit codes
|
|
180
|
+
|
|
181
|
+
| Exit code | Meaning |
|
|
182
|
+
|---|---|
|
|
183
|
+
| 0 | Successful operation/request |
|
|
184
|
+
| 1 | Unexpected I/O (also the default for any unmapped/unknown error code) |
|
|
185
|
+
| 2 | Invalid config/input |
|
|
186
|
+
| 3 | Ownership/live worker conflict |
|
|
187
|
+
| 4 | Evidence/plan/approval mismatch |
|
|
188
|
+
| 5 | Worker/verification/review failure |
|
|
189
|
+
| 6 | Integration conflict |
|
|
190
|
+
| 7 | Runtime not qualified |
|
|
191
|
+
| 8 | Paused awaiting human decision |
|
|
192
|
+
|
|
193
|
+
This table matches `bin/cli.js`'s `EXECUTOR_EXIT_CODE_BY_ERROR_CODE` mapping (plus a
|
|
194
|
+
suffix-based catch-all: any error code ending in `_VALIDATION_ERROR` maps to exit 2). A few
|
|
195
|
+
notable groupings from that mapping:
|
|
196
|
+
|
|
197
|
+
- **3** — every lease/ownership code: `LEASE_HELD`, `LEASE_NOT_OWNER`, `LEASE_STILL_LIVE`,
|
|
198
|
+
`LEASE_LIVENESS_UNKNOWN`, `LEASE_NOT_FOUND`, `GUARD_HELD`, `NO_LOCK_HELD`,
|
|
199
|
+
`COMPETING_COORDINATOR_LIVE`, `COMPETING_COORDINATOR_LIVENESS_UNKNOWN`,
|
|
200
|
+
`LIVE_WORKER_BLOCKS_REPLAN`.
|
|
201
|
+
- **4** — every "plan/evidence/approval doesn't match" code, including
|
|
202
|
+
`SUCCESSOR_NOT_APPROVED`, `PLAN_NOT_FOUND`, `PLAN_PARSE_ERROR`, `REPLAN_UNKNOWN_TASK_MAPPING`,
|
|
203
|
+
`STATE_NOT_FOUND`/`STATE_TASK_NOT_FOUND`/`STATE_ATTEMPT_NOT_FOUND`, `TREE_MISMATCH`,
|
|
204
|
+
`RECEIPT_CONFLICT`, `INTENT_CONFLICT`, and many more (see the source comment for the full
|
|
205
|
+
list).
|
|
206
|
+
- **5** — `CLAUDE_SPAWN_FAILED`, `AGENT_NOT_VERIFIED`, `RESOLUTION_FAILED`.
|
|
207
|
+
- **6** — `INTEGRATION_CONFLICT`.
|
|
208
|
+
- **7** — `PLATFORM_NOT_QUALIFIED` (see "Platform qualification" below).
|
|
209
|
+
- **1 (default)** — includes `NOT_IMPLEMENTED` explicitly, so `status`/`diagnose` calls exit 1
|
|
210
|
+
today, not some special code.
|
|
211
|
+
|
|
212
|
+
**Exit code 8 is a success-path outcome, never a thrown error** — unlike every other code
|
|
213
|
+
above, which is assigned only on the error path via `mapExecutorErrorToExitCode`. Per the
|
|
214
|
+
Tech-Spec, 8 means "paused awaiting human decision": `execute()`'s and `resume()`'s result
|
|
215
|
+
objects can both legitimately carry `runStatus: 'paused'` (set when a `stop()` request — either
|
|
216
|
+
mode — was observed at a loop boundary; `resume()` simply passes through whatever `runStatus`
|
|
217
|
+
was last persisted, including `'paused'`). `handleExecutorCommand`'s shared `emitSuccess()`
|
|
218
|
+
inspects `result.runStatus` and sets exit code **8** when it is `'paused'`, else **0** — this is
|
|
219
|
+
a no-op check for `reconcile`/`stop`/`replan` (their result shapes never carry a `runStatus`
|
|
220
|
+
field at all) and never applies to `status`/`diagnose` (still `NOT_IMPLEMENTED`, so they never
|
|
221
|
+
reach the success path).
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## JSON result schema
|
|
226
|
+
|
|
227
|
+
### Success (stdout, exit matches the table above)
|
|
228
|
+
|
|
229
|
+
Every command that has a real implementation returns a **versioned result object** —
|
|
230
|
+
`{ protocolVersion, ... }` — written to stdout as pretty-printed JSON. Per
|
|
231
|
+
`lib/task-executor/index.js`'s own JSDoc/return statements:
|
|
232
|
+
|
|
233
|
+
**`start` (`execute()`) and `resume`:**
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"protocolVersion": 1,
|
|
237
|
+
"runId": "<uuid>",
|
|
238
|
+
"runStatus": "completed | blocked | paused",
|
|
239
|
+
"tasks": [ { "taskId": "US-01-T01", "status": "checkpointed" } ]
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
`resume`'s object additionally carries `tasks[].nextAction` (the next-safe-action classification
|
|
243
|
+
from `reconcile()`) and a top-level `repairsApplied` array — both additive beyond the minimum
|
|
244
|
+
shape above.
|
|
245
|
+
|
|
246
|
+
**`stop`:**
|
|
247
|
+
```json
|
|
248
|
+
{
|
|
249
|
+
"protocolVersion": 1,
|
|
250
|
+
"runId": "<uuid>",
|
|
251
|
+
"requestAccepted": true,
|
|
252
|
+
"mode": "graceful | immediate",
|
|
253
|
+
"idempotent": true,
|
|
254
|
+
"terminationConfirmed": true,
|
|
255
|
+
"killAttempts": [
|
|
256
|
+
{ "taskId": "US-01-T01", "attemptNumber": 1, "processIdentity": "ai-toolkit-task:...", "confirmed": true }
|
|
257
|
+
]
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
`idempotent` is present only on an idempotent replay. `terminationConfirmed`/`killAttempts` are
|
|
261
|
+
present only for a newly-accepted `immediate` request — never for `graceful`, never for an
|
|
262
|
+
idempotent replay.
|
|
263
|
+
|
|
264
|
+
**`reconcile`:**
|
|
265
|
+
```json
|
|
266
|
+
{
|
|
267
|
+
"protocolVersion": 1,
|
|
268
|
+
"runId": "<uuid>",
|
|
269
|
+
"repairsApplied": [ { "taskId": "US-01-T01", "action": "registerCommitSHA" } ],
|
|
270
|
+
"classifications": [ { "taskId": "US-01-T01", "classification": "...", "action": "..." } ]
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**`replan`:**
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"protocolVersion": 1,
|
|
278
|
+
"originalRunId": "<uuid>",
|
|
279
|
+
"successorPlanDigest": "<digest>",
|
|
280
|
+
"taskMapping": [
|
|
281
|
+
{ "oldTaskId": "US-01-T01", "newTaskId": "US-01-T01", "carriesCompletion": true }
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**`status`/`diagnose`:** no success shape exists yet — both are `NOT_IMPLEMENTED` stubs (see
|
|
287
|
+
above); every call surfaces the error shape below with `code: "NOT_IMPLEMENTED"`.
|
|
288
|
+
|
|
289
|
+
### Error (stderr, exit per the table above)
|
|
290
|
+
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"status": "error",
|
|
294
|
+
"code": "NO_LOCK_HELD",
|
|
295
|
+
"message": "dispatchTaskAttempt: no repo-wide execution lease held by runId \"...\" at ..."
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
This is `bin/cli.js`'s real `emitError()` shape: `code` falls back to `'UNKNOWN'` if the thrown
|
|
300
|
+
error carries no `.code`; `message` falls back to `String(err)` if the error carries no
|
|
301
|
+
`.message`.
|
|
302
|
+
|
|
303
|
+
A malformed CLI invocation (e.g. missing a required flag) is a **usage error**, not one of the
|
|
304
|
+
JSON error shapes above: it writes a plain `Error: <message>` line to stderr and sets exit code 2
|
|
305
|
+
directly, without going through `mapExecutorErrorToExitCode`.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Platform qualification
|
|
310
|
+
|
|
311
|
+
`execute()` checks `process.platform !== 'win32'` as its very first validation step — before any
|
|
312
|
+
other argument validation, before deriving the execution root, before touching the plan
|
|
313
|
+
snapshot, and before acquiring the ownership lease — and throws `PLATFORM_NOT_QUALIFIED` (exit
|
|
314
|
+
7) on any non-Windows platform. This reflects FTR-018's Gate-1 binding constraint: the E-02
|
|
315
|
+
process-supervision evidence (tree-kill via `taskkill`, tag-based liveness scanning via
|
|
316
|
+
`Win32_Process` CIM queries) that the executor's crash-recovery/stop/worker-liveness machinery
|
|
317
|
+
depends on is qualified for Windows only (US-08-TASK-BE-05). There is no implicit
|
|
318
|
+
cross-platform guarantee — other platforms require their own dedicated qualification proof
|
|
319
|
+
before `start` will run on them at all.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Deterministic workflow control plane
|
|
2
|
+
|
|
3
|
+
The toolkit owns workflow execution in Node. An LLM is used only for document
|
|
4
|
+
generation, review, and assessment. A workflow script is not a registered worker
|
|
5
|
+
agent: never pass `pm-phase1`, `pm-phase2`, `pm-phase3`, `am-phase1`, or `am-phase2`
|
|
6
|
+
to `agentType`, `subagent_type`, or Claude's `--agent`.
|
|
7
|
+
|
|
8
|
+
## Invocation
|
|
9
|
+
|
|
10
|
+
Reinstall the toolkit runtime assets after upgrading the package. The host rejects
|
|
11
|
+
older installed workflows without `controlPlaneVersion: 1`, even if their old
|
|
12
|
+
manifest hashes match. Use one installation scope (local or global); ambiguous or
|
|
13
|
+
unverified agent installations fail closed.
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
ai-toolkit workflow run pm-phase1 --project . --claude-path "C:\path\claude.exe" -- "internal_docs/features/FTR-123-example/feature.md"
|
|
17
|
+
ai-toolkit workflow run pm-phase2 --project . --claude-path "C:\path\claude.exe" -- "internal_docs/features/FTR-123-example/feature.md"
|
|
18
|
+
ai-toolkit workflow run am-phase1 --project . --claude-path "C:\path\claude.exe" -- . --scope=security,devops --prefix ASSESS-123
|
|
19
|
+
ai-toolkit workflow run am-phase2 --project . -- --prefix ASSESS-123 --flagged INT-001 --ack "Reviewed by the user"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Resolve the actual Claude executable as described in `task-executor-bootstrap.md`.
|
|
23
|
+
`--home DIR` is available for explicit installation-scope resolution. Paths are
|
|
24
|
+
individual argv elements, never interpolated shell commands. Raw string arguments
|
|
25
|
+
in the JavaScript API support quoted paths and preserve Windows backslashes.
|
|
26
|
+
|
|
27
|
+
Exit 0 returns a JSON payload on stdout. A nonzero exit stops the pipeline; do not
|
|
28
|
+
present the next approval gate. Gate 1/Gate 2 and Findings Gate decisions remain in
|
|
29
|
+
the main loop. `am-phase2` requires explicit acknowledgement and a validated
|
|
30
|
+
intervention selection; it does not invent approval.
|
|
31
|
+
|
|
32
|
+
`pm-phase3` is a retired compatibility entry point and refuses execution. Since
|
|
33
|
+
FTR-018, implementation belongs to `ai-toolkit tasks start/run`, with its task
|
|
34
|
+
ownership, checkpoints, verification, review and resume protocol. The old workflow
|
|
35
|
+
must not provide a fallback execution path. No JobScheduler feature is involved
|
|
36
|
+
in this migration.
|
|
37
|
+
|
|
38
|
+
## Audit and replacements
|
|
39
|
+
|
|
40
|
+
| Original location | Deterministic operation formerly delegated to `agent()` | Replacement |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| pm-phase1 | agent resolution; self/worker ledger open/close | Verified host dispatch and owned activity lifecycle |
|
|
43
|
+
| pm-phase1 | feature discovery, prefix, existence/mtime checks, ledger creation | Direct filesystem operations; freshness includes upstream documents; corrupt ledgers are preserved and rejected |
|
|
44
|
+
| pm-phase1 | validation report existence/fabrication, process log writes | Require a nonempty fresh report from the validator; append log directly; structured validation verdict |
|
|
45
|
+
| pm-phase2 | resolution and ledger operations | Same host lifecycle; unsuccessful results cannot close the owner as done |
|
|
46
|
+
| pm-phase2 | validator/renderer execution, exit/output parsing and file checks | Toolkit CLI subprocess, `shell: false`, argv arrays, actual exit status, JSON parsing and disk reads |
|
|
47
|
+
| pm-phase2 | raw pricing read, WB metrics, Token/Effort estimates, process log | Direct JSON reads and deterministic templates; unavailable usage/cost stays unavailable |
|
|
48
|
+
| pm-phase3 | resolution, all ledger mutations, raw CSV/ledger/pricing reads, issue counts, test/git/PR commands, actuals writes | Legacy execution removed; implementation and CSV parsing already belong to the FTR-018 executor |
|
|
49
|
+
| am-phase1 | discovery CLI, catalog/file reads, scope filtering, estimates/index counts | Canonical assessor map, verified resolution, strict scope filtering and deterministic Markdown parsing/templates |
|
|
50
|
+
| am-phase2 | approval file, intervention selection, registry read/write/counts | Deterministic recorder with explicit input and readback |
|
|
51
|
+
| FTR-018 process helper | worker identity cache and dispatch identity | Reject orchestrators; require requested canonical ID/native name agreement; cache by project + home + ID |
|
|
52
|
+
|
|
53
|
+
Architecture, concurrency and quality have dedicated assessors. Security, DevOps,
|
|
54
|
+
domain-model and dependencies use the generic assessor with explicit scope
|
|
55
|
+
instructions. Multiple generic scopes are combined into one dispatch to avoid
|
|
56
|
+
parallel writers overwriting the same report. Remediation agents are recommendations
|
|
57
|
+
in intervention documents only; they are never used as assessors.
|
|
58
|
+
|
|
59
|
+
## Identity, state and ownership
|
|
60
|
+
|
|
61
|
+
Worker resolution checks the canonical catalog ID, verified status, exact native
|
|
62
|
+
name, current definition hash, agent frontmatter name/model, and worker kind.
|
|
63
|
+
Workflow resolution is separate and can never authorize worker dispatch.
|
|
64
|
+
|
|
65
|
+
Every invocation has a unique owner and activity IDs. A per-prefix lock prevents
|
|
66
|
+
two workflow runs from writing the same feature/assessment concurrently. The owner
|
|
67
|
+
opens before work, drains parallel workers, validates outputs and serializes the
|
|
68
|
+
result before its terminal transition. Worker usage and provenance are persisted.
|
|
69
|
+
Legacy `open/close/fail/skip/finalizeActivity` calls cannot mutate owned entries;
|
|
70
|
+
owned finalization requires the matching owner and a running entry. No name-only
|
|
71
|
+
fallback is used for owned transitions. Later invocations cannot reopen an old run.
|
|
72
|
+
|
|
73
|
+
The ownership token prevents accidental cross-finalization, not hostile filesystem
|
|
74
|
+
access: workers and the host still run under the user's OS account. This is not a
|
|
75
|
+
security sandbox.
|
|
76
|
+
|
|
77
|
+
A caught failure produces `failed`. A hard OS/process kill can leave `running`,
|
|
78
|
+
never a speculative `done`; workflow automatic resume/reconciliation is not added
|
|
79
|
+
by this change. Retrying uses a new owner and preserves the interrupted record.
|
|
80
|
+
If persistence itself fails, the error propagates rather than reporting success.
|
|
81
|
+
|
|
82
|
+
## Verification and limits
|
|
83
|
+
|
|
84
|
+
Regression tests execute the installed workflow source through the real host,
|
|
85
|
+
resolver, filesystem and ledger. Integration tests invoke the actual structural
|
|
86
|
+
validator/renderer and workflow CLI; LLM workers are deterministic test doubles.
|
|
87
|
+
Coverage includes worker failure, missing outputs, corrupt ledger, late terminal
|
|
88
|
+
calls, parallel rejection/drain, identity mismatch, cross-project cache leakage,
|
|
89
|
+
negative semantic verdict, unknown scopes, paths with spaces and explicit approval.
|
|
90
|
+
|
|
91
|
+
The live paid Claude model/permission flow has not been exercised by these tests.
|
|
92
|
+
The subprocess adapter reuses FTR-018's existing qualification; no new cross-platform
|
|
93
|
+
process-supervision guarantee is claimed. On Windows, the full executor test suite
|
|
94
|
+
requires Git Bash ahead of the WSL launcher in PATH. User-level installations are
|
|
95
|
+
not modified by running the isolated test fixtures.
|