@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.
- package/.github/workflows/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +173 -0
- package/CHANGELOG.md +54 -0
- package/CONTEXT.md +5 -1
- package/README.md +19 -8
- package/build/src/builder-flow-run-adapter.d.ts +80 -0
- package/build/src/builder-flow-run-adapter.js +241 -0
- package/build/src/builder-flow-runtime.d.ts +16 -0
- package/build/src/builder-flow-runtime.js +290 -0
- package/build/src/cli/assignment-provider.js +10 -1
- package/build/src/cli/builder-run.d.ts +1 -0
- package/build/src/cli/builder-run.js +27 -0
- package/build/src/cli/effective-backlog-settings.js +70 -2
- package/build/src/cli/init.d.ts +34 -0
- package/build/src/cli/init.js +341 -61
- package/build/src/cli/kit.js +55 -12
- package/build/src/cli/pull-work-provider.js +346 -5
- package/build/src/cli/skill-drift-check.d.ts +1 -0
- package/build/src/cli/skill-drift-check.js +165 -0
- package/build/src/cli/telemetry-doctor.d.ts +37 -0
- package/build/src/cli/telemetry-doctor.js +53 -6
- package/build/src/cli/validate-hook-influence.js +37 -7
- package/build/src/cli/workflow-artifact-cleanup-audit.js +418 -11
- package/build/src/cli/workflow-sidecar.d.ts +310 -1
- package/build/src/cli/workflow-sidecar.js +1914 -126
- package/build/src/cli.js +5 -0
- package/build/src/flow-kit/validate.d.ts +54 -34
- package/build/src/flow-kit/validate.js +237 -26
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/console-connect-options.d.ts +97 -0
- package/build/src/lib/console-connect-options.js +199 -0
- package/build/src/lib/console-telemetry-validate.d.ts +49 -0
- package/build/src/lib/console-telemetry-validate.js +91 -0
- package/build/src/lib/flow-resolver.d.ts +54 -1
- package/build/src/lib/flow-resolver.js +112 -5
- package/build/src/lib/fs.d.ts +17 -0
- package/build/src/lib/fs.js +172 -0
- package/build/src/lib/local-artifact-root.d.ts +44 -1
- package/build/src/lib/local-artifact-root.js +131 -3
- package/build/src/runtime-adapters.d.ts +39 -3
- package/build/src/runtime-adapters.js +77 -31
- package/build/src/tools/build-universal-bundles.js +40 -2
- package/build/src/tools/codex-agent-routing.d.ts +2 -0
- package/build/src/tools/codex-agent-routing.js +49 -0
- package/build/src/tools/generate-context-map.js +1 -0
- package/build/src/tools/validate-source-tree.js +30 -3
- package/context/contracts/artifact-contract.md +16 -2
- package/context/scripts/hooks/lib/kit-catalog.js +235 -0
- package/context/scripts/hooks/lib/runnable-command.js +177 -0
- package/context/scripts/hooks/stop-goal-fit.js +278 -48
- package/context/scripts/hooks/workflow-steering.js +194 -22
- package/context/scripts/package.json +3 -0
- package/context/scripts/telemetry/install-console-config.sh +25 -4
- package/context/scripts/telemetry/lib/config.sh +102 -12
- package/context/scripts/telemetry/lib/pricing.sh +50 -0
- package/context/scripts/telemetry/lib/session.sh +3 -0
- package/context/scripts/telemetry/lib/transport.sh +87 -0
- package/context/scripts/telemetry/lib/usage.sh +205 -4
- package/context/scripts/telemetry/telemetry.conf +6 -0
- package/context/scripts/telemetry/telemetry.sh +48 -0
- package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
- package/docs/agent-usage-feedback-loop.md +35 -0
- package/docs/architecture-engine-and-kits.md +110 -0
- package/docs/context-map.md +2 -0
- package/docs/coordination-guide.md +370 -0
- package/docs/decisions/agent-coordination.md +26 -9
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +5 -3
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/trust-reconcile.md +42 -9
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +6 -2
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +23 -9
- package/docs/integrations/flow-agents-console.md +275 -0
- package/docs/integrations/index.md +4 -0
- package/docs/kit-authoring-guide.md +52 -21
- package/docs/spec/builder-flow-runtime.md +80 -0
- package/docs/spec/runtime-hook-surface.md +45 -1
- package/docs/specs/economics-record-contract.md +270 -0
- package/docs/specs/harness-capability-matrix.md +74 -0
- package/docs/specs/learning-review-proposals-contract.md +340 -0
- package/docs/specs/routing-efficiency-review.md +59 -0
- package/docs/verifiable-trust.md +74 -25
- package/docs/workflow-artifact-lifecycle.md +38 -1
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +2 -0
- package/evals/ci/run-baseline.sh +78 -4
- package/evals/fixtures/economics/acceptance.json +12 -0
- package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
- package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
- package/evals/fixtures/economics/critique.json +22 -0
- package/evals/fixtures/economics/expected-record.json +71 -0
- package/evals/fixtures/economics/session-usage-event.json +1 -0
- package/evals/fixtures/economics/state.json +11 -0
- package/evals/fixtures/economics/transcript.jsonl +3 -0
- package/evals/fixtures/hook-influence/cases.json +7 -7
- package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
- package/evals/fixtures/reconcile-preflight/disputed-critique-unsuperseded.json +48 -0
- package/evals/fixtures/reconcile-preflight/standalone-disputed-session-local.json +59 -0
- package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
- package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -0
- package/evals/integration/test_builder_step_producers.sh +18 -10
- package/evals/integration/test_bundle_install.sh +172 -0
- package/evals/integration/test_checkpoint_signing.sh +10 -2
- package/evals/integration/test_ci_actor_identity.sh +221 -0
- package/evals/integration/test_console_tenant_isolation.sh +167 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
- package/evals/integration/test_dual_emit_flow_step.sh +10 -4
- package/evals/integration/test_economics_record.sh +674 -0
- package/evals/integration/test_effective_backlog_settings.sh +1 -1
- package/evals/integration/test_evidence_capture_hook.sh +17 -2
- package/evals/integration/test_exemption_usage_review.sh +198 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +83 -0
- package/evals/integration/test_flowdef_session_activation.sh +0 -1
- package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
- package/evals/integration/test_gate_lockdown.sh +7 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
- package/evals/integration/test_goal_fit_hook.sh +2031 -0
- package/evals/integration/test_hook_category_behaviors.sh +8 -1
- package/evals/integration/test_hook_influence_cases.sh +25 -1
- package/evals/integration/test_install_merge.sh +227 -2
- package/evals/integration/test_kit_conformance_levels.sh +6 -6
- package/evals/integration/test_learning_review_proposals.sh +329 -0
- package/evals/integration/test_liveness_conflict_injection.sh +26 -22
- package/evals/integration/test_liveness_console_relay.sh +166 -0
- package/evals/integration/test_liveness_heartbeat.sh +17 -17
- package/evals/integration/test_liveness_worktree_root.sh +575 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
- package/evals/integration/test_publish_delivery.sh +389 -2
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_reconcile_preflight.sh +304 -0
- package/evals/integration/test_record_check.sh +378 -0
- package/evals/integration/test_routing_efficiency.sh +71 -0
- package/evals/integration/test_runtime_adapter_activation.sh +28 -0
- package/evals/integration/test_session_resume_roundtrip.sh +16 -19
- package/evals/integration/test_skill_drift_check.sh +870 -0
- package/evals/integration/test_takeover_protocol.sh +340 -0
- package/evals/integration/test_telemetry.sh +445 -0
- package/evals/integration/test_telemetry_doctor.sh +66 -0
- package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +121 -13
- package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
- package/evals/integration/test_usage_cost.sh +61 -0
- package/evals/integration/test_verify_hold.sh +910 -0
- package/evals/integration/test_veritas_governance_kit.sh +257 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +575 -3
- package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
- package/evals/integration/test_workflow_steering_hook.sh +157 -16
- package/evals/integration/test_workspace_settings.sh +176 -0
- package/evals/lib/env.sh +26 -0
- package/evals/lib/node.sh +8 -0
- package/evals/run.sh +37 -0
- package/evals/static/test_ci_integration_coverage.sh +115 -0
- package/evals/static/test_declared_scope_forms_documented.sh +114 -0
- package/evals/static/test_universal_bundles.sh +34 -0
- package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
- package/evals/static/test_workflow_skills.sh +1 -1
- package/kits/builder/flows/build.flow.json +9 -18
- package/kits/builder/flows/publish-learn.flow.json +5 -1
- package/kits/builder/kit.json +120 -0
- package/kits/builder/skills/continue-work/SKILL.md +2 -0
- package/kits/builder/skills/deliver/SKILL.md +115 -0
- package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -0
- package/kits/builder/skills/learning-review/SKILL.md +51 -0
- package/kits/builder/skills/plan-work/SKILL.md +17 -20
- package/kits/builder/skills/pull-work/SKILL.md +33 -2
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +113 -7
- package/kits/veritas-governance/fixtures/exemption/approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption/not-approved.trust-bundle.json +74 -0
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/flows/exemption-issuance.flow.json +35 -0
- package/kits/veritas-governance/kit.json +19 -0
- package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
- package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
- package/package.json +2 -2
- package/packaging/manifest.json +29 -0
- package/schemas/backlog-provider-settings.schema.json +13 -0
- package/schemas/workflow-state.schema.json +44 -0
- package/scripts/README.md +4 -0
- package/scripts/check-content-boundary.cjs +8 -1
- package/scripts/ci/trust-reconcile.js +214 -253
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/actor-identity.js +82 -0
- package/scripts/hooks/lib/codex-exit-code.js +316 -0
- package/scripts/hooks/lib/kit-catalog.js +235 -0
- package/scripts/hooks/lib/liveness-write.js +28 -1
- package/scripts/hooks/lib/local-artifact-paths.js +97 -1
- package/scripts/hooks/lib/runnable-command.js +177 -0
- package/scripts/hooks/lib/skill-drift.js +350 -0
- package/scripts/hooks/stop-goal-fit.js +278 -48
- package/scripts/hooks/workflow-steering.js +194 -22
- package/scripts/install-codex-home.sh +97 -47
- package/scripts/install-merge.js +72 -14
- package/scripts/install-owned-files.js +178 -0
- package/scripts/lib/reconcile-shape.js +381 -0
- package/scripts/liveness/relay.sh +84 -0
- package/scripts/telemetry/economics-record.schema.json +145 -0
- package/scripts/telemetry/economics-record.sh +331 -0
- package/scripts/telemetry/install-console-config.sh +25 -4
- package/scripts/telemetry/learning-review-decide.sh +124 -0
- package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
- package/scripts/telemetry/learning-review-proposals.sh +484 -0
- package/scripts/telemetry/lib/config.sh +102 -12
- package/scripts/telemetry/lib/pricing.sh +14 -6
- package/scripts/telemetry/lib/session.sh +3 -0
- package/scripts/telemetry/lib/transport.sh +133 -15
- package/scripts/telemetry/lib/usage.sh +121 -28
- package/scripts/telemetry/routing-efficiency.sh +0 -0
- package/scripts/telemetry/telemetry.conf +6 -0
- package/scripts/telemetry/telemetry.sh +48 -0
- package/src/builder-flow-run-adapter.ts +357 -0
- package/src/builder-flow-runtime.ts +348 -0
- package/src/cli/assignment-provider.ts +12 -1
- package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
- package/src/cli/builder-flow-runtime.test.mjs +213 -0
- package/src/cli/builder-run.ts +28 -0
- package/src/cli/codex-agent-routing.test.mjs +44 -0
- package/src/cli/codex-exit-code.test.mjs +207 -0
- package/src/cli/console-connect-options.test.mjs +329 -0
- package/src/cli/console-telemetry-validate.test.mjs +157 -0
- package/src/cli/effective-backlog-settings.ts +68 -2
- package/src/cli/flow-resolver-composition.test.mjs +72 -0
- package/src/cli/init.test.mjs +161 -0
- package/src/cli/init.ts +407 -62
- package/src/cli/kit-metadata-security.test.mjs +443 -0
- package/src/cli/kit.ts +50 -12
- package/src/cli/pull-work-provider.ts +377 -3
- package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
- package/src/cli/skill-drift-check.ts +196 -0
- package/src/cli/telemetry-doctor.test.mjs +53 -0
- package/src/cli/telemetry-doctor.ts +50 -7
- package/src/cli/validate-hook-influence.ts +37 -6
- package/src/cli/workflow-artifact-cleanup-audit.ts +483 -10
- package/src/cli/workflow-sidecar.ts +1980 -119
- package/src/cli.ts +5 -0
- package/src/flow-kit/validate.ts +277 -38
- package/src/index.ts +19 -0
- package/src/lib/console-connect-options.ts +261 -0
- package/src/lib/console-telemetry-validate.ts +88 -0
- package/src/lib/flow-resolver.ts +117 -4
- package/src/lib/fs.ts +160 -0
- package/src/lib/local-artifact-root.ts +129 -3
- package/src/runtime-adapters.ts +113 -33
- package/src/tools/build-universal-bundles.ts +36 -2
- package/src/tools/codex-agent-routing.ts +48 -0
- package/src/tools/generate-context-map.ts +1 -0
- 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 —
|
|
1228
|
-
//
|
|
1229
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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.
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1310
|
-
//
|
|
1311
|
-
//
|
|
1312
|
-
//
|
|
1313
|
-
//
|
|
1314
|
-
//
|
|
1315
|
-
//
|
|
1316
|
-
//
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
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'` explicitly — this 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
|
-
|
|
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:
|
|
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(
|
|
244
|
+
const output = successOutput(event, guidanceFromStdout(effectiveRaw, result.stdout));
|
|
170
245
|
if (output) process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
171
246
|
}
|
|
172
247
|
|