loopforge 2.11.0 → 3.2.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/README.md +4 -4
- package/dist/audit.d.ts +50 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +158 -0
- package/dist/audit.js.map +1 -0
- package/dist/canonical-state.d.ts +21 -9
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +34 -16
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -6
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +40 -0
- package/dist/cognitive-governance.d.ts.map +1 -0
- package/dist/cognitive-governance.js +126 -0
- package/dist/cognitive-governance.js.map +1 -0
- package/dist/enforcement-gate.d.ts +8 -3
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +419 -92
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +47 -7
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +303 -60
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts +50 -0
- package/dist/evidence-claims.d.ts.map +1 -0
- package/dist/evidence-claims.js +134 -0
- package/dist/evidence-claims.js.map +1 -0
- package/dist/generate-schema.js +6 -1
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +41 -3
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +310 -38
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-projection.d.ts +26 -0
- package/dist/loop-projection.d.ts.map +1 -0
- package/dist/loop-projection.js +142 -0
- package/dist/loop-projection.js.map +1 -0
- package/dist/loop-store.d.ts +91 -41
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +243 -173
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +216 -0
- package/dist/mcp/round-lifecycle.d.ts.map +1 -0
- package/dist/mcp/round-lifecycle.js +926 -0
- package/dist/mcp/round-lifecycle.js.map +1 -0
- package/dist/mcp/server.d.ts +3 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +65 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +47 -139
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +284 -784
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +23 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +270 -79
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +11 -0
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +118 -21
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +26 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +13 -2
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +34 -1
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +320 -42
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +169 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +39 -0
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +5 -5
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +9 -8
- package/dist/replay.js.map +1 -1
- package/dist/round-coordinator.d.ts +6 -3
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +38 -19
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +9 -4
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +15 -7
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +7 -3
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +93 -88
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +31 -1
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +115 -12
- package/dist/self-eval.js.map +1 -1
- package/dist/storage.d.ts +3 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +5 -4
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +20 -7
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +68 -9
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +54 -2
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +433 -25
- package/dist/verification-gate.js.map +1 -1
- package/loop_policy.json +35 -8
- package/package.json +3 -2
- package/skills/perception/SKILL.md +24 -6
- package/dist/backends/interface.d.ts +0 -39
- package/dist/backends/interface.d.ts.map +0 -1
- package/dist/backends/interface.js +0 -6
- package/dist/backends/interface.js.map +0 -1
|
@@ -14,7 +14,37 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { getPolicy } from "./policy.js";
|
|
16
16
|
import { makeVerificationFlag, makeVerificationResult } from "./protocol.js";
|
|
17
|
-
import { jaccardSimilarity, entryRound as sharedEntryRound } from "./token-utils.js";
|
|
17
|
+
import { jaccardSimilarity, tokenize, entryRound as sharedEntryRound, isRecord } from "./token-utils.js";
|
|
18
|
+
import { deriveSubGoalId } from "./loop-compiler.js";
|
|
19
|
+
import { deriveClaimView, resolveRoundFiles } from "./evidence-claims.js";
|
|
20
|
+
import { effectiveSuccess } from "./self-eval.js";
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
+
// v2.12: Check-name constants — single source of truth so the enforcement
|
|
23
|
+
// gate and audit can reference checks without string-literal drift.
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
+
export const CHECK_PROGRESS_REGRESSION = "progress_regression";
|
|
26
|
+
export const CHECK_EMPTY_CHANGE_WITH_PASSING = "empty_change_with_passing";
|
|
27
|
+
export const CHECK_SUCCESS_WITH_REMAINING_CRITERIA = "success_with_remaining_criteria";
|
|
28
|
+
export const CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE = "success_without_verified_evidence";
|
|
29
|
+
export const CHECK_OUTCOME_SUCCESS_CONTRADICTION = "outcome_success_contradiction";
|
|
30
|
+
export const CHECK_SUCCESS_CLAIM_CONFLICT = "success_claim_conflict";
|
|
31
|
+
export const CHECK_BLOCKED_WITHOUT_BLOCKER = "blocked_without_blocker";
|
|
32
|
+
export const CHECK_RETROACTIVE_CLAIM_BAD_ROUND = "retroactive_claim_bad_round";
|
|
33
|
+
export const CHECK_RETROACTIVE_CLAIM_UNVERIFIED = "retroactive_claim_unverified";
|
|
34
|
+
export const CHECK_DUPLICATE_CONSTRAINT_DISCOVERY = "duplicate_constraint_discovery";
|
|
35
|
+
export const CHECK_RECURRING_VIOLATION = "recurring_violation";
|
|
36
|
+
export const CHECK_RETRACT_FRESH_CONSTRAINT = "retract_fresh_constraint";
|
|
37
|
+
export const CHECK_EVIDENCE_INTEGRITY = "evidence_integrity";
|
|
38
|
+
export const CHECK_REQUIRED_COMMAND_FAILED = "required_command_failed";
|
|
39
|
+
export const CHECK_COMMAND_EVIDENCE_MISMATCH = "command_evidence_mismatch";
|
|
40
|
+
export const CHECK_INTENT_DRIFT = "intent_drift";
|
|
41
|
+
export const CHECK_SUBGOAL_DRIFT = "subgoal_drift";
|
|
42
|
+
export const CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED = "backtrack_workspace_not_restored";
|
|
43
|
+
export const CHECK_CRITERIA_CLAIMS_UNVERIFIED = "criteria_claims_unverified";
|
|
44
|
+
/** v3.2: success declared with no machine-verified observation this round
|
|
45
|
+
* (providerStatus ≠ verified). Warn-level: the round still commits, but its
|
|
46
|
+
* success never enters the trajectory and trust drops. */
|
|
47
|
+
export const CHECK_SUCCESS_UNVERIFIED = "success_unverified";
|
|
18
48
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
49
|
// Helpers
|
|
20
50
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -34,6 +64,90 @@ function entryViolations(entry) {
|
|
|
34
64
|
return viols.filter((v) => typeof v === "string");
|
|
35
65
|
return [];
|
|
36
66
|
}
|
|
67
|
+
function passedAfterCommand(snapshots) {
|
|
68
|
+
return snapshots.find((snapshot) => isRecord(snapshot.data) &&
|
|
69
|
+
snapshot.data.kind === "command" &&
|
|
70
|
+
snapshot.data.phase === "after" &&
|
|
71
|
+
snapshot.data.status === "passed") ?? null;
|
|
72
|
+
}
|
|
73
|
+
/** v3.2: Derive the machine-verification status of a round. The verification
|
|
74
|
+
* capability is modeled explicitly (verified / unavailable / absent) instead
|
|
75
|
+
* of letting evidence-dependent checks silently disappear when snapshots are
|
|
76
|
+
* missing — the fix for the "weakest when it matters most" gap. */
|
|
77
|
+
export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
|
|
78
|
+
const gitSnap = evidenceSnapshots.find((snapshot) => snapshot.provider === "git") ?? null;
|
|
79
|
+
const gitObserved = gitSnap !== null && gitSnap.files.length > 0;
|
|
80
|
+
const command = passedAfterCommand(evidenceSnapshots);
|
|
81
|
+
const commandVerified = command !== null;
|
|
82
|
+
// testsMachineBacked: agent-reported test_results agree with a passed
|
|
83
|
+
// command's parsed stdout (the same comparison checkCommandEvidenceIntegrity
|
|
84
|
+
// performs — parseTestOutput lives in this module).
|
|
85
|
+
let testsMachineBacked = false;
|
|
86
|
+
const reported = selfEval.execution_evidence?.test_results;
|
|
87
|
+
if (command && reported) {
|
|
88
|
+
const parsed = parseTestOutput(String(command.data.stdout ?? ""));
|
|
89
|
+
testsMachineBacked = parsed !== null &&
|
|
90
|
+
parsed.passed === reported.passed &&
|
|
91
|
+
parsed.failed === reported.failed &&
|
|
92
|
+
parsed.skipped === reported.skipped;
|
|
93
|
+
}
|
|
94
|
+
// reportedFilesMatch: agent-reported files_changed equals the git diff set.
|
|
95
|
+
let reportedFilesMatch = false;
|
|
96
|
+
if (gitSnap && selfEval.execution_evidence) {
|
|
97
|
+
const reportedSet = [...selfEval.execution_evidence.files_changed].sort();
|
|
98
|
+
const actualSet = [...gitSnap.files].sort();
|
|
99
|
+
reportedFilesMatch = reportedSet.length === actualSet.length &&
|
|
100
|
+
reportedSet.every((file, index) => file === actualSet[index]);
|
|
101
|
+
}
|
|
102
|
+
const providerStatus = evidenceSnapshots.length === 0
|
|
103
|
+
? "absent"
|
|
104
|
+
: gitObserved || commandVerified ? "verified" : "unavailable";
|
|
105
|
+
return {
|
|
106
|
+
providerStatus,
|
|
107
|
+
gitObserved,
|
|
108
|
+
commandVerified,
|
|
109
|
+
testsMachineBacked,
|
|
110
|
+
reportedFilesMatch,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** v3.2: Per-round machine progress — whether git observed file changes in
|
|
114
|
+
* each of the last `lookback` committed rounds, rebuilt from the feedback
|
|
115
|
+
* entries' roundEvidence snapshots (already persisted at commit). Returns
|
|
116
|
+
* null when fewer than `lookback` rounds carry git snapshots — the machine
|
|
117
|
+
* signal is unavailable and callers (R4/R5 on the heuristic path) keep
|
|
118
|
+
* their legacy skip. The snapshot shape is parsed defensively here because
|
|
119
|
+
* this module must not import round-transaction (import cycle). */
|
|
120
|
+
export function machineProgressSeries(vaultEntries, currentRound, lookback) {
|
|
121
|
+
const byRound = new Map();
|
|
122
|
+
for (const entry of vaultEntries) {
|
|
123
|
+
const tid = String(entry.task_id ?? "");
|
|
124
|
+
if (!tid.endsWith(":feedback"))
|
|
125
|
+
continue;
|
|
126
|
+
const rnd = entryRound(entry);
|
|
127
|
+
if (rnd <= 0 || rnd >= currentRound)
|
|
128
|
+
continue;
|
|
129
|
+
const rt = isRecord(entry.loop_lineage)
|
|
130
|
+
? entry.loop_lineage.round_transaction
|
|
131
|
+
: null;
|
|
132
|
+
const snapshot = isRecord(rt) ? rt.snapshot : null;
|
|
133
|
+
const roundEvidence = isRecord(snapshot) ? snapshot.roundEvidence : null;
|
|
134
|
+
if (!Array.isArray(roundEvidence))
|
|
135
|
+
continue;
|
|
136
|
+
const git = roundEvidence.find((value) => isRecord(value) && value.provider === "git");
|
|
137
|
+
if (!git)
|
|
138
|
+
continue;
|
|
139
|
+
const gitFiles = git.files;
|
|
140
|
+
byRound.set(rnd, Array.isArray(gitFiles) && gitFiles.length > 0);
|
|
141
|
+
}
|
|
142
|
+
if (byRound.size < lookback)
|
|
143
|
+
return null;
|
|
144
|
+
const sorted = [...byRound.keys()].sort((a, b) => a - b);
|
|
145
|
+
const recent = sorted.slice(-lookback);
|
|
146
|
+
// Continuity: the recent rounds must be the actual last `lookback` rounds.
|
|
147
|
+
if (recent[0] < currentRound - lookback)
|
|
148
|
+
return null;
|
|
149
|
+
return recent.map((round) => byRound.get(round));
|
|
150
|
+
}
|
|
37
151
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
152
|
// Individual checks — each returns a VerificationFlag or null
|
|
39
153
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -52,7 +166,7 @@ function checkProgressRegression(selfEval, prevSelfEval) {
|
|
|
52
166
|
return makeVerificationFlag({
|
|
53
167
|
severity: "warn",
|
|
54
168
|
field: "progress_estimate",
|
|
55
|
-
check:
|
|
169
|
+
check: CHECK_PROGRESS_REGRESSION,
|
|
56
170
|
detail: `Progress dropped from ${prevProgress.toFixed(2)} to ` +
|
|
57
171
|
`${currProgress.toFixed(2)} (delta: ${(currProgress - prevProgress).toFixed(2)})`,
|
|
58
172
|
});
|
|
@@ -65,18 +179,18 @@ function checkEmptyChangeWithPassing(selfEval) {
|
|
|
65
179
|
const testsAllPass = ev.test_results !== null &&
|
|
66
180
|
ev.test_results.failed === 0 &&
|
|
67
181
|
ev.test_results.passed > 0;
|
|
68
|
-
if (!filesEmpty || !testsAllPass || !selfEval
|
|
182
|
+
if (!filesEmpty || !testsAllPass || !effectiveSuccess(selfEval))
|
|
69
183
|
return null;
|
|
70
184
|
return makeVerificationFlag({
|
|
71
185
|
severity: "warn",
|
|
72
186
|
field: "execution_evidence",
|
|
73
|
-
check:
|
|
187
|
+
check: CHECK_EMPTY_CHANGE_WITH_PASSING,
|
|
74
188
|
detail: "Agent claims success with no files changed and all tests passing — " +
|
|
75
189
|
"verify that work was actually performed",
|
|
76
190
|
});
|
|
77
191
|
}
|
|
78
192
|
function checkSuccessWithRemainingCriteria(selfEval) {
|
|
79
|
-
if (!selfEval
|
|
193
|
+
if (!effectiveSuccess(selfEval))
|
|
80
194
|
return null;
|
|
81
195
|
const remaining = selfEval.execution_evidence?.success_criteria_remaining;
|
|
82
196
|
if (!remaining || remaining.length === 0)
|
|
@@ -84,11 +198,154 @@ function checkSuccessWithRemainingCriteria(selfEval) {
|
|
|
84
198
|
return makeVerificationFlag({
|
|
85
199
|
severity: "error",
|
|
86
200
|
field: "success",
|
|
87
|
-
check:
|
|
201
|
+
check: CHECK_SUCCESS_WITH_REMAINING_CRITERIA,
|
|
88
202
|
detail: `Agent claims success but ${remaining.length} criteria remain unmet: ` +
|
|
89
203
|
remaining.slice(0, 3).join("; "),
|
|
90
204
|
});
|
|
91
205
|
}
|
|
206
|
+
/** v2.12: Criteria reported met but none machine-verified. The agent puts
|
|
207
|
+
* completed criteria into the ledger, but no passing test evidence or
|
|
208
|
+
* command snapshot backs them. Warn — completion still counts, but the
|
|
209
|
+
* next prompt tells the agent its claims are unverified. This is the
|
|
210
|
+
* mild form of the 3.x criterion-specific completion rule. */
|
|
211
|
+
function checkUnverifiedCriteriaClaims(selfEval, claimView) {
|
|
212
|
+
const met = selfEval.execution_evidence?.success_criteria_met;
|
|
213
|
+
if (!met || met.length === 0)
|
|
214
|
+
return null;
|
|
215
|
+
if (claimView.verifiedCount > 0 || claimView.hasMachineEvidence)
|
|
216
|
+
return null;
|
|
217
|
+
return makeVerificationFlag({
|
|
218
|
+
severity: "warn",
|
|
219
|
+
field: "execution_evidence",
|
|
220
|
+
check: CHECK_CRITERIA_CLAIMS_UNVERIFIED,
|
|
221
|
+
detail: `${met.length} criteria reported met but none machine-verified ` +
|
|
222
|
+
"(no passing test evidence or command snapshot)",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/** v2.12: Declared outcome vs legacy boolean consistency. A declared success
|
|
226
|
+
* with success=false is a self-contradiction (error); a declared non-success
|
|
227
|
+
* with success=true is a compat conflict (warn). Also: outcome==="blocked"
|
|
228
|
+
* without a blocker description gets a warn so the next prompt asks for it. */
|
|
229
|
+
function checkOutcomeConsistency(selfEval) {
|
|
230
|
+
const outcome = selfEval.outcome;
|
|
231
|
+
if (outcome === "success" && selfEval.success === false) {
|
|
232
|
+
return makeVerificationFlag({
|
|
233
|
+
severity: "error",
|
|
234
|
+
field: "outcome",
|
|
235
|
+
check: CHECK_OUTCOME_SUCCESS_CONTRADICTION,
|
|
236
|
+
detail: "outcome=success but success=false — self-contradictory",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (outcome !== undefined && outcome !== "success" && selfEval.success === true) {
|
|
240
|
+
return makeVerificationFlag({
|
|
241
|
+
severity: "warn",
|
|
242
|
+
field: "success",
|
|
243
|
+
check: CHECK_SUCCESS_CLAIM_CONFLICT,
|
|
244
|
+
detail: `outcome=${outcome} but success=true — the declared outcome wins`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (outcome === "blocked" &&
|
|
248
|
+
(typeof selfEval.blocker !== "string" || selfEval.blocker.trim().length === 0)) {
|
|
249
|
+
return makeVerificationFlag({
|
|
250
|
+
severity: "warn",
|
|
251
|
+
field: "blocker",
|
|
252
|
+
check: CHECK_BLOCKED_WITHOUT_BLOCKER,
|
|
253
|
+
detail: "outcome=blocked but no blocker description was provided",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
/** v2.12: Retroactive claims reference PRIOR rounds. Each claim's file
|
|
259
|
+
* paths are checked against the referenced round's committed git evidence
|
|
260
|
+
* (P0 provenance layer). Unverifiable claims warn — they never upgrade
|
|
261
|
+
* criteria status. */
|
|
262
|
+
function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
|
|
263
|
+
const claims = selfEval.retroactiveClaims;
|
|
264
|
+
if (!claims || claims.length === 0)
|
|
265
|
+
return null;
|
|
266
|
+
const loopId = vaultEntries.find((entry) => typeof entry.loop_id === "string")
|
|
267
|
+
?.loop_id;
|
|
268
|
+
const flags = [];
|
|
269
|
+
for (const claim of claims) {
|
|
270
|
+
if (claim.round < 1 || claim.round >= currentRound) {
|
|
271
|
+
flags.push(makeVerificationFlag({
|
|
272
|
+
severity: "warn",
|
|
273
|
+
field: "retroactiveClaims",
|
|
274
|
+
check: CHECK_RETROACTIVE_CLAIM_BAD_ROUND,
|
|
275
|
+
detail: `retroactive claim targets round ${claim.round} (must be a prior round)`,
|
|
276
|
+
}));
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (!loopId)
|
|
280
|
+
continue; // no vault context → cannot verify (fail-open)
|
|
281
|
+
const observed = resolveRoundFiles(vaultEntries, loopId, claim.round);
|
|
282
|
+
if (observed === null) {
|
|
283
|
+
flags.push(makeVerificationFlag({
|
|
284
|
+
severity: "warn",
|
|
285
|
+
field: "retroactiveClaims",
|
|
286
|
+
check: CHECK_RETROACTIVE_CLAIM_UNVERIFIED,
|
|
287
|
+
detail: `retroactive claim for round ${claim.round} has no observable git evidence`,
|
|
288
|
+
}));
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const paths = extractFilePaths(claim.claim);
|
|
292
|
+
const unverified = paths.filter((path) => !observed.some((file) => file === path || file.endsWith(path) || path.endsWith(file)));
|
|
293
|
+
if (paths.length > 0 && unverified.length > 0) {
|
|
294
|
+
flags.push(makeVerificationFlag({
|
|
295
|
+
severity: "warn",
|
|
296
|
+
field: "retroactiveClaims",
|
|
297
|
+
check: CHECK_RETROACTIVE_CLAIM_UNVERIFIED,
|
|
298
|
+
detail: `retroactive claim references files not observed in round ${claim.round}: ` +
|
|
299
|
+
unverified.slice(0, 3).join(", "),
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return flags.length > 0 ? flags[0] : null;
|
|
304
|
+
}
|
|
305
|
+
/** v2.12: success with zero machine-verifiable evidence. The agent claims
|
|
306
|
+
* completion but nothing the runtime observed backs it — no verified claims
|
|
307
|
+
* (test evidence), no test results, no required commands. A declared
|
|
308
|
+
* no_change_reason is the honest escape hatch: it downgrades to info. */
|
|
309
|
+
function checkSuccessWithoutVerifiedEvidence(selfEval, claimView) {
|
|
310
|
+
if (!effectiveSuccess(selfEval))
|
|
311
|
+
return null;
|
|
312
|
+
if (claimView.verifiedCount > 0 || claimView.hasMachineEvidence)
|
|
313
|
+
return null;
|
|
314
|
+
const noChange = typeof selfEval.no_change_reason === "string" &&
|
|
315
|
+
selfEval.no_change_reason.trim().length > 0;
|
|
316
|
+
return makeVerificationFlag({
|
|
317
|
+
severity: noChange ? "info" : "error",
|
|
318
|
+
field: "success",
|
|
319
|
+
check: CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE,
|
|
320
|
+
detail: noChange
|
|
321
|
+
? "success with zero verified claims (no_change_reason declared)"
|
|
322
|
+
: "success with zero verified claims and no test evidence",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/** v3.2: success declared but no machine-verified observation this round.
|
|
326
|
+
* Unlike R8 (which keys off the agent's SELF-REPORTED test_results), this
|
|
327
|
+
* keys off the runtime-derived providerStatus: git diff observed, or a
|
|
328
|
+
* passed after-command. Warn-level by design — the round commits, but its
|
|
329
|
+
* success never enters the trajectory (and trust drops). A declared
|
|
330
|
+
* no_change_reason is the honest escape hatch, mirroring R8.
|
|
331
|
+
* Crucially this check does NOT self-skip on the heuristic-extraction path:
|
|
332
|
+
* an unstructured success claim without machine observation is flagged
|
|
333
|
+
* exactly the same — the check degrades, it never disappears. */
|
|
334
|
+
function checkSuccessUnverified(selfEval, status) {
|
|
335
|
+
if (!effectiveSuccess(selfEval))
|
|
336
|
+
return null;
|
|
337
|
+
if (typeof selfEval.no_change_reason === "string" &&
|
|
338
|
+
selfEval.no_change_reason.trim().length > 0)
|
|
339
|
+
return null;
|
|
340
|
+
if (status.providerStatus === "verified")
|
|
341
|
+
return null;
|
|
342
|
+
return makeVerificationFlag({
|
|
343
|
+
severity: "warn",
|
|
344
|
+
field: "success",
|
|
345
|
+
check: CHECK_SUCCESS_UNVERIFIED,
|
|
346
|
+
detail: `success declared but no machine-verified observation this round (providerStatus: ${status.providerStatus})`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
92
349
|
function checkDuplicateConstraintDiscovery(selfEval, prevSelfEval, olderViolations) {
|
|
93
350
|
const discovered = selfEval.discovered_constraints;
|
|
94
351
|
if (!discovered || discovered.length === 0)
|
|
@@ -111,7 +368,7 @@ function checkDuplicateConstraintDiscovery(selfEval, prevSelfEval, olderViolatio
|
|
|
111
368
|
return makeVerificationFlag({
|
|
112
369
|
severity: "warn",
|
|
113
370
|
field: "discovered_constraints",
|
|
114
|
-
check:
|
|
371
|
+
check: CHECK_DUPLICATE_CONSTRAINT_DISCOVERY,
|
|
115
372
|
detail: `Constraint "${d}" was already known from a previous round`,
|
|
116
373
|
});
|
|
117
374
|
}
|
|
@@ -151,7 +408,7 @@ function checkRecurringViolation(selfEval, prevSelfEval, vaultEntries, currentRo
|
|
|
151
408
|
return makeVerificationFlag({
|
|
152
409
|
severity: "error",
|
|
153
410
|
field: "constraint_violations",
|
|
154
|
-
check:
|
|
411
|
+
check: CHECK_RECURRING_VIOLATION,
|
|
155
412
|
detail: `Constraint violation "${v}" has appeared in 3 consecutive rounds ` +
|
|
156
413
|
`(rounds ${currentRound - 2}–${currentRound}) without resolution`,
|
|
157
414
|
});
|
|
@@ -174,7 +431,7 @@ function checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound) {
|
|
|
174
431
|
return makeVerificationFlag({
|
|
175
432
|
severity: "warn",
|
|
176
433
|
field: "retracted_constraints",
|
|
177
|
-
check:
|
|
434
|
+
check: CHECK_RETRACT_FRESH_CONSTRAINT,
|
|
178
435
|
detail: `Retracting constraint "${r}" that was just discovered in round ` +
|
|
179
436
|
`${currentRound - 1} — may indicate rapid flip-flopping`,
|
|
180
437
|
});
|
|
@@ -200,7 +457,7 @@ function checkEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
|
200
457
|
return makeVerificationFlag({
|
|
201
458
|
severity: "warn",
|
|
202
459
|
field: "files_changed",
|
|
203
|
-
check:
|
|
460
|
+
check: CHECK_EVIDENCE_INTEGRITY,
|
|
204
461
|
detail: `Agent reported no files changed but evidence shows: ${actual.join(", ")}`,
|
|
205
462
|
});
|
|
206
463
|
}
|
|
@@ -215,7 +472,7 @@ function checkEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
|
215
472
|
return makeVerificationFlag({
|
|
216
473
|
severity: "warn",
|
|
217
474
|
field: "files_changed",
|
|
218
|
-
check:
|
|
475
|
+
check: CHECK_EVIDENCE_INTEGRITY,
|
|
219
476
|
detail: `Agent files_changed doesn't match evidence: ${parts.join("; ")}`,
|
|
220
477
|
});
|
|
221
478
|
}
|
|
@@ -224,7 +481,7 @@ function checkEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
|
224
481
|
/** A required, explicitly configured verification command is authoritative
|
|
225
482
|
* when the Agent claims success. Optional commands remain observational. */
|
|
226
483
|
function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
|
|
227
|
-
if (!selfEval
|
|
484
|
+
if (!effectiveSuccess(selfEval))
|
|
228
485
|
return null;
|
|
229
486
|
for (const snapshot of evidenceSnapshots) {
|
|
230
487
|
if (snapshot.data.kind !== "command")
|
|
@@ -243,7 +500,7 @@ function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
|
|
|
243
500
|
return makeVerificationFlag({
|
|
244
501
|
severity: "error",
|
|
245
502
|
field: "execution_evidence",
|
|
246
|
-
check:
|
|
503
|
+
check: CHECK_REQUIRED_COMMAND_FAILED,
|
|
247
504
|
detail: `Agent claims success but required command "${name}" ${String(status)}${exitCode}`,
|
|
248
505
|
});
|
|
249
506
|
}
|
|
@@ -251,30 +508,123 @@ function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
|
|
|
251
508
|
}
|
|
252
509
|
// ── Test output parsing ────────────────────────────────────────────────────
|
|
253
510
|
// jaccardSimilarity() imported from token-utils.ts (v2.1 — intent-action drift detection)
|
|
511
|
+
/** Extract stable sub-goal IDs (sg-XXXXXXXX) mentioned in text.
|
|
512
|
+
* Matches the ID format rendered in prompts (loop-compiler deriveSubGoalId). */
|
|
513
|
+
function extractSubGoalIds(text) {
|
|
514
|
+
return [...new Set(text.toLowerCase().match(/sg-[a-f0-9]{8}/g) ?? [])];
|
|
515
|
+
}
|
|
516
|
+
/** Extract file-path-like tokens (e.g. "src/auth/login.ts") from text. */
|
|
517
|
+
function extractFilePaths(text) {
|
|
518
|
+
return [...new Set(text.match(/[\w./-]+\.[a-z]{2,6}\b/gi) ?? [])];
|
|
519
|
+
}
|
|
520
|
+
/** Whether a changed path looks like a test file. */
|
|
521
|
+
function isTestFile(path) {
|
|
522
|
+
return /\.(test|spec)\.[a-z0-9]+$/i.test(path) ||
|
|
523
|
+
/(^|[\\/])tests?[\\/]/i.test(path) ||
|
|
524
|
+
/_test\.[a-z0-9]+$/i.test(path);
|
|
525
|
+
}
|
|
526
|
+
/** Map each referenced sub-goal ID to its known description(s), reconstructed
|
|
527
|
+
* from emerged_subtasks in the vault lineage and the previous self-eval. */
|
|
528
|
+
function referencedSubGoalDescriptions(ids, vaultEntries, prevSelfEval) {
|
|
529
|
+
const map = new Map();
|
|
530
|
+
for (const id of ids)
|
|
531
|
+
map.set(id, []);
|
|
532
|
+
const descriptions = [];
|
|
533
|
+
for (const entry of vaultEntries) {
|
|
534
|
+
const emerged = Array.isArray(entry.emerged_subtasks)
|
|
535
|
+
? entry.emerged_subtasks.filter((v) => typeof v === "string")
|
|
536
|
+
: [];
|
|
537
|
+
descriptions.push(...emerged);
|
|
538
|
+
}
|
|
539
|
+
descriptions.push(...(prevSelfEval?.emerged_subtasks ?? []));
|
|
540
|
+
for (const desc of descriptions) {
|
|
541
|
+
const trimmed = desc.trim();
|
|
542
|
+
if (!trimmed)
|
|
543
|
+
continue;
|
|
544
|
+
const id = deriveSubGoalId(trimmed);
|
|
545
|
+
if (map.has(id))
|
|
546
|
+
map.get(id).push(trimmed);
|
|
547
|
+
}
|
|
548
|
+
return map;
|
|
549
|
+
}
|
|
254
550
|
/** v2.1 (check 11): Compare the previous round's declared next_action with
|
|
255
551
|
* the current round's actual output_summary. Detect when the agent says it
|
|
256
552
|
* will do X but then does Y — silent drift without explanation.
|
|
257
553
|
*
|
|
258
554
|
* Only fires when both next_action and output_summary are non-empty.
|
|
259
|
-
*
|
|
260
|
-
|
|
555
|
+
*
|
|
556
|
+
* v2.14: Structured-ID-first detection. Alignment is decided by a waterfall
|
|
557
|
+
* of concrete signals before falling back to string similarity:
|
|
558
|
+
* 1. Sub-goal IDs (sg-XXXXXXXX) in next_action matched against this
|
|
559
|
+
* round's completed_subtasks (exact ID, ID derived from the
|
|
560
|
+
* description text, or Jaccard against the referenced sub-goal's
|
|
561
|
+
* known description).
|
|
562
|
+
* 2. File paths named in next_action appearing in files_changed.
|
|
563
|
+
* 3. Test evidence — next_action mentions tests and test files were
|
|
564
|
+
* changed with tests actually run.
|
|
565
|
+
* 4. Jaccard token similarity against the policy threshold (weak signal).
|
|
566
|
+
* Any signal aligning is sufficient — text similarity is never the sole
|
|
567
|
+
* authority when structured evidence exists. */
|
|
568
|
+
function checkIntentDrift(selfEval, prevSelfEval, vaultEntries) {
|
|
261
569
|
if (!prevSelfEval?.next_action?.trim())
|
|
262
570
|
return null;
|
|
263
571
|
if (!selfEval.output_summary.trim())
|
|
264
572
|
return null;
|
|
265
573
|
const intent = prevSelfEval.next_action.trim();
|
|
266
574
|
const actual = selfEval.output_summary.trim();
|
|
575
|
+
const policy = getPolicy();
|
|
576
|
+
const filesChanged = selfEval.execution_evidence?.files_changed ?? [];
|
|
577
|
+
// ── Signal 1: sub-goal IDs referenced by next_action → completed this round
|
|
578
|
+
const intentIds = extractSubGoalIds(intent);
|
|
579
|
+
if (intentIds.length > 0) {
|
|
580
|
+
const completed = selfEval.completed_subtasks ?? [];
|
|
581
|
+
const referenced = referencedSubGoalDescriptions(intentIds, vaultEntries, prevSelfEval);
|
|
582
|
+
const idMatched = intentIds.some((id) => completed.some((entry) => {
|
|
583
|
+
const text = entry.trim();
|
|
584
|
+
if (!text)
|
|
585
|
+
return false;
|
|
586
|
+
if (text.toLowerCase() === id)
|
|
587
|
+
return true;
|
|
588
|
+
if (deriveSubGoalId(text) === id)
|
|
589
|
+
return true;
|
|
590
|
+
const descriptions = referenced.get(id) ?? [];
|
|
591
|
+
return descriptions.some((desc) => jaccardSimilarity(text, desc) >= policy.evolution.subgoal_match_threshold);
|
|
592
|
+
}));
|
|
593
|
+
if (idMatched)
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
// ── Signal 2: file paths named in next_action appear in files_changed
|
|
597
|
+
const intentPaths = extractFilePaths(intent);
|
|
598
|
+
if (intentPaths.length > 0 && filesChanged.length > 0) {
|
|
599
|
+
const pathMatched = intentPaths.some((p) => filesChanged.some((f) => f === p || f.endsWith(p) || p.endsWith(f)));
|
|
600
|
+
if (pathMatched)
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
// ── Signal 3: test evidence — intent mentions tests, test files changed,
|
|
604
|
+
// and tests actually ran this round
|
|
605
|
+
const intentTokens = tokenize(intent);
|
|
606
|
+
const mentionsTests = intentTokens.has("test") || intentTokens.has("tests");
|
|
607
|
+
const testResults = selfEval.execution_evidence?.test_results;
|
|
608
|
+
const testsRan = !!testResults && testResults.passed + testResults.failed > 0;
|
|
609
|
+
const testFilesChanged = filesChanged.some((f) => isTestFile(f));
|
|
610
|
+
if (mentionsTests && testsRan && testFilesChanged)
|
|
611
|
+
return null;
|
|
612
|
+
// ── Signal 4 (weak): Jaccard token similarity
|
|
267
613
|
const score = jaccardSimilarity(intent, actual);
|
|
268
|
-
const threshold =
|
|
614
|
+
const threshold = policy.evolution.intent_drift_threshold;
|
|
269
615
|
if (score >= threshold)
|
|
270
616
|
return null;
|
|
617
|
+
const idNote = intentIds.length > 0
|
|
618
|
+
? `; ${intentIds.length} referenced sub-goal ID(s) did not match completed_subtasks`
|
|
619
|
+
: "";
|
|
271
620
|
return makeVerificationFlag({
|
|
272
621
|
severity: "warn",
|
|
273
622
|
field: "output_summary",
|
|
274
|
-
check:
|
|
623
|
+
check: CHECK_INTENT_DRIFT,
|
|
275
624
|
detail: `Declared intent was "${intent.slice(0, 120)}" ` +
|
|
276
625
|
`but actual output "${actual.slice(0, 120)}" — ` +
|
|
277
|
-
`similarity ${(score * 100).toFixed(0)}% below threshold ${(threshold * 100).toFixed(0)}
|
|
626
|
+
`similarity ${(score * 100).toFixed(0)}% below threshold ${(threshold * 100).toFixed(0)}%` +
|
|
627
|
+
idNote,
|
|
278
628
|
});
|
|
279
629
|
}
|
|
280
630
|
/** Best-effort parse of test counts from common test-runner output formats.
|
|
@@ -384,7 +734,7 @@ function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
|
384
734
|
return makeVerificationFlag({
|
|
385
735
|
severity,
|
|
386
736
|
field: "test_results",
|
|
387
|
-
check:
|
|
737
|
+
check: CHECK_COMMAND_EVIDENCE_MISMATCH,
|
|
388
738
|
detail: `Agent test_results don't match "${name}" output: ${mismatches.join("; ")}`,
|
|
389
739
|
});
|
|
390
740
|
}
|
|
@@ -393,7 +743,11 @@ function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
|
393
743
|
/** v2.2: Check if the agent's declared next_action aligns with any pending
|
|
394
744
|
* or in_progress sub-goal. When 3+ pending sub-goals exist but the agent
|
|
395
745
|
* plans work unrelated to any of them, it may indicate task drift.
|
|
396
|
-
* This is a warn-level advisory — the agent always owns prioritization.
|
|
746
|
+
* This is a warn-level advisory — the agent always owns prioritization.
|
|
747
|
+
*
|
|
748
|
+
* v2.14: Exact sub-goal ID alignment first — next_action naming a pending
|
|
749
|
+
* sub-goal by its stable ID (sg-XXXXXXXX) is aligned by definition.
|
|
750
|
+
* Jaccard description similarity remains as the fallback. */
|
|
397
751
|
function checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound) {
|
|
398
752
|
const nextAction = selfEval.next_action?.trim();
|
|
399
753
|
if (!nextAction)
|
|
@@ -438,6 +792,14 @@ function checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound) {
|
|
|
438
792
|
// Need at least 3 pending sub-goals for the check to be meaningful
|
|
439
793
|
if (pendingSubGoals.size < 3)
|
|
440
794
|
return null;
|
|
795
|
+
// v2.14: Stable ID match first — next_action naming a pending sub-goal
|
|
796
|
+
// by ID is aligned by definition, regardless of description wording.
|
|
797
|
+
const nextActionIds = extractSubGoalIds(nextAction);
|
|
798
|
+
if (nextActionIds.length > 0) {
|
|
799
|
+
const pendingIds = new Set([...pendingSubGoals].map((d) => deriveSubGoalId(d)));
|
|
800
|
+
if (nextActionIds.some((id) => pendingIds.has(id)))
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
441
803
|
// Check if next_action aligns with any pending sub-goal
|
|
442
804
|
let aligns = false;
|
|
443
805
|
for (const sg of pendingSubGoals) {
|
|
@@ -452,7 +814,7 @@ function checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound) {
|
|
|
452
814
|
return makeVerificationFlag({
|
|
453
815
|
severity: "warn",
|
|
454
816
|
field: "next_action",
|
|
455
|
-
check:
|
|
817
|
+
check: CHECK_SUBGOAL_DRIFT,
|
|
456
818
|
detail: `Agent's next_action doesn't align with any of ${pendingSubGoals.size} pending sub-goals. ` +
|
|
457
819
|
`Pending: ${sample}… Consider completing existing sub-goals before starting new work, ` +
|
|
458
820
|
`or cancel outdated sub-goals via canceled_subtasks.`,
|
|
@@ -479,7 +841,7 @@ function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles) {
|
|
|
479
841
|
return makeVerificationFlag({
|
|
480
842
|
severity: "error",
|
|
481
843
|
field: "files_changed",
|
|
482
|
-
check:
|
|
844
|
+
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
483
845
|
detail: `Agent modified ${overlap.length} file(s) that were part of ` +
|
|
484
846
|
`the skipped (failed) rounds: ${overlap.slice(0, 5).join(", ")}. ` +
|
|
485
847
|
`The workspace was likely not restored before working.`,
|
|
@@ -489,11 +851,35 @@ function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles) {
|
|
|
489
851
|
return makeVerificationFlag({
|
|
490
852
|
severity: "warn",
|
|
491
853
|
field: "files_changed",
|
|
492
|
-
check:
|
|
854
|
+
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
493
855
|
detail: `Agent modified files that overlap with skipped rounds: ` +
|
|
494
856
|
`${overlap.join(", ")}. Verify the workspace was restored.`,
|
|
495
857
|
});
|
|
496
858
|
}
|
|
859
|
+
/** v2.12: After a backtrack, the working tree must return to the restore
|
|
860
|
+
* point's git HEAD. Compares the first 12 characters (short hash), matching
|
|
861
|
+
* how git itself disambiguates. */
|
|
862
|
+
function checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead) {
|
|
863
|
+
if (!backtrackTargetGitHead)
|
|
864
|
+
return null;
|
|
865
|
+
const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git" && isRecord(snapshot.data) &&
|
|
866
|
+
typeof snapshot.data.head === "string");
|
|
867
|
+
if (!git)
|
|
868
|
+
return null; // git unavailable → cannot verify → skip (fail-open)
|
|
869
|
+
const currentHead = git.data.head;
|
|
870
|
+
if (currentHead.length === 0)
|
|
871
|
+
return null;
|
|
872
|
+
if (currentHead.slice(0, 12) === backtrackTargetGitHead.slice(0, 12))
|
|
873
|
+
return null;
|
|
874
|
+
return makeVerificationFlag({
|
|
875
|
+
severity: "error",
|
|
876
|
+
field: "workspace",
|
|
877
|
+
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
878
|
+
detail: `Workspace HEAD (${currentHead.slice(0, 12)}) does not match the ` +
|
|
879
|
+
`backtrack restore commit (${backtrackTargetGitHead.slice(0, 12)}). ` +
|
|
880
|
+
`Restore the working tree before continuing.`,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
497
883
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
498
884
|
// Main entry point
|
|
499
885
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -511,7 +897,11 @@ export function verifySelfEvaluation(selfEval, currentRound, vaultEntries, prevS
|
|
|
511
897
|
/** v2.13: Files from skipped backtrack rounds. If the agent's
|
|
512
898
|
* files_changed overlaps significantly with these, the workspace
|
|
513
899
|
* was not properly restored before working. */
|
|
514
|
-
backtrackSkippedFiles = []
|
|
900
|
+
backtrackSkippedFiles = [],
|
|
901
|
+
/** v2.12: Git HEAD commit of the backtrack restore point. When set, the
|
|
902
|
+
* current git snapshot must sit at this commit — otherwise the workspace
|
|
903
|
+
* was not restored and the round cannot be accepted. */
|
|
904
|
+
backtrackTargetGitHead) {
|
|
515
905
|
const flags = [];
|
|
516
906
|
// Collect violations from all previous vault entries for duplicate-discovery
|
|
517
907
|
// and other checks that need deeper history.
|
|
@@ -520,11 +910,27 @@ backtrackSkippedFiles = []) {
|
|
|
520
910
|
for (const v of entryViolations(entry))
|
|
521
911
|
olderViolations.push(v);
|
|
522
912
|
}
|
|
913
|
+
// v2.12: Derived claim provenance — which reported criteria completions
|
|
914
|
+
// are backed by machine evidence (pure derivation, not persisted).
|
|
915
|
+
const claimView = deriveClaimView(selfEval, evidenceSnapshots);
|
|
916
|
+
// v3.2: Runtime machine-verification status (git observed / command passed /
|
|
917
|
+
// absent). Feeds the success_unverified check that never self-skips.
|
|
918
|
+
const evidenceStatus = deriveEvidenceStatus(selfEval, evidenceSnapshots);
|
|
523
919
|
// Run all checks
|
|
524
920
|
const checks = [
|
|
525
921
|
() => checkProgressRegression(selfEval, prevSelfEval),
|
|
526
922
|
() => checkEmptyChangeWithPassing(selfEval),
|
|
527
923
|
() => checkSuccessWithRemainingCriteria(selfEval),
|
|
924
|
+
// v2.12: success with zero verified claims and no test evidence
|
|
925
|
+
() => checkSuccessWithoutVerifiedEvidence(selfEval, claimView),
|
|
926
|
+
// v3.2: success without a machine-verified observation (never self-skips)
|
|
927
|
+
() => checkSuccessUnverified(selfEval, evidenceStatus),
|
|
928
|
+
// v2.12: criteria met but none machine-verified (mild criterion-specific rule)
|
|
929
|
+
() => checkUnverifiedCriteriaClaims(selfEval, claimView),
|
|
930
|
+
// v2.12: declared outcome vs legacy boolean consistency
|
|
931
|
+
() => checkOutcomeConsistency(selfEval),
|
|
932
|
+
// v2.12: retroactive claims against prior rounds
|
|
933
|
+
() => checkRetroactiveClaims(selfEval, vaultEntries, currentRound),
|
|
528
934
|
() => checkDuplicateConstraintDiscovery(selfEval, prevSelfEval, olderViolations),
|
|
529
935
|
() => checkRecurringViolation(selfEval, prevSelfEval, vaultEntries, currentRound),
|
|
530
936
|
() => checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound),
|
|
@@ -534,11 +940,13 @@ backtrackSkippedFiles = []) {
|
|
|
534
940
|
// v2.0: Cross-validate agent-reported test_results against command output
|
|
535
941
|
() => checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots),
|
|
536
942
|
// v2.1: Detect intent-action drift — agent said X but did Y
|
|
537
|
-
() => checkIntentDrift(selfEval, prevSelfEval),
|
|
943
|
+
() => checkIntentDrift(selfEval, prevSelfEval, vaultEntries),
|
|
538
944
|
// v2.2: Detect sub-goal drift — next_action doesn't align with pending sub-goals
|
|
539
945
|
() => checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound),
|
|
540
946
|
// v2.13: Post-backtrack workspace restore check
|
|
541
947
|
() => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles),
|
|
948
|
+
// v2.12: Post-backtrack git HEAD restore check
|
|
949
|
+
() => checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead),
|
|
542
950
|
];
|
|
543
951
|
for (const run of checks) {
|
|
544
952
|
const flag = run();
|