loopforge 3.7.0 → 3.8.1
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/LICENSE +21 -21
- package/README.md +171 -157
- package/dist/audit.d.ts +53 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +106 -22
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +75 -69
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +276 -168
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +111 -12
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +13 -0
- package/dist/cognitive-facts.d.ts.map +1 -1
- package/dist/cognitive-facts.js +30 -16
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/cognitive-governance.d.ts +30 -6
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +74 -18
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +109 -24
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +235 -52
- package/dist/committed-round.js.map +1 -1
- package/dist/contract-items.d.ts +55 -0
- package/dist/contract-items.d.ts.map +1 -0
- package/dist/contract-items.js +180 -0
- package/dist/contract-items.js.map +1 -0
- package/dist/enforcement-gate.d.ts +43 -20
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +269 -378
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +11 -6
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +102 -67
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts +4 -4
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +16 -14
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +91 -90
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +349 -160
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +76 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +127 -0
- package/dist/explain.js.map +1 -0
- package/dist/generate-schema.js +250 -53
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +41 -40
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +275 -515
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +0 -1
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +0 -4
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts.map +1 -1
- package/dist/loop-projection.js +3 -1
- package/dist/loop-projection.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +40 -21
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +173 -120
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +63 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +53 -7
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +240 -139
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +89 -0
- package/dist/mcp/submission-boundary.d.ts.map +1 -0
- package/dist/mcp/submission-boundary.js +203 -0
- package/dist/mcp/submission-boundary.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +377 -125
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.js +2 -2
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +65 -95
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +151 -39
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +7 -33
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +313 -473
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +6 -7
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +411 -168
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +23 -69
- package/dist/protocol.js.map +1 -1
- package/dist/replay.js +2 -2
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +69 -57
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +104 -64
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +55 -16
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +179 -57
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +7 -5
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +2 -1
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +96 -0
- package/dist/round-facts.d.ts.map +1 -0
- package/dist/round-facts.js +216 -0
- package/dist/round-facts.js.map +1 -0
- package/dist/round-transaction.d.ts +23 -9
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +99 -22
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +127 -11
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +517 -36
- package/dist/self-eval.js.map +1 -1
- package/dist/subgoal-state.d.ts +77 -0
- package/dist/subgoal-state.d.ts.map +1 -0
- package/dist/subgoal-state.js +198 -0
- package/dist/subgoal-state.js.map +1 -0
- package/dist/token-utils.d.ts +52 -19
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +83 -72
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +27 -46
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +308 -557
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +17 -0
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +17 -25
- package/package.json +7 -7
- package/skills/perception/SKILL.md +266 -64
|
@@ -18,15 +18,16 @@
|
|
|
18
18
|
* changes run order or verdict aggregation. Round Contract checks are further
|
|
19
19
|
* framed as declaration / execution / closure stages in their doc comments.
|
|
20
20
|
*/
|
|
21
|
-
import {
|
|
21
|
+
import { isPassedAfterObservation } from "./evidence-provider.js";
|
|
22
|
+
import { getPolicy } from "./policy.js";
|
|
22
23
|
import { makeVerificationFlag, makeVerificationResult } from "./protocol.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import { stableStringify } from "./canonical-state.js";
|
|
24
|
+
import { isRecord, entryRound, extractFilePathTokens } from "./token-utils.js";
|
|
25
|
+
import { derivationRounds, machineGitMotionSeries, entryViolations, } from "./committed-round.js";
|
|
26
|
+
import { deriveActiveRoundContract, sameContract, } from "./round-contract.js";
|
|
27
|
+
import { deriveContractItemStatuses, } from "./contract-items.js";
|
|
28
28
|
import { deriveClaimView, resolveRoundFiles } from "./evidence-claims.js";
|
|
29
|
-
import {
|
|
29
|
+
import { claimedMetCriteria, claimedRemainingCriteria, effectiveSuccess } from "./self-eval.js";
|
|
30
|
+
import { deriveGate, preflightStructuredGate } from "./cognitive-governance.js";
|
|
30
31
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
31
32
|
// v2.12: Check-name constants — single source of truth so the enforcement
|
|
32
33
|
// gate and audit can reference checks without string-literal drift.
|
|
@@ -63,8 +64,6 @@ export const CHECK_VERIFICATION_ENTRYPOINT_MODIFIED = "verification_entrypoint_m
|
|
|
63
64
|
/** v3.3: Test files changed in the same round a verification command passed.
|
|
64
65
|
* Warn-only — the command result stays usable. */
|
|
65
66
|
export const CHECK_TEST_FILES_MODIFIED = "test_files_modified";
|
|
66
|
-
export const CHECK_INTENT_DRIFT = "intent_drift";
|
|
67
|
-
export const CHECK_SUBGOAL_DRIFT = "subgoal_drift";
|
|
68
67
|
export const CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED = "backtrack_workspace_not_restored";
|
|
69
68
|
export const CHECK_CRITERIA_CLAIMS_UNVERIFIED = "criteria_claims_unverified";
|
|
70
69
|
// v3.3 — Round Contract checks. v3.4: split by target. The submission's own
|
|
@@ -83,35 +82,23 @@ export const CHECK_CRITERIA_CLAIMS_UNVERIFIED = "criteria_claims_unverified";
|
|
|
83
82
|
// (completion truth: contract_completion_unverified; premature_boundary's
|
|
84
83
|
// claimed-met-unverified arm). The framing is documentation only — no phase
|
|
85
84
|
// field exists on the contract.
|
|
86
|
-
/** Contract
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
export const
|
|
90
|
-
/** done_when items exist but verification_plan is empty or names commands
|
|
91
|
-
* that are not configured AND enabled in policy.evidence.commands — the
|
|
92
|
-
* completion claims could never be machine-checked. Warn. */
|
|
93
|
-
export const CHECK_ROUND_UNVERIFIABLE = "round_unverifiable";
|
|
85
|
+
/** v3.8: Contract items claimed met but not machine-verified this round.
|
|
86
|
+
* The round commits; the debt is surfaced and the enforcement gate's
|
|
87
|
+
* verification-debt row handles a persistent pattern. */
|
|
88
|
+
export const CHECK_CONTRACT_ITEMS_UNVERIFIED = "contract_items_unverified";
|
|
94
89
|
/** The round's actual git changes include files outside the ACTIVE
|
|
95
|
-
* contract's declared scope. Warn
|
|
96
|
-
*
|
|
90
|
+
* contract's declared scope. Warn; v3.8: not waivable by explanation — the
|
|
91
|
+
* enforcement gate rejects, and repeated drift terminates. */
|
|
97
92
|
export const CHECK_ROUND_SCOPE_DRIFT = "round_scope_drift";
|
|
98
|
-
/** success claimed under the ACTIVE contract whose done_when items are
|
|
99
|
-
* either claimed met without machine-verified evidence or silently dropped
|
|
100
|
-
* (not in success_criteria_met NOR success_criteria_remaining). Error. */
|
|
101
|
-
export const CHECK_PREMATURE_BOUNDARY = "premature_boundary";
|
|
102
|
-
/** v3.5: Closing a Round Contract is a success-class claim and must be
|
|
103
|
-
* machine-backed. The eval's met claims satisfy every done_when of the
|
|
104
|
-
* ACTIVE contract but its verification_plan commands did not pass this
|
|
105
|
-
* round. Error; warn under evidence.machine_backed_success "warn"; never
|
|
106
|
-
* downgraded by no_change_reason (all done_when met contradicts "no
|
|
107
|
-
* change"). Fail-open: plan names no longer configured+enabled are not
|
|
108
|
-
* required (cannot observe). */
|
|
109
|
-
export const CHECK_CONTRACT_COMPLETION_UNVERIFIED = "contract_completion_unverified";
|
|
110
93
|
/** v3.5: The ACTIVE contract is still open (not completed, not blocked)
|
|
111
94
|
* while a different contract was proposed — the proposal is ignored until
|
|
112
95
|
* the active one closes. Warn: the walker still ignores it; this only
|
|
113
96
|
* surfaces the otherwise-silent state. */
|
|
114
97
|
export const CHECK_CONTRACT_PREMATURE = "contract_premature";
|
|
98
|
+
/** v3.7.1: A cited gate (evaluation.gate_ids) is not approved. Opt-in:
|
|
99
|
+
* checked ONLY when policy.gate.enabled — citations are meaningless when
|
|
100
|
+
* the gate layer is off. Error → enforcement rejects the round. */
|
|
101
|
+
export const CHECK_USER_GATE_UNRESOLVED = "user_gate_unresolved";
|
|
115
102
|
export const CHECK_DOMAIN = {
|
|
116
103
|
// ── evaluation_consistency ────────────────────────────────────────────────
|
|
117
104
|
[CHECK_SUCCESS_WITH_REMAINING_CRITERIA]: "evaluation_consistency",
|
|
@@ -131,14 +118,11 @@ export const CHECK_DOMAIN = {
|
|
|
131
118
|
[CHECK_VERIFICATION_ENTRYPOINT_MODIFIED]: "evidence_integrity",
|
|
132
119
|
[CHECK_TEST_FILES_MODIFIED]: "evidence_integrity",
|
|
133
120
|
// ── plan_contract ─────────────────────────────────────────────────────────
|
|
134
|
-
[
|
|
135
|
-
[CHECK_SUBGOAL_DRIFT]: "plan_contract",
|
|
136
|
-
[CHECK_ROUND_UNDERSPECIFIED]: "plan_contract",
|
|
137
|
-
[CHECK_ROUND_UNVERIFIABLE]: "plan_contract",
|
|
121
|
+
[CHECK_CONTRACT_ITEMS_UNVERIFIED]: "plan_contract",
|
|
138
122
|
[CHECK_ROUND_SCOPE_DRIFT]: "plan_contract",
|
|
139
|
-
[CHECK_PREMATURE_BOUNDARY]: "plan_contract",
|
|
140
|
-
[CHECK_CONTRACT_COMPLETION_UNVERIFIED]: "plan_contract",
|
|
141
123
|
[CHECK_CONTRACT_PREMATURE]: "plan_contract",
|
|
124
|
+
// v3.7.1: cited-gate authorization conformance (opt-in blocking layer).
|
|
125
|
+
[CHECK_USER_GATE_UNRESOLVED]: "plan_contract",
|
|
142
126
|
// ── progress_recovery ─────────────────────────────────────────────────────
|
|
143
127
|
[CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED]: "progress_recovery",
|
|
144
128
|
};
|
|
@@ -178,72 +162,46 @@ export function isFileInScope(file, scope) {
|
|
|
178
162
|
export function collectOutOfScopeFiles(files, scope) {
|
|
179
163
|
return files.filter((file) => !isFileInScope(file, scope));
|
|
180
164
|
}
|
|
181
|
-
/** v3.3: Detect verification-domain tampering for a command
|
|
182
|
-
* entrypointModified — a workspace file the command depends on
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (!gitSnapshot)
|
|
165
|
+
/** v3.3: Detect verification-domain tampering for a command observation.
|
|
166
|
+
* entrypointModified — a workspace file the command depends on changed this
|
|
167
|
+
* round: the runtime executed a script the agent just rewrote, so the
|
|
168
|
+
* "machine observation" has no stable baseline. testFilesModified — the
|
|
169
|
+
* round changed test files (isTestFile): normal in TDD, so warn-only; the
|
|
170
|
+
* command result stays usable. Pure derivation — no fs access. */
|
|
171
|
+
function commandTampered(observation, gitObservation) {
|
|
172
|
+
if (!gitObservation)
|
|
190
173
|
return { entrypointModified: false, testFilesModified: false };
|
|
191
|
-
const gitFiles = new Set(
|
|
192
|
-
const entrypoints =
|
|
193
|
-
|
|
194
|
-
? snapshot.data.entrypointFiles.filter((f) => typeof f === "string")
|
|
174
|
+
const gitFiles = new Set(gitObservation.files);
|
|
175
|
+
const entrypoints = observation.kind === "command"
|
|
176
|
+
? observation.data.entrypointFiles ?? []
|
|
195
177
|
: [];
|
|
196
178
|
const entrypointModified = entrypoints.some((file) => gitFiles.has(file));
|
|
197
179
|
const testFilesModified = [...gitFiles].some((file) => isTestFile(file));
|
|
198
180
|
return { entrypointModified, testFilesModified };
|
|
199
181
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// v3.3: a command whose entrypoint changed this round is not machine
|
|
207
|
-
// evidence — exclude it so providerStatus degrades to unavailable.
|
|
208
|
-
!commandTampered(snapshot, git).entrypointModified) ?? null;
|
|
209
|
-
}
|
|
210
|
-
/** v3.5: Names of commands observed passing in the after-phase this round.
|
|
211
|
-
* Tampered commands (entrypoint changed this round) are not machine
|
|
212
|
-
* evidence and are excluded — the same rule passedAfterCommand applies. */
|
|
213
|
-
function passedPlanCommandNames(snapshots) {
|
|
214
|
-
const git = snapshots.find((snapshot) => snapshot.provider === "git") ?? null;
|
|
215
|
-
const names = new Set();
|
|
216
|
-
for (const snapshot of snapshots) {
|
|
217
|
-
if (!isRecord(snapshot.data))
|
|
218
|
-
continue;
|
|
219
|
-
if (snapshot.data.kind !== "command")
|
|
220
|
-
continue;
|
|
221
|
-
if (snapshot.data.phase !== "after" || snapshot.data.status !== "passed")
|
|
222
|
-
continue;
|
|
223
|
-
if (commandTampered(snapshot, git).entrypointModified)
|
|
224
|
-
continue;
|
|
225
|
-
const name = snapshot.data.commandName;
|
|
226
|
-
if (typeof name === "string" && name.length > 0)
|
|
227
|
-
names.add(name);
|
|
228
|
-
}
|
|
229
|
-
return names;
|
|
182
|
+
/** v3.8: The single machine-backed predicate lives in evidence-provider.ts.
|
|
183
|
+
* This wrapper resolves the round's git observation once for a collection. */
|
|
184
|
+
function passedAfterCommand(observations) {
|
|
185
|
+
const git = observations.find((item) => item.providerId === "git") ?? null;
|
|
186
|
+
const changed = git ? new Set(git.files) : null;
|
|
187
|
+
return observations.find((item) => isPassedAfterObservation(item, changed)) ?? null;
|
|
230
188
|
}
|
|
231
189
|
/** v3.2: Derive the machine-verification status of a round. The verification
|
|
232
190
|
* capability is modeled explicitly (verified / unavailable / absent) instead
|
|
233
191
|
* of letting evidence-dependent checks silently disappear when snapshots are
|
|
234
192
|
* missing — the fix for the "weakest when it matters most" gap. */
|
|
235
193
|
export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
|
|
236
|
-
const gitSnap = evidenceSnapshots.find((
|
|
194
|
+
const gitSnap = evidenceSnapshots.find((item) => item.providerId === "git") ?? null;
|
|
237
195
|
const gitObserved = gitSnap !== null && gitSnap.files.length > 0;
|
|
238
196
|
const command = passedAfterCommand(evidenceSnapshots);
|
|
239
197
|
const commandVerified = command !== null;
|
|
240
|
-
// testsMachineBacked: agent-reported
|
|
198
|
+
// testsMachineBacked: agent-reported tests_reported agree with a passed
|
|
241
199
|
// command's parsed stdout (the same comparison checkCommandEvidenceIntegrity
|
|
242
200
|
// performs — parseTestOutput lives in this module).
|
|
243
201
|
let testsMachineBacked = false;
|
|
244
|
-
const reported = selfEval.
|
|
245
|
-
if (command && reported) {
|
|
246
|
-
const parsed = parseTestOutput(
|
|
202
|
+
const reported = selfEval.execution_report?.tests_reported;
|
|
203
|
+
if (command && command.kind === "command" && reported) {
|
|
204
|
+
const parsed = parseTestOutput(command.data.stdoutExcerpt ?? "");
|
|
247
205
|
testsMachineBacked = parsed !== null &&
|
|
248
206
|
parsed.passed === reported.passed &&
|
|
249
207
|
parsed.failed === reported.failed &&
|
|
@@ -251,8 +209,8 @@ export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
|
|
|
251
209
|
}
|
|
252
210
|
// reportedFilesMatch: agent-reported files_changed equals the git diff set.
|
|
253
211
|
let reportedFilesMatch = false;
|
|
254
|
-
if (gitSnap && selfEval.
|
|
255
|
-
const reportedSet = [...selfEval.
|
|
212
|
+
if (gitSnap && selfEval.execution_report) {
|
|
213
|
+
const reportedSet = [...(selfEval.execution_report.files_changed ?? [])].sort();
|
|
256
214
|
const actualSet = [...gitSnap.files].sort();
|
|
257
215
|
reportedFilesMatch = reportedSet.length === actualSet.length &&
|
|
258
216
|
reportedSet.every((file, index) => file === actualSet[index]);
|
|
@@ -278,7 +236,7 @@ export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
|
|
|
278
236
|
}
|
|
279
237
|
/** Per-round machine progress over decoded committed history. */
|
|
280
238
|
export function machineProgressSeries(vaultEntries, currentRound, lookback) {
|
|
281
|
-
return machineGitMotionSeries(
|
|
239
|
+
return machineGitMotionSeries(derivationRounds(vaultEntries, currentRound), currentRound, lookback);
|
|
282
240
|
}
|
|
283
241
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
284
242
|
// Individual checks — each returns a VerificationFlag or null
|
|
@@ -286,8 +244,8 @@ export function machineProgressSeries(vaultEntries, currentRound, lookback) {
|
|
|
286
244
|
function checkSuccessWithRemainingCriteria(selfEval) {
|
|
287
245
|
if (!effectiveSuccess(selfEval))
|
|
288
246
|
return null;
|
|
289
|
-
const remaining = selfEval.
|
|
290
|
-
if (
|
|
247
|
+
const remaining = claimedRemainingCriteria(selfEval.execution_report);
|
|
248
|
+
if (remaining.length === 0)
|
|
291
249
|
return null;
|
|
292
250
|
return makeVerificationFlag({
|
|
293
251
|
severity: "error",
|
|
@@ -303,14 +261,14 @@ function checkSuccessWithRemainingCriteria(selfEval) {
|
|
|
303
261
|
* next prompt tells the agent its claims are unverified. This is the
|
|
304
262
|
* mild form of the 3.x criterion-specific completion rule. */
|
|
305
263
|
function checkUnverifiedCriteriaClaims(selfEval, claimView) {
|
|
306
|
-
const met = selfEval.
|
|
307
|
-
if (
|
|
264
|
+
const met = claimedMetCriteria(selfEval.execution_report);
|
|
265
|
+
if (met.length === 0)
|
|
308
266
|
return null;
|
|
309
267
|
if (claimView.verifiedCount > 0 || claimView.hasMachineEvidence)
|
|
310
268
|
return null;
|
|
311
269
|
return makeVerificationFlag({
|
|
312
270
|
severity: "warn",
|
|
313
|
-
field: "
|
|
271
|
+
field: "execution_report",
|
|
314
272
|
check: CHECK_CRITERIA_CLAIMS_UNVERIFIED,
|
|
315
273
|
detail: `${met.length} criteria reported met but none machine-verified ` +
|
|
316
274
|
"(no passing test evidence or command snapshot)",
|
|
@@ -403,13 +361,12 @@ function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
|
|
|
403
361
|
/** v2.12/v3.6/v3.7: success without machine backing — the single
|
|
404
362
|
* "success evidence" semantic. Two arms, evaluated in order:
|
|
405
363
|
* 1. Empty/missing-evidence arm (v3.7: absorbed the enforcement-only
|
|
406
|
-
* empty_success rule): success=true with NO
|
|
407
|
-
* with files_changed empty AND
|
|
364
|
+
* empty_success rule): success=true with NO execution_report at all, or
|
|
365
|
+
* with files_changed empty AND tests_reported null, is an error
|
|
408
366
|
* unconditionally — BEFORE the providerStatus early-exit, so a passed
|
|
409
|
-
* command never rescues it (
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
* recorded is still an unbacked claim).
|
|
367
|
+
* command never rescues it (execution_report stays mandatory), and
|
|
368
|
+
* no declared no_change_reason downgrades it ("no change" with literally
|
|
369
|
+
* no evidence recorded is still an unbacked claim).
|
|
413
370
|
* 2. Claims arm (v3.6): success=true but the runtime observed no machine
|
|
414
371
|
* evidence for it. The trigger is the runtime-derived providerStatus
|
|
415
372
|
* (verified / unavailable / absent) — "verified" requires an UNTAMPERED
|
|
@@ -418,38 +375,51 @@ function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
|
|
|
418
375
|
* success — "required" rejects (error), "warn" tolerates with a warn
|
|
419
376
|
* (round commits, success excluded from the trajectory, trust drops). A
|
|
420
377
|
* declared no_change_reason is the honest escape hatch: it downgrades to
|
|
421
|
-
* info
|
|
378
|
+
* info — honored only while NO enabled verification command is
|
|
379
|
+
* configured (machine verification was structurally impossible); a
|
|
380
|
+
* configured command closes the escape. no_change_reason is honored HERE
|
|
381
|
+
* ONLY — contract checks refuse it. */
|
|
422
382
|
function checkSuccessWithoutVerifiedEvidence(selfEval, status) {
|
|
423
383
|
if (!effectiveSuccess(selfEval))
|
|
424
384
|
return null;
|
|
425
385
|
// v3.7: empty/missing-evidence arm (ex-R3 empty_success posture).
|
|
426
|
-
const ev = selfEval.
|
|
427
|
-
if (!ev || (ev.files_changed.length === 0 && ev.
|
|
386
|
+
const ev = selfEval.execution_report;
|
|
387
|
+
if (!ev || ((ev.files_changed ?? []).length === 0 && ev.tests_reported === null)) {
|
|
428
388
|
return makeVerificationFlag({
|
|
429
389
|
severity: "error",
|
|
430
390
|
field: "success",
|
|
431
391
|
check: CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE,
|
|
432
392
|
detail: ev
|
|
433
|
-
? "Agent claims success but
|
|
393
|
+
? "Agent claims success but execution_report shows no files changed " +
|
|
434
394
|
"and no tests were run. There is no verifiable evidence of work."
|
|
435
|
-
: "Agent claims success but provided no
|
|
436
|
-
"Every successful round MUST include
|
|
437
|
-
"files_changed,
|
|
395
|
+
: "Agent claims success but provided no execution_report. " +
|
|
396
|
+
"Every successful round MUST include execution_report with " +
|
|
397
|
+
"files_changed, tests_reported, and progress_estimate.",
|
|
438
398
|
});
|
|
439
399
|
}
|
|
440
400
|
// Claims arm (v3.6).
|
|
441
401
|
if (status.providerStatus === "verified")
|
|
442
402
|
return null;
|
|
443
|
-
|
|
403
|
+
// M2 (v3.7.x): the no_change_reason escape is only honest when machine
|
|
404
|
+
// verification was STRUCTURALLY impossible. With an enabled verification
|
|
405
|
+
// command configured, the runtime had a machine-verifiable path for this
|
|
406
|
+
// round — a declared reason cannot waive it (that would let an agent
|
|
407
|
+
// dodge the configured verification by writing a sentence, even while git
|
|
408
|
+
// observed real file changes). Verification-less policies (the default:
|
|
409
|
+
// commands: []) keep the escape: there is genuinely nothing to run.
|
|
410
|
+
const verificationConfigured = getPolicy().evidence.commands.some((command) => command.enabled);
|
|
411
|
+
const noChange = !verificationConfigured &&
|
|
412
|
+
typeof selfEval.no_change_reason === "string" &&
|
|
444
413
|
selfEval.no_change_reason.trim().length > 0;
|
|
445
|
-
// v3.
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
414
|
+
// v3.8: an unbacked success claim is "机器观察不足", not a contradiction —
|
|
415
|
+
// the round COMMITS with the claim recorded as insufficient, and the
|
|
416
|
+
// success stays out of the success trajectory (shouldPushSuccess keys on
|
|
417
|
+
// this warn). The bounded verification-debt row is what eventually acts on
|
|
418
|
+
// a persistent pattern. `no_change_reason` stays info, and only works when
|
|
419
|
+
// machine verification was structurally impossible (no enabled command).
|
|
420
|
+
// A success claim with NO execution_report at all is the other arm and
|
|
421
|
+
// remains an unconditional error.
|
|
422
|
+
const severity = noChange ? "info" : "warn";
|
|
453
423
|
return makeVerificationFlag({
|
|
454
424
|
severity,
|
|
455
425
|
field: "success",
|
|
@@ -553,16 +523,16 @@ function checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound) {
|
|
|
553
523
|
return null;
|
|
554
524
|
}
|
|
555
525
|
/** v1.18: Cross-validate agent-reported files_changed against git evidence
|
|
556
|
-
* from the configured evidence providers (
|
|
526
|
+
* from the configured evidence providers (MachineObservation array). */
|
|
557
527
|
function checkEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
558
|
-
if (!selfEval.
|
|
528
|
+
if (!selfEval.execution_report)
|
|
559
529
|
return null;
|
|
560
530
|
if (evidenceSnapshots.length === 0)
|
|
561
531
|
return null;
|
|
562
|
-
const gitSnap = evidenceSnapshots.find((s) => s.
|
|
532
|
+
const gitSnap = evidenceSnapshots.find((s) => s.providerId === "git");
|
|
563
533
|
if (!gitSnap)
|
|
564
534
|
return null;
|
|
565
|
-
const reported = [...selfEval.
|
|
535
|
+
const reported = [...(selfEval.execution_report.files_changed ?? [])].sort();
|
|
566
536
|
const actual = [...gitSnap.files].sort();
|
|
567
537
|
if (reported.length === 0 && actual.length === 0)
|
|
568
538
|
return null;
|
|
@@ -597,22 +567,20 @@ function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
|
|
|
597
567
|
if (!effectiveSuccess(selfEval))
|
|
598
568
|
return null;
|
|
599
569
|
for (const snapshot of evidenceSnapshots) {
|
|
600
|
-
if (snapshot.
|
|
570
|
+
if (snapshot.kind !== "command")
|
|
601
571
|
continue;
|
|
602
|
-
if (snapshot.
|
|
572
|
+
if (snapshot.phase !== "after" || snapshot.data.required !== true)
|
|
603
573
|
continue;
|
|
604
|
-
const status = snapshot.
|
|
574
|
+
const status = snapshot.status;
|
|
605
575
|
if (status === "passed")
|
|
606
576
|
continue;
|
|
607
|
-
const name =
|
|
608
|
-
? snapshot.data.commandName
|
|
609
|
-
: snapshot.provider;
|
|
577
|
+
const name = snapshot.data.commandId;
|
|
610
578
|
const exitCode = typeof snapshot.data.exitCode === "number"
|
|
611
579
|
? ` (exit ${snapshot.data.exitCode})`
|
|
612
580
|
: "";
|
|
613
581
|
return makeVerificationFlag({
|
|
614
582
|
severity: "error",
|
|
615
|
-
field: "
|
|
583
|
+
field: "execution_report",
|
|
616
584
|
check: CHECK_REQUIRED_COMMAND_FAILED,
|
|
617
585
|
detail: `Agent claims success but required command "${name}" ${String(status)}${exitCode}`,
|
|
618
586
|
});
|
|
@@ -620,122 +588,12 @@ function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
|
|
|
620
588
|
return null;
|
|
621
589
|
}
|
|
622
590
|
// ── Test output parsing ────────────────────────────────────────────────────
|
|
623
|
-
// jaccardSimilarity() imported from token-utils.ts (v2.1 — intent-action drift detection)
|
|
624
|
-
/** Extract stable sub-goal IDs (sg-XXXXXXXX) mentioned in text.
|
|
625
|
-
* Matches the ID format rendered in prompts (loop-compiler deriveSubGoalId). */
|
|
626
|
-
function extractSubGoalIds(text) {
|
|
627
|
-
return [...new Set(text.toLowerCase().match(/sg-[a-f0-9]{8}/g) ?? [])];
|
|
628
|
-
}
|
|
629
591
|
/** Whether a changed path looks like a test file. */
|
|
630
592
|
function isTestFile(path) {
|
|
631
593
|
return /\.(test|spec)\.[a-z0-9]+$/i.test(path) ||
|
|
632
594
|
/(^|[\\/])tests?[\\/]/i.test(path) ||
|
|
633
595
|
/_test\.[a-z0-9]+$/i.test(path);
|
|
634
596
|
}
|
|
635
|
-
/** Map each referenced sub-goal ID to its known description(s), reconstructed
|
|
636
|
-
* from emerged_subtasks in the vault lineage and the previous self-eval. */
|
|
637
|
-
function referencedSubGoalDescriptions(ids, vaultEntries, prevSelfEval) {
|
|
638
|
-
const map = new Map();
|
|
639
|
-
for (const id of ids)
|
|
640
|
-
map.set(id, []);
|
|
641
|
-
const descriptions = [];
|
|
642
|
-
for (const entry of vaultEntries) {
|
|
643
|
-
const emerged = Array.isArray(entry.emerged_subtasks)
|
|
644
|
-
? entry.emerged_subtasks.filter((v) => typeof v === "string")
|
|
645
|
-
: [];
|
|
646
|
-
descriptions.push(...emerged);
|
|
647
|
-
}
|
|
648
|
-
descriptions.push(...(prevSelfEval?.emerged_subtasks ?? []));
|
|
649
|
-
for (const desc of descriptions) {
|
|
650
|
-
const trimmed = desc.trim();
|
|
651
|
-
if (!trimmed)
|
|
652
|
-
continue;
|
|
653
|
-
const id = deriveSubGoalId(trimmed);
|
|
654
|
-
if (map.has(id))
|
|
655
|
-
map.get(id).push(trimmed);
|
|
656
|
-
}
|
|
657
|
-
return map;
|
|
658
|
-
}
|
|
659
|
-
/** v2.1 (check 11): Compare the previous round's declared next_action with
|
|
660
|
-
* the current round's actual output_summary. Detect when the agent says it
|
|
661
|
-
* will do X but then does Y — silent drift without explanation.
|
|
662
|
-
*
|
|
663
|
-
* Only fires when both next_action and output_summary are non-empty.
|
|
664
|
-
*
|
|
665
|
-
* v2.14: Structured-ID-first detection. Alignment is decided by a waterfall
|
|
666
|
-
* of concrete signals before falling back to string similarity:
|
|
667
|
-
* 1. Sub-goal IDs (sg-XXXXXXXX) in next_action matched against this
|
|
668
|
-
* round's completed_subtasks (exact ID, ID derived from the
|
|
669
|
-
* description text, or Jaccard against the referenced sub-goal's
|
|
670
|
-
* known description).
|
|
671
|
-
* 2. File paths named in next_action appearing in files_changed.
|
|
672
|
-
* 3. Test evidence — next_action mentions tests and test files were
|
|
673
|
-
* changed with tests actually run.
|
|
674
|
-
* 4. Jaccard token similarity against the policy threshold (weak signal).
|
|
675
|
-
* Any signal aligning is sufficient — text similarity is never the sole
|
|
676
|
-
* authority when structured evidence exists. */
|
|
677
|
-
function checkIntentDrift(selfEval, prevSelfEval, vaultEntries) {
|
|
678
|
-
if (!prevSelfEval?.next_action?.trim())
|
|
679
|
-
return null;
|
|
680
|
-
if (!selfEval.output_summary.trim())
|
|
681
|
-
return null;
|
|
682
|
-
const intent = prevSelfEval.next_action.trim();
|
|
683
|
-
const actual = selfEval.output_summary.trim();
|
|
684
|
-
const policy = getPolicy();
|
|
685
|
-
const filesChanged = selfEval.execution_evidence?.files_changed ?? [];
|
|
686
|
-
// ── Signal 1: sub-goal IDs referenced by next_action → completed this round
|
|
687
|
-
const intentIds = extractSubGoalIds(intent);
|
|
688
|
-
if (intentIds.length > 0) {
|
|
689
|
-
const completed = selfEval.completed_subtasks ?? [];
|
|
690
|
-
const referenced = referencedSubGoalDescriptions(intentIds, vaultEntries, prevSelfEval);
|
|
691
|
-
const idMatched = intentIds.some((id) => completed.some((entry) => {
|
|
692
|
-
const text = entry.trim();
|
|
693
|
-
if (!text)
|
|
694
|
-
return false;
|
|
695
|
-
if (text.toLowerCase() === id)
|
|
696
|
-
return true;
|
|
697
|
-
if (deriveSubGoalId(text) === id)
|
|
698
|
-
return true;
|
|
699
|
-
const descriptions = referenced.get(id) ?? [];
|
|
700
|
-
return descriptions.some((desc) => jaccardSimilarity(text, desc) >= policy.evolution.subgoal_match_threshold);
|
|
701
|
-
}));
|
|
702
|
-
if (idMatched)
|
|
703
|
-
return null;
|
|
704
|
-
}
|
|
705
|
-
// ── Signal 2: file paths named in next_action appear in files_changed
|
|
706
|
-
const intentPaths = extractFilePathTokens(intent);
|
|
707
|
-
if (intentPaths.length > 0 && filesChanged.length > 0) {
|
|
708
|
-
const pathMatched = intentPaths.some((p) => filesChanged.some((f) => f === p || f.endsWith(p) || p.endsWith(f)));
|
|
709
|
-
if (pathMatched)
|
|
710
|
-
return null;
|
|
711
|
-
}
|
|
712
|
-
// ── Signal 3: test evidence — intent mentions tests, test files changed,
|
|
713
|
-
// and tests actually ran this round
|
|
714
|
-
const intentTokens = tokenize(intent);
|
|
715
|
-
const mentionsTests = intentTokens.has("test") || intentTokens.has("tests");
|
|
716
|
-
const testResults = selfEval.execution_evidence?.test_results;
|
|
717
|
-
const testsRan = !!testResults && testResults.passed + testResults.failed > 0;
|
|
718
|
-
const testFilesChanged = filesChanged.some((f) => isTestFile(f));
|
|
719
|
-
if (mentionsTests && testsRan && testFilesChanged)
|
|
720
|
-
return null;
|
|
721
|
-
// ── Signal 4 (weak): Jaccard token similarity
|
|
722
|
-
const score = jaccardSimilarity(intent, actual);
|
|
723
|
-
const threshold = policy.evolution.intent_drift_threshold;
|
|
724
|
-
if (score >= threshold)
|
|
725
|
-
return null;
|
|
726
|
-
const idNote = intentIds.length > 0
|
|
727
|
-
? `; ${intentIds.length} referenced sub-goal ID(s) did not match completed_subtasks`
|
|
728
|
-
: "";
|
|
729
|
-
return makeVerificationFlag({
|
|
730
|
-
severity: "warn",
|
|
731
|
-
field: "output_summary",
|
|
732
|
-
check: CHECK_INTENT_DRIFT,
|
|
733
|
-
detail: `Declared intent was "${intent.slice(0, 120)}" ` +
|
|
734
|
-
`but actual output "${actual.slice(0, 120)}" — ` +
|
|
735
|
-
`similarity ${(score * 100).toFixed(0)}% below threshold ${(threshold * 100).toFixed(0)}%` +
|
|
736
|
-
idNote,
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
597
|
/** Best-effort parse of test counts from common test-runner output formats.
|
|
740
598
|
*
|
|
741
599
|
* Scans the last 2000 characters of stdout (where summary lines typically
|
|
@@ -800,7 +658,7 @@ export function parseTestOutput(stdout) {
|
|
|
800
658
|
}
|
|
801
659
|
return null;
|
|
802
660
|
}
|
|
803
|
-
/** Cross-validate agent-reported
|
|
661
|
+
/** Cross-validate agent-reported tests_reported against command evidence output.
|
|
804
662
|
*
|
|
805
663
|
* For each command provider whose status is "passed" and output is not
|
|
806
664
|
* truncated, attempts to parse test counts from stdout. Mismatched counts
|
|
@@ -810,17 +668,17 @@ export function parseTestOutput(stdout) {
|
|
|
810
668
|
* Non-command evidence providers are skipped (no structural cross-check is
|
|
811
669
|
* defined for them yet). */
|
|
812
670
|
function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
813
|
-
const reported = selfEval.
|
|
671
|
+
const reported = selfEval.execution_report?.tests_reported;
|
|
814
672
|
if (!reported)
|
|
815
673
|
return null;
|
|
816
674
|
for (const snapshot of evidenceSnapshots) {
|
|
817
|
-
if (snapshot.
|
|
675
|
+
if (snapshot.kind !== "command")
|
|
818
676
|
continue;
|
|
819
|
-
if (snapshot.
|
|
677
|
+
if (snapshot.status !== "passed")
|
|
820
678
|
continue;
|
|
821
679
|
if (snapshot.data.truncated === true)
|
|
822
680
|
continue;
|
|
823
|
-
const stdout = typeof snapshot.data.
|
|
681
|
+
const stdout = typeof snapshot.data.stdoutExcerpt === "string" ? snapshot.data.stdoutExcerpt : "";
|
|
824
682
|
const parsed = parseTestOutput(stdout);
|
|
825
683
|
if (!parsed)
|
|
826
684
|
continue;
|
|
@@ -835,16 +693,14 @@ function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
|
835
693
|
// Counts match exactly — evidence supports the agent's claim.
|
|
836
694
|
continue;
|
|
837
695
|
}
|
|
838
|
-
const name =
|
|
839
|
-
? snapshot.data.commandName
|
|
840
|
-
: snapshot.provider;
|
|
696
|
+
const name = snapshot.data.commandId;
|
|
841
697
|
// Agent hides failures → error. Other mismatch → warn.
|
|
842
698
|
const severity = parsed.failed > 0 && reported.failed === 0 ? "error" : "warn";
|
|
843
699
|
return makeVerificationFlag({
|
|
844
700
|
severity,
|
|
845
|
-
field: "
|
|
701
|
+
field: "tests_reported",
|
|
846
702
|
check: CHECK_COMMAND_EVIDENCE_MISMATCH,
|
|
847
|
-
detail: `Agent
|
|
703
|
+
detail: `Agent tests_reported don't match "${name}" output: ${mismatches.join("; ")}`,
|
|
848
704
|
});
|
|
849
705
|
}
|
|
850
706
|
return null;
|
|
@@ -856,149 +712,99 @@ function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
|
856
712
|
* confirm the tests still verify the task claims). Fail-open when the git
|
|
857
713
|
* snapshot is missing or the command has no resolvable entrypoint. */
|
|
858
714
|
function checkVerificationDomainIntegrity(evidenceSnapshots) {
|
|
859
|
-
const git = evidenceSnapshots.find((snapshot) => snapshot.
|
|
715
|
+
const git = evidenceSnapshots.find((snapshot) => snapshot.providerId === "git") ?? null;
|
|
860
716
|
if (!git)
|
|
861
717
|
return null;
|
|
718
|
+
// v3.8.1: EVERY after-phase command is scanned for a tampered entrypoint
|
|
719
|
+
// before the test-file warn may answer for one of them. `testFilesModified`
|
|
720
|
+
// is derived from the git file set alone, so it is true for every command as
|
|
721
|
+
// soon as the round touched a test file; answering with that warn on the
|
|
722
|
+
// first command in array order meant a rewritten verification entrypoint
|
|
723
|
+
// produced no error flag at all, and the enforcement row that reacts to it
|
|
724
|
+
// never fired. The verdict depended on observation order.
|
|
862
725
|
for (const snapshot of evidenceSnapshots) {
|
|
863
|
-
if (!isRecord(snapshot.data) || snapshot.
|
|
726
|
+
if (!isRecord(snapshot.data) || snapshot.kind !== "command")
|
|
864
727
|
continue;
|
|
865
|
-
if (snapshot.
|
|
728
|
+
if (snapshot.phase !== "after")
|
|
866
729
|
continue;
|
|
867
|
-
|
|
868
|
-
|
|
730
|
+
if (!commandTampered(snapshot, git).entrypointModified)
|
|
731
|
+
continue;
|
|
732
|
+
return makeVerificationFlag({
|
|
733
|
+
severity: "error",
|
|
734
|
+
field: "execution_report",
|
|
735
|
+
check: CHECK_VERIFICATION_ENTRYPOINT_MODIFIED,
|
|
736
|
+
detail: `Verification command "${snapshot.data.commandId}" entrypoint changed this round — ` +
|
|
737
|
+
"its result cannot be trusted as machine evidence. Keep the " +
|
|
738
|
+
"verification command stable or resubmit the same round with the " +
|
|
739
|
+
"entrypoint unchanged.",
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
// Only when NO entrypoint was tampered does the test-file warn stand in.
|
|
743
|
+
for (const snapshot of evidenceSnapshots) {
|
|
744
|
+
if (!isRecord(snapshot.data) || snapshot.kind !== "command")
|
|
745
|
+
continue;
|
|
746
|
+
if (snapshot.phase !== "after")
|
|
747
|
+
continue;
|
|
748
|
+
if (!commandTampered(snapshot, git).testFilesModified)
|
|
869
749
|
continue;
|
|
870
|
-
const name = typeof snapshot.data.commandName === "string"
|
|
871
|
-
? snapshot.data.commandName
|
|
872
|
-
: snapshot.provider;
|
|
873
|
-
if (entrypointModified) {
|
|
874
|
-
return makeVerificationFlag({
|
|
875
|
-
severity: "error",
|
|
876
|
-
field: "execution_evidence",
|
|
877
|
-
check: CHECK_VERIFICATION_ENTRYPOINT_MODIFIED,
|
|
878
|
-
detail: `Verification command "${name}" entrypoint changed this round — ` +
|
|
879
|
-
"its result cannot be trusted as machine evidence. Keep the " +
|
|
880
|
-
"verification command stable or resubmit the same round with the " +
|
|
881
|
-
"entrypoint unchanged.",
|
|
882
|
-
});
|
|
883
|
-
}
|
|
884
750
|
return makeVerificationFlag({
|
|
885
751
|
severity: "warn",
|
|
886
|
-
field: "
|
|
752
|
+
field: "execution_report",
|
|
887
753
|
check: CHECK_TEST_FILES_MODIFIED,
|
|
888
|
-
detail: `Test files changed in the same round command "${
|
|
754
|
+
detail: `Test files changed in the same round command "${snapshot.data.commandId}" passed — ` +
|
|
889
755
|
"confirm the tests still verify the task claims.",
|
|
890
756
|
});
|
|
891
757
|
}
|
|
892
758
|
return null;
|
|
893
759
|
}
|
|
894
|
-
/** v2.
|
|
895
|
-
*
|
|
896
|
-
* plans work unrelated to any of them, it may indicate task drift.
|
|
897
|
-
* This is a warn-level advisory — the agent always owns prioritization.
|
|
760
|
+
/** v2.13: After a backtrack, check that the agent restored the workspace
|
|
761
|
+
* before working. Two arms:
|
|
898
762
|
*
|
|
899
|
-
*
|
|
900
|
-
*
|
|
901
|
-
*
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
const
|
|
917
|
-
?
|
|
918
|
-
:
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
763
|
+
* 1. Machine arm (M3, error): a skipped file whose CURRENT git fingerprint
|
|
764
|
+
* still equals the fingerprint recorded at its failed round was never
|
|
765
|
+
* touched since the rollback — the workspace was provably not restored.
|
|
766
|
+
* Restored or re-modified files change fingerprint and can never be
|
|
767
|
+
* misjudged by this arm.
|
|
768
|
+
*
|
|
769
|
+
* 2. Claims arm (warn only): the agent's files_changed overlaps the skipped
|
|
770
|
+
* list. A self-report alone cannot distinguish a restored-then-redone
|
|
771
|
+
* file from a never-restored one — a legitimate multi-file redo would
|
|
772
|
+
* otherwise be falsely terminated — so overlap is guidance, not a
|
|
773
|
+
* verdict. (The old error tier for >= 3 overlapping files was removed
|
|
774
|
+
* for exactly this reason: it fired on honest redos.) */
|
|
775
|
+
function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtrackSkippedFingerprints = {}, evidenceSnapshots = []) {
|
|
776
|
+
// Machine arm — untouched-since-rollback proof, independent of claims.
|
|
777
|
+
const failedEntries = Object.entries(backtrackSkippedFingerprints);
|
|
778
|
+
if (failedEntries.length > 0) {
|
|
779
|
+
const git = evidenceSnapshots.find((snapshot) => snapshot.kind === "git" && isRecord(snapshot.data.fingerprints));
|
|
780
|
+
const currentFingerprints = git && git.kind === "git"
|
|
781
|
+
? git.data.fingerprints
|
|
782
|
+
: null;
|
|
783
|
+
if (currentFingerprints) {
|
|
784
|
+
const untouched = failedEntries
|
|
785
|
+
.filter(([file, failedFp]) => currentFingerprints[file] === failedFp)
|
|
786
|
+
.map(([file]) => file)
|
|
787
|
+
.sort();
|
|
788
|
+
if (untouched.length > 0) {
|
|
789
|
+
return makeVerificationFlag({
|
|
790
|
+
severity: "error",
|
|
791
|
+
field: "workspace",
|
|
792
|
+
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
793
|
+
detail: `File(s) from the failed rounds are byte-identical to their ` +
|
|
794
|
+
`state at the rollback: ${untouched.slice(0, 5).join(", ")}. The ` +
|
|
795
|
+
`workspace was never restored — restore it to the restore point ` +
|
|
796
|
+
`(git checkout/reset) before redoing the work.`,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
927
799
|
}
|
|
928
800
|
}
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
pendingSubGoals.add(desc.trim());
|
|
932
|
-
}
|
|
933
|
-
for (const desc of selfEval.completed_subtasks ?? []) {
|
|
934
|
-
completedOrCanceled.add(desc.trim());
|
|
935
|
-
}
|
|
936
|
-
for (const desc of selfEval.canceled_subtasks ?? []) {
|
|
937
|
-
completedOrCanceled.add(desc.trim());
|
|
938
|
-
}
|
|
939
|
-
// Remove completed/canceled from pending
|
|
940
|
-
for (const desc of completedOrCanceled) {
|
|
941
|
-
pendingSubGoals.delete(desc);
|
|
942
|
-
}
|
|
943
|
-
// Need at least 3 pending sub-goals for the check to be meaningful
|
|
944
|
-
if (pendingSubGoals.size < 3)
|
|
801
|
+
const ev = selfEval.execution_report;
|
|
802
|
+
if (!ev || (ev.files_changed ?? []).length === 0)
|
|
945
803
|
return null;
|
|
946
|
-
//
|
|
947
|
-
|
|
948
|
-
const nextActionIds = extractSubGoalIds(nextAction);
|
|
949
|
-
if (nextActionIds.length > 0) {
|
|
950
|
-
const pendingIds = new Set([...pendingSubGoals].map((d) => deriveSubGoalId(d)));
|
|
951
|
-
if (nextActionIds.some((id) => pendingIds.has(id)))
|
|
952
|
-
return null;
|
|
953
|
-
}
|
|
954
|
-
// Check if next_action aligns with any pending sub-goal
|
|
955
|
-
let aligns = false;
|
|
956
|
-
for (const sg of pendingSubGoals) {
|
|
957
|
-
const score = jaccardSimilarity(nextAction, sg);
|
|
958
|
-
if (score >= getPolicy().evolution.subgoal_drift_alignment_threshold) {
|
|
959
|
-
aligns = true;
|
|
960
|
-
break;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
if (!aligns) {
|
|
964
|
-
const sample = [...pendingSubGoals].slice(0, 3).join(", ");
|
|
965
|
-
return makeVerificationFlag({
|
|
966
|
-
severity: "warn",
|
|
967
|
-
field: "next_action",
|
|
968
|
-
check: CHECK_SUBGOAL_DRIFT,
|
|
969
|
-
detail: `Agent's next_action doesn't align with any of ${pendingSubGoals.size} pending sub-goals. ` +
|
|
970
|
-
`Pending: ${sample}… Consider completing existing sub-goals before starting new work, ` +
|
|
971
|
-
`or cancel outdated sub-goals via canceled_subtasks.`,
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
return null;
|
|
975
|
-
}
|
|
976
|
-
/** v2.13: After a backtrack, check that the agent restored the workspace
|
|
977
|
-
* before working. If the agent's files_changed overlaps with files from
|
|
978
|
-
* skipped (failed) rounds, the workspace was likely not clean. */
|
|
979
|
-
function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles) {
|
|
980
|
-
if (backtrackSkippedFiles.length === 0)
|
|
981
|
-
return null;
|
|
982
|
-
const ev = selfEval.execution_evidence;
|
|
983
|
-
if (!ev || ev.files_changed.length === 0)
|
|
984
|
-
return null;
|
|
985
|
-
// Check overlap: files the agent changed vs. files from skipped rounds
|
|
986
|
-
const overlap = ev.files_changed.filter((f) => backtrackSkippedFiles.some((sf) => sf === f || f.endsWith(sf) || sf.endsWith(f)));
|
|
804
|
+
// Claims arm — overlap as guidance only.
|
|
805
|
+
const overlap = (ev.files_changed ?? []).filter((f) => backtrackSkippedFiles.some((sf) => sf === f || f.endsWith(sf) || sf.endsWith(f)));
|
|
987
806
|
if (overlap.length === 0)
|
|
988
807
|
return null;
|
|
989
|
-
// Agent is modifying files that were part of failed rounds without
|
|
990
|
-
// having properly restored the workspace first.
|
|
991
|
-
if (overlap.length >= 3 || overlap.length === ev.files_changed.length) {
|
|
992
|
-
return makeVerificationFlag({
|
|
993
|
-
severity: "error",
|
|
994
|
-
field: "files_changed",
|
|
995
|
-
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
996
|
-
detail: `Agent modified ${overlap.length} file(s) that were part of ` +
|
|
997
|
-
`the skipped (failed) rounds: ${overlap.slice(0, 5).join(", ")}. ` +
|
|
998
|
-
`The workspace was likely not restored before working.`,
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
// Minor overlap — warn only
|
|
1002
808
|
return makeVerificationFlag({
|
|
1003
809
|
severity: "warn",
|
|
1004
810
|
field: "files_changed",
|
|
@@ -1013,7 +819,7 @@ function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles) {
|
|
|
1013
819
|
function checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead) {
|
|
1014
820
|
if (!backtrackTargetGitHead)
|
|
1015
821
|
return null;
|
|
1016
|
-
const git = evidenceSnapshots.find((snapshot) => snapshot.
|
|
822
|
+
const git = evidenceSnapshots.find((snapshot) => snapshot.providerId === "git" && isRecord(snapshot.data) &&
|
|
1017
823
|
typeof snapshot.data.head === "string");
|
|
1018
824
|
if (!git)
|
|
1019
825
|
return null; // git unavailable → cannot verify → skip (fail-open)
|
|
@@ -1032,67 +838,23 @@ function checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead)
|
|
|
1032
838
|
});
|
|
1033
839
|
}
|
|
1034
840
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1035
|
-
// v3.
|
|
841
|
+
// v3.8 — Round Contract checks (all silent when no contract is declared)
|
|
842
|
+
//
|
|
843
|
+
// The item model moved closure to the runtime: an item is verified only when
|
|
844
|
+
// its bound commands were observed passing (untampered, same config) in the
|
|
845
|
+
// claiming round. A claimed-but-unbacked item is therefore `insufficient`
|
|
846
|
+
// (recorded, never a rejection), and a premature closure is structurally
|
|
847
|
+
// impossible. The checks below only surface state the agent must see.
|
|
1036
848
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1037
|
-
/** A PROPOSAL that promises nothing is a wasted round's worth of trust:
|
|
1038
|
-
* done_when empty means the boundary can never be machine-checked.
|
|
1039
|
-
* v3.4: declaration-quality check on the submission's own proposal — never
|
|
1040
|
-
* on the (derived) active contract. */
|
|
1041
|
-
function checkRoundUnderspecified(selfEval) {
|
|
1042
|
-
const contract = selfEval.round_contract;
|
|
1043
|
-
if (!contract)
|
|
1044
|
-
return null;
|
|
1045
|
-
if (contract.done_when.length > 0)
|
|
1046
|
-
return null;
|
|
1047
|
-
return makeVerificationFlag({
|
|
1048
|
-
severity: "warn",
|
|
1049
|
-
field: "round_contract",
|
|
1050
|
-
check: CHECK_ROUND_UNDERSPECIFIED,
|
|
1051
|
-
detail: "Round Contract declared with an empty done_when — nothing is " +
|
|
1052
|
-
"promised, so completion cannot be verified. Declare what must be " +
|
|
1053
|
-
"true when the round is done.",
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
/** done_when items whose verification_plan cannot run (empty plan, or names
|
|
1057
|
-
* outside the configured, enabled evidence.commands) can never be
|
|
1058
|
-
* machine-checked at the boundary. v3.4: declaration-quality check on the
|
|
1059
|
-
* submission's own proposal. */
|
|
1060
|
-
function checkRoundUnverifiable(selfEval) {
|
|
1061
|
-
const contract = selfEval.round_contract;
|
|
1062
|
-
if (!contract)
|
|
1063
|
-
return null;
|
|
1064
|
-
if (contract.done_when.length === 0)
|
|
1065
|
-
return null;
|
|
1066
|
-
if (contract.verification_plan.length > 0 &&
|
|
1067
|
-
contract.verification_plan.every((name) => isConfiguredCommand(name))) {
|
|
1068
|
-
return null;
|
|
1069
|
-
}
|
|
1070
|
-
const unknown = contract.verification_plan.filter((name) => !isConfiguredCommand(name));
|
|
1071
|
-
const detail = contract.verification_plan.length === 0
|
|
1072
|
-
? "done_when items declared but verification_plan is empty — no command " +
|
|
1073
|
-
"can back the completion claims"
|
|
1074
|
-
: `verification_plan names commands that are not configured and enabled ` +
|
|
1075
|
-
`in evidence.commands: ${unknown.slice(0, 3).join(", ")}`;
|
|
1076
|
-
return makeVerificationFlag({
|
|
1077
|
-
severity: "warn",
|
|
1078
|
-
field: "round_contract",
|
|
1079
|
-
check: CHECK_ROUND_UNVERIFIABLE,
|
|
1080
|
-
detail: detail + ". Name only configured, enabled evidence.commands, or drop " +
|
|
1081
|
-
"done_when items that cannot be machine-verified.",
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
849
|
/** The round changed files outside the ACTIVE contract's declared scope.
|
|
1085
850
|
* Git diff files are the machine-authoritative "what actually changed" set.
|
|
1086
|
-
*
|
|
1087
|
-
* from committed evals, never the submission's own proposal. Silent when
|
|
1088
|
-
* no active contract exists (round 1 and whole-task rounds: the agent
|
|
1089
|
-
* executed the original task, not a contract boundary). */
|
|
851
|
+
* Silent when no active contract exists or it declares no scope. */
|
|
1090
852
|
function checkRoundScopeDrift(activeContract, evidenceSnapshots) {
|
|
1091
853
|
if (!activeContract)
|
|
1092
854
|
return null;
|
|
1093
855
|
if (activeContract.scope.length === 0)
|
|
1094
856
|
return null;
|
|
1095
|
-
const git = evidenceSnapshots.find((snapshot) => snapshot.
|
|
857
|
+
const git = evidenceSnapshots.find((snapshot) => snapshot.providerId === "git");
|
|
1096
858
|
if (!git)
|
|
1097
859
|
return null; // git unavailable → cannot observe → fail open
|
|
1098
860
|
const outOfScope = collectOutOfScopeFiles(git.files, activeContract.scope);
|
|
@@ -1106,142 +868,53 @@ function checkRoundScopeDrift(activeContract, evidenceSnapshots) {
|
|
|
1106
868
|
(outOfScope.length > 5
|
|
1107
869
|
? ` (${outOfScope.length} total, showing 5): ${outOfScope.slice(0, 5).join(", ")}`
|
|
1108
870
|
: `: ${outOfScope.join(", ")}`) +
|
|
1109
|
-
`. Revert them or
|
|
1110
|
-
});
|
|
1111
|
-
}
|
|
1112
|
-
/** success claimed under the ACTIVE contract whose done_when items were not
|
|
1113
|
-
* honestly and verifiably completed: claimed met with no machine-verified
|
|
1114
|
-
* evidence this round, or silently dropped (present in neither
|
|
1115
|
-
* success_criteria_met nor success_criteria_remaining).
|
|
1116
|
-
* v3.4: targets the derived ACTIVE contract (the round's real boundary) —
|
|
1117
|
-
* a proposal declared on a round without an active contract is never
|
|
1118
|
-
* checked for conformance against work the round did not do under it.
|
|
1119
|
-
* v3.5.1: a fully-met posture (every done_when claimed satisfied) is owned
|
|
1120
|
-
* by contract_completion_unverified — this check stays for mixed and
|
|
1121
|
-
* silently-dropped success claims only.
|
|
1122
|
-
* v3.6: no_change_reason never downgrades this check — the escape hatch
|
|
1123
|
-
* belongs to the R8 success-claim family alone; a declared "no change"
|
|
1124
|
-
* contradicts silently dropping contract done_when items. */
|
|
1125
|
-
function checkPrematureBoundary(activeContract, selfEval, claimView) {
|
|
1126
|
-
if (!activeContract || activeContract.done_when.length === 0)
|
|
1127
|
-
return null;
|
|
1128
|
-
if (!effectiveSuccess(selfEval))
|
|
1129
|
-
return null;
|
|
1130
|
-
const ev = selfEval.execution_evidence;
|
|
1131
|
-
const met = ev?.success_criteria_met ?? [];
|
|
1132
|
-
// v3.5.1: full-met → the completion check owns the posture (it also fires
|
|
1133
|
-
// on success=false, so nothing is lost — and under machine_backed_success
|
|
1134
|
-
// "warn" the same tolerance applies on both sides).
|
|
1135
|
-
if (contractDoneWhenSatisfied(activeContract, met))
|
|
1136
|
-
return null;
|
|
1137
|
-
const remaining = ev?.success_criteria_remaining ?? [];
|
|
1138
|
-
const problems = [];
|
|
1139
|
-
for (const item of activeContract.done_when) {
|
|
1140
|
-
// Shared matcher with the lifecycle walker (round-contract.ts) — the
|
|
1141
|
-
// compile-side closure decision and this conformance check can never
|
|
1142
|
-
// disagree on what counts as a satisfied item.
|
|
1143
|
-
const isMet = met.some((m) => contractItemMatches(item, m));
|
|
1144
|
-
const isRemaining = remaining.some((r) => contractItemMatches(item, r));
|
|
1145
|
-
if (isMet) {
|
|
1146
|
-
// Claimed met — needs round-level machine verification. The v3.3
|
|
1147
|
-
// claim model is round-uniform: any verified claim backs the round,
|
|
1148
|
-
// so zero verified claims = none of them are backed.
|
|
1149
|
-
if (claimView.verifiedCount === 0) {
|
|
1150
|
-
problems.push(`"${item.slice(0, 80)}" claimed met without machine-verified evidence`);
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
else if (!isRemaining) {
|
|
1154
|
-
// Neither met nor remaining — silently dropped while success is
|
|
1155
|
-
// claimed. Honest reporting would list it in remaining (R1's domain).
|
|
1156
|
-
problems.push(`"${item.slice(0, 80)}" neither met nor listed as remaining`);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
if (problems.length === 0)
|
|
1160
|
-
return null;
|
|
1161
|
-
return makeVerificationFlag({
|
|
1162
|
-
severity: "error",
|
|
1163
|
-
field: "round_contract",
|
|
1164
|
-
check: CHECK_PREMATURE_BOUNDARY,
|
|
1165
|
-
detail: `Round Contract boundary claimed prematurely: ${problems.slice(0, 3).join("; ")}. ` +
|
|
1166
|
-
`Run the contract's verification_plan commands and report real output, ` +
|
|
1167
|
-
`or set success=false and list the items in success_criteria_remaining.`,
|
|
871
|
+
`. Revert them, or close the active contract and declare the extended scope in a new proposal.`,
|
|
1168
872
|
});
|
|
1169
873
|
}
|
|
1170
|
-
/** v3.
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
* a claim-based closure that the machine cannot back must not advance the
|
|
1176
|
-
* contract state machine. Severity follows evidence.machine_backed_success
|
|
1177
|
-
* (the R8 tolerance switch). Deliberately no no_change_reason downgrade:
|
|
1178
|
-
* all done_when met contradicts "no change". Fail-open: a plan name that
|
|
1179
|
-
* is no longer a configured, enabled command cannot be observed and is
|
|
1180
|
-
* not required. The check observes that the commands ran, not what they
|
|
1181
|
-
* verified — R-C1's claim model remains the content bound. */
|
|
1182
|
-
function checkContractCompletionUnverified(activeContract, selfEval, evidenceSnapshots) {
|
|
1183
|
-
if (!activeContract || activeContract.verification_plan.length === 0)
|
|
1184
|
-
return null;
|
|
1185
|
-
const met = selfEval.execution_evidence?.success_criteria_met ?? [];
|
|
1186
|
-
if (!contractDoneWhenSatisfied(activeContract, met))
|
|
1187
|
-
return null; // walker does not close
|
|
1188
|
-
const required = activeContract.verification_plan
|
|
1189
|
-
.filter((name) => isConfiguredCommand(name));
|
|
1190
|
-
if (required.length === 0)
|
|
1191
|
-
return null; // fail open — cannot observe
|
|
1192
|
-
const passed = passedPlanCommandNames(evidenceSnapshots);
|
|
1193
|
-
const missing = required.filter((name) => !passed.has(name));
|
|
1194
|
-
if (missing.length === 0)
|
|
874
|
+
/** v3.8: Claimed-but-unverified contract items. The round COMMITS — the debt
|
|
875
|
+
* is surfaced (prompt, state file, handoff) and the enforcement gate's
|
|
876
|
+
* verification-debt row handles a persistent pattern. */
|
|
877
|
+
function checkContractItemsUnverified(statuses) {
|
|
878
|
+
if (statuses.insufficientCount === 0)
|
|
1195
879
|
return null;
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
880
|
+
const unverified = statuses.items
|
|
881
|
+
.filter((item) => item.status === "insufficient")
|
|
882
|
+
.slice(0, 3)
|
|
883
|
+
.map((item) => item.itemId);
|
|
1199
884
|
return makeVerificationFlag({
|
|
1200
|
-
severity,
|
|
885
|
+
severity: "warn",
|
|
1201
886
|
field: "round_contract",
|
|
1202
|
-
check:
|
|
1203
|
-
detail:
|
|
1204
|
-
|
|
1205
|
-
`${missing.slice(0, 3).join(", ")}. Closing a contract is a success-class claim — ` +
|
|
1206
|
-
`fix the underlying failure so the command${missing.length > 1 ? "s" : ""} pass and ` +
|
|
1207
|
-
`resubmit; no_change_reason does not apply to completion claims.`,
|
|
887
|
+
check: CHECK_CONTRACT_ITEMS_UNVERIFIED,
|
|
888
|
+
detail: `${statuses.insufficientCount} contract item(s) claimed met but not machine-verified: ` +
|
|
889
|
+
`${unverified.join(", ")}. Run the bound command(s) or report the item as remaining.`,
|
|
1208
890
|
});
|
|
1209
891
|
}
|
|
1210
|
-
/**
|
|
1211
|
-
*
|
|
1212
|
-
*
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
|
|
1216
|
-
* key-order-insensitive and normalized through parseRoundContract on both
|
|
1217
|
-
* sides so committed-raw vs parsed-submission key-set drift cannot cause
|
|
1218
|
-
* spurious warns. */
|
|
1219
|
-
function checkContractPremature(activeContract, selfEval) {
|
|
892
|
+
/** A different contract proposed while the ACTIVE contract is still open. The
|
|
893
|
+
* walker ignores the premature proposal — this warn only makes the ignored
|
|
894
|
+
* state visible. Silent when the active contract is closed (all items
|
|
895
|
+
* verified, or outcome=blocked), when no proposal is submitted, or when the
|
|
896
|
+
* proposal is a restate (content equality, never id equality). */
|
|
897
|
+
function checkContractPremature(activeContract, statuses, selfEval) {
|
|
1220
898
|
if (!activeContract)
|
|
1221
899
|
return null;
|
|
1222
|
-
|
|
1223
|
-
if (contractDoneWhenSatisfied(activeContract, met))
|
|
900
|
+
if (statuses.closure !== "open")
|
|
1224
901
|
return null; // closed
|
|
1225
902
|
if (selfEval.outcome === "blocked")
|
|
1226
903
|
return null; // closed
|
|
1227
904
|
const proposal = selfEval.round_contract;
|
|
1228
905
|
if (!proposal)
|
|
1229
906
|
return null;
|
|
1230
|
-
|
|
1231
|
-
stableStringify(parseRoundContract(proposal));
|
|
1232
|
-
if (same)
|
|
907
|
+
if (sameContract(proposal, activeContract))
|
|
1233
908
|
return null; // restate → continue
|
|
1234
909
|
return makeVerificationFlag({
|
|
1235
910
|
severity: "warn",
|
|
1236
911
|
field: "round_contract",
|
|
1237
912
|
check: CHECK_CONTRACT_PREMATURE,
|
|
1238
|
-
detail: "The ACTIVE Round Contract is still open (not
|
|
913
|
+
detail: "The ACTIVE Round Contract is still open (not every item verified, not blocked) " +
|
|
1239
914
|
"while a different contract was proposed — it is ignored until the active " +
|
|
1240
|
-
"contract
|
|
1241
|
-
"continue it.",
|
|
915
|
+
"contract closes. Restate the active contract unchanged to continue it.",
|
|
1242
916
|
});
|
|
1243
917
|
}
|
|
1244
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
1245
918
|
// Main entry point
|
|
1246
919
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1247
920
|
/** Verify a SelfEvaluation against the loop's cross-round lineage.
|
|
@@ -1256,15 +929,80 @@ function checkContractPremature(activeContract, selfEval) {
|
|
|
1256
929
|
* @param evidenceSnapshots v1.18: Evidence snapshots from configured providers.
|
|
1257
930
|
* Used by checkEvidenceIntegrity for multi-provider
|
|
1258
931
|
* cross-validation. Defaults to empty array. */
|
|
1259
|
-
|
|
932
|
+
/** v3.7.1: Opt-in round blocking for the gate layer. For every gate id the
|
|
933
|
+
* round cites (gate_ids), the gate_opened record must exist, must be a
|
|
934
|
+
* USER gate, and must have an approved gate_decision. Error flags list
|
|
935
|
+
* every failing id with a reason. Entirely skipped when the feature is
|
|
936
|
+
* disabled — there is nothing to cite against. */
|
|
937
|
+
function checkUserGateUnresolved(selfEval, gateEntries) {
|
|
938
|
+
if (!getPolicy().gate.enabled)
|
|
939
|
+
return null;
|
|
940
|
+
const cited = (selfEval.gate_ids ?? []).filter((id) => typeof id === "string");
|
|
941
|
+
if (cited.length === 0)
|
|
942
|
+
return null;
|
|
943
|
+
const opened = new Set();
|
|
944
|
+
const userKind = new Set();
|
|
945
|
+
const approved = new Set();
|
|
946
|
+
for (const entry of gateEntries) {
|
|
947
|
+
if (entry.task_type === "gate_opened" && typeof entry.gate_id === "string") {
|
|
948
|
+
opened.add(entry.gate_id);
|
|
949
|
+
// v3.7.1: structured records declare gate_kind; legacy flat records
|
|
950
|
+
// (blocked-round auto-records) classify their stored action text.
|
|
951
|
+
if (entry.gate_kind === "user") {
|
|
952
|
+
userKind.add(entry.gate_id);
|
|
953
|
+
}
|
|
954
|
+
else if (typeof entry.gate_action === "string") {
|
|
955
|
+
let kind = null;
|
|
956
|
+
try {
|
|
957
|
+
kind = preflightStructuredGate(JSON.parse(entry.gate_action)).kind;
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
kind = deriveGate(entry.gate_action).gate.kind;
|
|
961
|
+
}
|
|
962
|
+
if (kind === "user")
|
|
963
|
+
userKind.add(entry.gate_id);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (entry.task_type === "gate_decision" && entry.approved === true &&
|
|
967
|
+
typeof entry.gate_id === "string") {
|
|
968
|
+
approved.add(entry.gate_id);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
const failures = [];
|
|
972
|
+
for (const gateId of cited) {
|
|
973
|
+
if (!opened.has(gateId))
|
|
974
|
+
failures.push(`${gateId} (not_found)`);
|
|
975
|
+
else if (!userKind.has(gateId))
|
|
976
|
+
failures.push(`${gateId} (not_user_gate)`);
|
|
977
|
+
else if (!approved.has(gateId))
|
|
978
|
+
failures.push(`${gateId} (not_approved)`);
|
|
979
|
+
}
|
|
980
|
+
if (failures.length === 0)
|
|
981
|
+
return null;
|
|
982
|
+
return makeVerificationFlag({
|
|
983
|
+
severity: "error",
|
|
984
|
+
field: "evaluation",
|
|
985
|
+
check: CHECK_USER_GATE_UNRESOLVED,
|
|
986
|
+
detail: `The round cites gates without an approved human decision: ` +
|
|
987
|
+
`${failures.join("; ")}. Obtain approval via loopforge_gate_check / ` +
|
|
988
|
+
`loopforge_gate_resolve before claiming the action is done.`,
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
export function verifySelfEvaluation(selfEval, currentRound, vaultEntries, prevSelfEval = null, evidenceSnapshots = [],
|
|
1260
992
|
/** v2.13: Files from skipped backtrack rounds. If the agent's
|
|
1261
993
|
* files_changed overlaps significantly with these, the workspace
|
|
1262
994
|
* was not properly restored before working. */
|
|
1263
|
-
backtrackSkippedFiles = [],
|
|
995
|
+
backtrackSkippedFiles = [],
|
|
996
|
+
/** M3 (v3.7.x): skipped-file git fingerprints at their failed rounds —
|
|
997
|
+
* machine proof of "untouched since the rollback" for the restore check. */
|
|
998
|
+
backtrackSkippedFingerprints = {},
|
|
1264
999
|
/** v2.12: Git HEAD commit of the backtrack restore point. When set, the
|
|
1265
1000
|
* current git snapshot must sit at this commit — otherwise the workspace
|
|
1266
1001
|
* was not restored and the round cannot be accepted. */
|
|
1267
|
-
backtrackTargetGitHead
|
|
1002
|
+
backtrackTargetGitHead,
|
|
1003
|
+
/** v3.7.1: gate records (task_type gate_opened / gate_decision) live
|
|
1004
|
+
* outside the round prefix — the caller passes them in explicitly. */
|
|
1005
|
+
gateEntries = []) {
|
|
1268
1006
|
const flags = [];
|
|
1269
1007
|
// Collect violations from all previous vault entries for duplicate-discovery
|
|
1270
1008
|
// and other checks that need deeper history.
|
|
@@ -1278,7 +1016,18 @@ backtrackTargetGitHead) {
|
|
|
1278
1016
|
// round_contract is a PROPOSAL for the next round and never participates
|
|
1279
1017
|
// (the eval under verification is not yet committed, so it structurally
|
|
1280
1018
|
// cannot influence the derivation).
|
|
1281
|
-
const
|
|
1019
|
+
const committedRounds = derivationRounds(vaultEntries, currentRound);
|
|
1020
|
+
const activeContract = deriveActiveRoundContract(committedRounds);
|
|
1021
|
+
// v3.8: The item-level status of the ACTIVE contract for THIS round — the
|
|
1022
|
+
// same reducer the compile path, audit, and explain consume.
|
|
1023
|
+
const contractStatuses = deriveContractItemStatuses({
|
|
1024
|
+
contract: activeContract,
|
|
1025
|
+
rounds: committedRounds,
|
|
1026
|
+
currentRound,
|
|
1027
|
+
currentReport: selfEval.execution_report ?? null,
|
|
1028
|
+
currentObservations: evidenceSnapshots,
|
|
1029
|
+
commands: getPolicy().evidence.commands ?? [],
|
|
1030
|
+
});
|
|
1282
1031
|
// v2.12: Derived claim provenance — which reported criteria completions
|
|
1283
1032
|
// are backed by machine evidence (pure derivation, not persisted).
|
|
1284
1033
|
const claimView = deriveClaimView(selfEval, evidenceSnapshots);
|
|
@@ -1308,30 +1057,24 @@ backtrackTargetGitHead) {
|
|
|
1308
1057
|
// v1.18: Cross-validate agent-reported files_changed against git evidence
|
|
1309
1058
|
() => checkEvidenceIntegrity(selfEval, evidenceSnapshots),
|
|
1310
1059
|
() => checkRequiredCommandEvidence(selfEval, evidenceSnapshots),
|
|
1311
|
-
// v2.0: Cross-validate agent-reported
|
|
1060
|
+
// v2.0: Cross-validate agent-reported tests_reported against command output
|
|
1312
1061
|
() => checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots),
|
|
1313
1062
|
// v3.3: Verification domain integrity — command entrypoint / test files
|
|
1314
1063
|
// changed in the same round the command ran
|
|
1315
1064
|
() => checkVerificationDomainIntegrity(evidenceSnapshots),
|
|
1316
|
-
// v2.1: Detect intent-action drift — agent said X but did Y
|
|
1317
|
-
() => checkIntentDrift(selfEval, prevSelfEval, vaultEntries),
|
|
1318
|
-
// v2.2: Detect sub-goal drift — next_action doesn't align with pending sub-goals
|
|
1319
|
-
() => checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound),
|
|
1320
1065
|
// v2.13: Post-backtrack workspace restore check
|
|
1321
|
-
() => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles),
|
|
1066
|
+
() => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtrackSkippedFingerprints, evidenceSnapshots),
|
|
1322
1067
|
// v2.12: Post-backtrack git HEAD restore check
|
|
1323
1068
|
() => checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead),
|
|
1324
|
-
// v3.
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1327
|
-
//
|
|
1328
|
-
// visibility (warn).
|
|
1329
|
-
() => checkRoundUnderspecified(selfEval),
|
|
1330
|
-
() => checkRoundUnverifiable(selfEval),
|
|
1069
|
+
// v3.8: Round Contract checks over the derived item model. Declaration
|
|
1070
|
+
// quality is enforced structurally (contract_invalid) before the round
|
|
1071
|
+
// advances, so the gate only reports execution state: scope drift,
|
|
1072
|
+
// claimed-but-unverified items, and an ignored replacement proposal.
|
|
1331
1073
|
() => checkRoundScopeDrift(activeContract, evidenceSnapshots),
|
|
1332
|
-
() =>
|
|
1333
|
-
() =>
|
|
1334
|
-
|
|
1074
|
+
() => checkContractItemsUnverified(contractStatuses),
|
|
1075
|
+
() => checkContractPremature(activeContract, contractStatuses, selfEval),
|
|
1076
|
+
// v3.7.1: opt-in gate-layer blocking (skipped when disabled)
|
|
1077
|
+
() => checkUserGateUnresolved(selfEval, gateEntries),
|
|
1335
1078
|
];
|
|
1336
1079
|
for (const run of checks) {
|
|
1337
1080
|
const result = run();
|
|
@@ -1353,6 +1096,14 @@ backtrackTargetGitHead) {
|
|
|
1353
1096
|
else {
|
|
1354
1097
|
verdict = "trusted";
|
|
1355
1098
|
}
|
|
1356
|
-
|
|
1099
|
+
// v3.8.1: the machine-backed-success fact rides along with the result —
|
|
1100
|
+
// ONE derivation (`deriveEvidenceStatus`), so the progress evaluator reads
|
|
1101
|
+
// the same fact instead of inventing a second one. A round the machine
|
|
1102
|
+
// verified is the loop FINISHING; a motion window says nothing about it,
|
|
1103
|
+
// and the old self-reported `>= 0.95` guard said it with the agent's own
|
|
1104
|
+
// number.
|
|
1105
|
+
const machineBackedSuccess = effectiveSuccess(selfEval) &&
|
|
1106
|
+
deriveEvidenceStatus(selfEval, evidenceSnapshots).providerStatus === "verified";
|
|
1107
|
+
return makeVerificationResult({ verdict, flags, machineBackedSuccess });
|
|
1357
1108
|
}
|
|
1358
1109
|
//# sourceMappingURL=verification-gate.js.map
|