loopforge 3.6.0 → 3.7.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 +28 -13
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +3 -8
- package/dist/audit.js.map +1 -1
- package/dist/canonical-state.d.ts +23 -4
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +148 -47
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +8 -16
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-facts.d.ts.map +1 -1
- package/dist/cognitive-facts.js +2 -3
- package/dist/cognitive-facts.js.map +1 -1
- package/dist/cognitive-governance.d.ts +30 -7
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +75 -22
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/committed-round.d.ts +48 -4
- package/dist/committed-round.d.ts.map +1 -1
- package/dist/committed-round.js +177 -10
- package/dist/committed-round.js.map +1 -1
- package/dist/enforcement-gate.d.ts +24 -19
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +216 -146
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +10 -5
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +52 -31
- package/dist/engine.js.map +1 -1
- package/dist/evidence-claims.d.ts.map +1 -1
- package/dist/evidence-claims.js +3 -9
- package/dist/evidence-claims.js.map +1 -1
- package/dist/evidence-provider.d.ts +13 -19
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +65 -100
- package/dist/evidence-provider.js.map +1 -1
- package/dist/generate-schema.js +104 -43
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +18 -10
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +205 -343
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +11 -16
- 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 +18 -3
- package/dist/mcp/round-lifecycle.d.ts.map +1 -1
- package/dist/mcp/round-lifecycle.js +162 -35
- package/dist/mcp/round-lifecycle.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +12 -3
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +35 -4
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +169 -52
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +170 -39
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy.d.ts +10 -13
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +3 -6
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +6 -4
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +46 -100
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/protocol.d.ts +52 -51
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +5 -28
- package/dist/protocol.js.map +1 -1
- package/dist/round-coordinator.d.ts +19 -0
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +63 -4
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +4 -3
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +3 -10
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +4 -0
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +9 -0
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +41 -3
- package/dist/self-eval.d.ts.map +1 -1
- package/dist/self-eval.js +125 -13
- 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 +32 -24
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +262 -178
- 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/loop_policy.json +19 -7
- package/package.json +6 -6
- package/skills/perception/SKILL.md +56 -16
package/dist/loop-compiler.js
CHANGED
|
@@ -7,7 +7,7 @@ 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 {
|
|
10
|
+
import { decodeRound, entryLineage, entryCriteriaMet, entryCriteriaRemaining, entryActiveConstraints, entryRetractedConstraints, entryProgressEstimate, entryEmergedSubtasks, entrySubGoalUpdates, machineGitMotionSeries, mergedRoundsFromEntries, } from "./committed-round.js";
|
|
11
11
|
import { contractRoundEvaluations, deriveActiveRoundContract, } from "./round-contract.js";
|
|
12
12
|
import { assemblePromptArtifact } from "./prompt-assembler.js";
|
|
13
13
|
import { decidePromptLevel, } from "./prompt-policy.js";
|
|
@@ -17,15 +17,9 @@ function contextEntries(context) {
|
|
|
17
17
|
return [];
|
|
18
18
|
return context.results.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
|
|
19
19
|
}
|
|
20
|
-
function lineage(entry) {
|
|
21
|
-
const value = entry.loop_lineage ?? entry.lineage;
|
|
22
|
-
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
23
|
-
? value
|
|
24
|
-
: {};
|
|
25
|
-
}
|
|
26
20
|
function loopEntries(loopId, context) {
|
|
27
21
|
return contextEntries(context)
|
|
28
|
-
.filter((entry) => entry.loop_id === loopId ||
|
|
22
|
+
.filter((entry) => entry.loop_id === loopId || entryLineage(entry).loop_id === loopId)
|
|
29
23
|
.sort((a, b) => entryRound(a) - entryRound(b));
|
|
30
24
|
}
|
|
31
25
|
/** v3.3.1: The canonical entry for a round — its compile-time lineage entry
|
|
@@ -51,12 +45,6 @@ function roundCanonicalEntry(entries, round) {
|
|
|
51
45
|
}) ??
|
|
52
46
|
null);
|
|
53
47
|
}
|
|
54
|
-
/** Return the canonical read model when an entry is a committed round.
|
|
55
|
-
* Uncommitted compiler lineage has no durable decision yet and deliberately
|
|
56
|
-
* remains on the local compile path; it cannot be treated as history. */
|
|
57
|
-
function committedView(entry) {
|
|
58
|
-
return decodeMergedRound(entry) ?? decodeCommittedRound(entry);
|
|
59
|
-
}
|
|
60
48
|
export function computeGoalTextHash(text) {
|
|
61
49
|
const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
|
|
62
50
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
|
|
@@ -75,7 +63,7 @@ export function readPresentedBaseline(loopId, round, context) {
|
|
|
75
63
|
const entry = roundCanonicalEntry(loopEntries(loopId, context), round - 1);
|
|
76
64
|
if (!entry)
|
|
77
65
|
return null;
|
|
78
|
-
const data =
|
|
66
|
+
const data = entryLineage(entry);
|
|
79
67
|
const constraintIds = data.presented_constraint_ids;
|
|
80
68
|
const subGoals = data.presented_subgoals;
|
|
81
69
|
const milestoneRanges = data.presented_milestone_ranges;
|
|
@@ -97,7 +85,7 @@ export function getPreviousRound(loopId, round, context) {
|
|
|
97
85
|
const entry = roundCanonicalEntry(loopEntries(loopId, context), round);
|
|
98
86
|
if (!entry)
|
|
99
87
|
return null;
|
|
100
|
-
const data =
|
|
88
|
+
const data = entryLineage(entry);
|
|
101
89
|
return {
|
|
102
90
|
round,
|
|
103
91
|
goal_id: typeof data.goal_id === "string" ? data.goal_id : "",
|
|
@@ -156,9 +144,16 @@ function evolveObjective(request, context) {
|
|
|
156
144
|
let version = current.version ?? 1;
|
|
157
145
|
if (last?.objective_refinement?.trim()) {
|
|
158
146
|
const refinement = last.objective_refinement.trim();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
147
|
+
// M5: fold each refinement exactly once per persisted objective state.
|
|
148
|
+
// A retry recompiles the same round against the same committed
|
|
149
|
+
// last_round_result; without this guard every retry attempt re-appends
|
|
150
|
+
// the refinement and inflates the version — the objective text and the
|
|
151
|
+
// state file grew "Refinement: X" once per attempt.
|
|
152
|
+
if (!history.includes(refinement)) {
|
|
153
|
+
history.push(refinement);
|
|
154
|
+
objective = `${objective}\nRefinement: ${refinement}`;
|
|
155
|
+
version++;
|
|
156
|
+
}
|
|
162
157
|
}
|
|
163
158
|
const revisions = new Map((last?.revised_success_criteria ?? []).map((item) => [item.old, item.new]));
|
|
164
159
|
return makeLoopObjective({
|
|
@@ -188,7 +183,7 @@ function evolveConstraints(request, objective, previous, context) {
|
|
|
188
183
|
const lastRetractedRound = new Map();
|
|
189
184
|
if (window > 0) {
|
|
190
185
|
for (const entry of loopEntries(request.loop_id, context)) {
|
|
191
|
-
const retractedList =
|
|
186
|
+
const retractedList = entryLineage(entry).retracted_constraints;
|
|
192
187
|
if (!Array.isArray(retractedList))
|
|
193
188
|
continue;
|
|
194
189
|
const rnd = entryRound(entry);
|
|
@@ -221,47 +216,6 @@ function evolveConstraints(request, objective, previous, context) {
|
|
|
221
216
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
222
217
|
// Hierarchical Summary (v2.1)
|
|
223
218
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
224
|
-
/** Extract execution_evidence from a vault entry, handling both direct-field
|
|
225
|
-
* and lineage-nested shapes. Returns null when absent. */
|
|
226
|
-
function entryExecutionEvidence(entry) {
|
|
227
|
-
const view = committedView(entry);
|
|
228
|
-
if (view)
|
|
229
|
-
return view.executionEvidence;
|
|
230
|
-
const direct = entry.execution_evidence;
|
|
231
|
-
if (direct && typeof direct === "object" && !Array.isArray(direct)) {
|
|
232
|
-
return direct;
|
|
233
|
-
}
|
|
234
|
-
const lin = lineage(entry);
|
|
235
|
-
const nested = lin.execution_evidence;
|
|
236
|
-
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
237
|
-
return nested;
|
|
238
|
-
}
|
|
239
|
-
return null;
|
|
240
|
-
}
|
|
241
|
-
/** Read success_criteria_met from an entry's execution_evidence. */
|
|
242
|
-
function entryCriteriaMet(entry) {
|
|
243
|
-
const view = committedView(entry);
|
|
244
|
-
if (view)
|
|
245
|
-
return view.executionEvidence?.success_criteria_met
|
|
246
|
-
?.filter((value) => typeof value === "string") ?? [];
|
|
247
|
-
const ev = entryExecutionEvidence(entry);
|
|
248
|
-
if (!ev)
|
|
249
|
-
return [];
|
|
250
|
-
const arr = ev.success_criteria_met;
|
|
251
|
-
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
252
|
-
}
|
|
253
|
-
/** Read success_criteria_remaining from an entry's execution_evidence. */
|
|
254
|
-
function entryCriteriaRemaining(entry) {
|
|
255
|
-
const view = committedView(entry);
|
|
256
|
-
if (view)
|
|
257
|
-
return view.executionEvidence?.success_criteria_remaining
|
|
258
|
-
?.filter((value) => typeof value === "string") ?? [];
|
|
259
|
-
const ev = entryExecutionEvidence(entry);
|
|
260
|
-
if (!ev)
|
|
261
|
-
return [];
|
|
262
|
-
const arr = ev.success_criteria_remaining;
|
|
263
|
-
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
264
|
-
}
|
|
265
219
|
/** v3.2: Deterministic lessons learned — constraints violated repeatedly or
|
|
266
220
|
* verification checks failing repeatedly across rounds (full history, unlike
|
|
267
221
|
* the enforcement gate's R2 3-round window). Presentation only: the output
|
|
@@ -273,7 +227,7 @@ export function deriveLessons(loopId, context, currentRound) {
|
|
|
273
227
|
const warns = new Map();
|
|
274
228
|
for (const entry of entries) {
|
|
275
229
|
const rnd = entryRound(entry);
|
|
276
|
-
const view =
|
|
230
|
+
const view = decodeRound(entry);
|
|
277
231
|
const viols = view
|
|
278
232
|
? view.constraintViolations ?? []
|
|
279
233
|
: Array.isArray(entry.constraint_violations)
|
|
@@ -454,41 +408,10 @@ function detectNewCriteria(current, previous) {
|
|
|
454
408
|
return currMet;
|
|
455
409
|
return currMet.filter((curr) => prevMet.every((prev) => !criteriaMatch(curr, prev)));
|
|
456
410
|
}
|
|
457
|
-
/** Read constraints_active from an entry's lineage. */
|
|
458
|
-
function entryActiveConstraints(entry) {
|
|
459
|
-
const view = committedView(entry);
|
|
460
|
-
if (view)
|
|
461
|
-
return view.activeConstraints ?? [];
|
|
462
|
-
const lin = lineage(entry);
|
|
463
|
-
const arr = lin.constraints_active;
|
|
464
|
-
return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
|
|
465
|
-
}
|
|
466
|
-
/** Read retracted_constraints from an entry. */
|
|
467
|
-
function entryRetractedConstraints(entry) {
|
|
468
|
-
const view = committedView(entry);
|
|
469
|
-
if (view)
|
|
470
|
-
return view.retractedConstraints ?? [];
|
|
471
|
-
const direct = entry.retracted_constraints;
|
|
472
|
-
if (Array.isArray(direct))
|
|
473
|
-
return direct.filter((v) => typeof v === "string");
|
|
474
|
-
const lin = lineage(entry);
|
|
475
|
-
const nested = lin.retracted_constraints;
|
|
476
|
-
if (Array.isArray(nested))
|
|
477
|
-
return nested.filter((v) => typeof v === "string");
|
|
478
|
-
return [];
|
|
479
|
-
}
|
|
480
|
-
/** Read progress_estimate from an entry's execution_evidence. */
|
|
481
|
-
function entryProgressEstimate(entry) {
|
|
482
|
-
const ev = entryExecutionEvidence(entry);
|
|
483
|
-
if (!ev)
|
|
484
|
-
return 0;
|
|
485
|
-
const pe = ev.progress_estimate;
|
|
486
|
-
return typeof pe === "number" ? pe : 0;
|
|
487
|
-
}
|
|
488
411
|
/** Build a single MilestoneSummary from a range of completed round entries. */
|
|
489
412
|
function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, kind) {
|
|
490
413
|
const last = phaseEntries.at(-1);
|
|
491
|
-
const lastView = last ?
|
|
414
|
+
const lastView = last ? decodeRound(last) : null;
|
|
492
415
|
const outcomeRaw = lastView
|
|
493
416
|
? (lastView.outputSummary ?? "")
|
|
494
417
|
: last && typeof last.output_summary === "string" ? last.output_summary : "";
|
|
@@ -572,8 +495,8 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
572
495
|
const outcomes = [];
|
|
573
496
|
const issues = [];
|
|
574
497
|
for (const entry of windowEntries) {
|
|
575
|
-
const view =
|
|
576
|
-
const data =
|
|
498
|
+
const view = decodeRound(entry);
|
|
499
|
+
const data = entryLineage(entry);
|
|
577
500
|
const round = entryRound(entry);
|
|
578
501
|
const success = view ? view.success : entry.success ?? data.success;
|
|
579
502
|
const summary = view
|
|
@@ -606,7 +529,7 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
|
|
|
606
529
|
for (let i = 0; i < completedEntries.length; i++) {
|
|
607
530
|
const entry = completedEntries[i];
|
|
608
531
|
const rnd = entryRound(entry);
|
|
609
|
-
const lin =
|
|
532
|
+
const lin = entryLineage(entry);
|
|
610
533
|
// Signal 1: Agent-declared checkpoint
|
|
611
534
|
const isAgentCheckpoint = lin.compression_checkpoint === true;
|
|
612
535
|
// Signal 2: New criteria met (compare with previous vault entry)
|
|
@@ -685,66 +608,18 @@ export function deriveConstraintId(text) {
|
|
|
685
608
|
export function deriveCriterionId(text) {
|
|
686
609
|
return "cr-" + deriveItemId(text);
|
|
687
610
|
}
|
|
688
|
-
/** v2.11: Check whether a user-provided reference looks like a constraint ID.
|
|
689
|
-
* Matches the pattern c-XXXXXXXX where X is a hex digit. */
|
|
690
|
-
export function isConstraintId(ref) {
|
|
691
|
-
return /^c-[a-f0-9]{8}$/.test(ref);
|
|
692
|
-
}
|
|
693
611
|
/** v2.11: Check whether a user-provided reference looks like a criterion ID.
|
|
694
|
-
* Matches the pattern cr-XXXXXXXX where X is a hex digit.
|
|
695
|
-
|
|
612
|
+
* Matches the pattern cr-XXXXXXXX where X is a hex digit. Module-local:
|
|
613
|
+
* consumers with the same need (round-contract.ts) keep their own copy to
|
|
614
|
+
* avoid an import cycle. */
|
|
615
|
+
function isCriterionId(ref) {
|
|
696
616
|
return /^cr-[a-f0-9]{8}$/.test(ref);
|
|
697
617
|
}
|
|
698
|
-
/** Read emerged_subtasks from a vault entry (handles direct + lineage nesting). */
|
|
699
|
-
function entryEmergedSubtasks(entry) {
|
|
700
|
-
const view = committedView(entry);
|
|
701
|
-
if (view)
|
|
702
|
-
return view.emergedSubtasks ?? [];
|
|
703
|
-
const direct = entry.emerged_subtasks;
|
|
704
|
-
if (Array.isArray(direct))
|
|
705
|
-
return direct.filter((v) => typeof v === "string");
|
|
706
|
-
const lin = lineage(entry);
|
|
707
|
-
const nested = lin.emerged_subtasks;
|
|
708
|
-
if (Array.isArray(nested))
|
|
709
|
-
return nested.filter((v) => typeof v === "string");
|
|
710
|
-
return [];
|
|
711
|
-
}
|
|
712
|
-
/** Read completed_subtasks from an entry. */
|
|
713
|
-
function entryCompletedSubtasks(entry) {
|
|
714
|
-
const view = committedView(entry);
|
|
715
|
-
if (view)
|
|
716
|
-
return view.completedSubtasks ?? [];
|
|
717
|
-
const direct = entry.completed_subtasks;
|
|
718
|
-
if (Array.isArray(direct))
|
|
719
|
-
return direct.filter((v) => typeof v === "string");
|
|
720
|
-
return [];
|
|
721
|
-
}
|
|
722
|
-
/** Read blocked_subtasks from an entry. */
|
|
723
|
-
function entryBlockedSubtasks(entry) {
|
|
724
|
-
const view = committedView(entry);
|
|
725
|
-
if (view)
|
|
726
|
-
return view.blockedSubtasks ?? [];
|
|
727
|
-
const direct = entry.blocked_subtasks;
|
|
728
|
-
if (Array.isArray(direct))
|
|
729
|
-
return direct.filter((v) => typeof v === "string");
|
|
730
|
-
return [];
|
|
731
|
-
}
|
|
732
|
-
/** Read canceled_subtasks from an entry. */
|
|
733
|
-
function entryCanceledSubtasks(entry) {
|
|
734
|
-
const view = committedView(entry);
|
|
735
|
-
if (view)
|
|
736
|
-
return view.canceledSubtasks ?? [];
|
|
737
|
-
const direct = entry.canceled_subtasks;
|
|
738
|
-
if (Array.isArray(direct))
|
|
739
|
-
return direct.filter((v) => typeof v === "string");
|
|
740
|
-
return [];
|
|
741
|
-
}
|
|
742
618
|
/**
|
|
743
|
-
* Match a user-provided
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
747
|
-
* of the best-matching SubGoal, or -1 when no match reaches threshold.
|
|
619
|
+
* Match a user-provided description against existing SubGoals for DEDUP
|
|
620
|
+
* (emerged_subtasks creation). Exact ID first, then Jaccard similarity on
|
|
621
|
+
* descriptions. v3.7.1: status transitions no longer use this — they are
|
|
622
|
+
* strict ID references validated before the round advances.
|
|
748
623
|
*/
|
|
749
624
|
function matchSubGoal(target, subGoals, threshold) {
|
|
750
625
|
const trimmed = target.trim();
|
|
@@ -765,34 +640,90 @@ function matchSubGoal(target, subGoals, threshold) {
|
|
|
765
640
|
}
|
|
766
641
|
return bestScore >= threshold ? bestIdx : -1;
|
|
767
642
|
}
|
|
643
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
644
|
+
// Sub-goal transition matrix (v3.7.1 — single source)
|
|
645
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
646
|
+
/** Closed migration matrix. done/canceled are terminal (no out-edges); the
|
|
647
|
+
* matrix rejects re-opening. blocked → in_progress is the recovery path. */
|
|
648
|
+
export const SUBGOAL_TRANSITIONS = {
|
|
649
|
+
in_progress: ["pending", "blocked"],
|
|
650
|
+
done: ["pending", "in_progress", "blocked"],
|
|
651
|
+
blocked: ["pending", "in_progress"],
|
|
652
|
+
canceled: ["pending", "in_progress", "blocked"],
|
|
653
|
+
};
|
|
654
|
+
/** Whether a transition is legal. Same-status is a legal no-op (used by
|
|
655
|
+
* replay idempotency). References to terminal sub-goals are rejected by
|
|
656
|
+
* validateSubGoalUpdates before this is consulted. */
|
|
657
|
+
export function canTransitionSubGoal(from, to) {
|
|
658
|
+
if (from === to)
|
|
659
|
+
return true;
|
|
660
|
+
return SUBGOAL_TRANSITIONS[to].includes(from);
|
|
661
|
+
}
|
|
662
|
+
/** Referential validation of a payload's subgoal_updates against the
|
|
663
|
+
* derived sub-goal set. Returns one error per invalid entry:
|
|
664
|
+
* unknown_id | terminal_reference | illegal_transition. */
|
|
665
|
+
export function validateSubGoalUpdates(subGoals, updates) {
|
|
666
|
+
const byId = new Map(subGoals.map((sg) => [sg.id, sg]));
|
|
667
|
+
const errors = [];
|
|
668
|
+
for (const update of updates) {
|
|
669
|
+
const target = byId.get(update.id);
|
|
670
|
+
if (!target) {
|
|
671
|
+
errors.push({ id: update.id, reason: "unknown_id" });
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
if (target.status === "done" || target.status === "canceled") {
|
|
675
|
+
errors.push({ id: update.id, reason: "terminal_reference" });
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (!canTransitionSubGoal(target.status, update.status)) {
|
|
679
|
+
errors.push({ id: update.id, reason: "illegal_transition" });
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return errors;
|
|
683
|
+
}
|
|
684
|
+
/** Apply one transition to the accumulated list. Guards make replay of
|
|
685
|
+
* committed history idempotent: terminal sub-goals never change and
|
|
686
|
+
* same-status updates are no-ops. */
|
|
687
|
+
function applySubGoalUpdate(subGoals, update, rnd) {
|
|
688
|
+
const target = subGoals.find((sg) => sg.id === update.id);
|
|
689
|
+
if (!target)
|
|
690
|
+
return;
|
|
691
|
+
if (target.status === "done" || target.status === "canceled")
|
|
692
|
+
return;
|
|
693
|
+
if (!canTransitionSubGoal(target.status, update.status))
|
|
694
|
+
return;
|
|
695
|
+
if (target.status === update.status)
|
|
696
|
+
return;
|
|
697
|
+
target.status = update.status;
|
|
698
|
+
target.status_changed_at_round = rnd;
|
|
699
|
+
if (update.status === "done")
|
|
700
|
+
target.completed_at_round = rnd;
|
|
701
|
+
}
|
|
768
702
|
/**
|
|
769
703
|
* Manage the full sub-goal lifecycle across rounds.
|
|
770
704
|
*
|
|
771
|
-
*
|
|
772
|
-
* round
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
* 3. success_criteria_met match → auto done
|
|
705
|
+
* v3.7.1 derivation rule: EVERY committed round (round < currentRound) is
|
|
706
|
+
* replayed in round order — emerged_subtasks create pending items, then
|
|
707
|
+
* subgoal_updates apply explicit transitions. The round's own report
|
|
708
|
+
* (last_round_result, which the engine rebuilds from the same committed
|
|
709
|
+
* evaluation) contributes the same data on the fixture path, so applying
|
|
710
|
+
* it again is an idempotent no-op. Older transitions therefore never
|
|
711
|
+
* regress on later compiles, and no status is ever inferred.
|
|
779
712
|
*/
|
|
780
713
|
function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
|
|
781
714
|
const policy = getPolicy().evolution;
|
|
782
715
|
const allEntries = loopEntries(loopId, vaultContext);
|
|
783
|
-
const completedEntries = allEntries
|
|
716
|
+
const completedEntries = allEntries
|
|
717
|
+
.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound)
|
|
718
|
+
.sort((a, b) => entryRound(a) - entryRound(b));
|
|
784
719
|
const subGoals = [];
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
const rnd = entryRound(entry);
|
|
788
|
-
const emerged = entryEmergedSubtasks(entry);
|
|
789
|
-
for (const desc of emerged) {
|
|
790
|
-
const id = deriveSubGoalId(desc);
|
|
720
|
+
const createEmerged = (descs, rnd) => {
|
|
721
|
+
for (const desc of descs) {
|
|
791
722
|
// Dedup: skip if an existing sub-goal is too similar
|
|
792
723
|
if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
|
|
793
724
|
continue;
|
|
794
725
|
subGoals.push(makeSubGoal({
|
|
795
|
-
id,
|
|
726
|
+
id: deriveSubGoalId(desc),
|
|
796
727
|
description: desc.trim(),
|
|
797
728
|
status: "pending",
|
|
798
729
|
declared_at_round: rnd,
|
|
@@ -800,80 +731,23 @@ function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
|
|
|
800
731
|
priority: subGoals.length,
|
|
801
732
|
}));
|
|
802
733
|
}
|
|
734
|
+
};
|
|
735
|
+
// Phase 1 — Replay every committed round in order: create, then apply
|
|
736
|
+
// that round's declared transitions.
|
|
737
|
+
for (const entry of completedEntries) {
|
|
738
|
+
const rnd = entryRound(entry);
|
|
739
|
+
createEmerged(entryEmergedSubtasks(entry), rnd);
|
|
740
|
+
for (const update of entrySubGoalUpdates(entry)) {
|
|
741
|
+
applySubGoalUpdate(subGoals, update, rnd);
|
|
742
|
+
}
|
|
803
743
|
}
|
|
804
|
-
// Phase 2 —
|
|
744
|
+
// Phase 2 — The current round's own report (fixture/rebuild path; on the
|
|
745
|
+
// vault path this duplicates the last committed round and is idempotent).
|
|
805
746
|
if (lastRoundResult) {
|
|
806
747
|
const rnd = lastRoundResult.round || (currentRound - 1);
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
|
|
811
|
-
continue;
|
|
812
|
-
subGoals.push(makeSubGoal({
|
|
813
|
-
id: deriveSubGoalId(desc),
|
|
814
|
-
description: desc.trim(),
|
|
815
|
-
status: "pending",
|
|
816
|
-
declared_at_round: rnd,
|
|
817
|
-
status_changed_at_round: rnd,
|
|
818
|
-
priority: subGoals.length,
|
|
819
|
-
}));
|
|
820
|
-
}
|
|
821
|
-
// Completed sub-goals
|
|
822
|
-
const completed = lastRoundResult.completed_subtasks ?? [];
|
|
823
|
-
for (const desc of completed) {
|
|
824
|
-
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
825
|
-
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
826
|
-
subGoals[idx].status = "done";
|
|
827
|
-
subGoals[idx].status_changed_at_round = rnd;
|
|
828
|
-
subGoals[idx].completed_at_round = rnd;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
// Blocked sub-goals
|
|
832
|
-
const blocked = lastRoundResult.blocked_subtasks ?? [];
|
|
833
|
-
for (const desc of blocked) {
|
|
834
|
-
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
835
|
-
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
836
|
-
subGoals[idx].status = "blocked";
|
|
837
|
-
subGoals[idx].status_changed_at_round = rnd;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
// Canceled sub-goals
|
|
841
|
-
const canceled = lastRoundResult.canceled_subtasks ?? [];
|
|
842
|
-
for (const desc of canceled) {
|
|
843
|
-
const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
|
|
844
|
-
if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
|
|
845
|
-
subGoals[idx].status = "canceled";
|
|
846
|
-
subGoals[idx].status_changed_at_round = rnd;
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
// Phase 3 — Auto-derive: next_action match → in_progress
|
|
850
|
-
const nextAction = lastRoundResult.next_action?.trim();
|
|
851
|
-
if (nextAction) {
|
|
852
|
-
for (const sg of subGoals) {
|
|
853
|
-
if (sg.status === "pending") {
|
|
854
|
-
if (jaccardSimilarity(nextAction, sg.description) >= policy.subgoal_auto_in_progress_threshold) {
|
|
855
|
-
sg.status = "in_progress";
|
|
856
|
-
sg.status_changed_at_round = rnd;
|
|
857
|
-
break; // Only one sub-goal can be in_progress at a time
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
// Phase 4 — Auto-complete: new criteria match → done
|
|
863
|
-
const newCriteria = lastRoundResult.execution_evidence?.success_criteria_met ?? [];
|
|
864
|
-
if (newCriteria.length > 0) {
|
|
865
|
-
for (const sg of subGoals) {
|
|
866
|
-
if (sg.status === "pending" || sg.status === "in_progress") {
|
|
867
|
-
for (const crit of newCriteria) {
|
|
868
|
-
if (jaccardSimilarity(crit, sg.description) >= policy.subgoal_auto_complete_threshold) {
|
|
869
|
-
sg.status = "done";
|
|
870
|
-
sg.status_changed_at_round = rnd;
|
|
871
|
-
sg.completed_at_round = rnd;
|
|
872
|
-
break;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
}
|
|
748
|
+
createEmerged(lastRoundResult.emerged_subtasks ?? [], rnd);
|
|
749
|
+
for (const update of lastRoundResult.subgoal_updates ?? []) {
|
|
750
|
+
applySubGoalUpdate(subGoals, update, rnd);
|
|
877
751
|
}
|
|
878
752
|
}
|
|
879
753
|
// Sort for display: in_progress first, then pending by age desc, then blocked, then done, then canceled
|
|
@@ -915,40 +789,6 @@ function constraintSource(text, objective, constraintsFromPlan) {
|
|
|
915
789
|
return "criteria";
|
|
916
790
|
return "discovered";
|
|
917
791
|
}
|
|
918
|
-
/** Find the earliest round a constraint was discovered by scanning vault entries.
|
|
919
|
-
* v2.11: ID-first matching. Checks exact constraint ID match (c-XXXXXXXX)
|
|
920
|
-
* against the derived ID of each vault entry's discovered_constraints, then
|
|
921
|
-
* falls back to Jaccard similarity on text. */
|
|
922
|
-
function findDiscoveredRound(text, vaultEntries) {
|
|
923
|
-
const targetId = deriveConstraintId(text);
|
|
924
|
-
let earliest = 0;
|
|
925
|
-
for (const entry of vaultEntries) {
|
|
926
|
-
const rnd = entryRound(entry);
|
|
927
|
-
if (rnd < 1)
|
|
928
|
-
continue;
|
|
929
|
-
const view = committedView(entry);
|
|
930
|
-
const discovered = view
|
|
931
|
-
? view.discoveredConstraints ?? []
|
|
932
|
-
: Array.isArray(entry.discovered_constraints)
|
|
933
|
-
? entry.discovered_constraints.filter((v) => typeof v === "string")
|
|
934
|
-
: [];
|
|
935
|
-
for (const d of discovered) {
|
|
936
|
-
// Phase 1: exact ID match (v2.11)
|
|
937
|
-
if (d === targetId || deriveConstraintId(d) === targetId) {
|
|
938
|
-
if (earliest === 0 || rnd < earliest)
|
|
939
|
-
earliest = rnd;
|
|
940
|
-
break;
|
|
941
|
-
}
|
|
942
|
-
// Phase 2: Jaccard fallback for natural-language references.
|
|
943
|
-
if (jaccardSimilarity(text, d) >= getPolicy().evolution.constraint_match_threshold) {
|
|
944
|
-
if (earliest === 0 || rnd < earliest)
|
|
945
|
-
earliest = rnd;
|
|
946
|
-
break;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
return earliest;
|
|
951
|
-
}
|
|
952
792
|
/** Find the latest round a constraint was violated by scanning vault entries.
|
|
953
793
|
* v2.11: ID-first matching. Checks exact constraint ID match (c-XXXXXXXX)
|
|
954
794
|
* against the derived ID of each vault entry's constraint_violations, then
|
|
@@ -969,7 +809,7 @@ function findLastViolatedRound(text, vaultEntries) {
|
|
|
969
809
|
const rnd = entryRound(entry);
|
|
970
810
|
if (rnd < 1)
|
|
971
811
|
continue;
|
|
972
|
-
const view =
|
|
812
|
+
const view = decodeRound(entry);
|
|
973
813
|
const violations = view
|
|
974
814
|
? view.constraintViolations ?? []
|
|
975
815
|
: Array.isArray(entry.constraint_violations)
|
|
@@ -986,15 +826,17 @@ function findLastViolatedRound(text, vaultEntries) {
|
|
|
986
826
|
return latest;
|
|
987
827
|
}
|
|
988
828
|
/**
|
|
989
|
-
* Manage constraint lifecycle
|
|
829
|
+
* Manage constraint lifecycle metadata for the active set.
|
|
990
830
|
*
|
|
991
|
-
*
|
|
992
|
-
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
831
|
+
* v3.7: the v2.3 time-aware decay (discovered constraints demoted to
|
|
832
|
+
* inactive after N quiet rounds, auto-reactivation on a fresh violation)
|
|
833
|
+
* is gone — every active constraint stays active. What remains is the
|
|
834
|
+
* display metadata the v3 prompt surface needs: per-text source
|
|
835
|
+
* classification (v3.3.1: criteria texts carry the cr-XXXXXXXX namespace),
|
|
836
|
+
* and last_violated_at_round for the L1 collapse "(violated this round)"
|
|
837
|
+
* annotation (diffConstraints compares it to the current round).
|
|
995
838
|
*/
|
|
996
839
|
function manageConstraintLifecycle(active, objective, constraintsFromPlan, vaultContext, currentRound, lastRoundViolations = []) {
|
|
997
|
-
const policy = getPolicy().evolution;
|
|
998
840
|
const allEntries = loopEntries(objective.loop_id || "", vaultContext);
|
|
999
841
|
const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
|
|
1000
842
|
const metadata = [];
|
|
@@ -1004,57 +846,25 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
|
|
|
1004
846
|
// as a violation at currentRound — the previous rounds' scan cannot see
|
|
1005
847
|
// it (this round is not committed yet). This makes the L1 collapse
|
|
1006
848
|
// render "(violated this round)" (diffConstraints compares
|
|
1007
|
-
// last_violated_at_round === round)
|
|
1008
|
-
// discovered constraints.
|
|
849
|
+
// last_violated_at_round === round).
|
|
1009
850
|
const violatedThisRound = lastRoundViolations.some((v) => matchesConstraintText(text, v));
|
|
1010
|
-
//
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
// and an agent echoing the active-list ID into success_criteria_met
|
|
1018
|
-
// could never match.
|
|
1019
|
-
metadata.push(makeConstraintMeta({
|
|
1020
|
-
id: source === "criteria" ? deriveCriterionId(text) : deriveConstraintId(text),
|
|
1021
|
-
text,
|
|
1022
|
-
discovered_at_round: objective.created_at_round || 1,
|
|
1023
|
-
last_violated_at_round: violatedThisRound
|
|
1024
|
-
? currentRound
|
|
1025
|
-
: findLastViolatedRound(text, completedEntries),
|
|
1026
|
-
source,
|
|
1027
|
-
status: "active",
|
|
1028
|
-
}));
|
|
1029
|
-
continue;
|
|
1030
|
-
}
|
|
1031
|
-
// Discovered constraints: check for inactivity
|
|
1032
|
-
const discoveredAt = findDiscoveredRound(text, completedEntries);
|
|
1033
|
-
const lastViolated = violatedThisRound
|
|
1034
|
-
? currentRound
|
|
1035
|
-
: findLastViolatedRound(text, completedEntries);
|
|
1036
|
-
const roundsSinceRelevant = lastViolated > 0
|
|
1037
|
-
? currentRound - lastViolated
|
|
1038
|
-
: currentRound - (discoveredAt || currentRound);
|
|
1039
|
-
const status = roundsSinceRelevant > policy.constraint_inactive_rounds
|
|
1040
|
-
? "inactive"
|
|
1041
|
-
: "active";
|
|
851
|
+
// v3.3.1: one text, one identity — a success-criterion text that also
|
|
852
|
+
// lives in the merged active set belongs to the cr-XXXXXXXX namespace
|
|
853
|
+
// (its own Success Criteria section and the verification gate's
|
|
854
|
+
// criteriaMatch are ID-first on cr-). Rendering it under c-XXXXXXXX in
|
|
855
|
+
// the active list meant the same text carried two IDs in one prompt,
|
|
856
|
+
// and an agent echoing the active-list ID into success_criteria_met
|
|
857
|
+
// could never match.
|
|
1042
858
|
metadata.push(makeConstraintMeta({
|
|
1043
|
-
id: deriveConstraintId(text),
|
|
859
|
+
id: source === "criteria" ? deriveCriterionId(text) : deriveConstraintId(text),
|
|
1044
860
|
text,
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
861
|
+
last_violated_at_round: violatedThisRound
|
|
862
|
+
? currentRound
|
|
863
|
+
: findLastViolatedRound(text, completedEntries),
|
|
864
|
+
source,
|
|
1049
865
|
}));
|
|
1050
866
|
}
|
|
1051
|
-
|
|
1052
|
-
.filter((m) => m.status === "active")
|
|
1053
|
-
.map((m) => m.text);
|
|
1054
|
-
const inactive = metadata
|
|
1055
|
-
.filter((m) => m.status === "inactive")
|
|
1056
|
-
.map((m) => m.text);
|
|
1057
|
-
return { active: newActive, inactive, metadata };
|
|
867
|
+
return { active, metadata };
|
|
1058
868
|
}
|
|
1059
869
|
export function alignTask(proposedTask, request, context) {
|
|
1060
870
|
const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
|
|
@@ -1099,7 +909,7 @@ function levelDecision(request, context) {
|
|
|
1099
909
|
last?.revised_success_criteria?.length ||
|
|
1100
910
|
last?.wrong_assumptions?.length);
|
|
1101
911
|
const lastFullRound = loopEntries(request.loop_id, context)
|
|
1102
|
-
.filter((entry) =>
|
|
912
|
+
.filter((entry) => entryLineage(entry).recompile_level === "l2")
|
|
1103
913
|
.map(entryRound)
|
|
1104
914
|
.filter((round) => round < request.round)
|
|
1105
915
|
.at(-1) ?? 1;
|
|
@@ -1108,7 +918,7 @@ function levelDecision(request, context) {
|
|
|
1108
918
|
// rehydration (the decidePromptLevel recovery_boundary branch existed
|
|
1109
919
|
// but no caller ever set it).
|
|
1110
920
|
const recoveryBoundary = loopEntries(request.loop_id, context)
|
|
1111
|
-
.some((entry) =>
|
|
921
|
+
.some((entry) => entryLineage(entry).committed_action === "backtrack" &&
|
|
1112
922
|
entryRound(entry) === request.round);
|
|
1113
923
|
return decidePromptLevel({
|
|
1114
924
|
round: request.round,
|
|
@@ -1130,17 +940,17 @@ function levelDecision(request, context) {
|
|
|
1130
940
|
export function decideLevel(request, context) {
|
|
1131
941
|
return levelDecision(request, context).level;
|
|
1132
942
|
}
|
|
1133
|
-
export function buildSelfEvalBlock(round, prevDriftFlags,
|
|
943
|
+
export function buildSelfEvalBlock(round, prevDriftFlags,
|
|
1134
944
|
/** v2.12: L0 is the minimal retry template — the v2.12 declarative fields
|
|
1135
945
|
* (outcome/blocker/retroactiveClaims) are L1/L2 additions so the retry
|
|
1136
946
|
* prompt stays within its tight budget. */
|
|
1137
|
-
level,
|
|
947
|
+
level,
|
|
1138
948
|
/** v3.3/v3.4: Whether this round's Current Task IS the ACTIVE Round
|
|
1139
949
|
* Contract (derived from committed rounds — compileLoop passes
|
|
1140
950
|
* `activeContract != null`). Only then does the template ask the agent
|
|
1141
951
|
* to restate/propose it — a generic empty contract template would invite
|
|
1142
952
|
* placeholder submissions that trigger round_underspecified noise. */
|
|
1143
|
-
hasContract = false,
|
|
953
|
+
hasContract = false,
|
|
1144
954
|
/** v3.5: L2-only prose suggesting a Round Contract declaration when the
|
|
1145
955
|
* Current Task is NOT one (contract_nudge_on_l2 policy, computed at the
|
|
1146
956
|
* compileLoop call site). Mutually exclusive with hasContract. The prose
|
|
@@ -1168,9 +978,9 @@ proposalNudge = false) {
|
|
|
1168
978
|
},
|
|
1169
979
|
wrong_assumptions: [],
|
|
1170
980
|
next_action: "<next concrete action, or empty when complete>",
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
981
|
+
subgoal_updates: [
|
|
982
|
+
{ id: "<sg-XXXXXXXX from the Sub-Goal Dashboard>", status: "done" },
|
|
983
|
+
],
|
|
1174
984
|
};
|
|
1175
985
|
// v2.12: Declarative tri-state outcome (L1/L2 only — keeps L0 retry lean)
|
|
1176
986
|
if (declareOutcome) {
|
|
@@ -1219,7 +1029,8 @@ proposalNudge = false) {
|
|
|
1219
1029
|
lines.push("IMPORTANT: Replace every <placeholder> with your actual data.");
|
|
1220
1030
|
lines.push("Set success=true only when the full goal and ALL hard constraints are verified.");
|
|
1221
1031
|
lines.push("Use IDs for exact matching: c-XXXXXXXX, cr-XXXXXXXX, sg-XXXXXXXX.");
|
|
1222
|
-
lines.push("
|
|
1032
|
+
lines.push("Sub-goal changes go through `subgoal_updates` (active sub-goal IDs only;");
|
|
1033
|
+
lines.push("done/canceled are terminal — reopen via `emerged_subtasks`; bad IDs reject).");
|
|
1223
1034
|
// v3.5: L2 contract-less nudge (prose only — never the JSON key name, so
|
|
1224
1035
|
// contract-less rounds keep their template-lean assertions).
|
|
1225
1036
|
if (proposalNudge) {
|
|
@@ -1233,6 +1044,43 @@ proposalNudge = false) {
|
|
|
1233
1044
|
// v2.5: checkpoint() removed — agent_declared milestones from
|
|
1234
1045
|
// compression_checkpoint supersede the separate CheckpointSummary.
|
|
1235
1046
|
// See buildRollingSummary() signal 1.
|
|
1047
|
+
/** v3.7.1: Derived Recovery Brief for the state-file Recent tier. Present
|
|
1048
|
+
* ONLY while a committed backtrack decision is the current round's record
|
|
1049
|
+
* (the recovery window); the redo commit replaces that record, so the
|
|
1050
|
+
* brief exits the projection by construction. Sources are the committed
|
|
1051
|
+
* rollback decision's own fields — no new persistence, no rejected
|
|
1052
|
+
* payloads. */
|
|
1053
|
+
function committedBacktrackBrief(loopId, round, context) {
|
|
1054
|
+
const entry = loopEntries(loopId, context).find((e) => entryLineage(e).committed_action === "backtrack" && entryRound(e) === round);
|
|
1055
|
+
if (!entry)
|
|
1056
|
+
return undefined;
|
|
1057
|
+
// Read the facts the engine stamped onto the merged lineage entry from
|
|
1058
|
+
// the committed rollback decision (engine.ts mergeCommittedRound).
|
|
1059
|
+
const lineage = entryLineage(entry);
|
|
1060
|
+
const asStrings = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
1061
|
+
const asNumbers = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "number") : [];
|
|
1062
|
+
const failedRounds = asNumbers(lineage.backtrackFailedRounds);
|
|
1063
|
+
const approaches = asStrings(lineage.backtrackApproaches).slice(0, 6);
|
|
1064
|
+
const assumptions = asStrings(lineage.backtrackWrongAssumptions).slice(0, 5);
|
|
1065
|
+
const target = typeof lineage.backtrackTarget === "number" ? lineage.backtrackTarget : 0;
|
|
1066
|
+
const trigger = typeof lineage.backtrackTriggerRule === "string"
|
|
1067
|
+
? lineage.backtrackTriggerRule
|
|
1068
|
+
: "unknown";
|
|
1069
|
+
const lines = [
|
|
1070
|
+
`- **Trigger**: ${trigger}`,
|
|
1071
|
+
`- **Restored to round**: ${target} (redo round ${target + 1})`,
|
|
1072
|
+
];
|
|
1073
|
+
if (failedRounds.length > 0)
|
|
1074
|
+
lines.push(`- **Failed rounds**: ${failedRounds.join(", ")}`);
|
|
1075
|
+
for (const approach of approaches)
|
|
1076
|
+
lines.push(` - Failed approach: ${approach}`);
|
|
1077
|
+
if (assumptions.length > 0) {
|
|
1078
|
+
lines.push("- **Falsified assumptions** (do not rebuild on these):");
|
|
1079
|
+
for (const assumption of assumptions)
|
|
1080
|
+
lines.push(` - ${assumption}`);
|
|
1081
|
+
}
|
|
1082
|
+
return lines;
|
|
1083
|
+
}
|
|
1236
1084
|
export function compileLoop(request, context) {
|
|
1237
1085
|
const decision = levelDecision(request, context);
|
|
1238
1086
|
const previous = getPreviousRound(request.loop_id, request.round - 1, context);
|
|
@@ -1285,7 +1133,6 @@ export function compileLoop(request, context) {
|
|
|
1285
1133
|
lineage: [`${request.loop_id}:r${request.round}`],
|
|
1286
1134
|
constraints_active: constraintLifecycle.active,
|
|
1287
1135
|
constraints_retired: constraints.retired,
|
|
1288
|
-
constraints_inactive: constraintLifecycle.inactive,
|
|
1289
1136
|
constraint_metadata: constraintLifecycle.metadata,
|
|
1290
1137
|
loop_id: request.loop_id,
|
|
1291
1138
|
round: request.round,
|
|
@@ -1306,14 +1153,27 @@ export function compileLoop(request, context) {
|
|
|
1306
1153
|
warnings,
|
|
1307
1154
|
});
|
|
1308
1155
|
const policy = getPolicy();
|
|
1309
|
-
|
|
1156
|
+
// L7 (v3.7.x): the state-file path is empty when the file is disabled —
|
|
1157
|
+
// prompts must never point at (or collapse content behind) a file that
|
|
1158
|
+
// writeStateFile will not create. An empty path propagates into the
|
|
1159
|
+
// assembler's pointer lines and the L1 collapse gate below.
|
|
1160
|
+
const statePath = policy.state_file.enabled
|
|
1161
|
+
? `${policy.state_file.directory}/${request.loop_id}-state.md`
|
|
1162
|
+
: "";
|
|
1310
1163
|
const state = createCanonicalLoopState(request, response, statePath, {
|
|
1311
1164
|
roundStats,
|
|
1312
1165
|
machineStatus,
|
|
1313
1166
|
// v3.4: derived active contract — the Current Task's source of truth.
|
|
1314
1167
|
roundContract: activeContract,
|
|
1315
1168
|
});
|
|
1316
|
-
const markdown = renderCanonicalStateMarkdown(state
|
|
1169
|
+
const markdown = renderCanonicalStateMarkdown(state, {
|
|
1170
|
+
// v3.7.1: retry attempts are marked in the derived view (attempt is
|
|
1171
|
+
// request context, not a committed fact). The Recovery Brief renders in
|
|
1172
|
+
// the Recent tier only while a committed backtrack decision is this
|
|
1173
|
+
// round's record; the redo commit removes it by construction.
|
|
1174
|
+
attempt: request.attempt,
|
|
1175
|
+
recoveryBrief: committedBacktrackBrief(request.loop_id, request.round, context),
|
|
1176
|
+
});
|
|
1317
1177
|
// v2.4–v2.5: Adaptive L2 budget — scales with loop complexity
|
|
1318
1178
|
const adaptiveL2 = policy.prompt.l2_adaptive_enabled
|
|
1319
1179
|
? Math.min(policy.prompt.l2_max_chars
|
|
@@ -1324,23 +1184,25 @@ export function compileLoop(request, context) {
|
|
|
1324
1184
|
// v2.8: L2 pointer mode — when enabled, skip the monolithic markdown blob.
|
|
1325
1185
|
// Structured L2 sections (milestones, sub-goals, trust, progress) still
|
|
1326
1186
|
// render. The state file on disk is the durable source of truth.
|
|
1327
|
-
|
|
1187
|
+
// L7: pointer mode REQUIRES the file — with state_file disabled it would
|
|
1188
|
+
// silently drop the full state from L2 prompts and point at nothing.
|
|
1189
|
+
const l2Pointer = policy.state_file.enabled &&
|
|
1190
|
+
policy.prompt.l2_pointer_enabled && decision.level === "l2";
|
|
1328
1191
|
const artifact = assemblePromptArtifact({
|
|
1329
1192
|
state,
|
|
1330
1193
|
level: decision.level,
|
|
1331
1194
|
reasons: decision.reasons,
|
|
1332
|
-
mode: policy.prompt.injection_mode,
|
|
1333
1195
|
budgets: {
|
|
1334
1196
|
l0: policy.prompt.l0_max_chars,
|
|
1335
1197
|
l1: policy.prompt.l1_max_chars,
|
|
1336
1198
|
l2: adaptiveL2,
|
|
1337
1199
|
},
|
|
1338
1200
|
attempt: request.attempt,
|
|
1339
|
-
selfEvaluationBlock: buildSelfEvalBlock(request.round, request.verification_flags, decision.level,
|
|
1201
|
+
selfEvaluationBlock: buildSelfEvalBlock(request.round, request.verification_flags, decision.level,
|
|
1340
1202
|
// v3.4: restate the contract only when this prompt's Current Task IS
|
|
1341
1203
|
// the ACTIVE contract (derived — not the previous submission's field,
|
|
1342
1204
|
// which is a proposal and may differ from what this round executes).
|
|
1343
|
-
activeContract != null,
|
|
1205
|
+
activeContract != null,
|
|
1344
1206
|
// v3.5: L2-only declaration nudge when nothing is active (policy-gated;
|
|
1345
1207
|
// mutually exclusive with the restate template above).
|
|
1346
1208
|
decision.level === "l2" && activeContract === null &&
|