loopforge 1.3.0 → 1.7.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 (87) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +2 -0
  3. package/dist/backends/fs.d.ts +5 -0
  4. package/dist/backends/fs.d.ts.map +1 -1
  5. package/dist/backends/fs.js +67 -16
  6. package/dist/backends/fs.js.map +1 -1
  7. package/dist/builder.d.ts +0 -1
  8. package/dist/builder.d.ts.map +1 -1
  9. package/dist/builder.js +14 -25
  10. package/dist/builder.js.map +1 -1
  11. package/dist/engine.d.ts +14 -5
  12. package/dist/engine.d.ts.map +1 -1
  13. package/dist/engine.js +206 -105
  14. package/dist/engine.js.map +1 -1
  15. package/dist/index.d.ts +4 -8
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +4 -9
  18. package/dist/index.js.map +1 -1
  19. package/dist/loop-compiler.d.ts +5 -2
  20. package/dist/loop-compiler.d.ts.map +1 -1
  21. package/dist/loop-compiler.js +533 -19
  22. package/dist/loop-compiler.js.map +1 -1
  23. package/dist/mcp/session.d.ts +21 -2
  24. package/dist/mcp/session.d.ts.map +1 -1
  25. package/dist/mcp/session.js +257 -16
  26. package/dist/mcp/session.js.map +1 -1
  27. package/dist/mcp/tools.d.ts +153 -0
  28. package/dist/mcp/tools.d.ts.map +1 -1
  29. package/dist/mcp/tools.js +162 -6
  30. package/dist/mcp/tools.js.map +1 -1
  31. package/dist/observability.d.ts +23 -0
  32. package/dist/observability.d.ts.map +1 -0
  33. package/dist/observability.js +29 -0
  34. package/dist/observability.js.map +1 -0
  35. package/dist/policy.d.ts +15 -0
  36. package/dist/policy.d.ts.map +1 -1
  37. package/dist/policy.js +8 -0
  38. package/dist/policy.js.map +1 -1
  39. package/dist/protocol.d.ts +114 -5
  40. package/dist/protocol.d.ts.map +1 -1
  41. package/dist/protocol.js +44 -3
  42. package/dist/protocol.js.map +1 -1
  43. package/dist/runtime.d.ts +2 -0
  44. package/dist/runtime.d.ts.map +1 -1
  45. package/dist/runtime.js +51 -3
  46. package/dist/runtime.js.map +1 -1
  47. package/dist/tests/builder.test.js +1 -25
  48. package/dist/tests/builder.test.js.map +1 -1
  49. package/dist/tests/engine.test.js +213 -38
  50. package/dist/tests/engine.test.js.map +1 -1
  51. package/dist/tests/generate-schema.test.js +4 -4
  52. package/dist/tests/generate-schema.test.js.map +1 -1
  53. package/dist/tests/loop-compiler.test.js +212 -2
  54. package/dist/tests/loop-compiler.test.js.map +1 -1
  55. package/dist/tests/mcp.test.js +273 -0
  56. package/dist/tests/mcp.test.js.map +1 -1
  57. package/dist/tests/protocol.test.js +2 -4
  58. package/dist/tests/protocol.test.js.map +1 -1
  59. package/dist/tests/verification-gate.test.d.ts +2 -0
  60. package/dist/tests/verification-gate.test.d.ts.map +1 -0
  61. package/dist/tests/verification-gate.test.js +407 -0
  62. package/dist/tests/verification-gate.test.js.map +1 -0
  63. package/dist/verification-gate.d.ts +26 -0
  64. package/dist/verification-gate.d.ts.map +1 -0
  65. package/dist/verification-gate.js +232 -0
  66. package/dist/verification-gate.js.map +1 -0
  67. package/loop_policy.json +8 -0
  68. package/package.json +1 -2
  69. package/skills/perception/SKILL.md +102 -53
  70. package/dist/autonomous.d.ts +0 -84
  71. package/dist/autonomous.d.ts.map +0 -1
  72. package/dist/autonomous.js +0 -250
  73. package/dist/autonomous.js.map +0 -1
  74. package/dist/backends/ai.d.ts +0 -37
  75. package/dist/backends/ai.d.ts.map +0 -1
  76. package/dist/backends/ai.js +0 -98
  77. package/dist/backends/ai.js.map +0 -1
  78. package/dist/cli.d.ts +0 -7
  79. package/dist/cli.d.ts.map +0 -1
  80. package/dist/cli.js +0 -422
  81. package/dist/cli.js.map +0 -1
  82. package/dist/tests/adapter.test.js +0 -104
  83. package/dist/tests/adapter.test.js.map +0 -1
  84. package/dist/tests/autonomous.test.d.ts +0 -7
  85. package/dist/tests/autonomous.test.d.ts.map +0 -1
  86. package/dist/tests/autonomous.test.js +0 -294
  87. package/dist/tests/autonomous.test.js.map +0 -1
@@ -237,9 +237,140 @@ function computeConstraintRetirement(activeConstraints, loopId, currentRound, va
237
237
  return { active: pruned, retired };
238
238
  }
239
239
  // ═══════════════════════════════════════════════════════════════════════════
240
+ // P4: Progress evidence helpers
241
+ // ═══════════════════════════════════════════════════════════════════════════
242
+ /** Extract execution evidence from a previous round in the vault.
243
+ * Used for progress gradient calculation and trend detection. */
244
+ function getPreviousRoundEvidence(loopId, roundNum, vaultContext) {
245
+ if (vaultContext === null)
246
+ return null;
247
+ const results = vaultContext.results || [];
248
+ for (const r of results) {
249
+ const lineage = (r.loop_lineage || r.lineage || {});
250
+ if (lineage.loop_id !== loopId)
251
+ continue;
252
+ if (lineage.round !== roundNum)
253
+ continue;
254
+ const evidence = (r.execution_evidence ?? lineage.execution_evidence);
255
+ if (evidence) {
256
+ return {
257
+ progress_estimate: evidence.progress_estimate ?? 0,
258
+ success_criteria_met: evidence.success_criteria_met ?? [],
259
+ };
260
+ }
261
+ return null;
262
+ }
263
+ return null;
264
+ }
265
+ /** Count consecutive rounds (including current) where progress delta
266
+ * has been below the stall threshold. */
267
+ function countProgressStallRounds(loopId, currentRound, vaultContext, threshold) {
268
+ if (vaultContext === null || currentRound < 2)
269
+ return 0;
270
+ let count = 0;
271
+ for (let r = currentRound; r >= 2; r--) {
272
+ const curr = getPreviousRoundEvidence(loopId, r, vaultContext);
273
+ const prev = getPreviousRoundEvidence(loopId, r - 1, vaultContext);
274
+ if (curr && prev) {
275
+ const delta = curr.progress_estimate - prev.progress_estimate;
276
+ if (delta < threshold && curr.progress_estimate < 0.95) {
277
+ count++;
278
+ }
279
+ else {
280
+ break;
281
+ }
282
+ }
283
+ else {
284
+ break;
285
+ }
286
+ }
287
+ return count;
288
+ }
289
+ /** Detect repeated failure patterns in sampled rounds.
290
+ * A failure pattern = 2+ consecutive rounds where:
291
+ * 1. quality_score < 3 (failed or weak)
292
+ * 2. same technique was used
293
+ * 3. task text is similar (Jaccard > 0.4)
294
+ *
295
+ * Returns human-readable descriptions for injection into the prompt. */
296
+ function detectFailurePatterns(sampled) {
297
+ if (sampled.length < 2)
298
+ return [];
299
+ const patterns = [];
300
+ let runStart = -1;
301
+ for (let i = 0; i < sampled.length; i++) {
302
+ const r = sampled[i];
303
+ const isFailed = r.quality_score < 3;
304
+ if (isFailed && runStart === -1) {
305
+ runStart = i;
306
+ }
307
+ else if (!isFailed && runStart !== -1) {
308
+ if (i - runStart >= 2) {
309
+ const pattern = classifyRun(sampled.slice(runStart, i));
310
+ if (pattern)
311
+ patterns.push(pattern);
312
+ }
313
+ runStart = -1;
314
+ }
315
+ }
316
+ // Handle run at end of array
317
+ if (runStart !== -1 && sampled.length - runStart >= 2) {
318
+ const pattern = classifyRun(sampled.slice(runStart));
319
+ if (pattern)
320
+ patterns.push(pattern);
321
+ }
322
+ return patterns;
323
+ }
324
+ /** Classify a run of consecutive low-quality rounds as a failure pattern.
325
+ * Returns null if the rounds are too dissimilar to be the same pattern. */
326
+ function classifyRun(run) {
327
+ if (run.length < 2)
328
+ return null;
329
+ // Must all use the same technique
330
+ const technique = run[0].technique_used || "unknown";
331
+ if (!run.every((r) => (r.technique_used || "unknown") === technique)) {
332
+ return null;
333
+ }
334
+ // Task text must be similar across the run (pairwise Jaccard)
335
+ const tokensList = run.map((r) => tokenize(r.task.toLowerCase()));
336
+ for (let i = 0; i < tokensList.length - 1; i++) {
337
+ for (let j = i + 1; j < tokensList.length; j++) {
338
+ if (jaccard(tokensList[i], tokensList[j]) < 0.4)
339
+ return null;
340
+ }
341
+ }
342
+ const rounds = `R${run[0].round}-R${run[run.length - 1].round}`;
343
+ const taskPreview = run[0].task.slice(0, 80);
344
+ const avgScore = (run.reduce((s, r) => s + r.quality_score, 0) / run.length).toFixed(1);
345
+ return (`technique '${technique}' on task '${taskPreview}' ` +
346
+ `failed ${run.length} consecutive rounds (${rounds}, avg score ${avgScore}) — ` +
347
+ `consider strategy change`);
348
+ }
349
+ /** Extract round numbers referenced in failure pattern descriptions.
350
+ * Used by buildRollingSummary to identify which rounds to demote. */
351
+ function extractFailureRoundNums(patterns) {
352
+ const nums = new Set();
353
+ for (const p of patterns) {
354
+ // Match "R2-R4" or "R5"
355
+ const rangeMatch = p.match(/R(\d+)-R(\d+)/);
356
+ if (rangeMatch) {
357
+ const start = parseInt(rangeMatch[1], 10);
358
+ const end = parseInt(rangeMatch[2], 10);
359
+ for (let r = start; r <= end; r++)
360
+ nums.add(r);
361
+ }
362
+ else {
363
+ const singleMatch = p.match(/R(\d+)/);
364
+ if (singleMatch)
365
+ nums.add(parseInt(singleMatch[1], 10));
366
+ }
367
+ }
368
+ return nums;
369
+ }
370
+ // ═══════════════════════════════════════════════════════════════════════════
240
371
  // Rolling Summary (v3.5)
241
372
  // ═══════════════════════════════════════════════════════════════════════════
242
- function buildRollingSummary(loopId, currentRound, vaultContext) {
373
+ export function buildRollingSummary(loopId, currentRound, vaultContext) {
243
374
  if (vaultContext === null)
244
375
  return null;
245
376
  const results = vaultContext.results || [];
@@ -310,7 +441,7 @@ function buildRollingSummary(loopId, currentRound, vaultContext) {
310
441
  }
311
442
  }
312
443
  }
313
- const recurringIssues = [...violationCounts.entries()]
444
+ let recurringIssues = [...violationCounts.entries()]
314
445
  .filter(([, count]) => count >= 2)
315
446
  .sort((a, b) => b[1] - a[1])
316
447
  .map(([v, count]) => `${v} (appeared in ${count} rounds)`);
@@ -321,6 +452,57 @@ function buildRollingSummary(loopId, currentRound, vaultContext) {
321
452
  keyLessons.push(`[R${r.round}] ${String(r.output_summary).slice(0, 200)}`);
322
453
  }
323
454
  }
455
+ // v1.7: Failure lineage weighting — detect and demote failure patterns
456
+ const sampledRounds = sampled.map((r) => ({
457
+ round: r.round,
458
+ quality_score: r.quality_score,
459
+ task: r.task,
460
+ output_summary: r.output_summary,
461
+ constraint_violations: r.constraint_violations,
462
+ technique_used: r.technique_used,
463
+ }));
464
+ const failurePatterns = detectFailurePatterns(sampledRounds);
465
+ const failedRoundNums = extractFailureRoundNums(failurePatterns);
466
+ // Re-weight key_lessons: lessons from failure-pattern rounds go to end
467
+ if (failedRoundNums.size > 0 && keyLessons.length > 0) {
468
+ const promoted = [];
469
+ const demoted = [];
470
+ for (const kl of keyLessons) {
471
+ const rndMatch = kl.match(/^\[R(\d+)\]/);
472
+ if (rndMatch && failedRoundNums.has(parseInt(rndMatch[1], 10))) {
473
+ demoted.push(kl.replace(/^\[R\d+\]/, "$& [Consider alternatives]"));
474
+ }
475
+ else {
476
+ promoted.push(kl);
477
+ }
478
+ }
479
+ keyLessons.length = 0;
480
+ keyLessons.push(...promoted, ...demoted);
481
+ }
482
+ // Re-weight recurring_issues: mark issues that ONLY appear in failure-pattern rounds
483
+ if (failedRoundNums.size > 0 && recurringIssues.length > 0) {
484
+ // Build per-round violation sets
485
+ const failureViolations = new Set();
486
+ const successViolations = new Set();
487
+ for (const r of sampledRounds) {
488
+ const target = failedRoundNums.has(r.round) ? failureViolations : successViolations;
489
+ for (const v of r.constraint_violations) {
490
+ target.add(String(v).trim().toLowerCase());
491
+ }
492
+ }
493
+ recurringIssues.length = 0;
494
+ recurringIssues.push(...[...violationCounts.entries()]
495
+ .filter(([, count]) => count >= 2)
496
+ .sort((a, b) => b[1] - a[1])
497
+ .map(([v, count]) => {
498
+ const vNorm = v.trim().toLowerCase();
499
+ // Mark if this issue only appears in failure rounds (possible dead end)
500
+ if (failureViolations.has(vNorm) && !successViolations.has(vNorm)) {
501
+ return `${v} (appeared in ${count} rounds) ⚠️ [Possible dead end — only in failed rounds]`;
502
+ }
503
+ return `${v} (appeared in ${count} rounds)`;
504
+ }));
505
+ }
324
506
  return makeRollingSummary({
325
507
  quality_trajectory: trajectory,
326
508
  trajectory_direction: direction,
@@ -329,9 +511,10 @@ function buildRollingSummary(loopId, currentRound, vaultContext) {
329
511
  key_lessons: keyLessons,
330
512
  rounds_sampled: sampled.length,
331
513
  generated_at_round: currentRound,
514
+ failed_patterns: failurePatterns.length ? failurePatterns : [],
332
515
  });
333
516
  }
334
- function formatRollingSummaryForPrompt(rs) {
517
+ export function formatRollingSummaryForPrompt(rs) {
335
518
  if (rs === null || rs.rounds_sampled === 0)
336
519
  return "";
337
520
  const lines = [
@@ -353,6 +536,18 @@ function formatRollingSummaryForPrompt(rs) {
353
536
  lines.push(`- ⚠️ ${ri}`);
354
537
  lines.push("");
355
538
  }
539
+ // v1.7: Failure patterns — explicit warnings about repeated failure paths
540
+ if (rs.failed_patterns && rs.failed_patterns.length) {
541
+ lines.push("### ⚠️ Failure Patterns — Consider Strategy Change");
542
+ lines.push("");
543
+ lines.push("The following approaches failed repeatedly. The compiler has demoted " +
544
+ "their influence on key lessons. Consider a different technique or " +
545
+ "narrower scope.");
546
+ for (const fp of rs.failed_patterns) {
547
+ lines.push(`- 🚫 ${fp}`);
548
+ }
549
+ lines.push("");
550
+ }
356
551
  if (rs.key_lessons.length) {
357
552
  lines.push("**Key Lessons From High-Score Rounds**:");
358
553
  for (const kl of rs.key_lessons)
@@ -384,7 +579,23 @@ export function decideLevel(request, vaultContext) {
384
579
  // Gate 3: goal_id stability
385
580
  if (goalId !== prev.goal_id)
386
581
  return "l2";
387
- // Gate 4: Explicit failures or new constraints → patch
582
+ // Gate 4: P0–P5 cognitive evolution signals → full recompile (v1.7)
583
+ // Check BEFORE the simple repair/new-constraint gates because evolution signals
584
+ // (constraint discovery, objective refinement, self-correction) need full L2
585
+ // processing. Only structural evolution signals trigger L2 — execution_evidence
586
+ // alone does not (it is processed via the rolling summary at L1).
587
+ if (request.last_round_result) {
588
+ const lr = request.last_round_result;
589
+ if ((lr.discovered_constraints?.length ?? 0) > 0 ||
590
+ (lr.objective_refinement?.trim().length ?? 0) > 0 ||
591
+ (lr.emerged_subtasks?.length ?? 0) > 0 ||
592
+ (lr.retracted_constraints?.length ?? 0) > 0 ||
593
+ (lr.revised_success_criteria?.length ?? 0) > 0 ||
594
+ (lr.wrong_assumptions?.length ?? 0) > 0) {
595
+ return "l2";
596
+ }
597
+ }
598
+ // Gate 5: Explicit failures or new constraints → patch
388
599
  const hasNewConstraints = request.constraints_from_plan.length > 0;
389
600
  const hasNewFailures = request.last_round_result !== null &&
390
601
  !request.last_round_result.success;
@@ -601,7 +812,8 @@ function extractObjectiveFromPlan(planPath) {
601
812
  const goalPatterns = ["goal", "objective", "目标", "目的", "意图"];
602
813
  const successPatterns = [
603
814
  "success criteria", "acceptance criteria", "验收标准", "成功标准",
604
- "done when", "完成标准", "交付标准",
815
+ "done when", "完成标准", "交付标准", "outcome", "deliverable",
816
+ "结果", "产出", "交付物",
605
817
  ];
606
818
  const constraintPatterns = [
607
819
  "hard constraint", "constraint", "non-goal", "out of scope",
@@ -641,6 +853,12 @@ function extractObjectiveFromPlan(planPath) {
641
853
  sections.goal.push(stripped);
642
854
  }
643
855
  }
856
+ else if (stripped && currentSection === "success" && stripped.length > 10) {
857
+ // P3: Also capture free-text paragraphs under success criteria headers
858
+ if (!/^#/.test(stripped)) {
859
+ sections.success.push(stripped);
860
+ }
861
+ }
644
862
  }
645
863
  if (!Object.values(sections).some((v) => v.length))
646
864
  return null;
@@ -656,16 +874,33 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
656
874
  let objective = task.trim().slice(0, 200);
657
875
  const successCriteria = [];
658
876
  const hardConstraints = [...constraints];
877
+ // P3: Outcome-oriented default success criteria — describe the RESULT,
878
+ // not the process. The process emerges from the loop.
659
879
  if (/test|测试/i.test(task)) {
660
- successCriteria.push("All tests pass");
880
+ successCriteria.push("All tests pass with no regressions");
661
881
  }
662
882
  if (/compat|兼容/i.test(task)) {
663
- successCriteria.push("Backward compatibility maintained");
883
+ successCriteria.push("Existing API contracts and integrations unchanged");
664
884
  }
665
885
  if (/security|安全|audit/i.test(task)) {
666
- successCriteria.push("No security vulnerabilities found");
886
+ successCriteria.push("All identified vulnerabilities resolved or documented");
887
+ }
888
+ if (/fix|修复|bug/i.test(task)) {
889
+ successCriteria.push("Root cause addressed and verified, not just symptoms");
890
+ }
891
+ if (/build|create|implement|开发|实现|构建/i.test(task)) {
892
+ successCriteria.push("Deliverable matches specification and passes review");
667
893
  }
668
- // Try to extract richer objective from plan_source
894
+ if (/refactor|重构/i.test(task)) {
895
+ successCriteria.push("Behavior preserved, structure improved, no new failures");
896
+ }
897
+ if (/migrate|迁移|upgrade|升级/i.test(task)) {
898
+ successCriteria.push("Migration completes without data loss or downtime");
899
+ }
900
+ if (/perf|performance|optimize|性能|优化/i.test(task)) {
901
+ successCriteria.push("Measurable improvement verified by benchmarks");
902
+ }
903
+ // P3: Try to extract richer objective from plan_source
669
904
  if (request.plan_source) {
670
905
  const planExtracted = extractObjectiveFromPlan(request.plan_source);
671
906
  if (planExtracted) {
@@ -683,11 +918,27 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
683
918
  hardConstraints.push(`Follow plan: ${request.plan_source}`);
684
919
  }
685
920
  }
921
+ // P3: Derive domain from task content for context-aware defaults
686
922
  if (!successCriteria.length) {
687
- successCriteria.push("Task completed successfully");
923
+ if (/\.sol|solidity|contract|智能合约/i.test(task)) {
924
+ successCriteria.push("All functions respect check-effects-interactions pattern");
925
+ successCriteria.push("No unchecked external calls or reentrancy vectors");
926
+ }
927
+ else if (/\.tsx?|\.jsx?|react|component|组件/i.test(task)) {
928
+ successCriteria.push("Component renders correctly across all states (loading, empty, error)");
929
+ successCriteria.push("No prop-type or accessibility regressions");
930
+ }
931
+ else if (/api|endpoint|接口|route/i.test(task)) {
932
+ successCriteria.push("Endpoint responds correctly for all defined status codes");
933
+ successCriteria.push("Error responses follow the project's error schema");
934
+ }
935
+ else {
936
+ successCriteria.push("Task goal achieved with verifiable evidence");
937
+ }
688
938
  }
689
939
  if (!hardConstraints.length) {
690
- hardConstraints.push("Do not modify files outside scope");
940
+ hardConstraints.push("Do not modify files outside the stated scope");
941
+ hardConstraints.push("Preserve existing test coverage — no test deletions without replacement");
691
942
  }
692
943
  return makeLoopObjective({
693
944
  objective,
@@ -695,6 +946,8 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
695
946
  hard_constraints: hardConstraints,
696
947
  created_at_round: 1,
697
948
  loop_id: request.loop_id,
949
+ version: 1,
950
+ refinement_history: [],
698
951
  });
699
952
  }
700
953
  // ═══════════════════════════════════════════════════════════════════════════
@@ -825,8 +1078,19 @@ export function compileL2(request, vaultContext) {
825
1078
  const referenceFile = analysis.reference_file;
826
1079
  // v3.5: Rolling summary
827
1080
  const rollingSummary = buildRollingSummary(request.loop_id, request.round, vaultContext);
1081
+ // P5c: Wrong assumptions — must be applied BEFORE formatting so they appear
1082
+ // in the rendered cross-round summary text injected into the prompt.
1083
+ const lr = request.last_round_result;
1084
+ if (lr?.wrong_assumptions?.length) {
1085
+ if (rollingSummary) {
1086
+ rollingSummary.key_lessons = [
1087
+ ...rollingSummary.key_lessons,
1088
+ ...lr.wrong_assumptions.map((a) => `Wrong assumption corrected: ${a}`),
1089
+ ];
1090
+ }
1091
+ }
828
1092
  const rollingText = formatRollingSummaryForPrompt(rollingSummary);
829
- // Generate loop objective at round 1 if not provided
1093
+ // ── Loop Objective: create at round 1, load from vault at later rounds ──
830
1094
  let loopObjective = null;
831
1095
  if (request.round === 1) {
832
1096
  if (request.loop_objective) {
@@ -836,9 +1100,113 @@ export function compileL2(request, vaultContext) {
836
1100
  loopObjective = computeLoopObjectiveFromTask(request, vaultContext);
837
1101
  }
838
1102
  }
1103
+ else {
1104
+ // P1 fix: Load loop objective from vault for round > 1
1105
+ const vaultLo = vaultGetLoopObjective(request.loop_id, vaultContext);
1106
+ if (vaultLo) {
1107
+ const loData = vaultLo.loop_objective;
1108
+ if (loData) {
1109
+ loopObjective = makeLoopObjective({
1110
+ objective: loData.objective ?? "",
1111
+ success_criteria: loData.success_criteria ?? [],
1112
+ hard_constraints: loData.hard_constraints ?? [],
1113
+ created_at_round: loData.created_at_round ?? 1,
1114
+ loop_id: loData.loop_id ?? request.loop_id,
1115
+ version: loData.version ?? 1,
1116
+ refinement_history: loData.refinement_history ?? [],
1117
+ });
1118
+ }
1119
+ }
1120
+ if (!loopObjective && request.loop_objective) {
1121
+ loopObjective = request.loop_objective;
1122
+ }
1123
+ }
1124
+ // ── P1: Apply objective_refinement from last round ──
1125
+ if (loopObjective && request.last_round_result?.objective_refinement) {
1126
+ const refinement = request.last_round_result.objective_refinement.trim();
1127
+ if (refinement) {
1128
+ const version = (loopObjective.version ?? 1) + 1;
1129
+ const history = [...(loopObjective.refinement_history ?? []), refinement];
1130
+ // Enforce max versions from policy
1131
+ const policy = getPolicy();
1132
+ const maxVersions = policy.evolution?.max_objective_versions ?? 10;
1133
+ const prunedHistory = history.slice(-maxVersions);
1134
+ const refinedObjective = `${loopObjective.objective} [R${request.round}: ${refinement}]`;
1135
+ loopObjective = makeLoopObjective({
1136
+ ...loopObjective,
1137
+ objective: refinedObjective,
1138
+ version,
1139
+ refinement_history: prunedHistory,
1140
+ });
1141
+ }
1142
+ }
1143
+ // ── Build active constraints (base + LO + P0 discovered) ──
839
1144
  let constraints = [...request.constraints_from_plan];
840
1145
  if (loopObjective) {
841
- constraints = [...new Set([...constraints, ...loopObjective.hard_constraints])];
1146
+ constraints = [
1147
+ ...new Set([
1148
+ ...constraints,
1149
+ ...loopObjective.hard_constraints,
1150
+ ...loopObjective.success_criteria,
1151
+ ]),
1152
+ ];
1153
+ }
1154
+ // P0: Merge discovered constraints from last round
1155
+ if (request.last_round_result?.discovered_constraints?.length) {
1156
+ const policy = getPolicy();
1157
+ const maxPerRound = policy.evolution?.max_discovered_constraints_per_round ?? 5;
1158
+ const maxActive = policy.evolution?.max_active_constraints ?? 15;
1159
+ const newConstraints = request.last_round_result.discovered_constraints
1160
+ .slice(0, maxPerRound)
1161
+ .filter((c) => !constraints.includes(c));
1162
+ constraints = [...constraints, ...newConstraints].slice(0, maxActive);
1163
+ }
1164
+ // ═══════════════════════════════════════════════════════════════════════════
1165
+ // P5: Self-correction — MUST process retractions/revisions BEFORE prompt build
1166
+ // so the technician compiler sees the corrected constraints and loop objective.
1167
+ // ═══════════════════════════════════════════════════════════════════════════
1168
+ const warnings = [];
1169
+ const retired = [];
1170
+ // P5a: Retracted constraints — remove from active set
1171
+ if (lr?.retracted_constraints?.length) {
1172
+ for (const rc of lr.retracted_constraints) {
1173
+ const idx = constraints.indexOf(rc);
1174
+ if (idx >= 0) {
1175
+ constraints.splice(idx, 1);
1176
+ retired.push(`[agent-retracted] ${rc}`);
1177
+ }
1178
+ }
1179
+ }
1180
+ // P5b: Revised success criteria — update Loop Objective
1181
+ if (lr?.revised_success_criteria?.length && loopObjective) {
1182
+ const sc = [...loopObjective.success_criteria];
1183
+ for (const rev of lr.revised_success_criteria) {
1184
+ const idx = sc.indexOf(rev.old);
1185
+ if (idx >= 0) {
1186
+ sc[idx] = rev.new;
1187
+ }
1188
+ else {
1189
+ sc.push(rev.new);
1190
+ }
1191
+ }
1192
+ const version = (loopObjective.version ?? 1) + 1;
1193
+ const history = [
1194
+ ...(loopObjective.refinement_history ?? []),
1195
+ `R${request.round}: revised ${lr.revised_success_criteria.length} success criteria`,
1196
+ ];
1197
+ const updatedLo = makeLoopObjective({
1198
+ ...loopObjective,
1199
+ success_criteria: sc,
1200
+ version,
1201
+ refinement_history: history,
1202
+ });
1203
+ // Rebuild constraints to include revised criteria
1204
+ constraints = [...new Set([
1205
+ ...constraints.filter((c) => !updatedLo.hard_constraints.includes(c) && !updatedLo.success_criteria.includes(c)),
1206
+ ...updatedLo.hard_constraints,
1207
+ ...updatedLo.success_criteria,
1208
+ ])];
1209
+ loopObjective = updatedLo;
842
1210
  }
843
1211
  // ── Route to technique-specific specialist (v1.1 deep integration) ──
844
1212
  let prompt;
@@ -854,16 +1222,126 @@ export function compileL2(request, vaultContext) {
854
1222
  else {
855
1223
  prompt = compileGeneric(request, goalId, constraints, loopObjective, rollingText, analysis, referenceFile, technique);
856
1224
  }
1225
+ // ── P2: Emerged subtasks → suggested next task ──
1226
+ let suggestedNextTask = "";
1227
+ if (request.last_round_result?.emerged_subtasks?.length) {
1228
+ suggestedNextTask = request.last_round_result.emerged_subtasks
1229
+ .slice(0, 3)
1230
+ .join("; ");
1231
+ }
1232
+ // ═══════════════════════════════════════════════════════════════════════════
1233
+ // P4: Consistency validation — cross-check agent claims
1234
+ // ═══════════════════════════════════════════════════════════════════════════
1235
+ const evidence = lr?.execution_evidence;
1236
+ if (evidence) {
1237
+ // Check 1: Agent claims action but reports no files changed
1238
+ if (evidence.files_changed.length === 0 && lr.output_summary) {
1239
+ const actionWords = /\b(fix|modif|chang|updat|refactor|implement|add|remov|delet|rewrit|修补|修改|更改|更新|重构|实现|添加|删除|重写)\b/i;
1240
+ if (actionWords.test(lr.output_summary)) {
1241
+ warnings.push("P4: Agent output claims changes but execution_evidence.files_changed is empty — possible oversight");
1242
+ }
1243
+ }
1244
+ // Check 2: Agent reports success but tests failed
1245
+ if (evidence.test_results && evidence.test_results.failed > 0 && lr.success) {
1246
+ warnings.push(`P4: Agent reports success=true but ${evidence.test_results.failed} tests failed — claims may not match reality`);
1247
+ }
1248
+ // Check 3: Progress estimate mismatch with criteria tracking
1249
+ if (loopObjective && loopObjective.success_criteria.length > 0) {
1250
+ const metCount = evidence.success_criteria_met.length;
1251
+ const totalCount = loopObjective.success_criteria.length;
1252
+ const objectiveProgress = totalCount > 0 ? metCount / totalCount : 0;
1253
+ const subjectiveProgress = evidence.progress_estimate;
1254
+ const policy = getPolicy();
1255
+ const mismatchThreshold = policy.evolution?.progress_mismatch_threshold ?? 0.3;
1256
+ if (Math.abs(objectiveProgress - subjectiveProgress) > mismatchThreshold) {
1257
+ warnings.push(`P4: Progress estimate mismatch — agent estimates ${(subjectiveProgress * 100).toFixed(0)}% but ${metCount}/${totalCount} criteria met (${(objectiveProgress * 100).toFixed(0)}%)`);
1258
+ }
1259
+ }
1260
+ }
1261
+ // ═══════════════════════════════════════════════════════════════════════════
1262
+ // P4: Progress gradient — early stall detection
1263
+ // ═══════════════════════════════════════════════════════════════════════════
1264
+ if (evidence && vaultContext) {
1265
+ const prevEvidence = getPreviousRoundEvidence(request.loop_id, request.round - 1, vaultContext);
1266
+ if (prevEvidence) {
1267
+ const delta = evidence.progress_estimate - prevEvidence.progress_estimate;
1268
+ const policy = getPolicy();
1269
+ const stallThreshold = policy.evolution?.progress_stall_threshold ?? 0.05;
1270
+ const stallRounds = policy.evolution?.progress_stall_rounds ?? 2;
1271
+ if (delta < stallThreshold && evidence.progress_estimate < 0.95) {
1272
+ // Check how many consecutive rounds have been below threshold
1273
+ const stallCount = countProgressStallRounds(request.loop_id, request.round, vaultContext, stallThreshold);
1274
+ if (stallCount >= stallRounds) {
1275
+ warnings.push(`P4: Progress has stalled — <${(stallThreshold * 100).toFixed(0)}% improvement for ${stallCount} consecutive rounds. Current: ${(evidence.progress_estimate * 100).toFixed(0)}%`);
1276
+ }
1277
+ }
1278
+ }
1279
+ }
1280
+ // ═══════════════════════════════════════════════════════════════════════════
1281
+ // P4: Build Progress Dashboard for prompt injection
1282
+ // ═══════════════════════════════════════════════════════════════════════════
1283
+ let progressDashboard = "";
1284
+ if (evidence && loopObjective) {
1285
+ const totalCriteria = loopObjective.success_criteria.length;
1286
+ const metCriteria = evidence.success_criteria_met.length;
1287
+ const remainingCriteria = evidence.success_criteria_remaining;
1288
+ const objProgress = totalCriteria > 0 ? metCriteria / totalCriteria : evidence.progress_estimate;
1289
+ const subjProgress = evidence.progress_estimate;
1290
+ const lines = [
1291
+ "",
1292
+ "### Progress Dashboard",
1293
+ "",
1294
+ `**Overall**: ${metCriteria}/${totalCriteria} criteria met (${(objProgress * 100).toFixed(0)}%)`,
1295
+ ];
1296
+ if (Math.abs(objProgress - subjProgress) > 0.05) {
1297
+ lines.push(`**Agent estimate**: ${(subjProgress * 100).toFixed(0)}%`);
1298
+ }
1299
+ if (evidence.files_changed.length) {
1300
+ lines.push(`**Files changed**: ${evidence.files_changed.join(", ")}`);
1301
+ }
1302
+ if (evidence.test_results) {
1303
+ const tr = evidence.test_results;
1304
+ lines.push(`**Tests**: ${tr.passed} passed, ${tr.failed} failed, ${tr.skipped} skipped`);
1305
+ }
1306
+ if (remainingCriteria.length) {
1307
+ lines.push("**Remaining criteria**:");
1308
+ for (const rc of remainingCriteria.slice(0, 5)) {
1309
+ lines.push(`- ${rc}`);
1310
+ }
1311
+ if (remainingCriteria.length > 5) {
1312
+ lines.push(`- ... and ${remainingCriteria.length - 5} more`);
1313
+ }
1314
+ }
1315
+ lines.push("");
1316
+ // Compute trend from previous round
1317
+ if (vaultContext) {
1318
+ const prevEvidence = getPreviousRoundEvidence(request.loop_id, request.round - 1, vaultContext);
1319
+ if (prevEvidence) {
1320
+ const trend = objProgress - (prevEvidence.success_criteria_met.length / Math.max(totalCriteria, 1));
1321
+ if (trend > 0.03) {
1322
+ lines.push(`**Trend**: ↑ advancing (+${(trend * 100).toFixed(0)}% this round)`);
1323
+ }
1324
+ else if (trend > -0.03) {
1325
+ lines.push("**Trend**: → stable");
1326
+ }
1327
+ else {
1328
+ lines.push(`**Trend**: ↓ regressing (${(trend * 100).toFixed(0)}% this round)`);
1329
+ }
1330
+ lines.push("");
1331
+ }
1332
+ }
1333
+ progressDashboard = lines.join("\n");
1334
+ }
857
1335
  return makeLoopCompileResponse({
858
1336
  status: "ok",
859
- prompt,
1337
+ prompt: prompt + progressDashboard,
860
1338
  recompile_level: "l2",
861
1339
  diff_from_previous: request.round === 1 || request.plan_source
862
1340
  ? "Full recompile — new goal or first call."
863
1341
  : "Full recompile — goal_id changed or strategy collapse.",
864
1342
  lineage: [`${request.loop_id}:r${request.round}`],
865
1343
  constraints_active: constraints,
866
- constraints_retired: [],
1344
+ constraints_retired: retired,
867
1345
  technique_used: technique,
868
1346
  reference_file: referenceFile,
869
1347
  rolling_summary: rollingSummary,
@@ -873,7 +1351,8 @@ export function compileL2(request, vaultContext) {
873
1351
  goal_text_hash: computeGoalTextHash(request.task),
874
1352
  loop_objective: loopObjective,
875
1353
  plan_source: request.plan_source,
876
- warnings: [],
1354
+ suggested_next_task: suggestedNextTask,
1355
+ warnings,
877
1356
  });
878
1357
  }
879
1358
  function buildHeader(ctx, technique) {
@@ -1207,7 +1686,8 @@ function compileToT(request, goalId, constraints, loopObjective, rollingText, an
1207
1686
  // ═══════════════════════════════════════════════════════════════════════════
1208
1687
  /** Build the standardized self-evaluation block appended to every compiled prompt.
1209
1688
  * The agent MUST output a JSON self-evaluation between the delimiters.
1210
- * Only 4 fields — each consumed by at least one downstream function. */
1689
+ * 4 required fields + 3 optional evolution fields (P0–P2) —
1690
+ * each consumed by at least one downstream function. */
1211
1691
  export function buildSelfEvalBlock(round) {
1212
1692
  return [
1213
1693
  "",
@@ -1222,7 +1702,20 @@ export function buildSelfEvalBlock(round) {
1222
1702
  ' "success": true,',
1223
1703
  ` "output_summary": "<one paragraph — what was DONE in round ${round}, be specific>",`,
1224
1704
  ' "constraint_violations": [],',
1225
- ' "should_continue": true',
1705
+ ' "should_continue": true,',
1706
+ ' "discovered_constraints": [],',
1707
+ ' "objective_refinement": "",',
1708
+ ' "emerged_subtasks": [],',
1709
+ ' "execution_evidence": {',
1710
+ ' "files_changed": [],',
1711
+ ' "test_results": {"passed": 0, "failed": 0, "skipped": 0},',
1712
+ ' "success_criteria_met": [],',
1713
+ ' "success_criteria_remaining": [],',
1714
+ ' "progress_estimate": 0.0',
1715
+ ' },',
1716
+ ' "retracted_constraints": [],',
1717
+ ' "revised_success_criteria": [],',
1718
+ ' "wrong_assumptions": []',
1226
1719
  "}",
1227
1720
  "---end-loopforge-eval",
1228
1721
  "```",
@@ -1238,6 +1731,23 @@ export function buildSelfEvalBlock(round) {
1238
1731
  `- should_continue: false ONLY when the ENTIRE task is complete. ` +
1239
1732
  `If there is more to audit/implement/test, say true. ` +
1240
1733
  `This tells the autonomous runner when to stop.`,
1734
+ `- discovered_constraints (P0 — optional): New constraints you discovered this round. ` +
1735
+ `Omit or [] if none.`,
1736
+ `- objective_refinement (P1 — optional): Deepened understanding of the task goal. ` +
1737
+ `APPENDED to the original objective. Omit or "" if unchanged.`,
1738
+ `- emerged_subtasks (P2 — optional): Sub-problems that surfaced during execution. ` +
1739
+ `Omit or [] if none.`,
1740
+ `- execution_evidence (P4 — recommended): Structured record of what you actually did. ` +
1741
+ `files_changed: paths relative to project root. test_results: null if no tests run. ` +
1742
+ `success_criteria_met/remaining: track against the Loop Objective. ` +
1743
+ `progress_estimate: your honest estimate of overall completion (0.0 to 1.0). ` +
1744
+ `This enables real progress tracking and early stall detection.`,
1745
+ `- retracted_constraints (P5 — optional): Constraints you now believe are WRONG. ` +
1746
+ `Removed from active guardrails. Only retract if you have evidence. Omit or [] if none.`,
1747
+ `- revised_success_criteria (P5 — optional): Success criteria that need reformulation. ` +
1748
+ `Array of {old, new} objects. Applied to the Loop Objective. Omit or [] if none.`,
1749
+ `- wrong_assumptions (P5 — optional): Assumptions from earlier rounds that turned ` +
1750
+ `out to be incorrect. Recorded as key lessons. Omit or [] if none.`,
1241
1751
  `- The JSON MUST appear between the ---loopforge-eval and ---end-loopforge-eval markers`,
1242
1752
  `- Do NOT wrap the markers in code fences — output them as raw text`,
1243
1753
  "",
@@ -1264,7 +1774,11 @@ export function compileLoop(request, vaultContext = null) {
1264
1774
  const { warnings, suggestedNextTask, alignment, health } = computeAdvisories(request, vaultContext);
1265
1775
  // Merge advisories into response
1266
1776
  response.warnings = warnings;
1267
- response.suggested_next_task = suggestedNextTask;
1777
+ // Only overwrite suggested_next_task from advisories if the compile function
1778
+ // didn't already set one (e.g. L2 sets it from emerged_subtasks)
1779
+ if (!response.suggested_next_task) {
1780
+ response.suggested_next_task = suggestedNextTask;
1781
+ }
1268
1782
  response.task_alignment = alignment;
1269
1783
  response.loop_health = health;
1270
1784
  response.recompile_level = level;