loopforge 3.6.0 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +28 -13
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +3 -8
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +23 -4
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +148 -47
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -16
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts.map +1 -1
- package/dist/cognitive-facts.js +2 -3
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/cognitive-governance.d.ts +30 -7
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +75 -22
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +48 -4
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +177 -10
- package/dist/committed-round.js.map +1 -1
- package/dist/enforcement-gate.d.ts +24 -19
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +216 -146
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +10 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +52 -31
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +3 -9
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +13 -19
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +65 -100
- package/dist/evidence-provider.js.map +1 -1
- package/dist/generate-schema.js +104 -43
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +18 -10
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +205 -343
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +11 -16
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +21 -63
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +18 -3
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +162 -35
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +12 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +35 -4
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +169 -52
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +170 -39
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy.d.ts +10 -13
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +3 -6
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +6 -4
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +46 -100
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +52 -51
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +5 -28
- package/dist/protocol.js.map +1 -1
- package/dist/round-coordinator.d.ts +19 -0
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +63 -4
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +4 -3
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +3 -10
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +4 -0
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +9 -0
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +41 -3
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +125 -13
- package/dist/self-eval.js.map +1 -1
- package/dist/token-utils.d.ts +7 -3
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +18 -3
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +32 -24
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +262 -178
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/loop_policy.json +19 -7
- package/package.json +6 -6
- package/skills/perception/SKILL.md +56 -16
package/dist/enforcement-gate.js
CHANGED
|
@@ -17,17 +17,16 @@
|
|
|
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, CHECK_USER_GATE_UNRESOLVED } 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 { committedRoundsFromEntries, decodeCommittedRound, } from "./committed-round.js";
|
|
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
28
|
/** Collect progress estimates from vault entries into a round→estimate map.
|
|
30
|
-
* Shared by
|
|
29
|
+
* Shared by the progress evaluator's stall and flatline tiers. */
|
|
31
30
|
function collectProgressByRound(vaultEntries, currentRound) {
|
|
32
31
|
const progressByRound = new Map();
|
|
33
32
|
for (const round of committedRoundsFromEntries(vaultEntries, currentRound)) {
|
|
@@ -129,9 +128,7 @@ export function findBacktrackTargetGitHead(restoreRound, vaultEntries) {
|
|
|
129
128
|
.find((view) => view.round === restoreRound);
|
|
130
129
|
if (!committed)
|
|
131
130
|
return null;
|
|
132
|
-
const evidence = committed
|
|
133
|
-
? committed.afterEvidence
|
|
134
|
-
: committed.beforeEvidence;
|
|
131
|
+
const evidence = machineEvidenceForRound(committed);
|
|
135
132
|
for (const item of evidence) {
|
|
136
133
|
if (!isRecord(item) || item.provider !== "git" || !isRecord(item.data))
|
|
137
134
|
continue;
|
|
@@ -141,31 +138,62 @@ export function findBacktrackTargetGitHead(restoreRound, vaultEntries) {
|
|
|
141
138
|
}
|
|
142
139
|
return null;
|
|
143
140
|
}
|
|
144
|
-
|
|
145
|
-
*
|
|
146
|
-
* v2.13: Includes concrete workspace restore instructions with affected
|
|
147
|
-
* file lists from skipped rounds. The agent must restore the working tree
|
|
148
|
-
* to the clean round's state before proceeding. */
|
|
149
|
-
export function buildBacktrackPrompt(fromRound, toRound, triggerRule, skippedDiscoveries,
|
|
141
|
+
export function buildBacktrackPrompt(fromRound, toRound, triggerRule, skippedDiscoveries,
|
|
150
142
|
/** v2.13: Files changed in the skipped rounds (from evidence snapshots).
|
|
151
143
|
* Used to show the agent exactly what needs to be reverted. */
|
|
152
|
-
skippedFiles = [],
|
|
144
|
+
skippedFiles = [],
|
|
153
145
|
/** v2.12: Git HEAD commit hash at the backtrack point (current HEAD).
|
|
154
146
|
* The agent must discard work back to the clean round's state. */
|
|
155
|
-
gitHead
|
|
147
|
+
gitHead,
|
|
148
|
+
/** v3.7.1: The redo submission's roundId (loop:<id>:round:<toRound+1>). */
|
|
149
|
+
recoveryRoundId,
|
|
150
|
+
/** v3.7.1: Derived Recovery Brief facts — committed facts + the
|
|
151
|
+
* in-flight attempt only; never rejected payloads. */
|
|
152
|
+
recovery) {
|
|
156
153
|
const lines = [
|
|
157
154
|
`## ⛔ Backtrack — Round ${fromRound} → Restored to Round ${toRound}`,
|
|
158
155
|
"",
|
|
159
156
|
];
|
|
157
|
+
// ── Recovery Brief (v3.7.1) ──────────────────────────────────────────
|
|
158
|
+
// A compact structured summary at the top of the rollback directive. The
|
|
159
|
+
// detailed sections below (workspace restore, discoveries, instructions)
|
|
160
|
+
// keep their existing wording; the brief exists so the agent cannot miss
|
|
161
|
+
// why it was rolled back, what failed, and what the redo round is.
|
|
162
|
+
if (recovery) {
|
|
163
|
+
lines.push("### Recovery Brief", "");
|
|
164
|
+
lines.push(`- **Trigger**: ${triggerRule}`);
|
|
165
|
+
lines.push(`- **Restored to round**: ${toRound} (the redo is round ${toRound + 1})`);
|
|
166
|
+
if (recoveryRoundId) {
|
|
167
|
+
lines.push(`- **Recovery Round ID**: \`${recoveryRoundId}\``);
|
|
168
|
+
}
|
|
169
|
+
if (recovery.failedRounds.length > 0) {
|
|
170
|
+
lines.push(`- **Failed rounds**: ${recovery.failedRounds.join(", ")}`);
|
|
171
|
+
const shown = recovery.approaches.slice(0, 6);
|
|
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
|
+
}
|
|
185
|
+
lines.push("");
|
|
186
|
+
}
|
|
160
187
|
// ── Why This Happened ────────────────────────────────────────────────
|
|
188
|
+
// v3.7: stall and flatline are one evaluator emitting progress_stall; the
|
|
189
|
+
// former flatline-specific wording ("zero forward motion → radically
|
|
190
|
+
// different strategy") is folded into the single section — the disposition
|
|
191
|
+
// tier still distinguishes the enforcement reason itself.
|
|
161
192
|
if (triggerRule === "progress_stall") {
|
|
162
|
-
lines.push("### Why This Happened", "", `Progress stalled over rounds ${toRound + 1}–${fromRound}
|
|
163
|
-
`
|
|
164
|
-
`
|
|
165
|
-
|
|
166
|
-
else if (triggerRule === "progress_flatline") {
|
|
167
|
-
lines.push("### Why This Happened", "", `Progress was completely flat for multiple rounds leading up to Round ${fromRound}. ` +
|
|
168
|
-
`The agent made **zero forward motion** — the task is not advancing.`, "", "A **radically different** strategy is needed. The previous approach produced nothing.", "");
|
|
193
|
+
lines.push("### Why This Happened", "", `Progress stalled over rounds ${toRound + 1}–${fromRound}: the work ` +
|
|
194
|
+
`produced no verifiable forward motion — no new files changed, no ` +
|
|
195
|
+
`criteria met, no progress estimates moving.`, "", "The approach used in those rounds **did not work**. It should not be " +
|
|
196
|
+
"repeated — a **radically different** strategy is needed.", "");
|
|
169
197
|
}
|
|
170
198
|
// ── v2.13: Workspace Restore ──────────────────────────────────────────
|
|
171
199
|
lines.push("### ⚠️ Workspace Restore Required", "", `Your working directory still contains changes from the **failed** ` +
|
|
@@ -182,7 +210,10 @@ gitHead) {
|
|
|
182
210
|
}
|
|
183
211
|
lines.push("");
|
|
184
212
|
}
|
|
185
|
-
lines.push("**To restore a clean workspace, run:**", "", "```bash", "# Option A:
|
|
213
|
+
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 " +
|
|
214
|
+
"the next round: the skipped work would look unrestored and be rejected. " +
|
|
215
|
+
"Restore only files you own after the next round commits, via " +
|
|
216
|
+
"`git checkout stash@{0} -- <path>`.", "");
|
|
186
217
|
// v2.12: Restore to the exact commit observed at the clean round.
|
|
187
218
|
if (gitHead) {
|
|
188
219
|
lines.push(`**Restore to commit \`${gitHead.slice(0, 12)}\`** — the verification ` +
|
|
@@ -196,7 +227,7 @@ gitHead) {
|
|
|
196
227
|
"> with unrestored files will be **rejected**.", "");
|
|
197
228
|
// ── What Must Change ──────────────────────────────────────────────────
|
|
198
229
|
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 " +
|
|
199
|
-
"(`
|
|
230
|
+
"(`subgoal_updates` to canceled) and working on a different one.",
|
|
200
231
|
// v3.5: the restored Current Task may be the very Round Contract that
|
|
201
232
|
// stalled — the agent must be told the sanctioned way to revise it
|
|
202
233
|
// (outcome=blocked closes the active contract; the revised proposal in
|
|
@@ -305,51 +336,12 @@ function enforceVerificationEntrypointTampered(flags) {
|
|
|
305
336
|
check: "verification_entrypoint_modified",
|
|
306
337
|
});
|
|
307
338
|
}
|
|
308
|
-
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return null;
|
|
315
|
-
const ev = selfEval.execution_evidence;
|
|
316
|
-
// v1.17: Missing evidence when claiming success → reject.
|
|
317
|
-
// The agent MUST provide execution_evidence to back up a success claim.
|
|
318
|
-
if (!ev) {
|
|
319
|
-
return makeEnforcementResult({
|
|
320
|
-
action: "reject",
|
|
321
|
-
reason: "Agent claims success but provided no execution_evidence. " +
|
|
322
|
-
"Every successful round MUST include execution_evidence with " +
|
|
323
|
-
"files_changed, test_results, and progress_estimate.",
|
|
324
|
-
fix_instructions: "You must provide execution_evidence in your self-evaluation: " +
|
|
325
|
-
"(a) list the files you changed in execution_evidence.files_changed, " +
|
|
326
|
-
"(b) run tests and report results in execution_evidence.test_results, " +
|
|
327
|
-
"(c) estimate your progress in execution_evidence.progress_estimate. " +
|
|
328
|
-
"If you genuinely completed the task without file changes or tests, " +
|
|
329
|
-
"explain why in detail in your output_summary.",
|
|
330
|
-
check: "empty_success",
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
const filesEmpty = ev.files_changed.length === 0;
|
|
334
|
-
const testsNotRun = ev.test_results === null;
|
|
335
|
-
if (!filesEmpty || !testsNotRun)
|
|
336
|
-
return null;
|
|
337
|
-
return makeEnforcementResult({
|
|
338
|
-
action: "reject",
|
|
339
|
-
reason: "Agent claims success but execution_evidence shows no files changed " +
|
|
340
|
-
"and no tests were run. There is no verifiable evidence of work.",
|
|
341
|
-
fix_instructions: "You must provide verifiable evidence: " +
|
|
342
|
-
"(a) list the files you changed in execution_evidence.files_changed, " +
|
|
343
|
-
"and (b) run tests and report results in execution_evidence.test_results. " +
|
|
344
|
-
"If you genuinely completed the task without file changes or tests, " +
|
|
345
|
-
"explain why in detail in your output_summary.",
|
|
346
|
-
check: "empty_success",
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
/** R8 (v2.12): Success claimed without any machine-verifiable evidence.
|
|
350
|
-
* The verification gate produced success_without_verified_evidence (error):
|
|
351
|
-
* zero verified claims, no test results. First occurrence → reject with
|
|
352
|
-
* concrete evidence requirements; second consecutive → terminate. */
|
|
339
|
+
/** R8 (v2.12/v3.7): The single success-evidence row. The verification gate
|
|
340
|
+
* produced success_without_verified_evidence (error) — either the empty/
|
|
341
|
+
* missing-evidence arm (v3.7: the ex-R3 empty_success posture moved into
|
|
342
|
+
* checkSuccessWithoutVerifiedEvidence) or the claims arm (zero verified
|
|
343
|
+
* claims). First occurrence → reject with concrete evidence requirements;
|
|
344
|
+
* second consecutive → terminate. */
|
|
353
345
|
function enforceSuccessWithoutVerifiedEvidence(flags, consecutiveRejections) {
|
|
354
346
|
const flag = flags.find((f) => f.check === CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE && f.severity === "error");
|
|
355
347
|
if (!flag)
|
|
@@ -493,12 +485,22 @@ function enforceScopeDrift(flags, selfEval, consecutiveRejections, vaultEntries
|
|
|
493
485
|
return makeEnforcementResult({
|
|
494
486
|
action: "reject",
|
|
495
487
|
reason: flag.detail,
|
|
488
|
+
// L3 (v3.7.x): option (b) previously told the agent to "extend the scope
|
|
489
|
+
// in your re-declared round_contract" — structurally unreachable: while a
|
|
490
|
+
// contract is ACTIVE, a replacement proposal is ignored as premature, so
|
|
491
|
+
// the guidance dead-ended into repeated rejects/termination. The only
|
|
492
|
+
// legal scope change is to CLOSE the active contract first: resubmit with
|
|
493
|
+
// outcome="blocked" (blocker naming the too-narrow scope) and the
|
|
494
|
+
// extended scope as the NEXT round's contract in the same submission.
|
|
496
495
|
fix_instructions: "Files changed outside the contract's declared scope. You must either: " +
|
|
497
|
-
"(a) revert the out-of-scope changes, or (b) extend the scope
|
|
498
|
-
"
|
|
499
|
-
"
|
|
500
|
-
"
|
|
501
|
-
"the
|
|
496
|
+
"(a) revert the out-of-scope changes, or (b) legally extend the scope: " +
|
|
497
|
+
"a replacement contract is IGNORED while the current one is active, so " +
|
|
498
|
+
"resubmit this work with outcome: \"blocked\" (blocker naming the " +
|
|
499
|
+
"too-narrow scope) plus the extended scope as the next round's " +
|
|
500
|
+
"round_contract, and explain the pivot in drift_clarification with " +
|
|
501
|
+
"concrete file paths (≥ 20 characters, real anchors). Weak or missing " +
|
|
502
|
+
"clarifications are rejected; repeated scope drift terminates the loop." +
|
|
503
|
+
escalation,
|
|
502
504
|
check: "round_scope_drift",
|
|
503
505
|
});
|
|
504
506
|
}
|
|
@@ -534,8 +536,9 @@ function enforceBacktrackNotRestored(flags, vaultEntries = [], currentRound = 0)
|
|
|
534
536
|
check: "backtrack_workspace_not_restored",
|
|
535
537
|
});
|
|
536
538
|
}
|
|
537
|
-
/** Shared
|
|
538
|
-
* only: it may veto a
|
|
539
|
+
/** Shared observation pipeline for the progress evaluator (stall window,
|
|
540
|
+
* flatline tier). Machine git motion is exculpatory only: it may veto a
|
|
541
|
+
* self-reported stall but can never create one. */
|
|
539
542
|
function evaluateStallWindow(tier, selfEval, currentRound, vaultEntries) {
|
|
540
543
|
const window = tier === "progress_stall"
|
|
541
544
|
? 3
|
|
@@ -582,8 +585,9 @@ function evaluateStallWindow(tier, selfEval, currentRound, vaultEntries) {
|
|
|
582
585
|
}
|
|
583
586
|
return { tier, detail, window, threshold };
|
|
584
587
|
}
|
|
585
|
-
/** Shared
|
|
586
|
-
* keeps its steeper threshold-0 ladder while
|
|
588
|
+
/** Shared progress escalation and backtrack deadlock guard (v3.7: one
|
|
589
|
+
* evaluator). The flatline tier keeps its steeper threshold-0 ladder while
|
|
590
|
+
* using the same mechanics — the tier is an internal diagnostic only. */
|
|
587
591
|
function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiveRejections) {
|
|
588
592
|
const { tier, detail, window, threshold } = verdict;
|
|
589
593
|
const flatline = tier === "progress_flatline";
|
|
@@ -592,18 +596,21 @@ function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiv
|
|
|
592
596
|
? `Progress has been ${detail} for ${window} consecutive rounds.`
|
|
593
597
|
: `Progress has stalled: ${detail} over the last ${window} rounds` +
|
|
594
598
|
`${threshold > 0 ? ` (delta < ${(threshold * 100).toFixed(0)}% each round)` : ""}.`;
|
|
599
|
+
// v3.7: both tiers emit the single progress_stall check id — the tier
|
|
600
|
+
// survives only in the reason/fix wording (former progress_flatline id).
|
|
601
|
+
const check = "progress_stall";
|
|
595
602
|
if ((flatline && consecutiveRejections >= 1) || consecutiveRejections >= 2) {
|
|
596
603
|
return makeEnforcementResult({
|
|
597
604
|
action: "terminate",
|
|
598
605
|
reason: `${stalledReason} The stall persisted after escalation. Terminating loop.`,
|
|
599
|
-
check
|
|
606
|
+
check,
|
|
600
607
|
});
|
|
601
608
|
}
|
|
602
609
|
if (consecutiveRejections >= 1 && !escalation) {
|
|
603
610
|
return makeEnforcementResult({
|
|
604
611
|
action: "terminate",
|
|
605
612
|
reason: `${stalledReason} The stall persisted after the previous rejection. Terminating loop.`,
|
|
606
|
-
check
|
|
613
|
+
check,
|
|
607
614
|
});
|
|
608
615
|
}
|
|
609
616
|
const escalated = flatline ? escalation : consecutiveRejections >= 1 && escalation;
|
|
@@ -612,7 +619,7 @@ function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiv
|
|
|
612
619
|
return makeEnforcementResult({
|
|
613
620
|
action: "terminate",
|
|
614
621
|
reason: `${stalledReason} A backtrack already fired for this round; the stall persists after rollback.`,
|
|
615
|
-
check
|
|
622
|
+
check,
|
|
616
623
|
});
|
|
617
624
|
}
|
|
618
625
|
return makeEnforcementResult({
|
|
@@ -621,14 +628,14 @@ function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiv
|
|
|
621
628
|
fix_instructions: flatline
|
|
622
629
|
? "Progress is exactly flat. After rollback, choose a radically different approach."
|
|
623
630
|
: "After rollback, do not repeat the stalled approach; choose a different technique or task decomposition.",
|
|
624
|
-
check
|
|
631
|
+
check,
|
|
625
632
|
});
|
|
626
633
|
}
|
|
627
634
|
if (flatline && !escalation) {
|
|
628
635
|
return makeEnforcementResult({
|
|
629
636
|
action: "terminate",
|
|
630
637
|
reason: `${stalledReason} The agent is making zero forward motion and cannot recover.`,
|
|
631
|
-
check
|
|
638
|
+
check,
|
|
632
639
|
});
|
|
633
640
|
}
|
|
634
641
|
return makeEnforcementResult({
|
|
@@ -638,17 +645,20 @@ function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiv
|
|
|
638
645
|
? "Explain the fundamental blocker, choose a radically different approach, and set a concrete verifiable goal."
|
|
639
646
|
: "Explain the blocker, choose a different technique or task decomposition, and set a concrete verifiable goal for this retry.") +
|
|
640
647
|
(escalated ? buildEscalationNotice() : ""),
|
|
641
|
-
check
|
|
648
|
+
check,
|
|
642
649
|
});
|
|
643
650
|
}
|
|
651
|
+
/** v3.7: The single progress evaluator (former R4 stall and R5 flatline rows
|
|
652
|
+
* merged into one). The stall-tier window is consulted first — under the
|
|
653
|
+
* default positive threshold its predicate covers exactly-flat runs too,
|
|
654
|
+
* reproducing the v3.6 R4-before-R5 shadowing; the flatline tier is
|
|
655
|
+
* consulted only when the stall predicate cannot hold (threshold ≤ 0,
|
|
656
|
+
* shorter lookback configs), where it applies its steeper disposition
|
|
657
|
+
* ladder. Machine git motion is exculpatory only — it may veto a
|
|
658
|
+
* self-reported stall but can never create one. */
|
|
644
659
|
function enforceProgressStall(selfEval, _flags, currentRound, vaultEntries, consecutiveRejections) {
|
|
645
|
-
const verdict = evaluateStallWindow("progress_stall", selfEval, currentRound, vaultEntries)
|
|
646
|
-
|
|
647
|
-
? resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiveRejections)
|
|
648
|
-
: null;
|
|
649
|
-
}
|
|
650
|
-
function enforceProgressFlatline(selfEval, _flags, currentRound, vaultEntries, consecutiveRejections) {
|
|
651
|
-
const verdict = evaluateStallWindow("progress_flatline", selfEval, currentRound, vaultEntries);
|
|
660
|
+
const verdict = evaluateStallWindow("progress_stall", selfEval, currentRound, vaultEntries)
|
|
661
|
+
?? evaluateStallWindow("progress_flatline", selfEval, currentRound, vaultEntries);
|
|
652
662
|
return verdict
|
|
653
663
|
? resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiveRejections)
|
|
654
664
|
: null;
|
|
@@ -707,8 +717,8 @@ function extractAnchors(clarification) {
|
|
|
707
717
|
for (const match of clarification.matchAll(/[c]r?-[a-f0-9]{8}|sg-[a-f0-9]{8}/gi)) {
|
|
708
718
|
anchors.add(match[0].toLowerCase());
|
|
709
719
|
}
|
|
710
|
-
for (const
|
|
711
|
-
anchors.add(
|
|
720
|
+
for (const path of extractFilePathTokens(clarification)) {
|
|
721
|
+
anchors.add(path);
|
|
712
722
|
}
|
|
713
723
|
return [...anchors];
|
|
714
724
|
}
|
|
@@ -951,6 +961,31 @@ function buildDiagnosticGap(flags) {
|
|
|
951
961
|
lines.push("");
|
|
952
962
|
return lines.join("\n");
|
|
953
963
|
}
|
|
964
|
+
const RULE_TABLE = [
|
|
965
|
+
// R1/R2 join the contradiction umbrella per the documented four-class model
|
|
966
|
+
// (AGENTS/README/CHANGELOG): a row's category is its enforcement action
|
|
967
|
+
// class, NOT the trigger check's gate domain — claim-vs-self-reported-facts
|
|
968
|
+
// rows (outcome_success_contradiction included) belong to this class too.
|
|
969
|
+
{ category: "evidence_contradiction", checks: ["success_with_remaining_criteria"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
970
|
+
{ category: "evidence_contradiction", checks: ["recurring_violation"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
971
|
+
{ category: "evidence_contradiction", checks: ["required_command_failed", "command_evidence_mismatch", "outcome_success_contradiction"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
972
|
+
{ category: "evidence_contradiction", checks: ["verification_entrypoint_modified"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
973
|
+
{ category: "contract_scope", checks: ["contract_completion_unverified"], ladder: "internal" },
|
|
974
|
+
{ category: "contract_scope", checks: ["premature_boundary"], ladder: "internal" },
|
|
975
|
+
{ category: "evidence_contradiction", checks: ["success_without_verified_evidence"], ladder: "internal" },
|
|
976
|
+
{ category: "contract_scope", checks: ["round_scope_drift"], ladder: "internal", clarificationAllowed: true },
|
|
977
|
+
{ category: "progress_recovery", checks: ["progress_stall"], ladder: "internal", backtrackAllowed: true },
|
|
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 },
|
|
981
|
+
// v3.7.1: cited gates without an approved human decision
|
|
982
|
+
{ category: "contract_scope", checks: ["user_gate_unresolved"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
983
|
+
];
|
|
984
|
+
const RULE_TABLE_BY_ID = new Map();
|
|
985
|
+
for (const row of RULE_TABLE) {
|
|
986
|
+
for (const id of row.checks)
|
|
987
|
+
RULE_TABLE_BY_ID.set(id, row);
|
|
988
|
+
}
|
|
954
989
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
955
990
|
// Main entry point
|
|
956
991
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -968,84 +1003,119 @@ function buildDiagnosticGap(flags) {
|
|
|
968
1003
|
* @param consecutiveRejections How many consecutive rounds have already been rejected.
|
|
969
1004
|
* Starts at 0; increments on each reject; resets on accept.
|
|
970
1005
|
*/
|
|
971
|
-
|
|
1006
|
+
/** v3.7.1: user_gate_unresolved — the round cites gates without an
|
|
1007
|
+
* approved human decision. Reject; the uniform ladder escalates repeats
|
|
1008
|
+
* (the agent should pause and obtain approval — never resubmit a claim of
|
|
1009
|
+
* completed high-risk work). */
|
|
1010
|
+
function enforceUserGateUnresolved(flags) {
|
|
1011
|
+
const flag = flags.find((f) => f.check === CHECK_USER_GATE_UNRESOLVED && f.severity === "error");
|
|
1012
|
+
if (!flag)
|
|
1013
|
+
return null;
|
|
1014
|
+
return makeEnforcementResult({
|
|
1015
|
+
action: "reject",
|
|
1016
|
+
reason: flag.detail,
|
|
1017
|
+
fix_instructions: "The round depends on a high-risk action with no approved human " +
|
|
1018
|
+
"decision. Run loopforge_gate_check with the action, present the " +
|
|
1019
|
+
"approval question, and call loopforge_gate_resolve only after the " +
|
|
1020
|
+
"human decides. Then resubmit with the approved gate id in " +
|
|
1021
|
+
"evaluation.gate_ids. Do NOT claim the action is done while it is " +
|
|
1022
|
+
"unapproved — safe preparation work never needs a gate.",
|
|
1023
|
+
check: CHECK_USER_GATE_UNRESOLVED,
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
export function enforceRound(selfEval, verifyResult, currentRound, vaultEntries, consecutiveRejections = 0,
|
|
972
1027
|
/** v2.12: Current clarification streak for R7 escalation. */
|
|
973
|
-
driftClarificationStreak = 0
|
|
1028
|
+
driftClarificationStreak = 0,
|
|
1029
|
+
/** L4 (v3.7.x): which check rejected the PREVIOUS round. The coordinator
|
|
1030
|
+
* resets consecutiveRejections to 1 whenever the check changes, so the
|
|
1031
|
+
* counter only ever measures one check's streak — this field names that
|
|
1032
|
+
* check and lets uniform rows escalate on THEIR OWN streak instead of
|
|
1033
|
+
* inheriting an unrelated history. */
|
|
1034
|
+
lastRejectionCheck = "") {
|
|
974
1035
|
const { flags } = verifyResult;
|
|
975
|
-
// Run enforcement rules in priority order
|
|
976
|
-
//
|
|
1036
|
+
// Run enforcement rules in priority order — the array order IS the
|
|
1037
|
+
// priority; the R-labels in the comments are historical (RULE_TABLE is
|
|
1038
|
+
// the semantic description). The first rule that fires wins.
|
|
977
1039
|
const rules = [
|
|
978
1040
|
() => enforceSuccessWithRemainingCriteria(flags),
|
|
979
1041
|
() => enforceRecurringViolation(flags),
|
|
980
|
-
|
|
981
|
-
//
|
|
982
|
-
// outcome claim contradicts the success claim before commit
|
|
1042
|
+
// v2.14: R-EVID — a required command failure, hidden test failures, or
|
|
1043
|
+
// a self-contradictory outcome claim contradicts the success claim
|
|
983
1044
|
() => enforceEvidenceContradiction(flags),
|
|
984
1045
|
// v3.3: R-EVID-VERIFY — the verification command entrypoint changed in
|
|
985
|
-
// the same round it ran
|
|
986
|
-
//
|
|
987
|
-
// status degrades to unavailable), but this reason is more specific.
|
|
1046
|
+
// the same round it ran. Runs before R8: when the entrypoint is tainted
|
|
1047
|
+
// R8 also fires, but this reason is more specific.
|
|
988
1048
|
() => enforceVerificationEntrypointTampered(flags),
|
|
989
1049
|
// v3.5: contract completion claimed without its verification_plan
|
|
990
1050
|
// commands passing — runs before R-C1: the completion-truth question
|
|
991
|
-
// outranks boundary nuance
|
|
992
|
-
// evidence gets the completion reason first).
|
|
1051
|
+
// outranks boundary nuance.
|
|
993
1052
|
() => enforceContractCompletionUnverified(flags, consecutiveRejections),
|
|
994
1053
|
// v3.3: R-C1 — contract boundary claimed prematurely (contract-specific
|
|
995
1054
|
// wording and check counting; runs before R8 so contract rounds get the
|
|
996
|
-
// contract's own reason.
|
|
1055
|
+
// contract's own reason.)
|
|
997
1056
|
() => enforcePrematureBoundary(flags, consecutiveRejections),
|
|
998
|
-
// v2.12: R8 — success
|
|
1057
|
+
// v2.12/v3.7: R8 — the single success-evidence row (empty/missing-evidence
|
|
1058
|
+
// arm merged from the former R3 empty_success posture + claims arm)
|
|
999
1059
|
() => enforceSuccessWithoutVerifiedEvidence(flags, consecutiveRejections),
|
|
1000
1060
|
// v3.3: R-C2 — files changed outside the contract's declared scope
|
|
1001
1061
|
() => enforceScopeDrift(flags, selfEval, consecutiveRejections, vaultEntries),
|
|
1002
|
-
// v3.
|
|
1003
|
-
//
|
|
1004
|
-
// too (reject → backtrack/terminate). R5 stays as the reserved severity
|
|
1005
|
-
// tier (see its doc comment: active when progress_stall_threshold <= 0
|
|
1006
|
-
// or after a future window split).
|
|
1062
|
+
// v3.7: single progress evaluator (former R4/R5 slots merged) —
|
|
1063
|
+
// reject → backtrack → terminate with the deadlock guard
|
|
1007
1064
|
() => enforceProgressStall(selfEval, flags, currentRound, vaultEntries, consecutiveRejections),
|
|
1008
|
-
|
|
1065
|
+
// R6: rejection-counter catch-all (evaluated only when no row above
|
|
1066
|
+
// fired — a clean-looking round carrying a high persisted counter)
|
|
1067
|
+
// v3.7.1: cited gate without an approved human decision
|
|
1068
|
+
() => enforceUserGateUnresolved(flags),
|
|
1009
1069
|
() => 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
|
|
1010
1072
|
() => enforceIntentDrift(flags, selfEval, consecutiveRejections, driftClarificationStreak, vaultEntries),
|
|
1011
1073
|
// v2.12: R9 — post-backtrack workspace not restored
|
|
1012
1074
|
() => enforceBacktrackNotRestored(flags, vaultEntries, currentRound),
|
|
1013
1075
|
];
|
|
1014
|
-
// v3.
|
|
1015
|
-
// R-EVID-VERIFY)
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
//
|
|
1019
|
-
//
|
|
1020
|
-
//
|
|
1021
|
-
//
|
|
1022
|
-
//
|
|
1023
|
-
// (escalation notice), terminate on the third consecutive occurrence.
|
|
1024
|
-
// Rules with their own ladder (R4/R5/R6/R7/R8/R9/R-C1/R-C2) are untouched.
|
|
1025
|
-
const UNLADDERED_REJECT_CHECKS = new Set([
|
|
1026
|
-
"success_with_remaining_criteria", // R1
|
|
1027
|
-
"recurring_violation", // R2
|
|
1028
|
-
"empty_success", // R3
|
|
1029
|
-
"required_command_failed", // R-EVID
|
|
1030
|
-
"command_evidence_mismatch", // R-EVID
|
|
1031
|
-
"outcome_success_contradiction", // R-EVID
|
|
1032
|
-
"verification_entrypoint_modified", // R-EVID-VERIFY
|
|
1033
|
-
]);
|
|
1076
|
+
// v3.7: uniform-ladder escalation. Rows without their own escalation
|
|
1077
|
+
// ladder (R1/R2/R-EVID/R-EVID-VERIFY) escalate on the session's
|
|
1078
|
+
// consecutive-rejection count: repeat rejections carry the escalation
|
|
1079
|
+
// notice (when enabled — the pre-v3.7 comment promised this notice but the
|
|
1080
|
+
// UNLADDERED name-set patch never appended it), and a third consecutive
|
|
1081
|
+
// occurrence terminates. This replaces the UNLADDERED_REJECT_CHECKS patch,
|
|
1082
|
+
// which existed because the generic R6 ladder sits AFTER these rows and a
|
|
1083
|
+
// persistent offender's round never reached it. Rows with their own ladder
|
|
1084
|
+
// (RULE_TABLE.ladder === "internal" / "counter") are untouched.
|
|
1034
1085
|
for (const rule of rules) {
|
|
1035
|
-
|
|
1086
|
+
let result = rule();
|
|
1036
1087
|
if (!result)
|
|
1037
1088
|
continue;
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1089
|
+
const row = result.check !== undefined
|
|
1090
|
+
? RULE_TABLE_BY_ID.get(result.check)
|
|
1091
|
+
: undefined;
|
|
1092
|
+
if (result.action === "reject" && row && row.ladder === "uniform") {
|
|
1093
|
+
// L4: uniform rows escalate on their OWN consecutive streak only.
|
|
1094
|
+
// consecutiveRejections is the streak of the PREVIOUS round's check
|
|
1095
|
+
// (the coordinator resets it to 1 whenever the check changes) — a
|
|
1096
|
+
// streak earned by a DIFFERENT check must not convert this first
|
|
1097
|
+
// occurrence into termination. The user_gate row is the sharp case:
|
|
1098
|
+
// its recovery is human approval, so a gate cited after unrelated
|
|
1099
|
+
// rejections must reject with instructions, not kill the loop.
|
|
1100
|
+
const ownStreak = lastRejectionCheck === result.check
|
|
1101
|
+
? consecutiveRejections
|
|
1102
|
+
: 0;
|
|
1103
|
+
if (ownStreak >= (row.terminalAfter ?? 2)) {
|
|
1104
|
+
return makeEnforcementResult({
|
|
1105
|
+
action: "terminate",
|
|
1106
|
+
reason: `${ownStreak + 1} consecutive rejections for the same ` +
|
|
1107
|
+
`issue (${result.check}) without resolution — the agent cannot ` +
|
|
1108
|
+
`correct it even after escalation.`,
|
|
1109
|
+
check: result.check,
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
if (ownStreak >= 1 && row.noticeOnRepeat &&
|
|
1113
|
+
getPolicy().engine.enforcement_escalation_enabled) {
|
|
1114
|
+
result = makeEnforcementResult({
|
|
1115
|
+
...result,
|
|
1116
|
+
fix_instructions: result.fix_instructions + buildEscalationNotice(),
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1049
1119
|
}
|
|
1050
1120
|
return result;
|
|
1051
1121
|
}
|