loopforge 3.0.0 → 3.6.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 (194) hide show
  1. package/README.md +126 -143
  2. package/dist/audit.d.ts +50 -0
  3. package/dist/audit.d.ts.map +1 -0
  4. package/dist/audit.js +131 -0
  5. package/dist/audit.js.map +1 -0
  6. package/dist/canonical-state.d.ts +98 -23
  7. package/dist/canonical-state.d.ts.map +1 -1
  8. package/dist/canonical-state.js +257 -181
  9. package/dist/canonical-state.js.map +1 -1
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +40 -33
  12. package/dist/cli.js.map +1 -1
  13. package/dist/cognitive-facts.d.ts +17 -0
  14. package/dist/cognitive-facts.d.ts.map +1 -0
  15. package/dist/cognitive-facts.js +99 -0
  16. package/dist/cognitive-facts.js.map +1 -0
  17. package/dist/cognitive-governance.d.ts +25 -33
  18. package/dist/cognitive-governance.d.ts.map +1 -1
  19. package/dist/cognitive-governance.js +38 -138
  20. package/dist/cognitive-governance.js.map +1 -1
  21. package/dist/committed-round.d.ts +59 -0
  22. package/dist/committed-round.d.ts.map +1 -0
  23. package/dist/committed-round.js +222 -0
  24. package/dist/committed-round.js.map +1 -0
  25. package/dist/enforcement-gate.d.ts +78 -0
  26. package/dist/enforcement-gate.d.ts.map +1 -0
  27. package/dist/enforcement-gate.js +1109 -0
  28. package/dist/enforcement-gate.js.map +1 -0
  29. package/dist/engine.d.ts +111 -0
  30. package/dist/engine.d.ts.map +1 -0
  31. package/dist/engine.js +887 -0
  32. package/dist/engine.js.map +1 -0
  33. package/dist/evidence-claims.d.ts +50 -0
  34. package/dist/evidence-claims.d.ts.map +1 -0
  35. package/dist/evidence-claims.js +116 -0
  36. package/dist/evidence-claims.js.map +1 -0
  37. package/dist/evidence-provider.d.ts +50 -23
  38. package/dist/evidence-provider.d.ts.map +1 -1
  39. package/dist/evidence-provider.js +167 -50
  40. package/dist/evidence-provider.js.map +1 -1
  41. package/dist/generate-schema.js +1 -84
  42. package/dist/generate-schema.js.map +1 -1
  43. package/dist/index.d.ts +19 -22
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +16 -15
  46. package/dist/index.js.map +1 -1
  47. package/dist/loop-compiler.d.ts +58 -3
  48. package/dist/loop-compiler.d.ts.map +1 -1
  49. package/dist/loop-compiler.js +841 -304
  50. package/dist/loop-compiler.js.map +1 -1
  51. package/dist/loop-extras-parser.d.ts +2 -2
  52. package/dist/loop-extras-parser.d.ts.map +1 -1
  53. package/dist/loop-extras-parser.js +2 -2
  54. package/dist/loop-extras-parser.js.map +1 -1
  55. package/dist/loop-projection.d.ts +6 -0
  56. package/dist/loop-projection.d.ts.map +1 -0
  57. package/dist/loop-projection.js +26 -0
  58. package/dist/loop-projection.js.map +1 -0
  59. package/dist/loop-store.d.ts +115 -24
  60. package/dist/loop-store.d.ts.map +1 -1
  61. package/dist/loop-store.js +365 -218
  62. package/dist/loop-store.js.map +1 -1
  63. package/dist/mcp/round-lifecycle.d.ts +214 -0
  64. package/dist/mcp/round-lifecycle.d.ts.map +1 -0
  65. package/dist/mcp/round-lifecycle.js +935 -0
  66. package/dist/mcp/round-lifecycle.js.map +1 -0
  67. package/dist/mcp/server.d.ts +2 -1
  68. package/dist/mcp/server.d.ts.map +1 -1
  69. package/dist/mcp/server.js +29 -43
  70. package/dist/mcp/server.js.map +1 -1
  71. package/dist/mcp/session.d.ts +64 -101
  72. package/dist/mcp/session.d.ts.map +1 -1
  73. package/dist/mcp/session.js +433 -1110
  74. package/dist/mcp/session.js.map +1 -1
  75. package/dist/mcp/tools.d.ts +20 -11
  76. package/dist/mcp/tools.d.ts.map +1 -1
  77. package/dist/mcp/tools.js +587 -413
  78. package/dist/mcp/tools.js.map +1 -1
  79. package/dist/policy-metrics.d.ts +12 -24
  80. package/dist/policy-metrics.d.ts.map +1 -1
  81. package/dist/policy-metrics.js +78 -184
  82. package/dist/policy-metrics.js.map +1 -1
  83. package/dist/policy.d.ts +61 -50
  84. package/dist/policy.d.ts.map +1 -1
  85. package/dist/policy.js +48 -146
  86. package/dist/policy.js.map +1 -1
  87. package/dist/prompt-assembler.d.ts +37 -6
  88. package/dist/prompt-assembler.d.ts.map +1 -1
  89. package/dist/prompt-assembler.js +487 -278
  90. package/dist/prompt-assembler.js.map +1 -1
  91. package/dist/prompt-policy.d.ts +1 -8
  92. package/dist/prompt-policy.d.ts.map +1 -1
  93. package/dist/prompt-policy.js +8 -26
  94. package/dist/prompt-policy.js.map +1 -1
  95. package/dist/protocol.d.ts +398 -236
  96. package/dist/protocol.d.ts.map +1 -1
  97. package/dist/protocol.js +147 -17
  98. package/dist/protocol.js.map +1 -1
  99. package/dist/replay.d.ts +5 -13
  100. package/dist/replay.d.ts.map +1 -1
  101. package/dist/replay.js +32 -69
  102. package/dist/replay.js.map +1 -1
  103. package/dist/round-contract.d.ts +64 -0
  104. package/dist/round-contract.d.ts.map +1 -0
  105. package/dist/round-contract.js +80 -0
  106. package/dist/round-contract.js.map +1 -0
  107. package/dist/round-coordinator.d.ts +23 -25
  108. package/dist/round-coordinator.d.ts.map +1 -1
  109. package/dist/round-coordinator.js +103 -92
  110. package/dist/round-coordinator.js.map +1 -1
  111. package/dist/round-driver.d.ts +13 -13
  112. package/dist/round-driver.d.ts.map +1 -1
  113. package/dist/round-driver.js +25 -14
  114. package/dist/round-driver.js.map +1 -1
  115. package/dist/round-transaction.d.ts +14 -18
  116. package/dist/round-transaction.d.ts.map +1 -1
  117. package/dist/round-transaction.js +37 -47
  118. package/dist/round-transaction.js.map +1 -1
  119. package/dist/self-eval.d.ts +48 -0
  120. package/dist/self-eval.d.ts.map +1 -0
  121. package/dist/self-eval.js +248 -0
  122. package/dist/self-eval.js.map +1 -0
  123. package/dist/storage.d.ts +9 -4
  124. package/dist/storage.d.ts.map +1 -1
  125. package/dist/storage.js +18 -6
  126. package/dist/storage.js.map +1 -1
  127. package/dist/token-utils.d.ts +15 -3
  128. package/dist/token-utils.d.ts.map +1 -1
  129. package/dist/token-utils.js +24 -5
  130. package/dist/token-utils.js.map +1 -1
  131. package/dist/verification-gate.d.ts +149 -0
  132. package/dist/verification-gate.d.ts.map +1 -0
  133. package/dist/verification-gate.js +1343 -0
  134. package/dist/verification-gate.js.map +1 -0
  135. package/dist/workspace.d.ts +29 -0
  136. package/dist/workspace.d.ts.map +1 -0
  137. package/dist/workspace.js +61 -0
  138. package/dist/workspace.js.map +1 -0
  139. package/loop_policy.json +37 -16
  140. package/package.json +5 -10
  141. package/skills/perception/SKILL.md +184 -202
  142. package/dist/canonical-event-writer.d.ts +0 -20
  143. package/dist/canonical-event-writer.d.ts.map +0 -1
  144. package/dist/canonical-event-writer.js +0 -131
  145. package/dist/canonical-event-writer.js.map +0 -1
  146. package/dist/compiler-subgoals.d.ts +0 -7
  147. package/dist/compiler-subgoals.d.ts.map +0 -1
  148. package/dist/compiler-subgoals.js +0 -93
  149. package/dist/compiler-subgoals.js.map +0 -1
  150. package/dist/derive-id.d.ts +0 -12
  151. package/dist/derive-id.d.ts.map +0 -1
  152. package/dist/derive-id.js +0 -31
  153. package/dist/derive-id.js.map +0 -1
  154. package/dist/mcp/session-projection.d.ts +0 -5
  155. package/dist/mcp/session-projection.d.ts.map +0 -1
  156. package/dist/mcp/session-projection.js +0 -18
  157. package/dist/mcp/session-projection.js.map +0 -1
  158. package/dist/mcp/session-queue.d.ts +0 -3
  159. package/dist/mcp/session-queue.d.ts.map +0 -1
  160. package/dist/mcp/session-queue.js +0 -18
  161. package/dist/mcp/session-queue.js.map +0 -1
  162. package/dist/mcp/session-types.d.ts +0 -72
  163. package/dist/mcp/session-types.d.ts.map +0 -1
  164. package/dist/mcp/session-types.js +0 -2
  165. package/dist/mcp/session-types.js.map +0 -1
  166. package/dist/round-commit-store.d.ts +0 -12
  167. package/dist/round-commit-store.d.ts.map +0 -1
  168. package/dist/round-commit-store.js +0 -13
  169. package/dist/round-commit-store.js.map +0 -1
  170. package/dist/round-gate.d.ts +0 -31
  171. package/dist/round-gate.d.ts.map +0 -1
  172. package/dist/round-gate.js +0 -259
  173. package/dist/round-gate.js.map +0 -1
  174. package/dist/round-recovery.d.ts +0 -17
  175. package/dist/round-recovery.d.ts.map +0 -1
  176. package/dist/round-recovery.js +0 -117
  177. package/dist/round-recovery.js.map +0 -1
  178. package/dist/round-report.d.ts +0 -16
  179. package/dist/round-report.d.ts.map +0 -1
  180. package/dist/round-report.js +0 -401
  181. package/dist/round-report.js.map +0 -1
  182. package/dist/runtime-services.d.ts +0 -59
  183. package/dist/runtime-services.d.ts.map +0 -1
  184. package/dist/runtime-services.js +0 -460
  185. package/dist/runtime-services.js.map +0 -1
  186. package/dist/vault-format.d.ts +0 -4
  187. package/dist/vault-format.d.ts.map +0 -1
  188. package/dist/vault-format.js +0 -4
  189. package/dist/vault-format.js.map +0 -1
  190. package/dist/vault-types.d.ts +0 -24
  191. package/dist/vault-types.d.ts.map +0 -1
  192. package/dist/vault-types.js +0 -2
  193. package/dist/vault-types.js.map +0 -1
  194. package/loopforge-policy.json +0 -340
package/dist/engine.js ADDED
@@ -0,0 +1,887 @@
1
+ /** LoopForge-loop_compile — Engine (outer loop manager).
2
+ *
3
+ * 2-mode engine with vault-backed loop lineage persistence.
4
+ * invokeLoopCompile (primary), invokeFeedback.
5
+ * Enforcement gates prevent infinite stall loops; EngineMetrics records
6
+ * silent-failure counters for observability.
7
+ */
8
+ import { randomUUID } from "node:crypto";
9
+ import { getPolicy } from "./policy.js";
10
+ import { FileLoopStore, queryLoopEntries } from "./loop-store.js";
11
+ import { AgentStatus, Mode, makeExecutionFeedback, makeLoopCompileRequest, makeLoopObjective, makeLoopRoundResult, makeSessionState, makeTaskId, } from "./protocol.js";
12
+ import { compileLoop } from "./loop-compiler.js";
13
+ import { logEvent } from "./observability.js";
14
+ import { isRecord } from "./token-utils.js";
15
+ import { decodeCommittedRound } from "./committed-round.js";
16
+ import { policyMetrics } from "./policy-metrics.js";
17
+ import { parseExecutionEvidence, parseCriterionRevisions, parseWorkerResults, parsePromptRequests, } from "./self-eval.js";
18
+ import { parseLoopExtras } from "./loop-extras-parser.js";
19
+ // ── Re-export self-evaluation utilities (moved to self-eval.ts) ──────────
20
+ export { parseExecutionEvidence, parseCriterionRevisions, parseWorkerResults, buildSelfEvaluation, } from "./self-eval.js";
21
+ function makeEngineMetrics() {
22
+ return {
23
+ vaultWriteErrors: 0,
24
+ sessionStart: Date.now(),
25
+ };
26
+ }
27
+ // ═══════════════════════════════════════════════════════════════════════════
28
+ // LoopForgeEngine
29
+ // ═══════════════════════════════════════════════════════════════════════════
30
+ export class LoopForgeEngine {
31
+ state = null;
32
+ store = null;
33
+ metrics = null;
34
+ lastTask = null;
35
+ constructor(store) {
36
+ this.store = store ?? null;
37
+ }
38
+ resolveStore() {
39
+ if (this.store === null) {
40
+ this.store = new FileLoopStore(getPolicy().backend.root_dir);
41
+ }
42
+ return this.store;
43
+ }
44
+ /** Public accessor for the loop store — used by the round transaction
45
+ * coordinator and round driver. */
46
+ getStore() {
47
+ return this.resolveStore();
48
+ }
49
+ /** Expose engine health counters for observability (MCP status, logging). */
50
+ getMetrics() {
51
+ if (this.metrics === null) {
52
+ this.metrics = makeEngineMetrics();
53
+ }
54
+ return this.metrics;
55
+ }
56
+ ensureInit(request) {
57
+ if (this.state === null) {
58
+ this.state = makeSessionState(request.task_id || makeTaskId(request.task));
59
+ }
60
+ if (this.metrics === null) {
61
+ this.metrics = makeEngineMetrics();
62
+ }
63
+ }
64
+ // ═══════════════════════════════════════════════════════════════════════
65
+ // Feedback persistence
66
+ // ═══════════════════════════════════════════════════════════════════════
67
+ persistFeedbackToVault(signal) {
68
+ if (this.metrics === null) {
69
+ this.metrics = makeEngineMetrics();
70
+ }
71
+ // v2.6: write directly — the buffer was always flushed immediately
72
+ // (autoFeedback calls persistFeedbackToVault then immediately flushFeedbackBuffer).
73
+ const entry = this.buildFeedbackEntry(signal);
74
+ if (!entry)
75
+ return false;
76
+ try {
77
+ this.resolveStore().appendEntry(entry);
78
+ // v3.3.1: a feedback write that replaces an already-cached round
79
+ // (a backtrack redo reuses the same roundId, per the v3.2.1 replace
80
+ // semantics) leaves the hydration cache holding the stale merged
81
+ // entry — coveredRound has already passed that round, so the
82
+ // incremental path never re-reads it and the compiler would keep
83
+ // seeing the rolled-back decision (forced L2, stale state) for the
84
+ // rest of the process. Invalidate so the next compile rehydrates.
85
+ this.invalidateHydrationCacheForFeedback(String(entry.task_id ?? ""));
86
+ return true;
87
+ }
88
+ catch (error) {
89
+ if (this.metrics)
90
+ this.metrics.vaultWriteErrors++;
91
+ console.warn(`LoopForge: vault write failed (feedback_persist) — ${error}`);
92
+ logEvent("vault_write_error", { error: "feedback_persist" });
93
+ policyMetrics.recordVaultWriteError("feedback_persist");
94
+ return false;
95
+ }
96
+ }
97
+ buildFeedbackEntry(signal) {
98
+ try {
99
+ return {
100
+ id: randomUUID(),
101
+ task_id: signal.task_id ?? "feedback",
102
+ version_tag: "v1",
103
+ is_active: true,
104
+ timestamp: new Date().toISOString().replace(/\.\d+Z$/, ""),
105
+ user_intent: String(signal.task_type ?? "").slice(0, 200),
106
+ success: signal.success ?? false,
107
+ execution_feedback: JSON.stringify({
108
+ success: signal.success ?? false,
109
+ status: signal.success ? "success" : "partial",
110
+ constraint_compliance: {
111
+ all_hard_constraints_met: !Array.isArray(signal.violations) || signal.violations.length === 0,
112
+ violations: signal.violations ?? [],
113
+ },
114
+ output_summary: signal.task_type ?? "",
115
+ improvement_notes: signal.manual_fixes ?? "",
116
+ }),
117
+ task_type: signal.task_type ?? "",
118
+ tags: signal.skill_used ? [signal.skill_used] : [],
119
+ skill_used: signal.skill_used ?? "",
120
+ loop_id: signal.loop_id,
121
+ loop_lineage: signal.loop_lineage ?? {},
122
+ // Persist execution_evidence so the enforcement gate (R4/R5) can
123
+ // read progress_estimate from vault entries across rounds.
124
+ execution_evidence: signal.execution_evidence ?? null,
125
+ discovered_constraints: signal.discovered_constraints ?? [],
126
+ emerged_subtasks: signal.emerged_subtasks ?? [],
127
+ // v3.2.1: persisted so the subgoal_drift check can read prior
128
+ // rounds' completed/canceled sub-goals from committed entries.
129
+ completed_subtasks: signal.completed_subtasks ?? [],
130
+ blocked_subtasks: signal.blocked_subtasks ?? [],
131
+ canceled_subtasks: signal.canceled_subtasks ?? [],
132
+ retracted_constraints: signal.retracted_constraints ?? [],
133
+ worker_results: signal.worker_results ?? [],
134
+ };
135
+ }
136
+ catch {
137
+ if (this.metrics)
138
+ this.metrics.vaultWriteErrors++;
139
+ logEvent("vault_write_error", { error: "feedback_entry_build" });
140
+ return null;
141
+ }
142
+ }
143
+ // ═══════════════════════════════════════════════════════════════════════
144
+ // Lineage persistence
145
+ // ═══════════════════════════════════════════════════════════════════════
146
+ persistLoopLineage(response, request) {
147
+ if (this.metrics === null)
148
+ this.metrics = makeEngineMetrics();
149
+ const loopObjDict = response.loop_objective
150
+ ? response.loop_objective
151
+ : null;
152
+ const structuredLineage = {
153
+ loop_id: response.loop_id,
154
+ round: response.round,
155
+ goal_id: response.goal_id,
156
+ goal_text_hash: response.goal_text_hash,
157
+ recompile_level: response.recompile_level,
158
+ constraints_active: response.constraints_active,
159
+ task: request.task,
160
+ success: true,
161
+ };
162
+ // v3.2: Persist what this round's prompt actually presented (L1 only —
163
+ // L0/L2 compiles leave the artifact field undefined). The next L1 compile
164
+ // reads these as its collapse diff baseline. Field extension of the
165
+ // existing lineage entry — no new persistence format.
166
+ const presented = response.prompt_artifact?.presentedState;
167
+ if (presented) {
168
+ structuredLineage.presented_constraint_ids = presented.constraintIds;
169
+ structuredLineage.presented_subgoals = presented.subGoals;
170
+ structuredLineage.presented_milestone_ranges = presented.milestoneRanges;
171
+ }
172
+ let lastOutputSummary = "";
173
+ let lastViolations = [];
174
+ if (request.last_round_result) {
175
+ lastOutputSummary = request.last_round_result.output_summary || "";
176
+ lastViolations = request.last_round_result.constraint_violations || [];
177
+ }
178
+ const entry = {
179
+ id: randomUUID(),
180
+ task_id: `loop:${response.loop_id}:r${response.round}`,
181
+ version_tag: "v1",
182
+ is_active: true,
183
+ timestamp: new Date().toISOString().replace(/\.\d+Z$/, ""),
184
+ user_intent: `loop_compile round ${response.round} — ${response.goal_id}`,
185
+ task_type: "loop_lineage",
186
+ loop_id: response.loop_id,
187
+ loop_lineage: structuredLineage,
188
+ loop_objective: loopObjDict,
189
+ task: request.task,
190
+ output_summary: lastOutputSummary,
191
+ constraint_violations: lastViolations,
192
+ tags: [response.loop_id, response.recompile_level, response.goal_id],
193
+ };
194
+ // 1. JSON vault write (primary)
195
+ let vaultOk = false;
196
+ try {
197
+ this.resolveStore().appendEntry(entry);
198
+ vaultOk = true;
199
+ }
200
+ catch (error) {
201
+ if (this.metrics)
202
+ this.metrics.vaultWriteErrors++;
203
+ console.warn(`LoopForge: vault write failed (lineage_persist) — ${error}`);
204
+ logEvent("vault_write_error", { error: "persist_lineage_json" });
205
+ policyMetrics.recordVaultWriteError("lineage_persist");
206
+ }
207
+ return vaultOk;
208
+ }
209
+ // ═══════════════════════════════════════════════════════════════════════
210
+ // Delegation journal (v1.9 — AgentTool mode)
211
+ // ═══════════════════════════════════════════════════════════════════════
212
+ /** Record sub-agent delegations for this round into the vault.
213
+ * Written as a lightweight journal entry so the main agent's rolling
214
+ * summary can reference delegation history in subsequent rounds. */
215
+ recordDelegation(loopId, round, entries) {
216
+ if (!entries.length)
217
+ return;
218
+ const taskId = `loop:${loopId}:r${round}:delegations`;
219
+ const existing = queryLoopEntries(this.resolveStore(), loopId, { prefix: taskId })
220
+ .some((candidate) => candidate.task_id === taskId);
221
+ if (existing)
222
+ return;
223
+ const entry = {
224
+ id: randomUUID(),
225
+ task_id: taskId,
226
+ version_tag: "v1",
227
+ is_active: true,
228
+ timestamp: new Date().toISOString().replace(/\.\d+Z$/, ""),
229
+ user_intent: `Delegation journal — round ${round}`,
230
+ task_type: "delegation_journal",
231
+ loop_id: loopId,
232
+ loop_lineage: {
233
+ round,
234
+ delegations: entries.map((e) => ({
235
+ index: e.index,
236
+ agentId: e.agentId,
237
+ subAgentType: e.subAgentType,
238
+ subTask: e.subTask,
239
+ resultSummary: e.resultSummary,
240
+ success: e.success,
241
+ outcome: e.outcome ?? (e.success ? "success" : "failed"),
242
+ discoveredConstraints: e.discoveredConstraints,
243
+ })),
244
+ },
245
+ };
246
+ try {
247
+ this.resolveStore().appendEntry(entry);
248
+ }
249
+ catch (error) {
250
+ if (this.metrics)
251
+ this.metrics.vaultWriteErrors++;
252
+ console.warn(`LoopForge: vault write failed (delegation_persist) — ${error}`);
253
+ logEvent("vault_write_error", { error: "record_delegation" });
254
+ policyMetrics.recordVaultWriteError("delegation_persist");
255
+ }
256
+ }
257
+ // ═══════════════════════════════════════════════════════════════════════
258
+ // Hydrate loop context from vault
259
+ // ═══════════════════════════════════════════════════════════════════════
260
+ /** Eval-owned fields merged from the committed round decision onto the
261
+ * lineage entry. These are the agent's report of what happened during the
262
+ * round; compile-time lineage fields (constraints_active, goal_text_hash,
263
+ * recompile_level, …) are left untouched. */
264
+ static EVAL_MERGED_LINEAGE_FIELDS = [
265
+ "execution_evidence",
266
+ "compression_checkpoint",
267
+ "checkpoint_label",
268
+ "discovered_constraints",
269
+ "emerged_subtasks",
270
+ "retracted_constraints",
271
+ "wrong_assumptions",
272
+ "completed_subtasks",
273
+ "blocked_subtasks",
274
+ "canceled_subtasks",
275
+ "next_action",
276
+ "objective_refinement",
277
+ "outcome",
278
+ "blocker",
279
+ "drift_clarification",
280
+ "revised_success_criteria",
281
+ "worker_results",
282
+ "prompt_requests",
283
+ "round_contract",
284
+ ];
285
+ /** Merge the committed round decision (feedback entry) into the compile-time
286
+ * lineage entry so the compiler and projections see the full per-round
287
+ * truth: verification flags, round success, and the agent's own evaluation. */
288
+ mergeCommittedRound(entry, lineage, fb) {
289
+ const committed = decodeCommittedRound(fb);
290
+ const result = committed?.result ?? null;
291
+ const evaluation = committed?.evaluation ?? null;
292
+ if (result) {
293
+ if (Array.isArray(result.verificationFlags) &&
294
+ result.verificationFlags.length > 0) {
295
+ entry.verification_flags = result.verificationFlags;
296
+ }
297
+ if (typeof result.roundSuccess === "boolean") {
298
+ entry.success = result.roundSuccess;
299
+ lineage.success = result.roundSuccess;
300
+ }
301
+ // The committed decision's action — lets the compiler detect a
302
+ // post-backtrack recovery boundary (L2 rehydration) without a
303
+ // second persistence format.
304
+ lineage.committed_action = result.action;
305
+ }
306
+ if (committed) {
307
+ // v3.5.1: compile-side display readers (Round Stats rejected attempts,
308
+ // the Machine (git) dashboard row) never see raw :feedback entries —
309
+ // only this merged view — so the machine-observed data they need is
310
+ // stamped here. In-memory only (hydration cache; disk lineage entries
311
+ // never carry the stamp — a fresh hydration re-derives it identically
312
+ // from the feedback entry's transaction snapshot).
313
+ lineage.attempt = committed.attempt;
314
+ if (committed.roundEvidence.length > 0) {
315
+ lineage.round_evidence = committed.roundEvidence;
316
+ }
317
+ }
318
+ if (!evaluation)
319
+ return;
320
+ for (const key of LoopForgeEngine.EVAL_MERGED_LINEAGE_FIELDS) {
321
+ const value = evaluation[key];
322
+ if (value === undefined)
323
+ continue;
324
+ lineage[key] = value;
325
+ entry[key] = value;
326
+ }
327
+ if (typeof evaluation.success === "boolean") {
328
+ entry.success = evaluation.success;
329
+ lineage.success = evaluation.success;
330
+ }
331
+ if (typeof evaluation.output_summary === "string" && evaluation.output_summary) {
332
+ entry.output_summary = evaluation.output_summary;
333
+ }
334
+ if (Array.isArray(evaluation.constraint_violations)) {
335
+ entry.constraint_violations = evaluation.constraint_violations;
336
+ }
337
+ }
338
+ /** v3.0.1: Per-engine hydration cache. The vault is append-only per round —
339
+ * between two compiles of one loop only the newest round(s) changed. The
340
+ * cache holds the merged lineage entries (the exact shape the compiler
341
+ * consumes; feedback bodies are merged in, never returned), so a long-lived
342
+ * process compiles incrementally: one round document read per round
343
+ * boundary instead of the full history. Nothing is persisted; a fresh
344
+ * engine hydrates fully once. `coveredRound` is the highest CONTIGUOUS
345
+ * COMMITTED round (feedback merged) whose entries are in the cache — the
346
+ * compiler only reads rounds below the compile round, so
347
+ * `coveredRound >= round - 1` means the cache is complete for that compile.
348
+ * Lineage-only entries of not-yet-committed rounds may trail the cache but
349
+ * never advance coveredRound (a later read replaces them post-commit). */
350
+ hydrationCache = null;
351
+ /** v3.3.1: Drop the hydration cache when a feedback write targets a round
352
+ * the cache has already merged (coveredRound >= round). Cache entries are
353
+ * LINEAGE task_ids (loop:…:rN), so a task_id collision test could never
354
+ * fire for feedback writes (loop:…:rN:feedback) — the round number is the
355
+ * real collision signal. A backtrack redo reuses the roundId and REPLACES
356
+ * the round's committed feedback (v3.2.1 semantics); without this the
357
+ * fast path (coveredRound >= compileRound - 1) would serve the stale
358
+ * rolled-back entry for the rest of the process. A genuinely new round
359
+ * (round = coveredRound + 1) lands past the cache point and is merged by
360
+ * the next incremental read — no invalidation. Lineage writes never
361
+ * invalidate: they precede the commit by design. */
362
+ invalidateHydrationCacheForFeedback(taskId) {
363
+ const cache = this.hydrationCache;
364
+ if (!cache)
365
+ return;
366
+ const round = LoopForgeEngine.feedbackRound(taskId);
367
+ if (round !== null && cache.coveredRound >= round) {
368
+ this.hydrationCache = null;
369
+ }
370
+ }
371
+ /** The round a feedback entry belongs to, or null. */
372
+ static feedbackRound(taskId) {
373
+ const parts = taskId.split(":r");
374
+ if (parts.length < 2)
375
+ return null;
376
+ const roundStr = parts[1].split(":")[0];
377
+ const round = parseInt(roundStr, 10);
378
+ return Number.isNaN(round) ? null : round;
379
+ }
380
+ /** Highest contiguous round (from 1) present in the given feedback entries. */
381
+ static contiguousCommittedRound(fbEntries) {
382
+ const committed = new Set();
383
+ for (const fe of fbEntries) {
384
+ const round = LoopForgeEngine.feedbackRound(String(fe.task_id ?? ""));
385
+ if (round !== null)
386
+ committed.add(round);
387
+ }
388
+ let covered = 0;
389
+ while (committed.has(covered + 1))
390
+ covered++;
391
+ return covered;
392
+ }
393
+ /** Merge committed feedback entries into lineage entries and apply the
394
+ * canonical output_summary / constraint_violations projection. Shared by the
395
+ * full and incremental hydration paths so both produce identical shapes.
396
+ *
397
+ * The round commit entry (`loop:<id>:r<N>:feedback`) carries the full
398
+ * committed decision — verification flags, round success, and the agent's
399
+ * SelfEvaluation — which the compiler needs to derive milestones,
400
+ * constraint decay, sub-goal accumulation, and rolling summaries. Without
401
+ * this merge the cross-round features only see the 8 compile-time lineage
402
+ * fields. */
403
+ buildMergedEntries(fresh, fbFresh) {
404
+ const fbByRound = new Map();
405
+ for (const fe of fbFresh) {
406
+ const fbRound = LoopForgeEngine.feedbackRound(String(fe.task_id ?? ""));
407
+ if (fbRound !== null)
408
+ fbByRound.set(fbRound, fe);
409
+ }
410
+ for (const entry of fresh) {
411
+ const lineage = (entry.loop_lineage ?? entry.lineage ?? {});
412
+ const rnd = lineage.round;
413
+ const fb = rnd ? fbByRound.get(rnd) : undefined;
414
+ if (fb)
415
+ this.mergeCommittedRound(entry, lineage, fb);
416
+ if (!entry.output_summary) {
417
+ entry.output_summary = lineage.output_summary ?? "";
418
+ }
419
+ if (!entry.constraint_violations) {
420
+ entry.constraint_violations =
421
+ lineage.constraint_violations ?? [];
422
+ }
423
+ }
424
+ return fresh;
425
+ }
426
+ hydrateLoopContext(loopId, round) {
427
+ const store = this.resolveStore();
428
+ const prefix = `loop:${loopId}:r`;
429
+ // v3.0.1: cached hydration — fast path when the cache already covers the
430
+ // requested round (same-round retries), incremental read of only the
431
+ // rounds committed since the cache point otherwise.
432
+ if (round !== undefined && this.hydrationCache?.loopId === loopId) {
433
+ const cache = this.hydrationCache;
434
+ if (cache.coveredRound >= round - 1) {
435
+ return { results: cache.entries, global_entries: [] };
436
+ }
437
+ const sinceRound = cache.coveredRound + 1;
438
+ const fresh = queryLoopEntries(store, loopId, { prefix, sinceRound });
439
+ const freshFb = queryLoopEntries(store, loopId, {
440
+ prefix,
441
+ sinceRound,
442
+ feedbackOnly: true,
443
+ });
444
+ const merged = this.buildMergedEntries(fresh, freshFb);
445
+ for (const entry of merged) {
446
+ const tid = String(entry.task_id ?? "");
447
+ // Replace-by-task_id: a round read earlier as a lineage-only entry
448
+ // (not yet committed) must be replaced by its post-commit merged
449
+ // version — a stale unmerged entry would silently drop the round's
450
+ // committed decision (verification flags, committed_action, eval).
451
+ const index = cache.entries.findIndex((e) => String(e.task_id ?? "") === tid);
452
+ if (index >= 0)
453
+ cache.entries[index] = entry;
454
+ else
455
+ cache.entries.push(entry);
456
+ }
457
+ // Advance coveredRound only for rounds that actually committed since
458
+ // the cache point — the fresh reads start at coveredRound + 1, so the
459
+ // contiguous run extends from there.
460
+ const freshCommitted = new Set();
461
+ for (const fe of freshFb) {
462
+ const r = LoopForgeEngine.feedbackRound(String(fe.task_id ?? ""));
463
+ if (r !== null)
464
+ freshCommitted.add(r);
465
+ }
466
+ while (freshCommitted.has(cache.coveredRound + 1))
467
+ cache.coveredRound++;
468
+ return { results: cache.entries, global_entries: [] };
469
+ }
470
+ // Full path — cold engine, different loop, or a round-less hydrate (e.g.
471
+ // health checks). Merge all feedback entries, dedupe by task_id (round
472
+ // documents hold the latest write per key), and cache the merged view.
473
+ // coveredRound counts only contiguous COMMITTED rounds — lineage-only
474
+ // entries of uncommitted rounds may trail the cache but must not make the
475
+ // fast path serve unmerged state.
476
+ const results = queryLoopEntries(store, loopId, { prefix });
477
+ const fbEntries = queryLoopEntries(store, loopId, { prefix, feedbackOnly: true });
478
+ const merged = this.buildMergedEntries(results, fbEntries);
479
+ if (!merged.length)
480
+ return null;
481
+ const byTaskId = new Map();
482
+ for (const entry of merged)
483
+ byTaskId.set(String(entry.task_id ?? ""), entry);
484
+ const entries = [...byTaskId.values()];
485
+ this.hydrationCache = {
486
+ loopId,
487
+ coveredRound: LoopForgeEngine.contiguousCommittedRound(fbEntries),
488
+ entries,
489
+ };
490
+ return { results: entries, global_entries: [] };
491
+ }
492
+ // ═══════════════════════════════════════════════════════════════════════
493
+ // Feedback (public)
494
+ // ═══════════════════════════════════════════════════════════════════════
495
+ invokeFeedback(request, _hydrateResults) {
496
+ this.ensureInit(request);
497
+ this.lastTask = request.task;
498
+ const fb = request.feedback;
499
+ if (!fb) {
500
+ return {
501
+ status: AgentStatus.ERROR,
502
+ response: {
503
+ status: AgentStatus.ERROR,
504
+ prompt: null,
505
+ error: "Feedback mode requires a feedback payload.",
506
+ },
507
+ };
508
+ }
509
+ const success = fb.success;
510
+ const violations = fb.constraint_violations ?? [];
511
+ const fixes = fb.manual_fixes_needed ?? "";
512
+ // Loop-aware task_id for feedback→lineage backfill
513
+ const loopId = request.loop_id;
514
+ const loopRound = request.round;
515
+ // v2.14: feedback entries are loop-scoped — a request without loop_id
516
+ // and round can never be persisted. Fail loudly instead of reporting
517
+ // "Feedback Recorded" for a write that was silently dropped.
518
+ if (!loopId || loopRound === undefined) {
519
+ return {
520
+ status: AgentStatus.ERROR,
521
+ response: {
522
+ status: AgentStatus.ERROR,
523
+ prompt: null,
524
+ error: "Feedback requires loop_id and round — feedback entries are loop-scoped.",
525
+ },
526
+ };
527
+ }
528
+ const taskId = `loop:${loopId}:r${loopRound}:feedback`;
529
+ const signal = {
530
+ task_id: taskId,
531
+ task_type: request.task.slice(0, 80),
532
+ success,
533
+ skill_used: request.skill_name ?? "",
534
+ violations,
535
+ manual_fixes: fixes,
536
+ loop_id: loopId,
537
+ round: loopRound,
538
+ };
539
+ const persisted = this.persistFeedbackToVault(signal);
540
+ if (!persisted) {
541
+ return {
542
+ status: AgentStatus.ERROR,
543
+ response: {
544
+ status: AgentStatus.ERROR,
545
+ prompt: null,
546
+ error: "Feedback could not be persisted to the loop store — the round was not recorded.",
547
+ },
548
+ };
549
+ }
550
+ // Update state
551
+ this.state.call_count++;
552
+ this.state.success_trend.push(success);
553
+ if (this.state.success_trend.length > 20) {
554
+ this.state.success_trend = this.state.success_trend.slice(-20);
555
+ }
556
+ logEvent("round_complete", {
557
+ success,
558
+ loopId: loopId ?? "unknown",
559
+ round: loopRound ?? this.state.call_count,
560
+ });
561
+ return {
562
+ status: AgentStatus.OK,
563
+ response: {
564
+ status: AgentStatus.OK,
565
+ prompt: `## Feedback Recorded\n\nSuccess: ${success}\nSignals: 1`,
566
+ error: null,
567
+ },
568
+ };
569
+ }
570
+ // ═══════════════════════════════════════════════════════════════════════
571
+ // Auto-Feedback (v1.1 — autonomous loop, no human in the loop)
572
+ // ═══════════════════════════════════════════════════════════════════════
573
+ /** Record self-evaluation from agent output without human intervention.
574
+ * Converts SelfEvaluation → ExecutionFeedback → vault persistence.
575
+ * P0–P2: Also persists discovered_constraints, objective_refinement,
576
+ * and emerged_subtasks for the compiler to consume next round.
577
+ * Call this BEFORE invokeLoopCompile for the next round so that
578
+ * hydrateLoopContext picks up the latest success flags. */
579
+ autoFeedback(selfEval, loopId, round, task, roundTransaction) {
580
+ this.ensureInit({ task, mode: Mode.FEEDBACK, feedback: null, skill_name: null, task_id: null });
581
+ const fb = makeExecutionFeedback({
582
+ output: selfEval.output_summary,
583
+ success: selfEval.success,
584
+ constraint_violations: selfEval.constraint_violations,
585
+ manual_fixes_needed: "",
586
+ });
587
+ const taskId = `loop:${loopId}:r${round}:feedback`;
588
+ const transactionId = typeof roundTransaction?.round_id === "string"
589
+ ? roundTransaction.round_id
590
+ : null;
591
+ const transactionPersisted = (filter) => {
592
+ if (!transactionId)
593
+ return false;
594
+ return queryLoopEntries(this.resolveStore(), loopId, {
595
+ prefix: taskId,
596
+ feedbackOnly: true,
597
+ }).some((entry) => {
598
+ if (entry.task_id !== taskId)
599
+ return false;
600
+ if (filter && !filter(entry))
601
+ return false;
602
+ const lineage = entry.loop_lineage;
603
+ const transaction = lineage?.round_transaction;
604
+ return transaction !== null &&
605
+ typeof transaction === "object" &&
606
+ !Array.isArray(transaction) &&
607
+ transaction.round_id === transactionId;
608
+ });
609
+ };
610
+ // v3.2.1: a committed backtrack decision is a roll-back directive, not
611
+ // the round's outcome. The redo submission reuses the same roundId and
612
+ // must REPLACE the backtrack entry in the vault — otherwise the stale
613
+ // entry keeps poisoning the R4/R5 progress window and the redo's work
614
+ // never lands (it is replayed or its data is ignored forever).
615
+ const alreadyCommitted = transactionPersisted((entry) => {
616
+ const transaction = isRecord(entry.loop_lineage)
617
+ ? entry.loop_lineage.round_transaction
618
+ : null;
619
+ const result = isRecord(transaction) ? transaction.result : null;
620
+ return !(isRecord(result) && result.action === "backtrack");
621
+ });
622
+ // Multi-agent: Merge sub-agent discovered constraints into the main constraint flow
623
+ const subDiscovered = (selfEval.worker_results ?? [])
624
+ .flatMap((w) => w.discoveredConstraints ?? [])
625
+ .filter((c) => c.length > 0);
626
+ const mergedDiscovered = [
627
+ ...new Set([...(selfEval.discovered_constraints ?? []), ...subDiscovered]),
628
+ ];
629
+ const signal = {
630
+ task_id: taskId,
631
+ task_type: task.slice(0, 80),
632
+ success: fb.success,
633
+ skill_used: "",
634
+ violations: selfEval.constraint_violations,
635
+ manual_fixes: "",
636
+ loop_id: loopId,
637
+ round,
638
+ // P0–P2: Evolution fields
639
+ discovered_constraints: mergedDiscovered,
640
+ objective_refinement: selfEval.objective_refinement ?? "",
641
+ emerged_subtasks: selfEval.emerged_subtasks ?? [],
642
+ // v3.2.1: persist the sub-goal lifecycle fields — the subgoal_drift
643
+ // check reads them from committed feedback entries; before this they
644
+ // were never written, so prior rounds' completed/canceled sub-goals
645
+ // were invisible and the pending set grew stale.
646
+ completed_subtasks: selfEval.completed_subtasks ?? [],
647
+ blocked_subtasks: selfEval.blocked_subtasks ?? [],
648
+ canceled_subtasks: selfEval.canceled_subtasks ?? [],
649
+ // P4: Execution evidence
650
+ execution_evidence: selfEval.execution_evidence ?? null,
651
+ // P5: Self-correction
652
+ retracted_constraints: selfEval.retracted_constraints ?? [],
653
+ revised_success_criteria: selfEval.revised_success_criteria ?? [],
654
+ wrong_assumptions: selfEval.wrong_assumptions ?? [],
655
+ // Multi-agent: Worker delegation results
656
+ worker_results: selfEval.worker_results ?? [],
657
+ // v2.12: Tri-state outcome (audit data source)
658
+ outcome: selfEval.outcome,
659
+ loop_lineage: roundTransaction
660
+ ? {
661
+ round,
662
+ round_id: roundTransaction.round_id,
663
+ round_transaction: roundTransaction,
664
+ }
665
+ : {},
666
+ };
667
+ if (!alreadyCommitted) {
668
+ this.persistFeedbackToVault(signal);
669
+ if (transactionId && !transactionPersisted()) {
670
+ throw new Error(`Round feedback commit failed: ${transactionId}`);
671
+ }
672
+ }
673
+ // Multi-agent: Auto-record delegation journal from worker_results
674
+ if (selfEval.worker_results && selfEval.worker_results.length > 0) {
675
+ const entries = selfEval.worker_results.map((w, i) => ({
676
+ index: i + 1,
677
+ agentId: w.agentId,
678
+ subAgentType: w.subAgentType,
679
+ subTask: w.subTask,
680
+ resultSummary: w.resultSummary,
681
+ success: w.success,
682
+ outcome: w.outcome,
683
+ discoveredConstraints: w.discoveredConstraints ?? [],
684
+ }));
685
+ this.recordDelegation(loopId, round, entries);
686
+ }
687
+ // Replaying a committed transaction may repair derived delegation data,
688
+ // but must never apply the feedback to mutable engine state twice.
689
+ if (alreadyCommitted)
690
+ return fb.success;
691
+ // Update state
692
+ this.state.call_count++;
693
+ this.state.success_trend.push(fb.success);
694
+ if (this.state.success_trend.length > 20) {
695
+ this.state.success_trend = this.state.success_trend.slice(-20);
696
+ }
697
+ // v2.5: Old binary-success circuit breaker removed — stalled loops
698
+ // are now detected by the enforcement gate (R4/R5) using
699
+ // progress_estimate gradients rather than success=true/false count.
700
+ logEvent("round_complete", {
701
+ loopId,
702
+ round,
703
+ success: fb.success,
704
+ });
705
+ return fb.success;
706
+ }
707
+ // ═══════════════════════════════════════════════════════════════════════
708
+ // Loop Compile (public, primary)
709
+ // ═══════════════════════════════════════════════════════════════════════
710
+ invokeLoopCompile(request, hydrateResults, options = {}) {
711
+ this.ensureInit(request);
712
+ this.lastTask = request.task;
713
+ // Parse extras via typed extraction pipeline (loop-extras-parser.ts).
714
+ // Errors are collected but never thrown — the compiler always gets
715
+ // best-effort defaults so a malformed request doesn't crash the engine.
716
+ const extras = request;
717
+ const { parsed, ctx } = parseLoopExtras(extras, request.task_id ?? "");
718
+ if (ctx.errors.length > 0) {
719
+ logEvent("extras_parse_errors", {
720
+ errors: ctx.errors.map((e) => `${e.field}: ${e.message}`),
721
+ });
722
+ }
723
+ const lcr = makeLoopCompileRequest({
724
+ loop_id: parsed.loop_id,
725
+ round: parsed.round,
726
+ goal_id: parsed.goal_id,
727
+ task: request.task,
728
+ domain: parsed.domain,
729
+ next_task_proposal: parsed.next_task_proposal,
730
+ plan_source: parsed.plan_source,
731
+ constraints_from_plan: parsed.constraints_from_plan,
732
+ new_since_last_round: parsed.new_since_last_round,
733
+ force_level: parsed.force_level,
734
+ health_check_interval: parsed.health_check_interval,
735
+ external_context: parsed.external_context,
736
+ max_rounds: parsed.max_rounds,
737
+ verification_flags: parsed.verification_flags,
738
+ attempt: parsed.attempt,
739
+ consecutive_rejections: parsed.consecutive_rejections,
740
+ rejection_notice: parsed.rejection_notice,
741
+ });
742
+ // Convert last_round_result if present (object already validated by parser)
743
+ if (parsed.last_round_result) {
744
+ const rr = parsed.last_round_result;
745
+ // Parse P4 execution evidence (shared helper)
746
+ const executionEvidence = parseExecutionEvidence(rr.execution_evidence);
747
+ // Parse P5 revised_success_criteria (shared parser)
748
+ const revisedCriteria = parseCriterionRevisions(rr.revised_success_criteria);
749
+ lcr.last_round_result = makeLoopRoundResult({
750
+ round: rr.round ?? 0,
751
+ success: rr.success ?? false,
752
+ output_summary: rr.output_summary ?? "",
753
+ constraint_violations: rr.constraint_violations ?? [],
754
+ manual_fixes_needed: rr.manual_fixes_needed ?? "",
755
+ // P0–P2: Cognitive evolution fields
756
+ discovered_constraints: Array.isArray(rr.discovered_constraints)
757
+ ? rr.discovered_constraints.filter((v) => typeof v === "string")
758
+ : [],
759
+ objective_refinement: typeof rr.objective_refinement === "string"
760
+ ? rr.objective_refinement
761
+ : "",
762
+ emerged_subtasks: Array.isArray(rr.emerged_subtasks)
763
+ ? rr.emerged_subtasks.filter((v) => typeof v === "string")
764
+ : [],
765
+ // P4: Execution evidence
766
+ execution_evidence: executionEvidence,
767
+ // P5: Self-correction
768
+ retracted_constraints: Array.isArray(rr.retracted_constraints)
769
+ ? rr.retracted_constraints.filter((v) => typeof v === "string")
770
+ : [],
771
+ revised_success_criteria: revisedCriteria,
772
+ wrong_assumptions: Array.isArray(rr.wrong_assumptions)
773
+ ? rr.wrong_assumptions.filter((v) => typeof v === "string")
774
+ : [],
775
+ // v1.10: Checkpoint boundary
776
+ compression_checkpoint: typeof rr.compression_checkpoint === "boolean" ? rr.compression_checkpoint : false,
777
+ checkpoint_label: typeof rr.checkpoint_label === "string" ? rr.checkpoint_label : "",
778
+ // v2.2: Sub-goal lifecycle
779
+ completed_subtasks: Array.isArray(rr.completed_subtasks)
780
+ ? rr.completed_subtasks.filter((v) => typeof v === "string")
781
+ : [],
782
+ blocked_subtasks: Array.isArray(rr.blocked_subtasks)
783
+ ? rr.blocked_subtasks.filter((v) => typeof v === "string")
784
+ : [],
785
+ canceled_subtasks: Array.isArray(rr.canceled_subtasks)
786
+ ? rr.canceled_subtasks.filter((v) => typeof v === "string")
787
+ : [],
788
+ // v2.8: Drift clarification
789
+ drift_clarification: typeof rr.drift_clarification === "string"
790
+ ? rr.drift_clarification
791
+ : undefined,
792
+ // Multi-agent: Worker delegation results (shared parser)
793
+ worker_results: parseWorkerResults(rr.worker_results),
794
+ // v2.12: Tri-state outcome + blocker + retroactive claims
795
+ outcome: rr.outcome === "success" || rr.outcome === "partial" ||
796
+ rr.outcome === "failed" || rr.outcome === "blocked"
797
+ ? rr.outcome
798
+ : undefined,
799
+ blocker: typeof rr.blocker === "string" && rr.blocker.trim().length > 0
800
+ ? rr.blocker.slice(0, 500)
801
+ : undefined,
802
+ retroactiveClaims: Array.isArray(rr.retroactiveClaims)
803
+ ? rr.retroactiveClaims
804
+ .filter((item) => isRecord(item) &&
805
+ typeof item.round === "number" && item.round >= 1 &&
806
+ typeof item.claim === "string" && item.claim.length > 0)
807
+ .map((item) => ({ round: item.round, claim: item.claim.slice(0, 500) }))
808
+ .slice(0, 20)
809
+ : [],
810
+ // v3.3.1: next_action + prompt_requests — this whitelist rebuild
811
+ // previously dropped them, and since EVERY compile path (MCP
812
+ // advance/retry/backtrack + Runtime) funnels through
813
+ // invokeLoopCompile, the compiler never saw them on the production
814
+ // path: "Next Action" never rendered, suggested_next_task stayed
815
+ // empty, sub-goal Phase-3 auto in_progress never fired, and
816
+ // prompt_requests (emphasize/confusion_points) were never
817
+ // consumed. Unit tests fed compileLoop directly, so 783 greens
818
+ // missed the gap. Shared lenient parsers mirror the other fields.
819
+ next_action: typeof rr.next_action === "string" ? rr.next_action : undefined,
820
+ prompt_requests: parsePromptRequests(rr.prompt_requests),
821
+ });
822
+ }
823
+ // Convert loop_objective if present (object already validated by parser)
824
+ if (parsed.loop_objective) {
825
+ const obj = parsed.loop_objective;
826
+ lcr.loop_objective = makeLoopObjective({
827
+ objective: obj.objective ?? "",
828
+ success_criteria: obj.success_criteria ?? [],
829
+ hard_constraints: obj.hard_constraints ?? [],
830
+ created_at_round: obj.created_at_round ?? 1,
831
+ loop_id: obj.loop_id ?? "",
832
+ });
833
+ }
834
+ // Hydrate vault context for cross-round memory
835
+ let context = hydrateResults ?? null;
836
+ if (context === null && lcr.loop_id && lcr.round > 1) {
837
+ // v3.0.1: the compile round anchors the hydration cache — warm caches
838
+ // skip the read entirely; otherwise only rounds committed since the
839
+ // last hydration are read.
840
+ context = this.hydrateLoopContext(lcr.loop_id, lcr.round);
841
+ }
842
+ // Delegate to pure-function compiler
843
+ let response;
844
+ try {
845
+ response = compileLoop(lcr, context);
846
+ }
847
+ catch (exc) {
848
+ return {
849
+ status: AgentStatus.ERROR,
850
+ response: {
851
+ status: AgentStatus.ERROR,
852
+ prompt: null,
853
+ error: `loop_compile failed: ${exc}`,
854
+ },
855
+ };
856
+ }
857
+ // Persist lineage to vault
858
+ if (options.persistLineage !== false) {
859
+ this.persistLoopLineage(response, lcr);
860
+ }
861
+ return {
862
+ status: AgentStatus.OK,
863
+ response: {
864
+ status: AgentStatus.OK,
865
+ prompt: response.prompt,
866
+ error: null,
867
+ state_file_content: response.state_file_content,
868
+ prompt_artifact: response.prompt_artifact,
869
+ warnings: response.warnings,
870
+ // v2.12: Pass the compiler's derived state through for typed
871
+ // projections (consumed by getProjection without re-persisting).
872
+ loop_objective: response.loop_objective,
873
+ rolling_summary: response.rolling_summary,
874
+ sub_goals: response.sub_goals,
875
+ criterion_statuses: response.criterion_statuses,
876
+ suggested_next_task: response.suggested_next_task,
877
+ },
878
+ };
879
+ }
880
+ }
881
+ // ═══════════════════════════════════════════════════════════════════════════
882
+ // Factory
883
+ // ═══════════════════════════════════════════════════════════════════════════
884
+ export function createEngine(store) {
885
+ return new LoopForgeEngine(store);
886
+ }
887
+ //# sourceMappingURL=engine.js.map