loopforge 1.7.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 (88) hide show
  1. package/README.md +64 -3
  2. package/dist/backends/fs.d.ts.map +1 -1
  3. package/dist/backends/fs.js +1 -2
  4. package/dist/backends/fs.js.map +1 -1
  5. package/dist/backends/interface.d.ts +2 -0
  6. package/dist/backends/interface.d.ts.map +1 -1
  7. package/dist/builder.d.ts +6 -10
  8. package/dist/builder.d.ts.map +1 -1
  9. package/dist/builder.js +128 -63
  10. package/dist/builder.js.map +1 -1
  11. package/dist/engine.d.ts +16 -2
  12. package/dist/engine.d.ts.map +1 -1
  13. package/dist/engine.js +142 -54
  14. package/dist/engine.js.map +1 -1
  15. package/dist/index.d.ts +7 -6
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +5 -3
  18. package/dist/index.js.map +1 -1
  19. package/dist/loop-compiler.d.ts +33 -2
  20. package/dist/loop-compiler.d.ts.map +1 -1
  21. package/dist/loop-compiler.js +391 -135
  22. package/dist/loop-compiler.js.map +1 -1
  23. package/dist/mcp/server.d.ts.map +1 -1
  24. package/dist/mcp/server.js +6 -4
  25. package/dist/mcp/server.js.map +1 -1
  26. package/dist/mcp/session.d.ts +18 -4
  27. package/dist/mcp/session.d.ts.map +1 -1
  28. package/dist/mcp/session.js +225 -24
  29. package/dist/mcp/session.js.map +1 -1
  30. package/dist/mcp/tools.d.ts +32 -1
  31. package/dist/mcp/tools.d.ts.map +1 -1
  32. package/dist/mcp/tools.js +33 -14
  33. package/dist/mcp/tools.js.map +1 -1
  34. package/dist/memory-bridge.d.ts +47 -0
  35. package/dist/memory-bridge.d.ts.map +1 -0
  36. package/dist/memory-bridge.js +290 -0
  37. package/dist/memory-bridge.js.map +1 -0
  38. package/dist/observability.d.ts +1 -1
  39. package/dist/observability.js +1 -1
  40. package/dist/policy.d.ts +66 -9
  41. package/dist/policy.d.ts.map +1 -1
  42. package/dist/policy.js +41 -28
  43. package/dist/policy.js.map +1 -1
  44. package/dist/protocol.d.ts +120 -16
  45. package/dist/protocol.d.ts.map +1 -1
  46. package/dist/protocol.js +23 -9
  47. package/dist/protocol.js.map +1 -1
  48. package/dist/replay.js +7 -7
  49. package/dist/replay.js.map +1 -1
  50. package/dist/runtime.d.ts +20 -3
  51. package/dist/runtime.d.ts.map +1 -1
  52. package/dist/runtime.js +420 -177
  53. package/dist/runtime.js.map +1 -1
  54. package/dist/tests/builder.test.js +40 -28
  55. package/dist/tests/builder.test.js.map +1 -1
  56. package/dist/tests/engine.test.js +13 -17
  57. package/dist/tests/engine.test.js.map +1 -1
  58. package/dist/tests/generate-schema.test.js +10 -7
  59. package/dist/tests/generate-schema.test.js.map +1 -1
  60. package/dist/tests/loop-compiler.test.js +481 -27
  61. package/dist/tests/loop-compiler.test.js.map +1 -1
  62. package/dist/tests/mcp.test.js +103 -103
  63. package/dist/tests/mcp.test.js.map +1 -1
  64. package/dist/tests/memory-bridge.test.d.ts +8 -0
  65. package/dist/tests/memory-bridge.test.d.ts.map +1 -0
  66. package/dist/tests/memory-bridge.test.js +383 -0
  67. package/dist/tests/memory-bridge.test.js.map +1 -0
  68. package/dist/tests/policy.test.js +5 -15
  69. package/dist/tests/policy.test.js.map +1 -1
  70. package/dist/tests/protocol.test.js +15 -3
  71. package/dist/tests/protocol.test.js.map +1 -1
  72. package/dist/tests/replay.test.js +9 -9
  73. package/dist/tests/replay.test.js.map +1 -1
  74. package/dist/tests/runtime.test.js +8 -8
  75. package/dist/tests/runtime.test.js.map +1 -1
  76. package/dist/tests/verification-gate.test.js +1 -1
  77. package/dist/tests/verification-gate.test.js.map +1 -1
  78. package/dist/verification-gate.d.ts +2 -2
  79. package/dist/verification-gate.js +2 -2
  80. package/loop_policy.json +28 -23
  81. package/package.json +1 -1
  82. package/skills/perception/SKILL.md +64 -12
  83. package/dist/adapter.d.ts +0 -17
  84. package/dist/adapter.d.ts.map +0 -1
  85. package/dist/adapter.js +0 -228
  86. package/dist/adapter.js.map +0 -1
  87. package/dist/tests/adapter.test.d.ts +0 -2
  88. package/dist/tests/adapter.test.d.ts.map +0 -1
@@ -1,8 +1,8 @@
1
1
  /** Tests for loop compiler core: gates, advisories, L0/L1/L2 compilation. */
2
2
  import { describe, it, beforeEach } from "node:test";
3
3
  import assert from "node:assert/strict";
4
- import { decideLevel, compileLoop, computeGoalTextHash, deriveGoalId, alignTask, compileL2, buildRollingSummary, formatRollingSummaryForPrompt, } from "../loop-compiler.js";
5
- import { makeLoopCompileRequest, makeLoopRoundResult, makeLoopObjective, makeRollingSummary, } from "../protocol.js";
4
+ import { decideLevel, compileLoop, computeGoalTextHash, deriveGoalId, alignTask, compileL2, buildRollingSummary, formatRollingSummaryForPrompt, buildCheckpointSummary, formatCheckpointForPrompt, filterConstraintsForSubTask, formatDelegationPrompt, buildDelegationSummary, } from "../loop-compiler.js";
5
+ import { makeLoopCompileRequest, makeLoopRoundResult, makeLoopObjective, makeRollingSummary, makeCheckpointSummary, makeSelfEvaluation, } from "../protocol.js";
6
6
  import { resetPolicy } from "../policy.js";
7
7
  describe("Loop Compiler — Goal Identity", () => {
8
8
  it("computeGoalTextHash produces a 12-char hex string", () => {
@@ -409,16 +409,17 @@ describe("Loop Compiler — Cross-Round Scenarios", () => {
409
409
  // Failure Lineage Weighting (v1.7)
410
410
  // ═══════════════════════════════════════════════════════════════════════════
411
411
  function makeVaultResult(loopId, round, qualityScore, task, outputSummary = "", techniqueUsed = "zero-shot", constraintViolations = []) {
412
+ const success = qualityScore >= 3;
412
413
  return {
413
414
  task,
414
415
  output_summary: outputSummary,
415
416
  constraint_violations: constraintViolations,
416
417
  technique_used: techniqueUsed,
417
- quality_score: qualityScore,
418
+ success,
418
419
  loop_lineage: {
419
420
  loop_id: loopId,
420
421
  round,
421
- quality_score: qualityScore,
422
+ success,
422
423
  task,
423
424
  technique_used: techniqueUsed,
424
425
  },
@@ -437,12 +438,12 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
437
438
  const rs = buildRollingSummary("loop-1", 4, vault);
438
439
  assert.ok(rs !== null);
439
440
  assert.equal(rs.failed_patterns?.length ?? 0, 0);
440
- // key_lessons should not have [Consider alternatives] marker
441
- for (const kl of rs.key_lessons) {
442
- assert.ok(!kl.includes("[Consider alternatives]"));
441
+ // key_outcomes should not have [Consider alternatives] marker
442
+ for (const ko of rs.key_outcomes) {
443
+ assert.ok(!ko.includes("[Consider alternatives]"));
443
444
  }
444
445
  });
445
- it("detects 2 consecutive low-quality rounds with same technique and similar task", () => {
446
+ it("detects 2 consecutive failed rounds with same technique and similar task", () => {
446
447
  const vault = {
447
448
  results: [
448
449
  makeVaultResult("loop-1", 1, 2, "fix cache bug", "Tried cache fix A", "zero-shot"),
@@ -459,7 +460,7 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
459
460
  assert.ok(fp.includes("cache"));
460
461
  assert.ok(fp.includes("2 consecutive rounds"));
461
462
  });
462
- it("detects 3 consecutive low-quality rounds", () => {
463
+ it("detects 3 consecutive failed rounds", () => {
463
464
  const vault = {
464
465
  results: [
465
466
  makeVaultResult("loop-1", 1, 2, "fix memory leak", "Attempt 1", "few-shot"),
@@ -473,7 +474,7 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
473
474
  assert.ok((rs.failed_patterns?.length ?? 0) >= 1);
474
475
  assert.ok(rs.failed_patterns[0].includes("3 consecutive rounds"));
475
476
  });
476
- it("no pattern when low-quality rounds use different techniques", () => {
477
+ it("no pattern when failed rounds use different techniques", () => {
477
478
  const vault = {
478
479
  results: [
479
480
  makeVaultResult("loop-1", 1, 2, "fix cache bug", "Attempt", "zero-shot"),
@@ -485,7 +486,7 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
485
486
  assert.ok(rs !== null);
486
487
  assert.equal(rs.failed_patterns?.length ?? 0, 0);
487
488
  });
488
- it("no pattern when low-quality tasks are completely different (Jaccard < 0.4)", () => {
489
+ it("no pattern when failed tasks are completely different (Jaccard < 0.4)", () => {
489
490
  const vault = {
490
491
  results: [
491
492
  makeVaultResult("loop-1", 1, 2, "fix cache bug in Redis layer", "Attempt", "zero-shot"),
@@ -497,7 +498,7 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
497
498
  assert.ok(rs !== null);
498
499
  assert.equal(rs.failed_patterns?.length ?? 0, 0);
499
500
  });
500
- it("key_lessons from failure-pattern rounds are demoted to end with [Consider alternatives]", () => {
501
+ it("key_outcomes from failure-pattern rounds are demoted to end with [Consider alternatives]", () => {
501
502
  const vault = {
502
503
  results: [
503
504
  makeVaultResult("loop-1", 1, 2, "fix cache bug", "Tried cache approach", "zero-shot"),
@@ -508,14 +509,15 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
508
509
  };
509
510
  const rs = buildRollingSummary("loop-1", 4, vault);
510
511
  assert.ok(rs !== null);
511
- // key_lessons should have the successful round first, then demoted ones
512
- // (but since score >= 4 for key_lessons, rounds 1-2 with score=2 won't appear at all)
513
- // The real test is: no false positives in key_lessons
514
- for (const kl of rs.key_lessons) {
515
- if (kl.includes("cache")) {
516
- assert.ok(kl.includes("[Consider alternatives]"));
512
+ // key_outcomes only include score>=4 rounds (failure-pattern rounds score<3 don't appear)
513
+ // Round 3 (score=5) should be present and not marked
514
+ for (const ko of rs.key_outcomes) {
515
+ if (ko.includes("cache")) {
516
+ assert.ok(ko.includes("[Consider alternatives]"));
517
517
  }
518
518
  }
519
+ // Round 3 (score=5) should be present with auth fix outcome
520
+ assert.ok(rs.key_outcomes.some((ko) => ko.includes("Auth")));
519
521
  });
520
522
  it("recurring_issues from failure-only rounds marked as [Possible dead end]", () => {
521
523
  const vault = {
@@ -557,15 +559,12 @@ describe("Failure Lineage Weighting — buildRollingSummary", () => {
557
559
  describe("Failure Lineage Weighting — formatRollingSummaryForPrompt", () => {
558
560
  it("renders failure patterns section when present", () => {
559
561
  const rs = makeRollingSummary({
560
- quality_trajectory: [2, 2, 5],
561
- trajectory_direction: "improving",
562
- what_worked: ["R3 (score=5): Fixed auth"],
562
+ key_outcomes: ["[R3] ✓ (step-back): Fixed auth bug"],
563
563
  recurring_issues: [],
564
- key_lessons: ["[R3] Fixed auth bug"],
565
564
  rounds_sampled: 3,
566
565
  generated_at_round: 4,
567
566
  failed_patterns: [
568
- "technique 'zero-shot' on task 'fix cache bug' failed 2 consecutive rounds (R1-R2, avg score 2.0) — consider strategy change",
567
+ "technique 'zero-shot' on task 'fix cache bug' failed 2 consecutive rounds (R1-R2) — consider strategy change",
569
568
  ],
570
569
  });
571
570
  const text = formatRollingSummaryForPrompt(rs);
@@ -576,11 +575,8 @@ describe("Failure Lineage Weighting — formatRollingSummaryForPrompt", () => {
576
575
  });
577
576
  it("no failure patterns section when empty", () => {
578
577
  const rs = makeRollingSummary({
579
- quality_trajectory: [5, 4],
580
- trajectory_direction: "stable",
581
- what_worked: [],
578
+ key_outcomes: [],
582
579
  recurring_issues: [],
583
- key_lessons: [],
584
580
  rounds_sampled: 2,
585
581
  generated_at_round: 3,
586
582
  failed_patterns: [],
@@ -594,4 +590,462 @@ describe("Failure Lineage Weighting — formatRollingSummaryForPrompt", () => {
594
590
  assert.equal(formatRollingSummaryForPrompt(empty), "");
595
591
  });
596
592
  });
593
+ // ═══════════════════════════════════════════════════════════════════════════
594
+ // Delegation Helpers (v1.9 — AgentTool mode)
595
+ // ═══════════════════════════════════════════════════════════════════════════
596
+ describe("Delegation Helpers — filterConstraintsForSubTask", () => {
597
+ const constraints = [
598
+ "all APIs must have rate limiting",
599
+ "use TypeScript strict mode",
600
+ "all files must have license header",
601
+ "every function needs unit tests",
602
+ "禁止使用 any 类型",
603
+ ];
604
+ it("returns empty array for empty constraints", () => {
605
+ assert.deepEqual(filterConstraintsForSubTask([], "search for deprecated API"), []);
606
+ });
607
+ it("returns empty array for empty subTask", () => {
608
+ assert.deepEqual(filterConstraintsForSubTask(constraints, ""), []);
609
+ assert.deepEqual(filterConstraintsForSubTask(constraints, " "), []);
610
+ });
611
+ it("returns only relevant constraints by Jaccard similarity", () => {
612
+ // "search for deprecated API usage in src/" should NOT match
613
+ // rate limiting, strict mode, license headers, or unit tests
614
+ const result = filterConstraintsForSubTask(constraints, "search for deprecated API usage in src/");
615
+ assert.equal(result.length, 0);
616
+ });
617
+ it("returns relevant constraint when task overlaps", () => {
618
+ // "add rate limiting to all API endpoints" should match
619
+ // "all APIs must have rate limiting"
620
+ const result = filterConstraintsForSubTask(constraints, "add rate limiting to all API endpoints");
621
+ assert.ok(result.length >= 1);
622
+ assert.ok(result.some((c) => c.includes("rate limiting")));
623
+ });
624
+ it("returns at least one constraint when task has TypeScript+testing overlap", () => {
625
+ // "add TypeScript unit tests for all API functions" overlaps with
626
+ // "every function needs unit tests" (Jaccard ~0.2);
627
+ // "use TypeScript strict mode" overlap is too low (Jaccard ~0.1)
628
+ const result = filterConstraintsForSubTask(constraints, "add TypeScript unit tests for all API functions");
629
+ assert.ok(result.length >= 1);
630
+ assert.ok(result.some((c) => c.includes("unit tests")));
631
+ });
632
+ it("custom threshold filters more aggressively", () => {
633
+ // With threshold 0.5, should return very few constraints
634
+ const lenient = filterConstraintsForSubTask(constraints, "add rate limiting to all API endpoints", 0.15);
635
+ const strict = filterConstraintsForSubTask(constraints, "add rate limiting to all API endpoints", 0.5);
636
+ assert.ok(strict.length <= lenient.length);
637
+ });
638
+ it("supports CJK subTask with CJK constraints", () => {
639
+ const cjkConstraints = [
640
+ "所有API必须有速率限制",
641
+ "使用TypeScript严格模式",
642
+ ];
643
+ // "搜索废弃的API" doesn't overlap with rate limiting or strict mode
644
+ const result = filterConstraintsForSubTask(cjkConstraints, "搜索废弃的API使用情况");
645
+ assert.equal(result.length, 0);
646
+ });
647
+ it("matches CJK constraint with overlapping CJK subTask", () => {
648
+ const cjkConstraints = [
649
+ "所有API必须有速率限制",
650
+ "所有文件必须有许可证头",
651
+ ];
652
+ const result = filterConstraintsForSubTask(cjkConstraints, "为API添加速率限制");
653
+ assert.ok(result.length >= 1);
654
+ assert.ok(result.some((c) => c.includes("速率限制")));
655
+ });
656
+ });
657
+ describe("Delegation Helpers — formatDelegationPrompt", () => {
658
+ const subTask = "search for deprecated API usage in src/";
659
+ const constraints = ["all APIs must have rate limiting"];
660
+ it("generates self-contained explore prompt", () => {
661
+ const prompt = formatDelegationPrompt(subTask, "explore", []);
662
+ assert.ok(prompt.includes("### Delegated Task"));
663
+ assert.ok(prompt.includes(subTask));
664
+ assert.ok(prompt.includes("### Output Format"));
665
+ // Explore should NOT get constraints
666
+ assert.ok(!prompt.includes("### Relevant Constraints"));
667
+ // Must be self-contained
668
+ assert.ok(!prompt.match(/based on (above|previous|your findings)/i));
669
+ assert.ok(!prompt.includes("continue from"));
670
+ });
671
+ it("generates general-purpose prompt with constraints", () => {
672
+ const prompt = formatDelegationPrompt("fix the null pointer in src/auth/validate.ts:42", "general-purpose", constraints);
673
+ assert.ok(prompt.includes("### Delegated Task"));
674
+ assert.ok(prompt.includes("### Relevant Constraints"));
675
+ assert.ok(prompt.includes("rate limiting"));
676
+ assert.ok(prompt.includes("don't leave it half-done"));
677
+ // Must be self-contained
678
+ assert.ok(!prompt.match(/based on (above|previous|your findings)/i));
679
+ });
680
+ it("generates plan prompt with instructions", () => {
681
+ const prompt = formatDelegationPrompt("design authentication middleware", "plan", constraints, { context: "Current auth is JWT-based in src/auth/" });
682
+ assert.ok(prompt.includes("### Delegated Task"));
683
+ assert.ok(prompt.includes("### Context"));
684
+ assert.ok(prompt.includes("JWT-based"));
685
+ assert.ok(prompt.includes("### Relevant Constraints"));
686
+ assert.ok(prompt.includes("Critical files for implementation"));
687
+ assert.ok(prompt.includes("3-5 files"));
688
+ });
689
+ it("skips constraints section when empty", () => {
690
+ const prompt = formatDelegationPrompt(subTask, "general-purpose", []);
691
+ assert.ok(!prompt.includes("### Relevant Constraints"));
692
+ });
693
+ it("custom sub-agent type gets generic template", () => {
694
+ const prompt = formatDelegationPrompt(subTask, "custom-auditor", constraints);
695
+ assert.ok(prompt.includes("### Delegated Task"));
696
+ assert.ok(prompt.includes("### Relevant Constraints"));
697
+ assert.ok(prompt.includes("Complete the task above"));
698
+ });
699
+ it("output is self-contained — no parent-context references", () => {
700
+ const types = ["explore", "general-purpose", "plan", "custom"];
701
+ for (const t of types) {
702
+ const prompt = formatDelegationPrompt("do something", t, constraints);
703
+ // No references to parent conversation
704
+ assert.ok(!prompt.match(/based on (above|previous|your findings)/i), `${t}: should not reference parent context`);
705
+ assert.ok(!prompt.includes("continue from"), `${t}: should not say "continue from"`);
706
+ assert.ok(!prompt.includes("as discussed"), `${t}: should not say "as discussed"`);
707
+ assert.ok(!prompt.includes("see above"), `${t}: should not say "see above"`);
708
+ }
709
+ });
710
+ });
711
+ describe("Delegation — buildDelegationSummary", () => {
712
+ it("returns empty string for null vault context", () => {
713
+ assert.equal(buildDelegationSummary(null), "");
714
+ });
715
+ it("returns empty string when no delegation journal entries", () => {
716
+ const vault = { results: [{ task_type: "loop_lineage", loop_id: "test" }] };
717
+ assert.equal(buildDelegationSummary(vault), "");
718
+ });
719
+ it("returns empty string when results array is empty", () => {
720
+ const vault = { results: [] };
721
+ assert.equal(buildDelegationSummary(vault), "");
722
+ });
723
+ it("builds delegation table from journal entries", () => {
724
+ const vault = {
725
+ results: [
726
+ {
727
+ task_type: "delegation_journal",
728
+ loop_lineage: {
729
+ round: 2,
730
+ delegations: [
731
+ { index: 1, agentId: "abc-123", subAgentType: "explore", subTask: "search deprecated APIs", resultSummary: "found 12 instances", success: true, discoveredConstraints: [] },
732
+ { index: 2, agentId: "def-456", subAgentType: "general-purpose", subTask: "fix auth bug", resultSummary: "fixed null pointer", success: true, discoveredConstraints: [] },
733
+ ],
734
+ },
735
+ },
736
+ ],
737
+ };
738
+ const text = buildDelegationSummary(vault);
739
+ assert.ok(text.includes("### Delegation History"));
740
+ assert.ok(text.includes("R2"));
741
+ assert.ok(text.includes("search deprecated APIs"));
742
+ assert.ok(text.includes("fix auth bug"));
743
+ assert.ok(text.includes("✓"));
744
+ // Agent column should display agentId
745
+ assert.ok(text.includes("abc-123"));
746
+ assert.ok(text.includes("def-456"));
747
+ // Table header should include Agent column
748
+ assert.ok(text.includes("| Round | Agent | Type | Task | Result | ✓/✗ |"));
749
+ });
750
+ it("shows ✗ for failed delegations", () => {
751
+ const vault = {
752
+ results: [
753
+ {
754
+ task_type: "delegation_journal",
755
+ loop_lineage: {
756
+ round: 3,
757
+ delegations: [
758
+ { index: 1, agentId: "worker-xyz", subAgentType: "general-purpose", subTask: "fix complex bug", resultSummary: "failed — wrong approach", success: false, discoveredConstraints: [] },
759
+ ],
760
+ },
761
+ },
762
+ ],
763
+ };
764
+ const text = buildDelegationSummary(vault);
765
+ assert.ok(text.includes("✗"));
766
+ assert.ok(text.includes("fix complex bug"));
767
+ assert.ok(text.includes("wrong approach"));
768
+ });
769
+ it("handles multiple rounds of delegation", () => {
770
+ const vault = {
771
+ results: [
772
+ {
773
+ task_type: "delegation_journal",
774
+ loop_lineage: { round: 1, delegations: [{ index: 1, agentId: "agent-a", subAgentType: "explore", subTask: "research X", resultSummary: "done", success: true, discoveredConstraints: [] }] },
775
+ },
776
+ {
777
+ task_type: "delegation_journal",
778
+ loop_lineage: { round: 2, delegations: [{ index: 1, agentId: "agent-b", subAgentType: "general-purpose", subTask: "fix Y", resultSummary: "done", success: true, discoveredConstraints: [] }] },
779
+ },
780
+ ],
781
+ };
782
+ const text = buildDelegationSummary(vault);
783
+ assert.ok(text.includes("R1"));
784
+ assert.ok(text.includes("R2"));
785
+ });
786
+ it("falls back to worker-N when agentId is missing (backward compat)", () => {
787
+ const vault = {
788
+ results: [
789
+ {
790
+ task_type: "delegation_journal",
791
+ loop_lineage: {
792
+ round: 1,
793
+ delegations: [
794
+ { index: 3, subAgentType: "explore", subTask: "scan codebase", resultSummary: "done", success: true },
795
+ ],
796
+ },
797
+ },
798
+ ],
799
+ };
800
+ const text = buildDelegationSummary(vault);
801
+ assert.ok(text.includes("worker-3"), "should fall back to worker-3");
802
+ });
803
+ it("escapes pipe characters in markdown table cells", () => {
804
+ const vault = {
805
+ results: [
806
+ {
807
+ task_type: "delegation_journal",
808
+ loop_lineage: {
809
+ round: 1,
810
+ delegations: [
811
+ { index: 1, agentId: "agent-1", subAgentType: "explore", subTask: "audit A|B testing in src/", resultSummary: "found issues in x|y module", success: true, discoveredConstraints: [] },
812
+ ],
813
+ },
814
+ },
815
+ ],
816
+ };
817
+ const text = buildDelegationSummary(vault);
818
+ // Should contain escaped pipes, not raw pipes that would break the table
819
+ assert.ok(text.includes("A\\|B"), "pipe in task should be escaped");
820
+ assert.ok(text.includes("x\\|y"), "pipe in result should be escaped");
821
+ // Verify the table row still has the correct number of pipe chars:
822
+ // 7 structural (leading + 5 separators + trailing) + 2 escaped inside cells = 9 total
823
+ const tableRow = text.split("\n").find((l) => l.startsWith("| R1"));
824
+ assert.ok(tableRow, "should have a table row for R1");
825
+ const pipeCount = (tableRow.match(/\|/g) ?? []).length;
826
+ assert.equal(pipeCount, 9, "table row should have 9 pipe chars (7 structural + 2 escaped)");
827
+ });
828
+ });
829
+ // ═══════════════════════════════════════════════════════════════════════════
830
+ // Checkpoint Compression (v1.10)
831
+ // ═══════════════════════════════════════════════════════════════════════════
832
+ describe("Checkpoint — buildCheckpointSummary", () => {
833
+ it("builds from self-eval with checkpoint fields", () => {
834
+ const selfEval = makeSelfEvaluation({
835
+ success: true,
836
+ output_summary: "Completed ERC20 data model with full test coverage",
837
+ compression_checkpoint: true,
838
+ checkpoint_label: "数据模型层完成",
839
+ });
840
+ const cs = buildCheckpointSummary(selfEval, 8, ["constraint-A", "constraint-B", "constraint-C"], ["old-constraint"]);
841
+ assert.ok(cs !== null);
842
+ assert.equal(cs.label, "数据模型层完成");
843
+ assert.equal(cs.declared_at_round, 8);
844
+ assert.ok(cs.outcome.includes("ERC20 data model"));
845
+ assert.deepStrictEqual(cs.carried_constraints, ["constraint-A", "constraint-B", "constraint-C"]);
846
+ assert.deepStrictEqual(cs.resolved_constraints, ["old-constraint"]);
847
+ });
848
+ it("falls back to auto-generated label when checkpoint_label is empty", () => {
849
+ const selfEval = makeSelfEvaluation({
850
+ success: true,
851
+ output_summary: "Done with auth module",
852
+ compression_checkpoint: true,
853
+ checkpoint_label: "",
854
+ });
855
+ const cs = buildCheckpointSummary(selfEval, 5, ["c1"], []);
856
+ assert.ok(cs.label.includes("Subtask completed at round 5"));
857
+ });
858
+ it("caps carried and resolved constraints at policy limit", () => {
859
+ const selfEval = makeSelfEvaluation({
860
+ success: true,
861
+ output_summary: "Done",
862
+ compression_checkpoint: true,
863
+ checkpoint_label: "test",
864
+ });
865
+ const manyConstraints = Array.from({ length: 15 }, (_, i) => `c${i + 1}`);
866
+ const cs = buildCheckpointSummary(selfEval, 3, manyConstraints, manyConstraints);
867
+ // Default policy: max_carried_constraints = 10
868
+ assert.equal(cs.carried_constraints.length, 10);
869
+ assert.equal(cs.resolved_constraints.length, 10);
870
+ });
871
+ it("truncates outcome at policy limit", () => {
872
+ const selfEval = makeSelfEvaluation({
873
+ success: true,
874
+ output_summary: "x".repeat(300),
875
+ compression_checkpoint: true,
876
+ checkpoint_label: "test",
877
+ });
878
+ const cs = buildCheckpointSummary(selfEval, 1, [], []);
879
+ // Default policy: outcome_max_chars = 200
880
+ assert.ok(cs.outcome.length <= 200);
881
+ });
882
+ });
883
+ describe("Checkpoint — formatCheckpointForPrompt", () => {
884
+ it("renders checkpoint block with all fields", () => {
885
+ const cs = makeCheckpointSummary({
886
+ label: "Auth module complete",
887
+ declared_at_round: 5,
888
+ outcome: "Implemented JWT auth with refresh tokens",
889
+ carried_constraints: ["所有API需要认证", "token过期时间≤24h"],
890
+ resolved_constraints: ["旧密码哈希方式需更新"],
891
+ });
892
+ const text = formatCheckpointForPrompt(cs);
893
+ assert.ok(text.includes("### Checkpoint: Auth module complete (Round 5)"));
894
+ assert.ok(text.includes("Implemented JWT auth"));
895
+ assert.ok(text.includes("所有API需要认证"));
896
+ assert.ok(text.includes("旧密码哈希方式需更新"));
897
+ assert.ok(text.includes("Carried Constraints"));
898
+ assert.ok(text.includes("Resolved Constraints"));
899
+ });
900
+ it("returns empty string for null", () => {
901
+ assert.equal(formatCheckpointForPrompt(null), "");
902
+ });
903
+ it("returns empty string for empty checkpoint", () => {
904
+ const cs = makeCheckpointSummary({});
905
+ assert.equal(formatCheckpointForPrompt(cs), "");
906
+ });
907
+ it("omits resolved section when no resolved constraints", () => {
908
+ const cs = makeCheckpointSummary({
909
+ label: "test",
910
+ declared_at_round: 1,
911
+ outcome: "done",
912
+ carried_constraints: ["c1"],
913
+ resolved_constraints: [],
914
+ });
915
+ const text = formatCheckpointForPrompt(cs);
916
+ assert.ok(!text.includes("Resolved Constraints"));
917
+ });
918
+ });
919
+ describe("Checkpoint — buildRollingSummary with sinceRound", () => {
920
+ function makeVaultResult(loopId, round, qualityScore, task, outputSummary = "", techniqueUsed = "zero-shot", constraintViolations = []) {
921
+ const success = qualityScore >= 3;
922
+ return {
923
+ task, output_summary: outputSummary,
924
+ constraint_violations: constraintViolations,
925
+ technique_used: techniqueUsed, success,
926
+ loop_lineage: { loop_id: loopId, round, success, task, technique_used: techniqueUsed },
927
+ };
928
+ }
929
+ it("only includes rounds at or after sinceRound", () => {
930
+ const vault = {
931
+ results: [
932
+ makeVaultResult("loop-1", 1, 4, "task A", "did A"),
933
+ makeVaultResult("loop-1", 2, 5, "task B", "did B"),
934
+ makeVaultResult("loop-1", 3, 3, "task C", "did C"),
935
+ makeVaultResult("loop-1", 4, 5, "task D", "did D"),
936
+ makeVaultResult("loop-1", 5, 4, "task E", "did E"),
937
+ ],
938
+ };
939
+ const rs = buildRollingSummary("loop-1", 6, vault, 3);
940
+ assert.ok(rs !== null);
941
+ // Round 1 and 2 should be excluded (before sinceRound=3)
942
+ assert.equal(rs.rounds_sampled, 3);
943
+ // Verify key_outcomes contains rounds 3-5
944
+ assert.equal(rs.key_outcomes.length, 3);
945
+ assert.ok(rs.key_outcomes.some((k) => k.includes("R3")));
946
+ assert.ok(rs.key_outcomes.some((k) => k.includes("R5")));
947
+ });
948
+ it("sinceRound with no matching rounds returns null", () => {
949
+ const vault = {
950
+ results: [
951
+ makeVaultResult("loop-1", 1, 4, "task A", "did A"),
952
+ makeVaultResult("loop-1", 2, 5, "task B", "did B"),
953
+ ],
954
+ };
955
+ const rs = buildRollingSummary("loop-1", 3, vault, 5);
956
+ assert.equal(rs, null);
957
+ });
958
+ it("without sinceRound includes all rounds (backward compat)", () => {
959
+ const vault = {
960
+ results: [
961
+ makeVaultResult("loop-1", 1, 4, "task A", "did A"),
962
+ makeVaultResult("loop-1", 2, 5, "task B", "did B"),
963
+ ],
964
+ };
965
+ const rs = buildRollingSummary("loop-1", 3, vault);
966
+ assert.ok(rs !== null);
967
+ assert.equal(rs.rounds_sampled, 2);
968
+ });
969
+ });
970
+ describe("Checkpoint — compileL2 integration", () => {
971
+ it("renders checkpoint in prompt when last_round_result has checkpoint", () => {
972
+ const lr = makeLoopRoundResult({
973
+ round: 4,
974
+ success: true,
975
+ output_summary: "Completed data model layer",
976
+ constraint_violations: [],
977
+ compression_checkpoint: true,
978
+ checkpoint_label: "Data model done",
979
+ });
980
+ const req = makeLoopCompileRequest({
981
+ round: 5,
982
+ loop_id: "test-cp",
983
+ task: "Build API layer on top of data model",
984
+ last_round_result: lr,
985
+ });
986
+ // Empty vault — no prior checkpoint to load
987
+ const vault = { results: [], global_entries: [] };
988
+ const response = compileL2(req, vault);
989
+ assert.ok(response.prompt.includes("### Checkpoint: Data model done (Round 4)"));
990
+ assert.ok(response.prompt.includes("Completed data model layer"));
991
+ assert.ok(response.checkpoint_summary !== null);
992
+ assert.equal(response.checkpoint_summary.declared_at_round, 4);
993
+ });
994
+ it("no checkpoint in prompt when last_round_result has no checkpoint", () => {
995
+ const lr = makeLoopRoundResult({
996
+ round: 2,
997
+ success: true,
998
+ output_summary: "Fixed a bug",
999
+ constraint_violations: [],
1000
+ });
1001
+ const req = makeLoopCompileRequest({
1002
+ round: 3,
1003
+ loop_id: "test-nocp",
1004
+ task: "Continue fixing bugs",
1005
+ last_round_result: lr,
1006
+ });
1007
+ const vault = { results: [], global_entries: [] };
1008
+ const response = compileL2(req, vault);
1009
+ assert.ok(!response.prompt.includes("### Checkpoint:"));
1010
+ assert.equal(response.checkpoint_summary, null);
1011
+ });
1012
+ it("loads existing checkpoint from vault on later rounds", () => {
1013
+ // Simulate a previous round that stored a checkpoint in vault
1014
+ const vault = {
1015
+ results: [
1016
+ {
1017
+ loop_lineage: { loop_id: "test-cp2", round: 3, success: true },
1018
+ checkpoint_summary: {
1019
+ label: "API layer done",
1020
+ declared_at_round: 3,
1021
+ outcome: "Built REST API",
1022
+ carried_constraints: ["auth required"],
1023
+ resolved_constraints: [],
1024
+ },
1025
+ task: "Build API",
1026
+ output_summary: "Built REST API",
1027
+ technique_used: "least-to-most",
1028
+ },
1029
+ ],
1030
+ };
1031
+ const lr = makeLoopRoundResult({
1032
+ round: 4,
1033
+ success: true,
1034
+ output_summary: "Added tests",
1035
+ constraint_violations: [],
1036
+ });
1037
+ const req = makeLoopCompileRequest({
1038
+ round: 5,
1039
+ loop_id: "test-cp2",
1040
+ task: "Add integration tests",
1041
+ last_round_result: lr,
1042
+ });
1043
+ const response = compileL2(req, vault);
1044
+ // Should load checkpoint from round 3
1045
+ assert.ok(response.prompt.includes("### Checkpoint: API layer done (Round 3)"));
1046
+ assert.ok(response.prompt.includes("Built REST API"));
1047
+ assert.ok(response.checkpoint_summary !== null);
1048
+ assert.equal(response.checkpoint_summary.declared_at_round, 3);
1049
+ });
1050
+ });
597
1051
  //# sourceMappingURL=loop-compiler.test.js.map