@kontourai/flow-agents 2.3.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 +39 -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
@@ -32,9 +32,12 @@
32
32
  * --repo-root <path> Repository root. Default: TRUST_RECONCILE_REPO_ROOT or cwd.
33
33
  *
34
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.
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
+ * TRUST_RECONCILE_COMMAND_TIMEOUT_MS Timeout (ms) for each canonical/manifest command
39
+ * run (Step 1 fresh-verify + on-demand manifest
40
+ * reconcile). Default: 600000 (10 min).
38
41
  *
39
42
  * Auto-discovery (when no --bundle or TRUST_RECONCILE_BUNDLE is set):
40
43
  * Checks delivery/trust.bundle, then delivery/trust.checkpoint.json under repo root.
@@ -88,103 +91,218 @@ function isPassingValue(v) {
88
91
  // CI reconciler and the stop-goal-fit verifier apply the identical exit-code-mask
89
92
  // heuristic — see that module for the rules.
90
93
 
94
+ /**
95
+ * Default manifest/canonical-command execution timeout (ms). Overridable via
96
+ * TRUST_RECONCILE_COMMAND_TIMEOUT_MS. 10 minutes is comfortably above the slowest
97
+ * required-lane check (this repo's own CI lanes allow up to ~10 min); the prior
98
+ * hardcoded 180000ms (3 min) killed legitimately slow evals (e.g.
99
+ * evals/integration/test_workflow_sidecar_writer.sh, ~150s on a fast Mac, >180s on
100
+ * GitHub Actions runners) mid-run, producing a spurious divergence rather than a real
101
+ * failure.
102
+ */
103
+ const DEFAULT_COMMAND_TIMEOUT_MS = 600000;
104
+
105
+ /** Resolve the manifest/canonical-command timeout: TRUST_RECONCILE_COMMAND_TIMEOUT_MS env, else default. */
106
+ function resolveCommandTimeoutMs() {
107
+ const raw = process.env.TRUST_RECONCILE_COMMAND_TIMEOUT_MS;
108
+ if (raw !== undefined && raw !== '') {
109
+ const parsed = Number(raw);
110
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
111
+ }
112
+ return DEFAULT_COMMAND_TIMEOUT_MS;
113
+ }
114
+
91
115
  /**
92
116
  * Run a single shell command under bash, capturing exit code.
93
- * @returns {{ cmd, exitCode, passed, stdout, stderr }}
117
+ * @returns {{ cmd, exitCode, passed, timedOut, timeoutMs, stdout, stderr }}
94
118
  */
95
119
  function runCommand(cmd, repoRoot) {
120
+ const timeoutMs = resolveCommandTimeoutMs();
96
121
  const result = spawnSync('bash', ['-c', cmd], {
97
122
  cwd: repoRoot,
98
123
  encoding: 'utf8',
99
- timeout: 180000,
124
+ timeout: timeoutMs,
100
125
  killSignal: 'SIGKILL',
101
126
  stdio: ['ignore', 'pipe', 'pipe'],
102
127
  });
103
128
  const exitCode = (result.status !== null && result.status !== undefined)
104
129
  ? result.status
105
130
  : 1;
131
+ // spawnSync sets result.error (ETIMEDOUT) when the timeout kills the process — this is
132
+ // NOT a real command failure and must be distinguishable in the FAIL log line so a
133
+ // future reader chases the timeout config, not a phantom test bug.
134
+ const timedOut = !!(result.error && result.error.code === 'ETIMEDOUT');
106
135
  return {
107
136
  cmd,
108
137
  exitCode,
109
138
  passed: exitCode === 0 && !result.error,
139
+ timedOut,
140
+ timeoutMs,
110
141
  stdout: result.stdout || '',
111
142
  stderr: result.stderr || '',
112
143
  };
113
144
  }
114
145
 
115
146
  /**
116
- * Parse the trust.bundle and extract every evidence item asserting a command PASSED.
117
- * Returns an array of { cmd, claimId, evId, claimType, noEvidence? } objects.
147
+ * Classify a trust.bundle's claims into: reconcilable command claims (test_output +
148
+ * execution.label), session-local claims (attestation/observation/citation), never-captured
149
+ * or unbacked command claims (not-run divergence), and command-backed claims carrying a
150
+ * waiver (waiver-on-command divergence). Returns
151
+ * { reconcilable, sessionLocal, noEvidenceCommand, waiverOnCommand }.
118
152
  *
119
- * Source of truth: evidence[].execution.label is the command string recorded at
120
- * capture time. evidence[].passing (normalized) means the agent claimed this passed.
153
+ * Source of truth: evidence[].execution.label is the command string recorded at capture time.
154
+ * evidence[].passing (normalized) means the agent claimed this passed. `claim.status` is NOT
155
+ * trusted here — the caller re-derives it CI-side (see derive-claim-status.mjs / finding-3).
121
156
  *
122
- * Also collects workflow.check.command claims (or any claim with value "pass") that
123
- * have no matching evidence item these represent never-captured claimed passes
124
- * and will produce a not-run divergence (Part-B logic mirroring stop-goal-fit.js).
157
+ * WS8 iteration-2 hardening:
158
+ * - finding 1: ANY pass-asserting claim whose evidence is `evidenceType: test_output`
159
+ * (Surface's default when unset) but which did NOT reconcile i.e. it has no
160
+ * manifest-matchable execution.label — is a divergence, NOT session-local. A test_output
161
+ * claim either reconciles against the manifest or is a divergence; it is never accepted on
162
+ * self-reported status. (Previously only the literal claimType `workflow.check.command`
163
+ * was guarded, so a fabricated kind:"test" claim with no command slipped through.)
164
+ * - finding 4: a command-backed (test_output) claim carrying a waiver is a divergence — a
165
+ * command-backed check reconciles against CI or fails; it cannot be waived.
125
166
  */
126
- function getClaimedPassCommands(bundle) {
167
+ function classifyBundleClaims(bundle) {
127
168
  const evidence = Array.isArray(bundle.evidence) ? bundle.evidence : [];
128
169
  const claims = Array.isArray(bundle.claims) ? bundle.claims : [];
129
170
 
130
- // Build a map from claimId -> claim for fast lookup
131
171
  const claimById = new Map();
172
+ for (const c of claims) if (c && c.id) claimById.set(c.id, c);
173
+
174
+ // Evidence indexing. A missing evidenceType defaults to test_output for backward
175
+ // compatibility with pre-classification bundles (same default classifyEvidence uses).
176
+ const claimHasLabeledTestOutput = new Set(); // test_output evidence WITH an execution.label
177
+ const claimHasTestOutputEvidence = new Set(); // ANY test_output evidence (label or not)
178
+ // WS8 iteration-4 (converged finding): the session-local (non-test_output) evidenceType per
179
+ // claim, so the reconciler can name it on the loud ATTESTED marker below — a fabricated
180
+ // human_attestation/attestation/external claim with no --command is otherwise
181
+ // indistinguishable, in the reconciler's own output, from a genuinely re-runnable check.
182
+ const claimEvidenceType = new Map();
183
+ for (const ev of evidence) {
184
+ if (!ev || !ev.claimId) continue;
185
+ const evType = ev.evidenceType || 'test_output';
186
+ if (evType !== 'test_output') {
187
+ if (!claimEvidenceType.has(ev.claimId)) claimEvidenceType.set(ev.claimId, evType);
188
+ continue;
189
+ }
190
+ claimHasTestOutputEvidence.add(ev.claimId);
191
+ if (ev.execution && ev.execution.label) claimHasLabeledTestOutput.add(ev.claimId);
192
+ }
193
+
194
+ // finding 4: a command-backed (test_output-evidence) claim that also carries a waiver.
195
+ const waiverOnCommand = [];
132
196
  for (const c of claims) {
133
- if (c && c.id) claimById.set(c.id, c);
197
+ if (!c || !c.id) continue;
198
+ const waiver = (c.metadata && typeof c.metadata === 'object') ? c.metadata.waiver : undefined;
199
+ if (waiver && typeof waiver === 'object' && claimHasTestOutputEvidence.has(c.id)) {
200
+ waiverOnCommand.push({ claimId: c.id, claimType: String(c.claimType || ''), subject: c.subjectId || c.fieldOrBehavior || c.id });
201
+ }
134
202
  }
135
203
 
136
- const commands = [];
204
+ // (A) Reconcilable claimed-passes: evidence items that are test_output (CI-reconcilable),
205
+ // carry an execution.label, and assert pass. Session-local evidenceTypes
206
+ // (crawl_observation, human_attestation, attestation, policy_rule, source_excerpt,
207
+ // document_citation, calculation_trace) are NOT reconciled per-command — they are handled
208
+ // by the session-local/waiver path below.
209
+ const reconcilable = [];
210
+ const reconcilableClaimIds = new Set();
137
211
  const seen = new Set();
138
-
139
- // (A) Evidence items with execution.label and a passing value
140
212
  for (const ev of evidence) {
141
213
  if (!ev || !ev.execution || !ev.execution.label) continue;
142
- if (!isPassingValue(ev.passing)) continue; // only claimed-pass items
143
-
214
+ if (!isPassingValue(ev.passing)) continue;
215
+ const evType = ev.evidenceType || 'test_output';
216
+ if (evType !== 'test_output') continue; // session-local — not CI-reconcilable
144
217
  const cmd = normalizeCmd(ev.execution.label);
145
- if (!cmd || seen.has(cmd)) continue;
218
+ if (!cmd) continue;
219
+ reconcilableClaimIds.add(ev.claimId);
220
+ if (seen.has(cmd)) continue;
146
221
  seen.add(cmd);
147
-
148
222
  const claim = claimById.get(ev.claimId);
149
- const claimType = claim ? String(claim.claimType || '') : '';
150
-
151
- commands.push({ cmd, claimId: ev.claimId, evId: ev.id, claimType });
223
+ reconcilable.push({ cmd, claimId: ev.claimId, evId: ev.id, claimType: claim ? String(claim.claimType || '') : '' });
152
224
  }
153
225
 
154
- // (B) Claims with claimType workflow.check.command (or any claim asserting pass)
155
- // that have NO matching evidence item with execution.label — never-captured claimed passes.
156
- const evidenceClaimIds = new Set(
157
- evidence
158
- .filter(ev => ev && ev.claimId && ev.execution && ev.execution.label)
159
- .map(ev => ev.claimId)
160
- );
161
-
226
+ // (B) Session-local claims, never-captured command claims, and unreconciled test_output.
227
+ const sessionLocal = [];
228
+ const noEvidenceCommand = [];
229
+ const seenClaims = new Set();
162
230
  for (const c of claims) {
163
231
  if (!c || !c.id || typeof c.claimType !== 'string') continue;
164
- // Require claimType === workflow.check.command OR any claim asserting pass with no evidence
165
- const isCommandClaim = c.claimType === 'workflow.check.command';
166
- const assertsPass = isPassingValue(c.value) || c.status === 'verified';
167
- if (!isCommandClaim && !assertsPass) continue;
168
- if (evidenceClaimIds.has(c.id)) continue; // already covered in (A)
169
-
170
- // Use fieldOrBehavior or value as command identifier (may be non-executable — that's ok,
171
- // the key is that no evidence captured this pass claim, so emit not-run divergence).
172
- const rawCmd = c.fieldOrBehavior || c.value || '';
173
- const cmd = normalizeCmd(rawCmd);
174
- const synthetic = cmd || `[claim:${c.id}:${c.claimType}]`;
175
- if (seen.has(synthetic)) continue;
176
- seen.add(synthetic);
177
-
178
- commands.push({
179
- cmd: synthetic,
232
+ if (reconcilableClaimIds.has(c.id)) continue; // handled by (A)
233
+ if (seenClaims.has(c.id)) continue;
234
+ const status = String(c.status || '');
235
+ const assertsPass = isPassingValue(c.value) || status === 'verified' || status === 'assumed';
236
+ const isFailing = status === 'disputed' || status === 'rejected';
237
+ if (!assertsPass && !isFailing) continue; // pending/unknown non-asserting — ignore (as before)
238
+ seenClaims.add(c.id);
239
+
240
+ // finding 1: a pass-asserting claim backed by test_output evidence that did NOT reconcile
241
+ // (it has test_output evidence but no manifest-matchable execution.label — otherwise it
242
+ // would be in bucket A) is a not-run divergence. A test_output claim reconciles against the
243
+ // manifest or it is a divergence — it is NEVER accepted as session-local on self-report.
244
+ if (assertsPass && claimHasTestOutputEvidence.has(c.id)) {
245
+ const rawCmd = normalizeCmd(c.fieldOrBehavior || c.value || '');
246
+ noEvidenceCommand.push({ cmd: rawCmd || `[claim:${c.id}]`, claimId: c.id, claimType: c.claimType, reason: 'test_output-unreconciled' });
247
+ continue;
248
+ }
249
+
250
+ // A workflow.check.command claim with no captured (labeled) evidence is a never-captured
251
+ // claimed pass — not-run divergence (anti-gaming teeth preserved).
252
+ if (assertsPass && c.claimType === 'workflow.check.command' && !claimHasLabeledTestOutput.has(c.id)) {
253
+ const rawCmd = normalizeCmd(c.fieldOrBehavior || c.value || '');
254
+ noEvidenceCommand.push({ cmd: rawCmd || `[claim:${c.id}:${c.claimType}]`, claimId: c.id, claimType: c.claimType, reason: 'no-evidence-command' });
255
+ continue;
256
+ }
257
+
258
+ const waiver = (c.metadata && typeof c.metadata === 'object') ? c.metadata.waiver : undefined;
259
+ sessionLocal.push({
180
260
  claimId: c.id,
181
- evId: null,
182
261
  claimType: c.claimType,
183
- noEvidence: true,
262
+ assertedStatus: status,
263
+ value: c.value,
264
+ waiver: (waiver && typeof waiver === 'object') ? waiver : null,
265
+ subject: c.subjectId || c.fieldOrBehavior || c.id,
266
+ evidenceType: claimEvidenceType.get(c.id) || 'unknown',
184
267
  });
185
268
  }
186
269
 
187
- return commands;
270
+ return { reconcilable, sessionLocal, noEvidenceCommand, waiverOnCommand };
271
+ }
272
+
273
+ /**
274
+ * WS8 (finding 3): re-derive every claim's status CI-side from the bundle's OWN
275
+ * evidence/events/policies, using @kontourai/surface's canonical deriveClaimStatus (the same
276
+ * function the producer used). The reconciler MUST NOT trust a bundle's self-reported
277
+ * `claim.status`; it compares the asserted status to this re-derived status and treats a
278
+ * mismatch as a `status-misassertion` divergence.
279
+ *
280
+ * Runs the ESM helper scripts/ci/derive-claim-status.mjs via spawnSync (Surface is ESM-only;
281
+ * this reconciler is CJS with a synchronous entrypoint). Surface is resolved from the helper's
282
+ * own location (the reconciler's node_modules), so it works even when reconciling an adopter
283
+ * repo that does not itself depend on Surface.
284
+ *
285
+ * @returns {Map<string,string|null>|null} claimId → derived status; null if re-derivation is
286
+ * unavailable (Surface could not load / helper failed to run). A per-claim null value means
287
+ * that specific claim could not be derived.
288
+ */
289
+ function deriveClaimStatuses(bundlePath, repoRoot) {
290
+ const helper = path.join(__dirname, 'derive-claim-status.mjs');
291
+ if (!fs.existsSync(helper)) return null;
292
+ const res = spawnSync('node', [helper, bundlePath], { cwd: repoRoot, encoding: 'utf8', timeout: 60000 });
293
+ if (res.status !== 0 || !res.stdout) {
294
+ if (res.stderr) process.stderr.write(`[trust-reconcile] status re-derivation unavailable: ${res.stderr.trim()}
295
+ `);
296
+ return null;
297
+ }
298
+ try {
299
+ const obj = JSON.parse(res.stdout);
300
+ const m = new Map();
301
+ for (const [k, v] of Object.entries(obj)) m.set(k, v);
302
+ return m;
303
+ } catch {
304
+ return null;
305
+ }
188
306
  }
189
307
 
190
308
  // ---------------------------------------------------------------------------
@@ -192,7 +310,7 @@ function getClaimedPassCommands(bundle) {
192
310
  // ---------------------------------------------------------------------------
193
311
 
194
312
  function parseArgs(argv) {
195
- const args = { bundle: null, commands: [], repoRoot: null };
313
+ const args = { bundle: null, commands: [], repoRoot: null, manifest: null };
196
314
  for (let i = 0; i < argv.length; i++) {
197
315
  const arg = argv[i];
198
316
  const next = argv[i + 1];
@@ -203,11 +321,104 @@ function parseArgs(argv) {
203
321
  i++;
204
322
  } else if (arg === '--repo-root' && next) {
205
323
  args.repoRoot = next; i++;
324
+ } else if (arg === '--manifest' && next) {
325
+ args.manifest = next; i++;
206
326
  }
207
327
  }
208
328
  return args;
209
329
  }
210
330
 
331
+ // ---------------------------------------------------------------------------
332
+ // WS8 (ADR 0020): reconcile manifest resolution + slugification.
333
+ // The manifest is a list of named {id, command} entries the reconciler treats as
334
+ // individually CI-reconcilable. It is resolved (priority order) from:
335
+ // 1. CLI --manifest <json>
336
+ // 2. env TRUST_RECONCILE_MANIFEST <json>
337
+ // 3. package.json "trust-reconcile-manifest" (inline array, OR a string command
338
+ // that emits the JSON — this repo points it at run-baseline.sh --manifest-json)
339
+ // 4. evals/ci/run-baseline.sh --manifest-json (this repo's live LANE_* registry —
340
+ // every entry runs in a required ci.yml lane by construction)
341
+ // 5. legacy fallback: the resolved fresh-verify commands as a manifest of size N
342
+ // (so the historical single-command behavior is a strict subset — backward compat).
343
+ // ---------------------------------------------------------------------------
344
+
345
+ /** Slugify a label the same way evals/ci/run-baseline.sh's slugify() does. */
346
+ function slugifyLabel(s) {
347
+ return String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
348
+ }
349
+
350
+ /** Normalize a raw manifest array (of strings or {id, command}) to {id, command}[]. */
351
+ function normalizeManifestEntries(raw) {
352
+ if (!Array.isArray(raw)) return null;
353
+ const out = [];
354
+ for (const item of raw) {
355
+ if (typeof item === 'string') {
356
+ const cmd = normalizeCmd(item);
357
+ if (cmd) out.push({ id: slugifyLabel(cmd), command: cmd });
358
+ } else if (item && typeof item === 'object' && typeof item.command === 'string') {
359
+ const cmd = normalizeCmd(item.command);
360
+ if (cmd) out.push({ id: item.id ? String(item.id) : slugifyLabel(cmd), command: cmd });
361
+ }
362
+ }
363
+ return out.length ? out : null;
364
+ }
365
+
366
+ function parseManifestJson(text, label) {
367
+ try {
368
+ return normalizeManifestEntries(JSON.parse(text));
369
+ } catch {
370
+ process.stderr.write(`[trust-reconcile] ignoring ${label}: not a valid JSON array of {id, command} entries\n`);
371
+ return null;
372
+ }
373
+ }
374
+
375
+ function runBaselineManifest(repoRoot) {
376
+ const script = path.join(repoRoot, 'evals', 'ci', 'run-baseline.sh');
377
+ if (!fs.existsSync(script)) return null;
378
+ const res = spawnSync('bash', [script, '--manifest-json'], { cwd: repoRoot, encoding: 'utf8', timeout: 30000 });
379
+ if (res.status !== 0 || !res.stdout) return null;
380
+ return parseManifestJson(res.stdout, 'evals/ci/run-baseline.sh --manifest-json');
381
+ }
382
+
383
+ /**
384
+ * Resolve the reconcile manifest. Returns { entries: {id, command}[], source }.
385
+ * canonicalCommands is used only for the legacy size-N fallback (tier 5).
386
+ */
387
+ function resolveManifest(args, repoRoot, canonicalCommands) {
388
+ if (args.manifest) {
389
+ const m = parseManifestJson(args.manifest, '--manifest');
390
+ if (m) return { entries: m, source: 'cli:--manifest' };
391
+ }
392
+ if (process.env.TRUST_RECONCILE_MANIFEST) {
393
+ const m = parseManifestJson(process.env.TRUST_RECONCILE_MANIFEST, 'TRUST_RECONCILE_MANIFEST');
394
+ if (m) return { entries: m, source: 'env:TRUST_RECONCILE_MANIFEST' };
395
+ }
396
+ try {
397
+ const pkgPath = path.join(repoRoot, 'package.json');
398
+ if (fs.existsSync(pkgPath)) {
399
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
400
+ const raw = (pkg && pkg['trust-reconcile-manifest'])
401
+ || (pkg && pkg.scripts && pkg.scripts['trust-reconcile-manifest']);
402
+ if (Array.isArray(raw)) {
403
+ const m = normalizeManifestEntries(raw);
404
+ if (m) return { entries: m, source: 'package.json:trust-reconcile-manifest (inline array)' };
405
+ } else if (typeof raw === 'string' && raw.trim()) {
406
+ const res = spawnSync('bash', ['-c', raw], { cwd: repoRoot, encoding: 'utf8', timeout: 60000 });
407
+ if (res.status === 0 && res.stdout) {
408
+ const m = parseManifestJson(res.stdout, 'package.json trust-reconcile-manifest emit');
409
+ if (m) return { entries: m, source: `package.json:trust-reconcile-manifest (${raw})` };
410
+ }
411
+ }
412
+ }
413
+ } catch { /* ignore */ }
414
+ const rb = runBaselineManifest(repoRoot);
415
+ if (rb) return { entries: rb, source: 'evals/ci/run-baseline.sh' };
416
+ const legacy = (canonicalCommands || [])
417
+ .map((c) => ({ id: slugifyLabel(normalizeCmd(c)), command: normalizeCmd(c) }))
418
+ .filter((e) => e.command);
419
+ return { entries: legacy, source: 'legacy:fresh-verify-commands' };
420
+ }
421
+
211
422
  /**
212
423
  * Resolve the list of canonical verify commands.
213
424
  * Priority: CLI --commands > TRUST_RECONCILE_COMMANDS env > package.json scripts key.
@@ -275,7 +486,7 @@ function discoverBundle(repoRoot) {
275
486
  * @param {string|null} [opts.repoRoot] - Repo root path. null = TRUST_RECONCILE_REPO_ROOT env or cwd.
276
487
  * @returns {number} Exit code: 0 = pass, 1 = fail/divergence.
277
488
  */
278
- function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {}) {
489
+ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null, manifest = null } = {}) {
279
490
  const resolvedRepoRoot = path.resolve(
280
491
  repoRoot || process.env.TRUST_RECONCILE_REPO_ROOT || process.cwd()
281
492
  );
@@ -299,9 +510,22 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {
299
510
  return 1;
300
511
  }
301
512
 
513
+ // WS8 (ADR 0020): resolve the reconcile manifest (named {id, command} entries used
514
+ // for per-command classification-aware reconcile). The canonical (fresh-verify)
515
+ // commands above remain the standalone CI-truth run; the manifest is the set of
516
+ // required-lane commands a test_output claim is permitted to reconcile against.
517
+ const manifestResolution = resolveManifest({ manifest }, resolvedRepoRoot, canonicalCommands);
518
+ const manifestEntries = manifestResolution.entries;
519
+ const manifestByCmd = new Map();
520
+ for (const e of manifestEntries) manifestByCmd.set(normalizeCmd(e.command), e);
521
+
302
522
  process.stdout.write('[trust-reconcile] starting CI trust anchor reconcile\n');
303
523
  process.stdout.write(`[trust-reconcile] repo-root: ${resolvedRepoRoot}\n`);
304
524
  process.stdout.write(`[trust-reconcile] canonical commands: ${canonicalCommands.join(' | ')}\n`);
525
+ process.stdout.write(`[trust-reconcile] manifest: ${manifestEntries.length} entr${manifestEntries.length === 1 ? 'y' : 'ies'} (source: ${manifestResolution.source})\n`);
526
+ for (const e of manifestEntries) {
527
+ process.stdout.write(`[trust-reconcile] manifest[${e.id}] = ${e.command}\n`);
528
+ }
305
529
  if (bundlePath) {
306
530
  process.stdout.write(`[trust-reconcile] bundle: ${bundlePath}\n`);
307
531
  } else {
@@ -337,6 +561,8 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {
337
561
 
338
562
  if (result.passed) {
339
563
  process.stdout.write(`[trust-reconcile] PASS: ${cmd}\n`);
564
+ } else if (result.timedOut) {
565
+ process.stderr.write(`[trust-reconcile] FAIL: ${cmd} (TIMED OUT after ${result.timeoutMs}ms — this is a timeout kill, not a real command failure; if this command is a legitimately slow check, raise TRUST_RECONCILE_COMMAND_TIMEOUT_MS)\n`);
340
566
  } else {
341
567
  process.stderr.write(`[trust-reconcile] FAIL: ${cmd} (exit ${result.exitCode})\n`);
342
568
  if (result.stderr) {
@@ -407,25 +633,45 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {
407
633
  }
408
634
  }
409
635
  } else {
410
- // Full bundle: extract claimed-pass commands from evidence items + claims
411
- const claimedPasses = getClaimedPassCommands(bundle);
412
- process.stdout.write(`[trust-reconcile] found ${claimedPasses.length} claimed-pass command(s) in bundle\n`);
636
+ // WS8 (ADR 0020): classify the bundle's claims into reconcilable (test_output,
637
+ // manifest-matched), session-local (waiver/Surface-status), and never-captured
638
+ // command claims instead of forcing every claim to match one composite command.
639
+ const { reconcilable, sessionLocal, noEvidenceCommand, waiverOnCommand } = classifyBundleClaims(bundle);
640
+ process.stdout.write(`[trust-reconcile] classified bundle: ${reconcilable.length} reconcilable command claim(s), ${sessionLocal.length} session-local claim(s), ${noEvidenceCommand.length} unbacked/unreconciled command claim(s), ${waiverOnCommand.length} waiver-on-command claim(s)\n`);
641
+
642
+ // finding 3: re-derive every claim's status CI-side (never trust self-reported
643
+ // claim.status). Consumed by the session-local loop below to catch status-misassertion.
644
+ const derivedStatus = deriveClaimStatuses(bundlePath, resolvedRepoRoot);
645
+ if (derivedStatus) {
646
+ process.stdout.write(`[trust-reconcile] re-derived ${derivedStatus.size} claim status(es) CI-side from the bundle's own evidence/events/policies (self-reported claim.status is NOT trusted)\n`);
647
+ } else {
648
+ process.stderr.write(`[trust-reconcile] WARNING: CI-side status re-derivation is unavailable — every session-local pass-asserting claim will fail closed (cannot verify self-reported status)\n`);
649
+ }
413
650
 
414
- for (const { cmd, claimId, claimType, noEvidence } of claimedPasses) {
415
- const normalCmd = normalizeCmd(cmd);
651
+ // finding 4 (server-side): a command-backed (test_output-evidence) claim carrying a
652
+ // waiver is a divergence — a command-backed check reconciles against CI or fails; it
653
+ // cannot be waived away.
654
+ for (const { claimId, claimType, subject } of waiverOnCommand) {
655
+ issues.push({
656
+ type: 'waiver-on-command-check',
657
+ message: `trust divergence: claim '${claimId}' (${subject}, claimType: ${claimType}) carries a waiver but is backed by test_output evidence — a command-backed check reconciles against CI or fails and cannot be waived`,
658
+ });
659
+ }
416
660
 
417
- // (d) Claims with no evidence are an immediate not-run divergence.
418
- // A pass was claimed but never captured — CI cannot confirm it.
419
- if (noEvidence) {
420
- issues.push({
421
- type: 'not-run',
422
- cmd,
423
- message: `trust divergence: claim '${claimId}' (claimType: ${claimType}) asserts pass but has no supporting evidence item — command never captured`,
424
- });
425
- continue;
426
- }
661
+ // not-run divergences: never-captured command claims (no evidence) AND test_output
662
+ // claims that did not reconcile (no manifest-matchable execution.label).
663
+ for (const { cmd, claimId, claimType, reason } of noEvidenceCommand) {
664
+ const message = reason === 'test_output-unreconciled'
665
+ ? `trust divergence: claim '${claimId}' (claimType: ${claimType}) asserts pass with test_output evidence but has no manifest-matched execution.label — a test_output claim must reconcile against the manifest or it is a divergence (never accepted as session-local)`
666
+ : `trust divergence: claim '${claimId}' (claimType: ${claimType}) asserts pass but has no supporting evidence item — command never captured`;
667
+ issues.push({ type: 'not-run', cmd, message });
668
+ }
427
669
 
428
- // (a) Laundering operator check must come first (most specific signal)
670
+ // Reconcilable test_output claims: laundering manifest match CI fresh result.
671
+ for (const { cmd } of reconcilable) {
672
+ const normalCmd = normalizeCmd(cmd);
673
+
674
+ // (a) Laundering operator check — must come first (most specific signal).
429
675
  if (hasLaunderingOperator(cmd)) {
430
676
  issues.push({
431
677
  type: 'laundering',
@@ -435,21 +681,41 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {
435
681
  continue;
436
682
  }
437
683
 
438
- // (b/c) Look up in CI's fresh run results
439
- const ciResult = ciResults.get(normalCmd);
440
-
441
- if (!ciResult) {
442
- // (c) CI never ran this claimed-pass command — fail-closed
684
+ // A test_output claim MUST name a manifest (required-lane) command. An agent
685
+ // cannot self-label an arbitrary command test_output to dodge the manifest.
686
+ const entry = manifestByCmd.get(normalCmd);
687
+ if (!entry) {
443
688
  issues.push({
444
689
  type: 'not-run',
445
690
  cmd,
446
- message: `trust divergence: agent claimed '${cmd}' passed; CI did not run this command (not in canonical verify set)`,
691
+ message: `trust divergence: agent claimed '${cmd}' passed; command is not in the reconcile manifest — a test_output claim must name a manifest/required-lane command (CI cannot self-declare an arbitrary command)`,
447
692
  });
448
693
  continue;
449
694
  }
450
695
 
696
+ // Prefer the fresh-verify Step 1 result; otherwise re-run this (manifest, and
697
+ // therefore required-lane) command fresh now. Only manifest commands are ever
698
+ // run on demand, so the set is bounded by the registry.
699
+ let ciResult = ciResults.get(normalCmd);
700
+ if (!ciResult) {
701
+ process.stdout.write(`[trust-reconcile] manifest reconcile: running '${entry.command}' (id: ${entry.id})\n`);
702
+ const r = runCommand(entry.command, resolvedRepoRoot);
703
+ ciResult = { exitCode: r.exitCode, passed: r.passed };
704
+ ciResults.set(normalCmd, ciResult);
705
+ if (r.passed) {
706
+ process.stdout.write(`[trust-reconcile] PASS: ${entry.command}\n`);
707
+ } else if (r.timedOut) {
708
+ process.stderr.write(`[trust-reconcile] FAIL: ${entry.command} (TIMED OUT after ${r.timeoutMs}ms — this is a timeout kill, not a real command failure; if this manifest command is a legitimately slow check, raise TRUST_RECONCILE_COMMAND_TIMEOUT_MS)\n`);
709
+ } else {
710
+ process.stderr.write(`[trust-reconcile] FAIL: ${entry.command} (exit ${r.exitCode})\n`);
711
+ if (r.stderr) {
712
+ const tail = r.stderr.trim().split('\n').slice(-5).join('\n');
713
+ process.stderr.write(`[trust-reconcile] --- stderr tail ---\n${tail}\n[trust-reconcile] ---\n`);
714
+ }
715
+ }
716
+ }
717
+
451
718
  if (!ciResult.passed) {
452
- // (b) Claimed pass + CI fresh FAIL = divergence
453
719
  issues.push({
454
720
  type: 'divergence',
455
721
  cmd,
@@ -459,9 +725,99 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {
459
725
  continue;
460
726
  }
461
727
 
462
- // Clean: claimed pass, CI confirms pass
463
- process.stdout.write(`[trust-reconcile] RECONCILED: '${cmd}' — claimed pass, CI fresh run = PASS\n`);
728
+ process.stdout.write(`[trust-reconcile] RECONCILED: '${cmd}' (manifest id: ${entry.id}) — claimed pass, CI fresh run = PASS\n`);
464
729
  }
730
+
731
+ // Session-local claims: not CI-reconcilable, but NOT a pass bypass. Each must either
732
+ // (a) carry a loud, justified waiver, or (b) resolve a real CI-RE-DERIVED `verified`
733
+ // status. WS8 iteration-2 hardening:
734
+ // - finding 3: the status used here is RE-DERIVED CI-side, never the self-reported
735
+ // claim.status. A mismatch is a `status-misassertion` divergence. If re-derivation
736
+ // is unavailable we fail closed (`status-underivable`) — we never fall back to
737
+ // trusting the bundle's own status.
738
+ // - finding 2: `assumed` alone is NO LONGER a silent pass. `assumed` is acceptable
739
+ // ONLY with a waiver (printed as a loud WAIVED line). An unwaived `assumed` claim is
740
+ // an `unwaived-assumed` divergence (restores pre-WS8 semantics where `assumed` alone
741
+ // never satisfied assertsPass).
742
+ //
743
+ // WS8 iteration-4 (converged iteration-3 finding, both gates): a session-local claim
744
+ // that re-derives `verified` with NO waiver is, by definition, backed only by
745
+ // human_attestation/attestation/external/etc. evidence with no --command — i.e.
746
+ // `deriveClaimStatus` only proves the bundle is internally self-consistent, never that
747
+ // the underlying attestation is true. A fully fabricated bundle (fake claim + fake
748
+ // evidence + fake event, all hand-authored to agree) is INDISTINGUISHABLE from a real
749
+ // one at this layer. This was previously printed as a quiet `SESSION-LOCAL OK` line
750
+ // indistinguishable from a genuinely reconciled check. It is now always printed as a
751
+ // loud, distinct `ATTESTED (not independently verifiable at L0)` marker — see ADR 0020
752
+ // Residuals. This does NOT change the exit code (attestations are not blocked at L0;
753
+ // blocking every honest human-attestation use is not the fix) — it is a visibility-only
754
+ // change so a reviewer/downstream tool can grep for and count exactly how many claims in
755
+ // a passing bundle rest on bundle-internal consistency alone rather than independent
756
+ // (CI fresh-run or cryptographically-signed) verification.
757
+ let attestedCount = 0;
758
+ for (const { claimId, claimType, assertedStatus, waiver, subject, evidenceType } of sessionLocal) {
759
+ // finding 3: re-derive; never trust the asserted status.
760
+ let status;
761
+ if (!derivedStatus) {
762
+ issues.push({
763
+ type: 'status-underivable',
764
+ message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) asserts status '${assertedStatus || 'unknown'}' but CI-side re-derivation is unavailable — refusing to trust a self-reported status (fail-closed)`,
765
+ });
766
+ continue;
767
+ }
768
+ const derived = derivedStatus.get(claimId);
769
+ if (derived === undefined || derived === null) {
770
+ issues.push({
771
+ type: 'status-underivable',
772
+ message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) could not be re-derived CI-side from the bundle's own evidence/events/policies — refusing to trust its self-reported status '${assertedStatus || 'unknown'}' (fail-closed)`,
773
+ });
774
+ continue;
775
+ }
776
+ if (derived !== assertedStatus) {
777
+ issues.push({
778
+ type: 'status-misassertion',
779
+ message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) asserts status '${assertedStatus || 'unknown'}' but CI re-derivation from the bundle's own evidence/events/policies yields '${derived}' — the reconciler does not trust self-reported claim.status`,
780
+ });
781
+ continue;
782
+ }
783
+ status = derived;
784
+
785
+ if (status === 'disputed' || status === 'rejected') {
786
+ issues.push({
787
+ type: 'session-local-failed',
788
+ message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) has re-derived status '${status}' — a failing/rejected claim blocks (session-local classification is not a pass bypass)`,
789
+ });
790
+ continue;
791
+ }
792
+ // finding 2: a waiver is the ONLY way an `assumed` (or otherwise non-`verified`)
793
+ // session-local claim passes, and it is printed loudly. `verified` still passes on its
794
+ // own re-derived status.
795
+ if (waiver && waiver.reason && waiver.approved_by) {
796
+ process.stdout.write(`[trust-reconcile] WAIVED: ${subject} (${claimType}) status=${status} — ${waiver.reason} (approved by ${waiver.approved_by})\n`);
797
+ continue;
798
+ }
799
+ if (status === 'verified') {
800
+ attestedCount++;
801
+ process.stdout.write(`[trust-reconcile] ATTESTED (not independently verifiable at L0): '${claimId}' (${claimType}) evidenceType=${evidenceType} — accepted on bundle-internal consistency only; see ADR 0020 Residuals\n`);
802
+ continue;
803
+ }
804
+ if (status === 'assumed') {
805
+ issues.push({
806
+ type: 'unwaived-assumed',
807
+ message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) has re-derived status 'assumed' but carries no waiver — 'assumed' alone is not a pass; it requires a documented waiver (--accepted-gap-reason/--waived-by) to be accepted`,
808
+ });
809
+ continue;
810
+ }
811
+ issues.push({
812
+ type: 'unwaived-session-local',
813
+ message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) asserts pass with re-derived status '${status || 'unknown'}' but has no waiver and no CI-re-derived verified status`,
814
+ });
815
+ }
816
+
817
+ // WS8 iteration-4: always emit the summary count, even when zero, so a passing bundle
818
+ // with zero attested claims is distinguishable in the log from one where the count line
819
+ // is simply absent (grep-stable for downstream tooling / reviewers).
820
+ process.stdout.write(`[trust-reconcile] ${attestedCount} attested claim(s) accepted without independent verification\n`);
465
821
  }
466
822
  }
467
823
 
@@ -517,6 +873,7 @@ function main() {
517
873
  bundle: args.bundle || null,
518
874
  commands: args.commands,
519
875
  repoRoot: args.repoRoot || null,
876
+ manifest: args.manifest || null,
520
877
  }));
521
878
  }
522
879