loopforge 3.6.0 → 3.7.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 +28 -13
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +3 -8
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +23 -4
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +148 -47
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -16
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts.map +1 -1
- package/dist/cognitive-facts.js +2 -3
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/cognitive-governance.d.ts +30 -7
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +75 -22
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +48 -4
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +177 -10
- package/dist/committed-round.js.map +1 -1
- package/dist/enforcement-gate.d.ts +24 -19
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +216 -146
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +10 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +52 -31
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +3 -9
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +13 -19
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +65 -100
- package/dist/evidence-provider.js.map +1 -1
- package/dist/generate-schema.js +104 -43
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +18 -10
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +205 -343
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +11 -16
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +21 -63
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +18 -3
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +162 -35
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +12 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +35 -4
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +169 -52
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +170 -39
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy.d.ts +10 -13
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +3 -6
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +6 -4
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +46 -100
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +52 -51
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +5 -28
- package/dist/protocol.js.map +1 -1
- package/dist/round-coordinator.d.ts +19 -0
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +63 -4
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +4 -3
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +3 -10
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +4 -0
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +9 -0
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +41 -3
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +125 -13
- package/dist/self-eval.js.map +1 -1
- package/dist/token-utils.d.ts +7 -3
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +18 -3
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +32 -24
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +262 -178
- 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/loop_policy.json +19 -7
- package/package.json +6 -6
- package/skills/perception/SKILL.md +56 -16
|
@@ -11,26 +11,41 @@
|
|
|
11
11
|
* this round is excluded from the success trend (NOT
|
|
12
12
|
* modified). Flags become hard constraints — the agent
|
|
13
13
|
* must respond in the next round.
|
|
14
|
+
*
|
|
15
|
+
* v3.7: every check belongs to one of four verification domains (CHECK_DOMAIN
|
|
16
|
+
* below): evaluation consistency / evidence integrity / plan & contract /
|
|
17
|
+
* progress & recovery. A domain describes a check's semantic job — it never
|
|
18
|
+
* changes run order or verdict aggregation. Round Contract checks are further
|
|
19
|
+
* framed as declaration / execution / closure stages in their doc comments.
|
|
14
20
|
*/
|
|
15
21
|
import { getPolicy, isConfiguredCommand } from "./policy.js";
|
|
16
22
|
import { makeVerificationFlag, makeVerificationResult } from "./protocol.js";
|
|
17
|
-
import { jaccardSimilarity, tokenize, entryRound
|
|
23
|
+
import { jaccardSimilarity, tokenize, isRecord, entryRound, extractFilePathTokens } from "./token-utils.js";
|
|
18
24
|
import { deriveSubGoalId } from "./loop-compiler.js";
|
|
19
|
-
import { committedRoundsFromEntries, machineGitMotionSeries } from "./committed-round.js";
|
|
25
|
+
import { committedRoundsFromEntries, entrySubGoalUpdates, machineGitMotionSeries, entryViolations, } from "./committed-round.js";
|
|
20
26
|
import { contractRoundEvaluations, contractDoneWhenSatisfied, contractItemMatches, deriveActiveRoundContract, } from "./round-contract.js";
|
|
21
27
|
import { stableStringify } from "./canonical-state.js";
|
|
22
28
|
import { deriveClaimView, resolveRoundFiles } from "./evidence-claims.js";
|
|
23
29
|
import { effectiveSuccess, parseRoundContract } from "./self-eval.js";
|
|
30
|
+
import { deriveGate, preflightStructuredGate } from "./cognitive-governance.js";
|
|
24
31
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
32
|
// v2.12: Check-name constants — single source of truth so the enforcement
|
|
26
33
|
// gate and audit can reference checks without string-literal drift.
|
|
34
|
+
//
|
|
35
|
+
// v3.7: every check belongs to one of four verification domains (CHECK_DOMAIN
|
|
36
|
+
// below). The flag set was converged from 27 to 24:
|
|
37
|
+
// - progress_regression removed — self-report-only decoration; machine-side
|
|
38
|
+
// progress policing lives in the enforcement gate's progress stall row.
|
|
39
|
+
// - empty_change_with_passing removed — success without machine backing is
|
|
40
|
+
// already covered by the success-evidence arms below.
|
|
41
|
+
// - success_claim_conflict merged into outcome_success_contradiction.
|
|
27
42
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
28
|
-
export const CHECK_PROGRESS_REGRESSION = "progress_regression";
|
|
29
|
-
export const CHECK_EMPTY_CHANGE_WITH_PASSING = "empty_change_with_passing";
|
|
30
43
|
export const CHECK_SUCCESS_WITH_REMAINING_CRITERIA = "success_with_remaining_criteria";
|
|
31
44
|
export const CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE = "success_without_verified_evidence";
|
|
45
|
+
// v3.7: single id for the outcome-vs-success axis — error when a declared
|
|
46
|
+
// success contradicts success=false, warn when success=true but the outcome
|
|
47
|
+
// declares a non-success (the former success_claim_conflict direction).
|
|
32
48
|
export const CHECK_OUTCOME_SUCCESS_CONTRADICTION = "outcome_success_contradiction";
|
|
33
|
-
export const CHECK_SUCCESS_CLAIM_CONFLICT = "success_claim_conflict";
|
|
34
49
|
export const CHECK_BLOCKED_WITHOUT_BLOCKER = "blocked_without_blocker";
|
|
35
50
|
export const CHECK_RETROACTIVE_CLAIM_BAD_ROUND = "retroactive_claim_bad_round";
|
|
36
51
|
export const CHECK_RETROACTIVE_CLAIM_UNVERIFIED = "retroactive_claim_unverified";
|
|
@@ -62,6 +77,13 @@ export const CHECK_CRITERIA_CLAIMS_UNVERIFIED = "criteria_claims_unverified";
|
|
|
62
77
|
// silent when their target is absent: contract-less rounds behave exactly
|
|
63
78
|
// as before these checks existed, and round 1 (no committed rounds → no
|
|
64
79
|
// active contract) never produces declaration off-by-one noise.
|
|
80
|
+
// v3.7: the checks are framed as three contract stages — Declaration
|
|
81
|
+
// (proposal quality: round_underspecified / round_unverifiable /
|
|
82
|
+
// contract_premature), Execution (conformance under the ACTIVE contract:
|
|
83
|
+
// round_scope_drift; premature_boundary's silently-dropped arm), Closure
|
|
84
|
+
// (completion truth: contract_completion_unverified; premature_boundary's
|
|
85
|
+
// claimed-met-unverified arm). The framing is documentation only — no phase
|
|
86
|
+
// field exists on the contract.
|
|
65
87
|
/** Contract proposed with an empty done_when — nothing is promised, so
|
|
66
88
|
* nothing can be verified at the boundary. Warn: the contract can be fixed
|
|
67
89
|
* by re-declaring next round. */
|
|
@@ -91,25 +113,45 @@ export const CHECK_CONTRACT_COMPLETION_UNVERIFIED = "contract_completion_unverif
|
|
|
91
113
|
* the active one closes. Warn: the walker still ignores it; this only
|
|
92
114
|
* surfaces the otherwise-silent state. */
|
|
93
115
|
export const CHECK_CONTRACT_PREMATURE = "contract_premature";
|
|
116
|
+
/** v3.7.1: A cited gate (evaluation.gate_ids) is not approved. Opt-in:
|
|
117
|
+
* checked ONLY when policy.gate.enabled — citations are meaningless when
|
|
118
|
+
* the gate layer is off. Error → enforcement rejects the round. */
|
|
119
|
+
export const CHECK_USER_GATE_UNRESOLVED = "user_gate_unresolved";
|
|
120
|
+
export const CHECK_DOMAIN = {
|
|
121
|
+
// ── evaluation_consistency ────────────────────────────────────────────────
|
|
122
|
+
[CHECK_SUCCESS_WITH_REMAINING_CRITERIA]: "evaluation_consistency",
|
|
123
|
+
[CHECK_OUTCOME_SUCCESS_CONTRADICTION]: "evaluation_consistency",
|
|
124
|
+
[CHECK_BLOCKED_WITHOUT_BLOCKER]: "evaluation_consistency",
|
|
125
|
+
[CHECK_RETROACTIVE_CLAIM_BAD_ROUND]: "evaluation_consistency",
|
|
126
|
+
[CHECK_RETROACTIVE_CLAIM_UNVERIFIED]: "evaluation_consistency",
|
|
127
|
+
[CHECK_DUPLICATE_CONSTRAINT_DISCOVERY]: "evaluation_consistency",
|
|
128
|
+
[CHECK_RECURRING_VIOLATION]: "evaluation_consistency",
|
|
129
|
+
[CHECK_RETRACT_FRESH_CONSTRAINT]: "evaluation_consistency",
|
|
130
|
+
// ── evidence_integrity ────────────────────────────────────────────────────
|
|
131
|
+
[CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE]: "evidence_integrity",
|
|
132
|
+
[CHECK_CRITERIA_CLAIMS_UNVERIFIED]: "evidence_integrity",
|
|
133
|
+
[CHECK_EVIDENCE_INTEGRITY]: "evidence_integrity",
|
|
134
|
+
[CHECK_REQUIRED_COMMAND_FAILED]: "evidence_integrity",
|
|
135
|
+
[CHECK_COMMAND_EVIDENCE_MISMATCH]: "evidence_integrity",
|
|
136
|
+
[CHECK_VERIFICATION_ENTRYPOINT_MODIFIED]: "evidence_integrity",
|
|
137
|
+
[CHECK_TEST_FILES_MODIFIED]: "evidence_integrity",
|
|
138
|
+
// ── plan_contract ─────────────────────────────────────────────────────────
|
|
139
|
+
[CHECK_INTENT_DRIFT]: "plan_contract",
|
|
140
|
+
[CHECK_SUBGOAL_DRIFT]: "plan_contract",
|
|
141
|
+
[CHECK_ROUND_UNDERSPECIFIED]: "plan_contract",
|
|
142
|
+
[CHECK_ROUND_UNVERIFIABLE]: "plan_contract",
|
|
143
|
+
[CHECK_ROUND_SCOPE_DRIFT]: "plan_contract",
|
|
144
|
+
[CHECK_PREMATURE_BOUNDARY]: "plan_contract",
|
|
145
|
+
[CHECK_CONTRACT_COMPLETION_UNVERIFIED]: "plan_contract",
|
|
146
|
+
[CHECK_CONTRACT_PREMATURE]: "plan_contract",
|
|
147
|
+
// v3.7.1: cited-gate authorization conformance (opt-in blocking layer).
|
|
148
|
+
[CHECK_USER_GATE_UNRESOLVED]: "plan_contract",
|
|
149
|
+
// ── progress_recovery ─────────────────────────────────────────────────────
|
|
150
|
+
[CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED]: "progress_recovery",
|
|
151
|
+
};
|
|
94
152
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
95
153
|
// Helpers
|
|
96
154
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
97
|
-
/** Extract the round number from a vault entry's loop_lineage.
|
|
98
|
-
* Returns 0 if the entry has no lineage or no round field.
|
|
99
|
-
* In practice, persistLoopLineage always writes round ≥ 1, so 0
|
|
100
|
-
* unambiguously means "not a valid round entry" in this context.
|
|
101
|
-
* Exported for reuse by enforcement-gate.ts. */
|
|
102
|
-
export function entryRound(entry) {
|
|
103
|
-
return sharedEntryRound(entry);
|
|
104
|
-
}
|
|
105
|
-
/** Read constraint_violations from a vault entry (entry-level, stored from
|
|
106
|
-
* the previous round's last_round_result at persist time). */
|
|
107
|
-
function entryViolations(entry) {
|
|
108
|
-
const viols = entry.constraint_violations;
|
|
109
|
-
if (Array.isArray(viols))
|
|
110
|
-
return viols.filter((v) => typeof v === "string");
|
|
111
|
-
return [];
|
|
112
|
-
}
|
|
113
155
|
// ── v3.3: Round Contract scope matching ─────────────────────────────────────
|
|
114
156
|
/** Normalize a contract scope entry: backslashes → forward slashes, strip
|
|
115
157
|
* leading "./", trim, strip trailing slashes. "." / "./" / "" normalize to
|
|
@@ -248,44 +290,6 @@ export function machineProgressSeries(vaultEntries, currentRound, lookback) {
|
|
|
248
290
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
249
291
|
// Individual checks — each returns a VerificationFlag or null
|
|
250
292
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
251
|
-
function checkProgressRegression(selfEval, prevSelfEval) {
|
|
252
|
-
if (!prevSelfEval?.execution_evidence)
|
|
253
|
-
return null;
|
|
254
|
-
if (!selfEval.execution_evidence)
|
|
255
|
-
return null;
|
|
256
|
-
const prevProgress = prevSelfEval.execution_evidence.progress_estimate;
|
|
257
|
-
const currProgress = selfEval.execution_evidence.progress_estimate;
|
|
258
|
-
if (typeof prevProgress !== "number" || typeof currProgress !== "number")
|
|
259
|
-
return null;
|
|
260
|
-
// Use delta with epsilon to avoid IEEE 754 rounding issues (0.8 - 0.2 > 0.6 in float)
|
|
261
|
-
if (prevProgress - currProgress <= 0.2 + 1e-10)
|
|
262
|
-
return null;
|
|
263
|
-
return makeVerificationFlag({
|
|
264
|
-
severity: "warn",
|
|
265
|
-
field: "progress_estimate",
|
|
266
|
-
check: CHECK_PROGRESS_REGRESSION,
|
|
267
|
-
detail: `Progress dropped from ${prevProgress.toFixed(2)} to ` +
|
|
268
|
-
`${currProgress.toFixed(2)} (delta: ${(currProgress - prevProgress).toFixed(2)})`,
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
function checkEmptyChangeWithPassing(selfEval) {
|
|
272
|
-
const ev = selfEval.execution_evidence;
|
|
273
|
-
if (!ev)
|
|
274
|
-
return null;
|
|
275
|
-
const filesEmpty = ev.files_changed.length === 0;
|
|
276
|
-
const testsAllPass = ev.test_results !== null &&
|
|
277
|
-
ev.test_results.failed === 0 &&
|
|
278
|
-
ev.test_results.passed > 0;
|
|
279
|
-
if (!filesEmpty || !testsAllPass || !effectiveSuccess(selfEval))
|
|
280
|
-
return null;
|
|
281
|
-
return makeVerificationFlag({
|
|
282
|
-
severity: "warn",
|
|
283
|
-
field: "execution_evidence",
|
|
284
|
-
check: CHECK_EMPTY_CHANGE_WITH_PASSING,
|
|
285
|
-
detail: "Agent claims success with no files changed and all tests passing — " +
|
|
286
|
-
"verify that work was actually performed",
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
293
|
function checkSuccessWithRemainingCriteria(selfEval) {
|
|
290
294
|
if (!effectiveSuccess(selfEval))
|
|
291
295
|
return null;
|
|
@@ -319,10 +323,12 @@ function checkUnverifiedCriteriaClaims(selfEval, claimView) {
|
|
|
319
323
|
"(no passing test evidence or command snapshot)",
|
|
320
324
|
});
|
|
321
325
|
}
|
|
322
|
-
/** Declared outcome vs the core success flag
|
|
323
|
-
*
|
|
324
|
-
* success=
|
|
325
|
-
*
|
|
326
|
+
/** Declared outcome vs the core success flag — one check id for the whole
|
|
327
|
+
* axis (v3.7: the former success_claim_conflict warn direction merged here).
|
|
328
|
+
* A declared success with success=false is a self-contradiction (error);
|
|
329
|
+
* success=true with a non-success outcome is an inconsistent claim (warn).
|
|
330
|
+
* Also: outcome==="blocked" without a blocker description gets a warn so the
|
|
331
|
+
* next prompt asks for it. */
|
|
326
332
|
function checkOutcomeConsistency(selfEval) {
|
|
327
333
|
const outcome = selfEval.outcome;
|
|
328
334
|
if (outcome === "success" && selfEval.success === false) {
|
|
@@ -337,7 +343,7 @@ function checkOutcomeConsistency(selfEval) {
|
|
|
337
343
|
return makeVerificationFlag({
|
|
338
344
|
severity: "warn",
|
|
339
345
|
field: "success",
|
|
340
|
-
check:
|
|
346
|
+
check: CHECK_OUTCOME_SUCCESS_CONTRADICTION,
|
|
341
347
|
detail: `outcome=${outcome} but success=true — the declared outcome wins`,
|
|
342
348
|
});
|
|
343
349
|
}
|
|
@@ -385,7 +391,7 @@ function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
|
|
|
385
391
|
}));
|
|
386
392
|
continue;
|
|
387
393
|
}
|
|
388
|
-
const paths =
|
|
394
|
+
const paths = extractFilePathTokens(claim.claim);
|
|
389
395
|
const unverified = paths.filter((path) => !observed.some((file) => file === path || file.endsWith(path) || path.endsWith(file)));
|
|
390
396
|
if (paths.length > 0 && unverified.length > 0) {
|
|
391
397
|
flags.push(makeVerificationFlag({
|
|
@@ -401,25 +407,59 @@ function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
|
|
|
401
407
|
// hid all but the first problem until the next round.
|
|
402
408
|
return flags.length > 0 ? flags : null;
|
|
403
409
|
}
|
|
404
|
-
/** v2.12/v3.6: success without
|
|
405
|
-
*
|
|
406
|
-
* v3.
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
410
|
+
/** v2.12/v3.6/v3.7: success without machine backing — the single
|
|
411
|
+
* "success evidence" semantic. Two arms, evaluated in order:
|
|
412
|
+
* 1. Empty/missing-evidence arm (v3.7: absorbed the enforcement-only
|
|
413
|
+
* empty_success rule): success=true with NO execution_evidence at all, or
|
|
414
|
+
* with files_changed empty AND test_results null, is an error
|
|
415
|
+
* unconditionally — BEFORE the providerStatus early-exit, so a passed
|
|
416
|
+
* command never rescues it (execution_evidence stays mandatory), and
|
|
417
|
+
* neither evidence.machine_backed_success "warn" nor a declared
|
|
418
|
+
* no_change_reason downgrades it ("no change" with literally no evidence
|
|
419
|
+
* recorded is still an unbacked claim).
|
|
420
|
+
* 2. Claims arm (v3.6): success=true but the runtime observed no machine
|
|
421
|
+
* evidence for it. The trigger is the runtime-derived providerStatus
|
|
422
|
+
* (verified / unavailable / absent) — "verified" requires an UNTAMPERED
|
|
423
|
+
* passed after-command, so an entrypoint-tampered command no longer
|
|
424
|
+
* counts as machine evidence. Severity follows evidence.machine_backed_
|
|
425
|
+
* success — "required" rejects (error), "warn" tolerates with a warn
|
|
426
|
+
* (round commits, success excluded from the trajectory, trust drops). A
|
|
427
|
+
* declared no_change_reason is the honest escape hatch: it downgrades to
|
|
428
|
+
* info — honored only while NO enabled verification command is
|
|
429
|
+
* configured (machine verification was structurally impossible); a
|
|
430
|
+
* configured command closes the escape. no_change_reason is honored HERE
|
|
431
|
+
* ONLY — contract checks refuse it. */
|
|
417
432
|
function checkSuccessWithoutVerifiedEvidence(selfEval, status) {
|
|
418
433
|
if (!effectiveSuccess(selfEval))
|
|
419
434
|
return null;
|
|
435
|
+
// v3.7: empty/missing-evidence arm (ex-R3 empty_success posture).
|
|
436
|
+
const ev = selfEval.execution_evidence;
|
|
437
|
+
if (!ev || (ev.files_changed.length === 0 && ev.test_results === null)) {
|
|
438
|
+
return makeVerificationFlag({
|
|
439
|
+
severity: "error",
|
|
440
|
+
field: "success",
|
|
441
|
+
check: CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE,
|
|
442
|
+
detail: ev
|
|
443
|
+
? "Agent claims success but execution_evidence shows no files changed " +
|
|
444
|
+
"and no tests were run. There is no verifiable evidence of work."
|
|
445
|
+
: "Agent claims success but provided no execution_evidence. " +
|
|
446
|
+
"Every successful round MUST include execution_evidence with " +
|
|
447
|
+
"files_changed, test_results, and progress_estimate.",
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
// Claims arm (v3.6).
|
|
420
451
|
if (status.providerStatus === "verified")
|
|
421
452
|
return null;
|
|
422
|
-
|
|
453
|
+
// M2 (v3.7.x): the no_change_reason escape is only honest when machine
|
|
454
|
+
// verification was STRUCTURALLY impossible. With an enabled verification
|
|
455
|
+
// command configured, the runtime had a machine-verifiable path for this
|
|
456
|
+
// round — a declared reason cannot waive it (that would let an agent
|
|
457
|
+
// dodge the configured verification by writing a sentence, even while git
|
|
458
|
+
// observed real file changes). Verification-less policies (the default:
|
|
459
|
+
// commands: []) keep the escape: there is genuinely nothing to run.
|
|
460
|
+
const verificationConfigured = getPolicy().evidence.commands.some((command) => command.enabled);
|
|
461
|
+
const noChange = !verificationConfigured &&
|
|
462
|
+
typeof selfEval.no_change_reason === "string" &&
|
|
423
463
|
selfEval.no_change_reason.trim().length > 0;
|
|
424
464
|
// v3.3: severity follows evidence.machine_backed_success — "required"
|
|
425
465
|
// rejects (error), "warn" tolerates with a warn (round commits, success
|
|
@@ -605,52 +645,23 @@ function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
|
|
|
605
645
|
function extractSubGoalIds(text) {
|
|
606
646
|
return [...new Set(text.toLowerCase().match(/sg-[a-f0-9]{8}/g) ?? [])];
|
|
607
647
|
}
|
|
608
|
-
/** Extract file-path-like tokens (e.g. "src/auth/login.ts") from text. */
|
|
609
|
-
function extractFilePaths(text) {
|
|
610
|
-
return [...new Set(text.match(/[\w./-]+\.[a-z]{2,6}\b/gi) ?? [])];
|
|
611
|
-
}
|
|
612
648
|
/** Whether a changed path looks like a test file. */
|
|
613
649
|
function isTestFile(path) {
|
|
614
650
|
return /\.(test|spec)\.[a-z0-9]+$/i.test(path) ||
|
|
615
651
|
/(^|[\\/])tests?[\\/]/i.test(path) ||
|
|
616
652
|
/_test\.[a-z0-9]+$/i.test(path);
|
|
617
653
|
}
|
|
618
|
-
/** Map each referenced sub-goal ID to its known description(s), reconstructed
|
|
619
|
-
* from emerged_subtasks in the vault lineage and the previous self-eval. */
|
|
620
|
-
function referencedSubGoalDescriptions(ids, vaultEntries, prevSelfEval) {
|
|
621
|
-
const map = new Map();
|
|
622
|
-
for (const id of ids)
|
|
623
|
-
map.set(id, []);
|
|
624
|
-
const descriptions = [];
|
|
625
|
-
for (const entry of vaultEntries) {
|
|
626
|
-
const emerged = Array.isArray(entry.emerged_subtasks)
|
|
627
|
-
? entry.emerged_subtasks.filter((v) => typeof v === "string")
|
|
628
|
-
: [];
|
|
629
|
-
descriptions.push(...emerged);
|
|
630
|
-
}
|
|
631
|
-
descriptions.push(...(prevSelfEval?.emerged_subtasks ?? []));
|
|
632
|
-
for (const desc of descriptions) {
|
|
633
|
-
const trimmed = desc.trim();
|
|
634
|
-
if (!trimmed)
|
|
635
|
-
continue;
|
|
636
|
-
const id = deriveSubGoalId(trimmed);
|
|
637
|
-
if (map.has(id))
|
|
638
|
-
map.get(id).push(trimmed);
|
|
639
|
-
}
|
|
640
|
-
return map;
|
|
641
|
-
}
|
|
642
654
|
/** v2.1 (check 11): Compare the previous round's declared next_action with
|
|
643
655
|
* the current round's actual output_summary. Detect when the agent says it
|
|
644
656
|
* will do X but then does Y — silent drift without explanation.
|
|
645
657
|
*
|
|
646
658
|
* Only fires when both next_action and output_summary are non-empty.
|
|
647
659
|
*
|
|
648
|
-
* v2.14: Structured-ID-first detection. Alignment is decided by a
|
|
649
|
-
* of concrete signals before falling back to string similarity:
|
|
660
|
+
* v2.14/v3.7.1: Structured-ID-first detection. Alignment is decided by a
|
|
661
|
+
* waterfall of concrete signals before falling back to string similarity:
|
|
650
662
|
* 1. Sub-goal IDs (sg-XXXXXXXX) in next_action matched against this
|
|
651
|
-
* round's
|
|
652
|
-
*
|
|
653
|
-
* known description).
|
|
663
|
+
* round's declared done transitions (subgoal_updates). Description
|
|
664
|
+
* matching is gone — transitions are strict ID references now.
|
|
654
665
|
* 2. File paths named in next_action appearing in files_changed.
|
|
655
666
|
* 3. Test evidence — next_action mentions tests and test files were
|
|
656
667
|
* changed with tests actually run.
|
|
@@ -666,27 +677,18 @@ function checkIntentDrift(selfEval, prevSelfEval, vaultEntries) {
|
|
|
666
677
|
const actual = selfEval.output_summary.trim();
|
|
667
678
|
const policy = getPolicy();
|
|
668
679
|
const filesChanged = selfEval.execution_evidence?.files_changed ?? [];
|
|
669
|
-
// ── Signal 1: sub-goal IDs referenced by next_action →
|
|
680
|
+
// ── Signal 1: sub-goal IDs referenced by next_action → a done transition
|
|
670
681
|
const intentIds = extractSubGoalIds(intent);
|
|
671
682
|
if (intentIds.length > 0) {
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (!text)
|
|
677
|
-
return false;
|
|
678
|
-
if (text.toLowerCase() === id)
|
|
679
|
-
return true;
|
|
680
|
-
if (deriveSubGoalId(text) === id)
|
|
681
|
-
return true;
|
|
682
|
-
const descriptions = referenced.get(id) ?? [];
|
|
683
|
-
return descriptions.some((desc) => jaccardSimilarity(text, desc) >= policy.evolution.subgoal_match_threshold);
|
|
684
|
-
}));
|
|
683
|
+
const doneIds = new Set((selfEval.subgoal_updates ?? [])
|
|
684
|
+
.filter((u) => u.status === "done")
|
|
685
|
+
.map((u) => u.id));
|
|
686
|
+
const idMatched = intentIds.some((id) => doneIds.has(id));
|
|
685
687
|
if (idMatched)
|
|
686
688
|
return null;
|
|
687
689
|
}
|
|
688
690
|
// ── Signal 2: file paths named in next_action appear in files_changed
|
|
689
|
-
const intentPaths =
|
|
691
|
+
const intentPaths = extractFilePathTokens(intent);
|
|
690
692
|
if (intentPaths.length > 0 && filesChanged.length > 0) {
|
|
691
693
|
const pathMatched = intentPaths.some((p) => filesChanged.some((f) => f === p || f.endsWith(p) || p.endsWith(f)));
|
|
692
694
|
if (pathMatched)
|
|
@@ -707,7 +709,7 @@ function checkIntentDrift(selfEval, prevSelfEval, vaultEntries) {
|
|
|
707
709
|
if (score >= threshold)
|
|
708
710
|
return null;
|
|
709
711
|
const idNote = intentIds.length > 0
|
|
710
|
-
? `; ${intentIds.length} referenced sub-goal ID(s) did not match
|
|
712
|
+
? `; ${intentIds.length} referenced sub-goal ID(s) did not match a declared done transition`
|
|
711
713
|
: "";
|
|
712
714
|
return makeVerificationFlag({
|
|
713
715
|
severity: "warn",
|
|
@@ -886,9 +888,12 @@ function checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound) {
|
|
|
886
888
|
const nextAction = selfEval.next_action?.trim();
|
|
887
889
|
if (!nextAction)
|
|
888
890
|
return null;
|
|
889
|
-
// Reconstruct pending
|
|
891
|
+
// Reconstruct the pending set from vault entries and the current eval.
|
|
892
|
+
// v3.7.1: emerged descriptions are the creation channel; done/canceled are
|
|
893
|
+
// terminal ids reached through subgoal_updates (monotone — a terminal
|
|
894
|
+
// transition can never be undone, so the terminal id set only grows).
|
|
890
895
|
const pendingSubGoals = new Set();
|
|
891
|
-
const
|
|
896
|
+
const terminalIds = new Set();
|
|
892
897
|
for (const entry of vaultEntries) {
|
|
893
898
|
const emerged = Array.isArray(entry.emerged_subtasks)
|
|
894
899
|
? entry.emerged_subtasks.filter((v) => typeof v === "string")
|
|
@@ -896,32 +901,23 @@ function checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound) {
|
|
|
896
901
|
for (const desc of emerged) {
|
|
897
902
|
pendingSubGoals.add(desc.trim());
|
|
898
903
|
}
|
|
899
|
-
const
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
for (const desc of done) {
|
|
903
|
-
completedOrCanceled.add(desc.trim());
|
|
904
|
-
}
|
|
905
|
-
const canceled = Array.isArray(entry.canceled_subtasks)
|
|
906
|
-
? entry.canceled_subtasks.filter((v) => typeof v === "string")
|
|
907
|
-
: [];
|
|
908
|
-
for (const desc of canceled) {
|
|
909
|
-
completedOrCanceled.add(desc.trim());
|
|
904
|
+
for (const u of entrySubGoalUpdates(entry)) {
|
|
905
|
+
if (u.status === "done" || u.status === "canceled")
|
|
906
|
+
terminalIds.add(u.id);
|
|
910
907
|
}
|
|
911
908
|
}
|
|
912
|
-
// Also process current selfEval
|
|
909
|
+
// Also process the current selfEval
|
|
913
910
|
for (const desc of selfEval.emerged_subtasks ?? []) {
|
|
914
911
|
pendingSubGoals.add(desc.trim());
|
|
915
912
|
}
|
|
916
|
-
for (const
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
for (const desc of selfEval.canceled_subtasks ?? []) {
|
|
920
|
-
completedOrCanceled.add(desc.trim());
|
|
913
|
+
for (const u of selfEval.subgoal_updates ?? []) {
|
|
914
|
+
if (u.status === "done" || u.status === "canceled")
|
|
915
|
+
terminalIds.add(u.id);
|
|
921
916
|
}
|
|
922
|
-
// Remove
|
|
923
|
-
for (const desc of
|
|
924
|
-
|
|
917
|
+
// Remove descriptions whose derived id reached a terminal state
|
|
918
|
+
for (const desc of [...pendingSubGoals]) {
|
|
919
|
+
if (terminalIds.has(deriveSubGoalId(desc)))
|
|
920
|
+
pendingSubGoals.delete(desc);
|
|
925
921
|
}
|
|
926
922
|
// Need at least 3 pending sub-goals for the check to be meaningful
|
|
927
923
|
if (pendingSubGoals.size < 3)
|
|
@@ -951,37 +947,60 @@ function checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound) {
|
|
|
951
947
|
check: CHECK_SUBGOAL_DRIFT,
|
|
952
948
|
detail: `Agent's next_action doesn't align with any of ${pendingSubGoals.size} pending sub-goals. ` +
|
|
953
949
|
`Pending: ${sample}… Consider completing existing sub-goals before starting new work, ` +
|
|
954
|
-
`or cancel outdated sub-goals via
|
|
950
|
+
`or cancel outdated sub-goals via subgoal_updates.`,
|
|
955
951
|
});
|
|
956
952
|
}
|
|
957
953
|
return null;
|
|
958
954
|
}
|
|
959
955
|
/** v2.13: After a backtrack, check that the agent restored the workspace
|
|
960
|
-
* before working.
|
|
961
|
-
*
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
956
|
+
* before working. Two arms:
|
|
957
|
+
*
|
|
958
|
+
* 1. Machine arm (M3, error): a skipped file whose CURRENT git fingerprint
|
|
959
|
+
* still equals the fingerprint recorded at its failed round was never
|
|
960
|
+
* touched since the rollback — the workspace was provably not restored.
|
|
961
|
+
* Restored or re-modified files change fingerprint and can never be
|
|
962
|
+
* misjudged by this arm.
|
|
963
|
+
*
|
|
964
|
+
* 2. Claims arm (warn only): the agent's files_changed overlaps the skipped
|
|
965
|
+
* list. A self-report alone cannot distinguish a restored-then-redone
|
|
966
|
+
* file from a never-restored one — a legitimate multi-file redo would
|
|
967
|
+
* otherwise be falsely terminated — so overlap is guidance, not a
|
|
968
|
+
* verdict. (The old error tier for >= 3 overlapping files was removed
|
|
969
|
+
* for exactly this reason: it fired on honest redos.) */
|
|
970
|
+
function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtrackSkippedFingerprints = {}, evidenceSnapshots = []) {
|
|
971
|
+
// Machine arm — untouched-since-rollback proof, independent of claims.
|
|
972
|
+
const failedEntries = Object.entries(backtrackSkippedFingerprints);
|
|
973
|
+
if (failedEntries.length > 0) {
|
|
974
|
+
const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git" && isRecord(snapshot.data) &&
|
|
975
|
+
isRecord(snapshot.data.fingerprints));
|
|
976
|
+
const currentFingerprints = git
|
|
977
|
+
? git.data.fingerprints
|
|
978
|
+
: null;
|
|
979
|
+
if (currentFingerprints) {
|
|
980
|
+
const untouched = failedEntries
|
|
981
|
+
.filter(([file, failedFp]) => currentFingerprints[file] === failedFp)
|
|
982
|
+
.map(([file]) => file)
|
|
983
|
+
.sort();
|
|
984
|
+
if (untouched.length > 0) {
|
|
985
|
+
return makeVerificationFlag({
|
|
986
|
+
severity: "error",
|
|
987
|
+
field: "workspace",
|
|
988
|
+
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
989
|
+
detail: `File(s) from the failed rounds are byte-identical to their ` +
|
|
990
|
+
`state at the rollback: ${untouched.slice(0, 5).join(", ")}. The ` +
|
|
991
|
+
`workspace was never restored — restore it to the restore point ` +
|
|
992
|
+
`(git checkout/reset) before redoing the work.`,
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
965
997
|
const ev = selfEval.execution_evidence;
|
|
966
998
|
if (!ev || ev.files_changed.length === 0)
|
|
967
999
|
return null;
|
|
968
|
-
//
|
|
1000
|
+
// Claims arm — overlap as guidance only.
|
|
969
1001
|
const overlap = ev.files_changed.filter((f) => backtrackSkippedFiles.some((sf) => sf === f || f.endsWith(sf) || sf.endsWith(f)));
|
|
970
1002
|
if (overlap.length === 0)
|
|
971
1003
|
return null;
|
|
972
|
-
// Agent is modifying files that were part of failed rounds without
|
|
973
|
-
// having properly restored the workspace first.
|
|
974
|
-
if (overlap.length >= 3 || overlap.length === ev.files_changed.length) {
|
|
975
|
-
return makeVerificationFlag({
|
|
976
|
-
severity: "error",
|
|
977
|
-
field: "files_changed",
|
|
978
|
-
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
979
|
-
detail: `Agent modified ${overlap.length} file(s) that were part of ` +
|
|
980
|
-
`the skipped (failed) rounds: ${overlap.slice(0, 5).join(", ")}. ` +
|
|
981
|
-
`The workspace was likely not restored before working.`,
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
// Minor overlap — warn only
|
|
985
1004
|
return makeVerificationFlag({
|
|
986
1005
|
severity: "warn",
|
|
987
1006
|
field: "files_changed",
|
|
@@ -1239,15 +1258,80 @@ function checkContractPremature(activeContract, selfEval) {
|
|
|
1239
1258
|
* @param evidenceSnapshots v1.18: Evidence snapshots from configured providers.
|
|
1240
1259
|
* Used by checkEvidenceIntegrity for multi-provider
|
|
1241
1260
|
* cross-validation. Defaults to empty array. */
|
|
1242
|
-
|
|
1261
|
+
/** v3.7.1: Opt-in round blocking for the gate layer. For every gate id the
|
|
1262
|
+
* round cites (gate_ids), the gate_opened record must exist, must be a
|
|
1263
|
+
* USER gate, and must have an approved gate_decision. Error flags list
|
|
1264
|
+
* every failing id with a reason. Entirely skipped when the feature is
|
|
1265
|
+
* disabled — there is nothing to cite against. */
|
|
1266
|
+
function checkUserGateUnresolved(selfEval, gateEntries) {
|
|
1267
|
+
if (!getPolicy().gate.enabled)
|
|
1268
|
+
return null;
|
|
1269
|
+
const cited = (selfEval.gate_ids ?? []).filter((id) => typeof id === "string");
|
|
1270
|
+
if (cited.length === 0)
|
|
1271
|
+
return null;
|
|
1272
|
+
const opened = new Set();
|
|
1273
|
+
const userKind = new Set();
|
|
1274
|
+
const approved = new Set();
|
|
1275
|
+
for (const entry of gateEntries) {
|
|
1276
|
+
if (entry.task_type === "gate_opened" && typeof entry.gate_id === "string") {
|
|
1277
|
+
opened.add(entry.gate_id);
|
|
1278
|
+
// v3.7.1: structured records declare gate_kind; legacy flat records
|
|
1279
|
+
// (blocked-round auto-records) classify their stored action text.
|
|
1280
|
+
if (entry.gate_kind === "user") {
|
|
1281
|
+
userKind.add(entry.gate_id);
|
|
1282
|
+
}
|
|
1283
|
+
else if (typeof entry.gate_action === "string") {
|
|
1284
|
+
let kind = null;
|
|
1285
|
+
try {
|
|
1286
|
+
kind = preflightStructuredGate(JSON.parse(entry.gate_action)).kind;
|
|
1287
|
+
}
|
|
1288
|
+
catch {
|
|
1289
|
+
kind = deriveGate(entry.gate_action).gate.kind;
|
|
1290
|
+
}
|
|
1291
|
+
if (kind === "user")
|
|
1292
|
+
userKind.add(entry.gate_id);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (entry.task_type === "gate_decision" && entry.approved === true &&
|
|
1296
|
+
typeof entry.gate_id === "string") {
|
|
1297
|
+
approved.add(entry.gate_id);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
const failures = [];
|
|
1301
|
+
for (const gateId of cited) {
|
|
1302
|
+
if (!opened.has(gateId))
|
|
1303
|
+
failures.push(`${gateId} (not_found)`);
|
|
1304
|
+
else if (!userKind.has(gateId))
|
|
1305
|
+
failures.push(`${gateId} (not_user_gate)`);
|
|
1306
|
+
else if (!approved.has(gateId))
|
|
1307
|
+
failures.push(`${gateId} (not_approved)`);
|
|
1308
|
+
}
|
|
1309
|
+
if (failures.length === 0)
|
|
1310
|
+
return null;
|
|
1311
|
+
return makeVerificationFlag({
|
|
1312
|
+
severity: "error",
|
|
1313
|
+
field: "evaluation",
|
|
1314
|
+
check: CHECK_USER_GATE_UNRESOLVED,
|
|
1315
|
+
detail: `The round cites gates without an approved human decision: ` +
|
|
1316
|
+
`${failures.join("; ")}. Obtain approval via loopforge_gate_check / ` +
|
|
1317
|
+
`loopforge_gate_resolve before claiming the action is done.`,
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
export function verifySelfEvaluation(selfEval, currentRound, vaultEntries, prevSelfEval = null, evidenceSnapshots = [],
|
|
1243
1321
|
/** v2.13: Files from skipped backtrack rounds. If the agent's
|
|
1244
1322
|
* files_changed overlaps significantly with these, the workspace
|
|
1245
1323
|
* was not properly restored before working. */
|
|
1246
|
-
backtrackSkippedFiles = [],
|
|
1324
|
+
backtrackSkippedFiles = [],
|
|
1325
|
+
/** M3 (v3.7.x): skipped-file git fingerprints at their failed rounds —
|
|
1326
|
+
* machine proof of "untouched since the rollback" for the restore check. */
|
|
1327
|
+
backtrackSkippedFingerprints = {},
|
|
1247
1328
|
/** v2.12: Git HEAD commit of the backtrack restore point. When set, the
|
|
1248
1329
|
* current git snapshot must sit at this commit — otherwise the workspace
|
|
1249
1330
|
* was not restored and the round cannot be accepted. */
|
|
1250
|
-
backtrackTargetGitHead
|
|
1331
|
+
backtrackTargetGitHead,
|
|
1332
|
+
/** v3.7.1: gate records (task_type gate_opened / gate_decision) live
|
|
1333
|
+
* outside the round prefix — the caller passes them in explicitly. */
|
|
1334
|
+
gateEntries = []) {
|
|
1251
1335
|
const flags = [];
|
|
1252
1336
|
// Collect violations from all previous vault entries for duplicate-discovery
|
|
1253
1337
|
// and other checks that need deeper history.
|
|
@@ -1275,11 +1359,9 @@ backtrackTargetGitHead) {
|
|
|
1275
1359
|
// the list at the first problem, hiding every other bad retroactive claim
|
|
1276
1360
|
// from the agent until the next round.
|
|
1277
1361
|
const checks = [
|
|
1278
|
-
() => checkProgressRegression(selfEval, prevSelfEval),
|
|
1279
|
-
() => checkEmptyChangeWithPassing(selfEval),
|
|
1280
1362
|
() => checkSuccessWithRemainingCriteria(selfEval),
|
|
1281
|
-
// v2.12/v3.6:
|
|
1282
|
-
//
|
|
1363
|
+
// v2.12/v3.6/v3.7: the single success-evidence semantic — empty/missing-
|
|
1364
|
+
// evidence arm (ex-R3) + claims arm (providerStatus lens, tamper-aware)
|
|
1283
1365
|
() => checkSuccessWithoutVerifiedEvidence(selfEval, evidenceStatus),
|
|
1284
1366
|
// v2.12: criteria met but none machine-verified (mild criterion-specific rule)
|
|
1285
1367
|
() => checkUnverifiedCriteriaClaims(selfEval, claimView),
|
|
@@ -1303,7 +1385,7 @@ backtrackTargetGitHead) {
|
|
|
1303
1385
|
// v2.2: Detect sub-goal drift — next_action doesn't align with pending sub-goals
|
|
1304
1386
|
() => checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound),
|
|
1305
1387
|
// v2.13: Post-backtrack workspace restore check
|
|
1306
|
-
() => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles),
|
|
1388
|
+
() => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles, backtrackSkippedFingerprints, evidenceSnapshots),
|
|
1307
1389
|
// v2.12: Post-backtrack git HEAD restore check
|
|
1308
1390
|
() => checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead),
|
|
1309
1391
|
// v3.3: Round Contract checks. v3.4: split by target — structural
|
|
@@ -1317,6 +1399,8 @@ backtrackTargetGitHead) {
|
|
|
1317
1399
|
() => checkPrematureBoundary(activeContract, selfEval, claimView),
|
|
1318
1400
|
() => checkContractCompletionUnverified(activeContract, selfEval, evidenceSnapshots),
|
|
1319
1401
|
() => checkContractPremature(activeContract, selfEval),
|
|
1402
|
+
// v3.7.1: opt-in gate-layer blocking (skipped when disabled)
|
|
1403
|
+
() => checkUserGateUnresolved(selfEval, gateEntries),
|
|
1320
1404
|
];
|
|
1321
1405
|
for (const run of checks) {
|
|
1322
1406
|
const result = run();
|