loopforge 2.7.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 (173) hide show
  1. package/README.md +136 -182
  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 +211 -64
  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 +26 -26
  26. package/dist/evidence-provider.d.ts.map +1 -1
  27. package/dist/evidence-provider.js +45 -71
  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 +23 -29
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +16 -26
  34. package/dist/index.js.map +1 -1
  35. package/dist/loop-compiler.d.ts +2 -2
  36. package/dist/loop-compiler.d.ts.map +1 -1
  37. package/dist/loop-compiler.js +321 -315
  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 -311
  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 +123 -19
  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 +65 -84
  64. package/dist/mcp/session.d.ts.map +1 -1
  65. package/dist/mcp/session.js +667 -224
  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 +401 -254
  70. package/dist/mcp/tools.js.map +1 -1
  71. package/dist/policy-metrics.d.ts +24 -0
  72. package/dist/policy-metrics.d.ts.map +1 -1
  73. package/dist/policy-metrics.js +174 -0
  74. package/dist/policy-metrics.js.map +1 -1
  75. package/dist/policy.d.ts +74 -12
  76. package/dist/policy.d.ts.map +1 -1
  77. package/dist/policy.js +124 -19
  78. package/dist/policy.js.map +1 -1
  79. package/dist/prompt-assembler.d.ts +8 -4
  80. package/dist/prompt-assembler.d.ts.map +1 -1
  81. package/dist/prompt-assembler.js +533 -86
  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 +259 -173
  88. package/dist/protocol.d.ts.map +1 -1
  89. package/dist/protocol.js +17 -96
  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 +53 -22
  100. package/dist/round-coordinator.d.ts.map +1 -1
  101. package/dist/round-coordinator.js +140 -44
  102. package/dist/round-coordinator.js.map +1 -1
  103. package/dist/round-driver.d.ts +18 -10
  104. package/dist/round-driver.d.ts.map +1 -1
  105. package/dist/round-driver.js +11 -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 +23 -12
  120. package/dist/round-transaction.d.ts.map +1 -1
  121. package/dist/round-transaction.js +104 -91
  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/{engine.js → runtime-services.js} +157 -275
  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 +12 -8
  132. package/dist/token-utils.d.ts.map +1 -1
  133. package/dist/token-utils.js +56 -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 +13 -7
  146. package/skills/perception/SKILL.md +200 -90
  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 -50
  152. package/dist/enforcement-gate.d.ts.map +0 -1
  153. package/dist/enforcement-gate.js +0 -501
  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.map +0 -1
  158. package/dist/interop.d.ts +0 -35
  159. package/dist/interop.d.ts.map +0 -1
  160. package/dist/interop.js +0 -34
  161. package/dist/interop.js.map +0 -1
  162. package/dist/run.d.ts +0 -51
  163. package/dist/run.d.ts.map +0 -1
  164. package/dist/run.js +0 -200
  165. package/dist/run.js.map +0 -1
  166. package/dist/self-eval.d.ts +0 -23
  167. package/dist/self-eval.d.ts.map +0 -1
  168. package/dist/self-eval.js +0 -151
  169. package/dist/self-eval.js.map +0 -1
  170. package/dist/verification-gate.d.ts +0 -52
  171. package/dist/verification-gate.d.ts.map +0 -1
  172. package/dist/verification-gate.js +0 -527
  173. package/dist/verification-gate.js.map +0 -1
@@ -3,34 +3,38 @@
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";
13
- // unique() imported from token-utils.ts
15
+ import { manageSubGoals as deriveSubGoals } from "./compiler-subgoals.js";
14
16
  function contextEntries(context) {
15
17
  if (!context || !Array.isArray(context.results))
16
18
  return [];
17
19
  return context.results.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
18
20
  }
19
21
  function lineage(entry) {
20
- const value = entry.loop_lineage ?? entry.lineage;
22
+ const value = entry.loop_lineage;
21
23
  return value !== null && typeof value === "object" && !Array.isArray(value)
22
24
  ? value
23
25
  : {};
24
26
  }
25
- // entryRound() imported from token-utils.ts
26
27
  function loopEntries(loopId, context) {
27
28
  return contextEntries(context)
28
29
  .filter((entry) => entry.loop_id === loopId || lineage(entry).loop_id === loopId)
29
30
  .sort((a, b) => entryRound(a) - entryRound(b));
30
31
  }
32
+ function entryProjection(entry) {
33
+ const report = parseRoundReport(entry.round_report);
34
+ return report ? deriveCompilerProjection(report, entryRound(entry)) : null;
35
+ }
31
36
  export function computeGoalTextHash(text) {
32
- const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
33
- return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
37
+ return deriveTextHash(text, 12);
34
38
  }
35
39
  export function deriveGoalId(loopId, task, explicit = "") {
36
40
  return explicit.trim() || `${loopId}:${task.trim().toLowerCase()
@@ -57,9 +61,7 @@ export function getPreviousRound(loopId, round, context) {
57
61
  constraints_active: Array.isArray(data.constraints_active)
58
62
  ? data.constraints_active.filter((value) => typeof value === "string")
59
63
  : [],
60
- output_summary: typeof entry.output_summary === "string"
61
- ? entry.output_summary
62
- : typeof data.output_summary === "string" ? data.output_summary : "",
64
+ output_summary: entryProjection(entry)?.summary ?? "",
63
65
  };
64
66
  }
65
67
  function latestObjective(loopId, context) {
@@ -96,17 +98,17 @@ function evolveObjective(request, context) {
96
98
  created_at_round: 1,
97
99
  loop_id: request.loop_id,
98
100
  });
99
- const last = request.last_round_result;
101
+ const last = request.last_round_view;
100
102
  const history = [...(current.refinement_history ?? [])];
101
103
  let objective = current.objective || request.task;
102
104
  let version = current.version ?? 1;
103
- if (last?.objective_refinement?.trim()) {
104
- const refinement = last.objective_refinement.trim();
105
+ if (last?.objectiveRefinement?.trim()) {
106
+ const refinement = last.objectiveRefinement.trim();
105
107
  history.push(refinement);
106
108
  objective = `${objective}\nRefinement: ${refinement}`;
107
109
  version++;
108
110
  }
109
- 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]));
110
112
  return makeLoopObjective({
111
113
  ...current,
112
114
  objective,
@@ -118,9 +120,9 @@ function evolveObjective(request, context) {
118
120
  });
119
121
  }
120
122
  function evolveConstraints(request, objective, previous) {
121
- const retired = unique(request.last_round_result?.retracted_constraints ?? []);
123
+ const retired = unique(request.last_round_view?.constraintEvents?.retired ?? []);
122
124
  const retiredSet = new Set(retired);
123
- const discovered = unique(request.last_round_result?.discovered_constraints ?? [])
125
+ const discovered = unique(request.last_round_view?.discoveries?.newConstraints ?? [])
124
126
  .slice(0, getPolicy().evolution.max_discovered_constraints_per_round);
125
127
  const active = unique([
126
128
  ...(previous?.constraints_active ?? []),
@@ -135,31 +137,73 @@ function evolveConstraints(request, objective, previous) {
135
137
  // ═══════════════════════════════════════════════════════════════════════════
136
138
  // Hierarchical Summary (v2.1)
137
139
  // ═══════════════════════════════════════════════════════════════════════════
138
- /** Extract execution_evidence from a vault entry, handling both direct-field
139
- * and lineage-nested shapes. Returns null when absent. */
140
- function entryExecutionEvidence(entry) {
141
- const direct = entry.execution_evidence;
142
- if (direct && typeof direct === "object" && !Array.isArray(direct)) {
143
- return direct;
144
- }
145
- const lin = lineage(entry);
146
- const nested = lin.execution_evidence;
147
- if (nested && typeof nested === "object" && !Array.isArray(nested)) {
148
- return nested;
149
- }
150
- return null;
151
- }
152
- /** Read success_criteria_met from an entry's execution_evidence. */
153
140
  function entryCriteriaMet(entry) {
154
- const ev = entryExecutionEvidence(entry);
155
- if (!ev)
156
- return [];
157
- const arr = ev.success_criteria_met;
158
- 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() };
182
+ }
183
+ /** v2.11: Match two criterion references for deduplication.
184
+ * If either is a criterion ID (cr-XXXXXXXX), uses exact ID comparison.
185
+ * Otherwise falls back to Jaccard similarity. */
186
+ function criteriaMatch(a, b) {
187
+ const aIsId = isCriterionId(a);
188
+ const bIsId = isCriterionId(b);
189
+ // Both are IDs — exact string comparison
190
+ if (aIsId && bIsId)
191
+ return a === b;
192
+ // One is an ID — derive ID from the other and compare
193
+ if (aIsId)
194
+ return a === deriveCriterionId(b);
195
+ if (bIsId)
196
+ return deriveCriterionId(a) === b;
197
+ // Neither is an ID — Jaccard fallback
198
+ return jaccardSimilarity(a, b) >= getPolicy().evolution.criteria_dedup_threshold;
199
+ }
200
+ function isCriterionId(ref) {
201
+ return /^cr-[a-f0-9]{8}$/.test(ref);
159
202
  }
160
203
  /** Detect newly met criteria by comparing the current entry's
161
- * success_criteria_met against the previous entry's, using Jaccard
162
- * similarity with threshold 0.7 to avoid Surface-form mismatches.
204
+ * success_criteria_met against the previous entry's.
205
+ * v2.11: ID-first matching (cr-XXXXXXXX) with Jaccard similarity
206
+ * fallback for backward compatibility.
163
207
  * Returns empty array when there is no previous entry — the first
164
208
  * entry's criteria are the baseline, not a "new" event. */
165
209
  function detectNewCriteria(current, previous) {
@@ -173,7 +217,7 @@ function detectNewCriteria(current, previous) {
173
217
  const prevMet = entryCriteriaMet(previous);
174
218
  if (prevMet.length === 0)
175
219
  return currMet;
176
- return currMet.filter((curr) => prevMet.every((prev) => jaccardSimilarity(curr, prev) < getPolicy().evolution.criteria_dedup_threshold));
220
+ return currMet.filter((curr) => prevMet.every((prev) => !criteriaMatch(curr, prev)));
177
221
  }
178
222
  /** Read constraints_active from an entry's lineage. */
179
223
  function entryActiveConstraints(entry) {
@@ -183,29 +227,15 @@ function entryActiveConstraints(entry) {
183
227
  }
184
228
  /** Read retracted_constraints from an entry. */
185
229
  function entryRetractedConstraints(entry) {
186
- const direct = entry.retracted_constraints;
187
- if (Array.isArray(direct))
188
- return direct.filter((v) => typeof v === "string");
189
- const lin = lineage(entry);
190
- const nested = lin.retracted_constraints;
191
- if (Array.isArray(nested))
192
- return nested.filter((v) => typeof v === "string");
193
- return [];
230
+ return entryProjection(entry)?.constraintEvents?.retired ?? [];
194
231
  }
195
- /** Read progress_estimate from an entry's execution_evidence. */
196
232
  function entryProgressEstimate(entry) {
197
- const ev = entryExecutionEvidence(entry);
198
- if (!ev)
199
- return 0;
200
- const pe = ev.progress_estimate;
201
- return typeof pe === "number" ? pe : 0;
233
+ return entryProjection(entry)?.evidence?.progressRatio ?? 0;
202
234
  }
203
235
  /** Build a single MilestoneSummary from a range of completed round entries. */
204
236
  function buildMilestoneFromEntries(phaseEntries, startRound, endRound, label, kind) {
205
237
  const last = phaseEntries.at(-1);
206
- const outcomeRaw = last
207
- ? (typeof last.output_summary === "string" ? last.output_summary : "")
208
- : "";
238
+ const outcomeRaw = last ? entryProjection(last)?.summary ?? "" : "";
209
239
  const outcomeMax = getPolicy().checkpoint.outcome_max_chars;
210
240
  const outcome = outcomeRaw.length > outcomeMax
211
241
  ? outcomeRaw.slice(0, outcomeMax) + "…"
@@ -240,28 +270,48 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
240
270
  const data = lineage(entry);
241
271
  const round = entryRound(entry);
242
272
  const success = entry.success ?? data.success;
243
- const summary = typeof entry.output_summary === "string"
244
- ? entry.output_summary
245
- : typeof data.output_summary === "string" ? data.output_summary : "";
273
+ const summary = entryProjection(entry)?.summary ?? "";
246
274
  if (summary)
247
275
  outcomes.push(`[R${round}] ${success === false ? "failed" : "accepted"}: ${summary}`);
248
- const violations = Array.isArray(entry.constraint_violations)
249
- ? entry.constraint_violations
250
- : Array.isArray(data.constraint_violations) ? data.constraint_violations : [];
276
+ const violations = entryProjection(entry)?.violations ?? [];
251
277
  issues.push(...violations.filter((item) => typeof item === "string"));
252
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
+ }
253
287
  // ── Phase 2: Milestone Accumulation ──
254
288
  // Scan all completed entries in round order. For each entry, check three
255
289
  // trigger signals in priority order: agent_declared > criteria_milestone > auto.
256
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
+ }
257
305
  const milestones = [];
258
306
  let lastMilestoneRound = 0;
259
307
  for (let i = 0; i < completedEntries.length; i++) {
260
308
  const entry = completedEntries[i];
261
309
  const rnd = entryRound(entry);
262
310
  const lin = lineage(entry);
311
+ const report = parseRoundReport(entry.round_report);
263
312
  // Signal 1: Agent-declared checkpoint
264
- const isAgentCheckpoint = lin.compression_checkpoint === true;
313
+ const isAgentCheckpoint = Boolean(report?.phaseBoundaryProposal) ||
314
+ lin.compression_checkpoint === true;
265
315
  // Signal 2: New criteria met (compare with previous vault entry)
266
316
  const prev = i > 0 ? completedEntries[i - 1] : null;
267
317
  const newCriteria = detectNewCriteria(entry, prev);
@@ -275,9 +325,10 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
275
325
  ? "agent_declared"
276
326
  : "criteria_milestone";
277
327
  const label = isAgentCheckpoint
278
- ? (typeof lin.checkpoint_label === "string" && lin.checkpoint_label.trim()
279
- ? lin.checkpoint_label.trim()
280
- : `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}`))
281
332
  : `Completed: ${newCriteria.join(", ")}`;
282
333
  milestones.push(buildMilestoneFromEntries(phaseEntries, startRound, rnd, label, kind));
283
334
  lastMilestoneRound = rnd;
@@ -320,205 +371,20 @@ export function buildRollingSummary(loopId, currentRound, context, sinceRound =
320
371
  return null;
321
372
  return makeRollingSummary({
322
373
  key_outcomes: unique(outcomes),
323
- recurring_issues: unique(issues),
374
+ recurring_issues: [...issueCounts.values()]
375
+ .filter((issue) => issue.count >= 2)
376
+ .map((issue) => `${issue.text} (${issue.count} rounds)`),
324
377
  rounds_sampled: windowEntries.length,
325
378
  generated_at_round: currentRound,
326
- failed_patterns: [],
379
+ failed_patterns: failedGroups
380
+ .filter((failure) => failure.count >= 2)
381
+ .map((failure) => `${failure.text} (${failure.count} attempts)`),
327
382
  milestones: cappedMilestones,
328
383
  loop_synthesis: loopSynthesis || undefined,
329
384
  });
330
385
  }
331
- // ═══════════════════════════════════════════════════════════════════════════
332
- // Sub-Goal Structured Tracking (v2.2)
333
- // ═══════════════════════════════════════════════════════════════════════════
334
- /** Derive a stable sub-goal ID from its description hash. */
335
- function deriveSubGoalId(description) {
336
- return "sg-" + computeGoalTextHash(description).slice(0, 8);
337
- }
338
- /** Read emerged_subtasks from a vault entry (handles direct + lineage nesting). */
339
- function entryEmergedSubtasks(entry) {
340
- const direct = entry.emerged_subtasks;
341
- if (Array.isArray(direct))
342
- return direct.filter((v) => typeof v === "string");
343
- const lin = lineage(entry);
344
- const nested = lin.emerged_subtasks;
345
- if (Array.isArray(nested))
346
- return nested.filter((v) => typeof v === "string");
347
- return [];
348
- }
349
- /** Read completed_subtasks from an entry. */
350
- function entryCompletedSubtasks(entry) {
351
- const direct = entry.completed_subtasks;
352
- if (Array.isArray(direct))
353
- return direct.filter((v) => typeof v === "string");
354
- return [];
355
- }
356
- /** Read blocked_subtasks from an entry. */
357
- function entryBlockedSubtasks(entry) {
358
- const direct = entry.blocked_subtasks;
359
- if (Array.isArray(direct))
360
- return direct.filter((v) => typeof v === "string");
361
- return [];
362
- }
363
- /** Read canceled_subtasks from an entry. */
364
- function entryCanceledSubtasks(entry) {
365
- const direct = entry.canceled_subtasks;
366
- if (Array.isArray(direct))
367
- return direct.filter((v) => typeof v === "string");
368
- return [];
369
- }
370
- /**
371
- * Match a user-provided sub-task description against existing SubGoals.
372
- * Returns the index of the best-matching SubGoal with similarity >= threshold,
373
- * or -1 if no match found.
374
- */
375
- function matchSubGoal(target, subGoals, threshold) {
376
- let bestIdx = -1;
377
- let bestScore = 0;
378
- for (let i = 0; i < subGoals.length; i++) {
379
- const score = jaccardSimilarity(target, subGoals[i].description);
380
- if (score > bestScore) {
381
- bestScore = score;
382
- bestIdx = i;
383
- }
384
- }
385
- return bestScore >= threshold ? bestIdx : -1;
386
- }
387
- /**
388
- * Manage the full sub-goal lifecycle across rounds.
389
- *
390
- * Accumulates sub-goals from all prior vault entries, processes the current
391
- * round's declarations, derives status changes, and returns the complete
392
- * structured SubGoal list.
393
- *
394
- * Status derivation priority (compiler-driven):
395
- * 1. Agent declared done/canceled/blocked → explicit status change
396
- * 2. next_action match → auto in_progress
397
- * 3. success_criteria_met match → auto done
398
- */
399
- function manageSubGoals(loopId, currentRound, lastRoundResult, vaultContext) {
400
- const policy = getPolicy().evolution;
401
- const allEntries = loopEntries(loopId, vaultContext);
402
- const completedEntries = allEntries.filter((e) => entryRound(e) >= 1 && entryRound(e) < currentRound);
403
- const subGoals = [];
404
- // Phase 1 — Accumulate historical sub-goals from vault entries
405
- for (const entry of completedEntries) {
406
- const rnd = entryRound(entry);
407
- const emerged = entryEmergedSubtasks(entry);
408
- for (const desc of emerged) {
409
- const id = deriveSubGoalId(desc);
410
- // Dedup: skip if an existing sub-goal is too similar
411
- if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
412
- continue;
413
- subGoals.push(makeSubGoal({
414
- id,
415
- description: desc.trim(),
416
- status: "pending",
417
- declared_at_round: rnd,
418
- status_changed_at_round: rnd,
419
- priority: subGoals.length,
420
- }));
421
- }
422
- }
423
- // Phase 2 — Process current round's sub-task declarations
424
- if (lastRoundResult) {
425
- const rnd = lastRoundResult.round || (currentRound - 1);
426
- // New sub-goals from this round
427
- const emerged = lastRoundResult.emerged_subtasks ?? [];
428
- for (const desc of emerged) {
429
- if (matchSubGoal(desc, subGoals, policy.subgoal_dedup_threshold) >= 0)
430
- continue;
431
- subGoals.push(makeSubGoal({
432
- id: deriveSubGoalId(desc),
433
- description: desc.trim(),
434
- status: "pending",
435
- declared_at_round: rnd,
436
- status_changed_at_round: rnd,
437
- priority: subGoals.length,
438
- }));
439
- }
440
- // Completed sub-goals
441
- const completed = lastRoundResult.completed_subtasks ?? [];
442
- for (const desc of completed) {
443
- const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
444
- if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
445
- subGoals[idx].status = "done";
446
- subGoals[idx].status_changed_at_round = rnd;
447
- subGoals[idx].completed_at_round = rnd;
448
- }
449
- }
450
- // Blocked sub-goals
451
- const blocked = lastRoundResult.blocked_subtasks ?? [];
452
- for (const desc of blocked) {
453
- const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
454
- if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
455
- subGoals[idx].status = "blocked";
456
- subGoals[idx].status_changed_at_round = rnd;
457
- }
458
- }
459
- // Canceled sub-goals
460
- const canceled = lastRoundResult.canceled_subtasks ?? [];
461
- for (const desc of canceled) {
462
- const idx = matchSubGoal(desc, subGoals, policy.subgoal_match_threshold);
463
- if (idx >= 0 && subGoals[idx].status !== "done" && subGoals[idx].status !== "canceled") {
464
- subGoals[idx].status = "canceled";
465
- subGoals[idx].status_changed_at_round = rnd;
466
- }
467
- }
468
- // Phase 3 — Auto-derive: next_action match → in_progress
469
- const nextAction = lastRoundResult.next_action?.trim();
470
- if (nextAction) {
471
- for (const sg of subGoals) {
472
- if (sg.status === "pending") {
473
- if (jaccardSimilarity(nextAction, sg.description) >= policy.subgoal_auto_in_progress_threshold) {
474
- sg.status = "in_progress";
475
- sg.status_changed_at_round = rnd;
476
- break; // Only one sub-goal can be in_progress at a time
477
- }
478
- }
479
- }
480
- }
481
- // Phase 4 — Auto-complete: new criteria match → done
482
- const newCriteria = lastRoundResult.execution_evidence?.success_criteria_met ?? [];
483
- if (newCriteria.length > 0) {
484
- for (const sg of subGoals) {
485
- if (sg.status === "pending" || sg.status === "in_progress") {
486
- for (const crit of newCriteria) {
487
- if (jaccardSimilarity(crit, sg.description) >= policy.subgoal_auto_complete_threshold) {
488
- sg.status = "done";
489
- sg.status_changed_at_round = rnd;
490
- sg.completed_at_round = rnd;
491
- break;
492
- }
493
- }
494
- }
495
- }
496
- }
497
- }
498
- // Sort for display: in_progress first, then pending by age desc, then blocked, then done, then canceled
499
- subGoals.sort((a, b) => {
500
- const rank = (s) => {
501
- switch (s.status) {
502
- case "in_progress": return 0;
503
- case "pending": return 1;
504
- case "blocked": return 2;
505
- case "done": return 3;
506
- case "canceled": return 4;
507
- }
508
- };
509
- const ra = rank(a), rb = rank(b);
510
- if (ra !== rb)
511
- return ra - rb;
512
- // Within same status: pending by oldest first, done by most recent first
513
- if (a.status === "pending")
514
- return (currentRound - a.declared_at_round) - (currentRound - b.declared_at_round);
515
- if (a.status === "done")
516
- return (b.completed_at_round ?? 0) - (a.completed_at_round ?? 0);
517
- return a.declared_at_round - b.declared_at_round;
518
- });
519
- return subGoals;
520
- }
521
- // ═══════════════════════════════════════════════════════════════════════════
386
+ // Sub-goal lifecycle is implemented in compiler-subgoals.ts; this facade keeps
387
+ // the compiler's public entry point and projection flow unchanged.
522
388
  // Time-Aware Constraint Management (v2.3)
523
389
  // ═══════════════════════════════════════════════════════════════════════════
524
390
  /** Determine the source of a constraint by matching against known sources. */
@@ -533,17 +399,25 @@ function constraintSource(text, objective, constraintsFromPlan) {
533
399
  return "discovered";
534
400
  }
535
401
  /** Find the earliest round a constraint was discovered by scanning vault entries.
536
- * Uses similarity ≥ 0.5 for fuzzy matching. */
402
+ * v2.11: ID-first matching. Checks exact constraint ID match (c-XXXXXXXX)
403
+ * against the derived ID of each vault entry's discovered_constraints, then
404
+ * falls back to Jaccard similarity on text. */
537
405
  function findDiscoveredRound(text, vaultEntries) {
406
+ const targetId = deriveConstraintId(text);
538
407
  let earliest = 0;
539
408
  for (const entry of vaultEntries) {
540
409
  const rnd = entryRound(entry);
541
410
  if (rnd < 1)
542
411
  continue;
543
- const discovered = Array.isArray(entry.discovered_constraints)
544
- ? entry.discovered_constraints.filter((v) => typeof v === "string")
545
- : [];
412
+ const discovered = entryProjection(entry)?.discoveries?.newConstraints ?? [];
546
413
  for (const d of discovered) {
414
+ // Phase 1: exact ID match (v2.11)
415
+ if (d === targetId || deriveConstraintId(d) === targetId) {
416
+ if (earliest === 0 || rnd < earliest)
417
+ earliest = rnd;
418
+ break;
419
+ }
420
+ // Phase 2: Jaccard fallback (backward compatibility)
547
421
  if (jaccardSimilarity(text, d) >= getPolicy().evolution.constraint_match_threshold) {
548
422
  if (earliest === 0 || rnd < earliest)
549
423
  earliest = rnd;
@@ -554,17 +428,25 @@ function findDiscoveredRound(text, vaultEntries) {
554
428
  return earliest;
555
429
  }
556
430
  /** Find the latest round a constraint was violated by scanning vault entries.
557
- * Uses similarity ≥ 0.5 for fuzzy matching. Returns 0 if never violated. */
431
+ * v2.11: ID-first matching. Checks exact constraint ID match (c-XXXXXXXX)
432
+ * against the derived ID of each vault entry's constraint_violations, then
433
+ * falls back to Jaccard similarity on text. Returns 0 if never violated. */
558
434
  function findLastViolatedRound(text, vaultEntries) {
435
+ const targetId = deriveConstraintId(text);
559
436
  let latest = 0;
560
437
  for (const entry of vaultEntries) {
561
438
  const rnd = entryRound(entry);
562
439
  if (rnd < 1)
563
440
  continue;
564
- const violations = Array.isArray(entry.constraint_violations)
565
- ? entry.constraint_violations.filter((v) => typeof v === "string")
566
- : [];
441
+ const violations = entryProjection(entry)?.violations ?? [];
567
442
  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)
568
450
  if (jaccardSimilarity(text, v) >= getPolicy().evolution.constraint_match_threshold) {
569
451
  if (rnd > latest)
570
452
  latest = rnd;
@@ -592,6 +474,7 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
592
474
  // Structural constraints never decay
593
475
  if (source === "hard" || source === "plan" || source === "criteria") {
594
476
  metadata.push(makeConstraintMeta({
477
+ id: deriveConstraintId(text),
595
478
  text,
596
479
  discovered_at_round: objective.created_at_round || 1,
597
480
  last_violated_at_round: findLastViolatedRound(text, completedEntries),
@@ -610,6 +493,7 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
610
493
  ? "inactive"
611
494
  : "active";
612
495
  metadata.push(makeConstraintMeta({
496
+ id: deriveConstraintId(text),
613
497
  text,
614
498
  discovered_at_round: discoveredAt || 0,
615
499
  last_violated_at_round: lastViolated,
@@ -625,7 +509,6 @@ function manageConstraintLifecycle(active, objective, constraintsFromPlan, vault
625
509
  .map((m) => m.text);
626
510
  return { active: newActive, inactive, metadata };
627
511
  }
628
- // jaccardSimilarity() imported from token-utils.ts
629
512
  export function alignTask(proposedTask, request, context) {
630
513
  const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
631
514
  if (!objective)
@@ -641,7 +524,7 @@ export function alignTask(proposedTask, request, context) {
641
524
  export function checkLoopHealth(loopId, request, context) {
642
525
  const previous = getPreviousRound(loopId, request.round - 1, context);
643
526
  const alignment = alignTask(request.task, request, context);
644
- const violations = request.last_round_result?.constraint_violations.length ?? 0;
527
+ const violations = request.last_round_view?.violations?.length ?? 0;
645
528
  const integrity = Math.max(0, 1 - violations * 0.2);
646
529
  const continuity = previous ? jaccardSimilarity(previous.task, request.task) : 1;
647
530
  const drift = alignment.alignment_score < 0.3 || continuity < 0.2;
@@ -656,70 +539,133 @@ export function checkLoopHealth(loopId, request, context) {
656
539
  }
657
540
  function levelDecision(request, context) {
658
541
  const previous = getPreviousRound(request.loop_id, request.round - 1, context);
659
- const last = request.last_round_result;
542
+ const last = request.last_round_view;
660
543
  const hasNewInformation = Boolean(request.new_since_last_round.trim() ||
661
- last?.discovered_constraints?.length ||
662
- last?.objective_refinement?.trim() ||
663
- last?.emerged_subtasks?.length ||
664
- last?.retracted_constraints?.length ||
665
- last?.revised_success_criteria?.length ||
666
- 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);
667
551
  const lastFullRound = loopEntries(request.loop_id, context)
668
552
  .filter((entry) => lineage(entry).recompile_level === "l2")
669
553
  .map(entryRound)
670
554
  .filter((round) => round < request.round)
671
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;
672
576
  return decidePromptLevel({
673
577
  round: request.round,
674
578
  attempt: request.attempt,
675
579
  forceLevel: request.force_level,
676
580
  hasPlanSource: Boolean(request.plan_source),
677
- checkpointBoundary: last?.compression_checkpoint === true,
678
- 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),
679
585
  previousStateMissing: request.round > 1 && previous === null,
680
- previousFailedWithoutNewInformation: last?.success === false && !hasNewInformation,
586
+ previousFailedWithoutNewInformation: last?.outcome === "failed" && !hasNewInformation,
681
587
  verificationContradicted: (request.verification_flags ?? [])
682
588
  .some((flag) => flag.severity === "error"),
683
589
  consecutiveRejections: request.consecutive_rejections,
684
590
  fullRefreshInterval: getPolicy().prompt.full_refresh_interval,
685
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,
686
604
  });
687
605
  }
688
606
  export function decideLevel(request, context) {
689
607
  return levelDecision(request, context).level;
690
608
  }
691
- export function buildSelfEvalBlock(round) {
692
- return [
609
+ export function buildRoundReportBlock(round, prevDriftFlags) {
610
+ const hasIntentDrift = prevDriftFlags?.some(f => f.check === "intent_drift") ?? false;
611
+ const hasSubGoalDrift = prevDriftFlags?.some(f => f.check === "subgoal_drift") ?? false;
612
+ 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: "",
628
+ },
629
+ discoveries: {
630
+ wrongAssumptions: [],
631
+ emergedWork: [],
632
+ facts: [],
633
+ newConstraints: [],
634
+ },
635
+ subGoalEvents: { completed: [], blocked: [], canceled: [] },
636
+ };
637
+ // v2.8: Inject drift clarification field when previous round had drift flags
638
+ if (needsClarification) {
639
+ const flagTypes = [
640
+ hasIntentDrift ? "intent_drift" : "",
641
+ hasSubGoalDrift ? "subgoal_drift" : "",
642
+ ].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
+ };
651
+ }
652
+ const lines = [
693
653
  "### LoopForge Evaluation (Required)",
694
654
  "",
695
655
  `After completing Round ${round}, replace the placeholder values below`,
696
656
  "with your actual results and submit via `loopforge_next`.",
697
- "---loopforge-eval",
698
- JSON.stringify({
699
- success: false,
700
- output_summary: `<verified result of round ${round}>`,
701
- constraint_violations: [],
702
- should_continue: true,
703
- discovered_constraints: [],
704
- emerged_subtasks: [],
705
- execution_evidence: {
706
- files_changed: [],
707
- test_results: { passed: 0, failed: 0, skipped: 0 },
708
- success_criteria_met: [],
709
- success_criteria_remaining: [],
710
- progress_estimate: 0,
711
- },
712
- wrong_assumptions: [],
713
- next_action: "<next concrete action, or empty when complete>",
714
- completed_subtasks: [],
715
- blocked_subtasks: [],
716
- canceled_subtasks: [],
717
- }, null, 2),
718
- "---end-loopforge-eval",
719
- "",
720
- "IMPORTANT: Replace every <placeholder> with your actual data.",
721
- "Set success=true only when the full goal and ALL hard constraints are verified.",
722
- ].join("\n");
657
+ ];
658
+ 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.");
660
+ }
661
+ lines.push("Submit this object in the `report` parameter:");
662
+ lines.push(JSON.stringify(report, null, 2));
663
+ lines.push("");
664
+ 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.");
666
+ lines.push("Use IDs for exact matching: c-XXXXXXXX, cr-XXXXXXXX, sg-XXXXXXXX.");
667
+ lines.push("For subtasks, use sub-goal IDs from the Sub-Goal Dashboard above.");
668
+ return lines.join("\n");
723
669
  }
724
670
  // v2.5: checkpoint() removed — agent_declared milestones from
725
671
  // compression_checkpoint supersede the separate CheckpointSummary.
@@ -731,7 +677,7 @@ export function compileLoop(request, context) {
731
677
  const constraints = evolveConstraints(request, objective, previous);
732
678
  const constraintLifecycle = manageConstraintLifecycle(constraints.active, objective, request.constraints_from_plan, context, request.round);
733
679
  const rolling = buildRollingSummary(request.loop_id, request.round, context);
734
- 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);
735
681
  const alignment = alignTask(request.task, request, context);
736
682
  const health = checkLoopHealth(request.loop_id, { ...request, loop_objective: objective }, context);
737
683
  const warnings = unique([
@@ -751,13 +697,35 @@ export function compileLoop(request, context) {
751
697
  .filter((e) => entryRound(e) >= 1 && entryRound(e) < request.round);
752
698
  const trend = [];
753
699
  for (const entry of completedEntries.slice(-10)) {
754
- const viols = Array.isArray(entry.constraint_violations)
755
- ? entry.constraint_violations.filter((v) => typeof v === "string")
756
- : [];
700
+ const viols = entryProjection(entry)?.violations ?? [];
757
701
  // Simple proxy: no violations = perfect trust, each violation = -0.1
758
702
  const score = Math.max(0, 1.0 - viols.length * 0.1);
759
703
  trend.push(Number(score.toFixed(2)));
760
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
+ : [];
761
729
  const response = makeLoopCompileResponse({
762
730
  status: AgentStatus.OK,
763
731
  recompile_level: decision.level,
@@ -778,8 +746,18 @@ export function compileLoop(request, context) {
778
746
  sub_goals: subGoals,
779
747
  agent_trust_score: request.round > 1 ? Number(trustScore.toFixed(2)) : undefined,
780
748
  agent_trust_trend: request.round > 1 ? trend : [],
781
- suggested_next_task: request.last_round_result?.next_action?.trim() ||
782
- 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
+ }),
783
761
  plan_source: request.plan_source,
784
762
  warnings,
785
763
  });
@@ -794,6 +772,10 @@ export function compileLoop(request, context) {
794
772
  + ((rolling?.milestones?.length ?? 0) * policy.prompt.l2_adaptive_milestone_factor)
795
773
  + (subGoals.length * policy.prompt.l2_adaptive_subgoal_factor), policy.prompt.l2_adaptive_max_chars)
796
774
  : policy.prompt.l2_max_chars;
775
+ // v2.8: L2 pointer mode — when enabled, skip the monolithic markdown blob.
776
+ // Structured L2 sections (milestones, sub-goals, trust, progress) still
777
+ // render. The state file on disk is the durable source of truth.
778
+ const l2Pointer = policy.prompt.l2_pointer_enabled && decision.level === "l2";
797
779
  const artifact = assemblePromptArtifact({
798
780
  state,
799
781
  level: decision.level,
@@ -805,12 +787,36 @@ export function compileLoop(request, context) {
805
787
  l2: adaptiveL2,
806
788
  },
807
789
  attempt: request.attempt,
808
- selfEvaluationBlock: buildSelfEvalBlock(request.round),
809
- fullStateMarkdown: markdown,
790
+ roundReportBlock: buildRoundReportBlock(request.round, request.verification_flags),
791
+ 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);
810
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;
811
816
  response.prompt = artifact.renderedPrompt;
812
817
  response.prompt_artifact = artifact;
813
818
  response.state_file_content = policy.state_file.enabled ? markdown : undefined;
819
+ response.canonical_state = state;
814
820
  return response;
815
821
  }
816
822
  //# sourceMappingURL=loop-compiler.js.map