@kontourai/flow-agents 3.3.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 (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 +41 -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 +54 -1
  34. package/build/src/lib/flow-resolver.js +112 -5
  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 +72 -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 +117 -4
  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,674 @@
1
+ #!/usr/bin/env bash
2
+ # test_economics_record.sh — per-run kit-economics record (#349, console ADR 0003).
3
+ #
4
+ # Proves scripts/telemetry/economics-record.sh assembles exactly one immutable
5
+ # `kontour.console.economics` v0.1 fact per run — cost, time, iterations, defects caught — from the
6
+ # session.usage event's usage block (TRANSCRIPT ground truth, never re-estimated), the review sidecar
7
+ # critique.json (defects), and state.json (verdict / phase / iterations). And — non-negotiable —
8
+ # local-first holds (ADR 0003 call 6): the record is written to the local economics log FIRST, the
9
+ # console POST is opt-in + detached + fail-open, and a failing/absent console never blocks the run.
10
+ #
11
+ # Coverage → ACs:
12
+ # AC1/AC6 (R1,R7) schema: golden fixture validates; a cost-only doctored record (no defects) FAILS.
13
+ # AC2 (R2,R8) cost.* equals the session.usage usage block; exactly one record; local write.
14
+ # AC3 (R3) phase-sum invariant, phase-known AND phase-unknown (→ unattributed).
15
+ # AC4 (R4,R6) route-back / verdict: iterations.count==2, route_backs==1, verdict PASS, human_wait>0.
16
+ # AC5 (R5) gate_fires, per-severity findings_by_severity, caught_false_completions==1 (distinct).
17
+ # AC7 (R8) telemetry-disabled + console-500 fail-open: run unaffected, local record still exists.
18
+ # injection hostile task_slug/finding text → jq \u-escaped, never raw control bytes in the POST.
19
+ #
20
+ # Deterministic (a local stub HTTP server, no network / model spend, self-cleaning). The relay POST is
21
+ # detached/async so stub-received assertions poll with a timeout.
22
+ # Usage: bash evals/integration/test_economics_record.sh
23
+ set -uo pipefail
24
+
25
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
26
+ TELEMETRY="$ROOT/scripts/telemetry"
27
+ EMITTER="$TELEMETRY/economics-record.sh"
28
+ SCHEMA="$TELEMETRY/economics-record.schema.json"
29
+ FIX="$ROOT/evals/fixtures/economics"
30
+
31
+ TMP="$(mktemp -d)"
32
+ trap 'rm -rf "$TMP"; [[ -n "${STUB_PID:-}" ]] && kill "$STUB_PID" 2>/dev/null' EXIT
33
+ PORT=38812
34
+ RECV="$TMP/recv.jsonl"
35
+
36
+ # Hermetic isolation: this suite must be deterministic regardless of what the machine running it has
37
+ # configured for real (e.g. an operator's trusted .kontourai/telemetry-console.conf / ~/.flow-agents
38
+ # conf). Point every emitter invocation below at an empty scratch conf by default so config.sh's
39
+ # key-parser never resolves real console_telemetry_token/console_tenant_id/console_telemetry_url
40
+ # values into these assertions; explicit TELEMETRY_CONFIG_FILE exports in the config-driven (#469)
41
+ # cases below intentionally override this default per-case.
42
+ EMPTY_CONF="$TMP/empty-telemetry.conf"
43
+ : > "$EMPTY_CONF"
44
+ export TELEMETRY_CONFIG_FILE="$EMPTY_CONF"
45
+
46
+ errors=0
47
+ pass() { echo " [PASS] $1"; }
48
+ fail() { echo " [FAIL] $1"; errors=$((errors + 1)); }
49
+
50
+ if ! command -v jq >/dev/null 2>&1; then echo "jq not available; skipping economics record tests"; exit 0; fi
51
+
52
+ echo "=== economics record (#349) ==="
53
+
54
+ # ── Build the session.usage event from the fixture transcript via the REAL usage parser ────────────
55
+ # This is what wires "tokens come from transcript ground truth" into the test: the usage block is
56
+ # produced by usage_parse_transcript reading each assistant message's .message.usage — not hand-set.
57
+ # shellcheck source=/dev/null
58
+ source "$ROOT/scripts/telemetry/lib/usage.sh"
59
+ TU="$(usage_parse_transcript "$FIX/transcript.jsonl")" || { fail "usage_parse_transcript returned empty"; }
60
+ USAGE_EVENT="$(jq -cn --argjson tu "$TU" --arg model "claude-opus-4-8" '{
61
+ schema_version:"0.3.0", timestamp:"1751731200000",
62
+ session_id:"kontourai-flow-agents-349-run-01", event_id:"evt-01-usage",
63
+ event_type:"session.usage", agent:{name:"dev",runtime:"claude-code",version:"1"},
64
+ context:{cwd:"/workspace/flow-agents"},
65
+ usage:{ model:$model, duration_s:420, tool_invocations:12, delegations:3,
66
+ input_tokens:$tu.input_tokens, output_tokens:$tu.output_tokens,
67
+ cache_creation_input_tokens:$tu.cache_creation_input_tokens,
68
+ cache_read_input_tokens:$tu.cache_read_input_tokens,
69
+ estimated_cost_usd:$tu.estimated_cost_usd, pricing_version:$tu.pricing_version,
70
+ by_model:$tu.by_model }
71
+ }')"
72
+
73
+ # Run the emitter with an isolated local economics log; echo the assembled record.
74
+ run_emitter() { # <local_log> [extra emitter args...]
75
+ local log="$1"; shift
76
+ TELEMETRY_ECONOMICS_LOG_FILE="$log" bash "$EMITTER" "$USAGE_EVENT" "$@"
77
+ }
78
+
79
+ # ── AC2 + AC5 + AC4: full fixture (phase-known) → assert every field's source value ────────────────
80
+ echo "--- AC2/AC4/AC5: full fixture record — cost from usage, defects from critique, verdict from state ---"
81
+ LOG1="$TMP/econ1.jsonl"; : > "$LOG1"
82
+ run_emitter "$LOG1" --state "$FIX/state.json" --acceptance "$FIX/acceptance.json" --critique "$FIX/critique.json"
83
+ n=$(wc -l < "$LOG1" | tr -d ' ')
84
+ [[ "$n" == "1" ]] && pass "exactly one record written to the local log (local-first)" || fail "expected 1 record, got $n"
85
+ REC="$(cat "$LOG1")"
86
+
87
+ # cost.* == session.usage .usage.* (ground truth, verbatim)
88
+ assert_eq() { # <label> <jq-expr-on-REC> <expected>
89
+ local got; got="$(printf '%s' "$REC" | jq -c "$2" 2>/dev/null)"
90
+ [[ "$got" == "$3" ]] && pass "$1 ($got)" || fail "$1: expected $3 got $got"
91
+ }
92
+ assert_eq "cost.input_tokens == usage.input_tokens" '.cost.input_tokens' "$(printf '%s' "$USAGE_EVENT" | jq -c '.usage.input_tokens')"
93
+ assert_eq "cost.output_tokens == usage.output_tokens" '.cost.output_tokens' "$(printf '%s' "$USAGE_EVENT" | jq -c '.usage.output_tokens')"
94
+ assert_eq "cost.cache_read_input_tokens == usage" '.cost.cache_read_input_tokens' "$(printf '%s' "$USAGE_EVENT" | jq -c '.usage.cache_read_input_tokens')"
95
+ assert_eq "cost.estimated_cost_usd == usage" '.cost.estimated_cost_usd' "$(printf '%s' "$USAGE_EVENT" | jq -c '.usage.estimated_cost_usd')"
96
+ assert_eq "cost.by_model == usage.by_model verbatim" '.cost.by_model' "$(printf '%s' "$USAGE_EVENT" | jq -c '.usage.by_model')"
97
+ assert_eq "model == usage.model" '.model' '"claude-opus-4-8"'
98
+ assert_eq "pricing_version carried" '.pricing_version' '"2026-06-28"'
99
+ assert_eq "run_id == session_id" '.run_id' '"kontourai-flow-agents-349-run-01"'
100
+ assert_eq "task_slug from state.json" '.task_slug' '"kontourai-flow-agents-349"'
101
+ # AC5: defects from critique.json (severity grouping, missing→low; distinct false-completion)
102
+ assert_eq "findings_by_severity.critical" '.defects.findings_by_severity.critical' '1'
103
+ assert_eq "findings_by_severity.high" '.defects.findings_by_severity.high' '2'
104
+ assert_eq "findings_by_severity.medium" '.defects.findings_by_severity.medium' '1'
105
+ assert_eq "findings_by_severity.low (incl. missing-severity default)" '.defects.findings_by_severity.low' '2'
106
+ assert_eq "caught_false_completions == 1 (distinct)" '.defects.caught_false_completions' '1'
107
+ assert_eq "gate_fires from state.json" '.defects.gate_fires' '3'
108
+ # AC4: iterations + verdict + human_wait from state.json
109
+ assert_eq "iterations.count == 2" '.iterations.count' '2'
110
+ assert_eq "iterations.route_backs == 1" '.iterations.route_backs' '1'
111
+ assert_eq "verification_verdict == PASS" '.defects.verification_verdict' '"PASS"'
112
+ assert_eq "human_wait_s > 0 (decision pause present)" '.time.human_wait_s' '45'
113
+ assert_eq "wall_clock_s == usage.duration_s" '.time.wall_clock_s' '420'
114
+
115
+ # ── AC1/AC6: schema validation — golden validates; cost-only (no defects) FAILS (R7 Goodhart) ─────
116
+ echo "--- AC1/AC6: schema — golden validates positive; a cost-only record FAILS (co-required cost+defects) ---"
117
+ SCHEMA_CHECK="$(node -e '
118
+ const Ajv=require("ajv");
119
+ const a=new Ajv({allErrors:true,strict:false});
120
+ const v=a.compile(require(process.argv[1]));
121
+ const golden=require(process.argv[2]);
122
+ const good=v(golden);
123
+ const cur=JSON.parse(require("fs").readFileSync(process.argv[3],"utf8"));
124
+ const curOk=v(cur);
125
+ const bad=JSON.parse(JSON.stringify(golden)); delete bad.defects;
126
+ const badOk=v(bad);
127
+ console.log(JSON.stringify({good, curOk, badOk}));
128
+ ' "$SCHEMA" "$FIX/expected-record.json" "$LOG1" 2>&1)"
129
+ echo "$SCHEMA_CHECK" | grep -q '"good":true' && pass "golden fixture validates against economics-record.schema.json" || fail "golden did NOT validate: $SCHEMA_CHECK"
130
+ echo "$SCHEMA_CHECK" | grep -q '"curOk":true' && pass "freshly-emitted record validates against the schema" || fail "emitted record did NOT validate: $SCHEMA_CHECK"
131
+ echo "$SCHEMA_CHECK" | grep -q '"badOk":false' && pass "cost-only record (defects removed) FAILS validation — R7 co-required guard holds" || fail "cost-only record wrongly validated: $SCHEMA_CHECK"
132
+
133
+ # ── golden parity: the emitted record equals the committed golden (sorted) ─────────────────────────
134
+ if diff <(jq -S . "$LOG1") <(jq -S . "$FIX/expected-record.json") >/dev/null 2>&1; then
135
+ pass "emitted record byte-equals the committed golden fixture (expected-record.json)"
136
+ else
137
+ fail "emitted record drifted from golden: $(diff <(jq -S . "$LOG1") <(jq -S . "$FIX/expected-record.json") | head -20)"
138
+ fi
139
+
140
+ # ── AC3: phase-sum invariant — phase-known AND phase-unknown → unattributed ────────────────────────
141
+ echo "--- AC3: phase-sum invariant holds (phase-known → its bucket; phase-unknown → unattributed) ---"
142
+ check_phase_sum() { # <record> <expected_phase_label>
143
+ local rec="$1" want="$2"
144
+ local ok
145
+ ok="$(printf '%s' "$rec" | jq -c '
146
+ (.phases | map(.input_tokens) | add) as $pi
147
+ | (.phases | map(.output_tokens) | add) as $po
148
+ | (.phases | map(.cache_read_input_tokens) | add) as $pcr
149
+ | (.phases | map(.wall_clock_s) | add) as $pw
150
+ | ($pi == .cost.input_tokens and $po == .cost.output_tokens
151
+ and $pcr == .cost.cache_read_input_tokens and $pw == .time.wall_clock_s)')"
152
+ [[ "$ok" == "true" ]] && pass "phase-sum invariant holds ($want)" || fail "phase-sum invariant BROKEN ($want): $ok"
153
+ local lbl; lbl="$(printf '%s' "$rec" | jq -r '.phases[0].phase')"
154
+ [[ "$lbl" == "$want" ]] && pass "phase label is '$want'" || fail "phase label expected '$want' got '$lbl'"
155
+ }
156
+ check_phase_sum "$REC" "verify"
157
+ # phase-unknown: state.json with NO phase → single unattributed bucket, still sums.
158
+ STATE_NOPHASE="$TMP/state-nophase.json"
159
+ jq 'del(.phase)' "$FIX/state.json" > "$STATE_NOPHASE"
160
+ LOG2="$TMP/econ2.jsonl"; : > "$LOG2"
161
+ run_emitter "$LOG2" --state "$STATE_NOPHASE" --critique "$FIX/critique.json"
162
+ check_phase_sum "$(cat "$LOG2")" "unattributed"
163
+
164
+ # ── injection: hostile task_slug + finding text → jq-escaped, never raw control bytes ─────────────
165
+ echo "--- injection: hostile task_slug/finding text are \\u-escaped, never raw control bytes ---"
166
+ HOSTILE_STATE="$TMP/hostile-state.json"
167
+ HOSTILE_CRIT="$TMP/hostile-crit.json"
168
+ node -e '
169
+ const fs=require("fs");const esc=String.fromCharCode(27),bel=String.fromCharCode(7);
170
+ fs.writeFileSync(process.argv[1], JSON.stringify({schema_version:"1.0",task_slug:"slug"+esc+"[31m"+bel+"EVIL",phase:"exec"+esc+"ute",verification_verdict:"PASS"}));
171
+ fs.writeFileSync(process.argv[2], JSON.stringify({critiques:[{findings:[{severity:"high",description:"finding"+esc+"[2J"+bel+"text"}]}]}));
172
+ ' "$HOSTILE_STATE" "$HOSTILE_CRIT"
173
+ LOG3="$TMP/econ3.jsonl"; : > "$LOG3"
174
+ run_emitter "$LOG3" --state "$HOSTILE_STATE" --critique "$HOSTILE_CRIT"
175
+ # Scan for raw control bytes via node (cross-platform; macOS grep lacks -P). ESCAPED ⇒ jq \u-escaped.
176
+ INJ="$(node -e '
177
+ const fs=require("fs");const raw=fs.readFileSync(process.argv[1],"utf8");
178
+ if(!raw.trim()){console.log("EMPTY");process.exit(0)}
179
+ console.log(/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(raw)?"RAW":"ESCAPED");
180
+ ' "$LOG3" 2>&1)"
181
+ [[ "$INJ" == "ESCAPED" ]] && pass "hostile task_slug/finding control bytes are JSON-escaped, never raw in the record (injection discipline)" || fail "raw control bytes reached the record ($INJ)"
182
+ printf '%s' "$(cat "$LOG3")" | jq -e . >/dev/null 2>&1 && pass "record with hostile fields is still valid JSON" || fail "hostile fields produced invalid JSON"
183
+
184
+ # ── Stub Console endpoint (200 / 500) for the relay + fail-open cases ──────────────────────────────
185
+ start_stub() {
186
+ local mode="$1"
187
+ local fake_bin="$TMP/fake-curl-bin"
188
+ mkdir -p "$fake_bin"
189
+ cat > "$fake_bin/curl" <<'SH'
190
+ #!/usr/bin/env bash
191
+ config=""
192
+ while [[ $# -gt 0 ]]; do
193
+ case "$1" in
194
+ --config) config="$2"; shift 2 ;;
195
+ *) shift ;;
196
+ esac
197
+ done
198
+ [[ -n "$config" && -f "$config" ]] || exit 2
199
+ node - "$config" "$ECON_STUB_RECV" <<'NODE'
200
+ const fs = require("fs");
201
+ const config = fs.readFileSync(process.argv[2], "utf8").split(/\n/);
202
+ const out = process.argv[3];
203
+ const headers = {};
204
+ let url = "";
205
+ let method = "GET";
206
+ let body = "";
207
+ for (const line of config) {
208
+ const m = line.match(/^([a-z-]+) = "(.*)"$/);
209
+ if (!m) continue;
210
+ const [, key, value] = m;
211
+ if (key === "url") url = value;
212
+ if (key === "request") method = value;
213
+ if (key === "header") {
214
+ const i = value.indexOf(":");
215
+ if (i > 0) headers[value.slice(0, i).toLowerCase()] = value.slice(i + 1).trim();
216
+ }
217
+ if (key === "data-binary" && value.startsWith("@")) body = fs.readFileSync(value.slice(1), "utf8");
218
+ }
219
+ let path = url;
220
+ try {
221
+ path = new URL(url).pathname;
222
+ } catch {}
223
+ fs.appendFileSync(out, JSON.stringify({ method, url: path, headers, body }) + "\n");
224
+ NODE
225
+ [[ "${ECON_STUB_MODE:-ok}" == "fail" ]] && exit 22
226
+ exit 0
227
+ SH
228
+ chmod +x "$fake_bin/curl"
229
+ export ECON_STUB_RECV="$RECV" ECON_STUB_MODE="$mode"
230
+ case ":$PATH:" in
231
+ *":$fake_bin:"*) ;;
232
+ *) export PATH="$fake_bin:$PATH" ;;
233
+ esac
234
+ STUB_PID=""
235
+ return 0
236
+ }
237
+ stop_stub() { [[ -n "${STUB_PID:-}" ]] && kill "$STUB_PID" 2>/dev/null; STUB_PID=""; }
238
+ wait_for_post() { for _ in $(seq 1 25); do [[ -s "$RECV" ]] && return 0; sleep 0.2; done; return 1; }
239
+ ENDPOINT="http://127.0.0.1:${PORT}/records"
240
+
241
+ # ── local-first: NO console configured → local write happens, nothing POSTed ──────────────────────
242
+ echo "--- local-first: no console configured → local record written, nothing POSTed ---"
243
+ : > "$RECV"; start_stub ok || fail "stub server did not start"
244
+ LOG4="$TMP/econ4.jsonl"; : > "$LOG4"
245
+ (
246
+ unset FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL \
247
+ FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL
248
+ run_emitter "$LOG4" --state "$FIX/state.json" --critique "$FIX/critique.json"
249
+ )
250
+ sleep 1
251
+ [[ -s "$LOG4" ]] && pass "local economics record written with NO console configured (local-first)" || fail "local record missing with no console"
252
+ [[ ! -s "$RECV" ]] && pass "NO POST when no console endpoint is configured (opt-in respected)" || fail "a POST happened with no console configured"
253
+ stop_stub
254
+
255
+ # ── console configured + relay ON → POST lands with the record + auth headers ─────────────────────
256
+ echo "--- relay on+configured: record mirrors to the Console over the shared transport with auth ---"
257
+ : > "$RECV"; start_stub ok
258
+ LOG5="$TMP/econ5.jsonl"; : > "$LOG5"
259
+ (
260
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
261
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
262
+ export CONSOLE_TELEMETRY_TOKEN="tok-econ-123"
263
+ export CONSOLE_TENANT_ID="tenant-econ"
264
+ run_emitter "$LOG5" --state "$FIX/state.json" --critique "$FIX/critique.json"
265
+ )
266
+ [[ -s "$LOG5" ]] && pass "local record written even when relay is ON (local-first, still first)" || fail "local record missing with relay on"
267
+ if wait_for_post; then pass "record POSTed to the Console endpoint (opt-in relay)"; else fail "relay did NOT POST within timeout"; fi
268
+ node -e '
269
+ const fs=require("fs"); const raw=fs.readFileSync(process.argv[1],"utf8").trim();
270
+ if(!raw){console.log("NO_POST");process.exit(0)}
271
+ const r=JSON.parse(raw.split("\n")[0]); const b=JSON.parse(r.body);
272
+ const checks=[
273
+ ["method POST", r.method==="POST"],
274
+ ["authorization bearer", r.headers["authorization"]==="Bearer tok-econ-123"],
275
+ ["tenant header", r.headers["x-console-tenant-id"]==="tenant-econ"],
276
+ ["schema kontour.console.economics", b.schema==="kontour.console.economics"],
277
+ ["version 0.1", b.version==="0.1"],
278
+ ["cost.input_tokens 1000", b.cost.input_tokens===1000],
279
+ ["defects present (R7 pair shipped)", !!b.defects && typeof b.defects.gate_fires==="number"],
280
+ ];
281
+ for(const [n,ok] of checks) console.log((ok?"OK ":"BAD ")+n);
282
+ ' "$RECV" > "$TMP/checks.txt" 2>&1
283
+ while IFS= read -r line; do
284
+ [[ "$line" == OK* ]] && pass "POST body ${line#OK }" || fail "POST body ${line#BAD } — $(cat "$TMP/checks.txt")"
285
+ done < <(grep -E '^(OK|BAD) ' "$TMP/checks.txt")
286
+ stop_stub
287
+
288
+ # ── AC7: fail-open — console returns 500 → emitter exits 0, local record intact ───────────────────
289
+ echo "--- AC7: console 500 (detached) → emitter exits 0, local record still exists (fail-open) ---"
290
+ : > "$RECV"; start_stub fail
291
+ LOG6="$TMP/econ6.jsonl"; : > "$LOG6"
292
+ (
293
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
294
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
295
+ run_emitter "$LOG6" --state "$FIX/state.json" --critique "$FIX/critique.json"
296
+ )
297
+ rc=$?
298
+ [[ "$rc" -eq 0 ]] && pass "emitter exits 0 even when the console endpoint fails (non-fatal)" || fail "emitter exited non-zero ($rc) on console failure"
299
+ [[ -s "$LOG6" ]] && pass "local record written despite console 500 (local-first)" || fail "local record missing on console failure"
300
+ stop_stub
301
+
302
+ # ── AC7: telemetry disabled at the stop-hook → no economics call, run unaffected ──────────────────
303
+ echo "--- AC7: telemetry disabled (TELEMETRY_USAGE_TRACKING!=true) → stop hook emits no economics record ---"
304
+ # The wiring in telemetry.sh guards the economics call behind TELEMETRY_USAGE_TRACKING == true; the
305
+ # emitter itself is unconditional (it is only ever invoked from inside that guard). Assert the guard.
306
+ if grep -q 'economics-record.sh' "$TELEMETRY/telemetry.sh" \
307
+ && awk '/TELEMETRY_USAGE_TRACKING.*==.*true/{g=1} g&&/economics-record.sh/{found=1} END{exit !found}' "$TELEMETRY/telemetry.sh"; then
308
+ pass "stop-hook economics emission is nested under the TELEMETRY_USAGE_TRACKING==true guard (disabled ⇒ no emit)"
309
+ else
310
+ fail "economics emission is NOT guarded by TELEMETRY_USAGE_TRACKING in telemetry.sh"
311
+ fi
312
+
313
+ # ── config-driven activation (#469): install-console-config.sh sink → relay defaults ON ───────────
314
+ # Mirrors the env-var-driven relay-on/off cases above, but drives FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY /
315
+ # FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL through config.sh's key-parser + install-console-config.sh
316
+ # instead of setting them directly, proving the "no raw env var needed" path (AC2/AC4).
317
+ echo "--- config-driven (#469): install-console-config.sh (no economics flag) → relay defaults ON ---"
318
+ CONF_ON="$TMP/telemetry-cfg-on.conf"
319
+ (
320
+ unset FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL \
321
+ FLOW_AGENTS_CONSOLE_ENDPOINT_URL CONSOLE_TELEMETRY_ENDPOINT_URL \
322
+ FLOW_AGENTS_CONSOLE_TOKEN_FILE CONSOLE_TELEMETRY_TOKEN_FILE \
323
+ FLOW_AGENTS_CONSOLE_TENANT CONSOLE_TENANT_ID
324
+ bash "$TELEMETRY/install-console-config.sh" "$CONF_ON" \
325
+ --console-url "http://127.0.0.1:${PORT}" --console-tenant "tenant-cfg-on" >/dev/null
326
+ )
327
+ RELAY_RESOLVED="$(env -i PATH="$PATH" HOME="$TMP/home-empty" \
328
+ TELEMETRY_CONFIG_FILE="$CONF_ON" TELEMETRY_DATA_DIR="$TMP/cfg-on-data" TELEMETRY_SESSION_DIR="$TMP/cfg-on-data/sessions" \
329
+ bash -c "source '$ROOT/scripts/telemetry/lib/config.sh'; printf '%s' \"\$FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY\"")"
330
+ [[ "$RELAY_RESOLVED" == "1" ]] && pass "config.sh: FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY resolves truthy from a console-sink conf with no explicit key" \
331
+ || fail "config.sh: expected relay=1, got '$RELAY_RESOLVED'"
332
+
333
+ : > "$RECV"; start_stub ok
334
+ LOG8="$TMP/econ8.jsonl"; : > "$LOG8"
335
+ (
336
+ unset FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL \
337
+ FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL CONSOLE_TENANT_ID CONSOLE_TELEMETRY_TOKEN
338
+ export TELEMETRY_CONFIG_FILE="$CONF_ON"
339
+ export TELEMETRY_ECONOMICS_LOG_FILE="$LOG8"
340
+ bash "$EMITTER" "$USAGE_EVENT" --state "$FIX/state.json" --critique "$FIX/critique.json"
341
+ )
342
+ [[ -s "$LOG8" ]] && pass "config-driven activation: local record written (local-first)" || fail "config-driven activation: local record missing"
343
+ if wait_for_post; then pass "config-driven activation: emitter POSTs to the derived endpoint with no raw env var set"; else fail "config-driven activation: no POST within timeout"; fi
344
+ stop_stub
345
+
346
+ # ── config-driven opt-out: --no-economics-relay suppresses the POST, local record still holds ─────
347
+ echo "--- config-driven (#469): install-console-config.sh --no-economics-relay suppresses the POST ---"
348
+ CONF_OFF="$TMP/telemetry-cfg-off.conf"
349
+ (
350
+ unset FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL \
351
+ FLOW_AGENTS_CONSOLE_ENDPOINT_URL CONSOLE_TELEMETRY_ENDPOINT_URL \
352
+ FLOW_AGENTS_CONSOLE_TOKEN_FILE CONSOLE_TELEMETRY_TOKEN_FILE \
353
+ FLOW_AGENTS_CONSOLE_TENANT CONSOLE_TENANT_ID
354
+ bash "$TELEMETRY/install-console-config.sh" "$CONF_OFF" \
355
+ --console-url "http://127.0.0.1:${PORT}" --console-tenant "tenant-cfg-off" --no-economics-relay >/dev/null
356
+ )
357
+ grep -q '^console_economics_relay=0$' "$CONF_OFF" && pass "--no-economics-relay writes console_economics_relay=0" || fail "--no-economics-relay did not write console_economics_relay=0"
358
+
359
+ : > "$RECV"; start_stub ok
360
+ LOG9="$TMP/econ9.jsonl"; : > "$LOG9"
361
+ (
362
+ unset FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL \
363
+ FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL CONSOLE_TENANT_ID CONSOLE_TELEMETRY_TOKEN
364
+ export TELEMETRY_CONFIG_FILE="$CONF_OFF"
365
+ export TELEMETRY_ECONOMICS_LOG_FILE="$LOG9"
366
+ bash "$EMITTER" "$USAGE_EVENT" --state "$FIX/state.json" --critique "$FIX/critique.json"
367
+ )
368
+ sleep 1
369
+ [[ -s "$LOG9" ]] && pass "--no-economics-relay: local record still written (local-first holds)" || fail "--no-economics-relay: local record missing"
370
+ [[ ! -s "$RECV" ]] && pass "--no-economics-relay: relay suppressed even though a console sink is configured" || fail "--no-economics-relay: a POST happened despite opt-out"
371
+ stop_stub
372
+
373
+ # ── explicit console_economics_relay=0 in a hand-written conf suppresses relay too ─────────────────
374
+ echo "--- config-driven (#469): hand-written console_economics_relay=0 suppresses relay despite console URL ---"
375
+ CONF_HAND="$TMP/telemetry-cfg-hand.conf"
376
+ cat > "$CONF_HAND" <<CONF_HAND_EOF
377
+ console_telemetry_url=http://127.0.0.1:${PORT}
378
+ console_tenant_id=tenant-cfg-hand
379
+ console_economics_relay=0
380
+ CONF_HAND_EOF
381
+
382
+ : > "$RECV"; start_stub ok
383
+ LOG10="$TMP/econ10.jsonl"; : > "$LOG10"
384
+ (
385
+ unset FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL \
386
+ FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL CONSOLE_TENANT_ID CONSOLE_TELEMETRY_TOKEN
387
+ export TELEMETRY_CONFIG_FILE="$CONF_HAND"
388
+ export TELEMETRY_ECONOMICS_LOG_FILE="$LOG10"
389
+ bash "$EMITTER" "$USAGE_EVENT" --state "$FIX/state.json" --critique "$FIX/critique.json"
390
+ )
391
+ sleep 1
392
+ [[ -s "$LOG10" ]] && pass "hand-written console_economics_relay=0: local record still written (local-first holds)" || fail "hand-written conf: local record missing"
393
+ [[ ! -s "$RECV" ]] && pass "hand-written console_economics_relay=0 suppresses relay even with a console URL present" || fail "hand-written conf: a POST happened despite explicit opt-out"
394
+ stop_stub
395
+
396
+ # ── HIGH-finding regression (#469 review): endpoint-only sink (console_telemetry_endpoint_url set,
397
+ # NO console_telemetry_url) must still resolve an economics endpoint — economics-record.sh derives
398
+ # the origin from CONSOLE_TELEMETRY_ENDPOINT_URL (stripping its `/api/telemetry/records` path) and
399
+ # posts to the shared `<origin>/records` ingress, NEVER the telemetry endpoint path verbatim ──────
400
+ echo "--- HIGH regression (#469): endpoint-only sink (console_telemetry_endpoint_url, no console_telemetry_url) still relays economics to <origin>/records ---"
401
+ CONF_ENDPOINT_ONLY="$TMP/telemetry-cfg-endpoint-only.conf"
402
+ cat > "$CONF_ENDPOINT_ONLY" <<CONF_EP_EOF
403
+ console_telemetry_endpoint_url=http://127.0.0.1:${PORT}/api/telemetry/records
404
+ console_tenant_id=tenant-cfg-endpoint-only
405
+ CONF_EP_EOF
406
+
407
+ : > "$RECV"; start_stub ok
408
+ LOG11="$TMP/econ11.jsonl"; : > "$LOG11"
409
+ (
410
+ unset FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL \
411
+ FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL CONSOLE_TENANT_ID CONSOLE_TELEMETRY_TOKEN
412
+ export TELEMETRY_CONFIG_FILE="$CONF_ENDPOINT_ONLY"
413
+ export TELEMETRY_ECONOMICS_LOG_FILE="$LOG11"
414
+ bash "$EMITTER" "$USAGE_EVENT" --state "$FIX/state.json" --critique "$FIX/critique.json"
415
+ )
416
+ [[ -s "$LOG11" ]] && pass "endpoint-only sink: local record still written (local-first)" || fail "endpoint-only sink: local record missing"
417
+ if wait_for_post; then pass "endpoint-only sink: relay defaults ON (either console signal is truthful, #469 HIGH fix)"; else fail "endpoint-only sink: no POST within timeout (HIGH regression)"; fi
418
+ POSTED_URL="$(node -e '
419
+ const fs=require("fs"); const raw=fs.readFileSync(process.argv[1],"utf8").trim();
420
+ if(!raw){console.log("NO_POST");process.exit(0)}
421
+ const r=JSON.parse(raw.split("\n")[0]);
422
+ console.log(r.url);
423
+ ' "$RECV" 2>&1)"
424
+ [[ "$POSTED_URL" == "/records" ]] && pass "endpoint-only sink: POST landed on <origin>/records ($POSTED_URL), NOT the telemetry endpoint path verbatim" \
425
+ || fail "endpoint-only sink: expected POST to /records, got '$POSTED_URL'"
426
+ stop_stub
427
+
428
+ # ── explicit console_economics_endpoint_url conf key routes the POST to that exact URL ───────────
429
+ echo "--- config-driven (#469): explicit console_economics_endpoint_url conf key wins verbatim ---"
430
+ CONF_EXPLICIT_EP="$TMP/telemetry-cfg-explicit-economics-endpoint.conf"
431
+ cat > "$CONF_EXPLICIT_EP" <<CONF_EXP_EOF
432
+ console_economics_relay=1
433
+ console_economics_endpoint_url=http://127.0.0.1:${PORT}/records
434
+ CONF_EXP_EOF
435
+
436
+ : > "$RECV"; start_stub ok
437
+ LOG12="$TMP/econ12.jsonl"; : > "$LOG12"
438
+ (
439
+ unset FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL \
440
+ FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL CONSOLE_TENANT_ID CONSOLE_TELEMETRY_TOKEN
441
+ export TELEMETRY_CONFIG_FILE="$CONF_EXPLICIT_EP"
442
+ export TELEMETRY_ECONOMICS_LOG_FILE="$LOG12"
443
+ bash "$EMITTER" "$USAGE_EVENT" --state "$FIX/state.json" --critique "$FIX/critique.json"
444
+ )
445
+ [[ -s "$LOG12" ]] && pass "explicit console_economics_endpoint_url: local record still written (local-first)" || fail "explicit console_economics_endpoint_url: local record missing"
446
+ if wait_for_post; then pass "explicit console_economics_endpoint_url: relay POSTs (explicit key + relay=1, no console_telemetry_url needed)"; else fail "explicit console_economics_endpoint_url: no POST within timeout"; fi
447
+ POSTED_URL2="$(node -e '
448
+ const fs=require("fs"); const raw=fs.readFileSync(process.argv[1],"utf8").trim();
449
+ if(!raw){console.log("NO_POST");process.exit(0)}
450
+ const r=JSON.parse(raw.split("\n")[0]);
451
+ console.log(r.url);
452
+ ' "$RECV" 2>&1)"
453
+ [[ "$POSTED_URL2" == "/records" ]] && pass "explicit console_economics_endpoint_url: POST landed on the exact configured URL ($POSTED_URL2)" \
454
+ || fail "explicit console_economics_endpoint_url: expected POST to /records, got '$POSTED_URL2'"
455
+ stop_stub
456
+
457
+ # ── suppression guard: unattributed+$0+no-defect/gate signal never relays (economics-relay- ───────
458
+ # unattributed-suppression) — local write still happens; only the console POST is suppressed. Full
459
+ # signal matrix so a shortcut implementation (task_slug-only) cannot pass: (1) suppressed case, then
460
+ # each signal category alone (attribution / cost / defect-gate) proven to still relay unchanged.
461
+ echo "--- suppression guard: unattributed+\$0+no-defect record is NOT relayed (local write unaffected) ---"
462
+ ZERO_USAGE_EVENT="$(jq -cn '{
463
+ schema_version:"0.3.0", timestamp:"1751731200000",
464
+ session_id:"unattributed-run-01", event_id:"evt-02-usage",
465
+ event_type:"session.usage", agent:{name:"dev",runtime:"claude-code",version:"1"},
466
+ context:{cwd:"/workspace/flow-agents"},
467
+ usage:{ model:"claude-opus-4-8", duration_s:5, tool_invocations:0, delegations:0,
468
+ input_tokens:0, output_tokens:0, cache_creation_input_tokens:0, cache_read_input_tokens:0,
469
+ estimated_cost_usd:0, pricing_version:"2026-06-28", by_model:[] }
470
+ }')"
471
+
472
+ # Case 1: no --state/--acceptance/--critique at all (mirrors telemetry.sh's real invocation when
473
+ # econ_slug is empty) + zero-cost usage → local write happens, NO POST.
474
+ : > "$RECV"; start_stub ok || fail "stub server did not start (suppression case 1)"
475
+ LOG13="$TMP/econ13.jsonl"; : > "$LOG13"
476
+ (
477
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
478
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
479
+ export TELEMETRY_ECONOMICS_LOG_FILE="$LOG13"
480
+ bash "$EMITTER" "$ZERO_USAGE_EVENT"
481
+ )
482
+ sleep 1
483
+ [[ -s "$LOG13" ]] && pass "suppression case 1: local record still written (local-first unaffected)" || fail "suppression case 1: local record missing"
484
+ [[ ! -s "$RECV" ]] && pass "suppression case 1: unattributed+\$0+no-defect record is NOT POSTed (relay suppressed)" || fail "suppression case 1: a POST happened for an unattributed/\$0/no-defect record"
485
+ stop_stub
486
+
487
+ # Case 1b (HIGH review fix): unattributed + estimated_cost_usd null (unpriced/new model — cost
488
+ # legitimately degrades to null while TOKENS remain source-of-truth, per usage.sh's contract and
489
+ # the #477 fix) + real input/output token volume + no defects → still relays. Without a
490
+ # token-volume leg in has_signal, this record is wrongly suppressed — dropping real ROI data,
491
+ # the opposite of the suppression guard's intent.
492
+ echo "--- suppression guard (HIGH fix): unattributed + null cost + real tokens still relays (no false suppression on unpriced models) ---"
493
+ TOKENS_NULL_COST_USAGE_EVENT="$(jq -cn '{
494
+ schema_version:"0.3.0", timestamp:"1751731200000",
495
+ session_id:"unattributed-unpriced-run-01", event_id:"evt-03-usage",
496
+ event_type:"session.usage", agent:{name:"dev",runtime:"claude-code",version:"1"},
497
+ context:{cwd:"/workspace/flow-agents"},
498
+ usage:{ model:"claude-new-unpriced-model", duration_s:120, tool_invocations:4, delegations:0,
499
+ input_tokens:50000, output_tokens:20000, cache_creation_input_tokens:0, cache_read_input_tokens:0,
500
+ estimated_cost_usd:null, pricing_version:null, by_model:[] }
501
+ }')"
502
+ : > "$RECV"; start_stub ok
503
+ LOG13B="$TMP/econ13b.jsonl"; : > "$LOG13B"
504
+ (
505
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
506
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
507
+ export TELEMETRY_ECONOMICS_LOG_FILE="$LOG13B"
508
+ bash "$EMITTER" "$TOKENS_NULL_COST_USAGE_EVENT"
509
+ )
510
+ [[ -s "$LOG13B" ]] && pass "suppression case 1b: local record written" || fail "suppression case 1b: local record missing"
511
+ REC13B="$(cat "$LOG13B")"
512
+ COST13B="$(printf '%s' "$REC13B" | jq -c '.cost.estimated_cost_usd')"
513
+ [[ "$COST13B" == "0" ]] && pass "suppression case 1b: null cost coerces to 0 on the assembled record (estimated_cost_usd==$COST13B)" || fail "suppression case 1b: expected estimated_cost_usd 0, got $COST13B"
514
+ IT13B="$(printf '%s' "$REC13B" | jq -c '.cost.input_tokens')"
515
+ OT13B="$(printf '%s' "$REC13B" | jq -c '.cost.output_tokens')"
516
+ [[ "$IT13B" == "50000" && "$OT13B" == "20000" ]] && pass "suppression case 1b: real token volume carried (input=$IT13B, output=$OT13B)" || fail "suppression case 1b: expected input=50000/output=20000, got input=$IT13B/output=$OT13B"
517
+ if wait_for_post; then pass "suppression case 1b: unattributed + null/\$0 cost + real token volume STILL relays (token-volume leg, no false suppression)"; else fail "suppression case 1b: real-token record was WRONGLY suppressed (token-volume leg missing/broken)"; fi
518
+ stop_stub
519
+
520
+ # Case 2: same zero-cost usage event, but with real task attribution via --state → still relays.
521
+ echo "--- suppression guard: real task-slug attribution alone still relays (zero cost, no defects) ---"
522
+ : > "$RECV"; start_stub ok
523
+ LOG14="$TMP/econ14.jsonl"; : > "$LOG14"
524
+ (
525
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
526
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
527
+ export TELEMETRY_ECONOMICS_LOG_FILE="$LOG14"
528
+ bash "$EMITTER" "$ZERO_USAGE_EVENT" --state "$FIX/state.json"
529
+ )
530
+ [[ -s "$LOG14" ]] && pass "suppression case 2: local record written" || fail "suppression case 2: local record missing"
531
+ if wait_for_post; then pass "suppression case 2: real task-slug attribution alone still relays"; else fail "suppression case 2: attributed record was wrongly suppressed"; fi
532
+ stop_stub
533
+
534
+ # Case 3: real-transcript (non-zero cost) usage event, with NO --state/--acceptance/--critique at
535
+ # all (unattributed) → cost signal alone still relays.
536
+ echo "--- suppression guard: real cost alone still relays (unattributed, no defects) ---"
537
+ : > "$RECV"; start_stub ok
538
+ LOG15="$TMP/econ15.jsonl"; : > "$LOG15"
539
+ (
540
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
541
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
542
+ export TELEMETRY_ECONOMICS_LOG_FILE="$LOG15"
543
+ bash "$EMITTER" "$USAGE_EVENT"
544
+ )
545
+ [[ -s "$LOG15" ]] && pass "suppression case 3: local record written" || fail "suppression case 3: local record missing"
546
+ if wait_for_post; then pass "suppression case 3: real cost (unattributed) alone still relays"; else fail "suppression case 3: cost-bearing record was wrongly suppressed"; fi
547
+ stop_stub
548
+
549
+ # Case 4: zero-cost usage event, unattributed (task_slug deleted) but a real gate-fire/defect signal
550
+ # via a derived state.json → defect/gate signal alone still relays.
551
+ echo "--- suppression guard: real gate/defect signal alone still relays (unattributed, zero cost) ---"
552
+ STATE_NOSLUG_GATE="$TMP/state-noslug-gate.json"
553
+ jq 'del(.task_slug) | .gate_fires = 2' "$FIX/state.json" > "$STATE_NOSLUG_GATE"
554
+ : > "$RECV"; start_stub ok
555
+ LOG16="$TMP/econ16.jsonl"; : > "$LOG16"
556
+ (
557
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
558
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
559
+ export TELEMETRY_ECONOMICS_LOG_FILE="$LOG16"
560
+ bash "$EMITTER" "$ZERO_USAGE_EVENT" --state "$STATE_NOSLUG_GATE"
561
+ )
562
+ [[ -s "$LOG16" ]] && pass "suppression case 4: local record written" || fail "suppression case 4: local record missing"
563
+ if wait_for_post; then pass "suppression case 4: gate/defect signal alone (unattributed, \$0) still relays"; else fail "suppression case 4: gate-signal record was wrongly suppressed"; fi
564
+ stop_stub
565
+
566
+ # Case 5: zero-cost usage event, unattributed (no --state at all), gate_fires=0, findings all 0 —
567
+ # the ONLY signal present is a real caught_false_completions count (set directly on critique.json,
568
+ # not derived from a findings[] entry, so findings_by_severity stays all-zero) → defect sub-leg
569
+ # (caught_false_completions) alone still relays.
570
+ echo "--- suppression guard: caught_false_completions alone still relays (unattributed, \$0, gate_fires=0, findings=0) ---"
571
+ CRITIQUE_CFC_ONLY="$TMP/critique-cfc-only.json"
572
+ printf '%s' '{"gate_fires":0,"caught_false_completions":1}' > "$CRITIQUE_CFC_ONLY"
573
+ : > "$RECV"; start_stub ok
574
+ LOG17="$TMP/econ17.jsonl"; : > "$LOG17"
575
+ (
576
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
577
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
578
+ export TELEMETRY_ECONOMICS_LOG_FILE="$LOG17"
579
+ bash "$EMITTER" "$ZERO_USAGE_EVENT" --critique "$CRITIQUE_CFC_ONLY"
580
+ )
581
+ [[ -s "$LOG17" ]] && pass "suppression case 5: local record written" || fail "suppression case 5: local record missing"
582
+ REC17="$(cat "$LOG17")"
583
+ assert_case5() { local got; got="$(printf '%s' "$REC17" | jq -c "$1")"; [[ "$got" == "$2" ]] && pass "suppression case 5: $3 ($got)" || fail "suppression case 5: $3 expected $2 got $got"; }
584
+ assert_case5 '.defects.gate_fires' '0' "gate_fires stays 0 (isolating the cfc sub-leg)"
585
+ assert_case5 '.defects.caught_false_completions' '1' "caught_false_completions carried through"
586
+ assert_case5 '([.defects.findings_by_severity[]] | add)' '0' "findings_by_severity all-zero (isolating the cfc sub-leg)"
587
+ if wait_for_post; then pass "suppression case 5: caught_false_completions signal alone (unattributed, \$0, gate_fires=0) still relays"; else fail "suppression case 5: caught_false_completions-only record was wrongly suppressed"; fi
588
+ stop_stub
589
+
590
+ # Case 6: zero-cost usage event, unattributed (no --state at all), gate_fires=0,
591
+ # caught_false_completions=0 — the ONLY signal present is a nonzero findings_by_severity bucket
592
+ # (one real "high" finding) → defect sub-leg (findings_by_severity) alone still relays.
593
+ echo "--- suppression guard: a nonzero findings_by_severity bucket alone still relays (unattributed, \$0, gate_fires=0, cfc=0) ---"
594
+ CRITIQUE_FINDING_ONLY="$TMP/critique-finding-only.json"
595
+ printf '%s' '{"gate_fires":0,"caught_false_completions":0,"critiques":[{"findings":[{"severity":"high","description":"real finding, not a false-completion"}]}]}' > "$CRITIQUE_FINDING_ONLY"
596
+ : > "$RECV"; start_stub ok
597
+ LOG18="$TMP/econ18.jsonl"; : > "$LOG18"
598
+ (
599
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
600
+ export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
601
+ export TELEMETRY_ECONOMICS_LOG_FILE="$LOG18"
602
+ bash "$EMITTER" "$ZERO_USAGE_EVENT" --critique "$CRITIQUE_FINDING_ONLY"
603
+ )
604
+ [[ -s "$LOG18" ]] && pass "suppression case 6: local record written" || fail "suppression case 6: local record missing"
605
+ REC18="$(cat "$LOG18")"
606
+ assert_case6() { local got; got="$(printf '%s' "$REC18" | jq -c "$1")"; [[ "$got" == "$2" ]] && pass "suppression case 6: $3 ($got)" || fail "suppression case 6: $3 expected $2 got $got"; }
607
+ assert_case6 '.defects.gate_fires' '0' "gate_fires stays 0 (isolating the findings sub-leg)"
608
+ assert_case6 '.defects.caught_false_completions' '0' "caught_false_completions stays 0 (isolating the findings sub-leg)"
609
+ assert_case6 '.defects.findings_by_severity.high' '1' "findings_by_severity.high carried through"
610
+ if wait_for_post; then pass "suppression case 6: findings_by_severity signal alone (unattributed, \$0, gate_fires=0, cfc=0) still relays"; else fail "suppression case 6: findings-only record was wrongly suppressed"; fi
611
+ stop_stub
612
+
613
+
614
+ # ── AC8 (#415): delegations[] facts + orchestrator-observable outcome + signals capability block ──────
615
+ echo "--- AC8: delegations[] — role/model join, escalation supersession, honest outcome, signals ---"
616
+ LOG7="$TMP/econ7.jsonl"; : > "$LOG7"
617
+ run_emitter "$LOG7" --state "$FIX/state.json" --critique "$FIX/critique.json" --agents-dir "$FIX/agents"
618
+ DREC="$(cat "$LOG7")"
619
+ assert_eq_d() { # <label> <jq-expr-on-DREC> <expected>
620
+ local got; got="$(printf '%s' "$DREC" | jq -c "$2" 2>/dev/null)"
621
+ [[ "$got" == "$3" ]] && pass "$1 ($got)" || fail "$1: expected $3 got $got"
622
+ }
623
+ assert_eq_d "delegations has one entry per delegated agent_id" '.delegations | length' '5'
624
+ assert_eq_d "mechanical delegation carries its role+model" \
625
+ '.delegations[] | select(.agent_id=="tool-worker-1") | [.role,.resolved_model]' \
626
+ '["delegate-mechanical","claude-haiku-4-5@anthropic"]'
627
+ assert_eq_d "escalation supersedes the initial delegation (latest-wins → design/opus)" \
628
+ '.delegations[] | select(.agent_id=="tool-worker-2") | [.role,.resolved_model]' \
629
+ '["delegate-design","claude-opus-4-8@anthropic"]'
630
+ assert_eq_d "escalated_from records the tier it was promoted from" \
631
+ '.delegations[] | select(.agent_id=="tool-worker-2") | .escalated_from' \
632
+ '"delegate-implementation"'
633
+ assert_eq_d "non-escalated delegation has NO escalated_from key" \
634
+ '.delegations[] | select(.agent_id=="tool-worker-1") | has("escalated_from")' 'false'
635
+ # outcome derivation — ONLY from real signals, never fabricated:
636
+ assert_eq_d "outcome accepted (terminal evidence PASS, no escalation/redispatch)" \
637
+ '.delegations[] | select(.agent_id=="tool-worker-1") | .outcome' '"accepted"'
638
+ assert_eq_d "outcome rework (escalation happened)" \
639
+ '.delegations[] | select(.agent_id=="tool-worker-2") | .outcome' '"rework"'
640
+ assert_eq_d "outcome failed (terminal verdict FAIL)" \
641
+ '.delegations[] | select(.agent_id=="tool-worker-3") | .outcome' '"failed"'
642
+ assert_eq_d "outcome UNAVAILABLE when no verdict recorded — NOT assumed accepted" \
643
+ '.delegations[] | select(.agent_id=="tool-worker-4") | .outcome' '"unavailable"'
644
+ assert_eq_d "dispatch_count is orchestrator-observable (single dispatch = 1)" \
645
+ '.delegations[] | select(.agent_id=="tool-worker-1") | .dispatch_count' '1'
646
+ # signals capability block — distinguishes harness-blind from real zero
647
+ assert_eq_d "signals.per_delegation_tokens is false (no runtime isolates sub-agent tokens today)" \
648
+ '.signals.per_delegation_tokens' 'false'
649
+ assert_eq_d "signals.per_delegation_outcome == partial (4 of 5 delegations have a real outcome)" \
650
+ '.signals.per_delegation_outcome' '"partial"'
651
+ assert_eq_d "signals.runtime carried from the usage event" '.signals.runtime' '"claude-code"'
652
+ # re-dispatch WITHOUT escalation is rework via dispatch_count>1 — orchestrator-observable, no sub-agent peek
653
+ assert_eq_d "re-dispatch (2 delegations, no escalation) → dispatch_count=2, outcome rework" \
654
+ '.delegations[] | select(.agent_id=="tool-worker-5") | [.dispatch_count,.outcome]' \
655
+ '[2,"rework"]'
656
+ # absent --agents-dir → delegations [] and signals.per_delegation_outcome n/a (backward compatible)
657
+ assert_eq "delegations defaults to [] with no --agents-dir (golden path)" '.delegations' '[]'
658
+ assert_eq "signals.per_delegation_outcome == n/a with no delegations" '.signals.per_delegation_outcome' '"n/a"'
659
+ # the delegations-bearing record still validates against the schema
660
+ DVAL="$(node -e '
661
+ const Ajv=require("ajv"); const a=new Ajv({allErrors:true,strict:false});
662
+ const v=a.compile(require(process.argv[1]));
663
+ console.log(v(JSON.parse(require("fs").readFileSync(process.argv[2],"utf8")))?"VALID":"INVALID:"+JSON.stringify(v.errors));
664
+ ' "$SCHEMA" "$LOG7" 2>&1)"
665
+ [[ "$DVAL" == "VALID" ]] && pass "record with delegations[] + outcome + signals validates against the schema" || fail "delegations record invalid: $DVAL"
666
+
667
+ echo ""
668
+ if [[ "$errors" -eq 0 ]]; then
669
+ echo "test_economics_record: all checks passed."
670
+ exit 0
671
+ else
672
+ echo "test_economics_record: $errors check(s) failed."
673
+ exit 1
674
+ fi