loopforge 3.0.0 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/README.md +128 -145
  2. package/dist/audit.d.ts +50 -0
  3. package/dist/audit.d.ts.map +1 -0
  4. package/dist/audit.js +158 -0
  5. package/dist/audit.js.map +1 -0
  6. package/dist/canonical-state.d.ts +28 -20
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +82 -173
  9. package/dist/canonical-state.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +25 -16
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cognitive-governance.d.ts +25 -33
  14. package/dist/cognitive-governance.d.ts.map +1 -1
  15. package/dist/cognitive-governance.js +38 -138
  16. package/dist/cognitive-governance.js.map +1 -1
  17. package/dist/enforcement-gate.d.ts +76 -0
  18. package/dist/enforcement-gate.d.ts.map +1 -0
  19. package/dist/enforcement-gate.js +1071 -0
  20. package/dist/enforcement-gate.js.map +1 -0
  21. package/dist/engine.d.ts +99 -0
  22. package/dist/engine.d.ts.map +1 -0
  23. package/dist/engine.js +839 -0
  24. package/dist/engine.js.map +1 -0
  25. package/dist/evidence-claims.d.ts +50 -0
  26. package/dist/evidence-claims.d.ts.map +1 -0
  27. package/dist/evidence-claims.js +134 -0
  28. package/dist/evidence-claims.js.map +1 -0
  29. package/dist/evidence-provider.d.ts +21 -23
  30. package/dist/evidence-provider.d.ts.map +1 -1
  31. package/dist/evidence-provider.js +74 -34
  32. package/dist/evidence-provider.js.map +1 -1
  33. package/dist/generate-schema.js +1 -84
  34. package/dist/generate-schema.js.map +1 -1
  35. package/dist/index.d.ts +19 -22
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +16 -15
  38. package/dist/index.js.map +1 -1
  39. package/dist/loop-compiler.d.ts +41 -3
  40. package/dist/loop-compiler.d.ts.map +1 -1
  41. package/dist/loop-compiler.js +632 -273
  42. package/dist/loop-compiler.js.map +1 -1
  43. package/dist/loop-extras-parser.d.ts +2 -2
  44. package/dist/loop-extras-parser.d.ts.map +1 -1
  45. package/dist/loop-extras-parser.js +2 -2
  46. package/dist/loop-extras-parser.js.map +1 -1
  47. package/dist/loop-projection.d.ts +26 -0
  48. package/dist/loop-projection.d.ts.map +1 -0
  49. package/dist/loop-projection.js +142 -0
  50. package/dist/loop-projection.js.map +1 -0
  51. package/dist/loop-store.d.ts +112 -24
  52. package/dist/loop-store.d.ts.map +1 -1
  53. package/dist/loop-store.js +365 -218
  54. package/dist/loop-store.js.map +1 -1
  55. package/dist/mcp/round-lifecycle.d.ts +216 -0
  56. package/dist/mcp/round-lifecycle.d.ts.map +1 -0
  57. package/dist/mcp/round-lifecycle.js +926 -0
  58. package/dist/mcp/round-lifecycle.js.map +1 -0
  59. package/dist/mcp/server.d.ts +2 -1
  60. package/dist/mcp/server.d.ts.map +1 -1
  61. package/dist/mcp/server.js +28 -42
  62. package/dist/mcp/server.js.map +1 -1
  63. package/dist/mcp/session.d.ts +53 -101
  64. package/dist/mcp/session.d.ts.map +1 -1
  65. package/dist/mcp/session.js +306 -1105
  66. package/dist/mcp/session.js.map +1 -1
  67. package/dist/mcp/tools.d.ts +13 -16
  68. package/dist/mcp/tools.d.ts.map +1 -1
  69. package/dist/mcp/tools.js +479 -413
  70. package/dist/mcp/tools.js.map +1 -1
  71. package/dist/policy-metrics.d.ts +12 -17
  72. package/dist/policy-metrics.d.ts.map +1 -1
  73. package/dist/policy-metrics.js +95 -161
  74. package/dist/policy-metrics.js.map +1 -1
  75. package/dist/policy.d.ts +44 -35
  76. package/dist/policy.d.ts.map +1 -1
  77. package/dist/policy.js +31 -115
  78. package/dist/policy.js.map +1 -1
  79. package/dist/prompt-assembler.d.ts +37 -6
  80. package/dist/prompt-assembler.d.ts.map +1 -1
  81. package/dist/prompt-assembler.js +389 -160
  82. package/dist/prompt-assembler.js.map +1 -1
  83. package/dist/prompt-policy.d.ts +1 -8
  84. package/dist/prompt-policy.d.ts.map +1 -1
  85. package/dist/prompt-policy.js +8 -26
  86. package/dist/prompt-policy.js.map +1 -1
  87. package/dist/protocol.d.ts +357 -229
  88. package/dist/protocol.d.ts.map +1 -1
  89. package/dist/protocol.js +139 -15
  90. package/dist/protocol.js.map +1 -1
  91. package/dist/replay.d.ts +5 -13
  92. package/dist/replay.d.ts.map +1 -1
  93. package/dist/replay.js +19 -55
  94. package/dist/replay.js.map +1 -1
  95. package/dist/round-coordinator.d.ts +20 -24
  96. package/dist/round-coordinator.d.ts.map +1 -1
  97. package/dist/round-coordinator.js +99 -92
  98. package/dist/round-coordinator.js.map +1 -1
  99. package/dist/round-driver.d.ts +14 -13
  100. package/dist/round-driver.d.ts.map +1 -1
  101. package/dist/round-driver.js +26 -14
  102. package/dist/round-driver.js.map +1 -1
  103. package/dist/round-transaction.d.ts +15 -18
  104. package/dist/round-transaction.d.ts.map +1 -1
  105. package/dist/round-transaction.js +38 -47
  106. package/dist/round-transaction.js.map +1 -1
  107. package/dist/self-eval.d.ts +53 -0
  108. package/dist/self-eval.d.ts.map +1 -0
  109. package/dist/self-eval.js +284 -0
  110. package/dist/self-eval.js.map +1 -0
  111. package/dist/storage.d.ts +9 -4
  112. package/dist/storage.d.ts.map +1 -1
  113. package/dist/storage.js +18 -6
  114. package/dist/storage.js.map +1 -1
  115. package/dist/token-utils.d.ts +15 -3
  116. package/dist/token-utils.d.ts.map +1 -1
  117. package/dist/token-utils.js +24 -5
  118. package/dist/token-utils.js.map +1 -1
  119. package/dist/verification-gate.d.ts +108 -0
  120. package/dist/verification-gate.d.ts.map +1 -0
  121. package/dist/verification-gate.js +971 -0
  122. package/dist/verification-gate.js.map +1 -0
  123. package/loop_policy.json +44 -15
  124. package/package.json +5 -10
  125. package/skills/perception/SKILL.md +182 -199
  126. package/dist/canonical-event-writer.d.ts +0 -20
  127. package/dist/canonical-event-writer.d.ts.map +0 -1
  128. package/dist/canonical-event-writer.js +0 -131
  129. package/dist/canonical-event-writer.js.map +0 -1
  130. package/dist/compiler-subgoals.d.ts +0 -7
  131. package/dist/compiler-subgoals.d.ts.map +0 -1
  132. package/dist/compiler-subgoals.js +0 -93
  133. package/dist/compiler-subgoals.js.map +0 -1
  134. package/dist/derive-id.d.ts +0 -12
  135. package/dist/derive-id.d.ts.map +0 -1
  136. package/dist/derive-id.js +0 -31
  137. package/dist/derive-id.js.map +0 -1
  138. package/dist/mcp/session-projection.d.ts +0 -5
  139. package/dist/mcp/session-projection.d.ts.map +0 -1
  140. package/dist/mcp/session-projection.js +0 -18
  141. package/dist/mcp/session-projection.js.map +0 -1
  142. package/dist/mcp/session-queue.d.ts +0 -3
  143. package/dist/mcp/session-queue.d.ts.map +0 -1
  144. package/dist/mcp/session-queue.js +0 -18
  145. package/dist/mcp/session-queue.js.map +0 -1
  146. package/dist/mcp/session-types.d.ts +0 -72
  147. package/dist/mcp/session-types.d.ts.map +0 -1
  148. package/dist/mcp/session-types.js +0 -2
  149. package/dist/mcp/session-types.js.map +0 -1
  150. package/dist/round-commit-store.d.ts +0 -12
  151. package/dist/round-commit-store.d.ts.map +0 -1
  152. package/dist/round-commit-store.js +0 -13
  153. package/dist/round-commit-store.js.map +0 -1
  154. package/dist/round-gate.d.ts +0 -31
  155. package/dist/round-gate.d.ts.map +0 -1
  156. package/dist/round-gate.js +0 -259
  157. package/dist/round-gate.js.map +0 -1
  158. package/dist/round-recovery.d.ts +0 -17
  159. package/dist/round-recovery.d.ts.map +0 -1
  160. package/dist/round-recovery.js +0 -117
  161. package/dist/round-recovery.js.map +0 -1
  162. package/dist/round-report.d.ts +0 -16
  163. package/dist/round-report.d.ts.map +0 -1
  164. package/dist/round-report.js +0 -401
  165. package/dist/round-report.js.map +0 -1
  166. package/dist/runtime-services.d.ts +0 -59
  167. package/dist/runtime-services.d.ts.map +0 -1
  168. package/dist/runtime-services.js +0 -460
  169. package/dist/runtime-services.js.map +0 -1
  170. package/dist/vault-format.d.ts +0 -4
  171. package/dist/vault-format.d.ts.map +0 -1
  172. package/dist/vault-format.js +0 -4
  173. package/dist/vault-format.js.map +0 -1
  174. package/dist/vault-types.d.ts +0 -24
  175. package/dist/vault-types.d.ts.map +0 -1
  176. package/dist/vault-types.js +0 -2
  177. package/dist/vault-types.js.map +0 -1
  178. package/loopforge-policy.json +0 -340
@@ -3,23 +3,20 @@
3
3
  * The compiler evolves structured state and renders one prompt artifact.
4
4
  * L0/L1/L2 control state density only; the external Agent owns reasoning.
5
5
  */
6
- import { deriveConstraintId, deriveCriterionId, deriveTextHash, } from "./derive-id.js";
6
+ import { createHash } from "node:crypto";
7
7
  import { getPolicy } from "./policy.js";
8
- import { AgentStatus, makeLoopCompileResponse, makeLoopCompileProjection, makeLoopHealth, makeLoopObjective, makeConstraintMeta, makeMilestoneSummary, makeRollingSummary, makeTaskAlignment, } from "./protocol.js";
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 { auditEntries, deriveGate, deriveStructuredGate } from "./cognitive-governance.js";
11
- import { parseRoundReport, deriveCompilerProjection } from "./round-report.js";
12
10
  import { assemblePromptArtifact } from "./prompt-assembler.js";
13
11
  import { decidePromptLevel, } from "./prompt-policy.js";
14
- import { jaccardSimilarity, unique, entryRound } from "./token-utils.js";
15
- import { manageSubGoals as deriveSubGoals } from "./compiler-subgoals.js";
12
+ import { deriveItemId, jaccardSimilarity, unique, entryRound } from "./token-utils.js";
16
13
  function contextEntries(context) {
17
14
  if (!context || !Array.isArray(context.results))
18
15
  return [];
19
16
  return context.results.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
20
17
  }
21
18
  function lineage(entry) {
22
- const value = entry.loop_lineage;
19
+ const value = entry.loop_lineage ?? entry.lineage;
23
20
  return value !== null && typeof value === "object" && !Array.isArray(value)
24
21
  ? value
25
22
  : {};
@@ -29,12 +26,9 @@ function loopEntries(loopId, context) {
29
26
  .filter((entry) => entry.loop_id === loopId || lineage(entry).loop_id === loopId)
30
27
  .sort((a, b) => entryRound(a) - entryRound(b));
31
28
  }
32
- function entryProjection(entry) {
33
- const report = parseRoundReport(entry.round_report);
34
- return report ? deriveCompilerProjection(report, entryRound(entry)) : null;
35
- }
36
29
  export function computeGoalTextHash(text) {
37
- return deriveTextHash(text, 12);
30
+ const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
31
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
38
32
  }
39
33
  export function deriveGoalId(loopId, task, explicit = "") {
40
34
  return explicit.trim() || `${loopId}:${task.trim().toLowerCase()
@@ -42,6 +36,34 @@ export function deriveGoalId(loopId, task, explicit = "") {
42
36
  .replace(/^-|-$/g, "")
43
37
  .slice(0, 48) || computeGoalTextHash(task)}`;
44
38
  }
39
+ /** v3.2: Read the previous round's persisted L1 presentation snapshot — the
40
+ * diff baseline for L1 collapse. Returns null when the previous round's
41
+ * lineage entry exists but lacks all three presented_* fields (old-format
42
+ * entries, L0/L2 compiles) — callers then render in full, which keeps
43
+ * existing fixtures and pre-v3.2 loops on the full-render path. */
44
+ export function readPresentedBaseline(loopId, round, context) {
45
+ const entry = [...loopEntries(loopId, context)].reverse()
46
+ .find((candidate) => entryRound(candidate) === round - 1);
47
+ if (!entry)
48
+ return null;
49
+ const data = lineage(entry);
50
+ const constraintIds = data.presented_constraint_ids;
51
+ const subGoals = data.presented_subgoals;
52
+ const milestoneRanges = data.presented_milestone_ranges;
53
+ if (!Array.isArray(constraintIds) || !constraintIds.every((v) => typeof v === "string") ||
54
+ !Array.isArray(subGoals) || !subGoals.every((v) => Array.isArray(v) && v.length === 2 &&
55
+ typeof v[0] === "string" && typeof v[1] === "string") ||
56
+ !Array.isArray(milestoneRanges) || !milestoneRanges.every((v) => Array.isArray(v) && v.length === 2 &&
57
+ typeof v[0] === "number" && typeof v[1] === "number")) {
58
+ return null;
59
+ }
60
+ return {
61
+ round: round - 1,
62
+ constraintIds: constraintIds,
63
+ subGoals: subGoals,
64
+ milestoneRanges: milestoneRanges,
65
+ };
66
+ }
45
67
  export function getPreviousRound(loopId, round, context) {
46
68
  const entry = [...loopEntries(loopId, context)].reverse()
47
69
  .find((candidate) => entryRound(candidate) === round);
@@ -61,7 +83,9 @@ export function getPreviousRound(loopId, round, context) {
61
83
  constraints_active: Array.isArray(data.constraints_active)
62
84
  ? data.constraints_active.filter((value) => typeof value === "string")
63
85
  : [],
64
- output_summary: entryProjection(entry)?.summary ?? "",
86
+ output_summary: typeof entry.output_summary === "string"
87
+ ? entry.output_summary
88
+ : typeof data.output_summary === "string" ? data.output_summary : "",
65
89
  };
66
90
  }
67
91
  function latestObjective(loopId, context) {
@@ -98,17 +122,17 @@ function evolveObjective(request, context) {
98
122
  created_at_round: 1,
99
123
  loop_id: request.loop_id,
100
124
  });
101
- const last = request.last_round_view;
125
+ const last = request.last_round_result;
102
126
  const history = [...(current.refinement_history ?? [])];
103
127
  let objective = current.objective || request.task;
104
128
  let version = current.version ?? 1;
105
- if (last?.objectiveRefinement?.trim()) {
106
- const refinement = last.objectiveRefinement.trim();
129
+ if (last?.objective_refinement?.trim()) {
130
+ const refinement = last.objective_refinement.trim();
107
131
  history.push(refinement);
108
132
  objective = `${objective}\nRefinement: ${refinement}`;
109
133
  version++;
110
134
  }
111
- const revisions = new Map((last?.criterionRevisions ?? []).map((item) => [item.old, item.new]));
135
+ const revisions = new Map((last?.revised_success_criteria ?? []).map((item) => [item.old, item.new]));
112
136
  return makeLoopObjective({
113
137
  ...current,
114
138
  objective,
@@ -119,66 +143,183 @@ function evolveObjective(request, context) {
119
143
  loop_id: request.loop_id,
120
144
  });
121
145
  }
122
- function evolveConstraints(request, objective, previous) {
123
- const retired = unique(request.last_round_view?.constraintEvents?.retired ?? []);
146
+ function evolveConstraints(request, objective, previous, context) {
147
+ const retired = unique(request.last_round_result?.retracted_constraints ?? []);
124
148
  const retiredSet = new Set(retired);
125
- const discovered = unique(request.last_round_view?.discoveries?.newConstraints ?? [])
149
+ const discovered = unique(request.last_round_result?.discovered_constraints ?? [])
126
150
  .slice(0, getPolicy().evolution.max_discovered_constraints_per_round);
151
+ // v2.14: agent-declared retractions of plan/hard/criteria constraints
152
+ // persist for constraints.retire_window rounds (they are re-merged from
153
+ // the request/objective every round, so without this the documented
154
+ // "Removed from the active constraint set" never held for them). The
155
+ // retraction rounds are derived from vault entries — no new persistence
156
+ // format. After the window the constraint returns (hard/plan/criteria
157
+ // constraints never auto-decay — only agent action removes them, and the
158
+ // action's memory expires).
159
+ const window = getPolicy().constraints.retire_window;
160
+ const lastRetractedRound = new Map();
161
+ if (window > 0) {
162
+ for (const entry of loopEntries(request.loop_id, context)) {
163
+ const retractedList = lineage(entry).retracted_constraints;
164
+ if (!Array.isArray(retractedList))
165
+ continue;
166
+ const rnd = entryRound(entry);
167
+ for (const text of retractedList) {
168
+ if (typeof text !== "string" || !text)
169
+ continue;
170
+ const prev = lastRetractedRound.get(text);
171
+ if (prev === undefined || rnd > prev)
172
+ lastRetractedRound.set(text, rnd);
173
+ }
174
+ }
175
+ }
127
176
  const active = unique([
128
177
  ...(previous?.constraints_active ?? []),
129
178
  ...request.constraints_from_plan,
130
179
  ...objective.hard_constraints,
131
180
  ...objective.success_criteria,
132
181
  ...discovered,
133
- ]).filter((item) => !retiredSet.has(item))
134
- .slice(0, getPolicy().evolution.max_active_constraints);
182
+ ]).filter((item) => {
183
+ if (retiredSet.has(item))
184
+ return false;
185
+ const retractedRound = lastRetractedRound.get(item);
186
+ if (retractedRound !== undefined && request.round - retractedRound < window) {
187
+ return false;
188
+ }
189
+ return true;
190
+ }).slice(0, getPolicy().evolution.max_active_constraints);
135
191
  return { active, retired };
136
192
  }
137
193
  // ═══════════════════════════════════════════════════════════════════════════
138
194
  // Hierarchical Summary (v2.1)
139
195
  // ═══════════════════════════════════════════════════════════════════════════
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. */
140
211
  function entryCriteriaMet(entry) {
141
- return entryProjection(entry)?.evidence?.verifiedClaims ?? [];
142
- }
143
- function verifiedClaims(loopId, context) {
144
- const facts = new Set();
145
- const refs = new Set();
146
- for (const entry of loopEntries(loopId, context)) {
147
- const report = entry.round_report;
148
- if (!report || typeof report !== "object" || Array.isArray(report))
149
- continue;
150
- const evidence = report.evidence;
151
- if (!evidence || typeof evidence !== "object" || Array.isArray(evidence))
152
- continue;
153
- const envelope = evidence;
154
- const provenance = envelope.provenance;
155
- if (!provenance || typeof provenance !== "object" || Array.isArray(provenance))
156
- continue;
157
- const provenanceMap = provenance;
158
- if (!Array.isArray(envelope.claims))
159
- continue;
160
- for (const raw of envelope.claims) {
161
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
162
- continue;
163
- const claim = raw;
164
- if (typeof claim.targetId !== "string" || !Array.isArray(claim.evidenceRefs))
165
- continue;
166
- const verifiedRefs = claim.evidenceRefs.filter((ref) => {
167
- if (typeof ref !== "string")
168
- return false;
169
- const value = provenanceMap[ref];
170
- return Boolean(value && typeof value === "object" && !Array.isArray(value) &&
171
- value.status === "verified" &&
172
- value.source !== "agent");
173
- });
174
- if (verifiedRefs.length === 0)
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
+ /** v3.2: Deterministic lessons learned — constraints violated repeatedly or
227
+ * verification checks failing repeatedly across rounds (full history, unlike
228
+ * the enforcement gate's R2 3-round window). Presentation only: the output
229
+ * never feeds enforcement decisions. */
230
+ export function deriveLessons(loopId, context, currentRound) {
231
+ const entries = loopEntries(loopId, context).filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
232
+ const violations = new Map();
233
+ const errors = new Map();
234
+ const warns = new Map();
235
+ for (const entry of entries) {
236
+ const rnd = entryRound(entry);
237
+ const viols = Array.isArray(entry.constraint_violations)
238
+ ? entry.constraint_violations.filter((v) => typeof v === "string")
239
+ : [];
240
+ for (const text of viols) {
241
+ const list = violations.get(text) ?? [];
242
+ list.push(rnd);
243
+ violations.set(text, list);
244
+ }
245
+ const flags = Array.isArray(entry.verification_flags)
246
+ ? entry.verification_flags
247
+ : [];
248
+ for (const flag of flags) {
249
+ const target = flag.severity === "error"
250
+ ? errors
251
+ : flag.severity === "warn" ? warns : null;
252
+ if (!target)
175
253
  continue;
176
- facts.add(claim.targetId);
177
- for (const ref of verifiedRefs)
178
- refs.add(ref);
254
+ const list = target.get(flag.check) ?? [];
255
+ list.push(rnd);
256
+ target.set(flag.check, list);
257
+ }
258
+ }
259
+ const lessons = [];
260
+ for (const [text, rounds] of violations) {
261
+ if (rounds.length >= 2) {
262
+ lessons.push({ text, kind: "constraint_violation", count: rounds.length, rounds });
263
+ }
264
+ }
265
+ for (const [text, rounds] of errors) {
266
+ if (rounds.length >= 2) {
267
+ lessons.push({ text, kind: "verification_error", count: rounds.length, rounds });
179
268
  }
180
269
  }
181
- return { facts: [...facts].sort(), refs: [...refs].sort() };
270
+ for (const [text, rounds] of warns) {
271
+ if (rounds.length >= 2) {
272
+ lessons.push({ text, kind: "verification_warning", count: rounds.length, rounds });
273
+ }
274
+ }
275
+ return lessons.sort((a, b) => b.count - a.count);
276
+ }
277
+ /** v3.2: Derive per-criterion status — the "goal → criteria → evidence"
278
+ * vertical view. Each objective criterion gets: met/remaining/unknown
279
+ * (from per-round success_criteria_met/remaining reports, ID-first
280
+ * matching), the round it was first reported met, and any sub-goals whose
281
+ * description matches it (Jaccard). Zero persistence — re-derived from the
282
+ * vault every compile. */
283
+ export function deriveCriterionStatuses(loopId, context, objective, currentRound, subGoals, lastRoundResult) {
284
+ if (!objective || objective.success_criteria.length === 0)
285
+ return [];
286
+ const entries = loopEntries(loopId, context).filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
287
+ // The current round's report lives in last_round_result (the vault only
288
+ // gains it on commit); treat it as the report for round currentRound - 1.
289
+ const lastEv = lastRoundResult?.execution_evidence;
290
+ const lastMet = lastEv?.success_criteria_met ?? [];
291
+ const lastRemaining = lastEv?.success_criteria_remaining ?? [];
292
+ const lastEntry = entries[entries.length - 1] ?? null;
293
+ const lastEntryRemaining = lastEntry ? entryCriteriaRemaining(lastEntry) : [];
294
+ const matchThreshold = getPolicy().evolution.subgoal_match_threshold;
295
+ return objective.success_criteria.map((text) => {
296
+ let metAtRound = null;
297
+ for (const entry of entries) {
298
+ const rnd = entryRound(entry);
299
+ if (entryCriteriaMet(entry).some((met) => criteriaMatch(met, text))) {
300
+ metAtRound = rnd;
301
+ break; // first report wins — chronological scan
302
+ }
303
+ }
304
+ if (metAtRound === null && lastMet.some((met) => criteriaMatch(met, text))) {
305
+ metAtRound = currentRound - 1;
306
+ }
307
+ const status = metAtRound !== null
308
+ ? "met"
309
+ : lastRemaining.some((remaining) => criteriaMatch(remaining, text)) ||
310
+ lastEntryRemaining.some((remaining) => criteriaMatch(remaining, text))
311
+ ? "remaining"
312
+ : "unknown";
313
+ return {
314
+ id: deriveCriterionId(text),
315
+ text,
316
+ status,
317
+ ...(metAtRound !== null ? { met_at_round: metAtRound } : {}),
318
+ related_subgoal_ids: subGoals
319
+ .filter((sg) => jaccardSimilarity(text, sg.description) >= matchThreshold)
320
+ .map((sg) => sg.id),
321
+ };
322
+ });
182
323
  }
183
324
  /** v2.11: Match two criterion references for deduplication.
184
325
  * If either is a criterion ID (cr-XXXXXXXX), uses exact ID comparison.
@@ -197,9 +338,6 @@ function criteriaMatch(a, b) {
197
338
  // Neither is an ID — Jaccard fallback
198
339
  return jaccardSimilarity(a, b) >= getPolicy().evolution.criteria_dedup_threshold;
199
340
  }
200
- function isCriterionId(ref) {
201
- return /^cr-[a-f0-9]{8}$/.test(ref);
202
- }
203
341
  /** Detect newly met criteria by comparing the current entry's
204
342
  * success_criteria_met against the previous entry's.
205
343
  * v2.11: ID-first matching (cr-XXXXXXXX) with Jaccard similarity
@@ -227,15 +365,29 @@ function entryActiveConstraints(entry) {
227
365
  }
228
366
  /** Read retracted_constraints from an entry. */
229
367
  function entryRetractedConstraints(entry) {
230
- return entryProjection(entry)?.constraintEvents?.retired ?? [];
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 [];
231
376
  }
377
+ /** Read progress_estimate from an entry's execution_evidence. */
232
378
  function entryProgressEstimate(entry) {
233
- return entryProjection(entry)?.evidence?.progressRatio ?? 0;
379
+ const ev = entryExecutionEvidence(entry);
380
+ if (!ev)
381
+ return 0;
382
+ const pe = ev.progress_estimate;
383
+ return typeof pe === "number" ? pe : 0;
234
384
  }
235
385
  /** Build a single MilestoneSummary from a range of completed round entries. */
236
386
  function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, kind) {
237
387
  const last = phaseEntries.at(-1);
238
- const outcomeRaw = last ? entryProjection(last)?.summary ?? "" : "";
388
+ const outcomeRaw = last
389
+ ? (typeof last.output_summary === "string" ? last.output_summary : "")
390
+ : "";
239
391
  const outcomeMax = getPolicy().checkpoint.outcome_max_chars;
240
392
  const outcome = outcomeRaw.length > outcomeMax
241
393
  ? outcomeRaw.slice(0, outcomeMax) + "…"
@@ -254,7 +406,56 @@ function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, ki
254
406
  generated_at_round: endRound,
255
407
  });
256
408
  }
257
- export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0) {
409
+ /** v3.0.1: Evenly sample `budget` entries from `mid`, preferring
410
+ * agent_declared milestones (human/agent-labeled anchors) first and filling
411
+ * the remainder at even time positions. The caller guarantees
412
+ * `mid.length > budget`. */
413
+ function sampleMiddleMilestones(mid, budget) {
414
+ if (budget <= 0)
415
+ return [];
416
+ if (mid.length <= budget)
417
+ return mid;
418
+ const chosen = [];
419
+ const used = new Set();
420
+ for (const m of mid) {
421
+ if (m.kind === "agent_declared") {
422
+ chosen.push(m);
423
+ used.add(m);
424
+ if (chosen.length >= budget)
425
+ return chosen;
426
+ }
427
+ }
428
+ const rest = mid.filter((m) => !used.has(m));
429
+ const need = budget - chosen.length;
430
+ for (let i = 0; i < need; i++) {
431
+ const idx = Math.floor(((i + 0.5) * rest.length) / need);
432
+ chosen.push(rest[idx]);
433
+ }
434
+ return chosen;
435
+ }
436
+ /** v3.0.1: L1 milestone sampling — when milestones exceed max_milestones,
437
+ * keep the oldest `milestone_head_count` (history anchors), the newest
438
+ * `milestone_tail_count` (current progress), and an even sample of the
439
+ * middle. L2 (full rehydration) is never sampled: the recovery view keeps
440
+ * every milestone as the loop's memory skeleton. */
441
+ function sampleMilestones(milestones, policy, level) {
442
+ if (level === "l2")
443
+ return milestones;
444
+ if (milestones.length <= policy.max_milestones)
445
+ return milestones;
446
+ const head = Math.max(0, policy.milestone_head_count);
447
+ const tail = Math.max(0, policy.milestone_tail_count);
448
+ const midBudget = Math.max(0, policy.max_milestones - head - tail);
449
+ if (midBudget === 0)
450
+ return milestones.slice(0, head).concat(milestones.slice(milestones.length - tail));
451
+ const mid = milestones.slice(head, milestones.length - tail);
452
+ return [
453
+ ...milestones.slice(0, head),
454
+ ...sampleMiddleMilestones(mid, midBudget),
455
+ ...milestones.slice(milestones.length - tail),
456
+ ];
457
+ }
458
+ export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0, level) {
258
459
  const policy = getPolicy().summary;
259
460
  const allEntries = loopEntries(loopId, context);
260
461
  // ── Phase 1: Recent Window (preserves existing behavior) ──
@@ -270,48 +471,35 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
270
471
  const data = lineage(entry);
271
472
  const round = entryRound(entry);
272
473
  const success = entry.success ?? data.success;
273
- const summary = entryProjection(entry)?.summary ?? "";
474
+ const summary = typeof entry.output_summary === "string"
475
+ ? entry.output_summary
476
+ : typeof data.output_summary === "string" ? data.output_summary : "";
477
+ // v2.12: Declared outcome wins the label; the default wording stays
478
+ // "accepted" for zero regression on existing loop output.
479
+ const declared = entry.outcome;
480
+ const outcomeLabel = declared === "success" || declared === "partial" ||
481
+ declared === "failed" || declared === "blocked"
482
+ ? declared
483
+ : success === false ? "failed" : "accepted";
274
484
  if (summary)
275
- outcomes.push(`[R${round}] ${success === false ? "failed" : "accepted"}: ${summary}`);
276
- const violations = entryProjection(entry)?.violations ?? [];
485
+ outcomes.push(`[R${round}] ${outcomeLabel}: ${summary}`);
486
+ const violations = Array.isArray(entry.constraint_violations)
487
+ ? entry.constraint_violations
488
+ : Array.isArray(data.constraint_violations) ? data.constraint_violations : [];
277
489
  issues.push(...violations.filter((item) => typeof item === "string"));
278
490
  }
279
- const issueCounts = new Map();
280
- for (const issue of issues) {
281
- const key = issue.trim().replace(/\s+/g, " ").toLowerCase();
282
- if (!key)
283
- continue;
284
- const previous = issueCounts.get(key);
285
- issueCounts.set(key, { text: previous?.text ?? issue.trim(), count: (previous?.count ?? 0) + 1 });
286
- }
287
491
  // ── Phase 2: Milestone Accumulation ──
288
492
  // Scan all completed entries in round order. For each entry, check three
289
493
  // trigger signals in priority order: agent_declared > criteria_milestone > auto.
290
494
  const completedEntries = allEntries.filter((entry) => entryRound(entry) >= 1 && entryRound(entry) < currentRound);
291
- const failedGroups = [];
292
- for (const entry of completedEntries) {
293
- const data = lineage(entry);
294
- if ((entry.success ?? data.success) !== false)
295
- continue;
296
- const text = (entryProjection(entry)?.summary ?? "").trim();
297
- if (!text)
298
- continue;
299
- const group = failedGroups.find((candidate) => jaccardSimilarity(candidate.text, text) >= getPolicy().evolution.failed_pattern_match_threshold);
300
- if (group)
301
- group.count++;
302
- else
303
- failedGroups.push({ text, count: 1 });
304
- }
305
495
  const milestones = [];
306
496
  let lastMilestoneRound = 0;
307
497
  for (let i = 0; i < completedEntries.length; i++) {
308
498
  const entry = completedEntries[i];
309
499
  const rnd = entryRound(entry);
310
500
  const lin = lineage(entry);
311
- const report = parseRoundReport(entry.round_report);
312
501
  // Signal 1: Agent-declared checkpoint
313
- const isAgentCheckpoint = Boolean(report?.phaseBoundaryProposal) ||
314
- lin.compression_checkpoint === true;
502
+ const isAgentCheckpoint = lin.compression_checkpoint === true;
315
503
  // Signal 2: New criteria met (compare with previous vault entry)
316
504
  const prev = i > 0 ? completedEntries[i - 1] : null;
317
505
  const newCriteria = detectNewCriteria(entry, prev);
@@ -325,10 +513,9 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
325
513
  ? "agent_declared"
326
514
  : "criteria_milestone";
327
515
  const label = isAgentCheckpoint
328
- ? (report?.phaseBoundaryProposal?.label?.trim() ||
329
- (typeof lin.checkpoint_label === "string" && lin.checkpoint_label.trim()
330
- ? lin.checkpoint_label.trim()
331
- : `Round ${rnd}`))
516
+ ? (typeof lin.checkpoint_label === "string" && lin.checkpoint_label.trim()
517
+ ? lin.checkpoint_label.trim()
518
+ : `Round ${rnd}`)
332
519
  : `Completed: ${newCriteria.join(", ")}`;
333
520
  milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, rnd, label, kind));
334
521
  lastMilestoneRound = rnd;
@@ -349,7 +536,10 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
349
536
  milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, endRound, `Rounds ${startRound}–${endRound}`, "auto"));
350
537
  }
351
538
  }
352
- const cappedMilestones = milestones.slice(-policy.max_milestones);
539
+ // v3.0.1: L1 keeps head anchors + newest tail + an even middle sample;
540
+ // L2 (full rehydration) keeps every milestone — the recovery view is the
541
+ // loop's memory skeleton and must not be truncated.
542
+ const cappedMilestones = sampleMilestones(milestones, policy, level);
353
543
  // ── Phase 3: Loop Synthesis (formulaic) ──
354
544
  let loopSynthesis = "";
355
545
  if (policy.enable_loop_synthesis && currentRound > 1) {
@@ -371,20 +561,237 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
371
561
  return null;
372
562
  return makeRollingSummary({
373
563
  key_outcomes: unique(outcomes),
374
- recurring_issues: [...issueCounts.values()]
375
- .filter((issue) => issue.count >= 2)
376
- .map((issue) => `${issue.text} (${issue.count} rounds)`),
564
+ recurring_issues: unique(issues),
377
565
  rounds_sampled: windowEntries.length,
378
566
  generated_at_round: currentRound,
379
- failed_patterns: failedGroups
380
- .filter((failure) => failure.count >= 2)
381
- .map((failure) => `${failure.text} (${failure.count} attempts)`),
567
+ failed_patterns: [],
382
568
  milestones: cappedMilestones,
383
569
  loop_synthesis: loopSynthesis || undefined,
384
570
  });
385
571
  }
386
- // Sub-goal lifecycle is implemented in compiler-subgoals.ts; this facade keeps
387
- // the compiler's public entry point and projection flow unchanged.
572
+ // ═══════════════════════════════════════════════════════════════════════════
573
+ // Sub-Goal Structured Tracking (v2.2)
574
+ // ═══════════════════════════════════════════════════════════════════════════
575
+ /** Derive a stable sub-goal ID from its description hash.
576
+ * Exported as the single source of truth (verification-gate imports it). */
577
+ export function deriveSubGoalId(description) {
578
+ return "sg-" + deriveItemId(description);
579
+ }
580
+ /** v2.11: Derive a stable constraint ID from its text hash (c-XXXXXXXX).
581
+ * Same hash strategy as SubGoal — deterministic across rounds. */
582
+ export function deriveConstraintId(text) {
583
+ return "c-" + deriveItemId(text);
584
+ }
585
+ /** v2.11: Derive a stable criterion ID from its text hash (cr-XXXXXXXX).
586
+ * Same hash strategy as SubGoal — deterministic across rounds. */
587
+ export function deriveCriterionId(text) {
588
+ return "cr-" + deriveItemId(text);
589
+ }
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
+ /** 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
+ export function isCriterionId(ref) {
598
+ return /^cr-[a-f0-9]{8}$/.test(ref);
599
+ }
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
+ /**
633
+ * Match a user-provided sub-task reference against existing SubGoals.
634
+ *
635
+ * Tries exact SubGoal ID match first (e.g. "sg-a3f2b1c0"), then falls
636
+ * back to Jaccard token similarity on descriptions. Returns the index
637
+ * of the best-matching SubGoal, or -1 when no match reaches threshold.
638
+ */
639
+ function matchSubGoal(target, subGoals, threshold) {
640
+ const trimmed = target.trim();
641
+ // Phase 1: Exact ID match
642
+ for (let i = 0; i < subGoals.length; i++) {
643
+ if (subGoals[i].id === trimmed)
644
+ return i;
645
+ }
646
+ // Phase 2: Jaccard fallback on description text
647
+ let bestIdx = -1;
648
+ let bestScore = 0;
649
+ for (let i = 0; i < subGoals.length; i++) {
650
+ const score = jaccardSimilarity(trimmed, subGoals[i].description);
651
+ if (score > bestScore) {
652
+ bestScore = score;
653
+ bestIdx = i;
654
+ }
655
+ }
656
+ return bestScore >= threshold ? bestIdx : -1;
657
+ }
658
+ /**
659
+ * Manage the full sub-goal lifecycle across rounds.
660
+ *
661
+ * Accumulates sub-goals from all prior vault entries, processes the current
662
+ * round's declarations, derives status changes, and returns the complete
663
+ * structured SubGoal list.
664
+ *
665
+ * Status derivation priority (compiler-driven):
666
+ * 1. Agent declared done/canceled/blocked → explicit status change
667
+ * 2. next_action match → auto in_progress
668
+ * 3. success_criteria_met match → auto done
669
+ */
670
+ function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
671
+ const policy = getPolicy().evolution;
672
+ const allEntries = loopEntries(loopId, vaultContext);
673
+ const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
674
+ const subGoals = [];
675
+ // Phase 1 — Accumulate historical sub-goals from vault entries
676
+ for (const entry of completedEntries) {
677
+ const rnd = entryRound(entry);
678
+ const emerged = entryEmergedSubtasks(entry);
679
+ for (const desc of emerged) {
680
+ const id = deriveSubGoalId(desc);
681
+ // Dedup: skip if an existing sub-goal is too similar
682
+ if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
683
+ continue;
684
+ subGoals.push(makeSubGoal({
685
+ id,
686
+ description: desc.trim(),
687
+ status: "pending",
688
+ declared_at_round: rnd,
689
+ status_changed_at_round: rnd,
690
+ priority: subGoals.length,
691
+ }));
692
+ }
693
+ }
694
+ // Phase 2 — Process current round's sub-task declarations
695
+ if (lastRoundResult) {
696
+ const rnd = lastRoundResult.round || (currentRound - 1);
697
+ // New sub-goals from this round
698
+ const emerged = lastRoundResult.emerged_subtasks ?? [];
699
+ for (const desc of emerged) {
700
+ if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
701
+ continue;
702
+ subGoals.push(makeSubGoal({
703
+ id: deriveSubGoalId(desc),
704
+ description: desc.trim(),
705
+ status: "pending",
706
+ declared_at_round: rnd,
707
+ status_changed_at_round: rnd,
708
+ priority: subGoals.length,
709
+ }));
710
+ }
711
+ // Completed sub-goals
712
+ const completed = lastRoundResult.completed_subtasks ?? [];
713
+ for (const desc of completed) {
714
+ const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
715
+ if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
716
+ subGoals[idx].status = "done";
717
+ subGoals[idx].status_changed_at_round = rnd;
718
+ subGoals[idx].completed_at_round = rnd;
719
+ }
720
+ }
721
+ // Blocked sub-goals
722
+ const blocked = lastRoundResult.blocked_subtasks ?? [];
723
+ for (const desc of blocked) {
724
+ const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
725
+ if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
726
+ subGoals[idx].status = "blocked";
727
+ subGoals[idx].status_changed_at_round = rnd;
728
+ }
729
+ }
730
+ // Canceled sub-goals
731
+ const canceled = lastRoundResult.canceled_subtasks ?? [];
732
+ for (const desc of canceled) {
733
+ const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
734
+ if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
735
+ subGoals[idx].status = "canceled";
736
+ subGoals[idx].status_changed_at_round = rnd;
737
+ }
738
+ }
739
+ // Phase 3 — Auto-derive: next_action match → in_progress
740
+ const nextAction = lastRoundResult.next_action?.trim();
741
+ if (nextAction) {
742
+ for (const sg of subGoals) {
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
+ }
768
+ }
769
+ // Sort for display: in_progress first, then pending by age desc, then blocked, then done, then canceled
770
+ subGoals.sort((a, b) => {
771
+ const rank = (s) => {
772
+ switch (s.status) {
773
+ case "in_progress": return 0;
774
+ case "pending": return 1;
775
+ case "blocked": return 2;
776
+ case "done": return 3;
777
+ case "canceled": return 4;
778
+ }
779
+ };
780
+ const ra = rank(a), rb = rank(b);
781
+ if (ra !== rb)
782
+ return ra - rb;
783
+ // Within same status: pending by oldest first, done by most recent first
784
+ // (v2.14: the previous expression simplified to b.declared - a.declared,
785
+ // which sorted NEWEST first despite the comment)
786
+ if (a.status === "pending")
787
+ return a.declared_at_round - b.declared_at_round;
788
+ if (a.status === "done")
789
+ return (b.completed_at_round ?? 0) - (a.completed_at_round ?? 0);
790
+ return a.declared_at_round - b.declared_at_round;
791
+ });
792
+ return subGoals;
793
+ }
794
+ // ═══════════════════════════════════════════════════════════════════════════
388
795
  // Time-Aware Constraint Management (v2.3)
389
796
  // ═══════════════════════════════════════════════════════════════════════════
390
797
  /** Determine the source of a constraint by matching against known sources. */
@@ -409,7 +816,9 @@ function findDiscoveredRound(text, vaultEntries) {
409
816
  const rnd = entryRound(entry);
410
817
  if (rnd < 1)
411
818
  continue;
412
- const discovered = entryProjection(entry)?.discoveries?.newConstraints ?? [];
819
+ const discovered = Array.isArray(entry.discovered_constraints)
820
+ ? entry.discovered_constraints.filter((v) => typeof v === "string")
821
+ : [];
413
822
  for (const d of discovered) {
414
823
  // Phase 1: exact ID match (v2.11)
415
824
  if (d === targetId || deriveConstraintId(d) === targetId) {
@@ -431,23 +840,27 @@ function findDiscoveredRound(text, vaultEntries) {
431
840
  * v2.11: ID-first matching. Checks exact constraint ID match (c-XXXXXXXX)
432
841
  * against the derived ID of each vault entry's constraint_violations, then
433
842
  * falls back to Jaccard similarity on text. Returns 0 if never violated. */
434
- function findLastViolatedRound(text, vaultEntries) {
843
+ /** True when a reported violation text matches a constraint text — same
844
+ * ID-first + Jaccard fallback matching as findLastViolatedRound. */
845
+ function matchesConstraintText(text, violation) {
435
846
  const targetId = deriveConstraintId(text);
847
+ if (violation === targetId || deriveConstraintId(violation) === targetId) {
848
+ return true;
849
+ }
850
+ return jaccardSimilarity(text, violation) >=
851
+ getPolicy().evolution.constraint_match_threshold;
852
+ }
853
+ function findLastViolatedRound(text, vaultEntries) {
436
854
  let latest = 0;
437
855
  for (const entry of vaultEntries) {
438
856
  const rnd = entryRound(entry);
439
857
  if (rnd < 1)
440
858
  continue;
441
- const violations = entryProjection(entry)?.violations ?? [];
859
+ const violations = Array.isArray(entry.constraint_violations)
860
+ ? entry.constraint_violations.filter((v) => typeof v === "string")
861
+ : [];
442
862
  for (const v of violations) {
443
- // Phase 1: exact ID match (v2.11)
444
- if (v === targetId || deriveConstraintId(v) === targetId) {
445
- if (rnd > latest)
446
- latest = rnd;
447
- break;
448
- }
449
- // Phase 2: Jaccard fallback (backward compatibility)
450
- if (jaccardSimilarity(text, v) >= getPolicy().evolution.constraint_match_threshold) {
863
+ if (matchesConstraintText(text, v)) {
451
864
  if (rnd > latest)
452
865
  latest = rnd;
453
866
  break;
@@ -464,20 +877,29 @@ function findLastViolatedRound(text, vaultEntries) {
464
877
  * constraints never auto-decay. Inactive constraints are auto-reactivated
465
878
  * if violated again in the current round.
466
879
  */
467
- function manageConstraintLifecycle(active, objective, constraintsFromPlan, vaultContext, currentRound) {
880
+ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vaultContext, currentRound, lastRoundViolations = []) {
468
881
  const policy = getPolicy().evolution;
469
882
  const allEntries = loopEntries(objective.loop_id || "", vaultContext);
470
883
  const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
471
884
  const metadata = [];
472
885
  for (const text of active) {
473
886
  const source = constraintSource(text, objective, constraintsFromPlan);
887
+ // v3.2.1: a violation reported in THIS round (last_round_result) counts
888
+ // as a violation at currentRound — the previous rounds' scan cannot see
889
+ // it (this round is not committed yet). This makes the L1 collapse
890
+ // render "(violated this round)" (diffConstraints compares
891
+ // last_violated_at_round === round) and re-activates decayed
892
+ // discovered constraints.
893
+ const violatedThisRound = lastRoundViolations.some((v) => matchesConstraintText(text, v));
474
894
  // Structural constraints never decay
475
895
  if (source === "hard" || source === "plan" || source === "criteria") {
476
896
  metadata.push(makeConstraintMeta({
477
897
  id: deriveConstraintId(text),
478
898
  text,
479
899
  discovered_at_round: objective.created_at_round || 1,
480
- last_violated_at_round: findLastViolatedRound(text, completedEntries),
900
+ last_violated_at_round: violatedThisRound
901
+ ? currentRound
902
+ : findLastViolatedRound(text, completedEntries),
481
903
  source,
482
904
  status: "active",
483
905
  }));
@@ -485,7 +907,9 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
485
907
  }
486
908
  // Discovered constraints: check for inactivity
487
909
  const discoveredAt = findDiscoveredRound(text, completedEntries);
488
- const lastViolated = findLastViolatedRound(text, completedEntries);
910
+ const lastViolated = violatedThisRound
911
+ ? currentRound
912
+ : findLastViolatedRound(text, completedEntries);
489
913
  const roundsSinceRelevant = lastViolated > 0
490
914
  ? currentRound - lastViolated
491
915
  : currentRound - (discoveredAt || currentRound);
@@ -513,21 +937,25 @@ export function alignTask(proposedTask, request, context) {
513
937
  const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
514
938
  if (!objective)
515
939
  return makeTaskAlignment();
940
+ // v2.14: threshold is policy-driven (evolution.progress_mismatch_threshold)
941
+ const threshold = getPolicy().evolution.progress_mismatch_threshold;
516
942
  const score = jaccardSimilarity(proposedTask, [objective.objective, ...objective.success_criteria, ...objective.hard_constraints].join(" "));
517
943
  return makeTaskAlignment({
518
- is_aligned: score >= 0.3,
944
+ is_aligned: score >= threshold,
519
945
  alignment_score: Number(score.toFixed(2)),
520
- warning: score < 0.3 ? "Current task may be drifting from the loop objective." : "",
521
- escalation: score < 0.3 ? "warn" : "none",
946
+ warning: score < threshold ? "Current task may be drifting from the loop objective." : "",
947
+ escalation: score < threshold ? "warn" : "none",
522
948
  });
523
949
  }
524
950
  export function checkLoopHealth(loopId, request, context) {
525
951
  const previous = getPreviousRound(loopId, request.round - 1, context);
526
952
  const alignment = alignTask(request.task, request, context);
527
- const violations = request.last_round_view?.violations?.length ?? 0;
953
+ const violations = request.last_round_result?.constraint_violations.length ?? 0;
528
954
  const integrity = Math.max(0, 1 - violations * 0.2);
529
955
  const continuity = previous ? jaccardSimilarity(previous.task, request.task) : 1;
530
- const drift = alignment.alignment_score < 0.3 || continuity < 0.2;
956
+ // v2.14: thresholds are policy-driven
957
+ const drift = alignment.alignment_score < getPolicy().evolution.progress_mismatch_threshold ||
958
+ continuity < getPolicy().evolution.task_continuity_threshold;
531
959
  return makeLoopHealth({
532
960
  goal_alignment: alignment.alignment_score,
533
961
  constraint_integrity: integrity,
@@ -539,115 +967,91 @@ export function checkLoopHealth(loopId, request, context) {
539
967
  }
540
968
  function levelDecision(request, context) {
541
969
  const previous = getPreviousRound(request.loop_id, request.round - 1, context);
542
- const last = request.last_round_view;
970
+ const last = request.last_round_result;
543
971
  const hasNewInformation = Boolean(request.new_since_last_round.trim() ||
544
- last?.discoveries?.newConstraints?.length ||
545
- last?.objectiveRefinement?.trim() ||
546
- last?.discoveries?.emergedWork?.length ||
547
- last?.constraintEvents?.retired?.length ||
548
- last?.criterionRevisions?.length ||
549
- last?.discoveries?.wrongAssumptions?.length ||
550
- last?.evidence?.verifiedClaims.length);
972
+ last?.discovered_constraints?.length ||
973
+ last?.objective_refinement?.trim() ||
974
+ last?.emerged_subtasks?.length ||
975
+ last?.retracted_constraints?.length ||
976
+ last?.revised_success_criteria?.length ||
977
+ last?.wrong_assumptions?.length);
551
978
  const lastFullRound = loopEntries(request.loop_id, context)
552
979
  .filter((entry) => lineage(entry).recompile_level === "l2")
553
980
  .map(entryRound)
554
981
  .filter((round) => round < request.round)
555
982
  .at(-1) ?? 1;
556
- const entries = loopEntries(request.loop_id, context)
557
- .filter((entry) => entryRound(entry) < request.round);
558
- const lastMeaningfulRound = [...entries].reverse().find((entry) => {
559
- const data = lineage(entry);
560
- const evidence = entryProjection(entry)?.evidence;
561
- return Boolean(entryCriteriaMet(entry).length ||
562
- evidence?.observedFiles.length ||
563
- entryProjection(entry)?.discoveries?.newConstraints.length ||
564
- entryProjection(entry)?.discoveries?.emergedWork.length);
565
- });
566
- const meaningfulSections = 2 + [
567
- request.task.trim(),
568
- ...(last?.violations ?? []),
569
- ...(last?.evidence?.criteriaRemaining ?? []),
570
- ...(last?.evidence?.criteriaMet ?? []),
571
- ...(last?.discoveries?.newConstraints ?? []),
572
- ...(last?.discoveries?.emergedWork ?? []),
573
- ...(last?.evidence?.observedFiles ?? []),
574
- ].filter(Boolean).length;
575
- const promptPolicy = getPolicy().prompt;
983
+ // v2.14: a backtrack committed for this round means the loop is
984
+ // re-walking restored state — the recovery boundary forces L2
985
+ // rehydration (the decidePromptLevel recovery_boundary branch existed
986
+ // but no caller ever set it).
987
+ const recoveryBoundary = loopEntries(request.loop_id, context)
988
+ .some((entry) => lineage(entry).committed_action === "backtrack" &&
989
+ entryRound(entry) === request.round);
576
990
  return decidePromptLevel({
577
991
  round: request.round,
578
992
  attempt: request.attempt,
579
993
  forceLevel: request.force_level,
580
994
  hasPlanSource: Boolean(request.plan_source),
581
- checkpointBoundary: Boolean(last?.phaseBoundaryProposal),
582
- goalChanged: Boolean(last?.objectiveRefinement?.trim()) ||
583
- (previous !== null && previous.goal_id !== deriveGoalId(request.loop_id, request.task, request.goal_id)),
584
- constraintChanged: Boolean(last?.discoveries?.newConstraints?.length || last?.constraintEvents?.retired?.length),
995
+ recoveryBoundary,
996
+ checkpointBoundary: last?.compression_checkpoint === true,
997
+ goalChanged: previous !== null && previous.goal_id !== deriveGoalId(request.loop_id, request.task, request.goal_id),
585
998
  previousStateMissing: request.round > 1 && previous === null,
586
- previousFailedWithoutNewInformation: last?.outcome === "failed" && !hasNewInformation,
999
+ previousFailedWithoutNewInformation: last?.success === false && !hasNewInformation,
587
1000
  verificationContradicted: (request.verification_flags ?? [])
588
1001
  .some((flag) => flag.severity === "error"),
589
1002
  consecutiveRejections: request.consecutive_rejections,
590
1003
  fullRefreshInterval: getPolicy().prompt.full_refresh_interval,
591
1004
  lastFullRound,
592
- l1Streak: Math.max(0, request.round - lastFullRound - 1),
593
- maxL1Streak: promptPolicy.adaptive_refresh ? promptPolicy.max_l1_streak : undefined,
594
- meaningfulSections: request.round <= 2
595
- ? Math.max(meaningfulSections, promptPolicy.min_meaningful_sections)
596
- : meaningfulSections,
597
- minMeaningfulSections: promptPolicy.adaptive_refresh
598
- ? promptPolicy.min_meaningful_sections
599
- : undefined,
600
- staleProgressRounds: request.round - (lastMeaningfulRound ? entryRound(lastMeaningfulRound) : 1),
601
- staleRoundThreshold: promptPolicy.adaptive_refresh
602
- ? promptPolicy.stale_round_threshold
603
- : undefined,
604
1005
  });
605
1006
  }
606
1007
  export function decideLevel(request, context) {
607
1008
  return levelDecision(request, context).level;
608
1009
  }
609
- export function buildRoundReportBlock(round, prevDriftFlags) {
1010
+ export function buildSelfEvalBlock(round, prevDriftFlags,
1011
+ /** v2.12: L0 is the minimal retry template — the v2.12 declarative fields
1012
+ * (outcome/blocker/retroactiveClaims) are L1/L2 additions so the retry
1013
+ * prompt stays within its tight budget. */
1014
+ level) {
610
1015
  const hasIntentDrift = prevDriftFlags?.some(f => f.check === "intent_drift") ?? false;
611
1016
  const hasSubGoalDrift = prevDriftFlags?.some(f => f.check === "subgoal_drift") ?? false;
612
1017
  const needsClarification = hasIntentDrift || hasSubGoalDrift;
613
- const report = {
614
- format: "loopforge.round-report",
615
- outcome: "partial",
616
- continuation: "continue",
617
- summary: `<verified result of round ${round}>`,
618
- violations: [],
619
- evidence: {
620
- files: [],
621
- checks: [{ name: "<configured provider or command name>", status: "not_run" }],
622
- claims: [{
623
- id: "claim-<12 hex chars>",
624
- targetId: "<cr-XXXXXXXX or criterion text>",
625
- evidenceRefs: ["provider:<name>"],
626
- }],
627
- noChangeReason: "",
1018
+ const declareOutcome = level !== "l0";
1019
+ const evalObj = {
1020
+ success: false,
1021
+ output_summary: `<verified result of round ${round}>`,
1022
+ constraint_violations: ["<c-XXXXXXXX or text>"],
1023
+ should_continue: true,
1024
+ discovered_constraints: ["<new constraint>"],
1025
+ emerged_subtasks: [],
1026
+ execution_evidence: {
1027
+ files_changed: [],
1028
+ test_results: { passed: 0, failed: 0, skipped: 0 },
1029
+ success_criteria_met: ["<cr-XXXXXXXX or text>"],
1030
+ success_criteria_remaining: ["<cr-XXXXXXXX or text>"],
1031
+ progress_estimate: 0,
628
1032
  },
629
- discoveries: {
630
- wrongAssumptions: [],
631
- emergedWork: [],
632
- facts: [],
633
- newConstraints: [],
634
- },
635
- subGoalEvents: { completed: [], blocked: [], canceled: [] },
1033
+ wrong_assumptions: [],
1034
+ next_action: "<next concrete action, or empty when complete>",
1035
+ completed_subtasks: ["<sg-XXXXXXXX or text>"],
1036
+ blocked_subtasks: ["<sg-XXXXXXXX or text>"],
1037
+ canceled_subtasks: ["<sg-XXXXXXXX or text>"],
636
1038
  };
1039
+ // v2.12: Declarative tri-state outcome (L1/L2 only — keeps L0 retry lean)
1040
+ if (declareOutcome) {
1041
+ evalObj.outcome = "<success|partial|failed|blocked>";
1042
+ evalObj.blocker = "<required only when outcome=blocked>";
1043
+ evalObj.retroactiveClaims = [];
1044
+ }
637
1045
  // v2.8: Inject drift clarification field when previous round had drift flags
638
1046
  if (needsClarification) {
639
1047
  const flagTypes = [
640
1048
  hasIntentDrift ? "intent_drift" : "",
641
1049
  hasSubGoalDrift ? "subgoal_drift" : "",
642
1050
  ].filter(Boolean).join(" / ");
643
- report.discoveries = {
644
- ...report.discoveries,
645
- facts: [`Explain the evidence-backed pivot from ${flagTypes}`],
646
- };
647
- report.driftClarification = {
648
- explanation: "<explain why the current evidence requires this pivot>",
649
- anchors: ["<criterion ID, constraint ID, or observed file path>"],
650
- };
1051
+ evalObj.drift_clarification =
1052
+ `<explain why your Round ${round} actions diverged from your stated plan ` +
1053
+ `(${flagTypes} detected in previous round). ` +
1054
+ `Leave empty if no divergence occurred>`;
651
1055
  }
652
1056
  const lines = [
653
1057
  "### LoopForge Evaluation (Required)",
@@ -656,13 +1060,14 @@ export function buildRoundReportBlock(round, prevDriftFlags) {
656
1060
  "with your actual results and submit via `loopforge_next`.",
657
1061
  ];
658
1062
  if (needsClarification) {
659
- lines.push("", "> ⚠️ **Drift Detected:** The verification gate detected that your previous", "> round's previously suggested action did not match your actual output.", "> Explain the evidence-backed pivot in `driftClarification` below and include a runtime-verifiable anchor.", "> An honest explanation helps the enforcement gate distinguish intentional", "> course corrections from unacknowledged drift.");
1063
+ 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.");
660
1064
  }
661
- lines.push("Submit this object in the `report` parameter:");
662
- lines.push(JSON.stringify(report, null, 2));
1065
+ lines.push("---loopforge-eval");
1066
+ lines.push(JSON.stringify(evalObj, null, 2));
1067
+ lines.push("---end-loopforge-eval");
663
1068
  lines.push("");
664
1069
  lines.push("IMPORTANT: Replace every <placeholder> with your actual data.");
665
- lines.push("Set outcome=success only when every criterion has runtime-verifiable evidence; set continuation=stop only when no further round is requested.");
1070
+ lines.push("Set success=true only when the full goal and ALL hard constraints are verified.");
666
1071
  lines.push("Use IDs for exact matching: c-XXXXXXXX, cr-XXXXXXXX, sg-XXXXXXXX.");
667
1072
  lines.push("For subtasks, use sub-goal IDs from the Sub-Goal Dashboard above.");
668
1073
  return lines.join("\n");
@@ -674,10 +1079,14 @@ export function compileLoop(request, context) {
674
1079
  const decision = levelDecision(request, context);
675
1080
  const previous = getPreviousRound(request.loop_id, request.round - 1, context);
676
1081
  const objective = evolveObjective(request, context);
677
- const constraints = evolveConstraints(request, objective, previous);
678
- const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round);
679
- const rolling = buildRollingSummary(request.loop_id, request.round, context);
680
- const subGoals = deriveSubGoals(request.loop_id, request.round, request.last_round_view, context, entryProjection);
1082
+ const constraints = evolveConstraints(request, objective, previous, context);
1083
+ const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round, request.last_round_result?.constraint_violations ?? []);
1084
+ const rolling = buildRollingSummary(request.loop_id, request.round, context, 0, decision.level);
1085
+ const subGoals = manageSubGoals(request.loop_id, request.round, request.last_round_result, context);
1086
+ // v3.2: Goal → criteria → evidence vertical view (derived, zero persistence).
1087
+ const criterionStatuses = deriveCriterionStatuses(request.loop_id, context, objective, request.round, subGoals, request.last_round_result);
1088
+ // v3.2: Lessons learned — repeated violations / verification failures.
1089
+ const lessons = deriveLessons(request.loop_id, context, request.round);
681
1090
  const alignment = alignTask(request.task, request, context);
682
1091
  const health = checkLoopHealth(request.loop_id, { ...request, loop_objective: objective }, context);
683
1092
  const warnings = unique([
@@ -697,35 +1106,13 @@ export function compileLoop(request, context) {
697
1106
  .filter((e) => entryRound(e) >= 1 && entryRound(e) < request.round);
698
1107
  const trend = [];
699
1108
  for (const entry of completedEntries.slice(-10)) {
700
- const viols = entryProjection(entry)?.violations ?? [];
1109
+ const viols = Array.isArray(entry.constraint_violations)
1110
+ ? entry.constraint_violations.filter((v) => typeof v === "string")
1111
+ : [];
701
1112
  // Simple proxy: no violations = perfect trust, each violation = -0.1
702
1113
  const score = Math.max(0, 1.0 - viols.length * 0.1);
703
1114
  trend.push(Number(score.toFixed(2)));
704
1115
  }
705
- const verified = verifiedClaims(request.loop_id, context);
706
- const latestReport = [...loopEntries(request.loop_id, context)].reverse()
707
- .map((entry) => parseRoundReport(entry.round_report))
708
- .find((report) => report !== null);
709
- const audit = auditEntries(request.loop_id, loopEntries(request.loop_id, context));
710
- const regressionObligations = audit.regressionObligations.map((obligation) => ({
711
- id: obligation.id,
712
- description: `Re-run ${obligation.check}`,
713
- sourceRound: obligation.sourceRound,
714
- evidenceRefs: [`check:${obligation.check}`],
715
- status: obligation.status,
716
- }));
717
- const latestBlocker = latestReport?.outcome === "blocked" && latestReport.blocker;
718
- const blockerGate = latestBlocker
719
- ? latestBlocker.action
720
- ? deriveStructuredGate(latestBlocker.action)
721
- : deriveGate(latestBlocker.reason, latestBlocker.references)
722
- : null;
723
- const activeBlockers = latestBlocker && blockerGate?.gate.kind === "user" &&
724
- !audit.unresolvedUserGates.includes(blockerGate.id)
725
- ? []
726
- : latestBlocker
727
- ? [latestBlocker.reason]
728
- : [];
729
1116
  const response = makeLoopCompileResponse({
730
1117
  status: AgentStatus.OK,
731
1118
  recompile_level: decision.level,
@@ -744,20 +1131,12 @@ export function compileLoop(request, context) {
744
1131
  task_alignment: alignment,
745
1132
  rolling_summary: rolling,
746
1133
  sub_goals: subGoals,
1134
+ criterion_statuses: criterionStatuses,
1135
+ lessons,
747
1136
  agent_trust_score: request.round > 1 ? Number(trustScore.toFixed(2)) : undefined,
748
1137
  agent_trust_trend: request.round > 1 ? trend : [],
749
- // Agent intent is advisory only. Runtime suggestions are derived from
750
- // remaining criteria, active sub-goals and blockers; never copy intent
751
- // into the canonical next-action projection.
752
- projection: makeLoopCompileProjection({
753
- suggestedNextAction: request.last_round_view?.evidence?.criteriaRemaining?.[0] ||
754
- request.last_round_view?.discoveries?.emergedWork?.[0] || "",
755
- verifiedFacts: verified.facts,
756
- verifiedEvidenceRefs: verified.refs,
757
- regressionObligations,
758
- discoveredFacts: latestReport?.discoveries?.facts ?? [],
759
- activeBlockers,
760
- }),
1138
+ suggested_next_task: request.last_round_result?.next_action?.trim() ||
1139
+ request.last_round_result?.emerged_subtasks?.[0] || "",
761
1140
  plan_source: request.plan_source,
762
1141
  warnings,
763
1142
  });
@@ -787,36 +1166,16 @@ export function compileLoop(request, context) {
787
1166
  l2: adaptiveL2,
788
1167
  },
789
1168
  attempt: request.attempt,
790
- roundReportBlock: buildRoundReportBlock(request.round, request.verification_flags),
1169
+ selfEvaluationBlock: buildSelfEvalBlock(request.round, request.verification_flags, decision.level),
791
1170
  fullStateMarkdown: l2Pointer ? undefined : markdown,
792
- agentIntent: request.last_round_view?.agentIntent,
793
- promptRequests: request.last_round_view?.contextRequest ?? undefined,
794
- });
795
- const priorEntries = loopEntries(request.loop_id, context)
796
- .filter((entry) => entryRound(entry) < request.round);
797
- const previousL2 = [...priorEntries].reverse()
798
- .find((entry) => lineage(entry).recompile_level === "l2");
799
- artifact.lastL2Round = artifact.level === "l2"
800
- ? request.round
801
- : previousL2 ? entryRound(previousL2) : 0;
802
- const meaningfulEntry = [...priorEntries].reverse().find((entry) => {
803
- const data = lineage(entry);
804
- const evidence = entryProjection(entry)?.evidence;
805
- return Boolean(entryCriteriaMet(entry).length ||
806
- evidence?.observedFiles.length ||
807
- entryProjection(entry)?.discoveries?.newConstraints.length ||
808
- entryProjection(entry)?.discoveries?.emergedWork.length);
1171
+ // v2.9: Model's information needs from the previous round's SelfEvaluation
1172
+ promptRequests: request.last_round_result?.prompt_requests,
1173
+ // v3.2: Previous L1 presentation as the collapse diff baseline.
1174
+ presentedBaseline: readPresentedBaseline(request.loop_id, request.round, context),
809
1175
  });
810
- artifact.lastMeaningfulProgressRound = meaningfulEntry
811
- ? entryRound(meaningfulEntry)
812
- : request.round <= 1 ? 0 : 1;
813
- artifact.stateAgeRounds = artifact.lastMeaningfulProgressRound > 0
814
- ? Math.max(0, request.round - artifact.lastMeaningfulProgressRound)
815
- : request.round - 1;
816
1176
  response.prompt = artifact.renderedPrompt;
817
1177
  response.prompt_artifact = artifact;
818
1178
  response.state_file_content = policy.state_file.enabled ? markdown : undefined;
819
- response.canonical_state = state;
820
1179
  return response;
821
1180
  }
822
1181
  //# sourceMappingURL=loop-compiler.js.map