@kontourai/flow-agents 3.3.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 (256) hide show
  1. package/.github/workflows/add-to-project.yml +15 -0
  2. package/.github/workflows/ci.yml +161 -0
  3. package/CHANGELOG.md +41 -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/builder-run.d.ts +1 -0
  11. package/build/src/cli/builder-run.js +27 -0
  12. package/build/src/cli/effective-backlog-settings.js +70 -2
  13. package/build/src/cli/init.d.ts +34 -0
  14. package/build/src/cli/init.js +341 -61
  15. package/build/src/cli/kit.js +55 -12
  16. package/build/src/cli/pull-work-provider.js +346 -5
  17. package/build/src/cli/skill-drift-check.d.ts +1 -0
  18. package/build/src/cli/skill-drift-check.js +165 -0
  19. package/build/src/cli/telemetry-doctor.d.ts +37 -0
  20. package/build/src/cli/telemetry-doctor.js +53 -6
  21. package/build/src/cli/validate-hook-influence.js +37 -7
  22. package/build/src/cli/workflow-sidecar.d.ts +93 -8
  23. package/build/src/cli/workflow-sidecar.js +1175 -158
  24. package/build/src/cli.js +5 -0
  25. package/build/src/flow-kit/validate.d.ts +54 -34
  26. package/build/src/flow-kit/validate.js +237 -26
  27. package/build/src/index.d.ts +2 -0
  28. package/build/src/index.js +1 -0
  29. package/build/src/lib/console-connect-options.d.ts +97 -0
  30. package/build/src/lib/console-connect-options.js +199 -0
  31. package/build/src/lib/console-telemetry-validate.d.ts +49 -0
  32. package/build/src/lib/console-telemetry-validate.js +91 -0
  33. package/build/src/lib/flow-resolver.d.ts +54 -1
  34. package/build/src/lib/flow-resolver.js +112 -5
  35. package/build/src/lib/fs.d.ts +17 -0
  36. package/build/src/lib/fs.js +172 -0
  37. package/build/src/lib/local-artifact-root.d.ts +44 -1
  38. package/build/src/lib/local-artifact-root.js +131 -3
  39. package/build/src/runtime-adapters.d.ts +39 -3
  40. package/build/src/runtime-adapters.js +77 -31
  41. package/build/src/tools/build-universal-bundles.js +40 -2
  42. package/build/src/tools/codex-agent-routing.d.ts +2 -0
  43. package/build/src/tools/codex-agent-routing.js +49 -0
  44. package/build/src/tools/generate-context-map.js +1 -0
  45. package/build/src/tools/validate-source-tree.js +27 -1
  46. package/context/scripts/hooks/lib/kit-catalog.js +235 -0
  47. package/context/scripts/hooks/lib/runnable-command.js +177 -0
  48. package/context/scripts/hooks/stop-goal-fit.js +278 -48
  49. package/context/scripts/hooks/workflow-steering.js +121 -21
  50. package/context/scripts/package.json +3 -0
  51. package/context/scripts/telemetry/install-console-config.sh +25 -4
  52. package/context/scripts/telemetry/lib/config.sh +102 -12
  53. package/context/scripts/telemetry/lib/pricing.sh +50 -0
  54. package/context/scripts/telemetry/lib/session.sh +3 -0
  55. package/context/scripts/telemetry/lib/transport.sh +87 -0
  56. package/context/scripts/telemetry/lib/usage.sh +205 -4
  57. package/context/scripts/telemetry/telemetry.conf +6 -0
  58. package/context/scripts/telemetry/telemetry.sh +48 -0
  59. package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
  60. package/docs/agent-usage-feedback-loop.md +35 -0
  61. package/docs/architecture-engine-and-kits.md +110 -0
  62. package/docs/context-map.md +2 -0
  63. package/docs/decisions/embeddable-engine.md +152 -0
  64. package/docs/decisions/index.md +3 -1
  65. package/docs/decisions/trust-ledger-retention.md +88 -0
  66. package/docs/decisions/workflow-enforcement.md +31 -9
  67. package/docs/fixture-ownership.md +3 -0
  68. package/docs/implementing-trust-reconciliation.md +129 -0
  69. package/docs/index.md +19 -9
  70. package/docs/integrations/flow-agents-console.md +167 -0
  71. package/docs/kit-authoring-guide.md +52 -21
  72. package/docs/spec/builder-flow-runtime.md +80 -0
  73. package/docs/spec/runtime-hook-surface.md +45 -1
  74. package/docs/specs/economics-record-contract.md +270 -0
  75. package/docs/specs/harness-capability-matrix.md +74 -0
  76. package/docs/specs/learning-review-proposals-contract.md +340 -0
  77. package/docs/specs/routing-efficiency-review.md +59 -0
  78. package/docs/verifiable-trust.md +74 -25
  79. package/docs/workflow-usage-guide.md +10 -0
  80. package/evals/acceptance/prove-capture-teeth.sh +132 -0
  81. package/evals/ci/antigaming-suite.sh +1 -0
  82. package/evals/ci/run-baseline.sh +72 -4
  83. package/evals/fixtures/economics/acceptance.json +12 -0
  84. package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
  85. package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
  86. package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
  87. package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
  88. package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
  89. package/evals/fixtures/economics/critique.json +22 -0
  90. package/evals/fixtures/economics/expected-record.json +71 -0
  91. package/evals/fixtures/economics/session-usage-event.json +1 -0
  92. package/evals/fixtures/economics/state.json +11 -0
  93. package/evals/fixtures/economics/transcript.jsonl +3 -0
  94. package/evals/fixtures/hook-influence/cases.json +7 -7
  95. package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
  96. package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
  97. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
  98. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
  99. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
  100. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
  101. package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
  102. package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
  103. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
  104. package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
  105. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
  106. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
  107. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
  108. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
  109. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
  110. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
  111. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
  112. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
  113. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
  114. package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
  115. package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
  116. package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
  117. package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
  118. package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
  119. package/evals/integration/test_builder_entry_enforcement.sh +241 -0
  120. package/evals/integration/test_builder_step_producers.sh +18 -10
  121. package/evals/integration/test_bundle_install.sh +172 -0
  122. package/evals/integration/test_console_tenant_isolation.sh +167 -0
  123. package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
  124. package/evals/integration/test_dual_emit_flow_step.sh +10 -4
  125. package/evals/integration/test_economics_record.sh +674 -0
  126. package/evals/integration/test_effective_backlog_settings.sh +1 -1
  127. package/evals/integration/test_evidence_capture_hook.sh +17 -2
  128. package/evals/integration/test_exemption_usage_review.sh +198 -0
  129. package/evals/integration/test_fixture_retirement_audit.sh +2 -2
  130. package/evals/integration/test_flow_kit_install_git.sh +83 -0
  131. package/evals/integration/test_flowdef_session_activation.sh +0 -1
  132. package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
  133. package/evals/integration/test_gate_lockdown.sh +7 -0
  134. package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
  135. package/evals/integration/test_goal_fit_hook.sh +2031 -0
  136. package/evals/integration/test_hook_category_behaviors.sh +8 -1
  137. package/evals/integration/test_hook_influence_cases.sh +25 -1
  138. package/evals/integration/test_install_merge.sh +227 -2
  139. package/evals/integration/test_kit_conformance_levels.sh +6 -6
  140. package/evals/integration/test_learning_review_proposals.sh +329 -0
  141. package/evals/integration/test_liveness_conflict_injection.sh +26 -22
  142. package/evals/integration/test_liveness_console_relay.sh +166 -0
  143. package/evals/integration/test_liveness_heartbeat.sh +17 -17
  144. package/evals/integration/test_liveness_worktree_root.sh +575 -0
  145. package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
  146. package/evals/integration/test_publish_delivery.sh +331 -1
  147. package/evals/integration/test_pull_work_board.sh +200 -0
  148. package/evals/integration/test_pull_work_provider.sh +1 -1
  149. package/evals/integration/test_record_check.sh +378 -0
  150. package/evals/integration/test_routing_efficiency.sh +71 -0
  151. package/evals/integration/test_runtime_adapter_activation.sh +28 -0
  152. package/evals/integration/test_session_resume_roundtrip.sh +16 -19
  153. package/evals/integration/test_skill_drift_check.sh +870 -0
  154. package/evals/integration/test_telemetry.sh +445 -0
  155. package/evals/integration/test_telemetry_doctor.sh +66 -0
  156. package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
  157. package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
  158. package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
  159. package/evals/integration/test_usage_cost.sh +61 -0
  160. package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
  161. package/evals/integration/test_workflow_steering_hook.sh +157 -16
  162. package/evals/integration/test_workspace_settings.sh +176 -0
  163. package/evals/lib/env.sh +26 -0
  164. package/evals/lib/node.sh +8 -0
  165. package/evals/run.sh +29 -0
  166. package/evals/static/test_ci_integration_coverage.sh +115 -0
  167. package/evals/static/test_declared_scope_forms_documented.sh +114 -0
  168. package/evals/static/test_universal_bundles.sh +34 -0
  169. package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
  170. package/evals/static/test_workflow_skills.sh +1 -1
  171. package/kits/builder/flows/build.flow.json +9 -18
  172. package/kits/builder/flows/publish-learn.flow.json +5 -1
  173. package/kits/builder/kit.json +120 -0
  174. package/kits/builder/skills/deliver/SKILL.md +42 -0
  175. package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
  176. package/kits/builder/skills/execute-plan/SKILL.md +9 -0
  177. package/kits/builder/skills/learning-review/SKILL.md +51 -0
  178. package/kits/builder/skills/plan-work/SKILL.md +17 -20
  179. package/kits/builder/skills/pull-work/SKILL.md +21 -0
  180. package/kits/builder/skills/release-readiness/SKILL.md +12 -0
  181. package/kits/knowledge/kit.json +9 -0
  182. package/kits/veritas-governance/docs/README.md +35 -7
  183. package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
  184. package/kits/veritas-governance/kit.json +14 -0
  185. package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
  186. package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
  187. package/package.json +2 -2
  188. package/packaging/manifest.json +29 -0
  189. package/schemas/backlog-provider-settings.schema.json +13 -0
  190. package/schemas/workflow-state.schema.json +44 -0
  191. package/scripts/README.md +4 -0
  192. package/scripts/check-content-boundary.cjs +8 -1
  193. package/scripts/ci/trust-reconcile.js +136 -0
  194. package/scripts/hooks/codex-hook-adapter.js +77 -2
  195. package/scripts/hooks/evidence-capture.js +38 -5
  196. package/scripts/hooks/lib/codex-exit-code.js +316 -0
  197. package/scripts/hooks/lib/kit-catalog.js +235 -0
  198. package/scripts/hooks/lib/liveness-write.js +28 -1
  199. package/scripts/hooks/lib/local-artifact-paths.js +97 -1
  200. package/scripts/hooks/lib/runnable-command.js +177 -0
  201. package/scripts/hooks/lib/skill-drift.js +350 -0
  202. package/scripts/hooks/stop-goal-fit.js +278 -48
  203. package/scripts/hooks/workflow-steering.js +121 -21
  204. package/scripts/install-codex-home.sh +97 -47
  205. package/scripts/install-merge.js +72 -14
  206. package/scripts/install-owned-files.js +178 -0
  207. package/scripts/liveness/relay.sh +84 -0
  208. package/scripts/telemetry/economics-record.schema.json +145 -0
  209. package/scripts/telemetry/economics-record.sh +331 -0
  210. package/scripts/telemetry/install-console-config.sh +25 -4
  211. package/scripts/telemetry/learning-review-decide.sh +124 -0
  212. package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
  213. package/scripts/telemetry/learning-review-proposals.sh +484 -0
  214. package/scripts/telemetry/lib/config.sh +102 -12
  215. package/scripts/telemetry/lib/pricing.sh +14 -6
  216. package/scripts/telemetry/lib/session.sh +3 -0
  217. package/scripts/telemetry/lib/transport.sh +133 -15
  218. package/scripts/telemetry/lib/usage.sh +121 -28
  219. package/scripts/telemetry/routing-efficiency.sh +0 -0
  220. package/scripts/telemetry/telemetry.conf +6 -0
  221. package/scripts/telemetry/telemetry.sh +48 -0
  222. package/src/builder-flow-run-adapter.ts +357 -0
  223. package/src/builder-flow-runtime.ts +348 -0
  224. package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
  225. package/src/cli/builder-flow-runtime.test.mjs +213 -0
  226. package/src/cli/builder-run.ts +28 -0
  227. package/src/cli/codex-agent-routing.test.mjs +44 -0
  228. package/src/cli/codex-exit-code.test.mjs +207 -0
  229. package/src/cli/console-connect-options.test.mjs +329 -0
  230. package/src/cli/console-telemetry-validate.test.mjs +157 -0
  231. package/src/cli/effective-backlog-settings.ts +68 -2
  232. package/src/cli/flow-resolver-composition.test.mjs +72 -0
  233. package/src/cli/init.test.mjs +161 -0
  234. package/src/cli/init.ts +407 -62
  235. package/src/cli/kit-metadata-security.test.mjs +443 -0
  236. package/src/cli/kit.ts +50 -12
  237. package/src/cli/pull-work-provider.ts +377 -3
  238. package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
  239. package/src/cli/skill-drift-check.ts +196 -0
  240. package/src/cli/telemetry-doctor.test.mjs +53 -0
  241. package/src/cli/telemetry-doctor.ts +50 -7
  242. package/src/cli/validate-hook-influence.ts +37 -6
  243. package/src/cli/workflow-sidecar.ts +1150 -151
  244. package/src/cli.ts +5 -0
  245. package/src/flow-kit/validate.ts +277 -38
  246. package/src/index.ts +19 -0
  247. package/src/lib/console-connect-options.ts +261 -0
  248. package/src/lib/console-telemetry-validate.ts +88 -0
  249. package/src/lib/flow-resolver.ts +117 -4
  250. package/src/lib/fs.ts +160 -0
  251. package/src/lib/local-artifact-root.ts +129 -3
  252. package/src/runtime-adapters.ts +113 -33
  253. package/src/tools/build-universal-bundles.ts +36 -2
  254. package/src/tools/codex-agent-routing.ts +48 -0
  255. package/src/tools/generate-context-map.ts +1 -0
  256. package/src/tools/validate-source-tree.ts +26 -1
@@ -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
@@ -673,6 +692,111 @@ function isShaInRange(repoRoot, sha, from, to) {
673
692
  return isAncestorCommit(repoRoot, from, sha) && isAncestorCommit(repoRoot, sha, to);
674
693
  }
675
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
+
676
800
  /**
677
801
  * Match a SINGLE scope condition (one of the four forms) against the resolved context.
678
802
  * Extracted from matchesScope() so a compound (space-separated, AND) scope can validate
@@ -910,6 +1034,13 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null, mani
910
1034
  // to pick, among per-session candidates, the one that attests THIS change.
911
1035
  const scopeCtx = resolveScopeContext(resolvedRepoRoot);
912
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
+
913
1044
  let bundlePath = explicitBundlePath || discoverBundle(resolvedRepoRoot, scopeCtx.sha) || null;
914
1045
  const bundleWasAutoDiscovered = !explicitBundlePath && !!bundlePath;
915
1046
 
@@ -1321,6 +1452,11 @@ module.exports.noEvidenceCommandIssues = noEvidenceCommandIssues;
1321
1452
  module.exports.reconcilableManifestIssues = reconcilableManifestIssues;
1322
1453
  module.exports.sessionLocalShapeIssues = sessionLocalShapeIssues;
1323
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
+
1324
1460
  if (require.main === module) {
1325
1461
  main();
1326
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
 
@@ -12,7 +12,12 @@
12
12
  * Records to `.kontourai/flow-agents/<slug>/command-log.jsonl`, one JSON object per line:
13
13
  * {
14
14
  * "command": "<the command string the agent ran>",
15
- * "observedResult": "pass" | "fail", // deterministically inferred
15
+ * "observedResult": "pass" | "fail" | "ambiguous", // deterministically inferred
16
+ * // ("ambiguous": a bare, non-self-asserting
17
+ * // grep/diff exited 1 — could mean zero
18
+ * // matches/no differences (PASS for an
19
+ * // absence check) or an unintended miss
20
+ * // (FAIL for a presence check); #362)
16
21
  * "exitCode": <integer> | null, // null when only pass/fail is inferable
17
22
  * "capturedAt": "<ISO-8601 timestamp>",
18
23
  * "source": "postToolUse-capture",
@@ -38,7 +43,10 @@
38
43
  * (per docs/spec/runtime-hook-surface.md §1, postToolUse). A clean integer exit
39
44
  * code is host-dependent. We extract the real exit code where present; otherwise
40
45
  * we derive `observedResult` from `error`/stderr-style failure indication and
41
- * record `exitCode: null`. We never record the model's words about the outcome.
46
+ * record `exitCode: null`. Absent positive success evidence (a clean integer
47
+ * exit code of 0; no host currently surfaces a positive success flag), the
48
+ * result is 'ambiguous', never 'pass'. We never record the model's words about
49
+ * the outcome.
42
50
  *
43
51
  * Non-blocking — always exits 0. Idempotent/append-only. Fail-open on any error:
44
52
  * a capture failure must never block the agent or corrupt the log.
@@ -51,6 +59,7 @@ const path = require('path');
51
59
  const { flowAgentsArtifactRootsForRead } = require('./lib/local-artifact-paths');
52
60
  const { resolveActor } = require('./lib/actor-identity.js');
53
61
  const { readCurrentPointer } = require('./lib/current-pointer.js');
62
+ const { isAmbiguousAbsenceCommand } = require('./lib/runnable-command.js');
54
63
  const crypto = require('crypto');
55
64
 
56
65
  const MAX_STDIN = 1024 * 1024;
@@ -248,15 +257,32 @@ function cleanExitCode(value) {
248
257
  *
249
258
  * Priority:
250
259
  * 1. A clean integer exit code anywhere the host surfaces it → exitCode set;
251
- * observedResult = pass iff exitCode === 0.
260
+ * observedResult = pass iff exitCode === 0, EXCEPT (#362) a bare,
261
+ * non-self-asserting `grep`/`diff` invocation (per
262
+ * `isAmbiguousAbsenceCommand`, ./lib/runnable-command.js) that exits
263
+ * EXACTLY 1 → observedResult = 'ambiguous' instead of 'fail'. Exit 1 for
264
+ * such a command could mean zero matches/no differences (PASS for an
265
+ * absence check) or an unintended miss (FAIL for a presence check) —
266
+ * this is never coerced to 'pass' (that would trade a false-block for a
267
+ * false-pass) nor silently left as 'fail' (that is the exact false
268
+ * caught-completion #362 reports). Exit codes >= 2 for these two
269
+ * binaries are real tool errors and remain 'fail', unchanged.
252
270
  * 2. Else, a non-empty `error` field or stderr-style failure indication →
253
271
  * observedResult = fail, exitCode = null.
254
- * 3. Else → observedResult = pass, exitCode = null.
272
+ * 3. Else → observedResult = ambiguous, exitCode = null. Absent positive
273
+ * success evidence (a clean integer exit code of 0; no host currently
274
+ * surfaces a positive success flag), the result is 'ambiguous', never
275
+ * 'pass'.
276
+ *
277
+ * `input.command` (the raw command string, already in scope at the run()
278
+ * call site) is required to evaluate the #362 carve-out; when absent, the
279
+ * carve-out simply never fires and behavior is byte-identical to before #362.
255
280
  */
256
281
  function observeResult(input) {
257
282
  const response = input.tool_response;
258
283
  const output = input.tool_output;
259
284
  const error = input.error;
285
+ const command = typeof input.command === 'string' ? input.command : '';
260
286
 
261
287
  // Candidate locations for a host-provided exit code.
262
288
  const candidates = [];
@@ -274,6 +300,9 @@ function observeResult(input) {
274
300
  }
275
301
 
276
302
  if (exitCode !== null) {
303
+ if (exitCode === 1 && command && isAmbiguousAbsenceCommand(command)) {
304
+ return { exitCode, observedResult: 'ambiguous' };
305
+ }
277
306
  return { exitCode, observedResult: exitCode === 0 ? 'pass' : 'fail' };
278
307
  }
279
308
 
@@ -281,7 +310,7 @@ function observeResult(input) {
281
310
  if (isFailureIndicated(error, response, output)) {
282
311
  return { exitCode: null, observedResult: 'fail' };
283
312
  }
284
- return { exitCode: null, observedResult: 'pass' };
313
+ return { exitCode: null, observedResult: 'ambiguous' };
285
314
  }
286
315
 
287
316
  // True when the host surfaces a deterministic failure signal: a non-empty
@@ -326,6 +355,10 @@ function run(rawInput) {
326
355
  exit_code: input.exit_code,
327
356
  status: input.status,
328
357
  code: input.code,
358
+ // #362: thread the raw command string through so observeResult can apply the
359
+ // bare-grep/diff-exit-1 ambiguous carve-out. `command` is already in scope here
360
+ // (extracted above from input.tool_input.command).
361
+ command,
329
362
  });
330
363
 
331
364
  const record = {