loopforge 1.8.0 → 1.12.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 (71) hide show
  1. package/dist/backends/fs.d.ts.map +1 -1
  2. package/dist/backends/fs.js +1 -2
  3. package/dist/backends/fs.js.map +1 -1
  4. package/dist/backends/interface.d.ts +2 -0
  5. package/dist/backends/interface.d.ts.map +1 -1
  6. package/dist/builder.d.ts +6 -10
  7. package/dist/builder.d.ts.map +1 -1
  8. package/dist/builder.js +128 -63
  9. package/dist/builder.js.map +1 -1
  10. package/dist/engine.d.ts +2 -2
  11. package/dist/engine.d.ts.map +1 -1
  12. package/dist/engine.js +40 -53
  13. package/dist/engine.js.map +1 -1
  14. package/dist/index.d.ts +2 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/loop-compiler.d.ts +10 -2
  19. package/dist/loop-compiler.d.ts.map +1 -1
  20. package/dist/loop-compiler.js +207 -117
  21. package/dist/loop-compiler.js.map +1 -1
  22. package/dist/mcp/session.d.ts +3 -1
  23. package/dist/mcp/session.d.ts.map +1 -1
  24. package/dist/mcp/session.js +27 -20
  25. package/dist/mcp/session.js.map +1 -1
  26. package/dist/mcp/tools.js +4 -4
  27. package/dist/memory-bridge.js +1 -1
  28. package/dist/memory-bridge.js.map +1 -1
  29. package/dist/observability.d.ts +1 -1
  30. package/dist/observability.js +1 -1
  31. package/dist/policy.d.ts +9 -9
  32. package/dist/policy.d.ts.map +1 -1
  33. package/dist/policy.js +5 -28
  34. package/dist/policy.js.map +1 -1
  35. package/dist/protocol.d.ts +38 -15
  36. package/dist/protocol.d.ts.map +1 -1
  37. package/dist/protocol.js +19 -7
  38. package/dist/protocol.js.map +1 -1
  39. package/dist/replay.js +7 -7
  40. package/dist/replay.js.map +1 -1
  41. package/dist/runtime.d.ts +3 -3
  42. package/dist/runtime.d.ts.map +1 -1
  43. package/dist/runtime.js +27 -40
  44. package/dist/runtime.js.map +1 -1
  45. package/dist/tests/builder.test.js +40 -28
  46. package/dist/tests/builder.test.js.map +1 -1
  47. package/dist/tests/engine.test.js +13 -17
  48. package/dist/tests/engine.test.js.map +1 -1
  49. package/dist/tests/generate-schema.test.js +10 -7
  50. package/dist/tests/generate-schema.test.js.map +1 -1
  51. package/dist/tests/loop-compiler.test.js +245 -27
  52. package/dist/tests/loop-compiler.test.js.map +1 -1
  53. package/dist/tests/mcp.test.js +9 -9
  54. package/dist/tests/mcp.test.js.map +1 -1
  55. package/dist/tests/memory-bridge.test.js +0 -1
  56. package/dist/tests/memory-bridge.test.js.map +1 -1
  57. package/dist/tests/policy.test.js +5 -15
  58. package/dist/tests/policy.test.js.map +1 -1
  59. package/dist/tests/protocol.test.js +3 -2
  60. package/dist/tests/protocol.test.js.map +1 -1
  61. package/dist/tests/replay.test.js +9 -9
  62. package/dist/tests/replay.test.js.map +1 -1
  63. package/dist/tests/runtime.test.js +8 -8
  64. package/dist/tests/runtime.test.js.map +1 -1
  65. package/dist/tests/verification-gate.test.js +1 -1
  66. package/dist/tests/verification-gate.test.js.map +1 -1
  67. package/dist/verification-gate.d.ts +2 -2
  68. package/dist/verification-gate.js +2 -2
  69. package/loop_policy.json +5 -23
  70. package/package.json +1 -1
  71. package/skills/perception/SKILL.md +12 -11
@@ -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 { AgentStatus, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeRollingSummary, makeTaskAlignment, } from "./protocol.js";
15
+ import { AgentStatus, makeCheckpointSummary, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeRollingSummary, makeTaskAlignment, } from "./protocol.js";
16
16
  import { routeTechniqueAdaptive } from "./builder.js";
17
17
  // ═══════════════════════════════════════════════════════════════════════════
18
18
  // Repair cue detection
@@ -224,8 +224,7 @@ export function getPreviousRound(loopId, roundNum, vaultContext) {
224
224
  return {
225
225
  goal_id: lineage.goal_id ?? "",
226
226
  goal_text_hash: lineage.goal_text_hash ?? "",
227
- quality_score: lineage.quality_score ?? 0,
228
- success: r.success ?? true,
227
+ success: r.success ?? lineage.success ?? true,
229
228
  task: r.task ?? r.user_intent ?? "",
230
229
  constraints_active: lineage.constraints_active ?? [],
231
230
  prompt_text: r.full_prompt ?? "",
@@ -243,7 +242,7 @@ function getRecentRounds(loopId, n, vaultContext) {
243
242
  const lineage = (r.loop_lineage || r.lineage || {});
244
243
  if (lineage.loop_id === loopId) {
245
244
  rounds.push({
246
- quality_score: lineage.quality_score ?? 0,
245
+ success: r.success ?? lineage.success ?? false,
247
246
  round: lineage.round ?? 0,
248
247
  goal_text_hash: lineage.goal_text_hash ?? "",
249
248
  });
@@ -303,7 +302,7 @@ function strategyCollapse(loopId, vaultContext) {
303
302
  const recent = getRecentRounds(loopId, 3, vaultContext);
304
303
  if (recent.length < 3)
305
304
  return false;
306
- return recent.every((r) => r.quality_score < 3);
305
+ return recent.every((r) => r.success === false);
307
306
  }
308
307
  // ═══════════════════════════════════════════════════════════════════════════
309
308
  // Constraint Retirement (v3.5)
@@ -412,7 +411,7 @@ function countProgressStallRounds(loopId, currentRound, vaultContext, threshold)
412
411
  }
413
412
  /** Detect repeated failure patterns in sampled rounds.
414
413
  * A failure pattern = 2+ consecutive rounds where:
415
- * 1. quality_score < 3 (failed or weak)
414
+ * 1. success === false (failed)
416
415
  * 2. same technique was used
417
416
  * 3. task text is similar (Jaccard > 0.4)
418
417
  *
@@ -424,7 +423,7 @@ function detectFailurePatterns(sampled) {
424
423
  let runStart = -1;
425
424
  for (let i = 0; i < sampled.length; i++) {
426
425
  const r = sampled[i];
427
- const isFailed = r.quality_score < 3;
426
+ const isFailed = r.success === false;
428
427
  if (isFailed && runStart === -1) {
429
428
  runStart = i;
430
429
  }
@@ -445,7 +444,7 @@ function detectFailurePatterns(sampled) {
445
444
  }
446
445
  return patterns;
447
446
  }
448
- /** Classify a run of consecutive low-quality rounds as a failure pattern.
447
+ /** Classify a run of consecutive failed rounds as a failure pattern.
449
448
  * Returns null if the rounds are too dissimilar to be the same pattern. */
450
449
  function classifyRun(run) {
451
450
  if (run.length < 2)
@@ -465,9 +464,9 @@ function classifyRun(run) {
465
464
  }
466
465
  const rounds = `R${run[0].round}-R${run[run.length - 1].round}`;
467
466
  const taskPreview = run[0].task.slice(0, 80);
468
- const avgScore = (run.reduce((s, r) => s + r.quality_score, 0) / run.length).toFixed(1);
467
+ const failureRate = `${run.length}/${run.length}`;
469
468
  return (`technique '${technique}' on task '${taskPreview}' ` +
470
- `failed ${run.length} consecutive rounds (${rounds}, avg score ${avgScore}) — ` +
469
+ `failed ${run.length} consecutive rounds (${rounds}) — ` +
471
470
  `consider strategy change`);
472
471
  }
473
472
  /** Extract round numbers referenced in failure pattern descriptions.
@@ -494,7 +493,7 @@ function extractFailureRoundNums(patterns) {
494
493
  // ═══════════════════════════════════════════════════════════════════════════
495
494
  // Rolling Summary (v3.5)
496
495
  // ═══════════════════════════════════════════════════════════════════════════
497
- export function buildRollingSummary(loopId, currentRound, vaultContext) {
496
+ export function buildRollingSummary(loopId, currentRound, vaultContext, sinceRound) {
498
497
  if (vaultContext === null)
499
498
  return null;
500
499
  const results = vaultContext.results || [];
@@ -502,6 +501,7 @@ export function buildRollingSummary(loopId, currentRound, vaultContext) {
502
501
  return null;
503
502
  const policy = getPolicy();
504
503
  const window = policy.summary.window;
504
+ const minRound = sinceRound ?? 0;
505
505
  // Collect rounds matching this loop_id, excluding current
506
506
  const rounds = [];
507
507
  for (const r of results) {
@@ -511,9 +511,11 @@ export function buildRollingSummary(loopId, currentRound, vaultContext) {
511
511
  const rnd = lineage.round;
512
512
  if (rnd >= currentRound)
513
513
  continue;
514
+ if (rnd < minRound)
515
+ continue;
514
516
  rounds.push({
515
517
  round: rnd,
516
- quality_score: lineage.quality_score ?? 0,
518
+ success: r.success ?? lineage.success ?? false,
517
519
  task: r.task ?? r.user_intent ?? "",
518
520
  output_summary: r.output_summary ?? "",
519
521
  constraint_violations: r.constraint_violations || [],
@@ -525,32 +527,13 @@ export function buildRollingSummary(loopId, currentRound, vaultContext) {
525
527
  // Sort by round descending, take last N
526
528
  rounds.sort((a, b) => b.round - a.round);
527
529
  const sampled = rounds.slice(0, window).reverse(); // chronological order
528
- // Quality trajectory
529
- const trajectory = sampled.map((r) => r.quality_score);
530
- // Trajectory direction
531
- let direction = "stable";
532
- if (trajectory.length >= 2) {
533
- const diffs = trajectory.slice(1).map((v, i) => v - trajectory[i]);
534
- if (diffs.every((d) => d >= 0) && diffs.some((d) => d > 0)) {
535
- direction = "improving";
536
- }
537
- else if (diffs.every((d) => d <= 0) && diffs.some((d) => d < 0)) {
538
- direction = "declining";
539
- }
540
- else if (diffs.every((d) => d === 0)) {
541
- direction = "stable";
542
- }
543
- else {
544
- direction = "volatile";
545
- }
546
- }
547
- // What worked
548
- const whatWorked = [];
530
+ // v1.12: Key outcomes — all rounds with output_summary
531
+ const keyOutcomes = [];
549
532
  for (const r of sampled) {
550
- if (r.quality_score >= 4 &&
551
- r.output_summary) {
552
- whatWorked.push(`R${r.round} (score=${r.quality_score}, ` +
553
- `${r.technique_used || "n/a"}): ${String(r.output_summary).slice(0, 150)}`);
533
+ if (r.output_summary) {
534
+ const status = r.success ? "✓" : "✗";
535
+ keyOutcomes.push(`[R${r.round}] ${status} (${r.technique_used || "n/a"}): ` +
536
+ `${String(r.output_summary).slice(0, 200)}`);
554
537
  }
555
538
  }
556
539
  // Recurring issues
@@ -569,17 +552,10 @@ export function buildRollingSummary(loopId, currentRound, vaultContext) {
569
552
  .filter(([, count]) => count >= 2)
570
553
  .sort((a, b) => b[1] - a[1])
571
554
  .map(([v, count]) => `${v} (appeared in ${count} rounds)`);
572
- // Key lessons
573
- const keyLessons = [];
574
- for (const r of sampled) {
575
- if (r.quality_score >= 4 && r.output_summary) {
576
- keyLessons.push(`[R${r.round}] ${String(r.output_summary).slice(0, 200)}`);
577
- }
578
- }
579
555
  // v1.7: Failure lineage weighting — detect and demote failure patterns
580
556
  const sampledRounds = sampled.map((r) => ({
581
557
  round: r.round,
582
- quality_score: r.quality_score,
558
+ success: r.success,
583
559
  task: r.task,
584
560
  output_summary: r.output_summary,
585
561
  constraint_violations: r.constraint_violations,
@@ -587,21 +563,21 @@ export function buildRollingSummary(loopId, currentRound, vaultContext) {
587
563
  }));
588
564
  const failurePatterns = detectFailurePatterns(sampledRounds);
589
565
  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) {
566
+ // Re-weight key_outcomes: outcomes from failure-pattern rounds go to end
567
+ if (failedRoundNums.size > 0 && keyOutcomes.length > 0) {
592
568
  const promoted = [];
593
569
  const demoted = [];
594
- for (const kl of keyLessons) {
595
- const rndMatch = kl.match(/^\[R(\d+)\]/);
570
+ for (const ko of keyOutcomes) {
571
+ const rndMatch = ko.match(/^\[R(\d+)\]/);
596
572
  if (rndMatch && failedRoundNums.has(parseInt(rndMatch[1], 10))) {
597
- demoted.push(kl.replace(/^\[R\d+\]/, "$& [Consider alternatives]"));
573
+ demoted.push(ko.replace(/^\[R\d+\]/, "$& [Consider alternatives]"));
598
574
  }
599
575
  else {
600
- promoted.push(kl);
576
+ promoted.push(ko);
601
577
  }
602
578
  }
603
- keyLessons.length = 0;
604
- keyLessons.push(...promoted, ...demoted);
579
+ keyOutcomes.length = 0;
580
+ keyOutcomes.push(...promoted, ...demoted);
605
581
  }
606
582
  // Re-weight recurring_issues: mark issues that ONLY appear in failure-pattern rounds
607
583
  if (failedRoundNums.size > 0 && recurringIssues.length > 0) {
@@ -628,11 +604,8 @@ export function buildRollingSummary(loopId, currentRound, vaultContext) {
628
604
  }));
629
605
  }
630
606
  return makeRollingSummary({
631
- quality_trajectory: trajectory,
632
- trajectory_direction: direction,
633
- what_worked: whatWorked,
607
+ key_outcomes: keyOutcomes,
634
608
  recurring_issues: recurringIssues,
635
- key_lessons: keyLessons,
636
609
  rounds_sampled: sampled.length,
637
610
  generated_at_round: currentRound,
638
611
  failed_patterns: failurePatterns.length ? failurePatterns : [],
@@ -644,14 +617,13 @@ export function formatRollingSummaryForPrompt(rs) {
644
617
  const lines = [
645
618
  "### Cross-Round Summary (Accumulated)",
646
619
  "",
647
- `**Sampled**: ${rs.rounds_sampled} prior rounds | **Direction**: ${rs.trajectory_direction}`,
648
- `**Quality Trajectory**: [${rs.quality_trajectory.join(", ")}]`,
620
+ `**Sampled**: ${rs.rounds_sampled} prior rounds`,
649
621
  "",
650
622
  ];
651
- if (rs.what_worked.length) {
652
- lines.push("**What Worked (score >= 4)**:");
653
- for (const w of rs.what_worked)
654
- lines.push(`- ${w}`);
623
+ if (rs.key_outcomes.length) {
624
+ lines.push("**Key Outcomes**:");
625
+ for (const ko of rs.key_outcomes)
626
+ lines.push(`- ${ko}`);
655
627
  lines.push("");
656
628
  }
657
629
  if (rs.recurring_issues.length) {
@@ -665,21 +637,107 @@ export function formatRollingSummaryForPrompt(rs) {
665
637
  lines.push("### ⚠️ Failure Patterns — Consider Strategy Change");
666
638
  lines.push("");
667
639
  lines.push("The following approaches failed repeatedly. The compiler has demoted " +
668
- "their influence on key lessons. Consider a different technique or " +
640
+ "their influence on key outcomes. Consider a different technique or " +
669
641
  "narrower scope.");
670
642
  for (const fp of rs.failed_patterns) {
671
643
  lines.push(`- 🚫 ${fp}`);
672
644
  }
673
645
  lines.push("");
674
646
  }
675
- if (rs.key_lessons.length) {
676
- lines.push("**Key Lessons From High-Score Rounds**:");
677
- for (const kl of rs.key_lessons)
678
- lines.push(`- ${kl}`);
647
+ return lines.join("\n");
648
+ }
649
+ // ═══════════════════════════════════════════════════════════════════════════
650
+ // Checkpoint compression (v1.10 — subtask boundary snapshots)
651
+ // ═══════════════════════════════════════════════════════════════════════════
652
+ /** Build a CheckpointSummary from the current round's self-evaluation state.
653
+ * Called when the Agent sets compression_checkpoint: true in its self-eval.
654
+ * Snapshots the current constraints and outcome so they survive rolling-window
655
+ * eviction in subsequent rounds. */
656
+ export function buildCheckpointSummary(selfEval, round, activeConstraints, retiredConstraints) {
657
+ const policy = getPolicy();
658
+ const cpCfg = policy.checkpoint;
659
+ const maxCarried = cpCfg?.max_carried_constraints ?? 10;
660
+ const outcomeMaxChars = cpCfg?.outcome_max_chars ?? 200;
661
+ const label = selfEval.checkpoint_label?.trim() ||
662
+ `Subtask completed at round ${round}`;
663
+ const outcome = (selfEval.output_summary || "").trim().slice(0, outcomeMaxChars);
664
+ // Carried: constraints still active, capped
665
+ const carried = activeConstraints.slice(0, maxCarried);
666
+ // Resolved: constraints retired this round (retracted + the retire delta)
667
+ const resolved = retiredConstraints.slice(0, maxCarried);
668
+ return {
669
+ label,
670
+ declared_at_round: round,
671
+ outcome,
672
+ carried_constraints: carried,
673
+ resolved_constraints: resolved,
674
+ };
675
+ }
676
+ /** Format a CheckpointSummary as a prompt block.
677
+ * Renders as a fixed section that persists across rolling-window eviction.
678
+ * Returns empty string if the summary is null or has no meaningful data. */
679
+ export function formatCheckpointForPrompt(cs) {
680
+ if (!cs)
681
+ return "";
682
+ // Must have at least a label and some content to be meaningful
683
+ if (!cs.label && !cs.outcome && !cs.carried_constraints.length)
684
+ return "";
685
+ const lines = [
686
+ `### Checkpoint: ${cs.label} (Round ${cs.declared_at_round})`,
687
+ "",
688
+ ];
689
+ if (cs.outcome) {
690
+ lines.push(`**Outcome**: ${cs.outcome}`);
691
+ lines.push("");
692
+ }
693
+ if (cs.carried_constraints.length) {
694
+ lines.push("**Carried Constraints** (still active):");
695
+ for (const c of cs.carried_constraints) {
696
+ lines.push(`- ${c}`);
697
+ }
698
+ lines.push("");
699
+ }
700
+ if (cs.resolved_constraints.length) {
701
+ lines.push("**Resolved Constraints** (closed with this checkpoint):");
702
+ for (const c of cs.resolved_constraints) {
703
+ lines.push(`- ${c}`);
704
+ }
679
705
  lines.push("");
680
706
  }
681
707
  return lines.join("\n");
682
708
  }
709
+ /** Query the vault for the most recent checkpoint entry before the current
710
+ * round. Returns null if no checkpoint has been declared yet. */
711
+ function loadLatestCheckpoint(loopId, currentRound, vaultContext) {
712
+ if (!vaultContext)
713
+ return null;
714
+ const results = vaultContext.results || [];
715
+ let latest = null;
716
+ let latestRound = -1;
717
+ for (const r of results) {
718
+ const lineage = (r.loop_lineage || r.lineage || {});
719
+ if (lineage.loop_id !== loopId)
720
+ continue;
721
+ const rnd = lineage.round;
722
+ if (rnd >= currentRound)
723
+ continue;
724
+ // Check for stored checkpoint summary on this entry
725
+ const cp = r.checkpoint_summary;
726
+ if (cp && typeof cp.declared_at_round === "number" && cp.declared_at_round > latestRound) {
727
+ latest = cp;
728
+ latestRound = cp.declared_at_round;
729
+ }
730
+ }
731
+ if (!latest)
732
+ return null;
733
+ return makeCheckpointSummary({
734
+ label: latest.label ?? "",
735
+ declared_at_round: latest.declared_at_round ?? 0,
736
+ outcome: latest.outcome ?? "",
737
+ carried_constraints: latest.carried_constraints ?? [],
738
+ resolved_constraints: latest.resolved_constraints ?? [],
739
+ });
740
+ }
683
741
  // ═══════════════════════════════════════════════════════════════════════════
684
742
  // External Context formatting (v1.7 — memory system integration)
685
743
  // ═══════════════════════════════════════════════════════════════════════════
@@ -853,7 +911,7 @@ export function checkLoopHealth(loopId, request, vaultContext) {
853
911
  // 4. strategy_stability
854
912
  const recent = getRecentRounds(loopId, 3, vaultContext);
855
913
  const strategyStability = recent.length > 0
856
- ? recent.every((r) => r.quality_score >= 4)
914
+ ? recent.every((r) => r.success === true)
857
915
  : true;
858
916
  // 5. task_continuity
859
917
  let taskContinuity = 1.0;
@@ -890,15 +948,22 @@ function computeSuggestedNextTask(loopId, vaultContext) {
890
948
  if (vaultContext === null)
891
949
  return "";
892
950
  const results = vaultContext.results || [];
951
+ let bestTask = "";
952
+ let bestRound = -1;
893
953
  for (const r of results) {
894
954
  const lineage = (r.loop_lineage || r.lineage || {});
895
- if (lineage.loop_id === loopId) {
955
+ if (lineage.loop_id !== loopId)
956
+ continue;
957
+ const rnd = lineage.round ?? 0;
958
+ if (rnd > bestRound) {
896
959
  const task = r.task ?? r.user_intent ?? "";
897
- if (task)
898
- return `Previous round focused on: ${task.slice(0, 120)}`;
960
+ if (task) {
961
+ bestTask = task;
962
+ bestRound = rnd;
963
+ }
899
964
  }
900
965
  }
901
- return "";
966
+ return bestTask ? `[R${bestRound}] Previous task: ${bestTask.slice(0, 120)}` : "";
902
967
  }
903
968
  export function computeAdvisories(request, vaultContext) {
904
969
  const warnings = [];
@@ -914,7 +979,7 @@ export function computeAdvisories(request, vaultContext) {
914
979
  }
915
980
  // Strategy collapse check
916
981
  if (strategyCollapse(request.loop_id, vaultContext)) {
917
- warnings.push("strategy_collapse: 3 consecutive low-quality rounds — " +
982
+ warnings.push("strategy_collapse: 3 consecutive failed rounds — " +
918
983
  "consider force_level=L2 rebuild");
919
984
  }
920
985
  // Repair cue detection
@@ -1226,19 +1291,38 @@ function compileL1(request, vaultContext, prevRound) {
1226
1291
  }
1227
1292
  export function compileL2(request, vaultContext) {
1228
1293
  const goalId = deriveGoalId(request.loop_id, request.task, request.goal_id);
1229
- // v3.5: Adaptive technique routing
1230
- const analysis = routeTechniqueAdaptive(request.task, vaultContext, request.loop_id);
1294
+ // v4.0: Tier-gated technique routing
1295
+ const isCheckpoint = request.last_round_result?.compression_checkpoint === true;
1296
+ const analysis = routeTechniqueAdaptive(request.task, vaultContext, request.loop_id, isCheckpoint);
1231
1297
  const technique = analysis.technique;
1232
1298
  const referenceFile = analysis.reference_file;
1233
- // v3.5: Rolling summary
1234
- const rollingSummary = buildRollingSummary(request.loop_id, request.round, vaultContext);
1299
+ // v1.10: Checkpoint — load existing or build new from last round's self-eval
1300
+ let checkpointSummary = null;
1301
+ let checkpointRound = 0;
1302
+ // Check if the last round declared a checkpoint
1303
+ const lr = request.last_round_result;
1304
+ if (lr?.compression_checkpoint === true) {
1305
+ // Build fresh checkpoint from the last round's data
1306
+ // Constraints haven't been updated yet at this point, but we snapshot the
1307
+ // current state as seen by this compile call (pre-P5 correction).
1308
+ // We defer building until constraints are finalized below.
1309
+ checkpointRound = request.round - 1;
1310
+ }
1311
+ else if (request.round > 1) {
1312
+ // No new checkpoint — try loading the previous one from vault
1313
+ checkpointSummary = loadLatestCheckpoint(request.loop_id, request.round, vaultContext);
1314
+ if (checkpointSummary) {
1315
+ checkpointRound = checkpointSummary.declared_at_round;
1316
+ }
1317
+ }
1318
+ // v3.5: Rolling summary — optionally scoped to post-checkpoint rounds
1319
+ const rollingSummary = buildRollingSummary(request.loop_id, request.round, vaultContext, checkpointRound > 0 ? checkpointRound : undefined);
1235
1320
  // P5c: Wrong assumptions — must be applied BEFORE formatting so they appear
1236
1321
  // in the rendered cross-round summary text injected into the prompt.
1237
- const lr = request.last_round_result;
1238
1322
  if (lr?.wrong_assumptions?.length) {
1239
1323
  if (rollingSummary) {
1240
- rollingSummary.key_lessons = [
1241
- ...rollingSummary.key_lessons,
1324
+ rollingSummary.key_outcomes = [
1325
+ ...rollingSummary.key_outcomes,
1242
1326
  ...lr.wrong_assumptions.map((a) => `Wrong assumption corrected: ${a}`),
1243
1327
  ];
1244
1328
  }
@@ -1362,6 +1446,21 @@ export function compileL2(request, vaultContext) {
1362
1446
  ])];
1363
1447
  loopObjective = updatedLo;
1364
1448
  }
1449
+ // ── v1.10: Build checkpoint summary if last round declared one ──
1450
+ if (checkpointRound > 0 && !checkpointSummary && lr) {
1451
+ // checkpointRound was set but no summary yet → fresh declaration
1452
+ // We need the self-eval from last round. It's embedded in lr fields.
1453
+ // Build a synthetic SelfEvaluation from the last_round_result for the builder.
1454
+ const synthEval = {
1455
+ success: lr.success,
1456
+ output_summary: lr.output_summary,
1457
+ constraint_violations: lr.constraint_violations,
1458
+ should_continue: true,
1459
+ compression_checkpoint: true,
1460
+ checkpoint_label: lr.checkpoint_label ?? "",
1461
+ };
1462
+ checkpointSummary = buildCheckpointSummary(synthEval, checkpointRound, constraints, retired);
1463
+ }
1365
1464
  // ── Route to technique-specific specialist (v1.1 deep integration) ──
1366
1465
  let prompt;
1367
1466
  if (technique === "step-back") {
@@ -1376,6 +1475,12 @@ export function compileL2(request, vaultContext) {
1376
1475
  else {
1377
1476
  prompt = compileGeneric(request, goalId, constraints, loopObjective, rollingText, analysis, referenceFile, technique);
1378
1477
  }
1478
+ // ── v1.10: Inject checkpoint block (before rolling summary in prompt) ──
1479
+ // The checkpoint is rendered before the external context so it appears
1480
+ // in a natural position: after Loop Objective, before Cross-Round Summary.
1481
+ if (checkpointSummary) {
1482
+ prompt += "\n" + formatCheckpointForPrompt(checkpointSummary);
1483
+ }
1379
1484
  // ── v1.7: Inject external memory context (L2 only) ──
1380
1485
  const policy = getPolicy();
1381
1486
  if (policy.memory_injection.enabled && request.external_context) {
@@ -1504,6 +1609,7 @@ export function compileL2(request, vaultContext) {
1504
1609
  technique_used: technique,
1505
1610
  reference_file: referenceFile,
1506
1611
  rolling_summary: rollingSummary,
1612
+ checkpoint_summary: checkpointSummary,
1507
1613
  loop_id: request.loop_id,
1508
1614
  round: request.round,
1509
1615
  goal_id: goalId,
@@ -1594,10 +1700,8 @@ function compileGeneric(request, goalId, constraints, loopObjective, rollingText
1594
1700
  "### Generation Instructions",
1595
1701
  `1. Read \`${referenceFile}\` — study its structure rules, section count, and format requirements`,
1596
1702
  "2. Generate a complete prompt following that technique's structure",
1597
- "3. Inject all hard constraints and the loop objective into the prompt",
1598
- "4. If Cross-Round Summary is present above, incorporate its recurring issues and key lessons",
1599
- "5. The prompt must be self-contained — ready for a coding agent to execute",
1600
- "6. Output only the generated prompt — no preamble, no meta-commentary",
1703
+ "3. The prompt must be self-contained — ready for a coding agent to execute",
1704
+ "4. Output only the generated prompt — no preamble, no meta-commentary",
1601
1705
  ];
1602
1706
  return lines.join("\n");
1603
1707
  }
@@ -1656,19 +1760,12 @@ function compileStepBack(request, goalId, constraints, loopObjective, rollingTex
1656
1760
  "**Section 6 transition sentence (MANDATORY)**:",
1657
1761
  "\"基于上述抽象框架,实现以下所有功能。\"",
1658
1762
  "",
1659
- "### Quality Checklist",
1660
- "- [ ] Frameworks are abstract concepts/formulas/principles, NOT concrete code",
1661
- "- [ ] Section 6 starts with the mandatory transition sentence",
1662
- "- [ ] Abstraction level is consistent across frameworks",
1663
- "- [ ] Step-back question is tightened to minimum necessary generalisation",
1664
- "",
1665
1763
  "### Generation Instructions",
1666
1764
  "1. Extract 2-3 abstract principles/frameworks from the task domain",
1667
1765
  "2. Build ASCII diagrams for each framework (formulas, tables, rules)",
1668
1766
  "3. Apply the abstraction back to the concrete task",
1669
- "4. Inject all hard constraints and the loop objective into the prompt",
1670
- "5. If Cross-Round Summary is present above, incorporate its recurring issues and key lessons",
1671
- "6. Output only the generated prompt — no preamble, no meta-commentary",
1767
+ "4. Verify: the generated prompt satisfies all structural rules in the skeleton above",
1768
+ "5. Output only the generated prompt — no preamble, no meta-commentary",
1672
1769
  ];
1673
1770
  return lines.join("\n");
1674
1771
  }
@@ -1726,21 +1823,13 @@ function compileLeastToMost(request, goalId, constraints, loopObjective, rolling
1726
1823
  "- Each sub-problem must serve the original task — no unrelated steps",
1727
1824
  "- The final sub-problem must be equivalent to or directly address the original task",
1728
1825
  "",
1729
- "### Quality Checklist",
1730
- "- [ ] Sub-problems ordered simplest → most complex, dependency chain explicit",
1731
- "- [ ] Each sub-problem declares its prerequisite sub-problems",
1732
- "- [ ] Last sub-problem is \"综合实现完整模块\" with integration list",
1733
- "- [ ] Section 6 expands per output format list (NOT per sub-problem)",
1734
- "- [ ] Sub-problem count: 4-6, no more, no less",
1735
- "",
1736
1826
  "### Generation Instructions",
1737
1827
  "1. Decompose the task into 4-6 ordered sub-problems with explicit dependencies",
1738
1828
  "2. Sub-problem 1 starts with the simplest building block (data structures, enums, base config)",
1739
1829
  "3. Each subsequent sub-problem builds on prior results",
1740
1830
  "4. Final sub-problem integrates all components into the complete module",
1741
- "5. Inject all hard constraints and the loop objective into the prompt",
1742
- "6. If Cross-Round Summary is present above, incorporate its recurring issues and key lessons",
1743
- "7. Output only the generated prompt — no preamble, no meta-commentary",
1831
+ "5. Verify: the generated prompt satisfies all structural rules in the skeleton above",
1832
+ "6. Output only the generated prompt — no preamble, no meta-commentary",
1744
1833
  ];
1745
1834
  return lines.join("\n");
1746
1835
  }
@@ -1819,24 +1908,14 @@ function compileToT(request, goalId, constraints, loopObjective, rollingText, an
1819
1908
  "- thought is a PUBLIC intermediate semantic unit, not hidden chain-of-thought",
1820
1909
  "- State table MUST have: round, branch ID, candidate description, evaluation score/judgment, keep/prune decision",
1821
1910
  "",
1822
- "### Quality Checklist",
1823
- "- [ ] Branch count 2-4, depth ≤3 (prevents token explosion)",
1824
- "- [ ] Evaluation criteria table has ≥3 dimensions including correctness, feasibility, constraint matching",
1825
- "- [ ] State table format explicit (round/branch/candidate/evaluation/decision)",
1826
- "- [ ] Search strategy matches task type (beam/dfs/expert-panel)",
1827
- "- [ ] Hard constraints listed FIRST in evaluation criteria",
1828
- "- [ ] Not \"3 experts casually discussing\" — has explicit branch count, depth, scoring, and pruning rules",
1829
- "- [ ] Ends with model outputting final selection and rationale",
1830
- "",
1831
1911
  "### Generation Instructions",
1832
1912
  "1. Select the appropriate search strategy (beam/dfs/expert-panel) based on task type",
1833
1913
  "2. Define 2-4 candidate approaches as initial branches",
1834
1914
  "3. Build evaluation criteria table with hard constraints ranked first",
1835
1915
  "4. Define the state table format for tracking exploration",
1836
1916
  "5. Set branch count and max depth to prevent token explosion",
1837
- "6. Inject all hard constraints and the loop objective into the prompt",
1838
- "7. If Cross-Round Summary is present above, incorporate its recurring issues and key lessons",
1839
- "8. Output only the generated prompt — no preamble, no meta-commentary",
1917
+ "6. Verify: the generated prompt satisfies all structural rules in the skeleton above",
1918
+ "7. Output only the generated prompt — no preamble, no meta-commentary",
1840
1919
  ];
1841
1920
  return lines.join("\n");
1842
1921
  }
@@ -1874,7 +1953,9 @@ export function buildSelfEvalBlock(round) {
1874
1953
  ' },',
1875
1954
  ' "retracted_constraints": [],',
1876
1955
  ' "revised_success_criteria": [],',
1877
- ' "wrong_assumptions": []',
1956
+ ' "wrong_assumptions": [],',
1957
+ ' "compression_checkpoint": false,',
1958
+ ' "checkpoint_label": ""',
1878
1959
  "}",
1879
1960
  "---end-loopforge-eval",
1880
1961
  "```",
@@ -1907,6 +1988,15 @@ export function buildSelfEvalBlock(round) {
1907
1988
  `Array of {old, new} objects. Applied to the Loop Objective. Omit or [] if none.`,
1908
1989
  `- wrong_assumptions (P5 — optional): Assumptions from earlier rounds that turned ` +
1909
1990
  `out to be incorrect. Recorded as key lessons. Omit or [] if none.`,
1991
+ `- compression_checkpoint (v1.10 — optional): Set to true when you have COMPLETED a ` +
1992
+ `major subtask. The compiler will snapshot the current state (constraints, outcome) ` +
1993
+ `into a persistent checkpoint that survives context-window eviction. ` +
1994
+ `Use sparingly — only at genuine subtask boundaries, not every round. ` +
1995
+ `Omit or false otherwise.`,
1996
+ `- checkpoint_label (v1.10 — optional): Human-readable label for this checkpoint. ` +
1997
+ `e.g. "数据模型层完成", "API layer complete", "Auth module done". ` +
1998
+ `Used as the checkpoint heading in subsequent prompts. Only meaningful when ` +
1999
+ `compression_checkpoint is true. Omit or "" otherwise.`,
1910
2000
  `- The JSON MUST appear between the ---loopforge-eval and ---end-loopforge-eval markers`,
1911
2001
  `- Do NOT wrap the markers in code fences — output them as raw text`,
1912
2002
  "",