loopforge 2.11.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/README.md +145 -128
  2. package/dist/canonical-event-writer.d.ts +20 -0
  3. package/dist/canonical-event-writer.d.ts.map +1 -0
  4. package/dist/canonical-event-writer.js +131 -0
  5. package/dist/canonical-event-writer.js.map +1 -0
  6. package/dist/canonical-state.d.ts +20 -16
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +170 -61
  9. package/dist/canonical-state.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +11 -10
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cognitive-governance.d.ts +48 -0
  14. package/dist/cognitive-governance.d.ts.map +1 -0
  15. package/dist/cognitive-governance.js +226 -0
  16. package/dist/cognitive-governance.js.map +1 -0
  17. package/dist/compiler-subgoals.d.ts +7 -0
  18. package/dist/compiler-subgoals.d.ts.map +1 -0
  19. package/dist/compiler-subgoals.js +93 -0
  20. package/dist/compiler-subgoals.js.map +1 -0
  21. package/dist/derive-id.d.ts +12 -0
  22. package/dist/derive-id.d.ts.map +1 -0
  23. package/dist/derive-id.js +31 -0
  24. package/dist/derive-id.js.map +1 -0
  25. package/dist/evidence-provider.d.ts +23 -21
  26. package/dist/evidence-provider.d.ts.map +1 -1
  27. package/dist/evidence-provider.js +34 -74
  28. package/dist/evidence-provider.js.map +1 -1
  29. package/dist/generate-schema.js +90 -2
  30. package/dist/generate-schema.js.map +1 -1
  31. package/dist/index.d.ts +22 -19
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +15 -16
  34. package/dist/index.js.map +1 -1
  35. package/dist/loop-compiler.d.ts +1 -1
  36. package/dist/loop-compiler.d.ts.map +1 -1
  37. package/dist/loop-compiler.js +250 -337
  38. package/dist/loop-compiler.js.map +1 -1
  39. package/dist/loop-extras-parser.d.ts +2 -2
  40. package/dist/loop-extras-parser.d.ts.map +1 -1
  41. package/dist/loop-extras-parser.js +2 -2
  42. package/dist/loop-extras-parser.js.map +1 -1
  43. package/dist/loop-store.d.ts +20 -58
  44. package/dist/loop-store.d.ts.map +1 -1
  45. package/dist/loop-store.js +232 -309
  46. package/dist/loop-store.js.map +1 -1
  47. package/dist/mcp/server.d.ts +3 -3
  48. package/dist/mcp/server.d.ts.map +1 -1
  49. package/dist/mcp/server.js +83 -13
  50. package/dist/mcp/server.js.map +1 -1
  51. package/dist/mcp/session-projection.d.ts +5 -0
  52. package/dist/mcp/session-projection.d.ts.map +1 -0
  53. package/dist/mcp/session-projection.js +18 -0
  54. package/dist/mcp/session-projection.js.map +1 -0
  55. package/dist/mcp/session-queue.d.ts +3 -0
  56. package/dist/mcp/session-queue.d.ts.map +1 -0
  57. package/dist/mcp/session-queue.js +18 -0
  58. package/dist/mcp/session-queue.js.map +1 -0
  59. package/dist/mcp/session-types.d.ts +72 -0
  60. package/dist/mcp/session-types.d.ts.map +1 -0
  61. package/dist/mcp/session-types.js +2 -0
  62. package/dist/mcp/session-types.js.map +1 -0
  63. package/dist/mcp/session.d.ts +49 -93
  64. package/dist/mcp/session.d.ts.map +1 -1
  65. package/dist/mcp/session.js +544 -245
  66. package/dist/mcp/session.js.map +1 -1
  67. package/dist/mcp/tools.d.ts +26 -1
  68. package/dist/mcp/tools.d.ts.map +1 -1
  69. package/dist/mcp/tools.js +399 -274
  70. package/dist/mcp/tools.js.map +1 -1
  71. package/dist/policy-metrics.d.ts +17 -1
  72. package/dist/policy-metrics.d.ts.map +1 -1
  73. package/dist/policy-metrics.js +164 -1
  74. package/dist/policy-metrics.js.map +1 -1
  75. package/dist/policy.d.ts +34 -18
  76. package/dist/policy.d.ts.map +1 -1
  77. package/dist/policy.js +113 -18
  78. package/dist/policy.js.map +1 -1
  79. package/dist/prompt-assembler.d.ts +5 -3
  80. package/dist/prompt-assembler.d.ts.map +1 -1
  81. package/dist/prompt-assembler.js +126 -77
  82. package/dist/prompt-assembler.js.map +1 -1
  83. package/dist/prompt-policy.d.ts +8 -1
  84. package/dist/prompt-policy.d.ts.map +1 -1
  85. package/dist/prompt-policy.js +26 -8
  86. package/dist/prompt-policy.js.map +1 -1
  87. package/dist/protocol.d.ts +232 -192
  88. package/dist/protocol.d.ts.map +1 -1
  89. package/dist/protocol.js +15 -100
  90. package/dist/protocol.js.map +1 -1
  91. package/dist/replay.d.ts +11 -3
  92. package/dist/replay.d.ts.map +1 -1
  93. package/dist/replay.js +51 -14
  94. package/dist/replay.js.map +1 -1
  95. package/dist/round-commit-store.d.ts +12 -0
  96. package/dist/round-commit-store.d.ts.map +1 -0
  97. package/dist/round-commit-store.js +13 -0
  98. package/dist/round-commit-store.js.map +1 -0
  99. package/dist/round-coordinator.d.ts +25 -18
  100. package/dist/round-coordinator.d.ts.map +1 -1
  101. package/dist/round-coordinator.js +83 -71
  102. package/dist/round-coordinator.js.map +1 -1
  103. package/dist/round-driver.d.ts +14 -10
  104. package/dist/round-driver.d.ts.map +1 -1
  105. package/dist/round-driver.js +9 -13
  106. package/dist/round-driver.js.map +1 -1
  107. package/dist/round-gate.d.ts +31 -0
  108. package/dist/round-gate.d.ts.map +1 -0
  109. package/dist/round-gate.js +259 -0
  110. package/dist/round-gate.js.map +1 -0
  111. package/dist/round-recovery.d.ts +17 -0
  112. package/dist/round-recovery.d.ts.map +1 -0
  113. package/dist/round-recovery.js +117 -0
  114. package/dist/round-recovery.js.map +1 -0
  115. package/dist/round-report.d.ts +16 -0
  116. package/dist/round-report.d.ts.map +1 -0
  117. package/dist/round-report.js +401 -0
  118. package/dist/round-report.js.map +1 -0
  119. package/dist/round-transaction.d.ts +19 -12
  120. package/dist/round-transaction.d.ts.map +1 -1
  121. package/dist/round-transaction.js +102 -88
  122. package/dist/round-transaction.js.map +1 -1
  123. package/dist/runtime-services.d.ts +59 -0
  124. package/dist/runtime-services.d.ts.map +1 -0
  125. package/dist/runtime-services.js +460 -0
  126. package/dist/runtime-services.js.map +1 -0
  127. package/dist/storage.d.ts +3 -9
  128. package/dist/storage.d.ts.map +1 -1
  129. package/dist/storage.js +6 -17
  130. package/dist/storage.js.map +1 -1
  131. package/dist/token-utils.d.ts +9 -8
  132. package/dist/token-utils.d.ts.map +1 -1
  133. package/dist/token-utils.js +50 -10
  134. package/dist/token-utils.js.map +1 -1
  135. package/dist/vault-format.d.ts +4 -0
  136. package/dist/vault-format.d.ts.map +1 -0
  137. package/dist/vault-format.js +4 -0
  138. package/dist/vault-format.js.map +1 -0
  139. package/dist/vault-types.d.ts +24 -0
  140. package/dist/vault-types.d.ts.map +1 -0
  141. package/dist/vault-types.js +2 -0
  142. package/dist/vault-types.js.map +1 -0
  143. package/loop_policy.json +10 -12
  144. package/loopforge-policy.json +340 -0
  145. package/package.json +12 -6
  146. package/skills/perception/SKILL.md +199 -164
  147. package/dist/backends/interface.d.ts +0 -39
  148. package/dist/backends/interface.d.ts.map +0 -1
  149. package/dist/backends/interface.js +0 -6
  150. package/dist/backends/interface.js.map +0 -1
  151. package/dist/enforcement-gate.d.ts +0 -71
  152. package/dist/enforcement-gate.d.ts.map +0 -1
  153. package/dist/enforcement-gate.js +0 -744
  154. package/dist/enforcement-gate.js.map +0 -1
  155. package/dist/engine.d.ts +0 -59
  156. package/dist/engine.d.ts.map +0 -1
  157. package/dist/engine.js +0 -596
  158. package/dist/engine.js.map +0 -1
  159. package/dist/self-eval.d.ts +0 -23
  160. package/dist/self-eval.d.ts.map +0 -1
  161. package/dist/self-eval.js +0 -181
  162. package/dist/self-eval.js.map +0 -1
  163. package/dist/verification-gate.d.ts +0 -56
  164. package/dist/verification-gate.d.ts.map +0 -1
  165. package/dist/verification-gate.js +0 -563
  166. package/dist/verification-gate.js.map +0 -1
@@ -3,20 +3,23 @@
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 { createHash } from "node:crypto";
6
+ import { deriveConstraintId, deriveCriterionId, deriveTextHash, } from "./derive-id.js";
7
7
  import { getPolicy } from "./policy.js";
8
- import { AgentStatus, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeConstraintMeta, makeMilestoneSummary, makeRollingSummary, makeSubGoal, makeTaskAlignment, } from "./protocol.js";
8
+ import { AgentStatus, makeLoopCompileResponse, makeLoopCompileProjection, makeLoopHealth, makeLoopObjective, makeConstraintMeta, makeMilestoneSummary, makeRollingSummary, 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";
10
12
  import { assemblePromptArtifact } from "./prompt-assembler.js";
11
13
  import { decidePromptLevel, } from "./prompt-policy.js";
12
14
  import { jaccardSimilarity, unique, entryRound } from "./token-utils.js";
15
+ import { manageSubGoals as deriveSubGoals } from "./compiler-subgoals.js";
13
16
  function contextEntries(context) {
14
17
  if (!context || !Array.isArray(context.results))
15
18
  return [];
16
19
  return context.results.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
17
20
  }
18
21
  function lineage(entry) {
19
- const value = entry.loop_lineage ?? entry.lineage;
22
+ const value = entry.loop_lineage;
20
23
  return value !== null && typeof value === "object" && !Array.isArray(value)
21
24
  ? value
22
25
  : {};
@@ -26,9 +29,12 @@ function loopEntries(loopId, context) {
26
29
  .filter((entry) => entry.loop_id === loopId || lineage(entry).loop_id === loopId)
27
30
  .sort((a, b) => entryRound(a) - entryRound(b));
28
31
  }
32
+ function entryProjection(entry) {
33
+ const report = parseRoundReport(entry.round_report);
34
+ return report ? deriveCompilerProjection(report, entryRound(entry)) : null;
35
+ }
29
36
  export function computeGoalTextHash(text) {
30
- const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
31
- return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
37
+ return deriveTextHash(text, 12);
32
38
  }
33
39
  export function deriveGoalId(loopId, task, explicit = "") {
34
40
  return explicit.trim() || `${loopId}:${task.trim().toLowerCase()
@@ -55,9 +61,7 @@ export function getPreviousRound(loopId, round, context) {
55
61
  constraints_active: Array.isArray(data.constraints_active)
56
62
  ? data.constraints_active.filter((value) => typeof value === "string")
57
63
  : [],
58
- output_summary: typeof entry.output_summary === "string"
59
- ? entry.output_summary
60
- : typeof data.output_summary === "string" ? data.output_summary : "",
64
+ output_summary: entryProjection(entry)?.summary ?? "",
61
65
  };
62
66
  }
63
67
  function latestObjective(loopId, context) {
@@ -94,17 +98,17 @@ function evolveObjective(request, context) {
94
98
  created_at_round: 1,
95
99
  loop_id: request.loop_id,
96
100
  });
97
- const last = request.last_round_result;
101
+ const last = request.last_round_view;
98
102
  const history = [...(current.refinement_history ?? [])];
99
103
  let objective = current.objective || request.task;
100
104
  let version = current.version ?? 1;
101
- if (last?.objective_refinement?.trim()) {
102
- const refinement = last.objective_refinement.trim();
105
+ if (last?.objectiveRefinement?.trim()) {
106
+ const refinement = last.objectiveRefinement.trim();
103
107
  history.push(refinement);
104
108
  objective = `${objective}\nRefinement: ${refinement}`;
105
109
  version++;
106
110
  }
107
- const revisions = new Map((last?.revised_success_criteria ?? []).map((item) => [item.old, item.new]));
111
+ const revisions = new Map((last?.criterionRevisions ?? []).map((item) => [item.old, item.new]));
108
112
  return makeLoopObjective({
109
113
  ...current,
110
114
  objective,
@@ -116,9 +120,9 @@ function evolveObjective(request, context) {
116
120
  });
117
121
  }
118
122
  function evolveConstraints(request, objective, previous) {
119
- const retired = unique(request.last_round_result?.retracted_constraints ?? []);
123
+ const retired = unique(request.last_round_view?.constraintEvents?.retired ?? []);
120
124
  const retiredSet = new Set(retired);
121
- const discovered = unique(request.last_round_result?.discovered_constraints ?? [])
125
+ const discovered = unique(request.last_round_view?.discoveries?.newConstraints ?? [])
122
126
  .slice(0, getPolicy().evolution.max_discovered_constraints_per_round);
123
127
  const active = unique([
124
128
  ...(previous?.constraints_active ?? []),
@@ -133,27 +137,48 @@ function evolveConstraints(request, objective, previous) {
133
137
  // ═══════════════════════════════════════════════════════════════════════════
134
138
  // Hierarchical Summary (v2.1)
135
139
  // ═══════════════════════════════════════════════════════════════════════════
136
- /** Extract execution_evidence from a vault entry, handling both direct-field
137
- * and lineage-nested shapes. Returns null when absent. */
138
- function entryExecutionEvidence(entry) {
139
- const direct = entry.execution_evidence;
140
- if (direct && typeof direct === "object" && !Array.isArray(direct)) {
141
- return direct;
142
- }
143
- const lin = lineage(entry);
144
- const nested = lin.execution_evidence;
145
- if (nested && typeof nested === "object" && !Array.isArray(nested)) {
146
- return nested;
147
- }
148
- return null;
149
- }
150
- /** Read success_criteria_met from an entry's execution_evidence. */
151
140
  function entryCriteriaMet(entry) {
152
- const ev = entryExecutionEvidence(entry);
153
- if (!ev)
154
- return [];
155
- const arr = ev.success_criteria_met;
156
- return Array.isArray(arr) ? arr.filter((v) => typeof v === "string") : [];
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)
175
+ continue;
176
+ facts.add(claim.targetId);
177
+ for (const ref of verifiedRefs)
178
+ refs.add(ref);
179
+ }
180
+ }
181
+ return { facts: [...facts].sort(), refs: [...refs].sort() };
157
182
  }
158
183
  /** v2.11: Match two criterion references for deduplication.
159
184
  * If either is a criterion ID (cr-XXXXXXXX), uses exact ID comparison.
@@ -172,6 +197,9 @@ function criteriaMatch(a, b) {
172
197
  // Neither is an ID — Jaccard fallback
173
198
  return jaccardSimilarity(a, b) >= getPolicy().evolution.criteria_dedup_threshold;
174
199
  }
200
+ function isCriterionId(ref) {
201
+ return /^cr-[a-f0-9]{8}$/.test(ref);
202
+ }
175
203
  /** Detect newly met criteria by comparing the current entry's
176
204
  * success_criteria_met against the previous entry's.
177
205
  * v2.11: ID-first matching (cr-XXXXXXXX) with Jaccard similarity
@@ -199,29 +227,15 @@ function entryActiveConstraints(entry) {
199
227
  }
200
228
  /** Read retracted_constraints from an entry. */
201
229
  function entryRetractedConstraints(entry) {
202
- const direct = entry.retracted_constraints;
203
- if (Array.isArray(direct))
204
- return direct.filter((v) => typeof v === "string");
205
- const lin = lineage(entry);
206
- const nested = lin.retracted_constraints;
207
- if (Array.isArray(nested))
208
- return nested.filter((v) => typeof v === "string");
209
- return [];
230
+ return entryProjection(entry)?.constraintEvents?.retired ?? [];
210
231
  }
211
- /** Read progress_estimate from an entry's execution_evidence. */
212
232
  function entryProgressEstimate(entry) {
213
- const ev = entryExecutionEvidence(entry);
214
- if (!ev)
215
- return 0;
216
- const pe = ev.progress_estimate;
217
- return typeof pe === "number" ? pe : 0;
233
+ return entryProjection(entry)?.evidence?.progressRatio ?? 0;
218
234
  }
219
235
  /** Build a single MilestoneSummary from a range of completed round entries. */
220
236
  function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, kind) {
221
237
  const last = phaseEntries.at(-1);
222
- const outcomeRaw = last
223
- ? (typeof last.output_summary === "string" ? last.output_summary : "")
224
- : "";
238
+ const outcomeRaw = last ? entryProjection(last)?.summary ?? "" : "";
225
239
  const outcomeMax = getPolicy().checkpoint.outcome_max_chars;
226
240
  const outcome = outcomeRaw.length > outcomeMax
227
241
  ? outcomeRaw.slice(0, outcomeMax) + "…"
@@ -256,28 +270,48 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
256
270
  const data = lineage(entry);
257
271
  const round = entryRound(entry);
258
272
  const success = entry.success ?? data.success;
259
- const summary = typeof entry.output_summary === "string"
260
- ? entry.output_summary
261
- : typeof data.output_summary === "string" ? data.output_summary : "";
273
+ const summary = entryProjection(entry)?.summary ?? "";
262
274
  if (summary)
263
275
  outcomes.push(`[R${round}] ${success === false ? "failed" : "accepted"}: ${summary}`);
264
- const violations = Array.isArray(entry.constraint_violations)
265
- ? entry.constraint_violations
266
- : Array.isArray(data.constraint_violations) ? data.constraint_violations : [];
276
+ const violations = entryProjection(entry)?.violations ?? [];
267
277
  issues.push(...violations.filter((item) => typeof item === "string"));
268
278
  }
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
+ }
269
287
  // ── Phase 2: Milestone Accumulation ──
270
288
  // Scan all completed entries in round order. For each entry, check three
271
289
  // trigger signals in priority order: agent_declared > criteria_milestone > auto.
272
290
  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
+ }
273
305
  const milestones = [];
274
306
  let lastMilestoneRound = 0;
275
307
  for (let i = 0; i < completedEntries.length; i++) {
276
308
  const entry = completedEntries[i];
277
309
  const rnd = entryRound(entry);
278
310
  const lin = lineage(entry);
311
+ const report = parseRoundReport(entry.round_report);
279
312
  // Signal 1: Agent-declared checkpoint
280
- const isAgentCheckpoint = lin.compression_checkpoint === true;
313
+ const isAgentCheckpoint = Boolean(report?.phaseBoundaryProposal) ||
314
+ lin.compression_checkpoint === true;
281
315
  // Signal 2: New criteria met (compare with previous vault entry)
282
316
  const prev = i > 0 ? completedEntries[i - 1] : null;
283
317
  const newCriteria = detectNewCriteria(entry, prev);
@@ -291,9 +325,10 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
291
325
  ? "agent_declared"
292
326
  : "criteria_milestone";
293
327
  const label = isAgentCheckpoint
294
- ? (typeof lin.checkpoint_label === "string" && lin.checkpoint_label.trim()
295
- ? lin.checkpoint_label.trim()
296
- : `Round ${rnd}`)
328
+ ? (report?.phaseBoundaryProposal?.label?.trim() ||
329
+ (typeof lin.checkpoint_label === "string" && lin.checkpoint_label.trim()
330
+ ? lin.checkpoint_label.trim()
331
+ : `Round ${rnd}`))
297
332
  : `Completed: ${newCriteria.join(", ")}`;
298
333
  milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, rnd, label, kind));
299
334
  lastMilestoneRound = rnd;
@@ -336,234 +371,20 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
336
371
  return null;
337
372
  return makeRollingSummary({
338
373
  key_outcomes: unique(outcomes),
339
- recurring_issues: unique(issues),
374
+ recurring_issues: [...issueCounts.values()]
375
+ .filter((issue) => issue.count >= 2)
376
+ .map((issue) => `${issue.text} (${issue.count} rounds)`),
340
377
  rounds_sampled: windowEntries.length,
341
378
  generated_at_round: currentRound,
342
- failed_patterns: [],
379
+ failed_patterns: failedGroups
380
+ .filter((failure) => failure.count >= 2)
381
+ .map((failure) => `${failure.text} (${failure.count} attempts)`),
343
382
  milestones: cappedMilestones,
344
383
  loop_synthesis: loopSynthesis || undefined,
345
384
  });
346
385
  }
347
- // ═══════════════════════════════════════════════════════════════════════════
348
- // Sub-Goal Structured Tracking (v2.2)
349
- // ═══════════════════════════════════════════════════════════════════════════
350
- /** Derive a stable sub-goal ID from its description hash. */
351
- function deriveSubGoalId(description) {
352
- return "sg-" + computeGoalTextHash(description).slice(0, 8);
353
- }
354
- /** v2.11: Derive a stable constraint ID from its text hash (c-XXXXXXXX).
355
- * Same hash strategy as SubGoal — deterministic across rounds. */
356
- function deriveConstraintId(text) {
357
- return "c-" + computeGoalTextHash(text).slice(0, 8);
358
- }
359
- /** v2.11: Derive a stable criterion ID from its text hash (cr-XXXXXXXX).
360
- * Same hash strategy as SubGoal — deterministic across rounds. */
361
- function deriveCriterionId(text) {
362
- return "cr-" + computeGoalTextHash(text).slice(0, 8);
363
- }
364
- /** v2.11: Check whether a user-provided reference looks like a constraint ID.
365
- * Matches the pattern c-XXXXXXXX where X is a hex digit. */
366
- function isConstraintId(ref) {
367
- return /^c-[a-f0-9]{8}$/.test(ref);
368
- }
369
- /** v2.11: Check whether a user-provided reference looks like a criterion ID.
370
- * Matches the pattern cr-XXXXXXXX where X is a hex digit. */
371
- function isCriterionId(ref) {
372
- return /^cr-[a-f0-9]{8}$/.test(ref);
373
- }
374
- /** Read emerged_subtasks from a vault entry (handles direct + lineage nesting). */
375
- function entryEmergedSubtasks(entry) {
376
- const direct = entry.emerged_subtasks;
377
- if (Array.isArray(direct))
378
- return direct.filter((v) => typeof v === "string");
379
- const lin = lineage(entry);
380
- const nested = lin.emerged_subtasks;
381
- if (Array.isArray(nested))
382
- return nested.filter((v) => typeof v === "string");
383
- return [];
384
- }
385
- /** Read completed_subtasks from an entry. */
386
- function entryCompletedSubtasks(entry) {
387
- const direct = entry.completed_subtasks;
388
- if (Array.isArray(direct))
389
- return direct.filter((v) => typeof v === "string");
390
- return [];
391
- }
392
- /** Read blocked_subtasks from an entry. */
393
- function entryBlockedSubtasks(entry) {
394
- const direct = entry.blocked_subtasks;
395
- if (Array.isArray(direct))
396
- return direct.filter((v) => typeof v === "string");
397
- return [];
398
- }
399
- /** Read canceled_subtasks from an entry. */
400
- function entryCanceledSubtasks(entry) {
401
- const direct = entry.canceled_subtasks;
402
- if (Array.isArray(direct))
403
- return direct.filter((v) => typeof v === "string");
404
- return [];
405
- }
406
- /**
407
- * Match a user-provided sub-task reference against existing SubGoals.
408
- *
409
- * Tries exact SubGoal ID match first (e.g. "sg-a3f2b1c0"), then falls
410
- * back to Jaccard token similarity on descriptions. Returns the index
411
- * of the best-matching SubGoal, or -1 when no match reaches threshold.
412
- */
413
- function matchSubGoal(target, subGoals, threshold) {
414
- const trimmed = target.trim();
415
- // Phase 1: Exact ID match
416
- for (let i = 0; i < subGoals.length; i++) {
417
- if (subGoals[i].id === trimmed)
418
- return i;
419
- }
420
- // Phase 2: Jaccard fallback on description text
421
- let bestIdx = -1;
422
- let bestScore = 0;
423
- for (let i = 0; i < subGoals.length; i++) {
424
- const score = jaccardSimilarity(trimmed, subGoals[i].description);
425
- if (score > bestScore) {
426
- bestScore = score;
427
- bestIdx = i;
428
- }
429
- }
430
- return bestScore >= threshold ? bestIdx : -1;
431
- }
432
- /**
433
- * Manage the full sub-goal lifecycle across rounds.
434
- *
435
- * Accumulates sub-goals from all prior vault entries, processes the current
436
- * round's declarations, derives status changes, and returns the complete
437
- * structured SubGoal list.
438
- *
439
- * Status derivation priority (compiler-driven):
440
- * 1. Agent declared done/canceled/blocked → explicit status change
441
- * 2. next_action match → auto in_progress
442
- * 3. success_criteria_met match → auto done
443
- */
444
- function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
445
- const policy = getPolicy().evolution;
446
- const allEntries = loopEntries(loopId, vaultContext);
447
- const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
448
- const subGoals = [];
449
- // Phase 1 — Accumulate historical sub-goals from vault entries
450
- for (const entry of completedEntries) {
451
- const rnd = entryRound(entry);
452
- const emerged = entryEmergedSubtasks(entry);
453
- for (const desc of emerged) {
454
- const id = deriveSubGoalId(desc);
455
- // Dedup: skip if an existing sub-goal is too similar
456
- if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
457
- continue;
458
- subGoals.push(makeSubGoal({
459
- id,
460
- description: desc.trim(),
461
- status: "pending",
462
- declared_at_round: rnd,
463
- status_changed_at_round: rnd,
464
- priority: subGoals.length,
465
- }));
466
- }
467
- }
468
- // Phase 2 — Process current round's sub-task declarations
469
- if (lastRoundResult) {
470
- const rnd = lastRoundResult.round || (currentRound - 1);
471
- // New sub-goals from this round
472
- const emerged = lastRoundResult.emerged_subtasks ?? [];
473
- for (const desc of emerged) {
474
- if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
475
- continue;
476
- subGoals.push(makeSubGoal({
477
- id: deriveSubGoalId(desc),
478
- description: desc.trim(),
479
- status: "pending",
480
- declared_at_round: rnd,
481
- status_changed_at_round: rnd,
482
- priority: subGoals.length,
483
- }));
484
- }
485
- // Completed sub-goals
486
- const completed = lastRoundResult.completed_subtasks ?? [];
487
- for (const desc of completed) {
488
- const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
489
- if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
490
- subGoals[idx].status = "done";
491
- subGoals[idx].status_changed_at_round = rnd;
492
- subGoals[idx].completed_at_round = rnd;
493
- }
494
- }
495
- // Blocked sub-goals
496
- const blocked = lastRoundResult.blocked_subtasks ?? [];
497
- for (const desc of blocked) {
498
- const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
499
- if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
500
- subGoals[idx].status = "blocked";
501
- subGoals[idx].status_changed_at_round = rnd;
502
- }
503
- }
504
- // Canceled sub-goals
505
- const canceled = lastRoundResult.canceled_subtasks ?? [];
506
- for (const desc of canceled) {
507
- const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
508
- if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
509
- subGoals[idx].status = "canceled";
510
- subGoals[idx].status_changed_at_round = rnd;
511
- }
512
- }
513
- // Phase 3 — Auto-derive: next_action match → in_progress
514
- const nextAction = lastRoundResult.next_action?.trim();
515
- if (nextAction) {
516
- for (const sg of subGoals) {
517
- if (sg.status === "pending") {
518
- if (jaccardSimilarity(nextAction, sg.description) >= policy.subgoal_auto_in_progress_threshold) {
519
- sg.status = "in_progress";
520
- sg.status_changed_at_round = rnd;
521
- break; // Only one sub-goal can be in_progress at a time
522
- }
523
- }
524
- }
525
- }
526
- // Phase 4 — Auto-complete: new criteria match → done
527
- const newCriteria = lastRoundResult.execution_evidence?.success_criteria_met ?? [];
528
- if (newCriteria.length > 0) {
529
- for (const sg of subGoals) {
530
- if (sg.status === "pending" || sg.status === "in_progress") {
531
- for (const crit of newCriteria) {
532
- if (jaccardSimilarity(crit, sg.description) >= policy.subgoal_auto_complete_threshold) {
533
- sg.status = "done";
534
- sg.status_changed_at_round = rnd;
535
- sg.completed_at_round = rnd;
536
- break;
537
- }
538
- }
539
- }
540
- }
541
- }
542
- }
543
- // Sort for display: in_progress first, then pending by age desc, then blocked, then done, then canceled
544
- subGoals.sort((a, b) => {
545
- const rank = (s) => {
546
- switch (s.status) {
547
- case "in_progress": return 0;
548
- case "pending": return 1;
549
- case "blocked": return 2;
550
- case "done": return 3;
551
- case "canceled": return 4;
552
- }
553
- };
554
- const ra = rank(a), rb = rank(b);
555
- if (ra !== rb)
556
- return ra - rb;
557
- // Within same status: pending by oldest first, done by most recent first
558
- if (a.status === "pending")
559
- return (currentRound - a.declared_at_round) - (currentRound - b.declared_at_round);
560
- if (a.status === "done")
561
- return (b.completed_at_round ?? 0) - (a.completed_at_round ?? 0);
562
- return a.declared_at_round - b.declared_at_round;
563
- });
564
- return subGoals;
565
- }
566
- // ═══════════════════════════════════════════════════════════════════════════
386
+ // Sub-goal lifecycle is implemented in compiler-subgoals.ts; this facade keeps
387
+ // the compiler's public entry point and projection flow unchanged.
567
388
  // Time-Aware Constraint Management (v2.3)
568
389
  // ═══════════════════════════════════════════════════════════════════════════
569
390
  /** Determine the source of a constraint by matching against known sources. */
@@ -588,9 +409,7 @@ function findDiscoveredRound(text, vaultEntries) {
588
409
  const rnd = entryRound(entry);
589
410
  if (rnd < 1)
590
411
  continue;
591
- const discovered = Array.isArray(entry.discovered_constraints)
592
- ? entry.discovered_constraints.filter((v) => typeof v === "string")
593
- : [];
412
+ const discovered = entryProjection(entry)?.discoveries?.newConstraints ?? [];
594
413
  for (const d of discovered) {
595
414
  // Phase 1: exact ID match (v2.11)
596
415
  if (d === targetId || deriveConstraintId(d) === targetId) {
@@ -619,9 +438,7 @@ function findLastViolatedRound(text, vaultEntries) {
619
438
  const rnd = entryRound(entry);
620
439
  if (rnd < 1)
621
440
  continue;
622
- const violations = Array.isArray(entry.constraint_violations)
623
- ? entry.constraint_violations.filter((v) => typeof v === "string")
624
- : [];
441
+ const violations = entryProjection(entry)?.violations ?? [];
625
442
  for (const v of violations) {
626
443
  // Phase 1: exact ID match (v2.11)
627
444
  if (v === targetId || deriveConstraintId(v) === targetId) {
@@ -707,7 +524,7 @@ export function alignTask(proposedTask, request, context) {
707
524
  export function checkLoopHealth(loopId, request, context) {
708
525
  const previous = getPreviousRound(loopId, request.round - 1, context);
709
526
  const alignment = alignTask(request.task, request, context);
710
- const violations = request.last_round_result?.constraint_violations.length ?? 0;
527
+ const violations = request.last_round_view?.violations?.length ?? 0;
711
528
  const integrity = Math.max(0, 1 - violations * 0.2);
712
529
  const continuity = previous ? jaccardSimilarity(previous.task, request.task) : 1;
713
530
  const drift = alignment.alignment_score < 0.3 || continuity < 0.2;
@@ -722,61 +539,100 @@ export function checkLoopHealth(loopId, request, context) {
722
539
  }
723
540
  function levelDecision(request, context) {
724
541
  const previous = getPreviousRound(request.loop_id, request.round - 1, context);
725
- const last = request.last_round_result;
542
+ const last = request.last_round_view;
726
543
  const hasNewInformation = Boolean(request.new_since_last_round.trim() ||
727
- last?.discovered_constraints?.length ||
728
- last?.objective_refinement?.trim() ||
729
- last?.emerged_subtasks?.length ||
730
- last?.retracted_constraints?.length ||
731
- last?.revised_success_criteria?.length ||
732
- last?.wrong_assumptions?.length);
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);
733
551
  const lastFullRound = loopEntries(request.loop_id, context)
734
552
  .filter((entry) => lineage(entry).recompile_level === "l2")
735
553
  .map(entryRound)
736
554
  .filter((round) => round < request.round)
737
555
  .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;
738
576
  return decidePromptLevel({
739
577
  round: request.round,
740
578
  attempt: request.attempt,
741
579
  forceLevel: request.force_level,
742
580
  hasPlanSource: Boolean(request.plan_source),
743
- checkpointBoundary: last?.compression_checkpoint === true,
744
- goalChanged: previous !== null && previous.goal_id !== deriveGoalId(request.loop_id, request.task, request.goal_id),
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),
745
585
  previousStateMissing: request.round > 1 && previous === null,
746
- previousFailedWithoutNewInformation: last?.success === false && !hasNewInformation,
586
+ previousFailedWithoutNewInformation: last?.outcome === "failed" && !hasNewInformation,
747
587
  verificationContradicted: (request.verification_flags ?? [])
748
588
  .some((flag) => flag.severity === "error"),
749
589
  consecutiveRejections: request.consecutive_rejections,
750
590
  fullRefreshInterval: getPolicy().prompt.full_refresh_interval,
751
591
  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,
752
604
  });
753
605
  }
754
606
  export function decideLevel(request, context) {
755
607
  return levelDecision(request, context).level;
756
608
  }
757
- export function buildSelfEvalBlock(round, prevDriftFlags) {
609
+ export function buildRoundReportBlock(round, prevDriftFlags) {
758
610
  const hasIntentDrift = prevDriftFlags?.some(f => f.check === "intent_drift") ?? false;
759
611
  const hasSubGoalDrift = prevDriftFlags?.some(f => f.check === "subgoal_drift") ?? false;
760
612
  const needsClarification = hasIntentDrift || hasSubGoalDrift;
761
- const evalObj = {
762
- success: false,
763
- output_summary: `<verified result of round ${round}>`,
764
- constraint_violations: ["<c-XXXXXXXX or text>"],
765
- should_continue: true,
766
- discovered_constraints: ["<new constraint>"],
767
- emerged_subtasks: [],
768
- execution_evidence: {
769
- files_changed: [],
770
- test_results: { passed: 0, failed: 0, skipped: 0 },
771
- success_criteria_met: ["<cr-XXXXXXXX or text>"],
772
- success_criteria_remaining: ["<cr-XXXXXXXX or text>"],
773
- progress_estimate: 0,
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: "",
628
+ },
629
+ discoveries: {
630
+ wrongAssumptions: [],
631
+ emergedWork: [],
632
+ facts: [],
633
+ newConstraints: [],
774
634
  },
775
- wrong_assumptions: [],
776
- next_action: "<next concrete action, or empty when complete>",
777
- completed_subtasks: ["<sg-XXXXXXXX or text>"],
778
- blocked_subtasks: ["<sg-XXXXXXXX or text>"],
779
- canceled_subtasks: ["<sg-XXXXXXXX or text>"],
635
+ subGoalEvents: { completed: [], blocked: [], canceled: [] },
780
636
  };
781
637
  // v2.8: Inject drift clarification field when previous round had drift flags
782
638
  if (needsClarification) {
@@ -784,10 +640,14 @@ export function buildSelfEvalBlock(round, prevDriftFlags) {
784
640
  hasIntentDrift ? "intent_drift" : "",
785
641
  hasSubGoalDrift ? "subgoal_drift" : "",
786
642
  ].filter(Boolean).join(" / ");
787
- evalObj.drift_clarification =
788
- `<explain why your Round ${round} actions diverged from your stated plan ` +
789
- `(${flagTypes} detected in previous round). ` +
790
- `Leave empty if no divergence occurred>`;
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
+ };
791
651
  }
792
652
  const lines = [
793
653
  "### LoopForge Evaluation (Required)",
@@ -796,14 +656,13 @@ export function buildSelfEvalBlock(round, prevDriftFlags) {
796
656
  "with your actual results and submit via `loopforge_next`.",
797
657
  ];
798
658
  if (needsClarification) {
799
- 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.");
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.");
800
660
  }
801
- lines.push("---loopforge-eval");
802
- lines.push(JSON.stringify(evalObj, null, 2));
803
- lines.push("---end-loopforge-eval");
661
+ lines.push("Submit this object in the `report` parameter:");
662
+ lines.push(JSON.stringify(report, null, 2));
804
663
  lines.push("");
805
664
  lines.push("IMPORTANT: Replace every <placeholder> with your actual data.");
806
- lines.push("Set success=true only when the full goal and ALL hard constraints are verified.");
665
+ lines.push("Set outcome=success only when every criterion has runtime-verifiable evidence; set continuation=stop only when no further round is requested.");
807
666
  lines.push("Use IDs for exact matching: c-XXXXXXXX, cr-XXXXXXXX, sg-XXXXXXXX.");
808
667
  lines.push("For subtasks, use sub-goal IDs from the Sub-Goal Dashboard above.");
809
668
  return lines.join("\n");
@@ -818,7 +677,7 @@ export function compileLoop(request, context) {
818
677
  const constraints = evolveConstraints(request, objective, previous);
819
678
  const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round);
820
679
  const rolling = buildRollingSummary(request.loop_id, request.round, context);
821
- const subGoals = manageSubGoals(request.loop_id, request.round, request.last_round_result, context);
680
+ const subGoals = deriveSubGoals(request.loop_id, request.round, request.last_round_view, context, entryProjection);
822
681
  const alignment = alignTask(request.task, request, context);
823
682
  const health = checkLoopHealth(request.loop_id, { ...request, loop_objective: objective }, context);
824
683
  const warnings = unique([
@@ -838,13 +697,35 @@ export function compileLoop(request, context) {
838
697
  .filter((e) => entryRound(e) >= 1 && entryRound(e) < request.round);
839
698
  const trend = [];
840
699
  for (const entry of completedEntries.slice(-10)) {
841
- const viols = Array.isArray(entry.constraint_violations)
842
- ? entry.constraint_violations.filter((v) => typeof v === "string")
843
- : [];
700
+ const viols = entryProjection(entry)?.violations ?? [];
844
701
  // Simple proxy: no violations = perfect trust, each violation = -0.1
845
702
  const score = Math.max(0, 1.0 - viols.length * 0.1);
846
703
  trend.push(Number(score.toFixed(2)));
847
704
  }
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
+ : [];
848
729
  const response = makeLoopCompileResponse({
849
730
  status: AgentStatus.OK,
850
731
  recompile_level: decision.level,
@@ -865,8 +746,18 @@ export function compileLoop(request, context) {
865
746
  sub_goals: subGoals,
866
747
  agent_trust_score: request.round > 1 ? Number(trustScore.toFixed(2)) : undefined,
867
748
  agent_trust_trend: request.round > 1 ? trend : [],
868
- suggested_next_task: request.last_round_result?.next_action?.trim() ||
869
- request.last_round_result?.emerged_subtasks?.[0] || "",
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
+ }),
870
761
  plan_source: request.plan_source,
871
762
  warnings,
872
763
  });
@@ -896,14 +787,36 @@ export function compileLoop(request, context) {
896
787
  l2: adaptiveL2,
897
788
  },
898
789
  attempt: request.attempt,
899
- selfEvaluationBlock: buildSelfEvalBlock(request.round, request.verification_flags),
790
+ roundReportBlock: buildRoundReportBlock(request.round, request.verification_flags),
900
791
  fullStateMarkdown: l2Pointer ? undefined : markdown,
901
- // v2.9: Model's information needs from the previous round's SelfEvaluation
902
- promptRequests: request.last_round_result?.prompt_requests,
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);
903
809
  });
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;
904
816
  response.prompt = artifact.renderedPrompt;
905
817
  response.prompt_artifact = artifact;
906
818
  response.state_file_content = policy.state_file.enabled ? markdown : undefined;
819
+ response.canonical_state = state;
907
820
  return response;
908
821
  }
909
822
  //# sourceMappingURL=loop-compiler.js.map