loopforge 1.15.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +172 -95
  2. package/dist/backends/interface.d.ts +3 -9
  3. package/dist/backends/interface.d.ts.map +1 -1
  4. package/dist/backends/interface.js +1 -5
  5. package/dist/backends/interface.js.map +1 -1
  6. package/dist/canonical-state.d.ts +53 -0
  7. package/dist/canonical-state.d.ts.map +1 -0
  8. package/dist/canonical-state.js +161 -0
  9. package/dist/canonical-state.js.map +1 -0
  10. package/dist/cli.d.ts +4 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +227 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/enforcement-gate.d.ts.map +1 -1
  15. package/dist/enforcement-gate.js +28 -10
  16. package/dist/enforcement-gate.js.map +1 -1
  17. package/dist/engine.d.ts +7 -4
  18. package/dist/engine.d.ts.map +1 -1
  19. package/dist/engine.js +78 -138
  20. package/dist/engine.js.map +1 -1
  21. package/dist/evidence-provider.d.ts +113 -0
  22. package/dist/evidence-provider.d.ts.map +1 -0
  23. package/dist/evidence-provider.js +419 -0
  24. package/dist/evidence-provider.js.map +1 -0
  25. package/dist/index.d.ts +18 -10
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +15 -9
  28. package/dist/index.js.map +1 -1
  29. package/dist/init.d.ts +15 -0
  30. package/dist/init.d.ts.map +1 -0
  31. package/dist/init.js +49 -0
  32. package/dist/init.js.map +1 -0
  33. package/dist/interop.d.ts +30 -0
  34. package/dist/interop.d.ts.map +1 -0
  35. package/dist/interop.js +29 -0
  36. package/dist/interop.js.map +1 -0
  37. package/dist/loop-compiler.d.ts +14 -64
  38. package/dist/loop-compiler.d.ts.map +1 -1
  39. package/dist/loop-compiler.js +303 -2206
  40. package/dist/loop-compiler.js.map +1 -1
  41. package/dist/loop-store.d.ts +82 -0
  42. package/dist/loop-store.d.ts.map +1 -0
  43. package/dist/loop-store.js +332 -0
  44. package/dist/loop-store.js.map +1 -0
  45. package/dist/mcp/server.d.ts +9 -6
  46. package/dist/mcp/server.d.ts.map +1 -1
  47. package/dist/mcp/server.js +109 -73
  48. package/dist/mcp/server.js.map +1 -1
  49. package/dist/mcp/session.d.ts +75 -19
  50. package/dist/mcp/session.d.ts.map +1 -1
  51. package/dist/mcp/session.js +707 -361
  52. package/dist/mcp/session.js.map +1 -1
  53. package/dist/mcp/tools.d.ts +18 -268
  54. package/dist/mcp/tools.d.ts.map +1 -1
  55. package/dist/mcp/tools.js +225 -12
  56. package/dist/mcp/tools.js.map +1 -1
  57. package/dist/observability.d.ts +36 -16
  58. package/dist/observability.d.ts.map +1 -1
  59. package/dist/observability.js +82 -23
  60. package/dist/observability.js.map +1 -1
  61. package/dist/policy-metrics.d.ts +46 -0
  62. package/dist/policy-metrics.d.ts.map +1 -0
  63. package/dist/policy-metrics.js +146 -0
  64. package/dist/policy-metrics.js.map +1 -0
  65. package/dist/policy.d.ts +53 -140
  66. package/dist/policy.d.ts.map +1 -1
  67. package/dist/policy.js +119 -184
  68. package/dist/policy.js.map +1 -1
  69. package/dist/prompt-assembler.d.ts +30 -0
  70. package/dist/prompt-assembler.d.ts.map +1 -0
  71. package/dist/prompt-assembler.js +257 -0
  72. package/dist/prompt-assembler.js.map +1 -0
  73. package/dist/prompt-policy.d.ts +29 -0
  74. package/dist/prompt-policy.d.ts.map +1 -0
  75. package/dist/prompt-policy.js +51 -0
  76. package/dist/prompt-policy.js.map +1 -0
  77. package/dist/protocol.d.ts +88 -93
  78. package/dist/protocol.d.ts.map +1 -1
  79. package/dist/protocol.js +23 -39
  80. package/dist/protocol.js.map +1 -1
  81. package/dist/replay.d.ts.map +1 -1
  82. package/dist/replay.js +1 -27
  83. package/dist/replay.js.map +1 -1
  84. package/dist/round-coordinator.d.ts +90 -0
  85. package/dist/round-coordinator.d.ts.map +1 -0
  86. package/dist/round-coordinator.js +186 -0
  87. package/dist/round-coordinator.js.map +1 -0
  88. package/dist/round-driver.d.ts +53 -0
  89. package/dist/round-driver.d.ts.map +1 -0
  90. package/dist/round-driver.js +101 -0
  91. package/dist/round-driver.js.map +1 -0
  92. package/dist/round-transaction.d.ts +70 -0
  93. package/dist/round-transaction.d.ts.map +1 -0
  94. package/dist/round-transaction.js +290 -0
  95. package/dist/round-transaction.js.map +1 -0
  96. package/dist/runtime.d.ts +33 -19
  97. package/dist/runtime.d.ts.map +1 -1
  98. package/dist/runtime.js +355 -336
  99. package/dist/runtime.js.map +1 -1
  100. package/dist/storage.d.ts +38 -0
  101. package/dist/storage.d.ts.map +1 -0
  102. package/dist/storage.js +201 -0
  103. package/dist/storage.js.map +1 -0
  104. package/dist/verification-gate.d.ts +33 -7
  105. package/dist/verification-gate.d.ts.map +1 -1
  106. package/dist/verification-gate.js +195 -7
  107. package/dist/verification-gate.js.map +1 -1
  108. package/loop_policy.json +22 -48
  109. package/package.json +13 -10
  110. package/skills/perception/SKILL.md +80 -267
  111. package/dist/backends/fs.d.ts +0 -36
  112. package/dist/backends/fs.d.ts.map +0 -1
  113. package/dist/backends/fs.js +0 -362
  114. package/dist/backends/fs.js.map +0 -1
  115. package/dist/builder.d.ts +0 -19
  116. package/dist/builder.d.ts.map +0 -1
  117. package/dist/builder.js +0 -215
  118. package/dist/builder.js.map +0 -1
  119. package/dist/mcp-server.d.ts +0 -12
  120. package/dist/mcp-server.d.ts.map +0 -1
  121. package/dist/mcp-server.js +0 -14
  122. package/dist/mcp-server.js.map +0 -1
  123. package/dist/memory-bridge.d.ts +0 -47
  124. package/dist/memory-bridge.d.ts.map +0 -1
  125. package/dist/memory-bridge.js +0 -290
  126. package/dist/memory-bridge.js.map +0 -1
  127. package/dist/tests/_helpers.d.ts +0 -25
  128. package/dist/tests/_helpers.d.ts.map +0 -1
  129. package/dist/tests/_helpers.js +0 -47
  130. package/dist/tests/_helpers.js.map +0 -1
  131. package/dist/tests/builder.test.d.ts +0 -2
  132. package/dist/tests/builder.test.d.ts.map +0 -1
  133. package/dist/tests/builder.test.js +0 -112
  134. package/dist/tests/builder.test.js.map +0 -1
  135. package/dist/tests/enforcement-gate.test.d.ts +0 -2
  136. package/dist/tests/enforcement-gate.test.d.ts.map +0 -1
  137. package/dist/tests/enforcement-gate.test.js +0 -328
  138. package/dist/tests/enforcement-gate.test.js.map +0 -1
  139. package/dist/tests/engine.test.d.ts +0 -2
  140. package/dist/tests/engine.test.d.ts.map +0 -1
  141. package/dist/tests/engine.test.js +0 -375
  142. package/dist/tests/engine.test.js.map +0 -1
  143. package/dist/tests/generate-schema.test.d.ts +0 -8
  144. package/dist/tests/generate-schema.test.d.ts.map +0 -1
  145. package/dist/tests/generate-schema.test.js +0 -199
  146. package/dist/tests/generate-schema.test.js.map +0 -1
  147. package/dist/tests/loop-compiler.test.d.ts +0 -2
  148. package/dist/tests/loop-compiler.test.d.ts.map +0 -1
  149. package/dist/tests/loop-compiler.test.js +0 -1059
  150. package/dist/tests/loop-compiler.test.js.map +0 -1
  151. package/dist/tests/mcp.test.d.ts +0 -7
  152. package/dist/tests/mcp.test.d.ts.map +0 -1
  153. package/dist/tests/mcp.test.js +0 -505
  154. package/dist/tests/mcp.test.js.map +0 -1
  155. package/dist/tests/memory-bridge.test.d.ts +0 -8
  156. package/dist/tests/memory-bridge.test.d.ts.map +0 -1
  157. package/dist/tests/memory-bridge.test.js +0 -383
  158. package/dist/tests/memory-bridge.test.js.map +0 -1
  159. package/dist/tests/policy.test.d.ts +0 -2
  160. package/dist/tests/policy.test.d.ts.map +0 -1
  161. package/dist/tests/policy.test.js +0 -49
  162. package/dist/tests/policy.test.js.map +0 -1
  163. package/dist/tests/protocol.test.d.ts +0 -2
  164. package/dist/tests/protocol.test.d.ts.map +0 -1
  165. package/dist/tests/protocol.test.js +0 -73
  166. package/dist/tests/protocol.test.js.map +0 -1
  167. package/dist/tests/replay.test.d.ts +0 -2
  168. package/dist/tests/replay.test.d.ts.map +0 -1
  169. package/dist/tests/replay.test.js +0 -222
  170. package/dist/tests/replay.test.js.map +0 -1
  171. package/dist/tests/runtime.test.d.ts +0 -7
  172. package/dist/tests/runtime.test.d.ts.map +0 -1
  173. package/dist/tests/runtime.test.js +0 -337
  174. package/dist/tests/runtime.test.js.map +0 -1
  175. package/dist/tests/verification-gate.test.d.ts +0 -2
  176. package/dist/tests/verification-gate.test.d.ts.map +0 -1
  177. package/dist/tests/verification-gate.test.js +0 -407
  178. package/dist/tests/verification-gate.test.js.map +0 -1
  179. package/skills/prompt-techniques/SKILL.md +0 -52
  180. package/skills/prompt-techniques/references/chain-of-thought.md +0 -207
  181. package/skills/prompt-techniques/references/few-shot.md +0 -174
  182. package/skills/prompt-techniques/references/least-to-most.md +0 -182
  183. package/skills/prompt-techniques/references/step-back.md +0 -160
  184. package/skills/prompt-techniques/references/tree-of-thought.md +0 -191
  185. package/skills/prompt-techniques/references/zero-shot.md +0 -97
@@ -1,2254 +1,351 @@
1
- /** LoopForge-loop_compile — Loop Compiler (v3.5 core).
1
+ /** LoopForge cognitive-state compiler.
2
2
  *
3
- * Pure-function module for per-loop-iteration prompt compilation.
4
- *
5
- * Two layers:
6
- * Layer 1 (Hard Gates): decideLevel() — 4-gate routing that CAN change compile level.
7
- * Layer 2 (Soft Advisories): computeAdvisories() — warnings/alignment/health, NEVER
8
- * change compile level directly.
9
- *
10
- * Compilation: compileL0() / compileL1() / compileL2() produce the actual prompt.
3
+ * The compiler evolves structured state and renders one prompt artifact.
4
+ * L0/L1/L2 control state density only; the external Agent owns reasoning.
11
5
  */
12
6
  import { createHash } from "node:crypto";
13
- import { readFileSync } from "node:fs";
14
7
  import { getPolicy } from "./policy.js";
15
8
  import { AgentStatus, makeCheckpointSummary, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeRollingSummary, makeTaskAlignment, } from "./protocol.js";
16
- import { routeTechniqueAdaptive, SKILLS_DIR } from "./builder.js";
17
- // ═══════════════════════════════════════════════════════════════════════════
18
- // Repair cue detection
19
- // ═══════════════════════════════════════════════════════════════════════════
20
- const REPAIR_CUES = [
21
- "fix", "repair", "revise", "correct", "polish", "bug", "error",
22
- "修复", "修改", "修正", "纠错", "补充", "改一下",
23
- ];
24
- function detectsRepairSignal(request) {
25
- let text = (request.new_since_last_round || "").toLowerCase();
26
- if (request.last_round_result) {
27
- text += " " + (request.last_round_result.output_summary || "").toLowerCase();
28
- if (request.last_round_result.manual_fixes_needed) {
29
- text += " " + request.last_round_result.manual_fixes_needed.toLowerCase();
30
- }
31
- }
32
- return REPAIR_CUES.some((cue) => text.includes(cue));
33
- }
34
- // ═══════════════════════════════════════════════════════════════════════════
35
- // Tokenization helpers
36
- // ═══════════════════════════════════════════════════════════════════════════
37
- export function tokenize(text) {
38
- const tokens = text.split(/\s+/);
39
- const result = new Set();
40
- for (let token of tokens) {
41
- token = token.trim().replace(/^[.,;:!?()[\]{}'"]+|[.,;:!?()[\]{}'"]+$/g, "");
42
- if (token.length >= 2) {
43
- result.add(/^[\x00-\x7F]*$/.test(token) ? token.toLowerCase() : token);
44
- }
45
- // Add individual CJK chars as standalone tokens
46
- for (const ch of token) {
47
- if ((ch >= "一" && ch <= "鿿") ||
48
- (ch >= "぀" && ch <= "ヿ")) {
49
- result.add(ch);
50
- }
51
- }
52
- }
53
- return result;
54
- }
55
- export function jaccard(a, b) {
56
- if (a.size === 0 || b.size === 0)
57
- return 0.0;
58
- let intersection = 0;
59
- for (const item of a) {
60
- if (b.has(item))
61
- intersection++;
62
- }
63
- const union = a.size + b.size - intersection;
64
- return union > 0 ? intersection / union : 0.0;
65
- }
66
- // ═══════════════════════════════════════════════════════════════════════════
67
- // Delegation helpers (v1.9 — AgentTool mode lightweight utilities)
68
- // ═══════════════════════════════════════════════════════════════════════════
69
- /** Filter relevant constraints for a sub-agent task using Jaccard token similarity.
70
- * Returns constraints whose token overlap with the subTask exceeds the threshold.
71
- * Default threshold 0.15 is intentionally lower than the 0.3/0.5 alignment thresholds
72
- * — constraint filtering should err on the side of inclusion. */
73
- export function filterConstraintsForSubTask(allConstraints, subTask, threshold = 0.15) {
74
- if (!allConstraints.length || !subTask.trim())
75
- return [];
76
- const taskTokens = tokenize(subTask);
77
- if (taskTokens.size === 0)
78
- return [];
79
- return allConstraints.filter((c) => {
80
- const cTokens = tokenize(c);
81
- return jaccard(taskTokens, cTokens) >= threshold;
82
- });
83
- }
84
- /** Format a self-contained delegation prompt for a sub-agent.
85
- * Produces a prompt that stands alone — no references to parent conversation,
86
- * no "based on above", no "continue from previous". This matches the AgentTool
87
- * contract: "Workers can't see your conversation." */
88
- export function formatDelegationPrompt(subTask, subAgentType, relevantConstraints, options) {
89
- const lines = [];
90
- // Header
91
- lines.push("### Delegated Task");
92
- lines.push("");
93
- lines.push(subTask);
94
- lines.push("");
95
- // Context (optional — e.g. file paths, module names)
96
- if (options?.context) {
97
- lines.push("### Context");
98
- lines.push("");
99
- lines.push(options.context);
100
- lines.push("");
101
- }
102
- // Constraints (only for types that need them — Explore is read-only)
103
- if (relevantConstraints.length && subAgentType !== "explore") {
104
- lines.push("### Relevant Constraints");
105
- for (const c of relevantConstraints)
106
- lines.push(`- ${c}`);
107
- lines.push("");
108
- }
109
- // Output format (for Explore and Plan agents)
110
- if (subAgentType === "explore") {
111
- if (options?.outputFormat) {
112
- lines.push("### Output Format");
113
- lines.push("");
114
- lines.push(options.outputFormat);
115
- lines.push("");
116
- }
117
- else {
118
- lines.push("### Output Format");
119
- lines.push("");
120
- lines.push("Report findings as a structured list with file paths and line numbers.");
121
- lines.push("");
122
- }
123
- }
124
- if (subAgentType === "plan") {
125
- lines.push("### Instructions");
126
- lines.push("");
127
- lines.push("Design a concrete implementation plan. Output must include:");
128
- lines.push("- Step-by-step approach");
129
- lines.push("- Critical files for implementation (3-5 files with paths)");
130
- lines.push("- Key design decisions and tradeoffs");
131
- lines.push("");
132
- }
133
- if (subAgentType === "general-purpose") {
134
- lines.push("### Instructions");
135
- lines.push("");
136
- lines.push("Complete the task fully — don't gold-plate, but don't leave it half-done.");
137
- lines.push("Report what was done, what files were changed, and any issues encountered.");
138
- lines.push("");
139
- }
140
- if (!["explore", "general-purpose", "plan"].includes(subAgentType)) {
141
- // Custom sub-agent type — generic template
142
- lines.push("### Instructions");
143
- lines.push("");
144
- lines.push("Complete the task above and report results concisely.");
145
- lines.push("");
146
- }
147
- return lines.join("\n");
148
- }
149
- /** Build a delegation history summary from vault context (v1.9 — multi-agent).
150
- * Scans vault for delegation_journal entries and formats them as a table.
151
- * Returns empty string if no delegation history exists. */
152
- export function buildDelegationSummary(vaultContext) {
153
- if (!vaultContext)
154
- return "";
155
- const results = vaultContext.results;
156
- if (!results?.length)
157
- return "";
158
- const delegations = results.filter((r) => r?.task_type === "delegation_journal");
159
- if (!delegations.length)
160
- return "";
161
- // Helper: escape pipe characters so they don't break the markdown table
162
- const esc = (s) => s.replace(/\|/g, "\\|");
163
- // Collect all delegation entries across rounds
164
- const entries = [];
165
- for (const d of delegations) {
166
- const lineage = d.loop_lineage;
167
- const round = typeof lineage?.round === "number" ? lineage.round : 0;
168
- const dels = lineage?.delegations;
169
- if (!dels?.length)
170
- continue;
171
- for (const e of dels) {
172
- entries.push({
173
- round,
174
- agentId: typeof e.agentId === "string" ? e.agentId : (typeof e.index === "number" ? `worker-${e.index}` : "?"),
175
- type: typeof e.subAgentType === "string" ? e.subAgentType : "?",
176
- task: typeof e.subTask === "string" ? e.subTask : "?",
177
- result: typeof e.resultSummary === "string" ? e.resultSummary : "?",
178
- success: typeof e.success === "boolean" ? e.success : true,
179
- });
180
- }
181
- }
182
- if (!entries.length)
183
- return "";
184
- const lines = [
185
- "",
186
- "### Delegation History (Multi-Agent)",
187
- "",
188
- "| Round | Agent | Type | Task | Result | ✓/✗ |",
189
- "|-------|-------|------|------|--------|-----|",
190
- ];
191
- for (const e of entries) {
192
- const status = e.success ? "✓" : "✗";
193
- const shortAgent = e.agentId.length > 24 ? e.agentId.slice(0, 21) + "..." : e.agentId;
194
- const shortTask = e.task.length > 50 ? e.task.slice(0, 47) + "..." : e.task;
195
- const shortResult = e.result.length > 40 ? e.result.slice(0, 37) + "..." : e.result;
196
- lines.push(`| R${e.round} | ${esc(shortAgent)} | ${e.type} | ${esc(shortTask)} | ${esc(shortResult)} | ${status} |`);
197
- }
198
- lines.push("");
199
- return lines.join("\n");
200
- }
201
- // ═══════════════════════════════════════════════════════════════════════════
202
- // Goal identity
203
- // ═══════════════════════════════════════════════════════════════════════════
204
- export function computeGoalTextHash(task) {
205
- const normalized = (task || "").trim().toLowerCase().replace(/\s+/g, " ");
206
- return createHash("sha256").update(normalized, "utf-8").digest("hex").slice(0, 12);
207
- }
208
- export function deriveGoalId(loopId, task, explicitGoalId = "") {
209
- if (explicitGoalId)
210
- return explicitGoalId;
211
- let taskPrefix = (task || "unnamed").slice(0, 60).trim().toLowerCase();
212
- taskPrefix = taskPrefix.replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
213
- return loopId ? `${loopId}:${taskPrefix}` : taskPrefix;
214
- }
215
- export function getPreviousRound(loopId, roundNum, vaultContext) {
216
- if (vaultContext === null)
217
- return null;
218
- const results = vaultContext.results || [];
219
- if (!results.length)
220
- return null;
221
- for (const r of results) {
222
- const lineage = (r.loop_lineage || r.lineage || {});
223
- if (lineage.loop_id === loopId && lineage.round === roundNum) {
224
- return {
225
- goal_id: lineage.goal_id ?? "",
226
- goal_text_hash: lineage.goal_text_hash ?? "",
227
- success: r.success ?? lineage.success ?? true,
228
- task: r.task ?? r.user_intent ?? "",
229
- constraints_active: lineage.constraints_active ?? [],
230
- prompt_text: r.full_prompt ?? "",
231
- };
232
- }
233
- }
234
- return null;
235
- }
236
- function getRecentRounds(loopId, n, vaultContext) {
237
- if (vaultContext === null)
9
+ import { createCanonicalLoopState, renderCanonicalStateMarkdown, } from "./canonical-state.js";
10
+ import { assemblePromptArtifact } from "./prompt-assembler.js";
11
+ import { decidePromptLevel, } from "./prompt-policy.js";
12
+ function unique(values) {
13
+ return [...new Set(values
14
+ .filter((value) => typeof value === "string")
15
+ .map((value) => value.trim())
16
+ .filter(Boolean))];
17
+ }
18
+ function contextEntries(context) {
19
+ if (!context || !Array.isArray(context.results))
238
20
  return [];
239
- const results = vaultContext.results || [];
240
- const rounds = [];
241
- for (const r of results) {
242
- const lineage = (r.loop_lineage || r.lineage || {});
243
- if (lineage.loop_id === loopId) {
244
- rounds.push({
245
- success: r.success ?? lineage.success ?? false,
246
- round: lineage.round ?? 0,
247
- goal_text_hash: lineage.goal_text_hash ?? "",
248
- });
249
- }
250
- }
251
- rounds.sort((a, b) => b.round - a.round);
252
- return rounds.slice(0, n);
253
- }
254
- /** Extract the technique used in the most recent round before currentRound.
255
- * Scans vault entries for this loop_id and returns the technique_used field
256
- * from the highest round number below currentRound.
257
- * Returns null if no prior rounds exist (round 1 scenario). */
258
- function getPreviousTechnique(loopId, currentRound, vaultContext) {
259
- if (vaultContext === null || currentRound <= 1)
260
- return null;
261
- const results = vaultContext.results || [];
262
- if (!results.length)
263
- return null;
264
- let bestTechnique = null;
265
- let bestRound = -1;
266
- for (const r of results) {
267
- const lineage = (r.loop_lineage || r.lineage || {});
268
- if (lineage.loop_id !== loopId)
269
- continue;
270
- const rnd = lineage.round;
271
- if (rnd >= currentRound || rnd <= bestRound)
272
- continue;
273
- // technique_used can be on the entry directly or in lineage
274
- const technique = r.technique_used ??
275
- lineage.technique_used ?? "";
276
- if (technique) {
277
- bestTechnique = technique;
278
- bestRound = rnd;
279
- }
280
- }
281
- return bestTechnique;
282
- }
283
- function techniqueChanged(loopId, newTechnique, currentRound, vaultContext) {
284
- if (currentRound === 1)
285
- return true; // Round 1: first selection is always a "change"
286
- const prevTech = getPreviousTechnique(loopId, currentRound, vaultContext);
287
- if (!prevTech)
288
- return true; // No prior technique found → treat as change
289
- return newTechnique !== prevTech;
290
- }
291
- /** Returns the mandatory state file read instruction that goes at the top
292
- * of every compiled prompt. Tells the agent to read the loop state file
293
- * before executing the round. */
294
- function renderStateFilePointer(loopId) {
295
- return [
296
- `⚠️ **BEFORE anything else**: Read \`.loopforge/state/${loopId}-state.md\``,
297
- "for accumulated loop state — objective, constraints, progress, and cross-round summary.",
298
- "This file is rewritten each round and is the single source of truth for loop state.",
299
- "",
300
- ].join("\n");
301
- }
302
- /** Render the full loop state file as markdown. Called by compileL2() every round.
303
- * The file is a snapshot — rewritten in full each round, not appended.
304
- * All sections have hard size caps from policy to prevent bloat.
305
- *
306
- * Sections: Loop Objective, Progress, Cross-Round Summary, Active Constraints,
307
- * Checkpoints, Delegation Summary. */
308
- function renderStateFile(params) {
309
- const lines = [];
310
- const sfPolicy = getPolicy().state_file;
311
- // ── Header ──
312
- lines.push(`# LoopForge State — ${params.loopId}`);
313
- lines.push("");
314
- lines.push(`**Round**: ${params.currentRound}/${params.maxRounds}`);
315
- lines.push("");
316
- // ── Loop Objective ──
317
- if (params.loopObjective) {
318
- const lo = params.loopObjective;
319
- lines.push("## Loop Objective");
320
- if (lo.version && lo.version > 1) {
321
- lines.push(`> Version ${lo.version}, created at Round ${lo.created_at_round}`);
322
- }
323
- lines.push("");
324
- lines.push(`**Objective**: ${lo.objective}`);
325
- lines.push("");
326
- if (lo.success_criteria.length) {
327
- lines.push("### Success Criteria");
328
- for (const sc of lo.success_criteria) {
329
- // Check if this criterion has been met (from execution_evidence)
330
- lines.push(`- ${sc}`);
331
- }
332
- lines.push("");
333
- }
334
- if (lo.hard_constraints.length) {
335
- lines.push("### Hard Constraints");
336
- for (const hc of lo.hard_constraints) {
337
- lines.push(`- ${hc}`);
338
- }
339
- lines.push("");
340
- }
341
- }
342
- // ── Progress Dashboard ──
343
- if (params.progressDashboard) {
344
- lines.push(params.progressDashboard);
345
- lines.push("");
346
- }
347
- // ── Cross-Round Summary ──
348
- if (params.rollingSummary && params.rollingSummary.rounds_sampled > 0) {
349
- const rs = params.rollingSummary;
350
- const maxRounds = sfPolicy.max_summary_rounds;
351
- lines.push(`## Cross-Round Summary (Last ${Math.min(rs.rounds_sampled, maxRounds)} Rounds)`);
352
- lines.push("");
353
- if (rs.key_outcomes.length) {
354
- lines.push("**Key Outcomes**:");
355
- for (const ko of rs.key_outcomes.slice(0, maxRounds)) {
356
- lines.push(`- ${ko}`);
357
- }
358
- lines.push("");
359
- }
360
- if (rs.recurring_issues.length) {
361
- lines.push("**Recurring Issues**:");
362
- for (const ri of rs.recurring_issues) {
363
- lines.push(`- ⚠️ ${ri}`);
364
- }
365
- lines.push("");
366
- }
367
- if (rs.failed_patterns && rs.failed_patterns.length) {
368
- lines.push("**Failure Patterns**:");
369
- for (const fp of rs.failed_patterns) {
370
- lines.push(`- 🚫 ${fp}`);
371
- }
372
- lines.push("");
373
- }
374
- }
375
- // ── Active Constraints ──
376
- lines.push("## Active Constraints");
377
- lines.push("");
378
- if (params.constraints.length) {
379
- for (const c of params.constraints) {
380
- lines.push(`- ${c}`);
381
- }
382
- }
383
- else {
384
- lines.push("_(none)_");
385
- }
386
- lines.push("");
387
- if (params.retiredConstraints.length) {
388
- lines.push("## Retired Constraints");
389
- lines.push("");
390
- for (const c of params.retiredConstraints.slice(0, 10)) {
391
- lines.push(`- ~${c}~`);
392
- }
393
- lines.push("");
394
- }
395
- // ── Checkpoints ──
396
- if (params.checkpoints.length) {
397
- const maxCp = sfPolicy.max_checkpoints;
398
- const shown = params.checkpoints.slice(-maxCp).reverse(); // newest first
399
- lines.push(`## Checkpoints (${shown.length})`);
400
- lines.push("");
401
- for (const cp of shown) {
402
- lines.push(`### ${cp.label || `Round ${cp.declared_at_round}`}`);
403
- lines.push(`> Declared at Round ${cp.declared_at_round}`);
404
- lines.push("");
405
- if (cp.outcome) {
406
- lines.push(`**Outcome**: ${cp.outcome}`);
407
- lines.push("");
408
- }
409
- if (cp.carried_constraints.length) {
410
- lines.push("**Carried Constraints**:");
411
- for (const cc of cp.carried_constraints) {
412
- lines.push(`- ${cc}`);
413
- }
414
- lines.push("");
415
- }
416
- if (cp.resolved_constraints.length) {
417
- lines.push("**Resolved Constraints**:");
418
- for (const rc of cp.resolved_constraints) {
419
- lines.push(`- ✅ ${rc}`);
420
- }
421
- lines.push("");
422
- }
423
- }
424
- }
425
- // ── Delegation Summary ──
426
- if (params.delegationSummary) {
427
- lines.push(params.delegationSummary);
428
- }
429
- // ── External Context (current round only) ──
430
- if (params.externalContext) {
431
- lines.push("---");
432
- lines.push("");
433
- lines.push("## External Context (This Round)");
434
- lines.push("");
435
- lines.push(params.externalContext);
436
- lines.push("");
437
- }
438
- return lines.join("\n");
439
- }
440
- function getPreviousRoundTask(loopId, roundNum, vaultContext) {
441
- const prev = getPreviousRound(loopId, roundNum, vaultContext);
442
- return prev?.task ?? "";
443
- }
444
- function vaultGetLoopObjective(loopId, vaultContext) {
445
- if (vaultContext === null)
21
+ return context.results.filter((value) => value !== null && typeof value === "object" && !Array.isArray(value));
22
+ }
23
+ function lineage(entry) {
24
+ const value = entry.loop_lineage ?? entry.lineage;
25
+ return value !== null && typeof value === "object" && !Array.isArray(value)
26
+ ? value
27
+ : {};
28
+ }
29
+ function entryRound(entry) {
30
+ const value = lineage(entry).round;
31
+ return typeof value === "number" ? value : 0;
32
+ }
33
+ function loopEntries(loopId, context) {
34
+ return contextEntries(context)
35
+ .filter((entry) => entry.loop_id === loopId || lineage(entry).loop_id === loopId)
36
+ .sort((a, b) => entryRound(a) - entryRound(b));
37
+ }
38
+ export function computeGoalTextHash(text) {
39
+ const normalized = text.trim().replace(/\s+/g, " ").toLowerCase();
40
+ return createHash("sha256").update(normalized).digest("hex").slice(0, 12);
41
+ }
42
+ export function deriveGoalId(loopId, task, explicit = "") {
43
+ return explicit.trim() || `${loopId}:${task.trim().toLowerCase()
44
+ .replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-")
45
+ .replace(/^-|-$/g, "")
46
+ .slice(0, 48) || computeGoalTextHash(task)}`;
47
+ }
48
+ export function getPreviousRound(loopId, round, context) {
49
+ const entry = [...loopEntries(loopId, context)].reverse()
50
+ .find((candidate) => entryRound(candidate) === round);
51
+ if (!entry)
446
52
  return null;
447
- // Check global entries first
448
- const globalEntries = vaultContext.global_entries || [];
449
- for (const entry of globalEntries) {
450
- const lo = entry.loop_objective;
451
- if (lo && entry.loop_id === loopId) {
452
- return { loop_objective: lo, loop_id: loopId };
453
- }
454
- }
455
- // Check results
456
- const results = vaultContext.results || [];
457
- for (const r of results) {
458
- const lo = r.loop_objective;
459
- if (lo && r.loop_id === loopId) {
460
- return { loop_objective: lo, loop_id: loopId };
461
- }
462
- }
463
- return null;
464
- }
465
- function countConsecutiveHashMismatches(loopId, vaultContext) {
466
- if (vaultContext === null)
467
- return 0;
468
- const rounds = getRecentRounds(loopId, 20, vaultContext);
469
- if (rounds.length < 2)
470
- return 0;
471
- let count = 0;
472
- for (let i = 0; i < rounds.length - 1; i++) {
473
- const currHash = rounds[i].goal_text_hash;
474
- const prevHash = rounds[i + 1].goal_text_hash;
475
- if (currHash && prevHash && currHash !== prevHash) {
476
- count++;
477
- }
478
- else {
479
- break;
480
- }
481
- }
482
- return count;
483
- }
484
- // ═══════════════════════════════════════════════════════════════════════════
485
- // Strategy collapse detection
486
- // ═══════════════════════════════════════════════════════════════════════════
487
- // Constraint Retirement (v3.5)
488
- // ═══════════════════════════════════════════════════════════════════════════
489
- function computeConstraintRetirement(activeConstraints, loopId, currentRound, vaultContext) {
490
- if (!activeConstraints.length || vaultContext === null) {
491
- return { active: [...activeConstraints], retired: [] };
492
- }
493
- const results = vaultContext.results || [];
494
- if (!results.length) {
495
- return { active: [...activeConstraints], retired: [] };
496
- }
497
- const policy = getPolicy();
498
- const window = policy.constraints.retire_window;
499
- const targetRounds = new Set();
500
- for (let r = currentRound - window; r < currentRound; r++) {
501
- targetRounds.add(r);
502
- }
503
- // Collect all text per target round for activity detection
504
- const roundTexts = new Map();
505
- for (const r of results) {
506
- const lineage = (r.loop_lineage || r.lineage || {});
507
- if (lineage.loop_id !== loopId)
508
- continue;
509
- const rnd = lineage.round;
510
- if (!targetRounds.has(rnd))
511
- continue;
512
- let text = ((r.task ?? r.user_intent ?? "") + " " +
513
- (lineage.task ?? "") + " " +
514
- (r.output_summary ?? "")).toLowerCase();
515
- for (const v of r.constraint_violations || []) {
516
- text += " " + String(v).toLowerCase();
517
- }
518
- roundTexts.set(rnd, text);
519
- }
520
- const retired = [];
521
- const pruned = [];
522
- for (const constraint of activeConstraints) {
523
- const cLower = constraint.toLowerCase();
524
- const cNormalized = cLower.replace(/-/g, " ");
525
- let isActive = false;
526
- for (const text of roundTexts.values()) {
527
- if (text.includes(cLower) || text.includes(cNormalized)) {
528
- isActive = true;
529
- break;
530
- }
531
- }
532
- if (isActive || roundTexts.size < window) {
533
- pruned.push(constraint);
534
- }
535
- else {
536
- retired.push(constraint);
537
- }
538
- }
539
- return { active: pruned, retired };
53
+ const data = lineage(entry);
54
+ return {
55
+ round,
56
+ goal_id: typeof data.goal_id === "string" ? data.goal_id : "",
57
+ goal_text_hash: typeof data.goal_text_hash === "string" ? data.goal_text_hash : "",
58
+ success: typeof entry.success === "boolean"
59
+ ? entry.success
60
+ : typeof data.success === "boolean" ? data.success : false,
61
+ task: typeof data.task === "string"
62
+ ? data.task
63
+ : typeof entry.task === "string" ? entry.task : "",
64
+ constraints_active: Array.isArray(data.constraints_active)
65
+ ? data.constraints_active.filter((value) => typeof value === "string")
66
+ : [],
67
+ output_summary: typeof entry.output_summary === "string"
68
+ ? entry.output_summary
69
+ : typeof data.output_summary === "string" ? data.output_summary : "",
70
+ };
540
71
  }
541
- // ═══════════════════════════════════════════════════════════════════════════
542
- // P4: Progress evidence helpers
543
- // ═══════════════════════════════════════════════════════════════════════════
544
- /** Extract execution evidence from a previous round in the vault.
545
- * Used for progress gradient calculation and trend detection. */
546
- function getPreviousRoundEvidence(loopId, roundNum, vaultContext) {
547
- if (vaultContext === null)
548
- return null;
549
- const results = vaultContext.results || [];
550
- for (const r of results) {
551
- const lineage = (r.loop_lineage || r.lineage || {});
552
- if (lineage.loop_id !== loopId)
553
- continue;
554
- if (lineage.round !== roundNum)
72
+ function latestObjective(loopId, context) {
73
+ const candidates = loopEntries(loopId, context).reverse();
74
+ for (const entry of candidates) {
75
+ const raw = entry.loop_objective;
76
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
555
77
  continue;
556
- const evidence = (r.execution_evidence ?? lineage.execution_evidence);
557
- if (evidence) {
558
- return {
559
- progress_estimate: evidence.progress_estimate ?? 0,
560
- success_criteria_met: evidence.success_criteria_met ?? [],
561
- };
562
- }
563
- return null;
78
+ const value = raw;
79
+ return makeLoopObjective({
80
+ objective: typeof value.objective === "string" ? value.objective : "",
81
+ success_criteria: Array.isArray(value.success_criteria)
82
+ ? value.success_criteria.filter((item) => typeof item === "string")
83
+ : [],
84
+ hard_constraints: Array.isArray(value.hard_constraints)
85
+ ? value.hard_constraints.filter((item) => typeof item === "string")
86
+ : [],
87
+ created_at_round: typeof value.created_at_round === "number" ? value.created_at_round : 1,
88
+ loop_id: loopId,
89
+ version: typeof value.version === "number" ? value.version : 1,
90
+ refinement_history: Array.isArray(value.refinement_history)
91
+ ? value.refinement_history.filter((item) => typeof item === "string")
92
+ : [],
93
+ });
564
94
  }
565
95
  return null;
566
96
  }
567
- /** Count consecutive rounds (including current) where progress delta
568
- * has been below the stall threshold. */
569
- function countProgressStallRounds(loopId, currentRound, vaultContext, threshold) {
570
- if (vaultContext === null || currentRound < 2)
571
- return 0;
572
- let count = 0;
573
- for (let r = currentRound; r >= 2; r--) {
574
- const curr = getPreviousRoundEvidence(loopId, r, vaultContext);
575
- const prev = getPreviousRoundEvidence(loopId, r - 1, vaultContext);
576
- if (curr && prev) {
577
- const delta = curr.progress_estimate - prev.progress_estimate;
578
- if (delta < threshold && curr.progress_estimate < 0.95) {
579
- count++;
580
- }
581
- else {
582
- break;
583
- }
584
- }
585
- else {
586
- break;
587
- }
588
- }
589
- return count;
590
- }
591
- /** Detect repeated failure patterns in sampled rounds.
592
- * A failure pattern = 2+ consecutive rounds where:
593
- * 1. success === false (failed)
594
- * 2. same technique was used
595
- * 3. task text is similar (Jaccard > 0.4)
596
- *
597
- * Returns human-readable descriptions for injection into the prompt. */
598
- function detectFailurePatterns(sampled) {
599
- if (sampled.length < 2)
600
- return [];
601
- const patterns = [];
602
- let runStart = -1;
603
- for (let i = 0; i < sampled.length; i++) {
604
- const r = sampled[i];
605
- const isFailed = r.success === false;
606
- if (isFailed && runStart === -1) {
607
- runStart = i;
608
- }
609
- else if (!isFailed && runStart !== -1) {
610
- if (i - runStart >= 2) {
611
- const pattern = classifyRun(sampled.slice(runStart, i));
612
- if (pattern)
613
- patterns.push(pattern);
614
- }
615
- runStart = -1;
616
- }
617
- }
618
- // Handle run at end of array
619
- if (runStart !== -1 && sampled.length - runStart >= 2) {
620
- const pattern = classifyRun(sampled.slice(runStart));
621
- if (pattern)
622
- patterns.push(pattern);
623
- }
624
- return patterns;
625
- }
626
- /** Classify a run of consecutive failed rounds as a failure pattern.
627
- * Returns null if the rounds are too dissimilar to be the same pattern. */
628
- function classifyRun(run) {
629
- if (run.length < 2)
630
- return null;
631
- // Must all use the same technique
632
- const technique = run[0].technique_used || "unknown";
633
- if (!run.every((r) => (r.technique_used || "unknown") === technique)) {
634
- return null;
635
- }
636
- // Task text must be similar across the run (pairwise Jaccard)
637
- const tokensList = run.map((r) => tokenize(r.task.toLowerCase()));
638
- for (let i = 0; i < tokensList.length - 1; i++) {
639
- for (let j = i + 1; j < tokensList.length; j++) {
640
- if (jaccard(tokensList[i], tokensList[j]) < 0.4)
641
- return null;
642
- }
643
- }
644
- const rounds = `R${run[0].round}-R${run[run.length - 1].round}`;
645
- const taskPreview = run[0].task.slice(0, 80);
646
- const failureRate = `${run.length}/${run.length}`;
647
- return (`technique '${technique}' on task '${taskPreview}' ` +
648
- `failed ${run.length} consecutive rounds (${rounds}) — ` +
649
- `consider strategy change`);
650
- }
651
- /** Extract round numbers referenced in failure pattern descriptions.
652
- * Used by buildRollingSummary to identify which rounds to demote. */
653
- function extractFailureRoundNums(patterns) {
654
- const nums = new Set();
655
- for (const p of patterns) {
656
- // Match "R2-R4" or "R5"
657
- const rangeMatch = p.match(/R(\d+)-R(\d+)/);
658
- if (rangeMatch) {
659
- const start = parseInt(rangeMatch[1], 10);
660
- const end = parseInt(rangeMatch[2], 10);
661
- for (let r = start; r <= end; r++)
662
- nums.add(r);
663
- }
664
- else {
665
- const singleMatch = p.match(/R(\d+)/);
666
- if (singleMatch)
667
- nums.add(parseInt(singleMatch[1], 10));
668
- }
669
- }
670
- return nums;
671
- }
672
- // ═══════════════════════════════════════════════════════════════════════════
673
- // Rolling Summary (v3.5)
674
- // ═══════════════════════════════════════════════════════════════════════════
675
- export function buildRollingSummary(loopId, currentRound, vaultContext, sinceRound) {
676
- if (vaultContext === null)
677
- return null;
678
- const results = vaultContext.results || [];
679
- if (!results.length)
680
- return null;
681
- const policy = getPolicy();
682
- const window = policy.summary.window;
683
- const minRound = sinceRound ?? 0;
684
- // Collect rounds matching this loop_id, excluding current
685
- const rounds = [];
686
- for (const r of results) {
687
- const lineage = (r.loop_lineage || r.lineage || {});
688
- if (lineage.loop_id !== loopId)
689
- continue;
690
- const rnd = lineage.round;
691
- if (rnd >= currentRound)
692
- continue;
693
- if (rnd < minRound)
694
- continue;
695
- rounds.push({
696
- round: rnd,
697
- success: r.success ?? lineage.success ?? false,
698
- task: r.task ?? r.user_intent ?? "",
699
- output_summary: r.output_summary ?? "",
700
- constraint_violations: r.constraint_violations || [],
701
- technique_used: r.technique_used ?? "",
97
+ function evolveObjective(request, context) {
98
+ const current = request.loop_objective ?? latestObjective(request.loop_id, context) ??
99
+ makeLoopObjective({
100
+ objective: request.task,
101
+ success_criteria: ["Task goal achieved with verifiable evidence"],
102
+ hard_constraints: unique(request.constraints_from_plan),
103
+ created_at_round: 1,
104
+ loop_id: request.loop_id,
702
105
  });
703
- }
704
- if (!rounds.length)
106
+ const last = request.last_round_result;
107
+ const history = [...(current.refinement_history ?? [])];
108
+ let objective = current.objective || request.task;
109
+ let version = current.version ?? 1;
110
+ if (last?.objective_refinement?.trim()) {
111
+ const refinement = last.objective_refinement.trim();
112
+ history.push(refinement);
113
+ objective = `${objective}\nRefinement: ${refinement}`;
114
+ version++;
115
+ }
116
+ const revisions = new Map((last?.revised_success_criteria ?? []).map((item) => [item.old, item.new]));
117
+ return makeLoopObjective({
118
+ ...current,
119
+ objective,
120
+ version,
121
+ refinement_history: history.slice(-getPolicy().evolution.max_objective_versions),
122
+ success_criteria: unique(current.success_criteria.map((item) => revisions.get(item) ?? item)),
123
+ hard_constraints: unique(current.hard_constraints),
124
+ loop_id: request.loop_id,
125
+ });
126
+ }
127
+ function evolveConstraints(request, objective, previous) {
128
+ const retired = unique(request.last_round_result?.retracted_constraints ?? []);
129
+ const retiredSet = new Set(retired);
130
+ const discovered = unique(request.last_round_result?.discovered_constraints ?? [])
131
+ .slice(0, getPolicy().evolution.max_discovered_constraints_per_round);
132
+ const active = unique([
133
+ ...(previous?.constraints_active ?? []),
134
+ ...request.constraints_from_plan,
135
+ ...objective.hard_constraints,
136
+ ...objective.success_criteria,
137
+ ...discovered,
138
+ ]).filter((item) => !retiredSet.has(item))
139
+ .slice(0, getPolicy().evolution.max_active_constraints);
140
+ return { active, retired };
141
+ }
142
+ export function buildRollingSummary(loopId, currentRound, context, sinceRound = 0) {
143
+ const window = getPolicy().summary.window;
144
+ const entries = loopEntries(loopId, context)
145
+ .filter((entry) => {
146
+ const round = entryRound(entry);
147
+ return round >= sinceRound && round < currentRound;
148
+ })
149
+ .slice(-window);
150
+ if (entries.length === 0)
705
151
  return null;
706
- // Sort by round descending, take last N
707
- rounds.sort((a, b) => b.round - a.round);
708
- const sampled = rounds.slice(0, window).reverse(); // chronological order
709
- // v1.12: Key outcomes — all rounds with output_summary
710
- const keyOutcomes = [];
711
- for (const r of sampled) {
712
- if (r.output_summary) {
713
- const status = r.success ? "✓" : "✗";
714
- keyOutcomes.push(`[R${r.round}] ${status} (${r.technique_used || "n/a"}): ` +
715
- `${String(r.output_summary).slice(0, 200)}`);
716
- }
717
- }
718
- // Recurring issues
719
- const violationCounts = new Map();
720
- for (const r of sampled) {
721
- const seen = new Set();
722
- for (const v of r.constraint_violations || []) {
723
- const vNorm = String(v).trim().toLowerCase();
724
- if (vNorm && !seen.has(vNorm)) {
725
- violationCounts.set(vNorm, (violationCounts.get(vNorm) || 0) + 1);
726
- seen.add(vNorm);
727
- }
728
- }
729
- }
730
- let recurringIssues = [...violationCounts.entries()]
731
- .filter(([, count]) => count >= 2)
732
- .sort((a, b) => b[1] - a[1])
733
- .map(([v, count]) => `${v} (appeared in ${count} rounds)`);
734
- // v1.7: Failure lineage weighting — detect and demote failure patterns
735
- const sampledRounds = sampled.map((r) => ({
736
- round: r.round,
737
- success: r.success,
738
- task: r.task,
739
- output_summary: r.output_summary,
740
- constraint_violations: r.constraint_violations,
741
- technique_used: r.technique_used,
742
- }));
743
- const failurePatterns = detectFailurePatterns(sampledRounds);
744
- const failedRoundNums = extractFailureRoundNums(failurePatterns);
745
- // Re-weight key_outcomes: outcomes from failure-pattern rounds go to end
746
- if (failedRoundNums.size > 0 && keyOutcomes.length > 0) {
747
- const promoted = [];
748
- const demoted = [];
749
- for (const ko of keyOutcomes) {
750
- const rndMatch = ko.match(/^\[R(\d+)\]/);
751
- if (rndMatch && failedRoundNums.has(parseInt(rndMatch[1], 10))) {
752
- demoted.push(ko.replace(/^\[R\d+\]/, "$& [Consider alternatives]"));
753
- }
754
- else {
755
- promoted.push(ko);
756
- }
757
- }
758
- keyOutcomes.length = 0;
759
- keyOutcomes.push(...promoted, ...demoted);
760
- }
761
- // Re-weight recurring_issues: mark issues that ONLY appear in failure-pattern rounds
762
- if (failedRoundNums.size > 0 && recurringIssues.length > 0) {
763
- // Build per-round violation sets
764
- const failureViolations = new Set();
765
- const successViolations = new Set();
766
- for (const r of sampledRounds) {
767
- const target = failedRoundNums.has(r.round) ? failureViolations : successViolations;
768
- for (const v of r.constraint_violations) {
769
- target.add(String(v).trim().toLowerCase());
770
- }
771
- }
772
- recurringIssues.length = 0;
773
- recurringIssues.push(...[...violationCounts.entries()]
774
- .filter(([, count]) => count >= 2)
775
- .sort((a, b) => b[1] - a[1])
776
- .map(([v, count]) => {
777
- const vNorm = v.trim().toLowerCase();
778
- // Mark if this issue only appears in failure rounds (possible dead end)
779
- if (failureViolations.has(vNorm) && !successViolations.has(vNorm)) {
780
- return `${v} (appeared in ${count} rounds) ⚠️ [Possible dead end — only in failed rounds]`;
781
- }
782
- return `${v} (appeared in ${count} rounds)`;
783
- }));
152
+ const outcomes = [];
153
+ const issues = [];
154
+ for (const entry of entries) {
155
+ const data = lineage(entry);
156
+ const round = entryRound(entry);
157
+ const success = entry.success ?? data.success;
158
+ const summary = typeof entry.output_summary === "string"
159
+ ? entry.output_summary
160
+ : typeof data.output_summary === "string" ? data.output_summary : "";
161
+ if (summary)
162
+ outcomes.push(`[R${round}] ${success === false ? "failed" : "accepted"}: ${summary}`);
163
+ const violations = Array.isArray(entry.constraint_violations)
164
+ ? entry.constraint_violations
165
+ : Array.isArray(data.constraint_violations) ? data.constraint_violations : [];
166
+ issues.push(...violations.filter((item) => typeof item === "string"));
784
167
  }
785
168
  return makeRollingSummary({
786
- key_outcomes: keyOutcomes,
787
- recurring_issues: recurringIssues,
788
- rounds_sampled: sampled.length,
169
+ key_outcomes: unique(outcomes),
170
+ recurring_issues: unique(issues),
171
+ rounds_sampled: entries.length,
789
172
  generated_at_round: currentRound,
790
- failed_patterns: failurePatterns.length ? failurePatterns : [],
173
+ failed_patterns: [],
791
174
  });
792
175
  }
793
- export function formatRollingSummaryForPrompt(rs) {
794
- if (rs === null || rs.rounds_sampled === 0)
795
- return "";
796
- const lines = [
797
- "### Cross-Round Summary (Accumulated)",
798
- "",
799
- `**Sampled**: ${rs.rounds_sampled} prior rounds`,
800
- "",
801
- ];
802
- if (rs.key_outcomes.length) {
803
- lines.push("**Key Outcomes**:");
804
- for (const ko of rs.key_outcomes)
805
- lines.push(`- ${ko}`);
806
- lines.push("");
807
- }
808
- if (rs.recurring_issues.length) {
809
- lines.push("**Recurring Issues (appeared 2+ times)**:");
810
- for (const ri of rs.recurring_issues)
811
- lines.push(`- ⚠️ ${ri}`);
812
- lines.push("");
813
- }
814
- // v1.7: Failure patterns — explicit warnings about repeated failure paths
815
- if (rs.failed_patterns && rs.failed_patterns.length) {
816
- lines.push("### ⚠️ Failure Patterns — Consider Strategy Change");
817
- lines.push("");
818
- lines.push("The following approaches failed repeatedly. The compiler has demoted " +
819
- "their influence on key outcomes. Consider a different technique or " +
820
- "narrower scope.");
821
- for (const fp of rs.failed_patterns) {
822
- lines.push(`- 🚫 ${fp}`);
823
- }
824
- lines.push("");
825
- }
826
- return lines.join("\n");
827
- }
828
- // ═══════════════════════════════════════════════════════════════════════════
829
- // Checkpoint compression (v1.10 — subtask boundary snapshots)
830
- // ═══════════════════════════════════════════════════════════════════════════
831
- /** Build a CheckpointSummary from the current round's self-evaluation state.
832
- * Called when the Agent sets compression_checkpoint: true in its self-eval.
833
- * Snapshots the current constraints and outcome so they survive rolling-window
834
- * eviction in subsequent rounds. */
835
- export function buildCheckpointSummary(selfEval, round, activeConstraints, retiredConstraints) {
836
- const policy = getPolicy();
837
- const cpCfg = policy.checkpoint;
838
- const maxCarried = cpCfg?.max_carried_constraints ?? 10;
839
- const outcomeMaxChars = cpCfg?.outcome_max_chars ?? 200;
840
- const label = selfEval.checkpoint_label?.trim() ||
841
- `Subtask completed at round ${round}`;
842
- const outcome = (selfEval.output_summary || "").trim().slice(0, outcomeMaxChars);
843
- // Carried: constraints still active, capped
844
- const carried = activeConstraints.slice(0, maxCarried);
845
- // Resolved: constraints retired this round (retracted + the retire delta)
846
- const resolved = retiredConstraints.slice(0, maxCarried);
847
- return {
848
- label,
849
- declared_at_round: round,
850
- outcome,
851
- carried_constraints: carried,
852
- resolved_constraints: resolved,
853
- };
176
+ function tokens(text) {
177
+ return new Set(text.toLowerCase().match(/[a-z0-9\u4e00-\u9fff]+/g) ?? []);
854
178
  }
855
- /** Format a CheckpointSummary as a prompt block.
856
- * Renders as a fixed section that persists across rolling-window eviction.
857
- * Returns empty string if the summary is null or has no meaningful data. */
858
- export function formatCheckpointForPrompt(cs) {
859
- if (!cs)
860
- return "";
861
- // Must have at least a label and some content to be meaningful
862
- if (!cs.label && !cs.outcome && !cs.carried_constraints.length)
863
- return "";
864
- const lines = [
865
- `### Checkpoint: ${cs.label} (Round ${cs.declared_at_round})`,
866
- "",
867
- ];
868
- if (cs.outcome) {
869
- lines.push(`**Outcome**: ${cs.outcome}`);
870
- lines.push("");
871
- }
872
- if (cs.carried_constraints.length) {
873
- lines.push("**Carried Constraints** (still active):");
874
- for (const c of cs.carried_constraints) {
875
- lines.push(`- ${c}`);
876
- }
877
- lines.push("");
878
- }
879
- if (cs.resolved_constraints.length) {
880
- lines.push("**Resolved Constraints** (closed with this checkpoint):");
881
- for (const c of cs.resolved_constraints) {
882
- lines.push(`- ${c}`);
883
- }
884
- lines.push("");
885
- }
886
- return lines.join("\n");
179
+ function similarity(left, right) {
180
+ const a = tokens(left);
181
+ const b = tokens(right);
182
+ if (a.size === 0 || b.size === 0)
183
+ return 1;
184
+ let intersection = 0;
185
+ for (const value of a)
186
+ if (b.has(value))
187
+ intersection++;
188
+ return intersection / new Set([...a, ...b]).size;
887
189
  }
888
- /** Query the vault for the most recent checkpoint entry before the current
889
- * round. Returns null if no checkpoint has been declared yet. */
890
- function loadLatestCheckpoint(loopId, currentRound, vaultContext) {
891
- if (!vaultContext)
892
- return null;
893
- const results = vaultContext.results || [];
894
- let latest = null;
895
- let latestRound = -1;
896
- for (const r of results) {
897
- const lineage = (r.loop_lineage || r.lineage || {});
898
- if (lineage.loop_id !== loopId)
899
- continue;
900
- const rnd = lineage.round;
901
- if (rnd >= currentRound)
902
- continue;
903
- // Check for stored checkpoint summary on this entry
904
- const cp = r.checkpoint_summary;
905
- if (cp && typeof cp.declared_at_round === "number" && cp.declared_at_round > latestRound) {
906
- latest = cp;
907
- latestRound = cp.declared_at_round;
908
- }
909
- }
910
- if (!latest)
911
- return null;
912
- return makeCheckpointSummary({
913
- label: latest.label ?? "",
914
- declared_at_round: latest.declared_at_round ?? 0,
915
- outcome: latest.outcome ?? "",
916
- carried_constraints: latest.carried_constraints ?? [],
917
- resolved_constraints: latest.resolved_constraints ?? [],
190
+ export function alignTask(proposedTask, request, context) {
191
+ const objective = request.loop_objective ?? latestObjective(request.loop_id, context);
192
+ if (!objective)
193
+ return makeTaskAlignment();
194
+ const score = similarity(proposedTask, [objective.objective, ...objective.success_criteria, ...objective.hard_constraints].join(" "));
195
+ return makeTaskAlignment({
196
+ is_aligned: score >= 0.3,
197
+ alignment_score: Number(score.toFixed(2)),
198
+ warning: score < 0.3 ? "Current task may be drifting from the loop objective." : "",
199
+ escalation: score < 0.3 ? "warn" : "none",
918
200
  });
919
201
  }
920
- // ═══════════════════════════════════════════════════════════════════════════
921
- // Shared state evolution (P0/P1/P5) — used by both compileL1 and compileL2
922
- // ═══════════════════════════════════════════════════════════════════════════
923
- /** Apply P1 objective refinement from last round's self-evaluation.
924
- * Appends the refinement to the objective with a round marker.
925
- * Returns the updated loop objective (or unchanged if no refinement). */
926
- function applyObjectiveRefinement(loopObjective, lastRoundResult, currentRound) {
927
- if (!loopObjective || !lastRoundResult?.objective_refinement)
928
- return loopObjective;
929
- const refinement = lastRoundResult.objective_refinement.trim();
930
- if (!refinement)
931
- return loopObjective;
932
- const version = (loopObjective.version ?? 1) + 1;
933
- const history = [...(loopObjective.refinement_history ?? []), refinement];
934
- const policy = getPolicy();
935
- const maxVersions = policy.evolution?.max_objective_versions ?? 10;
936
- const prunedHistory = history.slice(-maxVersions);
937
- const refinedObjective = `${loopObjective.objective} [R${currentRound}: ${refinement}]`;
938
- return makeLoopObjective({
939
- ...loopObjective,
940
- objective: refinedObjective,
941
- version,
942
- refinement_history: prunedHistory,
202
+ export function checkLoopHealth(loopId, request, context) {
203
+ const previous = getPreviousRound(loopId, request.round - 1, context);
204
+ const alignment = alignTask(request.task, request, context);
205
+ const violations = request.last_round_result?.constraint_violations.length ?? 0;
206
+ const integrity = Math.max(0, 1 - violations * 0.2);
207
+ const continuity = previous ? similarity(previous.task, request.task) : 1;
208
+ const drift = alignment.alignment_score < 0.3 || continuity < 0.2;
209
+ return makeLoopHealth({
210
+ goal_alignment: alignment.alignment_score,
211
+ constraint_integrity: integrity,
212
+ drift_detected: drift,
213
+ strategy_stability: true,
214
+ task_continuity: Number(continuity.toFixed(2)),
215
+ escalation_recommended: drift || integrity < 0.6 ? "l2" : "none",
943
216
  });
944
217
  }
945
- /** Apply P0 discovered constraints and P5 retractions/revisions.
946
- * Pure function — returns updated constraints, loop objective, and retired list. */
947
- function applyConstraintEvolution(constraints, loopObjective, lastRoundResult, currentRound) {
948
- const retired = [];
949
- let lo = loopObjective;
950
- let cs = [...constraints];
951
- // P0: Merge discovered constraints
952
- if (lastRoundResult?.discovered_constraints?.length) {
953
- const policy = getPolicy();
954
- const maxPerRound = policy.evolution?.max_discovered_constraints_per_round ?? 5;
955
- const maxActive = policy.evolution?.max_active_constraints ?? 15;
956
- const newConstraints = lastRoundResult.discovered_constraints
957
- .slice(0, maxPerRound)
958
- .filter((c) => !cs.includes(c));
959
- cs = [...cs, ...newConstraints].slice(0, maxActive);
960
- }
961
- // P5a: Retracted constraints
962
- if (lastRoundResult?.retracted_constraints?.length) {
963
- for (const rc of lastRoundResult.retracted_constraints) {
964
- const idx = cs.indexOf(rc);
965
- if (idx >= 0) {
966
- cs.splice(idx, 1);
967
- retired.push(`[agent-retracted] ${rc}`);
968
- }
969
- }
970
- }
971
- // P5b: Revised success criteria
972
- if (lastRoundResult?.revised_success_criteria?.length && lo) {
973
- const sc = [...lo.success_criteria];
974
- for (const rev of lastRoundResult.revised_success_criteria) {
975
- const idx = sc.indexOf(rev.old);
976
- if (idx >= 0)
977
- sc[idx] = rev.new;
978
- else
979
- sc.push(rev.new);
980
- }
981
- const version = (lo.version ?? 1) + 1;
982
- const history = [
983
- ...(lo.refinement_history ?? []),
984
- `R${currentRound}: revised ${lastRoundResult.revised_success_criteria.length} success criteria`,
985
- ];
986
- const updatedLo = makeLoopObjective({
987
- ...lo,
988
- success_criteria: sc,
989
- version,
990
- refinement_history: history,
991
- });
992
- // Rebuild constraints to include revised criteria, deduplicate
993
- cs = [...new Set([
994
- ...cs.filter((c) => !updatedLo.hard_constraints.includes(c) && !updatedLo.success_criteria.includes(c)),
995
- ...updatedLo.hard_constraints,
996
- ...updatedLo.success_criteria,
997
- ])];
998
- lo = updatedLo;
999
- }
1000
- return { constraints: cs, loopObjective: lo, retired };
218
+ function levelDecision(request, context) {
219
+ const previous = getPreviousRound(request.loop_id, request.round - 1, context);
220
+ const last = request.last_round_result;
221
+ const hasNewInformation = Boolean(request.new_since_last_round.trim() ||
222
+ last?.discovered_constraints?.length ||
223
+ last?.objective_refinement?.trim() ||
224
+ last?.emerged_subtasks?.length ||
225
+ last?.retracted_constraints?.length ||
226
+ last?.revised_success_criteria?.length ||
227
+ last?.wrong_assumptions?.length);
228
+ const lastFullRound = loopEntries(request.loop_id, context)
229
+ .filter((entry) => lineage(entry).recompile_level === "l2")
230
+ .map(entryRound)
231
+ .filter((round) => round < request.round)
232
+ .at(-1) ?? 1;
233
+ return decidePromptLevel({
234
+ round: request.round,
235
+ attempt: request.attempt,
236
+ forceLevel: request.force_level,
237
+ hasPlanSource: Boolean(request.plan_source),
238
+ checkpointBoundary: last?.compression_checkpoint === true,
239
+ goalChanged: previous !== null && previous.goal_id !== deriveGoalId(request.loop_id, request.task, request.goal_id),
240
+ previousStateMissing: request.round > 1 && previous === null,
241
+ previousFailedWithoutNewInformation: last?.success === false && !hasNewInformation,
242
+ verificationContradicted: (request.verification_flags ?? [])
243
+ .some((flag) => flag.severity === "error"),
244
+ consecutiveRejections: request.consecutive_rejections,
245
+ fullRefreshInterval: getPolicy().prompt.full_refresh_interval,
246
+ lastFullRound,
247
+ });
1001
248
  }
1002
- /** Collect checkpoint summaries from vault entries for a given loop.
1003
- * Shared between compileL1 and compileL2 — same vault scanning logic. */
1004
- function collectVaultCheckpoints(vaultContext, loopId, excludeRounds) {
1005
- const checkpoints = [];
1006
- if (!vaultContext)
1007
- return checkpoints;
1008
- const results = vaultContext.results || [];
1009
- for (const r of results) {
1010
- const lineage = (r.loop_lineage || r.lineage || {});
1011
- if (lineage.loop_id !== loopId)
1012
- continue;
1013
- const cp = r.checkpoint_summary;
1014
- if (cp && typeof cp.declared_at_round === "number") {
1015
- if (excludeRounds?.has(cp.declared_at_round))
1016
- continue;
1017
- if (checkpoints.some((c) => c.declared_at_round === cp.declared_at_round))
1018
- continue;
1019
- checkpoints.push(makeCheckpointSummary({
1020
- label: cp.label ?? "",
1021
- declared_at_round: cp.declared_at_round,
1022
- outcome: cp.outcome ?? "",
1023
- carried_constraints: cp.carried_constraints ?? [],
1024
- resolved_constraints: cp.resolved_constraints ?? [],
1025
- }));
1026
- }
1027
- }
1028
- return checkpoints;
249
+ export function decideLevel(request, context) {
250
+ return levelDecision(request, context).level;
1029
251
  }
1030
- // ═══════════════════════════════════════════════════════════════════════════
1031
- // External Context formatting (v1.7 — memory system integration)
1032
- // ═══════════════════════════════════════════════════════════════════════════
1033
- /** Format an external context string for injection into an L2 prompt.
1034
- * Wraps the raw context in a marked section with a priority disclaimer.
1035
- * Returns empty string if context is empty or injection is disabled by policy. */
1036
- export function formatExternalContext(externalContext, sectionTitle, maxLength) {
1037
- if (!externalContext || externalContext.trim().length === 0)
1038
- return "";
1039
- const trimmed = externalContext.trim().slice(0, maxLength);
1040
- const truncationNote = trimmed.length < externalContext.trim().length
1041
- ? `\n> ⚠️ Context was truncated to ${maxLength} characters.`
1042
- : "";
252
+ export function buildSelfEvalBlock(round) {
1043
253
  return [
254
+ "### LoopForge Evaluation (Required)",
1044
255
  "",
1045
- sectionTitle,
1046
- "",
1047
- "> ⚠️ **Role**: Situational awareness only. These are point-in-time",
1048
- "> observations from past sessions — claims about code locations or",
1049
- "> behaviour may be outdated. If any insight contradicts the Loop",
1050
- "> Objective or Active Constraints above, the LoopForge specification",
1051
- "> takes absolute precedence.",
1052
- "",
1053
- trimmed,
1054
- truncationNote,
1055
- "",
1056
- "---",
256
+ `After completing Round ${round}, submit JSON between these markers:`,
257
+ "---loopforge-eval",
258
+ JSON.stringify({
259
+ success: false,
260
+ output_summary: `<verified result of round ${round}>`,
261
+ constraint_violations: [],
262
+ should_continue: true,
263
+ discovered_constraints: [],
264
+ emerged_subtasks: [],
265
+ execution_evidence: {
266
+ files_changed: [],
267
+ test_results: { passed: 0, failed: 0, skipped: 0 },
268
+ success_criteria_met: [],
269
+ success_criteria_remaining: [],
270
+ progress_estimate: 0,
271
+ },
272
+ wrong_assumptions: [],
273
+ next_action: "<next concrete action, or empty when complete>",
274
+ }, null, 2),
275
+ "---end-loopforge-eval",
1057
276
  "",
277
+ "Set success=true only when the full goal and hard constraints are verified.",
1058
278
  ].join("\n");
1059
279
  }
1060
- // ═══════════════════════════════════════════════════════════════════════════
1061
- // LAYER 1 — Hard Gates (can change compile level)
1062
- // ═══════════════════════════════════════════════════════════════════════════
1063
- export function decideLevel(request, vaultContext) {
1064
- // Gate 1: Explicit override (never overrides round 1 or plan_source)
1065
- if (request.force_level !== "auto" &&
1066
- ["l0", "l1", "l2"].includes(request.force_level)) {
1067
- if (request.round !== 1 && !request.plan_source) {
1068
- return request.force_level;
1069
- }
1070
- }
1071
- // ── L2 triggers: structural restarts ──────────────────────────────────
1072
- // Gate 2: First call or explicit plan → full rebuild
1073
- if (request.round === 1 || request.plan_source)
1074
- return "l2";
1075
- // Gate 3: Checkpoint boundary — subtask completed, re-plan
1076
- if (request.last_round_result?.compression_checkpoint === true)
1077
- return "l2";
1078
- // Gate 4: goal_id stability
1079
- const goalId = deriveGoalId(request.loop_id, request.task, request.goal_id);
1080
- const prev = getPreviousRound(request.loop_id, request.round - 1, vaultContext);
1081
- if (prev === null)
1082
- return "l2";
1083
- if (goalId !== prev.goal_id)
1084
- return "l2";
1085
- // ── L0 trigger: honest failure with no new information ────────────────
1086
- const lr = request.last_round_result;
1087
- const prevFailed = lr !== null && !lr.success;
1088
- const hasNoNewInfo = (lr?.discovered_constraints?.length ?? 0) === 0 &&
1089
- (lr?.objective_refinement?.trim().length ?? 0) === 0 &&
1090
- (lr?.emerged_subtasks?.length ?? 0) === 0 &&
1091
- (lr?.retracted_constraints?.length ?? 0) === 0 &&
1092
- (lr?.revised_success_criteria?.length ?? 0) === 0 &&
1093
- (lr?.wrong_assumptions?.length ?? 0) === 0 &&
1094
- request.constraints_from_plan.length === 0 &&
1095
- !detectsRepairSignal(request);
1096
- if (prevFailed && hasNoNewInfo)
1097
- return "l0";
1098
- // ── L1: default path — all P0-P5 state changes handled here ───────────
1099
- return "l1";
1100
- }
1101
- // ═══════════════════════════════════════════════════════════════════════════
1102
- // LAYER 2 — Soft Advisories (NEVER change compile level)
1103
- // ═══════════════════════════════════════════════════════════════════════════
1104
- export function alignTask(proposedTask, request, vaultContext) {
1105
- const objectiveEntry = vaultGetLoopObjective(request.loop_id, vaultContext);
1106
- let objData = null;
1107
- if (request.loop_objective) {
1108
- objData = request.loop_objective;
1109
- }
1110
- else if (objectiveEntry) {
1111
- const lo = objectiveEntry.loop_objective;
1112
- if (lo && typeof lo === "object") {
1113
- objData = lo;
1114
- }
1115
- }
1116
- if (objData === null)
1117
- return makeTaskAlignment();
1118
- const objective = objData.objective ?? "";
1119
- const successCriteria = objData.success_criteria ?? [];
1120
- const hardConstraints = objData.hard_constraints ?? [];
1121
- const proposedTokens = tokenize(proposedTask.toLowerCase());
1122
- const objText = `${objective} ${successCriteria.join(" ")} ${hardConstraints.join(" ")}`.toLowerCase();
1123
- const objTokens = tokenize(objText);
1124
- const score = proposedTokens.size && objTokens.size
1125
- ? jaccard(proposedTokens, objTokens)
1126
- : 1.0;
1127
- if (score >= 0.5) {
1128
- return makeTaskAlignment({
1129
- is_aligned: true,
1130
- alignment_score: Math.round(score * 100) / 100,
1131
- });
1132
- }
1133
- else if (score >= 0.3) {
1134
- return makeTaskAlignment({
1135
- is_aligned: true,
1136
- alignment_score: Math.round(score * 100) / 100,
1137
- warning: `Proposed task '${proposedTask.slice(0, 80)}' may be drifting from ` +
1138
- `loop objective '${objective}'. Consider narrowing scope.`,
1139
- escalation: "warn",
1140
- });
1141
- }
1142
- else {
1143
- return makeTaskAlignment({
1144
- is_aligned: false,
1145
- alignment_score: Math.round(score * 100) / 100,
1146
- warning: `Proposed task '${proposedTask.slice(0, 80)}' is OFF-OBJECTIVE. ` +
1147
- `Loop objective: '${objective}'. Full realignment recommended.`,
1148
- escalation: "block",
1149
- });
1150
- }
1151
- }
1152
- export function checkLoopHealth(loopId, request, vaultContext) {
1153
- const objectiveEntry = vaultGetLoopObjective(loopId, vaultContext);
1154
- let obj = null;
1155
- if (request.loop_objective) {
1156
- obj = request.loop_objective;
1157
- }
1158
- else if (objectiveEntry) {
1159
- const lo = objectiveEntry.loop_objective;
1160
- if (lo && typeof lo === "object") {
1161
- obj = lo;
1162
- }
1163
- }
1164
- if (obj === null)
1165
- return makeLoopHealth();
1166
- const objective = obj.objective ?? "";
1167
- const successCriteria = obj.success_criteria ?? [];
1168
- const hardConstraints = obj.hard_constraints ?? [];
1169
- // 1. goal_alignment
1170
- let goalAlignment = 1.0;
1171
- if (request.task) {
1172
- const taskTokens = tokenize(request.task.toLowerCase());
1173
- const objText = `${objective} ${successCriteria.join(" ")} ${hardConstraints.join(" ")}`.toLowerCase();
1174
- const objTokens = tokenize(objText);
1175
- goalAlignment =
1176
- taskTokens.size && objTokens.size
1177
- ? jaccard(taskTokens, objTokens)
1178
- : 1.0;
1179
- }
1180
- // 2. constraint_integrity
1181
- let constraintIntegrity = 1.0;
1182
- if (request.last_round_result && hardConstraints.length) {
1183
- const outputText = request.last_round_result.output_summary.toLowerCase();
1184
- const present = hardConstraints.filter((c) => c
1185
- .toLowerCase()
1186
- .split(/\s+/)
1187
- .some((word) => outputText.includes(word))).length;
1188
- constraintIntegrity = present / hardConstraints.length;
1189
- }
1190
- // 3. drift_detected
1191
- const driftDetected = countConsecutiveHashMismatches(loopId, vaultContext) >= 3;
1192
- // 4. strategy_stability
1193
- const recent = getRecentRounds(loopId, 3, vaultContext);
1194
- const strategyStability = recent.length > 0
1195
- ? recent.every((r) => r.success === true)
1196
- : true;
1197
- // 5. task_continuity
1198
- let taskContinuity = 1.0;
1199
- const prevTask = getPreviousRoundTask(loopId, request.round - 1, vaultContext);
1200
- if (prevTask && request.task) {
1201
- const currTokens = tokenize(request.task.toLowerCase());
1202
- const prevTokens = tokenize(prevTask.toLowerCase());
1203
- taskContinuity =
1204
- currTokens.size && prevTokens.size
1205
- ? jaccard(currTokens, prevTokens)
1206
- : 1.0;
1207
- }
1208
- // Escalation recommendation
1209
- let escalation = "none";
1210
- if (goalAlignment < 0.5) {
1211
- escalation = "l2";
1212
- }
1213
- else if (constraintIntegrity < 0.7) {
1214
- escalation = "l1";
1215
- }
1216
- else if (driftDetected) {
1217
- escalation = "l2";
1218
- }
1219
- return makeLoopHealth({
1220
- goal_alignment: Math.round(goalAlignment * 100) / 100,
1221
- constraint_integrity: Math.round(constraintIntegrity * 100) / 100,
1222
- drift_detected: driftDetected,
1223
- strategy_stability: strategyStability,
1224
- task_continuity: Math.round(taskContinuity * 100) / 100,
1225
- escalation_recommended: escalation,
1226
- });
1227
- }
1228
- function computeSuggestedNextTask(loopId, vaultContext) {
1229
- if (vaultContext === null)
1230
- return "";
1231
- const results = vaultContext.results || [];
1232
- let bestTask = "";
1233
- let bestRound = -1;
1234
- for (const r of results) {
1235
- const lineage = (r.loop_lineage || r.lineage || {});
1236
- if (lineage.loop_id !== loopId)
1237
- continue;
1238
- const rnd = lineage.round ?? 0;
1239
- if (rnd > bestRound) {
1240
- const task = r.task ?? r.user_intent ?? "";
1241
- if (task) {
1242
- bestTask = task;
1243
- bestRound = rnd;
1244
- }
1245
- }
1246
- }
1247
- return bestTask ? `[R${bestRound}] Previous task: ${bestTask.slice(0, 120)}` : "";
1248
- }
1249
- export function computeAdvisories(request, vaultContext) {
1250
- const warnings = [];
1251
- let alignment = null;
1252
- let health = null;
1253
- let suggested = "";
1254
- // goal_text_hash drift detection
1255
- const currentHash = computeGoalTextHash(request.task);
1256
- const prev = getPreviousRound(request.loop_id, request.round - 1, vaultContext);
1257
- if (prev && currentHash !== prev.goal_text_hash && prev.goal_text_hash) {
1258
- warnings.push(`goal_text_hash changed (${prev.goal_text_hash} → ${currentHash}) ` +
1259
- "but goal_id matched — wording drift detected");
1260
- }
1261
- // Repair cue detection
1262
- if (detectsRepairSignal(request)) {
1263
- warnings.push("repair signal detected — L1 patch applied");
1264
- }
1265
- // Task alignment
1266
- if (request.next_task_proposal) {
1267
- alignment = alignTask(request.next_task_proposal, request, vaultContext);
1268
- if (alignment.escalation !== "none") {
1269
- warnings.push(`task_alignment: ${alignment.warning}`);
1270
- }
1271
- }
1272
- // Loop health
1273
- const interval = Math.max(request.health_check_interval, 1);
1274
- if (request.round % interval === 0) {
1275
- health = checkLoopHealth(request.loop_id, request, vaultContext);
1276
- if (health.escalation_recommended !== "none") {
1277
- warnings.push(`loop_health recommends ${health.escalation_recommended}: ` +
1278
- `goal_alignment=${health.goal_alignment.toFixed(2)}, ` +
1279
- `constraint_integrity=${health.constraint_integrity.toFixed(2)}, ` +
1280
- `task_continuity=${health.task_continuity.toFixed(2)}`);
1281
- }
1282
- if (health.drift_detected) {
1283
- warnings.push("drift_detected: goal_text_hash diverged 3+ consecutive rounds");
1284
- }
1285
- }
1286
- // Forward hint
1287
- suggested = computeSuggestedNextTask(request.loop_id, vaultContext);
1288
- return { warnings, suggestedNextTask: suggested, alignment, health };
1289
- }
1290
- // ═══════════════════════════════════════════════════════════════════════════
1291
- // Plan extraction
1292
- // ═══════════════════════════════════════════════════════════════════════════
1293
- function extractObjectiveFromPlan(planPath) {
1294
- let text;
1295
- try {
1296
- text = readFileSync(planPath, "utf-8");
1297
- }
1298
- catch {
1299
- return null;
1300
- }
1301
- const sections = {
1302
- goal: [],
1303
- success: [],
1304
- constraints: [],
1305
- };
1306
- let currentSection = null;
1307
- const goalPatterns = ["goal", "objective", "目标", "目的", "意图"];
1308
- const successPatterns = [
1309
- "success criteria", "acceptance criteria", "验收标准", "成功标准",
1310
- "done when", "完成标准", "交付标准", "outcome", "deliverable",
1311
- "结果", "产出", "交付物",
1312
- ];
1313
- const constraintPatterns = [
1314
- "hard constraint", "constraint", "non-goal", "out of scope",
1315
- "硬约束", "约束", "非目标", "不做什么", "限制",
1316
- ];
1317
- for (const line of text.split("\n")) {
1318
- const stripped = line.trim();
1319
- const low = stripped
1320
- .toLowerCase()
1321
- .replace(/^#+\s*/, "");
1322
- if (stripped.startsWith("#")) {
1323
- if (goalPatterns.some((p) => low.includes(p))) {
1324
- currentSection = "goal";
1325
- continue;
1326
- }
1327
- if (successPatterns.some((p) => low.includes(p))) {
1328
- currentSection = "success";
1329
- continue;
1330
- }
1331
- if (constraintPatterns.some((p) => low.includes(p))) {
1332
- currentSection = "constraints";
1333
- continue;
1334
- }
1335
- currentSection = null;
1336
- continue;
1337
- }
1338
- if (!currentSection)
1339
- continue;
1340
- if (/^[-*•]/.test(stripped)) {
1341
- const item = stripped.replace(/^[-*•]\s*/, "").trim();
1342
- if (item && item.length > 3) {
1343
- sections[currentSection].push(item);
1344
- }
1345
- }
1346
- else if (stripped && currentSection === "goal") {
1347
- if (!sections.goal.length && stripped.length > 10) {
1348
- sections.goal.push(stripped);
1349
- }
1350
- }
1351
- else if (stripped && currentSection === "success" && stripped.length > 10) {
1352
- // P3: Also capture free-text paragraphs under success criteria headers
1353
- if (!/^#/.test(stripped)) {
1354
- sections.success.push(stripped);
1355
- }
1356
- }
1357
- }
1358
- if (!Object.values(sections).some((v) => v.length))
280
+ function checkpoint(request, active, retired) {
281
+ const last = request.last_round_result;
282
+ if (!last?.compression_checkpoint)
1359
283
  return null;
1360
- return {
1361
- objective: sections.goal[0] ?? "",
1362
- success_criteria: sections.success,
1363
- hard_constraints: sections.constraints,
1364
- };
1365
- }
1366
- function computeLoopObjectiveFromTask(request, _vaultContext) {
1367
- const task = request.task || "";
1368
- const constraints = [...request.constraints_from_plan];
1369
- let objective = task.trim().slice(0, 200);
1370
- const successCriteria = [];
1371
- const hardConstraints = [...constraints];
1372
- // P3: Outcome-oriented default success criteria — describe the RESULT,
1373
- // not the process. The process emerges from the loop.
1374
- if (/test|测试/i.test(task)) {
1375
- successCriteria.push("All tests pass with no regressions");
1376
- }
1377
- if (/compat|兼容/i.test(task)) {
1378
- successCriteria.push("Existing API contracts and integrations unchanged");
1379
- }
1380
- if (/security|安全|audit/i.test(task)) {
1381
- successCriteria.push("All identified vulnerabilities resolved or documented");
1382
- }
1383
- if (/fix|修复|bug/i.test(task)) {
1384
- successCriteria.push("Root cause addressed and verified, not just symptoms");
1385
- }
1386
- if (/build|create|implement|开发|实现|构建/i.test(task)) {
1387
- successCriteria.push("Deliverable matches specification and passes review");
1388
- }
1389
- if (/refactor|重构/i.test(task)) {
1390
- successCriteria.push("Behavior preserved, structure improved, no new failures");
1391
- }
1392
- if (/migrate|迁移|upgrade|升级/i.test(task)) {
1393
- successCriteria.push("Migration completes without data loss or downtime");
1394
- }
1395
- if (/perf|performance|optimize|性能|优化/i.test(task)) {
1396
- successCriteria.push("Measurable improvement verified by benchmarks");
1397
- }
1398
- // P3: Try to extract richer objective from plan_source
1399
- if (request.plan_source) {
1400
- const planExtracted = extractObjectiveFromPlan(request.plan_source);
1401
- if (planExtracted) {
1402
- if (planExtracted.objective) {
1403
- objective = planExtracted.objective;
1404
- }
1405
- if (planExtracted.success_criteria) {
1406
- successCriteria.push(...planExtracted.success_criteria);
1407
- }
1408
- if (planExtracted.hard_constraints) {
1409
- hardConstraints.push(...planExtracted.hard_constraints);
1410
- }
1411
- }
1412
- else {
1413
- hardConstraints.push(`Follow plan: ${request.plan_source}`);
1414
- }
1415
- }
1416
- // P3: Derive domain from task content for context-aware defaults
1417
- if (!successCriteria.length) {
1418
- if (/\.sol|solidity|contract|智能合约/i.test(task)) {
1419
- successCriteria.push("All functions respect check-effects-interactions pattern");
1420
- successCriteria.push("No unchecked external calls or reentrancy vectors");
1421
- }
1422
- else if (/\.tsx?|\.jsx?|react|component|组件/i.test(task)) {
1423
- successCriteria.push("Component renders correctly across all states (loading, empty, error)");
1424
- successCriteria.push("No prop-type or accessibility regressions");
1425
- }
1426
- else if (/api|endpoint|接口|route/i.test(task)) {
1427
- successCriteria.push("Endpoint responds correctly for all defined status codes");
1428
- successCriteria.push("Error responses follow the project's error schema");
1429
- }
1430
- else {
1431
- successCriteria.push("Task goal achieved with verifiable evidence");
1432
- }
1433
- }
1434
- if (!hardConstraints.length) {
1435
- hardConstraints.push("Do not modify files outside the stated scope");
1436
- hardConstraints.push("Preserve existing test coverage — no test deletions without replacement");
1437
- }
1438
- return makeLoopObjective({
1439
- objective,
1440
- success_criteria: successCriteria,
1441
- hard_constraints: hardConstraints,
1442
- created_at_round: 1,
1443
- loop_id: request.loop_id,
1444
- version: 1,
1445
- refinement_history: [],
284
+ const policy = getPolicy().checkpoint;
285
+ return makeCheckpointSummary({
286
+ label: last.checkpoint_label?.trim() || `Round ${last.round}`,
287
+ declared_at_round: last.round,
288
+ outcome: last.output_summary.slice(0, policy.outcome_max_chars),
289
+ carried_constraints: active.slice(0, policy.max_carried_constraints),
290
+ resolved_constraints: retired,
1446
291
  });
1447
292
  }
1448
- // ═══════════════════════════════════════════════════════════════════════════
1449
- // P4: Shared consistency checks and progress dashboard (used by L1 & L2)
1450
- // ═══════════════════════════════════════════════════════════════════════════
1451
- /** Run P4 consistency validation checks on agent execution evidence.
1452
- * Returns warning strings. Called by both compileL1 and compileL2.
1453
- * Three checks:
1454
- * 1. Agent claims file changes but files_changed is empty
1455
- * 2. Agent reports success but test results show failures
1456
- * 3. Progress estimate mismatch (objective vs subjective) */
1457
- function runP4ConsistencyChecks(evidence, outputSummary, success, loopObjective) {
1458
- const warnings = [];
1459
- // Check 1: Agent claims action but reports no files changed
1460
- if (evidence.files_changed.length === 0 && outputSummary) {
1461
- const actionWords = /\b(fix|modif|chang|updat|refactor|implement|add|remov|delet|rewrit|修补|修改|更改|更新|重构|实现|添加|删除|重写)\b/i;
1462
- if (actionWords.test(outputSummary)) {
1463
- warnings.push("P4: Agent output claims changes but execution_evidence.files_changed is empty — possible oversight");
1464
- }
1465
- }
1466
- // Check 2: Agent reports success but tests failed
1467
- if (evidence.test_results && evidence.test_results.failed > 0 && success) {
1468
- warnings.push(`P4: Agent reports success=true but ${evidence.test_results.failed} tests failed — claims may not match reality`);
1469
- }
1470
- // Check 3: Progress estimate mismatch with criteria tracking
1471
- if (loopObjective && loopObjective.success_criteria.length > 0) {
1472
- const metCount = evidence.success_criteria_met.length;
1473
- const totalCount = loopObjective.success_criteria.length;
1474
- const objectiveProgress = totalCount > 0 ? metCount / totalCount : 0;
1475
- const subjectiveProgress = evidence.progress_estimate;
1476
- const policy = getPolicy();
1477
- const mismatchThreshold = policy.evolution?.progress_mismatch_threshold ?? 0.3;
1478
- if (Math.abs(objectiveProgress - subjectiveProgress) > mismatchThreshold) {
1479
- warnings.push(`P4: Progress estimate mismatch — agent estimates ${(subjectiveProgress * 100).toFixed(0)}% but ${metCount}/${totalCount} criteria met (${(objectiveProgress * 100).toFixed(0)}%)`);
1480
- }
1481
- }
1482
- return warnings;
1483
- }
1484
- /** Build a progress dashboard string for prompt or state file injection.
1485
- * Used by both compileL1 and compileL2. When showTrend is true, also
1486
- * computes a trend arrow from vault history (L2 path). */
1487
- function buildProgressDashboard(evidence, loopObjective, vaultContext, loopId, currentRound, showTrend) {
1488
- const totalCriteria = loopObjective?.success_criteria.length ?? 0;
1489
- const metCriteria = evidence.success_criteria_met.length;
1490
- const remainingCriteria = evidence.success_criteria_remaining;
1491
- const objProgress = totalCriteria > 0 ? metCriteria / totalCriteria : evidence.progress_estimate;
1492
- const subjProgress = evidence.progress_estimate;
1493
- const lines = [
1494
- "",
1495
- "### Progress Dashboard",
1496
- "",
1497
- `**Overall**: ${metCriteria}/${totalCriteria} criteria met (${(objProgress * 100).toFixed(0)}%)`,
1498
- ];
1499
- if (Math.abs(objProgress - subjProgress) > 0.05) {
1500
- lines.push(`**Agent estimate**: ${(subjProgress * 100).toFixed(0)}%`);
1501
- }
1502
- if (evidence.files_changed.length) {
1503
- lines.push(`**Files changed**: ${evidence.files_changed.join(", ")}`);
1504
- }
1505
- if (evidence.test_results) {
1506
- const tr = evidence.test_results;
1507
- lines.push(`**Tests**: ${tr.passed} passed, ${tr.failed} failed, ${tr.skipped} skipped`);
1508
- }
1509
- if (remainingCriteria.length) {
1510
- lines.push("**Remaining criteria**:");
1511
- for (const rc of remainingCriteria.slice(0, 5)) {
1512
- lines.push(`- ${rc}`);
1513
- }
1514
- if (remainingCriteria.length > 5) {
1515
- lines.push(`- ... and ${remainingCriteria.length - 5} more`);
1516
- }
1517
- }
1518
- lines.push("");
1519
- // Trend arrow — only computed in L2 path where full vault history is available
1520
- if (showTrend && vaultContext && loopObjective) {
1521
- const prevEvidence = getPreviousRoundEvidence(loopId, currentRound - 1, vaultContext);
1522
- if (prevEvidence) {
1523
- const trend = objProgress - (prevEvidence.success_criteria_met.length / Math.max(totalCriteria, 1));
1524
- if (trend > 0.03) {
1525
- lines.push(`**Trend**: ↑ advancing (+${(trend * 100).toFixed(0)}% this round)`);
1526
- }
1527
- else if (trend > -0.03) {
1528
- lines.push("**Trend**: → stable");
1529
- }
1530
- else {
1531
- lines.push(`**Trend**: ↓ regressing (${(trend * 100).toFixed(0)}% this round)`);
1532
- }
1533
- lines.push("");
1534
- }
1535
- }
1536
- return lines.join("\n");
1537
- }
1538
- // ═══════════════════════════════════════════════════════════════════════════
1539
- // Compilation — L0 / L1 / L2
1540
- // ═══════════════════════════════════════════════════════════════════════════
1541
- function compileL0(request, vaultContext, prevRound) {
1542
- const prev = prevRound ??
1543
- getPreviousRound(request.loop_id, request.round - 1, vaultContext);
1544
- const cachedPrompt = prev?.prompt_text ?? "";
1545
- if (cachedPrompt) {
1546
- return makeLoopCompileResponse({
1547
- status: AgentStatus.OK,
1548
- prompt: cachedPrompt,
1549
- recompile_level: "l0",
1550
- diff_from_previous: `L0 cache hit — reusing prompt from round ${request.round - 1}`,
1551
- lineage: [`${request.loop_id}:r${request.round}`],
1552
- constraints_active: prev?.constraints_active ?? [],
1553
- constraints_retired: [],
1554
- technique_used: "cached",
1555
- loop_id: request.loop_id,
1556
- round: request.round,
1557
- goal_id: deriveGoalId(request.loop_id, request.task, request.goal_id),
1558
- goal_text_hash: computeGoalTextHash(request.task),
1559
- plan_source: request.plan_source,
1560
- warnings: [],
1561
- });
1562
- }
1563
- // Auto-escalate: no cached prompt → delegate to L2
1564
- const l2Response = compileL2(request, vaultContext);
1565
- l2Response.recompile_level = "l0";
1566
- l2Response.diff_from_previous =
1567
- `L0 auto-escalated to L2 — no cached prompt available from round ${request.round - 1}`;
1568
- return l2Response;
1569
- }
1570
- function compileL1(request, vaultContext, prevRound) {
1571
- const goalId = deriveGoalId(request.loop_id, request.task, request.goal_id);
1572
- const lr = request.last_round_result;
1573
- // ── v1.14: Technique routing (Tier 1 only — 4 techniques) ────────────
1574
- // L1 is the default path: technique evolves within Tier 1 unless
1575
- // decideLevel already escalated to L2 (strategy collapse / checkpoint).
1576
- const analysis = routeTechniqueAdaptive(request.task, vaultContext, request.loop_id,
1577
- /* isCheckpoint */ false);
1578
- const technique = analysis.technique;
1579
- const referenceFile = analysis.reference_file;
1580
- // Determine if technique changed from last round
1581
- const techChanged = techniqueChanged(request.loop_id, technique, request.round, vaultContext);
1582
- // ── Loop Objective: load from vault, apply P1 refinement ─────────────
1583
- let loopObjective = null;
1584
- const vaultLo = vaultGetLoopObjective(request.loop_id, vaultContext);
1585
- if (vaultLo) {
1586
- const loData = vaultLo.loop_objective;
1587
- if (loData) {
1588
- loopObjective = makeLoopObjective({
1589
- objective: loData.objective ?? "",
1590
- success_criteria: loData.success_criteria ?? [],
1591
- hard_constraints: loData.hard_constraints ?? [],
1592
- created_at_round: loData.created_at_round ?? 1,
1593
- loop_id: loData.loop_id ?? request.loop_id,
1594
- version: loData.version ?? 1,
1595
- refinement_history: loData.refinement_history ?? [],
1596
- });
1597
- }
1598
- }
1599
- if (!loopObjective && request.loop_objective) {
1600
- loopObjective = request.loop_objective;
1601
- }
1602
- // P1: Apply objective refinement (shared)
1603
- loopObjective = applyObjectiveRefinement(loopObjective, lr, request.round);
1604
- // ── Build active constraints (base + LO) ─────────────────────────────
1605
- let constraints = [...request.constraints_from_plan];
1606
- if (loopObjective) {
1607
- constraints = [
1608
- ...new Set([
1609
- ...constraints,
1610
- ...loopObjective.hard_constraints,
1611
- ...loopObjective.success_criteria,
1612
- ]),
1613
- ];
1614
- }
1615
- // ── P0/P5: Constraint evolution (shared) ─────────────────────────────
1616
- const { constraints: evolvedCs, loopObjective: evolvedLo, retired } = applyConstraintEvolution(constraints, loopObjective, lr, request.round);
1617
- constraints = evolvedCs;
1618
- loopObjective = evolvedLo;
1619
- // ── Constraint retirement ────────────────────────────────────────────
1620
- const { active, retired: autoRetired } = computeConstraintRetirement(constraints, request.loop_id, request.round, vaultContext);
1621
- for (const r of autoRetired) {
1622
- if (!retired.includes(r))
1623
- retired.push(r);
1624
- }
1625
- // ── Rolling summary + P5c wrong assumptions ──────────────────────────
1626
- const rollingSummary = buildRollingSummary(request.loop_id, request.round, vaultContext);
1627
- if (lr?.wrong_assumptions?.length) {
1628
- if (rollingSummary) {
1629
- rollingSummary.key_outcomes = [
1630
- ...rollingSummary.key_outcomes,
1631
- ...lr.wrong_assumptions.map((a) => `Wrong assumption corrected: ${a}`),
1632
- ];
1633
- }
1634
- }
1635
- const rollingText = formatRollingSummaryForPrompt(rollingSummary);
1636
- // ── P2: Emerged subtasks → suggested next task ───────────────────────
1637
- let suggestedNextTask = "";
1638
- if (lr?.emerged_subtasks?.length) {
1639
- suggestedNextTask = lr.emerged_subtasks.slice(0, 3).join("; ");
1640
- }
1641
- // ── P4: Consistency validation ───────────────────────────────────────
1642
- const evidence = lr?.execution_evidence;
1643
- const warnings = evidence
1644
- ? runP4ConsistencyChecks(evidence, lr.output_summary, lr.success, loopObjective)
1645
- : [];
1646
- // ── Route to specialist (all 4 techniques available via Tier 1) ──────
1647
- let prompt;
1648
- if (technique === "step-back") {
1649
- prompt = compileStepBack(request, goalId, active, loopObjective, rollingText, analysis, { embedSkeleton: techChanged, level: "l1" });
1650
- }
1651
- else if (technique === "least-to-most") {
1652
- prompt = compileLeastToMost(request, goalId, active, loopObjective, rollingText, analysis, { embedSkeleton: techChanged, level: "l1" });
1653
- }
1654
- else if (technique === "tree-of-thought") {
1655
- prompt = compileToT(request, goalId, active, loopObjective, rollingText, analysis, { embedSkeleton: techChanged, level: "l1" });
1656
- }
1657
- else {
1658
- prompt = compileGeneric(request, goalId, active, loopObjective, rollingText, analysis, referenceFile, technique, { embedSkeleton: techChanged, level: "l1" });
1659
- }
1660
- // ── P4: Progress Dashboard ───────────────────────────────────────────
1661
- const progressDashboard = (evidence && loopObjective)
1662
- ? buildProgressDashboard(evidence, loopObjective, vaultContext, request.loop_id, request.round, /* showTrend */ false)
1663
- : "";
1664
- // ── v1.14: Render state file ─────────────────────────────────────────
1665
- const sfPolicy = getPolicy().state_file;
1666
- let stateFileContent;
1667
- if (sfPolicy.enabled) {
1668
- const allCheckpoints = collectVaultCheckpoints(vaultContext, request.loop_id);
1669
- const delegationSummary = vaultContext
1670
- ? buildDelegationSummary(vaultContext)
1671
- : "";
1672
- stateFileContent = renderStateFile({
1673
- loopId: request.loop_id,
1674
- currentRound: request.round,
1675
- maxRounds: request.max_rounds ?? sfPolicy.max_summary_rounds * 2,
1676
- loopObjective,
1677
- rollingSummary,
1678
- constraints: active,
1679
- retiredConstraints: retired,
1680
- checkpoints: allCheckpoints,
1681
- delegationSummary,
1682
- progressDashboard,
1683
- externalContext: request.external_context,
1684
- });
1685
- prompt = renderStateFilePointer(request.loop_id) + "\n" + prompt;
1686
- }
1687
- // ── Build thin diff ──────────────────────────────────────────────────
1688
- const violations = lr?.constraint_violations ?? [];
1689
- const diffParts = [];
1690
- if (request.constraints_from_plan.length) {
1691
- diffParts.push(`new constraints: [${request.constraints_from_plan.join(", ")}]`);
1692
- }
1693
- if (retired.length) {
1694
- diffParts.push(`retired constraints: [${retired.join(", ")}]`);
1695
- }
1696
- if (violations.length) {
1697
- diffParts.push(`violations: [${violations.join(", ")}]`);
1698
- }
1699
- if (request.new_since_last_round) {
1700
- diffParts.push(`delta: ${request.new_since_last_round.slice(0, 200)}`);
1701
- }
1702
- // v1.14: When state file is enabled, progressDashboard lives in the state
1703
- // file (see renderStateFile above). Only inline it in the prompt when the
1704
- // state file is disabled — this keeps the prompt truly thin.
1705
- const finalPrompt = sfPolicy.enabled ? prompt : prompt + progressDashboard;
1706
- return makeLoopCompileResponse({
293
+ export function compileLoop(request, context) {
294
+ const decision = levelDecision(request, context);
295
+ const previous = getPreviousRound(request.loop_id, request.round - 1, context);
296
+ const objective = evolveObjective(request, context);
297
+ const constraints = evolveConstraints(request, objective, previous);
298
+ const rolling = buildRollingSummary(request.loop_id, request.round, context);
299
+ const alignment = alignTask(request.task, request, context);
300
+ const health = checkLoopHealth(request.loop_id, { ...request, loop_objective: objective }, context);
301
+ const warnings = unique([
302
+ alignment.warning,
303
+ health.escalation_recommended !== "none"
304
+ ? `Loop health recommends ${health.escalation_recommended}.`
305
+ : "",
306
+ ]);
307
+ const response = makeLoopCompileResponse({
1707
308
  status: AgentStatus.OK,
1708
- prompt: finalPrompt,
1709
- recompile_level: "l1",
1710
- diff_from_previous: diffParts.length ? diffParts.join("; ") : "L1 continue — state updated in file.",
309
+ recompile_level: decision.level,
310
+ diff_from_previous: decision.reasons.join(","),
1711
311
  lineage: [`${request.loop_id}:r${request.round}`],
1712
- constraints_active: active,
1713
- constraints_retired: retired,
1714
- technique_used: technique,
1715
- reference_file: referenceFile,
1716
- rolling_summary: rollingSummary,
312
+ constraints_active: constraints.active,
313
+ constraints_retired: constraints.retired,
1717
314
  loop_id: request.loop_id,
1718
315
  round: request.round,
1719
- goal_id: goalId,
316
+ goal_id: deriveGoalId(request.loop_id, request.task, request.goal_id),
1720
317
  goal_text_hash: computeGoalTextHash(request.task),
1721
- loop_objective: loopObjective,
318
+ loop_objective: objective,
319
+ loop_health: health,
320
+ task_alignment: alignment,
321
+ rolling_summary: rolling,
322
+ checkpoint_summary: checkpoint(request, constraints.active, constraints.retired),
323
+ suggested_next_task: request.last_round_result?.next_action?.trim() ||
324
+ request.last_round_result?.emerged_subtasks?.[0] || "",
1722
325
  plan_source: request.plan_source,
1723
- suggested_next_task: suggestedNextTask,
1724
326
  warnings,
1725
- state_file_content: stateFileContent,
1726
327
  });
1727
- }
1728
- export function compileL2(request, vaultContext) {
1729
- const goalId = deriveGoalId(request.loop_id, request.task, request.goal_id);
1730
- // v1.15: L2 no longer auto-selects a technique. The Agent reads the technique
1731
- // catalog (skills/prompt-techniques/SKILL.md), freely chooses the best fit
1732
- // based on loop state, reads the corresponding reference file, and applies
1733
- // it directly to the task.
1734
- // v1.10: Checkpoint — load existing or build new from last round's self-eval
1735
- let checkpointSummary = null;
1736
- let checkpointRound = 0;
1737
- // Check if the last round declared a checkpoint
1738
- const lr = request.last_round_result;
1739
- if (lr?.compression_checkpoint === true) {
1740
- // Build fresh checkpoint from the last round's data
1741
- // Constraints haven't been updated yet at this point, but we snapshot the
1742
- // current state as seen by this compile call (pre-P5 correction).
1743
- // We defer building until constraints are finalized below.
1744
- checkpointRound = request.round - 1;
1745
- }
1746
- else if (request.round > 1) {
1747
- // No new checkpoint — try loading the previous one from vault
1748
- checkpointSummary = loadLatestCheckpoint(request.loop_id, request.round, vaultContext);
1749
- if (checkpointSummary) {
1750
- checkpointRound = checkpointSummary.declared_at_round;
1751
- }
1752
- }
1753
- // v3.5: Rolling summary — optionally scoped to post-checkpoint rounds
1754
- const rollingSummary = buildRollingSummary(request.loop_id, request.round, vaultContext, checkpointRound > 0 ? checkpointRound : undefined);
1755
- // P5c: Wrong assumptions — must be applied BEFORE formatting so they appear
1756
- // in the rendered cross-round summary text injected into the prompt.
1757
- if (lr?.wrong_assumptions?.length) {
1758
- if (rollingSummary) {
1759
- rollingSummary.key_outcomes = [
1760
- ...rollingSummary.key_outcomes,
1761
- ...lr.wrong_assumptions.map((a) => `Wrong assumption corrected: ${a}`),
1762
- ];
1763
- }
1764
- }
1765
- const rollingText = formatRollingSummaryForPrompt(rollingSummary);
1766
- // ── Loop Objective: create at round 1, load from vault at later rounds ──
1767
- let loopObjective = null;
1768
- if (request.round === 1) {
1769
- if (request.loop_objective) {
1770
- loopObjective = request.loop_objective;
1771
- }
1772
- else {
1773
- loopObjective = computeLoopObjectiveFromTask(request, vaultContext);
1774
- }
1775
- }
1776
- else {
1777
- // P1 fix: Load loop objective from vault for round > 1
1778
- const vaultLo = vaultGetLoopObjective(request.loop_id, vaultContext);
1779
- if (vaultLo) {
1780
- const loData = vaultLo.loop_objective;
1781
- if (loData) {
1782
- loopObjective = makeLoopObjective({
1783
- objective: loData.objective ?? "",
1784
- success_criteria: loData.success_criteria ?? [],
1785
- hard_constraints: loData.hard_constraints ?? [],
1786
- created_at_round: loData.created_at_round ?? 1,
1787
- loop_id: loData.loop_id ?? request.loop_id,
1788
- version: loData.version ?? 1,
1789
- refinement_history: loData.refinement_history ?? [],
1790
- });
1791
- }
1792
- }
1793
- if (!loopObjective && request.loop_objective) {
1794
- loopObjective = request.loop_objective;
1795
- }
1796
- }
1797
- // ── P1: Apply objective refinement (shared) ──
1798
- loopObjective = applyObjectiveRefinement(loopObjective, lr, request.round);
1799
- // ── Build active constraints (base + LO) ──
1800
- let constraints = [...request.constraints_from_plan];
1801
- if (loopObjective) {
1802
- constraints = [
1803
- ...new Set([
1804
- ...constraints,
1805
- ...loopObjective.hard_constraints,
1806
- ...loopObjective.success_criteria,
1807
- ]),
1808
- ];
1809
- }
1810
- // ═══════════════════════════════════════════════════════════════════════════
1811
- // P0/P5: Constraint evolution (shared) — retractions/revisions BEFORE prompt
1812
- // ═══════════════════════════════════════════════════════════════════════════
1813
- const warnings = [];
1814
- const evoResult = applyConstraintEvolution(constraints, loopObjective, lr, request.round);
1815
- constraints = evoResult.constraints;
1816
- loopObjective = evoResult.loopObjective;
1817
- const retired = evoResult.retired;
1818
- // ── v1.10: Build checkpoint summary if last round declared one ──
1819
- if (checkpointRound > 0 && !checkpointSummary && lr) {
1820
- // checkpointRound was set but no summary yet → fresh declaration
1821
- // We need the self-eval from last round. It's embedded in lr fields.
1822
- // Build a synthetic SelfEvaluation from the last_round_result for the builder.
1823
- const synthEval = {
1824
- success: lr.success,
1825
- output_summary: lr.output_summary,
1826
- constraint_violations: lr.constraint_violations,
1827
- should_continue: true,
1828
- compression_checkpoint: true,
1829
- checkpoint_label: lr.checkpoint_label ?? "",
1830
- };
1831
- checkpointSummary = buildCheckpointSummary(synthEval, checkpointRound, constraints, retired);
1832
- }
1833
- // ── v1.15: Build L2 prompt with Technique Selection block ──
1834
- // The Agent freely chooses a technique by reading the catalog and reference
1835
- // files. LoopForge provides the loop state (objective, constraints, rolling
1836
- // summary) — the Agent provides the reasoning strategy.
1837
- let prompt = buildL2Prompt(request, goalId, constraints, loopObjective, rollingText);
1838
- // ── v1.10: Inject checkpoint block (before rolling summary in prompt) ──
1839
- // The checkpoint is rendered before the external context so it appears
1840
- // in a natural position: after Loop Objective, before Cross-Round Summary.
1841
- if (checkpointSummary) {
1842
- prompt += "\n" + formatCheckpointForPrompt(checkpointSummary);
1843
- }
1844
- // ── v1.7: Inject external memory context (L2 only) ──
1845
328
  const policy = getPolicy();
1846
- if (policy.memory_injection.enabled && request.external_context) {
1847
- prompt += formatExternalContext(request.external_context, policy.memory_injection.section_title, policy.memory_injection.max_context_length);
1848
- }
1849
- // ── P2: Emerged subtasks → suggested next task ──
1850
- let suggestedNextTask = "";
1851
- if (request.last_round_result?.emerged_subtasks?.length) {
1852
- suggestedNextTask = request.last_round_result.emerged_subtasks
1853
- .slice(0, 3)
1854
- .join("; ");
1855
- }
1856
- // ═══════════════════════════════════════════════════════════════════════════
1857
- // P4: Consistency validation — cross-check agent claims
1858
- // ═══════════════════════════════════════════════════════════════════════════
1859
- const evidence = lr?.execution_evidence;
1860
- if (evidence) {
1861
- warnings.push(...runP4ConsistencyChecks(evidence, lr.output_summary, lr.success, loopObjective));
1862
- }
1863
- // ═══════════════════════════════════════════════════════════════════════════
1864
- // P4: Progress gradient — early stall detection
1865
- // ═══════════════════════════════════════════════════════════════════════════
1866
- if (evidence && vaultContext) {
1867
- const prevEvidence = getPreviousRoundEvidence(request.loop_id, request.round - 1, vaultContext);
1868
- if (prevEvidence) {
1869
- const delta = evidence.progress_estimate - prevEvidence.progress_estimate;
1870
- const policy = getPolicy();
1871
- const stallThreshold = policy.evolution?.progress_stall_threshold ?? 0.05;
1872
- const stallRounds = policy.evolution?.progress_stall_rounds ?? 2;
1873
- if (delta < stallThreshold && evidence.progress_estimate < 0.95) {
1874
- // Check how many consecutive rounds have been below threshold
1875
- const stallCount = countProgressStallRounds(request.loop_id, request.round, vaultContext, stallThreshold);
1876
- if (stallCount >= stallRounds) {
1877
- warnings.push(`P4: Progress has stalled — <${(stallThreshold * 100).toFixed(0)}% improvement for ${stallCount} consecutive rounds. Current: ${(evidence.progress_estimate * 100).toFixed(0)}%`);
1878
- }
1879
- }
1880
- }
1881
- }
1882
- // ═══════════════════════════════════════════════════════════════════════════
1883
- // P4: Build Progress Dashboard for prompt injection
1884
- // ═══════════════════════════════════════════════════════════════════════════
1885
- const progressDashboard = (evidence && loopObjective)
1886
- ? buildProgressDashboard(evidence, loopObjective, vaultContext, request.loop_id, request.round, /* showTrend */ true)
1887
- : "";
1888
- // ── v1.14: Render state file ──────────────────────────────────────────
1889
- // Collect all accumulated state and render it as a markdown snapshot.
1890
- // The caller writes this to .loopforge/state/{loopId}-state.md.
1891
- const sfPolicy = getPolicy().state_file;
1892
- let stateFileContent;
1893
- if (sfPolicy.enabled) {
1894
- const excludeRounds = checkpointSummary
1895
- ? new Set([checkpointSummary.declared_at_round])
1896
- : undefined;
1897
- const allCheckpoints = collectVaultCheckpoints(vaultContext, request.loop_id, excludeRounds);
1898
- if (checkpointSummary)
1899
- allCheckpoints.unshift(checkpointSummary);
1900
- const delegationSummary = vaultContext
1901
- ? buildDelegationSummary(vaultContext)
1902
- : "";
1903
- stateFileContent = renderStateFile({
1904
- loopId: request.loop_id,
1905
- currentRound: request.round,
1906
- maxRounds: request.max_rounds ?? sfPolicy.max_summary_rounds * 2,
1907
- loopObjective,
1908
- rollingSummary,
1909
- constraints,
1910
- retiredConstraints: retired,
1911
- checkpoints: allCheckpoints,
1912
- delegationSummary,
1913
- progressDashboard,
1914
- externalContext: request.external_context,
1915
- });
1916
- // Prepend state file pointer to the prompt
1917
- prompt = renderStateFilePointer(request.loop_id) + "\n" + prompt;
1918
- }
1919
- // v1.14: When state file is enabled, progressDashboard lives in the state
1920
- // file (see renderStateFile above). Only inline it in the prompt when the
1921
- // state file is disabled — this keeps the prompt truly thin.
1922
- const finalPrompt = sfPolicy.enabled ? prompt : prompt + progressDashboard;
1923
- return makeLoopCompileResponse({
1924
- status: AgentStatus.OK,
1925
- prompt: finalPrompt,
1926
- recompile_level: "l2",
1927
- diff_from_previous: request.round === 1 || request.plan_source
1928
- ? "Full recompile — new goal or first call."
1929
- : "Full recompile — goal_id changed or checkpoint boundary.",
1930
- lineage: [`${request.loop_id}:r${request.round}`],
1931
- constraints_active: constraints,
1932
- constraints_retired: retired,
1933
- technique_used: "agent-selected",
1934
- reference_file: `${SKILLS_DIR}/prompt-techniques/SKILL.md`,
1935
- rolling_summary: rollingSummary,
1936
- checkpoint_summary: checkpointSummary,
1937
- loop_id: request.loop_id,
1938
- round: request.round,
1939
- goal_id: goalId,
1940
- goal_text_hash: computeGoalTextHash(request.task),
1941
- loop_objective: loopObjective,
1942
- plan_source: request.plan_source,
1943
- suggested_next_task: suggestedNextTask,
1944
- warnings,
1945
- state_file_content: stateFileContent,
329
+ const statePath = `${policy.state_file.directory}/${request.loop_id}-state.md`;
330
+ const state = createCanonicalLoopState(request, response, statePath);
331
+ const markdown = renderCanonicalStateMarkdown(state);
332
+ const artifact = assemblePromptArtifact({
333
+ state,
334
+ level: decision.level,
335
+ reasons: decision.reasons,
336
+ mode: policy.prompt.injection_mode,
337
+ budgets: {
338
+ l0: policy.prompt.l0_max_chars,
339
+ l1: policy.prompt.l1_max_chars,
340
+ l2: policy.prompt.l2_max_chars,
341
+ },
342
+ attempt: request.attempt,
343
+ selfEvaluationBlock: buildSelfEvalBlock(request.round),
344
+ fullStateMarkdown: markdown,
1946
345
  });
1947
- }
1948
- // ═══════════════════════════════════════════════════════════════════════════
1949
- // L2 Prompt Builder (v1.15 — Agent technique autonomy)
1950
- // ═══════════════════════════════════════════════════════════════════════════
1951
- /** Build the L2 restart prompt with a Technique Selection block.
1952
- * The Agent reads the technique catalog (SKILL.md), freely chooses the best
1953
- * technique based on loop state, reads the corresponding reference file, and
1954
- * applies it directly to the task. LoopForge provides context — the Agent
1955
- * provides the reasoning strategy. */
1956
- function buildL2Prompt(request, goalId, constraints, loopObjective, rollingText) {
1957
- const ctx = {
1958
- request, goalId, constraints, loopObjective, rollingText, level: "l2",
1959
- };
1960
- const lines = [
1961
- `## LoopForge L2 Compile — Round ${request.round}`,
1962
- "",
1963
- ];
1964
- // Cross-round summary
1965
- if (rollingText) {
1966
- lines.push(rollingText);
1967
- lines.push("");
1968
- }
1969
- // Technique Selection — Agent freely chooses
1970
- lines.push("### Technique Selection", "", "This is a strategy restart point. Read the technique catalog to choose", "the best reasoning approach for this round:", "", ` \`${SKILLS_DIR}/prompt-techniques/SKILL.md\``, "", "Based on the loop state (state file + context below), freely select", "the technique that best fits this round's challenge. Then read the", "full reference for your chosen technique:", "", ` \`${SKILLS_DIR}/prompt-techniques/references/<chosen>.md\``, "", "Apply the technique's method directly to the task — do NOT generate", "an intermediate prompt. Execute the task using the chosen technique", "as your thinking framework.", "");
1971
- // Loop Objective
1972
- lines.push(...buildLoopObjectiveBlock(loopObjective));
1973
- // Active Constraints
1974
- lines.push(...buildConstraintsBlock(constraints));
1975
- // Plan Source
1976
- if (request.plan_source) {
1977
- lines.push(`**Plan Source**: ${request.plan_source}`);
1978
- lines.push("");
1979
- }
1980
- // Task
1981
- lines.push(...buildTaskBlock(ctx));
1982
- // Loop Identity
1983
- lines.push(...buildIdentityBlock(ctx));
1984
- return lines.join("\n");
1985
- }
1986
- function buildHeader(ctx, technique) {
1987
- const levelLabel = ctx.level.toUpperCase();
1988
- const lines = [
1989
- `## LoopForge ${levelLabel} Compile — Round ${ctx.request.round}`,
1990
- `**Technique**: ${technique} (embedded)`,
1991
- "",
1992
- ];
1993
- if (ctx.rollingText) {
1994
- lines.push(ctx.rollingText);
1995
- lines.push("");
1996
- }
1997
- return lines;
1998
- }
1999
- function buildLoopObjectiveBlock(lo) {
2000
- if (!lo)
2001
- return [];
2002
- const lines = [
2003
- "### Loop Objective (Anchor)",
2004
- `**Objective**: ${lo.objective}`,
2005
- ];
2006
- if (lo.success_criteria.length) {
2007
- lines.push("**Success Criteria**:");
2008
- for (const sc of lo.success_criteria)
2009
- lines.push(`- ${sc}`);
2010
- }
2011
- if (lo.hard_constraints.length) {
2012
- lines.push("**Hard Constraints**:");
2013
- for (const hc of lo.hard_constraints)
2014
- lines.push(`- ${hc}`);
2015
- }
2016
- lines.push("");
2017
- return lines;
2018
- }
2019
- function buildConstraintsBlock(constraints) {
2020
- if (!constraints.length)
2021
- return [];
2022
- const lines = ["### Active Constraints"];
2023
- for (const c of constraints)
2024
- lines.push(`- ${c}`);
2025
- lines.push("");
2026
- return lines;
2027
- }
2028
- function buildTaskBlock(ctx) {
2029
- const lines = [
2030
- "### Task",
2031
- ctx.request.task,
2032
- "",
2033
- ];
2034
- if (ctx.request.domain) {
2035
- lines.push(`**Domain**: ${ctx.request.domain}`);
2036
- lines.push("");
2037
- }
2038
- return lines;
2039
- }
2040
- function buildIdentityBlock(ctx) {
2041
- return [
2042
- "### Loop Identity",
2043
- `- Loop ID: \`${ctx.request.loop_id}\``,
2044
- `- Goal ID: \`${ctx.goalId}\``,
2045
- `- Round: ${ctx.request.round}`,
2046
- "",
2047
- ];
2048
- }
2049
- /** Generic compiler — reads technique reference file (fallback for zero-shot/few-shot/cot). */
2050
- function compileGeneric(request, goalId, constraints, loopObjective, rollingText, analysis, referenceFile, technique, opts = {}) {
2051
- const embedSkeleton = opts.embedSkeleton !== false; // default true
2052
- const ctx = { request, goalId, constraints, loopObjective, rollingText, level: opts.level ?? "l2" };
2053
- const lines = [
2054
- ...buildHeader(ctx, technique),
2055
- ];
2056
- if (embedSkeleton) {
2057
- lines.push(`Read the technique reference BEFORE generating the prompt:`, ` Technique: ${technique}`, ` Reference: ${referenceFile}`, ` Rationale: ${analysis.rationale}`, "");
2058
- }
2059
- else {
2060
- lines.push(`**Technique**: ${technique} (see state file for full reference)`, `**Rationale**: ${analysis.rationale}`, "");
2061
- }
2062
- lines.push(...buildLoopObjectiveBlock(loopObjective), ...buildConstraintsBlock(constraints), ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []), ...buildTaskBlock(ctx), ...buildIdentityBlock(ctx));
2063
- if (embedSkeleton) {
2064
- lines.push("### Generation Instructions", `1. Read \`${referenceFile}\` — study its structure rules, section count, and format requirements`, "2. Generate a complete prompt following that technique's structure", "3. The prompt must be self-contained — ready for a coding agent to execute", "4. Output only the generated prompt — no preamble, no meta-commentary");
2065
- }
2066
- return lines.join("\n");
2067
- }
2068
- // ── Step-Back specialist ───────────────────────────────────────────────────────
2069
- function compileStepBack(request, goalId, constraints, loopObjective, rollingText, analysis, opts = {}) {
2070
- const embedSkeleton = opts.embedSkeleton !== false; // default true
2071
- const ctx = { request, goalId, constraints, loopObjective, rollingText, level: opts.level ?? "l2" };
2072
- const lines = [
2073
- ...buildHeader(ctx, "step-back"),
2074
- `**Rationale**: ${analysis.rationale}`,
2075
- "",
2076
- ];
2077
- if (embedSkeleton) {
2078
- lines.push("Generate a complete prompt using the **Step-Back** technique. Follow the 8-section skeleton below.", "Embed all structural constraints — do NOT reference an external file.", "");
2079
- }
2080
- else {
2081
- lines.push("**Technique**: step-back (see state file for the full skeleton).", "Generate a complete prompt following this technique.", "");
2082
- }
2083
- lines.push(...buildLoopObjectiveBlock(loopObjective), ...buildConstraintsBlock(constraints), ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []), ...buildTaskBlock(ctx), ...buildIdentityBlock(ctx));
2084
- if (embedSkeleton) {
2085
- lines.push("", "### 8-Section Skeleton (REQUIRED)", "", "| # | Section | Required | Notes |", "|---|---------|----------|-------|", "| 1 | 角色 | ✓ | Domain expert role appropriate to the task |", "| 2 | 任务 | ✓ | One-sentence task summary |", "| 3 | 输入 | ✓ | Target module/problem to analyse |", "| 4 | 输出格式 | ✓ | Numbered list of expected outputs |", "| 5 | **Step-Back 抽象框架** | ✓ | See format rules below |", "| 6 | 具体实现要求 | ✓ | MUST start with: **\"基于上述抽象框架,实现以下所有功能\"** |", "| 7 | 硬约束 | ✓ | Numbered list of hard constraints |", "| 8 | 生成要求 | ✓ | Acceptance criteria |", "", "### Section 5 — Step-Back 抽象框架 (CRITICAL)", "", "Must contain **2-3 abstract frameworks**. Each framework is an independent ASCII diagram.", "Frameworks are PARALLEL (peer-level), not sequential.", "", "Each framework format:", "```", "### 框架N:[Framework Name]", "", "[ASCII diagram with principles/formulas/classification tables/rule tables]", "", "Diagram content requirements:", "- Core principle/formula (e.g. \"Total = Σ(dimension × weight)\")", "- Component table (e.g. weight allocation table, type mapping table)", "- Rule description (e.g. \"Missing data dimensions use neutral value\")", "```", "", "**Tightening rules**:", "- The step-back question must be MORE ABSTRACT than the original problem, but cover all required information", "- Abstract ≠ vague. Use precise formulas, principles, standards, definitions, or causal mechanisms", "- Tighten to the MINIMUM generalisation layer that still covers the original problem", "- Reasoning must RETURN to the original problem — do not stay at the principle level", "", "**Section 6 transition sentence (MANDATORY)**:", "\"基于上述抽象框架,实现以下所有功能。\"", "", "### Generation Instructions", "1. Extract 2-3 abstract principles/frameworks from the task domain", "2. Build ASCII diagrams for each framework (formulas, tables, rules)", "3. Apply the abstraction back to the concrete task", "4. Verify: the generated prompt satisfies all structural rules in the skeleton above", "5. Output only the generated prompt — no preamble, no meta-commentary");
2086
- }
2087
- return lines.join("\n");
2088
- }
2089
- // ── Least-to-Most specialist ───────────────────────────────────────────────────
2090
- function compileLeastToMost(request, goalId, constraints, loopObjective, rollingText, analysis, opts = {}) {
2091
- const embedSkeleton = opts.embedSkeleton !== false; // default true
2092
- const ctx = { request, goalId, constraints, loopObjective, rollingText, level: opts.level ?? "l2" };
2093
- const lines = [
2094
- ...buildHeader(ctx, "least-to-most"),
2095
- `**Rationale**: ${analysis.rationale}`,
2096
- "",
2097
- ];
2098
- if (embedSkeleton) {
2099
- lines.push("Generate a complete prompt using the **Least-to-Most** technique. Follow the 8-section skeleton below.", "Embed all structural constraints — do NOT reference an external file.", "");
2100
- }
2101
- else {
2102
- lines.push("**Technique**: least-to-most (see state file for the full skeleton).", "Generate a complete prompt following this technique.", "");
2103
- }
2104
- lines.push(...buildLoopObjectiveBlock(loopObjective), ...buildConstraintsBlock(constraints), ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []), ...buildTaskBlock(ctx), ...buildIdentityBlock(ctx));
2105
- if (embedSkeleton) {
2106
- lines.push("", "### 8-Section Skeleton (REQUIRED)", "", "| # | Section | Required | Notes |", "|---|---------|----------|-------|", "| 1 | 角色 | ✓ | Domain expert role appropriate to the task |", "| 2 | 任务 | ✓ | One-sentence task summary |", "| 3 | 输入 | ✓ | Target module |", "| 4 | 输出格式 | ✓ | Numbered list (e.g. \"1. DDL, 2. API, 3. Enum...\") |", "| 5 | **Least-to-Most 逐步推理框架** | ✓ | See format rules below |", "| 6 | 具体实现要求 | ✓ | Expand per output format list (NOT per sub-problem — sub-problems fully expanded in §5) |", "| 7 | 硬约束 | ✓ | Numbered list |", "| 8 | 生成要求 | ✓ | Acceptance criteria, must include: \"严格按照子问题顺序逐步实现\" |", "", "### Section 5 — Least-to-Most 逐步推理框架 (CRITICAL)", "", "Must contain **4-6 ordered sub-problems**. Each sub-problem format:", "", "```", "### 子问题 N:[Sub-problem Name]", "", "**目标:** [What this sub-problem solves, which prior sub-problem(s) it depends on]", "", "**要求:**", "- Specific requirement list", "- [If sub-problem involves enums/mapping tables, list them]", "", "---", "```", "", "**Critical rules**:", "- Sub-problem 1 is the SIMPLEST (e.g. \"Define enums and base data structures\")", "- Each sub-problem declares: \"基于子问题 N-1 的结论\"", "- The LAST sub-problem MUST be: **\"综合实现完整模块\"** — list all components to integrate", "- Sub-problems separated by `---`", "- Order goes from LEAST complex → MOST complex, dependency chain must be explicit", "- Each sub-problem must serve the original task — no unrelated steps", "- The final sub-problem must be equivalent to or directly address the original task", "", "### Generation Instructions", "1. Decompose the task into 4-6 ordered sub-problems with explicit dependencies", "2. Sub-problem 1 starts with the simplest building block (data structures, enums, base config)", "3. Each subsequent sub-problem builds on prior results", "4. Final sub-problem integrates all components into the complete module", "5. Verify: the generated prompt satisfies all structural rules in the skeleton above", "6. Output only the generated prompt — no preamble, no meta-commentary");
2107
- }
2108
- return lines.join("\n");
2109
- }
2110
- // ── Tree-of-Thought specialist ──────────────────────────────────────────────────
2111
- function compileToT(request, goalId, constraints, loopObjective, rollingText, analysis, opts = {}) {
2112
- const embedSkeleton = opts.embedSkeleton !== false; // default true
2113
- const ctx = { request, goalId, constraints, loopObjective, rollingText, level: opts.level ?? "l2" };
2114
- const lines = [
2115
- ...buildHeader(ctx, "tree-of-thought"),
2116
- `**Rationale**: ${analysis.rationale}`,
2117
- "",
2118
- ];
2119
- if (embedSkeleton) {
2120
- lines.push("Generate a complete prompt using the **Tree-of-Thought** technique. Follow the 8-section skeleton below.", "Embed all structural constraints — do NOT reference an external file.", "");
2121
- }
2122
- else {
2123
- lines.push("**Technique**: tree-of-thought (see state file for the full skeleton).", "Generate a complete prompt following this technique.", "");
2124
- }
2125
- lines.push(...buildLoopObjectiveBlock(loopObjective), ...buildConstraintsBlock(constraints), ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []), ...buildTaskBlock(ctx), ...buildIdentityBlock(ctx));
2126
- if (embedSkeleton) {
2127
- lines.push("", "### 8-Section Skeleton (REQUIRED)", "", "| # | Section | Required | Notes |", "|---|---------|----------|-------|", "| 1 | 角色 | ✓ | Multi-path problem-solving expert; may introduce 3 expert personas |", "| 2 | 任务 | ✓ | One-sentence task summary |", "| 3 | 输入 | ✓ | High-risk complex problem |", "| 4 | 输出格式 | ✓ | \"先输出思维树过程,再输出最终答案/方案\" |", "| 5 | **思维树探索框架** | ✓ | See 3 sub-blocks below (REQUIRED) |", "| 6 | 具体实现要求 | ✓ | Expand per output format |", "| 7 | 硬约束 | ✓ | Include branch_count, max_depth, pruning rules, safety/performance constraints |", "| 8 | 生成要求 | ✓ | \"先探索多路径,再选择最优方案\" |", "", "### Section 5 — 思维树探索框架 (CRITICAL — 3 sub-blocks REQUIRED)", "", "**Sub-block A: Search Strategy Declaration**", "```", "搜索策略: [beam / dfs / expert-panel]", "分支数(branch_count): 2-4", "最大深度(max_depth): ≤3", "每轮保留数(keep_count): 1-2", "```", "Strategy selection guide:", "- **beam** (default): planning, creative, math search — generate multiple candidates per turn, keep top b", "- **dfs**: puzzles, constraint satisfaction, debugging — go deep on highest-score branch, backtrack on failure", "- **expert-panel**: 3+ experts each generate candidates, then cross-evaluate and revise", "", "**Sub-block B: Evaluation Criteria Table**", "| 标准 | 权重 | 说明 |", "|------|------|------|", "| 正确性 | 最高 | Logic/math correctness |", "| 可行性 | 高 | Can be actually implemented |", "| 约束匹配 | 高 | Satisfies hard constraints |", "| 性能 | 中 | Time/space efficiency |", "| 安全性 | 最高 | No vulnerabilities/privilege escalation |", "", "**Sub-block C: Thought Tree State Table Format**", "Require the model to output in this table format:", "```", "| 轮次 | 分支 | 候选方案 | 评估 | 决策 |", "|------|------|---------|------|------|", "| 1 | A | [方案描述] | 评分/判断 | 保留/剪枝 |", "| 1 | B | [方案描述] | 评分/判断 | 保留/剪枝 |", "| 1 | C | [方案描述] | 评分/判断 | 保留/剪枝 |", "| 2 | A1 | [深入展开] | ... | ... |", "| ... | ... | ... | ... | ... |", "", "最终选择: [最优方案 + 选择理由]", "```", "", "### Critical Rules", "- Hard constraints MUST be ranked FIRST in evaluation criteria", "- Branch count: 2-4, depth: ≤3 (prevents token explosion)", "- Every turn: generate multiple candidates → evaluate → keep/prune — never single-path", "- If all branches are low quality: backtrack to previous turn and regenerate", "- The core of ToT is NOT \"3 experts chatting\" — it's: candidate generation → state evaluation → search/prune → final selection", "- thought is a PUBLIC intermediate semantic unit, not hidden chain-of-thought", "- State table MUST have: round, branch ID, candidate description, evaluation score/judgment, keep/prune decision", "", "### Generation Instructions", "1. Select the appropriate search strategy (beam/dfs/expert-panel) based on task type", "2. Define 2-4 candidate approaches as initial branches", "3. Build evaluation criteria table with hard constraints ranked first", "4. Define the state table format for tracking exploration", "5. Set branch count and max depth to prevent token explosion", "6. Verify: the generated prompt satisfies all structural rules in the skeleton above", "7. Output only the generated prompt — no preamble, no meta-commentary");
2128
- }
2129
- return lines.join("\n");
2130
- }
2131
- // ═══════════════════════════════════════════════════════════════════════════
2132
- // Self-Evaluation block (v1.1 — autonomous loop feedback)
2133
- // ═══════════════════════════════════════════════════════════════════════════
2134
- /** Build the standardized self-evaluation block appended to every compiled prompt.
2135
- * The agent MUST output a JSON self-evaluation between the delimiters.
2136
- * 4 required fields + 3 optional evolution fields (P0–P2) —
2137
- * each consumed by at least one downstream function. */
2138
- export function buildSelfEvalBlock(round) {
2139
- return [
2140
- "",
2141
- "### LoopForge Self-Evaluation (REQUIRED)",
2142
- "",
2143
- `You are completing Round ${round}. After finishing the task above, ` +
2144
- "you MUST output a self-evaluation in this exact format:",
2145
- "",
2146
- "```",
2147
- "---loopforge-eval",
2148
- "{",
2149
- ' "success": true,',
2150
- ` "output_summary": "<one paragraph — what was DONE in round ${round}, be specific>",`,
2151
- ' "constraint_violations": [],',
2152
- ' "should_continue": true,',
2153
- ' "discovered_constraints": [],',
2154
- ' "objective_refinement": "",',
2155
- ' "emerged_subtasks": [],',
2156
- ' "execution_evidence": {',
2157
- ' "files_changed": [],',
2158
- ' "test_results": {"passed": 0, "failed": 0, "skipped": 0},',
2159
- ' "success_criteria_met": [],',
2160
- ' "success_criteria_remaining": [],',
2161
- ' "progress_estimate": 0.0',
2162
- ' },',
2163
- ' "retracted_constraints": [],',
2164
- ' "revised_success_criteria": [],',
2165
- ' "wrong_assumptions": [],',
2166
- ' "compression_checkpoint": false,',
2167
- ' "checkpoint_label": ""',
2168
- "}",
2169
- "---end-loopforge-eval",
2170
- "```",
2171
- "",
2172
- "Field rules:",
2173
- `- success: true ONLY if all hard constraints were met and the task goal was achieved`,
2174
- `- output_summary: Be specific about what was PRODUCED, not what you "tried". ` +
2175
- `Bad: "worked on audit". Good: "Found 3 vulns: reentrancy in withdraw(), ` +
2176
- `integer overflow in transfer(), missing access control in mint()". ` +
2177
- `This feeds cross-round knowledge distillation.`,
2178
- `- constraint_violations: List ONLY constraints you actually broke. Empty array [] if none. ` +
2179
- `This directly affects constraint-integrity scoring and retirement decisions.`,
2180
- `- should_continue: false ONLY when the ENTIRE task is complete. ` +
2181
- `If there is more to audit/implement/test, say true. ` +
2182
- `This tells the autonomous runner when to stop.`,
2183
- `- discovered_constraints (P0 — optional): New constraints you discovered this round. ` +
2184
- `Omit or [] if none.`,
2185
- `- objective_refinement (P1 — optional): Deepened understanding of the task goal. ` +
2186
- `APPENDED to the original objective. Omit or "" if unchanged.`,
2187
- `- emerged_subtasks (P2 — optional): Sub-problems that surfaced during execution. ` +
2188
- `Omit or [] if none.`,
2189
- `- execution_evidence (P4 — recommended): Structured record of what you actually did. ` +
2190
- `files_changed: paths relative to project root. test_results: null if no tests run. ` +
2191
- `success_criteria_met/remaining: track against the Loop Objective. ` +
2192
- `progress_estimate: your honest estimate of overall completion (0.0 to 1.0). ` +
2193
- `This enables real progress tracking and early stall detection.`,
2194
- `- retracted_constraints (P5 — optional): Constraints you now believe are WRONG. ` +
2195
- `Removed from active guardrails. Only retract if you have evidence. Omit or [] if none.`,
2196
- `- revised_success_criteria (P5 — optional): Success criteria that need reformulation. ` +
2197
- `Array of {old, new} objects. Applied to the Loop Objective. Omit or [] if none.`,
2198
- `- wrong_assumptions (P5 — optional): Assumptions from earlier rounds that turned ` +
2199
- `out to be incorrect. Recorded as key lessons. Omit or [] if none.`,
2200
- `- compression_checkpoint (v1.10 — optional): Set to true when you have COMPLETED a ` +
2201
- `major subtask. The compiler will snapshot the current state (constraints, outcome) ` +
2202
- `into a persistent checkpoint that survives context-window eviction. ` +
2203
- `Use sparingly — only at genuine subtask boundaries, not every round. ` +
2204
- `Omit or false otherwise.`,
2205
- `- checkpoint_label (v1.10 — optional): Human-readable label for this checkpoint. ` +
2206
- `e.g. "数据模型层完成", "API layer complete", "Auth module done". ` +
2207
- `Used as the checkpoint heading in subsequent prompts. Only meaningful when ` +
2208
- `compression_checkpoint is true. Omit or "" otherwise.`,
2209
- `- The JSON MUST appear between the ---loopforge-eval and ---end-loopforge-eval markers`,
2210
- `- Do NOT wrap the markers in code fences — output them as raw text`,
2211
- "",
2212
- ].join("\n");
2213
- }
2214
- // ═══════════════════════════════════════════════════════════════════════════
2215
- // Top-level compile — ties layers together
2216
- // ═══════════════════════════════════════════════════════════════════════════
2217
- export function compileLoop(request, vaultContext = null) {
2218
- // Layer 1: Decide compile level
2219
- const level = decideLevel(request, vaultContext);
2220
- // Compile at the decided level
2221
- let response;
2222
- if (level === "l0") {
2223
- response = compileL0(request, vaultContext);
2224
- }
2225
- else if (level === "l1") {
2226
- response = compileL1(request, vaultContext);
2227
- }
2228
- else {
2229
- response = compileL2(request, vaultContext);
2230
- }
2231
- // Layer 2: Compute advisories
2232
- const { warnings, suggestedNextTask, alignment, health } = computeAdvisories(request, vaultContext);
2233
- // Merge advisories into response (preserve P4 consistency warnings from L2)
2234
- response.warnings = [...(response.warnings || []), ...warnings];
2235
- // Only overwrite suggested_next_task from advisories if the compile function
2236
- // didn't already set one (e.g. L2 sets it from emerged_subtasks)
2237
- if (!response.suggested_next_task) {
2238
- response.suggested_next_task = suggestedNextTask;
2239
- }
2240
- response.task_alignment = alignment;
2241
- response.loop_health = health;
2242
- response.recompile_level = level;
2243
- // v1.1: Append self-evaluation block for autonomous loop feedback
2244
- response.prompt += buildSelfEvalBlock(request.round);
2245
- // Multi-agent: Inject delegation history if available
2246
- if (vaultContext) {
2247
- const delegationText = buildDelegationSummary(vaultContext);
2248
- if (delegationText) {
2249
- response.prompt += delegationText;
2250
- }
2251
- }
346
+ response.prompt = artifact.renderedPrompt;
347
+ response.prompt_artifact = artifact;
348
+ response.state_file_content = policy.state_file.enabled ? markdown : undefined;
2252
349
  return response;
2253
350
  }
2254
351
  //# sourceMappingURL=loop-compiler.js.map