loopforge 3.7.0 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +171 -157
- package/dist/audit.d.ts +53 -1
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +106 -22
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +75 -69
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +276 -168
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +111 -12
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +13 -0
- package/dist/cognitive-facts.d.ts.map +1 -1
- package/dist/cognitive-facts.js +30 -16
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/cognitive-governance.d.ts +30 -6
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +74 -18
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +109 -24
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +235 -52
- package/dist/committed-round.js.map +1 -1
- package/dist/contract-items.d.ts +55 -0
- package/dist/contract-items.d.ts.map +1 -0
- package/dist/contract-items.js +180 -0
- package/dist/contract-items.js.map +1 -0
- package/dist/enforcement-gate.d.ts +43 -20
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +269 -378
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +11 -6
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +102 -67
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts +4 -4
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +16 -14
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +91 -90
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +349 -160
- package/dist/evidence-provider.js.map +1 -1
- package/dist/explain.d.ts +76 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +127 -0
- package/dist/explain.js.map +1 -0
- package/dist/generate-schema.js +250 -53
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +41 -40
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +275 -515
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +0 -1
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +0 -4
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts.map +1 -1
- package/dist/loop-projection.js +3 -1
- package/dist/loop-projection.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +40 -21
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +173 -120
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +63 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +53 -7
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +240 -139
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/submission-boundary.d.ts +89 -0
- package/dist/mcp/submission-boundary.d.ts.map +1 -0
- package/dist/mcp/submission-boundary.js +203 -0
- package/dist/mcp/submission-boundary.js.map +1 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +377 -125
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.js +2 -2
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +65 -95
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +151 -39
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +7 -33
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +313 -473
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -4
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +6 -7
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +411 -168
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +23 -69
- package/dist/protocol.js.map +1 -1
- package/dist/replay.js +2 -2
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +69 -57
- package/dist/round-contract.d.ts.map +1 -1
- package/dist/round-contract.js +104 -64
- package/dist/round-contract.js.map +1 -1
- package/dist/round-coordinator.d.ts +55 -16
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +179 -57
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +7 -5
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +2 -1
- package/dist/round-driver.js.map +1 -1
- package/dist/round-facts.d.ts +96 -0
- package/dist/round-facts.d.ts.map +1 -0
- package/dist/round-facts.js +216 -0
- package/dist/round-facts.js.map +1 -0
- package/dist/round-transaction.d.ts +23 -9
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +99 -22
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +127 -11
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +517 -36
- package/dist/self-eval.js.map +1 -1
- package/dist/subgoal-state.d.ts +77 -0
- package/dist/subgoal-state.d.ts.map +1 -0
- package/dist/subgoal-state.js +198 -0
- package/dist/subgoal-state.js.map +1 -0
- package/dist/token-utils.d.ts +52 -19
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +83 -72
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +27 -46
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +308 -557
- package/dist/verification-gate.js.map +1 -1
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/dist/workspace.d.ts +9 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +17 -0
- package/dist/workspace.js.map +1 -1
- package/loop_policy.json +17 -25
- package/package.json +7 -7
- package/skills/perception/SKILL.md +266 -64
package/dist/loop-compiler.js
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { getPolicy } from "./policy.js";
|
|
8
|
-
import { AgentStatus, makeLoopCompileResponse,
|
|
8
|
+
import { AgentStatus, makeLoopCompileResponse, makeLoopObjective, makeConstraintMeta, makeMilestoneSummary, makeRollingSummary, } from "./protocol.js";
|
|
9
9
|
import { createCanonicalLoopState, renderCanonicalStateMarkdown, } from "./canonical-state.js";
|
|
10
|
-
import { decodeRound, entryLineage, entryCriteriaMet, entryCriteriaRemaining, entryActiveConstraints, entryRetractedConstraints, entryProgressEstimate,
|
|
11
|
-
import {
|
|
10
|
+
import { decodeBacktrackRecord, decodeRound, entryLineage, entryCriteriaMet, entryCriteriaRemaining, entryActiveConstraints, entryRetractedConstraints, entryProgressEstimate, machineGitMotionSeries, derivationRounds, } from "./committed-round.js";
|
|
11
|
+
import { NO_IN_FLIGHT_ROUND, deriveRoundFacts } from "./round-facts.js";
|
|
12
|
+
import { recoveryBriefLines } from "./enforcement-gate.js";
|
|
13
|
+
import { deriveSubGoals, duplicateEmergedDeclarations } from "./subgoal-state.js";
|
|
14
|
+
import { claimedMetCriteria, claimedRemainingCriteria } from "./self-eval.js";
|
|
12
15
|
import { assemblePromptArtifact } from "./prompt-assembler.js";
|
|
13
16
|
import { decidePromptLevel, } from "./prompt-policy.js";
|
|
14
|
-
import { deriveItemId,
|
|
17
|
+
import { deriveCriterionId, deriveItemId, isCriterionId, normalizeText, unique, entryRound, } from "./token-utils.js";
|
|
15
18
|
function contextEntries(context) {
|
|
16
19
|
if (!context || !Array.isArray(context.results))
|
|
17
20
|
return [];
|
|
@@ -55,32 +58,6 @@ export function deriveGoalId(loopId, task, explicit = "") {
|
|
|
55
58
|
.replace(/^-|-$/g, "")
|
|
56
59
|
.slice(0, 48) || computeGoalTextHash(task)}`;
|
|
57
60
|
}
|
|
58
|
-
/** v3.2: Read the previous round's persisted L1 presentation snapshot — the
|
|
59
|
-
* diff baseline for L1 collapse. Returns null when the previous round's
|
|
60
|
-
* lineage entry lacks the three presented_* fields (as on L0/L2 compiles),
|
|
61
|
-
* so callers render the full L1 state. */
|
|
62
|
-
export function readPresentedBaseline(loopId, round, context) {
|
|
63
|
-
const entry = roundCanonicalEntry(loopEntries(loopId, context), round - 1);
|
|
64
|
-
if (!entry)
|
|
65
|
-
return null;
|
|
66
|
-
const data = entryLineage(entry);
|
|
67
|
-
const constraintIds = data.presented_constraint_ids;
|
|
68
|
-
const subGoals = data.presented_subgoals;
|
|
69
|
-
const milestoneRanges = data.presented_milestone_ranges;
|
|
70
|
-
if (!Array.isArray(constraintIds) || !constraintIds.every((v) => typeof v === "string") ||
|
|
71
|
-
!Array.isArray(subGoals) || !subGoals.every((v) => Array.isArray(v) && v.length === 2 &&
|
|
72
|
-
typeof v[0] === "string" && typeof v[1] === "string") ||
|
|
73
|
-
!Array.isArray(milestoneRanges) || !milestoneRanges.every((v) => Array.isArray(v) && v.length === 2 &&
|
|
74
|
-
typeof v[0] === "number" && typeof v[1] === "number")) {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
return {
|
|
78
|
-
round: round - 1,
|
|
79
|
-
constraintIds: constraintIds,
|
|
80
|
-
subGoals: subGoals,
|
|
81
|
-
milestoneRanges: milestoneRanges,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
61
|
export function getPreviousRound(loopId, round, context) {
|
|
85
62
|
const entry = roundCanonicalEntry(loopEntries(loopId, context), round);
|
|
86
63
|
if (!entry)
|
|
@@ -144,9 +121,16 @@ function evolveObjective(request, context) {
|
|
|
144
121
|
let version = current.version ?? 1;
|
|
145
122
|
if (last?.objective_refinement?.trim()) {
|
|
146
123
|
const refinement = last.objective_refinement.trim();
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
124
|
+
// M5: fold each refinement exactly once per persisted objective state.
|
|
125
|
+
// A retry recompiles the same round against the same committed
|
|
126
|
+
// last_round_result; without this guard every retry attempt re-appends
|
|
127
|
+
// the refinement and inflates the version — the objective text and the
|
|
128
|
+
// state file grew "Refinement: X" once per attempt.
|
|
129
|
+
if (!history.includes(refinement)) {
|
|
130
|
+
history.push(refinement);
|
|
131
|
+
objective = `${objective}\nRefinement: ${refinement}`;
|
|
132
|
+
version++;
|
|
133
|
+
}
|
|
150
134
|
}
|
|
151
135
|
const revisions = new Map((last?.revised_success_criteria ?? []).map((item) => [item.old, item.new]));
|
|
152
136
|
return makeLoopObjective({
|
|
@@ -209,86 +193,50 @@ function evolveConstraints(request, objective, previous, context) {
|
|
|
209
193
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
210
194
|
// Hierarchical Summary (v2.1)
|
|
211
195
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
212
|
-
/** v3.
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
for (const flag of flags) {
|
|
238
|
-
const target = flag.severity === "error"
|
|
239
|
-
? errors
|
|
240
|
-
: flag.severity === "warn" ? warns : null;
|
|
241
|
-
if (!target)
|
|
242
|
-
continue;
|
|
243
|
-
const list = target.get(flag.check) ?? [];
|
|
244
|
-
list.push(rnd);
|
|
245
|
-
target.set(flag.check, list);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
const lessons = [];
|
|
249
|
-
for (const [text, rounds] of violations) {
|
|
250
|
-
if (rounds.length >= 2) {
|
|
251
|
-
lessons.push({ text, kind: "constraint_violation", count: rounds.length, rounds });
|
|
196
|
+
/** v3.8.1: THE recurring-fact derivation — one walk over the committed
|
|
197
|
+
* history, grouping repeated machine facts by what they are ABOUT.
|
|
198
|
+
*
|
|
199
|
+
* This replaces three separate answers to "what keeps going wrong":
|
|
200
|
+
* `deriveLessons` (whole history, count >= 2, keyed by constraint text or
|
|
201
|
+
* check id), `rolling_summary.recurring_issues` (the last 5 rounds' raw
|
|
202
|
+
* violation texts, with NO threshold at all despite the name), and the L1/L2
|
|
203
|
+
* sections built on top of each. Three windows over one fact set is exactly
|
|
204
|
+
* the parallel semantics this release removes.
|
|
205
|
+
*
|
|
206
|
+
* Callers FILTER this list rather than re-deriving: the state file renders
|
|
207
|
+
* the genuinely recurring set (count >= 2), the prompt renders the recent
|
|
208
|
+
* tail as Active Warnings.
|
|
209
|
+
*
|
|
210
|
+
* Presentation only: the result never feeds enforcement. */
|
|
211
|
+
export function deriveRecurringFlags(committedRounds) {
|
|
212
|
+
const byKey = new Map();
|
|
213
|
+
const record = (kind, subject, ref, round) => {
|
|
214
|
+
const key = `${kind}${subject}${ref}`;
|
|
215
|
+
const existing = byKey.get(key);
|
|
216
|
+
if (existing) {
|
|
217
|
+
existing.count++;
|
|
218
|
+
if (!existing.rounds.includes(round))
|
|
219
|
+
existing.rounds.push(round);
|
|
220
|
+
return;
|
|
252
221
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
222
|
+
byKey.set(key, { subject, kind, ref, count: 1, rounds: [round] });
|
|
223
|
+
};
|
|
224
|
+
for (const round of committedRounds) {
|
|
225
|
+
for (const text of round.constraintViolations ?? []) {
|
|
226
|
+
record("constraint_violation", text, "", round.round);
|
|
257
227
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
228
|
+
for (const flag of round.verificationFlags) {
|
|
229
|
+
const kind = flag.severity === "error"
|
|
230
|
+
? "verification_error"
|
|
231
|
+
: flag.severity === "warn" ? "verification_warning" : null;
|
|
232
|
+
if (!kind)
|
|
233
|
+
continue;
|
|
234
|
+
record(kind, flag.check, flag.ref ?? "", round.round);
|
|
262
235
|
}
|
|
263
236
|
}
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
* committed rounds — files changed, rejected attempts (snapshot.attempt
|
|
268
|
-
* minus one: the successful commit's attempt counts the redo), and the
|
|
269
|
-
* self-reported progress delta vs the previous round. One row per round,
|
|
270
|
-
* preferring the :feedback entry when both lineage and feedback entries
|
|
271
|
-
* exist. Zero persistence — re-derived from committed entries. */
|
|
272
|
-
function deriveRoundStats(rounds, window = 5) {
|
|
273
|
-
const stats = [];
|
|
274
|
-
let prevProgress = null;
|
|
275
|
-
for (const round of rounds.slice(-window)) {
|
|
276
|
-
const ev = round.executionEvidence;
|
|
277
|
-
const files = ev?.files_changed;
|
|
278
|
-
const rawProgress = ev?.progress_estimate;
|
|
279
|
-
const progress = typeof rawProgress === "number" ? rawProgress : null;
|
|
280
|
-
stats.push({
|
|
281
|
-
round: round.round,
|
|
282
|
-
filesChangedCount: Array.isArray(files) ? files.length : null,
|
|
283
|
-
rejectedAttempts: round.attempt >= 1 ? round.attempt - 1 : null,
|
|
284
|
-
progressDelta: progress !== null && prevProgress !== null
|
|
285
|
-
? Number((progress - prevProgress).toFixed(3))
|
|
286
|
-
: null,
|
|
287
|
-
});
|
|
288
|
-
if (progress !== null)
|
|
289
|
-
prevProgress = progress;
|
|
290
|
-
}
|
|
291
|
-
return stats;
|
|
237
|
+
return [...byKey.values()].sort((a, b) => b.count - a.count ||
|
|
238
|
+
a.subject.localeCompare(b.subject) ||
|
|
239
|
+
a.ref.localeCompare(b.ref));
|
|
292
240
|
}
|
|
293
241
|
/** v3.3: Machine git-motion cross-check over the last 3 committed rounds
|
|
294
242
|
* (the R4 window). Undefined when no committed rounds carry git snapshots
|
|
@@ -301,39 +249,36 @@ function deriveMachineStatus(rounds, currentRound) {
|
|
|
301
249
|
const motionRounds = series.filter(Boolean).length;
|
|
302
250
|
return { windowRounds: 3, gitMotion: motionRounds > 0, motionRounds };
|
|
303
251
|
}
|
|
304
|
-
/** v3.4: ACTIVE Round Contract for the compile round — derived from the
|
|
305
|
-
* committed evals of earlier rounds only, never from request data. Reads
|
|
306
|
-
* the hydrated merged lineage entries (one per round; a committed decision
|
|
307
|
-
* is merged onto the round's lineage entry with `committed_action` set),
|
|
308
|
-
* skipping rounds whose committed action was "backtrack" (a roll-back
|
|
309
|
-
* directive, not an executed round). Because it runs identically on every
|
|
310
|
-
* compile path — including rejection retries and resume / unpause /
|
|
311
|
-
* backtrack compiles that carry no last_round_result — a contract round's
|
|
312
|
-
* re-compiles keep showing its contract as the Current Task. Null when no
|
|
313
|
-
* active contract: the Current Task falls back to the original task text. */
|
|
314
|
-
function deriveActiveContract(loopId, context, currentRound) {
|
|
315
|
-
const entries = loopEntries(loopId, context);
|
|
316
|
-
const canonical = Array.from({ length: Math.max(0, currentRound - 1) }, (_, index) => roundCanonicalEntry(entries, index + 1)).filter((entry) => entry !== null);
|
|
317
|
-
return deriveActiveRoundContract(contractRoundEvaluations(mergedRoundsFromEntries(canonical, currentRound)));
|
|
318
|
-
}
|
|
319
252
|
/** v3.2: Derive per-criterion status — the "goal → criteria → evidence"
|
|
320
253
|
* vertical view. Each objective criterion gets: met/remaining/unknown
|
|
321
|
-
* (from per-round
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* vault every compile. */
|
|
325
|
-
export function deriveCriterionStatuses(loopId, context, objective, currentRound,
|
|
254
|
+
* (from per-round criterion_claims, ID-first or normalized-exact matching),
|
|
255
|
+
* the round it was first reported met, and the sub-goals a contract item
|
|
256
|
+
* referencing it also names (explicit `subgoal_refs`, never a text guess).
|
|
257
|
+
* Zero persistence — re-derived from the vault every compile. */
|
|
258
|
+
export function deriveCriterionStatuses(loopId, context, objective, currentRound, lastRoundResult,
|
|
259
|
+
/** v3.8.1: the criterion machine facts from the shared bundle. A criterion
|
|
260
|
+
* is `verified` / `contradicted` / `insufficient` only through an item that
|
|
261
|
+
* references it — a claim alone can never reach `verified` — and the fact
|
|
262
|
+
* is derived over the WHOLE committed history, so it survives its contract
|
|
263
|
+
* closing (see `deriveCriterionFacts`). */
|
|
264
|
+
criterionFacts = []) {
|
|
326
265
|
if (!objective || objective.success_criteria.length === 0)
|
|
327
266
|
return [];
|
|
328
267
|
const entries = loopEntries(loopId, context).filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
329
268
|
// The current round's report lives in last_round_result (the vault only
|
|
330
269
|
// gains it on commit); treat it as the report for round currentRound - 1.
|
|
331
|
-
const lastEv = lastRoundResult?.
|
|
332
|
-
const lastMet = lastEv
|
|
333
|
-
const lastRemaining = lastEv
|
|
270
|
+
const lastEv = lastRoundResult?.execution_report;
|
|
271
|
+
const lastMet = claimedMetCriteria(lastEv);
|
|
272
|
+
const lastRemaining = claimedRemainingCriteria(lastEv);
|
|
334
273
|
const lastEntry = entries[entries.length - 1] ?? null;
|
|
335
274
|
const lastEntryRemaining = lastEntry ? entryCriteriaRemaining(lastEntry) : [];
|
|
336
|
-
|
|
275
|
+
// v3.8.1: criterion → machine status + explicit sub-goal links, from the
|
|
276
|
+
// shared `RoundFacts` bundle (whole committed history). It used to be
|
|
277
|
+
// re-derived here from the ACTIVE contract, which made both maps empty the
|
|
278
|
+
// moment the contract closed — the criterion's machine fact, and the
|
|
279
|
+
// sub-goal links carried on the same item, disappeared while the sub-goal
|
|
280
|
+
// side of the very same fact survived. One derivation, one window.
|
|
281
|
+
const factsByCriterion = new Map(criterionFacts.map((fact) => [fact.criterion_id, fact]));
|
|
337
282
|
return objective.success_criteria.map((text) => {
|
|
338
283
|
let metAtRound = null;
|
|
339
284
|
for (const entry of entries) {
|
|
@@ -346,28 +291,33 @@ export function deriveCriterionStatuses(loopId, context, objective, currentRound
|
|
|
346
291
|
if (metAtRound === null && lastMet.some((met) => criteriaMatch(met, text))) {
|
|
347
292
|
metAtRound = currentRound - 1;
|
|
348
293
|
}
|
|
349
|
-
|
|
350
|
-
|
|
294
|
+
// v3.8: a claim reaches `claimed` at most. Only a machine fact about a
|
|
295
|
+
// contract item that references this criterion can go further.
|
|
296
|
+
const claimedStatus = metAtRound !== null
|
|
297
|
+
? "claimed"
|
|
351
298
|
: lastRemaining.some((remaining) => criteriaMatch(remaining, text)) ||
|
|
352
299
|
lastEntryRemaining.some((remaining) => criteriaMatch(remaining, text))
|
|
353
300
|
? "remaining"
|
|
354
301
|
: "unknown";
|
|
302
|
+
const fact = factsByCriterion.get(deriveCriterionId(text));
|
|
303
|
+
const status = fact?.status ?? claimedStatus;
|
|
355
304
|
return {
|
|
356
305
|
id: deriveCriterionId(text),
|
|
357
306
|
text,
|
|
358
307
|
status,
|
|
359
308
|
...(metAtRound !== null ? { met_at_round: metAtRound } : {}),
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
309
|
+
// v3.8.1: explicit refs only — empty when no contract item links this
|
|
310
|
+
// criterion to a sub-goal.
|
|
311
|
+
related_subgoal_ids: fact?.related_subgoal_ids ?? [],
|
|
363
312
|
};
|
|
364
313
|
});
|
|
365
314
|
}
|
|
366
|
-
/**
|
|
367
|
-
*
|
|
368
|
-
|
|
369
|
-
*
|
|
370
|
-
*
|
|
315
|
+
/** v3.8: Rank item statuses so the strongest machine fact wins when several
|
|
316
|
+
* items reference the same criterion. */
|
|
317
|
+
/** Match two criterion references. If either is a criterion id
|
|
318
|
+
* (cr-XXXXXXXX), compares ids; otherwise requires the two texts to be
|
|
319
|
+
* EXACTLY equal after normalization (v3.8.1 — the similarity fallback is
|
|
320
|
+
* gone, so a paraphrase is a different criterion). */
|
|
371
321
|
export function criteriaMatch(a, b) {
|
|
372
322
|
const aIsId = isCriterionId(a);
|
|
373
323
|
const bIsId = isCriterionId(b);
|
|
@@ -379,13 +329,16 @@ export function criteriaMatch(a, b) {
|
|
|
379
329
|
return a === deriveCriterionId(b);
|
|
380
330
|
if (bIsId)
|
|
381
331
|
return deriveCriterionId(a) === b;
|
|
382
|
-
//
|
|
383
|
-
|
|
332
|
+
// v3.8.1: normalized text must be EXACTLY equal. The former Jaccard
|
|
333
|
+
// fallback let two merely-similar criterion strings count as the same
|
|
334
|
+
// criterion, which could fuse distinct criteria (and suppress or invent a
|
|
335
|
+
// criteria milestone) on a similarity score rather than on an identity.
|
|
336
|
+
return normalizeText(a) === normalizeText(b);
|
|
384
337
|
}
|
|
385
338
|
/** Detect newly met criteria by comparing the current entry's
|
|
386
|
-
*
|
|
387
|
-
* v2.11:
|
|
388
|
-
*
|
|
339
|
+
* met criterion_claims against the previous entry's.
|
|
340
|
+
* v2.11/v3.8.1: id-first (cr-XXXXXXXX), then normalized-exact text —
|
|
341
|
+
* no similarity.
|
|
389
342
|
* Returns empty array when there is no previous entry — the first
|
|
390
343
|
* entry's criteria are the baseline, not a "new" event. */
|
|
391
344
|
function detectNewCriteria(current, previous) {
|
|
@@ -426,56 +379,14 @@ function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, ki
|
|
|
426
379
|
generated_at_round: endRound,
|
|
427
380
|
});
|
|
428
381
|
}
|
|
429
|
-
/** v3.
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
return mid;
|
|
438
|
-
const chosen = [];
|
|
439
|
-
const used = new Set();
|
|
440
|
-
for (const m of mid) {
|
|
441
|
-
if (m.kind === "agent_declared") {
|
|
442
|
-
chosen.push(m);
|
|
443
|
-
used.add(m);
|
|
444
|
-
if (chosen.length >= budget)
|
|
445
|
-
return chosen;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
const rest = mid.filter((m) => !used.has(m));
|
|
449
|
-
const need = budget - chosen.length;
|
|
450
|
-
for (let i = 0; i < need; i++) {
|
|
451
|
-
const idx = Math.floor(((i + 0.5) * rest.length) / need);
|
|
452
|
-
chosen.push(rest[idx]);
|
|
453
|
-
}
|
|
454
|
-
return chosen;
|
|
455
|
-
}
|
|
456
|
-
/** v3.0.1: L1 milestone sampling — when milestones exceed max_milestones,
|
|
457
|
-
* keep the oldest `milestone_head_count` (history anchors), the newest
|
|
458
|
-
* `milestone_tail_count` (current progress), and an even sample of the
|
|
459
|
-
* middle. L2 (full rehydration) is never sampled: the recovery view keeps
|
|
460
|
-
* every milestone as the loop's memory skeleton. */
|
|
461
|
-
function sampleMilestones(milestones, policy, level) {
|
|
462
|
-
if (level === "l2")
|
|
463
|
-
return milestones;
|
|
464
|
-
if (milestones.length <= policy.max_milestones)
|
|
465
|
-
return milestones;
|
|
466
|
-
const head = Math.max(0, policy.milestone_head_count);
|
|
467
|
-
const tail = Math.max(0, policy.milestone_tail_count);
|
|
468
|
-
const midBudget = Math.max(0, policy.max_milestones - head - tail);
|
|
469
|
-
if (midBudget === 0)
|
|
470
|
-
return milestones.slice(0, head).concat(milestones.slice(milestones.length - tail));
|
|
471
|
-
const mid = milestones.slice(head, milestones.length - tail);
|
|
472
|
-
return [
|
|
473
|
-
...milestones.slice(0, head),
|
|
474
|
-
...sampleMiddleMilestones(mid, midBudget),
|
|
475
|
-
...milestones.slice(milestones.length - tail),
|
|
476
|
-
];
|
|
477
|
-
}
|
|
478
|
-
export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0, level) {
|
|
382
|
+
/** v3.8.1: the milestone history bound. Applied identically at every prompt
|
|
383
|
+
* level and in the state file, so the milestone set is a function of
|
|
384
|
+
* committed facts rather than of which level happened to compile. (The
|
|
385
|
+
* v3.0.1 sampler chose which milestones to keep from an UNBOUNDED list and
|
|
386
|
+
* did nothing at L2 — so the state file’s phase history depended on the
|
|
387
|
+
* prompt level and had no bound of its own.) */
|
|
388
|
+
const MILESTONE_HISTORY_CAP = 50;
|
|
389
|
+
export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0) {
|
|
479
390
|
const policy = getPolicy().summary;
|
|
480
391
|
const allEntries = loopEntries(loopId, context);
|
|
481
392
|
// ── Phase 1: Recent Window (preserves existing behavior) ──
|
|
@@ -486,7 +397,6 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
486
397
|
})
|
|
487
398
|
.slice(-policy.window);
|
|
488
399
|
const outcomes = [];
|
|
489
|
-
const issues = [];
|
|
490
400
|
for (const entry of windowEntries) {
|
|
491
401
|
const view = decodeRound(entry);
|
|
492
402
|
const data = entryLineage(entry);
|
|
@@ -506,12 +416,6 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
506
416
|
: success === false ? "failed" : "accepted";
|
|
507
417
|
if (summary)
|
|
508
418
|
outcomes.push(`[R${round}] ${outcomeLabel}: ${summary}`);
|
|
509
|
-
const violations = view
|
|
510
|
-
? view.constraintViolations ?? []
|
|
511
|
-
: Array.isArray(entry.constraint_violations)
|
|
512
|
-
? entry.constraint_violations
|
|
513
|
-
: Array.isArray(data.constraint_violations) ? data.constraint_violations : [];
|
|
514
|
-
issues.push(...violations.filter((item) => typeof item === "string"));
|
|
515
419
|
}
|
|
516
420
|
// ── Phase 2: Milestone Accumulation ──
|
|
517
421
|
// Scan all completed entries in round order. For each entry, check three
|
|
@@ -568,15 +472,17 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
568
472
|
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, endRound, `Rounds ${startRound}–${endRound}`, "auto"));
|
|
569
473
|
}
|
|
570
474
|
}
|
|
571
|
-
// v3.
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
|
|
475
|
+
// v3.8.1: one bound, applied identically at every prompt level. The v3.0.1
|
|
476
|
+
// sampler kept head anchors + a newest tail + an even middle sample, and
|
|
477
|
+
// deliberately did nothing at L2 — so the milestone set written to the state
|
|
478
|
+
// file depended on which level happened to compile, not on committed facts.
|
|
479
|
+
// It also did not bound growth at all (it only chose WHICH milestones to
|
|
480
|
+
// keep from an unbounded list), which is why the cap has to live here.
|
|
481
|
+
const cappedMilestones = milestones.slice(-MILESTONE_HISTORY_CAP);
|
|
575
482
|
if (windowEntries.length === 0 && cappedMilestones.length === 0)
|
|
576
483
|
return null;
|
|
577
484
|
return makeRollingSummary({
|
|
578
485
|
key_outcomes: unique(outcomes),
|
|
579
|
-
recurring_issues: unique(issues),
|
|
580
486
|
rounds_sampled: windowEntries.length,
|
|
581
487
|
generated_at_round: currentRound,
|
|
582
488
|
failed_patterns: [],
|
|
@@ -586,165 +492,17 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
586
492
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
587
493
|
// Sub-Goal Structured Tracking (v2.2)
|
|
588
494
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
589
|
-
/** Derive a stable sub-goal ID from its description hash.
|
|
590
|
-
* Exported as the single source of truth (verification-gate imports it). */
|
|
591
|
-
export function deriveSubGoalId(description) {
|
|
592
|
-
return "sg-" + deriveItemId(description);
|
|
593
|
-
}
|
|
594
495
|
/** v2.11: Derive a stable constraint ID from its text hash (c-XXXXXXXX).
|
|
595
496
|
* Same hash strategy as SubGoal — deterministic across rounds. */
|
|
596
497
|
export function deriveConstraintId(text) {
|
|
597
498
|
return "c-" + deriveItemId(text);
|
|
598
499
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
* Matches the pattern cr-XXXXXXXX where X is a hex digit. Module-local:
|
|
606
|
-
* consumers with the same need (round-contract.ts) keep their own copy to
|
|
607
|
-
* avoid an import cycle. */
|
|
608
|
-
function isCriterionId(ref) {
|
|
609
|
-
return /^cr-[a-f0-9]{8}$/.test(ref);
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* Match a user-provided sub-task reference against existing SubGoals.
|
|
613
|
-
*
|
|
614
|
-
* Tries exact SubGoal ID match first (e.g. "sg-a3f2b1c0"), then falls
|
|
615
|
-
* back to Jaccard token similarity on descriptions. Returns the index
|
|
616
|
-
* of the best-matching SubGoal, or -1 when no match reaches threshold.
|
|
617
|
-
*/
|
|
618
|
-
function matchSubGoal(target, subGoals, threshold) {
|
|
619
|
-
const trimmed = target.trim();
|
|
620
|
-
// Phase 1: Exact ID match
|
|
621
|
-
for (let i = 0; i < subGoals.length; i++) {
|
|
622
|
-
if (subGoals[i].id === trimmed)
|
|
623
|
-
return i;
|
|
624
|
-
}
|
|
625
|
-
// Phase 2: Jaccard fallback on description text
|
|
626
|
-
let bestIdx = -1;
|
|
627
|
-
let bestScore = 0;
|
|
628
|
-
for (let i = 0; i < subGoals.length; i++) {
|
|
629
|
-
const score = jaccardSimilarity(trimmed, subGoals[i].description);
|
|
630
|
-
if (score > bestScore) {
|
|
631
|
-
bestScore = score;
|
|
632
|
-
bestIdx = i;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
return bestScore >= threshold ? bestIdx : -1;
|
|
636
|
-
}
|
|
637
|
-
/**
|
|
638
|
-
* Manage the full sub-goal lifecycle across rounds.
|
|
639
|
-
*
|
|
640
|
-
* Accumulates sub-goals from all prior vault entries, processes the current
|
|
641
|
-
* round's declarations, derives status changes, and returns the complete
|
|
642
|
-
* structured SubGoal list.
|
|
643
|
-
*
|
|
644
|
-
* Status derivation priority (compiler-driven):
|
|
645
|
-
* 1. Agent declared done/canceled/blocked → explicit status change
|
|
646
|
-
* 2. next_action match → auto in_progress
|
|
647
|
-
* 3. success_criteria_met match → auto done
|
|
648
|
-
*/
|
|
649
|
-
function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
|
|
650
|
-
const policy = getPolicy().evolution;
|
|
651
|
-
const allEntries = loopEntries(loopId, vaultContext);
|
|
652
|
-
const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
|
|
653
|
-
const subGoals = [];
|
|
654
|
-
// Phase 1 — Accumulate historical sub-goals from vault entries
|
|
655
|
-
for (const entry of completedEntries) {
|
|
656
|
-
const rnd = entryRound(entry);
|
|
657
|
-
const emerged = entryEmergedSubtasks(entry);
|
|
658
|
-
for (const desc of emerged) {
|
|
659
|
-
const id = deriveSubGoalId(desc);
|
|
660
|
-
// Dedup: skip if an existing sub-goal is too similar
|
|
661
|
-
if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
|
|
662
|
-
continue;
|
|
663
|
-
subGoals.push(makeSubGoal({
|
|
664
|
-
id,
|
|
665
|
-
description: desc.trim(),
|
|
666
|
-
status: "pending",
|
|
667
|
-
declared_at_round: rnd,
|
|
668
|
-
status_changed_at_round: rnd,
|
|
669
|
-
priority: subGoals.length,
|
|
670
|
-
}));
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
// Phase 2 — Process current round's sub-task declarations
|
|
674
|
-
if (lastRoundResult) {
|
|
675
|
-
const rnd = lastRoundResult.round || (currentRound - 1);
|
|
676
|
-
// New sub-goals from this round
|
|
677
|
-
const emerged = lastRoundResult.emerged_subtasks ?? [];
|
|
678
|
-
for (const desc of emerged) {
|
|
679
|
-
if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
|
|
680
|
-
continue;
|
|
681
|
-
subGoals.push(makeSubGoal({
|
|
682
|
-
id: deriveSubGoalId(desc),
|
|
683
|
-
description: desc.trim(),
|
|
684
|
-
status: "pending",
|
|
685
|
-
declared_at_round: rnd,
|
|
686
|
-
status_changed_at_round: rnd,
|
|
687
|
-
priority: subGoals.length,
|
|
688
|
-
}));
|
|
689
|
-
}
|
|
690
|
-
// Completed sub-goals
|
|
691
|
-
const completed = lastRoundResult.completed_subtasks ?? [];
|
|
692
|
-
for (const desc of completed) {
|
|
693
|
-
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
694
|
-
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
695
|
-
subGoals[idx].status = "done";
|
|
696
|
-
subGoals[idx].status_changed_at_round = rnd;
|
|
697
|
-
subGoals[idx].completed_at_round = rnd;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
// Blocked sub-goals
|
|
701
|
-
const blocked = lastRoundResult.blocked_subtasks ?? [];
|
|
702
|
-
for (const desc of blocked) {
|
|
703
|
-
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
704
|
-
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
705
|
-
subGoals[idx].status = "blocked";
|
|
706
|
-
subGoals[idx].status_changed_at_round = rnd;
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
// Canceled sub-goals
|
|
710
|
-
const canceled = lastRoundResult.canceled_subtasks ?? [];
|
|
711
|
-
for (const desc of canceled) {
|
|
712
|
-
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
713
|
-
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
714
|
-
subGoals[idx].status = "canceled";
|
|
715
|
-
subGoals[idx].status_changed_at_round = rnd;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
// v3.7: no auto status inference. The v2.3 fuzzy heuristics
|
|
719
|
-
// (next_action Jaccard → in_progress, criteria Jaccard → done) were
|
|
720
|
-
// removed — they fabricated dashboard motion the machine never
|
|
721
|
-
// verified. Statuses change only via the agent declarations above.
|
|
722
|
-
}
|
|
723
|
-
// Sort for display: in_progress first, then pending by age desc, then blocked, then done, then canceled
|
|
724
|
-
subGoals.sort((a, b) => {
|
|
725
|
-
const rank = (s) => {
|
|
726
|
-
switch (s.status) {
|
|
727
|
-
case "in_progress": return 0;
|
|
728
|
-
case "pending": return 1;
|
|
729
|
-
case "blocked": return 2;
|
|
730
|
-
case "done": return 3;
|
|
731
|
-
case "canceled": return 4;
|
|
732
|
-
}
|
|
733
|
-
};
|
|
734
|
-
const ra = rank(a), rb = rank(b);
|
|
735
|
-
if (ra !== rb)
|
|
736
|
-
return ra - rb;
|
|
737
|
-
// Within same status: pending by oldest first, done by most recent first
|
|
738
|
-
// (v2.14: the previous expression simplified to b.declared - a.declared,
|
|
739
|
-
// which sorted NEWEST first despite the comment)
|
|
740
|
-
if (a.status === "pending")
|
|
741
|
-
return a.declared_at_round - b.declared_at_round;
|
|
742
|
-
if (a.status === "done")
|
|
743
|
-
return (b.completed_at_round ?? 0) - (a.completed_at_round ?? 0);
|
|
744
|
-
return a.declared_at_round - b.declared_at_round;
|
|
745
|
-
});
|
|
746
|
-
return subGoals;
|
|
747
|
-
}
|
|
500
|
+
// v3.8.1: `deriveCriterionId` / `isCriterionId` moved to token-utils.ts, where
|
|
501
|
+
// the other stable-id helpers live — the criterion fact derivation in
|
|
502
|
+
// round-facts.ts needs them, and defining them here would make
|
|
503
|
+
// round-facts ⇄ loop-compiler an import cycle. Re-exported so this module's
|
|
504
|
+
// public surface ("./compiler") is unchanged.
|
|
505
|
+
export { deriveCriterionId, isCriterionId } from "./token-utils.js";
|
|
748
506
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
749
507
|
// Time-Aware Constraint Management (v2.3)
|
|
750
508
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -759,19 +517,18 @@ function constraintSource(text, objective, constraintsFromPlan) {
|
|
|
759
517
|
return "criteria";
|
|
760
518
|
return "discovered";
|
|
761
519
|
}
|
|
762
|
-
/**
|
|
763
|
-
* v2.11: ID-first
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
|
|
767
|
-
*
|
|
520
|
+
/** True when a reported violation text matches a constraint text.
|
|
521
|
+
* v2.11: ID-first — an exact `c-XXXXXXXX` match against the derived id.
|
|
522
|
+
* v3.8.1: the Jaccard fallback is gone; the remaining arm requires the
|
|
523
|
+
* reported text to be EXACTLY equal to the constraint after normalization,
|
|
524
|
+
* so a reported violation can no longer attach itself to a constraint it
|
|
525
|
+
* merely resembles. */
|
|
768
526
|
function matchesConstraintText(text, violation) {
|
|
769
527
|
const targetId = deriveConstraintId(text);
|
|
770
528
|
if (violation === targetId || deriveConstraintId(violation) === targetId) {
|
|
771
529
|
return true;
|
|
772
530
|
}
|
|
773
|
-
return
|
|
774
|
-
getPolicy().evolution.constraint_match_threshold;
|
|
531
|
+
return normalizeText(text) === normalizeText(violation);
|
|
775
532
|
}
|
|
776
533
|
function findLastViolatedRound(text, vaultEntries) {
|
|
777
534
|
let latest = 0;
|
|
@@ -823,7 +580,7 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
823
580
|
// (its own Success Criteria section and the verification gate's
|
|
824
581
|
// criteriaMatch are ID-first on cr-). Rendering it under c-XXXXXXXX in
|
|
825
582
|
// the active list meant the same text carried two IDs in one prompt,
|
|
826
|
-
// and an agent echoing the active-list ID into
|
|
583
|
+
// and an agent echoing the active-list ID into its criterion claims
|
|
827
584
|
// could never match.
|
|
828
585
|
metadata.push(makeConstraintMeta({
|
|
829
586
|
id: source === "criteria" ? deriveCriterionId(text) : deriveConstraintId(text),
|
|
@@ -836,38 +593,6 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
836
593
|
}
|
|
837
594
|
return { active, metadata };
|
|
838
595
|
}
|
|
839
|
-
export function alignTask(proposedTask, request, context) {
|
|
840
|
-
const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
|
|
841
|
-
if (!objective)
|
|
842
|
-
return makeTaskAlignment();
|
|
843
|
-
// v2.14: threshold is policy-driven (evolution.progress_mismatch_threshold)
|
|
844
|
-
const threshold = getPolicy().evolution.progress_mismatch_threshold;
|
|
845
|
-
const score = jaccardSimilarity(proposedTask, [objective.objective, ...objective.success_criteria, ...objective.hard_constraints].join(" "));
|
|
846
|
-
return makeTaskAlignment({
|
|
847
|
-
is_aligned: score >= threshold,
|
|
848
|
-
alignment_score: Number(score.toFixed(2)),
|
|
849
|
-
warning: score < threshold ? "Current task may be drifting from the loop objective." : "",
|
|
850
|
-
escalation: score < threshold ? "warn" : "none",
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
export function checkLoopHealth(loopId, request, context) {
|
|
854
|
-
const previous = getPreviousRound(loopId, request.round - 1, context);
|
|
855
|
-
const alignment = alignTask(request.task, request, context);
|
|
856
|
-
const violations = request.last_round_result?.constraint_violations.length ?? 0;
|
|
857
|
-
const integrity = Math.max(0, 1 - violations * 0.2);
|
|
858
|
-
const continuity = previous ? jaccardSimilarity(previous.task, request.task) : 1;
|
|
859
|
-
// v2.14: thresholds are policy-driven
|
|
860
|
-
const drift = alignment.alignment_score < getPolicy().evolution.progress_mismatch_threshold ||
|
|
861
|
-
continuity < getPolicy().evolution.task_continuity_threshold;
|
|
862
|
-
return makeLoopHealth({
|
|
863
|
-
goal_alignment: alignment.alignment_score,
|
|
864
|
-
constraint_integrity: integrity,
|
|
865
|
-
drift_detected: drift,
|
|
866
|
-
strategy_stability: true,
|
|
867
|
-
task_continuity: Number(continuity.toFixed(2)),
|
|
868
|
-
escalation_recommended: drift || integrity < 0.6 ? "l2" : "none",
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
596
|
function levelDecision(request, context) {
|
|
872
597
|
const previous = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
873
598
|
const last = request.last_round_result;
|
|
@@ -878,11 +603,6 @@ function levelDecision(request, context) {
|
|
|
878
603
|
last?.retracted_constraints?.length ||
|
|
879
604
|
last?.revised_success_criteria?.length ||
|
|
880
605
|
last?.wrong_assumptions?.length);
|
|
881
|
-
const lastFullRound = loopEntries(request.loop_id, context)
|
|
882
|
-
.filter((entry) => entryLineage(entry).recompile_level === "l2")
|
|
883
|
-
.map(entryRound)
|
|
884
|
-
.filter((round) => round < request.round)
|
|
885
|
-
.at(-1) ?? 1;
|
|
886
606
|
// v2.14: a backtrack committed for this round means the loop is
|
|
887
607
|
// re-walking restored state — the recovery boundary forces L2
|
|
888
608
|
// rehydration (the decidePromptLevel recovery_boundary branch existed
|
|
@@ -903,33 +623,22 @@ function levelDecision(request, context) {
|
|
|
903
623
|
verificationContradicted: (request.verification_flags ?? [])
|
|
904
624
|
.some((flag) => flag.severity === "error"),
|
|
905
625
|
consecutiveRejections: request.consecutive_rejections,
|
|
906
|
-
fullRefreshInterval: getPolicy().prompt.full_refresh_interval,
|
|
907
|
-
lastFullRound,
|
|
908
626
|
});
|
|
909
627
|
}
|
|
910
628
|
export function decideLevel(request, context) {
|
|
911
629
|
return levelDecision(request, context).level;
|
|
912
630
|
}
|
|
913
|
-
export function buildSelfEvalBlock(round,
|
|
631
|
+
export function buildSelfEvalBlock(round,
|
|
914
632
|
/** v2.12: L0 is the minimal retry template — the v2.12 declarative fields
|
|
915
633
|
* (outcome/blocker/retroactiveClaims) are L1/L2 additions so the retry
|
|
916
634
|
* prompt stays within its tight budget. */
|
|
917
|
-
level,
|
|
635
|
+
level,
|
|
918
636
|
/** v3.3/v3.4: Whether this round's Current Task IS the ACTIVE Round
|
|
919
637
|
* Contract (derived from committed rounds — compileLoop passes
|
|
920
638
|
* `activeContract != null`). Only then does the template ask the agent
|
|
921
639
|
* to restate/propose it — a generic empty contract template would invite
|
|
922
640
|
* placeholder submissions that trigger round_underspecified noise. */
|
|
923
|
-
hasContract = false
|
|
924
|
-
/** v3.5: L2-only prose suggesting a Round Contract declaration when the
|
|
925
|
-
* Current Task is NOT one (contract_nudge_on_l2 policy, computed at the
|
|
926
|
-
* compileLoop call site). Mutually exclusive with hasContract. The prose
|
|
927
|
-
* must never contain the JSON key name `round_contract` — contract-less
|
|
928
|
-
* L2 tests assert its lowercase absence. */
|
|
929
|
-
proposalNudge = false) {
|
|
930
|
-
const hasIntentDrift = prevDriftFlags?.some(f => f.check === "intent_drift") ?? false;
|
|
931
|
-
const hasSubGoalDrift = prevDriftFlags?.some(f => f.check === "subgoal_drift") ?? false;
|
|
932
|
-
const needsClarification = hasIntentDrift || hasSubGoalDrift;
|
|
641
|
+
hasContract = false) {
|
|
933
642
|
const declareOutcome = level !== "l0";
|
|
934
643
|
const restateContract = hasContract && declareOutcome;
|
|
935
644
|
const evalObj = {
|
|
@@ -939,24 +648,31 @@ proposalNudge = false) {
|
|
|
939
648
|
should_continue: true,
|
|
940
649
|
discovered_constraints: ["<new constraint>"],
|
|
941
650
|
emerged_subtasks: [],
|
|
942
|
-
|
|
651
|
+
execution_report: {
|
|
943
652
|
files_changed: [],
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
653
|
+
tests_reported: { passed: 0, failed: 0, skipped: 0 },
|
|
654
|
+
// The contract claim is functional (it is what advances an item), so it
|
|
655
|
+
// stays at every level. The advisory criterion claim is an L1/L2
|
|
656
|
+
// addition — the L0 retry template is a lean delta kept under budget.
|
|
657
|
+
contract_item_claims: [
|
|
658
|
+
{ item_id: "<rci-XXXXXXXX from the Current Task>", outcome: "met" },
|
|
659
|
+
],
|
|
947
660
|
progress_estimate: 0,
|
|
948
661
|
},
|
|
949
662
|
wrong_assumptions: [],
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
canceled_subtasks: ["<sg-XXXXXXXX or text>"],
|
|
663
|
+
subgoal_updates: [
|
|
664
|
+
{ id: "<sg-XXXXXXXX from the Sub-Goal Dashboard>", status: "done" },
|
|
665
|
+
],
|
|
954
666
|
};
|
|
955
|
-
// v2.12: Declarative
|
|
667
|
+
// v2.12: Declarative outcome (L1/L2 only — keeps L0 retry lean)
|
|
956
668
|
if (declareOutcome) {
|
|
957
669
|
evalObj.outcome = "<success|partial|failed|blocked>";
|
|
958
670
|
evalObj.blocker = "<required only when outcome=blocked>";
|
|
959
671
|
evalObj.retroactiveClaims = [];
|
|
672
|
+
// v3.8: the advisory criterion claim rides with the other L1/L2 additions.
|
|
673
|
+
evalObj.execution_report.criterion_claims = [
|
|
674
|
+
{ criterion_id: "<cr-XXXXXXXX or text>", outcome: "met" },
|
|
675
|
+
];
|
|
960
676
|
}
|
|
961
677
|
// v3.3/v3.4: Round Contract proposal template (L1/L2, active-contract
|
|
962
678
|
// rounds only — L0 retry stays lean, and rounds without an active
|
|
@@ -972,26 +688,12 @@ proposalNudge = false) {
|
|
|
972
688
|
scope: [],
|
|
973
689
|
};
|
|
974
690
|
}
|
|
975
|
-
// v2.8: Inject drift clarification field when previous round had drift flags
|
|
976
|
-
if (needsClarification) {
|
|
977
|
-
const flagTypes = [
|
|
978
|
-
hasIntentDrift ? "intent_drift" : "",
|
|
979
|
-
hasSubGoalDrift ? "subgoal_drift" : "",
|
|
980
|
-
].filter(Boolean).join(" / ");
|
|
981
|
-
evalObj.drift_clarification =
|
|
982
|
-
`<explain why your Round ${round} actions diverged from your stated plan ` +
|
|
983
|
-
`(${flagTypes} detected in previous round). ` +
|
|
984
|
-
`Leave empty if no divergence occurred>`;
|
|
985
|
-
}
|
|
986
691
|
const lines = [
|
|
987
692
|
"### LoopForge Evaluation (Required)",
|
|
988
693
|
"",
|
|
989
694
|
`After completing Round ${round}, replace the placeholder values below`,
|
|
990
695
|
"with your actual results and pass this object as `loopforge_next.evaluation`.",
|
|
991
696
|
];
|
|
992
|
-
if (needsClarification) {
|
|
993
|
-
lines.push("", "> ⚠️ **Drift Detected:** The verification gate detected that your previous", "> round's declared intent (next_action) did not match your actual output.", "> Please explain the pivot in the `drift_clarification` field below.", "> An honest explanation helps the enforcement gate distinguish intentional", "> course corrections from unacknowledged drift.");
|
|
994
|
-
}
|
|
995
697
|
lines.push("```json");
|
|
996
698
|
lines.push(JSON.stringify(evalObj, null, 2));
|
|
997
699
|
lines.push("```");
|
|
@@ -999,65 +701,119 @@ proposalNudge = false) {
|
|
|
999
701
|
lines.push("IMPORTANT: Replace every <placeholder> with your actual data.");
|
|
1000
702
|
lines.push("Set success=true only when the full goal and ALL hard constraints are verified.");
|
|
1001
703
|
lines.push("Use IDs for exact matching: c-XXXXXXXX, cr-XXXXXXXX, sg-XXXXXXXX.");
|
|
1002
|
-
lines.push("
|
|
1003
|
-
|
|
1004
|
-
// contract-less rounds keep their template-lean assertions).
|
|
1005
|
-
if (proposalNudge) {
|
|
1006
|
-
lines.push("", "If the remaining work will span several rounds, consider declaring a", "Round Contract for the next round — list its done_when items, the", "verification_plan commands that will back them, and the scope. This", "keeps each round's boundary machine-checkable.");
|
|
1007
|
-
}
|
|
704
|
+
lines.push("Sub-goal changes go through `subgoal_updates` (active sub-goal IDs only;");
|
|
705
|
+
lines.push("done/canceled are terminal — reopen via `emerged_subtasks`; bad IDs reject).");
|
|
1008
706
|
if (restateContract) {
|
|
1009
|
-
lines.push("Round Contract: restate the Current Task's contract UNCHANGED in", "`round_contract` (work_item/
|
|
707
|
+
lines.push("Round Contract: restate the Current Task's contract UNCHANGED in", "`round_contract` (work_item/items/scope) and run each item's Verify", "commands; claim the items you completed in", "`execution_report.contract_item_claims` using the `rci-XXXXXXXX` ids", "the Current Task renders. An item only becomes verified when its bound", "command is observed passing — a claim alone leaves it insufficient.", "When every item is verified, or the round is blocked (outcome=blocked", "+ blocker), declare the NEXT round's contract instead (or omit", "`round_contract` when the whole task is done — the Current Task", "reverts to the original task).");
|
|
1010
708
|
}
|
|
1011
709
|
return lines.join("\n");
|
|
1012
710
|
}
|
|
1013
711
|
// v2.5: checkpoint() removed — agent_declared milestones from
|
|
1014
712
|
// compression_checkpoint supersede the separate CheckpointSummary.
|
|
1015
713
|
// See buildRollingSummary() signal 1.
|
|
714
|
+
/** v3.7.1: Derived Recovery Brief for the state-file Recent tier. Present
|
|
715
|
+
* ONLY while a committed backtrack decision is the current round's record
|
|
716
|
+
* (the recovery window); the redo commit replaces that record, so the
|
|
717
|
+
* brief exits the projection by construction. Sources are the committed
|
|
718
|
+
* rollback decision's own fields — no new persistence, no rejected
|
|
719
|
+
* payloads. */
|
|
720
|
+
function committedBacktrackBrief(loopId, round, context) {
|
|
721
|
+
const entry = loopEntries(loopId, context).find((e) => entryLineage(e).committed_action === "backtrack" && entryRound(e) === round);
|
|
722
|
+
if (!entry)
|
|
723
|
+
return undefined;
|
|
724
|
+
// v3.8.1: one decoder for the committed rollback record and one renderer for
|
|
725
|
+
// the brief. The facts ride on the merged lineage entry (engine.ts stamps
|
|
726
|
+
// them from the committed decision) and the same `decodeBacktrackRecord`
|
|
727
|
+
// reads the durable :feedback envelope, so both carriers decode to one shape
|
|
728
|
+
// — and this brief and the rollback prompt no longer render the same
|
|
729
|
+
// rollback with two hand-written line builders.
|
|
730
|
+
const record = decodeBacktrackRecord(entryLineage(entry));
|
|
731
|
+
if (!record)
|
|
732
|
+
return undefined;
|
|
733
|
+
return recoveryBriefLines({
|
|
734
|
+
target: record.target,
|
|
735
|
+
triggerRule: record.triggerRule,
|
|
736
|
+
failedRounds: record.failedRounds,
|
|
737
|
+
approaches: record.approaches,
|
|
738
|
+
wrongAssumptions: record.wrongAssumptions,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
1016
741
|
export function compileLoop(request, context) {
|
|
1017
742
|
const decision = levelDecision(request, context);
|
|
1018
743
|
const previous = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
1019
744
|
const objective = evolveObjective(request, context);
|
|
1020
745
|
const constraints = evolveConstraints(request, objective, previous, context);
|
|
1021
746
|
const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round, request.last_round_result?.constraint_violations ?? []);
|
|
1022
|
-
const rolling = buildRollingSummary(request.loop_id, request.round, context, 0
|
|
1023
|
-
|
|
747
|
+
const rolling = buildRollingSummary(request.loop_id, request.round, context, 0);
|
|
748
|
+
// v3.8: ONE committed-round derivation feeds every projection below
|
|
749
|
+
// (sub-goals, the active contract, its item statuses, criteria).
|
|
750
|
+
const windowRounds = derivationRounds(loopEntries(request.loop_id, context), request.round);
|
|
751
|
+
// v3.8: sub-goal lifecycle lives in subgoal-state.ts and consumes the
|
|
752
|
+
// shared committed-round view — one history interpretation.
|
|
753
|
+
const subGoals = deriveSubGoals({
|
|
754
|
+
loopId: request.loop_id,
|
|
755
|
+
currentRound: request.round,
|
|
756
|
+
rounds: windowRounds,
|
|
757
|
+
currentReport: request.last_round_result
|
|
758
|
+
? {
|
|
759
|
+
round: request.last_round_result.round || (request.round - 1),
|
|
760
|
+
emergedSubtasks: request.last_round_result.emerged_subtasks ?? [],
|
|
761
|
+
subgoalUpdates: request.last_round_result.subgoal_updates ?? [],
|
|
762
|
+
}
|
|
763
|
+
: null,
|
|
764
|
+
});
|
|
765
|
+
// v3.4/v3.8/v3.8.1: the ACTIVE Round Contract, its per-item statuses and the
|
|
766
|
+
// verified sub-goal facts for this compile — ONE `deriveRoundFacts` call,
|
|
767
|
+
// the same bundle the projection consumes. Derived from committed rounds, so
|
|
768
|
+
// rejection retries, resume, unpause and backtrack compiles (which carry no
|
|
769
|
+
// last_round_result) keep showing the same contract, and a verified or
|
|
770
|
+
// blocked contract stops being shown. A verified item stays proven after its
|
|
771
|
+
// contract closes because the bundle reads the whole history, not the active
|
|
772
|
+
// contract. The compile path has no in-flight observations of its own.
|
|
773
|
+
const roundFacts = deriveRoundFacts({
|
|
774
|
+
rounds: windowRounds,
|
|
775
|
+
currentRound: request.round,
|
|
776
|
+
inFlight: NO_IN_FLIGHT_ROUND,
|
|
777
|
+
subGoals,
|
|
778
|
+
commands: getPolicy().evidence.commands ?? [],
|
|
779
|
+
});
|
|
780
|
+
// `roundFacts.verifiedSubGoals` is not read here — it reaches the state file
|
|
781
|
+
// and the projection through the bundle itself.
|
|
782
|
+
const { activeContract } = roundFacts;
|
|
1024
783
|
// v3.2: Goal → criteria → evidence vertical view (derived, zero persistence).
|
|
1025
|
-
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context, objective, request.round,
|
|
1026
|
-
// v3.
|
|
1027
|
-
|
|
1028
|
-
const
|
|
1029
|
-
|
|
784
|
+
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context, objective, request.round, request.last_round_result, roundFacts.criterionFacts);
|
|
785
|
+
// v3.8.1: the ONE recurring-fact derivation, over the shared window. The
|
|
786
|
+
// prompt renders its recent tail; the state file renders the recurring set.
|
|
787
|
+
const recurringFlags = deriveRecurringFlags(windowRounds);
|
|
788
|
+
// v3.8.1: a same-round exact repeat is the only sub-goal duplication the
|
|
789
|
+
// runtime can state as a fact — those entries really were dropped, so the
|
|
790
|
+
// agent is told. The cross-round near-duplicate similarity diagnostic is
|
|
791
|
+
// gone with the rest of the fuzzy matching, and so are the task-alignment
|
|
792
|
+
// and loop-health warnings: both were Jaccard scores over task/objective
|
|
793
|
+
// text, never machine facts.
|
|
794
|
+
const repeatedDeclarations = duplicateEmergedDeclarations(request.last_round_result?.emerged_subtasks ?? []);
|
|
1030
795
|
const warnings = unique([
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
796
|
+
repeatedDeclarations.length > 0
|
|
797
|
+
? `duplicate_declaration: ${repeatedDeclarations.length} sub-goal ` +
|
|
798
|
+
`declaration(s) repeated an earlier entry in the same round and were ` +
|
|
799
|
+
`not created: ${repeatedDeclarations.join("; ")}`
|
|
1034
800
|
: "",
|
|
1035
801
|
]);
|
|
1036
|
-
//
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
802
|
+
// v3.8.1: the agent trust score and trend are deleted. The score was
|
|
803
|
+
// `1 - errors*0.15 - warns*0.03` — arbitrary weights with no machine
|
|
804
|
+
// meaning, re-encoding flag counts that are already visible. The trend was
|
|
805
|
+
// worse: `1 - violations*0.1` per round, computed from
|
|
806
|
+
// `constraint_violations`, i.e. a DIFFERENT source from the score it was
|
|
807
|
+
// displayed next to. "Trend" was therefore not the score's history at all.
|
|
808
|
+
// Neither fed a decision.
|
|
1043
809
|
const completedEntries = loopEntries(request.loop_id, context)
|
|
1044
810
|
.filter((e) => entryRound(e) >= 1 && entryRound(e) < request.round);
|
|
1045
|
-
const committedRounds =
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const score = Math.max(0, 1.0 - viols.length * 0.1);
|
|
1051
|
-
trend.push(Number(score.toFixed(2)));
|
|
1052
|
-
}
|
|
1053
|
-
// v3.3: Round stats + machine git-motion status (display-only, derived).
|
|
1054
|
-
const roundStats = deriveRoundStats(committedRounds);
|
|
811
|
+
const committedRounds = derivationRounds(completedEntries, request.round);
|
|
812
|
+
// v3.3/v3.8.1: machine git-motion (display-only, derived). The per-round
|
|
813
|
+
// "round stats" table is deleted: its only renderer was the L2 prompt
|
|
814
|
+
// section removed above, so it was computed, put into the canonical state
|
|
815
|
+
// (and therefore into stateHash) and read by nothing.
|
|
1055
816
|
const machineStatus = deriveMachineStatus(committedRounds, request.round);
|
|
1056
|
-
// v3.4: ACTIVE Round Contract for this compile — derived from committed
|
|
1057
|
-
// rounds, so rejection retries / resume / unpause / backtrack compiles
|
|
1058
|
-
// (no last_round_result) keep showing the contract as Current Task, and a
|
|
1059
|
-
// satisfied or blocked contract stops being shown.
|
|
1060
|
-
const activeContract = deriveActiveContract(request.loop_id, context, request.round);
|
|
1061
817
|
const response = makeLoopCompileResponse({
|
|
1062
818
|
status: AgentStatus.OK,
|
|
1063
819
|
recompile_level: decision.level,
|
|
@@ -1071,39 +827,49 @@ export function compileLoop(request, context) {
|
|
|
1071
827
|
goal_id: deriveGoalId(request.loop_id, request.task, request.goal_id),
|
|
1072
828
|
goal_text_hash: computeGoalTextHash(request.task),
|
|
1073
829
|
loop_objective: objective,
|
|
1074
|
-
loop_health: health,
|
|
1075
|
-
task_alignment: alignment,
|
|
1076
830
|
rolling_summary: rolling,
|
|
1077
831
|
sub_goals: subGoals,
|
|
1078
832
|
criterion_statuses: criterionStatuses,
|
|
1079
|
-
|
|
1080
|
-
agent_trust_score: request.round > 1 ? Number(trustScore.toFixed(2)) : undefined,
|
|
1081
|
-
agent_trust_trend: request.round > 1 ? trend : [],
|
|
1082
|
-
suggested_next_task: request.last_round_result?.next_action?.trim() ||
|
|
1083
|
-
request.last_round_result?.emerged_subtasks?.[0] || "",
|
|
833
|
+
recurring_flags: recurringFlags,
|
|
1084
834
|
plan_source: request.plan_source,
|
|
1085
835
|
warnings,
|
|
1086
836
|
});
|
|
1087
837
|
const policy = getPolicy();
|
|
1088
|
-
|
|
838
|
+
// L7 (v3.7.x): the state-file path is empty when the file is disabled —
|
|
839
|
+
// prompts must never point at (or collapse content behind) a file that
|
|
840
|
+
// writeStateFile will not create. An empty path propagates into the
|
|
841
|
+
// assembler's pointer lines and the L1 collapse gate below.
|
|
842
|
+
const statePath = policy.state_file.enabled
|
|
843
|
+
? `${policy.state_file.directory}/${request.loop_id}-state.md`
|
|
844
|
+
: "";
|
|
1089
845
|
const state = createCanonicalLoopState(request, response, statePath, {
|
|
1090
|
-
roundStats,
|
|
1091
846
|
machineStatus,
|
|
1092
|
-
// v3.4:
|
|
1093
|
-
|
|
847
|
+
// v3.4/v3.8.1: the contract-fact bundle — the Current Task's source of
|
|
848
|
+
// truth, its verification view and the machine-verified sub-goal facts as
|
|
849
|
+
// ONE derivation. The static capability rides on the state itself.
|
|
850
|
+
roundFacts,
|
|
851
|
+
});
|
|
852
|
+
const markdown = renderCanonicalStateMarkdown(state, {
|
|
853
|
+
// v3.7.1: retry attempts are marked in the derived view (attempt is
|
|
854
|
+
// request context, not a committed fact). The Recovery Brief renders in
|
|
855
|
+
// the Recent tier only while a committed backtrack decision is this
|
|
856
|
+
// round's record; the redo commit removes it by construction.
|
|
857
|
+
attempt: request.attempt,
|
|
858
|
+
recoveryBrief: committedBacktrackBrief(request.loop_id, request.round, context),
|
|
1094
859
|
});
|
|
1095
|
-
|
|
1096
|
-
//
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
+ (subGoals.length * policy.prompt.l2_adaptive_subgoal_factor), policy.prompt.l2_adaptive_max_chars)
|
|
1102
|
-
: policy.prompt.l2_max_chars;
|
|
860
|
+
// v3.8.1: the L2 budget is a FIXED ceiling. The v2.4 adaptive budget
|
|
861
|
+
// scaled it by round count, milestone count and sub-goal count, which made
|
|
862
|
+
// the prompt's allowed content depend on how long the loop had been running
|
|
863
|
+
// rather than on the facts of this round — the same loop could render a
|
|
864
|
+
// section at round 5 and silently drop it at round 50.
|
|
865
|
+
//
|
|
1103
866
|
// v2.8: L2 pointer mode — when enabled, skip the monolithic markdown blob.
|
|
1104
867
|
// Structured L2 sections (milestones, sub-goals, trust, progress) still
|
|
1105
868
|
// render. The state file on disk is the durable source of truth.
|
|
1106
|
-
|
|
869
|
+
// L7: pointer mode REQUIRES the file — with state_file disabled it would
|
|
870
|
+
// silently drop the full state from L2 prompts and point at nothing.
|
|
871
|
+
const l2Pointer = policy.state_file.enabled &&
|
|
872
|
+
policy.prompt.l2_pointer_enabled && decision.level === "l2";
|
|
1107
873
|
const artifact = assemblePromptArtifact({
|
|
1108
874
|
state,
|
|
1109
875
|
level: decision.level,
|
|
@@ -1111,23 +877,17 @@ export function compileLoop(request, context) {
|
|
|
1111
877
|
budgets: {
|
|
1112
878
|
l0: policy.prompt.l0_max_chars,
|
|
1113
879
|
l1: policy.prompt.l1_max_chars,
|
|
1114
|
-
l2:
|
|
880
|
+
l2: policy.prompt.l2_max_chars,
|
|
1115
881
|
},
|
|
1116
882
|
attempt: request.attempt,
|
|
1117
|
-
selfEvaluationBlock: buildSelfEvalBlock(request.round,
|
|
883
|
+
selfEvaluationBlock: buildSelfEvalBlock(request.round, decision.level,
|
|
1118
884
|
// v3.4: restate the contract only when this prompt's Current Task IS
|
|
1119
885
|
// the ACTIVE contract (derived — not the previous submission's field,
|
|
1120
886
|
// which is a proposal and may differ from what this round executes).
|
|
1121
|
-
activeContract != null,
|
|
1122
|
-
// v3.5: L2-only declaration nudge when nothing is active (policy-gated;
|
|
1123
|
-
// mutually exclusive with the restate template above).
|
|
1124
|
-
decision.level === "l2" && activeContract === null &&
|
|
1125
|
-
getPolicy().prompt.contract_nudge_on_l2),
|
|
887
|
+
activeContract != null),
|
|
1126
888
|
fullStateMarkdown: l2Pointer ? undefined : markdown,
|
|
1127
889
|
// v2.9: Model's information needs from the previous round's SelfEvaluation
|
|
1128
890
|
promptRequests: request.last_round_result?.prompt_requests,
|
|
1129
|
-
// v3.2: Previous L1 presentation as the collapse diff baseline.
|
|
1130
|
-
presentedBaseline: readPresentedBaseline(request.loop_id, request.round, context),
|
|
1131
891
|
});
|
|
1132
892
|
response.prompt = artifact.renderedPrompt;
|
|
1133
893
|
response.prompt_artifact = artifact;
|