@kontourai/flow-agents 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CODEOWNERS +8 -0
- package/.github/workflows/ci.yml +32 -0
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +46 -0
- package/CONTEXT.md +14 -0
- package/README.md +9 -3
- package/agents/dev.json +1 -1
- package/agents/tool-code-reviewer.json +1 -1
- package/agents/tool-planner.json +3 -3
- package/agents/tool-verifier.json +3 -3
- package/build/src/cli/console-learning-projection.js +3 -2
- package/build/src/cli/init.js +104 -21
- package/build/src/cli/kit.js +37 -6
- package/build/src/cli/pull-work-provider.js +1 -1
- package/build/src/cli/usage-feedback.js +3 -3
- package/build/src/cli/validate-hook-influence.js +1 -0
- package/build/src/cli/validate-workflow-artifacts.js +22 -6
- package/build/src/cli/workflow-artifact-cleanup-audit.js +36 -2
- package/build/src/cli/workflow-sidecar.d.ts +14 -7
- package/build/src/cli/workflow-sidecar.js +792 -69
- package/build/src/flow-kit/validate.d.ts +23 -0
- package/build/src/flow-kit/validate.js +67 -1
- package/build/src/index.d.ts +1 -1
- package/build/src/index.js +1 -1
- package/build/src/lib/flow-resolver.d.ts +6 -4
- package/build/src/lib/flow-resolver.js +125 -58
- package/build/src/lib/local-artifact-root.d.ts +14 -2
- package/build/src/lib/local-artifact-root.js +22 -5
- package/build/src/lib/workflow-learning-projection.js +2 -2
- package/build/src/runtime-adapters.d.ts +12 -0
- package/build/src/runtime-adapters.js +56 -16
- package/build/src/tools/build-universal-bundles.js +49 -16
- package/build/src/tools/generate-context-map.js +10 -8
- package/build/src/tools/validate-source-tree.js +23 -1
- package/console.telemetry.json +22 -0
- package/context/contracts/artifact-contract.md +38 -7
- package/context/contracts/builder-kit-workflow-state-contract.md +3 -2
- package/context/contracts/decision-registry-contract.md +263 -0
- package/context/contracts/delivery-contract.md +1 -1
- package/context/contracts/planning-contract.md +2 -1
- package/context/contracts/review-contract.md +5 -3
- package/context/contracts/verification-contract.md +9 -1
- package/context/contracts/work-item-contract.md +1 -1
- package/context/deferred/parallelization.md +1 -1
- package/context/gate-awareness.md +4 -4
- package/context/scripts/git-status.sh +2 -2
- package/context/scripts/hooks/config-protection.js +502 -2
- package/context/scripts/hooks/lib/config-protection-remedies.js +69 -0
- package/context/scripts/hooks/run-hook.js +14 -1
- package/context/scripts/hooks/stop-goal-fit.js +1140 -99
- package/context/scripts/hooks/workflow-steering.js +212 -6
- package/docs/adr/0007-flow-skill-kit-tool-boundary.md +2 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +35 -0
- package/docs/adr/0019-kit-dependency-ownership.md +62 -0
- package/docs/adr/0020-trust-reconcile-manifest-and-claim-classification.md +290 -0
- package/docs/adr/0021-assignment-leases-and-stale-claim-takeover.md +221 -0
- package/docs/adr/0022-fail-closed-delivery-reconciliation-with-governed-exemptions.md +283 -0
- package/docs/agent-system-guidebook.md +3 -3
- package/docs/agent-usage-feedback-loop.md +6 -6
- package/docs/context-map.md +9 -7
- package/docs/decisions/decision-records.md +57 -0
- package/docs/decisions/index.md +14 -0
- package/docs/decisions/promotion-gate.md +52 -0
- package/docs/developer-architecture.md +1 -1
- package/docs/fixture-ownership.md +3 -0
- package/docs/flow-kit-repository-contract.md +5 -5
- package/docs/getting-started.md +12 -2
- package/docs/integrations/harness-install.md +2 -1
- package/docs/kit-authoring-guide.md +64 -5
- package/docs/knowledge-kit.md +5 -1
- package/docs/learnings/2026-07-improvement-program.md +203 -0
- package/docs/migrations.md +6 -5
- package/docs/north-star.md +2 -2
- package/docs/operating-layers.md +4 -3
- package/docs/repository-structure.md +5 -5
- package/docs/skills-map.md +8 -8
- package/docs/spec/runtime-hook-surface.md +19 -15
- package/docs/spikes/graph-provider-2026-07.md +155 -0
- package/docs/standards-register.md +7 -7
- package/docs/trust-anchor-adoption.md +105 -4
- package/docs/work-item-adapters.md +1 -1
- package/docs/workflow-artifact-lifecycle.md +72 -9
- package/docs/workflow-eval-strategy.md +3 -3
- package/docs/workflow-shared-contracts.md +1 -1
- package/docs/workflow-usage-guide.md +254 -21
- package/evals/acceptance/DEMO-false-completion.md +20 -11
- package/evals/acceptance/prove-capture-teeth-declared.sh +15 -15
- package/evals/acceptance/prove-capture-teeth.sh +11 -11
- package/evals/acceptance/test_kiro_harness.sh +1 -1
- package/evals/ci/antigaming-suite.sh +5 -0
- package/evals/ci/run-baseline.sh +65 -1
- package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/invalid-bad-dependency/kit.json +13 -0
- package/evals/fixtures/flow-kit-repository/valid-with-dependency/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/valid-with-dependency/kit.json +13 -0
- package/evals/fixtures/hook-influence/cases.json +32 -0
- package/evals/fixtures/pull-work-provider/github-issues.json +6 -6
- package/evals/fixtures/trust-reconcile-exploits/fabricated-attestation.json +59 -0
- package/evals/fixtures/trust-reconcile-exploits/no-label-bypass.json +36 -0
- package/evals/fixtures/trust-reconcile-exploits/skip-assumed-bypass.json +59 -0
- package/evals/fixtures/trust-reconcile-exploits/status-misassertion.json +48 -0
- package/evals/fixtures/trust-reconcile-exploits/waived-command-check.json +44 -0
- package/evals/fixtures/trust-reconcile-mixed-bundle/mixed-bundle.json +159 -0
- package/evals/fixtures/trust-reconcile-ws3/ws3-bundle.json +920 -0
- package/evals/integration/test_actor_identity.sh +254 -0
- package/evals/integration/test_builder_step_producers.sh +33 -23
- package/evals/integration/test_bundle_install.sh +22 -16
- package/evals/integration/test_bundle_lifecycle.sh +11 -11
- package/evals/integration/test_captured_fail_reconciliation.sh +50 -50
- package/evals/integration/test_checkpoint_signing.sh +43 -0
- package/evals/integration/test_claim_lookup.sh +4 -4
- package/evals/integration/test_codex_hook_resolution.sh +114 -0
- package/evals/integration/test_command_log_concurrency.sh +8 -8
- package/evals/integration/test_command_log_fork_classification.sh +3 -3
- package/evals/integration/test_command_log_integrity.sh +16 -16
- package/evals/integration/test_console_learning_projection.sh +1 -1
- package/evals/integration/test_dual_emit_flow_step.sh +145 -0
- package/evals/integration/test_enforcer_expects_driven.sh +12 -12
- package/evals/integration/test_evidence_capture_hook.sh +14 -13
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_agents_statusline.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +52 -0
- package/evals/integration/test_flow_kit_repository.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +6 -6
- package/evals/integration/test_flowdef_union_floor_regression.sh +429 -0
- package/evals/integration/test_gate_bypass_chain.sh +52 -40
- package/evals/integration/test_gate_lockdown.sh +97 -73
- package/evals/integration/test_gate_review_inquiry_records.sh +3 -3
- package/evals/integration/test_goal_fit_escape_hatch.sh +10 -10
- package/evals/integration/test_goal_fit_ghost_session.sh +104 -0
- package/evals/integration/test_goal_fit_hook.sh +32 -32
- package/evals/integration/test_goal_fit_rederive.sh +9 -9
- package/evals/integration/test_hook_category_behaviors.sh +103 -0
- package/evals/integration/test_hook_influence_cases.sh +1 -0
- package/evals/integration/test_install_merge.sh +262 -1
- package/evals/integration/test_kit_identity_trust.sh +6 -6
- package/evals/integration/test_liveness_conflict_injection.sh +587 -0
- package/evals/integration/test_liveness_heartbeat.sh +677 -0
- package/evals/integration/test_liveness_verdict.sh +394 -0
- package/evals/integration/test_local_flow_kit_install.sh +15 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +63 -11
- package/evals/integration/test_promote_gate.sh +178 -0
- package/evals/integration/test_publish_delivery.sh +2 -2
- package/evals/integration/test_pull_work_liveness_preflight.sh +386 -0
- package/evals/integration/test_pull_work_provider.sh +2 -2
- package/evals/integration/test_reconcile_soundness.sh +4 -4
- package/evals/integration/test_resolvefirststep_security.sh +22 -0
- package/evals/integration/test_runtime_adapter_activation.sh +87 -18
- package/evals/integration/test_session_resume_roundtrip.sh +147 -0
- package/evals/integration/test_sidecar_field_preservation.sh +249 -0
- package/evals/integration/test_trust_reconcile.sh +2 -2
- package/evals/integration/test_trust_reconcile_manifest.sh +79 -0
- package/evals/integration/test_trust_reconcile_mixed_bundle.sh +96 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +151 -0
- package/evals/integration/test_usage_feedback_global.sh +5 -5
- package/evals/integration/test_validate_artifacts_portability.sh +60 -0
- package/evals/integration/test_verify_cli.sh +2 -2
- package/evals/integration/test_veritas_governance_kit.sh +117 -0
- package/evals/integration/test_workflow_artifact_cleanup_audit.sh +26 -1
- package/evals/integration/test_workflow_artifacts.sh +33 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +723 -74
- package/evals/integration/test_workflow_steering_hook.sh +175 -5
- package/evals/lib/codex-provider.sh +1 -1
- package/evals/lib/node.sh +19 -1
- package/evals/run.sh +22 -0
- package/evals/static/test_decisions.sh +150 -0
- package/evals/static/test_flowdef_codeowners_coverage.sh +56 -0
- package/evals/static/test_package.sh +19 -10
- package/evals/static/test_universal_bundles.sh +48 -3
- package/evals/static/test_workflow_skills.sh +41 -8
- package/kits/builder/flows/build.flow.json +3 -74
- package/kits/builder/flows/publish-learn.flow.json +90 -0
- package/kits/builder/kit.json +11 -0
- package/kits/builder/skills/builder-shape/SKILL.md +1 -1
- package/kits/builder/skills/deliver/SKILL.md +13 -11
- package/kits/builder/skills/design-probe/SKILL.md +4 -4
- package/kits/builder/skills/evidence-gate/SKILL.md +26 -1
- package/kits/builder/skills/execute-plan/SKILL.md +2 -2
- package/kits/builder/skills/fix-bug/SKILL.md +2 -0
- package/kits/builder/skills/gate-review/SKILL.md +6 -6
- package/kits/builder/skills/idea-to-backlog/SKILL.md +1 -1
- package/kits/builder/skills/learning-review/SKILL.md +7 -7
- package/kits/builder/skills/pickup-probe/SKILL.md +5 -4
- package/kits/builder/skills/plan-work/SKILL.md +27 -5
- package/kits/builder/skills/pull-work/SKILL.md +80 -5
- package/kits/builder/skills/release-readiness/SKILL.md +4 -4
- package/kits/builder/skills/review-work/SKILL.md +24 -1
- package/kits/builder/skills/tdd-workflow/SKILL.md +2 -0
- package/kits/builder/skills/verify-work/SKILL.md +8 -1
- package/kits/catalog.json +6 -0
- package/kits/veritas-governance/adapter/readiness-to-trust-bundle.mjs +178 -0
- package/kits/veritas-governance/docs/README.md +75 -0
- package/kits/veritas-governance/fixtures/readiness/not-ready.readiness-report.json +4645 -0
- package/kits/veritas-governance/fixtures/readiness/ready.readiness-report.json +4403 -0
- package/kits/veritas-governance/flows/readiness-check.flow.json +35 -0
- package/kits/veritas-governance/kit.json +16 -0
- package/package.json +8 -4
- package/packaging/README.md +2 -2
- package/packaging/conformance/fixtures/config-protection--allow-read-state-json.json +20 -0
- package/packaging/conformance/fixtures/config-protection--block-write-state-json.json +20 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +1 -1
- package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +1 -1
- package/packaging/manifest.json +9 -10
- package/schemas/decision-record.schema.json +109 -0
- package/schemas/workflow-evidence.schema.json +1 -1
- package/schemas/workflow-state.schema.json +5 -0
- package/scripts/README.md +8 -3
- package/scripts/check-content-boundary.cjs +1 -1
- package/scripts/check-decisions.cjs +356 -0
- package/scripts/ci/derive-claim-status.mjs +74 -0
- package/scripts/ci/trust-reconcile.js +436 -79
- package/scripts/git-status.sh +2 -2
- package/scripts/hooks/claude-telemetry-hook.js +37 -3
- package/scripts/hooks/codex-telemetry-hook.js +36 -2
- package/scripts/hooks/config-protection.js +102 -30
- package/scripts/hooks/evidence-capture.js +3 -3
- package/scripts/hooks/lib/actor-identity.js +368 -0
- package/scripts/hooks/lib/config-protection-remedies.js +69 -0
- package/scripts/hooks/lib/liveness-heartbeat.js +361 -0
- package/scripts/hooks/lib/liveness-policy.js +127 -0
- package/scripts/hooks/lib/liveness-read.js +105 -12
- package/scripts/hooks/lib/liveness-write.js +46 -0
- package/scripts/hooks/lib/local-artifact-paths.js +12 -6
- package/scripts/hooks/opencode-telemetry-hook.js +27 -1
- package/scripts/hooks/pi-telemetry-hook.js +27 -1
- package/scripts/hooks/stop-goal-fit.js +157 -26
- package/scripts/hooks/workflow-steering.js +77 -3
- package/scripts/install-codex-home.sh +138 -38
- package/scripts/statusline/flow-agents-statusline.js +2 -2
- package/src/cli/console-learning-projection.ts +3 -2
- package/src/cli/init.ts +101 -21
- package/src/cli/kit.ts +37 -6
- package/src/cli/public-api.test.mjs +58 -6
- package/src/cli/pull-work-provider.ts +1 -1
- package/src/cli/trust-bundle-policy-order.test.mjs +87 -0
- package/src/cli/usage-feedback.ts +3 -3
- package/src/cli/validate-hook-influence.ts +1 -0
- package/src/cli/validate-workflow-artifacts.ts +22 -6
- package/src/cli/veritas-readiness-adapter.test.mjs +267 -0
- package/src/cli/workflow-artifact-cleanup-audit.ts +35 -2
- package/src/cli/workflow-sidecar.ts +795 -71
- package/src/flow-kit/validate.ts +74 -1
- package/src/index.ts +6 -2
- package/src/lib/flow-resolver.ts +123 -55
- package/src/lib/local-artifact-root.ts +24 -5
- package/src/lib/workflow-learning-projection.ts +2 -2
- package/src/runtime-adapters.ts +61 -15
- package/src/tools/build-universal-bundles.ts +49 -16
- package/src/tools/generate-context-map.ts +10 -8
- package/src/tools/validate-source-tree.ts +23 -1
|
@@ -32,9 +32,12 @@
|
|
|
32
32
|
* --repo-root <path> Repository root. Default: TRUST_RECONCILE_REPO_ROOT or cwd.
|
|
33
33
|
*
|
|
34
34
|
* Environment fallbacks:
|
|
35
|
-
* TRUST_RECONCILE_BUNDLE
|
|
36
|
-
* TRUST_RECONCILE_COMMANDS
|
|
37
|
-
* TRUST_RECONCILE_REPO_ROOT
|
|
35
|
+
* TRUST_RECONCILE_BUNDLE Path to delivered bundle (same as --bundle).
|
|
36
|
+
* TRUST_RECONCILE_COMMANDS Comma- or newline-separated canonical commands.
|
|
37
|
+
* TRUST_RECONCILE_REPO_ROOT Repository root.
|
|
38
|
+
* TRUST_RECONCILE_COMMAND_TIMEOUT_MS Timeout (ms) for each canonical/manifest command
|
|
39
|
+
* run (Step 1 fresh-verify + on-demand manifest
|
|
40
|
+
* reconcile). Default: 600000 (10 min).
|
|
38
41
|
*
|
|
39
42
|
* Auto-discovery (when no --bundle or TRUST_RECONCILE_BUNDLE is set):
|
|
40
43
|
* Checks delivery/trust.bundle, then delivery/trust.checkpoint.json under repo root.
|
|
@@ -88,103 +91,218 @@ function isPassingValue(v) {
|
|
|
88
91
|
// CI reconciler and the stop-goal-fit verifier apply the identical exit-code-mask
|
|
89
92
|
// heuristic — see that module for the rules.
|
|
90
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Default manifest/canonical-command execution timeout (ms). Overridable via
|
|
96
|
+
* TRUST_RECONCILE_COMMAND_TIMEOUT_MS. 10 minutes is comfortably above the slowest
|
|
97
|
+
* required-lane check (this repo's own CI lanes allow up to ~10 min); the prior
|
|
98
|
+
* hardcoded 180000ms (3 min) killed legitimately slow evals (e.g.
|
|
99
|
+
* evals/integration/test_workflow_sidecar_writer.sh, ~150s on a fast Mac, >180s on
|
|
100
|
+
* GitHub Actions runners) mid-run, producing a spurious divergence rather than a real
|
|
101
|
+
* failure.
|
|
102
|
+
*/
|
|
103
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 600000;
|
|
104
|
+
|
|
105
|
+
/** Resolve the manifest/canonical-command timeout: TRUST_RECONCILE_COMMAND_TIMEOUT_MS env, else default. */
|
|
106
|
+
function resolveCommandTimeoutMs() {
|
|
107
|
+
const raw = process.env.TRUST_RECONCILE_COMMAND_TIMEOUT_MS;
|
|
108
|
+
if (raw !== undefined && raw !== '') {
|
|
109
|
+
const parsed = Number(raw);
|
|
110
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
111
|
+
}
|
|
112
|
+
return DEFAULT_COMMAND_TIMEOUT_MS;
|
|
113
|
+
}
|
|
114
|
+
|
|
91
115
|
/**
|
|
92
116
|
* Run a single shell command under bash, capturing exit code.
|
|
93
|
-
* @returns {{ cmd, exitCode, passed, stdout, stderr }}
|
|
117
|
+
* @returns {{ cmd, exitCode, passed, timedOut, timeoutMs, stdout, stderr }}
|
|
94
118
|
*/
|
|
95
119
|
function runCommand(cmd, repoRoot) {
|
|
120
|
+
const timeoutMs = resolveCommandTimeoutMs();
|
|
96
121
|
const result = spawnSync('bash', ['-c', cmd], {
|
|
97
122
|
cwd: repoRoot,
|
|
98
123
|
encoding: 'utf8',
|
|
99
|
-
timeout:
|
|
124
|
+
timeout: timeoutMs,
|
|
100
125
|
killSignal: 'SIGKILL',
|
|
101
126
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
102
127
|
});
|
|
103
128
|
const exitCode = (result.status !== null && result.status !== undefined)
|
|
104
129
|
? result.status
|
|
105
130
|
: 1;
|
|
131
|
+
// spawnSync sets result.error (ETIMEDOUT) when the timeout kills the process — this is
|
|
132
|
+
// NOT a real command failure and must be distinguishable in the FAIL log line so a
|
|
133
|
+
// future reader chases the timeout config, not a phantom test bug.
|
|
134
|
+
const timedOut = !!(result.error && result.error.code === 'ETIMEDOUT');
|
|
106
135
|
return {
|
|
107
136
|
cmd,
|
|
108
137
|
exitCode,
|
|
109
138
|
passed: exitCode === 0 && !result.error,
|
|
139
|
+
timedOut,
|
|
140
|
+
timeoutMs,
|
|
110
141
|
stdout: result.stdout || '',
|
|
111
142
|
stderr: result.stderr || '',
|
|
112
143
|
};
|
|
113
144
|
}
|
|
114
145
|
|
|
115
146
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
147
|
+
* Classify a trust.bundle's claims into: reconcilable command claims (test_output +
|
|
148
|
+
* execution.label), session-local claims (attestation/observation/citation), never-captured
|
|
149
|
+
* or unbacked command claims (not-run divergence), and command-backed claims carrying a
|
|
150
|
+
* waiver (waiver-on-command divergence). Returns
|
|
151
|
+
* { reconcilable, sessionLocal, noEvidenceCommand, waiverOnCommand }.
|
|
118
152
|
*
|
|
119
|
-
* Source of truth: evidence[].execution.label is the command string recorded at
|
|
120
|
-
*
|
|
153
|
+
* Source of truth: evidence[].execution.label is the command string recorded at capture time.
|
|
154
|
+
* evidence[].passing (normalized) means the agent claimed this passed. `claim.status` is NOT
|
|
155
|
+
* trusted here — the caller re-derives it CI-side (see derive-claim-status.mjs / finding-3).
|
|
121
156
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
157
|
+
* WS8 iteration-2 hardening:
|
|
158
|
+
* - finding 1: ANY pass-asserting claim whose evidence is `evidenceType: test_output`
|
|
159
|
+
* (Surface's default when unset) but which did NOT reconcile — i.e. it has no
|
|
160
|
+
* manifest-matchable execution.label — is a divergence, NOT session-local. A test_output
|
|
161
|
+
* claim either reconciles against the manifest or is a divergence; it is never accepted on
|
|
162
|
+
* self-reported status. (Previously only the literal claimType `workflow.check.command`
|
|
163
|
+
* was guarded, so a fabricated kind:"test" claim with no command slipped through.)
|
|
164
|
+
* - finding 4: a command-backed (test_output) claim carrying a waiver is a divergence — a
|
|
165
|
+
* command-backed check reconciles against CI or fails; it cannot be waived.
|
|
125
166
|
*/
|
|
126
|
-
function
|
|
167
|
+
function classifyBundleClaims(bundle) {
|
|
127
168
|
const evidence = Array.isArray(bundle.evidence) ? bundle.evidence : [];
|
|
128
169
|
const claims = Array.isArray(bundle.claims) ? bundle.claims : [];
|
|
129
170
|
|
|
130
|
-
// Build a map from claimId -> claim for fast lookup
|
|
131
171
|
const claimById = new Map();
|
|
172
|
+
for (const c of claims) if (c && c.id) claimById.set(c.id, c);
|
|
173
|
+
|
|
174
|
+
// Evidence indexing. A missing evidenceType defaults to test_output for backward
|
|
175
|
+
// compatibility with pre-classification bundles (same default classifyEvidence uses).
|
|
176
|
+
const claimHasLabeledTestOutput = new Set(); // test_output evidence WITH an execution.label
|
|
177
|
+
const claimHasTestOutputEvidence = new Set(); // ANY test_output evidence (label or not)
|
|
178
|
+
// WS8 iteration-4 (converged finding): the session-local (non-test_output) evidenceType per
|
|
179
|
+
// claim, so the reconciler can name it on the loud ATTESTED marker below — a fabricated
|
|
180
|
+
// human_attestation/attestation/external claim with no --command is otherwise
|
|
181
|
+
// indistinguishable, in the reconciler's own output, from a genuinely re-runnable check.
|
|
182
|
+
const claimEvidenceType = new Map();
|
|
183
|
+
for (const ev of evidence) {
|
|
184
|
+
if (!ev || !ev.claimId) continue;
|
|
185
|
+
const evType = ev.evidenceType || 'test_output';
|
|
186
|
+
if (evType !== 'test_output') {
|
|
187
|
+
if (!claimEvidenceType.has(ev.claimId)) claimEvidenceType.set(ev.claimId, evType);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
claimHasTestOutputEvidence.add(ev.claimId);
|
|
191
|
+
if (ev.execution && ev.execution.label) claimHasLabeledTestOutput.add(ev.claimId);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// finding 4: a command-backed (test_output-evidence) claim that also carries a waiver.
|
|
195
|
+
const waiverOnCommand = [];
|
|
132
196
|
for (const c of claims) {
|
|
133
|
-
if (c
|
|
197
|
+
if (!c || !c.id) continue;
|
|
198
|
+
const waiver = (c.metadata && typeof c.metadata === 'object') ? c.metadata.waiver : undefined;
|
|
199
|
+
if (waiver && typeof waiver === 'object' && claimHasTestOutputEvidence.has(c.id)) {
|
|
200
|
+
waiverOnCommand.push({ claimId: c.id, claimType: String(c.claimType || ''), subject: c.subjectId || c.fieldOrBehavior || c.id });
|
|
201
|
+
}
|
|
134
202
|
}
|
|
135
203
|
|
|
136
|
-
|
|
204
|
+
// (A) Reconcilable claimed-passes: evidence items that are test_output (CI-reconcilable),
|
|
205
|
+
// carry an execution.label, and assert pass. Session-local evidenceTypes
|
|
206
|
+
// (crawl_observation, human_attestation, attestation, policy_rule, source_excerpt,
|
|
207
|
+
// document_citation, calculation_trace) are NOT reconciled per-command — they are handled
|
|
208
|
+
// by the session-local/waiver path below.
|
|
209
|
+
const reconcilable = [];
|
|
210
|
+
const reconcilableClaimIds = new Set();
|
|
137
211
|
const seen = new Set();
|
|
138
|
-
|
|
139
|
-
// (A) Evidence items with execution.label and a passing value
|
|
140
212
|
for (const ev of evidence) {
|
|
141
213
|
if (!ev || !ev.execution || !ev.execution.label) continue;
|
|
142
|
-
if (!isPassingValue(ev.passing)) continue;
|
|
143
|
-
|
|
214
|
+
if (!isPassingValue(ev.passing)) continue;
|
|
215
|
+
const evType = ev.evidenceType || 'test_output';
|
|
216
|
+
if (evType !== 'test_output') continue; // session-local — not CI-reconcilable
|
|
144
217
|
const cmd = normalizeCmd(ev.execution.label);
|
|
145
|
-
if (!cmd
|
|
218
|
+
if (!cmd) continue;
|
|
219
|
+
reconcilableClaimIds.add(ev.claimId);
|
|
220
|
+
if (seen.has(cmd)) continue;
|
|
146
221
|
seen.add(cmd);
|
|
147
|
-
|
|
148
222
|
const claim = claimById.get(ev.claimId);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
commands.push({ cmd, claimId: ev.claimId, evId: ev.id, claimType });
|
|
223
|
+
reconcilable.push({ cmd, claimId: ev.claimId, evId: ev.id, claimType: claim ? String(claim.claimType || '') : '' });
|
|
152
224
|
}
|
|
153
225
|
|
|
154
|
-
// (B)
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
.filter(ev => ev && ev.claimId && ev.execution && ev.execution.label)
|
|
159
|
-
.map(ev => ev.claimId)
|
|
160
|
-
);
|
|
161
|
-
|
|
226
|
+
// (B) Session-local claims, never-captured command claims, and unreconciled test_output.
|
|
227
|
+
const sessionLocal = [];
|
|
228
|
+
const noEvidenceCommand = [];
|
|
229
|
+
const seenClaims = new Set();
|
|
162
230
|
for (const c of claims) {
|
|
163
231
|
if (!c || !c.id || typeof c.claimType !== 'string') continue;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
232
|
+
if (reconcilableClaimIds.has(c.id)) continue; // handled by (A)
|
|
233
|
+
if (seenClaims.has(c.id)) continue;
|
|
234
|
+
const status = String(c.status || '');
|
|
235
|
+
const assertsPass = isPassingValue(c.value) || status === 'verified' || status === 'assumed';
|
|
236
|
+
const isFailing = status === 'disputed' || status === 'rejected';
|
|
237
|
+
if (!assertsPass && !isFailing) continue; // pending/unknown non-asserting — ignore (as before)
|
|
238
|
+
seenClaims.add(c.id);
|
|
239
|
+
|
|
240
|
+
// finding 1: a pass-asserting claim backed by test_output evidence that did NOT reconcile
|
|
241
|
+
// (it has test_output evidence but no manifest-matchable execution.label — otherwise it
|
|
242
|
+
// would be in bucket A) is a not-run divergence. A test_output claim reconciles against the
|
|
243
|
+
// manifest or it is a divergence — it is NEVER accepted as session-local on self-report.
|
|
244
|
+
if (assertsPass && claimHasTestOutputEvidence.has(c.id)) {
|
|
245
|
+
const rawCmd = normalizeCmd(c.fieldOrBehavior || c.value || '');
|
|
246
|
+
noEvidenceCommand.push({ cmd: rawCmd || `[claim:${c.id}]`, claimId: c.id, claimType: c.claimType, reason: 'test_output-unreconciled' });
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// A workflow.check.command claim with no captured (labeled) evidence is a never-captured
|
|
251
|
+
// claimed pass — not-run divergence (anti-gaming teeth preserved).
|
|
252
|
+
if (assertsPass && c.claimType === 'workflow.check.command' && !claimHasLabeledTestOutput.has(c.id)) {
|
|
253
|
+
const rawCmd = normalizeCmd(c.fieldOrBehavior || c.value || '');
|
|
254
|
+
noEvidenceCommand.push({ cmd: rawCmd || `[claim:${c.id}:${c.claimType}]`, claimId: c.id, claimType: c.claimType, reason: 'no-evidence-command' });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const waiver = (c.metadata && typeof c.metadata === 'object') ? c.metadata.waiver : undefined;
|
|
259
|
+
sessionLocal.push({
|
|
180
260
|
claimId: c.id,
|
|
181
|
-
evId: null,
|
|
182
261
|
claimType: c.claimType,
|
|
183
|
-
|
|
262
|
+
assertedStatus: status,
|
|
263
|
+
value: c.value,
|
|
264
|
+
waiver: (waiver && typeof waiver === 'object') ? waiver : null,
|
|
265
|
+
subject: c.subjectId || c.fieldOrBehavior || c.id,
|
|
266
|
+
evidenceType: claimEvidenceType.get(c.id) || 'unknown',
|
|
184
267
|
});
|
|
185
268
|
}
|
|
186
269
|
|
|
187
|
-
return
|
|
270
|
+
return { reconcilable, sessionLocal, noEvidenceCommand, waiverOnCommand };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* WS8 (finding 3): re-derive every claim's status CI-side from the bundle's OWN
|
|
275
|
+
* evidence/events/policies, using @kontourai/surface's canonical deriveClaimStatus (the same
|
|
276
|
+
* function the producer used). The reconciler MUST NOT trust a bundle's self-reported
|
|
277
|
+
* `claim.status`; it compares the asserted status to this re-derived status and treats a
|
|
278
|
+
* mismatch as a `status-misassertion` divergence.
|
|
279
|
+
*
|
|
280
|
+
* Runs the ESM helper scripts/ci/derive-claim-status.mjs via spawnSync (Surface is ESM-only;
|
|
281
|
+
* this reconciler is CJS with a synchronous entrypoint). Surface is resolved from the helper's
|
|
282
|
+
* own location (the reconciler's node_modules), so it works even when reconciling an adopter
|
|
283
|
+
* repo that does not itself depend on Surface.
|
|
284
|
+
*
|
|
285
|
+
* @returns {Map<string,string|null>|null} claimId → derived status; null if re-derivation is
|
|
286
|
+
* unavailable (Surface could not load / helper failed to run). A per-claim null value means
|
|
287
|
+
* that specific claim could not be derived.
|
|
288
|
+
*/
|
|
289
|
+
function deriveClaimStatuses(bundlePath, repoRoot) {
|
|
290
|
+
const helper = path.join(__dirname, 'derive-claim-status.mjs');
|
|
291
|
+
if (!fs.existsSync(helper)) return null;
|
|
292
|
+
const res = spawnSync('node', [helper, bundlePath], { cwd: repoRoot, encoding: 'utf8', timeout: 60000 });
|
|
293
|
+
if (res.status !== 0 || !res.stdout) {
|
|
294
|
+
if (res.stderr) process.stderr.write(`[trust-reconcile] status re-derivation unavailable: ${res.stderr.trim()}
|
|
295
|
+
`);
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const obj = JSON.parse(res.stdout);
|
|
300
|
+
const m = new Map();
|
|
301
|
+
for (const [k, v] of Object.entries(obj)) m.set(k, v);
|
|
302
|
+
return m;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
188
306
|
}
|
|
189
307
|
|
|
190
308
|
// ---------------------------------------------------------------------------
|
|
@@ -192,7 +310,7 @@ function getClaimedPassCommands(bundle) {
|
|
|
192
310
|
// ---------------------------------------------------------------------------
|
|
193
311
|
|
|
194
312
|
function parseArgs(argv) {
|
|
195
|
-
const args = { bundle: null, commands: [], repoRoot: null };
|
|
313
|
+
const args = { bundle: null, commands: [], repoRoot: null, manifest: null };
|
|
196
314
|
for (let i = 0; i < argv.length; i++) {
|
|
197
315
|
const arg = argv[i];
|
|
198
316
|
const next = argv[i + 1];
|
|
@@ -203,11 +321,104 @@ function parseArgs(argv) {
|
|
|
203
321
|
i++;
|
|
204
322
|
} else if (arg === '--repo-root' && next) {
|
|
205
323
|
args.repoRoot = next; i++;
|
|
324
|
+
} else if (arg === '--manifest' && next) {
|
|
325
|
+
args.manifest = next; i++;
|
|
206
326
|
}
|
|
207
327
|
}
|
|
208
328
|
return args;
|
|
209
329
|
}
|
|
210
330
|
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// WS8 (ADR 0020): reconcile manifest resolution + slugification.
|
|
333
|
+
// The manifest is a list of named {id, command} entries the reconciler treats as
|
|
334
|
+
// individually CI-reconcilable. It is resolved (priority order) from:
|
|
335
|
+
// 1. CLI --manifest <json>
|
|
336
|
+
// 2. env TRUST_RECONCILE_MANIFEST <json>
|
|
337
|
+
// 3. package.json "trust-reconcile-manifest" (inline array, OR a string command
|
|
338
|
+
// that emits the JSON — this repo points it at run-baseline.sh --manifest-json)
|
|
339
|
+
// 4. evals/ci/run-baseline.sh --manifest-json (this repo's live LANE_* registry —
|
|
340
|
+
// every entry runs in a required ci.yml lane by construction)
|
|
341
|
+
// 5. legacy fallback: the resolved fresh-verify commands as a manifest of size N
|
|
342
|
+
// (so the historical single-command behavior is a strict subset — backward compat).
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
/** Slugify a label the same way evals/ci/run-baseline.sh's slugify() does. */
|
|
346
|
+
function slugifyLabel(s) {
|
|
347
|
+
return String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Normalize a raw manifest array (of strings or {id, command}) to {id, command}[]. */
|
|
351
|
+
function normalizeManifestEntries(raw) {
|
|
352
|
+
if (!Array.isArray(raw)) return null;
|
|
353
|
+
const out = [];
|
|
354
|
+
for (const item of raw) {
|
|
355
|
+
if (typeof item === 'string') {
|
|
356
|
+
const cmd = normalizeCmd(item);
|
|
357
|
+
if (cmd) out.push({ id: slugifyLabel(cmd), command: cmd });
|
|
358
|
+
} else if (item && typeof item === 'object' && typeof item.command === 'string') {
|
|
359
|
+
const cmd = normalizeCmd(item.command);
|
|
360
|
+
if (cmd) out.push({ id: item.id ? String(item.id) : slugifyLabel(cmd), command: cmd });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return out.length ? out : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function parseManifestJson(text, label) {
|
|
367
|
+
try {
|
|
368
|
+
return normalizeManifestEntries(JSON.parse(text));
|
|
369
|
+
} catch {
|
|
370
|
+
process.stderr.write(`[trust-reconcile] ignoring ${label}: not a valid JSON array of {id, command} entries\n`);
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function runBaselineManifest(repoRoot) {
|
|
376
|
+
const script = path.join(repoRoot, 'evals', 'ci', 'run-baseline.sh');
|
|
377
|
+
if (!fs.existsSync(script)) return null;
|
|
378
|
+
const res = spawnSync('bash', [script, '--manifest-json'], { cwd: repoRoot, encoding: 'utf8', timeout: 30000 });
|
|
379
|
+
if (res.status !== 0 || !res.stdout) return null;
|
|
380
|
+
return parseManifestJson(res.stdout, 'evals/ci/run-baseline.sh --manifest-json');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Resolve the reconcile manifest. Returns { entries: {id, command}[], source }.
|
|
385
|
+
* canonicalCommands is used only for the legacy size-N fallback (tier 5).
|
|
386
|
+
*/
|
|
387
|
+
function resolveManifest(args, repoRoot, canonicalCommands) {
|
|
388
|
+
if (args.manifest) {
|
|
389
|
+
const m = parseManifestJson(args.manifest, '--manifest');
|
|
390
|
+
if (m) return { entries: m, source: 'cli:--manifest' };
|
|
391
|
+
}
|
|
392
|
+
if (process.env.TRUST_RECONCILE_MANIFEST) {
|
|
393
|
+
const m = parseManifestJson(process.env.TRUST_RECONCILE_MANIFEST, 'TRUST_RECONCILE_MANIFEST');
|
|
394
|
+
if (m) return { entries: m, source: 'env:TRUST_RECONCILE_MANIFEST' };
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const pkgPath = path.join(repoRoot, 'package.json');
|
|
398
|
+
if (fs.existsSync(pkgPath)) {
|
|
399
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
400
|
+
const raw = (pkg && pkg['trust-reconcile-manifest'])
|
|
401
|
+
|| (pkg && pkg.scripts && pkg.scripts['trust-reconcile-manifest']);
|
|
402
|
+
if (Array.isArray(raw)) {
|
|
403
|
+
const m = normalizeManifestEntries(raw);
|
|
404
|
+
if (m) return { entries: m, source: 'package.json:trust-reconcile-manifest (inline array)' };
|
|
405
|
+
} else if (typeof raw === 'string' && raw.trim()) {
|
|
406
|
+
const res = spawnSync('bash', ['-c', raw], { cwd: repoRoot, encoding: 'utf8', timeout: 60000 });
|
|
407
|
+
if (res.status === 0 && res.stdout) {
|
|
408
|
+
const m = parseManifestJson(res.stdout, 'package.json trust-reconcile-manifest emit');
|
|
409
|
+
if (m) return { entries: m, source: `package.json:trust-reconcile-manifest (${raw})` };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} catch { /* ignore */ }
|
|
414
|
+
const rb = runBaselineManifest(repoRoot);
|
|
415
|
+
if (rb) return { entries: rb, source: 'evals/ci/run-baseline.sh' };
|
|
416
|
+
const legacy = (canonicalCommands || [])
|
|
417
|
+
.map((c) => ({ id: slugifyLabel(normalizeCmd(c)), command: normalizeCmd(c) }))
|
|
418
|
+
.filter((e) => e.command);
|
|
419
|
+
return { entries: legacy, source: 'legacy:fresh-verify-commands' };
|
|
420
|
+
}
|
|
421
|
+
|
|
211
422
|
/**
|
|
212
423
|
* Resolve the list of canonical verify commands.
|
|
213
424
|
* Priority: CLI --commands > TRUST_RECONCILE_COMMANDS env > package.json scripts key.
|
|
@@ -275,7 +486,7 @@ function discoverBundle(repoRoot) {
|
|
|
275
486
|
* @param {string|null} [opts.repoRoot] - Repo root path. null = TRUST_RECONCILE_REPO_ROOT env or cwd.
|
|
276
487
|
* @returns {number} Exit code: 0 = pass, 1 = fail/divergence.
|
|
277
488
|
*/
|
|
278
|
-
function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {}) {
|
|
489
|
+
function runTrustReconcile({ bundle = null, commands = [], repoRoot = null, manifest = null } = {}) {
|
|
279
490
|
const resolvedRepoRoot = path.resolve(
|
|
280
491
|
repoRoot || process.env.TRUST_RECONCILE_REPO_ROOT || process.cwd()
|
|
281
492
|
);
|
|
@@ -299,9 +510,22 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {
|
|
|
299
510
|
return 1;
|
|
300
511
|
}
|
|
301
512
|
|
|
513
|
+
// WS8 (ADR 0020): resolve the reconcile manifest (named {id, command} entries used
|
|
514
|
+
// for per-command classification-aware reconcile). The canonical (fresh-verify)
|
|
515
|
+
// commands above remain the standalone CI-truth run; the manifest is the set of
|
|
516
|
+
// required-lane commands a test_output claim is permitted to reconcile against.
|
|
517
|
+
const manifestResolution = resolveManifest({ manifest }, resolvedRepoRoot, canonicalCommands);
|
|
518
|
+
const manifestEntries = manifestResolution.entries;
|
|
519
|
+
const manifestByCmd = new Map();
|
|
520
|
+
for (const e of manifestEntries) manifestByCmd.set(normalizeCmd(e.command), e);
|
|
521
|
+
|
|
302
522
|
process.stdout.write('[trust-reconcile] starting CI trust anchor reconcile\n');
|
|
303
523
|
process.stdout.write(`[trust-reconcile] repo-root: ${resolvedRepoRoot}\n`);
|
|
304
524
|
process.stdout.write(`[trust-reconcile] canonical commands: ${canonicalCommands.join(' | ')}\n`);
|
|
525
|
+
process.stdout.write(`[trust-reconcile] manifest: ${manifestEntries.length} entr${manifestEntries.length === 1 ? 'y' : 'ies'} (source: ${manifestResolution.source})\n`);
|
|
526
|
+
for (const e of manifestEntries) {
|
|
527
|
+
process.stdout.write(`[trust-reconcile] manifest[${e.id}] = ${e.command}\n`);
|
|
528
|
+
}
|
|
305
529
|
if (bundlePath) {
|
|
306
530
|
process.stdout.write(`[trust-reconcile] bundle: ${bundlePath}\n`);
|
|
307
531
|
} else {
|
|
@@ -337,6 +561,8 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {
|
|
|
337
561
|
|
|
338
562
|
if (result.passed) {
|
|
339
563
|
process.stdout.write(`[trust-reconcile] PASS: ${cmd}\n`);
|
|
564
|
+
} else if (result.timedOut) {
|
|
565
|
+
process.stderr.write(`[trust-reconcile] FAIL: ${cmd} (TIMED OUT after ${result.timeoutMs}ms — this is a timeout kill, not a real command failure; if this command is a legitimately slow check, raise TRUST_RECONCILE_COMMAND_TIMEOUT_MS)\n`);
|
|
340
566
|
} else {
|
|
341
567
|
process.stderr.write(`[trust-reconcile] FAIL: ${cmd} (exit ${result.exitCode})\n`);
|
|
342
568
|
if (result.stderr) {
|
|
@@ -407,25 +633,45 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {
|
|
|
407
633
|
}
|
|
408
634
|
}
|
|
409
635
|
} else {
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
636
|
+
// WS8 (ADR 0020): classify the bundle's claims into reconcilable (test_output,
|
|
637
|
+
// manifest-matched), session-local (waiver/Surface-status), and never-captured
|
|
638
|
+
// command claims — instead of forcing every claim to match one composite command.
|
|
639
|
+
const { reconcilable, sessionLocal, noEvidenceCommand, waiverOnCommand } = classifyBundleClaims(bundle);
|
|
640
|
+
process.stdout.write(`[trust-reconcile] classified bundle: ${reconcilable.length} reconcilable command claim(s), ${sessionLocal.length} session-local claim(s), ${noEvidenceCommand.length} unbacked/unreconciled command claim(s), ${waiverOnCommand.length} waiver-on-command claim(s)\n`);
|
|
641
|
+
|
|
642
|
+
// finding 3: re-derive every claim's status CI-side (never trust self-reported
|
|
643
|
+
// claim.status). Consumed by the session-local loop below to catch status-misassertion.
|
|
644
|
+
const derivedStatus = deriveClaimStatuses(bundlePath, resolvedRepoRoot);
|
|
645
|
+
if (derivedStatus) {
|
|
646
|
+
process.stdout.write(`[trust-reconcile] re-derived ${derivedStatus.size} claim status(es) CI-side from the bundle's own evidence/events/policies (self-reported claim.status is NOT trusted)\n`);
|
|
647
|
+
} else {
|
|
648
|
+
process.stderr.write(`[trust-reconcile] WARNING: CI-side status re-derivation is unavailable — every session-local pass-asserting claim will fail closed (cannot verify self-reported status)\n`);
|
|
649
|
+
}
|
|
413
650
|
|
|
414
|
-
|
|
415
|
-
|
|
651
|
+
// finding 4 (server-side): a command-backed (test_output-evidence) claim carrying a
|
|
652
|
+
// waiver is a divergence — a command-backed check reconciles against CI or fails; it
|
|
653
|
+
// cannot be waived away.
|
|
654
|
+
for (const { claimId, claimType, subject } of waiverOnCommand) {
|
|
655
|
+
issues.push({
|
|
656
|
+
type: 'waiver-on-command-check',
|
|
657
|
+
message: `trust divergence: claim '${claimId}' (${subject}, claimType: ${claimType}) carries a waiver but is backed by test_output evidence — a command-backed check reconciles against CI or fails and cannot be waived`,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
416
660
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
661
|
+
// not-run divergences: never-captured command claims (no evidence) AND test_output
|
|
662
|
+
// claims that did not reconcile (no manifest-matchable execution.label).
|
|
663
|
+
for (const { cmd, claimId, claimType, reason } of noEvidenceCommand) {
|
|
664
|
+
const message = reason === 'test_output-unreconciled'
|
|
665
|
+
? `trust divergence: claim '${claimId}' (claimType: ${claimType}) asserts pass with test_output evidence but has no manifest-matched execution.label — a test_output claim must reconcile against the manifest or it is a divergence (never accepted as session-local)`
|
|
666
|
+
: `trust divergence: claim '${claimId}' (claimType: ${claimType}) asserts pass but has no supporting evidence item — command never captured`;
|
|
667
|
+
issues.push({ type: 'not-run', cmd, message });
|
|
668
|
+
}
|
|
427
669
|
|
|
428
|
-
|
|
670
|
+
// Reconcilable test_output claims: laundering → manifest match → CI fresh result.
|
|
671
|
+
for (const { cmd } of reconcilable) {
|
|
672
|
+
const normalCmd = normalizeCmd(cmd);
|
|
673
|
+
|
|
674
|
+
// (a) Laundering operator check — must come first (most specific signal).
|
|
429
675
|
if (hasLaunderingOperator(cmd)) {
|
|
430
676
|
issues.push({
|
|
431
677
|
type: 'laundering',
|
|
@@ -435,21 +681,41 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {
|
|
|
435
681
|
continue;
|
|
436
682
|
}
|
|
437
683
|
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (!
|
|
442
|
-
// (c) CI never ran this claimed-pass command — fail-closed
|
|
684
|
+
// A test_output claim MUST name a manifest (required-lane) command. An agent
|
|
685
|
+
// cannot self-label an arbitrary command test_output to dodge the manifest.
|
|
686
|
+
const entry = manifestByCmd.get(normalCmd);
|
|
687
|
+
if (!entry) {
|
|
443
688
|
issues.push({
|
|
444
689
|
type: 'not-run',
|
|
445
690
|
cmd,
|
|
446
|
-
message: `trust divergence: agent claimed '${cmd}' passed;
|
|
691
|
+
message: `trust divergence: agent claimed '${cmd}' passed; command is not in the reconcile manifest — a test_output claim must name a manifest/required-lane command (CI cannot self-declare an arbitrary command)`,
|
|
447
692
|
});
|
|
448
693
|
continue;
|
|
449
694
|
}
|
|
450
695
|
|
|
696
|
+
// Prefer the fresh-verify Step 1 result; otherwise re-run this (manifest, and
|
|
697
|
+
// therefore required-lane) command fresh now. Only manifest commands are ever
|
|
698
|
+
// run on demand, so the set is bounded by the registry.
|
|
699
|
+
let ciResult = ciResults.get(normalCmd);
|
|
700
|
+
if (!ciResult) {
|
|
701
|
+
process.stdout.write(`[trust-reconcile] manifest reconcile: running '${entry.command}' (id: ${entry.id})\n`);
|
|
702
|
+
const r = runCommand(entry.command, resolvedRepoRoot);
|
|
703
|
+
ciResult = { exitCode: r.exitCode, passed: r.passed };
|
|
704
|
+
ciResults.set(normalCmd, ciResult);
|
|
705
|
+
if (r.passed) {
|
|
706
|
+
process.stdout.write(`[trust-reconcile] PASS: ${entry.command}\n`);
|
|
707
|
+
} else if (r.timedOut) {
|
|
708
|
+
process.stderr.write(`[trust-reconcile] FAIL: ${entry.command} (TIMED OUT after ${r.timeoutMs}ms — this is a timeout kill, not a real command failure; if this manifest command is a legitimately slow check, raise TRUST_RECONCILE_COMMAND_TIMEOUT_MS)\n`);
|
|
709
|
+
} else {
|
|
710
|
+
process.stderr.write(`[trust-reconcile] FAIL: ${entry.command} (exit ${r.exitCode})\n`);
|
|
711
|
+
if (r.stderr) {
|
|
712
|
+
const tail = r.stderr.trim().split('\n').slice(-5).join('\n');
|
|
713
|
+
process.stderr.write(`[trust-reconcile] --- stderr tail ---\n${tail}\n[trust-reconcile] ---\n`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
451
718
|
if (!ciResult.passed) {
|
|
452
|
-
// (b) Claimed pass + CI fresh FAIL = divergence
|
|
453
719
|
issues.push({
|
|
454
720
|
type: 'divergence',
|
|
455
721
|
cmd,
|
|
@@ -459,9 +725,99 @@ function runTrustReconcile({ bundle = null, commands = [], repoRoot = null } = {
|
|
|
459
725
|
continue;
|
|
460
726
|
}
|
|
461
727
|
|
|
462
|
-
|
|
463
|
-
process.stdout.write(`[trust-reconcile] RECONCILED: '${cmd}' — claimed pass, CI fresh run = PASS\n`);
|
|
728
|
+
process.stdout.write(`[trust-reconcile] RECONCILED: '${cmd}' (manifest id: ${entry.id}) — claimed pass, CI fresh run = PASS\n`);
|
|
464
729
|
}
|
|
730
|
+
|
|
731
|
+
// Session-local claims: not CI-reconcilable, but NOT a pass bypass. Each must either
|
|
732
|
+
// (a) carry a loud, justified waiver, or (b) resolve a real CI-RE-DERIVED `verified`
|
|
733
|
+
// status. WS8 iteration-2 hardening:
|
|
734
|
+
// - finding 3: the status used here is RE-DERIVED CI-side, never the self-reported
|
|
735
|
+
// claim.status. A mismatch is a `status-misassertion` divergence. If re-derivation
|
|
736
|
+
// is unavailable we fail closed (`status-underivable`) — we never fall back to
|
|
737
|
+
// trusting the bundle's own status.
|
|
738
|
+
// - finding 2: `assumed` alone is NO LONGER a silent pass. `assumed` is acceptable
|
|
739
|
+
// ONLY with a waiver (printed as a loud WAIVED line). An unwaived `assumed` claim is
|
|
740
|
+
// an `unwaived-assumed` divergence (restores pre-WS8 semantics where `assumed` alone
|
|
741
|
+
// never satisfied assertsPass).
|
|
742
|
+
//
|
|
743
|
+
// WS8 iteration-4 (converged iteration-3 finding, both gates): a session-local claim
|
|
744
|
+
// that re-derives `verified` with NO waiver is, by definition, backed only by
|
|
745
|
+
// human_attestation/attestation/external/etc. evidence with no --command — i.e.
|
|
746
|
+
// `deriveClaimStatus` only proves the bundle is internally self-consistent, never that
|
|
747
|
+
// the underlying attestation is true. A fully fabricated bundle (fake claim + fake
|
|
748
|
+
// evidence + fake event, all hand-authored to agree) is INDISTINGUISHABLE from a real
|
|
749
|
+
// one at this layer. This was previously printed as a quiet `SESSION-LOCAL OK` line
|
|
750
|
+
// indistinguishable from a genuinely reconciled check. It is now always printed as a
|
|
751
|
+
// loud, distinct `ATTESTED (not independently verifiable at L0)` marker — see ADR 0020
|
|
752
|
+
// Residuals. This does NOT change the exit code (attestations are not blocked at L0;
|
|
753
|
+
// blocking every honest human-attestation use is not the fix) — it is a visibility-only
|
|
754
|
+
// change so a reviewer/downstream tool can grep for and count exactly how many claims in
|
|
755
|
+
// a passing bundle rest on bundle-internal consistency alone rather than independent
|
|
756
|
+
// (CI fresh-run or cryptographically-signed) verification.
|
|
757
|
+
let attestedCount = 0;
|
|
758
|
+
for (const { claimId, claimType, assertedStatus, waiver, subject, evidenceType } of sessionLocal) {
|
|
759
|
+
// finding 3: re-derive; never trust the asserted status.
|
|
760
|
+
let status;
|
|
761
|
+
if (!derivedStatus) {
|
|
762
|
+
issues.push({
|
|
763
|
+
type: 'status-underivable',
|
|
764
|
+
message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) asserts status '${assertedStatus || 'unknown'}' but CI-side re-derivation is unavailable — refusing to trust a self-reported status (fail-closed)`,
|
|
765
|
+
});
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
const derived = derivedStatus.get(claimId);
|
|
769
|
+
if (derived === undefined || derived === null) {
|
|
770
|
+
issues.push({
|
|
771
|
+
type: 'status-underivable',
|
|
772
|
+
message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) could not be re-derived CI-side from the bundle's own evidence/events/policies — refusing to trust its self-reported status '${assertedStatus || 'unknown'}' (fail-closed)`,
|
|
773
|
+
});
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (derived !== assertedStatus) {
|
|
777
|
+
issues.push({
|
|
778
|
+
type: 'status-misassertion',
|
|
779
|
+
message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) asserts status '${assertedStatus || 'unknown'}' but CI re-derivation from the bundle's own evidence/events/policies yields '${derived}' — the reconciler does not trust self-reported claim.status`,
|
|
780
|
+
});
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
status = derived;
|
|
784
|
+
|
|
785
|
+
if (status === 'disputed' || status === 'rejected') {
|
|
786
|
+
issues.push({
|
|
787
|
+
type: 'session-local-failed',
|
|
788
|
+
message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) has re-derived status '${status}' — a failing/rejected claim blocks (session-local classification is not a pass bypass)`,
|
|
789
|
+
});
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
// finding 2: a waiver is the ONLY way an `assumed` (or otherwise non-`verified`)
|
|
793
|
+
// session-local claim passes, and it is printed loudly. `verified` still passes on its
|
|
794
|
+
// own re-derived status.
|
|
795
|
+
if (waiver && waiver.reason && waiver.approved_by) {
|
|
796
|
+
process.stdout.write(`[trust-reconcile] WAIVED: ${subject} (${claimType}) status=${status} — ${waiver.reason} (approved by ${waiver.approved_by})\n`);
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
if (status === 'verified') {
|
|
800
|
+
attestedCount++;
|
|
801
|
+
process.stdout.write(`[trust-reconcile] ATTESTED (not independently verifiable at L0): '${claimId}' (${claimType}) evidenceType=${evidenceType} — accepted on bundle-internal consistency only; see ADR 0020 Residuals\n`);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (status === 'assumed') {
|
|
805
|
+
issues.push({
|
|
806
|
+
type: 'unwaived-assumed',
|
|
807
|
+
message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) has re-derived status 'assumed' but carries no waiver — 'assumed' alone is not a pass; it requires a documented waiver (--accepted-gap-reason/--waived-by) to be accepted`,
|
|
808
|
+
});
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
issues.push({
|
|
812
|
+
type: 'unwaived-session-local',
|
|
813
|
+
message: `trust divergence: session-local claim '${claimId}' (claimType: ${claimType}) asserts pass with re-derived status '${status || 'unknown'}' but has no waiver and no CI-re-derived verified status`,
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// WS8 iteration-4: always emit the summary count, even when zero, so a passing bundle
|
|
818
|
+
// with zero attested claims is distinguishable in the log from one where the count line
|
|
819
|
+
// is simply absent (grep-stable for downstream tooling / reviewers).
|
|
820
|
+
process.stdout.write(`[trust-reconcile] ${attestedCount} attested claim(s) accepted without independent verification\n`);
|
|
465
821
|
}
|
|
466
822
|
}
|
|
467
823
|
|
|
@@ -517,6 +873,7 @@ function main() {
|
|
|
517
873
|
bundle: args.bundle || null,
|
|
518
874
|
commands: args.commands,
|
|
519
875
|
repoRoot: args.repoRoot || null,
|
|
876
|
+
manifest: args.manifest || null,
|
|
520
877
|
}));
|
|
521
878
|
}
|
|
522
879
|
|