@kontourai/flow-agents 3.2.0 → 3.4.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.
Files changed (281) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +173 -0
  3. package/CHANGELOG.md +54 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/assignment-provider.js +10 -1
  11. package/build/src/cli/builder-run.d.ts +1 -0
  12. package/build/src/cli/builder-run.js +27 -0
  13. package/build/src/cli/effective-backlog-settings.js +70 -2
  14. package/build/src/cli/init.d.ts +34 -0
  15. package/build/src/cli/init.js +341 -61
  16. package/build/src/cli/kit.js +55 -12
  17. package/build/src/cli/pull-work-provider.js +346 -5
  18. package/build/src/cli/skill-drift-check.d.ts +1 -0
  19. package/build/src/cli/skill-drift-check.js +165 -0
  20. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  21. package/build/src/cli/telemetry-doctor.js +53 -6
  22. package/build/src/cli/validate-hook-influence.js +37 -7
  23. package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
  24. package/build/src/cli/workflow-sidecar.d.ts +310 -1
  25. package/build/src/cli/workflow-sidecar.js +1914 -126
  26. package/build/src/cli.js +5 -0
  27. package/build/src/flow-kit/validate.d.ts +54 -34
  28. package/build/src/flow-kit/validate.js +237 -26
  29. package/build/src/index.d.ts +2 -0
  30. package/build/src/index.js +1 -0
  31. package/build/src/lib/console-connect-options.d.ts +97 -0
  32. package/build/src/lib/console-connect-options.js +199 -0
  33. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  34. package/build/src/lib/console-telemetry-validate.js +91 -0
  35. package/build/src/lib/flow-resolver.d.ts +54 -1
  36. package/build/src/lib/flow-resolver.js +112 -5
  37. package/build/src/lib/fs.d.ts +17 -0
  38. package/build/src/lib/fs.js +172 -0
  39. package/build/src/lib/local-artifact-root.d.ts +44 -1
  40. package/build/src/lib/local-artifact-root.js +131 -3
  41. package/build/src/runtime-adapters.d.ts +39 -3
  42. package/build/src/runtime-adapters.js +77 -31
  43. package/build/src/tools/build-universal-bundles.js +40 -2
  44. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  45. package/build/src/tools/codex-agent-routing.js +49 -0
  46. package/build/src/tools/generate-context-map.js +1 -0
  47. package/build/src/tools/validate-source-tree.js +30 -3
  48. package/context/contracts/artifact-contract.md +16 -2
  49. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  50. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  51. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  52. package/context/scripts/hooks/workflow-steering.js +194 -22
  53. package/context/scripts/package.json +3 -0
  54. package/context/scripts/telemetry/install-console-config.sh +25 -4
  55. package/context/scripts/telemetry/lib/config.sh +102 -12
  56. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  57. package/context/scripts/telemetry/lib/session.sh +3 -0
  58. package/context/scripts/telemetry/lib/transport.sh +87 -0
  59. package/context/scripts/telemetry/lib/usage.sh +205 -4
  60. package/context/scripts/telemetry/telemetry.conf +6 -0
  61. package/context/scripts/telemetry/telemetry.sh +48 -0
  62. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  63. package/docs/agent-usage-feedback-loop.md +35 -0
  64. package/docs/architecture-engine-and-kits.md +110 -0
  65. package/docs/context-map.md +2 -0
  66. package/docs/coordination-guide.md +370 -0
  67. package/docs/decisions/agent-coordination.md +26 -9
  68. package/docs/decisions/embeddable-engine.md +152 -0
  69. package/docs/decisions/index.md +5 -3
  70. package/docs/decisions/trust-ledger-retention.md +88 -0
  71. package/docs/decisions/trust-reconcile.md +42 -9
  72. package/docs/decisions/workflow-enforcement.md +31 -9
  73. package/docs/fixture-ownership.md +6 -2
  74. package/docs/implementing-trust-reconciliation.md +129 -0
  75. package/docs/index.md +23 -9
  76. package/docs/integrations/flow-agents-console.md +275 -0
  77. package/docs/integrations/index.md +4 -0
  78. package/docs/kit-authoring-guide.md +52 -21
  79. package/docs/spec/builder-flow-runtime.md +80 -0
  80. package/docs/spec/runtime-hook-surface.md +45 -1
  81. package/docs/specs/economics-record-contract.md +270 -0
  82. package/docs/specs/harness-capability-matrix.md +74 -0
  83. package/docs/specs/learning-review-proposals-contract.md +340 -0
  84. package/docs/specs/routing-efficiency-review.md +59 -0
  85. package/docs/verifiable-trust.md +74 -25
  86. package/docs/workflow-artifact-lifecycle.md +38 -1
  87. package/docs/workflow-usage-guide.md +10 -0
  88. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  89. package/evals/ci/antigaming-suite.sh +2 -0
  90. package/evals/ci/run-baseline.sh +78 -4
  91. package/evals/fixtures/economics/acceptance.json +12 -0
  92. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  93. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  94. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  95. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  96. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  97. package/evals/fixtures/economics/critique.json +22 -0
  98. package/evals/fixtures/economics/expected-record.json +71 -0
  99. package/evals/fixtures/economics/session-usage-event.json +1 -0
  100. package/evals/fixtures/economics/state.json +11 -0
  101. package/evals/fixtures/economics/transcript.jsonl +3 -0
  102. package/evals/fixtures/hook-influence/cases.json +7 -7
  103. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  104. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  105. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  106. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  108. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  116. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  117. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  118. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  119. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  120. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  121. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  122. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  123. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  124. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  125. package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
  126. package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
  127. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  128. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  129. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  130. package/evals/integration/test_builder_step_producers.sh +18 -10
  131. package/evals/integration/test_bundle_install.sh +172 -0
  132. package/evals/integration/test_checkpoint_signing.sh +10 -2
  133. package/evals/integration/test_ci_actor_identity.sh +221 -0
  134. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  135. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  136. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  137. package/evals/integration/test_economics_record.sh +674 -0
  138. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  139. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  140. package/evals/integration/test_exemption_usage_review.sh +198 -0
  141. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  142. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  143. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  144. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  145. package/evals/integration/test_gate_lockdown.sh +7 -0
  146. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  147. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  148. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  149. package/evals/integration/test_hook_influence_cases.sh +25 -1
  150. package/evals/integration/test_install_merge.sh +227 -2
  151. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  152. package/evals/integration/test_learning_review_proposals.sh +329 -0
  153. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  154. package/evals/integration/test_liveness_console_relay.sh +166 -0
  155. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  156. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  157. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  158. package/evals/integration/test_publish_delivery.sh +389 -2
  159. package/evals/integration/test_pull_work_board.sh +200 -0
  160. package/evals/integration/test_pull_work_provider.sh +1 -1
  161. package/evals/integration/test_reconcile_preflight.sh +304 -0
  162. package/evals/integration/test_record_check.sh +378 -0
  163. package/evals/integration/test_routing_efficiency.sh +71 -0
  164. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  165. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  166. package/evals/integration/test_skill_drift_check.sh +870 -0
  167. package/evals/integration/test_takeover_protocol.sh +340 -0
  168. package/evals/integration/test_telemetry.sh +445 -0
  169. package/evals/integration/test_telemetry_doctor.sh +66 -0
  170. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  171. package/evals/integration/test_trust_reconcile_negatives.sh +121 -13
  172. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  173. package/evals/integration/test_usage_cost.sh +61 -0
  174. package/evals/integration/test_verify_hold.sh +910 -0
  175. package/evals/integration/test_veritas_governance_kit.sh +257 -0
  176. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
  177. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  178. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  179. package/evals/integration/test_workspace_settings.sh +176 -0
  180. package/evals/lib/env.sh +26 -0
  181. package/evals/lib/node.sh +8 -0
  182. package/evals/run.sh +37 -0
  183. package/evals/static/test_ci_integration_coverage.sh +115 -0
  184. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  185. package/evals/static/test_universal_bundles.sh +34 -0
  186. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  187. package/evals/static/test_workflow_skills.sh +1 -1
  188. package/kits/builder/flows/build.flow.json +9 -18
  189. package/kits/builder/flows/publish-learn.flow.json +5 -1
  190. package/kits/builder/kit.json +120 -0
  191. package/kits/builder/skills/continue-work/SKILL.md +2 -0
  192. package/kits/builder/skills/deliver/SKILL.md +115 -0
  193. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  194. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  195. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  196. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  197. package/kits/builder/skills/pull-work/SKILL.md +33 -2
  198. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  199. package/kits/knowledge/kit.json +9 -0
  200. package/kits/veritas-governance/docs/README.md +113 -7
  201. package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
  202. package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
  203. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  204. package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
  205. package/kits/veritas-governance/kit.json +19 -0
  206. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  207. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  208. package/package.json +2 -2
  209. package/packaging/manifest.json +29 -0
  210. package/schemas/backlog-provider-settings.schema.json +13 -0
  211. package/schemas/workflow-state.schema.json +44 -0
  212. package/scripts/README.md +4 -0
  213. package/scripts/check-content-boundary.cjs +8 -1
  214. package/scripts/ci/trust-reconcile.js +214 -253
  215. package/scripts/hooks/codex-hook-adapter.js +77 -2
  216. package/scripts/hooks/evidence-capture.js +38 -5
  217. package/scripts/hooks/lib/actor-identity.js +82 -0
  218. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  219. package/scripts/hooks/lib/kit-catalog.js +235 -0
  220. package/scripts/hooks/lib/liveness-write.js +28 -1
  221. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  222. package/scripts/hooks/lib/runnable-command.js +177 -0
  223. package/scripts/hooks/lib/skill-drift.js +350 -0
  224. package/scripts/hooks/stop-goal-fit.js +278 -48
  225. package/scripts/hooks/workflow-steering.js +194 -22
  226. package/scripts/install-codex-home.sh +97 -47
  227. package/scripts/install-merge.js +72 -14
  228. package/scripts/install-owned-files.js +178 -0
  229. package/scripts/lib/reconcile-shape.js +381 -0
  230. package/scripts/liveness/relay.sh +84 -0
  231. package/scripts/telemetry/economics-record.schema.json +145 -0
  232. package/scripts/telemetry/economics-record.sh +331 -0
  233. package/scripts/telemetry/install-console-config.sh +25 -4
  234. package/scripts/telemetry/learning-review-decide.sh +124 -0
  235. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  236. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  237. package/scripts/telemetry/lib/config.sh +102 -12
  238. package/scripts/telemetry/lib/pricing.sh +14 -6
  239. package/scripts/telemetry/lib/session.sh +3 -0
  240. package/scripts/telemetry/lib/transport.sh +133 -15
  241. package/scripts/telemetry/lib/usage.sh +121 -28
  242. package/scripts/telemetry/routing-efficiency.sh +0 -0
  243. package/scripts/telemetry/telemetry.conf +6 -0
  244. package/scripts/telemetry/telemetry.sh +48 -0
  245. package/src/builder-flow-run-adapter.ts +357 -0
  246. package/src/builder-flow-runtime.ts +348 -0
  247. package/src/cli/assignment-provider.ts +12 -1
  248. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  249. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  250. package/src/cli/builder-run.ts +28 -0
  251. package/src/cli/codex-agent-routing.test.mjs +44 -0
  252. package/src/cli/codex-exit-code.test.mjs +207 -0
  253. package/src/cli/console-connect-options.test.mjs +329 -0
  254. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  255. package/src/cli/effective-backlog-settings.ts +68 -2
  256. package/src/cli/flow-resolver-composition.test.mjs +72 -0
  257. package/src/cli/init.test.mjs +161 -0
  258. package/src/cli/init.ts +407 -62
  259. package/src/cli/kit-metadata-security.test.mjs +443 -0
  260. package/src/cli/kit.ts +50 -12
  261. package/src/cli/pull-work-provider.ts +377 -3
  262. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  263. package/src/cli/skill-drift-check.ts +196 -0
  264. package/src/cli/telemetry-doctor.test.mjs +53 -0
  265. package/src/cli/telemetry-doctor.ts +50 -7
  266. package/src/cli/validate-hook-influence.ts +37 -6
  267. package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
  268. package/src/cli/workflow-sidecar.ts +1980 -119
  269. package/src/cli.ts +5 -0
  270. package/src/flow-kit/validate.ts +277 -38
  271. package/src/index.ts +19 -0
  272. package/src/lib/console-connect-options.ts +261 -0
  273. package/src/lib/console-telemetry-validate.ts +88 -0
  274. package/src/lib/flow-resolver.ts +117 -4
  275. package/src/lib/fs.ts +160 -0
  276. package/src/lib/local-artifact-root.ts +129 -3
  277. package/src/runtime-adapters.ts +113 -33
  278. package/src/tools/build-universal-bundles.ts +36 -2
  279. package/src/tools/codex-agent-routing.ts +48 -0
  280. package/src/tools/generate-context-map.ts +1 -0
  281. package/src/tools/validate-source-tree.ts +29 -3
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env bash
2
+ # test_record_check.sh — integration eval for `workflow-sidecar record-check` (#380, AC7).
3
+ #
4
+ # Proves:
5
+ # 1. `record-check <dir> -- <command>` RUNS the command and records a kind:"command" check
6
+ # with a real execution.label, composed losslessly into the trust.bundle (through the same
7
+ # readBundleState + mergeChecksById + writeTrustBundle path every other writer uses).
8
+ # 2. A passing command → exit 0, claim status "verified" (Surface deriveClaimStatus).
9
+ # 3. A failing command → exit NONZERO from record-check itself (loud, not silently swallowed)
10
+ # AND a claim status "disputed" is still recorded (the failure is captured, not dropped).
11
+ # 4. A prior check recorded by record-evidence survives a subsequent record-check call
12
+ # (compose-safe, not a parallel bundle-writing implementation — #298/#270 class).
13
+ # 5. `--command "<shell string>"` is accepted as an alternative to `-- <command...>` and is
14
+ # rejected when the string is not a runnable shell command (#412 parity with
15
+ # record-gate-claim --command).
16
+ # 6. record-check is reachable via the REAL CLI entry point (main()'s switch), not just as an
17
+ # exported function — this guards against the "added the function but forgot to wire it
18
+ # into the switch" stop-short risk called out in the delivery plan.
19
+ #
20
+ # Deterministic, no model spend, self-cleaning.
21
+ # Usage: bash evals/integration/test_record_check.sh
22
+
23
+ set -uo pipefail
24
+
25
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
26
+ source "$ROOT/evals/lib/node.sh"
27
+
28
+ WRITER="workflow-sidecar"
29
+ TMPDIR_EVAL="$(mktemp -d)"
30
+ errors=0
31
+
32
+ cleanup() { rm -rf "$TMPDIR_EVAL"; }
33
+ trap cleanup EXIT
34
+
35
+ _pass() { echo " ✓ $1"; }
36
+ _fail() { echo " ✗ $1"; errors=$((errors + 1)); }
37
+
38
+ SESSION_ROOT="$TMPDIR_EVAL/repo/.kontourai/flow-agents"
39
+ SLUG="record-check-eval"
40
+ SESSION_DIR="$SESSION_ROOT/$SLUG"
41
+ mkdir -p "$SESSION_ROOT"
42
+
43
+ flow_agents_node "$WRITER" ensure-session \
44
+ --artifact-root "$SESSION_ROOT" \
45
+ --task-slug "$SLUG" \
46
+ --title "record-check eval" \
47
+ --source-request "Integration eval for record-check (#380)." \
48
+ --summary "Seed session for record-check assertions." \
49
+ --criterion "record-check runs a command and records a capture-backed check" \
50
+ --timestamp "2026-07-05T09:00:00Z" >"$TMPDIR_EVAL/seed.out" 2>"$TMPDIR_EVAL/seed.err"
51
+
52
+ if [[ ! -d "$SESSION_DIR" ]]; then
53
+ _fail "eval setup: ensure-session did not create the session dir: $(cat "$TMPDIR_EVAL/seed.out" "$TMPDIR_EVAL/seed.err")"
54
+ echo ""
55
+ echo "$errors check(s) failed."
56
+ exit 1
57
+ fi
58
+
59
+ # ─── 1/2/6: record-check <dir> -- <command> (pass path), via the REAL CLI entry point ────────
60
+ if flow_agents_node "$WRITER" record-check "$SESSION_DIR" --timestamp "2026-07-05T09:01:00Z" -- echo record-check-eval-ok \
61
+ >"$TMPDIR_EVAL/rc-pass.out" 2>"$TMPDIR_EVAL/rc-pass.err"; then
62
+ _pass "record-check (pass path) exits 0 for a succeeding command, via the real CLI switch dispatch"
63
+ else
64
+ _fail "record-check (pass path) unexpectedly failed: $(cat "$TMPDIR_EVAL/rc-pass.out" "$TMPDIR_EVAL/rc-pass.err")"
65
+ fi
66
+
67
+ if [[ -f "$SESSION_DIR/trust.bundle" ]] && node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/rc-pass-assert.err"
68
+ import { readFileSync } from 'node:fs';
69
+ const bundle = JSON.parse(readFileSync('${SESSION_DIR}/trust.bundle', 'utf8'));
70
+ const claim = bundle.claims.find((c) => (c.metadata || {}).check_kind === 'command' && c.fieldOrBehavior && c.fieldOrBehavior.includes('echo record-check-eval-ok'));
71
+ if (!claim) { process.stderr.write('no kind:"command" claim found for the echo command\n'); process.exit(1); }
72
+ if (claim.status !== 'verified') { process.stderr.write('expected claim status verified for a passing command, got ' + claim.status + '\n'); process.exit(1); }
73
+ const ev = bundle.evidence.find((e) => e.claimId === claim.id);
74
+ if (!ev || !ev.execution || ev.execution.label !== 'echo record-check-eval-ok') { process.stderr.write('execution.label was not set to the executed command: ' + JSON.stringify(ev && ev.execution) + '\n'); process.exit(1); }
75
+ if (ev.execution.isError !== false) { process.stderr.write('execution.isError should be false for a passing command\n'); process.exit(1); }
76
+ NODEOF
77
+ then
78
+ _pass "AC7: record-check (pass) records a kind:\"command\" check with real execution.label and claim status verified"
79
+ else
80
+ _fail "AC7: record-check (pass) bundle assertion failed: $(cat "$TMPDIR_EVAL/rc-pass-assert.err")"
81
+ fi
82
+
83
+ # ─── #270/#380 MEDIUM fix: record-check persists a REAL sha256 digest of the captured output ──
84
+ # onto claim.metadata.output_digest, never the raw output text (secret-safe by construction).
85
+ # The executed command's TEXT ("rc-digest-output-8f2a1c9b3d" piped through base64) is deliberately DIFFERENT
86
+ # from its OUTPUT TEXT ("cmMtZGlnZXN0LW91dHB1dC04ZjJhMWM5YjNk") so that asserting the output text is absent from the
87
+ # bundle cannot be confused with the (legitimate, expected) presence of the command text itself
88
+ # in execution.label/fieldOrBehavior.
89
+ if flow_agents_node "$WRITER" record-check "$SESSION_DIR" --timestamp "2026-07-05T09:01:30Z" --id digest-check --command "printf 'rc-digest-output-8f2a1c9b3d' | base64" \
90
+ >"$TMPDIR_EVAL/rc-digest.out" 2>"$TMPDIR_EVAL/rc-digest.err"; then
91
+ _pass "record-check (digest eval command) exits 0"
92
+ else
93
+ _fail "record-check (digest eval command) unexpectedly failed: $(cat "$TMPDIR_EVAL/rc-digest.out" "$TMPDIR_EVAL/rc-digest.err")"
94
+ fi
95
+
96
+ if [[ -f "$SESSION_DIR/trust.bundle" ]] && node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/rc-digest-assert.err"
97
+ import { readFileSync } from 'node:fs';
98
+ const bundleText = readFileSync('${SESSION_DIR}/trust.bundle', 'utf8');
99
+ const bundle = JSON.parse(bundleText);
100
+ const claim = bundle.claims.find((c) => c.subjectId && c.subjectId.endsWith('/digest-check'));
101
+ if (!claim) { process.stderr.write('no claim found for the digest-check command\n'); process.exit(1); }
102
+ const od = (claim.metadata || {}).output_digest;
103
+ if (!od || typeof od !== 'object') { process.stderr.write('claim.metadata.output_digest is missing: ' + JSON.stringify(claim.metadata) + '\n'); process.exit(1); }
104
+ if (od.algorithm !== 'sha256') { process.stderr.write('expected algorithm sha256, got ' + od.algorithm + '\n'); process.exit(1); }
105
+ if (typeof od.hex !== 'string' || !/^[0-9a-f]{64}$/.test(od.hex)) { process.stderr.write('output_digest.hex is not a 64-char lowercase hex sha256 digest: ' + JSON.stringify(od.hex) + '\n'); process.exit(1); }
106
+ const expectedHex = '163efc60e68c74a22225e8844f8a9f8b7d9ed30b2424bdbf939a3e41085348fe';
107
+ if (od.hex !== expectedHex) { process.stderr.write('output_digest.hex does not match the expected sha256 of the captured output: got ' + od.hex + ' expected ' + expectedHex + '\n'); process.exit(1); }
108
+ // Secret-safety: the RAW output text must never appear anywhere in the persisted bundle text —
109
+ // only its digest may. (The command TEXT itself legitimately appears in execution.label/summary;
110
+ // that is NOT what this assertion is checking.)
111
+ if (bundleText.includes('cmMtZGlnZXN0LW91dHB1dC04ZjJhMWM5YjNk')) { process.stderr.write('SECURITY REGRESSION: the raw captured output text was found in trust.bundle — output_digest must be a hash only, never raw output\n'); process.exit(1); }
112
+ NODEOF
113
+ then
114
+ _pass "#270/#380: record-check persists a real 64-hex sha256 digest of the captured output onto claim.metadata.output_digest, and the raw output text is never persisted (secret-safe by construction)"
115
+ else
116
+ _fail "#270/#380: record-check digest assertion failed: $(cat "$TMPDIR_EVAL/rc-digest-assert.err")"
117
+ fi
118
+
119
+ # ─── 3: record-check <dir> -- <command> (fail path) ──────────────────────────────────────────
120
+ if flow_agents_node "$WRITER" record-check "$SESSION_DIR" --timestamp "2026-07-05T09:02:00Z" -- false \
121
+ >"$TMPDIR_EVAL/rc-fail.out" 2>"$TMPDIR_EVAL/rc-fail.err"; then
122
+ _fail "record-check (fail path) should exit nonzero for a failing command, but exited 0"
123
+ else
124
+ _pass "record-check (fail path) exits nonzero (loud) for a failing command, never silently swallowed"
125
+ fi
126
+
127
+ if [[ -f "$SESSION_DIR/trust.bundle" ]] && node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/rc-fail-assert.err"
128
+ import { readFileSync } from 'node:fs';
129
+ const bundle = JSON.parse(readFileSync('${SESSION_DIR}/trust.bundle', 'utf8'));
130
+ const claim = bundle.claims.find((c) => (c.metadata || {}).check_kind === 'command' && c.fieldOrBehavior && c.fieldOrBehavior.includes('false'));
131
+ if (!claim) { process.stderr.write('no kind:"command" claim found for the false command\n'); process.exit(1); }
132
+ if (claim.status !== 'disputed') { process.stderr.write('expected claim status disputed for a failing command, got ' + claim.status + '\n'); process.exit(1); }
133
+ const ev = bundle.evidence.find((e) => e.claimId === claim.id);
134
+ if (!ev || !ev.execution || ev.execution.isError !== true) { process.stderr.write('execution.isError should be true for a failing command\n'); process.exit(1); }
135
+ NODEOF
136
+ then
137
+ _pass "AC7: record-check (fail) records a kind:\"command\" check with status fail/disputed — the failure is captured, not dropped"
138
+ else
139
+ _fail "AC7: record-check (fail) bundle assertion failed: $(cat "$TMPDIR_EVAL/rc-fail-assert.err")"
140
+ fi
141
+
142
+ # ─── 4: a PRIOR check (recorded by record-evidence) survives a SUBSEQUENT record-check call ───
143
+ if flow_agents_node "$WRITER" record-evidence "$SESSION_DIR" \
144
+ --verdict pass \
145
+ --check-json '{"id":"prior-evidence-check","kind":"test","status":"pass","summary":"Prior evidence check passed."}' \
146
+ --timestamp "2026-07-05T09:03:00Z" >"$TMPDIR_EVAL/prior-evidence.out" 2>"$TMPDIR_EVAL/prior-evidence.err"; then
147
+ _pass "eval setup: record-evidence seeds a prior check before the compose-safety assertion"
148
+ else
149
+ _fail "eval setup: record-evidence failed: $(cat "$TMPDIR_EVAL/prior-evidence.out" "$TMPDIR_EVAL/prior-evidence.err")"
150
+ fi
151
+
152
+ if flow_agents_node "$WRITER" record-check "$SESSION_DIR" --timestamp "2026-07-05T09:04:00Z" -- echo second-record-check-call \
153
+ >"$TMPDIR_EVAL/rc-second.out" 2>"$TMPDIR_EVAL/rc-second.err"; then
154
+ _pass "record-check (second call) succeeds after a prior record-evidence call"
155
+ else
156
+ _fail "record-check (second call) unexpectedly failed: $(cat "$TMPDIR_EVAL/rc-second.out" "$TMPDIR_EVAL/rc-second.err")"
157
+ fi
158
+
159
+ if node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/rc-compose-assert.err"
160
+ import { readFileSync } from 'node:fs';
161
+ const bundle = JSON.parse(readFileSync('${SESSION_DIR}/trust.bundle', 'utf8'));
162
+ const claims = bundle.claims;
163
+ const priorCheck = claims.find((c) => c.subjectId && c.subjectId.endsWith('/prior-evidence-check'));
164
+ const passCheck = claims.find((c) => (c.metadata || {}).check_kind === 'command' && c.fieldOrBehavior && c.fieldOrBehavior.includes('echo record-check-eval-ok'));
165
+ const failCheck = claims.find((c) => (c.metadata || {}).check_kind === 'command' && c.fieldOrBehavior && c.fieldOrBehavior.includes('false'));
166
+ const secondCheck = claims.find((c) => (c.metadata || {}).check_kind === 'command' && c.fieldOrBehavior && c.fieldOrBehavior.includes('echo second-record-check-call'));
167
+ if (!priorCheck) { process.stderr.write('prior record-evidence check was LOST after a subsequent record-check call (#298/#270 compose-safety regression)\n'); process.exit(1); }
168
+ if (!passCheck || !failCheck || !secondCheck) { process.stderr.write('an earlier record-check claim was lost after a later record-check call\n'); process.exit(1); }
169
+ NODEOF
170
+ then
171
+ _pass "record-check is compose-safe: a prior record-evidence check AND earlier record-check claims all survive a subsequent record-check call"
172
+ else
173
+ _fail "record-check compose-safety assertion failed: $(cat "$TMPDIR_EVAL/rc-compose-assert.err")"
174
+ fi
175
+
176
+ # ─── 5: --command "<shell string>" parity flag; rejects prose (#412) ─────────────────────────
177
+ if flow_agents_node "$WRITER" record-check "$SESSION_DIR" --timestamp "2026-07-05T09:05:00Z" --command "git rev-parse HEAD" \
178
+ >"$TMPDIR_EVAL/rc-command-flag.out" 2>"$TMPDIR_EVAL/rc-command-flag.err"; then
179
+ _pass "record-check --command \"<shell string>\" (parity flag with record-gate-claim) succeeds"
180
+ else
181
+ _fail "record-check --command failed: $(cat "$TMPDIR_EVAL/rc-command-flag.out" "$TMPDIR_EVAL/rc-command-flag.err")"
182
+ fi
183
+
184
+ if flow_agents_node "$WRITER" record-check "$SESSION_DIR" --timestamp "2026-07-05T09:06:00Z" --command "Manually confirmed the output looks correct." \
185
+ >"$TMPDIR_EVAL/rc-command-prose.out" 2>"$TMPDIR_EVAL/rc-command-prose.err"; then
186
+ _fail "record-check --command should reject prose, but accepted it"
187
+ elif grep -qi "not a runnable shell command" "$TMPDIR_EVAL/rc-command-prose.out" "$TMPDIR_EVAL/rc-command-prose.err"; then
188
+ _pass "record-check --command rejects a prose value at record time (#412 parity with record-gate-claim --command)"
189
+ else
190
+ _fail "record-check --command prose rejection message was unexpected: $(cat "$TMPDIR_EVAL/rc-command-prose.out" "$TMPDIR_EVAL/rc-command-prose.err")"
191
+ fi
192
+
193
+ # ─── Usage error: neither `-- <command>` nor --command supplied ─────────────────────────────
194
+ if flow_agents_node "$WRITER" record-check "$SESSION_DIR" --timestamp "2026-07-05T09:07:00Z" \
195
+ >"$TMPDIR_EVAL/rc-no-command.out" 2>"$TMPDIR_EVAL/rc-no-command.err"; then
196
+ _fail "record-check with no command at all should die with a usage error, but exited 0"
197
+ elif grep -qi "requires a command" "$TMPDIR_EVAL/rc-no-command.out" "$TMPDIR_EVAL/rc-no-command.err"; then
198
+ _pass "record-check with neither -- <command> nor --command dies with an actionable usage error"
199
+ else
200
+ _fail "record-check no-command error message was unexpected: $(cat "$TMPDIR_EVAL/rc-no-command.out" "$TMPDIR_EVAL/rc-no-command.err")"
201
+ fi
202
+
203
+ # ─── #362 AC6/AC7: record-check's ambiguous-absence-command classification ──────────────────
204
+ # A bare (non-negated, non-count-asserted, non-chained) grep/diff invocation whose exit code is
205
+ # exactly 1 is exit-code-ambiguous (zero matches could be the author's intended PASS for an
206
+ # absence check, or an unintended miss for a presence check) — record-check stamps a distinct
207
+ # "not_verified" status (never silently pass, never silently fail) and surfaces a loud, actionable
208
+ # stderr note, but does NOT itself exit nonzero (the command DID run; this is not a tool error).
209
+ FIXTURE_FILE="$TMPDIR_EVAL/repo/ac6-fixture.txt"
210
+ echo "some content with no matching pattern here" >"$FIXTURE_FILE"
211
+
212
+ # AC6: bare `grep` exit 1 (pattern genuinely absent) → recorded status not_verified + loud stderr
213
+ # note, record-check itself exits 0 (never hard-fail on an ambiguous, not a tool-error, outcome).
214
+ if flow_agents_node "$WRITER" record-check "$SESSION_DIR" --timestamp "2026-07-05T09:08:00Z" --id ac6-ambiguous-grep -- grep -E 'this-pattern-does-not-exist-anywhere' "$FIXTURE_FILE" >"$TMPDIR_EVAL/rc-ambiguous.out" 2>"$TMPDIR_EVAL/rc-ambiguous.err"; then
215
+ _pass "AC6: record-check with a bare grep exit-1 (ambiguous absence) exits 0, never hard-fails"
216
+ else
217
+ _fail "AC6 REGRESSION: record-check with a bare grep exit-1 (ambiguous absence) should exit 0 (never a tool error, never hard-fail), but exited nonzero: $(cat "$TMPDIR_EVAL/rc-ambiguous.out" "$TMPDIR_EVAL/rc-ambiguous.err")"
218
+ fi
219
+
220
+ if grep -qi "ambiguous" "$TMPDIR_EVAL/rc-ambiguous.err" && grep -qi "not_verified" "$TMPDIR_EVAL/rc-ambiguous.err"; then
221
+ _pass "AC6: record-check emits a loud stderr note naming both 'ambiguous' and the recorded 'not_verified' status for a bare grep exit-1"
222
+ else
223
+ _fail "AC6: record-check's ambiguous stderr note was missing expected substrings ('ambiguous' and 'not_verified'): $(cat "$TMPDIR_EVAL/rc-ambiguous.err")"
224
+ fi
225
+
226
+ # AC7: the same stderr note nudges toward a self-asserting rewrite ('! grep ...' / 'grep -c ... | grep -qx 0').
227
+ if grep -qi "self-asserting" "$TMPDIR_EVAL/rc-ambiguous.err"; then
228
+ _pass "AC7: record-check's ambiguous stderr note nudges toward a self-asserting recorded-command form"
229
+ else
230
+ _fail "AC7: record-check's ambiguous stderr note did not mention 'self-asserting': $(cat "$TMPDIR_EVAL/rc-ambiguous.err")"
231
+ fi
232
+
233
+ if node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/rc-ambiguous-assert.err"
234
+ import { readFileSync } from 'node:fs';
235
+ const bundle = JSON.parse(readFileSync('${SESSION_DIR}/trust.bundle', 'utf8'));
236
+ const claim = bundle.claims.find((c) => c.subjectId && c.subjectId.endsWith('/ac6-ambiguous-grep'));
237
+ if (!claim) { process.stderr.write('no claim found for the ac6-ambiguous-grep check\n'); process.exit(1); }
238
+ if (claim.value !== 'not_verified') { process.stderr.write('expected claim value not_verified for an ambiguous bare-grep exit-1, got ' + claim.value + '\n'); process.exit(1); }
239
+ NODEOF
240
+ then
241
+ _pass "AC6: record-check stamps check status not_verified (not a new 'ambiguous' enum value — mapped onto the existing not_verified status per the plan/ADR 0008/0010) for a bare grep exit-1"
242
+ else
243
+ _fail "AC6: record-check ambiguous-status bundle assertion failed: $(cat "$TMPDIR_EVAL/rc-ambiguous-assert.err")"
244
+ fi
245
+
246
+ # Regression guard: a SELF-ASSERTING form (`! grep ...`, negation flips intent into the command's
247
+ # own exit code) exiting 0 records a clean pass — never misclassified ambiguous just because the
248
+ # underlying binary is grep. Uses the NATURAL `--command '! grep ...'` form (not a `bash -lc`
249
+ # workaround) — isRunnableCommandText now strips a leading `!` before evaluating runnability
250
+ # (coherence fix: the ambiguous-absence advisory's own suggested remediation must not itself be
251
+ # rejected by the #412 runnability guard), so `--command` accepts this literal negated form.
252
+ if flow_agents_node "$WRITER" record-check "$SESSION_DIR" --timestamp "2026-07-05T09:08:30Z" --id ac6-self-asserting-negation --command "! grep -E 'this-pattern-does-not-exist-anywhere' $FIXTURE_FILE" \
253
+ >"$TMPDIR_EVAL/rc-negation.out" 2>"$TMPDIR_EVAL/rc-negation.err"; then
254
+ _pass "record-check --command '! grep ...' (natural self-asserting negated form) exits 0, as expected for a plain pass"
255
+ else
256
+ _fail "record-check --command '! grep ...' unexpectedly failed: $(cat "$TMPDIR_EVAL/rc-negation.out" "$TMPDIR_EVAL/rc-negation.err")"
257
+ fi
258
+
259
+ if grep -qi "not a runnable shell command" "$TMPDIR_EVAL/rc-negation.out" "$TMPDIR_EVAL/rc-negation.err"; then
260
+ _fail "REGRESSION: record-check --command '! grep ...' was rejected as non-runnable — isRunnableCommandText's leading-'!' strip is not working"
261
+ else
262
+ _pass "record-check --command '! grep ...' is ACCEPTED by the runnability guard (leading '!' is stripped before evaluation)"
263
+ fi
264
+
265
+ if node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/rc-negation-assert.err"
266
+ import { readFileSync } from 'node:fs';
267
+ const bundle = JSON.parse(readFileSync('${SESSION_DIR}/trust.bundle', 'utf8'));
268
+ const claim = bundle.claims.find((c) => c.subjectId && c.subjectId.endsWith('/ac6-self-asserting-negation'));
269
+ if (!claim) { process.stderr.write('no claim found for the ac6-self-asserting-negation check\n'); process.exit(1); }
270
+ if (claim.value !== 'pass') { process.stderr.write('expected claim value pass for a self-asserting negated grep, got ' + claim.value + '\n'); process.exit(1); }
271
+ NODEOF
272
+ then
273
+ _pass "regression guard: a self-asserting '! grep ...' form records a clean pass, never misclassified ambiguous"
274
+ else
275
+ _fail "regression guard assertion failed: $(cat "$TMPDIR_EVAL/rc-negation-assert.err")"
276
+ fi
277
+
278
+ # AC5-parity regression guard: bare grep exit >= 2 (a real tool error — missing file) still hard
279
+ # FAILs (nonzero record-check exit), proving the exit-1-only carve-out is narrowly scoped.
280
+ if flow_agents_node "$WRITER" record-check "$SESSION_DIR" --timestamp "2026-07-05T09:09:00Z" --id ac6-exit2-hard-fail -- grep -E 'pattern' "$TMPDIR_EVAL/this-file-does-not-exist-ac6" >"$TMPDIR_EVAL/rc-exit2.out" 2>"$TMPDIR_EVAL/rc-exit2.err"; then
281
+ _fail "REGRESSION: record-check with a bare grep exit >= 2 (missing file, a real tool error) should hard-fail (nonzero exit), but exited 0"
282
+ else
283
+ _pass "record-check with a bare grep exit >= 2 (missing file) still hard-fails (nonzero exit) — the exit-1-only carve-out does not weaken real tool-error detection"
284
+ fi
285
+
286
+ if node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/rc-exit2-assert.err"
287
+ import { readFileSync } from 'node:fs';
288
+ const bundle = JSON.parse(readFileSync('${SESSION_DIR}/trust.bundle', 'utf8'));
289
+ const claim = bundle.claims.find((c) => c.subjectId && c.subjectId.endsWith('/ac6-exit2-hard-fail'));
290
+ if (!claim) { process.stderr.write('no claim found for the ac6-exit2-hard-fail check\n'); process.exit(1); }
291
+ if (claim.value !== 'fail') { process.stderr.write('expected claim value fail for a bare grep exit >= 2, got ' + claim.value + '\n'); process.exit(1); }
292
+ NODEOF
293
+ then
294
+ _pass "record-check with a bare grep exit >= 2 records a hard fail claim value (not ambiguous, not_verified)"
295
+ else
296
+ _fail "exit>=2 hard-fail claim-value assertion failed: $(cat "$TMPDIR_EVAL/rc-exit2-assert.err")"
297
+ fi
298
+
299
+ # ─── Mutation test (#362 AC6 recordCheck ambiguity branch): temporarily disable recordCheck's ──
300
+ # `ambiguous` computation in a SCRATCH COPY of the compiled build/ output, confirm the AC6 bare-
301
+ # grep-exit-1 fixture above now records status "fail" (not "not_verified") against that mutated
302
+ # binary (eval "goes red" without the branch), then restore the original compiled file
303
+ # immediately. Proves the eval is actually exercising this specific classification branch, not
304
+ # passing vacuously for an unrelated reason.
305
+ DIST_SIDECAR="$ROOT/build/src/cli/workflow-sidecar.js"
306
+ AMBIGUOUS_SCRATCH="$TMPDIR_EVAL/ambiguous-mutation-scratch"
307
+ mkdir -p "$AMBIGUOUS_SCRATCH"
308
+
309
+ if [[ -f "$DIST_SIDECAR" ]]; then
310
+ cp "$DIST_SIDECAR" "$AMBIGUOUS_SCRATCH/workflow-sidecar.orig.js"
311
+ node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/ambiguous-mutation-patch.err"
312
+ import { readFileSync, writeFileSync } from 'node:fs';
313
+ const file = '${AMBIGUOUS_SCRATCH}/workflow-sidecar.orig.js';
314
+ let src = readFileSync(file, 'utf8');
315
+ const needle = 'const ambiguous = exitCode === 1 && isAmbiguousAbsenceCommand(displayCommand ?? "");';
316
+ if (!src.includes(needle)) { process.stderr.write('mutation: recordCheck ambiguous-branch text not found — source pattern drifted, cannot mutation-test\n'); process.exit(1); }
317
+ src = src.split(needle).join('const ambiguous = false;');
318
+ writeFileSync('${AMBIGUOUS_SCRATCH}/workflow-sidecar.mutated.js', src);
319
+ NODEOF
320
+
321
+ if [[ -s "$TMPDIR_EVAL/ambiguous-mutation-patch.err" ]]; then
322
+ _fail "mutation-test setup failed (recordCheck ambiguous-branch source pattern did not match compiled output): $(cat "$TMPDIR_EVAL/ambiguous-mutation-patch.err")"
323
+ else
324
+ if node --check "$AMBIGUOUS_SCRATCH/workflow-sidecar.mutated.js" 2>"$TMPDIR_EVAL/ambiguous-mutation-syntax.err"; then
325
+ cp "$AMBIGUOUS_SCRATCH/workflow-sidecar.mutated.js" "$DIST_SIDECAR"
326
+
327
+ flow_agents_node "$WRITER" record-check "$SESSION_DIR" --timestamp "2026-07-05T09:10:00Z" --id ac6-mutation-check -- grep -E 'this-pattern-does-not-exist-anywhere' "$FIXTURE_FILE" >"$TMPDIR_EVAL/rc-mutation.out" 2>"$TMPDIR_EVAL/rc-mutation.err"
328
+ MUTATION_EXIT=$?
329
+
330
+ if node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/rc-mutation-assert.err"
331
+ import { readFileSync } from 'node:fs';
332
+ const bundle = JSON.parse(readFileSync('${SESSION_DIR}/trust.bundle', 'utf8'));
333
+ const claim = bundle.claims.find((c) => c.subjectId && c.subjectId.endsWith('/ac6-mutation-check'));
334
+ if (!claim) { process.stderr.write('no claim found for the ac6-mutation-check check\n'); process.exit(1); }
335
+ if (claim.value !== 'fail') { process.stderr.write('expected the mutated (ambiguity-disabled) binary to record claim value fail (not not_verified), got ' + claim.value + '\n'); process.exit(1); }
336
+ NODEOF
337
+ then
338
+ if [[ "$MUTATION_EXIT" -eq 0 ]]; then
339
+ _fail "mutation-test: with the ambiguous branch neutered, record-check should hard-fail (nonzero exit) on the same bare-grep exit-1 fixture, but it exited 0"
340
+ else
341
+ _pass "mutation-test: with the ambiguous branch neutered, the same bare-grep exit-1 fixture now records a hard FAIL and exits nonzero (eval correctly goes red without the branch, proving the eval exercises it)"
342
+ fi
343
+ else
344
+ _fail "mutation-test: bare-grep exit-1 fixture did not flip to a hard fail claim value even with the ambiguous branch neutered — the eval may not be exercising the intended branch: $(cat "$TMPDIR_EVAL/rc-mutation-assert.err")"
345
+ fi
346
+ else
347
+ _fail "mutation-test setup: mutated workflow-sidecar.js (recordCheck ambiguous branch) failed a syntax check, refusing to run it: $(cat "$TMPDIR_EVAL/ambiguous-mutation-syntax.err")"
348
+ fi
349
+
350
+ # Restore the real compiled branch immediately — never leave the mutated binary in place —
351
+ # and re-run the same fixture to confirm the restored binary classifies it ambiguous again.
352
+ cp "$AMBIGUOUS_SCRATCH/workflow-sidecar.orig.js" "$DIST_SIDECAR"
353
+ flow_agents_node "$WRITER" record-check "$SESSION_DIR" --timestamp "2026-07-05T09:10:30Z" --id ac6-restore-check -- grep -E 'this-pattern-does-not-exist-anywhere' "$FIXTURE_FILE" >"$TMPDIR_EVAL/rc-restore.out" 2>"$TMPDIR_EVAL/rc-restore.err"
354
+ RESTORE_EXIT=$?
355
+ if node --input-type=module <<NODEOF 2>"$TMPDIR_EVAL/rc-restore-assert.err"
356
+ import { readFileSync } from 'node:fs';
357
+ const bundle = JSON.parse(readFileSync('${SESSION_DIR}/trust.bundle', 'utf8'));
358
+ const claim = bundle.claims.find((c) => c.subjectId && c.subjectId.endsWith('/ac6-restore-check'));
359
+ if (!claim) { process.stderr.write('no claim found for the ac6-restore-check check\n'); process.exit(1); }
360
+ if (claim.value !== 'not_verified' || "$RESTORE_EXIT" != "0") { process.stderr.write('expected the restored binary to record claim value not_verified and exit 0, got value=' + claim.value + ' exit=$RESTORE_EXIT\n'); process.exit(1); }
361
+ NODEOF
362
+ then
363
+ _pass "mutation-test cleanup: the real compiled ambiguous branch is restored and classifies a bare-grep exit-1 as not_verified (exit 0) again"
364
+ else
365
+ _fail "mutation-test cleanup REGRESSION: the ambiguous branch did not come back correctly after restoring the original compiled file: $(cat "$TMPDIR_EVAL/rc-restore-assert.err")"
366
+ fi
367
+ fi
368
+ else
369
+ _fail "mutation-test setup: could not locate the compiled build/src/cli/workflow-sidecar.js to mutate for the recordCheck ambiguous branch (ran 'npm run build' first?)"
370
+ fi
371
+
372
+ if [[ "$errors" -eq 0 ]]; then
373
+ echo "record-check integration passed."
374
+ exit 0
375
+ fi
376
+
377
+ echo "record-check integration failed: $errors issue(s)."
378
+ exit 1
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck disable=SC2015
3
+ # ^ the `<cond> && pass ... || fail ...` assert idiom is deliberate (pass always returns 0).
4
+ # test_routing_efficiency.sh — routing-efficiency proposals from delegation outcomes (#415).
5
+ #
6
+ # Proves scripts/telemetry/routing-efficiency.sh turns real economics.jsonl delegation outcomes into
7
+ # ADVISORY per-(role,model) proposals, honestly: unavailable outcomes are excluded from every rate, a
8
+ # thin sample yields insufficient-signal (not a confident call), and the analyzer never writes config.
9
+ set -uo pipefail
10
+
11
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
12
+ ANALYZER="$ROOT/scripts/telemetry/routing-efficiency.sh"
13
+ TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
14
+ errors=0
15
+ pass() { echo " [PASS] $1"; }
16
+ fail() { echo " [FAIL] $1"; errors=$((errors + 1)); }
17
+ command -v jq >/dev/null 2>&1 || { echo "jq unavailable; skipping"; exit 0; }
18
+
19
+ echo "=== routing-efficiency (#415) ==="
20
+
21
+ # Fixture: mechanical is troubled (3 of 4 measurable reworked/diverged/failed), design is clean (3/3
22
+ # accepted), implementation is all-unavailable (no measurable signal on this harness).
23
+ LOG="$TMP/economics.jsonl"
24
+ cat > "$LOG" <<'EOF'
25
+ {"schema":"kontour.console.economics","version":"0.1","run_id":"r1","cost":{},"time":{},"iterations":{},"defects":{},"delegations":[{"role":"delegate-mechanical","resolved_model":"claude-haiku-4-5@anthropic","outcome":"rework"},{"role":"delegate-design","resolved_model":"claude-opus-4-8@anthropic","outcome":"accepted"}]}
26
+ {"schema":"kontour.console.economics","version":"0.1","run_id":"r2","cost":{},"time":{},"iterations":{},"defects":{},"delegations":[{"role":"delegate-mechanical","resolved_model":"claude-haiku-4-5@anthropic","outcome":"failed"},{"role":"delegate-design","resolved_model":"claude-opus-4-8@anthropic","outcome":"accepted"}]}
27
+ {"schema":"kontour.console.economics","version":"0.1","run_id":"r3","cost":{},"time":{},"iterations":{},"defects":{},"delegations":[{"role":"delegate-mechanical","resolved_model":"claude-haiku-4-5@anthropic","outcome":"diverged"},{"role":"delegate-design","resolved_model":"claude-opus-4-8@anthropic","outcome":"accepted"},{"role":"delegate-implementation","resolved_model":"claude-sonnet-5@anthropic","outcome":"unavailable"}]}
28
+ {"schema":"kontour.console.economics","version":"0.1","run_id":"r4","cost":{},"time":{},"iterations":{},"defects":{},"delegations":[{"role":"delegate-mechanical","resolved_model":"claude-haiku-4-5@anthropic","outcome":"accepted"},{"role":"delegate-implementation","resolved_model":"claude-sonnet-5@anthropic","outcome":"unavailable"}]}
29
+ EOF
30
+ OUT="$(bash "$ANALYZER" "$LOG")"
31
+ aeq() { # <label> <jq-expr> <expected>
32
+ local got; got="$(printf '%s' "$OUT" | jq -c "$2" 2>/dev/null)"
33
+ [[ "$got" == "$3" ]] && pass "$1 ($got)" || fail "$1: expected $3 got $got"
34
+ }
35
+
36
+ # AC: at least one real proposal (the #415 acceptance) — the troubled cheap tier flagged for escalation.
37
+ aeq "troubled cheap tier → escalate-minimum-tier proposal" \
38
+ '.proposals[] | select(.role=="delegate-mechanical") | .kind' '"escalate-minimum-tier"'
39
+ aeq "clean expensive tier → keep-tier proposal" \
40
+ '.proposals[] | select(.role=="delegate-design") | .kind' '"keep-tier"'
41
+ # AC: unavailable excluded from rates → thin measurable sample is insufficient-signal, NOT a confident call.
42
+ aeq "all-unavailable role → insufficient-signal (not a confident verdict)" \
43
+ '.proposals[] | select(.role=="delegate-implementation") | .kind' '"insufficient-signal"'
44
+ aeq "unavailable EXCLUDED from measurable (mechanical measurable == 4)" \
45
+ '.by_role_model[] | select(.role=="delegate-mechanical") | .measurable' '4'
46
+ aeq "implementation unavailable counted separately (== 2), measurable == 0" \
47
+ '.by_role_model[] | select(.role=="delegate-implementation") | [.unavailable,.measurable]' '[2,0]'
48
+ aeq "trouble_rate excludes unavailable (mechanical 3/4 == 0.75)" \
49
+ '.by_role_model[] | select(.role=="delegate-mechanical") | .trouble_rate' '0.75'
50
+ aeq "every proposal is advisory (never auto-applied)" \
51
+ '[.proposals[] | select(.severity != "advisory")] | length' '0'
52
+ # a real, human-readable rationale exists on the escalation proposal
53
+ HAS_RAT="$(printf '%s' "$OUT" | jq -r '.proposals[] | select(.role=="delegate-mechanical") | .rationale' 2>/dev/null)"
54
+ [[ "$HAS_RAT" == *"under-routed"* ]] && pass "escalation proposal carries a human rationale" || fail "no rationale: $HAS_RAT"
55
+
56
+ # empty / no-delegations input → empty proposals, never a fabricated call
57
+ EMPTY="$TMP/empty.jsonl"; : > "$EMPTY"
58
+ EOUT="$(bash "$ANALYZER" "$EMPTY")"
59
+ [[ "$(printf '%s' "$EOUT" | jq -c '.proposals')" == "[]" ]] && pass "empty input → no proposals (no fabrication)" || fail "empty input produced proposals"
60
+ [[ "$(printf '%s' "$EOUT" | jq -r '.schema')" == "kontour.routing-efficiency" ]] && pass "empty input still emits a valid envelope" || fail "empty envelope malformed"
61
+
62
+ # honesty guard: the analyzer must NOT write .datum/config.json (advisory only)
63
+ if grep -qE '\.datum/config\.json' "$ANALYZER" && ! grep -qE 'never (edit|auto-appl)' "$ANALYZER"; then
64
+ fail "analyzer references .datum/config.json outside a comment — must not write routing config"
65
+ else
66
+ pass "analyzer never writes .datum/config.json (advisory only, human-ratified)"
67
+ fi
68
+
69
+ echo ""
70
+ if [[ "$errors" -eq 0 ]]; then echo "test_routing_efficiency: all checks passed."; exit 0
71
+ else echo "test_routing_efficiency: $errors check(s) failed."; exit 1; fi
@@ -116,6 +116,34 @@ else
116
116
  fail "source kits/catalog.json changed during activation"
117
117
  fi
118
118
 
119
+ SYMLINK_DEST="$TMP_DIR/symlink-activation-dest"
120
+ SYMLINK_OUTSIDE="$TMP_DIR/symlink-activation-outside"
121
+ mkdir -p "$SYMLINK_DEST" "$SYMLINK_OUTSIDE"
122
+ flow_agents_node "$CLI" install "$MIXED_SRC" --dest "$SYMLINK_DEST" >/dev/null 2>&1
123
+ SYMLINK_OUTPUT="$SYMLINK_DEST/.kontourai/flow-agents/projections/codex/flows/mixed-runtime-kit/mixed.runtime.flow.json"
124
+ mkdir -p "$(dirname "$SYMLINK_OUTPUT")"
125
+ printf 'unchanged\n' > "$SYMLINK_OUTSIDE/output"
126
+ ln -s "$SYMLINK_OUTSIDE/output" "$SYMLINK_OUTPUT"
127
+ if flow_agents_node "$CLI" activate --dest "$SYMLINK_DEST" --source-root "$ROOT" >"$TMP_DIR/symlink-output.out" 2>&1; then
128
+ fail "activation output symlink should fail closed"
129
+ elif [[ "$(cat "$SYMLINK_OUTSIDE/output")" == "unchanged" ]]; then
130
+ pass "activation output symlink fails closed without outside write"
131
+ else
132
+ fail "activation output symlink modified outside file"
133
+ fi
134
+
135
+ rm -rf "$SYMLINK_DEST/.kontourai"
136
+ mkdir -p "$SYMLINK_DEST/.kontourai/flow-agents/projections/codex"
137
+ printf 'unchanged\n' > "$SYMLINK_OUTSIDE/manifest"
138
+ ln -s "$SYMLINK_OUTSIDE/manifest" "$SYMLINK_DEST/.kontourai/flow-agents/projections/codex/activation.json"
139
+ if flow_agents_node "$CLI" activate --dest "$SYMLINK_DEST" --source-root "$ROOT" >"$TMP_DIR/symlink-manifest.out" 2>&1; then
140
+ fail "activation manifest symlink should fail closed"
141
+ elif [[ "$(cat "$SYMLINK_OUTSIDE/manifest")" == "unchanged" ]]; then
142
+ pass "activation manifest symlink fails closed without outside write"
143
+ else
144
+ fail "activation manifest symlink modified outside file"
145
+ fi
146
+
119
147
  if flow_agents_node "$CLI" activate --dest "$DEST" --source-root "$ROOT" --adapter unknown --format json >"$UNKNOWN_OUT" 2>&1; then
120
148
  fail "unknown adapter should fail"
121
149
  sed -n '1,120p' "$UNKNOWN_OUT"
@@ -37,9 +37,9 @@ sha256_file() {
37
37
  # ─── Seed fixture ─────────────────────────────────────────────────────────────
38
38
  REPO="$TMPDIR_EVAL/repo"
39
39
  SLUG="test-slug-153"
40
- TASK_DIR="$REPO/.flow-agents/$SLUG"
40
+ TASK_DIR="$REPO/.kontourai/flow-agents/$SLUG"
41
41
  mkdir -p "$TASK_DIR"
42
- mkdir -p "$REPO/.flow-agents/liveness"
42
+ mkdir -p "$REPO/.kontourai/flow-agents/liveness"
43
43
  mkdir -p "$REPO/docs"
44
44
 
45
45
  printf '# Test Repo\n' > "$REPO/AGENTS.md"
@@ -98,7 +98,7 @@ cat > "$TASK_DIR/trust.bundle" << 'JSON'
98
98
  JSON
99
99
 
100
100
  # install.json — initial version
101
- cat > "$REPO/.flow-agents/install.json" << 'JSON'
101
+ cat > "$REPO/.kontourai/flow-agents/install.json" << 'JSON'
102
102
  {
103
103
  "version": "v0.0.1",
104
104
  "installedAt": "2026-06-25T00:00:00Z",
@@ -107,10 +107,10 @@ cat > "$REPO/.flow-agents/install.json" << 'JSON'
107
107
  JSON
108
108
 
109
109
  # Liveness stream: fresh other-agent event (5 min ago, within 1800 s TTL)
110
- # and a self (local) event — self should NOT trigger a warning
110
+ # and a self event — self should NOT trigger a warning
111
111
  FIVE_MIN_AGO="$(node -e "process.stdout.write(new Date(Date.now()-300000).toISOString().replace(/\\.\\d{3}Z$/,'Z'))")"
112
- printf '{"type":"claim","subjectId":"test-slug-153","actor":"other-agent","at":"%s","ttlSeconds":1800}\n' "$FIVE_MIN_AGO" > "$REPO/.flow-agents/liveness/events.jsonl"
113
- printf '{"type":"heartbeat","subjectId":"test-slug-153","actor":"local","at":"%s"}\n' "$FIVE_MIN_AGO" >> "$REPO/.flow-agents/liveness/events.jsonl"
112
+ printf '{"type":"claim","subjectId":"test-slug-153","actor":"other-agent","at":"%s","ttlSeconds":1800}\n' "$FIVE_MIN_AGO" > "$REPO/.kontourai/flow-agents/liveness/events.jsonl"
113
+ printf '{"type":"heartbeat","subjectId":"test-slug-153","actor":"self-actor","at":"%s"}\n' "$FIVE_MIN_AGO" >> "$REPO/.kontourai/flow-agents/liveness/events.jsonl"
114
114
 
115
115
  # ─── Snapshot checksums before hook run ───────────────────────────────────────
116
116
  CKSUM_STATE_BEFORE="$(sha256_file "$TASK_DIR/state.json")"
@@ -120,7 +120,7 @@ CKSUM_TRUST_BEFORE="$(sha256_file "$TASK_DIR/trust.bundle")"
120
120
  # ─── Hot-upgrade simulation: bump install.json version ───────────────────────
121
121
  node -e "
122
122
  const fs = require('fs');
123
- const f = '$REPO/.flow-agents/install.json';
123
+ const f = '$REPO/.kontourai/flow-agents/install.json';
124
124
  const obj = JSON.parse(fs.readFileSync(f,'utf8'));
125
125
  obj.version = 'v0.0.2';
126
126
  fs.writeFileSync(f, JSON.stringify(obj, null, 2) + '\n');
@@ -128,7 +128,7 @@ fs.writeFileSync(f, JSON.stringify(obj, null, 2) + '\n');
128
128
 
129
129
  # ─── Run hook with SessionStart ───────────────────────────────────────────────
130
130
  if echo "{\"hook_event_name\":\"SessionStart\",\"cwd\":\"$REPO\"}" | \
131
- FLOW_AGENTS_ACTOR="local" node "$ROOT/scripts/hooks/workflow-steering.js" > "$TMPDIR_EVAL/resume.out" 2>&1; then
131
+ FLOW_AGENTS_ACTOR="self-actor" node "$ROOT/scripts/hooks/workflow-steering.js" > "$TMPDIR_EVAL/resume.out" 2>&1; then
132
132
  _pass "hook exits 0 for SessionStart"
133
133
  else
134
134
  _fail "hook should exit 0 for SessionStart (exit $?)"
@@ -208,9 +208,9 @@ else
208
208
  _fail "other-agent actor missing from liveness warning"
209
209
  fi
210
210
 
211
- # Self-actor (local) should NOT appear as a liveness warning
212
- if ! grep -q "LIVENESS WARNING.*local\|local.*LIVENESS WARNING" "$TMPDIR_EVAL/resume.out"; then
213
- _pass "self-actor (local) correctly excluded from liveness warning"
211
+ # Self actor should NOT appear as a liveness warning
212
+ if ! grep -q "LIVENESS WARNING.*self-actor\|self-actor.*LIVENESS WARNING" "$TMPDIR_EVAL/resume.out"; then
213
+ _pass "self-actor correctly excluded from liveness warning"
214
214
  else
215
215
  _fail "self-actor should not be warned in liveness advisory"
216
216
  fi
@@ -240,7 +240,7 @@ fi
240
240
 
241
241
  # ─── Negative: UserPromptSubmit should produce NO RESUME block ────────────────
242
242
  echo "{\"hook_event_name\":\"UserPromptSubmit\",\"cwd\":\"$REPO\",\"prompt\":\"continue\"}" | \
243
- FLOW_AGENTS_ACTOR="local" node "$ROOT/scripts/hooks/workflow-steering.js" > "$TMPDIR_EVAL/prompt.out" 2>&1
243
+ FLOW_AGENTS_ACTOR="self-actor" node "$ROOT/scripts/hooks/workflow-steering.js" > "$TMPDIR_EVAL/prompt.out" 2>&1
244
244
 
245
245
  if ! grep -q "RESUME:" "$TMPDIR_EVAL/prompt.out"; then
246
246
  _pass "RESUME block absent for UserPromptSubmit (negative case)"
@@ -250,7 +250,7 @@ fi
250
250
 
251
251
  # ─── Negative: Empty liveness stream → no LIVENESS WARNING ───────────────────
252
252
  REPO2="$TMPDIR_EVAL/repo2"
253
- TASK_DIR2="$REPO2/.flow-agents/$SLUG"
253
+ TASK_DIR2="$REPO2/.kontourai/flow-agents/$SLUG"
254
254
  mkdir -p "$TASK_DIR2"
255
255
  mkdir -p "$REPO2/docs"
256
256
  printf '# Test Repo 2\n' > "$REPO2/AGENTS.md"
@@ -262,7 +262,7 @@ printf 'test-slug-153--plan-work.md stub\n' > "$TASK_DIR2/test-slug-153--plan-wo
262
262
  # No liveness directory → empty stream
263
263
 
264
264
  echo "{\"hook_event_name\":\"SessionStart\",\"cwd\":\"$REPO2\"}" | \
265
- FLOW_AGENTS_ACTOR="local" node "$ROOT/scripts/hooks/workflow-steering.js" > "$TMPDIR_EVAL/nolive.out" 2>&1
265
+ FLOW_AGENTS_ACTOR="self-actor" node "$ROOT/scripts/hooks/workflow-steering.js" > "$TMPDIR_EVAL/nolive.out" 2>&1
266
266
 
267
267
  if grep -q "RESUME:" "$TMPDIR_EVAL/nolive.out"; then
268
268
  _pass "RESUME block present when no liveness stream (absence case)"
@@ -284,12 +284,9 @@ fi
284
284
  # env, and assert that derived actor is excluded from LIVENESS WARNING while a
285
285
  # genuinely different actor is still reported.
286
286
  #
287
- # NOTE: this fixture seeds under ".kontourai/flow-agents/<slug>" (not
288
- # ".flow-agents/<slug>" as the REPO/REPO2 fixtures above do), because
287
+ # This and the fixtures above seed under ".kontourai/flow-agents/<slug>", because
289
288
  # scripts/hooks/lib/local-artifact-paths.js's flowAgentsArtifactRootsForRead()
290
- # only reads ".kontourai/flow-agents" — pre-existing drift from the REPO/REPO2
291
- # fixtures above, unrelated to #287, out of scope for this task (see plan
292
- # artifact task 3.2 investigation notes).
289
+ # reads the runtime artifact root, not durable ".flow-agents" config.
293
290
  REPO3="$TMPDIR_EVAL/repo3"
294
291
  SLUG3="test-slug-153-derived"
295
292
  TASK_DIR3="$REPO3/.kontourai/flow-agents/$SLUG3"