loopforge 3.7.1 → 3.8.2
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 +84 -130
- package/dist/audit.d.ts +53 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +106 -22
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +52 -67
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +134 -147
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +103 -4
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +13 -0
- package/dist/cognitive-facts.d.ts.map +1 -1
- package/dist/cognitive-facts.js +28 -13
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/committed-round.d.ts +103 -21
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +188 -45
- package/dist/committed-round.js.map +1 -1
- package/dist/contract-items.d.ts +55 -0
- package/dist/contract-items.d.ts.map +1 -0
- package/dist/contract-items.js +180 -0
- package/dist/contract-items.js.map +1 -0
- package/dist/enforcement-gate.d.ts +20 -2
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +197 -386
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +53 -38
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts +4 -4
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +16 -14
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +85 -89
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +328 -184
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +76 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +127 -0
- package/dist/explain.js.map +1 -0
- package/dist/generate-schema.js +152 -16
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +40 -53
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +229 -553
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +0 -1
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +0 -4
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts.map +1 -1
- package/dist/loop-projection.js +3 -1
- package/dist/loop-projection.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +30 -21
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +47 -103
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +51 -7
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +39 -23
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +148 -163
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +89 -0
- package/dist/mcp/submission-boundary.d.ts.map +1 -0
- package/dist/mcp/submission-boundary.js +203 -0
- package/dist/mcp/submission-boundary.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +249 -128
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.js +2 -2
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +55 -93
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +149 -38
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +1 -32
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +282 -435
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +6 -7
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +365 -142
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +21 -66
- package/dist/protocol.js.map +1 -1
- package/dist/replay.js +2 -2
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +69 -57
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +104 -64
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +36 -16
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +166 -102
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +3 -5
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +0 -1
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +96 -0
- package/dist/round-facts.d.ts.map +1 -0
- package/dist/round-facts.js +216 -0
- package/dist/round-facts.js.map +1 -0
- package/dist/round-transaction.d.ts +19 -9
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +90 -22
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +87 -9
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +396 -27
- package/dist/self-eval.js.map +1 -1
- package/dist/subgoal-state.d.ts +77 -0
- package/dist/subgoal-state.d.ts.map +1 -0
- package/dist/subgoal-state.js +198 -0
- package/dist/subgoal-state.js.map +1 -0
- package/dist/token-utils.d.ts +52 -19
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +83 -72
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +14 -31
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +178 -496
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +17 -0
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +59 -83
- package/package.json +67 -67
- package/skills/perception/SKILL.md +218 -204
package/dist/enforcement-gate.js
CHANGED
|
@@ -17,11 +17,10 @@
|
|
|
17
17
|
* with stopReason "enforcement_terminated".
|
|
18
18
|
*/
|
|
19
19
|
import { makeEnforcementResult } from "./protocol.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,
|
|
21
|
-
import { deriveConstraintId, deriveCriterionId, deriveSubGoalId } from "./loop-compiler.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_ROUND_SCOPE_DRIFT, CHECK_CONTRACT_ITEMS_UNVERIFIED, CHECK_USER_GATE_UNRESOLVED } from "./verification-gate.js";
|
|
22
21
|
import { getPolicy } from "./policy.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
22
|
+
import { isRecord, entryRound } from "./token-utils.js";
|
|
23
|
+
import { derivationRounds, decodeCommittedRound, machineEvidenceForRound, } from "./committed-round.js";
|
|
25
24
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
26
25
|
// Helpers
|
|
27
26
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -29,8 +28,8 @@ import { committedRoundsFromEntries, decodeCommittedRound, machineEvidenceForRou
|
|
|
29
28
|
* Shared by the progress evaluator's stall and flatline tiers. */
|
|
30
29
|
function collectProgressByRound(vaultEntries, currentRound) {
|
|
31
30
|
const progressByRound = new Map();
|
|
32
|
-
for (const round of
|
|
33
|
-
const progress = round.
|
|
31
|
+
for (const round of derivationRounds(vaultEntries, currentRound)) {
|
|
32
|
+
const progress = round.executionReport?.progress_estimate;
|
|
34
33
|
if (typeof progress === "number")
|
|
35
34
|
progressByRound.set(round.round, progress);
|
|
36
35
|
}
|
|
@@ -46,7 +45,7 @@ function collectProgressByRound(vaultEntries, currentRound) {
|
|
|
46
45
|
function progressWindow(vaultEntries, currentRound, selfEval) {
|
|
47
46
|
const progressByRound = collectProgressByRound(vaultEntries, currentRound);
|
|
48
47
|
if (hasCommittedBacktrack(vaultEntries, currentRound)) {
|
|
49
|
-
const pe = selfEval.
|
|
48
|
+
const pe = selfEval.execution_report?.progress_estimate;
|
|
50
49
|
if (typeof pe === "number")
|
|
51
50
|
progressByRound.set(currentRound, pe);
|
|
52
51
|
}
|
|
@@ -89,7 +88,7 @@ function isCleanRound(round) {
|
|
|
89
88
|
* Collects discovered_constraints from skipped rounds along the way.
|
|
90
89
|
* Returns null when no clean round is found within maxDepth. */
|
|
91
90
|
export function findSafeRestorePoint(currentRound, vaultEntries, maxDepth) {
|
|
92
|
-
const completed =
|
|
91
|
+
const completed = derivationRounds(vaultEntries, currentRound)
|
|
93
92
|
.sort((a, b) => b.round - a.round);
|
|
94
93
|
const skippedDiscoveries = [];
|
|
95
94
|
for (let depth = 1; depth <= maxDepth; depth++) {
|
|
@@ -124,13 +123,13 @@ export function findSafeRestorePoint(currentRound, vaultEntries, maxDepth) {
|
|
|
124
123
|
* evidence as fallback). Returns null when the round has no git snapshot
|
|
125
124
|
* or no committed feedback entry. */
|
|
126
125
|
export function findBacktrackTargetGitHead(restoreRound, vaultEntries) {
|
|
127
|
-
const committed =
|
|
126
|
+
const committed = derivationRounds(vaultEntries)
|
|
128
127
|
.find((view) => view.round === restoreRound);
|
|
129
128
|
if (!committed)
|
|
130
129
|
return null;
|
|
131
130
|
const evidence = machineEvidenceForRound(committed);
|
|
132
131
|
for (const item of evidence) {
|
|
133
|
-
if (!isRecord(item) || item.
|
|
132
|
+
if (!isRecord(item) || item.providerId !== "git" || !isRecord(item.data))
|
|
134
133
|
continue;
|
|
135
134
|
const head = item.data.head;
|
|
136
135
|
if (typeof head === "string" && head.length > 0)
|
|
@@ -138,6 +137,44 @@ export function findBacktrackTargetGitHead(restoreRound, vaultEntries) {
|
|
|
138
137
|
}
|
|
139
138
|
return null;
|
|
140
139
|
}
|
|
140
|
+
/** v3.8.1: the ONE Recovery Brief renderer.
|
|
141
|
+
*
|
|
142
|
+
* The rollback prompt and the state file's Recent tier used to carry two
|
|
143
|
+
* hand-written copies of these lines — differing in punctuation
|
|
144
|
+
* ("the redo is round N" vs "redo round N") and in whether an approach
|
|
145
|
+
* overflow count was shown at all. Two renderers over one rollback meant the
|
|
146
|
+
* two surfaces could describe the same event differently while both claiming
|
|
147
|
+
* to be derived facts. The facts are supplied by the caller: the coordinator
|
|
148
|
+
* from its live walk, the compile path from `decodeBacktrackRecord` on the
|
|
149
|
+
* committed decision. */
|
|
150
|
+
export function recoveryBriefLines(input) {
|
|
151
|
+
const maxApproaches = input.maxApproaches ?? 6;
|
|
152
|
+
const maxAssumptions = input.maxAssumptions ?? 5;
|
|
153
|
+
const lines = [
|
|
154
|
+
`- **Trigger**: ${input.triggerRule || "unknown"}`,
|
|
155
|
+
`- **Restored to round**: ${input.target} (the redo is round ${input.target + 1})`,
|
|
156
|
+
];
|
|
157
|
+
if (input.recoveryRoundId) {
|
|
158
|
+
lines.push(`- **Recovery Round ID**: \`${input.recoveryRoundId}\``);
|
|
159
|
+
}
|
|
160
|
+
if (input.failedRounds.length > 0) {
|
|
161
|
+
lines.push(`- **Failed rounds**: ${input.failedRounds.join(", ")}`);
|
|
162
|
+
}
|
|
163
|
+
const shown = input.approaches.slice(0, maxApproaches);
|
|
164
|
+
for (const approach of shown) {
|
|
165
|
+
lines.push(` - Failed approach: ${approach}`);
|
|
166
|
+
}
|
|
167
|
+
if (input.approaches.length > shown.length) {
|
|
168
|
+
lines.push(` - … and ${input.approaches.length - shown.length} more failed approaches`);
|
|
169
|
+
}
|
|
170
|
+
if (input.wrongAssumptions.length > 0) {
|
|
171
|
+
lines.push("- **Falsified assumptions** (do not rebuild on these):");
|
|
172
|
+
for (const assumption of input.wrongAssumptions.slice(0, maxAssumptions)) {
|
|
173
|
+
lines.push(` - ${assumption}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return lines;
|
|
177
|
+
}
|
|
141
178
|
export function buildBacktrackPrompt(fromRound, toRound, triggerRule, skippedDiscoveries,
|
|
142
179
|
/** v2.13: Files changed in the skipped rounds (from evidence snapshots).
|
|
143
180
|
* Used to show the agent exactly what needs to be reverted. */
|
|
@@ -161,27 +198,14 @@ recovery) {
|
|
|
161
198
|
// why it was rolled back, what failed, and what the redo round is.
|
|
162
199
|
if (recovery) {
|
|
163
200
|
lines.push("### Recovery Brief", "");
|
|
164
|
-
lines.push(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for (const approach of shown) {
|
|
173
|
-
lines.push(` - Failed approach: ${approach}`);
|
|
174
|
-
}
|
|
175
|
-
if (recovery.approaches.length > shown.length) {
|
|
176
|
-
lines.push(` - … and ${recovery.approaches.length - shown.length} more failed approaches`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
if (recovery.wrongAssumptions.length > 0) {
|
|
180
|
-
lines.push("- **Falsified assumptions** (do not rebuild on these):");
|
|
181
|
-
for (const assumption of recovery.wrongAssumptions.slice(0, 5)) {
|
|
182
|
-
lines.push(` - ${assumption}`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
201
|
+
lines.push(...recoveryBriefLines({
|
|
202
|
+
target: toRound,
|
|
203
|
+
triggerRule,
|
|
204
|
+
failedRounds: recovery.failedRounds,
|
|
205
|
+
approaches: recovery.approaches,
|
|
206
|
+
wrongAssumptions: recovery.wrongAssumptions,
|
|
207
|
+
recoveryRoundId,
|
|
208
|
+
}));
|
|
185
209
|
lines.push("");
|
|
186
210
|
}
|
|
187
211
|
// ── Why This Happened ────────────────────────────────────────────────
|
|
@@ -210,18 +234,27 @@ recovery) {
|
|
|
210
234
|
}
|
|
211
235
|
lines.push("");
|
|
212
236
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
//
|
|
237
|
+
// v3.8: FACTS, not commands. LoopForge never mutates the working tree, and it
|
|
238
|
+
// does not prescribe how the agent restores it either — the restore is the
|
|
239
|
+
// AGENT's action, so the prompt states what must be true (the target HEAD and
|
|
240
|
+
// the files that must be reverted) and leaves the means entirely to the
|
|
241
|
+
// agent. No `git stash` / `git reset` / `git checkout` / `git clean` line
|
|
242
|
+
// belongs here.
|
|
218
243
|
if (gitHead) {
|
|
219
|
-
lines.push(
|
|
220
|
-
"
|
|
221
|
-
"
|
|
244
|
+
lines.push("**The workspace must be returned to the restore point before you " +
|
|
245
|
+
"continue:**", "", `- HEAD must be at \`${gitHead.slice(0, 12)}\``, `- Every file listed above must be back in its state at Round ${toRound}`, `- No leftover change from rounds ${toRound + 1}–${fromRound} may remain`, "", "Your next submission's git observation is compared against that HEAD, " +
|
|
246
|
+
"and a file whose fingerprint still matches the failed round proves the " +
|
|
247
|
+
"restore did not happen.", "");
|
|
222
248
|
}
|
|
223
|
-
|
|
224
|
-
|
|
249
|
+
else {
|
|
250
|
+
lines.push(`**The workspace must be returned to its state at Round ${toRound}** — ` +
|
|
251
|
+
"every file listed above reverted, with nothing left over from the " +
|
|
252
|
+
"failed rounds.", "");
|
|
253
|
+
}
|
|
254
|
+
lines.push("Restoring the workspace is **your** responsibility. LoopForge never " +
|
|
255
|
+
"modifies the working tree; it only records the restore requirement and " +
|
|
256
|
+
"checks the result.", "", `**If git is unavailable**, revert the files listed above by hand to ` +
|
|
257
|
+
`their state at Round ${toRound}. Re-read the state file below — ` +
|
|
225
258
|
`it reflects the correct state at Round ${toRound}.`, "", "> 🛡️ **Verification:** The next round's evidence check will verify that " +
|
|
226
259
|
"> your workspace no longer contains the failed changes. Submissions " +
|
|
227
260
|
"> with unrestored files will be **rejected**.", "");
|
|
@@ -256,7 +289,7 @@ recovery) {
|
|
|
256
289
|
// Individual enforcement rules — each returns EnforcementResult | null
|
|
257
290
|
// Rules are ordered by priority. The first non-null result wins.
|
|
258
291
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
259
|
-
/** R1: Agent claims success but
|
|
292
|
+
/** R1: Agent claims success but declared criteria remain outstanding.
|
|
260
293
|
* This is a lie — the agent must either finish the criteria or
|
|
261
294
|
* set success=false honestly. Triggered by the verification gate's
|
|
262
295
|
* "success_with_remaining_criteria" error flag. */
|
|
@@ -363,118 +396,91 @@ function enforceSuccessWithoutVerifiedEvidence(flags, consecutiveRejections) {
|
|
|
363
396
|
"test evidence and no test results were observed. Success must be backed " +
|
|
364
397
|
"by machine-verifiable evidence.",
|
|
365
398
|
fix_instructions: "Provide verifiable evidence for your success claim: (a) run tests and " +
|
|
366
|
-
"report results in
|
|
399
|
+
"report results in execution_report.tests_reported, or (b) configure a " +
|
|
367
400
|
"required verification command and run it, or (c) if no change was " +
|
|
368
401
|
"genuinely needed, declare no_change_reason explaining why." + escalation,
|
|
369
402
|
check: "success_without_verified_evidence",
|
|
370
403
|
});
|
|
371
404
|
}
|
|
372
|
-
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
405
|
+
/** v3.8: Verification debt — contract items claimed met without machine
|
|
406
|
+
* verification. The round COMMITS (insufficient is recorded, not rejected);
|
|
407
|
+
* this row only fires once the debt has persisted for
|
|
408
|
+
* `engine.unverified_claim_streak_limit` consecutive rounds, then runs its
|
|
409
|
+
* OWN ladder: reject → terminate as `incomplete`.
|
|
410
|
+
*
|
|
411
|
+
* v3.8.1: the ladder lives here (RULE_TABLE marks the row "internal").
|
|
412
|
+
* Declaring it "uniform" as well ran the escalation twice — the notice was
|
|
413
|
+
* appended twice to one fix_instructions — and made the uniform terminate
|
|
414
|
+
* branch unreachable, while its bare `consecutiveRejections` read let a
|
|
415
|
+
* streak earned by a DIFFERENT check terminate this row on its FIRST
|
|
416
|
+
* occurrence, which is exactly what the L4 rule forbids.
|
|
417
|
+
*
|
|
418
|
+
* The streak is derived from COMMITTED round flags, so it survives restart
|
|
419
|
+
* and cannot be moved by an agent self-report. Git motion does not excuse
|
|
420
|
+
* it: churning code without verifying is exactly what this row catches. */
|
|
421
|
+
function enforceContractItemsUnverified(flags, currentRound, vaultEntries, consecutiveRejections, lastRejectionCheck) {
|
|
422
|
+
const limit = getPolicy().engine.unverified_claim_streak_limit;
|
|
423
|
+
if (!Number.isFinite(limit) || limit <= 0)
|
|
380
424
|
return null;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
425
|
+
const current = flags.find((flag) => flag.check === CHECK_CONTRACT_ITEMS_UNVERIFIED);
|
|
426
|
+
if (!current)
|
|
427
|
+
return null;
|
|
428
|
+
let streak = 1;
|
|
429
|
+
for (let round = currentRound - 1; round >= 1; round--) {
|
|
430
|
+
const entry = vaultEntries.find((item) => entryRound(item) === round && String(item.task_id ?? "").endsWith(":feedback"));
|
|
431
|
+
const committed = entry ? decodeCommittedRound(entry) : null;
|
|
432
|
+
const carried = committed?.verificationFlags.some((flag) => flag.check === CHECK_CONTRACT_ITEMS_UNVERIFIED);
|
|
433
|
+
if (!carried)
|
|
434
|
+
break;
|
|
435
|
+
streak++;
|
|
388
436
|
}
|
|
389
|
-
|
|
390
|
-
consecutiveRejections >= 1
|
|
391
|
-
? buildEscalationNotice()
|
|
392
|
-
: "";
|
|
393
|
-
return makeEnforcementResult({
|
|
394
|
-
action: "reject",
|
|
395
|
-
reason: flag.detail,
|
|
396
|
-
fix_instructions: "You claimed success under a Round Contract but done_when items are " +
|
|
397
|
-
"unfinished or unverified. You must: (a) run the contract's " +
|
|
398
|
-
"verification_plan commands (configured in loop_policy.json " +
|
|
399
|
-
"evidence.commands) and report their actual output, listing satisfied " +
|
|
400
|
-
"done_when items in success_criteria_met; or (b) set success=false and " +
|
|
401
|
-
"list what remains in success_criteria_remaining. " +
|
|
402
|
-
"Do NOT claim contract completion without machine evidence " +
|
|
403
|
-
"(no_change_reason does not apply to contract claims)." + escalation,
|
|
404
|
-
check: "premature_boundary",
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
/** v3.5: Round Contract completion claimed without its verification_plan
|
|
408
|
-
* commands passing this round. The gate produced
|
|
409
|
-
* contract_completion_unverified (error): the eval's met claims satisfy
|
|
410
|
-
* every done_when of the ACTIVE contract, but the machine never observed
|
|
411
|
-
* the plan's commands pass. Closing a contract is a success-class claim —
|
|
412
|
-
* first occurrence → reject with fix instructions; second consecutive →
|
|
413
|
-
* terminate (R-C1's ladder). Registered BEFORE R-C1: the completion-truth
|
|
414
|
-
* question outranks boundary nuance. */
|
|
415
|
-
function enforceContractCompletionUnverified(flags, consecutiveRejections) {
|
|
416
|
-
const flag = flags.find((f) => f.check === CHECK_CONTRACT_COMPLETION_UNVERIFIED && f.severity === "error");
|
|
417
|
-
if (!flag)
|
|
437
|
+
if (streak < limit)
|
|
418
438
|
return null;
|
|
419
|
-
|
|
439
|
+
// The counter measures the PREVIOUS round's check (the coordinator resets it
|
|
440
|
+
// whenever the check changes), so it only speaks for this row when it names
|
|
441
|
+
// this row — same rule the uniform rows apply.
|
|
442
|
+
const ownRejections = lastRejectionCheck === "contract_items_unverified"
|
|
443
|
+
? consecutiveRejections
|
|
444
|
+
: 0;
|
|
445
|
+
if (ownRejections >= 2) {
|
|
420
446
|
return makeEnforcementResult({
|
|
421
447
|
action: "terminate",
|
|
422
|
-
reason:
|
|
423
|
-
|
|
424
|
-
check: "
|
|
448
|
+
reason: `Verification debt persisted for ${streak} rounds — contract items ` +
|
|
449
|
+
`claimed met while no bound command was observed passing.`,
|
|
450
|
+
check: "contract_items_unverified",
|
|
451
|
+
stopReason: "incomplete",
|
|
425
452
|
});
|
|
426
453
|
}
|
|
427
454
|
const escalation = getPolicy().engine.enforcement_escalation_enabled &&
|
|
428
|
-
|
|
455
|
+
ownRejections >= 1
|
|
429
456
|
? buildEscalationNotice()
|
|
430
457
|
: "";
|
|
431
458
|
return makeEnforcementResult({
|
|
432
459
|
action: "reject",
|
|
433
|
-
reason:
|
|
434
|
-
fix_instructions:
|
|
435
|
-
"
|
|
436
|
-
"
|
|
437
|
-
"
|
|
438
|
-
"
|
|
439
|
-
|
|
440
|
-
"Completion claims are never accepted without machine verification — " +
|
|
441
|
-
"do NOT use no_change_reason for completion claims." + escalation,
|
|
442
|
-
check: "contract_completion_unverified",
|
|
460
|
+
reason: current.detail,
|
|
461
|
+
fix_instructions: `Contract items have been claimed met for ${streak} consecutive rounds ` +
|
|
462
|
+
"without machine verification. You must either: (a) run the item's bound " +
|
|
463
|
+
"command(s) so they are observed passing, or (b) report the item as " +
|
|
464
|
+
"remaining / declare outcome: \"blocked\" with a blocker. " +
|
|
465
|
+
"An unverified claim is never accepted as completion." + escalation,
|
|
466
|
+
check: "contract_items_unverified",
|
|
443
467
|
});
|
|
444
468
|
}
|
|
445
469
|
/** R-C2 (v3.3): Round Contract scope drift. The gate produced
|
|
446
470
|
* round_scope_drift (warn): files changed outside the contract's declared
|
|
447
|
-
* scope.
|
|
448
|
-
* the
|
|
449
|
-
* scope
|
|
450
|
-
*
|
|
451
|
-
|
|
452
|
-
function enforceScopeDrift(flags, selfEval, consecutiveRejections, vaultEntries = []) {
|
|
471
|
+
* scope. v3.8: scope drift is a MACHINE fact and is no longer waivable by an
|
|
472
|
+
* agent explanation (the drift_clarification channel was deleted) — the
|
|
473
|
+
* agent must revert the out-of-scope changes or close the active contract
|
|
474
|
+
* and declare an extended scope. Repeated drift terminates. */
|
|
475
|
+
function enforceScopeDrift(flags, consecutiveRejections) {
|
|
453
476
|
const flag = flags.find((f) => f.check === CHECK_ROUND_SCOPE_DRIFT);
|
|
454
477
|
if (!flag)
|
|
455
478
|
return null;
|
|
456
|
-
const clarification = selfEval.drift_clarification?.trim();
|
|
457
|
-
const hasAnchor = clarification
|
|
458
|
-
? clarificationIsSubstantive(clarification, selfEval, vaultEntries)
|
|
459
|
-
: false;
|
|
460
|
-
const isSubstantive = clarification !== undefined
|
|
461
|
-
&& clarification.length >= 20
|
|
462
|
-
&& hasAnchor;
|
|
463
|
-
if (isSubstantive) {
|
|
464
|
-
// Genuine scope expansion with concrete file references → accept.
|
|
465
|
-
return makeEnforcementResult({
|
|
466
|
-
action: "accept",
|
|
467
|
-
reason: "",
|
|
468
|
-
fix_instructions: "",
|
|
469
|
-
check: "round_scope_drift",
|
|
470
|
-
clarification_accepted: true,
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
479
|
if (consecutiveRejections >= 2) {
|
|
474
480
|
return makeEnforcementResult({
|
|
475
481
|
action: "terminate",
|
|
476
|
-
reason: `Repeated scope drift
|
|
477
|
-
`
|
|
482
|
+
reason: `Repeated scope drift — the agent keeps changing files outside its ` +
|
|
483
|
+
`declared contract scope.`,
|
|
478
484
|
check: "round_scope_drift",
|
|
479
485
|
});
|
|
480
486
|
}
|
|
@@ -497,9 +503,8 @@ function enforceScopeDrift(flags, selfEval, consecutiveRejections, vaultEntries
|
|
|
497
503
|
"a replacement contract is IGNORED while the current one is active, so " +
|
|
498
504
|
"resubmit this work with outcome: \"blocked\" (blocker naming the " +
|
|
499
505
|
"too-narrow scope) plus the extended scope as the next round's " +
|
|
500
|
-
"round_contract
|
|
501
|
-
"
|
|
502
|
-
"clarifications are rejected; repeated scope drift terminates the loop." +
|
|
506
|
+
"round_contract. Scope drift is a machine fact — no explanation waives " +
|
|
507
|
+
"it. Repeated scope drift terminates the loop." +
|
|
503
508
|
escalation,
|
|
504
509
|
check: "round_scope_drift",
|
|
505
510
|
});
|
|
@@ -529,27 +534,57 @@ function enforceBacktrackNotRestored(flags, vaultEntries = [], currentRound = 0)
|
|
|
529
534
|
return makeEnforcementResult({
|
|
530
535
|
action: "backtrack",
|
|
531
536
|
reason: "The workspace was not restored after backtrack: " + flag.detail,
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
"submitting the next round
|
|
537
|
+
// v3.8: the requirement, not the recipe — LoopForge never mutates the
|
|
538
|
+
// working tree and does not prescribe how the agent restores it.
|
|
539
|
+
fix_instructions: "Return the working directory to the clean round's state before " +
|
|
540
|
+
"submitting the next round: the skipped-round files must be reverted " +
|
|
541
|
+
"and HEAD must be back at the restore point the backtrack prompt named.",
|
|
536
542
|
check: "backtrack_workspace_not_restored",
|
|
537
543
|
});
|
|
538
544
|
}
|
|
539
545
|
/** Shared observation pipeline for the progress evaluator (stall window,
|
|
540
546
|
* flatline tier). Machine git motion is exculpatory only: it may veto a
|
|
541
547
|
* self-reported stall but can never create one. */
|
|
542
|
-
function evaluateStallWindow(tier, selfEval, currentRound, vaultEntries
|
|
548
|
+
function evaluateStallWindow(tier, selfEval, currentRound, vaultEntries,
|
|
549
|
+
/** v3.8.1: the round's success is machine-verified (`deriveEvidenceStatus`).
|
|
550
|
+
* A verified round is finishing work; a motion window cannot outvote it. */
|
|
551
|
+
machineBackedSuccess) {
|
|
552
|
+
// A round the machine verified is not churn, whatever the window says. This
|
|
553
|
+
// is the machine-backed replacement for the old self-reported `>= 0.95`
|
|
554
|
+
// near-completion guard: it lets a loop FINISH (a closing round that only
|
|
555
|
+
// runs the verification and declares success changes no files).
|
|
556
|
+
if (machineBackedSuccess)
|
|
557
|
+
return null;
|
|
543
558
|
const window = tier === "progress_stall"
|
|
544
559
|
? 3
|
|
545
560
|
: Math.max(1, getPolicy().engine.stall_lookback_rounds);
|
|
546
561
|
if (currentRound < window)
|
|
547
562
|
return null;
|
|
548
563
|
const progressByRound = progressWindow(vaultEntries, currentRound, selfEval);
|
|
564
|
+
const threshold = getPolicy().evolution.progress_stall_threshold;
|
|
549
565
|
let stalled = false;
|
|
550
566
|
let detail = "";
|
|
551
|
-
|
|
552
|
-
|
|
567
|
+
// v3.8.1: when the MACHINE series is observable it decides the verdict, and
|
|
568
|
+
// the agent's own estimates are not consulted at all. Reading the estimates
|
|
569
|
+
// first (the pre-3.8.1 order) let an agent cancel a stall by writing a
|
|
570
|
+
// rising `progress_estimate` every round — a verdict about machine motion
|
|
571
|
+
// that the agent's own report could move, which is the one thing an external
|
|
572
|
+
// verifier must not allow. Machine motion still only ever EXCUSES: it can
|
|
573
|
+
// veto a stall it did not create; it can never create one.
|
|
574
|
+
//
|
|
575
|
+
// The self-reported window is the FALLBACK for a loop with no machine
|
|
576
|
+
// history to read (no git provider, or fewer rounds than the window). There
|
|
577
|
+
// the evaluator can only catch a stall the agent's own reports state, and
|
|
578
|
+
// the near-completion guard keeps it from rejecting a loop that reports
|
|
579
|
+
// itself nearly done.
|
|
580
|
+
const machine = machineProgressSeries(vaultEntries, currentRound, window);
|
|
581
|
+
if (machine !== null) {
|
|
582
|
+
stalled = machine.every((value) => !value);
|
|
583
|
+
detail = tier === "progress_stall"
|
|
584
|
+
? `no machine-observed changes in the last ${window} rounds`
|
|
585
|
+
: `no machine-observed changes for ${window} consecutive rounds`;
|
|
586
|
+
}
|
|
587
|
+
else if (progressByRound.size >= window) {
|
|
553
588
|
const recent = [...progressByRound.keys()].sort((a, b) => a - b).slice(-window);
|
|
554
589
|
if (recent[0] < currentRound - window)
|
|
555
590
|
return null;
|
|
@@ -558,7 +593,8 @@ function evaluateStallWindow(tier, selfEval, currentRound, vaultEntries) {
|
|
|
558
593
|
return null;
|
|
559
594
|
if (tier === "progress_stall") {
|
|
560
595
|
stalled = values.slice(1).every((value, index) => value - values[index] < threshold);
|
|
561
|
-
detail = values.map((value) => `${(value * 100).toFixed(0)}%`).join(" → ")
|
|
596
|
+
detail = `${values.map((value) => `${(value * 100).toFixed(0)}%`).join(" → ")}` +
|
|
597
|
+
" (agent-reported, no machine history to read)";
|
|
562
598
|
}
|
|
563
599
|
else {
|
|
564
600
|
const first = values[0];
|
|
@@ -567,22 +603,10 @@ function evaluateStallWindow(tier, selfEval, currentRound, vaultEntries) {
|
|
|
567
603
|
}
|
|
568
604
|
}
|
|
569
605
|
else {
|
|
570
|
-
|
|
571
|
-
if (machine === null)
|
|
572
|
-
return null;
|
|
573
|
-
stalled = machine.every((value) => !value);
|
|
574
|
-
detail = tier === "progress_stall"
|
|
575
|
-
? `no machine-observed changes in the last ${window} rounds`
|
|
576
|
-
: `no machine-observed changes for ${window} consecutive rounds`;
|
|
606
|
+
return null;
|
|
577
607
|
}
|
|
578
608
|
if (!stalled)
|
|
579
609
|
return null;
|
|
580
|
-
const machine = machineProgressSeries(vaultEntries, currentRound, window);
|
|
581
|
-
if (machine?.some(Boolean))
|
|
582
|
-
return null;
|
|
583
|
-
if (machine !== null) {
|
|
584
|
-
detail += ` — no machine-observed git motion in rounds ${currentRound - window}–${currentRound - 1}`;
|
|
585
|
-
}
|
|
586
610
|
return { tier, detail, window, threshold };
|
|
587
611
|
}
|
|
588
612
|
/** Shared progress escalation and backtrack deadlock guard (v3.7: one
|
|
@@ -656,9 +680,9 @@ function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiv
|
|
|
656
680
|
* shorter lookback configs), where it applies its steeper disposition
|
|
657
681
|
* ladder. Machine git motion is exculpatory only — it may veto a
|
|
658
682
|
* self-reported stall but can never create one. */
|
|
659
|
-
function enforceProgressStall(selfEval, _flags, currentRound, vaultEntries, consecutiveRejections) {
|
|
660
|
-
const verdict = evaluateStallWindow("progress_stall", selfEval, currentRound, vaultEntries)
|
|
661
|
-
?? evaluateStallWindow("progress_flatline", selfEval, currentRound, vaultEntries);
|
|
683
|
+
function enforceProgressStall(selfEval, _flags, currentRound, vaultEntries, consecutiveRejections, machineBackedSuccess) {
|
|
684
|
+
const verdict = evaluateStallWindow("progress_stall", selfEval, currentRound, vaultEntries, machineBackedSuccess)
|
|
685
|
+
?? evaluateStallWindow("progress_flatline", selfEval, currentRound, vaultEntries, machineBackedSuccess);
|
|
662
686
|
return verdict
|
|
663
687
|
? resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiveRejections)
|
|
664
688
|
: null;
|
|
@@ -706,212 +730,6 @@ function enforceMaxRejections(consecutiveRejections) {
|
|
|
706
730
|
check: "max_rejections",
|
|
707
731
|
});
|
|
708
732
|
}
|
|
709
|
-
/** v2.12: Check if a drift_clarification contains at least one semantic
|
|
710
|
-
* anchor — a constraint/criterion/sub-goal ID (v2.11) or a file-path-like
|
|
711
|
-
* reference. This distinguishes genuine explanations from filler text that
|
|
712
|
-
* just happens to be ≥ 20 characters. */
|
|
713
|
-
/** Extract anchor candidates from a clarification: c-/cr-/sg- IDs and
|
|
714
|
-
* file-path-like tokens. */
|
|
715
|
-
function extractAnchors(clarification) {
|
|
716
|
-
const anchors = new Set();
|
|
717
|
-
for (const match of clarification.matchAll(/[c]r?-[a-f0-9]{8}|sg-[a-f0-9]{8}/gi)) {
|
|
718
|
-
anchors.add(match[0].toLowerCase());
|
|
719
|
-
}
|
|
720
|
-
for (const path of extractFilePathTokens(clarification)) {
|
|
721
|
-
anchors.add(path);
|
|
722
|
-
}
|
|
723
|
-
return [...anchors];
|
|
724
|
-
}
|
|
725
|
-
/** v2.12: A drift clarification is substantive only when at least one anchor
|
|
726
|
-
* is REAL — a stable ID that derives from a known constraint/criterion/
|
|
727
|
-
* sub-goal text in the vault, or a file path present in reported/observed
|
|
728
|
-
* files. Regex patterns alone (e.g. a fabricated `c-00000000`) no longer
|
|
729
|
-
* count, closing the v2.12 filler-text loophole. */
|
|
730
|
-
function clarificationIsSubstantive(clarification, selfEval, vaultEntries) {
|
|
731
|
-
const anchors = extractAnchors(clarification);
|
|
732
|
-
if (anchors.length === 0)
|
|
733
|
-
return false;
|
|
734
|
-
const knownFiles = new Set();
|
|
735
|
-
const knownTexts = new Set();
|
|
736
|
-
const collect = (values) => {
|
|
737
|
-
if (!Array.isArray(values))
|
|
738
|
-
return;
|
|
739
|
-
for (const value of values) {
|
|
740
|
-
if (typeof value === "string" && value.length > 0)
|
|
741
|
-
knownTexts.add(value);
|
|
742
|
-
}
|
|
743
|
-
};
|
|
744
|
-
// The agent's own round report is a known set: its constraints, criteria,
|
|
745
|
-
// and files are legitimate anchor targets.
|
|
746
|
-
for (const file of selfEval.execution_evidence?.files_changed ?? [])
|
|
747
|
-
knownFiles.add(file);
|
|
748
|
-
collect(selfEval.discovered_constraints);
|
|
749
|
-
collect(selfEval.constraint_violations);
|
|
750
|
-
const selfEvidence = selfEval.execution_evidence;
|
|
751
|
-
if (selfEvidence) {
|
|
752
|
-
collect(selfEvidence.success_criteria_met);
|
|
753
|
-
collect(selfEvidence.success_criteria_remaining);
|
|
754
|
-
}
|
|
755
|
-
for (const entry of vaultEntries) {
|
|
756
|
-
collect(entry.constraint_violations);
|
|
757
|
-
collect(entry.discovered_constraints);
|
|
758
|
-
const ev = entry.execution_evidence;
|
|
759
|
-
if (ev) {
|
|
760
|
-
collect(ev.files_changed);
|
|
761
|
-
collect(ev.success_criteria_met);
|
|
762
|
-
collect(ev.success_criteria_remaining);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
for (const anchor of anchors) {
|
|
766
|
-
if (STABLE_ID_RE.test(anchor)) {
|
|
767
|
-
const matches = anchor.startsWith("c-") && !anchor.startsWith("cr-")
|
|
768
|
-
? [...knownTexts].some((text) => deriveConstraintId(text) === anchor)
|
|
769
|
-
: anchor.startsWith("cr-")
|
|
770
|
-
? [...knownTexts].some((text) => deriveCriterionId(text) === anchor)
|
|
771
|
-
: [...knownTexts].some((text) => deriveSubGoalId(text) === anchor);
|
|
772
|
-
if (matches)
|
|
773
|
-
return true;
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
// File path: exact or suffix match against known files.
|
|
777
|
-
if ([...knownFiles].some((file) => file === anchor || file.endsWith(`/${anchor}`) || file.endsWith(`\\${anchor}`))) {
|
|
778
|
-
return true;
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
return false;
|
|
782
|
-
}
|
|
783
|
-
/** Build a stronger rejection notice as the clarification streak grows. */
|
|
784
|
-
function buildClarificationRejectionNotice(streak) {
|
|
785
|
-
const base = "Your declared next_action from the previous round does not match " +
|
|
786
|
-
"what you actually did this round. You must either: " +
|
|
787
|
-
"(a) explain why you pivoted in the drift_clarification field, " +
|
|
788
|
-
"mentioning specific constraint/criterion/sub-goal IDs or file paths " +
|
|
789
|
-
"that prompted the change, or " +
|
|
790
|
-
"(b) redo this round and do what you said you would do.";
|
|
791
|
-
if (streak >= 2) {
|
|
792
|
-
return (base +
|
|
793
|
-
"\n\n### ⚠️ Repeated Weak Clarification\n\n" +
|
|
794
|
-
`You have submitted drift_clarification without concrete references ` +
|
|
795
|
-
`for ${streak} consecutive rounds. Your explanation MUST include at ` +
|
|
796
|
-
`least one of: a constraint ID (c-XXXXXXXX), criterion ID (cr-XXXXXXXX), ` +
|
|
797
|
-
`sub-goal ID (sg-XXXXXXXX), or a file path you changed.\n\n` +
|
|
798
|
-
`🚨 If the next clarification also lacks concrete anchors, the loop ` +
|
|
799
|
-
`will be terminated.`);
|
|
800
|
-
}
|
|
801
|
-
return base;
|
|
802
|
-
}
|
|
803
|
-
/** R7: Agent declared a next_action but did something unrelated.
|
|
804
|
-
*
|
|
805
|
-
* v2.8: When the agent provides a substantive drift_clarification (≥ 20
|
|
806
|
-
* characters), the rejection is waived — the agent has acknowledged and
|
|
807
|
-
* explained the pivot. An empty or absent clarification still triggers
|
|
808
|
-
* the normal reject/terminate path.
|
|
809
|
-
*
|
|
810
|
-
* v2.12: Clarification now requires a semantic anchor (constraint/criterion/
|
|
811
|
-
* sub-goal ID or file path) in addition to the ≥ 20 char minimum. Weak
|
|
812
|
-
* clarifications (no anchors) increment a streak counter; 3 consecutive
|
|
813
|
-
* weak clarifications terminate the loop.
|
|
814
|
-
*
|
|
815
|
-
* First occurrence → reject (agent must explain the gap or realign).
|
|
816
|
-
* Second consecutive → terminate (agent cannot/will not follow its own plan). */
|
|
817
|
-
function enforceIntentDrift(flags, selfEval, consecutiveRejections, driftClarificationStreak = 0, vaultEntries = []) {
|
|
818
|
-
const flag = flags.find((f) => f.check === "intent_drift");
|
|
819
|
-
if (!flag)
|
|
820
|
-
return null;
|
|
821
|
-
const clarification = selfEval.drift_clarification?.trim();
|
|
822
|
-
// v2.12: Substantive anchors — the referenced ID/path must actually exist
|
|
823
|
-
// in the vault or in reported files; a fabricated ID no longer counts.
|
|
824
|
-
const hasAnchor = clarification
|
|
825
|
-
? clarificationIsSubstantive(clarification, selfEval, vaultEntries)
|
|
826
|
-
: false;
|
|
827
|
-
// v2.12: Substantive clarification = length check + semantic anchor
|
|
828
|
-
const isSubstantive = clarification
|
|
829
|
-
&& clarification.length >= 20
|
|
830
|
-
&& hasAnchor;
|
|
831
|
-
if (isSubstantive) {
|
|
832
|
-
// Genuine explanation with concrete references → accept the pivot
|
|
833
|
-
return makeEnforcementResult({
|
|
834
|
-
action: "accept",
|
|
835
|
-
reason: "",
|
|
836
|
-
fix_instructions: "",
|
|
837
|
-
check: "intent_drift",
|
|
838
|
-
clarification_accepted: true,
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
// v2.12: Clarification present but weak (no semantic anchors)
|
|
842
|
-
if (clarification && clarification.length >= 20 && !hasAnchor) {
|
|
843
|
-
const maxStreak = getPolicy().engine.drift_clarification_max_streak;
|
|
844
|
-
// When maxStreak is 0, disable the check (pre-v2.12 behavior)
|
|
845
|
-
if (maxStreak <= 0) {
|
|
846
|
-
return makeEnforcementResult({
|
|
847
|
-
action: "accept",
|
|
848
|
-
reason: "",
|
|
849
|
-
fix_instructions: "",
|
|
850
|
-
check: "intent_drift",
|
|
851
|
-
clarification_accepted: true,
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
const newStreak = driftClarificationStreak + 1;
|
|
855
|
-
if (newStreak >= maxStreak) {
|
|
856
|
-
return makeEnforcementResult({
|
|
857
|
-
action: "terminate",
|
|
858
|
-
reason: `Agent has submitted weak drift_clarification (no concrete IDs or ` +
|
|
859
|
-
`file references) for ${newStreak} consecutive rounds. The agent ` +
|
|
860
|
-
`systematically avoids following its own plan without substantive ` +
|
|
861
|
-
`explanation.`,
|
|
862
|
-
check: "intent_drift",
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
return makeEnforcementResult({
|
|
866
|
-
action: "reject",
|
|
867
|
-
reason: flag.detail,
|
|
868
|
-
fix_instructions: buildClarificationRejectionNotice(newStreak),
|
|
869
|
-
check: "intent_drift",
|
|
870
|
-
});
|
|
871
|
-
}
|
|
872
|
-
// No clarification or too short — the worst form of drift. v2.14: counts
|
|
873
|
-
// toward the SAME streak as weak clarifications (maxStreak consecutive
|
|
874
|
-
// un-explained drifts terminate, per the README's "three consecutive weak
|
|
875
|
-
// clarifications"). Previously this branch used the GLOBAL
|
|
876
|
-
// consecutiveRejections counter, so an unrelated earlier rejection
|
|
877
|
-
// (e.g. R1) terminated the loop on the very first drift.
|
|
878
|
-
const maxStreak = getPolicy().engine.drift_clarification_max_streak;
|
|
879
|
-
// When maxStreak is 0, disable the check (pre-v2.12 behavior)
|
|
880
|
-
if (maxStreak <= 0) {
|
|
881
|
-
return makeEnforcementResult({
|
|
882
|
-
action: "accept",
|
|
883
|
-
reason: "",
|
|
884
|
-
fix_instructions: "",
|
|
885
|
-
check: "intent_drift",
|
|
886
|
-
clarification_accepted: true,
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
const newStreak = driftClarificationStreak + 1;
|
|
890
|
-
if (newStreak >= maxStreak) {
|
|
891
|
-
return makeEnforcementResult({
|
|
892
|
-
action: "terminate",
|
|
893
|
-
reason: `Agent has drifted from its declared next_action for ${newStreak} ` +
|
|
894
|
-
`consecutive rounds without any drift_clarification. The agent ` +
|
|
895
|
-
`cannot or will not follow its own plan — terminating loop.`,
|
|
896
|
-
check: "intent_drift",
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
return makeEnforcementResult({
|
|
900
|
-
action: "reject",
|
|
901
|
-
reason: flag.detail,
|
|
902
|
-
fix_instructions: "Your declared next_action from the previous round does not match " +
|
|
903
|
-
"what you actually did this round. You must either: " +
|
|
904
|
-
"(a) explain why you pivoted and what you learned in the " +
|
|
905
|
-
"drift_clarification field, or " +
|
|
906
|
-
"(b) redo this round and do what you said you would do. " +
|
|
907
|
-
"If you intentionally changed direction, update your objective_refinement " +
|
|
908
|
-
"and set a new next_action that reflects the new plan.",
|
|
909
|
-
check: "intent_drift",
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
913
|
-
// Escalation notice builder (v2.7)
|
|
914
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
915
733
|
/** Build an escalation notice urging the agent to seek human guidance.
|
|
916
734
|
*
|
|
917
735
|
* Appended to fix_instructions when a rule would otherwise terminate.
|
|
@@ -962,24 +780,21 @@ function buildDiagnosticGap(flags) {
|
|
|
962
780
|
return lines.join("\n");
|
|
963
781
|
}
|
|
964
782
|
const RULE_TABLE = [
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
{
|
|
970
|
-
{
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
{
|
|
975
|
-
{
|
|
976
|
-
{
|
|
977
|
-
{
|
|
978
|
-
{ category: "progress_recovery", checks: ["max_rejections"], ladder: "counter" },
|
|
979
|
-
{ category: "plan_drift", checks: ["intent_drift"], ladder: "internal", clarificationAllowed: true },
|
|
980
|
-
{ category: "progress_recovery", checks: ["backtrack_workspace_not_restored"], ladder: "internal", backtrackAllowed: true },
|
|
783
|
+
{ checks: ["success_with_remaining_criteria"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
784
|
+
{ checks: ["recurring_violation"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
785
|
+
{ checks: ["required_command_failed", "command_evidence_mismatch", "outcome_success_contradiction"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
786
|
+
{ checks: ["verification_entrypoint_modified"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
787
|
+
{ checks: ["success_without_verified_evidence"], ladder: "internal" },
|
|
788
|
+
{ checks: ["round_scope_drift"], ladder: "internal" },
|
|
789
|
+
// v3.8.1: "internal" — the handler owns this row's ladder (it must terminate
|
|
790
|
+
// as `incomplete`, with its own reason). Declaring it uniform as well ran the
|
|
791
|
+
// escalation notice twice and left the uniform terminate branch dead.
|
|
792
|
+
{ checks: ["contract_items_unverified"], ladder: "internal" },
|
|
793
|
+
{ checks: ["progress_stall"], ladder: "internal" },
|
|
794
|
+
{ checks: ["max_rejections"], ladder: "counter" },
|
|
795
|
+
{ checks: ["backtrack_workspace_not_restored"], ladder: "internal" },
|
|
981
796
|
// v3.7.1: cited gates without an approved human decision
|
|
982
|
-
{
|
|
797
|
+
{ checks: ["user_gate_unresolved"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
983
798
|
];
|
|
984
799
|
const RULE_TABLE_BY_ID = new Map();
|
|
985
800
|
for (const row of RULE_TABLE) {
|
|
@@ -1024,8 +839,6 @@ function enforceUserGateUnresolved(flags) {
|
|
|
1024
839
|
});
|
|
1025
840
|
}
|
|
1026
841
|
export function enforceRound(selfEval, verifyResult, currentRound, vaultEntries, consecutiveRejections = 0,
|
|
1027
|
-
/** v2.12: Current clarification streak for R7 escalation. */
|
|
1028
|
-
driftClarificationStreak = 0,
|
|
1029
842
|
/** L4 (v3.7.x): which check rejected the PREVIOUS round. The coordinator
|
|
1030
843
|
* resets consecutiveRejections to 1 whenever the check changes, so the
|
|
1031
844
|
* counter only ever measures one check's streak — this field names that
|
|
@@ -1049,27 +862,25 @@ lastRejectionCheck = "") {
|
|
|
1049
862
|
// v3.5: contract completion claimed without its verification_plan
|
|
1050
863
|
// commands passing — runs before R-C1: the completion-truth question
|
|
1051
864
|
// outranks boundary nuance.
|
|
1052
|
-
() => enforceContractCompletionUnverified(flags, consecutiveRejections),
|
|
1053
865
|
// v3.3: R-C1 — contract boundary claimed prematurely (contract-specific
|
|
1054
866
|
// wording and check counting; runs before R8 so contract rounds get the
|
|
1055
867
|
// contract's own reason.)
|
|
1056
|
-
() => enforcePrematureBoundary(flags, consecutiveRejections),
|
|
1057
868
|
// v2.12/v3.7: R8 — the single success-evidence row (empty/missing-evidence
|
|
1058
869
|
// arm merged from the former R3 empty_success posture + claims arm)
|
|
1059
870
|
() => enforceSuccessWithoutVerifiedEvidence(flags, consecutiveRejections),
|
|
1060
871
|
// v3.3: R-C2 — files changed outside the contract's declared scope
|
|
1061
|
-
() => enforceScopeDrift(flags,
|
|
872
|
+
() => enforceScopeDrift(flags, consecutiveRejections),
|
|
873
|
+
// v3.8: verification debt — contract items claimed met without machine
|
|
874
|
+
// verification (fires only after the configured streak)
|
|
875
|
+
() => enforceContractItemsUnverified(flags, currentRound, vaultEntries, consecutiveRejections, lastRejectionCheck),
|
|
1062
876
|
// v3.7: single progress evaluator (former R4/R5 slots merged) —
|
|
1063
877
|
// reject → backtrack → terminate with the deadlock guard
|
|
1064
|
-
() => enforceProgressStall(selfEval, flags, currentRound, vaultEntries, consecutiveRejections),
|
|
878
|
+
() => enforceProgressStall(selfEval, flags, currentRound, vaultEntries, consecutiveRejections, verifyResult.machineBackedSuccess),
|
|
1065
879
|
// R6: rejection-counter catch-all (evaluated only when no row above
|
|
1066
880
|
// fired — a clean-looking round carrying a high persisted counter)
|
|
1067
881
|
// v3.7.1: cited gate without an approved human decision
|
|
1068
882
|
() => enforceUserGateUnresolved(flags),
|
|
1069
883
|
() => enforceMaxRejections(consecutiveRejections),
|
|
1070
|
-
// v2.12: R7 — intent drift; its weak-clarification streak is a separate
|
|
1071
|
-
// counter and never mixes with the global rejection count
|
|
1072
|
-
() => enforceIntentDrift(flags, selfEval, consecutiveRejections, driftClarificationStreak, vaultEntries),
|
|
1073
884
|
// v2.12: R9 — post-backtrack workspace not restored
|
|
1074
885
|
() => enforceBacktrackNotRestored(flags, vaultEntries, currentRound),
|
|
1075
886
|
];
|