loopforge 3.9.2 → 4.0.7

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