loopforge 3.9.1 → 3.9.4
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 +11 -8
- package/dist/canonical-state.d.ts +39 -1
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +114 -32
- package/dist/canonical-state.js.map +1 -1
- package/dist/committed-round.d.ts +27 -4
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +87 -14
- package/dist/committed-round.js.map +1 -1
- package/dist/enforcement-gate.d.ts +9 -32
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +9 -122
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +13 -2
- package/dist/engine.js.map +1 -1
- package/dist/explain.d.ts +1 -0
- package/dist/explain.d.ts.map +1 -1
- package/dist/explain.js +1 -0
- package/dist/explain.js.map +1 -1
- package/dist/files-reconciliation.d.ts +18 -0
- package/dist/files-reconciliation.d.ts.map +1 -0
- package/dist/files-reconciliation.js +59 -0
- package/dist/files-reconciliation.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +17 -1
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +220 -119
- package/dist/loop-compiler.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +31 -1
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +102 -60
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts +5 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +19 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +23 -5
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +1 -1
- package/dist/mcp/submission-boundary.d.ts.map +1 -1
- package/dist/mcp/submission-boundary.js +31 -8
- package/dist/mcp/submission-boundary.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +62 -6
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +4 -0
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +17 -3
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +5 -2
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +2 -2
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +8 -3
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +233 -240
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-coverage.d.ts +45 -0
- package/dist/prompt-coverage.d.ts.map +1 -0
- package/dist/prompt-coverage.js +84 -0
- package/dist/prompt-coverage.js.map +1 -0
- package/dist/prompt-freshness.d.ts +36 -0
- package/dist/prompt-freshness.d.ts.map +1 -0
- package/dist/prompt-freshness.js +83 -0
- package/dist/prompt-freshness.js.map +1 -0
- package/dist/prompt-policy.d.ts +5 -2
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +10 -7
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +70 -8
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +7 -1
- package/dist/protocol.js.map +1 -1
- package/dist/round-coordinator.d.ts +4 -2
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +7 -7
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-facts.d.ts.map +1 -1
- package/dist/round-facts.js +9 -3
- package/dist/round-facts.js.map +1 -1
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +19 -1
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +23 -7
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +88 -16
- package/dist/self-eval.js.map +1 -1
- package/dist/submission-guidance.d.ts +36 -4
- package/dist/submission-guidance.d.ts.map +1 -1
- package/dist/submission-guidance.js +225 -78
- package/dist/submission-guidance.js.map +1 -1
- package/dist/submission-receipt.d.ts.map +1 -1
- package/dist/submission-receipt.js +6 -0
- package/dist/submission-receipt.js.map +1 -1
- package/dist/token-utils.d.ts +26 -0
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +56 -6
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-actions.d.ts +13 -0
- package/dist/verification-actions.d.ts.map +1 -0
- package/dist/verification-actions.js +45 -0
- package/dist/verification-actions.js.map +1 -0
- package/dist/verification-gate.d.ts +5 -4
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +32 -45
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/loop_policy.json +58 -58
- package/package.json +68 -68
- package/skills/perception/SKILL.md +35 -11
package/dist/loop-compiler.js
CHANGED
|
@@ -7,7 +7,8 @@ import { createHash } from "node:crypto";
|
|
|
7
7
|
import { deriveConfiguredCapability, getPolicy } from "./policy.js";
|
|
8
8
|
import { AgentStatus, makeLoopCompileResponse, makeLoopObjective, makeConstraintMeta, makeMilestoneSummary, makeRollingSummary, } from "./protocol.js";
|
|
9
9
|
import { createCanonicalLoopState, renderCanonicalStateMarkdown, } from "./canonical-state.js";
|
|
10
|
-
import {
|
|
10
|
+
import { decodeRound, entryLineage, entryCriteriaMet, entryCriteriaRemaining, entryActiveConstraints, entryRetractedConstraints, pendingBacktrackRecord, roundResultFromCommittedRound, machineGitMotionSeries, derivationRounds, } from "./committed-round.js";
|
|
11
|
+
import { derivePromptFreshness, UNKNOWN_PROMPT_FRESHNESS, } from "./prompt-freshness.js";
|
|
11
12
|
import { NO_IN_FLIGHT_ROUND, deriveRoundFacts } from "./round-facts.js";
|
|
12
13
|
import { recoveryBriefLines } from "./enforcement-gate.js";
|
|
13
14
|
import { deriveSubGoals, duplicateEmergedDeclarations } from "./subgoal-state.js";
|
|
@@ -15,7 +16,7 @@ import { claimedMetCriteria, claimedRemainingCriteria } from "./self-eval.js";
|
|
|
15
16
|
import { assemblePromptArtifact } from "./prompt-assembler.js";
|
|
16
17
|
import { renderSelfEvalBlock, } from "./submission-guidance.js";
|
|
17
18
|
import { decidePromptLevel, } from "./prompt-policy.js";
|
|
18
|
-
import { deriveCriterionId, deriveItemId,
|
|
19
|
+
import { criterionRefMatches, deriveCriterionId, deriveItemId, normalizeText, unique, uniqueNormalized, entryRound, } from "./token-utils.js";
|
|
19
20
|
function contextEntries(context) {
|
|
20
21
|
if (!context || !Array.isArray(context.results))
|
|
21
22
|
return [];
|
|
@@ -82,8 +83,21 @@ export function getPreviousRound(loopId, round, context) {
|
|
|
82
83
|
: typeof data.output_summary === "string" ? data.output_summary : "",
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
|
-
function latestObjective(loopId, context
|
|
86
|
-
|
|
86
|
+
function latestObjective(loopId, context,
|
|
87
|
+
/** Exclusive compile-round bound. A deep-restore redo must never read the
|
|
88
|
+
* abandoned branch's objective (and its rolled-back refinements). */
|
|
89
|
+
beforeRound,
|
|
90
|
+
/** v3.9.4: a round-1 same-round retry has no earlier committed objective.
|
|
91
|
+
* Reading the prior compile of the SAME round recovers the initial
|
|
92
|
+
* criteria supplied to `loopforge_start`. It is never enabled for a first
|
|
93
|
+
* attempt or for later rounds, so it cannot read an abandoned branch. */
|
|
94
|
+
includeRoundOne = false) {
|
|
95
|
+
const candidates = loopEntries(loopId, context)
|
|
96
|
+
.filter((entry) => {
|
|
97
|
+
const round = entryRound(entry);
|
|
98
|
+
return round >= 1 && (round < beforeRound || (includeRoundOne && round === 1));
|
|
99
|
+
})
|
|
100
|
+
.reverse();
|
|
87
101
|
for (const entry of candidates) {
|
|
88
102
|
const raw = entry.loop_objective;
|
|
89
103
|
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
@@ -107,8 +121,9 @@ function latestObjective(loopId, context) {
|
|
|
107
121
|
}
|
|
108
122
|
return null;
|
|
109
123
|
}
|
|
110
|
-
function evolveObjective(request, context) {
|
|
111
|
-
const current = request.loop_objective ??
|
|
124
|
+
function evolveObjective(request, context, previous) {
|
|
125
|
+
const current = request.loop_objective ??
|
|
126
|
+
latestObjective(request.loop_id, context, request.round, request.round === 1 && (request.attempt ?? 1) > 1) ??
|
|
112
127
|
makeLoopObjective({
|
|
113
128
|
objective: request.task,
|
|
114
129
|
success_criteria: ["Task goal achieved with verifiable evidence"],
|
|
@@ -116,7 +131,7 @@ function evolveObjective(request, context) {
|
|
|
116
131
|
created_at_round: 1,
|
|
117
132
|
loop_id: request.loop_id,
|
|
118
133
|
});
|
|
119
|
-
const last =
|
|
134
|
+
const last = previous.result;
|
|
120
135
|
const history = [...(current.refinement_history ?? [])];
|
|
121
136
|
let objective = current.objective || request.task;
|
|
122
137
|
let version = current.version ?? 1;
|
|
@@ -124,9 +139,9 @@ function evolveObjective(request, context) {
|
|
|
124
139
|
const refinement = last.objective_refinement.trim();
|
|
125
140
|
// M5: fold each refinement exactly once per persisted objective state.
|
|
126
141
|
// A retry recompiles the same round against the same committed
|
|
127
|
-
//
|
|
128
|
-
// the refinement and inflates the version — the objective text
|
|
129
|
-
// state file grew "Refinement: X" once per attempt.
|
|
142
|
+
// previous-round projection; without this guard every retry attempt
|
|
143
|
+
// re-appends the refinement and inflates the version — the objective text
|
|
144
|
+
// and the state file grew "Refinement: X" once per attempt.
|
|
130
145
|
if (!history.includes(refinement)) {
|
|
131
146
|
history.push(refinement);
|
|
132
147
|
objective = `${objective}\nRefinement: ${refinement}`;
|
|
@@ -139,56 +154,69 @@ function evolveObjective(request, context) {
|
|
|
139
154
|
objective,
|
|
140
155
|
version,
|
|
141
156
|
refinement_history: history.slice(-getPolicy().evolution.max_objective_versions),
|
|
142
|
-
|
|
143
|
-
|
|
157
|
+
// v3.9.4: one normalized-exact text, one cr-* identity. `unique` was
|
|
158
|
+
// trim-only, so case/whitespace variants survived as two entries that
|
|
159
|
+
// derived to the SAME criterion id.
|
|
160
|
+
success_criteria: uniqueNormalized(current.success_criteria
|
|
161
|
+
.filter((item) => typeof item === "string")
|
|
162
|
+
.map((item) => revisions.get(item) ?? item)),
|
|
163
|
+
hard_constraints: uniqueNormalized(current.hard_constraints),
|
|
144
164
|
loop_id: request.loop_id,
|
|
145
165
|
});
|
|
146
166
|
}
|
|
147
|
-
function evolveConstraints(request, objective, previous,
|
|
148
|
-
const retired = unique(
|
|
149
|
-
|
|
150
|
-
|
|
167
|
+
function evolveConstraints(request, objective, previous, windowRounds) {
|
|
168
|
+
const retired = unique(previous.result?.retracted_constraints ?? []);
|
|
169
|
+
// v3.9.4: a revised-away criterion text is no longer an objective criterion.
|
|
170
|
+
// It must not survive as a "discovered" active constraint just because the
|
|
171
|
+
// previous round's active set carried it. This is a merge-time suppression,
|
|
172
|
+
// not an alias and not a constraint retirement.
|
|
173
|
+
const revisedAway = unique((previous.result?.revised_success_criteria ?? []).map((item) => item.old));
|
|
174
|
+
const immediateRetired = new Set([...retired, ...revisedAway].map((text) => normalizeText(text)));
|
|
175
|
+
const discovered = unique(previous.result?.discovered_constraints ?? [])
|
|
151
176
|
.slice(0, getPolicy().evolution.max_discovered_constraints_per_round);
|
|
152
177
|
// v2.14: agent-declared retractions of plan/hard/criteria constraints
|
|
153
178
|
// persist for constraints.retire_window rounds (they are re-merged from
|
|
154
179
|
// the request/objective every round, so without this the documented
|
|
155
180
|
// "Removed from the active constraint set" never held for them). The
|
|
156
|
-
// retraction rounds are derived from
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// action's memory expires).
|
|
181
|
+
// retraction rounds are derived from the SAME committed window as every
|
|
182
|
+
// other compile fact — an abandoned branch above the compile round can
|
|
183
|
+
// never suppress a constraint on the restored branch.
|
|
160
184
|
const window = getPolicy().constraints.retire_window;
|
|
161
185
|
const lastRetractedRound = new Map();
|
|
162
186
|
if (window > 0) {
|
|
163
|
-
for (const
|
|
164
|
-
const retractedList =
|
|
165
|
-
if (!Array.isArray(retractedList))
|
|
166
|
-
continue;
|
|
167
|
-
const rnd = entryRound(entry);
|
|
187
|
+
for (const round of windowRounds) {
|
|
188
|
+
const retractedList = round.retractedConstraints ?? [];
|
|
168
189
|
for (const text of retractedList) {
|
|
169
|
-
if (
|
|
190
|
+
if (!text)
|
|
170
191
|
continue;
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
192
|
+
const key = normalizeText(text);
|
|
193
|
+
const previousRound = lastRetractedRound.get(key);
|
|
194
|
+
if (previousRound === undefined || round.round > previousRound) {
|
|
195
|
+
lastRetractedRound.set(key, round.round);
|
|
196
|
+
}
|
|
174
197
|
}
|
|
175
198
|
}
|
|
176
199
|
}
|
|
177
|
-
|
|
178
|
-
|
|
200
|
+
// v3.9.4: normalized-exact dedupe. The trim-only `unique` let case/space
|
|
201
|
+
// variants coexist while deriving the SAME c-/cr- id. The silent
|
|
202
|
+
// max_active_constraints slice is gone: every merged active constraint is
|
|
203
|
+
// normative, and prompt budget pressure is reported as protectedOverflow.
|
|
204
|
+
const active = uniqueNormalized([
|
|
205
|
+
...(previous.view?.activeConstraints ?? []),
|
|
179
206
|
...request.constraints_from_plan,
|
|
180
207
|
...objective.hard_constraints,
|
|
181
208
|
...objective.success_criteria,
|
|
182
209
|
...discovered,
|
|
183
210
|
]).filter((item) => {
|
|
184
|
-
|
|
211
|
+
const key = normalizeText(item);
|
|
212
|
+
if (immediateRetired.has(key))
|
|
185
213
|
return false;
|
|
186
|
-
const retractedRound = lastRetractedRound.get(
|
|
214
|
+
const retractedRound = lastRetractedRound.get(key);
|
|
187
215
|
if (retractedRound !== undefined && request.round - retractedRound < window) {
|
|
188
216
|
return false;
|
|
189
217
|
}
|
|
190
218
|
return true;
|
|
191
|
-
})
|
|
219
|
+
});
|
|
192
220
|
return { active, retired };
|
|
193
221
|
}
|
|
194
222
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -266,8 +294,8 @@ criterionFacts = []) {
|
|
|
266
294
|
if (!objective || objective.success_criteria.length === 0)
|
|
267
295
|
return [];
|
|
268
296
|
const entries = loopEntries(loopId, context).filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
269
|
-
// The
|
|
270
|
-
//
|
|
297
|
+
// The previous round's report, resolved from the committed view (or the
|
|
298
|
+
// library fallback), is treated as the report for round currentRound - 1.
|
|
271
299
|
const lastEv = lastRoundResult?.execution_report;
|
|
272
300
|
const lastMet = claimedMetCriteria(lastEv);
|
|
273
301
|
const lastRemaining = claimedRemainingCriteria(lastEv);
|
|
@@ -280,7 +308,16 @@ criterionFacts = []) {
|
|
|
280
308
|
// sub-goal links carried on the same item, disappeared while the sub-goal
|
|
281
309
|
// side of the very same fact survived. One derivation, one window.
|
|
282
310
|
const factsByCriterion = new Map(criterionFacts.map((fact) => [fact.criterion_id, fact]));
|
|
283
|
-
|
|
311
|
+
// v3.9.4: one row per derived id. The objective is authored text; a case or
|
|
312
|
+
// whitespace variant derives the same cr-* and must not produce a second
|
|
313
|
+
// row (nor a second contract reference space).
|
|
314
|
+
const seenIds = new Set();
|
|
315
|
+
const statuses = [];
|
|
316
|
+
for (const text of objective.success_criteria) {
|
|
317
|
+
const id = deriveCriterionId(text);
|
|
318
|
+
if (seenIds.has(id))
|
|
319
|
+
continue;
|
|
320
|
+
seenIds.add(id);
|
|
284
321
|
let metAtRound = null;
|
|
285
322
|
for (const entry of entries) {
|
|
286
323
|
const rnd = entryRound(entry);
|
|
@@ -300,18 +337,19 @@ criterionFacts = []) {
|
|
|
300
337
|
lastEntryRemaining.some((remaining) => criteriaMatch(remaining, text))
|
|
301
338
|
? "remaining"
|
|
302
339
|
: "unknown";
|
|
303
|
-
const fact = factsByCriterion.get(
|
|
340
|
+
const fact = factsByCriterion.get(id);
|
|
304
341
|
const status = fact?.status ?? claimedStatus;
|
|
305
|
-
|
|
306
|
-
id
|
|
342
|
+
statuses.push({
|
|
343
|
+
id,
|
|
307
344
|
text,
|
|
308
345
|
status,
|
|
309
346
|
...(metAtRound !== null ? { met_at_round: metAtRound } : {}),
|
|
310
347
|
// v3.8.1: explicit refs only — empty when no contract item links this
|
|
311
348
|
// criterion to a sub-goal.
|
|
312
349
|
related_subgoal_ids: fact?.related_subgoal_ids ?? [],
|
|
313
|
-
};
|
|
314
|
-
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
return statuses;
|
|
315
353
|
}
|
|
316
354
|
/** v3.8: Rank item statuses so the strongest machine fact wins when several
|
|
317
355
|
* items reference the same criterion. */
|
|
@@ -320,21 +358,8 @@ criterionFacts = []) {
|
|
|
320
358
|
* EXACTLY equal after normalization (v3.8.1 — the similarity fallback is
|
|
321
359
|
* gone, so a paraphrase is a different criterion). */
|
|
322
360
|
export function criteriaMatch(a, b) {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// Both are IDs — exact string comparison
|
|
326
|
-
if (aIsId && bIsId)
|
|
327
|
-
return a === b;
|
|
328
|
-
// One is an ID — derive ID from the other and compare
|
|
329
|
-
if (aIsId)
|
|
330
|
-
return a === deriveCriterionId(b);
|
|
331
|
-
if (bIsId)
|
|
332
|
-
return deriveCriterionId(a) === b;
|
|
333
|
-
// v3.8.1: normalized text must be EXACTLY equal. The former Jaccard
|
|
334
|
-
// fallback let two merely-similar criterion strings count as the same
|
|
335
|
-
// criterion, which could fuse distinct criteria (and suppress or invent a
|
|
336
|
-
// criteria milestone) on a similarity score rather than on an identity.
|
|
337
|
-
return normalizeText(a) === normalizeText(b);
|
|
361
|
+
// v3.9.2: ONE identity rule, shared with the prompt and submission boundary.
|
|
362
|
+
return criterionRefMatches(a, b);
|
|
338
363
|
}
|
|
339
364
|
/** Detect newly met criteria by comparing the current entry's
|
|
340
365
|
* met criterion_claims against the previous entry's.
|
|
@@ -507,13 +532,16 @@ export { deriveCriterionId, isCriterionId } from "./token-utils.js";
|
|
|
507
532
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
508
533
|
/** Determine the source of a constraint by matching against known sources. */
|
|
509
534
|
function constraintSource(text, objective, constraintsFromPlan) {
|
|
510
|
-
const norm = text
|
|
511
|
-
|
|
535
|
+
const norm = normalizeText(text);
|
|
536
|
+
// v3.9.4: criteria identity wins. A text that is both a criterion and a
|
|
537
|
+
// hard/plan constraint keeps its cr-* id everywhere; the prompt partition
|
|
538
|
+
// renders it once, in the criteria section.
|
|
539
|
+
if (objective.success_criteria.some((c) => normalizeText(c) === norm))
|
|
540
|
+
return "criteria";
|
|
541
|
+
if (objective.hard_constraints.some((c) => normalizeText(c) === norm))
|
|
512
542
|
return "hard";
|
|
513
|
-
if (constraintsFromPlan.some((c) => c
|
|
543
|
+
if (constraintsFromPlan.some((c) => normalizeText(c) === norm))
|
|
514
544
|
return "plan";
|
|
515
|
-
if (objective.success_criteria.some((c) => c.toLowerCase().trim() === norm))
|
|
516
|
-
return "criteria";
|
|
517
545
|
return "discovered";
|
|
518
546
|
}
|
|
519
547
|
/** True when a reported violation text matches a constraint text.
|
|
@@ -592,40 +620,57 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
592
620
|
}
|
|
593
621
|
return { active, metadata };
|
|
594
622
|
}
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
623
|
+
/** Resolve the ONE previous-round fact bundle for a compile.
|
|
624
|
+
*
|
|
625
|
+
* The committed view wins whenever a round is committed. `last_round_result`
|
|
626
|
+
* is only the caller's in-flight projection for engine/library callers with
|
|
627
|
+
* no committed round in the context. In-flight attempt findings are
|
|
628
|
+
* authoritative even when empty: a retry must never inherit the previous
|
|
629
|
+
* committed round's flags. */
|
|
630
|
+
function resolvePreviousRoundFacts(request, context, windowRounds) {
|
|
631
|
+
const rounds = windowRounds ??
|
|
632
|
+
derivationRounds(loopEntries(request.loop_id, context), request.round);
|
|
633
|
+
const view = rounds.find((round) => round.round === request.round - 1) ?? null;
|
|
634
|
+
const recoveryBoundary = pendingBacktrackRecord(loopEntries(request.loop_id, context), request.round) !== null;
|
|
635
|
+
const inFlight = (request.attempt ?? 1) > 1 || recoveryBoundary;
|
|
636
|
+
return {
|
|
637
|
+
view,
|
|
638
|
+
result: view
|
|
639
|
+
? roundResultFromCommittedRound(view)
|
|
640
|
+
: request.last_round_result,
|
|
641
|
+
verificationFlags: inFlight
|
|
642
|
+
? (request.verification_flags ?? [])
|
|
643
|
+
: (view?.verificationFlags ?? request.verification_flags ?? []),
|
|
644
|
+
freshness: view ? derivePromptFreshness(view) : UNKNOWN_PROMPT_FRESHNESS,
|
|
645
|
+
recoveryBoundary,
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
function levelDecision(request, prior, previous) {
|
|
612
649
|
return decidePromptLevel({
|
|
613
650
|
round: request.round,
|
|
614
651
|
attempt: request.attempt,
|
|
615
652
|
forceLevel: request.force_level,
|
|
616
653
|
hasPlanSource: Boolean(request.plan_source),
|
|
617
|
-
recoveryBoundary,
|
|
618
|
-
checkpointBoundary:
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
654
|
+
recoveryBoundary: previous.recoveryBoundary,
|
|
655
|
+
checkpointBoundary: previous.result?.compression_checkpoint === true,
|
|
656
|
+
// An empty goal_id is unobservable, not a goal change: never fabricate L2.
|
|
657
|
+
goalChanged: prior !== null && prior.goal_id !== "" &&
|
|
658
|
+
prior.goal_id !== deriveGoalId(request.loop_id, request.task, request.goal_id),
|
|
659
|
+
previousStateMissing: request.round > 1 && prior === null,
|
|
660
|
+
previousRoundFailed: previous.view
|
|
661
|
+
? previous.view.success === false
|
|
662
|
+
: previous.result?.success === false,
|
|
663
|
+
previousRoundObservableNewInformation: previous.view
|
|
664
|
+
? previous.freshness.observableNewInformation
|
|
665
|
+
: null,
|
|
666
|
+
verificationContradicted: previous.verificationFlags
|
|
623
667
|
.some((flag) => flag.severity === "error"),
|
|
624
668
|
consecutiveRejections: request.consecutive_rejections,
|
|
625
669
|
});
|
|
626
670
|
}
|
|
627
671
|
export function decideLevel(request, context) {
|
|
628
|
-
|
|
672
|
+
const prior = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
673
|
+
return levelDecision(request, prior, resolvePreviousRoundFacts(request, context)).level;
|
|
629
674
|
}
|
|
630
675
|
/** The prompt's evaluation block. All content (examples, contract/sub-goal
|
|
631
676
|
* guidance, flag-specific hints) comes from the one FIELD_GUIDES table in
|
|
@@ -643,47 +688,85 @@ export function buildSelfEvalBlock(ctx) {
|
|
|
643
688
|
* rollback decision's own fields — no new persistence, no rejected
|
|
644
689
|
* payloads. */
|
|
645
690
|
function committedBacktrackBrief(loopId, round, context) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
//
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
// reads the durable :feedback envelope, so both carriers decode to one shape
|
|
653
|
-
// — and this brief and the rollback prompt no longer render the same
|
|
654
|
-
// rollback with two hand-written line builders.
|
|
655
|
-
const record = decodeBacktrackRecord(entryLineage(entry));
|
|
691
|
+
// The SAME pending-backtrack lookup the recovery boundary uses: a deep
|
|
692
|
+
// restore commits at the failed round while its redo targets
|
|
693
|
+
// `backtrackTarget + 1`, so an exact-round match loses both the boundary
|
|
694
|
+
// and this brief.
|
|
695
|
+
const view = pendingBacktrackRecord(loopEntries(loopId, context), round);
|
|
696
|
+
const record = view?.backtrack;
|
|
656
697
|
if (!record)
|
|
657
698
|
return undefined;
|
|
658
|
-
|
|
699
|
+
const lines = recoveryBriefLines({
|
|
659
700
|
target: record.target,
|
|
660
701
|
triggerRule: record.triggerRule,
|
|
702
|
+
// The committed decision's own sentence — the machine's reason for the
|
|
703
|
+
// rollback, preserved as a fact (the old prose header's "Why" content).
|
|
704
|
+
...(typeof view?.result?.enforcementReason === "string" &&
|
|
705
|
+
view.result.enforcementReason.trim()
|
|
706
|
+
? { failureReason: view.result.enforcementReason }
|
|
707
|
+
: {}),
|
|
661
708
|
failedRounds: record.failedRounds,
|
|
662
709
|
approaches: record.approaches,
|
|
663
710
|
wrongAssumptions: record.wrongAssumptions,
|
|
711
|
+
// The redo round's stable identity is a fact the old prepended prose
|
|
712
|
+
// carried; keep it inside the artifact now.
|
|
713
|
+
recoveryRoundId: `loop:${loopId}:round:${record.target + 1}`,
|
|
664
714
|
});
|
|
715
|
+
// v3.9.4: the restore point's machine facts belong in the prompt, not only
|
|
716
|
+
// in the prose backtrack header. Deterministic order and a visible overflow
|
|
717
|
+
// count keep the fact complete without making the section unbounded.
|
|
718
|
+
if (record.targetGitHead) {
|
|
719
|
+
lines.push(`- **Restore HEAD**: \`${record.targetGitHead}\``);
|
|
720
|
+
}
|
|
721
|
+
if (record.skippedFiles.length > 0) {
|
|
722
|
+
const files = [...record.skippedFiles].sort();
|
|
723
|
+
const shown = files.slice(0, 15);
|
|
724
|
+
lines.push("- **Files changed in failed rounds**:");
|
|
725
|
+
for (const file of shown)
|
|
726
|
+
lines.push(` - \`${file}\``);
|
|
727
|
+
if (files.length > shown.length) {
|
|
728
|
+
lines.push(` - … and ${files.length - shown.length} more files`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
// The machine requirement the recovery checkpoint enforces. A fact, not a
|
|
732
|
+
// recipe: LoopForge never says HOW to restore the workspace.
|
|
733
|
+
if (record.targetGitHead || record.skippedFiles.length > 0) {
|
|
734
|
+
lines.push(`- **Restore requirement**: the next submission's workspace is checked ` +
|
|
735
|
+
`against round ${record.target}` +
|
|
736
|
+
(record.targetGitHead ? ` (HEAD \`${record.targetGitHead}\`)` : "") +
|
|
737
|
+
"; a leftover change from the failed rounds rejects the recovery checkpoint.");
|
|
738
|
+
}
|
|
739
|
+
return lines;
|
|
665
740
|
}
|
|
666
741
|
export function compileLoop(request, context) {
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const constraints = evolveConstraints(request, objective, previous, context);
|
|
671
|
-
const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round, request.last_round_result?.constraint_violations ?? []);
|
|
672
|
-
const rolling = buildRollingSummary(request.loop_id, request.round, context, 0);
|
|
673
|
-
// v3.8: ONE committed-round derivation feeds every projection below
|
|
674
|
-
// (sub-goals, the active contract, its item statuses, criteria).
|
|
742
|
+
// ONE history window and ONE previous-round fact bundle for the whole
|
|
743
|
+
// compile. Every consumer below reads these values; none re-reads the
|
|
744
|
+
// caller's mutable last_round_result.
|
|
675
745
|
const windowRounds = derivationRounds(loopEntries(request.loop_id, context), request.round);
|
|
746
|
+
const prior = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
747
|
+
const previous = resolvePreviousRoundFacts(request, context, windowRounds);
|
|
748
|
+
const decision = levelDecision(request, prior, previous);
|
|
749
|
+
const objective = evolveObjective(request, context, previous);
|
|
750
|
+
const constraints = evolveConstraints(request, objective, previous, windowRounds);
|
|
751
|
+
const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round,
|
|
752
|
+
// "Violated this round" means the CURRENT compile round's report. A
|
|
753
|
+
// committed previous round's violations are found by the committed
|
|
754
|
+
// history scan at their real round, not relabelled as this round.
|
|
755
|
+
previous.view ? [] : (request.last_round_result?.constraint_violations ?? []));
|
|
756
|
+
const rolling = buildRollingSummary(request.loop_id, request.round, context, 0);
|
|
676
757
|
// v3.8: sub-goal lifecycle lives in subgoal-state.ts and consumes the
|
|
677
|
-
// shared committed-round view — one history interpretation.
|
|
758
|
+
// shared committed-round view — one history interpretation. The previous
|
|
759
|
+
// round's report is idempotently replayed; a committed round is already in
|
|
760
|
+
// the window, while the library fallback has no view.
|
|
678
761
|
const subGoals = deriveSubGoals({
|
|
679
762
|
loopId: request.loop_id,
|
|
680
763
|
currentRound: request.round,
|
|
681
764
|
rounds: windowRounds,
|
|
682
|
-
currentReport:
|
|
765
|
+
currentReport: previous.result
|
|
683
766
|
? {
|
|
684
|
-
round:
|
|
685
|
-
emergedSubtasks:
|
|
686
|
-
subgoalUpdates:
|
|
767
|
+
round: previous.result.round || (request.round - 1),
|
|
768
|
+
emergedSubtasks: previous.result.emerged_subtasks ?? [],
|
|
769
|
+
subgoalUpdates: previous.result.subgoal_updates ?? [],
|
|
687
770
|
}
|
|
688
771
|
: null,
|
|
689
772
|
});
|
|
@@ -705,8 +788,18 @@ export function compileLoop(request, context) {
|
|
|
705
788
|
// `roundFacts.verifiedSubGoals` is not read here — it reaches the state file
|
|
706
789
|
// and the projection through the bundle itself.
|
|
707
790
|
const { activeContract } = roundFacts;
|
|
791
|
+
// v3.9.4: the criterion reference space is the ACTIVE criterion set, not
|
|
792
|
+
// every text the objective ever carried. A retracted criterion must not be
|
|
793
|
+
// claimable or contract-referenceable while the prompt does not render it.
|
|
794
|
+
const activeConstraintKeys = new Set(constraintLifecycle.active.map((text) => normalizeText(text)));
|
|
795
|
+
const activeCriteria = uniqueNormalized(objective.success_criteria)
|
|
796
|
+
.filter((text) => activeConstraintKeys.has(normalizeText(text)));
|
|
797
|
+
const criterionObjective = {
|
|
798
|
+
...objective,
|
|
799
|
+
success_criteria: activeCriteria,
|
|
800
|
+
};
|
|
708
801
|
// v3.2: Goal → criteria → evidence vertical view (derived, zero persistence).
|
|
709
|
-
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context,
|
|
802
|
+
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context, criterionObjective, request.round, previous.result, roundFacts.criterionFacts);
|
|
710
803
|
// v3.8.1: the ONE recurring-fact derivation, over the shared window. The
|
|
711
804
|
// prompt renders its recent tail; the state file renders the recurring set.
|
|
712
805
|
const recurringFlags = deriveRecurringFlags(windowRounds);
|
|
@@ -716,7 +809,7 @@ export function compileLoop(request, context) {
|
|
|
716
809
|
// gone with the rest of the fuzzy matching, and so are the task-alignment
|
|
717
810
|
// and loop-health warnings: both were Jaccard scores over task/objective
|
|
718
811
|
// text, never machine facts.
|
|
719
|
-
const repeatedDeclarations = duplicateEmergedDeclarations(
|
|
812
|
+
const repeatedDeclarations = duplicateEmergedDeclarations(previous.result?.emerged_subtasks ?? []);
|
|
720
813
|
const warnings = unique([
|
|
721
814
|
repeatedDeclarations.length > 0
|
|
722
815
|
? `duplicate_declaration: ${repeatedDeclarations.length} sub-goal ` +
|
|
@@ -731,14 +824,12 @@ export function compileLoop(request, context) {
|
|
|
731
824
|
// `constraint_violations`, i.e. a DIFFERENT source from the score it was
|
|
732
825
|
// displayed next to. "Trend" was therefore not the score's history at all.
|
|
733
826
|
// Neither fed a decision.
|
|
734
|
-
const completedEntries = loopEntries(request.loop_id, context)
|
|
735
|
-
.filter((e) => entryRound(e) >= 1 && entryRound(e) < request.round);
|
|
736
|
-
const committedRounds = derivationRounds(completedEntries, request.round);
|
|
737
827
|
// v3.3/v3.8.1: machine git-motion (display-only, derived). The per-round
|
|
738
828
|
// "round stats" table is deleted: its only renderer was the L2 prompt
|
|
739
829
|
// section removed above, so it was computed, put into the canonical state
|
|
740
|
-
// (and therefore into stateHash) and read by nothing.
|
|
741
|
-
|
|
830
|
+
// (and therefore into stateHash) and read by nothing. `windowRounds` is the
|
|
831
|
+
// ONE window computed at the top of this function.
|
|
832
|
+
const machineStatus = deriveMachineStatus(windowRounds, request.round);
|
|
742
833
|
const response = makeLoopCompileResponse({
|
|
743
834
|
status: AgentStatus.OK,
|
|
744
835
|
recompile_level: decision.level,
|
|
@@ -773,6 +864,10 @@ export function compileLoop(request, context) {
|
|
|
773
864
|
// truth, its verification view and the machine-verified sub-goal facts as
|
|
774
865
|
// ONE derivation. The static capability rides on the state itself.
|
|
775
866
|
roundFacts,
|
|
867
|
+
// 3.9.3: the same previous-round bundle the level decision used. This is
|
|
868
|
+
// what keeps changes/discoveries/blockers/remaining/progress identical on
|
|
869
|
+
// live, resume, and unpause compiles.
|
|
870
|
+
previousRound: previous,
|
|
776
871
|
});
|
|
777
872
|
// The recovery brief is a current-round fact, computed once and shared by
|
|
778
873
|
// the state file and the evaluation block. It never becomes prompt control
|
|
@@ -821,13 +916,19 @@ export function compileLoop(request, context) {
|
|
|
821
916
|
commands: contractCommands,
|
|
822
917
|
contractVerificationAvailable: configuredCapability.contractVerificationAvailable,
|
|
823
918
|
subGoals,
|
|
824
|
-
verificationFlags:
|
|
919
|
+
verificationFlags: previous.verificationFlags,
|
|
825
920
|
recovery: recoveryBrief !== undefined,
|
|
826
921
|
gateEnabled: policy.gate.enabled === true,
|
|
922
|
+
criteria: criterionStatuses,
|
|
923
|
+
currentRequirement: state.currentTask,
|
|
827
924
|
}),
|
|
828
925
|
fullStateMarkdown: l2Pointer ? undefined : markdown,
|
|
926
|
+
// v3.9.4: the SAME committed-backtrack facts the state file renders. The
|
|
927
|
+
// recovery section is part of the artifact, so the delivered prompt is
|
|
928
|
+
// exactly `artifact.renderedPrompt` (no post-compile prepend).
|
|
929
|
+
recoveryBrief,
|
|
829
930
|
// v2.9: Model's information needs from the previous round's SelfEvaluation
|
|
830
|
-
promptRequests:
|
|
931
|
+
promptRequests: previous.result?.prompt_requests,
|
|
831
932
|
});
|
|
832
933
|
response.prompt = artifact.renderedPrompt;
|
|
833
934
|
response.prompt_artifact = artifact;
|