@kontourai/flow-agents 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (281) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +173 -0
  3. package/CHANGELOG.md +54 -0
  4. package/CONTEXT.md +5 -1
  5. package/README.md +19 -8
  6. package/build/src/builder-flow-run-adapter.d.ts +80 -0
  7. package/build/src/builder-flow-run-adapter.js +241 -0
  8. package/build/src/builder-flow-runtime.d.ts +16 -0
  9. package/build/src/builder-flow-runtime.js +290 -0
  10. package/build/src/cli/assignment-provider.js +10 -1
  11. package/build/src/cli/builder-run.d.ts +1 -0
  12. package/build/src/cli/builder-run.js +27 -0
  13. package/build/src/cli/effective-backlog-settings.js +70 -2
  14. package/build/src/cli/init.d.ts +34 -0
  15. package/build/src/cli/init.js +341 -61
  16. package/build/src/cli/kit.js +55 -12
  17. package/build/src/cli/pull-work-provider.js +346 -5
  18. package/build/src/cli/skill-drift-check.d.ts +1 -0
  19. package/build/src/cli/skill-drift-check.js +165 -0
  20. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  21. package/build/src/cli/telemetry-doctor.js +53 -6
  22. package/build/src/cli/validate-hook-influence.js +37 -7
  23. package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
  24. package/build/src/cli/workflow-sidecar.d.ts +310 -1
  25. package/build/src/cli/workflow-sidecar.js +1914 -126
  26. package/build/src/cli.js +5 -0
  27. package/build/src/flow-kit/validate.d.ts +54 -34
  28. package/build/src/flow-kit/validate.js +237 -26
  29. package/build/src/index.d.ts +2 -0
  30. package/build/src/index.js +1 -0
  31. package/build/src/lib/console-connect-options.d.ts +97 -0
  32. package/build/src/lib/console-connect-options.js +199 -0
  33. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  34. package/build/src/lib/console-telemetry-validate.js +91 -0
  35. package/build/src/lib/flow-resolver.d.ts +54 -1
  36. package/build/src/lib/flow-resolver.js +112 -5
  37. package/build/src/lib/fs.d.ts +17 -0
  38. package/build/src/lib/fs.js +172 -0
  39. package/build/src/lib/local-artifact-root.d.ts +44 -1
  40. package/build/src/lib/local-artifact-root.js +131 -3
  41. package/build/src/runtime-adapters.d.ts +39 -3
  42. package/build/src/runtime-adapters.js +77 -31
  43. package/build/src/tools/build-universal-bundles.js +40 -2
  44. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  45. package/build/src/tools/codex-agent-routing.js +49 -0
  46. package/build/src/tools/generate-context-map.js +1 -0
  47. package/build/src/tools/validate-source-tree.js +30 -3
  48. package/context/contracts/artifact-contract.md +16 -2
  49. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  50. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  51. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  52. package/context/scripts/hooks/workflow-steering.js +194 -22
  53. package/context/scripts/package.json +3 -0
  54. package/context/scripts/telemetry/install-console-config.sh +25 -4
  55. package/context/scripts/telemetry/lib/config.sh +102 -12
  56. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  57. package/context/scripts/telemetry/lib/session.sh +3 -0
  58. package/context/scripts/telemetry/lib/transport.sh +87 -0
  59. package/context/scripts/telemetry/lib/usage.sh +205 -4
  60. package/context/scripts/telemetry/telemetry.conf +6 -0
  61. package/context/scripts/telemetry/telemetry.sh +48 -0
  62. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  63. package/docs/agent-usage-feedback-loop.md +35 -0
  64. package/docs/architecture-engine-and-kits.md +110 -0
  65. package/docs/context-map.md +2 -0
  66. package/docs/coordination-guide.md +370 -0
  67. package/docs/decisions/agent-coordination.md +26 -9
  68. package/docs/decisions/embeddable-engine.md +152 -0
  69. package/docs/decisions/index.md +5 -3
  70. package/docs/decisions/trust-ledger-retention.md +88 -0
  71. package/docs/decisions/trust-reconcile.md +42 -9
  72. package/docs/decisions/workflow-enforcement.md +31 -9
  73. package/docs/fixture-ownership.md +6 -2
  74. package/docs/implementing-trust-reconciliation.md +129 -0
  75. package/docs/index.md +23 -9
  76. package/docs/integrations/flow-agents-console.md +275 -0
  77. package/docs/integrations/index.md +4 -0
  78. package/docs/kit-authoring-guide.md +52 -21
  79. package/docs/spec/builder-flow-runtime.md +80 -0
  80. package/docs/spec/runtime-hook-surface.md +45 -1
  81. package/docs/specs/economics-record-contract.md +270 -0
  82. package/docs/specs/harness-capability-matrix.md +74 -0
  83. package/docs/specs/learning-review-proposals-contract.md +340 -0
  84. package/docs/specs/routing-efficiency-review.md +59 -0
  85. package/docs/verifiable-trust.md +74 -25
  86. package/docs/workflow-artifact-lifecycle.md +38 -1
  87. package/docs/workflow-usage-guide.md +10 -0
  88. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  89. package/evals/ci/antigaming-suite.sh +2 -0
  90. package/evals/ci/run-baseline.sh +78 -4
  91. package/evals/fixtures/economics/acceptance.json +12 -0
  92. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  93. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  94. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  95. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  96. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  97. package/evals/fixtures/economics/critique.json +22 -0
  98. package/evals/fixtures/economics/expected-record.json +71 -0
  99. package/evals/fixtures/economics/session-usage-event.json +1 -0
  100. package/evals/fixtures/economics/state.json +11 -0
  101. package/evals/fixtures/economics/transcript.jsonl +3 -0
  102. package/evals/fixtures/hook-influence/cases.json +7 -7
  103. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  104. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  105. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  106. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  108. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  116. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  117. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  118. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  119. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  120. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  121. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  122. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  123. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  124. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  125. package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
  126. package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
  127. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  128. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  129. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  130. package/evals/integration/test_builder_step_producers.sh +18 -10
  131. package/evals/integration/test_bundle_install.sh +172 -0
  132. package/evals/integration/test_checkpoint_signing.sh +10 -2
  133. package/evals/integration/test_ci_actor_identity.sh +221 -0
  134. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  135. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  136. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  137. package/evals/integration/test_economics_record.sh +674 -0
  138. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  139. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  140. package/evals/integration/test_exemption_usage_review.sh +198 -0
  141. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  142. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  143. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  144. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  145. package/evals/integration/test_gate_lockdown.sh +7 -0
  146. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  147. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  148. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  149. package/evals/integration/test_hook_influence_cases.sh +25 -1
  150. package/evals/integration/test_install_merge.sh +227 -2
  151. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  152. package/evals/integration/test_learning_review_proposals.sh +329 -0
  153. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  154. package/evals/integration/test_liveness_console_relay.sh +166 -0
  155. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  156. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  157. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  158. package/evals/integration/test_publish_delivery.sh +389 -2
  159. package/evals/integration/test_pull_work_board.sh +200 -0
  160. package/evals/integration/test_pull_work_provider.sh +1 -1
  161. package/evals/integration/test_reconcile_preflight.sh +304 -0
  162. package/evals/integration/test_record_check.sh +378 -0
  163. package/evals/integration/test_routing_efficiency.sh +71 -0
  164. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  165. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  166. package/evals/integration/test_skill_drift_check.sh +870 -0
  167. package/evals/integration/test_takeover_protocol.sh +340 -0
  168. package/evals/integration/test_telemetry.sh +445 -0
  169. package/evals/integration/test_telemetry_doctor.sh +66 -0
  170. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  171. package/evals/integration/test_trust_reconcile_negatives.sh +121 -13
  172. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  173. package/evals/integration/test_usage_cost.sh +61 -0
  174. package/evals/integration/test_verify_hold.sh +910 -0
  175. package/evals/integration/test_veritas_governance_kit.sh +257 -0
  176. package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
  177. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  178. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  179. package/evals/integration/test_workspace_settings.sh +176 -0
  180. package/evals/lib/env.sh +26 -0
  181. package/evals/lib/node.sh +8 -0
  182. package/evals/run.sh +37 -0
  183. package/evals/static/test_ci_integration_coverage.sh +115 -0
  184. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  185. package/evals/static/test_universal_bundles.sh +34 -0
  186. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  187. package/evals/static/test_workflow_skills.sh +1 -1
  188. package/kits/builder/flows/build.flow.json +9 -18
  189. package/kits/builder/flows/publish-learn.flow.json +5 -1
  190. package/kits/builder/kit.json +120 -0
  191. package/kits/builder/skills/continue-work/SKILL.md +2 -0
  192. package/kits/builder/skills/deliver/SKILL.md +115 -0
  193. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  194. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  195. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  196. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  197. package/kits/builder/skills/pull-work/SKILL.md +33 -2
  198. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  199. package/kits/knowledge/kit.json +9 -0
  200. package/kits/veritas-governance/docs/README.md +113 -7
  201. package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
  202. package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
  203. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  204. package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
  205. package/kits/veritas-governance/kit.json +19 -0
  206. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  207. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  208. package/package.json +2 -2
  209. package/packaging/manifest.json +29 -0
  210. package/schemas/backlog-provider-settings.schema.json +13 -0
  211. package/schemas/workflow-state.schema.json +44 -0
  212. package/scripts/README.md +4 -0
  213. package/scripts/check-content-boundary.cjs +8 -1
  214. package/scripts/ci/trust-reconcile.js +214 -253
  215. package/scripts/hooks/codex-hook-adapter.js +77 -2
  216. package/scripts/hooks/evidence-capture.js +38 -5
  217. package/scripts/hooks/lib/actor-identity.js +82 -0
  218. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  219. package/scripts/hooks/lib/kit-catalog.js +235 -0
  220. package/scripts/hooks/lib/liveness-write.js +28 -1
  221. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  222. package/scripts/hooks/lib/runnable-command.js +177 -0
  223. package/scripts/hooks/lib/skill-drift.js +350 -0
  224. package/scripts/hooks/stop-goal-fit.js +278 -48
  225. package/scripts/hooks/workflow-steering.js +194 -22
  226. package/scripts/install-codex-home.sh +97 -47
  227. package/scripts/install-merge.js +72 -14
  228. package/scripts/install-owned-files.js +178 -0
  229. package/scripts/lib/reconcile-shape.js +381 -0
  230. package/scripts/liveness/relay.sh +84 -0
  231. package/scripts/telemetry/economics-record.schema.json +145 -0
  232. package/scripts/telemetry/economics-record.sh +331 -0
  233. package/scripts/telemetry/install-console-config.sh +25 -4
  234. package/scripts/telemetry/learning-review-decide.sh +124 -0
  235. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  236. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  237. package/scripts/telemetry/lib/config.sh +102 -12
  238. package/scripts/telemetry/lib/pricing.sh +14 -6
  239. package/scripts/telemetry/lib/session.sh +3 -0
  240. package/scripts/telemetry/lib/transport.sh +133 -15
  241. package/scripts/telemetry/lib/usage.sh +121 -28
  242. package/scripts/telemetry/routing-efficiency.sh +0 -0
  243. package/scripts/telemetry/telemetry.conf +6 -0
  244. package/scripts/telemetry/telemetry.sh +48 -0
  245. package/src/builder-flow-run-adapter.ts +357 -0
  246. package/src/builder-flow-runtime.ts +348 -0
  247. package/src/cli/assignment-provider.ts +12 -1
  248. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  249. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  250. package/src/cli/builder-run.ts +28 -0
  251. package/src/cli/codex-agent-routing.test.mjs +44 -0
  252. package/src/cli/codex-exit-code.test.mjs +207 -0
  253. package/src/cli/console-connect-options.test.mjs +329 -0
  254. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  255. package/src/cli/effective-backlog-settings.ts +68 -2
  256. package/src/cli/flow-resolver-composition.test.mjs +72 -0
  257. package/src/cli/init.test.mjs +161 -0
  258. package/src/cli/init.ts +407 -62
  259. package/src/cli/kit-metadata-security.test.mjs +443 -0
  260. package/src/cli/kit.ts +50 -12
  261. package/src/cli/pull-work-provider.ts +377 -3
  262. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  263. package/src/cli/skill-drift-check.ts +196 -0
  264. package/src/cli/telemetry-doctor.test.mjs +53 -0
  265. package/src/cli/telemetry-doctor.ts +50 -7
  266. package/src/cli/validate-hook-influence.ts +37 -6
  267. package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
  268. package/src/cli/workflow-sidecar.ts +1980 -119
  269. package/src/cli.ts +5 -0
  270. package/src/flow-kit/validate.ts +277 -38
  271. package/src/index.ts +19 -0
  272. package/src/lib/console-connect-options.ts +261 -0
  273. package/src/lib/console-telemetry-validate.ts +88 -0
  274. package/src/lib/flow-resolver.ts +117 -4
  275. package/src/lib/fs.ts +160 -0
  276. package/src/lib/local-artifact-root.ts +129 -3
  277. package/src/runtime-adapters.ts +113 -33
  278. package/src/tools/build-universal-bundles.ts +36 -2
  279. package/src/tools/codex-agent-routing.ts +48 -0
  280. package/src/tools/generate-context-map.ts +1 -0
  281. package/src/tools/validate-source-tree.ts +29 -3
@@ -91,6 +91,25 @@
91
91
  * 0022 addendum, part 4) — see
92
92
  * resolveEnforcementEvent(). Default (unset/
93
93
  * unrecognized): treated as a gating event.
94
+ * TRUST_RECONCILE_BASE_REF Optional PR base ref (falls back to
95
+ * GITHUB_BASE_REF, set by GitHub Actions on
96
+ * `pull_request` events only) used SOLELY by the
97
+ * audit-only runtime-session-trailer diagnostic
98
+ * below to bound its commit-log scan
99
+ * (`git log <base>..<sha>`); unset/unresolvable →
100
+ * diagnostic narrows to scanning just the head
101
+ * commit's own message (documented, accepted
102
+ * fallback — never a crash, never widens scope).
103
+ * Never affects Step 1/Step 2/exit-code logic.
104
+ *
105
+ * Runtime-session commit-trailer diagnostic (ADR 0022 §1, issue #305 — AUDIT ONLY, never
106
+ * gating): every run logs a `[trust-reconcile] identified: runtime-session trailer
107
+ * '<trailer>' on <ref>` line for each distinct `Claude-Session:` / `Codex-Session:` /
108
+ * `Opencode-Session:` / `Pi-Session:` commit trailer found in the resolved commit range
109
+ * (see findRuntimeSessionTrailers()/logRuntimeSessionTrailers()). Logs the trailer KEY only
110
+ * (e.g. `Claude-Session`) — never the trailer VALUE (typically a session URL) — per the
111
+ * ADR's own quoted line format. This is diagnostic/attribution only: it never writes to
112
+ * ciResults/issues[] and never changes the exit code, on any path.
94
113
  *
95
114
  * Auto-discovery (when no --bundle or TRUST_RECONCILE_BUNDLE is set):
96
115
  * Checks delivery/trust.bundle, then delivery/trust.checkpoint.json under repo root
@@ -124,6 +143,18 @@ const path = require('path');
124
143
  // copy here had drifted (it was missing the trailing `/bin/true` check), which is
125
144
  // exactly why this is now imported rather than duplicated.
126
145
  const { hasLaunderingOperator } = require('../lib/command-log-chain.js');
146
+ // #356: the bundle-shape classification (classifyBundleClaims) and the pure,
147
+ // shape-level divergence-construction helpers used by Step 2 below are shared with a
148
+ // local pre-push preflight (reconcile-preflight) via this module, so the preflight can
149
+ // never drift from what this CI reconciler actually enforces. See that module's own
150
+ // header comment for the extraction rationale.
151
+ const {
152
+ classifyBundleClaims,
153
+ waiverOnCommandIssues,
154
+ noEvidenceCommandIssues,
155
+ reconcilableManifestIssues,
156
+ sessionLocalShapeIssues,
157
+ } = require('../lib/reconcile-shape.js');
127
158
 
128
159
  // ---------------------------------------------------------------------------
129
160
  // Helpers
@@ -199,138 +230,6 @@ function runCommand(cmd, repoRoot) {
199
230
  };
200
231
  }
201
232
 
202
- /**
203
- * Classify a trust.bundle's claims into: reconcilable command claims (test_output +
204
- * execution.label), session-local claims (attestation/observation/citation), never-captured
205
- * or unbacked command claims (not-run divergence), and command-backed claims carrying a
206
- * waiver (waiver-on-command divergence). Returns
207
- * { reconcilable, sessionLocal, noEvidenceCommand, waiverOnCommand }.
208
- *
209
- * Source of truth: evidence[].execution.label is the command string recorded at capture time.
210
- * evidence[].passing (normalized) means the agent claimed this passed. `claim.status` is NOT
211
- * trusted here — the caller re-derives it CI-side (see derive-claim-status.mjs / finding-3).
212
- *
213
- * WS8 iteration-2 hardening:
214
- * - finding 1: ANY pass-asserting claim whose evidence is `evidenceType: test_output`
215
- * (Surface's default when unset) but which did NOT reconcile — i.e. it has no
216
- * manifest-matchable execution.label — is a divergence, NOT session-local. A test_output
217
- * claim either reconciles against the manifest or is a divergence; it is never accepted on
218
- * self-reported status. (Previously only the literal claimType `workflow.check.command`
219
- * was guarded, so a fabricated kind:"test" claim with no command slipped through.)
220
- * - finding 4: a command-backed (test_output) claim carrying a waiver is a divergence — a
221
- * command-backed check reconciles against CI or fails; it cannot be waived.
222
- */
223
- function classifyBundleClaims(bundle) {
224
- const evidence = Array.isArray(bundle.evidence) ? bundle.evidence : [];
225
- const claims = Array.isArray(bundle.claims) ? bundle.claims : [];
226
-
227
- const claimById = new Map();
228
- for (const c of claims) if (c && c.id) claimById.set(c.id, c);
229
-
230
- // Evidence indexing. A missing evidenceType defaults to test_output for backward
231
- // compatibility with pre-classification bundles (same default classifyEvidence uses).
232
- const claimHasLabeledTestOutput = new Set(); // test_output evidence WITH an execution.label
233
- const claimHasTestOutputEvidence = new Set(); // ANY test_output evidence (label or not)
234
- // WS8 iteration-4 (converged finding): the session-local (non-test_output) evidenceType per
235
- // claim, so the reconciler can name it on the loud ATTESTED marker below — a fabricated
236
- // human_attestation/attestation/external claim with no --command is otherwise
237
- // indistinguishable, in the reconciler's own output, from a genuinely re-runnable check.
238
- const claimEvidenceType = new Map();
239
- for (const ev of evidence) {
240
- if (!ev || !ev.claimId) continue;
241
- const evType = ev.evidenceType || 'test_output';
242
- if (evType !== 'test_output') {
243
- if (!claimEvidenceType.has(ev.claimId)) claimEvidenceType.set(ev.claimId, evType);
244
- continue;
245
- }
246
- claimHasTestOutputEvidence.add(ev.claimId);
247
- if (ev.execution && ev.execution.label) claimHasLabeledTestOutput.add(ev.claimId);
248
- }
249
-
250
- // finding 4: a command-backed (test_output-evidence) claim that also carries a waiver.
251
- const waiverOnCommand = [];
252
- for (const c of claims) {
253
- if (!c || !c.id) continue;
254
- const waiver = (c.metadata && typeof c.metadata === 'object') ? c.metadata.waiver : undefined;
255
- if (waiver && typeof waiver === 'object' && claimHasTestOutputEvidence.has(c.id)) {
256
- waiverOnCommand.push({ claimId: c.id, claimType: String(c.claimType || ''), subject: c.subjectId || c.fieldOrBehavior || c.id });
257
- }
258
- }
259
-
260
- // (A) Reconcilable claimed-passes: evidence items that are test_output (CI-reconcilable),
261
- // carry an execution.label, and assert pass. Session-local evidenceTypes
262
- // (crawl_observation, human_attestation, attestation, policy_rule, source_excerpt,
263
- // document_citation, calculation_trace) are NOT reconciled per-command — they are handled
264
- // by the session-local/waiver path below.
265
- const reconcilable = [];
266
- const reconcilableClaimIds = new Set();
267
- const seen = new Set();
268
- for (const ev of evidence) {
269
- if (!ev || !ev.execution || !ev.execution.label) continue;
270
- if (!isPassingValue(ev.passing)) continue;
271
- const evType = ev.evidenceType || 'test_output';
272
- if (evType !== 'test_output') continue; // session-local — not CI-reconcilable
273
- const cmd = normalizeCmd(ev.execution.label);
274
- if (!cmd) continue;
275
- reconcilableClaimIds.add(ev.claimId);
276
- if (seen.has(cmd)) continue;
277
- seen.add(cmd);
278
- const claim = claimById.get(ev.claimId);
279
- reconcilable.push({ cmd, claimId: ev.claimId, evId: ev.id, claimType: claim ? String(claim.claimType || '') : '' });
280
- }
281
-
282
- // (B) Session-local claims, never-captured command claims, and unreconciled test_output.
283
- const sessionLocal = [];
284
- const noEvidenceCommand = [];
285
- const seenClaims = new Set();
286
- for (const c of claims) {
287
- if (!c || !c.id || typeof c.claimType !== 'string') continue;
288
- // #267/#282: a superseded critique write is HISTORY — excluded from reconcile evaluation so a
289
- // resolved session converges (a fail critique that a later same-reviewer pass superseded no
290
- // longer blocks). Scoped to NON-test_output claims so a command-backed claim can never launder
291
- // a real failure by carrying superseded_by — a test_output claim always reconciles or diverges.
292
- if (c.metadata && typeof c.metadata === 'object' && c.metadata.superseded_by && !claimHasTestOutputEvidence.has(c.id)) continue;
293
- if (reconcilableClaimIds.has(c.id)) continue; // handled by (A)
294
- if (seenClaims.has(c.id)) continue;
295
- const status = String(c.status || '');
296
- const assertsPass = isPassingValue(c.value) || status === 'verified' || status === 'assumed';
297
- const isFailing = status === 'disputed' || status === 'rejected';
298
- if (!assertsPass && !isFailing) continue; // pending/unknown non-asserting — ignore (as before)
299
- seenClaims.add(c.id);
300
-
301
- // finding 1: a pass-asserting claim backed by test_output evidence that did NOT reconcile
302
- // (it has test_output evidence but no manifest-matchable execution.label — otherwise it
303
- // would be in bucket A) is a not-run divergence. A test_output claim reconciles against the
304
- // manifest or it is a divergence — it is NEVER accepted as session-local on self-report.
305
- if (assertsPass && claimHasTestOutputEvidence.has(c.id)) {
306
- const rawCmd = normalizeCmd(c.fieldOrBehavior || c.value || '');
307
- noEvidenceCommand.push({ cmd: rawCmd || `[claim:${c.id}]`, claimId: c.id, claimType: c.claimType, reason: 'test_output-unreconciled' });
308
- continue;
309
- }
310
-
311
- // A workflow.check.command claim with no captured (labeled) evidence is a never-captured
312
- // claimed pass — not-run divergence (anti-gaming teeth preserved).
313
- if (assertsPass && c.claimType === 'workflow.check.command' && !claimHasLabeledTestOutput.has(c.id)) {
314
- const rawCmd = normalizeCmd(c.fieldOrBehavior || c.value || '');
315
- noEvidenceCommand.push({ cmd: rawCmd || `[claim:${c.id}:${c.claimType}]`, claimId: c.id, claimType: c.claimType, reason: 'no-evidence-command' });
316
- continue;
317
- }
318
-
319
- const waiver = (c.metadata && typeof c.metadata === 'object') ? c.metadata.waiver : undefined;
320
- sessionLocal.push({
321
- claimId: c.id,
322
- claimType: c.claimType,
323
- assertedStatus: status,
324
- value: c.value,
325
- waiver: (waiver && typeof waiver === 'object') ? waiver : null,
326
- subject: c.subjectId || c.fieldOrBehavior || c.id,
327
- evidenceType: claimEvidenceType.get(c.id) || 'unknown',
328
- });
329
- }
330
-
331
- return { reconcilable, sessionLocal, noEvidenceCommand, waiverOnCommand };
332
- }
333
-
334
233
  /**
335
234
  * WS8 (finding 3): re-derive every claim's status CI-side from the bundle's OWN
336
235
  * evidence/events/policies, using @kontourai/surface's canonical deriveClaimStatus (the same
@@ -793,6 +692,111 @@ function isShaInRange(repoRoot, sha, from, to) {
793
692
  return isAncestorCommit(repoRoot, from, sha) && isAncestorCommit(repoRoot, sha, to);
794
693
  }
795
694
 
695
+ /**
696
+ * Runtime-session commit-trailer names this reconciler recognizes for the audit-only
697
+ * diagnostic below (ADR 0022 §1, table row 1). These are commit-message TRAILER keys
698
+ * (e.g. `Claude-Session: https://claude.ai/code/session_...`), a distinct vocabulary from
699
+ * scripts/hooks/lib/actor-identity.js's RUNTIME_SESSION_ID_VARS (env var names) — but the
700
+ * same four-runtime enumeration (claude-code/codex/opencode/pi).
701
+ */
702
+ const RUNTIME_SESSION_TRAILER_NAMES = [
703
+ 'Claude-Session',
704
+ 'Codex-Session',
705
+ 'Opencode-Session',
706
+ 'Pi-Session',
707
+ ];
708
+
709
+ /**
710
+ * Diagnostic-only (ADR 0022 §1): find runtime-session commit trailers (`Claude-Session:`,
711
+ * `Codex-Session:`, `Opencode-Session:`, `Pi-Session:`) across the commit range this run is
712
+ * evaluating, and report which trailer KEYS were found — never the trailer VALUE (typically
713
+ * a session URL). "Commit trailer present — logged by the reconciler ... for audit; does not
714
+ * change the exit path" (ADR 0022 §1). This function is write-only diagnostics: its return
715
+ * value / any failure NEVER feeds ciResults, issues[], or the exit code.
716
+ *
717
+ * Range resolution (accepted, documented narrower fallback — ADR 0022 §1 Residuals):
718
+ * - Prefer `git log <baseRef>..<sha>` when a PR base ref is resolvable
719
+ * (TRUST_RECONCILE_BASE_REF env override, else GITHUB_BASE_REF — set by GitHub Actions
720
+ * on `pull_request` events only).
721
+ * - No base ref resolvable (local/manual invocation, `push`/`workflow_dispatch` events,
722
+ * which have no PR base) — falls back to scanning just `ctx.sha`'s own commit message.
723
+ * This is a narrower-than-ideal default (misses trailers on other commits in an
724
+ * unresolvable range) but is NEVER a crash and NEVER silently expands scope beyond what
725
+ * is actually derivable — documented gap, not a silent limitation.
726
+ * - No usable sha at all — zero trailers found (nothing to scan).
727
+ *
728
+ * @param {string} repoRoot - Repository root (cwd for git subprocess calls).
729
+ * @param {{ref: string, sha: string}} ctx - Resolved scope context (resolveScopeContext()).
730
+ * @returns {Array<{trailerName: string, ref: string}>} Distinct (trailerName) diagnostics
731
+ * found in range, each paired with the ref to log against. Empty array on any failure or
732
+ * when no trailer is present — never throws.
733
+ */
734
+ function findRuntimeSessionTrailers(repoRoot, ctx) {
735
+ const sha = (ctx && ctx.sha) || '';
736
+ if (!sha) return [];
737
+ const ref = (ctx && ctx.ref) || sha;
738
+
739
+ const baseRef = (process.env.TRUST_RECONCILE_BASE_REF || process.env.GITHUB_BASE_REF || '').trim();
740
+
741
+ let commitMessages = [];
742
+ try {
743
+ if (baseRef) {
744
+ const res = spawnSync('git', ['log', '--format=%B%x00', `${baseRef}..${sha}`], {
745
+ cwd: repoRoot, encoding: 'utf8',
746
+ });
747
+ if (res && !res.error && res.status === 0 && typeof res.stdout === 'string') {
748
+ commitMessages = res.stdout.split('\0').map((m) => m.trim()).filter(Boolean);
749
+ }
750
+ }
751
+ // No base ref resolvable, or the ranged log produced nothing usable — fall back to
752
+ // just the head commit's own message (accepted narrower default, never a crash).
753
+ if (!baseRef || commitMessages.length === 0) {
754
+ const res = spawnSync('git', ['log', '--format=%B', '-1', sha], {
755
+ cwd: repoRoot, encoding: 'utf8',
756
+ });
757
+ if (res && !res.error && res.status === 0 && typeof res.stdout === 'string' && res.stdout.trim()) {
758
+ commitMessages = [res.stdout.trim()];
759
+ }
760
+ }
761
+ } catch {
762
+ return [];
763
+ }
764
+
765
+ const found = [];
766
+ const seen = new Set();
767
+ for (const message of commitMessages) {
768
+ for (const trailerName of RUNTIME_SESSION_TRAILER_NAMES) {
769
+ if (seen.has(trailerName)) continue;
770
+ const prefix = `${trailerName}:`;
771
+ const hasTrailer = message.split('\n').some((line) => line.trim().startsWith(prefix));
772
+ if (hasTrailer) {
773
+ seen.add(trailerName);
774
+ found.push({ trailerName, ref });
775
+ }
776
+ }
777
+ }
778
+ return found;
779
+ }
780
+
781
+ /**
782
+ * Diagnostic-only (ADR 0022 §1): write the audit line for each runtime-session commit
783
+ * trailer found in range. Never touches ciResults/issues[]/exit code — wrapped so any
784
+ * unexpected failure degrades to zero lines written, never a thrown error.
785
+ *
786
+ * @param {string} repoRoot
787
+ * @param {{ref: string, sha: string}} ctx
788
+ */
789
+ function logRuntimeSessionTrailers(repoRoot, ctx) {
790
+ try {
791
+ const trailers = findRuntimeSessionTrailers(repoRoot, ctx);
792
+ for (const { trailerName, ref } of trailers) {
793
+ process.stdout.write(`[trust-reconcile] identified: runtime-session trailer '${trailerName}' on ${ref}\n`);
794
+ }
795
+ } catch {
796
+ // Diagnostic-only — any failure here must never affect the reconciler's outcome.
797
+ }
798
+ }
799
+
796
800
  /**
797
801
  * Match a SINGLE scope condition (one of the four forms) against the resolved context.
798
802
  * Extracted from matchesScope() so a compound (space-separated, AND) scope can validate
@@ -1030,6 +1034,13 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null, mani
1030
1034
  // to pick, among per-session candidates, the one that attests THIS change.
1031
1035
  const scopeCtx = resolveScopeContext(resolvedRepoRoot);
1032
1036
 
1037
+ // Diagnostic-only (ADR 0022 §1, issue #305): audit-log any runtime-session commit
1038
+ // trailer found in range — logged for attribution, never gating. Runs unconditionally,
1039
+ // regardless of bundle/DECLARED branch, immediately after scopeCtx resolves and before
1040
+ // bundle auto-discovery/Step 1, so it always fires the same way whether or not a bundle
1041
+ // or DECLARED exemption is later found. Never affects ciResults/issues[]/exit code.
1042
+ logRuntimeSessionTrailers(resolvedRepoRoot, scopeCtx);
1043
+
1033
1044
  let bundlePath = explicitBundlePath || discoverBundle(resolvedRepoRoot, scopeCtx.sha) || null;
1034
1045
  const bundleWasAutoDiscovered = !explicitBundlePath && !!bundlePath;
1035
1046
 
@@ -1224,50 +1235,24 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null, mani
1224
1235
  }
1225
1236
 
1226
1237
  // finding 4 (server-side): a command-backed (test_output-evidence) claim carrying a
1227
- // waiver is a divergence — a command-backed check reconciles against CI or fails; it
1228
- // cannot be waived away.
1229
- for (const { claimId, claimType, subject } of waiverOnCommand) {
1230
- issues.push({
1231
- type: 'waiver-on-command-check',
1232
- 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`,
1233
- });
1234
- }
1238
+ // waiver is a divergence — shape-only, delegated to scripts/lib/reconcile-shape.js
1239
+ // (used byte-identically here and by the local reconcile-preflight).
1240
+ issues.push(...waiverOnCommandIssues(waiverOnCommand));
1235
1241
 
1236
1242
  // not-run divergences: never-captured command claims (no evidence) AND test_output
1237
- // claims that did not reconcile (no manifest-matchable execution.label).
1238
- for (const { cmd, claimId, claimType, reason } of noEvidenceCommand) {
1239
- const message = reason === 'test_output-unreconciled'
1240
- ? `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)`
1241
- : `trust divergence: claim '${claimId}' (claimType: ${claimType}) asserts pass but has no supporting evidence item — command never captured`;
1242
- issues.push({ type: 'not-run', cmd, message });
1243
- }
1244
-
1245
- // Reconcilable test_output claims: laundering → manifest match → CI fresh result.
1246
- for (const { cmd } of reconcilable) {
1243
+ // claims that did not reconcile (no manifest-matchable execution.label). Shape-only,
1244
+ // delegated to scripts/lib/reconcile-shape.js.
1245
+ issues.push(...noEvidenceCommandIssues(noEvidenceCommand));
1246
+
1247
+ // Reconcilable test_output claims: laundering manifest match (shape-only, delegated
1248
+ // to scripts/lib/reconcile-shape.js) CI fresh result (CI-only, stays here — requires
1249
+ // a live command execution the local preflight must not perform).
1250
+ const { issues: manifestIssues, unresolved: reconcilableUnresolved } =
1251
+ reconcilableManifestIssues(reconcilable, manifestByCmd);
1252
+ issues.push(...manifestIssues);
1253
+ for (const { cmd, manifestEntry: entry } of reconcilableUnresolved) {
1247
1254
  const normalCmd = normalizeCmd(cmd);
1248
1255
 
1249
- // (a) Laundering operator check — must come first (most specific signal).
1250
- if (hasLaunderingOperator(cmd)) {
1251
- issues.push({
1252
- type: 'laundering',
1253
- cmd,
1254
- message: `trust divergence: agent claimed '${cmd}' passed; command contains exit-code-laundering operator (|| ... / ; true / ; exit 0 / etc.)`,
1255
- });
1256
- continue;
1257
- }
1258
-
1259
- // A test_output claim MUST name a manifest (required-lane) command. An agent
1260
- // cannot self-label an arbitrary command test_output to dodge the manifest.
1261
- const entry = manifestByCmd.get(normalCmd);
1262
- if (!entry) {
1263
- issues.push({
1264
- type: 'not-run',
1265
- cmd,
1266
- 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)`,
1267
- });
1268
- continue;
1269
- }
1270
-
1271
1256
  // Prefer the fresh-verify Step 1 result; otherwise re-run this (manifest, and
1272
1257
  // therefore required-lane) command fresh now. Only manifest commands are ever
1273
1258
  // run on demand, so the set is bounded by the registry.
@@ -1303,90 +1288,26 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null, mani
1303
1288
  process.stdout.write(`[trust-reconcile] RECONCILED: '${cmd}' (manifest id: ${entry.id}) — claimed pass, CI fresh run = PASS\n`);
1304
1289
  }
1305
1290
 
1306
- // Session-local claims: not CI-reconcilable, but NOT a pass bypass. Each must either
1307
- // (a) carry a loud, justified waiver, or (b) resolve a real CI-RE-DERIVED `verified`
1308
- // status. WS8 iteration-2 hardening:
1309
- // - finding 3: the status used here is RE-DERIVED CI-side, never the self-reported
1310
- // claim.status. A mismatch is a `status-misassertion` divergence. If re-derivation
1311
- // is unavailable we fail closed (`status-underivable`) we never fall back to
1312
- // trusting the bundle's own status.
1313
- // - finding 2: `assumed` alone is NO LONGER a silent pass. `assumed` is acceptable
1314
- // ONLY with a waiver (printed as a loud WAIVED line). An unwaived `assumed` claim is
1315
- // an `unwaived-assumed` divergence (restores pre-WS8 semantics where `assumed` alone
1316
- // never satisfied assertsPass).
1317
- //
1318
- // WS8 iteration-4 (converged iteration-3 finding, both gates): a session-local claim
1319
- // that re-derives `verified` with NO waiver is, by definition, backed only by
1320
- // human_attestation/attestation/external/etc. evidence with no --command i.e.
1321
- // `deriveClaimStatus` only proves the bundle is internally self-consistent, never that
1322
- // the underlying attestation is true. A fully fabricated bundle (fake claim + fake
1323
- // evidence + fake event, all hand-authored to agree) is INDISTINGUISHABLE from a real
1324
- // one at this layer. This was previously printed as a quiet `SESSION-LOCAL OK` line
1325
- // indistinguishable from a genuinely reconciled check. It is now always printed as a
1326
- // loud, distinct `ATTESTED (not independently verifiable at L0)` marker — see ADR 0020
1327
- // Residuals. This does NOT change the exit code (attestations are not blocked at L0;
1328
- // blocking every honest human-attestation use is not the fix) — it is a visibility-only
1329
- // change so a reviewer/downstream tool can grep for and count exactly how many claims in
1330
- // a passing bundle rest on bundle-internal consistency alone rather than independent
1331
- // (CI fresh-run or cryptographically-signed) verification.
1332
- let attestedCount = 0;
1333
- for (const { claimId, claimType, assertedStatus, waiver, subject, evidenceType } of sessionLocal) {
1334
- // finding 3: re-derive; never trust the asserted status.
1335
- let status;
1336
- if (!derivedStatus) {
1337
- issues.push({
1338
- type: 'status-underivable',
1339
- 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)`,
1340
- });
1341
- continue;
1342
- }
1343
- const derived = derivedStatus.get(claimId);
1344
- if (derived === undefined || derived === null) {
1345
- issues.push({
1346
- type: 'status-underivable',
1347
- 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)`,
1348
- });
1349
- continue;
1350
- }
1351
- if (derived !== assertedStatus) {
1352
- issues.push({
1353
- type: 'status-misassertion',
1354
- 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`,
1355
- });
1356
- continue;
1357
- }
1358
- status = derived;
1359
-
1360
- if (status === 'disputed' || status === 'rejected') {
1361
- issues.push({
1362
- type: 'session-local-failed',
1363
- 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)`,
1364
- });
1365
- continue;
1366
- }
1367
- // finding 2: a waiver is the ONLY way an `assumed` (or otherwise non-`verified`)
1368
- // session-local claim passes, and it is printed loudly. `verified` still passes on its
1369
- // own re-derived status.
1370
- if (waiver && waiver.reason && waiver.approved_by) {
1291
+ // Session-local claims: not CI-reconcilable, but NOT a pass bypass. Shape-only,
1292
+ // delegated to scripts/lib/reconcile-shape.js (sessionLocalShapeIssues) see that
1293
+ // module for the full WS8 finding-2/finding-3/iteration-4 rationale (waiver-only pass
1294
+ // for non-`verified` status, CI-re-derived status never self-reported, loud ATTESTED
1295
+ // marker for bundle-internal-only consistency). iteration-1 F1: CI MUST pass
1296
+ // `onUnderivable: 'fail'` explicitlythis is CI's trust anchor and must never silently
1297
+ // degrade to trusting a self-reported status when re-derivation is unavailable (that
1298
+ // reduced-coverage mode is opt-in only, for the LOCAL preflight see reconcile-shape.js).
1299
+ // iteration-1 F3: the WAIVED/ATTESTED log lines below are driven by the shared function's
1300
+ // own `logEvents` classification (not a parallel re-derivation) so CI's stdout narrative
1301
+ // can never drift from what sessionLocalShapeIssues actually decided.
1302
+ const { issues: sessionLocalIssues, attestedCount, logEvents } =
1303
+ sessionLocalShapeIssues(sessionLocal, derivedStatus, { onUnderivable: 'fail' });
1304
+ issues.push(...sessionLocalIssues);
1305
+ for (const { kind, claimId, claimType, subject, evidenceType, status, waiver } of logEvents) {
1306
+ if (kind === 'waived') {
1371
1307
  process.stdout.write(`[trust-reconcile] WAIVED: ${subject} (${claimType}) status=${status} — ${waiver.reason} (approved by ${waiver.approved_by})\n`);
1372
- continue;
1373
- }
1374
- if (status === 'verified') {
1375
- attestedCount++;
1308
+ } else if (kind === 'attested') {
1376
1309
  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`);
1377
- continue;
1378
- }
1379
- if (status === 'assumed') {
1380
- issues.push({
1381
- type: 'unwaived-assumed',
1382
- 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`,
1383
- });
1384
- continue;
1385
1310
  }
1386
- issues.push({
1387
- type: 'unwaived-session-local',
1388
- 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`,
1389
- });
1390
1311
  }
1391
1312
 
1392
1313
  // WS8 iteration-4: always emit the summary count, even when zero, so a passing bundle
@@ -1496,6 +1417,46 @@ function main() {
1496
1417
  // The direct-CLI behavior is preserved: when run as a script, main() is called below.
1497
1418
  module.exports.runTrustReconcile = runTrustReconcile;
1498
1419
 
1420
+ // #356: manifest-resolution helpers exported for reuse by the local reconcile-preflight
1421
+ // (Wave 2) so it resolves the manifest via the EXACT SAME priority chain this CI reconciler
1422
+ // uses (CLI --manifest > TRUST_RECONCILE_MANIFEST > package.json > run-baseline.sh
1423
+ // --manifest-json > legacy fresh-verify-commands fallback) — never a second implementation.
1424
+ // These stay defined here (not moved to scripts/lib/reconcile-shape.js) because they are
1425
+ // CI-adjacent resolution logic (they may spawn `evals/ci/run-baseline.sh --manifest-json`,
1426
+ // a cheap static-registry emit, not a fresh test run) rather than pure bundle-shape
1427
+ // classification.
1428
+ module.exports.resolveManifest = resolveManifest;
1429
+ module.exports.runBaselineManifest = runBaselineManifest;
1430
+ module.exports.normalizeManifestEntries = normalizeManifestEntries;
1431
+ module.exports.slugifyLabel = slugifyLabel;
1432
+ module.exports.normalizeCmd = normalizeCmd;
1433
+ module.exports.isAncestorCommit = isAncestorCommit;
1434
+ // #356: resolveManifest's legacy fallback tier (tier 5, "legacy:fresh-verify-commands")
1435
+ // folds the CANONICAL verify commands into the manifest when no dedicated manifest source
1436
+ // resolves. Exporting resolveCanonicalCommands too so the local preflight's manifest
1437
+ // resolution has genuine parity with CI on that fallback tier as well — otherwise a repo (or
1438
+ // a fixture/test repo) with no run-baseline.sh/package.json manifest source would silently
1439
+ // resolve an EMPTY legacy-fallback manifest locally while CI's own resolution (which always
1440
+ // threads its own resolved canonicalCommands into this same fallback) resolves a non-empty
1441
+ // one, a real parity gap, not merely a fixture quirk.
1442
+ module.exports.resolveCanonicalCommands = resolveCanonicalCommands;
1443
+
1444
+ // #356: re-export the shared bundle-shape classification/issue-construction functions
1445
+ // (primary home: scripts/lib/reconcile-shape.js) so a caller that already requires
1446
+ // trust-reconcile.js (e.g. an existing test harness) can reach them without also needing
1447
+ // to know the shared module's path. This is a re-export, not a second implementation —
1448
+ // the single definition lives in scripts/lib/reconcile-shape.js.
1449
+ module.exports.classifyBundleClaims = classifyBundleClaims;
1450
+ module.exports.waiverOnCommandIssues = waiverOnCommandIssues;
1451
+ module.exports.noEvidenceCommandIssues = noEvidenceCommandIssues;
1452
+ module.exports.reconcilableManifestIssues = reconcilableManifestIssues;
1453
+ module.exports.sessionLocalShapeIssues = sessionLocalShapeIssues;
1454
+
1455
+ // Exported for evals (#305 trailer-diagnostic coverage) — read-only diagnostic helpers,
1456
+ // never gating; see the header comment's "Runtime-session commit-trailer diagnostic" note.
1457
+ module.exports.findRuntimeSessionTrailers = findRuntimeSessionTrailers;
1458
+ module.exports.logRuntimeSessionTrailers = logRuntimeSessionTrailers;
1459
+
1499
1460
  if (require.main === module) {
1500
1461
  main();
1501
1462
  }
@@ -12,6 +12,7 @@
12
12
 
13
13
  const path = require('path');
14
14
  const { spawnSync } = require('child_process');
15
+ const { extractExitCodeFromBanner, readExitCodeFromRollout } = require('./lib/codex-exit-code');
15
16
 
16
17
  const MAX_STDIN = 1024 * 1024;
17
18
 
@@ -127,6 +128,73 @@ function blockedOutput(event, reason) {
127
128
  };
128
129
  }
129
130
 
131
+ /**
132
+ * enrichEvidenceCapturePayload(raw, hookId, event) → string
133
+ *
134
+ * Codex-only, evidence-capture-only enrichment (#470): codex's PostToolUse
135
+ * payload carries no structured exit code, so `evidence-capture.js`'s rule 3
136
+ * (no signal) would otherwise fire on every codex command, including
137
+ * genuinely failing ones. This injects a deterministic `tool_response.exitCode`
138
+ * extracted from the codex host banner (`Process exited with code N`) BEFORE
139
+ * the payload reaches evidence-capture, so capture stays host-agnostic and
140
+ * never itself scans narration (docs/spec/runtime-hook-surface.md §2.5).
141
+ *
142
+ * Narrowly gated to hookId === 'evidence-capture' && event === 'PostToolUse'
143
+ * so every other codex hook (quality-gate, workflow-steering, stop-goal-fit,
144
+ * ...) is byte-unchanged. Fail-open: any parse error, missing field, or
145
+ * missing/oversized/unreadable rollout returns `raw` unchanged — a hook must
146
+ * never crash the agent, and a miss simply lets evidence-capture's own rule 3
147
+ * (ambiguous default) apply.
148
+ */
149
+ function enrichEvidenceCapturePayload(raw, hookId, event) {
150
+ if (hookId !== 'evidence-capture' || event !== 'PostToolUse') return raw;
151
+ try {
152
+ const payload = JSON.parse(raw);
153
+ if (!payload || typeof payload !== 'object') return raw;
154
+
155
+ // Tier 1: a payload output field, if codex ever surfaces one directly
156
+ // (cheap; observed absent on codex today — checked defensively).
157
+ const outputFields = [
158
+ payload.tool_response && payload.tool_response.output,
159
+ payload.tool_response && payload.tool_response.stdout,
160
+ payload.tool_output && payload.tool_output.output,
161
+ payload.tool_output && payload.tool_output.stdout,
162
+ payload.output,
163
+ ];
164
+ let exitCode = null;
165
+ for (const field of outputFields) {
166
+ if (typeof field !== 'string' || !field) continue;
167
+ const found = extractExitCodeFromBanner(field);
168
+ if (found !== null) { exitCode = found; break; }
169
+ }
170
+
171
+ // Tier 2: bounded tail read of the session rollout at transcript_path.
172
+ if (exitCode === null && typeof payload.transcript_path === 'string' && payload.transcript_path) {
173
+ // No documented codex field correlates a PostToolUse payload to a
174
+ // rollout function_call_output's call_id today; try the plausible
175
+ // candidates defensively and fall back to "last banner in the tail"
176
+ // when none are present (see codex-exit-code.js doc comment). `command`
177
+ // is passed alongside for the command cross-check correlation (Decision
178
+ // B, #470 iteration 2): absent a call_id match, it lets the helper
179
+ // decline (rather than mis-attribute) when the newest rollout banner's
180
+ // paired call doesn't match the command that triggered this PostToolUse.
181
+ const callId = payload.call_id || payload.tool_call_id || payload.id || null;
182
+ const command = payload.tool_input && payload.tool_input.command;
183
+ exitCode = readExitCodeFromRollout(payload.transcript_path, { callId, command });
184
+ }
185
+
186
+ if (exitCode === null) return raw;
187
+
188
+ const existingResponse = payload.tool_response && typeof payload.tool_response === 'object'
189
+ ? payload.tool_response
190
+ : {};
191
+ payload.tool_response = { ...existingResponse, exitCode };
192
+ return JSON.stringify(payload);
193
+ } catch {
194
+ return raw; // fail-open: malformed payload or helper throw — pass through unchanged
195
+ }
196
+ }
197
+
130
198
  async function main() {
131
199
  const [, , hookId, relScriptPath, profilesCsv] = process.argv;
132
200
  const { raw, truncated } = await readStdinRaw();
@@ -138,9 +206,16 @@ async function main() {
138
206
  return;
139
207
  }
140
208
 
209
+ // Enrich only the evidence-capture PostToolUse invocation with a structured
210
+ // exit code extracted from the codex host banner (#470). `effectiveRaw` is
211
+ // used for BOTH the spawn input and the stdout echo-diff below so the two
212
+ // stay consistent (an enriched-vs-original mismatch would leak the raw JSON
213
+ // as additionalContext via guidanceFromStdout).
214
+ const effectiveRaw = enrichEvidenceCapturePayload(raw, hookId, event);
215
+
141
216
  const runHookPath = path.resolve(__dirname, 'run-hook.js');
142
217
  const result = spawnSync(process.execPath, [runHookPath, hookId, relScriptPath, profilesCsv || ''], {
143
- input: raw,
218
+ input: effectiveRaw,
144
219
  encoding: 'utf8',
145
220
  cwd: process.cwd(),
146
221
  env: {
@@ -166,7 +241,7 @@ async function main() {
166
241
  }
167
242
 
168
243
  if (result.stderr) process.stderr.write(result.stderr);
169
- const output = successOutput(event, guidanceFromStdout(raw, result.stdout));
244
+ const output = successOutput(event, guidanceFromStdout(effectiveRaw, result.stdout));
170
245
  if (output) process.stdout.write(`${JSON.stringify(output)}\n`);
171
246
  }
172
247