loopforge 3.9.2 → 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/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/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 +217 -104
- 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.js +14 -4
- 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.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 +45 -8
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +4 -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 +8 -4
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +55 -5
- package/dist/self-eval.js.map +1 -1
- package/dist/submission-guidance.d.ts +25 -11
- package/dist/submission-guidance.d.ts.map +1 -1
- package/dist/submission-guidance.js +102 -44
- package/dist/submission-guidance.js.map +1 -1
- package/dist/submission-receipt.d.ts.map +1 -1
- package/dist/submission-receipt.js +1 -0
- package/dist/submission-receipt.js.map +1 -1
- package/dist/token-utils.d.ts +19 -0
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +41 -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 -1
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +19 -7
- 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 +2 -2
- package/package.json +68 -68
- package/skills/perception/SKILL.md +25 -6
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 { criterionRefMatches, deriveCriterionId, deriveItemId, normalizeText, unique, entryRound, } from "./token-utils.js";
|
|
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. */
|
|
@@ -494,13 +532,16 @@ export { deriveCriterionId, isCriterionId } from "./token-utils.js";
|
|
|
494
532
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
495
533
|
/** Determine the source of a constraint by matching against known sources. */
|
|
496
534
|
function constraintSource(text, objective, constraintsFromPlan) {
|
|
497
|
-
const norm = text
|
|
498
|
-
|
|
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))
|
|
499
542
|
return "hard";
|
|
500
|
-
if (constraintsFromPlan.some((c) => c
|
|
543
|
+
if (constraintsFromPlan.some((c) => normalizeText(c) === norm))
|
|
501
544
|
return "plan";
|
|
502
|
-
if (objective.success_criteria.some((c) => c.toLowerCase().trim() === norm))
|
|
503
|
-
return "criteria";
|
|
504
545
|
return "discovered";
|
|
505
546
|
}
|
|
506
547
|
/** True when a reported violation text matches a constraint text.
|
|
@@ -579,40 +620,57 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
579
620
|
}
|
|
580
621
|
return { active, metadata };
|
|
581
622
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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) {
|
|
599
649
|
return decidePromptLevel({
|
|
600
650
|
round: request.round,
|
|
601
651
|
attempt: request.attempt,
|
|
602
652
|
forceLevel: request.force_level,
|
|
603
653
|
hasPlanSource: Boolean(request.plan_source),
|
|
604
|
-
recoveryBoundary,
|
|
605
|
-
checkpointBoundary:
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
|
610
667
|
.some((flag) => flag.severity === "error"),
|
|
611
668
|
consecutiveRejections: request.consecutive_rejections,
|
|
612
669
|
});
|
|
613
670
|
}
|
|
614
671
|
export function decideLevel(request, context) {
|
|
615
|
-
|
|
672
|
+
const prior = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
673
|
+
return levelDecision(request, prior, resolvePreviousRoundFacts(request, context)).level;
|
|
616
674
|
}
|
|
617
675
|
/** The prompt's evaluation block. All content (examples, contract/sub-goal
|
|
618
676
|
* guidance, flag-specific hints) comes from the one FIELD_GUIDES table in
|
|
@@ -630,47 +688,85 @@ export function buildSelfEvalBlock(ctx) {
|
|
|
630
688
|
* rollback decision's own fields — no new persistence, no rejected
|
|
631
689
|
* payloads. */
|
|
632
690
|
function committedBacktrackBrief(loopId, round, context) {
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
// reads the durable :feedback envelope, so both carriers decode to one shape
|
|
640
|
-
// — and this brief and the rollback prompt no longer render the same
|
|
641
|
-
// rollback with two hand-written line builders.
|
|
642
|
-
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;
|
|
643
697
|
if (!record)
|
|
644
698
|
return undefined;
|
|
645
|
-
|
|
699
|
+
const lines = recoveryBriefLines({
|
|
646
700
|
target: record.target,
|
|
647
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
|
+
: {}),
|
|
648
708
|
failedRounds: record.failedRounds,
|
|
649
709
|
approaches: record.approaches,
|
|
650
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}`,
|
|
651
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;
|
|
652
740
|
}
|
|
653
741
|
export function compileLoop(request, context) {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const constraints = evolveConstraints(request, objective, previous, context);
|
|
658
|
-
const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round, request.last_round_result?.constraint_violations ?? []);
|
|
659
|
-
const rolling = buildRollingSummary(request.loop_id, request.round, context, 0);
|
|
660
|
-
// v3.8: ONE committed-round derivation feeds every projection below
|
|
661
|
-
// (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.
|
|
662
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);
|
|
663
757
|
// v3.8: sub-goal lifecycle lives in subgoal-state.ts and consumes the
|
|
664
|
-
// 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.
|
|
665
761
|
const subGoals = deriveSubGoals({
|
|
666
762
|
loopId: request.loop_id,
|
|
667
763
|
currentRound: request.round,
|
|
668
764
|
rounds: windowRounds,
|
|
669
|
-
currentReport:
|
|
765
|
+
currentReport: previous.result
|
|
670
766
|
? {
|
|
671
|
-
round:
|
|
672
|
-
emergedSubtasks:
|
|
673
|
-
subgoalUpdates:
|
|
767
|
+
round: previous.result.round || (request.round - 1),
|
|
768
|
+
emergedSubtasks: previous.result.emerged_subtasks ?? [],
|
|
769
|
+
subgoalUpdates: previous.result.subgoal_updates ?? [],
|
|
674
770
|
}
|
|
675
771
|
: null,
|
|
676
772
|
});
|
|
@@ -692,8 +788,18 @@ export function compileLoop(request, context) {
|
|
|
692
788
|
// `roundFacts.verifiedSubGoals` is not read here — it reaches the state file
|
|
693
789
|
// and the projection through the bundle itself.
|
|
694
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
|
+
};
|
|
695
801
|
// v3.2: Goal → criteria → evidence vertical view (derived, zero persistence).
|
|
696
|
-
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context,
|
|
802
|
+
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context, criterionObjective, request.round, previous.result, roundFacts.criterionFacts);
|
|
697
803
|
// v3.8.1: the ONE recurring-fact derivation, over the shared window. The
|
|
698
804
|
// prompt renders its recent tail; the state file renders the recurring set.
|
|
699
805
|
const recurringFlags = deriveRecurringFlags(windowRounds);
|
|
@@ -703,7 +809,7 @@ export function compileLoop(request, context) {
|
|
|
703
809
|
// gone with the rest of the fuzzy matching, and so are the task-alignment
|
|
704
810
|
// and loop-health warnings: both were Jaccard scores over task/objective
|
|
705
811
|
// text, never machine facts.
|
|
706
|
-
const repeatedDeclarations = duplicateEmergedDeclarations(
|
|
812
|
+
const repeatedDeclarations = duplicateEmergedDeclarations(previous.result?.emerged_subtasks ?? []);
|
|
707
813
|
const warnings = unique([
|
|
708
814
|
repeatedDeclarations.length > 0
|
|
709
815
|
? `duplicate_declaration: ${repeatedDeclarations.length} sub-goal ` +
|
|
@@ -718,14 +824,12 @@ export function compileLoop(request, context) {
|
|
|
718
824
|
// `constraint_violations`, i.e. a DIFFERENT source from the score it was
|
|
719
825
|
// displayed next to. "Trend" was therefore not the score's history at all.
|
|
720
826
|
// Neither fed a decision.
|
|
721
|
-
const completedEntries = loopEntries(request.loop_id, context)
|
|
722
|
-
.filter((e) => entryRound(e) >= 1 && entryRound(e) < request.round);
|
|
723
|
-
const committedRounds = derivationRounds(completedEntries, request.round);
|
|
724
827
|
// v3.3/v3.8.1: machine git-motion (display-only, derived). The per-round
|
|
725
828
|
// "round stats" table is deleted: its only renderer was the L2 prompt
|
|
726
829
|
// section removed above, so it was computed, put into the canonical state
|
|
727
|
-
// (and therefore into stateHash) and read by nothing.
|
|
728
|
-
|
|
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);
|
|
729
833
|
const response = makeLoopCompileResponse({
|
|
730
834
|
status: AgentStatus.OK,
|
|
731
835
|
recompile_level: decision.level,
|
|
@@ -760,6 +864,10 @@ export function compileLoop(request, context) {
|
|
|
760
864
|
// truth, its verification view and the machine-verified sub-goal facts as
|
|
761
865
|
// ONE derivation. The static capability rides on the state itself.
|
|
762
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,
|
|
763
871
|
});
|
|
764
872
|
// The recovery brief is a current-round fact, computed once and shared by
|
|
765
873
|
// the state file and the evaluation block. It never becomes prompt control
|
|
@@ -808,14 +916,19 @@ export function compileLoop(request, context) {
|
|
|
808
916
|
commands: contractCommands,
|
|
809
917
|
contractVerificationAvailable: configuredCapability.contractVerificationAvailable,
|
|
810
918
|
subGoals,
|
|
811
|
-
verificationFlags:
|
|
919
|
+
verificationFlags: previous.verificationFlags,
|
|
812
920
|
recovery: recoveryBrief !== undefined,
|
|
813
921
|
gateEnabled: policy.gate.enabled === true,
|
|
814
922
|
criteria: criterionStatuses,
|
|
923
|
+
currentRequirement: state.currentTask,
|
|
815
924
|
}),
|
|
816
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,
|
|
817
930
|
// v2.9: Model's information needs from the previous round's SelfEvaluation
|
|
818
|
-
promptRequests:
|
|
931
|
+
promptRequests: previous.result?.prompt_requests,
|
|
819
932
|
});
|
|
820
933
|
response.prompt = artifact.renderedPrompt;
|
|
821
934
|
response.prompt_artifact = artifact;
|