@kontourai/flow-agents 2.3.0 → 3.0.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 (301) hide show
  1. package/.github/CODEOWNERS +16 -0
  2. package/.github/workflows/ci.yml +44 -0
  3. package/.github/workflows/trust-reconcile.yml +62 -4
  4. package/AGENTS.md +4 -4
  5. package/CHANGELOG.md +62 -0
  6. package/CONTEXT.md +35 -0
  7. package/README.md +9 -3
  8. package/agents/dev.json +1 -1
  9. package/agents/tool-code-reviewer.json +1 -1
  10. package/agents/tool-planner.json +3 -3
  11. package/agents/tool-verifier.json +3 -3
  12. package/build/src/cli/assignment-provider.d.ts +1 -0
  13. package/build/src/cli/assignment-provider.js +748 -0
  14. package/build/src/cli/console-learning-projection.js +3 -2
  15. package/build/src/cli/effective-assignment-provider-settings.d.ts +1 -0
  16. package/build/src/cli/effective-assignment-provider-settings.js +125 -0
  17. package/build/src/cli/init.js +104 -21
  18. package/build/src/cli/kit.js +37 -6
  19. package/build/src/cli/pull-work-provider.js +1 -1
  20. package/build/src/cli/usage-feedback.js +3 -3
  21. package/build/src/cli/validate-hook-influence.js +1 -0
  22. package/build/src/cli/validate-workflow-artifacts.js +27 -7
  23. package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
  24. package/build/src/cli/workflow-sidecar.d.ts +14 -7
  25. package/build/src/cli/workflow-sidecar.js +938 -168
  26. package/build/src/cli.js +6 -0
  27. package/build/src/flow-kit/validate.d.ts +23 -0
  28. package/build/src/flow-kit/validate.js +67 -1
  29. package/build/src/index.d.ts +1 -1
  30. package/build/src/index.js +1 -1
  31. package/build/src/lib/flow-resolver.d.ts +6 -4
  32. package/build/src/lib/flow-resolver.js +125 -58
  33. package/build/src/lib/local-artifact-root.d.ts +14 -2
  34. package/build/src/lib/local-artifact-root.js +22 -5
  35. package/build/src/lib/workflow-learning-projection.js +2 -2
  36. package/build/src/runtime-adapters.d.ts +12 -0
  37. package/build/src/runtime-adapters.js +56 -16
  38. package/build/src/tools/build-universal-bundles.js +49 -16
  39. package/build/src/tools/generate-context-map.js +10 -8
  40. package/build/src/tools/validate-source-tree.js +24 -1
  41. package/console.telemetry.json +22 -0
  42. package/context/contracts/artifact-contract.md +40 -7
  43. package/context/contracts/assignment-provider-contract.md +239 -0
  44. package/context/contracts/builder-kit-workflow-state-contract.md +5 -2
  45. package/context/contracts/decision-registry-contract.md +265 -0
  46. package/context/contracts/delivery-contract.md +3 -1
  47. package/context/contracts/execution-contract.md +25 -0
  48. package/context/contracts/governance-adapter-contract.md +2 -0
  49. package/context/contracts/knowledge-store-contract.md +197 -0
  50. package/context/contracts/planning-contract.md +4 -1
  51. package/context/contracts/review-contract.md +7 -3
  52. package/context/contracts/sandbox-policy.md +2 -0
  53. package/context/contracts/standing-directives.md +13 -0
  54. package/context/contracts/verification-contract.md +11 -1
  55. package/context/contracts/work-item-contract.md +3 -1
  56. package/context/deferred/parallelization.md +1 -1
  57. package/context/gate-awareness.md +4 -4
  58. package/context/scripts/git-status.sh +2 -2
  59. package/context/scripts/hooks/config-protection.js +502 -2
  60. package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
  61. package/context/scripts/hooks/run-hook.js +14 -1
  62. package/context/scripts/hooks/stop-goal-fit.js +1140 -99
  63. package/context/scripts/hooks/workflow-steering.js +212 -6
  64. package/context/settings/assignment-provider-settings.json +33 -0
  65. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  66. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
  67. package/docs/adr/0019-kit-dependency-ownership.md +62 -0
  68. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
  69. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
  70. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +463 -0
  71. package/docs/agent-system-guidebook.md +3 -3
  72. package/docs/agent-usage-feedback-loop.md +6 -6
  73. package/docs/context-map.md +10 -7
  74. package/docs/decisions/decision-records.md +57 -0
  75. package/docs/decisions/index.md +17 -0
  76. package/docs/decisions/knowledge-store-provider.md +51 -0
  77. package/docs/decisions/model-routing.md +63 -0
  78. package/docs/decisions/promotion-gate.md +52 -0
  79. package/docs/decisions/standing-directives.md +66 -0
  80. package/docs/developer-architecture.md +1 -1
  81. package/docs/fixture-ownership.md +4 -0
  82. package/docs/flow-kit-repository-contract.md +5 -5
  83. package/docs/getting-started.md +12 -2
  84. package/docs/integrations/harness-install.md +2 -1
  85. package/docs/kit-authoring-guide.md +64 -5
  86. package/docs/knowledge-kit.md +5 -1
  87. package/docs/learnings/2026-07-improvement-program.md +203 -0
  88. package/docs/migrations.md +6 -5
  89. package/docs/north-star.md +2 -2
  90. package/docs/operating-layers.md +4 -3
  91. package/docs/repository-structure.md +5 -5
  92. package/docs/skills-map.md +8 -8
  93. package/docs/spec/runtime-hook-surface.md +19 -15
  94. package/docs/spikes/graph-provider-2026-07.md +155 -0
  95. package/docs/standards-register.md +7 -7
  96. package/docs/trust-anchor-adoption.md +105 -4
  97. package/docs/work-item-adapters.md +1 -1
  98. package/docs/workflow-artifact-lifecycle.md +72 -9
  99. package/docs/workflow-eval-strategy.md +3 -3
  100. package/docs/workflow-shared-contracts.md +3 -2
  101. package/docs/workflow-usage-guide.md +260 -21
  102. package/evals/acceptance/DEMO-false-completion.md +20 -11
  103. package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
  104. package/evals/acceptance/prove-capture-teeth.sh +11 -11
  105. package/evals/acceptance/test_kiro_harness.sh +1 -1
  106. package/evals/ci/antigaming-suite.sh +5 -0
  107. package/evals/ci/run-baseline.sh +71 -1
  108. package/evals/fixtures/assignment-provider/actor-a.json +6 -0
  109. package/evals/fixtures/assignment-provider/actor-b.json +6 -0
  110. package/evals/fixtures/assignment-provider/github-issue-claimed.json +27 -0
  111. package/evals/fixtures/assignment-provider/github-issue-unassigned.json +7 -0
  112. package/evals/fixtures/assignment-provider/liveness-fresh.json +9 -0
  113. package/evals/fixtures/assignment-provider/liveness-stale.json +9 -0
  114. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
  115. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
  116. package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
  117. package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
  118. package/evals/fixtures/hook-influence/cases.json +32 -0
  119. package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
  120. package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
  121. package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
  122. package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
  123. package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
  124. package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
  125. package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
  126. package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
  127. package/evals/integration/test_actor_identity.sh +254 -0
  128. package/evals/integration/test_assignment_provider_github.sh +318 -0
  129. package/evals/integration/test_assignment_provider_local_file.sh +222 -0
  130. package/evals/integration/test_builder_step_producers.sh +33 -23
  131. package/evals/integration/test_bundle_install.sh +22 -16
  132. package/evals/integration/test_bundle_lifecycle.sh +11 -11
  133. package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
  134. package/evals/integration/test_checkpoint_signing.sh +43 -0
  135. package/evals/integration/test_claim_lookup.sh +4 -4
  136. package/evals/integration/test_codex_hook_resolution.sh +114 -0
  137. package/evals/integration/test_command_log_concurrency.sh +8 -8
  138. package/evals/integration/test_command_log_fork_classification.sh +3 -3
  139. package/evals/integration/test_command_log_integrity.sh +16 -16
  140. package/evals/integration/test_console_learning_projection.sh +1 -1
  141. package/evals/integration/test_critique_supersession_roundtrip.sh +182 -0
  142. package/evals/integration/test_dual_emit_flow_step.sh +145 -0
  143. package/evals/integration/test_enforcer_expects_driven.sh +12 -12
  144. package/evals/integration/test_evidence_capture_hook.sh +14 -13
  145. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  146. package/evals/integration/test_flow_agents_statusline.sh +2 -2
  147. package/evals/integration/test_flow_kit_install_git.sh +52 -0
  148. package/evals/integration/test_flow_kit_repository.sh +2 -0
  149. package/evals/integration/test_flowdef_session_activation.sh +6 -6
  150. package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
  151. package/evals/integration/test_gate_bypass_chain.sh +52 -40
  152. package/evals/integration/test_gate_lockdown.sh +97 -73
  153. package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
  154. package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
  155. package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
  156. package/evals/integration/test_goal_fit_hook.sh +32 -32
  157. package/evals/integration/test_goal_fit_rederive.sh +9 -9
  158. package/evals/integration/test_hook_category_behaviors.sh +103 -0
  159. package/evals/integration/test_hook_influence_cases.sh +1 -0
  160. package/evals/integration/test_install_merge.sh +262 -1
  161. package/evals/integration/test_kit_identity_trust.sh +6 -6
  162. package/evals/integration/test_liveness_conflict_injection.sh +587 -0
  163. package/evals/integration/test_liveness_heartbeat.sh +677 -0
  164. package/evals/integration/test_liveness_verdict.sh +394 -0
  165. package/evals/integration/test_local_flow_kit_install.sh +15 -0
  166. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
  167. package/evals/integration/test_promote_gate.sh +178 -0
  168. package/evals/integration/test_publish_delivery.sh +23 -6
  169. package/evals/integration/test_pull_work_assignment_join.sh +132 -0
  170. package/evals/integration/test_pull_work_liveness_preflight.sh +394 -0
  171. package/evals/integration/test_pull_work_provider.sh +2 -2
  172. package/evals/integration/test_reconcile_soundness.sh +37 -13
  173. package/evals/integration/test_resolvefirststep_security.sh +22 -0
  174. package/evals/integration/test_runtime_adapter_activation.sh +87 -18
  175. package/evals/integration/test_session_resume_roundtrip.sh +147 -0
  176. package/evals/integration/test_sidecar_field_preservation.sh +249 -0
  177. package/evals/integration/test_trust_reconcile.sh +11 -10
  178. package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
  179. package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
  180. package/evals/integration/test_trust_reconcile_negatives.sh +759 -0
  181. package/evals/integration/test_usage_feedback_global.sh +5 -5
  182. package/evals/integration/test_validate_artifacts_portability.sh +60 -0
  183. package/evals/integration/test_verify_cli.sh +2 -2
  184. package/evals/integration/test_veritas_governance_kit.sh +117 -0
  185. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
  186. package/evals/integration/test_workflow_artifacts.sh +33 -0
  187. package/evals/integration/test_workflow_sidecar_writer.sh +802 -74
  188. package/evals/integration/test_workflow_steering_hook.sh +175 -5
  189. package/evals/lib/codex-provider.sh +1 -1
  190. package/evals/lib/node.sh +19 -1
  191. package/evals/run.sh +32 -0
  192. package/evals/static/test_decisions.sh +150 -0
  193. package/evals/static/test_flowdef_codeowners_coverage.sh +62 -0
  194. package/evals/static/test_knowledge_providers.sh +23 -0
  195. package/evals/static/test_package.sh +19 -10
  196. package/evals/static/test_universal_bundles.sh +48 -3
  197. package/evals/static/test_workflow_skills.sh +41 -8
  198. package/kits/builder/flows/build.flow.json +3 -74
  199. package/kits/builder/flows/publish-learn.flow.json +90 -0
  200. package/kits/builder/kit.json +11 -0
  201. package/kits/builder/skills/builder-shape/SKILL.md +1 -1
  202. package/kits/builder/skills/deliver/SKILL.md +32 -11
  203. package/kits/builder/skills/design-probe/SKILL.md +4 -4
  204. package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
  205. package/kits/builder/skills/execute-plan/SKILL.md +2 -2
  206. package/kits/builder/skills/fix-bug/SKILL.md +2 -0
  207. package/kits/builder/skills/gate-review/SKILL.md +6 -6
  208. package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
  209. package/kits/builder/skills/learning-review/SKILL.md +7 -7
  210. package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
  211. package/kits/builder/skills/plan-work/SKILL.md +27 -5
  212. package/kits/builder/skills/pull-work/SKILL.md +148 -5
  213. package/kits/builder/skills/release-readiness/SKILL.md +4 -4
  214. package/kits/builder/skills/review-work/SKILL.md +24 -1
  215. package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
  216. package/kits/builder/skills/verify-work/SKILL.md +8 -1
  217. package/kits/catalog.json +6 -0
  218. package/kits/knowledge/kit.json +35 -0
  219. package/kits/knowledge/providers/conformance/fixtures/git-repo/CONTEXT.md +12 -0
  220. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/old-sprocket-shape.md +13 -0
  221. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/sprocket-shape.md +14 -0
  222. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/decisions/widget-format.md +14 -0
  223. package/kits/knowledge/providers/conformance/fixtures/git-repo/docs/learnings/fixture-learning.md +7 -0
  224. package/kits/knowledge/providers/conformance/fixtures/work-item/issues.json +30 -0
  225. package/kits/knowledge/providers/conformance/suite.test.js +125 -0
  226. package/kits/knowledge/providers/git-repo/index.js +236 -0
  227. package/kits/knowledge/providers/health/health-pass.test.js +99 -0
  228. package/kits/knowledge/providers/health/index.js +153 -0
  229. package/kits/knowledge/providers/index.js +24 -0
  230. package/kits/knowledge/providers/lib/model.js +91 -0
  231. package/kits/knowledge/providers/lib/schema-validate.js +119 -0
  232. package/kits/knowledge/providers/markdown-vault/index.js +169 -0
  233. package/kits/knowledge/providers/work-item/index.js +204 -0
  234. package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
  235. package/kits/veritas-governance/docs/README.md +75 -0
  236. package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
  237. package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
  238. package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
  239. package/kits/veritas-governance/kit.json +16 -0
  240. package/package.json +11 -5
  241. package/packaging/README.md +2 -2
  242. package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
  243. package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
  244. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
  245. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
  246. package/packaging/manifest.json +9 -10
  247. package/schemas/assignment-provider-settings.schema.json +125 -0
  248. package/schemas/decision-record.schema.json +109 -0
  249. package/schemas/knowledge/edge.schema.json +54 -0
  250. package/schemas/knowledge/health-report.schema.json +45 -0
  251. package/schemas/knowledge/node.schema.json +49 -0
  252. package/schemas/knowledge/proposal.schema.json +53 -0
  253. package/schemas/workflow-evidence.schema.json +1 -1
  254. package/schemas/workflow-state.schema.json +5 -0
  255. package/scripts/README.md +8 -3
  256. package/scripts/check-content-boundary.cjs +1 -1
  257. package/scripts/check-decisions.cjs +356 -0
  258. package/scripts/ci/derive-claim-status.mjs +74 -0
  259. package/scripts/ci/trust-reconcile.js +957 -103
  260. package/scripts/git-status.sh +2 -2
  261. package/scripts/hooks/claude-telemetry-hook.js +37 -3
  262. package/scripts/hooks/codex-telemetry-hook.js +36 -2
  263. package/scripts/hooks/config-protection.js +102 -30
  264. package/scripts/hooks/evidence-capture.js +3 -3
  265. package/scripts/hooks/lib/actor-identity.js +368 -0
  266. package/scripts/hooks/lib/config-protection-remedies.js +69 -0
  267. package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
  268. package/scripts/hooks/lib/liveness-policy.js +127 -0
  269. package/scripts/hooks/lib/liveness-read.js +105 -12
  270. package/scripts/hooks/lib/liveness-write.js +46 -0
  271. package/scripts/hooks/lib/local-artifact-paths.js +12 -6
  272. package/scripts/hooks/opencode-telemetry-hook.js +27 -1
  273. package/scripts/hooks/pi-telemetry-hook.js +27 -1
  274. package/scripts/hooks/stop-goal-fit.js +157 -26
  275. package/scripts/hooks/workflow-steering.js +77 -3
  276. package/scripts/install-codex-home.sh +138 -38
  277. package/scripts/statusline/flow-agents-statusline.js +2 -2
  278. package/src/cli/assignment-provider.ts +845 -0
  279. package/src/cli/console-learning-projection.ts +3 -2
  280. package/src/cli/effective-assignment-provider-settings.ts +112 -0
  281. package/src/cli/init.ts +101 -21
  282. package/src/cli/kit.ts +37 -6
  283. package/src/cli/public-api.test.mjs +58 -6
  284. package/src/cli/pull-work-provider.ts +1 -1
  285. package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
  286. package/src/cli/usage-feedback.ts +3 -3
  287. package/src/cli/validate-hook-influence.ts +1 -0
  288. package/src/cli/validate-workflow-artifacts.ts +27 -7
  289. package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
  290. package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
  291. package/src/cli/workflow-sidecar.ts +933 -168
  292. package/src/cli.ts +6 -0
  293. package/src/flow-kit/validate.ts +74 -1
  294. package/src/index.ts +6 -2
  295. package/src/lib/flow-resolver.ts +123 -55
  296. package/src/lib/local-artifact-root.ts +24 -5
  297. package/src/lib/workflow-learning-projection.ts +2 -2
  298. package/src/runtime-adapters.ts +61 -15
  299. package/src/tools/build-universal-bundles.ts +49 -16
  300. package/src/tools/generate-context-map.ts +10 -8
  301. package/src/tools/validate-source-tree.ts +24 -1
@@ -0,0 +1,49 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://flow-agents.dev/schemas/knowledge/node.schema.json",
4
+ "title": "Knowledge Graph Node",
5
+ "description": "A typed node in the Knowledge Kit graph model. type is EXTENSIBLE: the recommended core vocabulary is note, decision, issue, session, person, but providers MAY emit additional types. Every node carries provenance so any assertion can be traced back to the store it came from.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["id", "type", "title", "provenance"],
9
+ "properties": {
10
+ "id": {
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "description": "Provider-scoped stable identifier, unique within a single readGraph result."
14
+ },
15
+ "type": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "description": "Node type. Recommended core vocabulary: note, decision, issue, session, person. Extensible — unknown types MUST NOT be rejected."
19
+ },
20
+ "title": {
21
+ "type": "string",
22
+ "minLength": 1,
23
+ "description": "Human-readable title used for display and duplicate detection."
24
+ },
25
+ "body": {
26
+ "type": "string",
27
+ "description": "Primary content. Format is provider/type-specific."
28
+ },
29
+ "attributes": {
30
+ "type": "object",
31
+ "description": "Provider-specific structured fields (e.g. issue state/labels, decision status, note category/tags)."
32
+ },
33
+ "provenance": { "$ref": "#/$defs/provenance" }
34
+ },
35
+ "$defs": {
36
+ "provenance": {
37
+ "type": "object",
38
+ "additionalProperties": false,
39
+ "required": ["provider", "source", "retrieved_at"],
40
+ "properties": {
41
+ "provider": { "type": "string", "minLength": 1, "description": "Provider id that produced this element (e.g. markdown-vault, git-repo, work-item)." },
42
+ "source": { "type": "string", "minLength": 1, "description": "The concrete origin: a file path, an issue ref, or a URL." },
43
+ "locator": { "type": "string", "description": "Optional finer locator within the source (line, anchor, frontmatter field)." },
44
+ "retrieved_at": { "type": "string", "minLength": 1, "description": "ISO-8601 timestamp the element was read." },
45
+ "agent": { "type": "string", "description": "Optional agent/process id that performed the read." }
46
+ }
47
+ }
48
+ }
49
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://flow-agents.dev/schemas/knowledge/proposal.schema.json",
4
+ "title": "Knowledge Store Write Proposal",
5
+ "description": "The ONLY write artifact a knowledge store provider may emit. Providers over human-curated stores are proposals-only: proposeWrite() never mutates the store and can only ever return a Proposal whose status is the const \"proposed\". Enactment happens downstream (a human edit, the decision-registry promote step, or a gh comment/label the operator files) — never inside the provider. The `rendered` field carries the provider-native form the enactor would apply (decision-registry topic frontmatter for git-repo; an issue comment/label body for work-item; a vault note with frontmatter+wikilinks for markdown-vault).",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["schema_version", "provider", "kind", "target", "status", "provenance"],
9
+ "properties": {
10
+ "schema_version": { "const": "1.0" },
11
+ "provider": { "type": "string", "minLength": 1 },
12
+ "kind": {
13
+ "type": "string",
14
+ "enum": ["create-node", "update-node", "add-edge", "comment", "label", "decision-topic"],
15
+ "description": "The shape of the proposed change."
16
+ },
17
+ "target": {
18
+ "type": "object",
19
+ "description": "Where the change would land if enacted (file path, issue number, store id). Descriptive only — the provider does not touch it."
20
+ },
21
+ "payload": {
22
+ "type": "object",
23
+ "description": "The structured node/edge/field the proposal would introduce."
24
+ },
25
+ "rendered": {
26
+ "type": "string",
27
+ "description": "The provider-native rendered form an enactor would apply (topic-file frontmatter, gh comment body, vault note)."
28
+ },
29
+ "status": {
30
+ "const": "proposed",
31
+ "description": "Always the literal \"proposed\". A provider can NEVER return an applied proposal — this const is the structural expression of proposals-only discipline."
32
+ },
33
+ "rationale": {
34
+ "type": "string",
35
+ "description": "Why the change is proposed."
36
+ },
37
+ "provenance": { "$ref": "#/$defs/provenance" }
38
+ },
39
+ "$defs": {
40
+ "provenance": {
41
+ "type": "object",
42
+ "additionalProperties": false,
43
+ "required": ["provider", "source", "retrieved_at"],
44
+ "properties": {
45
+ "provider": { "type": "string", "minLength": 1 },
46
+ "source": { "type": "string", "minLength": 1 },
47
+ "locator": { "type": "string" },
48
+ "retrieved_at": { "type": "string", "minLength": 1 },
49
+ "agent": { "type": "string" }
50
+ }
51
+ }
52
+ }
53
+ }
@@ -174,7 +174,7 @@
174
174
  },
175
175
  "kind": {
176
176
  "type": "string",
177
- "enum": ["build", "types", "lint", "test", "security", "diff", "browser", "runtime", "policy", "external"]
177
+ "enum": ["build", "types", "lint", "test", "command", "security", "diff", "browser", "runtime", "policy", "external"]
178
178
  },
179
179
  "status": {
180
180
  "type": "string",
@@ -36,6 +36,11 @@
36
36
  "owner": {
37
37
  "type": "string"
38
38
  },
39
+ "branch": {
40
+ "type": "string",
41
+ "minLength": 1,
42
+ "description": "Routing branch for this session, e.g. agent/<actor>/<slug> (ADR 0021 \u00a73/\u00a75, issue #289). Optional for migration honesty: legacy/in-flight sessions predate this field. ensure-session always writes it for newly created sessions; validate-workflow-artifacts warns (does not fail) when it is absent."
43
+ },
39
44
  "created_at": {
40
45
  "type": "string",
41
46
  "format": "date-time"
package/scripts/README.md CHANGED
@@ -52,7 +52,7 @@ renamed, or changes category, update the table and the validator together.
52
52
  | `codex-telemetry-hook.js` | telemetry shim | `evals/integration/test_hook_category_behaviors.sh`, `evals/integration/test_telemetry.sh` | Captures Codex hook telemetry and fails open. |
53
53
  | `run-hook.js` | hook runner | `evals/integration/test_hook_category_behaviors.sh`, `evals/integration/test_goal_fit_hook.sh`, `evals/integration/test_workflow_steering_hook.sh` | Applies profile/disable flags, traversal checks, and hook execution. |
54
54
  | `config-protection.js` | policy hook | `evals/integration/test_hook_category_behaviors.sh` | Blocks unsafe runtime config edits. |
55
- | `evidence-capture.js` | policy hook | `evals/integration/test_evidence_capture_hook.sh` | Deterministically captures command executions to `.flow-agents/<slug>/command-log.jsonl` so evidence is machine-recorded, not model-claimed (cross-referenced by stop-goal-fit). |
55
+ | `evidence-capture.js` | policy hook | `evals/integration/test_evidence_capture_hook.sh` | Deterministically captures command executions to `.kontourai/flow-agents/<slug>/command-log.jsonl` so evidence is machine-recorded, not model-claimed (cross-referenced by stop-goal-fit). |
56
56
  | `governance-audit.sh` | policy hook | `evals/integration/test_hook_category_behaviors.sh`, `evals/integration/test_telemetry.sh` | Emits governance/Veritas audit context when configured. |
57
57
  | `opencode-hook-adapter.js` | runtime adapter | `evals/integration/test_bundle_install.sh` | Translates opencode plugin events into the shared hook runner contract. |
58
58
  | `opencode-telemetry-hook.js` | telemetry shim | `evals/integration/test_bundle_install.sh` | Captures opencode plugin telemetry and fails open. |
@@ -67,10 +67,15 @@ renamed, or changes category, update the table and the validator together.
67
67
  | `workflow-steering.js` | policy hook | `evals/integration/test_workflow_steering_hook.sh` | Provides workflow guidance from current artifact state. |
68
68
  | `pre-commit-quality.js` | repo guardrail hook | `evals/integration/test_hook_category_behaviors.sh` | Supports repository Git hook checks, not installed runtime hooks. |
69
69
  | `desktop-notify.sh` | local notification helper | `evals/integration/test_hook_category_behaviors.sh` | Optional local desktop notification helper. |
70
+ | `lib/actor-identity.js` | shared hook library | `evals/integration/test_actor_identity.sh` | Shared runtime-agnostic actor identity resolver (`resolveActor`) consumed by `workflow-steering.js` and `workflow-sidecar.js` to retire the shared `"local"` liveness-actor default (issue #287). |
70
71
  | `lib/audit-transport.sh` | shared hook library | `evals/integration/test_hook_category_behaviors.sh`, `evals/integration/test_telemetry.sh` | Shared audit event transport functions. |
72
+ | `lib/config-protection-remedies.js` | shared hook library | `evals/integration/test_hook_category_behaviors.sh` | Sanctioned-remedy table for `config-protection.js` (WS8/ADR 0020 extraction). |
71
73
  | `lib/hook-flags.js` | shared hook library | `evals/integration/test_hook_category_behaviors.sh` | Shared profile/disable flag parsing. |
74
+ | `lib/liveness-heartbeat.js` | shared hook library | `evals/integration/test_liveness_heartbeat.sh` | Shared tool-activity liveness heartbeat (`maybeEmitHeartbeat`); rides `postToolUse` across all four telemetry hook wrappers, throttled and fail-open (issue #288). |
75
+ | `lib/liveness-policy.js` | shared hook library | `evals/integration/test_workflow_sidecar_writer.sh`, `evals/integration/test_liveness_heartbeat.sh` | Shared liveness on/off predicate (`isLivenessEnabled`, default-on/opt-out) and TTL/heartbeat-throttle default resolution (issue #288). |
72
76
  | `lib/liveness-read.js` | shared hook library | `evals/integration/test_session_resume_roundtrip.sh` | Shared liveness event reader + freshness check (`readLivenessEvents`, `freshHolders`); consumed by the reground hook and `workflow-sidecar liveness status`. |
73
- | `lib/local-artifact-paths.js` | shared hook library | `evals/integration/test_migrate_local_artifacts.sh`, `evals/integration/test_workflow_sidecar_writer.sh` | Shared `.kontourai/flow-agents` artifact-root helpers with legacy `.flow-agents` read compatibility for CJS hooks. |
77
+ | `lib/liveness-write.js` | shared hook library | `evals/integration/test_workflow_sidecar_writer.sh`, `evals/integration/test_liveness_heartbeat.sh` | Shared liveness event stream writer (`livenessStreamFile`, `appendLivenessEvent`), lifted from `workflow-sidecar.ts` so the CLI and hook wrappers share one writer (issue #288). |
78
+ | `lib/local-artifact-paths.js` | shared hook library | `evals/integration/test_migrate_local_artifacts.sh`, `evals/integration/test_workflow_sidecar_writer.sh` | Shared `.kontourai/flow-agents` artifact-root helpers for CJS hooks. |
74
79
  | `lib/patterns.sh` | shared hook library | `evals/integration/test_hook_category_behaviors.sh`, `evals/integration/test_telemetry.sh` | Shared shell pattern constants. |
75
80
  | `lib/resolve-formatter.js` | shared hook library | `evals/integration/test_hook_category_behaviors.sh` | Shared formatter resolution helper. |
76
81
 
@@ -103,7 +108,7 @@ prompting. Add `--allow-network` to probe a non-local HTTPS Console endpoint.
103
108
 
104
109
  ## Install And Repo Utilities
105
110
 
106
- - `install-codex-home.sh`: installs the isolated generated Codex home.
111
+ - `install-codex-home.sh`: installs the generated Codex bundle into `CODEX_HOME`, or `~/.codex` when `CODEX_HOME` is unset. A positional destination argument is the explicit override for isolated homes and tests.
107
112
  - `setup-repo-hooks.sh`: configures this clone's Git hook path.
108
113
  - `check-content-boundary.cjs`, `detect-tools.sh`, `discover-agents.sh`, `git-status.sh`: repo-local helper commands.
109
114
  - `context-budget/` and `statusline/`: specialized support tooling copied into bundles where needed.
@@ -42,7 +42,7 @@ function isIgnoredPath(filePath) {
42
42
  }
43
43
 
44
44
  function isWorkflowRuntimeArtifact(filePath) {
45
- return filePath.startsWith(".flow-agents/");
45
+ return filePath.startsWith(".kontourai/flow-agents/") || filePath.startsWith(".flow-agents/");
46
46
  }
47
47
 
48
48
  function lineNumberFor(content, index) {
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // check-decisions.cjs — validator + index generator for the topic-keyed
5
+ // decision registry (context/contracts/decision-registry-contract.md).
6
+ //
7
+ // Usage:
8
+ // node scripts/check-decisions.cjs check # validate all topic files + assert index is current (default)
9
+ // node scripts/check-decisions.cjs gen-index # (re)write docs/decisions/index.md deterministically
10
+ //
11
+ // Zero runtime dependencies (mirrors scripts/check-content-boundary.cjs): the
12
+ // decision-record JSON schema at schemas/decision-record.schema.json is the
13
+ // normative structural contract; this script enforces the same rules directly
14
+ // so it runs in `npm ci` environments without a JSON-schema/YAML library.
15
+
16
+ const fs = require("node:fs");
17
+ const path = require("node:path");
18
+
19
+ const ROOT = path.resolve(__dirname, "..");
20
+ // FLOW_AGENTS_DECISIONS_DIR lets the eval suite point the validator/generator at
21
+ // a throwaway fixture directory (mirrors FLOW_AGENTS_CONTENT_BOUNDARY_FILES).
22
+ const DECISIONS_DIR = process.env.FLOW_AGENTS_DECISIONS_DIR
23
+ ? path.resolve(process.env.FLOW_AGENTS_DECISIONS_DIR)
24
+ : path.join(ROOT, "docs", "decisions");
25
+ const INDEX_PATH = path.join(DECISIONS_DIR, "index.md");
26
+ const INDEX_SLUG = "index";
27
+
28
+ const STATUSES = ["current", "superseded", "merged", "needs-decision"];
29
+ const EVIDENCE_KINDS = ["issue", "pr", "commit", "session-archive", "adr", "doc", "url"];
30
+ const SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
31
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
32
+
33
+ // Secret-shaped literal detectors — mirror scripts/hooks/lib/patterns.sh so the
34
+ // registry never captures a credential in an evidence ref.
35
+ const SECRET_PATTERNS = [
36
+ { label: "aws access key id", re: /AKIA[A-Z0-9]{16}/ },
37
+ { label: "aws sts key id", re: /ASIA[A-Z0-9]{16}/ },
38
+ { label: "github token", re: /gh[pousr]_[A-Za-z0-9_]{36,}/ },
39
+ { label: "private key block", re: /BEGIN[A-Z ]*PRIVATE KEY/ },
40
+ { label: "jwt", re: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
41
+ {
42
+ label: "generic secret literal",
43
+ re: /(secret|password|token|api[_-]?key)\s*[:=]\s*["'][^"']{8,}/i,
44
+ },
45
+ ];
46
+
47
+ function listTopicFiles() {
48
+ if (!fs.existsSync(DECISIONS_DIR)) return [];
49
+ return fs
50
+ .readdirSync(DECISIONS_DIR)
51
+ .filter((name) => name.endsWith(".md") && name !== "index.md")
52
+ .sort();
53
+ }
54
+
55
+ // Minimal, strict YAML-frontmatter reader. The decision-record frontmatter is a
56
+ // flat map of scalars plus two list shapes (evidence[] as `- kind: .. ` blocks
57
+ // and slug arrays as inline `[a, b]` or block `- slug`). We parse exactly that
58
+ // shape and reject anything we do not understand rather than guessing.
59
+ function parseFrontmatter(raw, errors) {
60
+ if (!raw.startsWith("---\n") && !raw.startsWith("---\r\n")) {
61
+ errors.push("file must begin with a YAML frontmatter block delimited by '---'");
62
+ return null;
63
+ }
64
+ const lines = raw.split(/\r?\n/);
65
+ let end = -1;
66
+ for (let i = 1; i < lines.length; i += 1) {
67
+ if (lines[i].trim() === "---") {
68
+ end = i;
69
+ break;
70
+ }
71
+ }
72
+ if (end === -1) {
73
+ errors.push("frontmatter block is not terminated by a closing '---'");
74
+ return null;
75
+ }
76
+ const body = lines.slice(1, end);
77
+ const data = {};
78
+ let cursorKey = null;
79
+
80
+ for (let i = 0; i < body.length; i += 1) {
81
+ const line = body[i];
82
+ if (line.trim() === "" || line.trim().startsWith("#")) continue;
83
+
84
+ const topMatch = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
85
+ const listItem = line.match(/^\s*-\s+(.*)$/);
86
+
87
+ if (topMatch && !line.startsWith(" ")) {
88
+ const key = topMatch[1];
89
+ const rest = topMatch[2].trim();
90
+ cursorKey = key;
91
+ if (rest === "") {
92
+ data[key] = { __pending_list: true, items: [] };
93
+ } else if (rest.startsWith("[") && rest.endsWith("]")) {
94
+ const inner = rest.slice(1, -1).trim();
95
+ data[key] = inner === "" ? [] : inner.split(",").map((s) => stripScalar(s.trim()));
96
+ } else {
97
+ data[key] = stripScalar(rest);
98
+ }
99
+ continue;
100
+ }
101
+
102
+ if (listItem && cursorKey) {
103
+ const container = data[cursorKey];
104
+ if (!container || !container.__pending_list) {
105
+ errors.push(`unexpected list item under '${cursorKey}': ${line.trim()}`);
106
+ continue;
107
+ }
108
+ const itemBody = listItem[1].trim();
109
+ // Object list item: `- kind: pr, ref: ...` OR `- kind: pr` then following ` ref: ...`
110
+ const inlinePair = itemBody.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
111
+ if (inlinePair) {
112
+ const obj = {};
113
+ obj[inlinePair[1]] = stripScalar(inlinePair[2].trim());
114
+ // consume subsequent indented `key: value` continuation lines
115
+ while (i + 1 < body.length && /^\s+[A-Za-z0-9_]+:\s*/.test(body[i + 1]) && !/^\s*-\s+/.test(body[i + 1])) {
116
+ const cont = body[i + 1].trim().match(/^([A-Za-z0-9_]+):\s*(.*)$/);
117
+ if (!cont) break;
118
+ obj[cont[1]] = stripScalar(cont[2].trim());
119
+ i += 1;
120
+ }
121
+ container.items.push(obj);
122
+ } else {
123
+ container.items.push(stripScalar(itemBody));
124
+ }
125
+ continue;
126
+ }
127
+
128
+ errors.push(`unparseable frontmatter line: ${line}`);
129
+ }
130
+
131
+ // Normalize pending lists to plain arrays.
132
+ for (const key of Object.keys(data)) {
133
+ if (data[key] && data[key].__pending_list) {
134
+ data[key] = data[key].items;
135
+ }
136
+ }
137
+ return data;
138
+ }
139
+
140
+ function stripScalar(value) {
141
+ let v = value.trim();
142
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
143
+ v = v.slice(1, -1);
144
+ }
145
+ return v;
146
+ }
147
+
148
+ function firstBodyLine(raw) {
149
+ const lines = raw.split(/\r?\n/);
150
+ let seenClose = false;
151
+ let count = 0;
152
+ for (const line of lines) {
153
+ if (line.trim() === "---") {
154
+ count += 1;
155
+ if (count === 2) seenClose = true;
156
+ continue;
157
+ }
158
+ if (!seenClose) continue;
159
+ const t = line.trim();
160
+ if (t === "") continue;
161
+ return t.replace(/^#+\s*/, "").trim();
162
+ }
163
+ return "";
164
+ }
165
+
166
+ function validateFile(slug, raw, knownSlugs) {
167
+ const errors = [];
168
+ const fm = parseFrontmatter(raw, errors);
169
+ if (!fm) return errors;
170
+
171
+ if (!SLUG_RE.test(slug)) {
172
+ errors.push(`filename slug '${slug}' is not a valid kebab-case topic slug`);
173
+ }
174
+
175
+ // status
176
+ if (!("status" in fm)) {
177
+ errors.push("missing required field: status");
178
+ } else if (!STATUSES.includes(fm.status)) {
179
+ errors.push(`unknown status '${fm.status}' (allowed: ${STATUSES.join(", ")})`);
180
+ }
181
+
182
+ // subject
183
+ if (!("subject" in fm) || typeof fm.subject !== "string" || fm.subject.trim() === "") {
184
+ errors.push("missing required field: subject (non-empty)");
185
+ }
186
+
187
+ // decided
188
+ if (!("decided" in fm)) {
189
+ errors.push("missing required field: decided");
190
+ } else if (typeof fm.decided !== "string" || !DATE_RE.test(fm.decided)) {
191
+ errors.push(`decided must be an ISO date (YYYY-MM-DD); got '${fm.decided}'`);
192
+ }
193
+
194
+ // evidence
195
+ if (!("evidence" in fm)) {
196
+ errors.push("missing required field: evidence[]");
197
+ } else if (!Array.isArray(fm.evidence) || fm.evidence.length === 0) {
198
+ errors.push("evidence must be a non-empty array of {kind, ref}");
199
+ } else {
200
+ fm.evidence.forEach((item, idx) => {
201
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
202
+ errors.push(`evidence[${idx}] must be an object {kind, ref}`);
203
+ return;
204
+ }
205
+ if (!EVIDENCE_KINDS.includes(item.kind)) {
206
+ errors.push(`evidence[${idx}].kind '${item.kind}' is not one of ${EVIDENCE_KINDS.join(", ")}`);
207
+ }
208
+ if (typeof item.ref !== "string" || item.ref.trim() === "") {
209
+ errors.push(`evidence[${idx}].ref must be a non-empty string`);
210
+ return;
211
+ }
212
+ for (const pat of SECRET_PATTERNS) {
213
+ if (pat.re.test(item.ref)) {
214
+ errors.push(`evidence[${idx}].ref contains a secret-shaped literal (${pat.label}); link durable provenance, never a credential`);
215
+ break;
216
+ }
217
+ }
218
+ });
219
+ }
220
+
221
+ // tombstone / relationship fields
222
+ const relSingles = ["superseded_by", "merged_into"];
223
+ for (const key of relSingles) {
224
+ if (key in fm) {
225
+ const target = fm[key];
226
+ if (typeof target !== "string" || !SLUG_RE.test(target)) {
227
+ errors.push(`${key} must be a single topic slug; got '${target}'`);
228
+ } else if (!knownSlugs.has(target)) {
229
+ errors.push(`${key} points at missing topic slug '${target}' (no docs/decisions/${target}.md)`);
230
+ } else if (target === slug) {
231
+ errors.push(`${key} must not point at its own slug`);
232
+ }
233
+ }
234
+ }
235
+ if ("supersedes" in fm) {
236
+ const arr = Array.isArray(fm.supersedes) ? fm.supersedes : [fm.supersedes];
237
+ arr.forEach((target) => {
238
+ if (typeof target !== "string" || !SLUG_RE.test(target)) {
239
+ errors.push(`supersedes entry must be a topic slug; got '${target}'`);
240
+ } else if (!knownSlugs.has(target)) {
241
+ errors.push(`supersedes points at missing topic slug '${target}'`);
242
+ }
243
+ });
244
+ }
245
+
246
+ // status-conditional relationship rules
247
+ if (fm.status === "current" || fm.status === "needs-decision") {
248
+ if ("superseded_by" in fm || "merged_into" in fm) {
249
+ errors.push(`status '${fm.status}' must not carry superseded_by or merged_into`);
250
+ }
251
+ }
252
+ if (fm.status === "superseded") {
253
+ if (!("superseded_by" in fm)) errors.push("status 'superseded' requires superseded_by");
254
+ if ("merged_into" in fm) errors.push("status 'superseded' must not carry merged_into");
255
+ }
256
+ if (fm.status === "merged") {
257
+ if (!("merged_into" in fm)) errors.push("status 'merged' requires merged_into");
258
+ if ("superseded_by" in fm) errors.push("status 'merged' must not carry superseded_by");
259
+ }
260
+
261
+ return errors;
262
+ }
263
+
264
+ function readTopics() {
265
+ const files = listTopicFiles();
266
+ const knownSlugs = new Set(files.map((f) => f.replace(/\.md$/, "")));
267
+ return files.map((file) => {
268
+ const slug = file.replace(/\.md$/, "");
269
+ const raw = fs.readFileSync(path.join(DECISIONS_DIR, file), "utf8");
270
+ const errors = validateFile(slug, raw, knownSlugs);
271
+ const fm = parseFrontmatter(raw, []);
272
+ return { slug, raw, fm: fm || {}, errors };
273
+ });
274
+ }
275
+
276
+ function oneLiner(topic) {
277
+ const subject = topic.fm && typeof topic.fm.subject === "string" ? topic.fm.subject.trim() : "";
278
+ if (subject) return subject;
279
+ const body = firstBodyLine(topic.raw);
280
+ return body || topic.slug;
281
+ }
282
+
283
+ function renderIndex(topics) {
284
+ const lines = [];
285
+ lines.push("---");
286
+ lines.push("title: Decision Index");
287
+ lines.push("---");
288
+ lines.push("");
289
+ lines.push("# Decision Index");
290
+ lines.push("");
291
+ lines.push("Generated by `npm run gen:decisions-index`. Do not edit by hand.");
292
+ lines.push("Topic-keyed living decision records per `context/contracts/decision-registry-contract.md`.");
293
+ lines.push("Numbered ADRs under `docs/adr/` are frozen history and are not listed here.");
294
+ lines.push("");
295
+ lines.push("| Topic | Status | Decision |");
296
+ lines.push("| --- | --- | --- |");
297
+ const sorted = [...topics].sort((a, b) => a.slug.localeCompare(b.slug));
298
+ for (const topic of sorted) {
299
+ const status = topic.fm && typeof topic.fm.status === "string" ? topic.fm.status : "unknown";
300
+ const summary = oneLiner(topic).replace(/\|/g, "\\|");
301
+ lines.push(`| [${topic.slug}](./${topic.slug}.md) | ${status} | ${summary} |`);
302
+ }
303
+ lines.push("");
304
+ return lines.join("\n");
305
+ }
306
+
307
+ function cmdGenIndex() {
308
+ fs.mkdirSync(DECISIONS_DIR, { recursive: true });
309
+ const topics = readTopics();
310
+ const content = renderIndex(topics);
311
+ fs.writeFileSync(INDEX_PATH, content, "utf8");
312
+ console.log(`Wrote ${path.relative(ROOT, INDEX_PATH)} (${topics.length} topic${topics.length === 1 ? "" : "s"}).`);
313
+ return 0;
314
+ }
315
+
316
+ function cmdCheck() {
317
+ const topics = readTopics();
318
+ let failed = false;
319
+
320
+ for (const topic of topics) {
321
+ if (topic.errors.length > 0) {
322
+ failed = true;
323
+ console.error(`FAIL docs/decisions/${topic.slug}.md`);
324
+ for (const err of topic.errors) console.error(` - ${err}`);
325
+ }
326
+ }
327
+
328
+ // Index freshness: check regenerates deterministically and must match on disk
329
+ // (this is what makes regeneration idempotent + diff-clean, AC2).
330
+ const expectedIndex = renderIndex(topics);
331
+ const actualIndex = fs.existsSync(INDEX_PATH) ? fs.readFileSync(INDEX_PATH, "utf8") : null;
332
+ if (actualIndex === null) {
333
+ failed = true;
334
+ console.error("FAIL docs/decisions/index.md is missing; run `npm run gen:decisions-index`");
335
+ } else if (actualIndex !== expectedIndex) {
336
+ failed = true;
337
+ console.error("FAIL docs/decisions/index.md is stale; run `npm run gen:decisions-index`");
338
+ }
339
+
340
+ if (failed) {
341
+ console.error("Decision registry check failed.");
342
+ return 1;
343
+ }
344
+ console.log(`Decision registry check passed (${topics.length} topic${topics.length === 1 ? "" : "s"}).`);
345
+ return 0;
346
+ }
347
+
348
+ function main() {
349
+ const mode = process.argv[2] || "check";
350
+ if (mode === "check") return process.exit(cmdCheck());
351
+ if (mode === "gen-index") return process.exit(cmdGenIndex());
352
+ console.error(`Unknown mode '${mode}'. Usage: check-decisions.cjs [check|gen-index]`);
353
+ return process.exit(2);
354
+ }
355
+
356
+ main();
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ // derive-claim-status.mjs — WS8 (AC1/finding-3) CI-side status re-derivation helper.
3
+ //
4
+ // The trust reconciler MUST NOT trust a bundle's self-reported `claim.status`. This helper
5
+ // re-derives each claim's status from the bundle's OWN evidence/events/policies using
6
+ // @kontourai/surface's canonical `deriveClaimStatus` — the exact function the producer used
7
+ // — so the reconciler can detect a status-misassertion (asserted status != derived status).
8
+ //
9
+ // It is a separate ESM module because @kontourai/surface is ESM-only while
10
+ // scripts/ci/trust-reconcile.js is CommonJS with a synchronous entrypoint; the reconciler
11
+ // invokes this via spawnSync and parses the JSON on stdout. Surface is resolved from THIS
12
+ // file's location (the reconciler's own node_modules), NOT the target repo-root, so an
13
+ // adopter repo without Surface still gets CI-side re-derivation from the anchor's copy.
14
+ //
15
+ // Usage: node derive-claim-status.mjs <bundle-path>
16
+ // Output (stdout): {"claimId": "<TrustStatus>", ...} — value is null if that claim threw.
17
+ // Exit: 0 on success (Surface loaded); 2 if Surface is unavailable; 3 on bad input.
18
+
19
+ import { readFileSync } from "node:fs";
20
+
21
+ async function main() {
22
+ const bundlePath = process.argv[2];
23
+ if (!bundlePath) { process.stderr.write("derive-claim-status: bundle path argument required\n"); return 3; }
24
+
25
+ let surface;
26
+ try {
27
+ surface = await import("@kontourai/surface");
28
+ } catch (err) {
29
+ process.stderr.write(`derive-claim-status: @kontourai/surface unavailable: ${err && err.message ? err.message : String(err)}\n`);
30
+ return 2;
31
+ }
32
+ const { deriveClaimStatus } = surface;
33
+ if (typeof deriveClaimStatus !== "function") {
34
+ process.stderr.write("derive-claim-status: @kontourai/surface deriveClaimStatus missing\n");
35
+ return 2;
36
+ }
37
+
38
+ let bundle;
39
+ try {
40
+ bundle = JSON.parse(readFileSync(bundlePath, "utf8"));
41
+ } catch (err) {
42
+ process.stderr.write(`derive-claim-status: cannot read bundle: ${err && err.message ? err.message : String(err)}\n`);
43
+ return 3;
44
+ }
45
+
46
+ const claims = Array.isArray(bundle.claims) ? bundle.claims : [];
47
+ const allEvidence = Array.isArray(bundle.evidence) ? bundle.evidence : [];
48
+ const allEvents = Array.isArray(bundle.events) ? bundle.events : [];
49
+ const allPolicies = Array.isArray(bundle.policies) ? bundle.policies : [];
50
+
51
+ const out = {};
52
+ for (const claim of claims) {
53
+ if (!claim || !claim.id) continue;
54
+ // Filter evidence by claimId (deriveTrustStatus does NOT filter evidence internally — it
55
+ // only filters events — so passing the whole array would let another claim's evidence
56
+ // type/blocking-failure bleed in). Events are filtered internally by claimId; policies are
57
+ // resolved per-claim via verificationPolicyId. This mirrors the producer's per-claim call.
58
+ const evidence = allEvidence.filter((e) => e && e.claimId === claim.id);
59
+ try {
60
+ const { status } = deriveClaimStatus({ claim, evidence, events: allEvents, policies: allPolicies });
61
+ out[claim.id] = status;
62
+ } catch {
63
+ out[claim.id] = null;
64
+ }
65
+ }
66
+
67
+ process.stdout.write(JSON.stringify(out));
68
+ return 0;
69
+ }
70
+
71
+ main().then((code) => process.exit(code)).catch((err) => {
72
+ process.stderr.write(`derive-claim-status: ${err && err.message ? err.message : String(err)}\n`);
73
+ process.exit(2);
74
+ });