loopforge 3.2.1 → 3.7.0
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 +74 -71
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +6 -38
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +71 -6
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +176 -29
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +16 -26
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts +17 -0
- package/dist/cognitive-facts.d.ts.map +1 -0
- package/dist/cognitive-facts.js +99 -0
- package/dist/cognitive-facts.js.map +1 -0
- package/dist/cognitive-governance.d.ts +0 -1
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +1 -4
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +100 -0
- package/dist/committed-round.d.ts.map +1 -0
- package/dist/committed-round.js +349 -0
- package/dist/committed-round.js.map +1 -0
- package/dist/enforcement-gate.d.ts +4 -2
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +385 -357
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +16 -4
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +65 -16
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +15 -39
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +35 -17
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +105 -108
- package/dist/evidence-provider.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +21 -10
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +232 -276
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-projection.d.ts +5 -25
- package/dist/loop-projection.d.ts.map +1 -1
- package/dist/loop-projection.js +18 -134
- package/dist/loop-projection.js.map +1 -1
- package/dist/loop-store.d.ts +16 -18
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +21 -63
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +13 -10
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +95 -68
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.js +3 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +17 -5
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +177 -54
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +12 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +161 -53
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +0 -7
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +15 -55
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +22 -31
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +24 -42
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +0 -3
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +124 -191
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +61 -46
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +2 -18
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +21 -22
- package/dist/replay.js.map +1 -1
- package/dist/round-contract.d.ts +64 -0
- package/dist/round-contract.d.ts.map +1 -0
- package/dist/round-contract.js +80 -0
- package/dist/round-contract.js.map +1 -0
- package/dist/round-coordinator.d.ts +5 -3
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +34 -29
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +0 -4
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +1 -11
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +0 -1
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +0 -1
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +29 -34
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +112 -148
- package/dist/self-eval.js.map +1 -1
- package/dist/token-utils.d.ts +7 -3
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +18 -3
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +72 -21
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +544 -157
- package/dist/verification-gate.js.map +1 -1
- package/dist/workspace.d.ts +29 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +61 -0
- package/dist/workspace.js.map +1 -0
- package/loop_policy.json +5 -17
- package/package.json +2 -2
- package/skills/perception/SKILL.md +137 -136
package/dist/loop-compiler.js
CHANGED
|
@@ -7,6 +7,8 @@ import { createHash } from "node:crypto";
|
|
|
7
7
|
import { getPolicy } from "./policy.js";
|
|
8
8
|
import { AgentStatus, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeConstraintMeta, makeMilestoneSummary, makeRollingSummary, makeSubGoal, makeTaskAlignment, } from "./protocol.js";
|
|
9
9
|
import { createCanonicalLoopState, renderCanonicalStateMarkdown, } from "./canonical-state.js";
|
|
10
|
+
import { decodeRound, entryLineage, entryCriteriaMet, entryCriteriaRemaining, entryActiveConstraints, entryRetractedConstraints, entryProgressEstimate, entryEmergedSubtasks, machineGitMotionSeries, mergedRoundsFromEntries, } from "./committed-round.js";
|
|
11
|
+
import { contractRoundEvaluations, deriveActiveRoundContract, } from "./round-contract.js";
|
|
10
12
|
import { assemblePromptArtifact } from "./prompt-assembler.js";
|
|
11
13
|
import { decidePromptLevel, } from "./prompt-policy.js";
|
|
12
14
|
import { deriveItemId, jaccardSimilarity, unique, entryRound } from "./token-utils.js";
|
|
@@ -15,17 +17,34 @@ function contextEntries(context) {
|
|
|
15
17
|
return [];
|
|
16
18
|
return context.results.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
|
|
17
19
|
}
|
|
18
|
-
function lineage(entry) {
|
|
19
|
-
const value = entry.loop_lineage ?? entry.lineage;
|
|
20
|
-
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
21
|
-
? value
|
|
22
|
-
: {};
|
|
23
|
-
}
|
|
24
20
|
function loopEntries(loopId, context) {
|
|
25
21
|
return contextEntries(context)
|
|
26
|
-
.filter((entry) => entry.loop_id === loopId ||
|
|
22
|
+
.filter((entry) => entry.loop_id === loopId || entryLineage(entry).loop_id === loopId)
|
|
27
23
|
.sort((a, b) => entryRound(a) - entryRound(b));
|
|
28
24
|
}
|
|
25
|
+
/** v3.3.1: The canonical entry for a round — its compile-time lineage entry
|
|
26
|
+
* (task_type "loop_lineage"; engine hydration merges the committed
|
|
27
|
+
* decision into it, so one round = one merged lineage entry in the
|
|
28
|
+
* compiler's view). Event/journal entries (delegation journals, gate
|
|
29
|
+
* records) share lineage.round with the round but carry none of its
|
|
30
|
+
* compile-time fields; round-level consumers that matched on the round
|
|
31
|
+
* number alone could hit them first (they sort after the lineage entry of
|
|
32
|
+
* the same round), producing an empty goal_id / task / constraints_active
|
|
33
|
+
* and forcing spurious L2 recovery. A direct :feedback entry is accepted
|
|
34
|
+
* when the caller supplies a raw committed view instead of hydration. */
|
|
35
|
+
function roundCanonicalEntry(entries, round) {
|
|
36
|
+
const candidates = entries.filter((entry) => entryRound(entry) === round);
|
|
37
|
+
return (candidates.find((entry) => entry.task_type === "loop_lineage") ??
|
|
38
|
+
candidates.find((entry) => String(entry.task_id ?? "").endsWith(":feedback")) ??
|
|
39
|
+
// Untyped compiler inputs are treated as round views, not events. Known event
|
|
40
|
+
// types (delegation journals, gate records) are excluded explicitly so
|
|
41
|
+
// they can never shadow a round again.
|
|
42
|
+
candidates.find((entry) => {
|
|
43
|
+
const type = String(entry.task_type ?? "");
|
|
44
|
+
return type === "" || (!type.startsWith("delegation") && !type.endsWith("journal"));
|
|
45
|
+
}) ??
|
|
46
|
+
null);
|
|
47
|
+
}
|
|
29
48
|
export function computeGoalTextHash(text) {
|
|
30
49
|
const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
|
|
31
50
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
|
|
@@ -38,15 +57,13 @@ export function deriveGoalId(loopId, task, explicit = "") {
|
|
|
38
57
|
}
|
|
39
58
|
/** v3.2: Read the previous round's persisted L1 presentation snapshot — the
|
|
40
59
|
* diff baseline for L1 collapse. Returns null when the previous round's
|
|
41
|
-
* lineage entry
|
|
42
|
-
*
|
|
43
|
-
* existing fixtures and pre-v3.2 loops on the full-render path. */
|
|
60
|
+
* lineage entry lacks the three presented_* fields (as on L0/L2 compiles),
|
|
61
|
+
* so callers render the full L1 state. */
|
|
44
62
|
export function readPresentedBaseline(loopId, round, context) {
|
|
45
|
-
const entry =
|
|
46
|
-
.find((candidate) => entryRound(candidate) === round - 1);
|
|
63
|
+
const entry = roundCanonicalEntry(loopEntries(loopId, context), round - 1);
|
|
47
64
|
if (!entry)
|
|
48
65
|
return null;
|
|
49
|
-
const data =
|
|
66
|
+
const data = entryLineage(entry);
|
|
50
67
|
const constraintIds = data.presented_constraint_ids;
|
|
51
68
|
const subGoals = data.presented_subgoals;
|
|
52
69
|
const milestoneRanges = data.presented_milestone_ranges;
|
|
@@ -65,11 +82,10 @@ export function readPresentedBaseline(loopId, round, context) {
|
|
|
65
82
|
};
|
|
66
83
|
}
|
|
67
84
|
export function getPreviousRound(loopId, round, context) {
|
|
68
|
-
const entry =
|
|
69
|
-
.find((candidate) => entryRound(candidate) === round);
|
|
85
|
+
const entry = roundCanonicalEntry(loopEntries(loopId, context), round);
|
|
70
86
|
if (!entry)
|
|
71
87
|
return null;
|
|
72
|
-
const data =
|
|
88
|
+
const data = entryLineage(entry);
|
|
73
89
|
return {
|
|
74
90
|
round,
|
|
75
91
|
goal_id: typeof data.goal_id === "string" ? data.goal_id : "",
|
|
@@ -160,7 +176,7 @@ function evolveConstraints(request, objective, previous, context) {
|
|
|
160
176
|
const lastRetractedRound = new Map();
|
|
161
177
|
if (window > 0) {
|
|
162
178
|
for (const entry of loopEntries(request.loop_id, context)) {
|
|
163
|
-
const retractedList =
|
|
179
|
+
const retractedList = entryLineage(entry).retracted_constraints;
|
|
164
180
|
if (!Array.isArray(retractedList))
|
|
165
181
|
continue;
|
|
166
182
|
const rnd = entryRound(entry);
|
|
@@ -193,36 +209,6 @@ function evolveConstraints(request, objective, previous, context) {
|
|
|
193
209
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
194
210
|
// Hierarchical Summary (v2.1)
|
|
195
211
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
196
|
-
/** Extract execution_evidence from a vault entry, handling both direct-field
|
|
197
|
-
* and lineage-nested shapes. Returns null when absent. */
|
|
198
|
-
function entryExecutionEvidence(entry) {
|
|
199
|
-
const direct = entry.execution_evidence;
|
|
200
|
-
if (direct && typeof direct === "object" && !Array.isArray(direct)) {
|
|
201
|
-
return direct;
|
|
202
|
-
}
|
|
203
|
-
const lin = lineage(entry);
|
|
204
|
-
const nested = lin.execution_evidence;
|
|
205
|
-
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
206
|
-
return nested;
|
|
207
|
-
}
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
/** Read success_criteria_met from an entry's execution_evidence. */
|
|
211
|
-
function entryCriteriaMet(entry) {
|
|
212
|
-
const ev = entryExecutionEvidence(entry);
|
|
213
|
-
if (!ev)
|
|
214
|
-
return [];
|
|
215
|
-
const arr = ev.success_criteria_met;
|
|
216
|
-
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
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
212
|
/** v3.2: Deterministic lessons learned — constraints violated repeatedly or
|
|
227
213
|
* verification checks failing repeatedly across rounds (full history, unlike
|
|
228
214
|
* the enforcement gate's R2 3-round window). Presentation only: the output
|
|
@@ -234,17 +220,20 @@ export function deriveLessons(loopId, context, currentRound) {
|
|
|
234
220
|
const warns = new Map();
|
|
235
221
|
for (const entry of entries) {
|
|
236
222
|
const rnd = entryRound(entry);
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
223
|
+
const view = decodeRound(entry);
|
|
224
|
+
const viols = view
|
|
225
|
+
? view.constraintViolations ?? []
|
|
226
|
+
: Array.isArray(entry.constraint_violations)
|
|
227
|
+
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
228
|
+
: [];
|
|
240
229
|
for (const text of viols) {
|
|
241
230
|
const list = violations.get(text) ?? [];
|
|
242
231
|
list.push(rnd);
|
|
243
232
|
violations.set(text, list);
|
|
244
233
|
}
|
|
245
|
-
const flags =
|
|
246
|
-
?
|
|
247
|
-
: [];
|
|
234
|
+
const flags = view
|
|
235
|
+
? view.verificationFlags
|
|
236
|
+
: Array.isArray(entry.verification_flags) ? entry.verification_flags : [];
|
|
248
237
|
for (const flag of flags) {
|
|
249
238
|
const target = flag.severity === "error"
|
|
250
239
|
? errors
|
|
@@ -274,6 +263,59 @@ export function deriveLessons(loopId, context, currentRound) {
|
|
|
274
263
|
}
|
|
275
264
|
return lessons.sort((a, b) => b.count - a.count);
|
|
276
265
|
}
|
|
266
|
+
/** v3.3: Derive display-only round statistics over the last `window`
|
|
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;
|
|
292
|
+
}
|
|
293
|
+
/** v3.3: Machine git-motion cross-check over the last 3 committed rounds
|
|
294
|
+
* (the R4 window). Undefined when no committed rounds carry git snapshots
|
|
295
|
+
* — the dashboard then shows no machine row. Display-only companion to the
|
|
296
|
+
* R4/R5 exculpatory signal. */
|
|
297
|
+
function deriveMachineStatus(rounds, currentRound) {
|
|
298
|
+
const series = machineGitMotionSeries(rounds, currentRound, 3);
|
|
299
|
+
if (series === null)
|
|
300
|
+
return undefined;
|
|
301
|
+
const motionRounds = series.filter(Boolean).length;
|
|
302
|
+
return { windowRounds: 3, gitMotion: motionRounds > 0, motionRounds };
|
|
303
|
+
}
|
|
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
|
+
}
|
|
277
319
|
/** v3.2: Derive per-criterion status — the "goal → criteria → evidence"
|
|
278
320
|
* vertical view. Each objective criterion gets: met/remaining/unknown
|
|
279
321
|
* (from per-round success_criteria_met/remaining reports, ID-first
|
|
@@ -323,8 +365,10 @@ export function deriveCriterionStatuses(loopId, context, objective, currentRound
|
|
|
323
365
|
}
|
|
324
366
|
/** v2.11: Match two criterion references for deduplication.
|
|
325
367
|
* If either is a criterion ID (cr-XXXXXXXX), uses exact ID comparison.
|
|
326
|
-
* Otherwise falls back to Jaccard similarity.
|
|
327
|
-
|
|
368
|
+
* Otherwise falls back to Jaccard similarity.
|
|
369
|
+
* v3.3: exported for the verification gate's windowed criteria-completion
|
|
370
|
+
* scan (R4/R5 exculpatory cross-check). */
|
|
371
|
+
export function criteriaMatch(a, b) {
|
|
328
372
|
const aIsId = isCriterionId(a);
|
|
329
373
|
const bIsId = isCriterionId(b);
|
|
330
374
|
// Both are IDs — exact string comparison
|
|
@@ -341,7 +385,7 @@ function criteriaMatch(a, b) {
|
|
|
341
385
|
/** Detect newly met criteria by comparing the current entry's
|
|
342
386
|
* success_criteria_met against the previous entry's.
|
|
343
387
|
* v2.11: ID-first matching (cr-XXXXXXXX) with Jaccard similarity
|
|
344
|
-
* fallback for
|
|
388
|
+
* fallback for natural-language references.
|
|
345
389
|
* Returns empty array when there is no previous entry — the first
|
|
346
390
|
* entry's criteria are the baseline, not a "new" event. */
|
|
347
391
|
function detectNewCriteria(current, previous) {
|
|
@@ -357,37 +401,13 @@ function detectNewCriteria(current, previous) {
|
|
|
357
401
|
return currMet;
|
|
358
402
|
return currMet.filter((curr) => prevMet.every((prev) => !criteriaMatch(curr, prev)));
|
|
359
403
|
}
|
|
360
|
-
/** Read constraints_active from an entry's lineage. */
|
|
361
|
-
function entryActiveConstraints(entry) {
|
|
362
|
-
const lin = lineage(entry);
|
|
363
|
-
const arr = lin.constraints_active;
|
|
364
|
-
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
365
|
-
}
|
|
366
|
-
/** Read retracted_constraints from an entry. */
|
|
367
|
-
function entryRetractedConstraints(entry) {
|
|
368
|
-
const direct = entry.retracted_constraints;
|
|
369
|
-
if (Array.isArray(direct))
|
|
370
|
-
return direct.filter((v) => typeof v === "string");
|
|
371
|
-
const lin = lineage(entry);
|
|
372
|
-
const nested = lin.retracted_constraints;
|
|
373
|
-
if (Array.isArray(nested))
|
|
374
|
-
return nested.filter((v) => typeof v === "string");
|
|
375
|
-
return [];
|
|
376
|
-
}
|
|
377
|
-
/** Read progress_estimate from an entry's execution_evidence. */
|
|
378
|
-
function entryProgressEstimate(entry) {
|
|
379
|
-
const ev = entryExecutionEvidence(entry);
|
|
380
|
-
if (!ev)
|
|
381
|
-
return 0;
|
|
382
|
-
const pe = ev.progress_estimate;
|
|
383
|
-
return typeof pe === "number" ? pe : 0;
|
|
384
|
-
}
|
|
385
404
|
/** Build a single MilestoneSummary from a range of completed round entries. */
|
|
386
405
|
function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, kind) {
|
|
387
406
|
const last = phaseEntries.at(-1);
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
407
|
+
const lastView = last ? decodeRound(last) : null;
|
|
408
|
+
const outcomeRaw = lastView
|
|
409
|
+
? (lastView.outputSummary ?? "")
|
|
410
|
+
: last && typeof last.output_summary === "string" ? last.output_summary : "";
|
|
391
411
|
const outcomeMax = getPolicy().checkpoint.outcome_max_chars;
|
|
392
412
|
const outcome = outcomeRaw.length > outcomeMax
|
|
393
413
|
? outcomeRaw.slice(0, outcomeMax) + "…"
|
|
@@ -468,24 +488,29 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
468
488
|
const outcomes = [];
|
|
469
489
|
const issues = [];
|
|
470
490
|
for (const entry of windowEntries) {
|
|
471
|
-
const
|
|
491
|
+
const view = decodeRound(entry);
|
|
492
|
+
const data = entryLineage(entry);
|
|
472
493
|
const round = entryRound(entry);
|
|
473
|
-
const success = entry.success ?? data.success;
|
|
474
|
-
const summary =
|
|
475
|
-
?
|
|
476
|
-
: typeof
|
|
494
|
+
const success = view ? view.success : entry.success ?? data.success;
|
|
495
|
+
const summary = view
|
|
496
|
+
? view.outputSummary ?? ""
|
|
497
|
+
: typeof entry.output_summary === "string"
|
|
498
|
+
? entry.output_summary
|
|
499
|
+
: typeof data.output_summary === "string" ? data.output_summary : "";
|
|
477
500
|
// v2.12: Declared outcome wins the label; the default wording stays
|
|
478
501
|
// "accepted" for zero regression on existing loop output.
|
|
479
|
-
const declared = entry.outcome;
|
|
502
|
+
const declared = view ? view.outcome : entry.outcome;
|
|
480
503
|
const outcomeLabel = declared === "success" || declared === "partial" ||
|
|
481
504
|
declared === "failed" || declared === "blocked"
|
|
482
505
|
? declared
|
|
483
506
|
: success === false ? "failed" : "accepted";
|
|
484
507
|
if (summary)
|
|
485
508
|
outcomes.push(`[R${round}] ${outcomeLabel}: ${summary}`);
|
|
486
|
-
const violations =
|
|
487
|
-
?
|
|
488
|
-
: Array.isArray(
|
|
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 : [];
|
|
489
514
|
issues.push(...violations.filter((item) => typeof item === "string"));
|
|
490
515
|
}
|
|
491
516
|
// ── Phase 2: Milestone Accumulation ──
|
|
@@ -497,7 +522,7 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
497
522
|
for (let i = 0; i < completedEntries.length; i++) {
|
|
498
523
|
const entry = completedEntries[i];
|
|
499
524
|
const rnd = entryRound(entry);
|
|
500
|
-
const lin =
|
|
525
|
+
const lin = entryLineage(entry);
|
|
501
526
|
// Signal 1: Agent-declared checkpoint
|
|
502
527
|
const isAgentCheckpoint = lin.compression_checkpoint === true;
|
|
503
528
|
// Signal 2: New criteria met (compare with previous vault entry)
|
|
@@ -521,10 +546,17 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
521
546
|
lastMilestoneRound = rnd;
|
|
522
547
|
}
|
|
523
548
|
}
|
|
524
|
-
// Signal 3: Safety net — auto milestone when gap exceeds milestone_interval
|
|
549
|
+
// Signal 3: Safety net — auto milestone when gap exceeds milestone_interval.
|
|
550
|
+
// v3.3.1: the policy contract (milestone_interval: "only fires when no
|
|
551
|
+
// agent_declared or criteria_milestone has been created for this many
|
|
552
|
+
// rounds") includes loops that NEVER created a milestone — but the guard
|
|
553
|
+
// `lastMilestoneRound > 0` made the gap start from the (nonexistent) first
|
|
554
|
+
// milestone, so a checkpoint-less loop could run forever with an empty
|
|
555
|
+
// Phase History and no memory skeleton. With lastMilestoneRound == 0 the
|
|
556
|
+
// gap is measured from round 1 and the safety net fires from the very
|
|
557
|
+
// first interval.
|
|
525
558
|
const gapSinceLast = (currentRound - 1) - lastMilestoneRound;
|
|
526
|
-
if (
|
|
527
|
-
gapSinceLast >= policy.milestone_interval &&
|
|
559
|
+
if (gapSinceLast >= policy.milestone_interval &&
|
|
528
560
|
completedEntries.length > 0) {
|
|
529
561
|
const startRound = lastMilestoneRound + 1;
|
|
530
562
|
const endRound = currentRound - 1;
|
|
@@ -540,23 +572,6 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
540
572
|
// L2 (full rehydration) keeps every milestone — the recovery view is the
|
|
541
573
|
// loop's memory skeleton and must not be truncated.
|
|
542
574
|
const cappedMilestones = sampleMilestones(milestones, policy, level);
|
|
543
|
-
// ── Phase 3: Loop Synthesis (formulaic) ──
|
|
544
|
-
let loopSynthesis = "";
|
|
545
|
-
if (policy.enable_loop_synthesis && currentRound > 1) {
|
|
546
|
-
const totalRounds = currentRound - 1;
|
|
547
|
-
const phaseCount = cappedMilestones.length;
|
|
548
|
-
const lastProgress = completedEntries.length > 0
|
|
549
|
-
? entryProgressEstimate(completedEntries[completedEntries.length - 1])
|
|
550
|
-
: 0;
|
|
551
|
-
const activeCount = completedEntries.length > 0
|
|
552
|
-
? entryActiveConstraints(completedEntries[completedEntries.length - 1]).length
|
|
553
|
-
: 0;
|
|
554
|
-
const totalResolved = cappedMilestones.reduce((sum, m) => sum + m.resolved_constraints.length, 0);
|
|
555
|
-
loopSynthesis =
|
|
556
|
-
`Loop spans ${totalRounds} rounds across ${phaseCount} phases. ` +
|
|
557
|
-
`Overall progress: ${(lastProgress * 100).toFixed(0)}%. ` +
|
|
558
|
-
`${activeCount} active constraints, ${totalResolved} resolved across all phases.`;
|
|
559
|
-
}
|
|
560
575
|
if (windowEntries.length === 0 && cappedMilestones.length === 0)
|
|
561
576
|
return null;
|
|
562
577
|
return makeRollingSummary({
|
|
@@ -566,7 +581,6 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
566
581
|
generated_at_round: currentRound,
|
|
567
582
|
failed_patterns: [],
|
|
568
583
|
milestones: cappedMilestones,
|
|
569
|
-
loop_synthesis: loopSynthesis || undefined,
|
|
570
584
|
});
|
|
571
585
|
}
|
|
572
586
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -587,48 +601,13 @@ export function deriveConstraintId(text) {
|
|
|
587
601
|
export function deriveCriterionId(text) {
|
|
588
602
|
return "cr-" + deriveItemId(text);
|
|
589
603
|
}
|
|
590
|
-
/** v2.11: Check whether a user-provided reference looks like a constraint ID.
|
|
591
|
-
* Matches the pattern c-XXXXXXXX where X is a hex digit. */
|
|
592
|
-
export function isConstraintId(ref) {
|
|
593
|
-
return /^c-[a-f0-9]{8}$/.test(ref);
|
|
594
|
-
}
|
|
595
604
|
/** v2.11: Check whether a user-provided reference looks like a criterion ID.
|
|
596
|
-
* Matches the pattern cr-XXXXXXXX where X is a hex digit.
|
|
597
|
-
|
|
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) {
|
|
598
609
|
return /^cr-[a-f0-9]{8}$/.test(ref);
|
|
599
610
|
}
|
|
600
|
-
/** Read emerged_subtasks from a vault entry (handles direct + lineage nesting). */
|
|
601
|
-
function entryEmergedSubtasks(entry) {
|
|
602
|
-
const direct = entry.emerged_subtasks;
|
|
603
|
-
if (Array.isArray(direct))
|
|
604
|
-
return direct.filter((v) => typeof v === "string");
|
|
605
|
-
const lin = lineage(entry);
|
|
606
|
-
const nested = lin.emerged_subtasks;
|
|
607
|
-
if (Array.isArray(nested))
|
|
608
|
-
return nested.filter((v) => typeof v === "string");
|
|
609
|
-
return [];
|
|
610
|
-
}
|
|
611
|
-
/** Read completed_subtasks from an entry. */
|
|
612
|
-
function entryCompletedSubtasks(entry) {
|
|
613
|
-
const direct = entry.completed_subtasks;
|
|
614
|
-
if (Array.isArray(direct))
|
|
615
|
-
return direct.filter((v) => typeof v === "string");
|
|
616
|
-
return [];
|
|
617
|
-
}
|
|
618
|
-
/** Read blocked_subtasks from an entry. */
|
|
619
|
-
function entryBlockedSubtasks(entry) {
|
|
620
|
-
const direct = entry.blocked_subtasks;
|
|
621
|
-
if (Array.isArray(direct))
|
|
622
|
-
return direct.filter((v) => typeof v === "string");
|
|
623
|
-
return [];
|
|
624
|
-
}
|
|
625
|
-
/** Read canceled_subtasks from an entry. */
|
|
626
|
-
function entryCanceledSubtasks(entry) {
|
|
627
|
-
const direct = entry.canceled_subtasks;
|
|
628
|
-
if (Array.isArray(direct))
|
|
629
|
-
return direct.filter((v) => typeof v === "string");
|
|
630
|
-
return [];
|
|
631
|
-
}
|
|
632
611
|
/**
|
|
633
612
|
* Match a user-provided sub-task reference against existing SubGoals.
|
|
634
613
|
*
|
|
@@ -736,35 +715,10 @@ function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
|
|
|
736
715
|
subGoals[idx].status_changed_at_round = rnd;
|
|
737
716
|
}
|
|
738
717
|
}
|
|
739
|
-
//
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
if (sg.status === "pending") {
|
|
744
|
-
if (jaccardSimilarity(nextAction, sg.description) >= policy.subgoal_auto_in_progress_threshold) {
|
|
745
|
-
sg.status = "in_progress";
|
|
746
|
-
sg.status_changed_at_round = rnd;
|
|
747
|
-
break; // Only one sub-goal can be in_progress at a time
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
// Phase 4 — Auto-complete: new criteria match → done
|
|
753
|
-
const newCriteria = lastRoundResult.execution_evidence?.success_criteria_met ?? [];
|
|
754
|
-
if (newCriteria.length > 0) {
|
|
755
|
-
for (const sg of subGoals) {
|
|
756
|
-
if (sg.status === "pending" || sg.status === "in_progress") {
|
|
757
|
-
for (const crit of newCriteria) {
|
|
758
|
-
if (jaccardSimilarity(crit, sg.description) >= policy.subgoal_auto_complete_threshold) {
|
|
759
|
-
sg.status = "done";
|
|
760
|
-
sg.status_changed_at_round = rnd;
|
|
761
|
-
sg.completed_at_round = rnd;
|
|
762
|
-
break;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
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.
|
|
768
722
|
}
|
|
769
723
|
// Sort for display: in_progress first, then pending by age desc, then blocked, then done, then canceled
|
|
770
724
|
subGoals.sort((a, b) => {
|
|
@@ -805,37 +759,6 @@ function constraintSource(text, objective, constraintsFromPlan) {
|
|
|
805
759
|
return "criteria";
|
|
806
760
|
return "discovered";
|
|
807
761
|
}
|
|
808
|
-
/** Find the earliest round a constraint was discovered by scanning vault entries.
|
|
809
|
-
* v2.11: ID-first matching. Checks exact constraint ID match (c-XXXXXXXX)
|
|
810
|
-
* against the derived ID of each vault entry's discovered_constraints, then
|
|
811
|
-
* falls back to Jaccard similarity on text. */
|
|
812
|
-
function findDiscoveredRound(text, vaultEntries) {
|
|
813
|
-
const targetId = deriveConstraintId(text);
|
|
814
|
-
let earliest = 0;
|
|
815
|
-
for (const entry of vaultEntries) {
|
|
816
|
-
const rnd = entryRound(entry);
|
|
817
|
-
if (rnd < 1)
|
|
818
|
-
continue;
|
|
819
|
-
const discovered = Array.isArray(entry.discovered_constraints)
|
|
820
|
-
? entry.discovered_constraints.filter((v) => typeof v === "string")
|
|
821
|
-
: [];
|
|
822
|
-
for (const d of discovered) {
|
|
823
|
-
// Phase 1: exact ID match (v2.11)
|
|
824
|
-
if (d === targetId || deriveConstraintId(d) === targetId) {
|
|
825
|
-
if (earliest === 0 || rnd < earliest)
|
|
826
|
-
earliest = rnd;
|
|
827
|
-
break;
|
|
828
|
-
}
|
|
829
|
-
// Phase 2: Jaccard fallback (backward compatibility)
|
|
830
|
-
if (jaccardSimilarity(text, d) >= getPolicy().evolution.constraint_match_threshold) {
|
|
831
|
-
if (earliest === 0 || rnd < earliest)
|
|
832
|
-
earliest = rnd;
|
|
833
|
-
break;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
return earliest;
|
|
838
|
-
}
|
|
839
762
|
/** Find the latest round a constraint was violated by scanning vault entries.
|
|
840
763
|
* v2.11: ID-first matching. Checks exact constraint ID match (c-XXXXXXXX)
|
|
841
764
|
* against the derived ID of each vault entry's constraint_violations, then
|
|
@@ -856,9 +779,12 @@ function findLastViolatedRound(text, vaultEntries) {
|
|
|
856
779
|
const rnd = entryRound(entry);
|
|
857
780
|
if (rnd < 1)
|
|
858
781
|
continue;
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
782
|
+
const view = decodeRound(entry);
|
|
783
|
+
const violations = view
|
|
784
|
+
? view.constraintViolations ?? []
|
|
785
|
+
: Array.isArray(entry.constraint_violations)
|
|
786
|
+
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
787
|
+
: [];
|
|
862
788
|
for (const v of violations) {
|
|
863
789
|
if (matchesConstraintText(text, v)) {
|
|
864
790
|
if (rnd > latest)
|
|
@@ -870,15 +796,17 @@ function findLastViolatedRound(text, vaultEntries) {
|
|
|
870
796
|
return latest;
|
|
871
797
|
}
|
|
872
798
|
/**
|
|
873
|
-
* Manage constraint lifecycle
|
|
799
|
+
* Manage constraint lifecycle metadata for the active set.
|
|
874
800
|
*
|
|
875
|
-
*
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
801
|
+
* v3.7: the v2.3 time-aware decay (discovered constraints demoted to
|
|
802
|
+
* inactive after N quiet rounds, auto-reactivation on a fresh violation)
|
|
803
|
+
* is gone — every active constraint stays active. What remains is the
|
|
804
|
+
* display metadata the v3 prompt surface needs: per-text source
|
|
805
|
+
* classification (v3.3.1: criteria texts carry the cr-XXXXXXXX namespace),
|
|
806
|
+
* and last_violated_at_round for the L1 collapse "(violated this round)"
|
|
807
|
+
* annotation (diffConstraints compares it to the current round).
|
|
879
808
|
*/
|
|
880
809
|
function manageConstraintLifecycle(active, objective, constraintsFromPlan, vaultContext, currentRound, lastRoundViolations = []) {
|
|
881
|
-
const policy = getPolicy().evolution;
|
|
882
810
|
const allEntries = loopEntries(objective.loop_id || "", vaultContext);
|
|
883
811
|
const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
|
|
884
812
|
const metadata = [];
|
|
@@ -888,50 +816,25 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
888
816
|
// as a violation at currentRound — the previous rounds' scan cannot see
|
|
889
817
|
// it (this round is not committed yet). This makes the L1 collapse
|
|
890
818
|
// render "(violated this round)" (diffConstraints compares
|
|
891
|
-
// last_violated_at_round === round)
|
|
892
|
-
// discovered constraints.
|
|
819
|
+
// last_violated_at_round === round).
|
|
893
820
|
const violatedThisRound = lastRoundViolations.some((v) => matchesConstraintText(text, v));
|
|
894
|
-
//
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
? currentRound
|
|
902
|
-
: findLastViolatedRound(text, completedEntries),
|
|
903
|
-
source,
|
|
904
|
-
status: "active",
|
|
905
|
-
}));
|
|
906
|
-
continue;
|
|
907
|
-
}
|
|
908
|
-
// Discovered constraints: check for inactivity
|
|
909
|
-
const discoveredAt = findDiscoveredRound(text, completedEntries);
|
|
910
|
-
const lastViolated = violatedThisRound
|
|
911
|
-
? currentRound
|
|
912
|
-
: findLastViolatedRound(text, completedEntries);
|
|
913
|
-
const roundsSinceRelevant = lastViolated > 0
|
|
914
|
-
? currentRound - lastViolated
|
|
915
|
-
: currentRound - (discoveredAt || currentRound);
|
|
916
|
-
const status = roundsSinceRelevant > policy.constraint_inactive_rounds
|
|
917
|
-
? "inactive"
|
|
918
|
-
: "active";
|
|
821
|
+
// v3.3.1: one text, one identity — a success-criterion text that also
|
|
822
|
+
// lives in the merged active set belongs to the cr-XXXXXXXX namespace
|
|
823
|
+
// (its own Success Criteria section and the verification gate's
|
|
824
|
+
// criteriaMatch are ID-first on cr-). Rendering it under c-XXXXXXXX in
|
|
825
|
+
// the active list meant the same text carried two IDs in one prompt,
|
|
826
|
+
// and an agent echoing the active-list ID into success_criteria_met
|
|
827
|
+
// could never match.
|
|
919
828
|
metadata.push(makeConstraintMeta({
|
|
920
|
-
id: deriveConstraintId(text),
|
|
829
|
+
id: source === "criteria" ? deriveCriterionId(text) : deriveConstraintId(text),
|
|
921
830
|
text,
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
831
|
+
last_violated_at_round: violatedThisRound
|
|
832
|
+
? currentRound
|
|
833
|
+
: findLastViolatedRound(text, completedEntries),
|
|
834
|
+
source,
|
|
926
835
|
}));
|
|
927
836
|
}
|
|
928
|
-
|
|
929
|
-
.filter((m) => m.status === "active")
|
|
930
|
-
.map((m) => m.text);
|
|
931
|
-
const inactive = metadata
|
|
932
|
-
.filter((m) => m.status === "inactive")
|
|
933
|
-
.map((m) => m.text);
|
|
934
|
-
return { active: newActive, inactive, metadata };
|
|
837
|
+
return { active, metadata };
|
|
935
838
|
}
|
|
936
839
|
export function alignTask(proposedTask, request, context) {
|
|
937
840
|
const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
|
|
@@ -976,7 +879,7 @@ function levelDecision(request, context) {
|
|
|
976
879
|
last?.revised_success_criteria?.length ||
|
|
977
880
|
last?.wrong_assumptions?.length);
|
|
978
881
|
const lastFullRound = loopEntries(request.loop_id, context)
|
|
979
|
-
.filter((entry) =>
|
|
882
|
+
.filter((entry) => entryLineage(entry).recompile_level === "l2")
|
|
980
883
|
.map(entryRound)
|
|
981
884
|
.filter((round) => round < request.round)
|
|
982
885
|
.at(-1) ?? 1;
|
|
@@ -985,7 +888,7 @@ function levelDecision(request, context) {
|
|
|
985
888
|
// rehydration (the decidePromptLevel recovery_boundary branch existed
|
|
986
889
|
// but no caller ever set it).
|
|
987
890
|
const recoveryBoundary = loopEntries(request.loop_id, context)
|
|
988
|
-
.some((entry) =>
|
|
891
|
+
.some((entry) => entryLineage(entry).committed_action === "backtrack" &&
|
|
989
892
|
entryRound(entry) === request.round);
|
|
990
893
|
return decidePromptLevel({
|
|
991
894
|
round: request.round,
|
|
@@ -1011,11 +914,24 @@ export function buildSelfEvalBlock(round, prevDriftFlags,
|
|
|
1011
914
|
/** v2.12: L0 is the minimal retry template — the v2.12 declarative fields
|
|
1012
915
|
* (outcome/blocker/retroactiveClaims) are L1/L2 additions so the retry
|
|
1013
916
|
* prompt stays within its tight budget. */
|
|
1014
|
-
level
|
|
917
|
+
level,
|
|
918
|
+
/** v3.3/v3.4: Whether this round's Current Task IS the ACTIVE Round
|
|
919
|
+
* Contract (derived from committed rounds — compileLoop passes
|
|
920
|
+
* `activeContract != null`). Only then does the template ask the agent
|
|
921
|
+
* to restate/propose it — a generic empty contract template would invite
|
|
922
|
+
* 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) {
|
|
1015
930
|
const hasIntentDrift = prevDriftFlags?.some(f => f.check === "intent_drift") ?? false;
|
|
1016
931
|
const hasSubGoalDrift = prevDriftFlags?.some(f => f.check === "subgoal_drift") ?? false;
|
|
1017
932
|
const needsClarification = hasIntentDrift || hasSubGoalDrift;
|
|
1018
933
|
const declareOutcome = level !== "l0";
|
|
934
|
+
const restateContract = hasContract && declareOutcome;
|
|
1019
935
|
const evalObj = {
|
|
1020
936
|
success: false,
|
|
1021
937
|
output_summary: `<verified result of round ${round}>`,
|
|
@@ -1042,6 +958,20 @@ level) {
|
|
|
1042
958
|
evalObj.blocker = "<required only when outcome=blocked>";
|
|
1043
959
|
evalObj.retroactiveClaims = [];
|
|
1044
960
|
}
|
|
961
|
+
// v3.3/v3.4: Round Contract proposal template (L1/L2, active-contract
|
|
962
|
+
// rounds only — L0 retry stays lean, and rounds without an active
|
|
963
|
+
// contract never see the template). When the prompt's Current Task IS the
|
|
964
|
+
// ACTIVE contract, the agent restates it unchanged as its proposal; when
|
|
965
|
+
// the active contract's done_when are all satisfied (or the round is
|
|
966
|
+
// blocked), a NEW contract is declared for the next round instead.
|
|
967
|
+
if (restateContract) {
|
|
968
|
+
evalObj.round_contract = {
|
|
969
|
+
work_item: "<this round's focus, or empty>",
|
|
970
|
+
done_when: [],
|
|
971
|
+
verification_plan: [],
|
|
972
|
+
scope: [],
|
|
973
|
+
};
|
|
974
|
+
}
|
|
1045
975
|
// v2.8: Inject drift clarification field when previous round had drift flags
|
|
1046
976
|
if (needsClarification) {
|
|
1047
977
|
const flagTypes = [
|
|
@@ -1057,19 +987,27 @@ level) {
|
|
|
1057
987
|
"### LoopForge Evaluation (Required)",
|
|
1058
988
|
"",
|
|
1059
989
|
`After completing Round ${round}, replace the placeholder values below`,
|
|
1060
|
-
"with your actual results and
|
|
990
|
+
"with your actual results and pass this object as `loopforge_next.evaluation`.",
|
|
1061
991
|
];
|
|
1062
992
|
if (needsClarification) {
|
|
1063
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.");
|
|
1064
994
|
}
|
|
1065
|
-
lines.push("
|
|
995
|
+
lines.push("```json");
|
|
1066
996
|
lines.push(JSON.stringify(evalObj, null, 2));
|
|
1067
|
-
lines.push("
|
|
997
|
+
lines.push("```");
|
|
1068
998
|
lines.push("");
|
|
1069
999
|
lines.push("IMPORTANT: Replace every <placeholder> with your actual data.");
|
|
1070
1000
|
lines.push("Set success=true only when the full goal and ALL hard constraints are verified.");
|
|
1071
1001
|
lines.push("Use IDs for exact matching: c-XXXXXXXX, cr-XXXXXXXX, sg-XXXXXXXX.");
|
|
1072
1002
|
lines.push("For subtasks, use sub-goal IDs from the Sub-Goal Dashboard above.");
|
|
1003
|
+
// v3.5: L2 contract-less nudge (prose only — never the JSON key name, so
|
|
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
|
+
}
|
|
1008
|
+
if (restateContract) {
|
|
1009
|
+
lines.push("Round Contract: restate the Current Task's contract UNCHANGED in", "`round_contract` (work_item/done_when/verification_plan/scope) and run", "its Verify commands; list satisfied done_when items in", "`success_criteria_met`. When the current contract's done_when items are", "all satisfied, 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
|
+
}
|
|
1073
1011
|
return lines.join("\n");
|
|
1074
1012
|
}
|
|
1075
1013
|
// v2.5: checkpoint() removed — agent_declared milestones from
|
|
@@ -1104,15 +1042,22 @@ export function compileLoop(request, context) {
|
|
|
1104
1042
|
// No new persistence; reconstructed same as milestones/sub-goals.
|
|
1105
1043
|
const completedEntries = loopEntries(request.loop_id, context)
|
|
1106
1044
|
.filter((e) => entryRound(e) >= 1 && entryRound(e) < request.round);
|
|
1045
|
+
const committedRounds = mergedRoundsFromEntries(completedEntries, request.round);
|
|
1107
1046
|
const trend = [];
|
|
1108
|
-
for (const
|
|
1109
|
-
const viols =
|
|
1110
|
-
? entry.constraint_violations.filter((v) => typeof v === "string")
|
|
1111
|
-
: [];
|
|
1047
|
+
for (const round of committedRounds.slice(-10)) {
|
|
1048
|
+
const viols = round.evaluation?.constraint_violations ?? [];
|
|
1112
1049
|
// Simple proxy: no violations = perfect trust, each violation = -0.1
|
|
1113
1050
|
const score = Math.max(0, 1.0 - viols.length * 0.1);
|
|
1114
1051
|
trend.push(Number(score.toFixed(2)));
|
|
1115
1052
|
}
|
|
1053
|
+
// v3.3: Round stats + machine git-motion status (display-only, derived).
|
|
1054
|
+
const roundStats = deriveRoundStats(committedRounds);
|
|
1055
|
+
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);
|
|
1116
1061
|
const response = makeLoopCompileResponse({
|
|
1117
1062
|
status: AgentStatus.OK,
|
|
1118
1063
|
recompile_level: decision.level,
|
|
@@ -1120,7 +1065,6 @@ export function compileLoop(request, context) {
|
|
|
1120
1065
|
lineage: [`${request.loop_id}:r${request.round}`],
|
|
1121
1066
|
constraints_active: constraintLifecycle.active,
|
|
1122
1067
|
constraints_retired: constraints.retired,
|
|
1123
|
-
constraints_inactive: constraintLifecycle.inactive,
|
|
1124
1068
|
constraint_metadata: constraintLifecycle.metadata,
|
|
1125
1069
|
loop_id: request.loop_id,
|
|
1126
1070
|
round: request.round,
|
|
@@ -1142,7 +1086,12 @@ export function compileLoop(request, context) {
|
|
|
1142
1086
|
});
|
|
1143
1087
|
const policy = getPolicy();
|
|
1144
1088
|
const statePath = `${policy.state_file.directory}/${request.loop_id}-state.md`;
|
|
1145
|
-
const state = createCanonicalLoopState(request, response, statePath
|
|
1089
|
+
const state = createCanonicalLoopState(request, response, statePath, {
|
|
1090
|
+
roundStats,
|
|
1091
|
+
machineStatus,
|
|
1092
|
+
// v3.4: derived active contract — the Current Task's source of truth.
|
|
1093
|
+
roundContract: activeContract,
|
|
1094
|
+
});
|
|
1146
1095
|
const markdown = renderCanonicalStateMarkdown(state);
|
|
1147
1096
|
// v2.4–v2.5: Adaptive L2 budget — scales with loop complexity
|
|
1148
1097
|
const adaptiveL2 = policy.prompt.l2_adaptive_enabled
|
|
@@ -1159,14 +1108,21 @@ export function compileLoop(request, context) {
|
|
|
1159
1108
|
state,
|
|
1160
1109
|
level: decision.level,
|
|
1161
1110
|
reasons: decision.reasons,
|
|
1162
|
-
mode: policy.prompt.injection_mode,
|
|
1163
1111
|
budgets: {
|
|
1164
1112
|
l0: policy.prompt.l0_max_chars,
|
|
1165
1113
|
l1: policy.prompt.l1_max_chars,
|
|
1166
1114
|
l2: adaptiveL2,
|
|
1167
1115
|
},
|
|
1168
1116
|
attempt: request.attempt,
|
|
1169
|
-
selfEvaluationBlock: buildSelfEvalBlock(request.round, request.verification_flags, decision.level
|
|
1117
|
+
selfEvaluationBlock: buildSelfEvalBlock(request.round, request.verification_flags, decision.level,
|
|
1118
|
+
// v3.4: restate the contract only when this prompt's Current Task IS
|
|
1119
|
+
// the ACTIVE contract (derived — not the previous submission's field,
|
|
1120
|
+
// 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),
|
|
1170
1126
|
fullStateMarkdown: l2Pointer ? undefined : markdown,
|
|
1171
1127
|
// v2.9: Model's information needs from the previous round's SelfEvaluation
|
|
1172
1128
|
promptRequests: request.last_round_result?.prompt_requests,
|