@kontourai/flow-agents 1.4.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.github/CODEOWNERS +29 -0
  2. package/.github/actions/trust-verify/action.yml +145 -0
  3. package/.github/workflows/ci.yml +11 -4
  4. package/.github/workflows/kit-gates-demo.yml +2 -2
  5. package/.github/workflows/publish-npm.yml +10 -2
  6. package/.github/workflows/release-please.yml +1 -1
  7. package/.github/workflows/runtime-compat.yml +1 -1
  8. package/.github/workflows/trust-reconcile.yml +113 -0
  9. package/AGENTS.md +13 -0
  10. package/CHANGELOG.md +103 -0
  11. package/CONTRIBUTING.md +4 -4
  12. package/README.md +1 -0
  13. package/agents/tool-planner.json +1 -1
  14. package/build/src/cli/init.js +242 -20
  15. package/build/src/cli/validate-workflow-artifacts.js +19 -2
  16. package/build/src/cli/verify.d.ts +1 -0
  17. package/build/src/cli/verify.js +90 -0
  18. package/build/src/cli/workflow-sidecar.d.ts +316 -8
  19. package/build/src/cli/workflow-sidecar.js +1996 -91
  20. package/build/src/cli.js +2 -3
  21. package/build/src/lib/flow-resolver.d.ts +111 -0
  22. package/build/src/lib/flow-resolver.js +308 -0
  23. package/build/src/tools/build-universal-bundles.js +34 -22
  24. package/build/src/tools/generate-context-map.js +3 -16
  25. package/build/src/tools/validate-source-tree.d.ts +1 -1
  26. package/build/src/tools/validate-source-tree.js +42 -162
  27. package/context/contracts/artifact-contract.md +10 -0
  28. package/context/contracts/delivery-contract.md +1 -0
  29. package/context/contracts/review-contract.md +1 -0
  30. package/context/contracts/verification-contract.md +2 -0
  31. package/context/gate-awareness.md +39 -0
  32. package/context/scripts/hooks/stop-goal-fit.js +632 -70
  33. package/docs/adr/0001-flow-agents-consumes-flow.md +1 -1
  34. package/docs/adr/0002-flow-kits-as-extension-unit.md +1 -1
  35. package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
  36. package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
  37. package/docs/adr/0007-skill-audit.md +1 -1
  38. package/docs/adr/0009-canonical-hook-core-kit-boundary.md +95 -0
  39. package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +139 -0
  40. package/docs/adr/0011-mcp-posture.md +100 -0
  41. package/docs/adr/0012-agent-coordination-as-liveness-claims.md +119 -0
  42. package/docs/adr/0013-context-lifecycle.md +151 -0
  43. package/docs/adr/0014-core-vs-domain-kit-boundary.md +143 -0
  44. package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +120 -0
  45. package/docs/adr/0016-three-hard-boundary-model.md +71 -0
  46. package/docs/adr/0017-anti-gaming-trust-security-model.md +155 -0
  47. package/docs/agent-system-guidebook.md +5 -12
  48. package/docs/context-map.md +4 -10
  49. package/docs/index.md +3 -2
  50. package/docs/integrations/framework-adapter.md +19 -6
  51. package/docs/integrations/index.md +2 -2
  52. package/docs/north-star.md +4 -4
  53. package/docs/operating-layers.md +3 -3
  54. package/docs/plans/adr-0010-phase2-gate-recompute.md +55 -0
  55. package/docs/repository-structure.md +2 -2
  56. package/docs/skills-map.md +1 -0
  57. package/docs/spec/runtime-hook-surface.md +62 -9
  58. package/docs/standards-register.md +3 -3
  59. package/docs/survey-utterance-check.md +1 -1
  60. package/docs/trust-anchor-adoption.md +197 -0
  61. package/docs/verifiable-trust.md +95 -0
  62. package/docs/veritas-integration.md +2 -2
  63. package/docs/workflow-usage-guide.md +69 -0
  64. package/evals/acceptance/DEMO-false-completion.md +144 -0
  65. package/evals/acceptance/demo-cast.sh +92 -0
  66. package/evals/acceptance/demo-false-completion.sh +72 -0
  67. package/evals/acceptance/demo-real-evidence.sh +104 -0
  68. package/evals/acceptance/demo.tape +29 -0
  69. package/evals/acceptance/prove-capture-teeth-declared.sh +335 -0
  70. package/evals/acceptance/prove-capture-teeth.sh +114 -0
  71. package/evals/acceptance/prove-teeth.sh +105 -0
  72. package/evals/ci/antigaming-suite.sh +55 -0
  73. package/evals/ci/run-baseline.sh +2 -0
  74. package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/flows/review.flow.json +26 -0
  75. package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/kit.json +20 -0
  76. package/evals/fixtures/flow-kit-repository/valid-unknown-extension/flows/review.flow.json +26 -0
  77. package/evals/fixtures/flow-kit-repository/valid-unknown-extension/kit.json +18 -0
  78. package/evals/integration/test_builder_step_producers.sh +379 -0
  79. package/evals/integration/test_bundle_install.sh +35 -71
  80. package/evals/integration/test_bundle_lifecycle.sh +39 -2
  81. package/evals/integration/test_captured_fail_reconciliation.sh +820 -0
  82. package/evals/integration/test_checkpoint_signing.sh +489 -0
  83. package/evals/integration/test_claim_lookup.sh +352 -0
  84. package/evals/integration/test_command_log_fork_classification.sh +134 -0
  85. package/evals/integration/test_command_log_integrity.sh +275 -0
  86. package/evals/integration/test_context_map.sh +0 -2
  87. package/evals/integration/test_dual_emit_flow_step.sh +278 -0
  88. package/evals/integration/test_enforcer_expects_driven.sh +281 -0
  89. package/evals/integration/test_evidence_capture_hook.sh +185 -0
  90. package/evals/integration/test_flow_kit_repository.sh +2 -0
  91. package/evals/integration/test_flowdef_session_activation.sh +273 -0
  92. package/evals/integration/test_flowdef_session_history_preservation.sh +250 -0
  93. package/evals/integration/test_gate_bypass_chain.sh +448 -0
  94. package/evals/integration/test_gate_lockdown.sh +1137 -0
  95. package/evals/integration/test_gate_review_inquiry_records.sh +399 -0
  96. package/evals/integration/test_goal_fit_escape_hatch.sh +73 -0
  97. package/evals/integration/test_goal_fit_hook.sh +69 -4
  98. package/evals/integration/test_goal_fit_rederive.sh +263 -0
  99. package/evals/integration/test_install_merge.sh +1176 -0
  100. package/evals/integration/test_kit_identity_trust.sh +393 -0
  101. package/evals/integration/test_mint_attestation.sh +373 -0
  102. package/evals/integration/test_phase_map_and_gate_claim.sh +365 -0
  103. package/evals/integration/test_publish_delivery.sh +269 -0
  104. package/evals/integration/test_reconcile_soundness.sh +528 -0
  105. package/evals/integration/test_resolvefirststep_security.sh +208 -0
  106. package/evals/integration/test_session_resume_roundtrip.sh +286 -0
  107. package/evals/integration/test_trust_checkpoint.sh +325 -0
  108. package/evals/integration/test_trust_reconcile.sh +293 -0
  109. package/evals/integration/test_verify_cli.sh +208 -0
  110. package/evals/integration/test_workflow_sidecar_writer.sh +549 -34
  111. package/evals/lib/node.sh +0 -6
  112. package/evals/run.sh +47 -0
  113. package/evals/static/test_workflow_skills.sh +6 -13
  114. package/install.sh +0 -7
  115. package/integrations/strands-ts/README.md +25 -15
  116. package/integrations/veritas/flow-agents.adapter.json +1 -2
  117. package/kits/builder/flows/build.flow.json +59 -12
  118. package/kits/builder/kit.json +85 -15
  119. package/kits/builder/skills/continue-work/SKILL.md +116 -0
  120. package/kits/builder/skills/deliver/SKILL.md +36 -6
  121. package/kits/builder/skills/design-probe/SKILL.md +28 -0
  122. package/kits/builder/skills/execute-plan/SKILL.md +9 -1
  123. package/kits/builder/skills/gate-review/SKILL.md +234 -0
  124. package/kits/builder/skills/learning-review/SKILL.md +30 -0
  125. package/kits/builder/skills/pickup-probe/SKILL.md +29 -0
  126. package/kits/builder/skills/plan-work/SKILL.md +13 -1
  127. package/kits/builder/skills/pull-work/SKILL.md +19 -0
  128. package/kits/knowledge/adapters/default-store/index.js +38 -0
  129. package/kits/knowledge/adapters/flow-runner/index.js +1620 -0
  130. package/kits/knowledge/adapters/obsidian-store/index.js +36 -6
  131. package/kits/knowledge/docs/store-contract.md +314 -0
  132. package/kits/knowledge/evals/audit-freshness/suite.test.js +368 -0
  133. package/kits/knowledge/evals/canonicalize-category/suite.test.js +383 -0
  134. package/kits/knowledge/evals/contract-suite/suite.test.js +111 -0
  135. package/kits/knowledge/evals/detect-contradictions/suite.test.js +324 -0
  136. package/kits/knowledge/evals/entities/suite.test.js +40 -0
  137. package/kits/knowledge/evals/glossary-sync/suite.test.js +416 -0
  138. package/kits/knowledge/evals/hygiene-review/suite.test.js +396 -0
  139. package/kits/knowledge/evals/retirement/suite.test.js +145 -0
  140. package/kits/knowledge/flows/audit-freshness.flow.json +44 -0
  141. package/kits/knowledge/flows/canonicalize-category.flow.json +44 -0
  142. package/kits/knowledge/flows/detect-contradictions.flow.json +44 -0
  143. package/kits/knowledge/flows/glossary-sync.flow.json +61 -0
  144. package/kits/knowledge/flows/hygiene-review.flow.json +43 -0
  145. package/kits/knowledge/kit.json +51 -1
  146. package/package.json +6 -6
  147. package/packaging/conformance/README.md +10 -2
  148. package/packaging/conformance/fixtures/evidence-capture--allow-records-command.json +29 -0
  149. package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +29 -0
  150. package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +30 -0
  151. package/packaging/conformance/fixtures/stop-goal-fit--block-mode.json +23 -0
  152. package/packaging/conformance/fixtures/stop-goal-fit--off-mode.json +24 -0
  153. package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +5 -2
  154. package/packaging/conformance/fixtures/stop-goal-fit--warn-no-bundle.json +23 -0
  155. package/packaging/conformance/fixtures/workflow-steering--reground-active-prompt.json +30 -0
  156. package/packaging/conformance/fixtures/workflow-steering--reground-session-start.json +30 -0
  157. package/packaging/conformance/run-conformance.js +1 -1
  158. package/scripts/README.md +2 -1
  159. package/scripts/build-universal-bundles.js +0 -1
  160. package/scripts/ci/mint-attestation.js +221 -0
  161. package/scripts/ci/trust-reconcile.js +545 -0
  162. package/scripts/hooks/config-protection.js +423 -1
  163. package/scripts/hooks/evidence-capture.js +348 -0
  164. package/scripts/hooks/lib/liveness-read.js +113 -0
  165. package/scripts/hooks/run-hook.js +6 -1
  166. package/scripts/hooks/stop-goal-fit.js +1524 -79
  167. package/scripts/hooks/workflow-steering.js +135 -5
  168. package/scripts/install-codex-home.sh +39 -0
  169. package/scripts/install-merge.js +330 -0
  170. package/scripts/repair-command-log.js +115 -0
  171. package/src/cli/init.ts +218 -20
  172. package/src/cli/validate-workflow-artifacts.ts +18 -2
  173. package/src/cli/verify.ts +100 -0
  174. package/src/cli/workflow-sidecar.ts +2127 -84
  175. package/src/cli.ts +2 -3
  176. package/src/lib/flow-resolver.ts +369 -0
  177. package/src/tools/build-universal-bundles.ts +34 -21
  178. package/src/tools/generate-context-map.ts +3 -17
  179. package/src/tools/validate-source-tree.ts +44 -104
  180. package/build/src/tools/filter-installed-packs.d.ts +0 -2
  181. package/build/src/tools/filter-installed-packs.js +0 -135
  182. package/packaging/packs.json +0 -49
  183. package/scripts/filter-installed-packs.js +0 -2
  184. package/src/tools/filter-installed-packs.ts +0 -132
@@ -0,0 +1,545 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * trust-reconcile.js — CI trust anchor (Phase 1).
4
+ *
5
+ * EXTERNAL anti-gaming anchor. Runs in a clean CI environment the agent does not
6
+ * control. Enforces:
7
+ *
8
+ * Step 1 Re-run canonical verification FRESH. Real exit codes from CI. Authoritative.
9
+ * Step 2 If a delivered bundle is present, RECONCILE: for every evidence item
10
+ * asserting a command PASSED (evidence.passing normalized to boolean + claims
11
+ * asserting pass), look up CI's fresh result for that command.
12
+ * a. Claimed-pass + laundering operator (|| ..., ; true, ; exit 0, etc.) → DIVERGENCE
13
+ * b. Claimed-pass + CI FAIL → DIVERGENCE
14
+ * c. Claimed-pass + CI never ran the command → DIVERGENCE
15
+ * d. workflow.check.command claim with no evidence (never captured) → DIVERGENCE
16
+ * e. checkpoint-only bundle (no evidence/claims) → DIVERGENCE (full bundle required)
17
+ *
18
+ * Exit codes:
19
+ * 0 — fresh verify passed AND no divergence (or no bundle present)
20
+ * 1 — fresh verify failed OR any divergence detected OR compile-only fallback
21
+ *
22
+ * Fail-open on bundle absence: if no bundle is provided (and none auto-discovered at
23
+ * delivery/trust.bundle or delivery/trust.checkpoint.json), only the fresh verify is
24
+ * enforced. Fail-closed on divergence: any claimed-pass command CI cannot confirm blocks.
25
+ * Fail-closed on compile-only: if no comprehensive verify is configured, exits 1.
26
+ *
27
+ * Inputs (CLI args take precedence over env):
28
+ * --bundle <path> Delivered trust.bundle (JSON) path. May also be a
29
+ * trust.checkpoint.json for lightweight checkpoint-level checks.
30
+ * --commands <cmd,...> Canonical verify commands. Comma-separated.
31
+ * May be specified multiple times (each appended).
32
+ * --repo-root <path> Repository root. Default: TRUST_RECONCILE_REPO_ROOT or cwd.
33
+ *
34
+ * Environment fallbacks:
35
+ * TRUST_RECONCILE_BUNDLE Path to delivered bundle (same as --bundle).
36
+ * TRUST_RECONCILE_COMMANDS Comma- or newline-separated canonical commands.
37
+ * TRUST_RECONCILE_REPO_ROOT Repository root.
38
+ *
39
+ * Auto-discovery (when no --bundle or TRUST_RECONCILE_BUNDLE is set):
40
+ * Checks delivery/trust.bundle, then delivery/trust.checkpoint.json under repo root.
41
+ * If neither exists, continues fail-open (fresh verify only).
42
+ *
43
+ * Canonical commands resolution (fail-closed on compile-only):
44
+ * Priority: CLI --commands > TRUST_RECONCILE_COMMANDS env > package.json
45
+ * scripts["trust-reconcile-verify"]. If NONE of those is configured, exits 1 with
46
+ * "no comprehensive trust-reconcile-verify configured" — refuses to attest a
47
+ * compile-only check.
48
+ *
49
+ * NOTE: This job is intended to be a REQUIRED status check in GitHub branch
50
+ * protection (making it the un-disablable CI anchor). Enabling it as required is
51
+ * a server-side branch-protection step — see .github/workflows/trust-reconcile.yml.
52
+ *
53
+ * Programmatic use:
54
+ * const { runTrustReconcile } = require('./trust-reconcile.js');
55
+ * const exitCode = runTrustReconcile({ bundle, commands, repoRoot });
56
+ */
57
+
58
+ 'use strict';
59
+
60
+ const { spawnSync } = require('child_process');
61
+ const fs = require('fs');
62
+ const os = require('os');
63
+ const path = require('path');
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Helpers
67
+ // ---------------------------------------------------------------------------
68
+
69
+ /** Normalize a command string: collapse whitespace, trim. */
70
+ function normalizeCmd(cmd) {
71
+ return String(cmd || '').replace(/\s+/g, ' ').trim();
72
+ }
73
+
74
+ /**
75
+ * Normalize ev.passing to a boolean.
76
+ * Treats true / 1 / "true" / "pass" as passing.
77
+ * Prevents a claim from dodging reconciliation via a non-boolean value.
78
+ */
79
+ function isPassingValue(v) {
80
+ return v === true || v === 1 || v === 'true' || v === 'pass';
81
+ }
82
+
83
+ /**
84
+ * Returns true when a command string contains an exit-code-laundering operator.
85
+ * These operators mask real exit codes so the real sub-command may have failed silently.
86
+ *
87
+ * Rules (applied to claimed verification commands only):
88
+ * - ANY || operator — verify commands must not contain ||. This catches:
89
+ * || exit 0, || echo ok, || /bin/true, || true, || :, etc.
90
+ * - ; or newline followed by true / : / exit 0 — trailing success injection
91
+ *
92
+ * NOTE: Logic must stay identical to scripts/hooks/stop-goal-fit.js hasLaunderingOperator.
93
+ * Centralize into a shared module as a follow-up (coordinate-free duplication for now).
94
+ */
95
+ function hasLaunderingOperator(cmd) {
96
+ // Flag ANY || operator — masks the exit code of the left-hand command.
97
+ if (/\|\|/.test(cmd)) return true;
98
+ // Flag ; or newline followed by true / : / exit 0
99
+ if (/[;\n]\s*true\b/.test(cmd)) return true;
100
+ if (/[;\n]\s*:\s*(?:$|\s|;|\n)/.test(cmd)) return true;
101
+ if (/[;\n]\s*exit\s+0\b/.test(cmd)) return true;
102
+ // Flag pipe to true (pipeline absorbs exit code)
103
+ if (/\|\s*true\b/.test(cmd)) return true;
104
+ return false;
105
+ }
106
+
107
+ /**
108
+ * Run a single shell command under bash, capturing exit code.
109
+ * @returns {{ cmd, exitCode, passed, stdout, stderr }}
110
+ */
111
+ function runCommand(cmd, repoRoot) {
112
+ const result = spawnSync('bash', ['-c', cmd], {
113
+ cwd: repoRoot,
114
+ encoding: 'utf8',
115
+ timeout: 180000,
116
+ killSignal: 'SIGKILL',
117
+ stdio: ['ignore', 'pipe', 'pipe'],
118
+ });
119
+ const exitCode = (result.status !== null && result.status !== undefined)
120
+ ? result.status
121
+ : 1;
122
+ return {
123
+ cmd,
124
+ exitCode,
125
+ passed: exitCode === 0 && !result.error,
126
+ stdout: result.stdout || '',
127
+ stderr: result.stderr || '',
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Parse the trust.bundle and extract every evidence item asserting a command PASSED.
133
+ * Returns an array of { cmd, claimId, evId, claimType, noEvidence? } objects.
134
+ *
135
+ * Source of truth: evidence[].execution.label is the command string recorded at
136
+ * capture time. evidence[].passing (normalized) means the agent claimed this passed.
137
+ *
138
+ * Also collects workflow.check.command claims (or any claim with value "pass") that
139
+ * have no matching evidence item — these represent never-captured claimed passes
140
+ * and will produce a not-run divergence (Part-B logic mirroring stop-goal-fit.js).
141
+ */
142
+ function getClaimedPassCommands(bundle) {
143
+ const evidence = Array.isArray(bundle.evidence) ? bundle.evidence : [];
144
+ const claims = Array.isArray(bundle.claims) ? bundle.claims : [];
145
+
146
+ // Build a map from claimId -> claim for fast lookup
147
+ const claimById = new Map();
148
+ for (const c of claims) {
149
+ if (c && c.id) claimById.set(c.id, c);
150
+ }
151
+
152
+ const commands = [];
153
+ const seen = new Set();
154
+
155
+ // (A) Evidence items with execution.label and a passing value
156
+ for (const ev of evidence) {
157
+ if (!ev || !ev.execution || !ev.execution.label) continue;
158
+ if (!isPassingValue(ev.passing)) continue; // only claimed-pass items
159
+
160
+ const cmd = normalizeCmd(ev.execution.label);
161
+ if (!cmd || seen.has(cmd)) continue;
162
+ seen.add(cmd);
163
+
164
+ const claim = claimById.get(ev.claimId);
165
+ const claimType = claim ? String(claim.claimType || '') : '';
166
+
167
+ commands.push({ cmd, claimId: ev.claimId, evId: ev.id, claimType });
168
+ }
169
+
170
+ // (B) Claims with claimType workflow.check.command (or any claim asserting pass)
171
+ // that have NO matching evidence item with execution.label — never-captured claimed passes.
172
+ const evidenceClaimIds = new Set(
173
+ evidence
174
+ .filter(ev => ev && ev.claimId && ev.execution && ev.execution.label)
175
+ .map(ev => ev.claimId)
176
+ );
177
+
178
+ for (const c of claims) {
179
+ if (!c || !c.id || typeof c.claimType !== 'string') continue;
180
+ // Require claimType === workflow.check.command OR any claim asserting pass with no evidence
181
+ const isCommandClaim = c.claimType === 'workflow.check.command';
182
+ const assertsPass = isPassingValue(c.value) || c.status === 'verified';
183
+ if (!isCommandClaim && !assertsPass) continue;
184
+ if (evidenceClaimIds.has(c.id)) continue; // already covered in (A)
185
+
186
+ // Use fieldOrBehavior or value as command identifier (may be non-executable — that's ok,
187
+ // the key is that no evidence captured this pass claim, so emit not-run divergence).
188
+ const rawCmd = c.fieldOrBehavior || c.value || '';
189
+ const cmd = normalizeCmd(rawCmd);
190
+ const synthetic = cmd || `[claim:${c.id}:${c.claimType}]`;
191
+ if (seen.has(synthetic)) continue;
192
+ seen.add(synthetic);
193
+
194
+ commands.push({
195
+ cmd: synthetic,
196
+ claimId: c.id,
197
+ evId: null,
198
+ claimType: c.claimType,
199
+ noEvidence: true,
200
+ });
201
+ }
202
+
203
+ return commands;
204
+ }
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Argument + config parsing
208
+ // ---------------------------------------------------------------------------
209
+
210
+ function parseArgs(argv) {
211
+ const args = { bundle: null, commands: [], repoRoot: null };
212
+ for (let i = 0; i < argv.length; i++) {
213
+ const arg = argv[i];
214
+ const next = argv[i + 1];
215
+ if (arg === '--bundle' && next) {
216
+ args.bundle = next; i++;
217
+ } else if (arg === '--commands' && next) {
218
+ args.commands.push(...next.split(',').map(c => c.trim()).filter(Boolean));
219
+ i++;
220
+ } else if (arg === '--repo-root' && next) {
221
+ args.repoRoot = next; i++;
222
+ }
223
+ }
224
+ return args;
225
+ }
226
+
227
+ /**
228
+ * Resolve the list of canonical verify commands.
229
+ * Priority: CLI --commands > TRUST_RECONCILE_COMMANDS env > package.json scripts key.
230
+ *
231
+ * FAIL-CLOSED: if none of those is configured (only the bare "npm run build" default
232
+ * would apply), returns null — main() must exit 1 with a diagnostic message.
233
+ * A compile-only check is NOT sufficient for attestation.
234
+ */
235
+ function resolveCanonicalCommands(args, repoRoot) {
236
+ if (args.commands.length > 0) return args.commands;
237
+
238
+ const envCmds = process.env.TRUST_RECONCILE_COMMANDS;
239
+ if (envCmds) {
240
+ const parsed = envCmds.split(/[,\n]/).map(c => c.trim()).filter(Boolean);
241
+ if (parsed.length > 0) return parsed;
242
+ }
243
+
244
+ // Check package.json for a "trust-reconcile-verify" scripts key
245
+ try {
246
+ const pkgPath = path.join(repoRoot, 'package.json');
247
+ if (fs.existsSync(pkgPath)) {
248
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
249
+ const customKey = pkg && pkg.scripts && pkg.scripts['trust-reconcile-verify'];
250
+ if (customKey && typeof customKey === 'string') {
251
+ return ['npm run trust-reconcile-verify'];
252
+ }
253
+ }
254
+ } catch { /* ignore */ }
255
+
256
+ // FAIL CLOSED: no comprehensive verify configured.
257
+ // Returning null signals to main() to exit 1 — refuse compile-only attestation.
258
+ return null;
259
+ }
260
+
261
+ /**
262
+ * Auto-discover the delivery bundle when no explicit path is provided.
263
+ * Checks delivery/trust.bundle, then delivery/trust.checkpoint.json under repo root.
264
+ * Returns null if neither is present (fail-open on bundle absence).
265
+ */
266
+ function discoverBundle(repoRoot) {
267
+ const candidates = [
268
+ path.join(repoRoot, 'delivery', 'trust.bundle'),
269
+ path.join(repoRoot, 'delivery', 'trust.checkpoint.json'),
270
+ ];
271
+ for (const candidate of candidates) {
272
+ if (fs.existsSync(candidate)) return candidate;
273
+ }
274
+ return null;
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // Core reconcile function (exported for programmatic use)
279
+ // ---------------------------------------------------------------------------
280
+
281
+ /**
282
+ * Run the full trust reconcile logic and return an exit code.
283
+ *
284
+ * This is the same logic as the CLI entrypoint, extracted so it can be called
285
+ * programmatically (e.g. from the `flow-agents verify` CLI subcommand).
286
+ * All output is written directly to process.stdout/stderr.
287
+ *
288
+ * @param {object} opts
289
+ * @param {string|null} [opts.bundle] - Explicit bundle path. null = env fallback + auto-discovery.
290
+ * @param {string[]} [opts.commands] - Canonical verify commands. [] = env + package.json fallback.
291
+ * @param {string|null} [opts.repoRoot] - Repo root path. null = TRUST_RECONCILE_REPO_ROOT env or cwd.
292
+ * @returns {number} Exit code: 0 = pass, 1 = fail/divergence.
293
+ */
294
+ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {}) {
295
+ const resolvedRepoRoot = path.resolve(
296
+ repoRoot || process.env.TRUST_RECONCILE_REPO_ROOT || process.cwd()
297
+ );
298
+
299
+ // Resolve bundle path: explicit arg > env > auto-discovery > null (fail-open)
300
+ const bundlePath = bundle
301
+ || process.env.TRUST_RECONCILE_BUNDLE
302
+ || discoverBundle(resolvedRepoRoot)
303
+ || null;
304
+
305
+ const canonicalCommands = resolveCanonicalCommands({ commands: commands || [] }, resolvedRepoRoot);
306
+
307
+ // FAIL-CLOSED: no comprehensive verify configured — refuse compile-only attestation.
308
+ if (canonicalCommands === null) {
309
+ process.stderr.write(
310
+ '[trust-reconcile] FAILED — no comprehensive trust-reconcile-verify configured.\n' +
311
+ '[trust-reconcile] Refusing to attest a compile-only check.\n' +
312
+ '[trust-reconcile] Declare package.json scripts["trust-reconcile-verify"] or set TRUST_RECONCILE_COMMANDS.\n' +
313
+ '[trust-reconcile] Example: add "trust-reconcile-verify": "npm run build && npm run eval:static && npm run eval:integration"\n'
314
+ );
315
+ return 1;
316
+ }
317
+
318
+ process.stdout.write('[trust-reconcile] starting CI trust anchor reconcile\n');
319
+ process.stdout.write(`[trust-reconcile] repo-root: ${resolvedRepoRoot}\n`);
320
+ process.stdout.write(`[trust-reconcile] canonical commands: ${canonicalCommands.join(' | ')}\n`);
321
+ if (bundlePath) {
322
+ process.stdout.write(`[trust-reconcile] bundle: ${bundlePath}\n`);
323
+ } else {
324
+ process.stdout.write('[trust-reconcile] no bundle present — fresh verify only (fail-open on bundle absence)\n');
325
+ }
326
+
327
+ // -------------------------------------------------------------------------
328
+ // Step 1: re-run canonical verification FRESH (authoritative CI truth)
329
+ // -------------------------------------------------------------------------
330
+ process.stdout.write('\n[trust-reconcile] Step 1: re-running canonical verification fresh...\n');
331
+
332
+ // The canonical verify is the anchor's own truth source — it must not be
333
+ // exit-code-laundered (e.g. `npm run build || true`). If it is, the fresh run
334
+ // would report PASS regardless of the real result. Fail closed.
335
+ // (Residual: a wrapper script that exits 0 without `||` still evades — covered
336
+ // by the anti-gaming suite running in a required lane + CODEOWNERS on the verify
337
+ // config; noted honestly.)
338
+ for (const cmd of canonicalCommands) {
339
+ if (hasLaunderingOperator(cmd)) {
340
+ process.stderr.write(`[trust-reconcile] FAILED — canonical verify command is laundered ('${cmd}') — refusing to attest a result whose exit code is masked.\n`);
341
+ return 1;
342
+ }
343
+ }
344
+
345
+ /** @type {Map<string, {exitCode: number, passed: boolean}>} */
346
+ const ciResults = new Map();
347
+
348
+ for (const cmd of canonicalCommands) {
349
+ process.stdout.write(`[trust-reconcile] running: ${cmd}\n`);
350
+ const result = runCommand(cmd, resolvedRepoRoot);
351
+ const key = normalizeCmd(cmd);
352
+ ciResults.set(key, { exitCode: result.exitCode, passed: result.passed });
353
+
354
+ if (result.passed) {
355
+ process.stdout.write(`[trust-reconcile] PASS: ${cmd}\n`);
356
+ } else {
357
+ process.stderr.write(`[trust-reconcile] FAIL: ${cmd} (exit ${result.exitCode})\n`);
358
+ if (result.stderr) {
359
+ const lines = result.stderr.trim().split('\n');
360
+ const tail = lines.slice(-5).join('\n');
361
+ process.stderr.write(`[trust-reconcile] --- stderr tail ---\n${tail}\n[trust-reconcile] ---\n`);
362
+ }
363
+ }
364
+ }
365
+
366
+ // -------------------------------------------------------------------------
367
+ // Collect issues (Step 1 failures + Step 2 divergences)
368
+ // -------------------------------------------------------------------------
369
+ /** @type {Array<{type: string, cmd?: string, exitCode?: number, message: string}>} */
370
+ const issues = [];
371
+
372
+ // Step 1 failures
373
+ for (const [cmd, result] of ciResults) {
374
+ if (!result.passed) {
375
+ issues.push({
376
+ type: 'fresh-fail',
377
+ cmd,
378
+ exitCode: result.exitCode,
379
+ message: `verification failed in CI: '${cmd}' exited ${result.exitCode}`,
380
+ });
381
+ }
382
+ }
383
+
384
+ // -------------------------------------------------------------------------
385
+ // Step 2: reconcile against delivered bundle (if present)
386
+ // -------------------------------------------------------------------------
387
+ if (bundlePath) {
388
+ process.stdout.write('\n[trust-reconcile] Step 2: reconciling claimed-pass commands against CI fresh results...\n');
389
+
390
+ let bundle;
391
+ try {
392
+ bundle = JSON.parse(fs.readFileSync(bundlePath, 'utf8'));
393
+ } catch (err) {
394
+ process.stderr.write(`[trust-reconcile] failed to read bundle at ${bundlePath}: ${err.message}\n`);
395
+ return 1;
396
+ }
397
+
398
+ const hasEvidence = Array.isArray(bundle.evidence) && bundle.evidence.length > 0;
399
+ const hasClaims = Array.isArray(bundle.claims) && bundle.claims.length > 0;
400
+
401
+ if (!hasEvidence && !hasClaims) {
402
+ // Checkpoint-only bundle: no evidence/claims → DIVERGENCE.
403
+ // Cannot perform per-command reconcile without evidence. A full trust.bundle is
404
+ // required. (Auto-discovery always prefers delivery/trust.bundle over
405
+ // delivery/trust.checkpoint.json — this fires only when a checkpoint was
406
+ // explicitly provided or no full bundle exists alongside it.)
407
+ process.stderr.write('[trust-reconcile] checkpoint-only bundle detected: no evidence[] or claims[]\n');
408
+ issues.push({
409
+ type: 'checkpoint-bypass',
410
+ message: 'trust divergence: checkpoint-only bundle cannot be reconciled per-command; full trust.bundle required',
411
+ });
412
+
413
+ // Still check checkpoint statusByClaimId for explicit failures (belt-and-suspenders)
414
+ const statusByClaimId = bundle.checkpoint && bundle.checkpoint.statusByClaimId;
415
+ if (statusByClaimId && typeof statusByClaimId === 'object') {
416
+ for (const [claimId, status] of Object.entries(statusByClaimId)) {
417
+ if (status === 'failed' || status === 'rejected' || status === 'disputed') {
418
+ issues.push({
419
+ type: 'checkpoint-fail',
420
+ message: `trust divergence: checkpoint shows claim '${claimId}' with status '${status}'`,
421
+ });
422
+ }
423
+ }
424
+ }
425
+ } else {
426
+ // Full bundle: extract claimed-pass commands from evidence items + claims
427
+ const claimedPasses = getClaimedPassCommands(bundle);
428
+ process.stdout.write(`[trust-reconcile] found ${claimedPasses.length} claimed-pass command(s) in bundle\n`);
429
+
430
+ for (const { cmd, claimId, claimType, noEvidence } of claimedPasses) {
431
+ const normalCmd = normalizeCmd(cmd);
432
+
433
+ // (d) Claims with no evidence are an immediate not-run divergence.
434
+ // A pass was claimed but never captured — CI cannot confirm it.
435
+ if (noEvidence) {
436
+ issues.push({
437
+ type: 'not-run',
438
+ cmd,
439
+ message: `trust divergence: claim '${claimId}' (claimType: ${claimType}) asserts pass but has no supporting evidence item — command never captured`,
440
+ });
441
+ continue;
442
+ }
443
+
444
+ // (a) Laundering operator check — must come first (most specific signal)
445
+ if (hasLaunderingOperator(cmd)) {
446
+ issues.push({
447
+ type: 'laundering',
448
+ cmd,
449
+ message: `trust divergence: agent claimed '${cmd}' passed; command contains exit-code-laundering operator (|| ... / ; true / ; exit 0 / etc.)`,
450
+ });
451
+ continue;
452
+ }
453
+
454
+ // (b/c) Look up in CI's fresh run results
455
+ const ciResult = ciResults.get(normalCmd);
456
+
457
+ if (!ciResult) {
458
+ // (c) CI never ran this claimed-pass command — fail-closed
459
+ issues.push({
460
+ type: 'not-run',
461
+ cmd,
462
+ message: `trust divergence: agent claimed '${cmd}' passed; CI did not run this command (not in canonical verify set)`,
463
+ });
464
+ continue;
465
+ }
466
+
467
+ if (!ciResult.passed) {
468
+ // (b) Claimed pass + CI fresh FAIL = divergence
469
+ issues.push({
470
+ type: 'divergence',
471
+ cmd,
472
+ exitCode: ciResult.exitCode,
473
+ message: `trust divergence: agent claimed '${cmd}' passed; CI fresh run = FAIL (exit ${ciResult.exitCode})`,
474
+ });
475
+ continue;
476
+ }
477
+
478
+ // Clean: claimed pass, CI confirms pass
479
+ process.stdout.write(`[trust-reconcile] RECONCILED: '${cmd}' — claimed pass, CI fresh run = PASS\n`);
480
+ }
481
+ }
482
+ }
483
+
484
+ // -------------------------------------------------------------------------
485
+ // Report and exit
486
+ // -------------------------------------------------------------------------
487
+ if (issues.length === 0) {
488
+ process.stdout.write('\n[trust-reconcile] exit 0: fresh verify passed');
489
+ if (bundlePath) process.stdout.write(', all claimed-pass commands reconciled clean');
490
+ process.stdout.write('\n');
491
+
492
+ // Write CI results file for mint-attestation.js to consume.
493
+ // Path mirrors what mint-attestation.js reads: RUNNER_TEMP/ci-trust-reconcile-results.json
494
+ // (or os.tmpdir() locally). Failure is non-fatal — mint-attestation synthesizes if missing.
495
+ const resultsDir = process.env.RUNNER_TEMP || os.tmpdir();
496
+ const resultsFilePath = path.join(resultsDir, 'ci-trust-reconcile-results.json');
497
+ const ciResultsArr = Array.from(ciResults.entries()).map(([cmd, r]) => ({
498
+ command: cmd,
499
+ exitCode: r.exitCode,
500
+ passed: r.passed,
501
+ }));
502
+ const resultsPayload = {
503
+ commit_sha: process.env.GITHUB_SHA || 'unknown',
504
+ canonical_commands: ciResultsArr,
505
+ reconciled: bundlePath !== null,
506
+ built_at: new Date().toISOString(),
507
+ };
508
+ try {
509
+ fs.writeFileSync(resultsFilePath, JSON.stringify(resultsPayload, null, 2));
510
+ process.stdout.write(`[trust-reconcile] results written: ${resultsFilePath}\n`);
511
+ } catch (err) {
512
+ // Non-fatal: mint-attestation will synthesize from env if this file is absent.
513
+ process.stdout.write(`[trust-reconcile] results write skipped (${err.message})\n`);
514
+ }
515
+
516
+ return 0;
517
+ }
518
+
519
+ process.stderr.write(`\n[trust-reconcile] FAILED — ${issues.length} issue(s) detected:\n`);
520
+ for (const issue of issues) {
521
+ process.stderr.write(` [${issue.type}] ${issue.message}\n`);
522
+ }
523
+ return 1;
524
+ }
525
+
526
+ // ---------------------------------------------------------------------------
527
+ // CLI entrypoint (direct script invocation)
528
+ // ---------------------------------------------------------------------------
529
+
530
+ function main() {
531
+ const args = parseArgs(process.argv.slice(2));
532
+ process.exit(runTrustReconcile({
533
+ bundle: args.bundle || null,
534
+ commands: args.commands,
535
+ repoRoot: args.repoRoot || null,
536
+ }));
537
+ }
538
+
539
+ // Export core function for programmatic use (e.g. flow-agents verify CLI subcommand).
540
+ // The direct-CLI behavior is preserved: when run as a script, main() is called below.
541
+ module.exports.runTrustReconcile = runTrustReconcile;
542
+
543
+ if (require.main === module) {
544
+ main();
545
+ }