loopforge 3.2.1 → 3.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 (122) hide show
  1. package/README.md +74 -71
  2. package/dist/audit.d.ts.map +1 -1
  3. package/dist/audit.js +6 -38
  4. package/dist/audit.js.map +1 -1
  5. package/dist/canonical-state.d.ts +71 -6
  6. package/dist/canonical-state.d.ts.map +1 -1
  7. package/dist/canonical-state.js +176 -29
  8. package/dist/canonical-state.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +16 -26
  11. package/dist/cli.js.map +1 -1
  12. package/dist/cognitive-facts.d.ts +17 -0
  13. package/dist/cognitive-facts.d.ts.map +1 -0
  14. package/dist/cognitive-facts.js +99 -0
  15. package/dist/cognitive-facts.js.map +1 -0
  16. package/dist/cognitive-governance.d.ts +0 -1
  17. package/dist/cognitive-governance.d.ts.map +1 -1
  18. package/dist/cognitive-governance.js +1 -4
  19. package/dist/cognitive-governance.js.map +1 -1
  20. package/dist/committed-round.d.ts +100 -0
  21. package/dist/committed-round.d.ts.map +1 -0
  22. package/dist/committed-round.js +349 -0
  23. package/dist/committed-round.js.map +1 -0
  24. package/dist/enforcement-gate.d.ts +4 -2
  25. package/dist/enforcement-gate.d.ts.map +1 -1
  26. package/dist/enforcement-gate.js +385 -357
  27. package/dist/enforcement-gate.js.map +1 -1
  28. package/dist/engine.d.ts +16 -4
  29. package/dist/engine.d.ts.map +1 -1
  30. package/dist/engine.js +65 -16
  31. package/dist/engine.js.map +1 -1
  32. package/dist/evidence-claims.d.ts.map +1 -1
  33. package/dist/evidence-claims.js +15 -39
  34. package/dist/evidence-claims.js.map +1 -1
  35. package/dist/evidence-provider.d.ts +35 -17
  36. package/dist/evidence-provider.d.ts.map +1 -1
  37. package/dist/evidence-provider.js +105 -108
  38. package/dist/evidence-provider.js.map +1 -1
  39. package/dist/index.d.ts +4 -4
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +3 -3
  42. package/dist/index.js.map +1 -1
  43. package/dist/loop-compiler.d.ts +21 -10
  44. package/dist/loop-compiler.d.ts.map +1 -1
  45. package/dist/loop-compiler.js +232 -276
  46. package/dist/loop-compiler.js.map +1 -1
  47. package/dist/loop-projection.d.ts +5 -25
  48. package/dist/loop-projection.d.ts.map +1 -1
  49. package/dist/loop-projection.js +18 -134
  50. package/dist/loop-projection.js.map +1 -1
  51. package/dist/loop-store.d.ts +16 -18
  52. package/dist/loop-store.d.ts.map +1 -1
  53. package/dist/loop-store.js +21 -63
  54. package/dist/loop-store.js.map +1 -1
  55. package/dist/mcp/round-lifecycle.d.ts +13 -10
  56. package/dist/mcp/round-lifecycle.d.ts.map +1 -1
  57. package/dist/mcp/round-lifecycle.js +95 -68
  58. package/dist/mcp/round-lifecycle.js.map +1 -1
  59. package/dist/mcp/server.js +3 -3
  60. package/dist/mcp/server.js.map +1 -1
  61. package/dist/mcp/session.d.ts +17 -5
  62. package/dist/mcp/session.d.ts.map +1 -1
  63. package/dist/mcp/session.js +177 -54
  64. package/dist/mcp/session.js.map +1 -1
  65. package/dist/mcp/tools.d.ts +12 -0
  66. package/dist/mcp/tools.d.ts.map +1 -1
  67. package/dist/mcp/tools.js +161 -53
  68. package/dist/mcp/tools.js.map +1 -1
  69. package/dist/policy-metrics.d.ts +0 -7
  70. package/dist/policy-metrics.d.ts.map +1 -1
  71. package/dist/policy-metrics.js +15 -55
  72. package/dist/policy-metrics.js.map +1 -1
  73. package/dist/policy.d.ts +22 -31
  74. package/dist/policy.d.ts.map +1 -1
  75. package/dist/policy.js +24 -42
  76. package/dist/policy.js.map +1 -1
  77. package/dist/prompt-assembler.d.ts +0 -3
  78. package/dist/prompt-assembler.d.ts.map +1 -1
  79. package/dist/prompt-assembler.js +124 -191
  80. package/dist/prompt-assembler.js.map +1 -1
  81. package/dist/protocol.d.ts +61 -46
  82. package/dist/protocol.d.ts.map +1 -1
  83. package/dist/protocol.js +2 -18
  84. package/dist/protocol.js.map +1 -1
  85. package/dist/replay.d.ts.map +1 -1
  86. package/dist/replay.js +21 -22
  87. package/dist/replay.js.map +1 -1
  88. package/dist/round-contract.d.ts +64 -0
  89. package/dist/round-contract.d.ts.map +1 -0
  90. package/dist/round-contract.js +80 -0
  91. package/dist/round-contract.js.map +1 -0
  92. package/dist/round-coordinator.d.ts +5 -3
  93. package/dist/round-coordinator.d.ts.map +1 -1
  94. package/dist/round-coordinator.js +34 -29
  95. package/dist/round-coordinator.js.map +1 -1
  96. package/dist/round-driver.d.ts +0 -4
  97. package/dist/round-driver.d.ts.map +1 -1
  98. package/dist/round-driver.js +1 -11
  99. package/dist/round-driver.js.map +1 -1
  100. package/dist/round-transaction.d.ts +0 -1
  101. package/dist/round-transaction.d.ts.map +1 -1
  102. package/dist/round-transaction.js +0 -1
  103. package/dist/round-transaction.js.map +1 -1
  104. package/dist/self-eval.d.ts +29 -34
  105. package/dist/self-eval.d.ts.map +1 -1
  106. package/dist/self-eval.js +112 -148
  107. package/dist/self-eval.js.map +1 -1
  108. package/dist/token-utils.d.ts +7 -3
  109. package/dist/token-utils.d.ts.map +1 -1
  110. package/dist/token-utils.js +18 -3
  111. package/dist/token-utils.js.map +1 -1
  112. package/dist/verification-gate.d.ts +72 -21
  113. package/dist/verification-gate.d.ts.map +1 -1
  114. package/dist/verification-gate.js +544 -157
  115. package/dist/verification-gate.js.map +1 -1
  116. package/dist/workspace.d.ts +29 -0
  117. package/dist/workspace.d.ts.map +1 -0
  118. package/dist/workspace.js +61 -0
  119. package/dist/workspace.js.map +1 -0
  120. package/loop_policy.json +5 -17
  121. package/package.json +2 -2
  122. package/skills/perception/SKILL.md +137 -136
@@ -5,11 +5,10 @@
5
5
  * are never truncated; budgets are soft and overflow is recorded.
6
6
  */
7
7
  import { createHash } from "node:crypto";
8
- import { hashCanonicalState } from "./canonical-state.js";
8
+ import { buildRoadmap, hashCanonicalState, milestoneHeading, trustBarLine } from "./canonical-state.js";
9
9
  import { deriveItemId, STABLE_ID_RE, jaccardSimilarity } from "./token-utils.js";
10
10
  import { getPolicy } from "./policy.js";
11
11
  export const PROMPT_ARTIFACT_SCHEMA_VERSION = 1;
12
- export const BASE_PROMPT_VERSION = "2.0.0";
13
12
  export const DEFAULT_PROMPT_BUDGETS = {
14
13
  l0: 3000,
15
14
  l1: 7000,
@@ -39,19 +38,40 @@ function bulletsWithIds(values, prefix) {
39
38
  return `- [\`${id}\`] ${trimmed}`;
40
39
  }).join("\n");
41
40
  }
41
+ /** v3.3.1: Render an identity-HETEROGENEOUS list — the merged active
42
+ * constraint set holds hard/plan/criteria/discovered texts together, so no
43
+ * single prefix fits. Each item's authoritative ID comes from constraint
44
+ * metadata (criteria texts carry cr-XXXXXXXX, everything else
45
+ * c-XXXXXXXX), keeping the active list in the same identity namespace as
46
+ * the item's own section — one text never renders under two prefixes in
47
+ * one prompt. Falls back to a c- derivation only when metadata is missing
48
+ * (defensive: metadata covers every active text by construction). */
49
+ function bulletsWithIdentities(values, metadata) {
50
+ const idEnabled = getPolicy().evolution.constraint_id_enabled;
51
+ if (!idEnabled)
52
+ return bullets(values);
53
+ const idByText = new Map(metadata.map((meta) => [meta.text, meta.id]));
54
+ return values.map((v) => {
55
+ const trimmed = v.trim();
56
+ if (STABLE_ID_RE.test(trimmed)) {
57
+ return `- [\`${trimmed}\`] ${trimmed}`;
58
+ }
59
+ const id = idByText.get(trimmed) ?? `c-${deriveItemId(trimmed)}`;
60
+ return `- [\`${id}\`] ${trimmed}`;
61
+ }).join("\n");
62
+ }
42
63
  /** v3.2: Short imperative fix/action per verification check, appended to the
43
64
  * L1/L2 Verification Gate lines. Deliberately shorter than the rejection-
44
65
  * prompt wording in enforcement-gate.ts; keys are the plain check-name
45
66
  * strings (NOT imported from verification-gate.ts — it already imports
46
67
  * loop-compiler, which imports this module, so importing back would cycle).
47
- * Full coverage of the 19 CHECK_* constants is enforced by a test. */
68
+ * Full coverage of every CHECK_* constant is enforced by a test that
69
+ * iterates the verification-gate module's CHECK_ exports (no hardcoded
70
+ * list — a new constant can never silently lack an action entry). */
48
71
  const VERIFICATION_ACTIONS = {
49
- progress_regression: "Correct progress_estimate or explain the change in output_summary.",
50
- empty_change_with_passing: "Report the actual files_changed, or set success=false with the real state.",
51
72
  success_with_remaining_criteria: "Complete the remaining criteria before claiming success, or set success=false and report what remains.",
52
- success_without_verified_evidence: "Back the success claim with machine-verifiable evidence: run tests or a required verification command and report the actual output.",
73
+ success_without_verified_evidence: "Back the success claim with machine-verifiable evidence: run tests or a required verification command and report the actual output. If you genuinely performed no work, provide execution_evidence and declare no_change_reason.",
53
74
  outcome_success_contradiction: "Reconcile the declared outcome with the success flag and resubmit an honest evaluation.",
54
- success_claim_conflict: "Align the success claim with your own evaluation data before resubmitting.",
55
75
  blocked_without_blocker: "State the concrete blocker, or drop the blocked outcome.",
56
76
  retroactive_claim_bad_round: "Correct the referenced round number, or withdraw the retroactive claim.",
57
77
  retroactive_claim_unverified: "Provide evidence for the retroactive claim, or withdraw it.",
@@ -65,6 +85,17 @@ const VERIFICATION_ACTIONS = {
65
85
  subgoal_drift: "Reconcile the sub-goal list with your reported work before resubmitting.",
66
86
  backtrack_workspace_not_restored: "Restore the skipped-round files and the expected git HEAD, then resubmit.",
67
87
  criteria_claims_unverified: "Provide evidence for each claimed success criterion.",
88
+ // v3.3 — Round Contract checks (and the three v3.3 checks that predated
89
+ // this table entry and fell back to the generic action).
90
+ verification_entrypoint_modified: "Re-run the verification command on the changed entrypoint, or revert the entrypoint change and re-run.",
91
+ test_files_modified: "Re-run the verification command after the test-file changes and report the fresh output.",
92
+ round_underspecified: "Declare this round's contract with a non-empty done_when list — what must be true when the round is done?",
93
+ round_unverifiable: "Name only configured, enabled evidence.commands in verification_plan, or drop done_when items that cannot be machine-verified.",
94
+ round_scope_drift: "Revert the out-of-scope file changes, or extend the contract scope to cover them and explain why in drift_clarification.",
95
+ premature_boundary: "Run the contract's verification_plan commands and report real output before claiming done_when met; or set success=false and list the items in success_criteria_remaining (no_change_reason does not apply to contract claims).",
96
+ // v3.5 — contract completion machine-backing + premature-replacement warn
97
+ contract_completion_unverified: "Completion under the ACTIVE Round Contract requires its verification_plan commands to pass this round — fix the underlying failure so they pass and resubmit. Completion claims are not accepted without machine verification (no_change_reason does not apply).",
98
+ contract_premature: "The ACTIVE contract is still open — restate it unchanged to continue it; a different contract is ignored until the active one is completed or blocked.",
68
99
  };
69
100
  /** Fallback for unmapped check names (forward-compat with future checks). */
70
101
  const VERIFICATION_ACTION_FALLBACK = "Re-examine the flagged claim and correct your evaluation before resubmitting.";
@@ -250,7 +281,7 @@ function l1Sections(state, baseline, emphasized) {
250
281
  if (renderFull) {
251
282
  sections.push({
252
283
  id: "active_constraints",
253
- text: section("Active Constraints / Success Criteria", bulletsWithIds(active, "c")),
284
+ text: section("Active Constraints / Success Criteria", bulletsWithIdentities(active, state.constraintMetadata)),
254
285
  mandatory: false,
255
286
  });
256
287
  }
@@ -366,6 +397,15 @@ function l1Sections(state, baseline, emphasized) {
366
397
  mandatory: false,
367
398
  });
368
399
  }
400
+ // v3.3: Forward-looking roadmap (same buildRoadmap as state.md and L2).
401
+ const roadmapLines = buildRoadmap(state);
402
+ if (roadmapLines.length > 0) {
403
+ sections.push({
404
+ id: "roadmap",
405
+ text: section("Roadmap", roadmapLines.join("\n")),
406
+ mandatory: false,
407
+ });
408
+ }
369
409
  // v3.2: Recurring Issues carries the deterministic violation lessons (with
370
410
  // counts and rounds) when available; falls back to the rolling window text.
371
411
  if (state.recurringIssues.length > 0 || state.lessons.length > 0) {
@@ -416,12 +456,19 @@ function l2Sections(state, fullStateMarkdown) {
416
456
  ["failed_patterns", "Failed Patterns", state.failedPatterns, null],
417
457
  ];
418
458
  for (const [id, title, values, idPrefix] of groups) {
419
- if (values.length > 0) {
420
- const body = idPrefix
459
+ if (values.length === 0)
460
+ continue;
461
+ // v3.3.1: the active set is identity-heterogeneous (hard/plan/criteria/
462
+ // discovered merged) — render through constraint metadata so criteria
463
+ // texts keep their cr- identity here too (same namespace as their own
464
+ // Success Criteria section). Every other group is homogeneous and keeps
465
+ // its single-prefix renderer.
466
+ const body = id === "active_constraints"
467
+ ? bulletsWithIdentities(values, state.constraintMetadata)
468
+ : idPrefix
421
469
  ? bulletsWithIds(values, idPrefix)
422
470
  : bullets(values);
423
- sections.push({ id, text: section(title, body), mandatory: false });
424
- }
471
+ sections.push({ id, text: section(title, body), mandatory: false });
425
472
  }
426
473
  // v3.2: Lessons learned — repeated violations / verification failures
427
474
  // across the whole loop (not just the rolling window).
@@ -441,14 +488,22 @@ function l2Sections(state, fullStateMarkdown) {
441
488
  mandatory: false,
442
489
  });
443
490
  }
491
+ // v3.3: Forward-looking roadmap — rendered right before the (backward-
492
+ // looking) Phase History so the agent sees where it is before where it's
493
+ // been. Same buildRoadmap as state.md and L1.
494
+ const roadmapLines = buildRoadmap(state);
495
+ if (roadmapLines.length > 0) {
496
+ sections.push({
497
+ id: "roadmap",
498
+ text: section("Roadmap", roadmapLines.join("\n")),
499
+ mandatory: false,
500
+ });
501
+ }
444
502
  if (state.milestones.length > 0) {
445
503
  const milestoneText = state.milestones
446
504
  .map((m) => {
447
- const kindIcon = m.kind === "agent_declared"
448
- ? "🏁"
449
- : m.kind === "criteria_milestone" ? "✅" : "📍";
450
505
  const lines = [
451
- `**${kindIcon} ${m.label}** (Rounds ${m.round_range.start}–${m.round_range.end}, ${(m.progress_at_boundary * 100).toFixed(0)}%)`,
506
+ milestoneHeading(m),
452
507
  ` Outcome: ${m.outcome}`,
453
508
  ];
454
509
  if (m.resolved_constraints.length > 0) {
@@ -507,13 +562,6 @@ function l2Sections(state, fullStateMarkdown) {
507
562
  mandatory: false,
508
563
  });
509
564
  }
510
- if (state.loopSynthesis) {
511
- sections.push({
512
- id: "loop_synthesis",
513
- text: section("Loop Summary", state.loopSynthesis),
514
- mandatory: false,
515
- });
516
- }
517
565
  if (state.nextAction) {
518
566
  sections.push({
519
567
  id: "next_action",
@@ -534,7 +582,8 @@ function l2Sections(state, fullStateMarkdown) {
534
582
  state.progress.criteriaMet.length > 0 ||
535
583
  state.progress.criteriaRemaining.length > 0 ||
536
584
  state.progress.filesChanged.length > 0 ||
537
- state.progress.tests !== null) {
585
+ state.progress.tests !== null ||
586
+ state.machineStatus !== undefined) {
538
587
  const lines = [];
539
588
  const total = state.progress.criteriaMet.length + state.progress.criteriaRemaining.length;
540
589
  if (total > 0) {
@@ -542,6 +591,22 @@ function l2Sections(state, fullStateMarkdown) {
542
591
  }
543
592
  if (state.progress.estimate !== null) {
544
593
  lines.push(`**Estimated Completion**: ${(state.progress.estimate * 100).toFixed(0)}%`);
594
+ // v3.3: honest labeling — the estimate is the agent's own number.
595
+ lines.push("**Signal source**: self-reported estimate (unverified until machine-backed)");
596
+ }
597
+ // v3.3: machine side of the comparison — git motion over committed
598
+ // rounds (v3.6: the object always carries definite values — no
599
+ // "unavailable" arm).
600
+ if (state.machineStatus) {
601
+ const ms = state.machineStatus;
602
+ const motion = ms.gitMotion
603
+ ? `changes in ${ms.motionRounds}/${ms.windowRounds} recent committed rounds`
604
+ : `no git changes in the last ${ms.windowRounds} committed rounds`;
605
+ lines.push(`**Machine (git)**: ${motion}`);
606
+ }
607
+ if (state.criterionStatuses.length > 0) {
608
+ const metCount = state.criterionStatuses.filter((cs) => cs.status === "met").length;
609
+ lines.push(`**Machine (criteria)**: ${metCount}/${state.criterionStatuses.length} met across committed rounds`);
545
610
  }
546
611
  if (state.progress.tests) {
547
612
  lines.push(`**Tests**: ${state.progress.tests.passed} passed, ` +
@@ -577,6 +642,32 @@ function l2Sections(state, fullStateMarkdown) {
577
642
  mandatory: false,
578
643
  });
579
644
  }
645
+ // v3.3: Per-round statistics — lets the agent calibrate round granularity
646
+ // (files per round, rejected attempts, self-reported progress deltas).
647
+ // v3.6: source-labeled — files/Δ are agent-reported; only rejected
648
+ // attempts (and the Machine (git) row above) are machine-recorded.
649
+ if (state.roundStats && state.roundStats.length > 0) {
650
+ const statLines = state.roundStats.map((stat) => {
651
+ const parts = [];
652
+ if (stat.filesChangedCount !== null) {
653
+ parts.push(`${stat.filesChangedCount} file${stat.filesChangedCount === 1 ? "" : "s"}`);
654
+ }
655
+ if (stat.rejectedAttempts !== null && stat.rejectedAttempts > 0) {
656
+ parts.push(`${stat.rejectedAttempts} rejected attempt${stat.rejectedAttempts === 1 ? "" : "s"}`);
657
+ }
658
+ if (stat.progressDelta !== null) {
659
+ parts.push(`Δ${stat.progressDelta >= 0 ? "+" : ""}${stat.progressDelta.toFixed(2)}`);
660
+ }
661
+ return `- R${stat.round}: ${parts.length > 0 ? parts.join(", ") : "no data"}`;
662
+ });
663
+ statLines.push("> files & Δ are agent-reported; rejected attempts are machine-recorded " +
664
+ "(the Machine (git) row above is machine-observed)");
665
+ sections.push({
666
+ id: "round_stats",
667
+ text: section("Round Stats", statLines.join("\n")),
668
+ mandatory: false,
669
+ });
670
+ }
580
671
  // Retired Constraints
581
672
  if (state.retiredConstraints.length > 0) {
582
673
  const lines = state.retiredConstraints.map((c) => `- ~${c}~ (retired)`);
@@ -586,35 +677,10 @@ function l2Sections(state, fullStateMarkdown) {
586
677
  mandatory: false,
587
678
  });
588
679
  }
589
- // Inactive Constraints (with decay metadata)
590
- if (state.inactiveConstraints.length > 0) {
591
- const lines = [
592
- "> Demoted after prolonged inactivity. Auto-reactivated if violated again.",
593
- "",
594
- ];
595
- for (const c of state.inactiveConstraints) {
596
- const meta = state.constraintMetadata.find((m) => m.text === c);
597
- const age = meta
598
- ? state.round - (meta.last_violated_at_round || meta.discovered_at_round || 0)
599
- : "?";
600
- const lastV = meta?.last_violated_at_round
601
- ? `last violated R${meta.last_violated_at_round}`
602
- : "never violated";
603
- lines.push(`- ${c} (${lastV}, ${age} rounds inactive)`);
604
- }
605
- sections.push({
606
- id: "inactive_constraints",
607
- text: section("Inactive Constraints", lines.join("\n")),
608
- mandatory: false,
609
- });
610
- }
611
680
  // Agent Trust Score + Trend
612
681
  if (state.agentTrustScore !== undefined) {
613
- const barLen = 10;
614
- const filled = Math.round(state.agentTrustScore * barLen);
615
- const bar = "█".repeat(filled) + "░".repeat(barLen - filled);
616
682
  const lines = [
617
- `${bar} ${(state.agentTrustScore * 100).toFixed(0)}%`,
683
+ trustBarLine(state.agentTrustScore),
618
684
  ];
619
685
  if (state.agentTrustTrend.length > 0) {
620
686
  const avg = (state.agentTrustTrend.reduce((a, b) => a + b, 0)
@@ -631,9 +697,6 @@ function l2Sections(state, fullStateMarkdown) {
631
697
  return sections;
632
698
  }
633
699
  function selectSections(input, emphasized) {
634
- if (input.mode === "full") {
635
- return l2Sections(input.state, input.fullStateMarkdown);
636
- }
637
700
  if (input.level === "l0")
638
701
  return l0Sections(input.state);
639
702
  if (input.level === "l1") {
@@ -693,7 +756,7 @@ function matchEmphasize(targets, candidates, cap) {
693
756
  }
694
757
  /** Render the "Confusion Alerts" section that appears at the prompt top.
695
758
  * Compiler auto-matches each confusion point against state sections. */
696
- function renderConfusionAlerts(points, state, expandAll) {
759
+ function renderConfusionAlerts(points, state, fullDensity) {
697
760
  if (points.length === 0)
698
761
  return "";
699
762
  const lines = [
@@ -712,11 +775,10 @@ function renderConfusionAlerts(points, state, expandAll) {
712
775
  { keyword: "criteria", label: "Success Criteria", ref: "## Success Criteria" },
713
776
  { keyword: "phase", label: "Phase History", ref: "## Phase History" },
714
777
  { keyword: "objective", label: "Loop Objective", ref: "## Loop Objective" },
715
- { keyword: "loop", label: "Loop Summary", ref: "## Loop Summary" },
716
778
  ];
717
779
  // v2.14: L2 renders up to the policy cap (was: every confusion point,
718
780
  // unbounded by max_confusion_points); L1 keeps a single alert.
719
- const shown = expandAll
781
+ const shown = fullDensity
720
782
  ? points.slice(0, getPolicy().prompt.max_confusion_points)
721
783
  : points.slice(0, 1);
722
784
  for (const point of shown) {
@@ -738,7 +800,7 @@ function renderConfusionAlerts(points, state, expandAll) {
738
800
  lines.push(` ${pointer}`);
739
801
  lines.push("");
740
802
  }
741
- if (!expandAll && points.length > 1) {
803
+ if (!fullDensity && points.length > 1) {
742
804
  lines.push(`*… and ${points.length - 1} more confusion point(s) — see state file.*`);
743
805
  lines.push("");
744
806
  }
@@ -761,121 +823,15 @@ function renderCriticalContext(emphasized, state) {
761
823
  lines.push("");
762
824
  return lines.join("\n");
763
825
  }
764
- /** Expand a specific section to L2-level detail within a L1 prompt.
765
- * Returns the expanded section text, or empty string if the section
766
- * has no content to expand. */
767
- function renderExpandedSection(sectionName, state) {
768
- switch (sectionName) {
769
- case "milestones": {
770
- if (state.milestones.length === 0)
771
- return "";
772
- const text = state.milestones
773
- .map((m) => {
774
- const icon = m.kind === "agent_declared" ? "🏁" : m.kind === "criteria_milestone" ? "✅" : "📍";
775
- const lines = [
776
- `**${icon} ${m.label}** (R${m.round_range.start}–R${m.round_range.end}, ${(m.progress_at_boundary * 100).toFixed(0)}%)`,
777
- ` Outcome: ${m.outcome}`,
778
- ];
779
- if (m.carried_constraints.length > 0)
780
- lines.push(` Carried: ${m.carried_constraints.slice(0, 3).join("; ")}`);
781
- if (m.resolved_constraints.length > 0)
782
- lines.push(` Resolved: ${m.resolved_constraints.slice(0, 3).join("; ")}`);
783
- return lines.join("\n");
784
- })
785
- .join("\n\n");
786
- return section("Expanded Phase History", text);
787
- }
788
- case "sub_goals": {
789
- if (state.subGoals.length === 0)
790
- return "";
791
- const lines = [];
792
- for (const sg of state.subGoals) {
793
- const icon = sg.status === "in_progress" ? "🔄" :
794
- sg.status === "done" ? "✅" :
795
- sg.status === "blocked" ? "🚫" :
796
- sg.status === "canceled" ? "❌" : "⏳";
797
- const stale = sg.status === "pending" && state.round - sg.declared_at_round >= 10 ? " ⚠️ stale" : "";
798
- const detail = sg.status === "done" && sg.completed_at_round
799
- ? ` (done, R${sg.completed_at_round})`
800
- : sg.status === "in_progress" ? ` (since R${sg.status_changed_at_round})`
801
- : sg.status === "pending" ? ` (since R${sg.declared_at_round})`
802
- : sg.status === "blocked" ? ` (blocked R${sg.status_changed_at_round})`
803
- : ` (canceled R${sg.status_changed_at_round})`;
804
- lines.push(`- ${icon} [\`${sg.id}\`] ${sg.description}${detail}${stale}`);
805
- }
806
- return section("Expanded Sub-Goal Dashboard", lines.join("\n"));
807
- }
808
- case "constraint_lifecycle": {
809
- if (state.inactiveConstraints.length === 0 && state.retiredConstraints.length === 0)
810
- return "";
811
- const lines = [];
812
- if (state.inactiveConstraints.length > 0) {
813
- lines.push("**Inactive (auto-decayed):**");
814
- for (const c of state.inactiveConstraints) {
815
- const meta = state.constraintMetadata.find((m) => m.text === c);
816
- const age = meta ? state.round - (meta.last_violated_at_round || meta.discovered_at_round || 0) : "?";
817
- lines.push(`- ${c} (${age} rounds inactive)`);
818
- }
819
- }
820
- if (state.retiredConstraints.length > 0) {
821
- lines.push("**Retired:**");
822
- for (const c of state.retiredConstraints)
823
- lines.push(`- ~${c}~`);
824
- }
825
- return section("Constraint Lifecycle", lines.join("\n"));
826
- }
827
- case "agent_trust": {
828
- if (state.agentTrustScore === undefined)
829
- return "";
830
- const barLen = 10;
831
- const filled = Math.round(state.agentTrustScore * barLen);
832
- const bar = "█".repeat(filled) + "░".repeat(barLen - filled);
833
- const lines = [
834
- `${bar} ${(state.agentTrustScore * 100).toFixed(0)}%`,
835
- ];
836
- if (state.agentTrustTrend.length > 0) {
837
- lines.push(`Trend: ${state.agentTrustTrend.join(" → ")}`);
838
- }
839
- return section("Agent Trust (Expanded)", lines.join("\n"));
840
- }
841
- case "progress": {
842
- const p = state.progress;
843
- if (p.estimate === null && p.criteriaMet.length === 0 && p.criteriaRemaining.length === 0)
844
- return "";
845
- const lines = [];
846
- const total = p.criteriaMet.length + p.criteriaRemaining.length;
847
- if (total > 0)
848
- lines.push(`**Criteria**: ${p.criteriaMet.length}/${total} met`);
849
- if (p.estimate !== null)
850
- lines.push(`**Estimate**: ${(p.estimate * 100).toFixed(0)}%`);
851
- if (p.tests)
852
- lines.push(`**Tests**: ${p.tests.passed} passed, ${p.tests.failed} failed, ${p.tests.skipped} skipped`);
853
- if (p.filesChanged.length > 0) {
854
- lines.push("**Files Changed**:");
855
- for (const f of p.filesChanged.slice(0, 10))
856
- lines.push(`- ${f}`);
857
- }
858
- return section("Progress Dashboard (Expanded)", lines.join("\n"));
859
- }
860
- case "loop_synthesis": {
861
- if (!state.loopSynthesis)
862
- return "";
863
- return section("Loop Summary", state.loopSynthesis);
864
- }
865
- default:
866
- return "";
867
- }
868
- }
869
826
  export function assemblePromptArtifact(input) {
870
- const mode = input.mode ?? "adaptive";
871
827
  const budgets = { ...DEFAULT_PROMPT_BUDGETS, ...input.budgets };
872
828
  const budget = budgets[input.level];
873
829
  const stateHash = hashCanonicalState(input.state);
874
830
  const attempt = Math.max(1, input.attempt ?? 1);
875
831
  // ── State file reference + conditional read instruction ────────────
876
- // L2 rounds (full rehydration), recovery, rejection, drift, and pointer
877
- // mode all benefit from the model reading the durable state file rather
878
- // than relying on potentially-compacted conversation history.
832
+ // L2 rounds (full rehydration), recovery, rejection, and drift all
833
+ // benefit from the model reading the durable state file rather than
834
+ // relying on potentially-compacted conversation history.
879
835
  const fullContextReasons = new Set([
880
836
  "first_round", "plan_boundary", "checkpoint_boundary", "goal_changed",
881
837
  "missing_previous_state", "verification_contradicted",
@@ -883,7 +839,6 @@ export function assemblePromptArtifact(input) {
883
839
  "periodic_refresh",
884
840
  ]);
885
841
  const needsFullContext = input.level === "l2"
886
- || input.mode === "pointer"
887
842
  || input.reasons.some((r) => fullContextReasons.has(r));
888
843
  const stateFilePath = input.state.stateFilePath;
889
844
  // v3.2: L1 annotates the pointer with the round the freshly-regenerated
@@ -1005,11 +960,6 @@ export function assemblePromptArtifact(input) {
1005
960
  milestoneRanges: input.state.milestones.map((m) => [m.round_range.start, m.round_range.end]),
1006
961
  };
1007
962
  }
1008
- // Expand: L1 only, max 1 section (L2 already expanded, L0 ignored)
1009
- const expandCap = levelIsL1 ? policy.max_expand_l1 : 0;
1010
- const expandedSection = (pr?.expand?.length && expandCap > 0)
1011
- ? renderExpandedSection(pr.expand[0], input.state)
1012
- : "";
1013
963
  // Inject confusion alerts into the header area (before mandatory sections)
1014
964
  const fixedText = header + confusionText;
1015
965
  // Inject critical context as a mandatory section
@@ -1024,34 +974,17 @@ export function assemblePromptArtifact(input) {
1024
974
  ...sections,
1025
975
  ];
1026
976
  }
1027
- const selected = mode === "pointer"
1028
- ? renderWithinBudget(augmentedSections.filter((item) => item.mandatory), fixedText, budget)
1029
- : renderWithinBudget(augmentedSections, fixedText, budget - footer.length - expandedSection.length);
1030
- const renderedPrompt = selected.rendered + expandedSection + footer;
1031
- const includedSections = [
1032
- ...selected.included,
1033
- ...(confusionText ? ["confusion_alerts"] : []),
1034
- ...(criticalContextText ? ["critical_context"] : []),
1035
- ...(expandedSection ? [`expand:${pr?.expand?.[0] ?? ""}`] : []),
1036
- ...(pointer ? ["state_pointer"] : []),
1037
- ...(evaluation ? ["self_evaluation"] : []),
1038
- ];
977
+ const selected = renderWithinBudget(augmentedSections, fixedText, budget - footer.length);
978
+ const renderedPrompt = selected.rendered + footer;
1039
979
  const promptHash = createHash("sha256").update(renderedPrompt).digest("hex");
1040
980
  return {
1041
981
  schemaVersion: PROMPT_ARTIFACT_SCHEMA_VERSION,
1042
982
  roundId: `loop:${input.state.loopId}:round:${input.state.round}`,
1043
983
  attempt,
1044
984
  level: input.level,
1045
- levelReasons: [...input.reasons],
1046
985
  renderedPrompt,
1047
986
  promptHash,
1048
987
  stateHash,
1049
- basePromptVersion: BASE_PROMPT_VERSION,
1050
- includedSections,
1051
- budgetChars: budget,
1052
- charCount: renderedPrompt.length,
1053
- budgetExceeded: renderedPrompt.length > budget,
1054
- generatedAt: Date.now(),
1055
988
  ...(presentedState ? { presentedState } : {}),
1056
989
  };
1057
990
  }