loopforge 2.11.0 → 3.2.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/README.md +4 -4
- package/dist/audit.d.ts +50 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +158 -0
- package/dist/audit.js.map +1 -0
- package/dist/canonical-state.d.ts +21 -9
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +34 -16
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -6
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +40 -0
- package/dist/cognitive-governance.d.ts.map +1 -0
- package/dist/cognitive-governance.js +126 -0
- package/dist/cognitive-governance.js.map +1 -0
- package/dist/enforcement-gate.d.ts +8 -3
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +419 -92
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +47 -7
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +303 -60
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts +50 -0
- package/dist/evidence-claims.d.ts.map +1 -0
- package/dist/evidence-claims.js +134 -0
- package/dist/evidence-claims.js.map +1 -0
- package/dist/generate-schema.js +6 -1
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +41 -3
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +310 -38
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-projection.d.ts +26 -0
- package/dist/loop-projection.d.ts.map +1 -0
- package/dist/loop-projection.js +142 -0
- package/dist/loop-projection.js.map +1 -0
- package/dist/loop-store.d.ts +91 -41
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +243 -173
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +216 -0
- package/dist/mcp/round-lifecycle.d.ts.map +1 -0
- package/dist/mcp/round-lifecycle.js +926 -0
- package/dist/mcp/round-lifecycle.js.map +1 -0
- package/dist/mcp/server.d.ts +3 -2
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +65 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +47 -139
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +284 -784
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +23 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +270 -79
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +11 -0
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +118 -21
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +26 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +13 -2
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +34 -1
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +320 -42
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +169 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +39 -0
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +5 -5
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +9 -8
- package/dist/replay.js.map +1 -1
- package/dist/round-coordinator.d.ts +6 -3
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +38 -19
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +9 -4
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +15 -7
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +7 -3
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +93 -88
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +31 -1
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +115 -12
- package/dist/self-eval.js.map +1 -1
- package/dist/storage.d.ts +3 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +5 -4
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +20 -7
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +68 -9
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +54 -2
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +433 -25
- package/dist/verification-gate.js.map +1 -1
- package/loop_policy.json +35 -8
- package/package.json +3 -2
- package/skills/perception/SKILL.md +24 -6
- package/dist/backends/interface.d.ts +0 -39
- package/dist/backends/interface.d.ts.map +0 -1
- package/dist/backends/interface.js +0 -6
- package/dist/backends/interface.js.map +0 -1
package/dist/loop-compiler.js
CHANGED
|
@@ -9,7 +9,7 @@ import { AgentStatus, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective
|
|
|
9
9
|
import { createCanonicalLoopState, renderCanonicalStateMarkdown, } from "./canonical-state.js";
|
|
10
10
|
import { assemblePromptArtifact } from "./prompt-assembler.js";
|
|
11
11
|
import { decidePromptLevel, } from "./prompt-policy.js";
|
|
12
|
-
import { jaccardSimilarity, unique, entryRound } from "./token-utils.js";
|
|
12
|
+
import { deriveItemId, jaccardSimilarity, unique, entryRound } from "./token-utils.js";
|
|
13
13
|
function contextEntries(context) {
|
|
14
14
|
if (!context || !Array.isArray(context.results))
|
|
15
15
|
return [];
|
|
@@ -36,6 +36,34 @@ export function deriveGoalId(loopId, task, explicit = "") {
|
|
|
36
36
|
.replace(/^-|-$/g, "")
|
|
37
37
|
.slice(0, 48) || computeGoalTextHash(task)}`;
|
|
38
38
|
}
|
|
39
|
+
/** v3.2: Read the previous round's persisted L1 presentation snapshot — the
|
|
40
|
+
* diff baseline for L1 collapse. Returns null when the previous round's
|
|
41
|
+
* lineage entry exists but lacks all three presented_* fields (old-format
|
|
42
|
+
* entries, L0/L2 compiles) — callers then render in full, which keeps
|
|
43
|
+
* existing fixtures and pre-v3.2 loops on the full-render path. */
|
|
44
|
+
export function readPresentedBaseline(loopId, round, context) {
|
|
45
|
+
const entry = [...loopEntries(loopId, context)].reverse()
|
|
46
|
+
.find((candidate) => entryRound(candidate) === round - 1);
|
|
47
|
+
if (!entry)
|
|
48
|
+
return null;
|
|
49
|
+
const data = lineage(entry);
|
|
50
|
+
const constraintIds = data.presented_constraint_ids;
|
|
51
|
+
const subGoals = data.presented_subgoals;
|
|
52
|
+
const milestoneRanges = data.presented_milestone_ranges;
|
|
53
|
+
if (!Array.isArray(constraintIds) || !constraintIds.every((v) => typeof v === "string") ||
|
|
54
|
+
!Array.isArray(subGoals) || !subGoals.every((v) => Array.isArray(v) && v.length === 2 &&
|
|
55
|
+
typeof v[0] === "string" && typeof v[1] === "string") ||
|
|
56
|
+
!Array.isArray(milestoneRanges) || !milestoneRanges.every((v) => Array.isArray(v) && v.length === 2 &&
|
|
57
|
+
typeof v[0] === "number" && typeof v[1] === "number")) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
round: round - 1,
|
|
62
|
+
constraintIds: constraintIds,
|
|
63
|
+
subGoals: subGoals,
|
|
64
|
+
milestoneRanges: milestoneRanges,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
39
67
|
export function getPreviousRound(loopId, round, context) {
|
|
40
68
|
const entry = [...loopEntries(loopId, context)].reverse()
|
|
41
69
|
.find((candidate) => entryRound(candidate) === round);
|
|
@@ -115,19 +143,51 @@ function evolveObjective(request, context) {
|
|
|
115
143
|
loop_id: request.loop_id,
|
|
116
144
|
});
|
|
117
145
|
}
|
|
118
|
-
function evolveConstraints(request, objective, previous) {
|
|
146
|
+
function evolveConstraints(request, objective, previous, context) {
|
|
119
147
|
const retired = unique(request.last_round_result?.retracted_constraints ?? []);
|
|
120
148
|
const retiredSet = new Set(retired);
|
|
121
149
|
const discovered = unique(request.last_round_result?.discovered_constraints ?? [])
|
|
122
150
|
.slice(0, getPolicy().evolution.max_discovered_constraints_per_round);
|
|
151
|
+
// v2.14: agent-declared retractions of plan/hard/criteria constraints
|
|
152
|
+
// persist for constraints.retire_window rounds (they are re-merged from
|
|
153
|
+
// the request/objective every round, so without this the documented
|
|
154
|
+
// "Removed from the active constraint set" never held for them). The
|
|
155
|
+
// retraction rounds are derived from vault entries — no new persistence
|
|
156
|
+
// format. After the window the constraint returns (hard/plan/criteria
|
|
157
|
+
// constraints never auto-decay — only agent action removes them, and the
|
|
158
|
+
// action's memory expires).
|
|
159
|
+
const window = getPolicy().constraints.retire_window;
|
|
160
|
+
const lastRetractedRound = new Map();
|
|
161
|
+
if (window > 0) {
|
|
162
|
+
for (const entry of loopEntries(request.loop_id, context)) {
|
|
163
|
+
const retractedList = lineage(entry).retracted_constraints;
|
|
164
|
+
if (!Array.isArray(retractedList))
|
|
165
|
+
continue;
|
|
166
|
+
const rnd = entryRound(entry);
|
|
167
|
+
for (const text of retractedList) {
|
|
168
|
+
if (typeof text !== "string" || !text)
|
|
169
|
+
continue;
|
|
170
|
+
const prev = lastRetractedRound.get(text);
|
|
171
|
+
if (prev === undefined || rnd > prev)
|
|
172
|
+
lastRetractedRound.set(text, rnd);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
123
176
|
const active = unique([
|
|
124
177
|
...(previous?.constraints_active ?? []),
|
|
125
178
|
...request.constraints_from_plan,
|
|
126
179
|
...objective.hard_constraints,
|
|
127
180
|
...objective.success_criteria,
|
|
128
181
|
...discovered,
|
|
129
|
-
]).filter((item) =>
|
|
130
|
-
|
|
182
|
+
]).filter((item) => {
|
|
183
|
+
if (retiredSet.has(item))
|
|
184
|
+
return false;
|
|
185
|
+
const retractedRound = lastRetractedRound.get(item);
|
|
186
|
+
if (retractedRound !== undefined && request.round - retractedRound < window) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
return true;
|
|
190
|
+
}).slice(0, getPolicy().evolution.max_active_constraints);
|
|
131
191
|
return { active, retired };
|
|
132
192
|
}
|
|
133
193
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -155,6 +215,112 @@ function entryCriteriaMet(entry) {
|
|
|
155
215
|
const arr = ev.success_criteria_met;
|
|
156
216
|
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
157
217
|
}
|
|
218
|
+
/** Read success_criteria_remaining from an entry's execution_evidence. */
|
|
219
|
+
function entryCriteriaRemaining(entry) {
|
|
220
|
+
const ev = entryExecutionEvidence(entry);
|
|
221
|
+
if (!ev)
|
|
222
|
+
return [];
|
|
223
|
+
const arr = ev.success_criteria_remaining;
|
|
224
|
+
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
225
|
+
}
|
|
226
|
+
/** v3.2: Deterministic lessons learned — constraints violated repeatedly or
|
|
227
|
+
* verification checks failing repeatedly across rounds (full history, unlike
|
|
228
|
+
* the enforcement gate's R2 3-round window). Presentation only: the output
|
|
229
|
+
* never feeds enforcement decisions. */
|
|
230
|
+
export function deriveLessons(loopId, context, currentRound) {
|
|
231
|
+
const entries = loopEntries(loopId, context).filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
232
|
+
const violations = new Map();
|
|
233
|
+
const errors = new Map();
|
|
234
|
+
const warns = new Map();
|
|
235
|
+
for (const entry of entries) {
|
|
236
|
+
const rnd = entryRound(entry);
|
|
237
|
+
const viols = Array.isArray(entry.constraint_violations)
|
|
238
|
+
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
239
|
+
: [];
|
|
240
|
+
for (const text of viols) {
|
|
241
|
+
const list = violations.get(text) ?? [];
|
|
242
|
+
list.push(rnd);
|
|
243
|
+
violations.set(text, list);
|
|
244
|
+
}
|
|
245
|
+
const flags = Array.isArray(entry.verification_flags)
|
|
246
|
+
? entry.verification_flags
|
|
247
|
+
: [];
|
|
248
|
+
for (const flag of flags) {
|
|
249
|
+
const target = flag.severity === "error"
|
|
250
|
+
? errors
|
|
251
|
+
: flag.severity === "warn" ? warns : null;
|
|
252
|
+
if (!target)
|
|
253
|
+
continue;
|
|
254
|
+
const list = target.get(flag.check) ?? [];
|
|
255
|
+
list.push(rnd);
|
|
256
|
+
target.set(flag.check, list);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const lessons = [];
|
|
260
|
+
for (const [text, rounds] of violations) {
|
|
261
|
+
if (rounds.length >= 2) {
|
|
262
|
+
lessons.push({ text, kind: "constraint_violation", count: rounds.length, rounds });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const [text, rounds] of errors) {
|
|
266
|
+
if (rounds.length >= 2) {
|
|
267
|
+
lessons.push({ text, kind: "verification_error", count: rounds.length, rounds });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
for (const [text, rounds] of warns) {
|
|
271
|
+
if (rounds.length >= 2) {
|
|
272
|
+
lessons.push({ text, kind: "verification_warning", count: rounds.length, rounds });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return lessons.sort((a, b) => b.count - a.count);
|
|
276
|
+
}
|
|
277
|
+
/** v3.2: Derive per-criterion status — the "goal → criteria → evidence"
|
|
278
|
+
* vertical view. Each objective criterion gets: met/remaining/unknown
|
|
279
|
+
* (from per-round success_criteria_met/remaining reports, ID-first
|
|
280
|
+
* matching), the round it was first reported met, and any sub-goals whose
|
|
281
|
+
* description matches it (Jaccard). Zero persistence — re-derived from the
|
|
282
|
+
* vault every compile. */
|
|
283
|
+
export function deriveCriterionStatuses(loopId, context, objective, currentRound, subGoals, lastRoundResult) {
|
|
284
|
+
if (!objective || objective.success_criteria.length === 0)
|
|
285
|
+
return [];
|
|
286
|
+
const entries = loopEntries(loopId, context).filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
|
|
287
|
+
// The current round's report lives in last_round_result (the vault only
|
|
288
|
+
// gains it on commit); treat it as the report for round currentRound - 1.
|
|
289
|
+
const lastEv = lastRoundResult?.execution_evidence;
|
|
290
|
+
const lastMet = lastEv?.success_criteria_met ?? [];
|
|
291
|
+
const lastRemaining = lastEv?.success_criteria_remaining ?? [];
|
|
292
|
+
const lastEntry = entries[entries.length - 1] ?? null;
|
|
293
|
+
const lastEntryRemaining = lastEntry ? entryCriteriaRemaining(lastEntry) : [];
|
|
294
|
+
const matchThreshold = getPolicy().evolution.subgoal_match_threshold;
|
|
295
|
+
return objective.success_criteria.map((text) => {
|
|
296
|
+
let metAtRound = null;
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
const rnd = entryRound(entry);
|
|
299
|
+
if (entryCriteriaMet(entry).some((met) => criteriaMatch(met, text))) {
|
|
300
|
+
metAtRound = rnd;
|
|
301
|
+
break; // first report wins — chronological scan
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (metAtRound === null && lastMet.some((met) => criteriaMatch(met, text))) {
|
|
305
|
+
metAtRound = currentRound - 1;
|
|
306
|
+
}
|
|
307
|
+
const status = metAtRound !== null
|
|
308
|
+
? "met"
|
|
309
|
+
: lastRemaining.some((remaining) => criteriaMatch(remaining, text)) ||
|
|
310
|
+
lastEntryRemaining.some((remaining) => criteriaMatch(remaining, text))
|
|
311
|
+
? "remaining"
|
|
312
|
+
: "unknown";
|
|
313
|
+
return {
|
|
314
|
+
id: deriveCriterionId(text),
|
|
315
|
+
text,
|
|
316
|
+
status,
|
|
317
|
+
...(metAtRound !== null ? { met_at_round: metAtRound } : {}),
|
|
318
|
+
related_subgoal_ids: subGoals
|
|
319
|
+
.filter((sg) => jaccardSimilarity(text, sg.description) >= matchThreshold)
|
|
320
|
+
.map((sg) => sg.id),
|
|
321
|
+
};
|
|
322
|
+
});
|
|
323
|
+
}
|
|
158
324
|
/** v2.11: Match two criterion references for deduplication.
|
|
159
325
|
* If either is a criterion ID (cr-XXXXXXXX), uses exact ID comparison.
|
|
160
326
|
* Otherwise falls back to Jaccard similarity. */
|
|
@@ -240,7 +406,56 @@ function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, ki
|
|
|
240
406
|
generated_at_round: endRound,
|
|
241
407
|
});
|
|
242
408
|
}
|
|
243
|
-
|
|
409
|
+
/** v3.0.1: Evenly sample `budget` entries from `mid`, preferring
|
|
410
|
+
* agent_declared milestones (human/agent-labeled anchors) first and filling
|
|
411
|
+
* the remainder at even time positions. The caller guarantees
|
|
412
|
+
* `mid.length > budget`. */
|
|
413
|
+
function sampleMiddleMilestones(mid, budget) {
|
|
414
|
+
if (budget <= 0)
|
|
415
|
+
return [];
|
|
416
|
+
if (mid.length <= budget)
|
|
417
|
+
return mid;
|
|
418
|
+
const chosen = [];
|
|
419
|
+
const used = new Set();
|
|
420
|
+
for (const m of mid) {
|
|
421
|
+
if (m.kind === "agent_declared") {
|
|
422
|
+
chosen.push(m);
|
|
423
|
+
used.add(m);
|
|
424
|
+
if (chosen.length >= budget)
|
|
425
|
+
return chosen;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const rest = mid.filter((m) => !used.has(m));
|
|
429
|
+
const need = budget - chosen.length;
|
|
430
|
+
for (let i = 0; i < need; i++) {
|
|
431
|
+
const idx = Math.floor(((i + 0.5) * rest.length) / need);
|
|
432
|
+
chosen.push(rest[idx]);
|
|
433
|
+
}
|
|
434
|
+
return chosen;
|
|
435
|
+
}
|
|
436
|
+
/** v3.0.1: L1 milestone sampling — when milestones exceed max_milestones,
|
|
437
|
+
* keep the oldest `milestone_head_count` (history anchors), the newest
|
|
438
|
+
* `milestone_tail_count` (current progress), and an even sample of the
|
|
439
|
+
* middle. L2 (full rehydration) is never sampled: the recovery view keeps
|
|
440
|
+
* every milestone as the loop's memory skeleton. */
|
|
441
|
+
function sampleMilestones(milestones, policy, level) {
|
|
442
|
+
if (level === "l2")
|
|
443
|
+
return milestones;
|
|
444
|
+
if (milestones.length <= policy.max_milestones)
|
|
445
|
+
return milestones;
|
|
446
|
+
const head = Math.max(0, policy.milestone_head_count);
|
|
447
|
+
const tail = Math.max(0, policy.milestone_tail_count);
|
|
448
|
+
const midBudget = Math.max(0, policy.max_milestones - head - tail);
|
|
449
|
+
if (midBudget === 0)
|
|
450
|
+
return milestones.slice(0, head).concat(milestones.slice(milestones.length - tail));
|
|
451
|
+
const mid = milestones.slice(head, milestones.length - tail);
|
|
452
|
+
return [
|
|
453
|
+
...milestones.slice(0, head),
|
|
454
|
+
...sampleMiddleMilestones(mid, midBudget),
|
|
455
|
+
...milestones.slice(milestones.length - tail),
|
|
456
|
+
];
|
|
457
|
+
}
|
|
458
|
+
export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0, level) {
|
|
244
459
|
const policy = getPolicy().summary;
|
|
245
460
|
const allEntries = loopEntries(loopId, context);
|
|
246
461
|
// ── Phase 1: Recent Window (preserves existing behavior) ──
|
|
@@ -259,8 +474,15 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
259
474
|
const summary = typeof entry.output_summary === "string"
|
|
260
475
|
? entry.output_summary
|
|
261
476
|
: typeof data.output_summary === "string" ? data.output_summary : "";
|
|
477
|
+
// v2.12: Declared outcome wins the label; the default wording stays
|
|
478
|
+
// "accepted" for zero regression on existing loop output.
|
|
479
|
+
const declared = entry.outcome;
|
|
480
|
+
const outcomeLabel = declared === "success" || declared === "partial" ||
|
|
481
|
+
declared === "failed" || declared === "blocked"
|
|
482
|
+
? declared
|
|
483
|
+
: success === false ? "failed" : "accepted";
|
|
262
484
|
if (summary)
|
|
263
|
-
outcomes.push(`[R${round}] ${
|
|
485
|
+
outcomes.push(`[R${round}] ${outcomeLabel}: ${summary}`);
|
|
264
486
|
const violations = Array.isArray(entry.constraint_violations)
|
|
265
487
|
? entry.constraint_violations
|
|
266
488
|
: Array.isArray(data.constraint_violations) ? data.constraint_violations : [];
|
|
@@ -314,7 +536,10 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
314
536
|
milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, endRound, `Rounds ${startRound}–${endRound}`, "auto"));
|
|
315
537
|
}
|
|
316
538
|
}
|
|
317
|
-
|
|
539
|
+
// v3.0.1: L1 keeps head anchors + newest tail + an even middle sample;
|
|
540
|
+
// L2 (full rehydration) keeps every milestone — the recovery view is the
|
|
541
|
+
// loop's memory skeleton and must not be truncated.
|
|
542
|
+
const cappedMilestones = sampleMilestones(milestones, policy, level);
|
|
318
543
|
// ── Phase 3: Loop Synthesis (formulaic) ──
|
|
319
544
|
let loopSynthesis = "";
|
|
320
545
|
if (policy.enable_loop_synthesis && currentRound > 1) {
|
|
@@ -347,28 +572,29 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
347
572
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
348
573
|
// Sub-Goal Structured Tracking (v2.2)
|
|
349
574
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
350
|
-
/** Derive a stable sub-goal ID from its description hash.
|
|
351
|
-
|
|
352
|
-
|
|
575
|
+
/** Derive a stable sub-goal ID from its description hash.
|
|
576
|
+
* Exported as the single source of truth (verification-gate imports it). */
|
|
577
|
+
export function deriveSubGoalId(description) {
|
|
578
|
+
return "sg-" + deriveItemId(description);
|
|
353
579
|
}
|
|
354
580
|
/** v2.11: Derive a stable constraint ID from its text hash (c-XXXXXXXX).
|
|
355
581
|
* Same hash strategy as SubGoal — deterministic across rounds. */
|
|
356
|
-
function deriveConstraintId(text) {
|
|
357
|
-
return "c-" +
|
|
582
|
+
export function deriveConstraintId(text) {
|
|
583
|
+
return "c-" + deriveItemId(text);
|
|
358
584
|
}
|
|
359
585
|
/** v2.11: Derive a stable criterion ID from its text hash (cr-XXXXXXXX).
|
|
360
586
|
* Same hash strategy as SubGoal — deterministic across rounds. */
|
|
361
|
-
function deriveCriterionId(text) {
|
|
362
|
-
return "cr-" +
|
|
587
|
+
export function deriveCriterionId(text) {
|
|
588
|
+
return "cr-" + deriveItemId(text);
|
|
363
589
|
}
|
|
364
590
|
/** v2.11: Check whether a user-provided reference looks like a constraint ID.
|
|
365
591
|
* Matches the pattern c-XXXXXXXX where X is a hex digit. */
|
|
366
|
-
function isConstraintId(ref) {
|
|
592
|
+
export function isConstraintId(ref) {
|
|
367
593
|
return /^c-[a-f0-9]{8}$/.test(ref);
|
|
368
594
|
}
|
|
369
595
|
/** v2.11: Check whether a user-provided reference looks like a criterion ID.
|
|
370
596
|
* Matches the pattern cr-XXXXXXXX where X is a hex digit. */
|
|
371
|
-
function isCriterionId(ref) {
|
|
597
|
+
export function isCriterionId(ref) {
|
|
372
598
|
return /^cr-[a-f0-9]{8}$/.test(ref);
|
|
373
599
|
}
|
|
374
600
|
/** Read emerged_subtasks from a vault entry (handles direct + lineage nesting). */
|
|
@@ -555,8 +781,10 @@ function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
|
|
|
555
781
|
if (ra !== rb)
|
|
556
782
|
return ra - rb;
|
|
557
783
|
// Within same status: pending by oldest first, done by most recent first
|
|
784
|
+
// (v2.14: the previous expression simplified to b.declared - a.declared,
|
|
785
|
+
// which sorted NEWEST first despite the comment)
|
|
558
786
|
if (a.status === "pending")
|
|
559
|
-
return
|
|
787
|
+
return a.declared_at_round - b.declared_at_round;
|
|
560
788
|
if (a.status === "done")
|
|
561
789
|
return (b.completed_at_round ?? 0) - (a.completed_at_round ?? 0);
|
|
562
790
|
return a.declared_at_round - b.declared_at_round;
|
|
@@ -612,8 +840,17 @@ function findDiscoveredRound(text, vaultEntries) {
|
|
|
612
840
|
* v2.11: ID-first matching. Checks exact constraint ID match (c-XXXXXXXX)
|
|
613
841
|
* against the derived ID of each vault entry's constraint_violations, then
|
|
614
842
|
* falls back to Jaccard similarity on text. Returns 0 if never violated. */
|
|
615
|
-
|
|
843
|
+
/** True when a reported violation text matches a constraint text — same
|
|
844
|
+
* ID-first + Jaccard fallback matching as findLastViolatedRound. */
|
|
845
|
+
function matchesConstraintText(text, violation) {
|
|
616
846
|
const targetId = deriveConstraintId(text);
|
|
847
|
+
if (violation === targetId || deriveConstraintId(violation) === targetId) {
|
|
848
|
+
return true;
|
|
849
|
+
}
|
|
850
|
+
return jaccardSimilarity(text, violation) >=
|
|
851
|
+
getPolicy().evolution.constraint_match_threshold;
|
|
852
|
+
}
|
|
853
|
+
function findLastViolatedRound(text, vaultEntries) {
|
|
617
854
|
let latest = 0;
|
|
618
855
|
for (const entry of vaultEntries) {
|
|
619
856
|
const rnd = entryRound(entry);
|
|
@@ -623,14 +860,7 @@ function findLastViolatedRound(text, vaultEntries) {
|
|
|
623
860
|
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
624
861
|
: [];
|
|
625
862
|
for (const v of violations) {
|
|
626
|
-
|
|
627
|
-
if (v === targetId || deriveConstraintId(v) === targetId) {
|
|
628
|
-
if (rnd > latest)
|
|
629
|
-
latest = rnd;
|
|
630
|
-
break;
|
|
631
|
-
}
|
|
632
|
-
// Phase 2: Jaccard fallback (backward compatibility)
|
|
633
|
-
if (jaccardSimilarity(text, v) >= getPolicy().evolution.constraint_match_threshold) {
|
|
863
|
+
if (matchesConstraintText(text, v)) {
|
|
634
864
|
if (rnd > latest)
|
|
635
865
|
latest = rnd;
|
|
636
866
|
break;
|
|
@@ -647,20 +877,29 @@ function findLastViolatedRound(text, vaultEntries) {
|
|
|
647
877
|
* constraints never auto-decay. Inactive constraints are auto-reactivated
|
|
648
878
|
* if violated again in the current round.
|
|
649
879
|
*/
|
|
650
|
-
function manageConstraintLifecycle(active, objective, constraintsFromPlan, vaultContext, currentRound) {
|
|
880
|
+
function manageConstraintLifecycle(active, objective, constraintsFromPlan, vaultContext, currentRound, lastRoundViolations = []) {
|
|
651
881
|
const policy = getPolicy().evolution;
|
|
652
882
|
const allEntries = loopEntries(objective.loop_id || "", vaultContext);
|
|
653
883
|
const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
|
|
654
884
|
const metadata = [];
|
|
655
885
|
for (const text of active) {
|
|
656
886
|
const source = constraintSource(text, objective, constraintsFromPlan);
|
|
887
|
+
// v3.2.1: a violation reported in THIS round (last_round_result) counts
|
|
888
|
+
// as a violation at currentRound — the previous rounds' scan cannot see
|
|
889
|
+
// it (this round is not committed yet). This makes the L1 collapse
|
|
890
|
+
// render "(violated this round)" (diffConstraints compares
|
|
891
|
+
// last_violated_at_round === round) and re-activates decayed
|
|
892
|
+
// discovered constraints.
|
|
893
|
+
const violatedThisRound = lastRoundViolations.some((v) => matchesConstraintText(text, v));
|
|
657
894
|
// Structural constraints never decay
|
|
658
895
|
if (source === "hard" || source === "plan" || source === "criteria") {
|
|
659
896
|
metadata.push(makeConstraintMeta({
|
|
660
897
|
id: deriveConstraintId(text),
|
|
661
898
|
text,
|
|
662
899
|
discovered_at_round: objective.created_at_round || 1,
|
|
663
|
-
last_violated_at_round:
|
|
900
|
+
last_violated_at_round: violatedThisRound
|
|
901
|
+
? currentRound
|
|
902
|
+
: findLastViolatedRound(text, completedEntries),
|
|
664
903
|
source,
|
|
665
904
|
status: "active",
|
|
666
905
|
}));
|
|
@@ -668,7 +907,9 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
668
907
|
}
|
|
669
908
|
// Discovered constraints: check for inactivity
|
|
670
909
|
const discoveredAt = findDiscoveredRound(text, completedEntries);
|
|
671
|
-
const lastViolated =
|
|
910
|
+
const lastViolated = violatedThisRound
|
|
911
|
+
? currentRound
|
|
912
|
+
: findLastViolatedRound(text, completedEntries);
|
|
672
913
|
const roundsSinceRelevant = lastViolated > 0
|
|
673
914
|
? currentRound - lastViolated
|
|
674
915
|
: currentRound - (discoveredAt || currentRound);
|
|
@@ -696,12 +937,14 @@ export function alignTask(proposedTask, request, context) {
|
|
|
696
937
|
const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
|
|
697
938
|
if (!objective)
|
|
698
939
|
return makeTaskAlignment();
|
|
940
|
+
// v2.14: threshold is policy-driven (evolution.progress_mismatch_threshold)
|
|
941
|
+
const threshold = getPolicy().evolution.progress_mismatch_threshold;
|
|
699
942
|
const score = jaccardSimilarity(proposedTask, [objective.objective, ...objective.success_criteria, ...objective.hard_constraints].join(" "));
|
|
700
943
|
return makeTaskAlignment({
|
|
701
|
-
is_aligned: score >=
|
|
944
|
+
is_aligned: score >= threshold,
|
|
702
945
|
alignment_score: Number(score.toFixed(2)),
|
|
703
|
-
warning: score <
|
|
704
|
-
escalation: score <
|
|
946
|
+
warning: score < threshold ? "Current task may be drifting from the loop objective." : "",
|
|
947
|
+
escalation: score < threshold ? "warn" : "none",
|
|
705
948
|
});
|
|
706
949
|
}
|
|
707
950
|
export function checkLoopHealth(loopId, request, context) {
|
|
@@ -710,7 +953,9 @@ export function checkLoopHealth(loopId, request, context) {
|
|
|
710
953
|
const violations = request.last_round_result?.constraint_violations.length ?? 0;
|
|
711
954
|
const integrity = Math.max(0, 1 - violations * 0.2);
|
|
712
955
|
const continuity = previous ? jaccardSimilarity(previous.task, request.task) : 1;
|
|
713
|
-
|
|
956
|
+
// v2.14: thresholds are policy-driven
|
|
957
|
+
const drift = alignment.alignment_score < getPolicy().evolution.progress_mismatch_threshold ||
|
|
958
|
+
continuity < getPolicy().evolution.task_continuity_threshold;
|
|
714
959
|
return makeLoopHealth({
|
|
715
960
|
goal_alignment: alignment.alignment_score,
|
|
716
961
|
constraint_integrity: integrity,
|
|
@@ -735,11 +980,19 @@ function levelDecision(request, context) {
|
|
|
735
980
|
.map(entryRound)
|
|
736
981
|
.filter((round) => round < request.round)
|
|
737
982
|
.at(-1) ?? 1;
|
|
983
|
+
// v2.14: a backtrack committed for this round means the loop is
|
|
984
|
+
// re-walking restored state — the recovery boundary forces L2
|
|
985
|
+
// rehydration (the decidePromptLevel recovery_boundary branch existed
|
|
986
|
+
// but no caller ever set it).
|
|
987
|
+
const recoveryBoundary = loopEntries(request.loop_id, context)
|
|
988
|
+
.some((entry) => lineage(entry).committed_action === "backtrack" &&
|
|
989
|
+
entryRound(entry) === request.round);
|
|
738
990
|
return decidePromptLevel({
|
|
739
991
|
round: request.round,
|
|
740
992
|
attempt: request.attempt,
|
|
741
993
|
forceLevel: request.force_level,
|
|
742
994
|
hasPlanSource: Boolean(request.plan_source),
|
|
995
|
+
recoveryBoundary,
|
|
743
996
|
checkpointBoundary: last?.compression_checkpoint === true,
|
|
744
997
|
goalChanged: previous !== null && previous.goal_id !== deriveGoalId(request.loop_id, request.task, request.goal_id),
|
|
745
998
|
previousStateMissing: request.round > 1 && previous === null,
|
|
@@ -754,10 +1007,15 @@ function levelDecision(request, context) {
|
|
|
754
1007
|
export function decideLevel(request, context) {
|
|
755
1008
|
return levelDecision(request, context).level;
|
|
756
1009
|
}
|
|
757
|
-
export function buildSelfEvalBlock(round, prevDriftFlags
|
|
1010
|
+
export function buildSelfEvalBlock(round, prevDriftFlags,
|
|
1011
|
+
/** v2.12: L0 is the minimal retry template — the v2.12 declarative fields
|
|
1012
|
+
* (outcome/blocker/retroactiveClaims) are L1/L2 additions so the retry
|
|
1013
|
+
* prompt stays within its tight budget. */
|
|
1014
|
+
level) {
|
|
758
1015
|
const hasIntentDrift = prevDriftFlags?.some(f => f.check === "intent_drift") ?? false;
|
|
759
1016
|
const hasSubGoalDrift = prevDriftFlags?.some(f => f.check === "subgoal_drift") ?? false;
|
|
760
1017
|
const needsClarification = hasIntentDrift || hasSubGoalDrift;
|
|
1018
|
+
const declareOutcome = level !== "l0";
|
|
761
1019
|
const evalObj = {
|
|
762
1020
|
success: false,
|
|
763
1021
|
output_summary: `<verified result of round ${round}>`,
|
|
@@ -778,6 +1036,12 @@ export function buildSelfEvalBlock(round, prevDriftFlags) {
|
|
|
778
1036
|
blocked_subtasks: ["<sg-XXXXXXXX or text>"],
|
|
779
1037
|
canceled_subtasks: ["<sg-XXXXXXXX or text>"],
|
|
780
1038
|
};
|
|
1039
|
+
// v2.12: Declarative tri-state outcome (L1/L2 only — keeps L0 retry lean)
|
|
1040
|
+
if (declareOutcome) {
|
|
1041
|
+
evalObj.outcome = "<success|partial|failed|blocked>";
|
|
1042
|
+
evalObj.blocker = "<required only when outcome=blocked>";
|
|
1043
|
+
evalObj.retroactiveClaims = [];
|
|
1044
|
+
}
|
|
781
1045
|
// v2.8: Inject drift clarification field when previous round had drift flags
|
|
782
1046
|
if (needsClarification) {
|
|
783
1047
|
const flagTypes = [
|
|
@@ -815,10 +1079,14 @@ export function compileLoop(request, context) {
|
|
|
815
1079
|
const decision = levelDecision(request, context);
|
|
816
1080
|
const previous = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
817
1081
|
const objective = evolveObjective(request, context);
|
|
818
|
-
const constraints = evolveConstraints(request, objective, previous);
|
|
819
|
-
const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round);
|
|
820
|
-
const rolling = buildRollingSummary(request.loop_id, request.round, context);
|
|
1082
|
+
const constraints = evolveConstraints(request, objective, previous, context);
|
|
1083
|
+
const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round, request.last_round_result?.constraint_violations ?? []);
|
|
1084
|
+
const rolling = buildRollingSummary(request.loop_id, request.round, context, 0, decision.level);
|
|
821
1085
|
const subGoals = manageSubGoals(request.loop_id, request.round, request.last_round_result, context);
|
|
1086
|
+
// v3.2: Goal → criteria → evidence vertical view (derived, zero persistence).
|
|
1087
|
+
const criterionStatuses = deriveCriterionStatuses(request.loop_id, context, objective, request.round, subGoals, request.last_round_result);
|
|
1088
|
+
// v3.2: Lessons learned — repeated violations / verification failures.
|
|
1089
|
+
const lessons = deriveLessons(request.loop_id, context, request.round);
|
|
822
1090
|
const alignment = alignTask(request.task, request, context);
|
|
823
1091
|
const health = checkLoopHealth(request.loop_id, { ...request, loop_objective: objective }, context);
|
|
824
1092
|
const warnings = unique([
|
|
@@ -863,6 +1131,8 @@ export function compileLoop(request, context) {
|
|
|
863
1131
|
task_alignment: alignment,
|
|
864
1132
|
rolling_summary: rolling,
|
|
865
1133
|
sub_goals: subGoals,
|
|
1134
|
+
criterion_statuses: criterionStatuses,
|
|
1135
|
+
lessons,
|
|
866
1136
|
agent_trust_score: request.round > 1 ? Number(trustScore.toFixed(2)) : undefined,
|
|
867
1137
|
agent_trust_trend: request.round > 1 ? trend : [],
|
|
868
1138
|
suggested_next_task: request.last_round_result?.next_action?.trim() ||
|
|
@@ -896,10 +1166,12 @@ export function compileLoop(request, context) {
|
|
|
896
1166
|
l2: adaptiveL2,
|
|
897
1167
|
},
|
|
898
1168
|
attempt: request.attempt,
|
|
899
|
-
selfEvaluationBlock: buildSelfEvalBlock(request.round, request.verification_flags),
|
|
1169
|
+
selfEvaluationBlock: buildSelfEvalBlock(request.round, request.verification_flags, decision.level),
|
|
900
1170
|
fullStateMarkdown: l2Pointer ? undefined : markdown,
|
|
901
1171
|
// v2.9: Model's information needs from the previous round's SelfEvaluation
|
|
902
1172
|
promptRequests: request.last_round_result?.prompt_requests,
|
|
1173
|
+
// v3.2: Previous L1 presentation as the collapse diff baseline.
|
|
1174
|
+
presentedBaseline: readPresentedBaseline(request.loop_id, request.round, context),
|
|
903
1175
|
});
|
|
904
1176
|
response.prompt = artifact.renderedPrompt;
|
|
905
1177
|
response.prompt_artifact = artifact;
|