loopforge 3.2.1 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -71
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +6 -38
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +71 -6
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +176 -29
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -26
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +17 -0
- package/dist/cognitive-facts.d.ts.map +1 -0
- package/dist/cognitive-facts.js +99 -0
- package/dist/cognitive-facts.js.map +1 -0
- package/dist/cognitive-governance.d.ts +0 -1
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +1 -4
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +100 -0
- package/dist/committed-round.d.ts.map +1 -0
- package/dist/committed-round.js +349 -0
- package/dist/committed-round.js.map +1 -0
- package/dist/enforcement-gate.d.ts +4 -2
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +385 -357
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +16 -4
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +65 -16
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +15 -39
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +35 -17
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +105 -108
- package/dist/evidence-provider.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +21 -10
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +232 -276
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-projection.d.ts +5 -25
- package/dist/loop-projection.d.ts.map +1 -1
- package/dist/loop-projection.js +18 -134
- package/dist/loop-projection.js.map +1 -1
- package/dist/loop-store.d.ts +16 -18
- 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 +13 -10
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +95 -68
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.js +3 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +17 -5
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +177 -54
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +12 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +161 -53
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +0 -7
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +15 -55
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +22 -31
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +24 -42
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +0 -3
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +124 -191
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +61 -46
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +2 -18
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +21 -22
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +64 -0
- package/dist/round-contract.d.ts.map +1 -0
- package/dist/round-contract.js +80 -0
- package/dist/round-contract.js.map +1 -0
- package/dist/round-coordinator.d.ts +5 -3
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +34 -29
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +0 -4
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +1 -11
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +0 -1
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +0 -1
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +29 -34
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +112 -148
- 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 +72 -21
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +544 -157
- package/dist/verification-gate.js.map +1 -1
- package/dist/workspace.d.ts +29 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +61 -0
- package/dist/workspace.js.map +1 -0
- package/loop_policy.json +5 -17
- package/package.json +2 -2
- package/skills/perception/SKILL.md +137 -136
package/dist/enforcement-gate.js
CHANGED
|
@@ -17,35 +17,22 @@
|
|
|
17
17
|
* with stopReason "enforcement_terminated".
|
|
18
18
|
*/
|
|
19
19
|
import { makeEnforcementResult } from "./protocol.js";
|
|
20
|
-
import {
|
|
21
|
-
import { effectiveSuccess } from "./self-eval.js";
|
|
20
|
+
import { machineProgressSeries, CHECK_SUCCESS_WITH_REMAINING_CRITERIA, CHECK_RECURRING_VIOLATION, CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE, CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED, CHECK_REQUIRED_COMMAND_FAILED, CHECK_COMMAND_EVIDENCE_MISMATCH, CHECK_OUTCOME_SUCCESS_CONTRADICTION, CHECK_VERIFICATION_ENTRYPOINT_MODIFIED, CHECK_PREMATURE_BOUNDARY, CHECK_ROUND_SCOPE_DRIFT, CHECK_CONTRACT_COMPLETION_UNVERIFIED } from "./verification-gate.js";
|
|
22
21
|
import { deriveConstraintId, deriveCriterionId, deriveSubGoalId } from "./loop-compiler.js";
|
|
23
22
|
import { getPolicy } from "./policy.js";
|
|
24
|
-
import { STABLE_ID_RE, isRecord } from "./token-utils.js";
|
|
25
|
-
import {
|
|
23
|
+
import { STABLE_ID_RE, isRecord, entryRound, extractFilePathTokens } from "./token-utils.js";
|
|
24
|
+
import { committedRoundsFromEntries, decodeCommittedRound, machineEvidenceForRound, } from "./committed-round.js";
|
|
26
25
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
26
|
// Helpers
|
|
28
27
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
-
/** Extract progress_estimate from a vault entry's execution_evidence.
|
|
30
|
-
* Returns null if no execution evidence is available. */
|
|
31
|
-
function entryProgress(entry) {
|
|
32
|
-
const evidence = entry.execution_evidence;
|
|
33
|
-
if (!evidence)
|
|
34
|
-
return null;
|
|
35
|
-
const pe = evidence.progress_estimate;
|
|
36
|
-
return typeof pe === "number" ? pe : null;
|
|
37
|
-
}
|
|
38
28
|
/** Collect progress estimates from vault entries into a round→estimate map.
|
|
39
|
-
* Shared by
|
|
29
|
+
* Shared by the progress evaluator's stall and flatline tiers. */
|
|
40
30
|
function collectProgressByRound(vaultEntries, currentRound) {
|
|
41
31
|
const progressByRound = new Map();
|
|
42
|
-
for (const
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
const pe = entryProgress(entry);
|
|
47
|
-
if (pe !== null)
|
|
48
|
-
progressByRound.set(rnd, pe);
|
|
32
|
+
for (const round of committedRoundsFromEntries(vaultEntries, currentRound)) {
|
|
33
|
+
const progress = round.executionEvidence?.progress_estimate;
|
|
34
|
+
if (typeof progress === "number")
|
|
35
|
+
progressByRound.set(round.round, progress);
|
|
49
36
|
}
|
|
50
37
|
return progressByRound;
|
|
51
38
|
}
|
|
@@ -73,13 +60,7 @@ function progressWindow(vaultEntries, currentRound, selfEval) {
|
|
|
73
60
|
* decision under loop_lineage.round_transaction; lineage entries carry
|
|
74
61
|
* nothing at the top level. */
|
|
75
62
|
function committedRoundResult(entry) {
|
|
76
|
-
|
|
77
|
-
const rt = lineage && isRecord(lineage.round_transaction)
|
|
78
|
-
? lineage.round_transaction
|
|
79
|
-
: null;
|
|
80
|
-
if (!rt)
|
|
81
|
-
return null;
|
|
82
|
-
return isProcessResult(rt.result) ? rt.result : null;
|
|
63
|
+
return decodeCommittedRound(entry)?.result ?? null;
|
|
83
64
|
}
|
|
84
65
|
/** True when a backtrack already committed at or after the current round —
|
|
85
66
|
* i.e. the loop is re-walking territory the system already rolled back
|
|
@@ -91,7 +72,7 @@ function hasCommittedBacktrack(vaultEntries, currentRound) {
|
|
|
91
72
|
return committedRoundResult(entry)?.action === "backtrack";
|
|
92
73
|
});
|
|
93
74
|
}
|
|
94
|
-
/** Check whether a
|
|
75
|
+
/** Check whether a committed round represents a clean restore point.
|
|
95
76
|
* as a safe restore point. A clean round was accepted (not rejected)
|
|
96
77
|
* and had no error-level verification flags.
|
|
97
78
|
*
|
|
@@ -101,61 +82,30 @@ function hasCommittedBacktrack(vaultEntries, currentRound) {
|
|
|
101
82
|
* valid backtrack target (the most common backtrack trigger is exactly a
|
|
102
83
|
* run of such rounds). Rejected/terminated rounds never commit, and
|
|
103
84
|
* rolled-back rounds are excluded below. */
|
|
104
|
-
function isCleanRound(
|
|
105
|
-
|
|
106
|
-
// A rolled-back round (already backtracked) is not a valid restore point.
|
|
107
|
-
if (committed?.action === "backtrack")
|
|
108
|
-
return false;
|
|
109
|
-
// Rejected/terminated rounds are not clean. (They never commit in the
|
|
110
|
-
// real vault — this guards legacy or mixed-shape data.)
|
|
111
|
-
if (committed && (committed.action === "reject" || committed.action === "terminate")) {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
// Rounds with error-level verification flags are not clean
|
|
115
|
-
const flags = Array.isArray(entry.verification_flags)
|
|
116
|
-
? entry.verification_flags
|
|
117
|
-
: committed?.verificationFlags ?? [];
|
|
118
|
-
for (const f of flags) {
|
|
119
|
-
if (f !== null && typeof f === "object" &&
|
|
120
|
-
f.severity === "error")
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
return true;
|
|
85
|
+
function isCleanRound(round) {
|
|
86
|
+
return !round.verificationFlags.some((flag) => flag.severity === "error");
|
|
124
87
|
}
|
|
125
88
|
/** Scan backwards from currentRound to find the most recent clean round.
|
|
126
89
|
* Collects discovered_constraints from skipped rounds along the way.
|
|
127
90
|
* Returns null when no clean round is found within maxDepth. */
|
|
128
91
|
export function findSafeRestorePoint(currentRound, vaultEntries, maxDepth) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// round_transaction), and it precedes the feedback entry in the flat view.
|
|
132
|
-
// Treating it as a decision made every round look clean, so the restore
|
|
133
|
-
// point was always currentRound-1: dirty/rejected rounds were never
|
|
134
|
-
// skipped, depth > 1 never collected skippedDiscoveries, and the restore
|
|
135
|
-
// prompt's file list (round-coordinator) was always empty.
|
|
136
|
-
const completed = vaultEntries
|
|
137
|
-
.filter((e) => {
|
|
138
|
-
const rnd = entryRound(e);
|
|
139
|
-
return rnd >= 1 && rnd < currentRound && e.task_type !== "loop_lineage";
|
|
140
|
-
})
|
|
141
|
-
.sort((a, b) => entryRound(b) - entryRound(a)); // newest first
|
|
92
|
+
const completed = committedRoundsFromEntries(vaultEntries, currentRound)
|
|
93
|
+
.sort((a, b) => b.round - a.round);
|
|
142
94
|
const skippedDiscoveries = [];
|
|
143
95
|
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
144
96
|
const targetRound = currentRound - depth;
|
|
145
97
|
if (targetRound < 1)
|
|
146
98
|
break;
|
|
147
|
-
const
|
|
148
|
-
if (!
|
|
99
|
+
const round = completed.find((item) => item.round === targetRound);
|
|
100
|
+
if (!round)
|
|
149
101
|
continue;
|
|
150
102
|
// Collect discoveries from rounds we're about to skip (depth > 1)
|
|
151
103
|
if (depth > 1) {
|
|
152
104
|
for (let d = 1; d < depth; d++) {
|
|
153
105
|
const skippedRound = currentRound - d;
|
|
154
|
-
const skippedEntry = completed.find((
|
|
155
|
-
if (skippedEntry) {
|
|
156
|
-
const discovered =
|
|
157
|
-
? skippedEntry.discovered_constraints.filter((v) => typeof v === "string")
|
|
158
|
-
: [];
|
|
106
|
+
const skippedEntry = completed.find((item) => item.round === skippedRound);
|
|
107
|
+
if (skippedEntry?.evaluation) {
|
|
108
|
+
const discovered = skippedEntry.evaluation.discovered_constraints ?? [];
|
|
159
109
|
for (const item of discovered) {
|
|
160
110
|
if (!skippedDiscoveries.includes(item))
|
|
161
111
|
skippedDiscoveries.push(item);
|
|
@@ -163,7 +113,7 @@ export function findSafeRestorePoint(currentRound, vaultEntries, maxDepth) {
|
|
|
163
113
|
}
|
|
164
114
|
}
|
|
165
115
|
}
|
|
166
|
-
if (isCleanRound(
|
|
116
|
+
if (isCleanRound(round)) {
|
|
167
117
|
return { round: targetRound, skippedDiscoveries };
|
|
168
118
|
}
|
|
169
119
|
}
|
|
@@ -174,23 +124,11 @@ export function findSafeRestorePoint(currentRound, vaultEntries, maxDepth) {
|
|
|
174
124
|
* evidence as fallback). Returns null when the round has no git snapshot
|
|
175
125
|
* or no committed feedback entry. */
|
|
176
126
|
export function findBacktrackTargetGitHead(restoreRound, vaultEntries) {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
: null;
|
|
181
|
-
const transaction = lineage && isRecord(lineage.round_transaction)
|
|
182
|
-
? lineage.round_transaction
|
|
183
|
-
: null;
|
|
184
|
-
const snapshot = transaction && isRecord(transaction.snapshot)
|
|
185
|
-
? transaction.snapshot
|
|
186
|
-
: null;
|
|
187
|
-
if (!snapshot)
|
|
127
|
+
const committed = committedRoundsFromEntries(vaultEntries)
|
|
128
|
+
.find((view) => view.round === restoreRound);
|
|
129
|
+
if (!committed)
|
|
188
130
|
return null;
|
|
189
|
-
const evidence =
|
|
190
|
-
? snapshot.afterEvidence
|
|
191
|
-
: Array.isArray(snapshot.beforeEvidence)
|
|
192
|
-
? snapshot.beforeEvidence
|
|
193
|
-
: [];
|
|
131
|
+
const evidence = machineEvidenceForRound(committed);
|
|
194
132
|
for (const item of evidence) {
|
|
195
133
|
if (!isRecord(item) || item.provider !== "git" || !isRecord(item.data))
|
|
196
134
|
continue;
|
|
@@ -217,14 +155,15 @@ gitHead) {
|
|
|
217
155
|
"",
|
|
218
156
|
];
|
|
219
157
|
// ── Why This Happened ────────────────────────────────────────────────
|
|
158
|
+
// v3.7: stall and flatline are one evaluator emitting progress_stall; the
|
|
159
|
+
// former flatline-specific wording ("zero forward motion → radically
|
|
160
|
+
// different strategy") is folded into the single section — the disposition
|
|
161
|
+
// tier still distinguishes the enforcement reason itself.
|
|
220
162
|
if (triggerRule === "progress_stall") {
|
|
221
|
-
lines.push("### Why This Happened", "", `Progress stalled over rounds ${toRound + 1}–${fromRound}
|
|
222
|
-
`
|
|
223
|
-
`
|
|
224
|
-
|
|
225
|
-
else if (triggerRule === "progress_stall_terminal") {
|
|
226
|
-
lines.push("### Why This Happened", "", `Progress was completely flat for multiple rounds leading up to Round ${fromRound}. ` +
|
|
227
|
-
`The agent made **zero forward motion** — the task is not advancing.`, "", "A **radically different** strategy is needed. The previous approach produced nothing.", "");
|
|
163
|
+
lines.push("### Why This Happened", "", `Progress stalled over rounds ${toRound + 1}–${fromRound}: the work ` +
|
|
164
|
+
`produced no verifiable forward motion — no new files changed, no ` +
|
|
165
|
+
`criteria met, no progress estimates moving.`, "", "The approach used in those rounds **did not work**. It should not be " +
|
|
166
|
+
"repeated — a **radically different** strategy is needed.", "");
|
|
228
167
|
}
|
|
229
168
|
// ── v2.13: Workspace Restore ──────────────────────────────────────────
|
|
230
169
|
lines.push("### ⚠️ Workspace Restore Required", "", `Your working directory still contains changes from the **failed** ` +
|
|
@@ -241,7 +180,10 @@ gitHead) {
|
|
|
241
180
|
}
|
|
242
181
|
lines.push("");
|
|
243
182
|
}
|
|
244
|
-
lines.push("**To restore a clean workspace, run:**", "", "```bash", "# Option A:
|
|
183
|
+
lines.push("**To restore a clean workspace, run:**", "", "```bash", "# Option A: Keep everything as a safety net (recommended) — reversible", `git stash push -u -m "backtrack-safety-net-round-${fromRound}"`, "", "# Option B: Discard ALL uncommitted AND untracked changes (permanent)", "git checkout -- .", "git clean -fd", "```", "", "> Option A is a safety net — do NOT `git stash pop` before submitting " +
|
|
184
|
+
"the next round: the skipped work would look unrestored and be rejected. " +
|
|
185
|
+
"Restore only files you own after the next round commits, via " +
|
|
186
|
+
"`git checkout stash@{0} -- <path>`.", "");
|
|
245
187
|
// v2.12: Restore to the exact commit observed at the clean round.
|
|
246
188
|
if (gitHead) {
|
|
247
189
|
lines.push(`**Restore to commit \`${gitHead.slice(0, 12)}\`** — the verification ` +
|
|
@@ -255,7 +197,16 @@ gitHead) {
|
|
|
255
197
|
"> with unrestored files will be **rejected**.", "");
|
|
256
198
|
// ── What Must Change ──────────────────────────────────────────────────
|
|
257
199
|
lines.push("### What Must Change", "", "- Do **NOT** repeat the approach used in the skipped rounds.", "- Try a **different** task decomposition or technique.", "- If the current sub-goal is stuck, consider canceling it " +
|
|
258
|
-
"(`canceled_subtasks`) and working on a different one.",
|
|
200
|
+
"(`canceled_subtasks`) and working on a different one.",
|
|
201
|
+
// v3.5: the restored Current Task may be the very Round Contract that
|
|
202
|
+
// stalled — the agent must be told the sanctioned way to revise it
|
|
203
|
+
// (outcome=blocked closes the active contract; the revised proposal in
|
|
204
|
+
// the same submission becomes active next round). Without this the
|
|
205
|
+
// agent's revision would be silently ignored as premature.
|
|
206
|
+
"- If the restored Current Task is a Round Contract that caused the stall: " +
|
|
207
|
+
"close it in this submission with `outcome=\"blocked\"` (+ a blocker " +
|
|
208
|
+
"explaining why it stalled) and declare the REVISED contract in the " +
|
|
209
|
+
"same submission. Do **NOT** silently restate the stalled contract.", "- Use the `prompt_requests.confusion_points` field in your self-evaluation " +
|
|
259
210
|
"to flag anything you don't understand about the task or the blockage.", "");
|
|
260
211
|
// ── Preserved Discoveries ─────────────────────────────────────────────
|
|
261
212
|
if (skippedDiscoveries.length > 0) {
|
|
@@ -333,53 +284,34 @@ function enforceEvidenceContradiction(flags) {
|
|
|
333
284
|
check: contradiction.check,
|
|
334
285
|
});
|
|
335
286
|
}
|
|
336
|
-
/**
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const ev = selfEval.execution_evidence;
|
|
346
|
-
// v1.17: Missing evidence when claiming success → reject.
|
|
347
|
-
// The agent MUST provide execution_evidence to back up a success claim.
|
|
348
|
-
if (!ev) {
|
|
349
|
-
return makeEnforcementResult({
|
|
350
|
-
action: "reject",
|
|
351
|
-
reason: "Agent claims success but provided no execution_evidence. " +
|
|
352
|
-
"Every successful round MUST include execution_evidence with " +
|
|
353
|
-
"files_changed, test_results, and progress_estimate.",
|
|
354
|
-
fix_instructions: "You must provide execution_evidence in your self-evaluation: " +
|
|
355
|
-
"(a) list the files you changed in execution_evidence.files_changed, " +
|
|
356
|
-
"(b) run tests and report results in execution_evidence.test_results, " +
|
|
357
|
-
"(c) estimate your progress in execution_evidence.progress_estimate. " +
|
|
358
|
-
"If you genuinely completed the task without file changes or tests, " +
|
|
359
|
-
"explain why in detail in your output_summary.",
|
|
360
|
-
check: "empty_success",
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
const filesEmpty = ev.files_changed.length === 0;
|
|
364
|
-
const testsNotRun = ev.test_results === null;
|
|
365
|
-
if (!filesEmpty || !testsNotRun)
|
|
287
|
+
/** v3.3: R-EVID-VERIFY — the verification command's entrypoint changed in
|
|
288
|
+
* the same round the command ran. The runtime executed a script the agent
|
|
289
|
+
* just rewrote, so the "machine observation" has no stable baseline and
|
|
290
|
+
* cannot back the success claim. Reject so the agent resubmits the SAME
|
|
291
|
+
* round with the entrypoint unchanged (a freshly created entrypoint passes
|
|
292
|
+
* on the redo — the diff no longer contains it). */
|
|
293
|
+
function enforceVerificationEntrypointTampered(flags) {
|
|
294
|
+
const flag = flags.find((f) => f.check === CHECK_VERIFICATION_ENTRYPOINT_MODIFIED && f.severity === "error");
|
|
295
|
+
if (!flag)
|
|
366
296
|
return null;
|
|
367
297
|
return makeEnforcementResult({
|
|
368
298
|
action: "reject",
|
|
369
|
-
reason:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
"(
|
|
373
|
-
"
|
|
374
|
-
"
|
|
375
|
-
"
|
|
376
|
-
check: "
|
|
299
|
+
reason: flag.detail,
|
|
300
|
+
fix_instructions: "The verification command entrypoint changed in this round, so its " +
|
|
301
|
+
"result cannot be trusted as machine evidence. You must: (a) keep the " +
|
|
302
|
+
"verification command (and its entrypoint files) stable, (b) resubmit " +
|
|
303
|
+
"your SelfEvaluation for the SAME round with the entrypoint unchanged " +
|
|
304
|
+
"so the runtime can re-run it against a stable baseline, and (c) do " +
|
|
305
|
+
"NOT rewrite the verification script to make it pass.",
|
|
306
|
+
check: "verification_entrypoint_modified",
|
|
377
307
|
});
|
|
378
308
|
}
|
|
379
|
-
/** R8 (v2.12):
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
309
|
+
/** R8 (v2.12/v3.7): The single success-evidence row. The verification gate
|
|
310
|
+
* produced success_without_verified_evidence (error) — either the empty/
|
|
311
|
+
* missing-evidence arm (v3.7: the ex-R3 empty_success posture moved into
|
|
312
|
+
* checkSuccessWithoutVerifiedEvidence) or the claims arm (zero verified
|
|
313
|
+
* claims). First occurrence → reject with concrete evidence requirements;
|
|
314
|
+
* second consecutive → terminate. */
|
|
383
315
|
function enforceSuccessWithoutVerifiedEvidence(flags, consecutiveRejections) {
|
|
384
316
|
const flag = flags.find((f) => f.check === CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE && f.severity === "error");
|
|
385
317
|
if (!flag)
|
|
@@ -407,6 +339,131 @@ function enforceSuccessWithoutVerifiedEvidence(flags, consecutiveRejections) {
|
|
|
407
339
|
check: "success_without_verified_evidence",
|
|
408
340
|
});
|
|
409
341
|
}
|
|
342
|
+
/** R-C1 (v3.3): Round Contract boundary claimed prematurely. The gate
|
|
343
|
+
* produced premature_boundary (error): success claimed while done_when
|
|
344
|
+
* items were claimed met without machine-verified evidence or silently
|
|
345
|
+
* dropped. First occurrence → reject with contract-specific fix
|
|
346
|
+
* instructions; second consecutive → terminate (R8 ladder). */
|
|
347
|
+
function enforcePrematureBoundary(flags, consecutiveRejections) {
|
|
348
|
+
const flag = flags.find((f) => f.check === CHECK_PREMATURE_BOUNDARY && f.severity === "error");
|
|
349
|
+
if (!flag)
|
|
350
|
+
return null;
|
|
351
|
+
if (consecutiveRejections >= 2) {
|
|
352
|
+
return makeEnforcementResult({
|
|
353
|
+
action: "terminate",
|
|
354
|
+
reason: "Repeated premature Round Contract boundary claims — done_when items " +
|
|
355
|
+
"claimed without machine evidence or dropped while success is claimed.",
|
|
356
|
+
check: "premature_boundary",
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const escalation = getPolicy().engine.enforcement_escalation_enabled &&
|
|
360
|
+
consecutiveRejections >= 1
|
|
361
|
+
? buildEscalationNotice()
|
|
362
|
+
: "";
|
|
363
|
+
return makeEnforcementResult({
|
|
364
|
+
action: "reject",
|
|
365
|
+
reason: flag.detail,
|
|
366
|
+
fix_instructions: "You claimed success under a Round Contract but done_when items are " +
|
|
367
|
+
"unfinished or unverified. You must: (a) run the contract's " +
|
|
368
|
+
"verification_plan commands (configured in loop_policy.json " +
|
|
369
|
+
"evidence.commands) and report their actual output, listing satisfied " +
|
|
370
|
+
"done_when items in success_criteria_met; or (b) set success=false and " +
|
|
371
|
+
"list what remains in success_criteria_remaining. " +
|
|
372
|
+
"Do NOT claim contract completion without machine evidence " +
|
|
373
|
+
"(no_change_reason does not apply to contract claims)." + escalation,
|
|
374
|
+
check: "premature_boundary",
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
/** v3.5: Round Contract completion claimed without its verification_plan
|
|
378
|
+
* commands passing this round. The gate produced
|
|
379
|
+
* contract_completion_unverified (error): the eval's met claims satisfy
|
|
380
|
+
* every done_when of the ACTIVE contract, but the machine never observed
|
|
381
|
+
* the plan's commands pass. Closing a contract is a success-class claim —
|
|
382
|
+
* first occurrence → reject with fix instructions; second consecutive →
|
|
383
|
+
* terminate (R-C1's ladder). Registered BEFORE R-C1: the completion-truth
|
|
384
|
+
* question outranks boundary nuance. */
|
|
385
|
+
function enforceContractCompletionUnverified(flags, consecutiveRejections) {
|
|
386
|
+
const flag = flags.find((f) => f.check === CHECK_CONTRACT_COMPLETION_UNVERIFIED && f.severity === "error");
|
|
387
|
+
if (!flag)
|
|
388
|
+
return null;
|
|
389
|
+
if (consecutiveRejections >= 2) {
|
|
390
|
+
return makeEnforcementResult({
|
|
391
|
+
action: "terminate",
|
|
392
|
+
reason: "Repeated unverified Round Contract completion claims — done_when items " +
|
|
393
|
+
"claimed complete while the verification_plan commands never passed.",
|
|
394
|
+
check: "contract_completion_unverified",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const escalation = getPolicy().engine.enforcement_escalation_enabled &&
|
|
398
|
+
consecutiveRejections >= 1
|
|
399
|
+
? buildEscalationNotice()
|
|
400
|
+
: "";
|
|
401
|
+
return makeEnforcementResult({
|
|
402
|
+
action: "reject",
|
|
403
|
+
reason: flag.detail,
|
|
404
|
+
fix_instructions: "You claimed completion under the ACTIVE Round Contract, but its " +
|
|
405
|
+
"verification_plan commands did not pass this round. You must: (a) fix " +
|
|
406
|
+
"the underlying failure, re-run the contract's verification_plan " +
|
|
407
|
+
"commands (configured in loop_policy.json evidence.commands), and list " +
|
|
408
|
+
"satisfied done_when items in success_criteria_met; or (b) set " +
|
|
409
|
+
"success=false and list what remains in success_criteria_remaining. " +
|
|
410
|
+
"Completion claims are never accepted without machine verification — " +
|
|
411
|
+
"do NOT use no_change_reason for completion claims." + escalation,
|
|
412
|
+
check: "contract_completion_unverified",
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/** R-C2 (v3.3): Round Contract scope drift. The gate produced
|
|
416
|
+
* round_scope_drift (warn): files changed outside the contract's declared
|
|
417
|
+
* scope. A substantive drift_clarification (≥ 20 chars with real anchors —
|
|
418
|
+
* the out-of-scope file paths count) accepts the legitimately expanded
|
|
419
|
+
* scope; no/weak clarification rejects; repeated unclarified drift
|
|
420
|
+
* terminates. Mirrors R8's ladder, not R7's streak counter — no new
|
|
421
|
+
* session state. */
|
|
422
|
+
function enforceScopeDrift(flags, selfEval, consecutiveRejections, vaultEntries = []) {
|
|
423
|
+
const flag = flags.find((f) => f.check === CHECK_ROUND_SCOPE_DRIFT);
|
|
424
|
+
if (!flag)
|
|
425
|
+
return null;
|
|
426
|
+
const clarification = selfEval.drift_clarification?.trim();
|
|
427
|
+
const hasAnchor = clarification
|
|
428
|
+
? clarificationIsSubstantive(clarification, selfEval, vaultEntries)
|
|
429
|
+
: false;
|
|
430
|
+
const isSubstantive = clarification !== undefined
|
|
431
|
+
&& clarification.length >= 20
|
|
432
|
+
&& hasAnchor;
|
|
433
|
+
if (isSubstantive) {
|
|
434
|
+
// Genuine scope expansion with concrete file references → accept.
|
|
435
|
+
return makeEnforcementResult({
|
|
436
|
+
action: "accept",
|
|
437
|
+
reason: "",
|
|
438
|
+
fix_instructions: "",
|
|
439
|
+
check: "round_scope_drift",
|
|
440
|
+
clarification_accepted: true,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
if (consecutiveRejections >= 2) {
|
|
444
|
+
return makeEnforcementResult({
|
|
445
|
+
action: "terminate",
|
|
446
|
+
reason: `Repeated scope drift without substantive drift_clarification — ` +
|
|
447
|
+
`the agent keeps changing files outside its declared contract scope.`,
|
|
448
|
+
check: "round_scope_drift",
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const escalation = getPolicy().engine.enforcement_escalation_enabled &&
|
|
452
|
+
consecutiveRejections >= 1
|
|
453
|
+
? buildEscalationNotice()
|
|
454
|
+
: "";
|
|
455
|
+
return makeEnforcementResult({
|
|
456
|
+
action: "reject",
|
|
457
|
+
reason: flag.detail,
|
|
458
|
+
fix_instructions: "Files changed outside the contract's declared scope. You must either: " +
|
|
459
|
+
"(a) revert the out-of-scope changes, or (b) extend the scope in your " +
|
|
460
|
+
"re-declared round_contract AND explain the pivot in drift_clarification " +
|
|
461
|
+
"with concrete file paths (≥ 20 characters, real anchors). Weak or " +
|
|
462
|
+
"missing clarifications are rejected; repeated scope drift terminates " +
|
|
463
|
+
"the loop." + escalation,
|
|
464
|
+
check: "round_scope_drift",
|
|
465
|
+
});
|
|
466
|
+
}
|
|
410
467
|
/** R9 (v2.12): Post-backtrack workspace not restored. The verification gate
|
|
411
468
|
* produced backtrack_workspace_not_restored (error): either skipped-round
|
|
412
469
|
* files are still dirty, or the working tree is not at the restore point's
|
|
@@ -439,237 +496,132 @@ function enforceBacktrackNotRestored(flags, vaultEntries = [], currentRound = 0)
|
|
|
439
496
|
check: "backtrack_workspace_not_restored",
|
|
440
497
|
});
|
|
441
498
|
}
|
|
442
|
-
/**
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
499
|
+
/** Shared observation pipeline for the progress evaluator (stall window,
|
|
500
|
+
* flatline tier). Machine git motion is exculpatory only: it may veto a
|
|
501
|
+
* self-reported stall but can never create one. */
|
|
502
|
+
function evaluateStallWindow(tier, selfEval, currentRound, vaultEntries) {
|
|
503
|
+
const window = tier === "progress_stall"
|
|
504
|
+
? 3
|
|
505
|
+
: Math.max(1, getPolicy().engine.stall_lookback_rounds);
|
|
506
|
+
if (currentRound < window)
|
|
449
507
|
return null;
|
|
450
|
-
|
|
451
|
-
let
|
|
452
|
-
let
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
508
|
+
const progressByRound = progressWindow(vaultEntries, currentRound, selfEval);
|
|
509
|
+
let stalled = false;
|
|
510
|
+
let detail = "";
|
|
511
|
+
const threshold = getPolicy().evolution.progress_stall_threshold;
|
|
512
|
+
if (progressByRound.size >= window) {
|
|
513
|
+
const recent = [...progressByRound.keys()].sort((a, b) => a - b).slice(-window);
|
|
514
|
+
if (recent[0] < currentRound - window)
|
|
515
|
+
return null;
|
|
516
|
+
const values = recent.map((round) => progressByRound.get(round));
|
|
517
|
+
if (values[values.length - 1] >= 0.95)
|
|
459
518
|
return null;
|
|
460
|
-
|
|
461
|
-
|
|
519
|
+
if (tier === "progress_stall") {
|
|
520
|
+
stalled = values.slice(1).every((value, index) => value - values[index] < threshold);
|
|
521
|
+
detail = values.map((value) => `${(value * 100).toFixed(0)}%`).join(" → ");
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
const first = values[0];
|
|
525
|
+
stalled = values.every((value) => Math.abs(value - first) < 1e-10);
|
|
526
|
+
detail = `completely flat at ${(first * 100).toFixed(0)}%`;
|
|
527
|
+
}
|
|
462
528
|
}
|
|
463
529
|
else {
|
|
464
|
-
const
|
|
465
|
-
if (
|
|
466
|
-
return null;
|
|
467
|
-
const sortedRounds = [...progressByRound.keys()].sort((a, b) => a - b);
|
|
468
|
-
const last3 = sortedRounds.slice(-3);
|
|
469
|
-
// Require all three data points to be within [currentRound-3, currentRound-1].
|
|
470
|
-
// (collectProgressByRound already filters rnd < currentRound, so the upper
|
|
471
|
-
// bound is always satisfied — the lower bound is the real continuity guard.)
|
|
472
|
-
const expectedMin = currentRound - 3;
|
|
473
|
-
if (last3[0] < expectedMin)
|
|
530
|
+
const machine = machineProgressSeries(vaultEntries, currentRound, window);
|
|
531
|
+
if (machine === null)
|
|
474
532
|
return null;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const delta12 = p2 - p1;
|
|
480
|
-
const delta23 = p3 - p2;
|
|
481
|
-
// Both deltas must be below threshold AND not near completion
|
|
482
|
-
isStalling = delta12 < stallThreshold && delta23 < stallThreshold && p3 < 0.95;
|
|
483
|
-
stallDetail = `${(p1 * 100).toFixed(0)}% → ${(p2 * 100).toFixed(0)}% → ${(p3 * 100).toFixed(0)}%`;
|
|
533
|
+
stalled = machine.every((value) => !value);
|
|
534
|
+
detail = tier === "progress_stall"
|
|
535
|
+
? `no machine-observed changes in the last ${window} rounds`
|
|
536
|
+
: `no machine-observed changes for ${window} consecutive rounds`;
|
|
484
537
|
}
|
|
485
|
-
if (!
|
|
538
|
+
if (!stalled)
|
|
486
539
|
return null;
|
|
540
|
+
const machine = machineProgressSeries(vaultEntries, currentRound, window);
|
|
541
|
+
if (machine?.some(Boolean))
|
|
542
|
+
return null;
|
|
543
|
+
if (machine !== null) {
|
|
544
|
+
detail += ` — no machine-observed git motion in rounds ${currentRound - window}–${currentRound - 1}`;
|
|
545
|
+
}
|
|
546
|
+
return { tier, detail, window, threshold };
|
|
547
|
+
}
|
|
548
|
+
/** Shared progress escalation and backtrack deadlock guard (v3.7: one
|
|
549
|
+
* evaluator). The flatline tier keeps its steeper threshold-0 ladder while
|
|
550
|
+
* using the same mechanics — the tier is an internal diagnostic only. */
|
|
551
|
+
function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiveRejections) {
|
|
552
|
+
const { tier, detail, window, threshold } = verdict;
|
|
553
|
+
const flatline = tier === "progress_flatline";
|
|
487
554
|
const escalation = getPolicy().engine.enforcement_escalation_enabled;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
555
|
+
const stalledReason = flatline
|
|
556
|
+
? `Progress has been ${detail} for ${window} consecutive rounds.`
|
|
557
|
+
: `Progress has stalled: ${detail} over the last ${window} rounds` +
|
|
558
|
+
`${threshold > 0 ? ` (delta < ${(threshold * 100).toFixed(0)}% each round)` : ""}.`;
|
|
559
|
+
// v3.7: both tiers emit the single progress_stall check id — the tier
|
|
560
|
+
// survives only in the reason/fix wording (former progress_flatline id).
|
|
561
|
+
const check = "progress_stall";
|
|
562
|
+
if ((flatline && consecutiveRejections >= 1) || consecutiveRejections >= 2) {
|
|
492
563
|
return makeEnforcementResult({
|
|
493
564
|
action: "terminate",
|
|
494
|
-
reason:
|
|
495
|
-
|
|
496
|
-
`and agent did not resolve after escalation. Terminating loop.`,
|
|
497
|
-
check: "progress_stall",
|
|
565
|
+
reason: `${stalledReason} The stall persisted after escalation. Terminating loop.`,
|
|
566
|
+
check,
|
|
498
567
|
});
|
|
499
568
|
}
|
|
500
|
-
if (consecutiveRejections >= 1 && escalation) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
return makeEnforcementResult({
|
|
511
|
-
action: "terminate",
|
|
512
|
-
reason: `Progress has stalled over the last 3 rounds ` +
|
|
513
|
-
`(${stallDetail}) and a backtrack already fired for ` +
|
|
514
|
-
`this round. The stall persists after rollback — terminating.`,
|
|
515
|
-
check: "progress_stall",
|
|
516
|
-
});
|
|
517
|
-
}
|
|
569
|
+
if (consecutiveRejections >= 1 && !escalation) {
|
|
570
|
+
return makeEnforcementResult({
|
|
571
|
+
action: "terminate",
|
|
572
|
+
reason: `${stalledReason} The stall persisted after the previous rejection. Terminating loop.`,
|
|
573
|
+
check,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const escalated = flatline ? escalation : consecutiveRejections >= 1 && escalation;
|
|
577
|
+
if (escalated && getPolicy().engine.backtrack_enabled) {
|
|
578
|
+
if (hasCommittedBacktrack(vaultEntries, currentRound)) {
|
|
518
579
|
return makeEnforcementResult({
|
|
519
|
-
action: "
|
|
520
|
-
reason:
|
|
521
|
-
|
|
522
|
-
fix_instructions: "Your progress has been flat for 3 rounds. You are being rolled back " +
|
|
523
|
-
"to the last clean round. Do NOT repeat the approach that led to the stall. " +
|
|
524
|
-
"Choose a different technique or task decomposition.",
|
|
525
|
-
check: "progress_stall",
|
|
580
|
+
action: "terminate",
|
|
581
|
+
reason: `${stalledReason} A backtrack already fired for this round; the stall persists after rollback.`,
|
|
582
|
+
check,
|
|
526
583
|
});
|
|
527
584
|
}
|
|
528
585
|
return makeEnforcementResult({
|
|
529
|
-
action: "
|
|
530
|
-
reason:
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
"
|
|
534
|
-
|
|
535
|
-
"(c) set a concrete, verifiable goal for the redo of this round. " +
|
|
536
|
-
"Do NOT repeat the same approach — it has not moved progress forward." +
|
|
537
|
-
buildEscalationNotice(),
|
|
538
|
-
check: "progress_stall",
|
|
586
|
+
action: "backtrack",
|
|
587
|
+
reason: stalledReason,
|
|
588
|
+
fix_instructions: flatline
|
|
589
|
+
? "Progress is exactly flat. After rollback, choose a radically different approach."
|
|
590
|
+
: "After rollback, do not repeat the stalled approach; choose a different technique or task decomposition.",
|
|
591
|
+
check,
|
|
539
592
|
});
|
|
540
593
|
}
|
|
541
|
-
if (
|
|
594
|
+
if (flatline && !escalation) {
|
|
542
595
|
return makeEnforcementResult({
|
|
543
596
|
action: "terminate",
|
|
544
|
-
reason:
|
|
545
|
-
|
|
546
|
-
`and agent did not resolve after previous rejection. Terminating loop.`,
|
|
547
|
-
check: "progress_stall",
|
|
597
|
+
reason: `${stalledReason} The agent is making zero forward motion and cannot recover.`,
|
|
598
|
+
check,
|
|
548
599
|
});
|
|
549
600
|
}
|
|
550
601
|
return makeEnforcementResult({
|
|
551
602
|
action: "reject",
|
|
552
|
-
reason:
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
"
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
"Do NOT repeat the same approach — it has not moved progress forward.",
|
|
559
|
-
check: "progress_stall",
|
|
603
|
+
reason: stalledReason,
|
|
604
|
+
fix_instructions: (flatline
|
|
605
|
+
? "Explain the fundamental blocker, choose a radically different approach, and set a concrete verifiable goal."
|
|
606
|
+
: "Explain the blocker, choose a different technique or task decomposition, and set a concrete verifiable goal for this retry.") +
|
|
607
|
+
(escalated ? buildEscalationNotice() : ""),
|
|
608
|
+
check,
|
|
560
609
|
});
|
|
561
610
|
}
|
|
562
|
-
/**
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
let allFlat = false;
|
|
577
|
-
let flatDetail = "";
|
|
578
|
-
if (skipEvidenceRules) {
|
|
579
|
-
// v3.2: heuristic path has no execution_evidence — fall back to machine
|
|
580
|
-
// observations: N consecutive rounds with no git change = flatline.
|
|
581
|
-
// No git signal → keep the legacy skip (the machine cannot observe).
|
|
582
|
-
const machine = machineProgressSeries(vaultEntries, currentRound, breakerSize);
|
|
583
|
-
if (machine === null)
|
|
584
|
-
return null;
|
|
585
|
-
allFlat = machine.every((value) => !value);
|
|
586
|
-
flatDetail = `no machine-observed changes for ${breakerSize} consecutive rounds`;
|
|
587
|
-
}
|
|
588
|
-
else {
|
|
589
|
-
const progressByRound = progressWindow(vaultEntries, currentRound, selfEval);
|
|
590
|
-
if (progressByRound.size < breakerSize)
|
|
591
|
-
return null;
|
|
592
|
-
const sortedRounds = [...progressByRound.keys()].sort((a, b) => a - b);
|
|
593
|
-
const recent = sortedRounds.slice(-breakerSize);
|
|
594
|
-
// v2.14: continuity guard (mirrors R4) — "consecutive" means the most
|
|
595
|
-
// recent rounds. Rounds without execution_evidence are unknown motion,
|
|
596
|
-
// not zero motion; taking "the last N rounds that happened to report
|
|
597
|
-
// progress" terminated loops whose recent rounds simply had no data.
|
|
598
|
-
if (recent[0] < currentRound - breakerSize)
|
|
599
|
-
return null;
|
|
600
|
-
// Check that progress values are ALL identical (delta = 0).
|
|
601
|
-
// Use epsilon comparison — JSON round-trips can produce float drift
|
|
602
|
-
// (e.g. 0.3 vs 0.30000000000000004) that would defeat strict === .
|
|
603
|
-
const EPSILON = 1e-10;
|
|
604
|
-
const first = progressByRound.get(recent[0]);
|
|
605
|
-
allFlat = recent.every((r) => Math.abs((progressByRound.get(r) ?? 0) - first) < EPSILON);
|
|
606
|
-
if (!allFlat || first >= 0.95)
|
|
607
|
-
return null;
|
|
608
|
-
flatDetail = `completely flat at ${(first * 100).toFixed(0)}%`;
|
|
609
|
-
}
|
|
610
|
-
if (!allFlat)
|
|
611
|
-
return null;
|
|
612
|
-
const escalation = getPolicy().engine.enforcement_escalation_enabled;
|
|
613
|
-
// v2.7: When escalation is enabled, give one rejection round with a
|
|
614
|
-
// "Seek Human Guidance" notice before terminating.
|
|
615
|
-
if (consecutiveRejections >= 1) {
|
|
616
|
-
return makeEnforcementResult({
|
|
617
|
-
action: "terminate",
|
|
618
|
-
reason: `Progress has been ${flatDetail} ` +
|
|
619
|
-
`for ${breakerSize} consecutive rounds and agent did not resolve ` +
|
|
620
|
-
`after escalation. The agent is making zero forward motion.`,
|
|
621
|
-
check: "progress_stall_terminal",
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
if (escalation) {
|
|
625
|
-
// v2.10: When backtrack is enabled, escalate to backtrack instead of
|
|
626
|
-
// reject. The agent is rolled back to the last clean round with a
|
|
627
|
-
// "radically different strategy" lesson injected.
|
|
628
|
-
if (getPolicy().engine.backtrack_enabled) {
|
|
629
|
-
// v2.14: Same deadlock guard as R4 — a backtrack that already fired
|
|
630
|
-
// for this round means the loop is re-walking rolled-back territory.
|
|
631
|
-
if (hasCommittedBacktrack(vaultEntries, currentRound)) {
|
|
632
|
-
return makeEnforcementResult({
|
|
633
|
-
action: "terminate",
|
|
634
|
-
reason: `Progress has been ${flatDetail} ` +
|
|
635
|
-
`for ${breakerSize} consecutive rounds and a backtrack already ` +
|
|
636
|
-
`fired for this round. Flatline persists after rollback — ` +
|
|
637
|
-
`terminating the loop.`,
|
|
638
|
-
check: "progress_stall_terminal",
|
|
639
|
-
});
|
|
640
|
-
}
|
|
641
|
-
return makeEnforcementResult({
|
|
642
|
-
action: "backtrack",
|
|
643
|
-
reason: `Progress has been ${flatDetail} ` +
|
|
644
|
-
`for ${breakerSize} consecutive rounds. The agent is making zero ` +
|
|
645
|
-
`forward motion.`,
|
|
646
|
-
fix_instructions: "Your progress has been exactly flat — zero forward motion. " +
|
|
647
|
-
"You are being rolled back to the last clean round. " +
|
|
648
|
-
"Choose a RADICALLY different approach. The previous one produced nothing.",
|
|
649
|
-
check: "progress_stall_terminal",
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
return makeEnforcementResult({
|
|
653
|
-
action: "reject",
|
|
654
|
-
reason: `Progress has been ${flatDetail} ` +
|
|
655
|
-
`for ${breakerSize} consecutive rounds. The agent is making zero ` +
|
|
656
|
-
`forward motion.`,
|
|
657
|
-
fix_instructions: "Your progress has been exactly flat — you are making zero forward " +
|
|
658
|
-
"motion. You must: (a) explain what is fundamentally blocking you, " +
|
|
659
|
-
"(b) propose a radically different approach, and " +
|
|
660
|
-
"(c) set a concrete verifiable goal. " +
|
|
661
|
-
"Do NOT resubmit the same output." +
|
|
662
|
-
buildEscalationNotice(),
|
|
663
|
-
check: "progress_stall_terminal",
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
return makeEnforcementResult({
|
|
667
|
-
action: "terminate",
|
|
668
|
-
reason: `Progress has been ${flatDetail} ` +
|
|
669
|
-
`for ${breakerSize} consecutive rounds. The agent is making zero ` +
|
|
670
|
-
`forward motion and cannot recover.`,
|
|
671
|
-
check: "progress_stall_terminal",
|
|
672
|
-
});
|
|
611
|
+
/** v3.7: The single progress evaluator (former R4 stall and R5 flatline rows
|
|
612
|
+
* merged into one). The stall-tier window is consulted first — under the
|
|
613
|
+
* default positive threshold its predicate covers exactly-flat runs too,
|
|
614
|
+
* reproducing the v3.6 R4-before-R5 shadowing; the flatline tier is
|
|
615
|
+
* consulted only when the stall predicate cannot hold (threshold ≤ 0,
|
|
616
|
+
* shorter lookback configs), where it applies its steeper disposition
|
|
617
|
+
* ladder. Machine git motion is exculpatory only — it may veto a
|
|
618
|
+
* self-reported stall but can never create one. */
|
|
619
|
+
function enforceProgressStall(selfEval, _flags, currentRound, vaultEntries, consecutiveRejections) {
|
|
620
|
+
const verdict = evaluateStallWindow("progress_stall", selfEval, currentRound, vaultEntries)
|
|
621
|
+
?? evaluateStallWindow("progress_flatline", selfEval, currentRound, vaultEntries);
|
|
622
|
+
return verdict
|
|
623
|
+
? resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiveRejections)
|
|
624
|
+
: null;
|
|
673
625
|
}
|
|
674
626
|
/** R6: Two consecutive rejections → escalate (v2.7) or terminate.
|
|
675
627
|
*
|
|
@@ -725,8 +677,8 @@ function extractAnchors(clarification) {
|
|
|
725
677
|
for (const match of clarification.matchAll(/[c]r?-[a-f0-9]{8}|sg-[a-f0-9]{8}/gi)) {
|
|
726
678
|
anchors.add(match[0].toLowerCase());
|
|
727
679
|
}
|
|
728
|
-
for (const
|
|
729
|
-
anchors.add(
|
|
680
|
+
for (const path of extractFilePathTokens(clarification)) {
|
|
681
|
+
anchors.add(path);
|
|
730
682
|
}
|
|
731
683
|
return [...anchors];
|
|
732
684
|
}
|
|
@@ -969,13 +921,38 @@ function buildDiagnosticGap(flags) {
|
|
|
969
921
|
lines.push("");
|
|
970
922
|
return lines.join("\n");
|
|
971
923
|
}
|
|
924
|
+
const RULE_TABLE = [
|
|
925
|
+
// R1/R2 join the contradiction umbrella per the documented four-class model
|
|
926
|
+
// (AGENTS/README/CHANGELOG): a row's category is its enforcement action
|
|
927
|
+
// class, NOT the trigger check's gate domain — claim-vs-self-reported-facts
|
|
928
|
+
// rows (outcome_success_contradiction included) belong to this class too.
|
|
929
|
+
{ category: "evidence_contradiction", checks: ["success_with_remaining_criteria"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
930
|
+
{ category: "evidence_contradiction", checks: ["recurring_violation"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
931
|
+
{ category: "evidence_contradiction", checks: ["required_command_failed", "command_evidence_mismatch", "outcome_success_contradiction"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
932
|
+
{ category: "evidence_contradiction", checks: ["verification_entrypoint_modified"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
933
|
+
{ category: "contract_scope", checks: ["contract_completion_unverified"], ladder: "internal" },
|
|
934
|
+
{ category: "contract_scope", checks: ["premature_boundary"], ladder: "internal" },
|
|
935
|
+
{ category: "evidence_contradiction", checks: ["success_without_verified_evidence"], ladder: "internal" },
|
|
936
|
+
{ category: "contract_scope", checks: ["round_scope_drift"], ladder: "internal", clarificationAllowed: true },
|
|
937
|
+
{ category: "progress_recovery", checks: ["progress_stall"], ladder: "internal", backtrackAllowed: true },
|
|
938
|
+
{ category: "progress_recovery", checks: ["max_rejections"], ladder: "counter" },
|
|
939
|
+
{ category: "plan_drift", checks: ["intent_drift"], ladder: "internal", clarificationAllowed: true },
|
|
940
|
+
{ category: "progress_recovery", checks: ["backtrack_workspace_not_restored"], ladder: "internal", backtrackAllowed: true },
|
|
941
|
+
];
|
|
942
|
+
const RULE_TABLE_BY_ID = new Map();
|
|
943
|
+
for (const row of RULE_TABLE) {
|
|
944
|
+
for (const id of row.checks)
|
|
945
|
+
RULE_TABLE_BY_ID.set(id, row);
|
|
946
|
+
}
|
|
972
947
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
973
948
|
// Main entry point
|
|
974
949
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
975
950
|
/** Enforce round-boundary rules based on the verification gate's findings
|
|
976
951
|
* and the agent's self-evaluation integrity.
|
|
977
952
|
*
|
|
978
|
-
* Rules run in priority order
|
|
953
|
+
* Rules run in priority order — the rules array below IS the priority
|
|
954
|
+
* order; numeric IDs (R1–R9, R-EVID) reflect insertion history, not
|
|
955
|
+
* priority. The first rule that fires wins.
|
|
979
956
|
*
|
|
980
957
|
* @param selfEval The agent's self-evaluation for the current round.
|
|
981
958
|
* @param verifyResult The verification gate's output (from verifySelfEvaluation).
|
|
@@ -984,32 +961,83 @@ function buildDiagnosticGap(flags) {
|
|
|
984
961
|
* @param consecutiveRejections How many consecutive rounds have already been rejected.
|
|
985
962
|
* Starts at 0; increments on each reject; resets on accept.
|
|
986
963
|
*/
|
|
987
|
-
export function enforceRound(selfEval, verifyResult, currentRound, vaultEntries, consecutiveRejections = 0,
|
|
964
|
+
export function enforceRound(selfEval, verifyResult, currentRound, vaultEntries, consecutiveRejections = 0,
|
|
988
965
|
/** v2.12: Current clarification streak for R7 escalation. */
|
|
989
966
|
driftClarificationStreak = 0) {
|
|
990
967
|
const { flags } = verifyResult;
|
|
991
|
-
// Run enforcement rules in priority order
|
|
992
|
-
//
|
|
968
|
+
// Run enforcement rules in priority order — the array order IS the
|
|
969
|
+
// priority; the R-labels in the comments are historical (RULE_TABLE is
|
|
970
|
+
// the semantic description). The first rule that fires wins.
|
|
993
971
|
const rules = [
|
|
994
972
|
() => enforceSuccessWithRemainingCriteria(flags),
|
|
995
973
|
() => enforceRecurringViolation(flags),
|
|
996
|
-
|
|
997
|
-
//
|
|
998
|
-
// outcome claim contradicts the success claim before commit
|
|
974
|
+
// v2.14: R-EVID — a required command failure, hidden test failures, or
|
|
975
|
+
// a self-contradictory outcome claim contradicts the success claim
|
|
999
976
|
() => enforceEvidenceContradiction(flags),
|
|
1000
|
-
//
|
|
977
|
+
// v3.3: R-EVID-VERIFY — the verification command entrypoint changed in
|
|
978
|
+
// the same round it ran. Runs before R8: when the entrypoint is tainted
|
|
979
|
+
// R8 also fires, but this reason is more specific.
|
|
980
|
+
() => enforceVerificationEntrypointTampered(flags),
|
|
981
|
+
// v3.5: contract completion claimed without its verification_plan
|
|
982
|
+
// commands passing — runs before R-C1: the completion-truth question
|
|
983
|
+
// outranks boundary nuance.
|
|
984
|
+
() => enforceContractCompletionUnverified(flags, consecutiveRejections),
|
|
985
|
+
// v3.3: R-C1 — contract boundary claimed prematurely (contract-specific
|
|
986
|
+
// wording and check counting; runs before R8 so contract rounds get the
|
|
987
|
+
// contract's own reason.)
|
|
988
|
+
() => enforcePrematureBoundary(flags, consecutiveRejections),
|
|
989
|
+
// v2.12/v3.7: R8 — the single success-evidence row (empty/missing-evidence
|
|
990
|
+
// arm merged from the former R3 empty_success posture + claims arm)
|
|
1001
991
|
() => enforceSuccessWithoutVerifiedEvidence(flags, consecutiveRejections),
|
|
1002
|
-
|
|
1003
|
-
() =>
|
|
992
|
+
// v3.3: R-C2 — files changed outside the contract's declared scope
|
|
993
|
+
() => enforceScopeDrift(flags, selfEval, consecutiveRejections, vaultEntries),
|
|
994
|
+
// v3.7: single progress evaluator (former R4/R5 slots merged) —
|
|
995
|
+
// reject → backtrack → terminate with the deadlock guard
|
|
996
|
+
() => enforceProgressStall(selfEval, flags, currentRound, vaultEntries, consecutiveRejections),
|
|
997
|
+
// R6: rejection-counter catch-all (evaluated only when no row above
|
|
998
|
+
// fired — a clean-looking round carrying a high persisted counter)
|
|
1004
999
|
() => enforceMaxRejections(consecutiveRejections),
|
|
1000
|
+
// v2.12: R7 — intent drift; its weak-clarification streak is a separate
|
|
1001
|
+
// counter and never mixes with the global rejection count
|
|
1005
1002
|
() => enforceIntentDrift(flags, selfEval, consecutiveRejections, driftClarificationStreak, vaultEntries),
|
|
1006
1003
|
// v2.12: R9 — post-backtrack workspace not restored
|
|
1007
1004
|
() => enforceBacktrackNotRestored(flags, vaultEntries, currentRound),
|
|
1008
1005
|
];
|
|
1006
|
+
// v3.7: uniform-ladder escalation. Rows without their own escalation
|
|
1007
|
+
// ladder (R1/R2/R-EVID/R-EVID-VERIFY) escalate on the session's
|
|
1008
|
+
// consecutive-rejection count: repeat rejections carry the escalation
|
|
1009
|
+
// notice (when enabled — the pre-v3.7 comment promised this notice but the
|
|
1010
|
+
// UNLADDERED name-set patch never appended it), and a third consecutive
|
|
1011
|
+
// occurrence terminates. This replaces the UNLADDERED_REJECT_CHECKS patch,
|
|
1012
|
+
// which existed because the generic R6 ladder sits AFTER these rows and a
|
|
1013
|
+
// persistent offender's round never reached it. Rows with their own ladder
|
|
1014
|
+
// (RULE_TABLE.ladder === "internal" / "counter") are untouched.
|
|
1009
1015
|
for (const rule of rules) {
|
|
1010
|
-
|
|
1011
|
-
if (result)
|
|
1012
|
-
|
|
1016
|
+
let result = rule();
|
|
1017
|
+
if (!result)
|
|
1018
|
+
continue;
|
|
1019
|
+
const row = result.check !== undefined
|
|
1020
|
+
? RULE_TABLE_BY_ID.get(result.check)
|
|
1021
|
+
: undefined;
|
|
1022
|
+
if (result.action === "reject" && row && row.ladder === "uniform") {
|
|
1023
|
+
if (consecutiveRejections >= (row.terminalAfter ?? 2)) {
|
|
1024
|
+
return makeEnforcementResult({
|
|
1025
|
+
action: "terminate",
|
|
1026
|
+
reason: `${consecutiveRejections + 1} consecutive rejections for the same ` +
|
|
1027
|
+
`issue (${result.check}) without resolution — the agent cannot ` +
|
|
1028
|
+
`correct it even after escalation.`,
|
|
1029
|
+
check: result.check,
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
if (consecutiveRejections >= 1 && row.noticeOnRepeat &&
|
|
1033
|
+
getPolicy().engine.enforcement_escalation_enabled) {
|
|
1034
|
+
result = makeEnforcementResult({
|
|
1035
|
+
...result,
|
|
1036
|
+
fix_instructions: result.fix_instructions + buildEscalationNotice(),
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return result;
|
|
1013
1041
|
}
|
|
1014
1042
|
// All rules passed — accept the round
|
|
1015
1043
|
return makeEnforcementResult({
|