loopforge 1.12.0 → 1.15.1

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 +12 -8
  3. package/dist/backends/fs.js.map +1 -1
  4. package/dist/builder.d.ts +8 -1
  5. package/dist/builder.d.ts.map +1 -1
  6. package/dist/builder.js +28 -72
  7. package/dist/builder.js.map +1 -1
  8. package/dist/enforcement-gate.d.ts +44 -0
  9. package/dist/enforcement-gate.d.ts.map +1 -0
  10. package/dist/enforcement-gate.js +253 -0
  11. package/dist/enforcement-gate.js.map +1 -0
  12. package/dist/engine.d.ts +11 -4
  13. package/dist/engine.d.ts.map +1 -1
  14. package/dist/engine.js +87 -118
  15. package/dist/engine.js.map +1 -1
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +4 -4
  19. package/dist/index.js.map +1 -1
  20. package/dist/loop-compiler.d.ts.map +1 -1
  21. package/dist/loop-compiler.js +689 -480
  22. package/dist/loop-compiler.js.map +1 -1
  23. package/dist/mcp/session.d.ts +9 -1
  24. package/dist/mcp/session.d.ts.map +1 -1
  25. package/dist/mcp/session.js +100 -44
  26. package/dist/mcp/session.js.map +1 -1
  27. package/dist/observability.d.ts +1 -1
  28. package/dist/observability.js +1 -1
  29. package/dist/policy.d.ts +72 -2
  30. package/dist/policy.d.ts.map +1 -1
  31. package/dist/policy.js +96 -2
  32. package/dist/policy.js.map +1 -1
  33. package/dist/protocol.d.ts +33 -2
  34. package/dist/protocol.d.ts.map +1 -1
  35. package/dist/protocol.js +22 -2
  36. package/dist/protocol.js.map +1 -1
  37. package/dist/runtime.d.ts +8 -4
  38. package/dist/runtime.d.ts.map +1 -1
  39. package/dist/runtime.js +102 -108
  40. package/dist/runtime.js.map +1 -1
  41. package/dist/tests/builder.test.js +5 -3
  42. package/dist/tests/builder.test.js.map +1 -1
  43. package/dist/tests/enforcement-gate.test.d.ts +2 -0
  44. package/dist/tests/enforcement-gate.test.d.ts.map +1 -0
  45. package/dist/tests/enforcement-gate.test.js +328 -0
  46. package/dist/tests/enforcement-gate.test.js.map +1 -0
  47. package/dist/tests/engine.test.js +9 -5
  48. package/dist/tests/engine.test.js.map +1 -1
  49. package/dist/tests/generate-schema.test.js +2 -2
  50. package/dist/tests/loop-compiler.test.js +28 -20
  51. package/dist/tests/loop-compiler.test.js.map +1 -1
  52. package/dist/tests/mcp.test.js +22 -2
  53. package/dist/tests/mcp.test.js.map +1 -1
  54. package/dist/tests/policy.test.js +1 -1
  55. package/dist/tests/policy.test.js.map +1 -1
  56. package/dist/tests/runtime.test.js +19 -5
  57. package/dist/tests/runtime.test.js.map +1 -1
  58. package/dist/verification-gate.d.ts +6 -0
  59. package/dist/verification-gate.d.ts.map +1 -1
  60. package/dist/verification-gate.js +3 -2
  61. package/dist/verification-gate.js.map +1 -1
  62. package/loop_policy.json +8 -3
  63. package/package.json +1 -1
  64. package/skills/perception/SKILL.md +35 -17
  65. package/skills/prompt-techniques/SKILL.md +52 -52
  66. package/skills/prompt-techniques/references/chain-of-thought.md +207 -207
  67. package/skills/prompt-techniques/references/few-shot.md +174 -174
  68. package/skills/prompt-techniques/references/least-to-most.md +182 -182
  69. package/skills/prompt-techniques/references/step-back.md +160 -160
  70. package/skills/prompt-techniques/references/tree-of-thought.md +191 -191
  71. package/skills/prompt-techniques/references/zero-shot.md +97 -97
@@ -13,7 +13,7 @@ import { createHash } from "node:crypto";
13
13
  import { readFileSync } from "node:fs";
14
14
  import { getPolicy } from "./policy.js";
15
15
  import { AgentStatus, makeCheckpointSummary, makeLoopCompileResponse, makeLoopHealth, makeLoopObjective, makeRollingSummary, makeTaskAlignment, } from "./protocol.js";
16
- import { routeTechniqueAdaptive } from "./builder.js";
16
+ import { routeTechniqueAdaptive, SKILLS_DIR } from "./builder.js";
17
17
  // ═══════════════════════════════════════════════════════════════════════════
18
18
  // Repair cue detection
19
19
  // ═══════════════════════════════════════════════════════════════════════════
@@ -251,6 +251,192 @@ function getRecentRounds(loopId, n, vaultContext) {
251
251
  rounds.sort((a, b) => b.round - a.round);
252
252
  return rounds.slice(0, n);
253
253
  }
254
+ /** Extract the technique used in the most recent round before currentRound.
255
+ * Scans vault entries for this loop_id and returns the technique_used field
256
+ * from the highest round number below currentRound.
257
+ * Returns null if no prior rounds exist (round 1 scenario). */
258
+ function getPreviousTechnique(loopId, currentRound, vaultContext) {
259
+ if (vaultContext === null || currentRound <= 1)
260
+ return null;
261
+ const results = vaultContext.results || [];
262
+ if (!results.length)
263
+ return null;
264
+ let bestTechnique = null;
265
+ let bestRound = -1;
266
+ for (const r of results) {
267
+ const lineage = (r.loop_lineage || r.lineage || {});
268
+ if (lineage.loop_id !== loopId)
269
+ continue;
270
+ const rnd = lineage.round;
271
+ if (rnd >= currentRound || rnd <= bestRound)
272
+ continue;
273
+ // technique_used can be on the entry directly or in lineage
274
+ const technique = r.technique_used ??
275
+ lineage.technique_used ?? "";
276
+ if (technique) {
277
+ bestTechnique = technique;
278
+ bestRound = rnd;
279
+ }
280
+ }
281
+ return bestTechnique;
282
+ }
283
+ function techniqueChanged(loopId, newTechnique, currentRound, vaultContext) {
284
+ if (currentRound === 1)
285
+ return true; // Round 1: first selection is always a "change"
286
+ const prevTech = getPreviousTechnique(loopId, currentRound, vaultContext);
287
+ if (!prevTech)
288
+ return true; // No prior technique found → treat as change
289
+ return newTechnique !== prevTech;
290
+ }
291
+ /** Returns the mandatory state file read instruction that goes at the top
292
+ * of every compiled prompt. Tells the agent to read the loop state file
293
+ * before executing the round. */
294
+ function renderStateFilePointer(loopId) {
295
+ return [
296
+ `⚠️ **BEFORE anything else**: Read \`.loopforge/state/${loopId}-state.md\``,
297
+ "for accumulated loop state — objective, constraints, progress, and cross-round summary.",
298
+ "This file is rewritten each round and is the single source of truth for loop state.",
299
+ "",
300
+ ].join("\n");
301
+ }
302
+ /** Render the full loop state file as markdown. Called by compileL2() every round.
303
+ * The file is a snapshot — rewritten in full each round, not appended.
304
+ * All sections have hard size caps from policy to prevent bloat.
305
+ *
306
+ * Sections: Loop Objective, Progress, Cross-Round Summary, Active Constraints,
307
+ * Checkpoints, Delegation Summary. */
308
+ function renderStateFile(params) {
309
+ const lines = [];
310
+ const sfPolicy = getPolicy().state_file;
311
+ // ── Header ──
312
+ lines.push(`# LoopForge State — ${params.loopId}`);
313
+ lines.push("");
314
+ lines.push(`**Round**: ${params.currentRound}/${params.maxRounds}`);
315
+ lines.push("");
316
+ // ── Loop Objective ──
317
+ if (params.loopObjective) {
318
+ const lo = params.loopObjective;
319
+ lines.push("## Loop Objective");
320
+ if (lo.version && lo.version > 1) {
321
+ lines.push(`> Version ${lo.version}, created at Round ${lo.created_at_round}`);
322
+ }
323
+ lines.push("");
324
+ lines.push(`**Objective**: ${lo.objective}`);
325
+ lines.push("");
326
+ if (lo.success_criteria.length) {
327
+ lines.push("### Success Criteria");
328
+ for (const sc of lo.success_criteria) {
329
+ // Check if this criterion has been met (from execution_evidence)
330
+ lines.push(`- ${sc}`);
331
+ }
332
+ lines.push("");
333
+ }
334
+ if (lo.hard_constraints.length) {
335
+ lines.push("### Hard Constraints");
336
+ for (const hc of lo.hard_constraints) {
337
+ lines.push(`- ${hc}`);
338
+ }
339
+ lines.push("");
340
+ }
341
+ }
342
+ // ── Progress Dashboard ──
343
+ if (params.progressDashboard) {
344
+ lines.push(params.progressDashboard);
345
+ lines.push("");
346
+ }
347
+ // ── Cross-Round Summary ──
348
+ if (params.rollingSummary && params.rollingSummary.rounds_sampled > 0) {
349
+ const rs = params.rollingSummary;
350
+ const maxRounds = sfPolicy.max_summary_rounds;
351
+ lines.push(`## Cross-Round Summary (Last ${Math.min(rs.rounds_sampled, maxRounds)} Rounds)`);
352
+ lines.push("");
353
+ if (rs.key_outcomes.length) {
354
+ lines.push("**Key Outcomes**:");
355
+ for (const ko of rs.key_outcomes.slice(0, maxRounds)) {
356
+ lines.push(`- ${ko}`);
357
+ }
358
+ lines.push("");
359
+ }
360
+ if (rs.recurring_issues.length) {
361
+ lines.push("**Recurring Issues**:");
362
+ for (const ri of rs.recurring_issues) {
363
+ lines.push(`- ⚠️ ${ri}`);
364
+ }
365
+ lines.push("");
366
+ }
367
+ if (rs.failed_patterns && rs.failed_patterns.length) {
368
+ lines.push("**Failure Patterns**:");
369
+ for (const fp of rs.failed_patterns) {
370
+ lines.push(`- 🚫 ${fp}`);
371
+ }
372
+ lines.push("");
373
+ }
374
+ }
375
+ // ── Active Constraints ──
376
+ lines.push("## Active Constraints");
377
+ lines.push("");
378
+ if (params.constraints.length) {
379
+ for (const c of params.constraints) {
380
+ lines.push(`- ${c}`);
381
+ }
382
+ }
383
+ else {
384
+ lines.push("_(none)_");
385
+ }
386
+ lines.push("");
387
+ if (params.retiredConstraints.length) {
388
+ lines.push("## Retired Constraints");
389
+ lines.push("");
390
+ for (const c of params.retiredConstraints.slice(0, 10)) {
391
+ lines.push(`- ~${c}~`);
392
+ }
393
+ lines.push("");
394
+ }
395
+ // ── Checkpoints ──
396
+ if (params.checkpoints.length) {
397
+ const maxCp = sfPolicy.max_checkpoints;
398
+ const shown = params.checkpoints.slice(-maxCp).reverse(); // newest first
399
+ lines.push(`## Checkpoints (${shown.length})`);
400
+ lines.push("");
401
+ for (const cp of shown) {
402
+ lines.push(`### ${cp.label || `Round ${cp.declared_at_round}`}`);
403
+ lines.push(`> Declared at Round ${cp.declared_at_round}`);
404
+ lines.push("");
405
+ if (cp.outcome) {
406
+ lines.push(`**Outcome**: ${cp.outcome}`);
407
+ lines.push("");
408
+ }
409
+ if (cp.carried_constraints.length) {
410
+ lines.push("**Carried Constraints**:");
411
+ for (const cc of cp.carried_constraints) {
412
+ lines.push(`- ${cc}`);
413
+ }
414
+ lines.push("");
415
+ }
416
+ if (cp.resolved_constraints.length) {
417
+ lines.push("**Resolved Constraints**:");
418
+ for (const rc of cp.resolved_constraints) {
419
+ lines.push(`- ✅ ${rc}`);
420
+ }
421
+ lines.push("");
422
+ }
423
+ }
424
+ }
425
+ // ── Delegation Summary ──
426
+ if (params.delegationSummary) {
427
+ lines.push(params.delegationSummary);
428
+ }
429
+ // ── External Context (current round only) ──
430
+ if (params.externalContext) {
431
+ lines.push("---");
432
+ lines.push("");
433
+ lines.push("## External Context (This Round)");
434
+ lines.push("");
435
+ lines.push(params.externalContext);
436
+ lines.push("");
437
+ }
438
+ return lines.join("\n");
439
+ }
254
440
  function getPreviousRoundTask(loopId, roundNum, vaultContext) {
255
441
  const prev = getPreviousRound(loopId, roundNum, vaultContext);
256
442
  return prev?.task ?? "";
@@ -298,13 +484,6 @@ function countConsecutiveHashMismatches(loopId, vaultContext) {
298
484
  // ═══════════════════════════════════════════════════════════════════════════
299
485
  // Strategy collapse detection
300
486
  // ═══════════════════════════════════════════════════════════════════════════
301
- function strategyCollapse(loopId, vaultContext) {
302
- const recent = getRecentRounds(loopId, 3, vaultContext);
303
- if (recent.length < 3)
304
- return false;
305
- return recent.every((r) => r.success === false);
306
- }
307
- // ═══════════════════════════════════════════════════════════════════════════
308
487
  // Constraint Retirement (v3.5)
309
488
  // ═══════════════════════════════════════════════════════════════════════════
310
489
  function computeConstraintRetirement(activeConstraints, loopId, currentRound, vaultContext) {
@@ -739,6 +918,116 @@ function loadLatestCheckpoint(loopId, currentRound, vaultContext) {
739
918
  });
740
919
  }
741
920
  // ═══════════════════════════════════════════════════════════════════════════
921
+ // Shared state evolution (P0/P1/P5) — used by both compileL1 and compileL2
922
+ // ═══════════════════════════════════════════════════════════════════════════
923
+ /** Apply P1 objective refinement from last round's self-evaluation.
924
+ * Appends the refinement to the objective with a round marker.
925
+ * Returns the updated loop objective (or unchanged if no refinement). */
926
+ function applyObjectiveRefinement(loopObjective, lastRoundResult, currentRound) {
927
+ if (!loopObjective || !lastRoundResult?.objective_refinement)
928
+ return loopObjective;
929
+ const refinement = lastRoundResult.objective_refinement.trim();
930
+ if (!refinement)
931
+ return loopObjective;
932
+ const version = (loopObjective.version ?? 1) + 1;
933
+ const history = [...(loopObjective.refinement_history ?? []), refinement];
934
+ const policy = getPolicy();
935
+ const maxVersions = policy.evolution?.max_objective_versions ?? 10;
936
+ const prunedHistory = history.slice(-maxVersions);
937
+ const refinedObjective = `${loopObjective.objective} [R${currentRound}: ${refinement}]`;
938
+ return makeLoopObjective({
939
+ ...loopObjective,
940
+ objective: refinedObjective,
941
+ version,
942
+ refinement_history: prunedHistory,
943
+ });
944
+ }
945
+ /** Apply P0 discovered constraints and P5 retractions/revisions.
946
+ * Pure function — returns updated constraints, loop objective, and retired list. */
947
+ function applyConstraintEvolution(constraints, loopObjective, lastRoundResult, currentRound) {
948
+ const retired = [];
949
+ let lo = loopObjective;
950
+ let cs = [...constraints];
951
+ // P0: Merge discovered constraints
952
+ if (lastRoundResult?.discovered_constraints?.length) {
953
+ const policy = getPolicy();
954
+ const maxPerRound = policy.evolution?.max_discovered_constraints_per_round ?? 5;
955
+ const maxActive = policy.evolution?.max_active_constraints ?? 15;
956
+ const newConstraints = lastRoundResult.discovered_constraints
957
+ .slice(0, maxPerRound)
958
+ .filter((c) => !cs.includes(c));
959
+ cs = [...cs, ...newConstraints].slice(0, maxActive);
960
+ }
961
+ // P5a: Retracted constraints
962
+ if (lastRoundResult?.retracted_constraints?.length) {
963
+ for (const rc of lastRoundResult.retracted_constraints) {
964
+ const idx = cs.indexOf(rc);
965
+ if (idx >= 0) {
966
+ cs.splice(idx, 1);
967
+ retired.push(`[agent-retracted] ${rc}`);
968
+ }
969
+ }
970
+ }
971
+ // P5b: Revised success criteria
972
+ if (lastRoundResult?.revised_success_criteria?.length && lo) {
973
+ const sc = [...lo.success_criteria];
974
+ for (const rev of lastRoundResult.revised_success_criteria) {
975
+ const idx = sc.indexOf(rev.old);
976
+ if (idx >= 0)
977
+ sc[idx] = rev.new;
978
+ else
979
+ sc.push(rev.new);
980
+ }
981
+ const version = (lo.version ?? 1) + 1;
982
+ const history = [
983
+ ...(lo.refinement_history ?? []),
984
+ `R${currentRound}: revised ${lastRoundResult.revised_success_criteria.length} success criteria`,
985
+ ];
986
+ const updatedLo = makeLoopObjective({
987
+ ...lo,
988
+ success_criteria: sc,
989
+ version,
990
+ refinement_history: history,
991
+ });
992
+ // Rebuild constraints to include revised criteria, deduplicate
993
+ cs = [...new Set([
994
+ ...cs.filter((c) => !updatedLo.hard_constraints.includes(c) && !updatedLo.success_criteria.includes(c)),
995
+ ...updatedLo.hard_constraints,
996
+ ...updatedLo.success_criteria,
997
+ ])];
998
+ lo = updatedLo;
999
+ }
1000
+ return { constraints: cs, loopObjective: lo, retired };
1001
+ }
1002
+ /** Collect checkpoint summaries from vault entries for a given loop.
1003
+ * Shared between compileL1 and compileL2 — same vault scanning logic. */
1004
+ function collectVaultCheckpoints(vaultContext, loopId, excludeRounds) {
1005
+ const checkpoints = [];
1006
+ if (!vaultContext)
1007
+ return checkpoints;
1008
+ const results = vaultContext.results || [];
1009
+ for (const r of results) {
1010
+ const lineage = (r.loop_lineage || r.lineage || {});
1011
+ if (lineage.loop_id !== loopId)
1012
+ continue;
1013
+ const cp = r.checkpoint_summary;
1014
+ if (cp && typeof cp.declared_at_round === "number") {
1015
+ if (excludeRounds?.has(cp.declared_at_round))
1016
+ continue;
1017
+ if (checkpoints.some((c) => c.declared_at_round === cp.declared_at_round))
1018
+ continue;
1019
+ checkpoints.push(makeCheckpointSummary({
1020
+ label: cp.label ?? "",
1021
+ declared_at_round: cp.declared_at_round,
1022
+ outcome: cp.outcome ?? "",
1023
+ carried_constraints: cp.carried_constraints ?? [],
1024
+ resolved_constraints: cp.resolved_constraints ?? [],
1025
+ }));
1026
+ }
1027
+ }
1028
+ return checkpoints;
1029
+ }
1030
+ // ═══════════════════════════════════════════════════════════════════════════
742
1031
  // External Context formatting (v1.7 — memory system integration)
743
1032
  // ═══════════════════════════════════════════════════════════════════════════
744
1033
  /** Format an external context string for injection into an L2 prompt.
@@ -779,43 +1068,35 @@ export function decideLevel(request, vaultContext) {
779
1068
  return request.force_level;
780
1069
  }
781
1070
  }
782
- // Gate 2: First call or explicit plan input → full rebuild
783
- if (request.round === 1 || request.plan_source) {
1071
+ // ── L2 triggers: structural restarts ──────────────────────────────────
1072
+ // Gate 2: First call or explicit plan → full rebuild
1073
+ if (request.round === 1 || request.plan_source)
784
1074
  return "l2";
785
- }
786
- // Derive goal_id
1075
+ // Gate 3: Checkpoint boundary — subtask completed, re-plan
1076
+ if (request.last_round_result?.compression_checkpoint === true)
1077
+ return "l2";
1078
+ // Gate 4: goal_id stability
787
1079
  const goalId = deriveGoalId(request.loop_id, request.task, request.goal_id);
788
1080
  const prev = getPreviousRound(request.loop_id, request.round - 1, vaultContext);
789
1081
  if (prev === null)
790
1082
  return "l2";
791
- // Gate 3: goal_id stability
792
1083
  if (goalId !== prev.goal_id)
793
1084
  return "l2";
794
- // Gate 4: P0–P5 cognitive evolution signals → full recompile (v1.7)
795
- // Check BEFORE the simple repair/new-constraint gates because evolution signals
796
- // (constraint discovery, objective refinement, self-correction) need full L2
797
- // processing. Only structural evolution signals trigger L2 — execution_evidence
798
- // alone does not (it is processed via the rolling summary at L1).
799
- if (request.last_round_result) {
800
- const lr = request.last_round_result;
801
- if ((lr.discovered_constraints?.length ?? 0) > 0 ||
802
- (lr.objective_refinement?.trim().length ?? 0) > 0 ||
803
- (lr.emerged_subtasks?.length ?? 0) > 0 ||
804
- (lr.retracted_constraints?.length ?? 0) > 0 ||
805
- (lr.revised_success_criteria?.length ?? 0) > 0 ||
806
- (lr.wrong_assumptions?.length ?? 0) > 0) {
807
- return "l2";
808
- }
809
- }
810
- // Gate 5: Explicit failures or new constraints → patch
811
- const hasNewConstraints = request.constraints_from_plan.length > 0;
812
- const hasNewFailures = request.last_round_result !== null &&
813
- !request.last_round_result.success;
814
- const hasRepair = detectsRepairSignal(request);
815
- if (hasNewConstraints || hasNewFailures || hasRepair)
816
- return "l1";
817
- // Nothing triggered → fast path
818
- return "l0";
1085
+ // ── L0 trigger: honest failure with no new information ────────────────
1086
+ const lr = request.last_round_result;
1087
+ const prevFailed = lr !== null && !lr.success;
1088
+ const hasNoNewInfo = (lr?.discovered_constraints?.length ?? 0) === 0 &&
1089
+ (lr?.objective_refinement?.trim().length ?? 0) === 0 &&
1090
+ (lr?.emerged_subtasks?.length ?? 0) === 0 &&
1091
+ (lr?.retracted_constraints?.length ?? 0) === 0 &&
1092
+ (lr?.revised_success_criteria?.length ?? 0) === 0 &&
1093
+ (lr?.wrong_assumptions?.length ?? 0) === 0 &&
1094
+ request.constraints_from_plan.length === 0 &&
1095
+ !detectsRepairSignal(request);
1096
+ if (prevFailed && hasNoNewInfo)
1097
+ return "l0";
1098
+ // ── L1: default path — all P0-P5 state changes handled here ───────────
1099
+ return "l1";
819
1100
  }
820
1101
  // ═══════════════════════════════════════════════════════════════════════════
821
1102
  // LAYER 2 — Soft Advisories (NEVER change compile level)
@@ -977,11 +1258,6 @@ export function computeAdvisories(request, vaultContext) {
977
1258
  warnings.push(`goal_text_hash changed (${prev.goal_text_hash} → ${currentHash}) ` +
978
1259
  "but goal_id matched — wording drift detected");
979
1260
  }
980
- // Strategy collapse check
981
- if (strategyCollapse(request.loop_id, vaultContext)) {
982
- warnings.push("strategy_collapse: 3 consecutive failed rounds — " +
983
- "consider force_level=L2 rebuild");
984
- }
985
1261
  // Repair cue detection
986
1262
  if (detectsRepairSignal(request)) {
987
1263
  warnings.push("repair signal detected — L1 patch applied");
@@ -1170,6 +1446,96 @@ function computeLoopObjectiveFromTask(request, _vaultContext) {
1170
1446
  });
1171
1447
  }
1172
1448
  // ═══════════════════════════════════════════════════════════════════════════
1449
+ // P4: Shared consistency checks and progress dashboard (used by L1 & L2)
1450
+ // ═══════════════════════════════════════════════════════════════════════════
1451
+ /** Run P4 consistency validation checks on agent execution evidence.
1452
+ * Returns warning strings. Called by both compileL1 and compileL2.
1453
+ * Three checks:
1454
+ * 1. Agent claims file changes but files_changed is empty
1455
+ * 2. Agent reports success but test results show failures
1456
+ * 3. Progress estimate mismatch (objective vs subjective) */
1457
+ function runP4ConsistencyChecks(evidence, outputSummary, success, loopObjective) {
1458
+ const warnings = [];
1459
+ // Check 1: Agent claims action but reports no files changed
1460
+ if (evidence.files_changed.length === 0 && outputSummary) {
1461
+ const actionWords = /\b(fix|modif|chang|updat|refactor|implement|add|remov|delet|rewrit|修补|修改|更改|更新|重构|实现|添加|删除|重写)\b/i;
1462
+ if (actionWords.test(outputSummary)) {
1463
+ warnings.push("P4: Agent output claims changes but execution_evidence.files_changed is empty — possible oversight");
1464
+ }
1465
+ }
1466
+ // Check 2: Agent reports success but tests failed
1467
+ if (evidence.test_results && evidence.test_results.failed > 0 && success) {
1468
+ warnings.push(`P4: Agent reports success=true but ${evidence.test_results.failed} tests failed — claims may not match reality`);
1469
+ }
1470
+ // Check 3: Progress estimate mismatch with criteria tracking
1471
+ if (loopObjective && loopObjective.success_criteria.length > 0) {
1472
+ const metCount = evidence.success_criteria_met.length;
1473
+ const totalCount = loopObjective.success_criteria.length;
1474
+ const objectiveProgress = totalCount > 0 ? metCount / totalCount : 0;
1475
+ const subjectiveProgress = evidence.progress_estimate;
1476
+ const policy = getPolicy();
1477
+ const mismatchThreshold = policy.evolution?.progress_mismatch_threshold ?? 0.3;
1478
+ if (Math.abs(objectiveProgress - subjectiveProgress) > mismatchThreshold) {
1479
+ warnings.push(`P4: Progress estimate mismatch — agent estimates ${(subjectiveProgress * 100).toFixed(0)}% but ${metCount}/${totalCount} criteria met (${(objectiveProgress * 100).toFixed(0)}%)`);
1480
+ }
1481
+ }
1482
+ return warnings;
1483
+ }
1484
+ /** Build a progress dashboard string for prompt or state file injection.
1485
+ * Used by both compileL1 and compileL2. When showTrend is true, also
1486
+ * computes a trend arrow from vault history (L2 path). */
1487
+ function buildProgressDashboard(evidence, loopObjective, vaultContext, loopId, currentRound, showTrend) {
1488
+ const totalCriteria = loopObjective?.success_criteria.length ?? 0;
1489
+ const metCriteria = evidence.success_criteria_met.length;
1490
+ const remainingCriteria = evidence.success_criteria_remaining;
1491
+ const objProgress = totalCriteria > 0 ? metCriteria / totalCriteria : evidence.progress_estimate;
1492
+ const subjProgress = evidence.progress_estimate;
1493
+ const lines = [
1494
+ "",
1495
+ "### Progress Dashboard",
1496
+ "",
1497
+ `**Overall**: ${metCriteria}/${totalCriteria} criteria met (${(objProgress * 100).toFixed(0)}%)`,
1498
+ ];
1499
+ if (Math.abs(objProgress - subjProgress) > 0.05) {
1500
+ lines.push(`**Agent estimate**: ${(subjProgress * 100).toFixed(0)}%`);
1501
+ }
1502
+ if (evidence.files_changed.length) {
1503
+ lines.push(`**Files changed**: ${evidence.files_changed.join(", ")}`);
1504
+ }
1505
+ if (evidence.test_results) {
1506
+ const tr = evidence.test_results;
1507
+ lines.push(`**Tests**: ${tr.passed} passed, ${tr.failed} failed, ${tr.skipped} skipped`);
1508
+ }
1509
+ if (remainingCriteria.length) {
1510
+ lines.push("**Remaining criteria**:");
1511
+ for (const rc of remainingCriteria.slice(0, 5)) {
1512
+ lines.push(`- ${rc}`);
1513
+ }
1514
+ if (remainingCriteria.length > 5) {
1515
+ lines.push(`- ... and ${remainingCriteria.length - 5} more`);
1516
+ }
1517
+ }
1518
+ lines.push("");
1519
+ // Trend arrow — only computed in L2 path where full vault history is available
1520
+ if (showTrend && vaultContext && loopObjective) {
1521
+ const prevEvidence = getPreviousRoundEvidence(loopId, currentRound - 1, vaultContext);
1522
+ if (prevEvidence) {
1523
+ const trend = objProgress - (prevEvidence.success_criteria_met.length / Math.max(totalCriteria, 1));
1524
+ if (trend > 0.03) {
1525
+ lines.push(`**Trend**: ↑ advancing (+${(trend * 100).toFixed(0)}% this round)`);
1526
+ }
1527
+ else if (trend > -0.03) {
1528
+ lines.push("**Trend**: → stable");
1529
+ }
1530
+ else {
1531
+ lines.push(`**Trend**: ↓ regressing (${(trend * 100).toFixed(0)}% this round)`);
1532
+ }
1533
+ lines.push("");
1534
+ }
1535
+ }
1536
+ return lines.join("\n");
1537
+ }
1538
+ // ═══════════════════════════════════════════════════════════════════════════
1173
1539
  // Compilation — L0 / L1 / L2
1174
1540
  // ═══════════════════════════════════════════════════════════════════════════
1175
1541
  function compileL0(request, vaultContext, prevRound) {
@@ -1202,100 +1568,169 @@ function compileL0(request, vaultContext, prevRound) {
1202
1568
  return l2Response;
1203
1569
  }
1204
1570
  function compileL1(request, vaultContext, prevRound) {
1205
- const prev = prevRound ??
1206
- getPreviousRound(request.loop_id, request.round - 1, vaultContext);
1207
1571
  const goalId = deriveGoalId(request.loop_id, request.task, request.goal_id);
1208
- const newConstraints = [...request.constraints_from_plan];
1209
- let activeRaw = [...(prev?.constraints_active ?? []), ...newConstraints];
1210
- // Deduplicate preserving order
1211
- activeRaw = [...new Set(activeRaw)];
1212
- // v3.5: Constraint retirement
1213
- const { active, retired } = computeConstraintRetirement(activeRaw, request.loop_id, request.round, vaultContext);
1214
- // v3.5: Rolling summary
1572
+ const lr = request.last_round_result;
1573
+ // ── v1.14: Technique routing (Tier 1 only — 4 techniques) ────────────
1574
+ // L1 is the default path: technique evolves within Tier 1 unless
1575
+ // decideLevel already escalated to L2 (strategy collapse / checkpoint).
1576
+ const analysis = routeTechniqueAdaptive(request.task, vaultContext, request.loop_id,
1577
+ /* isCheckpoint */ false);
1578
+ const technique = analysis.technique;
1579
+ const referenceFile = analysis.reference_file;
1580
+ // Determine if technique changed from last round
1581
+ const techChanged = techniqueChanged(request.loop_id, technique, request.round, vaultContext);
1582
+ // ── Loop Objective: load from vault, apply P1 refinement ─────────────
1583
+ let loopObjective = null;
1584
+ const vaultLo = vaultGetLoopObjective(request.loop_id, vaultContext);
1585
+ if (vaultLo) {
1586
+ const loData = vaultLo.loop_objective;
1587
+ if (loData) {
1588
+ loopObjective = makeLoopObjective({
1589
+ objective: loData.objective ?? "",
1590
+ success_criteria: loData.success_criteria ?? [],
1591
+ hard_constraints: loData.hard_constraints ?? [],
1592
+ created_at_round: loData.created_at_round ?? 1,
1593
+ loop_id: loData.loop_id ?? request.loop_id,
1594
+ version: loData.version ?? 1,
1595
+ refinement_history: loData.refinement_history ?? [],
1596
+ });
1597
+ }
1598
+ }
1599
+ if (!loopObjective && request.loop_objective) {
1600
+ loopObjective = request.loop_objective;
1601
+ }
1602
+ // P1: Apply objective refinement (shared)
1603
+ loopObjective = applyObjectiveRefinement(loopObjective, lr, request.round);
1604
+ // ── Build active constraints (base + LO) ─────────────────────────────
1605
+ let constraints = [...request.constraints_from_plan];
1606
+ if (loopObjective) {
1607
+ constraints = [
1608
+ ...new Set([
1609
+ ...constraints,
1610
+ ...loopObjective.hard_constraints,
1611
+ ...loopObjective.success_criteria,
1612
+ ]),
1613
+ ];
1614
+ }
1615
+ // ── P0/P5: Constraint evolution (shared) ─────────────────────────────
1616
+ const { constraints: evolvedCs, loopObjective: evolvedLo, retired } = applyConstraintEvolution(constraints, loopObjective, lr, request.round);
1617
+ constraints = evolvedCs;
1618
+ loopObjective = evolvedLo;
1619
+ // ── Constraint retirement ────────────────────────────────────────────
1620
+ const { active, retired: autoRetired } = computeConstraintRetirement(constraints, request.loop_id, request.round, vaultContext);
1621
+ for (const r of autoRetired) {
1622
+ if (!retired.includes(r))
1623
+ retired.push(r);
1624
+ }
1625
+ // ── Rolling summary + P5c wrong assumptions ──────────────────────────
1215
1626
  const rollingSummary = buildRollingSummary(request.loop_id, request.round, vaultContext);
1627
+ if (lr?.wrong_assumptions?.length) {
1628
+ if (rollingSummary) {
1629
+ rollingSummary.key_outcomes = [
1630
+ ...rollingSummary.key_outcomes,
1631
+ ...lr.wrong_assumptions.map((a) => `Wrong assumption corrected: ${a}`),
1632
+ ];
1633
+ }
1634
+ }
1216
1635
  const rollingText = formatRollingSummaryForPrompt(rollingSummary);
1217
- const violations = request.last_round_result?.constraint_violations ?? [];
1218
- const diffParts = [];
1219
- if (newConstraints.length) {
1220
- diffParts.push(`new constraints: [${newConstraints.join(", ")}]`);
1636
+ // ── P2: Emerged subtasks → suggested next task ───────────────────────
1637
+ let suggestedNextTask = "";
1638
+ if (lr?.emerged_subtasks?.length) {
1639
+ suggestedNextTask = lr.emerged_subtasks.slice(0, 3).join("; ");
1221
1640
  }
1222
- if (retired.length) {
1223
- diffParts.push(`retired constraints: [${retired.join(", ")}]`);
1641
+ // ── P4: Consistency validation ───────────────────────────────────────
1642
+ const evidence = lr?.execution_evidence;
1643
+ const warnings = evidence
1644
+ ? runP4ConsistencyChecks(evidence, lr.output_summary, lr.success, loopObjective)
1645
+ : [];
1646
+ // ── Route to specialist (all 4 techniques available via Tier 1) ──────
1647
+ let prompt;
1648
+ if (technique === "step-back") {
1649
+ prompt = compileStepBack(request, goalId, active, loopObjective, rollingText, analysis, { embedSkeleton: techChanged, level: "l1" });
1224
1650
  }
1225
- if (violations.length) {
1226
- diffParts.push(`violations from last round: [${violations.join(", ")}]`);
1651
+ else if (technique === "least-to-most") {
1652
+ prompt = compileLeastToMost(request, goalId, active, loopObjective, rollingText, analysis, { embedSkeleton: techChanged, level: "l1" });
1227
1653
  }
1228
- if (request.new_since_last_round) {
1229
- diffParts.push(`delta: ${request.new_since_last_round.slice(0, 200)}`);
1654
+ else if (technique === "tree-of-thought") {
1655
+ prompt = compileToT(request, goalId, active, loopObjective, rollingText, analysis, { embedSkeleton: techChanged, level: "l1" });
1230
1656
  }
1231
- // Build patched prompt
1232
- const lines = [
1233
- `## Loop Round ${request.round} — L1 Patch`,
1234
- "",
1235
- `**Goal**: ${request.task}`,
1236
- `**Loop ID**: ${request.loop_id}`,
1237
- `**Goal ID**: ${goalId}`,
1238
- "",
1239
- ];
1240
- if (rollingText) {
1241
- lines.push(rollingText);
1242
- lines.push("");
1657
+ else {
1658
+ prompt = compileGeneric(request, goalId, active, loopObjective, rollingText, analysis, referenceFile, technique, { embedSkeleton: techChanged, level: "l1" });
1243
1659
  }
1244
- if (active.length) {
1245
- lines.push("### Active Constraints (inherited + new, pruned)");
1246
- for (const c of active)
1247
- lines.push(`- ${c}`);
1248
- lines.push("");
1660
+ // ── P4: Progress Dashboard ───────────────────────────────────────────
1661
+ const progressDashboard = (evidence && loopObjective)
1662
+ ? buildProgressDashboard(evidence, loopObjective, vaultContext, request.loop_id, request.round, /* showTrend */ false)
1663
+ : "";
1664
+ // ── v1.14: Render state file ─────────────────────────────────────────
1665
+ const sfPolicy = getPolicy().state_file;
1666
+ let stateFileContent;
1667
+ if (sfPolicy.enabled) {
1668
+ const allCheckpoints = collectVaultCheckpoints(vaultContext, request.loop_id);
1669
+ const delegationSummary = vaultContext
1670
+ ? buildDelegationSummary(vaultContext)
1671
+ : "";
1672
+ stateFileContent = renderStateFile({
1673
+ loopId: request.loop_id,
1674
+ currentRound: request.round,
1675
+ maxRounds: request.max_rounds ?? sfPolicy.max_summary_rounds * 2,
1676
+ loopObjective,
1677
+ rollingSummary,
1678
+ constraints: active,
1679
+ retiredConstraints: retired,
1680
+ checkpoints: allCheckpoints,
1681
+ delegationSummary,
1682
+ progressDashboard,
1683
+ externalContext: request.external_context,
1684
+ });
1685
+ prompt = renderStateFilePointer(request.loop_id) + "\n" + prompt;
1686
+ }
1687
+ // ── Build thin diff ──────────────────────────────────────────────────
1688
+ const violations = lr?.constraint_violations ?? [];
1689
+ const diffParts = [];
1690
+ if (request.constraints_from_plan.length) {
1691
+ diffParts.push(`new constraints: [${request.constraints_from_plan.join(", ")}]`);
1249
1692
  }
1250
1693
  if (retired.length) {
1251
- lines.push("### Retired Constraints (no recent activity)");
1252
- for (const c of retired)
1253
- lines.push(`- ~${c}~`);
1254
- lines.push("");
1694
+ diffParts.push(`retired constraints: [${retired.join(", ")}]`);
1255
1695
  }
1256
1696
  if (violations.length) {
1257
- lines.push("### Violations From Last Round (must fix)");
1258
- for (const v of violations)
1259
- lines.push(`- ${v}`);
1260
- lines.push("");
1697
+ diffParts.push(`violations: [${violations.join(", ")}]`);
1261
1698
  }
1262
1699
  if (request.new_since_last_round) {
1263
- lines.push("### What Changed Since Last Round");
1264
- lines.push(request.new_since_last_round);
1265
- lines.push("");
1266
- }
1267
- if (request.last_round_result?.output_summary) {
1268
- lines.push("### Last Round Summary");
1269
- lines.push(request.last_round_result.output_summary);
1270
- lines.push("");
1700
+ diffParts.push(`delta: ${request.new_since_last_round.slice(0, 200)}`);
1271
1701
  }
1272
- lines.push("### Task");
1273
- lines.push(request.task);
1702
+ // v1.14: When state file is enabled, progressDashboard lives in the state
1703
+ // file (see renderStateFile above). Only inline it in the prompt when the
1704
+ // state file is disabled — this keeps the prompt truly thin.
1705
+ const finalPrompt = sfPolicy.enabled ? prompt : prompt + progressDashboard;
1274
1706
  return makeLoopCompileResponse({
1275
1707
  status: AgentStatus.OK,
1276
- prompt: lines.join("\n"),
1708
+ prompt: finalPrompt,
1277
1709
  recompile_level: "l1",
1278
- diff_from_previous: diffParts.length ? diffParts.join("; ") : "Patch applied.",
1710
+ diff_from_previous: diffParts.length ? diffParts.join("; ") : "L1 continue — state updated in file.",
1279
1711
  lineage: [`${request.loop_id}:r${request.round}`],
1280
1712
  constraints_active: active,
1281
1713
  constraints_retired: retired,
1282
- technique_used: "patch",
1714
+ technique_used: technique,
1715
+ reference_file: referenceFile,
1283
1716
  rolling_summary: rollingSummary,
1284
1717
  loop_id: request.loop_id,
1285
1718
  round: request.round,
1286
1719
  goal_id: goalId,
1287
1720
  goal_text_hash: computeGoalTextHash(request.task),
1721
+ loop_objective: loopObjective,
1288
1722
  plan_source: request.plan_source,
1289
- warnings: [],
1723
+ suggested_next_task: suggestedNextTask,
1724
+ warnings,
1725
+ state_file_content: stateFileContent,
1290
1726
  });
1291
1727
  }
1292
1728
  export function compileL2(request, vaultContext) {
1293
1729
  const goalId = deriveGoalId(request.loop_id, request.task, request.goal_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);
1297
- const technique = analysis.technique;
1298
- const referenceFile = analysis.reference_file;
1730
+ // v1.15: L2 no longer auto-selects a technique. The Agent reads the technique
1731
+ // catalog (skills/prompt-techniques/SKILL.md), freely chooses the best fit
1732
+ // based on loop state, reads the corresponding reference file, and applies
1733
+ // it directly to the task.
1299
1734
  // v1.10: Checkpoint — load existing or build new from last round's self-eval
1300
1735
  let checkpointSummary = null;
1301
1736
  let checkpointRound = 0;
@@ -1359,26 +1794,9 @@ export function compileL2(request, vaultContext) {
1359
1794
  loopObjective = request.loop_objective;
1360
1795
  }
1361
1796
  }
1362
- // ── P1: Apply objective_refinement from last round ──
1363
- if (loopObjective && request.last_round_result?.objective_refinement) {
1364
- const refinement = request.last_round_result.objective_refinement.trim();
1365
- if (refinement) {
1366
- const version = (loopObjective.version ?? 1) + 1;
1367
- const history = [...(loopObjective.refinement_history ?? []), refinement];
1368
- // Enforce max versions from policy
1369
- const policy = getPolicy();
1370
- const maxVersions = policy.evolution?.max_objective_versions ?? 10;
1371
- const prunedHistory = history.slice(-maxVersions);
1372
- const refinedObjective = `${loopObjective.objective} [R${request.round}: ${refinement}]`;
1373
- loopObjective = makeLoopObjective({
1374
- ...loopObjective,
1375
- objective: refinedObjective,
1376
- version,
1377
- refinement_history: prunedHistory,
1378
- });
1379
- }
1380
- }
1381
- // ── Build active constraints (base + LO + P0 discovered) ──
1797
+ // ── P1: Apply objective refinement (shared) ──
1798
+ loopObjective = applyObjectiveRefinement(loopObjective, lr, request.round);
1799
+ // ── Build active constraints (base + LO) ──
1382
1800
  let constraints = [...request.constraints_from_plan];
1383
1801
  if (loopObjective) {
1384
1802
  constraints = [
@@ -1389,63 +1807,14 @@ export function compileL2(request, vaultContext) {
1389
1807
  ]),
1390
1808
  ];
1391
1809
  }
1392
- // P0: Merge discovered constraints from last round
1393
- if (request.last_round_result?.discovered_constraints?.length) {
1394
- const policy = getPolicy();
1395
- const maxPerRound = policy.evolution?.max_discovered_constraints_per_round ?? 5;
1396
- const maxActive = policy.evolution?.max_active_constraints ?? 15;
1397
- const newConstraints = request.last_round_result.discovered_constraints
1398
- .slice(0, maxPerRound)
1399
- .filter((c) => !constraints.includes(c));
1400
- constraints = [...constraints, ...newConstraints].slice(0, maxActive);
1401
- }
1402
1810
  // ═══════════════════════════════════════════════════════════════════════════
1403
- // P5: Self-correction — MUST process retractions/revisions BEFORE prompt build
1404
- // so the technician compiler sees the corrected constraints and loop objective.
1811
+ // P0/P5: Constraint evolution (shared) — retractions/revisions BEFORE prompt
1405
1812
  // ═══════════════════════════════════════════════════════════════════════════
1406
1813
  const warnings = [];
1407
- const retired = [];
1408
- // P5a: Retracted constraints — remove from active set
1409
- if (lr?.retracted_constraints?.length) {
1410
- for (const rc of lr.retracted_constraints) {
1411
- const idx = constraints.indexOf(rc);
1412
- if (idx >= 0) {
1413
- constraints.splice(idx, 1);
1414
- retired.push(`[agent-retracted] ${rc}`);
1415
- }
1416
- }
1417
- }
1418
- // P5b: Revised success criteria — update Loop Objective
1419
- if (lr?.revised_success_criteria?.length && loopObjective) {
1420
- const sc = [...loopObjective.success_criteria];
1421
- for (const rev of lr.revised_success_criteria) {
1422
- const idx = sc.indexOf(rev.old);
1423
- if (idx >= 0) {
1424
- sc[idx] = rev.new;
1425
- }
1426
- else {
1427
- sc.push(rev.new);
1428
- }
1429
- }
1430
- const version = (loopObjective.version ?? 1) + 1;
1431
- const history = [
1432
- ...(loopObjective.refinement_history ?? []),
1433
- `R${request.round}: revised ${lr.revised_success_criteria.length} success criteria`,
1434
- ];
1435
- const updatedLo = makeLoopObjective({
1436
- ...loopObjective,
1437
- success_criteria: sc,
1438
- version,
1439
- refinement_history: history,
1440
- });
1441
- // Rebuild constraints to include revised criteria
1442
- constraints = [...new Set([
1443
- ...constraints.filter((c) => !updatedLo.hard_constraints.includes(c) && !updatedLo.success_criteria.includes(c)),
1444
- ...updatedLo.hard_constraints,
1445
- ...updatedLo.success_criteria,
1446
- ])];
1447
- loopObjective = updatedLo;
1448
- }
1814
+ const evoResult = applyConstraintEvolution(constraints, loopObjective, lr, request.round);
1815
+ constraints = evoResult.constraints;
1816
+ loopObjective = evoResult.loopObjective;
1817
+ const retired = evoResult.retired;
1449
1818
  // ── v1.10: Build checkpoint summary if last round declared one ──
1450
1819
  if (checkpointRound > 0 && !checkpointSummary && lr) {
1451
1820
  // checkpointRound was set but no summary yet → fresh declaration
@@ -1461,20 +1830,11 @@ export function compileL2(request, vaultContext) {
1461
1830
  };
1462
1831
  checkpointSummary = buildCheckpointSummary(synthEval, checkpointRound, constraints, retired);
1463
1832
  }
1464
- // ── Route to technique-specific specialist (v1.1 deep integration) ──
1465
- let prompt;
1466
- if (technique === "step-back") {
1467
- prompt = compileStepBack(request, goalId, constraints, loopObjective, rollingText, analysis);
1468
- }
1469
- else if (technique === "least-to-most") {
1470
- prompt = compileLeastToMost(request, goalId, constraints, loopObjective, rollingText, analysis);
1471
- }
1472
- else if (technique === "tree-of-thought") {
1473
- prompt = compileToT(request, goalId, constraints, loopObjective, rollingText, analysis);
1474
- }
1475
- else {
1476
- prompt = compileGeneric(request, goalId, constraints, loopObjective, rollingText, analysis, referenceFile, technique);
1477
- }
1833
+ // ── v1.15: Build L2 prompt with Technique Selection block ──
1834
+ // The Agent freely chooses a technique by reading the catalog and reference
1835
+ // files. LoopForge provides the loop state (objective, constraints, rolling
1836
+ // summary) — the Agent provides the reasoning strategy.
1837
+ let prompt = buildL2Prompt(request, goalId, constraints, loopObjective, rollingText);
1478
1838
  // ── v1.10: Inject checkpoint block (before rolling summary in prompt) ──
1479
1839
  // The checkpoint is rendered before the external context so it appears
1480
1840
  // in a natural position: after Loop Objective, before Cross-Round Summary.
@@ -1498,29 +1858,7 @@ export function compileL2(request, vaultContext) {
1498
1858
  // ═══════════════════════════════════════════════════════════════════════════
1499
1859
  const evidence = lr?.execution_evidence;
1500
1860
  if (evidence) {
1501
- // Check 1: Agent claims action but reports no files changed
1502
- if (evidence.files_changed.length === 0 && lr.output_summary) {
1503
- const actionWords = /\b(fix|modif|chang|updat|refactor|implement|add|remov|delet|rewrit|修补|修改|更改|更新|重构|实现|添加|删除|重写)\b/i;
1504
- if (actionWords.test(lr.output_summary)) {
1505
- warnings.push("P4: Agent output claims changes but execution_evidence.files_changed is empty — possible oversight");
1506
- }
1507
- }
1508
- // Check 2: Agent reports success but tests failed
1509
- if (evidence.test_results && evidence.test_results.failed > 0 && lr.success) {
1510
- warnings.push(`P4: Agent reports success=true but ${evidence.test_results.failed} tests failed — claims may not match reality`);
1511
- }
1512
- // Check 3: Progress estimate mismatch with criteria tracking
1513
- if (loopObjective && loopObjective.success_criteria.length > 0) {
1514
- const metCount = evidence.success_criteria_met.length;
1515
- const totalCount = loopObjective.success_criteria.length;
1516
- const objectiveProgress = totalCount > 0 ? metCount / totalCount : 0;
1517
- const subjectiveProgress = evidence.progress_estimate;
1518
- const policy = getPolicy();
1519
- const mismatchThreshold = policy.evolution?.progress_mismatch_threshold ?? 0.3;
1520
- if (Math.abs(objectiveProgress - subjectiveProgress) > mismatchThreshold) {
1521
- warnings.push(`P4: Progress estimate mismatch — agent estimates ${(subjectiveProgress * 100).toFixed(0)}% but ${metCount}/${totalCount} criteria met (${(objectiveProgress * 100).toFixed(0)}%)`);
1522
- }
1523
- }
1861
+ warnings.push(...runP4ConsistencyChecks(evidence, lr.output_summary, lr.success, loopObjective));
1524
1862
  }
1525
1863
  // ═══════════════════════════════════════════════════════════════════════════
1526
1864
  // P4: Progress gradient — early stall detection
@@ -1544,70 +1882,56 @@ export function compileL2(request, vaultContext) {
1544
1882
  // ═══════════════════════════════════════════════════════════════════════════
1545
1883
  // P4: Build Progress Dashboard for prompt injection
1546
1884
  // ═══════════════════════════════════════════════════════════════════════════
1547
- let progressDashboard = "";
1548
- if (evidence && loopObjective) {
1549
- const totalCriteria = loopObjective.success_criteria.length;
1550
- const metCriteria = evidence.success_criteria_met.length;
1551
- const remainingCriteria = evidence.success_criteria_remaining;
1552
- const objProgress = totalCriteria > 0 ? metCriteria / totalCriteria : evidence.progress_estimate;
1553
- const subjProgress = evidence.progress_estimate;
1554
- const lines = [
1555
- "",
1556
- "### Progress Dashboard",
1557
- "",
1558
- `**Overall**: ${metCriteria}/${totalCriteria} criteria met (${(objProgress * 100).toFixed(0)}%)`,
1559
- ];
1560
- if (Math.abs(objProgress - subjProgress) > 0.05) {
1561
- lines.push(`**Agent estimate**: ${(subjProgress * 100).toFixed(0)}%`);
1562
- }
1563
- if (evidence.files_changed.length) {
1564
- lines.push(`**Files changed**: ${evidence.files_changed.join(", ")}`);
1565
- }
1566
- if (evidence.test_results) {
1567
- const tr = evidence.test_results;
1568
- lines.push(`**Tests**: ${tr.passed} passed, ${tr.failed} failed, ${tr.skipped} skipped`);
1569
- }
1570
- if (remainingCriteria.length) {
1571
- lines.push("**Remaining criteria**:");
1572
- for (const rc of remainingCriteria.slice(0, 5)) {
1573
- lines.push(`- ${rc}`);
1574
- }
1575
- if (remainingCriteria.length > 5) {
1576
- lines.push(`- ... and ${remainingCriteria.length - 5} more`);
1577
- }
1578
- }
1579
- lines.push("");
1580
- // Compute trend from previous round
1581
- if (vaultContext) {
1582
- const prevEvidence = getPreviousRoundEvidence(request.loop_id, request.round - 1, vaultContext);
1583
- if (prevEvidence) {
1584
- const trend = objProgress - (prevEvidence.success_criteria_met.length / Math.max(totalCriteria, 1));
1585
- if (trend > 0.03) {
1586
- lines.push(`**Trend**: ↑ advancing (+${(trend * 100).toFixed(0)}% this round)`);
1587
- }
1588
- else if (trend > -0.03) {
1589
- lines.push("**Trend**: → stable");
1590
- }
1591
- else {
1592
- lines.push(`**Trend**: ↓ regressing (${(trend * 100).toFixed(0)}% this round)`);
1593
- }
1594
- lines.push("");
1595
- }
1596
- }
1597
- progressDashboard = lines.join("\n");
1885
+ const progressDashboard = (evidence && loopObjective)
1886
+ ? buildProgressDashboard(evidence, loopObjective, vaultContext, request.loop_id, request.round, /* showTrend */ true)
1887
+ : "";
1888
+ // ── v1.14: Render state file ──────────────────────────────────────────
1889
+ // Collect all accumulated state and render it as a markdown snapshot.
1890
+ // The caller writes this to .loopforge/state/{loopId}-state.md.
1891
+ const sfPolicy = getPolicy().state_file;
1892
+ let stateFileContent;
1893
+ if (sfPolicy.enabled) {
1894
+ const excludeRounds = checkpointSummary
1895
+ ? new Set([checkpointSummary.declared_at_round])
1896
+ : undefined;
1897
+ const allCheckpoints = collectVaultCheckpoints(vaultContext, request.loop_id, excludeRounds);
1898
+ if (checkpointSummary)
1899
+ allCheckpoints.unshift(checkpointSummary);
1900
+ const delegationSummary = vaultContext
1901
+ ? buildDelegationSummary(vaultContext)
1902
+ : "";
1903
+ stateFileContent = renderStateFile({
1904
+ loopId: request.loop_id,
1905
+ currentRound: request.round,
1906
+ maxRounds: request.max_rounds ?? sfPolicy.max_summary_rounds * 2,
1907
+ loopObjective,
1908
+ rollingSummary,
1909
+ constraints,
1910
+ retiredConstraints: retired,
1911
+ checkpoints: allCheckpoints,
1912
+ delegationSummary,
1913
+ progressDashboard,
1914
+ externalContext: request.external_context,
1915
+ });
1916
+ // Prepend state file pointer to the prompt
1917
+ prompt = renderStateFilePointer(request.loop_id) + "\n" + prompt;
1598
1918
  }
1919
+ // v1.14: When state file is enabled, progressDashboard lives in the state
1920
+ // file (see renderStateFile above). Only inline it in the prompt when the
1921
+ // state file is disabled — this keeps the prompt truly thin.
1922
+ const finalPrompt = sfPolicy.enabled ? prompt : prompt + progressDashboard;
1599
1923
  return makeLoopCompileResponse({
1600
1924
  status: AgentStatus.OK,
1601
- prompt: prompt + progressDashboard,
1925
+ prompt: finalPrompt,
1602
1926
  recompile_level: "l2",
1603
1927
  diff_from_previous: request.round === 1 || request.plan_source
1604
1928
  ? "Full recompile — new goal or first call."
1605
- : "Full recompile — goal_id changed or strategy collapse.",
1929
+ : "Full recompile — goal_id changed or checkpoint boundary.",
1606
1930
  lineage: [`${request.loop_id}:r${request.round}`],
1607
1931
  constraints_active: constraints,
1608
1932
  constraints_retired: retired,
1609
- technique_used: technique,
1610
- reference_file: referenceFile,
1933
+ technique_used: "agent-selected",
1934
+ reference_file: `${SKILLS_DIR}/prompt-techniques/SKILL.md`,
1611
1935
  rolling_summary: rollingSummary,
1612
1936
  checkpoint_summary: checkpointSummary,
1613
1937
  loop_id: request.loop_id,
@@ -1618,11 +1942,51 @@ export function compileL2(request, vaultContext) {
1618
1942
  plan_source: request.plan_source,
1619
1943
  suggested_next_task: suggestedNextTask,
1620
1944
  warnings,
1945
+ state_file_content: stateFileContent,
1621
1946
  });
1622
1947
  }
1948
+ // ═══════════════════════════════════════════════════════════════════════════
1949
+ // L2 Prompt Builder (v1.15 — Agent technique autonomy)
1950
+ // ═══════════════════════════════════════════════════════════════════════════
1951
+ /** Build the L2 restart prompt with a Technique Selection block.
1952
+ * The Agent reads the technique catalog (SKILL.md), freely chooses the best
1953
+ * technique based on loop state, reads the corresponding reference file, and
1954
+ * applies it directly to the task. LoopForge provides context — the Agent
1955
+ * provides the reasoning strategy. */
1956
+ function buildL2Prompt(request, goalId, constraints, loopObjective, rollingText) {
1957
+ const ctx = {
1958
+ request, goalId, constraints, loopObjective, rollingText, level: "l2",
1959
+ };
1960
+ const lines = [
1961
+ `## LoopForge L2 Compile — Round ${request.round}`,
1962
+ "",
1963
+ ];
1964
+ // Cross-round summary
1965
+ if (rollingText) {
1966
+ lines.push(rollingText);
1967
+ lines.push("");
1968
+ }
1969
+ // Technique Selection — Agent freely chooses
1970
+ lines.push("### Technique Selection", "", "This is a strategy restart point. Read the technique catalog to choose", "the best reasoning approach for this round:", "", ` \`${SKILLS_DIR}/prompt-techniques/SKILL.md\``, "", "Based on the loop state (state file + context below), freely select", "the technique that best fits this round's challenge. Then read the", "full reference for your chosen technique:", "", ` \`${SKILLS_DIR}/prompt-techniques/references/<chosen>.md\``, "", "Apply the technique's method directly to the task — do NOT generate", "an intermediate prompt. Execute the task using the chosen technique", "as your thinking framework.", "");
1971
+ // Loop Objective
1972
+ lines.push(...buildLoopObjectiveBlock(loopObjective));
1973
+ // Active Constraints
1974
+ lines.push(...buildConstraintsBlock(constraints));
1975
+ // Plan Source
1976
+ if (request.plan_source) {
1977
+ lines.push(`**Plan Source**: ${request.plan_source}`);
1978
+ lines.push("");
1979
+ }
1980
+ // Task
1981
+ lines.push(...buildTaskBlock(ctx));
1982
+ // Loop Identity
1983
+ lines.push(...buildIdentityBlock(ctx));
1984
+ return lines.join("\n");
1985
+ }
1623
1986
  function buildHeader(ctx, technique) {
1987
+ const levelLabel = ctx.level.toUpperCase();
1624
1988
  const lines = [
1625
- `## LoopForge L2 Compile — Round ${ctx.request.round}`,
1989
+ `## LoopForge ${levelLabel} Compile — Round ${ctx.request.round}`,
1626
1990
  `**Technique**: ${technique} (embedded)`,
1627
1991
  "",
1628
1992
  ];
@@ -1683,240 +2047,85 @@ function buildIdentityBlock(ctx) {
1683
2047
  ];
1684
2048
  }
1685
2049
  /** Generic compiler — reads technique reference file (fallback for zero-shot/few-shot/cot). */
1686
- function compileGeneric(request, goalId, constraints, loopObjective, rollingText, analysis, referenceFile, technique) {
1687
- const ctx = { request, goalId, constraints, loopObjective, rollingText };
2050
+ function compileGeneric(request, goalId, constraints, loopObjective, rollingText, analysis, referenceFile, technique, opts = {}) {
2051
+ const embedSkeleton = opts.embedSkeleton !== false; // default true
2052
+ const ctx = { request, goalId, constraints, loopObjective, rollingText, level: opts.level ?? "l2" };
1688
2053
  const lines = [
1689
2054
  ...buildHeader(ctx, technique),
1690
- `Read the technique reference BEFORE generating the prompt:`,
1691
- ` Technique: ${technique}`,
1692
- ` Reference: ${referenceFile}`,
1693
- ` Rationale: ${analysis.rationale}`,
1694
- "",
1695
- ...buildLoopObjectiveBlock(loopObjective),
1696
- ...buildConstraintsBlock(constraints),
1697
- ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []),
1698
- ...buildTaskBlock(ctx),
1699
- ...buildIdentityBlock(ctx),
1700
- "### Generation Instructions",
1701
- `1. Read \`${referenceFile}\` — study its structure rules, section count, and format requirements`,
1702
- "2. Generate a complete prompt following that technique's structure",
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",
1705
2055
  ];
2056
+ if (embedSkeleton) {
2057
+ lines.push(`Read the technique reference BEFORE generating the prompt:`, ` Technique: ${technique}`, ` Reference: ${referenceFile}`, ` Rationale: ${analysis.rationale}`, "");
2058
+ }
2059
+ else {
2060
+ lines.push(`**Technique**: ${technique} (see state file for full reference)`, `**Rationale**: ${analysis.rationale}`, "");
2061
+ }
2062
+ lines.push(...buildLoopObjectiveBlock(loopObjective), ...buildConstraintsBlock(constraints), ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []), ...buildTaskBlock(ctx), ...buildIdentityBlock(ctx));
2063
+ if (embedSkeleton) {
2064
+ lines.push("### Generation Instructions", `1. Read \`${referenceFile}\` — study its structure rules, section count, and format requirements`, "2. Generate a complete prompt following that technique's structure", "3. The prompt must be self-contained — ready for a coding agent to execute", "4. Output only the generated prompt — no preamble, no meta-commentary");
2065
+ }
1706
2066
  return lines.join("\n");
1707
2067
  }
1708
2068
  // ── Step-Back specialist ───────────────────────────────────────────────────────
1709
- function compileStepBack(request, goalId, constraints, loopObjective, rollingText, analysis) {
1710
- const ctx = { request, goalId, constraints, loopObjective, rollingText };
2069
+ function compileStepBack(request, goalId, constraints, loopObjective, rollingText, analysis, opts = {}) {
2070
+ const embedSkeleton = opts.embedSkeleton !== false; // default true
2071
+ const ctx = { request, goalId, constraints, loopObjective, rollingText, level: opts.level ?? "l2" };
1711
2072
  const lines = [
1712
2073
  ...buildHeader(ctx, "step-back"),
1713
2074
  `**Rationale**: ${analysis.rationale}`,
1714
2075
  "",
1715
- "Generate a complete prompt using the **Step-Back** technique. Follow the 8-section skeleton below.",
1716
- "Embed all structural constraints — do NOT reference an external file.",
1717
- "",
1718
- ...buildLoopObjectiveBlock(loopObjective),
1719
- ...buildConstraintsBlock(constraints),
1720
- ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []),
1721
- ...buildTaskBlock(ctx),
1722
- ...buildIdentityBlock(ctx),
1723
- "",
1724
- "### 8-Section Skeleton (REQUIRED)",
1725
- "",
1726
- "| # | Section | Required | Notes |",
1727
- "|---|---------|----------|-------|",
1728
- "| 1 | 角色 | ✓ | Domain expert role appropriate to the task |",
1729
- "| 2 | 任务 | ✓ | One-sentence task summary |",
1730
- "| 3 | 输入 | ✓ | Target module/problem to analyse |",
1731
- "| 4 | 输出格式 | ✓ | Numbered list of expected outputs |",
1732
- "| 5 | **Step-Back 抽象框架** | ✓ | See format rules below |",
1733
- "| 6 | 具体实现要求 | ✓ | MUST start with: **\"基于上述抽象框架,实现以下所有功能\"** |",
1734
- "| 7 | 硬约束 | ✓ | Numbered list of hard constraints |",
1735
- "| 8 | 生成要求 | ✓ | Acceptance criteria |",
1736
- "",
1737
- "### Section 5 — Step-Back 抽象框架 (CRITICAL)",
1738
- "",
1739
- "Must contain **2-3 abstract frameworks**. Each framework is an independent ASCII diagram.",
1740
- "Frameworks are PARALLEL (peer-level), not sequential.",
1741
- "",
1742
- "Each framework format:",
1743
- "```",
1744
- "### 框架N:[Framework Name]",
1745
- "",
1746
- "[ASCII diagram with principles/formulas/classification tables/rule tables]",
1747
- "",
1748
- "Diagram content requirements:",
1749
- "- Core principle/formula (e.g. \"Total = Σ(dimension × weight)\")",
1750
- "- Component table (e.g. weight allocation table, type mapping table)",
1751
- "- Rule description (e.g. \"Missing data dimensions use neutral value\")",
1752
- "```",
1753
- "",
1754
- "**Tightening rules**:",
1755
- "- The step-back question must be MORE ABSTRACT than the original problem, but cover all required information",
1756
- "- Abstract ≠ vague. Use precise formulas, principles, standards, definitions, or causal mechanisms",
1757
- "- Tighten to the MINIMUM generalisation layer that still covers the original problem",
1758
- "- Reasoning must RETURN to the original problem — do not stay at the principle level",
1759
- "",
1760
- "**Section 6 transition sentence (MANDATORY)**:",
1761
- "\"基于上述抽象框架,实现以下所有功能。\"",
1762
- "",
1763
- "### Generation Instructions",
1764
- "1. Extract 2-3 abstract principles/frameworks from the task domain",
1765
- "2. Build ASCII diagrams for each framework (formulas, tables, rules)",
1766
- "3. Apply the abstraction back to the concrete task",
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",
1769
2076
  ];
2077
+ if (embedSkeleton) {
2078
+ lines.push("Generate a complete prompt using the **Step-Back** technique. Follow the 8-section skeleton below.", "Embed all structural constraints — do NOT reference an external file.", "");
2079
+ }
2080
+ else {
2081
+ lines.push("**Technique**: step-back (see state file for the full skeleton).", "Generate a complete prompt following this technique.", "");
2082
+ }
2083
+ lines.push(...buildLoopObjectiveBlock(loopObjective), ...buildConstraintsBlock(constraints), ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []), ...buildTaskBlock(ctx), ...buildIdentityBlock(ctx));
2084
+ if (embedSkeleton) {
2085
+ lines.push("", "### 8-Section Skeleton (REQUIRED)", "", "| # | Section | Required | Notes |", "|---|---------|----------|-------|", "| 1 | 角色 | ✓ | Domain expert role appropriate to the task |", "| 2 | 任务 | ✓ | One-sentence task summary |", "| 3 | 输入 | ✓ | Target module/problem to analyse |", "| 4 | 输出格式 | ✓ | Numbered list of expected outputs |", "| 5 | **Step-Back 抽象框架** | ✓ | See format rules below |", "| 6 | 具体实现要求 | ✓ | MUST start with: **\"基于上述抽象框架,实现以下所有功能\"** |", "| 7 | 硬约束 | ✓ | Numbered list of hard constraints |", "| 8 | 生成要求 | ✓ | Acceptance criteria |", "", "### Section 5 — Step-Back 抽象框架 (CRITICAL)", "", "Must contain **2-3 abstract frameworks**. Each framework is an independent ASCII diagram.", "Frameworks are PARALLEL (peer-level), not sequential.", "", "Each framework format:", "```", "### 框架N:[Framework Name]", "", "[ASCII diagram with principles/formulas/classification tables/rule tables]", "", "Diagram content requirements:", "- Core principle/formula (e.g. \"Total = Σ(dimension × weight)\")", "- Component table (e.g. weight allocation table, type mapping table)", "- Rule description (e.g. \"Missing data dimensions use neutral value\")", "```", "", "**Tightening rules**:", "- The step-back question must be MORE ABSTRACT than the original problem, but cover all required information", "- Abstract ≠ vague. Use precise formulas, principles, standards, definitions, or causal mechanisms", "- Tighten to the MINIMUM generalisation layer that still covers the original problem", "- Reasoning must RETURN to the original problem — do not stay at the principle level", "", "**Section 6 transition sentence (MANDATORY)**:", "\"基于上述抽象框架,实现以下所有功能。\"", "", "### Generation Instructions", "1. Extract 2-3 abstract principles/frameworks from the task domain", "2. Build ASCII diagrams for each framework (formulas, tables, rules)", "3. Apply the abstraction back to the concrete task", "4. Verify: the generated prompt satisfies all structural rules in the skeleton above", "5. Output only the generated prompt — no preamble, no meta-commentary");
2086
+ }
1770
2087
  return lines.join("\n");
1771
2088
  }
1772
2089
  // ── Least-to-Most specialist ───────────────────────────────────────────────────
1773
- function compileLeastToMost(request, goalId, constraints, loopObjective, rollingText, analysis) {
1774
- const ctx = { request, goalId, constraints, loopObjective, rollingText };
2090
+ function compileLeastToMost(request, goalId, constraints, loopObjective, rollingText, analysis, opts = {}) {
2091
+ const embedSkeleton = opts.embedSkeleton !== false; // default true
2092
+ const ctx = { request, goalId, constraints, loopObjective, rollingText, level: opts.level ?? "l2" };
1775
2093
  const lines = [
1776
2094
  ...buildHeader(ctx, "least-to-most"),
1777
2095
  `**Rationale**: ${analysis.rationale}`,
1778
2096
  "",
1779
- "Generate a complete prompt using the **Least-to-Most** technique. Follow the 8-section skeleton below.",
1780
- "Embed all structural constraints — do NOT reference an external file.",
1781
- "",
1782
- ...buildLoopObjectiveBlock(loopObjective),
1783
- ...buildConstraintsBlock(constraints),
1784
- ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []),
1785
- ...buildTaskBlock(ctx),
1786
- ...buildIdentityBlock(ctx),
1787
- "",
1788
- "### 8-Section Skeleton (REQUIRED)",
1789
- "",
1790
- "| # | Section | Required | Notes |",
1791
- "|---|---------|----------|-------|",
1792
- "| 1 | 角色 | ✓ | Domain expert role appropriate to the task |",
1793
- "| 2 | 任务 | ✓ | One-sentence task summary |",
1794
- "| 3 | 输入 | ✓ | Target module |",
1795
- "| 4 | 输出格式 | ✓ | Numbered list (e.g. \"1. DDL, 2. API, 3. Enum...\") |",
1796
- "| 5 | **Least-to-Most 逐步推理框架** | ✓ | See format rules below |",
1797
- "| 6 | 具体实现要求 | ✓ | Expand per output format list (NOT per sub-problem — sub-problems fully expanded in §5) |",
1798
- "| 7 | 硬约束 | ✓ | Numbered list |",
1799
- "| 8 | 生成要求 | ✓ | Acceptance criteria, must include: \"严格按照子问题顺序逐步实现\" |",
1800
- "",
1801
- "### Section 5 — Least-to-Most 逐步推理框架 (CRITICAL)",
1802
- "",
1803
- "Must contain **4-6 ordered sub-problems**. Each sub-problem format:",
1804
- "",
1805
- "```",
1806
- "### 子问题 N:[Sub-problem Name]",
1807
- "",
1808
- "**目标:** [What this sub-problem solves, which prior sub-problem(s) it depends on]",
1809
- "",
1810
- "**要求:**",
1811
- "- Specific requirement list",
1812
- "- [If sub-problem involves enums/mapping tables, list them]",
1813
- "",
1814
- "---",
1815
- "```",
1816
- "",
1817
- "**Critical rules**:",
1818
- "- Sub-problem 1 is the SIMPLEST (e.g. \"Define enums and base data structures\")",
1819
- "- Each sub-problem declares: \"基于子问题 N-1 的结论\"",
1820
- "- The LAST sub-problem MUST be: **\"综合实现完整模块\"** — list all components to integrate",
1821
- "- Sub-problems separated by `---`",
1822
- "- Order goes from LEAST complex → MOST complex, dependency chain must be explicit",
1823
- "- Each sub-problem must serve the original task — no unrelated steps",
1824
- "- The final sub-problem must be equivalent to or directly address the original task",
1825
- "",
1826
- "### Generation Instructions",
1827
- "1. Decompose the task into 4-6 ordered sub-problems with explicit dependencies",
1828
- "2. Sub-problem 1 starts with the simplest building block (data structures, enums, base config)",
1829
- "3. Each subsequent sub-problem builds on prior results",
1830
- "4. Final sub-problem integrates all components into the complete module",
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",
1833
2097
  ];
2098
+ if (embedSkeleton) {
2099
+ lines.push("Generate a complete prompt using the **Least-to-Most** technique. Follow the 8-section skeleton below.", "Embed all structural constraints — do NOT reference an external file.", "");
2100
+ }
2101
+ else {
2102
+ lines.push("**Technique**: least-to-most (see state file for the full skeleton).", "Generate a complete prompt following this technique.", "");
2103
+ }
2104
+ lines.push(...buildLoopObjectiveBlock(loopObjective), ...buildConstraintsBlock(constraints), ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []), ...buildTaskBlock(ctx), ...buildIdentityBlock(ctx));
2105
+ if (embedSkeleton) {
2106
+ lines.push("", "### 8-Section Skeleton (REQUIRED)", "", "| # | Section | Required | Notes |", "|---|---------|----------|-------|", "| 1 | 角色 | ✓ | Domain expert role appropriate to the task |", "| 2 | 任务 | ✓ | One-sentence task summary |", "| 3 | 输入 | ✓ | Target module |", "| 4 | 输出格式 | ✓ | Numbered list (e.g. \"1. DDL, 2. API, 3. Enum...\") |", "| 5 | **Least-to-Most 逐步推理框架** | ✓ | See format rules below |", "| 6 | 具体实现要求 | ✓ | Expand per output format list (NOT per sub-problem — sub-problems fully expanded in §5) |", "| 7 | 硬约束 | ✓ | Numbered list |", "| 8 | 生成要求 | ✓ | Acceptance criteria, must include: \"严格按照子问题顺序逐步实现\" |", "", "### Section 5 — Least-to-Most 逐步推理框架 (CRITICAL)", "", "Must contain **4-6 ordered sub-problems**. Each sub-problem format:", "", "```", "### 子问题 N:[Sub-problem Name]", "", "**目标:** [What this sub-problem solves, which prior sub-problem(s) it depends on]", "", "**要求:**", "- Specific requirement list", "- [If sub-problem involves enums/mapping tables, list them]", "", "---", "```", "", "**Critical rules**:", "- Sub-problem 1 is the SIMPLEST (e.g. \"Define enums and base data structures\")", "- Each sub-problem declares: \"基于子问题 N-1 的结论\"", "- The LAST sub-problem MUST be: **\"综合实现完整模块\"** — list all components to integrate", "- Sub-problems separated by `---`", "- Order goes from LEAST complex → MOST complex, dependency chain must be explicit", "- Each sub-problem must serve the original task — no unrelated steps", "- The final sub-problem must be equivalent to or directly address the original task", "", "### Generation Instructions", "1. Decompose the task into 4-6 ordered sub-problems with explicit dependencies", "2. Sub-problem 1 starts with the simplest building block (data structures, enums, base config)", "3. Each subsequent sub-problem builds on prior results", "4. Final sub-problem integrates all components into the complete module", "5. Verify: the generated prompt satisfies all structural rules in the skeleton above", "6. Output only the generated prompt — no preamble, no meta-commentary");
2107
+ }
1834
2108
  return lines.join("\n");
1835
2109
  }
1836
2110
  // ── Tree-of-Thought specialist ──────────────────────────────────────────────────
1837
- function compileToT(request, goalId, constraints, loopObjective, rollingText, analysis) {
1838
- const ctx = { request, goalId, constraints, loopObjective, rollingText };
2111
+ function compileToT(request, goalId, constraints, loopObjective, rollingText, analysis, opts = {}) {
2112
+ const embedSkeleton = opts.embedSkeleton !== false; // default true
2113
+ const ctx = { request, goalId, constraints, loopObjective, rollingText, level: opts.level ?? "l2" };
1839
2114
  const lines = [
1840
2115
  ...buildHeader(ctx, "tree-of-thought"),
1841
2116
  `**Rationale**: ${analysis.rationale}`,
1842
2117
  "",
1843
- "Generate a complete prompt using the **Tree-of-Thought** technique. Follow the 8-section skeleton below.",
1844
- "Embed all structural constraints — do NOT reference an external file.",
1845
- "",
1846
- ...buildLoopObjectiveBlock(loopObjective),
1847
- ...buildConstraintsBlock(constraints),
1848
- ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []),
1849
- ...buildTaskBlock(ctx),
1850
- ...buildIdentityBlock(ctx),
1851
- "",
1852
- "### 8-Section Skeleton (REQUIRED)",
1853
- "",
1854
- "| # | Section | Required | Notes |",
1855
- "|---|---------|----------|-------|",
1856
- "| 1 | 角色 | ✓ | Multi-path problem-solving expert; may introduce 3 expert personas |",
1857
- "| 2 | 任务 | ✓ | One-sentence task summary |",
1858
- "| 3 | 输入 | ✓ | High-risk complex problem |",
1859
- "| 4 | 输出格式 | ✓ | \"先输出思维树过程,再输出最终答案/方案\" |",
1860
- "| 5 | **思维树探索框架** | ✓ | See 3 sub-blocks below (REQUIRED) |",
1861
- "| 6 | 具体实现要求 | ✓ | Expand per output format |",
1862
- "| 7 | 硬约束 | ✓ | Include branch_count, max_depth, pruning rules, safety/performance constraints |",
1863
- "| 8 | 生成要求 | ✓ | \"先探索多路径,再选择最优方案\" |",
1864
- "",
1865
- "### Section 5 — 思维树探索框架 (CRITICAL — 3 sub-blocks REQUIRED)",
1866
- "",
1867
- "**Sub-block A: Search Strategy Declaration**",
1868
- "```",
1869
- "搜索策略: [beam / dfs / expert-panel]",
1870
- "分支数(branch_count): 2-4",
1871
- "最大深度(max_depth): ≤3",
1872
- "每轮保留数(keep_count): 1-2",
1873
- "```",
1874
- "Strategy selection guide:",
1875
- "- **beam** (default): planning, creative, math search — generate multiple candidates per turn, keep top b",
1876
- "- **dfs**: puzzles, constraint satisfaction, debugging — go deep on highest-score branch, backtrack on failure",
1877
- "- **expert-panel**: 3+ experts each generate candidates, then cross-evaluate and revise",
1878
- "",
1879
- "**Sub-block B: Evaluation Criteria Table**",
1880
- "| 标准 | 权重 | 说明 |",
1881
- "|------|------|------|",
1882
- "| 正确性 | 最高 | Logic/math correctness |",
1883
- "| 可行性 | 高 | Can be actually implemented |",
1884
- "| 约束匹配 | 高 | Satisfies hard constraints |",
1885
- "| 性能 | 中 | Time/space efficiency |",
1886
- "| 安全性 | 最高 | No vulnerabilities/privilege escalation |",
1887
- "",
1888
- "**Sub-block C: Thought Tree State Table Format**",
1889
- "Require the model to output in this table format:",
1890
- "```",
1891
- "| 轮次 | 分支 | 候选方案 | 评估 | 决策 |",
1892
- "|------|------|---------|------|------|",
1893
- "| 1 | A | [方案描述] | 评分/判断 | 保留/剪枝 |",
1894
- "| 1 | B | [方案描述] | 评分/判断 | 保留/剪枝 |",
1895
- "| 1 | C | [方案描述] | 评分/判断 | 保留/剪枝 |",
1896
- "| 2 | A1 | [深入展开] | ... | ... |",
1897
- "| ... | ... | ... | ... | ... |",
1898
- "",
1899
- "最终选择: [最优方案 + 选择理由]",
1900
- "```",
1901
- "",
1902
- "### Critical Rules",
1903
- "- Hard constraints MUST be ranked FIRST in evaluation criteria",
1904
- "- Branch count: 2-4, depth: ≤3 (prevents token explosion)",
1905
- "- Every turn: generate multiple candidates → evaluate → keep/prune — never single-path",
1906
- "- If all branches are low quality: backtrack to previous turn and regenerate",
1907
- "- The core of ToT is NOT \"3 experts chatting\" — it's: candidate generation → state evaluation → search/prune → final selection",
1908
- "- thought is a PUBLIC intermediate semantic unit, not hidden chain-of-thought",
1909
- "- State table MUST have: round, branch ID, candidate description, evaluation score/judgment, keep/prune decision",
1910
- "",
1911
- "### Generation Instructions",
1912
- "1. Select the appropriate search strategy (beam/dfs/expert-panel) based on task type",
1913
- "2. Define 2-4 candidate approaches as initial branches",
1914
- "3. Build evaluation criteria table with hard constraints ranked first",
1915
- "4. Define the state table format for tracking exploration",
1916
- "5. Set branch count and max depth to prevent token explosion",
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",
1919
2118
  ];
2119
+ if (embedSkeleton) {
2120
+ lines.push("Generate a complete prompt using the **Tree-of-Thought** technique. Follow the 8-section skeleton below.", "Embed all structural constraints — do NOT reference an external file.", "");
2121
+ }
2122
+ else {
2123
+ lines.push("**Technique**: tree-of-thought (see state file for the full skeleton).", "Generate a complete prompt following this technique.", "");
2124
+ }
2125
+ lines.push(...buildLoopObjectiveBlock(loopObjective), ...buildConstraintsBlock(constraints), ...(request.plan_source ? [`**Plan Source**: ${request.plan_source}`, ""] : []), ...buildTaskBlock(ctx), ...buildIdentityBlock(ctx));
2126
+ if (embedSkeleton) {
2127
+ lines.push("", "### 8-Section Skeleton (REQUIRED)", "", "| # | Section | Required | Notes |", "|---|---------|----------|-------|", "| 1 | 角色 | ✓ | Multi-path problem-solving expert; may introduce 3 expert personas |", "| 2 | 任务 | ✓ | One-sentence task summary |", "| 3 | 输入 | ✓ | High-risk complex problem |", "| 4 | 输出格式 | ✓ | \"先输出思维树过程,再输出最终答案/方案\" |", "| 5 | **思维树探索框架** | ✓ | See 3 sub-blocks below (REQUIRED) |", "| 6 | 具体实现要求 | ✓ | Expand per output format |", "| 7 | 硬约束 | ✓ | Include branch_count, max_depth, pruning rules, safety/performance constraints |", "| 8 | 生成要求 | ✓ | \"先探索多路径,再选择最优方案\" |", "", "### Section 5 — 思维树探索框架 (CRITICAL — 3 sub-blocks REQUIRED)", "", "**Sub-block A: Search Strategy Declaration**", "```", "搜索策略: [beam / dfs / expert-panel]", "分支数(branch_count): 2-4", "最大深度(max_depth): ≤3", "每轮保留数(keep_count): 1-2", "```", "Strategy selection guide:", "- **beam** (default): planning, creative, math search — generate multiple candidates per turn, keep top b", "- **dfs**: puzzles, constraint satisfaction, debugging — go deep on highest-score branch, backtrack on failure", "- **expert-panel**: 3+ experts each generate candidates, then cross-evaluate and revise", "", "**Sub-block B: Evaluation Criteria Table**", "| 标准 | 权重 | 说明 |", "|------|------|------|", "| 正确性 | 最高 | Logic/math correctness |", "| 可行性 | 高 | Can be actually implemented |", "| 约束匹配 | 高 | Satisfies hard constraints |", "| 性能 | 中 | Time/space efficiency |", "| 安全性 | 最高 | No vulnerabilities/privilege escalation |", "", "**Sub-block C: Thought Tree State Table Format**", "Require the model to output in this table format:", "```", "| 轮次 | 分支 | 候选方案 | 评估 | 决策 |", "|------|------|---------|------|------|", "| 1 | A | [方案描述] | 评分/判断 | 保留/剪枝 |", "| 1 | B | [方案描述] | 评分/判断 | 保留/剪枝 |", "| 1 | C | [方案描述] | 评分/判断 | 保留/剪枝 |", "| 2 | A1 | [深入展开] | ... | ... |", "| ... | ... | ... | ... | ... |", "", "最终选择: [最优方案 + 选择理由]", "```", "", "### Critical Rules", "- Hard constraints MUST be ranked FIRST in evaluation criteria", "- Branch count: 2-4, depth: ≤3 (prevents token explosion)", "- Every turn: generate multiple candidates → evaluate → keep/prune — never single-path", "- If all branches are low quality: backtrack to previous turn and regenerate", "- The core of ToT is NOT \"3 experts chatting\" — it's: candidate generation → state evaluation → search/prune → final selection", "- thought is a PUBLIC intermediate semantic unit, not hidden chain-of-thought", "- State table MUST have: round, branch ID, candidate description, evaluation score/judgment, keep/prune decision", "", "### Generation Instructions", "1. Select the appropriate search strategy (beam/dfs/expert-panel) based on task type", "2. Define 2-4 candidate approaches as initial branches", "3. Build evaluation criteria table with hard constraints ranked first", "4. Define the state table format for tracking exploration", "5. Set branch count and max depth to prevent token explosion", "6. Verify: the generated prompt satisfies all structural rules in the skeleton above", "7. Output only the generated prompt — no preamble, no meta-commentary");
2128
+ }
1920
2129
  return lines.join("\n");
1921
2130
  }
1922
2131
  // ═══════════════════════════════════════════════════════════════════════════