loopforge 1.3.1 → 1.8.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 (91) hide show
  1. package/README.md +64 -3
  2. package/dist/backends/fs.d.ts +5 -0
  3. package/dist/backends/fs.d.ts.map +1 -1
  4. package/dist/backends/fs.js +66 -16
  5. package/dist/backends/fs.js.map +1 -1
  6. package/dist/builder.d.ts +0 -1
  7. package/dist/builder.d.ts.map +1 -1
  8. package/dist/builder.js +14 -25
  9. package/dist/builder.js.map +1 -1
  10. package/dist/engine.d.ts +28 -5
  11. package/dist/engine.d.ts.map +1 -1
  12. package/dist/engine.js +303 -105
  13. package/dist/engine.js.map +1 -1
  14. package/dist/index.d.ts +9 -12
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7 -10
  17. package/dist/index.js.map +1 -1
  18. package/dist/loop-compiler.d.ts +28 -2
  19. package/dist/loop-compiler.d.ts.map +1 -1
  20. package/dist/loop-compiler.js +718 -38
  21. package/dist/loop-compiler.js.map +1 -1
  22. package/dist/mcp/server.d.ts.map +1 -1
  23. package/dist/mcp/server.js +6 -4
  24. package/dist/mcp/server.js.map +1 -1
  25. package/dist/mcp/session.d.ts +24 -4
  26. package/dist/mcp/session.d.ts.map +1 -1
  27. package/dist/mcp/session.js +303 -34
  28. package/dist/mcp/session.js.map +1 -1
  29. package/dist/mcp/tools.d.ts +165 -1
  30. package/dist/mcp/tools.d.ts.map +1 -1
  31. package/dist/mcp/tools.js +144 -15
  32. package/dist/mcp/tools.js.map +1 -1
  33. package/dist/memory-bridge.d.ts +47 -0
  34. package/dist/memory-bridge.d.ts.map +1 -0
  35. package/dist/memory-bridge.js +290 -0
  36. package/dist/memory-bridge.js.map +1 -0
  37. package/dist/observability.d.ts +23 -0
  38. package/dist/observability.d.ts.map +1 -0
  39. package/dist/observability.js +29 -0
  40. package/dist/observability.js.map +1 -0
  41. package/dist/policy.d.ts +72 -0
  42. package/dist/policy.d.ts.map +1 -1
  43. package/dist/policy.js +44 -0
  44. package/dist/policy.js.map +1 -1
  45. package/dist/protocol.d.ts +197 -7
  46. package/dist/protocol.d.ts.map +1 -1
  47. package/dist/protocol.js +48 -5
  48. package/dist/protocol.js.map +1 -1
  49. package/dist/runtime.d.ts +19 -0
  50. package/dist/runtime.d.ts.map +1 -1
  51. package/dist/runtime.js +446 -142
  52. package/dist/runtime.js.map +1 -1
  53. package/dist/tests/builder.test.js +1 -25
  54. package/dist/tests/builder.test.js.map +1 -1
  55. package/dist/tests/engine.test.js +213 -38
  56. package/dist/tests/engine.test.js.map +1 -1
  57. package/dist/tests/generate-schema.test.js +4 -4
  58. package/dist/tests/generate-schema.test.js.map +1 -1
  59. package/dist/tests/loop-compiler.test.js +427 -2
  60. package/dist/tests/loop-compiler.test.js.map +1 -1
  61. package/dist/tests/mcp.test.js +166 -79
  62. package/dist/tests/mcp.test.js.map +1 -1
  63. package/dist/tests/memory-bridge.test.d.ts +8 -0
  64. package/dist/tests/memory-bridge.test.d.ts.map +1 -0
  65. package/dist/tests/memory-bridge.test.js +384 -0
  66. package/dist/tests/memory-bridge.test.js.map +1 -0
  67. package/dist/tests/protocol.test.js +14 -5
  68. package/dist/tests/protocol.test.js.map +1 -1
  69. package/dist/tests/verification-gate.test.d.ts +2 -0
  70. package/dist/tests/verification-gate.test.d.ts.map +1 -0
  71. package/dist/tests/verification-gate.test.js +407 -0
  72. package/dist/tests/verification-gate.test.js.map +1 -0
  73. package/dist/verification-gate.d.ts +26 -0
  74. package/dist/verification-gate.d.ts.map +1 -0
  75. package/dist/verification-gate.js +232 -0
  76. package/dist/verification-gate.js.map +1 -0
  77. package/loop_policy.json +31 -0
  78. package/package.json +1 -2
  79. package/skills/perception/SKILL.md +147 -55
  80. package/dist/adapter.d.ts +0 -17
  81. package/dist/adapter.d.ts.map +0 -1
  82. package/dist/adapter.js +0 -228
  83. package/dist/adapter.js.map +0 -1
  84. package/dist/cli.d.ts +0 -7
  85. package/dist/cli.d.ts.map +0 -1
  86. package/dist/cli.js +0 -458
  87. package/dist/cli.js.map +0 -1
  88. package/dist/tests/adapter.test.d.ts +0 -2
  89. package/dist/tests/adapter.test.d.ts.map +0 -1
  90. package/dist/tests/adapter.test.js +0 -104
  91. package/dist/tests/adapter.test.js.map +0 -1
@@ -12,7 +12,7 @@
12
12
  import { createHash } from "node:crypto";
13
13
  import { readFileSync } from "node:fs";
14
14
  import { getPolicy } from "./policy.js";
15
- import { makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeRollingSummary, makeTaskAlignment, } from "./protocol.js";
15
+ import { AgentStatus, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeRollingSummary, makeTaskAlignment, } from "./protocol.js";
16
16
  import { routeTechniqueAdaptive } from "./builder.js";
17
17
  // ═══════════════════════════════════════════════════════════════════════════
18
18
  // Repair cue detection
@@ -34,7 +34,7 @@ function detectsRepairSignal(request) {
34
34
  // ═══════════════════════════════════════════════════════════════════════════
35
35
  // Tokenization helpers
36
36
  // ═══════════════════════════════════════════════════════════════════════════
37
- function tokenize(text) {
37
+ export function tokenize(text) {
38
38
  const tokens = text.split(/\s+/);
39
39
  const result = new Set();
40
40
  for (let token of tokens) {
@@ -52,7 +52,7 @@ function tokenize(text) {
52
52
  }
53
53
  return result;
54
54
  }
55
- function jaccard(a, b) {
55
+ export function jaccard(a, b) {
56
56
  if (a.size === 0 || b.size === 0)
57
57
  return 0.0;
58
58
  let intersection = 0;
@@ -64,6 +64,141 @@ function jaccard(a, b) {
64
64
  return union > 0 ? intersection / union : 0.0;
65
65
  }
66
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
+ // ═══════════════════════════════════════════════════════════════════════════
67
202
  // Goal identity
68
203
  // ═══════════════════════════════════════════════════════════════════════════
69
204
  export function computeGoalTextHash(task) {
@@ -77,17 +212,6 @@ export function deriveGoalId(loopId, task, explicitGoalId = "") {
77
212
  taskPrefix = taskPrefix.replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
78
213
  return loopId ? `${loopId}:${taskPrefix}` : taskPrefix;
79
214
  }
80
- function makePreviousRound() {
81
- return {
82
- goal_id: "",
83
- goal_text_hash: "",
84
- quality_score: 0,
85
- success: true,
86
- task: "",
87
- constraints_active: [],
88
- prompt_text: "",
89
- };
90
- }
91
215
  export function getPreviousRound(loopId, roundNum, vaultContext) {
92
216
  if (vaultContext === null)
93
217
  return null;
@@ -237,9 +361,140 @@ function computeConstraintRetirement(activeConstraints, loopId, currentRound, va
237
361
  return { active: pruned, retired };
238
362
  }
239
363
  // ═══════════════════════════════════════════════════════════════════════════
364
+ // P4: Progress evidence helpers
365
+ // ═══════════════════════════════════════════════════════════════════════════
366
+ /** Extract execution evidence from a previous round in the vault.
367
+ * Used for progress gradient calculation and trend detection. */
368
+ function getPreviousRoundEvidence(loopId, roundNum, vaultContext) {
369
+ if (vaultContext === null)
370
+ return null;
371
+ const results = vaultContext.results || [];
372
+ for (const r of results) {
373
+ const lineage = (r.loop_lineage || r.lineage || {});
374
+ if (lineage.loop_id !== loopId)
375
+ continue;
376
+ if (lineage.round !== roundNum)
377
+ continue;
378
+ const evidence = (r.execution_evidence ?? lineage.execution_evidence);
379
+ if (evidence) {
380
+ return {
381
+ progress_estimate: evidence.progress_estimate ?? 0,
382
+ success_criteria_met: evidence.success_criteria_met ?? [],
383
+ };
384
+ }
385
+ return null;
386
+ }
387
+ return null;
388
+ }
389
+ /** Count consecutive rounds (including current) where progress delta
390
+ * has been below the stall threshold. */
391
+ function countProgressStallRounds(loopId, currentRound, vaultContext, threshold) {
392
+ if (vaultContext === null || currentRound < 2)
393
+ return 0;
394
+ let count = 0;
395
+ for (let r = currentRound; r >= 2; r--) {
396
+ const curr = getPreviousRoundEvidence(loopId, r, vaultContext);
397
+ const prev = getPreviousRoundEvidence(loopId, r - 1, vaultContext);
398
+ if (curr && prev) {
399
+ const delta = curr.progress_estimate - prev.progress_estimate;
400
+ if (delta < threshold && curr.progress_estimate < 0.95) {
401
+ count++;
402
+ }
403
+ else {
404
+ break;
405
+ }
406
+ }
407
+ else {
408
+ break;
409
+ }
410
+ }
411
+ return count;
412
+ }
413
+ /** Detect repeated failure patterns in sampled rounds.
414
+ * A failure pattern = 2+ consecutive rounds where:
415
+ * 1. quality_score < 3 (failed or weak)
416
+ * 2. same technique was used
417
+ * 3. task text is similar (Jaccard > 0.4)
418
+ *
419
+ * Returns human-readable descriptions for injection into the prompt. */
420
+ function detectFailurePatterns(sampled) {
421
+ if (sampled.length < 2)
422
+ return [];
423
+ const patterns = [];
424
+ let runStart = -1;
425
+ for (let i = 0; i < sampled.length; i++) {
426
+ const r = sampled[i];
427
+ const isFailed = r.quality_score < 3;
428
+ if (isFailed && runStart === -1) {
429
+ runStart = i;
430
+ }
431
+ else if (!isFailed && runStart !== -1) {
432
+ if (i - runStart >= 2) {
433
+ const pattern = classifyRun(sampled.slice(runStart, i));
434
+ if (pattern)
435
+ patterns.push(pattern);
436
+ }
437
+ runStart = -1;
438
+ }
439
+ }
440
+ // Handle run at end of array
441
+ if (runStart !== -1 && sampled.length - runStart >= 2) {
442
+ const pattern = classifyRun(sampled.slice(runStart));
443
+ if (pattern)
444
+ patterns.push(pattern);
445
+ }
446
+ return patterns;
447
+ }
448
+ /** Classify a run of consecutive low-quality rounds as a failure pattern.
449
+ * Returns null if the rounds are too dissimilar to be the same pattern. */
450
+ function classifyRun(run) {
451
+ if (run.length < 2)
452
+ return null;
453
+ // Must all use the same technique
454
+ const technique = run[0].technique_used || "unknown";
455
+ if (!run.every((r) => (r.technique_used || "unknown") === technique)) {
456
+ return null;
457
+ }
458
+ // Task text must be similar across the run (pairwise Jaccard)
459
+ const tokensList = run.map((r) => tokenize(r.task.toLowerCase()));
460
+ for (let i = 0; i < tokensList.length - 1; i++) {
461
+ for (let j = i + 1; j < tokensList.length; j++) {
462
+ if (jaccard(tokensList[i], tokensList[j]) < 0.4)
463
+ return null;
464
+ }
465
+ }
466
+ const rounds = `R${run[0].round}-R${run[run.length - 1].round}`;
467
+ const taskPreview = run[0].task.slice(0, 80);
468
+ const avgScore = (run.reduce((s, r) => s + r.quality_score, 0) / run.length).toFixed(1);
469
+ return (`technique '${technique}' on task '${taskPreview}' ` +
470
+ `failed ${run.length} consecutive rounds (${rounds}, avg score ${avgScore}) — ` +
471
+ `consider strategy change`);
472
+ }
473
+ /** Extract round numbers referenced in failure pattern descriptions.
474
+ * Used by buildRollingSummary to identify which rounds to demote. */
475
+ function extractFailureRoundNums(patterns) {
476
+ const nums = new Set();
477
+ for (const p of patterns) {
478
+ // Match "R2-R4" or "R5"
479
+ const rangeMatch = p.match(/R(\d+)-R(\d+)/);
480
+ if (rangeMatch) {
481
+ const start = parseInt(rangeMatch[1], 10);
482
+ const end = parseInt(rangeMatch[2], 10);
483
+ for (let r = start; r <= end; r++)
484
+ nums.add(r);
485
+ }
486
+ else {
487
+ const singleMatch = p.match(/R(\d+)/);
488
+ if (singleMatch)
489
+ nums.add(parseInt(singleMatch[1], 10));
490
+ }
491
+ }
492
+ return nums;
493
+ }
494
+ // ═══════════════════════════════════════════════════════════════════════════
240
495
  // Rolling Summary (v3.5)
241
496
  // ═══════════════════════════════════════════════════════════════════════════
242
- function buildRollingSummary(loopId, currentRound, vaultContext) {
497
+ export function buildRollingSummary(loopId, currentRound, vaultContext) {
243
498
  if (vaultContext === null)
244
499
  return null;
245
500
  const results = vaultContext.results || [];
@@ -310,7 +565,7 @@ function buildRollingSummary(loopId, currentRound, vaultContext) {
310
565
  }
311
566
  }
312
567
  }
313
- const recurringIssues = [...violationCounts.entries()]
568
+ let recurringIssues = [...violationCounts.entries()]
314
569
  .filter(([, count]) => count >= 2)
315
570
  .sort((a, b) => b[1] - a[1])
316
571
  .map(([v, count]) => `${v} (appeared in ${count} rounds)`);
@@ -321,6 +576,57 @@ function buildRollingSummary(loopId, currentRound, vaultContext) {
321
576
  keyLessons.push(`[R${r.round}] ${String(r.output_summary).slice(0, 200)}`);
322
577
  }
323
578
  }
579
+ // v1.7: Failure lineage weighting — detect and demote failure patterns
580
+ const sampledRounds = sampled.map((r) => ({
581
+ round: r.round,
582
+ quality_score: r.quality_score,
583
+ task: r.task,
584
+ output_summary: r.output_summary,
585
+ constraint_violations: r.constraint_violations,
586
+ technique_used: r.technique_used,
587
+ }));
588
+ const failurePatterns = detectFailurePatterns(sampledRounds);
589
+ const failedRoundNums = extractFailureRoundNums(failurePatterns);
590
+ // Re-weight key_lessons: lessons from failure-pattern rounds go to end
591
+ if (failedRoundNums.size > 0 && keyLessons.length > 0) {
592
+ const promoted = [];
593
+ const demoted = [];
594
+ for (const kl of keyLessons) {
595
+ const rndMatch = kl.match(/^\[R(\d+)\]/);
596
+ if (rndMatch && failedRoundNums.has(parseInt(rndMatch[1], 10))) {
597
+ demoted.push(kl.replace(/^\[R\d+\]/, "$& [Consider alternatives]"));
598
+ }
599
+ else {
600
+ promoted.push(kl);
601
+ }
602
+ }
603
+ keyLessons.length = 0;
604
+ keyLessons.push(...promoted, ...demoted);
605
+ }
606
+ // Re-weight recurring_issues: mark issues that ONLY appear in failure-pattern rounds
607
+ if (failedRoundNums.size > 0 && recurringIssues.length > 0) {
608
+ // Build per-round violation sets
609
+ const failureViolations = new Set();
610
+ const successViolations = new Set();
611
+ for (const r of sampledRounds) {
612
+ const target = failedRoundNums.has(r.round) ? failureViolations : successViolations;
613
+ for (const v of r.constraint_violations) {
614
+ target.add(String(v).trim().toLowerCase());
615
+ }
616
+ }
617
+ recurringIssues.length = 0;
618
+ recurringIssues.push(...[...violationCounts.entries()]
619
+ .filter(([, count]) => count >= 2)
620
+ .sort((a, b) => b[1] - a[1])
621
+ .map(([v, count]) => {
622
+ const vNorm = v.trim().toLowerCase();
623
+ // Mark if this issue only appears in failure rounds (possible dead end)
624
+ if (failureViolations.has(vNorm) && !successViolations.has(vNorm)) {
625
+ return `${v} (appeared in ${count} rounds) ⚠️ [Possible dead end — only in failed rounds]`;
626
+ }
627
+ return `${v} (appeared in ${count} rounds)`;
628
+ }));
629
+ }
324
630
  return makeRollingSummary({
325
631
  quality_trajectory: trajectory,
326
632
  trajectory_direction: direction,
@@ -329,9 +635,10 @@ function buildRollingSummary(loopId, currentRound, vaultContext) {
329
635
  key_lessons: keyLessons,
330
636
  rounds_sampled: sampled.length,
331
637
  generated_at_round: currentRound,
638
+ failed_patterns: failurePatterns.length ? failurePatterns : [],
332
639
  });
333
640
  }
334
- function formatRollingSummaryForPrompt(rs) {
641
+ export function formatRollingSummaryForPrompt(rs) {
335
642
  if (rs === null || rs.rounds_sampled === 0)
336
643
  return "";
337
644
  const lines = [
@@ -353,6 +660,18 @@ function formatRollingSummaryForPrompt(rs) {
353
660
  lines.push(`- ⚠️ ${ri}`);
354
661
  lines.push("");
355
662
  }
663
+ // v1.7: Failure patterns — explicit warnings about repeated failure paths
664
+ if (rs.failed_patterns && rs.failed_patterns.length) {
665
+ lines.push("### ⚠️ Failure Patterns — Consider Strategy Change");
666
+ lines.push("");
667
+ lines.push("The following approaches failed repeatedly. The compiler has demoted " +
668
+ "their influence on key lessons. Consider a different technique or " +
669
+ "narrower scope.");
670
+ for (const fp of rs.failed_patterns) {
671
+ lines.push(`- 🚫 ${fp}`);
672
+ }
673
+ lines.push("");
674
+ }
356
675
  if (rs.key_lessons.length) {
357
676
  lines.push("**Key Lessons From High-Score Rounds**:");
358
677
  for (const kl of rs.key_lessons)
@@ -362,6 +681,36 @@ function formatRollingSummaryForPrompt(rs) {
362
681
  return lines.join("\n");
363
682
  }
364
683
  // ═══════════════════════════════════════════════════════════════════════════
684
+ // External Context formatting (v1.7 — memory system integration)
685
+ // ═══════════════════════════════════════════════════════════════════════════
686
+ /** Format an external context string for injection into an L2 prompt.
687
+ * Wraps the raw context in a marked section with a priority disclaimer.
688
+ * Returns empty string if context is empty or injection is disabled by policy. */
689
+ export function formatExternalContext(externalContext, sectionTitle, maxLength) {
690
+ if (!externalContext || externalContext.trim().length === 0)
691
+ return "";
692
+ const trimmed = externalContext.trim().slice(0, maxLength);
693
+ const truncationNote = trimmed.length < externalContext.trim().length
694
+ ? `\n> ⚠️ Context was truncated to ${maxLength} characters.`
695
+ : "";
696
+ return [
697
+ "",
698
+ sectionTitle,
699
+ "",
700
+ "> ⚠️ **Role**: Situational awareness only. These are point-in-time",
701
+ "> observations from past sessions — claims about code locations or",
702
+ "> behaviour may be outdated. If any insight contradicts the Loop",
703
+ "> Objective or Active Constraints above, the LoopForge specification",
704
+ "> takes absolute precedence.",
705
+ "",
706
+ trimmed,
707
+ truncationNote,
708
+ "",
709
+ "---",
710
+ "",
711
+ ].join("\n");
712
+ }
713
+ // ═══════════════════════════════════════════════════════════════════════════
365
714
  // LAYER 1 — Hard Gates (can change compile level)
366
715
  // ═══════════════════════════════════════════════════════════════════════════
367
716
  export function decideLevel(request, vaultContext) {
@@ -384,7 +733,23 @@ export function decideLevel(request, vaultContext) {
384
733
  // Gate 3: goal_id stability
385
734
  if (goalId !== prev.goal_id)
386
735
  return "l2";
387
- // Gate 4: Explicit failures or new constraints → patch
736
+ // Gate 4: P0–P5 cognitive evolution signals → full recompile (v1.7)
737
+ // Check BEFORE the simple repair/new-constraint gates because evolution signals
738
+ // (constraint discovery, objective refinement, self-correction) need full L2
739
+ // processing. Only structural evolution signals trigger L2 — execution_evidence
740
+ // alone does not (it is processed via the rolling summary at L1).
741
+ if (request.last_round_result) {
742
+ const lr = request.last_round_result;
743
+ if ((lr.discovered_constraints?.length ?? 0) > 0 ||
744
+ (lr.objective_refinement?.trim().length ?? 0) > 0 ||
745
+ (lr.emerged_subtasks?.length ?? 0) > 0 ||
746
+ (lr.retracted_constraints?.length ?? 0) > 0 ||
747
+ (lr.revised_success_criteria?.length ?? 0) > 0 ||
748
+ (lr.wrong_assumptions?.length ?? 0) > 0) {
749
+ return "l2";
750
+ }
751
+ }
752
+ // Gate 5: Explicit failures or new constraints → patch
388
753
  const hasNewConstraints = request.constraints_from_plan.length > 0;
389
754
  const hasNewFailures = request.last_round_result !== null &&
390
755
  !request.last_round_result.success;
@@ -601,7 +966,8 @@ function extractObjectiveFromPlan(planPath) {
601
966
  const goalPatterns = ["goal", "objective", "目标", "目的", "意图"];
602
967
  const successPatterns = [
603
968
  "success criteria", "acceptance criteria", "验收标准", "成功标准",
604
- "done when", "完成标准", "交付标准",
969
+ "done when", "完成标准", "交付标准", "outcome", "deliverable",
970
+ "结果", "产出", "交付物",
605
971
  ];
606
972
  const constraintPatterns = [
607
973
  "hard constraint", "constraint", "non-goal", "out of scope",
@@ -641,6 +1007,12 @@ function extractObjectiveFromPlan(planPath) {
641
1007
  sections.goal.push(stripped);
642
1008
  }
643
1009
  }
1010
+ else if (stripped && currentSection === "success" && stripped.length > 10) {
1011
+ // P3: Also capture free-text paragraphs under success criteria headers
1012
+ if (!/^#/.test(stripped)) {
1013
+ sections.success.push(stripped);
1014
+ }
1015
+ }
644
1016
  }
645
1017
  if (!Object.values(sections).some((v) => v.length))
646
1018
  return null;
@@ -656,16 +1028,33 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
656
1028
  let objective = task.trim().slice(0, 200);
657
1029
  const successCriteria = [];
658
1030
  const hardConstraints = [...constraints];
1031
+ // P3: Outcome-oriented default success criteria — describe the RESULT,
1032
+ // not the process. The process emerges from the loop.
659
1033
  if (/test|测试/i.test(task)) {
660
- successCriteria.push("All tests pass");
1034
+ successCriteria.push("All tests pass with no regressions");
661
1035
  }
662
1036
  if (/compat|兼容/i.test(task)) {
663
- successCriteria.push("Backward compatibility maintained");
1037
+ successCriteria.push("Existing API contracts and integrations unchanged");
664
1038
  }
665
1039
  if (/security|安全|audit/i.test(task)) {
666
- successCriteria.push("No security vulnerabilities found");
1040
+ successCriteria.push("All identified vulnerabilities resolved or documented");
667
1041
  }
668
- // Try to extract richer objective from plan_source
1042
+ if (/fix|修复|bug/i.test(task)) {
1043
+ successCriteria.push("Root cause addressed and verified, not just symptoms");
1044
+ }
1045
+ if (/build|create|implement|开发|实现|构建/i.test(task)) {
1046
+ successCriteria.push("Deliverable matches specification and passes review");
1047
+ }
1048
+ if (/refactor|重构/i.test(task)) {
1049
+ successCriteria.push("Behavior preserved, structure improved, no new failures");
1050
+ }
1051
+ if (/migrate|迁移|upgrade|升级/i.test(task)) {
1052
+ successCriteria.push("Migration completes without data loss or downtime");
1053
+ }
1054
+ if (/perf|performance|optimize|性能|优化/i.test(task)) {
1055
+ successCriteria.push("Measurable improvement verified by benchmarks");
1056
+ }
1057
+ // P3: Try to extract richer objective from plan_source
669
1058
  if (request.plan_source) {
670
1059
  const planExtracted = extractObjectiveFromPlan(request.plan_source);
671
1060
  if (planExtracted) {
@@ -683,11 +1072,27 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
683
1072
  hardConstraints.push(`Follow plan: ${request.plan_source}`);
684
1073
  }
685
1074
  }
1075
+ // P3: Derive domain from task content for context-aware defaults
686
1076
  if (!successCriteria.length) {
687
- successCriteria.push("Task completed successfully");
1077
+ if (/\.sol|solidity|contract|智能合约/i.test(task)) {
1078
+ successCriteria.push("All functions respect check-effects-interactions pattern");
1079
+ successCriteria.push("No unchecked external calls or reentrancy vectors");
1080
+ }
1081
+ else if (/\.tsx?|\.jsx?|react|component|组件/i.test(task)) {
1082
+ successCriteria.push("Component renders correctly across all states (loading, empty, error)");
1083
+ successCriteria.push("No prop-type or accessibility regressions");
1084
+ }
1085
+ else if (/api|endpoint|接口|route/i.test(task)) {
1086
+ successCriteria.push("Endpoint responds correctly for all defined status codes");
1087
+ successCriteria.push("Error responses follow the project's error schema");
1088
+ }
1089
+ else {
1090
+ successCriteria.push("Task goal achieved with verifiable evidence");
1091
+ }
688
1092
  }
689
1093
  if (!hardConstraints.length) {
690
- hardConstraints.push("Do not modify files outside scope");
1094
+ hardConstraints.push("Do not modify files outside the stated scope");
1095
+ hardConstraints.push("Preserve existing test coverage — no test deletions without replacement");
691
1096
  }
692
1097
  return makeLoopObjective({
693
1098
  objective,
@@ -695,6 +1100,8 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
695
1100
  hard_constraints: hardConstraints,
696
1101
  created_at_round: 1,
697
1102
  loop_id: request.loop_id,
1103
+ version: 1,
1104
+ refinement_history: [],
698
1105
  });
699
1106
  }
700
1107
  // ═══════════════════════════════════════════════════════════════════════════
@@ -706,7 +1113,7 @@ function compileL0(request, vaultContext, prevRound) {
706
1113
  const cachedPrompt = prev?.prompt_text ?? "";
707
1114
  if (cachedPrompt) {
708
1115
  return makeLoopCompileResponse({
709
- status: "ok",
1116
+ status: AgentStatus.OK,
710
1117
  prompt: cachedPrompt,
711
1118
  recompile_level: "l0",
712
1119
  diff_from_previous: `L0 cache hit — reusing prompt from round ${request.round - 1}`,
@@ -800,7 +1207,7 @@ function compileL1(request, vaultContext, prevRound) {
800
1207
  lines.push("### Task");
801
1208
  lines.push(request.task);
802
1209
  return makeLoopCompileResponse({
803
- status: "ok",
1210
+ status: AgentStatus.OK,
804
1211
  prompt: lines.join("\n"),
805
1212
  recompile_level: "l1",
806
1213
  diff_from_previous: diffParts.length ? diffParts.join("; ") : "Patch applied.",
@@ -825,8 +1232,19 @@ export function compileL2(request, vaultContext) {
825
1232
  const referenceFile = analysis.reference_file;
826
1233
  // v3.5: Rolling summary
827
1234
  const rollingSummary = buildRollingSummary(request.loop_id, request.round, vaultContext);
1235
+ // P5c: Wrong assumptions — must be applied BEFORE formatting so they appear
1236
+ // in the rendered cross-round summary text injected into the prompt.
1237
+ const lr = request.last_round_result;
1238
+ if (lr?.wrong_assumptions?.length) {
1239
+ if (rollingSummary) {
1240
+ rollingSummary.key_lessons = [
1241
+ ...rollingSummary.key_lessons,
1242
+ ...lr.wrong_assumptions.map((a) => `Wrong assumption corrected: ${a}`),
1243
+ ];
1244
+ }
1245
+ }
828
1246
  const rollingText = formatRollingSummaryForPrompt(rollingSummary);
829
- // Generate loop objective at round 1 if not provided
1247
+ // ── Loop Objective: create at round 1, load from vault at later rounds ──
830
1248
  let loopObjective = null;
831
1249
  if (request.round === 1) {
832
1250
  if (request.loop_objective) {
@@ -836,9 +1254,113 @@ export function compileL2(request, vaultContext) {
836
1254
  loopObjective = computeLoopObjectiveFromTask(request, vaultContext);
837
1255
  }
838
1256
  }
1257
+ else {
1258
+ // P1 fix: Load loop objective from vault for round > 1
1259
+ const vaultLo = vaultGetLoopObjective(request.loop_id, vaultContext);
1260
+ if (vaultLo) {
1261
+ const loData = vaultLo.loop_objective;
1262
+ if (loData) {
1263
+ loopObjective = makeLoopObjective({
1264
+ objective: loData.objective ?? "",
1265
+ success_criteria: loData.success_criteria ?? [],
1266
+ hard_constraints: loData.hard_constraints ?? [],
1267
+ created_at_round: loData.created_at_round ?? 1,
1268
+ loop_id: loData.loop_id ?? request.loop_id,
1269
+ version: loData.version ?? 1,
1270
+ refinement_history: loData.refinement_history ?? [],
1271
+ });
1272
+ }
1273
+ }
1274
+ if (!loopObjective && request.loop_objective) {
1275
+ loopObjective = request.loop_objective;
1276
+ }
1277
+ }
1278
+ // ── P1: Apply objective_refinement from last round ──
1279
+ if (loopObjective && request.last_round_result?.objective_refinement) {
1280
+ const refinement = request.last_round_result.objective_refinement.trim();
1281
+ if (refinement) {
1282
+ const version = (loopObjective.version ?? 1) + 1;
1283
+ const history = [...(loopObjective.refinement_history ?? []), refinement];
1284
+ // Enforce max versions from policy
1285
+ const policy = getPolicy();
1286
+ const maxVersions = policy.evolution?.max_objective_versions ?? 10;
1287
+ const prunedHistory = history.slice(-maxVersions);
1288
+ const refinedObjective = `${loopObjective.objective} [R${request.round}: ${refinement}]`;
1289
+ loopObjective = makeLoopObjective({
1290
+ ...loopObjective,
1291
+ objective: refinedObjective,
1292
+ version,
1293
+ refinement_history: prunedHistory,
1294
+ });
1295
+ }
1296
+ }
1297
+ // ── Build active constraints (base + LO + P0 discovered) ──
839
1298
  let constraints = [...request.constraints_from_plan];
840
1299
  if (loopObjective) {
841
- constraints = [...new Set([...constraints, ...loopObjective.hard_constraints, ...loopObjective.success_criteria])];
1300
+ constraints = [
1301
+ ...new Set([
1302
+ ...constraints,
1303
+ ...loopObjective.hard_constraints,
1304
+ ...loopObjective.success_criteria,
1305
+ ]),
1306
+ ];
1307
+ }
1308
+ // P0: Merge discovered constraints from last round
1309
+ if (request.last_round_result?.discovered_constraints?.length) {
1310
+ const policy = getPolicy();
1311
+ const maxPerRound = policy.evolution?.max_discovered_constraints_per_round ?? 5;
1312
+ const maxActive = policy.evolution?.max_active_constraints ?? 15;
1313
+ const newConstraints = request.last_round_result.discovered_constraints
1314
+ .slice(0, maxPerRound)
1315
+ .filter((c) => !constraints.includes(c));
1316
+ constraints = [...constraints, ...newConstraints].slice(0, maxActive);
1317
+ }
1318
+ // ═══════════════════════════════════════════════════════════════════════════
1319
+ // P5: Self-correction — MUST process retractions/revisions BEFORE prompt build
1320
+ // so the technician compiler sees the corrected constraints and loop objective.
1321
+ // ═══════════════════════════════════════════════════════════════════════════
1322
+ const warnings = [];
1323
+ const retired = [];
1324
+ // P5a: Retracted constraints — remove from active set
1325
+ if (lr?.retracted_constraints?.length) {
1326
+ for (const rc of lr.retracted_constraints) {
1327
+ const idx = constraints.indexOf(rc);
1328
+ if (idx >= 0) {
1329
+ constraints.splice(idx, 1);
1330
+ retired.push(`[agent-retracted] ${rc}`);
1331
+ }
1332
+ }
1333
+ }
1334
+ // P5b: Revised success criteria — update Loop Objective
1335
+ if (lr?.revised_success_criteria?.length && loopObjective) {
1336
+ const sc = [...loopObjective.success_criteria];
1337
+ for (const rev of lr.revised_success_criteria) {
1338
+ const idx = sc.indexOf(rev.old);
1339
+ if (idx >= 0) {
1340
+ sc[idx] = rev.new;
1341
+ }
1342
+ else {
1343
+ sc.push(rev.new);
1344
+ }
1345
+ }
1346
+ const version = (loopObjective.version ?? 1) + 1;
1347
+ const history = [
1348
+ ...(loopObjective.refinement_history ?? []),
1349
+ `R${request.round}: revised ${lr.revised_success_criteria.length} success criteria`,
1350
+ ];
1351
+ const updatedLo = makeLoopObjective({
1352
+ ...loopObjective,
1353
+ success_criteria: sc,
1354
+ version,
1355
+ refinement_history: history,
1356
+ });
1357
+ // Rebuild constraints to include revised criteria
1358
+ constraints = [...new Set([
1359
+ ...constraints.filter((c) => !updatedLo.hard_constraints.includes(c) && !updatedLo.success_criteria.includes(c)),
1360
+ ...updatedLo.hard_constraints,
1361
+ ...updatedLo.success_criteria,
1362
+ ])];
1363
+ loopObjective = updatedLo;
842
1364
  }
843
1365
  // ── Route to technique-specific specialist (v1.1 deep integration) ──
844
1366
  let prompt;
@@ -854,16 +1376,131 @@ export function compileL2(request, vaultContext) {
854
1376
  else {
855
1377
  prompt = compileGeneric(request, goalId, constraints, loopObjective, rollingText, analysis, referenceFile, technique);
856
1378
  }
1379
+ // ── v1.7: Inject external memory context (L2 only) ──
1380
+ const policy = getPolicy();
1381
+ if (policy.memory_injection.enabled && request.external_context) {
1382
+ prompt += formatExternalContext(request.external_context, policy.memory_injection.section_title, policy.memory_injection.max_context_length);
1383
+ }
1384
+ // ── P2: Emerged subtasks → suggested next task ──
1385
+ let suggestedNextTask = "";
1386
+ if (request.last_round_result?.emerged_subtasks?.length) {
1387
+ suggestedNextTask = request.last_round_result.emerged_subtasks
1388
+ .slice(0, 3)
1389
+ .join("; ");
1390
+ }
1391
+ // ═══════════════════════════════════════════════════════════════════════════
1392
+ // P4: Consistency validation — cross-check agent claims
1393
+ // ═══════════════════════════════════════════════════════════════════════════
1394
+ const evidence = lr?.execution_evidence;
1395
+ if (evidence) {
1396
+ // Check 1: Agent claims action but reports no files changed
1397
+ if (evidence.files_changed.length === 0 && lr.output_summary) {
1398
+ const actionWords = /\b(fix|modif|chang|updat|refactor|implement|add|remov|delet|rewrit|修补|修改|更改|更新|重构|实现|添加|删除|重写)\b/i;
1399
+ if (actionWords.test(lr.output_summary)) {
1400
+ warnings.push("P4: Agent output claims changes but execution_evidence.files_changed is empty — possible oversight");
1401
+ }
1402
+ }
1403
+ // Check 2: Agent reports success but tests failed
1404
+ if (evidence.test_results && evidence.test_results.failed > 0 && lr.success) {
1405
+ warnings.push(`P4: Agent reports success=true but ${evidence.test_results.failed} tests failed — claims may not match reality`);
1406
+ }
1407
+ // Check 3: Progress estimate mismatch with criteria tracking
1408
+ if (loopObjective && loopObjective.success_criteria.length > 0) {
1409
+ const metCount = evidence.success_criteria_met.length;
1410
+ const totalCount = loopObjective.success_criteria.length;
1411
+ const objectiveProgress = totalCount > 0 ? metCount / totalCount : 0;
1412
+ const subjectiveProgress = evidence.progress_estimate;
1413
+ const policy = getPolicy();
1414
+ const mismatchThreshold = policy.evolution?.progress_mismatch_threshold ?? 0.3;
1415
+ if (Math.abs(objectiveProgress - subjectiveProgress) > mismatchThreshold) {
1416
+ warnings.push(`P4: Progress estimate mismatch — agent estimates ${(subjectiveProgress * 100).toFixed(0)}% but ${metCount}/${totalCount} criteria met (${(objectiveProgress * 100).toFixed(0)}%)`);
1417
+ }
1418
+ }
1419
+ }
1420
+ // ═══════════════════════════════════════════════════════════════════════════
1421
+ // P4: Progress gradient — early stall detection
1422
+ // ═══════════════════════════════════════════════════════════════════════════
1423
+ if (evidence && vaultContext) {
1424
+ const prevEvidence = getPreviousRoundEvidence(request.loop_id, request.round - 1, vaultContext);
1425
+ if (prevEvidence) {
1426
+ const delta = evidence.progress_estimate - prevEvidence.progress_estimate;
1427
+ const policy = getPolicy();
1428
+ const stallThreshold = policy.evolution?.progress_stall_threshold ?? 0.05;
1429
+ const stallRounds = policy.evolution?.progress_stall_rounds ?? 2;
1430
+ if (delta < stallThreshold && evidence.progress_estimate < 0.95) {
1431
+ // Check how many consecutive rounds have been below threshold
1432
+ const stallCount = countProgressStallRounds(request.loop_id, request.round, vaultContext, stallThreshold);
1433
+ if (stallCount >= stallRounds) {
1434
+ warnings.push(`P4: Progress has stalled — <${(stallThreshold * 100).toFixed(0)}% improvement for ${stallCount} consecutive rounds. Current: ${(evidence.progress_estimate * 100).toFixed(0)}%`);
1435
+ }
1436
+ }
1437
+ }
1438
+ }
1439
+ // ═══════════════════════════════════════════════════════════════════════════
1440
+ // P4: Build Progress Dashboard for prompt injection
1441
+ // ═══════════════════════════════════════════════════════════════════════════
1442
+ let progressDashboard = "";
1443
+ if (evidence && loopObjective) {
1444
+ const totalCriteria = loopObjective.success_criteria.length;
1445
+ const metCriteria = evidence.success_criteria_met.length;
1446
+ const remainingCriteria = evidence.success_criteria_remaining;
1447
+ const objProgress = totalCriteria > 0 ? metCriteria / totalCriteria : evidence.progress_estimate;
1448
+ const subjProgress = evidence.progress_estimate;
1449
+ const lines = [
1450
+ "",
1451
+ "### Progress Dashboard",
1452
+ "",
1453
+ `**Overall**: ${metCriteria}/${totalCriteria} criteria met (${(objProgress * 100).toFixed(0)}%)`,
1454
+ ];
1455
+ if (Math.abs(objProgress - subjProgress) > 0.05) {
1456
+ lines.push(`**Agent estimate**: ${(subjProgress * 100).toFixed(0)}%`);
1457
+ }
1458
+ if (evidence.files_changed.length) {
1459
+ lines.push(`**Files changed**: ${evidence.files_changed.join(", ")}`);
1460
+ }
1461
+ if (evidence.test_results) {
1462
+ const tr = evidence.test_results;
1463
+ lines.push(`**Tests**: ${tr.passed} passed, ${tr.failed} failed, ${tr.skipped} skipped`);
1464
+ }
1465
+ if (remainingCriteria.length) {
1466
+ lines.push("**Remaining criteria**:");
1467
+ for (const rc of remainingCriteria.slice(0, 5)) {
1468
+ lines.push(`- ${rc}`);
1469
+ }
1470
+ if (remainingCriteria.length > 5) {
1471
+ lines.push(`- ... and ${remainingCriteria.length - 5} more`);
1472
+ }
1473
+ }
1474
+ lines.push("");
1475
+ // Compute trend from previous round
1476
+ if (vaultContext) {
1477
+ const prevEvidence = getPreviousRoundEvidence(request.loop_id, request.round - 1, vaultContext);
1478
+ if (prevEvidence) {
1479
+ const trend = objProgress - (prevEvidence.success_criteria_met.length / Math.max(totalCriteria, 1));
1480
+ if (trend > 0.03) {
1481
+ lines.push(`**Trend**: ↑ advancing (+${(trend * 100).toFixed(0)}% this round)`);
1482
+ }
1483
+ else if (trend > -0.03) {
1484
+ lines.push("**Trend**: → stable");
1485
+ }
1486
+ else {
1487
+ lines.push(`**Trend**: ↓ regressing (${(trend * 100).toFixed(0)}% this round)`);
1488
+ }
1489
+ lines.push("");
1490
+ }
1491
+ }
1492
+ progressDashboard = lines.join("\n");
1493
+ }
857
1494
  return makeLoopCompileResponse({
858
- status: "ok",
859
- prompt,
1495
+ status: AgentStatus.OK,
1496
+ prompt: prompt + progressDashboard,
860
1497
  recompile_level: "l2",
861
1498
  diff_from_previous: request.round === 1 || request.plan_source
862
1499
  ? "Full recompile — new goal or first call."
863
1500
  : "Full recompile — goal_id changed or strategy collapse.",
864
1501
  lineage: [`${request.loop_id}:r${request.round}`],
865
1502
  constraints_active: constraints,
866
- constraints_retired: [],
1503
+ constraints_retired: retired,
867
1504
  technique_used: technique,
868
1505
  reference_file: referenceFile,
869
1506
  rolling_summary: rollingSummary,
@@ -873,7 +1510,8 @@ export function compileL2(request, vaultContext) {
873
1510
  goal_text_hash: computeGoalTextHash(request.task),
874
1511
  loop_objective: loopObjective,
875
1512
  plan_source: request.plan_source,
876
- warnings: [],
1513
+ suggested_next_task: suggestedNextTask,
1514
+ warnings,
877
1515
  });
878
1516
  }
879
1517
  function buildHeader(ctx, technique) {
@@ -1207,7 +1845,8 @@ function compileToT(request, goalId, constraints, loopObjective, rollingText, an
1207
1845
  // ═══════════════════════════════════════════════════════════════════════════
1208
1846
  /** Build the standardized self-evaluation block appended to every compiled prompt.
1209
1847
  * The agent MUST output a JSON self-evaluation between the delimiters.
1210
- * Only 4 fields — each consumed by at least one downstream function. */
1848
+ * 4 required fields + 3 optional evolution fields (P0–P2) —
1849
+ * each consumed by at least one downstream function. */
1211
1850
  export function buildSelfEvalBlock(round) {
1212
1851
  return [
1213
1852
  "",
@@ -1222,7 +1861,20 @@ export function buildSelfEvalBlock(round) {
1222
1861
  ' "success": true,',
1223
1862
  ` "output_summary": "<one paragraph — what was DONE in round ${round}, be specific>",`,
1224
1863
  ' "constraint_violations": [],',
1225
- ' "should_continue": true',
1864
+ ' "should_continue": true,',
1865
+ ' "discovered_constraints": [],',
1866
+ ' "objective_refinement": "",',
1867
+ ' "emerged_subtasks": [],',
1868
+ ' "execution_evidence": {',
1869
+ ' "files_changed": [],',
1870
+ ' "test_results": {"passed": 0, "failed": 0, "skipped": 0},',
1871
+ ' "success_criteria_met": [],',
1872
+ ' "success_criteria_remaining": [],',
1873
+ ' "progress_estimate": 0.0',
1874
+ ' },',
1875
+ ' "retracted_constraints": [],',
1876
+ ' "revised_success_criteria": [],',
1877
+ ' "wrong_assumptions": []',
1226
1878
  "}",
1227
1879
  "---end-loopforge-eval",
1228
1880
  "```",
@@ -1238,6 +1890,23 @@ export function buildSelfEvalBlock(round) {
1238
1890
  `- should_continue: false ONLY when the ENTIRE task is complete. ` +
1239
1891
  `If there is more to audit/implement/test, say true. ` +
1240
1892
  `This tells the autonomous runner when to stop.`,
1893
+ `- discovered_constraints (P0 — optional): New constraints you discovered this round. ` +
1894
+ `Omit or [] if none.`,
1895
+ `- objective_refinement (P1 — optional): Deepened understanding of the task goal. ` +
1896
+ `APPENDED to the original objective. Omit or "" if unchanged.`,
1897
+ `- emerged_subtasks (P2 — optional): Sub-problems that surfaced during execution. ` +
1898
+ `Omit or [] if none.`,
1899
+ `- execution_evidence (P4 — recommended): Structured record of what you actually did. ` +
1900
+ `files_changed: paths relative to project root. test_results: null if no tests run. ` +
1901
+ `success_criteria_met/remaining: track against the Loop Objective. ` +
1902
+ `progress_estimate: your honest estimate of overall completion (0.0 to 1.0). ` +
1903
+ `This enables real progress tracking and early stall detection.`,
1904
+ `- retracted_constraints (P5 — optional): Constraints you now believe are WRONG. ` +
1905
+ `Removed from active guardrails. Only retract if you have evidence. Omit or [] if none.`,
1906
+ `- revised_success_criteria (P5 — optional): Success criteria that need reformulation. ` +
1907
+ `Array of {old, new} objects. Applied to the Loop Objective. Omit or [] if none.`,
1908
+ `- wrong_assumptions (P5 — optional): Assumptions from earlier rounds that turned ` +
1909
+ `out to be incorrect. Recorded as key lessons. Omit or [] if none.`,
1241
1910
  `- The JSON MUST appear between the ---loopforge-eval and ---end-loopforge-eval markers`,
1242
1911
  `- Do NOT wrap the markers in code fences — output them as raw text`,
1243
1912
  "",
@@ -1262,14 +1931,25 @@ export function compileLoop(request, vaultContext = null) {
1262
1931
  }
1263
1932
  // Layer 2: Compute advisories
1264
1933
  const { warnings, suggestedNextTask, alignment, health } = computeAdvisories(request, vaultContext);
1265
- // Merge advisories into response
1266
- response.warnings = warnings;
1267
- response.suggested_next_task = suggestedNextTask;
1934
+ // Merge advisories into response (preserve P4 consistency warnings from L2)
1935
+ response.warnings = [...(response.warnings || []), ...warnings];
1936
+ // Only overwrite suggested_next_task from advisories if the compile function
1937
+ // didn't already set one (e.g. L2 sets it from emerged_subtasks)
1938
+ if (!response.suggested_next_task) {
1939
+ response.suggested_next_task = suggestedNextTask;
1940
+ }
1268
1941
  response.task_alignment = alignment;
1269
1942
  response.loop_health = health;
1270
1943
  response.recompile_level = level;
1271
1944
  // v1.1: Append self-evaluation block for autonomous loop feedback
1272
1945
  response.prompt += buildSelfEvalBlock(request.round);
1946
+ // Multi-agent: Inject delegation history if available
1947
+ if (vaultContext) {
1948
+ const delegationText = buildDelegationSummary(vaultContext);
1949
+ if (delegationText) {
1950
+ response.prompt += delegationText;
1951
+ }
1952
+ }
1273
1953
  return response;
1274
1954
  }
1275
1955
  //# sourceMappingURL=loop-compiler.js.map