@kontourai/flow-agents 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +173 -0
  3. package/CHANGELOG.md +54 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/assignment-provider.js +10 -1
  11. package/build/src/cli/builder-run.d.ts +1 -0
  12. package/build/src/cli/builder-run.js +27 -0
  13. package/build/src/cli/effective-backlog-settings.js +70 -2
  14. package/build/src/cli/init.d.ts +34 -0
  15. package/build/src/cli/init.js +341 -61
  16. package/build/src/cli/kit.js +55 -12
  17. package/build/src/cli/pull-work-provider.js +346 -5
  18. package/build/src/cli/skill-drift-check.d.ts +1 -0
  19. package/build/src/cli/skill-drift-check.js +165 -0
  20. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  21. package/build/src/cli/telemetry-doctor.js +53 -6
  22. package/build/src/cli/validate-hook-influence.js +37 -7
  23. package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
  24. package/build/src/cli/workflow-sidecar.d.ts +310 -1
  25. package/build/src/cli/workflow-sidecar.js +1914 -126
  26. package/build/src/cli.js +5 -0
  27. package/build/src/flow-kit/validate.d.ts +54 -34
  28. package/build/src/flow-kit/validate.js +237 -26
  29. package/build/src/index.d.ts +2 -0
  30. package/build/src/index.js +1 -0
  31. package/build/src/lib/console-connect-options.d.ts +97 -0
  32. package/build/src/lib/console-connect-options.js +199 -0
  33. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  34. package/build/src/lib/console-telemetry-validate.js +91 -0
  35. package/build/src/lib/flow-resolver.d.ts +54 -1
  36. package/build/src/lib/flow-resolver.js +112 -5
  37. package/build/src/lib/fs.d.ts +17 -0
  38. package/build/src/lib/fs.js +172 -0
  39. package/build/src/lib/local-artifact-root.d.ts +44 -1
  40. package/build/src/lib/local-artifact-root.js +131 -3
  41. package/build/src/runtime-adapters.d.ts +39 -3
  42. package/build/src/runtime-adapters.js +77 -31
  43. package/build/src/tools/build-universal-bundles.js +40 -2
  44. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  45. package/build/src/tools/codex-agent-routing.js +49 -0
  46. package/build/src/tools/generate-context-map.js +1 -0
  47. package/build/src/tools/validate-source-tree.js +30 -3
  48. package/context/contracts/artifact-contract.md +16 -2
  49. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  50. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  51. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  52. package/context/scripts/hooks/workflow-steering.js +194 -22
  53. package/context/scripts/package.json +3 -0
  54. package/context/scripts/telemetry/install-console-config.sh +25 -4
  55. package/context/scripts/telemetry/lib/config.sh +102 -12
  56. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  57. package/context/scripts/telemetry/lib/session.sh +3 -0
  58. package/context/scripts/telemetry/lib/transport.sh +87 -0
  59. package/context/scripts/telemetry/lib/usage.sh +205 -4
  60. package/context/scripts/telemetry/telemetry.conf +6 -0
  61. package/context/scripts/telemetry/telemetry.sh +48 -0
  62. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  63. package/docs/agent-usage-feedback-loop.md +35 -0
  64. package/docs/architecture-engine-and-kits.md +110 -0
  65. package/docs/context-map.md +2 -0
  66. package/docs/coordination-guide.md +370 -0
  67. package/docs/decisions/agent-coordination.md +26 -9
  68. package/docs/decisions/embeddable-engine.md +152 -0
  69. package/docs/decisions/index.md +5 -3
  70. package/docs/decisions/trust-ledger-retention.md +88 -0
  71. package/docs/decisions/trust-reconcile.md +42 -9
  72. package/docs/decisions/workflow-enforcement.md +31 -9
  73. package/docs/fixture-ownership.md +6 -2
  74. package/docs/implementing-trust-reconciliation.md +129 -0
  75. package/docs/index.md +23 -9
  76. package/docs/integrations/flow-agents-console.md +275 -0
  77. package/docs/integrations/index.md +4 -0
  78. package/docs/kit-authoring-guide.md +52 -21
  79. package/docs/spec/builder-flow-runtime.md +80 -0
  80. package/docs/spec/runtime-hook-surface.md +45 -1
  81. package/docs/specs/economics-record-contract.md +270 -0
  82. package/docs/specs/harness-capability-matrix.md +74 -0
  83. package/docs/specs/learning-review-proposals-contract.md +340 -0
  84. package/docs/specs/routing-efficiency-review.md +59 -0
  85. package/docs/verifiable-trust.md +74 -25
  86. package/docs/workflow-artifact-lifecycle.md +38 -1
  87. package/docs/workflow-usage-guide.md +10 -0
  88. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  89. package/evals/ci/antigaming-suite.sh +2 -0
  90. package/evals/ci/run-baseline.sh +78 -4
  91. package/evals/fixtures/economics/acceptance.json +12 -0
  92. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  93. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  94. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  95. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  96. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  97. package/evals/fixtures/economics/critique.json +22 -0
  98. package/evals/fixtures/economics/expected-record.json +71 -0
  99. package/evals/fixtures/economics/session-usage-event.json +1 -0
  100. package/evals/fixtures/economics/state.json +11 -0
  101. package/evals/fixtures/economics/transcript.jsonl +3 -0
  102. package/evals/fixtures/hook-influence/cases.json +7 -7
  103. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  104. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  105. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  106. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  108. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  116. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  117. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  118. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  119. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  120. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  121. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  122. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  123. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  124. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  125. package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
  126. package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
  127. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  128. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  129. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  130. package/evals/integration/test_builder_step_producers.sh +18 -10
  131. package/evals/integration/test_bundle_install.sh +172 -0
  132. package/evals/integration/test_checkpoint_signing.sh +10 -2
  133. package/evals/integration/test_ci_actor_identity.sh +221 -0
  134. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  135. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  136. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  137. package/evals/integration/test_economics_record.sh +674 -0
  138. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  139. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  140. package/evals/integration/test_exemption_usage_review.sh +198 -0
  141. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  142. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  143. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  144. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  145. package/evals/integration/test_gate_lockdown.sh +7 -0
  146. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  147. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  148. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  149. package/evals/integration/test_hook_influence_cases.sh +25 -1
  150. package/evals/integration/test_install_merge.sh +227 -2
  151. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  152. package/evals/integration/test_learning_review_proposals.sh +329 -0
  153. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  154. package/evals/integration/test_liveness_console_relay.sh +166 -0
  155. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  156. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  157. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  158. package/evals/integration/test_publish_delivery.sh +389 -2
  159. package/evals/integration/test_pull_work_board.sh +200 -0
  160. package/evals/integration/test_pull_work_provider.sh +1 -1
  161. package/evals/integration/test_reconcile_preflight.sh +304 -0
  162. package/evals/integration/test_record_check.sh +378 -0
  163. package/evals/integration/test_routing_efficiency.sh +71 -0
  164. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  165. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  166. package/evals/integration/test_skill_drift_check.sh +870 -0
  167. package/evals/integration/test_takeover_protocol.sh +340 -0
  168. package/evals/integration/test_telemetry.sh +445 -0
  169. package/evals/integration/test_telemetry_doctor.sh +66 -0
  170. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  171. package/evals/integration/test_trust_reconcile_negatives.sh +121 -13
  172. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  173. package/evals/integration/test_usage_cost.sh +61 -0
  174. package/evals/integration/test_verify_hold.sh +910 -0
  175. package/evals/integration/test_veritas_governance_kit.sh +257 -0
  176. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
  177. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  178. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  179. package/evals/integration/test_workspace_settings.sh +176 -0
  180. package/evals/lib/env.sh +26 -0
  181. package/evals/lib/node.sh +8 -0
  182. package/evals/run.sh +37 -0
  183. package/evals/static/test_ci_integration_coverage.sh +115 -0
  184. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  185. package/evals/static/test_universal_bundles.sh +34 -0
  186. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  187. package/evals/static/test_workflow_skills.sh +1 -1
  188. package/kits/builder/flows/build.flow.json +9 -18
  189. package/kits/builder/flows/publish-learn.flow.json +5 -1
  190. package/kits/builder/kit.json +120 -0
  191. package/kits/builder/skills/continue-work/SKILL.md +2 -0
  192. package/kits/builder/skills/deliver/SKILL.md +115 -0
  193. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  194. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  195. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  196. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  197. package/kits/builder/skills/pull-work/SKILL.md +33 -2
  198. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  199. package/kits/knowledge/kit.json +9 -0
  200. package/kits/veritas-governance/docs/README.md +113 -7
  201. package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
  202. package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
  203. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  204. package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
  205. package/kits/veritas-governance/kit.json +19 -0
  206. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  207. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  208. package/package.json +2 -2
  209. package/packaging/manifest.json +29 -0
  210. package/schemas/backlog-provider-settings.schema.json +13 -0
  211. package/schemas/workflow-state.schema.json +44 -0
  212. package/scripts/README.md +4 -0
  213. package/scripts/check-content-boundary.cjs +8 -1
  214. package/scripts/ci/trust-reconcile.js +214 -253
  215. package/scripts/hooks/codex-hook-adapter.js +77 -2
  216. package/scripts/hooks/evidence-capture.js +38 -5
  217. package/scripts/hooks/lib/actor-identity.js +82 -0
  218. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  219. package/scripts/hooks/lib/kit-catalog.js +235 -0
  220. package/scripts/hooks/lib/liveness-write.js +28 -1
  221. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  222. package/scripts/hooks/lib/runnable-command.js +177 -0
  223. package/scripts/hooks/lib/skill-drift.js +350 -0
  224. package/scripts/hooks/stop-goal-fit.js +278 -48
  225. package/scripts/hooks/workflow-steering.js +194 -22
  226. package/scripts/install-codex-home.sh +97 -47
  227. package/scripts/install-merge.js +72 -14
  228. package/scripts/install-owned-files.js +178 -0
  229. package/scripts/lib/reconcile-shape.js +381 -0
  230. package/scripts/liveness/relay.sh +84 -0
  231. package/scripts/telemetry/economics-record.schema.json +145 -0
  232. package/scripts/telemetry/economics-record.sh +331 -0
  233. package/scripts/telemetry/install-console-config.sh +25 -4
  234. package/scripts/telemetry/learning-review-decide.sh +124 -0
  235. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  236. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  237. package/scripts/telemetry/lib/config.sh +102 -12
  238. package/scripts/telemetry/lib/pricing.sh +14 -6
  239. package/scripts/telemetry/lib/session.sh +3 -0
  240. package/scripts/telemetry/lib/transport.sh +133 -15
  241. package/scripts/telemetry/lib/usage.sh +121 -28
  242. package/scripts/telemetry/routing-efficiency.sh +0 -0
  243. package/scripts/telemetry/telemetry.conf +6 -0
  244. package/scripts/telemetry/telemetry.sh +48 -0
  245. package/src/builder-flow-run-adapter.ts +357 -0
  246. package/src/builder-flow-runtime.ts +348 -0
  247. package/src/cli/assignment-provider.ts +12 -1
  248. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  249. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  250. package/src/cli/builder-run.ts +28 -0
  251. package/src/cli/codex-agent-routing.test.mjs +44 -0
  252. package/src/cli/codex-exit-code.test.mjs +207 -0
  253. package/src/cli/console-connect-options.test.mjs +329 -0
  254. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  255. package/src/cli/effective-backlog-settings.ts +68 -2
  256. package/src/cli/flow-resolver-composition.test.mjs +72 -0
  257. package/src/cli/init.test.mjs +161 -0
  258. package/src/cli/init.ts +407 -62
  259. package/src/cli/kit-metadata-security.test.mjs +443 -0
  260. package/src/cli/kit.ts +50 -12
  261. package/src/cli/pull-work-provider.ts +377 -3
  262. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  263. package/src/cli/skill-drift-check.ts +196 -0
  264. package/src/cli/telemetry-doctor.test.mjs +53 -0
  265. package/src/cli/telemetry-doctor.ts +50 -7
  266. package/src/cli/validate-hook-influence.ts +37 -6
  267. package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
  268. package/src/cli/workflow-sidecar.ts +1980 -119
  269. package/src/cli.ts +5 -0
  270. package/src/flow-kit/validate.ts +277 -38
  271. package/src/index.ts +19 -0
  272. package/src/lib/console-connect-options.ts +261 -0
  273. package/src/lib/console-telemetry-validate.ts +88 -0
  274. package/src/lib/flow-resolver.ts +117 -4
  275. package/src/lib/fs.ts +160 -0
  276. package/src/lib/local-artifact-root.ts +129 -3
  277. package/src/runtime-adapters.ts +113 -33
  278. package/src/tools/build-universal-bundles.ts +36 -2
  279. package/src/tools/codex-agent-routing.ts +48 -0
  280. package/src/tools/generate-context-map.ts +1 -0
  281. package/src/tools/validate-source-tree.ts +29 -3
@@ -0,0 +1,381 @@
1
+ 'use strict';
2
+ //
3
+ // Shared bundle-shape classification/divergence-construction, extracted from
4
+ // scripts/ci/trust-reconcile.js so a local, pre-push preflight (issue #356) can reuse
5
+ // EXACTLY the same shape checks CI enforces, rather than risk a forked copy that
6
+ // silently drifts from what trust-reconcile.js actually does (the historical failure
7
+ // mode this module exists to close off — see command-log-chain.js for the identical
8
+ // rationale applied to the hash-chain/laundering primitives).
9
+ //
10
+ // This module is SHAPE-only: it classifies a bundle's own claims/evidence and builds
11
+ // the `issues[]` entries that do not require a fresh CI command re-run (no `runCommand`,
12
+ // no manifest command execution). The ACTUAL fresh-run comparison for reconcilable
13
+ // command claims (`ciResult.passed`) stays in trust-reconcile.js, since that requires a
14
+ // live CI/local command execution a local preflight must not perform.
15
+ //
16
+ // trust-reconcile.js requires this module instead of defining these functions inline —
17
+ // see its own comments at the require() site and the (former) location of
18
+ // classifyBundleClaims for the extraction history.
19
+
20
+ // hasLaunderingOperator is imported (not re-implemented) so this module and
21
+ // scripts/ci/trust-reconcile.js apply the identical exit-code-mask heuristic.
22
+ const { hasLaunderingOperator } = require('./command-log-chain.js');
23
+
24
+ /**
25
+ * Classify a trust.bundle's claims into: reconcilable command claims (test_output +
26
+ * execution.label), session-local claims (attestation/observation/citation), never-captured
27
+ * or unbacked command claims (not-run divergence), and command-backed claims carrying a
28
+ * waiver (waiver-on-command divergence). Returns
29
+ * { reconcilable, sessionLocal, noEvidenceCommand, waiverOnCommand }.
30
+ *
31
+ * Source of truth: evidence[].execution.label is the command string recorded at capture time.
32
+ * evidence[].passing (normalized) means the agent claimed this passed. `claim.status` is NOT
33
+ * trusted here — the caller re-derives it CI-side (see derive-claim-status.mjs / finding-3).
34
+ *
35
+ * WS8 iteration-2 hardening:
36
+ * - finding 1: ANY pass-asserting claim whose evidence is `evidenceType: test_output`
37
+ * (Surface's default when unset) but which did NOT reconcile — i.e. it has no
38
+ * manifest-matchable execution.label — is a divergence, NOT session-local. A test_output
39
+ * claim either reconciles against the manifest or is a divergence; it is never accepted on
40
+ * self-reported status. (Previously only the literal claimType `workflow.check.command`
41
+ * was guarded, so a fabricated kind:"test" claim with no command slipped through.)
42
+ * - finding 4: a command-backed (test_output) claim carrying a waiver is a divergence — a
43
+ * command-backed check reconciles against CI or fails; it cannot be waived.
44
+ */
45
+ function classifyBundleClaims(bundle) {
46
+ const evidence = Array.isArray(bundle.evidence) ? bundle.evidence : [];
47
+ const claims = Array.isArray(bundle.claims) ? bundle.claims : [];
48
+
49
+ const claimById = new Map();
50
+ for (const c of claims) if (c && c.id) claimById.set(c.id, c);
51
+
52
+ // Evidence indexing. A missing evidenceType defaults to test_output for backward
53
+ // compatibility with pre-classification bundles (same default classifyEvidence uses).
54
+ const claimHasLabeledTestOutput = new Set(); // test_output evidence WITH an execution.label
55
+ const claimHasTestOutputEvidence = new Set(); // ANY test_output evidence (label or not)
56
+ // WS8 iteration-4 (converged finding): the session-local (non-test_output) evidenceType per
57
+ // claim, so the reconciler can name it on the loud ATTESTED marker below — a fabricated
58
+ // human_attestation/attestation/external claim with no --command is otherwise
59
+ // indistinguishable, in the reconciler's own output, from a genuinely re-runnable check.
60
+ const claimEvidenceType = new Map();
61
+ for (const ev of evidence) {
62
+ if (!ev || !ev.claimId) continue;
63
+ const evType = ev.evidenceType || 'test_output';
64
+ if (evType !== 'test_output') {
65
+ if (!claimEvidenceType.has(ev.claimId)) claimEvidenceType.set(ev.claimId, evType);
66
+ continue;
67
+ }
68
+ claimHasTestOutputEvidence.add(ev.claimId);
69
+ if (ev.execution && ev.execution.label) claimHasLabeledTestOutput.add(ev.claimId);
70
+ }
71
+
72
+ // finding 4: a command-backed (test_output-evidence) claim that also carries a waiver.
73
+ const waiverOnCommand = [];
74
+ for (const c of claims) {
75
+ if (!c || !c.id) continue;
76
+ const waiver = (c.metadata && typeof c.metadata === 'object') ? c.metadata.waiver : undefined;
77
+ if (waiver && typeof waiver === 'object' && claimHasTestOutputEvidence.has(c.id)) {
78
+ waiverOnCommand.push({ claimId: c.id, claimType: String(c.claimType || ''), subject: c.subjectId || c.fieldOrBehavior || c.id });
79
+ }
80
+ }
81
+
82
+ // (A) Reconcilable claimed-passes: evidence items that are test_output (CI-reconcilable),
83
+ // carry an execution.label, and assert pass. Session-local evidenceTypes
84
+ // (crawl_observation, human_attestation, attestation, policy_rule, source_excerpt,
85
+ // document_citation, calculation_trace) are NOT reconciled per-command — they are handled
86
+ // by the session-local/waiver path below.
87
+ const reconcilable = [];
88
+ const reconcilableClaimIds = new Set();
89
+ const seen = new Set();
90
+ for (const ev of evidence) {
91
+ if (!ev || !ev.execution || !ev.execution.label) continue;
92
+ if (!isPassingValue(ev.passing)) continue;
93
+ const evType = ev.evidenceType || 'test_output';
94
+ if (evType !== 'test_output') continue; // session-local — not CI-reconcilable
95
+ const cmd = normalizeCmd(ev.execution.label);
96
+ if (!cmd) continue;
97
+ reconcilableClaimIds.add(ev.claimId);
98
+ if (seen.has(cmd)) continue;
99
+ seen.add(cmd);
100
+ const claim = claimById.get(ev.claimId);
101
+ reconcilable.push({ cmd, claimId: ev.claimId, evId: ev.id, claimType: claim ? String(claim.claimType || '') : '' });
102
+ }
103
+
104
+ // (B) Session-local claims, never-captured command claims, and unreconciled test_output.
105
+ const sessionLocal = [];
106
+ const noEvidenceCommand = [];
107
+ const seenClaims = new Set();
108
+ for (const c of claims) {
109
+ if (!c || !c.id || typeof c.claimType !== 'string') continue;
110
+ // #267/#282: a superseded critique write is HISTORY — excluded from reconcile evaluation so a
111
+ // resolved session converges (a fail critique that a later same-reviewer pass superseded no
112
+ // longer blocks). Scoped to NON-test_output claims so a command-backed claim can never launder
113
+ // a real failure by carrying superseded_by — a test_output claim always reconciles or diverges.
114
+ if (c.metadata && typeof c.metadata === 'object' && c.metadata.superseded_by && !claimHasTestOutputEvidence.has(c.id)) continue;
115
+ if (reconcilableClaimIds.has(c.id)) continue; // handled by (A)
116
+ if (seenClaims.has(c.id)) continue;
117
+ const status = String(c.status || '');
118
+ const assertsPass = isPassingValue(c.value) || status === 'verified' || status === 'assumed';
119
+ const isFailing = status === 'disputed' || status === 'rejected';
120
+ if (!assertsPass && !isFailing) continue; // pending/unknown non-asserting — ignore (as before)
121
+ seenClaims.add(c.id);
122
+
123
+ // finding 1: a pass-asserting claim backed by test_output evidence that did NOT reconcile
124
+ // (it has test_output evidence but no manifest-matchable execution.label — otherwise it
125
+ // would be in bucket A) is a not-run divergence. A test_output claim reconciles against the
126
+ // manifest or it is a divergence — it is NEVER accepted as session-local on self-report.
127
+ if (assertsPass && claimHasTestOutputEvidence.has(c.id)) {
128
+ const rawCmd = normalizeCmd(c.fieldOrBehavior || c.value || '');
129
+ noEvidenceCommand.push({ cmd: rawCmd || `[claim:${c.id}]`, claimId: c.id, claimType: c.claimType, reason: 'test_output-unreconciled' });
130
+ continue;
131
+ }
132
+
133
+ // A workflow.check.command claim with no captured (labeled) evidence is a never-captured
134
+ // claimed pass — not-run divergence (anti-gaming teeth preserved).
135
+ if (assertsPass && c.claimType === 'workflow.check.command' && !claimHasLabeledTestOutput.has(c.id)) {
136
+ const rawCmd = normalizeCmd(c.fieldOrBehavior || c.value || '');
137
+ noEvidenceCommand.push({ cmd: rawCmd || `[claim:${c.id}:${c.claimType}]`, claimId: c.id, claimType: c.claimType, reason: 'no-evidence-command' });
138
+ continue;
139
+ }
140
+
141
+ const waiver = (c.metadata && typeof c.metadata === 'object') ? c.metadata.waiver : undefined;
142
+ sessionLocal.push({
143
+ claimId: c.id,
144
+ claimType: c.claimType,
145
+ assertedStatus: status,
146
+ value: c.value,
147
+ waiver: (waiver && typeof waiver === 'object') ? waiver : null,
148
+ subject: c.subjectId || c.fieldOrBehavior || c.id,
149
+ evidenceType: claimEvidenceType.get(c.id) || 'unknown',
150
+ });
151
+ }
152
+
153
+ return { reconcilable, sessionLocal, noEvidenceCommand, waiverOnCommand };
154
+ }
155
+
156
+ /** Normalize a command string: collapse whitespace, trim. (Mirrors trust-reconcile.js's own.) */
157
+ function normalizeCmd(cmd) {
158
+ return String(cmd || '').replace(/\s+/g, ' ').trim();
159
+ }
160
+
161
+ /**
162
+ * Normalize ev.passing to a boolean.
163
+ * Treats true / 1 / "true" / "pass" as passing.
164
+ * Prevents a claim from dodging reconciliation via a non-boolean value.
165
+ */
166
+ function isPassingValue(v) {
167
+ return v === true || v === 1 || v === 'true' || v === 'pass';
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // Pure shape-level divergence ("issue") construction.
172
+ //
173
+ // Each function below takes already-computed classification inputs (the buckets
174
+ // classifyBundleClaims returns, plus a resolved manifest / derived-status map) and
175
+ // returns an issues[] array structurally IDENTICAL in shape (`{ type, cmd?, message }`)
176
+ // to what trust-reconcile.js's Step 2 block pushes inline. None of these functions
177
+ // execute a command or otherwise perform a fresh CI run — they are pure, local, and fast.
178
+ // ---------------------------------------------------------------------------
179
+
180
+ /**
181
+ * finding 4 (server-side): a command-backed (test_output-evidence) claim carrying a
182
+ * waiver is a divergence — a command-backed check reconciles against CI or fails; it
183
+ * cannot be waived away.
184
+ */
185
+ function waiverOnCommandIssues(waiverOnCommand) {
186
+ const issues = [];
187
+ for (const { claimId, claimType, subject } of waiverOnCommand || []) {
188
+ issues.push({
189
+ type: 'waiver-on-command-check',
190
+ message: `trust divergence: claim '${claimId}' (${subject}, claimType: ${claimType}) carries a waiver but is backed by test_output evidence — a command-backed check reconciles against CI or fails and cannot be waived`,
191
+ });
192
+ }
193
+ return issues;
194
+ }
195
+
196
+ /**
197
+ * not-run divergences: never-captured command claims (no evidence) AND test_output
198
+ * claims that did not reconcile (no manifest-matchable execution.label).
199
+ */
200
+ function noEvidenceCommandIssues(noEvidenceCommand) {
201
+ const issues = [];
202
+ for (const { cmd, claimId, claimType, reason } of noEvidenceCommand || []) {
203
+ const message = reason === 'test_output-unreconciled'
204
+ ? `trust divergence: claim '${claimId}' (claimType: ${claimType}) asserts pass with test_output evidence but has no manifest-matched execution.label — a test_output claim must reconcile against the manifest or it is a divergence (never accepted as session-local)`
205
+ : `trust divergence: claim '${claimId}' (claimType: ${claimType}) asserts pass but has no supporting evidence item — command never captured`;
206
+ issues.push({ type: 'not-run', cmd, message });
207
+ }
208
+ return issues;
209
+ }
210
+
211
+ /**
212
+ * Manifest-membership subset of the `reconcilable` loop: ONLY the "not in the reconcile
213
+ * manifest" `not-run` case, plus the laundering check (via the shared hasLaunderingOperator).
214
+ * The ACTUAL fresh-run comparison (`ciResult.passed`) is NOT here — it requires a live CI/
215
+ * local command execution the preflight must not perform, and stays in trust-reconcile.js.
216
+ *
217
+ * Returns { issues, unresolved } where `unresolved` is the subset of `reconcilable` entries
218
+ * that passed the laundering + manifest-membership checks and therefore DO require a fresh
219
+ * CI run to fully reconcile — callers that need full parity (trust-reconcile.js) continue
220
+ * from there; callers that are shape-only (the local preflight) simply do not resolve them
221
+ * further and treat "manifest-matched, not laundered" as shape-clean.
222
+ */
223
+ function reconcilableManifestIssues(reconcilable, manifestByCmd) {
224
+ const issues = [];
225
+ const unresolved = [];
226
+ for (const entry of reconcilable || []) {
227
+ const { cmd } = entry;
228
+ const normalCmd = normalizeCmd(cmd);
229
+
230
+ // (a) Laundering operator check — must come first (most specific signal).
231
+ if (hasLaunderingOperator(cmd)) {
232
+ issues.push({
233
+ type: 'laundering',
234
+ cmd,
235
+ message: `trust divergence: agent claimed '${cmd}' passed; command contains exit-code-laundering operator (|| ... / ; true / ; exit 0 / etc.)`,
236
+ });
237
+ continue;
238
+ }
239
+
240
+ // A test_output claim MUST name a manifest (required-lane) command. An agent
241
+ // cannot self-label an arbitrary command test_output to dodge the manifest.
242
+ const manifestEntry = manifestByCmd.get(normalCmd);
243
+ if (!manifestEntry) {
244
+ issues.push({
245
+ type: 'not-run',
246
+ cmd,
247
+ message: `trust divergence: agent claimed '${cmd}' passed; command is not in the reconcile manifest — a test_output claim must name a manifest/required-lane command (CI cannot self-declare an arbitrary command)`,
248
+ });
249
+ continue;
250
+ }
251
+
252
+ unresolved.push({ ...entry, manifestEntry });
253
+ }
254
+ return { issues, unresolved };
255
+ }
256
+
257
+ /**
258
+ * Session-local claims: not CI-reconcilable, but NOT a pass bypass. Each must either
259
+ * (a) carry a loud, justified waiver, or (b) resolve a real CI-RE-DERIVED `verified`
260
+ * status. WS8 iteration-2 hardening:
261
+ * - finding 3: the status used here is RE-DERIVED CI-side, never the self-reported
262
+ * claim.status. A mismatch is a `status-misassertion` divergence.
263
+ * - finding 2: `assumed` alone is NO LONGER a silent pass. `assumed` is acceptable
264
+ * ONLY with a waiver (printed as a loud WAIVED line by the caller). An unwaived
265
+ * `assumed` claim is an `unwaived-assumed` divergence (restores pre-WS8 semantics
266
+ * where `assumed` alone never satisfied assertsPass).
267
+ *
268
+ * Q1/iteration-1-F1 (extraction-granularity + caller-controlled mode): `derivedStatus` is a
269
+ * `Map<string,string|null>|null` — the SAME value trust-reconcile.js's `deriveClaimStatuses()`
270
+ * produces (shells out to derive-claim-status.mjs, local-only, no CI command execution).
271
+ * `opts.onUnderivable` makes the `derivedStatus === null` behavior an EXPLICIT caller choice —
272
+ * there is no silent default that fails open:
273
+ * - `'fail'` (DEFAULT — the safe/original CI behavior; a caller that forgets `opts` never
274
+ * fails open): when `derivedStatus` is null, EVERY session-local pass-asserting claim
275
+ * becomes a `status-underivable` divergence (verbatim pre-#356 message + `continue`) —
276
+ * we never fall back to trusting the bundle's own status. `scripts/ci/trust-reconcile.js`
277
+ * MUST use this mode; it is CI's trust anchor.
278
+ * - `'reduce'` (LOCAL-PREFLIGHT-ONLY opt-in): when `derivedStatus` is null, DEGRADE to a
279
+ * documented reduced-coverage mode — status-misassertion/status-underivable checks are
280
+ * skipped entirely (nothing to re-derive against), but the waiver/unwaived-assumed/
281
+ * session-local-failed/unwaived-session-local checks still run against the claim's own
282
+ * self-reported `assertedStatus`. Only `src/cli/workflow-sidecar.ts`'s local
283
+ * `runReconcilePreflight` opts into this (and surfaces the reduced coverage to the user via
284
+ * a warning) — CI must never reach this branch.
285
+ * When `derivedStatus` is non-null, both modes behave identically (full parity with CI).
286
+ *
287
+ * Returns { issues, attestedCount, logEvents } — attestedCount mirrors trust-reconcile.js's
288
+ * own "N attested claim(s) accepted without independent verification" summary line;
289
+ * logEvents is the ordered list of WAIVED/ATTESTED terminal classifications (F3, iteration-1)
290
+ * so a caller's stdout narrative (e.g. trust-reconcile.js's WAIVED/ATTESTED log lines) is
291
+ * driven by this single classification instead of a parallel re-derivation.
292
+ */
293
+ function sessionLocalShapeIssues(sessionLocal, derivedStatus, opts) {
294
+ const onUnderivable = (opts && opts.onUnderivable) || 'fail';
295
+ const issues = [];
296
+ let attestedCount = 0;
297
+ // F3 (iteration-1): single source of truth for the WAIVED/ATTESTED classification, so
298
+ // trust-reconcile.js's stdout narrative loop consumes this instead of re-deriving its own
299
+ // (previously parallel, driftable) copy. Only populated when a claim reaches the WAIVED or
300
+ // ATTESTED terminal below (never for issues) — callers that don't log can ignore it.
301
+ const logEvents = [];
302
+
303
+ for (const { claimId, claimType, assertedStatus, waiver, subject, evidenceType } of sessionLocal || []) {
304
+ let status;
305
+ if (derivedStatus) {
306
+ // finding 3: re-derive; never trust the asserted status.
307
+ const derived = derivedStatus.get(claimId);
308
+ if (derived === undefined || derived === null) {
309
+ issues.push({
310
+ type: 'status-underivable',
311
+ message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) could not be re-derived CI-side from the bundle's own evidence/events/policies — refusing to trust its self-reported status '${assertedStatus || 'unknown'}' (fail-closed)`,
312
+ });
313
+ continue;
314
+ }
315
+ if (derived !== assertedStatus) {
316
+ issues.push({
317
+ type: 'status-misassertion',
318
+ message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) asserts status '${assertedStatus || 'unknown'}' but CI re-derivation from the bundle's own evidence/events/policies yields '${derived}' — the reconciler does not trust self-reported claim.status`,
319
+ });
320
+ continue;
321
+ }
322
+ status = derived;
323
+ } else if (onUnderivable === 'reduce') {
324
+ // Reduced-coverage mode (derivedStatus === null, explicit local-preflight opt-in): trust
325
+ // the self-reported status for the remaining shape checks only. status-misassertion/
326
+ // status-underivable are, by definition, not checkable without a derivation source —
327
+ // documented gap, not a bug.
328
+ status = assertedStatus;
329
+ } else {
330
+ // Fail-closed mode (default; CI): restores the pre-#356 inline behavior verbatim — we
331
+ // never fall back to trusting a self-reported status.
332
+ issues.push({
333
+ type: 'status-underivable',
334
+ message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) asserts status '${assertedStatus || 'unknown'}' but CI-side re-derivation is unavailable — refusing to trust a self-reported status (fail-closed)`,
335
+ });
336
+ continue;
337
+ }
338
+
339
+ if (status === 'disputed' || status === 'rejected') {
340
+ issues.push({
341
+ type: 'session-local-failed',
342
+ message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) has re-derived status '${status}' — a failing/rejected claim blocks (session-local classification is not a pass bypass)`,
343
+ });
344
+ continue;
345
+ }
346
+ // finding 2: a waiver is the ONLY way an `assumed` (or otherwise non-`verified`)
347
+ // session-local claim passes. `verified` still passes on its own re-derived status.
348
+ if (waiver && waiver.reason && waiver.approved_by) {
349
+ logEvents.push({ kind: 'waived', claimId, claimType, subject, evidenceType, status, waiver });
350
+ continue; // WAIVED — caller may log this loudly; not an issue.
351
+ }
352
+ if (status === 'verified') {
353
+ attestedCount++;
354
+ logEvents.push({ kind: 'attested', claimId, claimType, subject, evidenceType, status });
355
+ continue; // ATTESTED (not independently verifiable at L0) — caller may log; not an issue.
356
+ }
357
+ if (status === 'assumed') {
358
+ issues.push({
359
+ type: 'unwaived-assumed',
360
+ message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) has re-derived status 'assumed' but carries no waiver — 'assumed' alone is not a pass; it requires a documented waiver (--accepted-gap-reason/--waived-by) to be accepted`,
361
+ });
362
+ continue;
363
+ }
364
+ issues.push({
365
+ type: 'unwaived-session-local',
366
+ message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) asserts pass with re-derived status '${status || 'unknown'}' but has no waiver and no CI-re-derived verified status`,
367
+ });
368
+ }
369
+
370
+ return { issues, attestedCount, logEvents };
371
+ }
372
+
373
+ module.exports = {
374
+ classifyBundleClaims,
375
+ normalizeCmd,
376
+ isPassingValue,
377
+ waiverOnCommandIssues,
378
+ noEvidenceCommandIssues,
379
+ reconcilableManifestIssues,
380
+ sessionLocalShapeIssues,
381
+ };
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # relay.sh — OPTIONAL console liveness relay (#295, ADR 0021 §4/§7).
3
+ #
4
+ # Mirrors a single liveness event (claim/heartbeat/release) to the hosted Console as a
5
+ # `kontour.console.liveness` record, reusing the telemetry transport's shared `console_post_json`
6
+ # (endpoint-allow gate, Bearer + tenant auth, timeouts, temp-file handling, detached fire — one core,
7
+ # never forked). The Console side ingests this record type and projects the fleet view + runs the
8
+ # janitor (console repo #125); this script is only the flow-agents EMIT half.
9
+ #
10
+ # STRICTLY OPTIONAL and local-first (ADR 0012 §5): a no-op unless FLOW_AGENTS_CONSOLE_LIVENESS_RELAY
11
+ # is enabled AND a console endpoint is configured. Best-effort throughout — it must NEVER block, slow,
12
+ # or fail the local liveness write that already happened before this was invoked. Every failure path
13
+ # is a quiet `exit 0`.
14
+ #
15
+ # Invoked (fully detached, best-effort) from scripts/hooks/lib/liveness-write.js after the durable
16
+ # local append. Usage: relay.sh '<liveness-event-json>'
17
+ set -uo pipefail
18
+
19
+ # Opt-in gate — off by default.
20
+ case "${FLOW_AGENTS_CONSOLE_LIVENESS_RELAY:-}" in
21
+ 1 | true | TRUE | yes | on) ;;
22
+ *) exit 0 ;;
23
+ esac
24
+
25
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" || exit 0
26
+
27
+ # transport.sh sources ${TELEMETRY_DIR}/lib/redact.sh at load — point it at the telemetry lib dir,
28
+ # then reuse the SAME console POST core the telemetry mirror uses.
29
+ export TELEMETRY_DIR="${TELEMETRY_DIR:-$SCRIPT_DIR/../telemetry}"
30
+ [[ -f "$TELEMETRY_DIR/lib/transport.sh" ]] || exit 0
31
+ # shellcheck source=/dev/null
32
+ source "$TELEMETRY_DIR/lib/transport.sh" 2>/dev/null || exit 0
33
+
34
+ # Resolve the liveness records endpoint: an explicit override wins, else the base console URL + /records
35
+ # ("POST /records with a liveness record type"). No console configured ⇒ no-op.
36
+ endpoint="${FLOW_AGENTS_CONSOLE_LIVENESS_ENDPOINT_URL:-}"
37
+ if [[ -z "$endpoint" ]]; then
38
+ base="${FLOW_AGENTS_CONSOLE_URL:-${CONSOLE_TELEMETRY_URL:-${CONSOLE_URL:-}}}"
39
+ [[ -z "$base" ]] && exit 0
40
+ endpoint="${base%/}/records"
41
+ fi
42
+
43
+ # Auth reuses the env names console_post_json reads (CONSOLE_TELEMETRY_TOKEN / CONSOLE_TENANT_ID),
44
+ # accepting FLOW_AGENTS_CONSOLE_* aliases and an optional token file. Never fatal on a missing/bad
45
+ # token — console_post_json only adds a header when the value passes its safety check.
46
+ if [[ -z "${CONSOLE_TELEMETRY_TOKEN:-}" ]]; then
47
+ token_file="${FLOW_AGENTS_CONSOLE_TOKEN_FILE:-${CONSOLE_TELEMETRY_TOKEN_FILE:-}}"
48
+ if [[ -n "$token_file" && -r "$token_file" ]]; then
49
+ CONSOLE_TELEMETRY_TOKEN="$(tr -d '\r\n' < "$token_file" 2>/dev/null || true)"
50
+ export CONSOLE_TELEMETRY_TOKEN
51
+ fi
52
+ fi
53
+ if [[ -z "${CONSOLE_TENANT_ID:-}" && -n "${FLOW_AGENTS_CONSOLE_TENANT:-}" ]]; then
54
+ export CONSOLE_TENANT_ID="$FLOW_AGENTS_CONSOLE_TENANT"
55
+ fi
56
+
57
+ # The liveness event arrives as a single JSON argument.
58
+ event_json="${1:-}"
59
+ [[ -z "$event_json" ]] && exit 0
60
+
61
+ # Build the kontour.console.liveness record with jq — guarantees valid JSON and, critically,
62
+ # proper string escaping of every untrusted field (actor/subjectId/branch/artifact_dir), so hostile
63
+ # control bytes are \u-escaped rather than emitted raw (injection discipline). No jq ⇒ no-op.
64
+ command -v jq >/dev/null 2>&1 || exit 0
65
+ record="$(printf '%s' "$event_json" | jq -c '{
66
+ schema: "kontour.console.liveness",
67
+ version: "0.1",
68
+ type: (.type // "claim"),
69
+ subjectId: .subjectId,
70
+ actor: .actor,
71
+ actor_key: (.actor_key // .actorKey // null),
72
+ at: .at,
73
+ ttlSeconds: (.ttlSeconds // null),
74
+ host: (.host // null),
75
+ branch: (.branch // null),
76
+ artifact_dir: (.artifact_dir // .artifactDir // null)
77
+ }' 2>/dev/null)" || exit 0
78
+ [[ -z "$record" || "$record" == "null" ]] && exit 0
79
+
80
+ # Optional field redaction (default none — jq escaping already neutralizes injection), then the
81
+ # shared best-effort POST. console_post_json enforces the https/localhost endpoint-allow gate.
82
+ processed="$(redact_event "$record" "${FLOW_AGENTS_CONSOLE_LIVENESS_REDACT:-none}")"
83
+ console_post_json "$endpoint" "$processed"
84
+ exit 0
@@ -0,0 +1,145 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://kontourai.dev/schemas/kontour.console.economics/0.1.json",
4
+ "title": "kontour.console.economics",
5
+ "description": "Per-run kit-economics record (flow-agents #349, console ADR 0003). Immutable per-run fact: cost, time, iterations, and defects caught. cost and defects are co-required (R7 Goodhart guard).",
6
+ "type": "object",
7
+ "additionalProperties": true,
8
+ "required": ["schema", "version", "run_id", "cost", "time", "iterations", "defects"],
9
+ "properties": {
10
+ "schema": { "const": "kontour.console.economics" },
11
+ "version": { "const": "0.1" },
12
+ "run_id": { "type": "string" },
13
+ "at": { "type": ["string", "null"] },
14
+ "task_slug": { "type": ["string", "null"] },
15
+ "model": { "type": ["string", "null"] },
16
+ "pricing_version": { "type": ["string", "null"] },
17
+ "cost": {
18
+ "type": "object",
19
+ "additionalProperties": true,
20
+ "required": [
21
+ "input_tokens",
22
+ "output_tokens",
23
+ "cache_creation_input_tokens",
24
+ "cache_read_input_tokens",
25
+ "estimated_cost_usd",
26
+ "by_model"
27
+ ],
28
+ "properties": {
29
+ "input_tokens": { "type": "number" },
30
+ "output_tokens": { "type": "number" },
31
+ "cache_creation_input_tokens": { "type": "number" },
32
+ "cache_read_input_tokens": { "type": "number" },
33
+ "estimated_cost_usd": { "type": "number" },
34
+ "by_model": { "type": "array" }
35
+ }
36
+ },
37
+ "time": {
38
+ "type": "object",
39
+ "additionalProperties": true,
40
+ "required": ["wall_clock_s", "human_wait_s"],
41
+ "properties": {
42
+ "wall_clock_s": { "type": "number" },
43
+ "human_wait_s": { "type": "number" }
44
+ }
45
+ },
46
+ "phases": {
47
+ "type": "array",
48
+ "items": {
49
+ "type": "object",
50
+ "additionalProperties": true,
51
+ "required": ["phase"],
52
+ "properties": {
53
+ "phase": { "type": "string" },
54
+ "input_tokens": { "type": "number" },
55
+ "output_tokens": { "type": "number" },
56
+ "cache_creation_input_tokens": { "type": "number" },
57
+ "cache_read_input_tokens": { "type": "number" },
58
+ "estimated_cost_usd": { "type": "number" },
59
+ "wall_clock_s": { "type": "number" }
60
+ }
61
+ }
62
+ },
63
+ "iterations": {
64
+ "type": "object",
65
+ "additionalProperties": true,
66
+ "required": ["count", "route_backs"],
67
+ "properties": {
68
+ "count": { "type": "number" },
69
+ "route_backs": { "type": "number" }
70
+ }
71
+ },
72
+ "defects": {
73
+ "type": "object",
74
+ "additionalProperties": true,
75
+ "required": [
76
+ "gate_fires",
77
+ "findings_by_severity",
78
+ "caught_false_completions",
79
+ "verification_verdict"
80
+ ],
81
+ "properties": {
82
+ "gate_fires": { "type": "number" },
83
+ "findings_by_severity": {
84
+ "type": "object",
85
+ "additionalProperties": true,
86
+ "required": ["critical", "high", "medium", "low"],
87
+ "properties": {
88
+ "critical": { "type": "number" },
89
+ "high": { "type": "number" },
90
+ "medium": { "type": "number" },
91
+ "low": { "type": "number" }
92
+ }
93
+ },
94
+ "caught_false_completions": { "type": "number" },
95
+ "verification_verdict": {
96
+ "type": "string",
97
+ "enum": ["PASS", "FAIL", "NOT_VERIFIED"]
98
+ }
99
+ }
100
+ },
101
+ "delegations": {
102
+ "type": "array",
103
+ "description": "Per-sub-agent delegation facts (#415), one entry per delegated agent_id, joined from <slug>/agents/<agent-id>/events.jsonl (latest delegation/escalation event wins for role/model; outcome derived from the agent's full event set). No per-delegation cost (no runtime isolates per-sub-agent tokens yet — see signals.per_delegation_tokens; cost-per-(role,model) is a console projection). Empty when no --agents-dir or no delegations.",
104
+ "items": {
105
+ "type": "object",
106
+ "additionalProperties": true,
107
+ "required": ["role", "resolved_model"],
108
+ "properties": {
109
+ "agent_id": { "type": ["string", "null"] },
110
+ "role": { "type": "string" },
111
+ "resolved_model": { "type": "string" },
112
+ "summary": { "type": ["string", "null"] },
113
+ "escalated_from": { "type": "string" },
114
+ "dispatch_count": {
115
+ "type": "number",
116
+ "description": "How many times the orchestrator (re)dispatched this agent_id (delegation + escalation events). >1 means it was re-prompted — an orchestrator-observable rework signal that needs no sub-agent introspection."
117
+ },
118
+ "outcome": {
119
+ "type": "string",
120
+ "description": "Derived ONLY from real recorded signals, never fabricated. 'unavailable' = no terminal verdict recorded on this harness, distinct from a real accept.",
121
+ "enum": ["accepted", "rework", "diverged", "failed", "unavailable"]
122
+ }
123
+ }
124
+ }
125
+ },
126
+ "signals": {
127
+ "type": "object",
128
+ "additionalProperties": true,
129
+ "description": "What telemetry the current runtime actually exposed (harness-capability declaration — see docs/specs/harness-capability-matrix.md). Lets a consumer distinguish a real zero from a harness-blind gap.",
130
+ "properties": {
131
+ "runtime": { "type": ["string", "null"] },
132
+ "per_delegation_tokens": {
133
+ "type": "boolean",
134
+ "description": "Whether the runtime isolates per-sub-agent token usage. False on every runtime today → per-delegation cost is unavailable."
135
+ },
136
+ "per_delegation_outcome": {
137
+ "type": "string",
138
+ "enum": ["full", "partial", "none", "n/a"],
139
+ "description": "Coverage of the per-delegation outcome signal on this run: full=every delegation has a real outcome; partial=some; none=delegations exist but none had a verdict/escalation; n/a=no delegations observed."
140
+ }
141
+ }
142
+ },
143
+ "tenant_id": { "type": ["string", "null"] }
144
+ }
145
+ }