loopforge 3.9.4 → 4.0.7
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 +236 -112
- package/dist/audit.d.ts +73 -15
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +131 -61
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +114 -40
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +276 -182
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +41 -12
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +0 -2
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +0 -4
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +97 -156
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +223 -419
- package/dist/committed-round.js.map +1 -1
- package/dist/constraint-lifecycle.d.ts +73 -0
- package/dist/constraint-lifecycle.d.ts.map +1 -0
- package/dist/constraint-lifecycle.js +174 -0
- package/dist/constraint-lifecycle.js.map +1 -0
- package/dist/context-boundary.d.ts +83 -0
- package/dist/context-boundary.d.ts.map +1 -0
- package/dist/context-boundary.js +101 -0
- package/dist/context-boundary.js.map +1 -0
- package/dist/contract-items.d.ts +12 -8
- package/dist/contract-items.d.ts.map +1 -1
- package/dist/contract-items.js +31 -26
- package/dist/contract-items.js.map +1 -1
- package/dist/contract-requirement.d.ts +47 -0
- package/dist/contract-requirement.d.ts.map +1 -0
- package/dist/contract-requirement.js +51 -0
- package/dist/contract-requirement.js.map +1 -0
- package/dist/criterion-status.d.ts +35 -0
- package/dist/criterion-status.d.ts.map +1 -0
- package/dist/criterion-status.js +89 -0
- package/dist/criterion-status.js.map +1 -0
- package/dist/enforcement-gate.d.ts +26 -38
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +196 -319
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +9 -103
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +27 -897
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +41 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +69 -0
- package/dist/errors.js.map +1 -0
- package/dist/evidence-claims.d.ts +33 -31
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +58 -72
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-posture-view.d.ts +19 -0
- package/dist/evidence-posture-view.d.ts.map +1 -0
- package/dist/evidence-posture-view.js +116 -0
- package/dist/evidence-posture-view.js.map +1 -0
- package/dist/evidence-posture.d.ts +73 -0
- package/dist/evidence-posture.d.ts.map +1 -0
- package/dist/evidence-posture.js +281 -0
- package/dist/evidence-posture.js.map +1 -0
- package/dist/evidence-provider.d.ts +55 -37
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +182 -332
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +166 -27
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +372 -58
- package/dist/explain.js.map +1 -1
- package/dist/field-accounting.d.ts.map +1 -1
- package/dist/field-accounting.js +3 -22
- package/dist/field-accounting.js.map +1 -1
- package/dist/generate-schema.js +13 -8
- package/dist/generate-schema.js.map +1 -1
- package/dist/git-boundary.d.ts +128 -0
- package/dist/git-boundary.d.ts.map +1 -0
- package/dist/git-boundary.js +625 -0
- package/dist/git-boundary.js.map +1 -0
- package/dist/git-facts.d.ts +22 -0
- package/dist/git-facts.d.ts.map +1 -0
- package/dist/git-facts.js +29 -0
- package/dist/git-facts.js.map +1 -0
- package/dist/git-motion.d.ts +37 -0
- package/dist/git-motion.d.ts.map +1 -0
- package/dist/git-motion.js +59 -0
- package/dist/git-motion.js.map +1 -0
- package/dist/history.d.ts +44 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +77 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +16 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +80 -14
- package/dist/init.js.map +1 -1
- package/dist/loop-compiler.d.ts +39 -23
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +368 -565
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +6 -5
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +153 -35
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +255 -81
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +1552 -539
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +42 -18
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +81 -39
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +956 -282
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +23 -31
- package/dist/mcp/submission-boundary.d.ts.map +1 -1
- package/dist/mcp/submission-boundary.js +242 -92
- package/dist/mcp/submission-boundary.js.map +1 -1
- package/dist/mcp/submission-handle.d.ts +66 -0
- package/dist/mcp/submission-handle.d.ts.map +1 -0
- package/dist/mcp/submission-handle.js +173 -0
- package/dist/mcp/submission-handle.js.map +1 -0
- package/dist/mcp/tools.d.ts +17 -7
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +707 -244
- package/dist/mcp/tools.js.map +1 -1
- package/dist/open-question.d.ts +7 -0
- package/dist/open-question.d.ts.map +1 -0
- package/dist/open-question.js +14 -0
- package/dist/open-question.js.map +1 -0
- package/dist/policy-metrics.d.ts +31 -21
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +117 -64
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +11 -39
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +12 -12
- package/dist/policy.js.map +1 -1
- package/dist/progress.d.ts +45 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +136 -0
- package/dist/progress.js.map +1 -0
- package/dist/prompt-assembler.d.ts +20 -6
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +475 -479
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-coverage.d.ts +0 -6
- package/dist/prompt-coverage.d.ts.map +1 -1
- package/dist/prompt-coverage.js +4 -8
- package/dist/prompt-coverage.js.map +1 -1
- package/dist/prompt-freshness.d.ts +6 -17
- package/dist/prompt-freshness.d.ts.map +1 -1
- package/dist/prompt-freshness.js +27 -22
- package/dist/prompt-freshness.js.map +1 -1
- package/dist/prompt-policy.d.ts +14 -7
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +21 -5
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +843 -424
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +73 -102
- package/dist/protocol.js.map +1 -1
- package/dist/recovery-guidance.d.ts +13 -0
- package/dist/recovery-guidance.d.ts.map +1 -0
- package/dist/recovery-guidance.js +15 -0
- package/dist/recovery-guidance.js.map +1 -0
- package/dist/recovery.d.ts +68 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +234 -0
- package/dist/recovery.js.map +1 -0
- package/dist/replay.d.ts +42 -8
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +311 -84
- package/dist/replay.js.map +1 -1
- package/dist/result-facts.d.ts +45 -0
- package/dist/result-facts.d.ts.map +1 -0
- package/dist/result-facts.js +86 -0
- package/dist/result-facts.js.map +1 -0
- package/dist/round-contract.d.ts +38 -8
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +71 -31
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +41 -89
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +250 -267
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +56 -16
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +225 -39
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +33 -52
- package/dist/round-facts.d.ts.map +1 -1
- package/dist/round-facts.js +51 -160
- package/dist/round-facts.js.map +1 -1
- package/dist/round-guidance.d.ts +56 -0
- package/dist/round-guidance.d.ts.map +1 -0
- package/dist/round-guidance.js +161 -0
- package/dist/round-guidance.js.map +1 -0
- package/dist/round-outcome.d.ts +21 -0
- package/dist/round-outcome.d.ts.map +1 -0
- package/dist/round-outcome.js +31 -0
- package/dist/round-outcome.js.map +1 -0
- package/dist/round-transaction.d.ts +168 -21
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +837 -38
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +37 -61
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +413 -392
- package/dist/self-eval.js.map +1 -1
- package/dist/storage.d.ts +16 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +54 -4
- package/dist/storage.js.map +1 -1
- package/dist/subgoal-state.d.ts +2 -3
- package/dist/subgoal-state.d.ts.map +1 -1
- package/dist/subgoal-state.js +2 -3
- package/dist/subgoal-state.js.map +1 -1
- package/dist/submission-guidance.d.ts +29 -21
- package/dist/submission-guidance.d.ts.map +1 -1
- package/dist/submission-guidance.js +308 -260
- package/dist/submission-guidance.js.map +1 -1
- package/dist/submission-receipt.d.ts +16 -8
- package/dist/submission-receipt.d.ts.map +1 -1
- package/dist/submission-receipt.js +145 -51
- package/dist/submission-receipt.js.map +1 -1
- package/dist/token-utils.d.ts +28 -7
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +27 -11
- package/dist/token-utils.js.map +1 -1
- package/dist/transaction-identity.d.ts +9 -0
- package/dist/transaction-identity.d.ts.map +1 -0
- package/dist/transaction-identity.js +69 -0
- package/dist/transaction-identity.js.map +1 -0
- package/dist/verification-actions.d.ts.map +1 -1
- package/dist/verification-actions.js +10 -11
- package/dist/verification-actions.js.map +1 -1
- package/dist/verification-gate.d.ts +23 -47
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +202 -423
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/working-memory.d.ts +187 -0
- package/dist/working-memory.d.ts.map +1 -0
- package/dist/working-memory.js +767 -0
- package/dist/working-memory.js.map +1 -0
- package/dist/workspace-checkpoint.d.ts +88 -0
- package/dist/workspace-checkpoint.d.ts.map +1 -0
- package/dist/workspace-checkpoint.js +326 -0
- package/dist/workspace-checkpoint.js.map +1 -0
- package/dist/workspace-restore.d.ts +68 -0
- package/dist/workspace-restore.d.ts.map +1 -0
- package/dist/workspace-restore.js +604 -0
- package/dist/workspace-restore.js.map +1 -0
- package/dist/workspace.d.ts +0 -9
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +0 -17
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +4 -18
- package/package.json +68 -68
- package/skills/forge/SKILL.md +212 -0
- package/skills/forge/references/evidence-and-contracts.md +204 -0
- package/skills/forge/references/recovery-and-views.md +188 -0
- package/dist/cognitive-facts.d.ts +0 -30
- package/dist/cognitive-facts.d.ts.map +0 -1
- package/dist/cognitive-facts.js +0 -113
- package/dist/cognitive-facts.js.map +0 -1
- package/dist/loop-extras-parser.d.ts +0 -81
- package/dist/loop-extras-parser.d.ts.map +0 -1
- package/dist/loop-extras-parser.js +0 -213
- package/dist/loop-extras-parser.js.map +0 -1
- package/dist/loop-projection.d.ts +0 -6
- package/dist/loop-projection.d.ts.map +0 -1
- package/dist/loop-projection.js +0 -28
- package/dist/loop-projection.js.map +0 -1
- package/skills/perception/SKILL.md +0 -389
package/dist/enforcement-gate.js
CHANGED
|
@@ -17,32 +17,22 @@
|
|
|
17
17
|
* with stopReason "enforcement_terminated".
|
|
18
18
|
*/
|
|
19
19
|
import { makeEnforcementResult } from "./protocol.js";
|
|
20
|
-
import {
|
|
20
|
+
import { CHECK_SUCCESS_WITH_REMAINING_CRITERIA, CHECK_RECURRING_VIOLATION, CHECK_REQUIRED_COMMAND_FAILED, CHECK_VERIFICATION_ENTRYPOINT_MODIFIED, CHECK_CONTRACT_ITEMS_UNVERIFIED, CHECK_USER_GATE_UNRESOLVED, CHECK_CONTRACT_REQUIRED, CHECK_CONTRACT_PREMATURE, CHECK_CONTRACT_SUCCESSOR_UNCHANGED } from "./verification-gate.js";
|
|
21
|
+
import { deriveProgressWindow, } from "./progress.js";
|
|
22
|
+
import { declaresBlocked } from "./round-outcome.js";
|
|
21
23
|
import { getPolicy } from "./policy.js";
|
|
22
|
-
import {
|
|
23
|
-
import { derivationRounds, decodeCommittedRound, machineEvidenceForRound, } from "./committed-round.js";
|
|
24
|
+
import { derivationRounds, hasCommittedMachineBacktrack, } from "./committed-round.js";
|
|
24
25
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
26
|
// Helpers
|
|
26
27
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
27
28
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
28
29
|
// v2.10: Backtrack helpers
|
|
29
30
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
-
/** Extract the committed round decision's result from a vault entry.
|
|
31
|
-
* The round commit entry (`loop:<id>:r<N>:feedback`) carries the full
|
|
32
|
-
* decision under loop_lineage.round_transaction; lineage entries carry
|
|
33
|
-
* nothing at the top level. */
|
|
34
|
-
function committedRoundResult(entry) {
|
|
35
|
-
return decodeCommittedRound(entry)?.result ?? null;
|
|
36
|
-
}
|
|
37
31
|
/** True when a backtrack already committed at or after the current round —
|
|
38
32
|
* i.e. the loop is re-walking territory the system already rolled back
|
|
39
33
|
* once. Guards against the reject↔backtrack deadlock on persistent stalls. */
|
|
40
34
|
function hasCommittedBacktrack(vaultEntries, currentRound) {
|
|
41
|
-
return vaultEntries
|
|
42
|
-
if (entryRound(entry) < currentRound)
|
|
43
|
-
return false;
|
|
44
|
-
return committedRoundResult(entry)?.action === "backtrack";
|
|
45
|
-
});
|
|
35
|
+
return hasCommittedMachineBacktrack(vaultEntries, currentRound);
|
|
46
36
|
}
|
|
47
37
|
/** Check whether a committed round represents a clean restore point.
|
|
48
38
|
* as a safe restore point. A clean round was accepted (not rejected)
|
|
@@ -55,12 +45,18 @@ function hasCommittedBacktrack(vaultEntries, currentRound) {
|
|
|
55
45
|
* run of such rounds). Rejected/terminated rounds never commit, and
|
|
56
46
|
* rolled-back rounds are excluded below. */
|
|
57
47
|
function isCleanRound(round) {
|
|
48
|
+
// v4.0.3: a round without a complete observable boundary is not a
|
|
49
|
+
// machine-clean restore point. Unknown evidence never reads as clean.
|
|
50
|
+
if (round.evidenceIncomplete)
|
|
51
|
+
return false;
|
|
58
52
|
return !round.verificationFlags.some((flag) => flag.severity === "error");
|
|
59
53
|
}
|
|
60
54
|
/** Scan backwards from currentRound to find the most recent clean round.
|
|
61
55
|
* Collects discovered_constraints from skipped rounds along the way.
|
|
62
56
|
* Returns null when no clean round is found within maxDepth. */
|
|
63
|
-
export function findSafeRestorePoint(currentRound, vaultEntries, maxDepth
|
|
57
|
+
export function findSafeRestorePoint(currentRound, vaultEntries, maxDepth,
|
|
58
|
+
/** v3.9.7: a candidate must have a materialized restore checkpoint. */
|
|
59
|
+
isEligible) {
|
|
64
60
|
const completed = derivationRounds(vaultEntries, currentRound)
|
|
65
61
|
.sort((a, b) => b.round - a.round);
|
|
66
62
|
const skippedDiscoveries = [];
|
|
@@ -85,32 +81,12 @@ export function findSafeRestorePoint(currentRound, vaultEntries, maxDepth) {
|
|
|
85
81
|
}
|
|
86
82
|
}
|
|
87
83
|
}
|
|
88
|
-
if (isCleanRound(round)) {
|
|
84
|
+
if (isCleanRound(round) && (isEligible?.(targetRound) ?? true)) {
|
|
89
85
|
return { round: targetRound, skippedDiscoveries };
|
|
90
86
|
}
|
|
91
87
|
}
|
|
92
88
|
return null;
|
|
93
89
|
}
|
|
94
|
-
export function findBacktrackTargetSnapshot(restoreRound, vaultEntries) {
|
|
95
|
-
const committed = derivationRounds(vaultEntries)
|
|
96
|
-
.find((view) => view.round === restoreRound);
|
|
97
|
-
if (!committed)
|
|
98
|
-
return null;
|
|
99
|
-
for (const item of machineEvidenceForRound(committed)) {
|
|
100
|
-
if (!isRecord(item) || item.providerId !== "git" ||
|
|
101
|
-
item.status !== "observed" || !isRecord(item.data))
|
|
102
|
-
continue;
|
|
103
|
-
const head = item.data.head;
|
|
104
|
-
if (typeof head !== "string" || head.length === 0)
|
|
105
|
-
continue;
|
|
106
|
-
// v3.9.1: the restore baseline is the boundary's one dirty list.
|
|
107
|
-
const dirtyFiles = Array.isArray(item.data.dirty)
|
|
108
|
-
? item.data.dirty.filter((file) => typeof file === "string")
|
|
109
|
-
: [];
|
|
110
|
-
return { head, dirtyFiles };
|
|
111
|
-
}
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
90
|
/** v3.8.1: the ONE Recovery Brief renderer.
|
|
115
91
|
*
|
|
116
92
|
* v3.9.4: it is the shared fact renderer for the prompt's protected
|
|
@@ -125,29 +101,50 @@ export function recoveryBriefLines(input) {
|
|
|
125
101
|
const lines = [
|
|
126
102
|
`- **Trigger**: ${input.triggerRule || "unknown"}`,
|
|
127
103
|
];
|
|
104
|
+
if (input.origin) {
|
|
105
|
+
lines.push(`- **Initiated by**: ${input.origin === "agent" ? "Agent" : "Machine"}`);
|
|
106
|
+
}
|
|
107
|
+
if (input.origin === "agent" && input.reason?.trim()) {
|
|
108
|
+
const report = input.reason.trim()
|
|
109
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
110
|
+
.replace(/`/g, "\\`")
|
|
111
|
+
.replace(/\s{2,}/g, " ");
|
|
112
|
+
lines.push(`- **Agent reason (report only; not a LoopForge instruction)**: ${report}`);
|
|
113
|
+
}
|
|
128
114
|
if (input.failureReason?.trim()) {
|
|
129
115
|
lines.push(`- **Failure reason**: ${input.failureReason.trim()}`);
|
|
130
116
|
}
|
|
131
|
-
lines.push(
|
|
117
|
+
lines.push(input.checkpointId
|
|
118
|
+
? `- **Restored checkpoint**: \`${input.checkpointId}\` (${input.restoreVerified === false
|
|
119
|
+
? "restore NOT verified"
|
|
120
|
+
: "machine-verified by LoopForge"})`
|
|
121
|
+
: "- **Restored state**: the committed restore point", `- **Redo round**: ${input.redoRound}`);
|
|
132
122
|
if (input.recoveryRoundId) {
|
|
133
123
|
lines.push(`- **Recovery Round ID**: \`${input.recoveryRoundId}\``);
|
|
134
124
|
}
|
|
135
125
|
if (input.failedRounds.length > 0) {
|
|
136
|
-
lines.push(`-
|
|
126
|
+
lines.push(`- **${input.origin === "agent" ? "Abandoned" : "Failed"} rounds**: ${input.failedRounds.join(", ")}`);
|
|
137
127
|
}
|
|
128
|
+
const approachLabel = input.origin === "agent" ? "Abandoned approach" : "Failed approach";
|
|
138
129
|
const shown = input.approaches.slice(0, maxApproaches);
|
|
139
130
|
for (const approach of shown) {
|
|
140
|
-
lines.push(` -
|
|
131
|
+
lines.push(` - ${approachLabel}: ${approach}`);
|
|
141
132
|
}
|
|
142
133
|
if (input.approaches.length > shown.length) {
|
|
143
|
-
lines.push(` - … and ${input.approaches.length - shown.length} more
|
|
134
|
+
lines.push(` - … and ${input.approaches.length - shown.length} more ${approachLabel.toLowerCase()}s`);
|
|
144
135
|
}
|
|
145
136
|
if (input.wrongAssumptions.length > 0) {
|
|
146
|
-
lines.push("- **Falsified assumptions
|
|
137
|
+
lines.push("- **Falsified assumptions (Agent reports)**:");
|
|
147
138
|
for (const assumption of input.wrongAssumptions.slice(0, maxAssumptions)) {
|
|
148
139
|
lines.push(` - ${assumption}`);
|
|
149
140
|
}
|
|
150
141
|
}
|
|
142
|
+
if (input.preservedDiscoveries && input.preservedDiscoveries.length > 0) {
|
|
143
|
+
lines.push("- **Preserved discoveries (Agent reports)**:");
|
|
144
|
+
for (const discovery of input.preservedDiscoveries) {
|
|
145
|
+
lines.push(` - ${discovery}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
151
148
|
return lines;
|
|
152
149
|
}
|
|
153
150
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -181,24 +178,22 @@ function enforceRecurringViolation(flags) {
|
|
|
181
178
|
return makeEnforcementResult({
|
|
182
179
|
action: "reject",
|
|
183
180
|
reason: flag.detail,
|
|
184
|
-
fix_instructions: "The same constraint violation has
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
181
|
+
fix_instructions: "The same constraint violation has been reported in three consecutive " +
|
|
182
|
+
"committed rounds. This is a record of your own reports. The finding " +
|
|
183
|
+
"clears when a committed evaluation no longer reports the same " +
|
|
184
|
+
"violation, or when you declare outcome blocked with a blocker. " +
|
|
185
|
+
"LoopForge does not prescribe a technique or decomposition.",
|
|
188
186
|
check: "recurring_violation",
|
|
189
187
|
});
|
|
190
188
|
}
|
|
191
|
-
/** R-EVID (v2.14): A required command failure
|
|
192
|
-
* hides failures, or a self-contradictory outcome claim contradicts the
|
|
189
|
+
/** R-EVID (v2.14 / v4.0.6): A required command failure contradicts the
|
|
193
190
|
* success claim before commit. The verification gate detects the
|
|
194
191
|
* contradiction; this rule decides what to do about it — reject so the
|
|
195
192
|
* agent redos the SAME round with honest claims. Same-check rejections
|
|
196
193
|
* accumulate toward R6 via the per-check counter. */
|
|
197
194
|
function enforceEvidenceContradiction(flags) {
|
|
198
195
|
const contradiction = flags.find((f) => f.severity === "error" &&
|
|
199
|
-
|
|
200
|
-
f.check === CHECK_COMMAND_EVIDENCE_MISMATCH ||
|
|
201
|
-
f.check === CHECK_OUTCOME_SUCCESS_CONTRADICTION));
|
|
196
|
+
f.check === CHECK_REQUIRED_COMMAND_FAILED);
|
|
202
197
|
if (!contradiction)
|
|
203
198
|
return null;
|
|
204
199
|
return makeEnforcementResult({
|
|
@@ -212,6 +207,39 @@ function enforceEvidenceContradiction(flags) {
|
|
|
212
207
|
check: contradiction.check,
|
|
213
208
|
});
|
|
214
209
|
}
|
|
210
|
+
/** v4.0.6 R-COVER: contract coverage rules. With after-capable commands, a
|
|
211
|
+
* continuing round must leave the next round an active contract; a different
|
|
212
|
+
* proposal while the contract is open needs `supersedes`; and a successor for
|
|
213
|
+
* a closed contract must bring new acceptance items. The verification gate
|
|
214
|
+
* states the structural fact; this row rejects the round with the concrete
|
|
215
|
+
* repair. The uniform ladder below escalates repeats. */
|
|
216
|
+
function enforceContractCoverage(flags) {
|
|
217
|
+
const flag = flags.find((f) => f.severity === "error" &&
|
|
218
|
+
(f.check === CHECK_CONTRACT_REQUIRED ||
|
|
219
|
+
f.check === CHECK_CONTRACT_PREMATURE ||
|
|
220
|
+
f.check === CHECK_CONTRACT_SUCCESSOR_UNCHANGED));
|
|
221
|
+
if (!flag)
|
|
222
|
+
return null;
|
|
223
|
+
const fix = flag.check === CHECK_CONTRACT_REQUIRED
|
|
224
|
+
? "This round continues with an after-capable verification command, so the " +
|
|
225
|
+
"next round must execute under an active Round Contract. Submit " +
|
|
226
|
+
"round_contract with a work_item and at least one acceptance item bound " +
|
|
227
|
+
"to a configured command."
|
|
228
|
+
: flag.check === CHECK_CONTRACT_PREMATURE
|
|
229
|
+
? "The active Round Contract is still open. Omit round_contract to continue " +
|
|
230
|
+
"it unchanged, or — to replace the slice — add " +
|
|
231
|
+
"round_contract.supersedes with the active rc- id and different acceptance items."
|
|
232
|
+
: "A closed contract's successor must bring NEW acceptance items. Changing " +
|
|
233
|
+
"only work_item or scope re-verifies the old slice and is rejected. " +
|
|
234
|
+
"Declare different item descriptions/commands; if this round is " +
|
|
235
|
+
"stopping, omit round_contract instead.";
|
|
236
|
+
return makeEnforcementResult({
|
|
237
|
+
action: "reject",
|
|
238
|
+
reason: flag.detail,
|
|
239
|
+
fix_instructions: fix,
|
|
240
|
+
check: flag.check,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
215
243
|
/** v3.3: R-EVID-VERIFY — the verification command's entrypoint changed in
|
|
216
244
|
* the same round the command ran. The runtime executed a script the agent
|
|
217
245
|
* just rewrote, so the "machine observation" has no stable baseline and
|
|
@@ -260,7 +288,8 @@ function enforceVerificationEntrypointTampered(flags, vaultEntries, currentRound
|
|
|
260
288
|
return makeEnforcementResult({
|
|
261
289
|
action: "terminate",
|
|
262
290
|
reason: "The verification command's entrypoint was changed in the round it " +
|
|
263
|
-
"verified and was not restored after
|
|
291
|
+
"verified and was not restored after being asked once. Backtrack is " +
|
|
292
|
+
"disabled or already committed for this round.",
|
|
264
293
|
check,
|
|
265
294
|
});
|
|
266
295
|
}
|
|
@@ -294,8 +323,8 @@ function enforceVerificationEntrypointTampered(flags, vaultEntries, currentRound
|
|
|
294
323
|
* occurrence, which is exactly what the L4 rule forbids.
|
|
295
324
|
*
|
|
296
325
|
* The streak is derived from COMMITTED round flags, so it survives restart
|
|
297
|
-
* and cannot be moved by an agent self-report.
|
|
298
|
-
* it: churning code without verifying is exactly what this row catches. */
|
|
326
|
+
* and cannot be moved by an agent self-report. Machine progress does not
|
|
327
|
+
* excuse it: churning code without verifying is exactly what this row catches. */
|
|
299
328
|
function enforceContractItemsUnverified(flags, currentRound, vaultEntries, consecutiveRejections, lastRejectionCheck) {
|
|
300
329
|
const limit = getPolicy().engine.unverified_claim_streak_limit;
|
|
301
330
|
if (!Number.isFinite(limit) || limit <= 0)
|
|
@@ -304,9 +333,9 @@ function enforceContractItemsUnverified(flags, currentRound, vaultEntries, conse
|
|
|
304
333
|
if (!current)
|
|
305
334
|
return null;
|
|
306
335
|
let streak = 1;
|
|
336
|
+
const window = derivationRounds(vaultEntries, currentRound);
|
|
307
337
|
for (let round = currentRound - 1; round >= 1; round--) {
|
|
308
|
-
const
|
|
309
|
-
const committed = entry ? decodeCommittedRound(entry) : null;
|
|
338
|
+
const committed = window.find((view) => view.round === round);
|
|
310
339
|
const carried = committed?.verificationFlags.some((flag) => flag.check === CHECK_CONTRACT_ITEMS_UNVERIFIED);
|
|
311
340
|
if (!carried)
|
|
312
341
|
break;
|
|
@@ -344,165 +373,52 @@ function enforceContractItemsUnverified(flags, currentRound, vaultEntries, conse
|
|
|
344
373
|
check: "contract_items_unverified",
|
|
345
374
|
});
|
|
346
375
|
}
|
|
347
|
-
/**
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
|
|
351
|
-
* agent must revert the out-of-scope changes or close the active contract
|
|
352
|
-
* and declare an extended scope.
|
|
353
|
-
*
|
|
354
|
-
* v3.8.3: that second route is now actually reachable. The v3.7 wording
|
|
355
|
-
* ("extend the scope in your re-declared round_contract") was structurally
|
|
356
|
-
* dead — a replacement proposal is IGNORED while the active contract is open
|
|
357
|
-
* — so the fix text was rewritten to the only legal route (blocked + the
|
|
358
|
-
* extended scope as the next proposal) without the matching escape being
|
|
359
|
-
* implemented. `scopeDriftWaiver` is that missing half: the coordinator has
|
|
360
|
-
* verified that the round declares blocked AND that its proposal covers every
|
|
361
|
-
* drifted file. What is waived is the REJECTION, never the fact. */
|
|
362
|
-
function enforceScopeDrift(flags, consecutiveRejections, lastRejectionCheck, scopeDriftWaiver) {
|
|
363
|
-
const flag = flags.find((f) => f.check === CHECK_ROUND_SCOPE_DRIFT);
|
|
364
|
-
if (!flag)
|
|
365
|
-
return null;
|
|
366
|
-
// v3.8.3: the legal exit. A round that declares itself blocked may switch
|
|
367
|
-
// contracts, but only by declaring a proposal that covers every file it
|
|
368
|
-
// changed out of scope — the coverage test is the coordinator's, because
|
|
369
|
-
// only there are the git files and the active contract both in reach.
|
|
370
|
-
//
|
|
371
|
-
// Checked ABOVE the terminate arm on purpose: the counter accumulates while
|
|
372
|
-
// the agent tries to get the declaration right, so an escape that only
|
|
373
|
-
// works at streak 0 or 1 is not an escape — it is a race the agent loses.
|
|
374
|
-
// The flag is NOT suppressed; it stays on the committed round, so
|
|
375
|
-
// audit/explain still report the drift as a machine fact. */
|
|
376
|
-
if (scopeDriftWaiver)
|
|
377
|
-
return null;
|
|
378
|
-
const ownStreak = lastRejectionCheck === CHECK_ROUND_SCOPE_DRIFT
|
|
379
|
-
? consecutiveRejections
|
|
380
|
-
: 0;
|
|
381
|
-
if (ownStreak >= 2) {
|
|
382
|
-
return makeEnforcementResult({
|
|
383
|
-
action: "terminate",
|
|
384
|
-
reason: `Repeated scope drift — the agent keeps changing files outside its ` +
|
|
385
|
-
`declared contract scope.`,
|
|
386
|
-
check: "round_scope_drift",
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
const escalation = getPolicy().engine.enforcement_escalation_enabled &&
|
|
390
|
-
ownStreak >= 1
|
|
391
|
-
? buildEscalationNotice()
|
|
392
|
-
: "";
|
|
393
|
-
return makeEnforcementResult({
|
|
394
|
-
action: "reject",
|
|
395
|
-
reason: flag.detail,
|
|
396
|
-
// L3 (v3.7.x): option (b) previously told the agent to "extend the scope
|
|
397
|
-
// in your re-declared round_contract" — structurally unreachable: while a
|
|
398
|
-
// contract is ACTIVE, a replacement proposal is ignored as premature, so
|
|
399
|
-
// the guidance dead-ended into repeated rejects/termination. The only
|
|
400
|
-
// legal scope change is to CLOSE the active contract first: resubmit with
|
|
401
|
-
// outcome="blocked" (blocker naming the too-narrow scope) and the
|
|
402
|
-
// extended scope as the NEXT round's contract in the same submission.
|
|
403
|
-
// v3.8.3: that route now works, and its one condition is stated here —
|
|
404
|
-
// the new scope must cover EVERY drifted file, not just the listed ones
|
|
405
|
-
// (the detail above truncates its list).
|
|
406
|
-
fix_instructions: "Files changed outside the contract's declared scope. You must either: " +
|
|
407
|
-
"(a) revert the out-of-scope changes, or (b) legally extend the scope: " +
|
|
408
|
-
"a replacement contract is IGNORED while the current one is active, so " +
|
|
409
|
-
"resubmit this work with outcome: \"blocked\" (blocker naming the " +
|
|
410
|
-
"too-narrow scope) plus a round_contract whose scope covers EVERY file " +
|
|
411
|
-
"listed above (and any other file this round changed outside the current " +
|
|
412
|
-
"scope). Scope drift is a machine fact — no explanation waives " +
|
|
413
|
-
"it. Repeated scope drift terminates the loop." +
|
|
414
|
-
escalation,
|
|
415
|
-
check: "round_scope_drift",
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
/** R9 (v2.12): Post-backtrack workspace not restored. The verification gate
|
|
419
|
-
* produced backtrack_workspace_not_restored (error): either skipped-round
|
|
420
|
-
* files are still dirty, or the working tree is not at the restore point's
|
|
421
|
-
* git HEAD. The agent must restore the workspace, not redo the round. */
|
|
422
|
-
function enforceBacktrackNotRestored(flags, vaultEntries = [], currentRound = 0) {
|
|
423
|
-
const flag = flags.find((f) => f.check === CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED && f.severity === "error");
|
|
424
|
-
if (!flag)
|
|
425
|
-
return null;
|
|
426
|
-
if (!getPolicy().engine.backtrack_enabled)
|
|
427
|
-
return null;
|
|
428
|
-
// v2.14: same deadlock guard as R4/R5 — a backtrack that already fired
|
|
429
|
-
// for this round means a non-restoring agent would otherwise cycle
|
|
430
|
-
// R9-backtrack forever (backtracks do not increment the rejection
|
|
431
|
-
// counter). One rollback chance, then terminate.
|
|
432
|
-
if (currentRound > 0 && hasCommittedBacktrack(vaultEntries, currentRound)) {
|
|
433
|
-
return makeEnforcementResult({
|
|
434
|
-
action: "terminate",
|
|
435
|
-
reason: "The workspace is still not restored after the previous backtrack: " +
|
|
436
|
-
flag.detail,
|
|
437
|
-
check: "backtrack_workspace_not_restored",
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
return makeEnforcementResult({
|
|
441
|
-
action: "backtrack",
|
|
442
|
-
reason: "The workspace was not restored after backtrack: " + flag.detail,
|
|
443
|
-
// v3.8: the requirement, not the recipe — LoopForge never mutates the
|
|
444
|
-
// working tree and does not prescribe how the agent restores it.
|
|
445
|
-
fix_instructions: "Return the working directory to the clean round's state before " +
|
|
446
|
-
"submitting the next round: the skipped-round files must be reverted " +
|
|
447
|
-
"and HEAD must be back at the restore point the backtrack prompt named.",
|
|
448
|
-
check: "backtrack_workspace_not_restored",
|
|
449
|
-
});
|
|
450
|
-
}
|
|
451
|
-
/** Shared observation pipeline for the progress evaluator (stall window,
|
|
452
|
-
* flatline tier). Only observable machine history can establish a stall. */
|
|
453
|
-
function evaluateStallWindow(tier, currentRound, vaultEntries,
|
|
376
|
+
/** The ONE progress-stall predicate. A stall requires machine proof that both
|
|
377
|
+
* the committed window AND the submitted round are unchanged; unknown and
|
|
378
|
+
* not_applicable never produce a verdict. */
|
|
379
|
+
function evaluateStallWindow(currentRound, vaultEntries,
|
|
454
380
|
/** v3.8.1: the round's success is machine-verified (`deriveEvidenceStatus`).
|
|
455
|
-
* A verified round is finishing work; a
|
|
381
|
+
* A verified round is finishing work; a progress window cannot outvote it. */
|
|
456
382
|
machineBackedSuccess,
|
|
457
|
-
/**
|
|
458
|
-
|
|
459
|
-
inFlightMotion) {
|
|
460
|
-
// A round the machine verified is not churn, whatever the window says. This
|
|
461
|
-
// is the machine-backed replacement for the old self-reported `>= 0.95`
|
|
462
|
-
// near-completion guard: it lets a loop FINISH (a closing round that only
|
|
463
|
-
// runs the verification and declares success changes no files).
|
|
383
|
+
/** The submitted round's own provider-neutral progress. */
|
|
384
|
+
inFlightProgress) {
|
|
464
385
|
if (machineBackedSuccess)
|
|
465
386
|
return null;
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
// injection; a round that moves nothing still reaches the verdict and its
|
|
472
|
-
// deadlock guard.
|
|
473
|
-
if (inFlightMotion === true)
|
|
387
|
+
if (inFlightProgress.state === "changed")
|
|
388
|
+
return null;
|
|
389
|
+
// "Unknown never stalls": a submitted round whose progress cannot be
|
|
390
|
+
// proven unchanged gets no verdict.
|
|
391
|
+
if (inFlightProgress.state !== "unchanged")
|
|
474
392
|
return null;
|
|
475
|
-
const window =
|
|
476
|
-
? 3
|
|
477
|
-
: Math.max(1, getPolicy().engine.stall_lookback_rounds);
|
|
393
|
+
const window = Math.max(1, getPolicy().engine.stall_lookback_rounds);
|
|
478
394
|
if (currentRound < window)
|
|
479
395
|
return null;
|
|
480
|
-
const
|
|
481
|
-
if (
|
|
396
|
+
const progress = deriveProgressWindow(derivationRounds(vaultEntries, currentRound), currentRound, window);
|
|
397
|
+
if (progress.state !== "unchanged")
|
|
482
398
|
return null;
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
399
|
+
// The verdict is about the Git workspace coverage ONLY. A custom provider's
|
|
400
|
+
// fact — changed, unchanged or unknown — is never part of this sentence.
|
|
401
|
+
const providers = progress.providers.length > 0
|
|
402
|
+
? ` (providers: ${progress.providers.join(", ")})`
|
|
403
|
+
: "";
|
|
404
|
+
return {
|
|
405
|
+
detail: `no machine-observed progress within the Git workspace coverage in the ` +
|
|
406
|
+
`last ${window} committed rounds${providers}`,
|
|
407
|
+
window,
|
|
408
|
+
};
|
|
487
409
|
}
|
|
488
|
-
/**
|
|
489
|
-
*
|
|
490
|
-
* using the same mechanics — the tier is an internal diagnostic only. */
|
|
410
|
+
/** One progress disposition ladder. The reason states the machine fact; the
|
|
411
|
+
* fix states the legal protocol paths, never a semantic strategy. */
|
|
491
412
|
function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiveRejections, lastRejectionCheck) {
|
|
492
|
-
const {
|
|
493
|
-
const flatline = tier === "progress_flatline";
|
|
413
|
+
const { detail } = verdict;
|
|
494
414
|
const escalation = getPolicy().engine.enforcement_escalation_enabled;
|
|
495
|
-
const stalledReason =
|
|
496
|
-
? `Progress has been ${detail} for ${window} consecutive rounds.`
|
|
497
|
-
: `Progress has stalled: ${detail}.`;
|
|
498
|
-
// v3.7: both tiers emit the single progress_stall check id — the tier
|
|
499
|
-
// survives only in the reason/fix wording (former progress_flatline id).
|
|
415
|
+
const stalledReason = `Machine-observed progress is flat: ${detail}.`;
|
|
500
416
|
const check = "progress_stall";
|
|
501
417
|
// v3.8.3: the stall escalates on its OWN streak. The persisted counter is
|
|
502
|
-
// one scalar shared by every check, so two rejections of any other kind
|
|
503
|
-
//
|
|
418
|
+
// one scalar shared by every check, so two rejections of any other kind must
|
|
419
|
+
// not turn a first-time stall straight into termination.
|
|
504
420
|
const ownStreak = lastRejectionCheck === check ? consecutiveRejections : 0;
|
|
505
|
-
if (
|
|
421
|
+
if (ownStreak >= 2) {
|
|
506
422
|
return makeEnforcementResult({
|
|
507
423
|
action: "terminate",
|
|
508
424
|
reason: `${stalledReason} The stall persisted after escalation. Terminating loop.`,
|
|
@@ -516,7 +432,7 @@ function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiv
|
|
|
516
432
|
check,
|
|
517
433
|
});
|
|
518
434
|
}
|
|
519
|
-
const escalated =
|
|
435
|
+
const escalated = ownStreak >= 1 && escalation;
|
|
520
436
|
if (escalated && getPolicy().engine.backtrack_enabled) {
|
|
521
437
|
if (hasCommittedBacktrack(vaultEntries, currentRound)) {
|
|
522
438
|
return makeEnforcementResult({
|
|
@@ -528,34 +444,26 @@ function resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiv
|
|
|
528
444
|
return makeEnforcementResult({
|
|
529
445
|
action: "backtrack",
|
|
530
446
|
reason: stalledReason,
|
|
531
|
-
fix_instructions:
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
if (flatline && !escalation) {
|
|
538
|
-
return makeEnforcementResult({
|
|
539
|
-
action: "terminate",
|
|
540
|
-
reason: `${stalledReason} The agent is making zero forward motion and cannot recover.`,
|
|
447
|
+
fix_instructions: "This is a machine liveness fact, not a judgment about your plan. " +
|
|
448
|
+
"The loop will restore the last clean round. After the restore you own " +
|
|
449
|
+
"the next action: continue with a new evaluation, request recovery to " +
|
|
450
|
+
"an earlier committed point, or declare blocked/stop.",
|
|
541
451
|
check,
|
|
542
452
|
});
|
|
543
453
|
}
|
|
544
454
|
return makeEnforcementResult({
|
|
545
455
|
action: "reject",
|
|
546
456
|
reason: stalledReason,
|
|
547
|
-
fix_instructions:
|
|
548
|
-
|
|
549
|
-
|
|
457
|
+
fix_instructions: "This is a machine liveness fact, not a judgment about your plan. " +
|
|
458
|
+
"You own the next action: submit a new evaluation, request recovery to " +
|
|
459
|
+
"an earlier committed point, or declare outcome blocked with a blocker. " +
|
|
460
|
+
"LoopForge does not prescribe a technique or decomposition." +
|
|
550
461
|
(escalated ? buildEscalationNotice() : ""),
|
|
551
462
|
check,
|
|
552
463
|
});
|
|
553
464
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
function enforceProgressStall(_flags, currentRound, vaultEntries, consecutiveRejections, machineBackedSuccess, lastRejectionCheck, inFlightMotion) {
|
|
557
|
-
const verdict = evaluateStallWindow("progress_stall", currentRound, vaultEntries, machineBackedSuccess, inFlightMotion)
|
|
558
|
-
?? evaluateStallWindow("progress_flatline", currentRound, vaultEntries, machineBackedSuccess, inFlightMotion);
|
|
465
|
+
function enforceProgressStall(_flags, currentRound, vaultEntries, consecutiveRejections, machineBackedSuccess, lastRejectionCheck, inFlightProgress) {
|
|
466
|
+
const verdict = evaluateStallWindow(currentRound, vaultEntries, machineBackedSuccess, inFlightProgress);
|
|
559
467
|
return verdict
|
|
560
468
|
? resolveStallDisposition(verdict, currentRound, vaultEntries, consecutiveRejections, lastRejectionCheck)
|
|
561
469
|
: null;
|
|
@@ -567,15 +475,7 @@ function enforceProgressStall(_flags, currentRound, vaultEntries, consecutiveRej
|
|
|
567
475
|
* instead of terminating immediately. Termination occurs on the third
|
|
568
476
|
* consecutive rejection. When the flag is disabled, behavior is unchanged
|
|
569
477
|
* (terminate at ≥2 rejections). */
|
|
570
|
-
function enforceMaxRejections(consecutiveRejections
|
|
571
|
-
// v3.8.3: this row is the catch-all for a round that LOOKS clean while the
|
|
572
|
-
// persisted counter says otherwise. A round waived past scope drift is not
|
|
573
|
-
// that shape: it declares blocked and its new proposal covers every file it
|
|
574
|
-
// drifted into, so the counter it carries is the drift streak it just
|
|
575
|
-
// resolved — not unresolved persistence. Without this, the legal exit would
|
|
576
|
-
// still dead-end here at a counter of 2 or more.
|
|
577
|
-
if (driftWaived)
|
|
578
|
-
return null;
|
|
478
|
+
function enforceMaxRejections(consecutiveRejections) {
|
|
579
479
|
const escalation = getPolicy().engine.enforcement_escalation_enabled;
|
|
580
480
|
if (escalation) {
|
|
581
481
|
if (consecutiveRejections >= 3) {
|
|
@@ -593,9 +493,9 @@ function enforceMaxRejections(consecutiveRejections, driftWaived) {
|
|
|
593
493
|
reason: `${consecutiveRejections} consecutive enforcement rejections for the ` +
|
|
594
494
|
`same issue without resolution.`,
|
|
595
495
|
fix_instructions: "You have been rejected multiple times for the same issue. " +
|
|
596
|
-
"
|
|
597
|
-
"If you
|
|
598
|
-
"
|
|
496
|
+
"Address every concrete finding in the mandatory Verification Gate. " +
|
|
497
|
+
"If you need human input, stop with needs_human_input; there is no " +
|
|
498
|
+
"semantic waiver for the findings." +
|
|
599
499
|
buildEscalationNotice(),
|
|
600
500
|
check: "max_rejections",
|
|
601
501
|
});
|
|
@@ -619,64 +519,41 @@ function enforceMaxRejections(consecutiveRejections, driftWaived) {
|
|
|
619
519
|
function buildEscalationNotice() {
|
|
620
520
|
return [
|
|
621
521
|
"",
|
|
622
|
-
"### 🆘 Escalation —
|
|
522
|
+
"### 🆘 Escalation — Human Decision Required",
|
|
623
523
|
"",
|
|
624
|
-
"
|
|
625
|
-
"
|
|
524
|
+
"This check has rejected the same round more than once. Before retrying, " +
|
|
525
|
+
"ask the human whether to continue the current scope, change the " +
|
|
526
|
+
"operator-owned scope, or stop.",
|
|
626
527
|
"",
|
|
627
|
-
"1.
|
|
628
|
-
"2. Ask the human for
|
|
629
|
-
"3.
|
|
528
|
+
"1. State the machine fact and the attempt already made.",
|
|
529
|
+
"2. Ask the human for a decision (continue / change scope / stop).",
|
|
530
|
+
"3. Record the decision in the next evaluation, or stop.",
|
|
630
531
|
"",
|
|
631
|
-
"⚠️ If
|
|
632
|
-
"Do NOT resubmit without meaningful changes.",
|
|
532
|
+
"⚠️ If the same check rejects again, the loop will be **terminated**.",
|
|
633
533
|
].join("\n");
|
|
634
534
|
}
|
|
635
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
636
|
-
// Diagnostic gap builder (v2.7)
|
|
637
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
638
|
-
/** Build a diagnostic gap section from verification flags.
|
|
639
|
-
*
|
|
640
|
-
* Translates each error/warn-level VerificationFlag into a concrete
|
|
641
|
-
* claim-vs-evidence mismatch statement. Info-level flags are excluded
|
|
642
|
-
* because they are purely informational and do not represent gaps.
|
|
643
|
-
*
|
|
644
|
-
* Returns an empty string when there are no diagnostic flags to report. */
|
|
645
|
-
function buildDiagnosticGap(flags) {
|
|
646
|
-
const diagnosticFlags = flags.filter((f) => f.severity === "error" || f.severity === "warn");
|
|
647
|
-
if (diagnosticFlags.length === 0)
|
|
648
|
-
return "";
|
|
649
|
-
const lines = [
|
|
650
|
-
"### Evidence Gap",
|
|
651
|
-
"",
|
|
652
|
-
"The enforcement gate detected mismatches between your claims and the evidence. " +
|
|
653
|
-
"Each item below shows a specific discrepancy — address every one before re-submitting.",
|
|
654
|
-
"",
|
|
655
|
-
];
|
|
656
|
-
for (const flag of diagnosticFlags) {
|
|
657
|
-
const icon = flag.severity === "error" ? "🚫" : "⚠️";
|
|
658
|
-
lines.push(`- ${icon} **[${flag.check}]** \`${flag.field}\` — ${flag.detail}`);
|
|
659
|
-
}
|
|
660
|
-
lines.push("");
|
|
661
|
-
return lines.join("\n");
|
|
662
|
-
}
|
|
663
535
|
const RULE_TABLE = [
|
|
664
536
|
{ checks: ["success_with_remaining_criteria"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
665
537
|
{ checks: ["recurring_violation"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
666
|
-
{ checks: ["required_command_failed"
|
|
538
|
+
{ checks: ["required_command_failed"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
539
|
+
// v4.0.6: contract coverage — a continuing round must leave an active
|
|
540
|
+
// contract; a different proposal needs an explicit supersede; a successor
|
|
541
|
+
// must change the acceptance items. Reject and let the uniform ladder
|
|
542
|
+
// escalate repeats (three same-check rejections terminate).
|
|
543
|
+
{ checks: ["contract_required", "contract_premature", "contract_successor_unchanged"], ladder: "uniform", terminalAfter: 3, noticeOnRepeat: true },
|
|
667
544
|
// v3.8.3: own ladder — reject → backtrack → terminate. A rewritten
|
|
668
545
|
// entrypoint is not fixed by asking again (the round's baseline is frozen on
|
|
669
546
|
// purpose), so the second occurrence rolls the work back instead of killing
|
|
670
547
|
// the loop. See enforceVerificationEntrypointTampered.
|
|
671
548
|
{ checks: ["verification_entrypoint_modified"], ladder: "internal" },
|
|
672
|
-
|
|
549
|
+
// v3.9.9: round_scope_drift is REPORT-ONLY. It has no enforcement row, no
|
|
550
|
+
// waiver, no rejection counter, and no stall/backtrack interaction.
|
|
673
551
|
// v3.8.1: "internal" — the handler owns this row's ladder (it must terminate
|
|
674
552
|
// as `incomplete`, with its own reason). Declaring it uniform as well ran the
|
|
675
553
|
// escalation notice twice and left the uniform terminate branch dead.
|
|
676
554
|
{ checks: ["contract_items_unverified"], ladder: "internal" },
|
|
677
555
|
{ checks: ["progress_stall"], ladder: "internal" },
|
|
678
556
|
{ checks: ["max_rejections"], ladder: "counter" },
|
|
679
|
-
{ checks: ["backtrack_workspace_not_restored"], ladder: "internal" },
|
|
680
557
|
// v3.7.1: cited gates without an approved human decision
|
|
681
558
|
{ checks: ["user_gate_unresolved"], ladder: "uniform", terminalAfter: 2, noticeOnRepeat: true },
|
|
682
559
|
];
|
|
@@ -729,16 +606,24 @@ export function enforceRound(selfEval, verifyResult, currentRound, vaultEntries,
|
|
|
729
606
|
* check and lets uniform rows escalate on THEIR OWN streak instead of
|
|
730
607
|
* inheriting an unrelated history. */
|
|
731
608
|
lastRejectionCheck = "",
|
|
732
|
-
/** v3.
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
/** v3.9: the submitted round's own Git motion — see `inFlightGitMotion`.
|
|
739
|
-
* null when no Git observation was made. */
|
|
740
|
-
inFlightMotion = null) {
|
|
609
|
+
/** v3.9.7: the submitted round's provider-neutral progress. */
|
|
610
|
+
inFlightProgress = {
|
|
611
|
+
state: "unknown",
|
|
612
|
+
providers: [],
|
|
613
|
+
coverages: [],
|
|
614
|
+
}) {
|
|
741
615
|
const { flags } = verifyResult;
|
|
616
|
+
// v3.9.9: `round_scope_drift` is a retired enforcement check. A session
|
|
617
|
+
// restored from an older runtime may still name it; it is not a current
|
|
618
|
+
// check, and its persisted count must not reach the max_rejections
|
|
619
|
+
// catch-all as unresolved persistence.
|
|
620
|
+
const retiredScopeCheck = lastRejectionCheck === "round_scope_drift";
|
|
621
|
+
const effectiveRejections = retiredScopeCheck ? 0 : consecutiveRejections;
|
|
622
|
+
const effectiveLastCheck = retiredScopeCheck ? "" : lastRejectionCheck;
|
|
623
|
+
// A valid blocked stop is the Agent's protocol action. Liveness/retry rows
|
|
624
|
+
// must not convert it into a same-round rejection; machine-integrity rows
|
|
625
|
+
// still apply below because their facts make the submission untrustworthy.
|
|
626
|
+
const blockedStop = declaresBlocked(selfEval) && selfEval.should_continue === false;
|
|
742
627
|
// Run enforcement rules in priority order — the array order IS the
|
|
743
628
|
// priority; the R-labels in the comments are historical (RULE_TABLE is
|
|
744
629
|
// the semantic description). The first rule that fires wins.
|
|
@@ -748,9 +633,11 @@ inFlightMotion = null) {
|
|
|
748
633
|
// v2.14: R-EVID — a required command failure, hidden test failures, or
|
|
749
634
|
// a self-contradictory outcome claim contradicts the success claim
|
|
750
635
|
() => enforceEvidenceContradiction(flags),
|
|
636
|
+
// v4.0.6: contract coverage (required / premature / successor unchanged)
|
|
637
|
+
() => enforceContractCoverage(flags),
|
|
751
638
|
// v3.3: R-EVID-VERIFY — the verification command entrypoint changed in
|
|
752
639
|
// the same round it ran.
|
|
753
|
-
() => enforceVerificationEntrypointTampered(flags, vaultEntries, currentRound,
|
|
640
|
+
() => enforceVerificationEntrypointTampered(flags, vaultEntries, currentRound, effectiveRejections, effectiveLastCheck),
|
|
754
641
|
// v3.5: contract completion claimed without its verification_plan
|
|
755
642
|
// commands passing — runs before R-C1: the completion-truth question
|
|
756
643
|
// outranks boundary nuance.
|
|
@@ -760,23 +647,26 @@ inFlightMotion = null) {
|
|
|
760
647
|
// success_without_verified_evidence, which is now a warn: report coverage
|
|
761
648
|
// no longer decides acceptance, and the machine-evidence warn still keeps
|
|
762
649
|
// an unbacked success out of the trajectory.
|
|
763
|
-
// v3.
|
|
764
|
-
() => enforceScopeDrift(flags, consecutiveRejections, lastRejectionCheck, scopeDriftWaiver),
|
|
650
|
+
// v3.9.9: scope drift is an INFO fact only; it never appears here.
|
|
765
651
|
// v3.8: verification debt — contract items claimed met without machine
|
|
766
652
|
// verification (fires only after the configured streak)
|
|
767
|
-
() =>
|
|
768
|
-
|
|
653
|
+
() => blockedStop
|
|
654
|
+
? null
|
|
655
|
+
: enforceContractItemsUnverified(flags, currentRound, vaultEntries, effectiveRejections, effectiveLastCheck),
|
|
656
|
+
// v3.9.7: one provider-neutral progress evaluator —
|
|
769
657
|
// reject → backtrack → terminate with the deadlock guard
|
|
770
|
-
() =>
|
|
658
|
+
() => blockedStop
|
|
659
|
+
? null
|
|
660
|
+
: enforceProgressStall(flags, currentRound, vaultEntries, effectiveRejections, verifyResult.machineBackedSuccess, effectiveLastCheck, inFlightProgress),
|
|
771
661
|
// R6: rejection-counter catch-all (evaluated only when no row above
|
|
772
662
|
// fired — a clean-looking round carrying a high persisted counter)
|
|
773
663
|
// v3.7.1: cited gate without an approved human decision
|
|
774
664
|
() => enforceUserGateUnresolved(flags),
|
|
775
|
-
// v3.
|
|
776
|
-
//
|
|
777
|
-
() =>
|
|
778
|
-
|
|
779
|
-
|
|
665
|
+
// v3.9.9: round_scope_drift is not a rejection source, so this catch-all
|
|
666
|
+
// has no drift-waiver special case anymore.
|
|
667
|
+
() => blockedStop
|
|
668
|
+
? null
|
|
669
|
+
: enforceMaxRejections(effectiveRejections),
|
|
780
670
|
];
|
|
781
671
|
// v3.7: uniform-ladder escalation. Rows without their own escalation
|
|
782
672
|
// ladder (R1/R2/R-EVID/R-EVID-VERIFY) escalate on the session's
|
|
@@ -802,8 +692,8 @@ inFlightMotion = null) {
|
|
|
802
692
|
// occurrence into termination. The user_gate row is the sharp case:
|
|
803
693
|
// its recovery is human approval, so a gate cited after unrelated
|
|
804
694
|
// rejections must reject with instructions, not kill the loop.
|
|
805
|
-
const ownStreak =
|
|
806
|
-
?
|
|
695
|
+
const ownStreak = effectiveLastCheck === result.check
|
|
696
|
+
? effectiveRejections
|
|
807
697
|
: 0;
|
|
808
698
|
if (ownStreak >= (row.terminalAfter ?? 2)) {
|
|
809
699
|
return makeEnforcementResult({
|
|
@@ -831,54 +721,41 @@ inFlightMotion = null) {
|
|
|
831
721
|
fix_instructions: "",
|
|
832
722
|
});
|
|
833
723
|
}
|
|
834
|
-
/** Build
|
|
724
|
+
/** Build the protected retry notice for the agent.
|
|
835
725
|
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
* mismatch statement so the agent knows exactly what to correct rather
|
|
842
|
-
* than retrying blindly.
|
|
726
|
+
* v3.9.7: this is a protocol fact, not a second task narrative. It carries
|
|
727
|
+
* the enforcement reason, the required fix and the same-round instruction.
|
|
728
|
+
* Findings are rendered exactly once by the mandatory Verification Gate; the
|
|
729
|
+
* original task/contract is rendered by Objective/Current Task. The notice
|
|
730
|
+
* never promises that an explanation will re-evaluate a machine fact.
|
|
843
731
|
*
|
|
844
732
|
* @param currentRound The round number that was rejected (NOT incremented).
|
|
845
|
-
* @param
|
|
733
|
+
* @param _task Kept for the public signature; not rendered here.
|
|
846
734
|
* @param enforceResult The enforcement decision with reason and fix instructions.
|
|
847
|
-
* @param
|
|
848
|
-
*
|
|
849
|
-
export function buildRejectionPrompt(currentRound,
|
|
735
|
+
* @param _verificationFlags Kept for the public signature; findings render in
|
|
736
|
+
* the mandatory Verification Gate. */
|
|
737
|
+
export function buildRejectionPrompt(currentRound, _task, enforceResult, _verificationFlags = []) {
|
|
850
738
|
const lines = [
|
|
851
739
|
"## ⛔ Round " + currentRound + " — REJECTED",
|
|
852
740
|
"",
|
|
853
|
-
"
|
|
854
|
-
|
|
741
|
+
"The enforcement gate rejected this round. This is a machine/protocol",
|
|
742
|
+
"decision, not a judgment about your reasoning strategy.",
|
|
855
743
|
"",
|
|
856
744
|
"### Reason",
|
|
857
745
|
"",
|
|
858
746
|
enforceResult.reason,
|
|
859
747
|
"",
|
|
860
748
|
];
|
|
861
|
-
// v2.7: Diagnostic gap — show the agent exactly what doesn't match.
|
|
862
|
-
const gap = buildDiagnosticGap(verificationFlags);
|
|
863
|
-
if (gap) {
|
|
864
|
-
lines.push(gap);
|
|
865
|
-
}
|
|
866
749
|
if (enforceResult.fix_instructions) {
|
|
867
750
|
lines.push("### Required Fix");
|
|
868
751
|
lines.push("");
|
|
869
752
|
lines.push(enforceResult.fix_instructions);
|
|
870
753
|
lines.push("");
|
|
871
754
|
}
|
|
872
|
-
lines.push("### Your Task (Round " + currentRound + " — Retry)");
|
|
873
|
-
lines.push("");
|
|
874
|
-
lines.push(task);
|
|
875
|
-
lines.push("");
|
|
876
755
|
lines.push("### Instructions");
|
|
877
756
|
lines.push("");
|
|
878
757
|
lines.push("1. Read and address each issue in **Required Fix** above.", "2. Re-execute **Round " + currentRound +
|
|
879
|
-
"** — do NOT advance to the next round.", "3. Submit a corrected
|
|
880
|
-
"claiming success when criteria are unmet will be rejected again.", "5. If you believe this rejection is incorrect, " +
|
|
881
|
-
"explain why in your output_summary and the enforcement gate will re-evaluate.");
|
|
758
|
+
"** — do NOT advance to the next round.", "3. Submit a corrected evaluation via `loopforge_next` using the latest", " `roundId` returned by LoopForge.", "4. If you believe a finding is factually wrong, identify the specific", " machine fact you dispute and submit a corrected evaluation. The gate", " re-evaluates facts, not explanations.");
|
|
882
759
|
return lines.join("\n");
|
|
883
760
|
}
|
|
884
761
|
//# sourceMappingURL=enforcement-gate.js.map
|