@kontourai/flow-agents 3.3.0 → 3.4.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 (256) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +161 -0
  3. package/CHANGELOG.md +48 -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/builder-run.d.ts +1 -0
  11. package/build/src/cli/builder-run.js +27 -0
  12. package/build/src/cli/effective-backlog-settings.js +70 -2
  13. package/build/src/cli/init.d.ts +34 -0
  14. package/build/src/cli/init.js +341 -61
  15. package/build/src/cli/kit.js +55 -12
  16. package/build/src/cli/pull-work-provider.js +346 -5
  17. package/build/src/cli/skill-drift-check.d.ts +1 -0
  18. package/build/src/cli/skill-drift-check.js +165 -0
  19. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  20. package/build/src/cli/telemetry-doctor.js +53 -6
  21. package/build/src/cli/validate-hook-influence.js +37 -7
  22. package/build/src/cli/workflow-sidecar.d.ts +93 -8
  23. package/build/src/cli/workflow-sidecar.js +1175 -158
  24. package/build/src/cli.js +5 -0
  25. package/build/src/flow-kit/validate.d.ts +54 -34
  26. package/build/src/flow-kit/validate.js +237 -26
  27. package/build/src/index.d.ts +2 -0
  28. package/build/src/index.js +1 -0
  29. package/build/src/lib/console-connect-options.d.ts +97 -0
  30. package/build/src/lib/console-connect-options.js +199 -0
  31. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  32. package/build/src/lib/console-telemetry-validate.js +91 -0
  33. package/build/src/lib/flow-resolver.d.ts +56 -3
  34. package/build/src/lib/flow-resolver.js +151 -11
  35. package/build/src/lib/fs.d.ts +17 -0
  36. package/build/src/lib/fs.js +172 -0
  37. package/build/src/lib/local-artifact-root.d.ts +44 -1
  38. package/build/src/lib/local-artifact-root.js +131 -3
  39. package/build/src/runtime-adapters.d.ts +39 -3
  40. package/build/src/runtime-adapters.js +77 -31
  41. package/build/src/tools/build-universal-bundles.js +40 -2
  42. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  43. package/build/src/tools/codex-agent-routing.js +49 -0
  44. package/build/src/tools/generate-context-map.js +1 -0
  45. package/build/src/tools/validate-source-tree.js +27 -1
  46. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  47. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  48. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  49. package/context/scripts/hooks/workflow-steering.js +121 -21
  50. package/context/scripts/package.json +3 -0
  51. package/context/scripts/telemetry/install-console-config.sh +25 -4
  52. package/context/scripts/telemetry/lib/config.sh +102 -12
  53. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  54. package/context/scripts/telemetry/lib/session.sh +3 -0
  55. package/context/scripts/telemetry/lib/transport.sh +87 -0
  56. package/context/scripts/telemetry/lib/usage.sh +205 -4
  57. package/context/scripts/telemetry/telemetry.conf +6 -0
  58. package/context/scripts/telemetry/telemetry.sh +48 -0
  59. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  60. package/docs/agent-usage-feedback-loop.md +35 -0
  61. package/docs/architecture-engine-and-kits.md +110 -0
  62. package/docs/context-map.md +2 -0
  63. package/docs/decisions/embeddable-engine.md +152 -0
  64. package/docs/decisions/index.md +3 -1
  65. package/docs/decisions/trust-ledger-retention.md +88 -0
  66. package/docs/decisions/workflow-enforcement.md +31 -9
  67. package/docs/fixture-ownership.md +3 -0
  68. package/docs/implementing-trust-reconciliation.md +129 -0
  69. package/docs/index.md +19 -9
  70. package/docs/integrations/flow-agents-console.md +167 -0
  71. package/docs/kit-authoring-guide.md +52 -21
  72. package/docs/spec/builder-flow-runtime.md +80 -0
  73. package/docs/spec/runtime-hook-surface.md +45 -1
  74. package/docs/specs/economics-record-contract.md +270 -0
  75. package/docs/specs/harness-capability-matrix.md +74 -0
  76. package/docs/specs/learning-review-proposals-contract.md +340 -0
  77. package/docs/specs/routing-efficiency-review.md +59 -0
  78. package/docs/verifiable-trust.md +74 -25
  79. package/docs/workflow-usage-guide.md +10 -0
  80. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  81. package/evals/ci/antigaming-suite.sh +1 -0
  82. package/evals/ci/run-baseline.sh +72 -4
  83. package/evals/fixtures/economics/acceptance.json +12 -0
  84. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  85. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  86. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  87. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  88. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  89. package/evals/fixtures/economics/critique.json +22 -0
  90. package/evals/fixtures/economics/expected-record.json +71 -0
  91. package/evals/fixtures/economics/session-usage-event.json +1 -0
  92. package/evals/fixtures/economics/state.json +11 -0
  93. package/evals/fixtures/economics/transcript.jsonl +3 -0
  94. package/evals/fixtures/hook-influence/cases.json +7 -7
  95. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  96. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  97. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  98. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  99. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  100. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  101. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  102. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  103. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  104. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  105. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  106. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  108. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  116. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  117. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  118. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  119. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  120. package/evals/integration/test_builder_step_producers.sh +18 -10
  121. package/evals/integration/test_bundle_install.sh +172 -0
  122. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  123. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  124. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  125. package/evals/integration/test_economics_record.sh +674 -0
  126. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  127. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  128. package/evals/integration/test_exemption_usage_review.sh +198 -0
  129. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  130. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  131. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  132. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  133. package/evals/integration/test_gate_lockdown.sh +7 -0
  134. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  135. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  136. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  137. package/evals/integration/test_hook_influence_cases.sh +25 -1
  138. package/evals/integration/test_install_merge.sh +227 -2
  139. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  140. package/evals/integration/test_learning_review_proposals.sh +329 -0
  141. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  142. package/evals/integration/test_liveness_console_relay.sh +166 -0
  143. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  144. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  145. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  146. package/evals/integration/test_publish_delivery.sh +331 -1
  147. package/evals/integration/test_pull_work_board.sh +200 -0
  148. package/evals/integration/test_pull_work_provider.sh +1 -1
  149. package/evals/integration/test_record_check.sh +378 -0
  150. package/evals/integration/test_routing_efficiency.sh +71 -0
  151. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  152. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  153. package/evals/integration/test_skill_drift_check.sh +870 -0
  154. package/evals/integration/test_telemetry.sh +445 -0
  155. package/evals/integration/test_telemetry_doctor.sh +66 -0
  156. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  157. package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
  158. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  159. package/evals/integration/test_usage_cost.sh +61 -0
  160. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  161. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  162. package/evals/integration/test_workspace_settings.sh +176 -0
  163. package/evals/lib/env.sh +26 -0
  164. package/evals/lib/node.sh +8 -0
  165. package/evals/run.sh +29 -0
  166. package/evals/static/test_ci_integration_coverage.sh +115 -0
  167. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  168. package/evals/static/test_universal_bundles.sh +34 -0
  169. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  170. package/evals/static/test_workflow_skills.sh +1 -1
  171. package/kits/builder/flows/build.flow.json +9 -18
  172. package/kits/builder/flows/publish-learn.flow.json +5 -1
  173. package/kits/builder/kit.json +120 -0
  174. package/kits/builder/skills/deliver/SKILL.md +42 -0
  175. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  176. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  177. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  178. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  179. package/kits/builder/skills/pull-work/SKILL.md +21 -0
  180. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  181. package/kits/knowledge/kit.json +9 -0
  182. package/kits/veritas-governance/docs/README.md +35 -7
  183. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  184. package/kits/veritas-governance/kit.json +14 -0
  185. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  186. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  187. package/package.json +2 -2
  188. package/packaging/manifest.json +29 -0
  189. package/schemas/backlog-provider-settings.schema.json +13 -0
  190. package/schemas/workflow-state.schema.json +44 -0
  191. package/scripts/README.md +4 -0
  192. package/scripts/check-content-boundary.cjs +8 -1
  193. package/scripts/ci/trust-reconcile.js +136 -0
  194. package/scripts/hooks/codex-hook-adapter.js +77 -2
  195. package/scripts/hooks/evidence-capture.js +38 -5
  196. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  197. package/scripts/hooks/lib/kit-catalog.js +235 -0
  198. package/scripts/hooks/lib/liveness-write.js +28 -1
  199. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  200. package/scripts/hooks/lib/runnable-command.js +177 -0
  201. package/scripts/hooks/lib/skill-drift.js +350 -0
  202. package/scripts/hooks/stop-goal-fit.js +278 -48
  203. package/scripts/hooks/workflow-steering.js +121 -21
  204. package/scripts/install-codex-home.sh +97 -47
  205. package/scripts/install-merge.js +72 -14
  206. package/scripts/install-owned-files.js +178 -0
  207. package/scripts/liveness/relay.sh +84 -0
  208. package/scripts/telemetry/economics-record.schema.json +145 -0
  209. package/scripts/telemetry/economics-record.sh +331 -0
  210. package/scripts/telemetry/install-console-config.sh +25 -4
  211. package/scripts/telemetry/learning-review-decide.sh +124 -0
  212. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  213. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  214. package/scripts/telemetry/lib/config.sh +102 -12
  215. package/scripts/telemetry/lib/pricing.sh +14 -6
  216. package/scripts/telemetry/lib/session.sh +3 -0
  217. package/scripts/telemetry/lib/transport.sh +133 -15
  218. package/scripts/telemetry/lib/usage.sh +121 -28
  219. package/scripts/telemetry/routing-efficiency.sh +0 -0
  220. package/scripts/telemetry/telemetry.conf +6 -0
  221. package/scripts/telemetry/telemetry.sh +48 -0
  222. package/src/builder-flow-run-adapter.ts +357 -0
  223. package/src/builder-flow-runtime.ts +348 -0
  224. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  225. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  226. package/src/cli/builder-run.ts +28 -0
  227. package/src/cli/codex-agent-routing.test.mjs +44 -0
  228. package/src/cli/codex-exit-code.test.mjs +207 -0
  229. package/src/cli/console-connect-options.test.mjs +329 -0
  230. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  231. package/src/cli/effective-backlog-settings.ts +68 -2
  232. package/src/cli/flow-resolver-composition.test.mjs +101 -0
  233. package/src/cli/init.test.mjs +161 -0
  234. package/src/cli/init.ts +407 -62
  235. package/src/cli/kit-metadata-security.test.mjs +443 -0
  236. package/src/cli/kit.ts +50 -12
  237. package/src/cli/pull-work-provider.ts +377 -3
  238. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  239. package/src/cli/skill-drift-check.ts +196 -0
  240. package/src/cli/telemetry-doctor.test.mjs +53 -0
  241. package/src/cli/telemetry-doctor.ts +50 -7
  242. package/src/cli/validate-hook-influence.ts +37 -6
  243. package/src/cli/workflow-sidecar.ts +1150 -151
  244. package/src/cli.ts +5 -0
  245. package/src/flow-kit/validate.ts +277 -38
  246. package/src/index.ts +19 -0
  247. package/src/lib/console-connect-options.ts +261 -0
  248. package/src/lib/console-telemetry-validate.ts +88 -0
  249. package/src/lib/flow-resolver.ts +153 -10
  250. package/src/lib/fs.ts +160 -0
  251. package/src/lib/local-artifact-root.ts +129 -3
  252. package/src/runtime-adapters.ts +113 -33
  253. package/src/tools/build-universal-bundles.ts +36 -2
  254. package/src/tools/codex-agent-routing.ts +48 -0
  255. package/src/tools/generate-context-map.ts +1 -0
  256. package/src/tools/validate-source-tree.ts +26 -1
@@ -0,0 +1,329 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck disable=SC2015
3
+ # ^ the `<cond> && pass ... || fail ...` assert idiom is deliberate (pass always returns 0).
4
+ # test_learning_review_proposals.sh — kit/gate tuning proposals from economics records (#352).
5
+ #
6
+ # Proves scripts/telemetry/learning-review-proposals.sh turns a window of real
7
+ # kontour.console.economics records into ADVISORY, evidence-cited, per-kit/per-gate proposals —
8
+ # generalizing scripts/telemetry/routing-efficiency.sh (#415) from per-(role,model) to per-kit/
9
+ # per-gate — and that scripts/telemetry/learning-review-decide.sh records human ratify/reject/
10
+ # defer decisions in place, never auto-applying anything.
11
+ #
12
+ # Coverage -> ACs (see docs/specs/learning-review-proposals-contract.md):
13
+ # AC1 (R1) pattern-present fixture's by_kit[]/by_gate[] aggregates match hand-computed values
14
+ # (evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json,
15
+ # arithmetic shown in expected-aggregates.md) at exact 4-decimal precision.
16
+ # AC2 (R2) pattern-present yields >=1 evidence-cited proposal (cost+defect paired); balanced
17
+ # fixture (proportional cost/findings movement) yields zero proposals.
18
+ # AC3 (R3) analyzer + decide script make zero mutation to kits/**/.datum/config.json; decide
19
+ # refuses --follow-on-ref without --ratify.
20
+ # AC4 (R4) running twice over an identical window de-dupes (no duplicate proposal_id in the
21
+ # ledger, second run marks already_proposed:true); under-threshold record count
22
+ # yields an explicit "insufficient-data" outcome, never a confident thin call.
23
+ # AC5 (R5) a ratified proposal is linked to a follow_on_ref; a later window's pass over the
24
+ # same target fills in effect_observed (metric/before/after/moved).
25
+ # schema emitted output validates against learning-review-proposals.schema.json; a doctored
26
+ # cost-only (no defect) proposal FAILS validation (Goodhart-guard co-requirement).
27
+ #
28
+ # Deterministic (no network, no model spend); fixtures live under
29
+ # evals/fixtures/learning-review-proposals/**. Each case uses its own mktemp-sandboxed ledger.
30
+ set -uo pipefail
31
+
32
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
33
+ TELEMETRY="$ROOT/scripts/telemetry"
34
+ ANALYZER="$TELEMETRY/learning-review-proposals.sh"
35
+ DECIDER="$TELEMETRY/learning-review-decide.sh"
36
+ SCHEMA="$TELEMETRY/learning-review-proposals.schema.json"
37
+ FIX="$ROOT/evals/fixtures/learning-review-proposals"
38
+
39
+ TMP="$(mktemp -d)"
40
+ trap 'rm -rf "$TMP"' EXIT
41
+ errors=0
42
+ pass() { echo " [PASS] $1"; }
43
+ fail() { echo " [FAIL] $1"; errors=$((errors + 1)); }
44
+
45
+ command -v jq >/dev/null 2>&1 || { echo "jq unavailable; skipping"; exit 0; }
46
+
47
+ echo "=== learning-review-proposals (#352) ==="
48
+
49
+ # ── AC1/AC2: pattern-present — hand-computed aggregates + evidence-cited proposals ─────────────────
50
+ echo "--- AC1/AC2: pattern-present fixture — hand-computed aggregates match; evidence-cited proposals fire ---"
51
+ PP="$FIX/pattern-present"
52
+ LEDGER_PP="$TMP/ledger-pattern-present.jsonl"
53
+ OUT_PP="$(bash "$ANALYZER" --sessions-root "$PP/sessions" --ledger "$LEDGER_PP" "$PP/economics.jsonl")"
54
+
55
+ aeq() { # <out-json> <label> <jq-expr> <expected>
56
+ local out="$1" label="$2" expr="$3" exp="$4"
57
+ local got; got="$(printf '%s' "$out" | jq -c "$expr" 2>/dev/null)"
58
+ [[ "$got" == "$exp" ]] && pass "$label ($got)" || fail "$label: expected $exp got $got"
59
+ }
60
+
61
+ aeq "$OUT_PP" "records_considered == 6" '.records_considered' '6'
62
+ aeq "$OUT_PP" "outcome == ok" '.outcome' '"ok"'
63
+
64
+ # by_kit[] — exact 4-decimal-rounded values, hand-computed in expected-aggregates.md
65
+ BYKIT='.aggregates.by_kit[] | select(.kit_id=="builder")'
66
+ aeq "$OUT_PP" "by_kit[builder].runs == 6" "$BYKIT | .runs" '6'
67
+ aeq "$OUT_PP" "by_kit[builder].first_half_avg_cost_usd == 0.1000" "$BYKIT | .first_half_avg_cost_usd" '0.1'
68
+ aeq "$OUT_PP" "by_kit[builder].second_half_avg_cost_usd == 0.2000" "$BYKIT | .second_half_avg_cost_usd" '0.2'
69
+ aeq "$OUT_PP" "by_kit[builder].cost_trend_pct == 100.0000" "$BYKIT | .cost_trend_pct" '100'
70
+ aeq "$OUT_PP" "by_kit[builder].first_half_findings_total == 6" "$BYKIT | .first_half_findings_total" '6'
71
+ aeq "$OUT_PP" "by_kit[builder].second_half_findings_total == 6" "$BYKIT | .second_half_findings_total" '6'
72
+ aeq "$OUT_PP" "by_kit[builder].findings_delta_pct == 0.0000" "$BYKIT | .findings_delta_pct" '0'
73
+ aeq "$OUT_PP" "by_kit[builder].avg_wall_clock_s == 100.0000" "$BYKIT | .avg_wall_clock_s" '100'
74
+ aeq "$OUT_PP" "by_kit[builder].avg_human_wait_s == 10.0000" "$BYKIT | .avg_human_wait_s" '10'
75
+ aeq "$OUT_PP" "by_kit[builder].route_back_rate == 0.5000" "$BYKIT | .route_back_rate" '0.5'
76
+ aeq "$OUT_PP" "by_kit[builder].caught_false_completions_total == 6" "$BYKIT | .caught_false_completions_total" '6'
77
+
78
+ # by_gate[] — exact 4-decimal-rounded values
79
+ BYGATE='.aggregates.by_gate[] | select(.gate_id=="unit tests pass")'
80
+ aeq "$OUT_PP" "by_gate[unit tests pass].fire_count == 4" "$BYGATE | .fire_count" '4'
81
+ aeq "$OUT_PP" "by_gate[unit tests pass].correct_count == 1" "$BYGATE | .correct_count" '1'
82
+ aeq "$OUT_PP" "by_gate[unit tests pass].false_block_count == 3" "$BYGATE | .false_block_count" '3'
83
+ aeq "$OUT_PP" "by_gate[unit tests pass].missed_block_count == 0" "$BYGATE | .missed_block_count" '0'
84
+ aeq "$OUT_PP" "by_gate[unit tests pass].false_block_rate == 0.7500" "$BYGATE | .false_block_rate" '0.75'
85
+ aeq "$OUT_PP" "by_gate[unit tests pass].avg_wall_clock_s_when_fired == 100.0000" "$BYGATE | .avg_wall_clock_s_when_fired" '100'
86
+ aeq "$OUT_PP" "by_gate[unit tests pass].avg_human_wait_s_when_fired == 10.0000" "$BYGATE | .avg_human_wait_s_when_fired" '10'
87
+
88
+ # whole-document by_kit/by_gate equal the independently hand-computed expected-aggregates.json exactly.
89
+ EXPECTED_AGG="$(cat "$PP/expected-aggregates.json")"
90
+ GOT_BYKIT="$(printf '%s' "$OUT_PP" | jq -c '.aggregates.by_kit')"
91
+ EXP_BYKIT="$(printf '%s' "$EXPECTED_AGG" | jq -c '.by_kit')"
92
+ [[ "$GOT_BYKIT" == "$EXP_BYKIT" ]] && pass "by_kit[] equals expected-aggregates.json exactly" || fail "by_kit[] drifted from expected-aggregates.json: got=$GOT_BYKIT want=$EXP_BYKIT"
93
+ GOT_BYGATE="$(printf '%s' "$OUT_PP" | jq -c '.aggregates.by_gate')"
94
+ EXP_BYGATE="$(printf '%s' "$EXPECTED_AGG" | jq -c '.by_gate')"
95
+ [[ "$GOT_BYGATE" == "$EXP_BYGATE" ]] && pass "by_gate[] equals expected-aggregates.json exactly" || fail "by_gate[] drifted from expected-aggregates.json: got=$GOT_BYGATE want=$EXP_BYGATE"
96
+
97
+ # AC2: at least one evidence-cited kit-review-cost-inflation proposal, cost+defect paired.
98
+ KIT_PROP="$(printf '%s' "$OUT_PP" | jq -c '.proposals[] | select(.pattern=="kit-review-cost-inflation" and .target.id=="builder")')"
99
+ [[ -n "$KIT_PROP" ]] && pass "pattern-present yields a kit-review-cost-inflation proposal for builder" || fail "no kit-review-cost-inflation proposal found"
100
+ aeq "$OUT_PP" "kit proposal evidence.cost non-null" '.proposals[] | select(.pattern=="kit-review-cost-inflation") | (.evidence.cost != null)' 'true'
101
+ aeq "$OUT_PP" "kit proposal evidence.defect non-null" '.proposals[] | select(.pattern=="kit-review-cost-inflation") | (.evidence.defect != null)' 'true'
102
+ aeq "$OUT_PP" "kit proposal evidence.cost.cost_trend_pct == 100" '.proposals[] | select(.pattern=="kit-review-cost-inflation") | .evidence.cost.cost_trend_pct' '100'
103
+ aeq "$OUT_PP" "kit proposal evidence.defect.findings_delta_pct == 0" '.proposals[] | select(.pattern=="kit-review-cost-inflation") | .evidence.defect.findings_delta_pct' '0'
104
+
105
+ # AC2: evidence-cited gate-false-block-review proposal, cost+defect paired.
106
+ GATE_PROP="$(printf '%s' "$OUT_PP" | jq -c '.proposals[] | select(.pattern=="gate-false-block-review" and .target.id=="unit tests pass")')"
107
+ [[ -n "$GATE_PROP" ]] && pass "pattern-present yields a gate-false-block-review proposal" || fail "no gate-false-block-review proposal found"
108
+ aeq "$OUT_PP" "gate proposal evidence.cost non-null" '.proposals[] | select(.pattern=="gate-false-block-review") | (.evidence.cost != null)' 'true'
109
+ aeq "$OUT_PP" "gate proposal evidence.defect non-null" '.proposals[] | select(.pattern=="gate-false-block-review") | (.evidence.defect != null)' 'true'
110
+ aeq "$OUT_PP" "gate proposal evidence.defect.false_block_count == 3" '.proposals[] | select(.pattern=="gate-false-block-review") | .evidence.defect.false_block_count' '3'
111
+ aeq "$OUT_PP" "every proposal is severity advisory" '[.proposals[] | select(.severity != "advisory")] | length' '0'
112
+
113
+ # ── AC2: balanced — proportional cost+findings movement -> zero proposals ─────────────────────────
114
+ echo "--- AC2: balanced fixture — proportional cost+findings rise (30%/30%) yields zero proposals ---"
115
+ BAL="$FIX/balanced"
116
+ LEDGER_BAL="$TMP/ledger-balanced.jsonl"
117
+ OUT_BAL="$(bash "$ANALYZER" --ledger "$LEDGER_BAL" "$BAL/economics.jsonl")"
118
+ aeq "$OUT_BAL" "balanced: outcome == ok (enough samples, just no pattern)" '.outcome' '"ok"'
119
+ aeq "$OUT_BAL" "balanced: proposals == []" '.proposals' '[]'
120
+ aeq "$OUT_BAL" "balanced: by_kit cost_trend_pct == 30 (engineered, not a threshold accident)" '.aggregates.by_kit[0].cost_trend_pct' '30'
121
+ aeq "$OUT_BAL" "balanced: by_kit findings_delta_pct == 30 (rising in step with cost -> guarded)" '.aggregates.by_kit[0].findings_delta_pct' '30'
122
+
123
+ # ── AC4: under-threshold — below LR_MIN_WINDOW_SAMPLE -> explicit insufficient-data ────────────────
124
+ echo "--- AC4: under-threshold fixture — below LR_MIN_WINDOW_SAMPLE -> insufficient-data, not a thin confident call ---"
125
+ UT="$FIX/under-threshold"
126
+ LEDGER_UT="$TMP/ledger-under-threshold.jsonl"
127
+ OUT_UT="$(bash "$ANALYZER" --ledger "$LEDGER_UT" "$UT/economics.jsonl")"
128
+ aeq "$OUT_UT" "under-threshold: outcome == insufficient-data" '.outcome' '"insufficient-data"'
129
+ aeq "$OUT_UT" "under-threshold: proposals == []" '.proposals' '[]'
130
+ aeq "$OUT_UT" "under-threshold: aggregates.partial == true" '.aggregates.partial' 'true'
131
+ aeq "$OUT_UT" "under-threshold: records_considered == 3 (< default 5)" '.records_considered' '3'
132
+
133
+ # ── AC4: repeat-window — identical window run twice -> no duplicate proposal_id, already_proposed ─
134
+ echo "--- AC4: repeat-window fixture — running twice over the identical window de-dupes ---"
135
+ RW="$FIX/repeat-window"
136
+ LEDGER_RW="$TMP/ledger-repeat-window.jsonl"
137
+ OUT_RW1="$(bash "$ANALYZER" --ledger "$LEDGER_RW" "$RW/economics.jsonl")"
138
+ aeq "$OUT_RW1" "repeat-window run1: yields a real proposal to de-dupe" '[.proposals[] | select(.pattern=="kit-review-cost-inflation")] | length' '1'
139
+ aeq "$OUT_RW1" "repeat-window run1: already_proposed == false (first time)" '.proposals[] | select(.pattern=="kit-review-cost-inflation") | .already_proposed' 'false'
140
+ LINES_AFTER_RUN1=$(wc -l < "$LEDGER_RW" | tr -d ' ')
141
+ OUT_RW2="$(bash "$ANALYZER" --ledger "$LEDGER_RW" "$RW/economics.jsonl")"
142
+ aeq "$OUT_RW2" "repeat-window run2: already_proposed == true (de-duped)" '.proposals[] | select(.pattern=="kit-review-cost-inflation") | .already_proposed' 'true'
143
+ LINES_AFTER_RUN2=$(wc -l < "$LEDGER_RW" | tr -d ' ')
144
+ [[ "$LINES_AFTER_RUN1" == "$LINES_AFTER_RUN2" ]] && pass "ledger line count unchanged across the second run ($LINES_AFTER_RUN2 lines)" || fail "ledger grew on the second run: $LINES_AFTER_RUN1 -> $LINES_AFTER_RUN2"
145
+ UNIQUE_IDS=$(jq -r '.proposal_id' "$LEDGER_RW" | sort -u | wc -l | tr -d ' ')
146
+ TOTAL_IDS=$(jq -r '.proposal_id' "$LEDGER_RW" | wc -l | tr -d ' ')
147
+ [[ "$UNIQUE_IDS" == "$TOTAL_IDS" ]] && pass "no duplicate proposal_id in the ledger ($TOTAL_IDS line(s), $UNIQUE_IDS unique)" || fail "duplicate proposal_id found: $TOTAL_IDS lines but $UNIQUE_IDS unique"
148
+
149
+ # ── AC3: zero mutation to kit/gate/flow config; decide.sh refuses --follow-on-ref w/o --ratify ────
150
+ echo "--- AC3: analyzer + decide.sh make zero mutation to kits/**/.datum/config.json ---"
151
+ _snapshot() { find "$ROOT/kits" "$ROOT/.datum" -type f 2>/dev/null | sort | xargs shasum 2>/dev/null | shasum | awk '{print $1}'; }
152
+ BEFORE_HASH="$(_snapshot)"
153
+ # Re-run the analyzer over every fixture (already exercised above) plus a decide.sh call, all
154
+ # pointed at TMP-sandboxed ledgers/paths only — never at anything under kits/ or .datum/.
155
+ : "$(bash "$ANALYZER" --sessions-root "$PP/sessions" --ledger "$TMP/ledger-ac3-scratch.jsonl" "$PP/economics.jsonl")"
156
+ SCRATCH_PID="$(jq -r '.proposal_id' "$TMP/ledger-ac3-scratch.jsonl" | head -1)"
157
+ : "$(bash "$DECIDER" "$TMP/ledger-ac3-scratch.jsonl" "$SCRATCH_PID" --ratify --decided-by tester --follow-on-ref "https://example.invalid/1" 2>/dev/null)"
158
+ AFTER_HASH="$(_snapshot)"
159
+ [[ "$BEFORE_HASH" == "$AFTER_HASH" ]] && pass "kits/ and .datum/ content hash unchanged after analyzer + decide.sh runs" || fail "kits/ or .datum/ content changed (mutation detected!)"
160
+
161
+ # decide.sh refuses --follow-on-ref without --ratify (checksum-verified no-write).
162
+ DECIDE_LEDGER="$TMP/ledger-decide-guard.jsonl"
163
+ cp "$LEDGER_RW" "$DECIDE_LEDGER"
164
+ GUARD_PID="$(jq -r '.proposal_id' "$DECIDE_LEDGER" | head -1)"
165
+ CHECKSUM_BEFORE="$(shasum "$DECIDE_LEDGER" | awk '{print $1}')"
166
+ bash "$DECIDER" "$DECIDE_LEDGER" "$GUARD_PID" --reject --decided-by tester --follow-on-ref "https://example.invalid/2" >/dev/null 2>&1
167
+ GUARD_RC=$?
168
+ CHECKSUM_AFTER="$(shasum "$DECIDE_LEDGER" | awk '{print $1}')"
169
+ [[ "$GUARD_RC" -ne 0 ]] && pass "decide.sh exits non-zero for --follow-on-ref without --ratify" || fail "decide.sh should have refused --follow-on-ref without --ratify"
170
+ [[ "$CHECKSUM_BEFORE" == "$CHECKSUM_AFTER" ]] && pass "decide.sh wrote nothing when refusing --follow-on-ref without --ratify" || fail "decide.sh wrote despite refusing"
171
+
172
+ # decide.sh also refuses an unknown proposal-id, and more than one of --ratify/--reject/--defer.
173
+ if bash "$DECIDER" "$DECIDE_LEDGER" "no-such-proposal-id" --ratify --decided-by tester >/dev/null 2>&1; then
174
+ fail "decide.sh should refuse an unknown proposal-id"
175
+ else
176
+ pass "decide.sh refuses an unknown proposal-id"
177
+ fi
178
+ if bash "$DECIDER" "$DECIDE_LEDGER" "$GUARD_PID" --ratify --reject --decided-by tester >/dev/null 2>&1; then
179
+ fail "decide.sh should refuse conflicting decision flags"
180
+ else
181
+ pass "decide.sh refuses more than one of --ratify/--reject/--defer"
182
+ fi
183
+
184
+ # ── AC5: ratify a pattern-present proposal, then a later window fills in effect_observed ──────────
185
+ echo "--- AC5: ratify pattern-present proposal -> follow_on_ref set; later window fills effect_observed ---"
186
+ EF="$FIX/effect-follow-up"
187
+ LEDGER_AC5="$TMP/ledger-ac5.jsonl"
188
+ OUT_AC5_1="$(bash "$ANALYZER" --sessions-root "$PP/sessions" --ledger "$LEDGER_AC5" "$PP/economics.jsonl")"
189
+ AC5_PID="$(printf '%s' "$OUT_AC5_1" | jq -r '.proposals[] | select(.pattern=="kit-review-cost-inflation") | .proposal_id')"
190
+ FOLLOW_ON_URL="https://github.com/kontourai/flow-agents/issues/99999"
191
+ bash "$DECIDER" "$LEDGER_AC5" "$AC5_PID" --ratify --decided-by "eval-tester" --follow-on-ref "$FOLLOW_ON_URL" >/dev/null
192
+ RATIFIED_ENTRY="$(jq -c --arg pid "$AC5_PID" 'select(.proposal_id == $pid)' "$LEDGER_AC5")"
193
+ [[ "$(printf '%s' "$RATIFIED_ENTRY" | jq -r '.decision.status')" == "ratified" ]] && pass "ledger entry decision.status == ratified after decide.sh --ratify" || fail "decision.status not ratified"
194
+ [[ "$(printf '%s' "$RATIFIED_ENTRY" | jq -r '.follow_on_ref')" == "$FOLLOW_ON_URL" ]] && pass "ledger entry follow_on_ref set to the fixture issue URL" || fail "follow_on_ref not set"
195
+ [[ "$(printf '%s' "$RATIFIED_ENTRY" | jq -c '.effect_observed')" == "null" ]] && pass "effect_observed still null before the later window pass" || fail "effect_observed should still be null before a later pass"
196
+
197
+ OUT_AC5_2="$(bash "$ANALYZER" --sessions-root "$EF/sessions" --ledger "$LEDGER_AC5" "$EF/economics.jsonl")"
198
+ RATIFIED_ENTRY_2="$(jq -c --arg pid "$AC5_PID" 'select(.proposal_id == $pid)' "$LEDGER_AC5")"
199
+ EFFECT="$(printf '%s' "$RATIFIED_ENTRY_2" | jq -c '.effect_observed')"
200
+ [[ "$EFFECT" != "null" ]] && pass "effect_observed populated after the later effect-follow-up window ($EFFECT)" || fail "effect_observed still null after the later window pass"
201
+ aeq "$RATIFIED_ENTRY_2" "effect_observed.metric == avg_cost_usd" '.effect_observed.metric' '"avg_cost_usd"'
202
+ aeq "$RATIFIED_ENTRY_2" "effect_observed.before == 0.2 (original evidence.cost.second_half_avg_cost_usd)" '.effect_observed.before' '0.2'
203
+ aeq "$RATIFIED_ENTRY_2" "effect_observed.after == 0.05 (effect-follow-up's second_half_avg_cost_usd)" '.effect_observed.after' '0.05'
204
+ aeq "$RATIFIED_ENTRY_2" "effect_observed.moved == improved (cost fell, expected_effect.direction was decrease)" '.effect_observed.moved' '"improved"'
205
+ : "$OUT_AC5_2"
206
+
207
+ # ── regression: --since/--until ISO-8601 (date-only, Z, +00:00) all parse to a real windowed pass ──
208
+ # Code-review iteration 2 finding [HIGH]: to_epoch_ms's old `sub("Z$"; "+00:00")` direction broke
209
+ # every ISO-8601 input (jq's fromdateiso8601 only understands a literal "Z", never "+00:00"), so a
210
+ # genuinely parseable --since/--until silently produced a FALSE "insufficient-data"/fallback-envelope
211
+ # outcome instead of a real windowed analysis. pattern-present's fixture .at values all fall on
212
+ # 2025-07-02 (23:46:40-45 UTC) — cover date-only, full-ISO-with-Z, and full-ISO-with-+00:00 bounds.
213
+ echo "--- regression: ISO-8601 --since/--until (date-only, Z, +00:00) parse correctly (not a false insufficient-data) ---"
214
+ OUT_ISO_DATEONLY="$(bash "$ANALYZER" --since 2025-07-02 --until 2025-07-03 --ledger "$TMP/ledger-iso-dateonly.jsonl" "$PP/economics.jsonl")"
215
+ aeq "$OUT_ISO_DATEONLY" "date-only --since/--until: outcome == ok (real windowed pass, not a parse-failure fallback)" '.outcome' '"ok"'
216
+ aeq "$OUT_ISO_DATEONLY" "date-only --since/--until: records_considered == 6" '.records_considered' '6'
217
+
218
+ OUT_ISO_Z="$(bash "$ANALYZER" --since 2025-07-02T00:00:00Z --until 2025-07-03T00:00:00Z --ledger "$TMP/ledger-iso-z.jsonl" "$PP/economics.jsonl")"
219
+ aeq "$OUT_ISO_Z" "full ISO-8601 with Z: outcome == ok" '.outcome' '"ok"'
220
+ aeq "$OUT_ISO_Z" "full ISO-8601 with Z: records_considered == 6" '.records_considered' '6'
221
+
222
+ OUT_ISO_OFFSET="$(bash "$ANALYZER" --since 2025-07-02T00:00:00+00:00 --until 2025-07-03T00:00:00+00:00 --ledger "$TMP/ledger-iso-offset.jsonl" "$PP/economics.jsonl")"
223
+ aeq "$OUT_ISO_OFFSET" "full ISO-8601 with +00:00 offset: outcome == ok" '.outcome' '"ok"'
224
+ aeq "$OUT_ISO_OFFSET" "full ISO-8601 with +00:00 offset: records_considered == 6" '.records_considered' '6'
225
+
226
+ # all three forms resolve to byte-identical effective window bounds.
227
+ WIN_DATEONLY="$(printf '%s' "$OUT_ISO_DATEONLY" | jq -c '.window')"
228
+ WIN_Z="$(printf '%s' "$OUT_ISO_Z" | jq -c '.window')"
229
+ WIN_OFFSET="$(printf '%s' "$OUT_ISO_OFFSET" | jq -c '.window')"
230
+ [[ "$WIN_DATEONLY" == "$WIN_Z" && "$WIN_Z" == "$WIN_OFFSET" ]] && pass "date-only / Z / +00:00 all resolve to the identical effective window ($WIN_Z)" || fail "window bounds diverged: dateonly=$WIN_DATEONLY z=$WIN_Z offset=$WIN_OFFSET"
231
+
232
+ # a record's own ISO .at (not just --since/--until) also parses: reuse balanced fixture's epoch .at
233
+ # values by re-deriving an equivalent ISO-stamped record set on the fly (no new fixture file needed).
234
+ ISO_AT_LOG="$TMP/iso-at.jsonl"
235
+ jq -c '.at |= ((tonumber / 1000) | todateiso8601)' "$PP/economics.jsonl" > "$ISO_AT_LOG"
236
+ OUT_ISO_AT="$(bash "$ANALYZER" --ledger "$TMP/ledger-iso-at.jsonl" "$ISO_AT_LOG")"
237
+ aeq "$OUT_ISO_AT" "record .at as full ISO-8601 (Z) parses: outcome == ok" '.outcome' '"ok"'
238
+ aeq "$OUT_ISO_AT" "record .at as full ISO-8601 (Z) parses: records_considered == 6" '.records_considered' '6'
239
+
240
+ # ── regression: fractional-second ISO-8601 (.sssZ / .sss+00:00) parses to a real windowed pass ─────
241
+ # Code-review iteration 3 finding [MEDIUM]: a JS Date.toISOString()-style fractional-second
242
+ # timestamp (e.g. 2025-07-01T00:00:00.123Z) used to hit the old `test("Z$")` branch and throw
243
+ # inside fromdateiso8601 (which only understands whole seconds), silently degrading to a false
244
+ # insufficient-data/fallback-envelope outcome instead of a real windowed analysis.
245
+ echo "--- regression: fractional-second ISO-8601 (.sssZ / .sss+00:00) --since/--until parse correctly ---"
246
+ OUT_FRAC_Z="$(bash "$ANALYZER" --since "2025-07-02T00:00:00.000Z" --until "2025-07-03T00:00:00.999Z" --ledger "$TMP/ledger-frac-z.jsonl" "$PP/economics.jsonl")"
247
+ aeq "$OUT_FRAC_Z" "fractional-second ISO with Z: outcome == ok" '.outcome' '"ok"'
248
+ aeq "$OUT_FRAC_Z" "fractional-second ISO with Z: records_considered == 6" '.records_considered' '6'
249
+
250
+ OUT_FRAC_OFFSET="$(bash "$ANALYZER" --since "2025-07-02T00:00:00.000+00:00" --until "2025-07-03T00:00:00.999+00:00" --ledger "$TMP/ledger-frac-offset.jsonl" "$PP/economics.jsonl")"
251
+ aeq "$OUT_FRAC_OFFSET" "fractional-second ISO with +00:00 offset: outcome == ok" '.outcome' '"ok"'
252
+ aeq "$OUT_FRAC_OFFSET" "fractional-second ISO with +00:00 offset: records_considered == 6" '.records_considered' '6'
253
+
254
+ # ── regression: a garbage --since/--until FAILS LOUD (never masquerades as insufficient-data) ──────
255
+ # Code-review iteration 3 finding [MEDIUM] item 2: an unrecognized --since/--until value is
256
+ # operator-correctable CLI input and must exit non-zero with a stderr message — NEVER silently
257
+ # look like an honest "insufficient-data" result (which would hide a usage error as a data gap).
258
+ echo "--- regression: garbage --since exits non-zero with a stderr message, NOT insufficient-data ---"
259
+ GARBAGE_STDOUT="$TMP/garbage-since-stdout.txt"
260
+ GARBAGE_STDERR="$TMP/garbage-since-stderr.txt"
261
+ LEDGER_GARBAGE="$TMP/ledger-garbage-since.jsonl"
262
+ bash "$ANALYZER" --since "notadate" --ledger "$LEDGER_GARBAGE" "$PP/economics.jsonl" >"$GARBAGE_STDOUT" 2>"$GARBAGE_STDERR"
263
+ GARBAGE_RC=$?
264
+ [[ "$GARBAGE_RC" -ne 0 ]] && pass "garbage --since exits non-zero (rc=$GARBAGE_RC)" || fail "garbage --since should have exited non-zero (rc=$GARBAGE_RC)"
265
+ [[ ! -s "$GARBAGE_STDOUT" ]] && pass "garbage --since prints no stdout document (never a fabricated insufficient-data envelope)" || fail "garbage --since printed a document: $(cat "$GARBAGE_STDOUT")"
266
+ grep -qi "invalid --since" "$GARBAGE_STDERR" && pass "garbage --since prints a clear stderr diagnostic" || fail "garbage --since produced no clear stderr diagnostic: $(cat "$GARBAGE_STDERR")"
267
+ [[ ! -e "$LEDGER_GARBAGE" ]] && pass "garbage --since writes no ledger file" || fail "garbage --since unexpectedly created a ledger file"
268
+
269
+ # ── regression: a corrupted ledger line FAILS LOUD, writes NOTHING, valid entries survive ──────────
270
+ # Code-review iteration 2 finding [CRITICAL]: `jq -s` on a malformed ledger line used to fail
271
+ # silently, fall back to "[]", and the later mv would overwrite the ledger — silently destroying
272
+ # every recorded human ratify/reject/defer decision. Must now exit non-zero with zero bytes written.
273
+ echo "--- regression: malformed ledger line -> analyzer exits non-zero, writes nothing, valid entry survives ---"
274
+ CORRUPT_LEDGER="$TMP/ledger-corrupt.jsonl"
275
+ cat > "$CORRUPT_LEDGER" <<'EOF2'
276
+ {"proposal_id":"valid-1","target":{"kind":"kit","id":"builder"},"pattern":"kit-review-cost-inflation","proposed_change":"x","severity":"advisory","evidence":{"cost":{},"defect":{}},"expected_effect":{"metric":"avg_cost_usd","direction":"decrease","description":"x"},"decision":{"status":"ratified","decided_by":"tester","decided_at":"2026-01-01T00:00:00Z","rationale":"r"},"follow_on_ref":"https://example.invalid/1","effect_observed":null,"window":{"since":1000,"until":2000}}
277
+ {this line is not valid json at all
278
+ EOF2
279
+ CORRUPT_BEFORE_HASH="$(shasum "$CORRUPT_LEDGER" | awk '{print $1}')"
280
+ EMPTY_LOG="$TMP/empty-for-corrupt.jsonl"
281
+ : > "$EMPTY_LOG"
282
+ CORRUPT_STDOUT="$(bash "$ANALYZER" --ledger "$CORRUPT_LEDGER" "$EMPTY_LOG" 2>"$TMP/corrupt-stderr.txt")"
283
+ CORRUPT_RC=$?
284
+ CORRUPT_AFTER_HASH="$(shasum "$CORRUPT_LEDGER" | awk '{print $1}')"
285
+ [[ "$CORRUPT_RC" -ne 0 ]] && pass "analyzer exits non-zero on a corrupted ledger (rc=$CORRUPT_RC)" || fail "analyzer should have exited non-zero on a corrupted ledger (rc=$CORRUPT_RC)"
286
+ [[ -z "$CORRUPT_STDOUT" ]] && pass "analyzer prints no stdout document on a corrupted ledger (fail loud, not a silent fallback envelope)" || fail "analyzer printed a document despite a corrupted ledger: $CORRUPT_STDOUT"
287
+ [[ -s "$TMP/corrupt-stderr.txt" ]] && pass "analyzer prints a stderr diagnostic on a corrupted ledger" || fail "analyzer produced no stderr diagnostic"
288
+ [[ "$CORRUPT_BEFORE_HASH" == "$CORRUPT_AFTER_HASH" ]] && pass "corrupted ledger file is byte-identical after the run (zero bytes written)" || fail "corrupted ledger file was mutated (data-loss regression!)"
289
+ grep -q '"proposal_id":"valid-1"' "$CORRUPT_LEDGER" && pass "the valid ledger entry (with its ratified decision) survives untouched" || fail "the valid ledger entry was lost"
290
+
291
+ # ── Schema validation (Ajv, mirroring test_economics_record.sh's approach) ─────────────────────────
292
+ echo "--- schema: pattern-present output validates; a doctored cost-only proposal FAILS (Goodhart guard) ---"
293
+ if node -e "require.resolve('ajv')" >/dev/null 2>&1; then
294
+ SCHEMA_CHECK="$(node -e '
295
+ const Ajv=require("ajv");
296
+ const a=new Ajv({allErrors:true,strict:false});
297
+ const v=a.compile(require(process.argv[1]));
298
+ const doc=JSON.parse(process.argv[2]);
299
+ const docOk=v(doc);
300
+ const bad=JSON.parse(JSON.stringify(doc));
301
+ if (bad.proposals && bad.proposals.length > 0) { delete bad.proposals[0].evidence.defect; }
302
+ const badOk=v(bad);
303
+ console.log(JSON.stringify({docOk, badOk, errors: v.errors}));
304
+ ' "$SCHEMA" "$OUT_PP" 2>&1)"
305
+ echo "$SCHEMA_CHECK" | grep -q '"docOk":true' && pass "pattern-present output validates against learning-review-proposals.schema.json" || fail "pattern-present output did NOT validate: $SCHEMA_CHECK"
306
+ echo "$SCHEMA_CHECK" | grep -q '"badOk":false' && pass "cost-only proposal (defect deleted) FAILS validation — Goodhart co-required guard holds" || fail "cost-only proposal wrongly validated: $SCHEMA_CHECK"
307
+ else
308
+ echo " NOT_VERIFIED: ajv module unavailable (no node_modules/ajv) — schema validation skipped. Run \`npm ci\` to enable this check."
309
+ fi
310
+
311
+ # ── static guard: neither script writes to kits/** or .datum/config.json (mirrors #415's guard) ───
312
+ echo "--- static guard: neither script references kits/**/.datum/config.json outside a 'never' disclaimer ---"
313
+ for f in "$ANALYZER" "$DECIDER"; do
314
+ name="$(basename "$f")"
315
+ if grep -qE '\.datum/config\.json' "$f" && ! grep -qE 'never (writes?|edit|auto-appl)' "$f"; then
316
+ fail "$name references .datum/config.json outside a 'never write' disclaimer"
317
+ else
318
+ pass "$name never writes .datum/config.json (advisory only, human-ratified)"
319
+ fi
320
+ if grep -qE 'kits/\*\*|kits/<' "$f" && ! grep -qE 'never (writes?|edit|auto-appl)' "$f"; then
321
+ fail "$name references kits/** outside a 'never write' disclaimer"
322
+ else
323
+ pass "$name never writes kits/** (advisory only, human-ratified)"
324
+ fi
325
+ done
326
+
327
+ echo ""
328
+ if [[ "$errors" -eq 0 ]]; then echo "test_learning_review_proposals: all checks passed."; exit 0
329
+ else echo "test_learning_review_proposals: $errors check(s) failed."; exit 1; fi
@@ -124,17 +124,13 @@ NODE
124
124
 
125
125
  # is_valid_json <string> — true if the given string parses as JSON.
126
126
  is_valid_json() {
127
- printf '%s' "$1" | node - <<'NODE'
128
- let raw = '';
129
- process.stdin.on('data', c => (raw += c));
130
- process.stdin.on('end', () => {
131
- try {
132
- JSON.parse(raw);
133
- process.exit(0);
134
- } catch {
135
- process.exit(1);
136
- }
137
- });
127
+ JSON_ARG="$1" node - <<'NODE'
128
+ try {
129
+ JSON.parse(process.env.JSON_ARG);
130
+ process.exit(0);
131
+ } catch {
132
+ process.exit(1);
133
+ }
138
134
  NODE
139
135
  }
140
136
 
@@ -358,6 +354,10 @@ echo "--- D. Wrapper-level conflict injection (Claude Code, Codex) ---"
358
354
 
359
355
  POST_TOOL_USE_PAYLOAD='{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"echo hi"},"tool_response":{"stdout":"hi"}}'
360
356
  CODEX_PAYLOAD='{"hook_event_name":"PostToolUse","tool_name":"shell","tool_input":{"command":"echo hi"}}'
357
+ POST_TOOL_USE_PAYLOAD_FILE="$TMPDIR_EVAL/post-tool-use-payload.json"
358
+ CODEX_PAYLOAD_FILE="$TMPDIR_EVAL/codex-payload.json"
359
+ printf '%s' "$POST_TOOL_USE_PAYLOAD" >"$POST_TOOL_USE_PAYLOAD_FILE"
360
+ printf '%s' "$CODEX_PAYLOAD" >"$CODEX_PAYLOAD_FILE"
361
361
 
362
362
  # seed_conflict_fixture <root> <slug> <self_actor> <other_actor>
363
363
  # Our own claim is well past the throttle window (real clock); the other actor's claim is
@@ -375,7 +375,7 @@ seed_conflict_fixture() {
375
375
  D1_ROOT="$(new_scratch)"
376
376
  seed_conflict_fixture "$D1_ROOT" "d1-subj" "agent-hb" "agent-conflict-claude"
377
377
  D1_OUT="$(cd "$D1_ROOT" && TELEMETRY_ENABLED=false FLOW_AGENTS_ACTOR=agent-hb \
378
- node "$CLAUDE_HOOK" PostToolUse dev <<<"$POST_TOOL_USE_PAYLOAD")"
378
+ node "$CLAUDE_HOOK" PostToolUse dev <"$POST_TOOL_USE_PAYLOAD_FILE")"
379
379
  D1_STATUS=$?
380
380
  D1_CONTEXT="$(field_of "$D1_OUT" hookSpecificOutput.additionalContext)"
381
381
  if [[ "$D1_STATUS" -eq 0 ]] && is_valid_json "$D1_OUT" \
@@ -390,7 +390,7 @@ fi
390
390
  D2_ROOT="$(new_scratch)"
391
391
  seed_conflict_fixture "$D2_ROOT" "d2-subj" "agent-hb" "agent-conflict-codex"
392
392
  D2_OUT="$(cd "$D2_ROOT" && TELEMETRY_ENABLED=false FLOW_AGENTS_ACTOR=agent-hb \
393
- node "$CODEX_HOOK" PostToolUse dev <<<"$CODEX_PAYLOAD")"
393
+ node "$CODEX_HOOK" PostToolUse dev <"$CODEX_PAYLOAD_FILE")"
394
394
  D2_STATUS=$?
395
395
  D2_CONTEXT="$(field_of "$D2_OUT" hookSpecificOutput.additionalContext)"
396
396
  if [[ "$D2_STATUS" -eq 0 ]] && is_valid_json "$D2_OUT" \
@@ -406,6 +406,10 @@ echo "--- E. Degraded-runtime proof (opencode, pi) ---"
406
406
 
407
407
  OPENCODE_PAYLOAD='{"hook_event_name":"tool.execute.after"}'
408
408
  PI_PAYLOAD='{"hook_event_name":"tool_result"}'
409
+ OPENCODE_PAYLOAD_FILE="$TMPDIR_EVAL/opencode-payload.json"
410
+ PI_PAYLOAD_FILE="$TMPDIR_EVAL/pi-payload.json"
411
+ printf '%s' "$OPENCODE_PAYLOAD" >"$OPENCODE_PAYLOAD_FILE"
412
+ printf '%s' "$PI_PAYLOAD" >"$PI_PAYLOAD_FILE"
409
413
 
410
414
  # E1: opencode-telemetry-hook.js — no stdout context-injection field (stdout byte-empty), but a
411
415
  # stderr diagnostic naming the conflicting actor (the plugin does not consume this wrapper's
@@ -413,7 +417,7 @@ PI_PAYLOAD='{"hook_event_name":"tool_result"}'
413
417
  E1_ROOT="$(new_scratch)"
414
418
  seed_conflict_fixture "$E1_ROOT" "e1-subj" "agent-hb" "agent-conflict-opencode"
415
419
  E1_OUT="$(cd "$E1_ROOT" && TELEMETRY_ENABLED=false FLOW_AGENTS_ACTOR=agent-hb \
416
- node "$OPENCODE_HOOK" 'tool.execute.after' dev <<<"$OPENCODE_PAYLOAD" 2>"$TMPDIR_EVAL/e1.err")"
420
+ node "$OPENCODE_HOOK" 'tool.execute.after' dev <"$OPENCODE_PAYLOAD_FILE" 2>"$TMPDIR_EVAL/e1.err")"
417
421
  E1_STATUS=$?
418
422
  E1_ERR="$(cat "$TMPDIR_EVAL/e1.err")"
419
423
  if [[ "$E1_STATUS" -eq 0 ]] && [[ -z "$E1_OUT" ]] \
@@ -427,7 +431,7 @@ fi
427
431
  E2_ROOT="$(new_scratch)"
428
432
  seed_conflict_fixture "$E2_ROOT" "e2-subj" "agent-hb" "agent-conflict-pi"
429
433
  E2_OUT="$(cd "$E2_ROOT" && TELEMETRY_ENABLED=false FLOW_AGENTS_ACTOR=agent-hb \
430
- node "$PI_HOOK" 'tool_result' dev <<<"$PI_PAYLOAD" 2>"$TMPDIR_EVAL/e2.err")"
434
+ node "$PI_HOOK" 'tool_result' dev <"$PI_PAYLOAD_FILE" 2>"$TMPDIR_EVAL/e2.err")"
431
435
  E2_STATUS=$?
432
436
  E2_ERR="$(cat "$TMPDIR_EVAL/e2.err")"
433
437
  if [[ "$E2_STATUS" -eq 0 ]] && [[ -z "$E2_OUT" ]] \
@@ -452,7 +456,7 @@ seed_solo_fixture() {
452
456
  F1_ROOT="$(new_scratch)"
453
457
  seed_solo_fixture "$F1_ROOT" "f1-subj" "agent-hb"
454
458
  F1_OUT="$(cd "$F1_ROOT" && TELEMETRY_ENABLED=false FLOW_AGENTS_ACTOR=agent-hb \
455
- node "$CLAUDE_HOOK" PostToolUse dev <<<"$POST_TOOL_USE_PAYLOAD")"
459
+ node "$CLAUDE_HOOK" PostToolUse dev <"$POST_TOOL_USE_PAYLOAD_FILE")"
456
460
  if [[ "$F1_OUT" == '{"continue":true,"suppressOutput":true}' ]]; then
457
461
  _pass "claude-telemetry-hook.js no-conflict PostToolUse output is byte-stable vs the pre-existing fixed shape (AC4)"
458
462
  else
@@ -462,7 +466,7 @@ fi
462
466
  F2_ROOT="$(new_scratch)"
463
467
  seed_solo_fixture "$F2_ROOT" "f2-subj" "agent-hb"
464
468
  F2_OUT="$(cd "$F2_ROOT" && TELEMETRY_ENABLED=false FLOW_AGENTS_ACTOR=agent-hb \
465
- node "$CODEX_HOOK" PostToolUse dev <<<"$CODEX_PAYLOAD")"
469
+ node "$CODEX_HOOK" PostToolUse dev <"$CODEX_PAYLOAD_FILE")"
466
470
  if [[ -z "$F2_OUT" ]]; then
467
471
  _pass "codex-telemetry-hook.js no-conflict PostToolUse output is byte-stable (empty stdout, unchanged) vs the pre-existing fixed shape (AC4)"
468
472
  else
@@ -472,7 +476,7 @@ fi
472
476
  F3_ROOT="$(new_scratch)"
473
477
  seed_solo_fixture "$F3_ROOT" "f3-subj" "agent-hb"
474
478
  F3_OUT="$(cd "$F3_ROOT" && TELEMETRY_ENABLED=false FLOW_AGENTS_ACTOR=agent-hb \
475
- node "$OPENCODE_HOOK" 'tool.execute.after' dev <<<"$OPENCODE_PAYLOAD" 2>"$TMPDIR_EVAL/f3.err")"
479
+ node "$OPENCODE_HOOK" 'tool.execute.after' dev <"$OPENCODE_PAYLOAD_FILE" 2>"$TMPDIR_EVAL/f3.err")"
476
480
  F3_ERR="$(cat "$TMPDIR_EVAL/f3.err")"
477
481
  if [[ -z "$F3_OUT" ]] && [[ "$F3_ERR" != *"liveness conflict"* ]]; then
478
482
  _pass "opencode-telemetry-hook.js no-conflict output is byte-stable (empty stdout, no diagnostic) vs the pre-existing fixed shape (AC4)"
@@ -483,7 +487,7 @@ fi
483
487
  F4_ROOT="$(new_scratch)"
484
488
  seed_solo_fixture "$F4_ROOT" "f4-subj" "agent-hb"
485
489
  F4_OUT="$(cd "$F4_ROOT" && TELEMETRY_ENABLED=false FLOW_AGENTS_ACTOR=agent-hb \
486
- node "$PI_HOOK" 'tool_result' dev <<<"$PI_PAYLOAD" 2>"$TMPDIR_EVAL/f4.err")"
490
+ node "$PI_HOOK" 'tool_result' dev <"$PI_PAYLOAD_FILE" 2>"$TMPDIR_EVAL/f4.err")"
487
491
  F4_ERR="$(cat "$TMPDIR_EVAL/f4.err")"
488
492
  if [[ -z "$F4_OUT" ]] && [[ "$F4_ERR" != *"liveness conflict"* ]]; then
489
493
  _pass "pi-telemetry-hook.js no-conflict output is byte-stable (empty stdout, no diagnostic) vs the pre-existing fixed shape (AC4)"
@@ -498,7 +502,7 @@ G_ROOT="$(new_scratch)"
498
502
  mkdir -p "$G_ROOT/.kontourai/flow-agents/liveness"
499
503
  printf '{not valid json' >"$G_ROOT/.kontourai/flow-agents/$SNAPSHOT_FILENAME"
500
504
  G_OUT="$(cd "$G_ROOT" && TELEMETRY_ENABLED=false FLOW_AGENTS_ACTOR=agent-hb \
501
- node "$CLAUDE_HOOK" PostToolUse dev <<<"$POST_TOOL_USE_PAYLOAD" 2>"$TMPDIR_EVAL/g.err")"
505
+ node "$CLAUDE_HOOK" PostToolUse dev <"$POST_TOOL_USE_PAYLOAD_FILE" 2>"$TMPDIR_EVAL/g.err")"
502
506
  G_STATUS=$?
503
507
  if [[ "$G_STATUS" -eq 0 ]] && is_valid_json "$G_OUT" && [[ "$(stream_line_count "$G_ROOT")" -eq 0 ]]; then
504
508
  _pass "claude-telemetry-hook.js fails open on a corrupted sidecar snapshot during the conflict check: exit 0, normal JSON contract, no heartbeat fabricated (AC8)"
@@ -546,7 +550,7 @@ H1_ROOT="$(new_scratch)"
546
550
  seed_claim "$H1_ROOT" "h1-subj" "agent-hb" "$(iso_offset_ms -300000)" 1800
547
551
  append_hostile_claim "$H1_ROOT" "h1-subj" "$HOSTILE_ACTOR" "$(iso_offset_ms -10000)" 1800
548
552
  H1_OUT="$(cd "$H1_ROOT" && TELEMETRY_ENABLED=false FLOW_AGENTS_ACTOR=agent-hb \
549
- node "$CLAUDE_HOOK" PostToolUse dev <<<"$POST_TOOL_USE_PAYLOAD")"
553
+ node "$CLAUDE_HOOK" PostToolUse dev <"$POST_TOOL_USE_PAYLOAD_FILE")"
550
554
  H1_STATUS=$?
551
555
  H1_CONTEXT="$(field_of "$H1_OUT" hookSpecificOutput.additionalContext)"
552
556
  if [[ "$H1_STATUS" -eq 0 ]] && is_valid_json "$H1_OUT" \
@@ -564,7 +568,7 @@ H2_ROOT="$(new_scratch)"
564
568
  seed_claim "$H2_ROOT" "h2-subj" "agent-hb" "$(iso_offset_ms -300000)" 1800
565
569
  append_hostile_claim "$H2_ROOT" "h2-subj" "$HOSTILE_ACTOR" "$(iso_offset_ms -10000)" 1800
566
570
  H2_OUT="$(cd "$H2_ROOT" && TELEMETRY_ENABLED=false FLOW_AGENTS_ACTOR=agent-hb \
567
- node "$OPENCODE_HOOK" 'tool.execute.after' dev <<<"$OPENCODE_PAYLOAD" 2>"$TMPDIR_EVAL/h2.err")"
571
+ node "$OPENCODE_HOOK" 'tool.execute.after' dev <"$OPENCODE_PAYLOAD_FILE" 2>"$TMPDIR_EVAL/h2.err")"
568
572
  H2_STATUS=$?
569
573
  H2_ERR="$(cat "$TMPDIR_EVAL/h2.err")"
570
574
  if [[ "$H2_STATUS" -eq 0 ]] && [[ -z "$H2_OUT" ]] \