@kontourai/flow-agents 2.2.0 → 2.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 (250) hide show
  1. package/.github/CODEOWNERS +8 -0
  2. package/.github/workflows/ci.yml +32 -0
  3. package/AGENTS.md +4 -4
  4. package/CHANGELOG.md +46 -0
  5. package/CONTEXT.md +14 -0
  6. package/README.md +9 -3
  7. package/agents/dev.json +1 -1
  8. package/agents/tool-code-reviewer.json +1 -1
  9. package/agents/tool-planner.json +3 -3
  10. package/agents/tool-verifier.json +3 -3
  11. package/build/src/cli/console-learning-projection.js +3 -2
  12. package/build/src/cli/init.js +104 -21
  13. package/build/src/cli/kit.js +37 -6
  14. package/build/src/cli/pull-work-provider.js +1 -1
  15. package/build/src/cli/usage-feedback.js +3 -3
  16. package/build/src/cli/validate-hook-influence.js +1 -0
  17. package/build/src/cli/validate-workflow-artifacts.js +22 -6
  18. package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
  19. package/build/src/cli/workflow-sidecar.d.ts +14 -7
  20. package/build/src/cli/workflow-sidecar.js +792 -69
  21. package/build/src/flow-kit/validate.d.ts +23 -0
  22. package/build/src/flow-kit/validate.js +67 -1
  23. package/build/src/index.d.ts +1 -1
  24. package/build/src/index.js +1 -1
  25. package/build/src/lib/flow-resolver.d.ts +6 -4
  26. package/build/src/lib/flow-resolver.js +125 -58
  27. package/build/src/lib/local-artifact-root.d.ts +14 -2
  28. package/build/src/lib/local-artifact-root.js +22 -5
  29. package/build/src/lib/workflow-learning-projection.js +2 -2
  30. package/build/src/runtime-adapters.d.ts +12 -0
  31. package/build/src/runtime-adapters.js +56 -16
  32. package/build/src/tools/build-universal-bundles.js +49 -16
  33. package/build/src/tools/generate-context-map.js +10 -8
  34. package/build/src/tools/validate-source-tree.js +23 -1
  35. package/console.telemetry.json +22 -0
  36. package/context/contracts/artifact-contract.md +38 -7
  37. package/context/contracts/builder-kit-workflow-state-contract.md +3 -2
  38. package/context/contracts/decision-registry-contract.md +263 -0
  39. package/context/contracts/delivery-contract.md +1 -1
  40. package/context/contracts/planning-contract.md +2 -1
  41. package/context/contracts/review-contract.md +5 -3
  42. package/context/contracts/verification-contract.md +9 -1
  43. package/context/contracts/work-item-contract.md +1 -1
  44. package/context/deferred/parallelization.md +1 -1
  45. package/context/gate-awareness.md +4 -4
  46. package/context/scripts/git-status.sh +2 -2
  47. package/context/scripts/hooks/config-protection.js +502 -2
  48. package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
  49. package/context/scripts/hooks/run-hook.js +14 -1
  50. package/context/scripts/hooks/stop-goal-fit.js +1140 -99
  51. package/context/scripts/hooks/workflow-steering.js +212 -6
  52. package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
  53. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
  54. package/docs/adr/0019-kit-dependency-ownership.md +62 -0
  55. package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
  56. package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
  57. package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +283 -0
  58. package/docs/agent-system-guidebook.md +3 -3
  59. package/docs/agent-usage-feedback-loop.md +6 -6
  60. package/docs/context-map.md +9 -7
  61. package/docs/decisions/decision-records.md +57 -0
  62. package/docs/decisions/index.md +14 -0
  63. package/docs/decisions/promotion-gate.md +52 -0
  64. package/docs/developer-architecture.md +1 -1
  65. package/docs/fixture-ownership.md +3 -0
  66. package/docs/flow-kit-repository-contract.md +5 -5
  67. package/docs/getting-started.md +12 -2
  68. package/docs/integrations/harness-install.md +2 -1
  69. package/docs/kit-authoring-guide.md +64 -5
  70. package/docs/knowledge-kit.md +5 -1
  71. package/docs/learnings/2026-07-improvement-program.md +203 -0
  72. package/docs/migrations.md +6 -5
  73. package/docs/north-star.md +2 -2
  74. package/docs/operating-layers.md +4 -3
  75. package/docs/repository-structure.md +5 -5
  76. package/docs/skills-map.md +8 -8
  77. package/docs/spec/runtime-hook-surface.md +19 -15
  78. package/docs/spikes/graph-provider-2026-07.md +155 -0
  79. package/docs/standards-register.md +7 -7
  80. package/docs/trust-anchor-adoption.md +105 -4
  81. package/docs/work-item-adapters.md +1 -1
  82. package/docs/workflow-artifact-lifecycle.md +72 -9
  83. package/docs/workflow-eval-strategy.md +3 -3
  84. package/docs/workflow-shared-contracts.md +1 -1
  85. package/docs/workflow-usage-guide.md +254 -21
  86. package/evals/acceptance/DEMO-false-completion.md +20 -11
  87. package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
  88. package/evals/acceptance/prove-capture-teeth.sh +11 -11
  89. package/evals/acceptance/test_kiro_harness.sh +1 -1
  90. package/evals/ci/antigaming-suite.sh +5 -0
  91. package/evals/ci/run-baseline.sh +65 -1
  92. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
  93. package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
  94. package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
  95. package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
  96. package/evals/fixtures/hook-influence/cases.json +32 -0
  97. package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
  98. package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
  99. package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
  100. package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
  101. package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
  102. package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
  103. package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
  104. package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
  105. package/evals/integration/test_actor_identity.sh +254 -0
  106. package/evals/integration/test_builder_step_producers.sh +33 -23
  107. package/evals/integration/test_bundle_install.sh +22 -16
  108. package/evals/integration/test_bundle_lifecycle.sh +11 -11
  109. package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
  110. package/evals/integration/test_checkpoint_signing.sh +43 -0
  111. package/evals/integration/test_claim_lookup.sh +4 -4
  112. package/evals/integration/test_codex_hook_resolution.sh +114 -0
  113. package/evals/integration/test_command_log_concurrency.sh +8 -8
  114. package/evals/integration/test_command_log_fork_classification.sh +3 -3
  115. package/evals/integration/test_command_log_integrity.sh +16 -16
  116. package/evals/integration/test_console_learning_projection.sh +1 -1
  117. package/evals/integration/test_dual_emit_flow_step.sh +145 -0
  118. package/evals/integration/test_enforcer_expects_driven.sh +12 -12
  119. package/evals/integration/test_evidence_capture_hook.sh +14 -13
  120. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  121. package/evals/integration/test_flow_agents_statusline.sh +2 -2
  122. package/evals/integration/test_flow_kit_install_git.sh +52 -0
  123. package/evals/integration/test_flow_kit_repository.sh +2 -0
  124. package/evals/integration/test_flowdef_session_activation.sh +6 -6
  125. package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
  126. package/evals/integration/test_gate_bypass_chain.sh +52 -40
  127. package/evals/integration/test_gate_lockdown.sh +97 -73
  128. package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
  129. package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
  130. package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
  131. package/evals/integration/test_goal_fit_hook.sh +32 -32
  132. package/evals/integration/test_goal_fit_rederive.sh +9 -9
  133. package/evals/integration/test_hook_category_behaviors.sh +103 -0
  134. package/evals/integration/test_hook_influence_cases.sh +1 -0
  135. package/evals/integration/test_install_merge.sh +262 -1
  136. package/evals/integration/test_kit_identity_trust.sh +6 -6
  137. package/evals/integration/test_liveness_conflict_injection.sh +587 -0
  138. package/evals/integration/test_liveness_heartbeat.sh +677 -0
  139. package/evals/integration/test_liveness_verdict.sh +394 -0
  140. package/evals/integration/test_local_flow_kit_install.sh +15 -0
  141. package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
  142. package/evals/integration/test_promote_gate.sh +178 -0
  143. package/evals/integration/test_publish_delivery.sh +2 -2
  144. package/evals/integration/test_pull_work_liveness_preflight.sh +386 -0
  145. package/evals/integration/test_pull_work_provider.sh +2 -2
  146. package/evals/integration/test_reconcile_soundness.sh +4 -4
  147. package/evals/integration/test_resolvefirststep_security.sh +22 -0
  148. package/evals/integration/test_runtime_adapter_activation.sh +87 -18
  149. package/evals/integration/test_session_resume_roundtrip.sh +147 -0
  150. package/evals/integration/test_sidecar_field_preservation.sh +249 -0
  151. package/evals/integration/test_trust_reconcile.sh +2 -2
  152. package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
  153. package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
  154. package/evals/integration/test_trust_reconcile_negatives.sh +151 -0
  155. package/evals/integration/test_usage_feedback_global.sh +5 -5
  156. package/evals/integration/test_validate_artifacts_portability.sh +60 -0
  157. package/evals/integration/test_verify_cli.sh +2 -2
  158. package/evals/integration/test_veritas_governance_kit.sh +117 -0
  159. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
  160. package/evals/integration/test_workflow_artifacts.sh +33 -0
  161. package/evals/integration/test_workflow_sidecar_writer.sh +723 -74
  162. package/evals/integration/test_workflow_steering_hook.sh +175 -5
  163. package/evals/lib/codex-provider.sh +1 -1
  164. package/evals/lib/node.sh +19 -1
  165. package/evals/run.sh +22 -0
  166. package/evals/static/test_decisions.sh +150 -0
  167. package/evals/static/test_flowdef_codeowners_coverage.sh +56 -0
  168. package/evals/static/test_package.sh +19 -10
  169. package/evals/static/test_universal_bundles.sh +48 -3
  170. package/evals/static/test_workflow_skills.sh +41 -8
  171. package/kits/builder/flows/build.flow.json +3 -74
  172. package/kits/builder/flows/publish-learn.flow.json +90 -0
  173. package/kits/builder/kit.json +11 -0
  174. package/kits/builder/skills/builder-shape/SKILL.md +1 -1
  175. package/kits/builder/skills/deliver/SKILL.md +13 -11
  176. package/kits/builder/skills/design-probe/SKILL.md +4 -4
  177. package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
  178. package/kits/builder/skills/execute-plan/SKILL.md +2 -2
  179. package/kits/builder/skills/fix-bug/SKILL.md +2 -0
  180. package/kits/builder/skills/gate-review/SKILL.md +6 -6
  181. package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
  182. package/kits/builder/skills/learning-review/SKILL.md +7 -7
  183. package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
  184. package/kits/builder/skills/plan-work/SKILL.md +27 -5
  185. package/kits/builder/skills/pull-work/SKILL.md +80 -5
  186. package/kits/builder/skills/release-readiness/SKILL.md +4 -4
  187. package/kits/builder/skills/review-work/SKILL.md +24 -1
  188. package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
  189. package/kits/builder/skills/verify-work/SKILL.md +8 -1
  190. package/kits/catalog.json +6 -0
  191. package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
  192. package/kits/veritas-governance/docs/README.md +75 -0
  193. package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
  194. package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
  195. package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
  196. package/kits/veritas-governance/kit.json +16 -0
  197. package/package.json +8 -4
  198. package/packaging/README.md +2 -2
  199. package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
  200. package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
  201. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
  202. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
  203. package/packaging/manifest.json +9 -10
  204. package/schemas/decision-record.schema.json +109 -0
  205. package/schemas/workflow-evidence.schema.json +1 -1
  206. package/schemas/workflow-state.schema.json +5 -0
  207. package/scripts/README.md +8 -3
  208. package/scripts/check-content-boundary.cjs +1 -1
  209. package/scripts/check-decisions.cjs +356 -0
  210. package/scripts/ci/derive-claim-status.mjs +74 -0
  211. package/scripts/ci/trust-reconcile.js +436 -79
  212. package/scripts/git-status.sh +2 -2
  213. package/scripts/hooks/claude-telemetry-hook.js +37 -3
  214. package/scripts/hooks/codex-telemetry-hook.js +36 -2
  215. package/scripts/hooks/config-protection.js +102 -30
  216. package/scripts/hooks/evidence-capture.js +3 -3
  217. package/scripts/hooks/lib/actor-identity.js +368 -0
  218. package/scripts/hooks/lib/config-protection-remedies.js +69 -0
  219. package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
  220. package/scripts/hooks/lib/liveness-policy.js +127 -0
  221. package/scripts/hooks/lib/liveness-read.js +105 -12
  222. package/scripts/hooks/lib/liveness-write.js +46 -0
  223. package/scripts/hooks/lib/local-artifact-paths.js +12 -6
  224. package/scripts/hooks/opencode-telemetry-hook.js +27 -1
  225. package/scripts/hooks/pi-telemetry-hook.js +27 -1
  226. package/scripts/hooks/stop-goal-fit.js +157 -26
  227. package/scripts/hooks/workflow-steering.js +77 -3
  228. package/scripts/install-codex-home.sh +138 -38
  229. package/scripts/statusline/flow-agents-statusline.js +2 -2
  230. package/src/cli/console-learning-projection.ts +3 -2
  231. package/src/cli/init.ts +101 -21
  232. package/src/cli/kit.ts +37 -6
  233. package/src/cli/public-api.test.mjs +58 -6
  234. package/src/cli/pull-work-provider.ts +1 -1
  235. package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
  236. package/src/cli/usage-feedback.ts +3 -3
  237. package/src/cli/validate-hook-influence.ts +1 -0
  238. package/src/cli/validate-workflow-artifacts.ts +22 -6
  239. package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
  240. package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
  241. package/src/cli/workflow-sidecar.ts +795 -71
  242. package/src/flow-kit/validate.ts +74 -1
  243. package/src/index.ts +6 -2
  244. package/src/lib/flow-resolver.ts +123 -55
  245. package/src/lib/local-artifact-root.ts +24 -5
  246. package/src/lib/workflow-learning-projection.ts +2 -2
  247. package/src/runtime-adapters.ts +61 -15
  248. package/src/tools/build-universal-bundles.ts +49 -16
  249. package/src/tools/generate-context-map.ts +10 -8
  250. package/src/tools/validate-source-tree.ts +23 -1
@@ -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
+ });