@kontourai/flow-agents 2.0.1 → 2.1.1

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.
Files changed (41) hide show
  1. package/.github/actions/trust-verify/action.yml +4 -2
  2. package/.github/workflows/ci.yml +16 -4
  3. package/.github/workflows/docs-pages.yml +1 -1
  4. package/.github/workflows/kit-gates-demo.yml +2 -2
  5. package/.github/workflows/publish-npm.yml +2 -2
  6. package/.github/workflows/runtime-compat.yml +2 -2
  7. package/.github/workflows/trust-reconcile.yml +1 -1
  8. package/CHANGELOG.md +28 -0
  9. package/README.md +3 -3
  10. package/build/src/cli/workflow-sidecar.js +8 -2
  11. package/context/scripts/telemetry/lib/config.sh +15 -0
  12. package/context/scripts/telemetry/telemetry.conf +4 -0
  13. package/context/scripts/telemetry/telemetry.sh +23 -1
  14. package/docs/design/flowrun-eventsourcing-design.md +216 -0
  15. package/docs/design/workflowrun-observability-design.md +431 -0
  16. package/evals/ci/antigaming-suite.sh +1 -0
  17. package/evals/ci/run-baseline.sh +2 -0
  18. package/evals/integration/test_command_log_concurrency.sh +114 -0
  19. package/evals/integration/test_gate_lockdown.sh +21 -6
  20. package/evals/integration/test_usage_cost.sh +119 -0
  21. package/evals/integration/test_verify_cli.sh +23 -0
  22. package/integrations/strands/flow_agents_strands/hooks.py +126 -1
  23. package/integrations/strands/flow_agents_strands/telemetry.py +172 -0
  24. package/integrations/strands/tests/test_usage.py +129 -0
  25. package/integrations/strands-ts/src/hooks.ts +135 -1
  26. package/integrations/strands-ts/src/telemetry.ts +170 -0
  27. package/integrations/strands-ts/test/test-usage.ts +85 -0
  28. package/package.json +2 -2
  29. package/scripts/ci/trust-reconcile.js +7 -23
  30. package/scripts/hooks/evidence-capture.js +85 -50
  31. package/scripts/hooks/stop-goal-fit.js +18 -45
  32. package/scripts/lib/command-log-chain.js +73 -0
  33. package/scripts/repair-command-log.js +8 -15
  34. package/scripts/telemetry/lib/config.sh +15 -0
  35. package/scripts/telemetry/lib/pricing.sh +42 -0
  36. package/scripts/telemetry/lib/usage.sh +108 -0
  37. package/scripts/telemetry/pricing.golden.json +15 -0
  38. package/scripts/telemetry/pricing.json +31 -0
  39. package/scripts/telemetry/telemetry.conf +4 -0
  40. package/scripts/telemetry/telemetry.sh +23 -1
  41. package/src/cli/workflow-sidecar.ts +8 -2
@@ -0,0 +1,431 @@
1
+ # ADR draft + Migration Plan: `WorkflowRun` — Event-Sourced Workflow State for Replay / Trace / Observability
2
+
3
+ **Status:** DRAFT for owner review (not yet an accepted ADR). Design doc only — no code.
4
+ **Date:** 2026-06-27
5
+ **Author:** design exploration (read-only)
6
+ **Relates to:** ADR 0001 (consume Flow), ADR 0010 (trust bundle), ADR 0012 (liveness), ADR 0013 (context lifecycle), ADR 0016 (three-hard-boundary), ADR 0017 (anti-gaming security model)
7
+
8
+ ---
9
+
10
+ ## 0. TL;DR
11
+
12
+ Flow Agents already has **four** append-ish event streams and **one mutable control record**, but no single
13
+ event log that can *replay a whole session*. This doc proposes modeling a workflow run as an **append-only event
14
+ log** with the current sidecars (`state.json`, `acceptance.json`, `evidence.json`/`trust.bundle`, `handoff.json`)
15
+ re-derived as **projections (folds)** over that log.
16
+
17
+ **The most important finding up front (reuse-vs-build, §8):** `@kontourai/flow` **already ships a `FlowRun`
18
+ primitive** (`startRun`, `loadRun`, `saveRun`, `evaluateRun`, `projectFlowRun`, `validateRunTransition`,
19
+ `createRunWatcher`, a frozen run layout, an evidence manifest). Per ADR 0001 "Flow Agents consumes Flow" this is
20
+ where a `WorkflowRun` domain *should* live — **not** as a new bespoke Flow Agents log. But note: Flow's
21
+ `FlowRunState` is a **mutable record** (`extends MutableRecord`) with an in-place `transitions[]` array
22
+ (`node_modules/@kontourai/flow/dist/contracts/flow-types.d.ts`, `FlowRunState`), **not** a pure event-sourced
23
+ fold. So the real decision is **where event-sourcing lives**, not whether to build a parallel log. The honest
24
+ recommendation (§9) is: **do the thinnest read-only slice in Flow Agents first** (a hash-chained event log that
25
+ *unifies the streams we already write*, plus a `replay`/`trace` command), prove the projection reproduces
26
+ today's sidecars byte-for-byte, and only then negotiate with Flow about pushing event-sourcing upstream.
27
+
28
+ ---
29
+
30
+ ## 1. Context — what exists today (grounded)
31
+
32
+ ### 1.1 The mutable control record: `state.json`
33
+
34
+ `state.json` is written by `initSidecars` (`src/cli/workflow-sidecar.ts:858–873`) and mutated in place by
35
+ `writeState` (`src/cli/workflow-sidecar.ts:1109–1110`): every `advance-state` does a read-modify-write
36
+ (`{ ...loadJson(state.json), ...status, phase, updated_at, next_action }`). It carries:
37
+
38
+ - `status` ∈ the 13-value set at `workflow-sidecar.ts:13` (`new`…`accepted`/`archived`)
39
+ - `phase` ∈ the 11-value ordered list at `workflow-sidecar.ts:14` (`idea`→`done`)
40
+ - `next_action`, `artifact_paths`, `created_at`/`updated_at`
41
+
42
+ **It is destructive.** Each advance overwrites the prior status/phase; there is no record of *how* the run got
43
+ to its current state beyond `updated_at`. ADR 0010 deliberately keeps `state.json` as **lifecycle/control state**
44
+ (the "WHAT-step", owned by Flow per ADR 0007) and explicitly *out* of the trust bundle
45
+ (`docs/adr/0010-...md:42–46`), and ADR 0010 Phase 4 keeps `state.json` even after the bespoke sidecars are
46
+ retired (`docs/adr/0010-...md:113`).
47
+
48
+ ### 1.2 The trust state: `trust.bundle` (derived, already a fold over evidence + events)
49
+
50
+ `buildTrustBundle` (`workflow-sidecar.ts:260`) and `writeTrustBundle` (`workflow-sidecar.ts:523–563`) turn
51
+ checks/criteria/critiques **plus the command-log** into a Hachure `trust.bundle` of **claims + evidence +
52
+ events + policies**. This is *already* a projection: claim status is **recomputed from evidence**, not stored —
53
+ the Surface module's `deriveClaimStatus({ claim, evidence, events, policies })` is the fold
54
+ (`SurfaceModule` interface, `workflow-sidecar.ts` ~155–165). ADR 0010 maps:
55
+ `evidence.json`→claims+evidence, `acceptance.json`→claims, `command-log.jsonl`→"evidence/traces the claims
56
+ recompute *from* — the event stream behind the bundle", `critique.json`→claims/findings
57
+ (`docs/adr/0010-...md:36–40`). As of ADR 0010 Phase 4, `evidence.json`/`critique.json` are retired and the
58
+ **`trust.bundle` is the sole verification artifact** (`docs/adr/0010-...md:8`).
59
+
60
+ **So the trust *sub-domain* is already event-sourced-ish.** What is missing is the same discipline for the
61
+ *lifecycle* (`state.json`) and a *single unified* log that ties trust events, lifecycle events, and agent
62
+ activity into one replayable timeline.
63
+
64
+ ### 1.3 The append logs that already exist (this is the crux — do NOT add a 5th)
65
+
66
+ | Stream | File | Writer | Hash-chained? | Scope |
67
+ |---|---|---|---|---|
68
+ | **Command capture** | `.flow-agents/<slug>/command-log.jsonl` | `evidence-capture.js` (PostToolUse hook) | **YES** — `_chain:{seq,prevHash,hash}` | per-task |
69
+ | **Agent events** | `.flow-agents/<slug>/agents/<agent>/events.jsonl` | `recordAgentEvent` (`workflow-sidecar.ts:917–931`) | no | per-agent |
70
+ | **Liveness** | `.flow-agents/liveness/events.jsonl` | `appendLivenessEvent` / `livenessLifecycle` (`workflow-sidecar.ts:2383–2387, 2417–2426`) | no | per-root (cross-task) |
71
+ | **Surface VerificationEvents** | embedded *inside* `trust.bundle` | `buildTrustBundle` | n/a (bundle is signed at seal) | per-task |
72
+ | **Transitions (Flow)** | Flow's `state.json.transitions[]` | `@kontourai/flow` `saveRun` (not yet wired in Flow Agents) | no | per-run |
73
+
74
+ The hash-chain is the security spine: `command-log.jsonl` records `hash = sha256(prevHash + canonicalJson(record))`
75
+ with a genesis sentinel and a serialized read→compute→append critical section under a lock so parallel agents
76
+ cannot fork the chain (`scripts/hooks/evidence-capture.js:12–26, 95–122, 357–384`). This is the tamper-evidence
77
+ primitive ADR 0017 Layer 1 leans on ("independent capture", `docs/adr/0017-...md:45–48`).
78
+
79
+ **Reconciliation requirement (§2):** a `WorkflowRun` event log must *subsume or generalize* these, not become
80
+ a competing parallel log. ADR 0010 already rejected "a new bespoke stream-to-Console mechanism"
81
+ (`docs/adr/0010-...md:128–130`); the same anti-fork instinct applies here.
82
+
83
+ ### 1.4 How a run "seals": checkpoint → sign → deliver
84
+
85
+ On `advance-state … --status delivered` (`workflow-sidecar.ts:1368–1372`):
86
+ 1. `sealTrustCheckpoint` (`workflow-sidecar.ts:1489–1527`) builds a Surface trust *report* from the bundle,
87
+ freezes a `checkpointFromReport` derivation, and writes `trust.checkpoint.json` (envelope: slug, status,
88
+ phase, `sealed_at`, `commit_sha`, `checkpoint`).
89
+ 2. `signCheckpointAttestation` (`workflow-sidecar.ts:1548+`) computes `sha256(trust.checkpoint.json)` as the
90
+ subject, wraps it in an in-toto Statement, and either Sigstore-signs it (CI/OIDC →
91
+ `trust.checkpoint.sig.json`) or writes an unsigned in-toto statement (local →
92
+ `trust.checkpoint.intoto.json`). The checkpoint file is **never modified after its digest is computed**
93
+ (`workflow-sidecar.ts:1609`) — it is the external anchor.
94
+ 3. `publishDelivery` (`workflow-sidecar.ts:1671+`) copies bundle + checkpoint companions to `delivery/` for the
95
+ CI `trust-reconcile` anchor (ADR 0017 Layer 2, `docs/adr/0017-...md:61–73, 87–90`).
96
+
97
+ **A sealed checkpoint is the "compiled note" of the run.** The owner's framing — *"raw notes may be less helpful
98
+ than the compiled notes or derived deterministic metadata"* — already has a home: the checkpoint is the verified,
99
+ signed, derived metadata; the raw event log is the secondary, retrievable detail. The design just needs to make
100
+ the **pointer** explicit (§3).
101
+
102
+ ### 1.5 How the gate consumes state (the invariant that must not weaken)
103
+
104
+ `stop-goal-fit.js` reads `state.json` as the **primary** lifecycle source (`scripts/hooks/stop-goal-fit.js:502,
105
+ 520`) and reads `trust.bundle` for verdict/checks/critique/criteria (`stop-goal-fit.js:48–49, 544+`). Critically
106
+ it **re-derives** claim status from evidence via Surface `deriveClaimStatus` rather than trusting stored status
107
+ (ADR 0017 L1, `docs/adr/0017-...md:42–48`), and cross-references claimed passes against the hash-chained
108
+ `command-log.jsonl` ("caught false-completion" blocks). The threat model names `state.json.status` as an
109
+ **agent-controlled input** (`docs/adr/0017-...md:33–38`) and `config-protection.js` blocks agent writes to
110
+ `state.json`/`current.json`/`trust.bundle`/`delivery/trust.bundle` (`docs/adr/0017-...md:56–58`).
111
+
112
+ **Invariant for this design:** event-sourcing must *strengthen* this, never weaken it. An event log that is
113
+ hash-chained and whose lifecycle projection is recomputed (not trusted) closes part of the "`state.json` is
114
+ forgeable" residual — but only if the *projection*, not a stored status, is what the gate keys off (§5).
115
+
116
+ ---
117
+
118
+ ## 2. Core idea — the event taxonomy and the projections
119
+
120
+ ### 2.1 Principle
121
+
122
+ Model the run as an **append-only, hash-chained sequence of events**. The current sidecars become **pure folds**
123
+ over that log:
124
+
125
+ ```
126
+ state.json = foldLifecycle(events)
127
+ acceptance.json = foldAcceptance(events) # criteria + goal_fit
128
+ trust.bundle = foldTrust(events) # claims+evidence+events → Surface deriveClaimStatus
129
+ handoff.json = foldHandoff(events)
130
+ current.json = foldCurrent(events_across_runs) # active run pointer
131
+ ```
132
+
133
+ A projection is *deterministic and versioned* (this matches ADR 0013's "AGENTS.md is a projection of claims"
134
+ framing, `docs/adr/0013-...md:99–104`, and ADR 0010's "claim status recomputed by a pure, versioned function",
135
+ `docs/adr/0010-...md:27`). The event log is the source of truth; the sidecars are caches you can delete and
136
+ rebuild.
137
+
138
+ ### 2.2 Event taxonomy (proposed)
139
+
140
+ Every event shares an envelope (reusing the `command-log.jsonl` chain shape, `evidence-capture.js:12–26`):
141
+
142
+ ```jsonc
143
+ {
144
+ "type": "PhaseAdvanced",
145
+ "run_id": "<slug or flow run_id>",
146
+ "seq": 42,
147
+ "at": "2026-06-27T12:00:00Z",
148
+ "actor": "tool-worker|local|workflow-sidecar|evidence-capture",
149
+ "source": "advance-state", // the command/hook that emitted it
150
+ "payload": { ... }, // type-specific
151
+ "_chain": { "seq": 42, "prevHash": "…", "hash": "sha256(prevHash + canonicalJson(record))" }
152
+ }
153
+ ```
154
+
155
+ Proposed event types, mapped to today's writers:
156
+
157
+ | Event | Emitted by today | Payload | Folds into |
158
+ |---|---|---|---|
159
+ | `SessionStarted` | `ensureSession` / `initSidecars` (`workflow-sidecar.ts:858–906`) | slug, source_request, flow_id, step_id, criteria[] | state.json, acceptance.json |
160
+ | `PhaseAdvanced` | `advanceState` (`:1321–1374`) | from_phase, to_phase, status, summary, next_action | state.json, handoff.json |
161
+ | `RouteBack` | `advanceState` route-back guard (`:1338–1348`) | from_phase, to_phase, reason, attempt_count | state.json, `transition-attempts.json` |
162
+ | `EvidenceRecorded` | `recordEvidence` (`:1209–1223`) | check(kind,status,evidence_refs) | trust.bundle |
163
+ | `ClaimMade` | `recordGateClaim` (`:1261–1317`) | claimType, subjectType, status, evidence | trust.bundle |
164
+ | `CommandObserved` | `evidence-capture.js:349–384` (**already an event**) | command, observedResult, exitCode | trust.bundle evidence/traces |
165
+ | `CritiqueRecorded` | `recordCritique` (`:1381–1396`) | findings[], verdict | trust.bundle |
166
+ | `LearningRecorded` | `recordLearning` (`:1752+`) | correction/prevention | learning projection |
167
+ | `AgentEvent` | `recordAgentEvent` (`:917–931`, **already an event**) | agent_id, kind, status, summary, ref | per-agent timeline |
168
+ | `LivenessSignal` | `livenessLifecycle` (`:2417–2426`, **already an event**) | claim/heartbeat/release, ttl | liveness stream |
169
+ | `CheckpointSealed` | `sealTrustCheckpoint` (`:1489–1527`) | checkpoint digest, commit_sha, attestation ref | trust.checkpoint.json (the seal) |
170
+ | `DeliveryPublished` | `publishDelivery` (`:1671+`) | delivery paths | delivery/ |
171
+
172
+ **Note three of these (`CommandObserved`, `AgentEvent`, `LivenessSignal`) are *literally already* append-only
173
+ events today.** The taxonomy is mostly *naming and unifying what is already emitted*, plus turning the destructive
174
+ `writeState` into `PhaseAdvanced`/`RouteBack` events.
175
+
176
+ ### 2.3 The projection functions (folds that reproduce today's sidecars)
177
+
178
+ - `foldLifecycle(events) → state.json`: scan events in `seq` order; the latest `PhaseAdvanced`/`SessionStarted`
179
+ sets `status`/`phase`/`next_action`; `RouteBack` events feed `transition-attempts.json`. Reproduces
180
+ `writeState`'s output (`:1109–1110`) exactly, but now derivable *at any seq* (replay).
181
+ - `foldAcceptance(events) → acceptance.json`: `SessionStarted.criteria` seeds the list; `EvidenceRecorded`/
182
+ `ClaimMade` referencing a criterion update its status; `goal_fit` derived from the goal-fit claim. Reproduces
183
+ `initSidecars`' acceptance shape (`:865–869`).
184
+ - `foldTrust(events) → trust.bundle`: the existing `buildTrustBundle` (`:260`) **already is this fold** — feed
185
+ it the `EvidenceRecorded`/`ClaimMade`/`CritiqueRecorded`/`CommandObserved` events instead of re-reading bundle
186
+ files. Status stays **recomputed** via Surface `deriveClaimStatus` (no change to the trust security model).
187
+ - `foldHandoff(events) → handoff.json`: latest summary + next_steps from `PhaseAdvanced` (`:1352`).
188
+
189
+ **Acceptance test for the whole design:** for any existing `.flow-agents/<slug>/`, replaying its derived event
190
+ log must reproduce the current `state.json`/`acceptance.json`/`trust.bundle`/`handoff.json` **byte-identically**
191
+ (modulo `updated_at`). This is the migration's correctness oracle (§6, Phase A exit criteria).
192
+
193
+ ---
194
+
195
+ ## 3. The "rebuild the session" capability (replay / trace)
196
+
197
+ ### 3.1 Replay to any point
198
+
199
+ Because every projection is `fold(events[0..n])`, you can fold to **any `seq`** and get the exact state at that
200
+ moment: `replay --at <seq|timestamp>` reconstructs `state.json` + `trust.bundle` as they *were*. This is what the
201
+ owner means by *"recreate the session and what happened … corroborating evidence."* Today this is impossible —
202
+ `writeState` overwrote it.
203
+
204
+ ### 3.2 Trace / timeline
205
+
206
+ A `trace` (or `timeline`) command renders the event log as an ordered who-did-what-when:
207
+
208
+ ```
209
+ seq at actor event detail
210
+ 1 12:00:00Z workflow-sidecar SessionStarted slug=foo, 4 criteria, flow=builder.build
211
+ 7 12:03:10Z tool-worker CommandObserved `npm test` → pass (exit 0)
212
+ 8 12:03:12Z workflow-sidecar EvidenceRecorded builder.verify.tests = pass
213
+ 12 12:05:00Z workflow-sidecar PhaseAdvanced execution → verification
214
+ 15 12:06:00Z evidence-capture CommandObserved `npm run build` → fail (exit 1)
215
+ 16 12:06:30Z workflow-sidecar RouteBack verification → execution (implementation_defect, attempt 1)
216
+
217
+ 40 12:20:00Z workflow-sidecar CheckpointSealed digest=ab12…, signed (CI) / unsigned (local)
218
+ ```
219
+
220
+ This *is* the observability/trace role (§4). Flow already ships `renderResume`/`renderSummary`/`projectFlowRun`
221
+ (see §8) that could render this if the events live in a `FlowRun`.
222
+
223
+ ### 3.3 Separation but rebuildable — "compiled notes vs raw notes"
224
+
225
+ Map the owner's framing directly onto existing artifacts:
226
+
227
+ | Owner's term | Artifact | Property |
228
+ |---|---|---|
229
+ | **Compiled notes / verified state** | `trust.checkpoint.json` (+ `.sig`/`.intoto`) and the `trust.bundle` it seals | signed, derived, the thing the gate + CI trust |
230
+ | **Derived deterministic metadata** | the projections (`state.json`, `acceptance.json`) | recomputable, versioned folds |
231
+ | **Raw notes / corroborating evidence** | the full event log (`run-events.jsonl`) | secondary, retrievable, hash-chained |
232
+ | **The pointer** | `event_log_ref` + `event_log_head_hash` fields added to the checkpoint envelope (`:1506–1517`) | sealed state points at the raw log without inlining it |
233
+
234
+ So: the sealed checkpoint carries a **pointer** (`event_log_ref` + the head chain hash) to the raw event log.
235
+ *"If an agent (or anyone) asks, we have a pointer to this part as it's less relevant to the verified state but
236
+ still retrievable."* The verified state travels light (checkpoint + bundle to CI per ADR 0017 delivery
237
+ transport, `docs/adr/0017-...md:87–90`); the raw log stays local/retrievable. This mirrors ADR 0010's
238
+ "local file is the source of truth; Console is an optional projection" split (`docs/adr/0010-...md:57–66`).
239
+
240
+ ---
241
+
242
+ ## 4. Observability / trace role — what a consumer gets
243
+
244
+ - **Agent (or future session):** ADR 0013's "gleaning in-progress work" (`docs/adr/0013-...md:54–65`) gets a
245
+ real timeline instead of a flattened `state.json`. It can *replay* a prior session to see intent + verified
246
+ facts, respecting claim status (the ADR 0013 safety line).
247
+ - **Human / owner:** "why did this phase advance?" → the `PhaseAdvanced` event + the `EvidenceRecorded`/
248
+ `ClaimMade` events that preceded it, in order. "Why did the gate block?" → replay to the gate's `seq` and
249
+ inspect the exact bundle it saw.
250
+ - **Future Console:** ADR 0010 already designates Surface as the projection owner
251
+ (`docs/adr/0010-...md:57–66`); `@kontourai/flow` already ships console projections
252
+ (`projectFlowRun`, `FlowConsoleProjection`, `startFlowConsoleServer` — §8). The event log feeds these for free.
253
+ - **Debugging a gate decision:** the killer feature. Because the gate re-derives from evidence, replaying the
254
+ event log to the exact `seq` the gate ran reproduces its verdict deterministically — a true debugger for the
255
+ trust gate.
256
+
257
+ ---
258
+
259
+ ## 5. Trust / security invariants preserved (ADR 0017 must not weaken)
260
+
261
+ | ADR 0017 invariant | Source | How event-sourcing preserves / strengthens it |
262
+ |---|---|---|
263
+ | Gate **re-derives** verdict from evidence, never trusts stored status | `docs/adr/0017-...md:42–48` | `foldTrust` *is* the re-derivation (Surface `deriveClaimStatus`). Unchanged. The gate keys off the **projection**, not a stored `status`. |
264
+ | **Independent, hash-chained** capture | `evidence-capture.js:95–122`; `docs/adr/0017-...md:45–48` | The event log **reuses the same `_chain` construction**. Generalizing the chain to *all* events extends tamper-evidence to lifecycle + agent events, which today are unchained. **Net strengthening.** |
265
+ | `state.json` is agent-forgeable → don't trust it | `docs/adr/0017-...md:33–38, 98` | A *derived, chained* lifecycle projection is harder to forge silently than a free-form mutable file: tampering breaks the chain. The gate should treat the **fold output** as authoritative, and `config-protection.js` must additionally protect `run-events.jsonl` (add it to the protected set, `docs/adr/0017-...md:56–58`). |
266
+ | Checkpoint seals + signs; CI reconciles | `:1489–1527, 1548+`; `docs/adr/0017-...md:61–90` | `CheckpointSealed` event + the pointer (§3.3) make the seal an *event over the log*; the signed checkpoint digest still anchors externally. CI `trust-reconcile` is unchanged (it reconciles the bundle, which is still a fold). |
267
+ | External CI anchor is the real boundary | `docs/adr/0017-...md:113–130` | Unchanged. Event-sourcing is a *local* observability/integrity gain; it does **not** claim to replace the CI anchor. Be explicit about this so the design isn't oversold. |
268
+
269
+ **Hard rule:** the event log is **append-only and chained**; "edit history" = append a compensating event, never
270
+ rewrite. Any projection that disagrees with a fresh fold is a tamper signal. The anti-gaming regression suite
271
+ (ADR 0017 L4, `docs/adr/0017-...md:79–85`) must gain a test that asserts "a hand-edited event log breaks the
272
+ chain and the gate notices."
273
+
274
+ ---
275
+
276
+ ## 6. Migration plan — phased, honoring "no legacy / no fallbacks"
277
+
278
+ Standing constraint (owner, verbatim): *"long term.. no legacy or fallbacks please.. fine if you're just using
279
+ it in execution transition."* So dual-write is allowed **as transition scaffolding**, but the **end state has
280
+ zero `state.json`-as-source-of-truth fallback**. Each phase is proof-gated (mirroring ADR 0010's proof-gated
281
+ phases, `docs/adr/0010-...md:84–115`) — keep `prove-capture-teeth` and the anti-gaming suite green throughout.
282
+
283
+ ### Phase A — Emit the unified event log (additive, read-only, reversible)
284
+ **Ships:** every existing writer *also* appends a typed, hash-chained event to `run-events.jsonl`
285
+ (`SessionStarted` from `:858`, `PhaseAdvanced`/`RouteBack` from `:1321–1348`, reuse the already-emitted
286
+ `CommandObserved`/`AgentEvent`/`LivenessSignal`). Sidecars stay the source of truth. **No behavior change.**
287
+ **Exit criteria:** `replay`/`trace` command exists (read-only); folding the event log reproduces the live
288
+ sidecars byte-identically for every task in `.flow-agents/`. **Reversible:** delete the new file + command.
289
+
290
+ ### Phase B — Switch projections to be the source of truth (dual-write scaffolding)
291
+ **Ships:** `writeState`/`writeTrustBundle`/etc. are reimplemented as `append event → fold → write sidecar`. The
292
+ sidecars become **caches written from the fold**, not authored directly. The gate and CI still read the sidecar
293
+ files (no consumer change yet). This is the *"using it in execution transition"* the owner OK'd.
294
+ **Exit criteria:** a `--rebuild` flag regenerates every sidecar purely from events and matches; an integrity
295
+ check (`fold(events) == sidecar`) runs in the anti-gaming suite. **Reversible:** flip the writer back to direct.
296
+
297
+ ### Phase C — Move consumers onto the projection API; REMOVE the mutable writes (clean end state)
298
+ **Ships:** the gate (`stop-goal-fit.js`) and CI (`trust-reconcile.js`) read via the projection/fold (or a
299
+ generated read-model), not by parsing a hand-authored `state.json`. Then **delete** the direct `writeState`
300
+ mutation path and the "`state.json` is primary" reads (`stop-goal-fit.js:502, 520`). `state.json` either (a)
301
+ disappears in favor of the fold, or (b) remains strictly as a **generated, read-only cache** of `foldLifecycle`
302
+ with no independent authority. **No fallback to a hand-written `state.json` remains.**
303
+ **Exit criteria:** grep shows no code path writes lifecycle state except via an event; `config-protection.js`
304
+ protects `run-events.jsonl`; the anti-gaming suite proves a tampered log is caught. **This is the legacy-free
305
+ end state.**
306
+
307
+ ### Phase D — (negotiated) push event-sourcing upstream into Flow's `FlowRun`
308
+ **Ships:** per ADR 0001, the generic run/event kernel belongs to Flow. If Flow accepts an append-only event log
309
+ under `FlowRun` (today it's a mutable `state.json` + `transitions[]`, §8), Flow Agents *consumes* it and deletes
310
+ its bespoke log entirely — the ultimate "no fork." **This is a cross-repo decision, an open question (§7), not a
311
+ commitment of this ADR.**
312
+
313
+ **End state (zero legacy):** one hash-chained event log per run; all sidecars are generated projections or gone;
314
+ the gate/CI key off re-derived projections; the checkpoint points at the raw log; no hand-authored mutable
315
+ `state.json` with independent authority anywhere.
316
+
317
+ ---
318
+
319
+ ## 7. Risks / open questions / smallest first slice
320
+
321
+ ### 7.1 Thinnest valuable first slice (recommended)
322
+ **Phase A only, scoped to read-only.** Concretely: emit `run-events.jsonl` from the writers that *don't already*
323
+ emit events (`SessionStarted`, `PhaseAdvanced`, `RouteBack` — i.e. wrap `writeState`/`advanceState`), unify the
324
+ three existing event streams *by reference* (don't move them yet), and ship a `workflow-sidecar trace <dir>` +
325
+ `replay --at <seq>` read-only command. **Source of truth does not change.** This delivers the owner's
326
+ "recreate the session" value immediately, is fully reversible, touches no security-critical consumer, and
327
+ produces the correctness oracle (fold == sidecar) that de-risks every later phase. **Smallest demo:** `trace`
328
+ on an existing finished task in `.flow-agents/` showing its real timeline incl. the route-back and the seal.
329
+
330
+ ### 7.2 Top open questions for the owner
331
+ 1. **Reuse vs build — where does event-sourcing live (ADR 0001 boundary)?** Flow already owns Flow Runs
332
+ (`docs/adr/0001-...md:30–36`) and ships a `FlowRun` primitive — but it's *mutable state + a transitions
333
+ array*, not an event-sourced fold (§8). Do we (a) build the event log in Flow Agents and keep it (violates
334
+ "consume Flow" a bit), (b) build it in Flow Agents now and push it upstream later (Phase D), or (c) get Flow
335
+ to add event-sourcing first and block on that? This is *the* decision; everything else follows.
336
+ 2. **One log or keep the streams separate-but-indexed?** Do we physically merge `command-log.jsonl` +
337
+ `agents/*/events.jsonl` + `liveness/events.jsonl` into one `run-events.jsonl` (cleaner replay, but
338
+ `command-log` is the ADR 0017 *independent* capture truth source — merging it with sidecar-written events may
339
+ weaken its independence), or keep them physically separate and merge *only at read/replay time* via a unified
340
+ index? Leaning **separate-but-indexed** to preserve the independence ADR 0017 L1 relies on.
341
+ 3. **Does the gate key off the fold or the cached sidecar in the end state?** If the gate folds the event log
342
+ live, that's the strongest integrity story (tamper breaks the chain) but adds a fold to the hot Stop-hook path
343
+ (ADR 0010 already flagged hook weight as a cost, `docs/adr/0010-...md:77–82`). If it reads a generated cache,
344
+ we need the integrity check to run somewhere authoritative. Which?
345
+
346
+ ### 7.3 Other risks (eyes open)
347
+ - **Two source-of-truth files during Phase B** (event log + sidecar cache) is exactly the kind of dual-write the
348
+ owner dislikes; it's only acceptable because it's bounded transition scaffolding with a deletion deadline
349
+ (Phase C). Name the deadline or it becomes permanent legacy.
350
+ - **Replay determinism depends on projection-version pinning.** If `buildTrustBundle`/Surface `deriveClaimStatus`
351
+ change, replaying old events may yield a different bundle. Need to record `statusFunctionVersion`
352
+ (already exposed on the Surface module) in events, as ADR 0010 already treats status as a *versioned* function.
353
+ - **`command-log` independence vs unification.** Folding sidecar-authored events and the independent capture into
354
+ one chain could let a compromised sidecar writer influence the capture chain. Keep them separate (Q2).
355
+ - **Cost/benefit honesty:** this is a sizable refactor of the single most security-sensitive subsystem (the one
356
+ ADR 0017 says "twice broke the capture loop from haste", `docs/adr/0010-...md:80–82`). The read-only first
357
+ slice delivers ~80% of the *observability* value at ~10% of the risk. Resist doing Phases B–C until the owner
358
+ confirms the §7.2 Q1 boundary.
359
+
360
+ ---
361
+
362
+ ## 8. Reuse vs build — `@kontourai/flow` already has a `FlowRun` (this is decisive)
363
+
364
+ Per ADR 0001 "Flow owns Flow Definitions, **Flow Runs**, steps, gates, transitions, gate evidence, exceptions,
365
+ continuation, Flow Reports" (`docs/adr/0001-...md:30–36`) and "Flow Agents has to map existing sidecars into
366
+ Flow concepts over time" (`docs/adr/0001-...md:61`). The installed `@kontourai/flow` package **already exports a
367
+ run primitive** (`node_modules/@kontourai/flow/dist/index.d.ts`):
368
+
369
+ - **Run lifecycle:** `startRun`, `loadRun`, `saveRun`, `evaluateRun`, `listRuns`, `scaffoldDemoRun`
370
+ - **Transitions:** `validateRunTransition`, `validateTransitionRequest`, `validateEvaluationTransition`,
371
+ `validateRunStateIdentity`, `routeBackAttempt`/`routeBackDecision`/`routeTargetForReason`
372
+ - **Projection / reporting:** `projectFlowRun`, `projectFlowRunFromFiles`, `renderResume`, `renderSummary`,
373
+ `renderMarkdownReport`, `reportJson`, plus Console projections (`FlowConsoleProjection`,
374
+ `startFlowConsoleServer`)
375
+ - **Watch / observe:** `createRunWatcher`, `RunWatcher`
376
+ - **Run layout + evidence manifest:** `FLOW_RUN_LAYOUT` (frozen), `FLOW_RUN_STATE_FILE` (`state.json`),
377
+ `FLOW_RUN_EVIDENCE_DIR` (`evidence`), `FLOW_RUN_EVIDENCE_MANIFEST_*`, `FLOW_RUN_REPORT_{JSON,MARKDOWN}_FILE`,
378
+ `runDir`, `assertSafeRunId`
379
+
380
+ **But the shape is state-based, not event-sourced.** `FlowRunState extends MutableRecord` with
381
+ `gate_outcomes: GateOutcome[]` and `transitions: MutableRecord[]` accumulated *in place*
382
+ (`node_modules/@kontourai/flow/dist/contracts/flow-types.d.ts`, `FlowRunState`). It is a richer mutable
383
+ `state.json` (it keeps a transitions log) — closer to event-sourcing than Flow Agents' current `state.json`, but
384
+ **not** a pure append-only event log you can fold to any point. Flow Agents does not yet wire its sidecars to
385
+ this (`@kontourai/flow` is consumed today only for kit-container validation, `src/flow-kit/validate.ts:94–115`,
386
+ and FlowDefinition routing).
387
+
388
+ **Implication for this design:**
389
+ - **Do not build a bespoke parallel run/event domain** that ignores Flow's `FlowRun` — that's the exact
390
+ consume-never-fork violation ADR 0001/0010 warn against.
391
+ - The honest path: (1) the **thin read-only slice in Flow Agents** (§7.1) using the existing chain primitive,
392
+ which is cheap and reversible; (2) in parallel, take the **event-sourcing-vs-mutable-`transitions[]`** question
393
+ to Flow, because the right long-term home for an append-only `WorkflowRun` event log is *inside* `FlowRun`
394
+ (Phase D). If Flow adds the event log, Flow Agents consumes it and deletes its own — the cleanest legacy-free
395
+ end state and the one most faithful to ADR 0001.
396
+
397
+ ---
398
+
399
+ ## 9. Recommendation
400
+
401
+ 1. **Ship the read-only first slice (Phase A):** unify the timeline, emit `run-events.jsonl` (reusing the
402
+ `evidence-capture.js` hash-chain), add `trace` + `replay --at`. Delivers the owner's "recreate the session"
403
+ value now, fully reversible, no consumer/security change.
404
+ 2. **Treat the projection-fold == sidecar equality as the correctness oracle** for everything after.
405
+ 3. **Resolve §7.2 Q1 (where event-sourcing lives) with the owner + Flow before Phases B–C.** Because Flow already
406
+ owns Flow Runs and ships a `FlowRun` primitive, the likely-correct end state is *event-sourcing inside Flow's
407
+ `FlowRun`, consumed by Flow Agents* — not a permanent bespoke Flow Agents log.
408
+ 4. **Preserve ADR 0017 by construction:** keep the capture chain independent, make the gate key off the
409
+ re-derived projection (not stored status), protect `run-events.jsonl`, and add a "tampered-log-is-caught"
410
+ test to the anti-gaming suite. Sell event-sourcing as a *local integrity + observability* gain — **not** as a
411
+ replacement for the external CI anchor, which remains the real boundary (`docs/adr/0017-...md:113–130`).
412
+
413
+ ---
414
+
415
+ ## Appendix — key file:line references
416
+
417
+ - State machine: `src/cli/workflow-sidecar.ts:13` (statuses), `:14` (phases)
418
+ - `state.json` writes: `initSidecars` `:858–873`; `writeState` `:1109–1110`
419
+ - `advance-state` + route-back + terminal seal: `:1321–1374` (route-back `:1338–1348`, seal `:1368–1372`)
420
+ - Agent events (already append-only): `recordAgentEvent` `:917–931`
421
+ - Liveness stream (already append-only): `:2382–2387, 2417–2426`
422
+ - Trust bundle fold: `buildTrustBundle` `:260`; `writeTrustBundle` `:523–563`; Surface module `deriveClaimStatus` (~`:155–165`)
423
+ - Checkpoint seal / sign / deliver: `sealTrustCheckpoint` `:1489–1527`; `signCheckpointAttestation` `:1548+` (digest immutability `:1609`); `publishDelivery` `:1671+`
424
+ - Command-log hash chain (already append-only): `scripts/hooks/evidence-capture.js:12–26, 95–122, 357–384`
425
+ - Gate state/bundle reads: `scripts/hooks/stop-goal-fit.js:48–49, 502, 520, 544+`
426
+ - ADR 0001 (Flow owns Flow Runs): `docs/adr/0001-...md:30–36, 61`
427
+ - ADR 0010 (bundle fold; state.json stays lifecycle; Phase 4): `docs/adr/0010-...md:27, 36–46, 57–66, 113`
428
+ - ADR 0013 (projection-of-claims; gleaning): `docs/adr/0013-...md:54–65, 99–104`
429
+ - ADR 0017 (re-derive; chain; state.json forgeable; CI anchor): `docs/adr/0017-...md:33–48, 56–58, 61–90, 113–130`
430
+ - `@kontourai/flow` FlowRun primitive: `node_modules/@kontourai/flow/dist/index.d.ts` (exports); `FlowRunState` in `.../contracts/flow-types.d.ts`; `FLOW_RUN_LAYOUT` in `.../runtime/flow-files.js`
431
+ - Flow consumed today only for validation/routing: `src/flow-kit/validate.ts:94–115`
@@ -20,6 +20,7 @@ TESTS=(
20
20
  "evals/integration/test_captured_fail_reconciliation.sh"
21
21
  "evals/integration/test_command_log_integrity.sh"
22
22
  "evals/integration/test_command_log_fork_classification.sh"
23
+ "evals/integration/test_command_log_concurrency.sh"
23
24
  "evals/integration/test_resolvefirststep_security.sh"
24
25
  "evals/integration/test_enforcer_expects_driven.sh"
25
26
  "evals/integration/test_goal_fit_rederive.sh"
@@ -37,6 +37,7 @@ CHECKS=(
37
37
  "Flow agents statusline integration|bash evals/integration/test_flow_agents_statusline.sh"
38
38
  "Telemetry contract integration|bash evals/integration/test_telemetry.sh"
39
39
  "Telemetry doctor integration|bash evals/integration/test_telemetry_doctor.sh"
40
+ "Usage and cost integration|bash evals/integration/test_usage_cost.sh"
40
41
  "Utterance check integration|bash evals/integration/test_utterance_check.sh"
41
42
  "Pull work provider integration|bash evals/integration/test_pull_work_provider.sh"
42
43
  "Anti-gaming and trust suite|bash evals/ci/antigaming-suite.sh"
@@ -81,6 +82,7 @@ LANE_RUNTIME_AND_KIT=(
81
82
  "Flow agents statusline integration"
82
83
  "Telemetry contract integration"
83
84
  "Telemetry doctor integration"
85
+ "Usage and cost integration"
84
86
  "Utterance check integration"
85
87
  "Pull work provider integration"
86
88
  "Anti-gaming and trust suite"
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env bash
2
+ # test_command_log_concurrency.sh — concurrent captures must NOT fork the chain.
3
+ #
4
+ # Regression test for the benign-race that broke command-log.jsonl integrity:
5
+ # two capture processes writing to the SAME log concurrently each read the same
6
+ # prevHash and appended entries with an identical seq/prevHash, forking the
7
+ # hash-chain so the tamper-evidence verifier reported "broken" on honest work.
8
+ #
9
+ # evidence-capture.js now serializes the read→compute→append critical section
10
+ # with an atomic lockfile. This test launches many captures in parallel against
11
+ # one log and asserts:
12
+ # 1. Every launched entry is present (capture never drops a record).
13
+ # 2. seq values are unique and contiguous (no fork — the lock held).
14
+ # 3. verifyCommandLogChain() returns "ok" (chain verifies end-to-end).
15
+ # 4. No stale .lock file is left behind.
16
+ #
17
+ # Usage: bash evals/integration/test_command_log_concurrency.sh
18
+ set -uo pipefail
19
+
20
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
21
+ CAPTURE="$ROOT/scripts/hooks/evidence-capture.js"
22
+ GATE="$ROOT/scripts/hooks/stop-goal-fit.js"
23
+
24
+ TMP="$(mktemp -d)"
25
+ errors=0
26
+ _pass() { echo " ✓ $1"; }
27
+ _fail() { echo " ✗ $1"; errors=$((errors + 1)); }
28
+ cleanup() { rm -rf "$TMP"; }
29
+ trap cleanup EXIT
30
+
31
+ REPO="$TMP/repo"
32
+ SLUG="conc"
33
+ mkdir -p "$REPO/.flow-agents/$SLUG"
34
+ printf '# Repo\n' > "$REPO/AGENTS.md"
35
+ # Anchor the capture log to this slug. evidence-capture.js resolves the artifact
36
+ # dir via .flow-agents/current.json (active_slug) or the newest state.json; seed
37
+ # both so resolveArtifactDir() points at .flow-agents/<slug>/.
38
+ printf '{"active_slug":"%s","artifact_dir":".flow-agents/%s"}' "$SLUG" "$SLUG" \
39
+ > "$REPO/.flow-agents/current.json"
40
+ printf '%s' "{\"schema_version\":\"1.0\",\"task_slug\":\"$SLUG\",\"status\":\"in_progress\",\"phase\":\"build\",\"updated_at\":\"2026-06-23T00:00:00Z\",\"next_action\":{\"status\":\"in_progress\",\"summary\":\"work\"}}" \
41
+ > "$REPO/.flow-agents/$SLUG/state.json"
42
+
43
+ N=24
44
+ echo "Test: $N concurrent captures into one command-log must not fork the chain"
45
+
46
+ # Launch N capture processes in parallel against the same log. Each is a fresh
47
+ # process reading its event from stdin, exactly like the PostToolUse hook.
48
+ for i in $(seq 1 "$N"); do
49
+ printf '{"hook_event_name":"PostToolUse","tool_name":"Bash","cwd":"%s","tool_input":{"command":"echo cmd-%s"},"tool_response":{"exitCode":0,"stdout":"ok"}}' "$REPO" "$i" \
50
+ | node "$CAPTURE" >/dev/null 2>&1 &
51
+ done
52
+ wait
53
+
54
+ LOG="$REPO/.flow-agents/$SLUG/command-log.jsonl"
55
+
56
+ # 1. All N records present.
57
+ count=$(grep -c '' "$LOG" 2>/dev/null || echo 0)
58
+ if [[ "$count" -eq "$N" ]]; then
59
+ _pass "all $N records captured (none dropped)"
60
+ else
61
+ _fail "expected $N records, found $count"
62
+ fi
63
+
64
+ # 2. seq values unique and contiguous 0..N-1 (no fork).
65
+ seq_report=$(python3 - "$LOG" "$N" << 'PY'
66
+ import json, sys
67
+ log, n = sys.argv[1], int(sys.argv[2])
68
+ seqs = []
69
+ for line in open(log):
70
+ line = line.strip()
71
+ if not line:
72
+ continue
73
+ e = json.loads(line)
74
+ ch = e.get('_chain')
75
+ if not ch:
76
+ print("UNCHAINED") # an entry without a chain link = a gap = fork risk
77
+ sys.exit(0)
78
+ seqs.append(ch['seq'])
79
+ expected = list(range(n))
80
+ if sorted(seqs) == expected:
81
+ print("OK")
82
+ else:
83
+ dups = sorted({s for s in seqs if seqs.count(s) > 1})
84
+ print(f"BAD seqs={sorted(seqs)} dups={dups}")
85
+ PY
86
+ )
87
+ if [[ "$seq_report" == "OK" ]]; then
88
+ _pass "seq values unique and contiguous 0..$((N - 1)) — no fork"
89
+ else
90
+ _fail "seq integrity broken: $seq_report"
91
+ fi
92
+
93
+ # 3. The verifier confirms an intact chain.
94
+ chain_status=$(node -e "const g = require('$GATE'); console.log(g.verifyCommandLogChain('$REPO/.flow-agents/$SLUG').status);")
95
+ if [[ "$chain_status" == "ok" ]]; then
96
+ _pass "verifyCommandLogChain → ok under concurrency"
97
+ else
98
+ _fail "expected ok, got $chain_status"
99
+ fi
100
+
101
+ # 4. No stale lock left behind.
102
+ if [[ ! -e "$LOG.lock" ]]; then
103
+ _pass "lockfile cleaned up (no stale .lock)"
104
+ else
105
+ _fail "stale lockfile remains: $LOG.lock"
106
+ fi
107
+
108
+ echo ""
109
+ if [[ "$errors" -eq 0 ]]; then
110
+ echo "command-log concurrency test passed."
111
+ exit 0
112
+ fi
113
+ echo "command-log concurrency test FAILED: $errors issue(s)."
114
+ exit 1
@@ -790,6 +790,15 @@ echo "=== AC3.1 — Surface unavailable fail-closed ==="
790
790
  echo ""
791
791
  echo "--- AC3.1a: Isolated (no @kontourai/surface) with high-impact claim → BLOCKS ---"
792
792
 
793
+ # The gate imports the shared scripts/lib/command-log-chain.js helpers. A real
794
+ # install rsyncs the whole tree, so the lib always sits beside the hooks. Mirror that:
795
+ # the isolated gates live at "$TMP/surface-iso*/stop-goal-fit.js", so "../lib" resolves
796
+ # to "$TMP/lib" for both. This keeps the test exercising surface-unavailable fail-closed
797
+ # (not a spurious module-not-found crash).
798
+ ISO_LIBDIR="$TMP/lib"
799
+ mkdir -p "$ISO_LIBDIR"
800
+ cp "$ROOT/scripts/lib/command-log-chain.js" "$ISO_LIBDIR/"
801
+
793
802
  # Create isolated node context that can't find @kontourai/surface
794
803
  ISO_DIR="$TMP/surface-iso"
795
804
  mkdir -p "$ISO_DIR/repo/.flow-agents/surftest"
@@ -1014,13 +1023,19 @@ else
1014
1023
  fi
1015
1024
 
1016
1025
  echo ""
1017
- echo "--- AC3.3c: Both files use the SAME genesis constant value ---"
1018
- genesis_ec=$(grep "const CHAIN_GENESIS = " "$ROOT/scripts/hooks/evidence-capture.js" | sed "s/.*= '//;s/'.*//")
1019
- genesis_sg=$(grep "const CHAIN_GENESIS_VERIFY = " "$ROOT/scripts/hooks/stop-goal-fit.js" | sed "s/.*= '//;s/'.*//")
1020
- if [ "$genesis_ec" = "$genesis_sg" ] && [ -n "$genesis_ec" ]; then
1021
- _pass "AC3.3: Both files use the same genesis constant ($genesis_ec)"
1026
+ echo "--- AC3.3c: genesis is single-sourced and imported by writer + verifier (cannot diverge) ---"
1027
+ # Stronger than the old "two literals match" check: the genesis literal now lives in
1028
+ # exactly ONE module, and both the writer and verifier import it — so divergence is
1029
+ # structurally impossible rather than merely currently-equal.
1030
+ genesis_lib=$(grep "const CHAIN_GENESIS = " "$ROOT/scripts/lib/command-log-chain.js" | sed "s/.*= '//;s/'.*//")
1031
+ if [ -n "$genesis_lib" ] \
1032
+ && grep -q "require.*command-log-chain" "$ROOT/scripts/hooks/evidence-capture.js" \
1033
+ && grep -q "require.*command-log-chain" "$ROOT/scripts/hooks/stop-goal-fit.js" \
1034
+ && ! grep -qE "const CHAIN_GENESIS = '" "$ROOT/scripts/hooks/evidence-capture.js" \
1035
+ && ! grep -qE "const CHAIN_GENESIS_VERIFY = '" "$ROOT/scripts/hooks/stop-goal-fit.js"; then
1036
+ _pass "AC3.3: genesis single-sourced in scripts/lib/command-log-chain.js ($genesis_lib); writer + verifier import it, no divergent literal"
1022
1037
  else
1023
- _fail "AC3.3: Genesis constant mismatch: evidence-capture=$genesis_ec stop-goal-fit=$genesis_sg"
1038
+ _fail "AC3.3: genesis not single-sourced (lib='$genesis_lib') or a divergent literal remains in a consumer"
1024
1039
  fi
1025
1040
 
1026
1041