pan-wizard 3.32.0 → 3.34.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 (209) hide show
  1. package/README.md +23 -20
  2. package/agents/pan-conductor.md +18 -17
  3. package/agents/pan-counterfactual.md +2 -2
  4. package/agents/pan-debugger.md +27 -8
  5. package/agents/pan-design-checker.md +1 -1
  6. package/agents/pan-designer.md +2 -2
  7. package/agents/pan-distiller.md +3 -3
  8. package/agents/pan-document_code.md +16 -16
  9. package/agents/pan-executor.md +15 -11
  10. package/agents/pan-experiment-runner.md +15 -17
  11. package/agents/pan-hardener.md +15 -15
  12. package/agents/pan-integration-checker.md +2 -2
  13. package/agents/pan-knowledge.md +2 -2
  14. package/agents/pan-meta-reviewer.md +5 -5
  15. package/agents/pan-optimizer.md +40 -44
  16. package/agents/pan-phase-researcher.md +10 -10
  17. package/agents/pan-plan-checker.md +36 -30
  18. package/agents/pan-planner.md +77 -53
  19. package/agents/pan-previewer.md +1 -1
  20. package/agents/pan-project-researcher.md +21 -78
  21. package/agents/pan-release.md +1 -1
  22. package/agents/pan-research-synthesizer.md +2 -2
  23. package/agents/pan-reviewer.md +24 -2
  24. package/agents/pan-roadmapper.md +20 -62
  25. package/agents/pan-verifier.md +53 -30
  26. package/bin/install-lib.cjs +228 -38
  27. package/bin/install.js +86 -15
  28. package/commands/pan/army.md +16 -12
  29. package/commands/pan/assumptions.md +2 -2
  30. package/commands/pan/audit-deployment.md +11 -12
  31. package/commands/pan/cost.md +8 -4
  32. package/commands/pan/debug.md +10 -7
  33. package/commands/pan/design-phase.md +3 -3
  34. package/commands/pan/discuss-phase.md +2 -0
  35. package/commands/pan/exec-phase.md +21 -37
  36. package/commands/pan/experiment.md +11 -10
  37. package/commands/pan/focus-auto.md +33 -27
  38. package/commands/pan/focus-design.md +9 -6
  39. package/commands/pan/focus-doc-audit.md +4 -3
  40. package/commands/pan/focus-drift-walking.md +4 -3
  41. package/commands/pan/focus-exec.md +7 -7
  42. package/commands/pan/focus-plan.md +12 -11
  43. package/commands/pan/focus-scan.md +7 -10
  44. package/commands/pan/focus-sync.md +3 -2
  45. package/commands/pan/git.md +8 -8
  46. package/commands/pan/health.md +2 -2
  47. package/commands/pan/hud.md +3 -2
  48. package/commands/pan/hygiene.md +14 -7
  49. package/commands/pan/knowledge.md +9 -9
  50. package/commands/pan/learn.md +10 -7
  51. package/commands/pan/links.md +4 -1
  52. package/commands/pan/map-codebase.md +5 -5
  53. package/commands/pan/mcp-bridge.md +18 -18
  54. package/commands/pan/milestone-done.md +7 -5
  55. package/commands/pan/milestone-new.md +1 -1
  56. package/commands/pan/new-project.md +10 -10
  57. package/commands/pan/optimize.md +16 -14
  58. package/commands/pan/patches.md +2 -6
  59. package/commands/pan/pause.md +1 -1
  60. package/commands/pan/phase-tests.md +4 -3
  61. package/commands/pan/plan-phase.md +9 -19
  62. package/commands/pan/preview.md +1 -1
  63. package/commands/pan/profile.md +4 -4
  64. package/commands/pan/quick.md +1 -1
  65. package/commands/pan/remove-phase.md +1 -1
  66. package/commands/pan/report.md +2 -2
  67. package/commands/pan/research-phase.md +14 -15
  68. package/commands/pan/resume.md +9 -11
  69. package/commands/pan/retro.md +5 -3
  70. package/commands/pan/review-deep.md +10 -10
  71. package/commands/pan/settings.md +1 -1
  72. package/commands/pan/todo-add.md +2 -2
  73. package/commands/pan/todo-check.md +2 -2
  74. package/commands/pan/verify-phase.md +0 -11
  75. package/commands/pan/what-if.md +2 -2
  76. package/hooks/dist/pan-context-monitor.js +343 -44
  77. package/hooks/dist/pan-cost-logger.js +30 -0
  78. package/hooks/dist/pan-state-reinject.js +140 -15
  79. package/hooks/dist/pan-statusline.js +68 -22
  80. package/hooks/dist/pan-stop-guard.js +7 -2
  81. package/package.json +1 -1
  82. package/pan-wizard-core/bin/lib/agents-md.cjs +48 -7
  83. package/pan-wizard-core/bin/lib/bridge.cjs +6 -5
  84. package/pan-wizard-core/bin/lib/campaign.cjs +4 -2
  85. package/pan-wizard-core/bin/lib/commands-learnings.cjs +19 -2
  86. package/pan-wizard-core/bin/lib/commands.cjs +126 -5
  87. package/pan-wizard-core/bin/lib/config.cjs +112 -1
  88. package/pan-wizard-core/bin/lib/constants.cjs +29 -15
  89. package/pan-wizard-core/bin/lib/context-budget.cjs +12 -9
  90. package/pan-wizard-core/bin/lib/core.cjs +77 -23
  91. package/pan-wizard-core/bin/lib/cost-rebuild.cjs +13 -1
  92. package/pan-wizard-core/bin/lib/cost.cjs +149 -10
  93. package/pan-wizard-core/bin/lib/distill.cjs +6 -3
  94. package/pan-wizard-core/bin/lib/doc-lint.cjs +30 -11
  95. package/pan-wizard-core/bin/lib/findings.cjs +15 -4
  96. package/pan-wizard-core/bin/lib/focus.cjs +13 -4
  97. package/pan-wizard-core/bin/lib/frontmatter.cjs +105 -26
  98. package/pan-wizard-core/bin/lib/hygiene.cjs +178 -17
  99. package/pan-wizard-core/bin/lib/init.cjs +12 -5
  100. package/pan-wizard-core/bin/lib/knowledge.cjs +17 -1
  101. package/pan-wizard-core/bin/lib/learn-index.cjs +111 -10
  102. package/pan-wizard-core/bin/lib/learn-lint.cjs +30 -3
  103. package/pan-wizard-core/bin/lib/memory-optimize.cjs +8 -4
  104. package/pan-wizard-core/bin/lib/memory.cjs +508 -54
  105. package/pan-wizard-core/bin/lib/milestone.cjs +8 -3
  106. package/pan-wizard-core/bin/lib/optimize.cjs +84 -16
  107. package/pan-wizard-core/bin/lib/phase-remove.cjs +85 -21
  108. package/pan-wizard-core/bin/lib/phase-report.cjs +8 -2
  109. package/pan-wizard-core/bin/lib/phase.cjs +67 -15
  110. package/pan-wizard-core/bin/lib/preview.cjs +3 -1
  111. package/pan-wizard-core/bin/lib/review-deep.cjs +2 -2
  112. package/pan-wizard-core/bin/lib/roadmap-compact.cjs +267 -0
  113. package/pan-wizard-core/bin/lib/roadmap.cjs +243 -9
  114. package/pan-wizard-core/bin/lib/runner.cjs +2 -1
  115. package/pan-wizard-core/bin/lib/state-compact.cjs +11 -6
  116. package/pan-wizard-core/bin/lib/state.cjs +47 -31
  117. package/pan-wizard-core/bin/lib/template.cjs +11 -4
  118. package/pan-wizard-core/bin/lib/verdict.cjs +54 -4
  119. package/pan-wizard-core/bin/lib/verify-drift.cjs +14 -5
  120. package/pan-wizard-core/bin/lib/verify-preflight.cjs +4 -13
  121. package/pan-wizard-core/bin/lib/verify-retro.cjs +5 -3
  122. package/pan-wizard-core/bin/lib/verify-scope.cjs +74 -4
  123. package/pan-wizard-core/bin/lib/verify.cjs +117 -35
  124. package/pan-wizard-core/bin/lib/whatif.cjs +5 -3
  125. package/pan-wizard-core/bin/pan-tools.cjs +111 -17
  126. package/pan-wizard-core/learnings/index.json +5 -5
  127. package/pan-wizard-core/mcp/server.cjs +18 -9
  128. package/pan-wizard-core/opencode/pan-wizard.js +111 -0
  129. package/pan-wizard-core/references/checkpoints.md +7 -5
  130. package/pan-wizard-core/references/continuation-format.md +3 -3
  131. package/pan-wizard-core/references/decimal-phase-calculation.md +2 -3
  132. package/pan-wizard-core/references/design-methodology.md +1 -1
  133. package/pan-wizard-core/references/git-integration.md +4 -6
  134. package/pan-wizard-core/references/git-planning-commit.md +2 -2
  135. package/pan-wizard-core/references/guardrails.md +2 -2
  136. package/pan-wizard-core/references/handoff-decisions.md +3 -3
  137. package/pan-wizard-core/references/model-profile-resolution.md +5 -5
  138. package/pan-wizard-core/references/model-profiles.md +14 -9
  139. package/pan-wizard-core/references/phase-argument-parsing.md +7 -7
  140. package/pan-wizard-core/references/planning-config.md +4 -4
  141. package/pan-wizard-core/references/tdd.md +10 -1
  142. package/pan-wizard-core/references/verdict-contract.md +2 -1
  143. package/pan-wizard-core/references/verification-patterns.md +5 -5
  144. package/pan-wizard-core/templates/codebase/architecture.md +2 -2
  145. package/pan-wizard-core/templates/codebase/best-practices.md +2 -2
  146. package/pan-wizard-core/templates/codebase/concerns.md +3 -3
  147. package/pan-wizard-core/templates/codebase/integrations.md +6 -6
  148. package/pan-wizard-core/templates/codebase/relationships.md +2 -2
  149. package/pan-wizard-core/templates/codebase/stack.md +2 -2
  150. package/pan-wizard-core/templates/codebase/structure.md +4 -4
  151. package/pan-wizard-core/templates/codebase/testing.md +4 -4
  152. package/pan-wizard-core/templates/continue-here.md +1 -1
  153. package/pan-wizard-core/templates/debug-subagent-prompt.md +3 -2
  154. package/pan-wizard-core/templates/debug.md +15 -1
  155. package/pan-wizard-core/templates/discovery.md +1 -1
  156. package/pan-wizard-core/templates/milestone-archive.md +6 -6
  157. package/pan-wizard-core/templates/milestone.md +1 -1
  158. package/pan-wizard-core/templates/phase-prompt.md +31 -18
  159. package/pan-wizard-core/templates/planner-subagent-prompt.md +3 -5
  160. package/pan-wizard-core/templates/research-project/architecture.md +3 -3
  161. package/pan-wizard-core/templates/research-project/features.md +1 -1
  162. package/pan-wizard-core/templates/research-project/pitfalls.md +1 -1
  163. package/pan-wizard-core/templates/research-project/stack.md +3 -3
  164. package/pan-wizard-core/templates/research.md +5 -5
  165. package/pan-wizard-core/templates/retrospective.md +1 -1
  166. package/pan-wizard-core/templates/roadmap.md +15 -15
  167. package/pan-wizard-core/templates/summary-complex.md +1 -0
  168. package/pan-wizard-core/templates/summary-minimal.md +1 -0
  169. package/pan-wizard-core/templates/summary-standard.md +2 -1
  170. package/pan-wizard-core/templates/summary.md +18 -2
  171. package/pan-wizard-core/templates/uat.md +3 -3
  172. package/pan-wizard-core/templates/user-setup.md +8 -8
  173. package/pan-wizard-core/templates/verification-report.md +54 -4
  174. package/pan-wizard-core/workflows/add-phase.md +1 -1
  175. package/pan-wizard-core/workflows/assumptions.md +1 -1
  176. package/pan-wizard-core/workflows/discuss-phase.md +9 -8
  177. package/pan-wizard-core/workflows/exec-phase.md +34 -60
  178. package/pan-wizard-core/workflows/execute-plan.md +8 -7
  179. package/pan-wizard-core/workflows/health.md +13 -11
  180. package/pan-wizard-core/workflows/help.md +8 -9
  181. package/pan-wizard-core/workflows/learn.md +13 -11
  182. package/pan-wizard-core/workflows/map-codebase.md +10 -10
  183. package/pan-wizard-core/workflows/milestone-audit.md +10 -10
  184. package/pan-wizard-core/workflows/milestone-gaps.md +17 -24
  185. package/pan-wizard-core/workflows/milestone-new.md +7 -6
  186. package/pan-wizard-core/workflows/new-project.md +10 -10
  187. package/pan-wizard-core/workflows/optimize.md +11 -9
  188. package/pan-wizard-core/workflows/pause.md +6 -1
  189. package/pan-wizard-core/workflows/phase-tests.md +4 -4
  190. package/pan-wizard-core/workflows/plan-phase.md +31 -34
  191. package/pan-wizard-core/workflows/profile.md +2 -2
  192. package/pan-wizard-core/workflows/progress.md +5 -7
  193. package/pan-wizard-core/workflows/quick.md +10 -8
  194. package/pan-wizard-core/workflows/remove-phase.md +1 -1
  195. package/pan-wizard-core/workflows/research-phase.md +9 -5
  196. package/pan-wizard-core/workflows/resume-project.md +4 -6
  197. package/pan-wizard-core/workflows/retro.md +3 -1
  198. package/pan-wizard-core/workflows/settings.md +16 -24
  199. package/pan-wizard-core/workflows/todo-check.md +1 -1
  200. package/pan-wizard-core/workflows/transition.md +12 -12
  201. package/pan-wizard-core/workflows/update.md +25 -17
  202. package/pan-wizard-core/workflows/verify-phase.md +24 -17
  203. package/pan-zcode/KNOWN-BETA-RISKS.md +2 -2
  204. package/pan-zcode/README.md +7 -7
  205. package/pan-zcode/bin/install-zcode.js +1 -1
  206. package/scripts/build-agent-plugin.js +19 -5
  207. package/scripts/build-plugin.js +3 -1
  208. package/scripts/generate-skills-docs.py +13 -4
  209. package/scripts/test-surface.cjs +4 -0
@@ -6,7 +6,7 @@ const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { PHASES_DIR, MILESTONES_DIR, ROADMAP_FILE, REQUIREMENTS_FILE, STATE_FILE, isPlanFile } = require('./constants.cjs');
8
8
  const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, fileAccessible, planningRel } = require('./utils.cjs');
9
- const { output, error, isGitRepo, execGit, escapeRegex } = require('./core.cjs');
9
+ const { output, error, isGitRepo, isGitIgnored, execGit, escapeRegex, loadConfig } = require('./core.cjs');
10
10
  const { extractFrontmatter } = require('./frontmatter.cjs');
11
11
  const { writeStateMd } = require('./state.cjs');
12
12
 
@@ -258,8 +258,13 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
258
258
  result.archive_warnings = archiveResult.warnings;
259
259
  }
260
260
 
261
- // Auto-commit + tag unless --no-commit or not a git repo
262
- if (!options.noCommit && isGitRepo(cwd)) {
261
+ // Auto-commit + tag unless --no-commit, not a git repo, or the project keeps planning
262
+ // files out of git (`commit_docs: false` or a gitignored planning tree), the rules
263
+ // `phase complete` and `pan-tools commit` follow (this commit used to ignore both).
264
+ const noCommit = options.noCommit
265
+ || loadConfig(cwd).commit_docs === false
266
+ || isGitIgnored(cwd, planningRel());
267
+ if (!noCommit && isGitRepo(cwd)) {
263
268
  execGit(cwd, ['add', planningRel() + '/']);
264
269
  const commitMsg = `docs: milestone ${version} complete`;
265
270
  const commitResult = execGit(cwd, ['commit', '-m', commitMsg]);
@@ -8,7 +8,7 @@
8
8
 
9
9
  const fs = require('fs');
10
10
  const path = require('path');
11
- const { output, escapeRegex, execGit } = require('./core.cjs');
11
+ const { output, escapeRegex, execGit, toLf } = require('./core.cjs');
12
12
  const { planningPath, planningRel } = require('./utils.cjs');
13
13
 
14
14
  // ─── Storage layout ──────────────────────────────────────────────────────────
@@ -793,6 +793,27 @@ function revertedIndices(rows) {
793
793
  return out;
794
794
  }
795
795
 
796
+ /** The trace session a report belongs to, from its name (`<session>-opt-report.md`, `<session>-analysis.json`). */
797
+ function reportSessionId(reportPath) {
798
+ const m = path.basename(String(reportPath || '')).match(/^(.+?)-(?:opt-report\.md|analysis\.json)$/);
799
+ return m ? m[1] : null;
800
+ }
801
+
802
+ /**
803
+ * An older report's memory actions (`memory`, `memory_append`, `memory_entry`) still
804
+ * write into `.planning/memory/`, but PAN's workflows no longer load that folder into
805
+ * agents (ADR-0036, amended 2026-10-04). Say so in the result rather than let the
806
+ * apply look like it taught anyone anything.
807
+ */
808
+ const MEMORY_NOT_LOADED_WARNING = "stored in .planning/memory/, which PAN's workflows do not load into agents (ADR-0036): "
809
+ + "write a lesson where the agents read — the project's instructions, a test, or the code";
810
+
811
+ function notLoadedWarning(cwd, relPath) {
812
+ const abs = path.resolve(cwd, relPath || '');
813
+ if (path.dirname(abs) !== path.resolve(planningPath(cwd), 'memory')) return {};
814
+ return { warning: MEMORY_NOT_LOADED_WARNING };
815
+ }
816
+
796
817
  function applyReportRecommendations(cwd, reportPath) {
797
818
  let reportContent;
798
819
  try {
@@ -842,15 +863,23 @@ function applyReportRecommendations(cwd, reportPath) {
842
863
  const resolvedCwd = path.resolve(cwd);
843
864
  actions.forEach((action, i) => {
844
865
  try {
845
- // Containment: action.path on memory writes is report/agent-authored and
846
- // must stay inside the project. A `../` (or absolute) path otherwise
847
- // escapes and writes anywhere the process can (M23, ADR audit 2026-08).
866
+ // Containment: action.path on memory writes is report/agent-authored. A `../`
867
+ // (or absolute) path escaped the project (M23, ADR audit 2026-08), and a path
868
+ // inside it reached any file: a report's `memory_append` to CLAUDE.md made
869
+ // agent-written text a standing instruction with no review (ADR-0040), and
870
+ // `/pan:learn --apply` runs this on the optimizer's own report. A memory action
871
+ // writes memory: `.planning/memory/` and nothing else.
848
872
  if (action.type === 'memory' || action.type === 'memory_append') {
849
873
  const abs = path.resolve(cwd, action.path || '');
874
+ const memoryDir = path.resolve(planningPath(cwd), 'memory');
850
875
  if (abs !== resolvedCwd && !abs.startsWith(resolvedCwd + path.sep)) {
851
876
  skipped.push({ action, reason: 'path escapes project root — skipped' });
852
877
  return;
853
878
  }
879
+ if (!abs.startsWith(memoryDir + path.sep)) {
880
+ skipped.push({ action, reason: 'a memory action writes only into .planning/memory/ — skipped; make a change to the project\'s instructions yourself' });
881
+ return;
882
+ }
854
883
  }
855
884
  const earlier = live.get(actionSig(action));
856
885
  if (earlier) {
@@ -867,11 +896,29 @@ function applyReportRecommendations(cwd, reportPath) {
867
896
  fs.mkdirSync(path.dirname(memPath), { recursive: true });
868
897
  fs.writeFileSync(memPath, action.content, 'utf-8');
869
898
  records.push({ i, type: action.type, path: rel(memPath), kind: 'created', sha256_after: normalisedHash(fs.readFileSync(memPath, 'utf-8')), action_sig: actionSig(action) });
870
- applied.push({ action, result: `Written to ${action.path}` });
899
+ applied.push({ action, result: `Written to ${action.path}`, ...notLoadedWarning(cwd, action.path) });
871
900
  }
872
901
  } else if (action.type === 'memory_append') {
873
902
  appendRecorded(path.join(cwd, action.path), '\n' + action.content, action, i);
874
- applied.push({ action, result: `Appended to ${action.path}` });
903
+ applied.push({ action, result: `Appended to ${action.path}`, ...notLoadedWarning(cwd, action.path) });
904
+ } else if (action.type === 'memory_entry') {
905
+ // The gated write path (O6): a lesson drawn from this report's trace session,
906
+ // recorded only if it carries citations that hold and passes `memory record`'s
907
+ // other rules. The optimizer no longer proposes these; an older report's are
908
+ // still recorded, as created/appended so revert works.
909
+ const session = reportSessionId(reportPath);
910
+ if (!session) { skipped.push({ action, reason: 'a memory entry needs the trace session the report came from, and this report\'s name does not carry one' }); return; }
911
+ const { recordLesson } = require('./memory.cjs');
912
+ const logPath = path.join(planningPath(cwd), 'memory', `${String(action.agent || '')}.md`);
913
+ let before = null;
914
+ try { before = fs.readFileSync(logPath, 'utf-8'); } catch { /* a new log */ }
915
+ const r = recordLesson(cwd, String(action.agent || ''), { lesson: action.lesson, trace: session, cites: action.cites });
916
+ if (r.error || !r.recorded) { skipped.push({ action, reason: r.error || `memory record refused it: ${r.reason}` }); return; }
917
+ const after = fs.readFileSync(logPath, 'utf-8');
918
+ records.push(before === null
919
+ ? { i, type: action.type, path: rel(logPath), kind: 'created', sha256_after: normalisedHash(after), action_sig: actionSig(action) }
920
+ : { i, type: action.type, path: rel(logPath), kind: 'appended', text: toLf(after).slice(toLf(before).length), sha256_after: normalisedHash(after), action_sig: actionSig(action) });
921
+ applied.push({ action, result: `Recorded for ${r.agent} from ${r.evidence}`, warning: MEMORY_NOT_LOADED_WARNING });
875
922
  } else if (action.type === 'note') {
876
923
  // Write a human-readable suggestion note
877
924
  const entry = `\n## ${new Date().toISOString()}: ${action.description || 'Suggestion'}\n\n${action.content || action.suggestion || ''}\n\n**Target:** ${action.target || 'unspecified'}\n`;
@@ -992,7 +1039,11 @@ function revertApply(cwd, which) {
992
1039
  return { apply_id: apply.apply_id, reverted: out.sort((x, y) => x - y), refused, status: remaining === 0 ? 'reverted' : (out.length ? 'partial' : 'refused') };
993
1040
  }
994
1041
 
995
- // Derive basic memory actions from a raw JSON analysis when no optimizer agent
1042
+ // Where a lesson reaches the next run's agents: they read the project's
1043
+ // instructions, and PAN's workflows no longer load `.planning/memory/` (ADR-0036).
1044
+ const LESSON_TARGET = "the project's instructions (CLAUDE.md or AGENTS.md, outside PAN's section)";
1045
+
1046
+ // Derive suggestion notes from a raw JSON analysis when no optimizer agent
996
1047
  // report is available (fallback for /pan:optimize apply on a JSON file).
997
1048
  function deriveActionsFromAnalysis(analysis) {
998
1049
  const actions = [];
@@ -1002,9 +1053,9 @@ function deriveActionsFromAnalysis(analysis) {
1002
1053
  memory_miss_patterns.slice(0, 3).forEach(p => {
1003
1054
  actions.push({
1004
1055
  type: 'note',
1005
- description: `Memory miss: ${p.pattern}`,
1006
- content: `This topic was missing from memory ${p.count} time(s) during the traced session. Consider adding a memory entry for it.`,
1007
- target: '.planning/memory/',
1056
+ description: `Missing knowledge: ${p.pattern}`,
1057
+ content: `An agent logged this as missing ${p.count} time(s) during the traced session. Write the answer where the agents read: ${LESSON_TARGET}, or a comment at the code it concerns.`,
1058
+ target: LESSON_TARGET,
1008
1059
  });
1009
1060
  });
1010
1061
  }
@@ -1016,8 +1067,8 @@ function deriveActionsFromAnalysis(analysis) {
1016
1067
  actions.push({
1017
1068
  type: 'note',
1018
1069
  description: `Recurring tool failure: ${p.agent} ${p.tool} (${p.error_class})`,
1019
- content: `${p.agent}'s ${p.tool} call failed with "${p.message}" in ${p.spawns} spawns across ${p.sessions} session(s) (${p.occurrences} times in all). Record the working form of the call, or the precondition it needs, as a memory entry for ${p.agent}.`,
1020
- target: '.planning/memory/',
1070
+ content: `${p.agent}'s ${p.tool} call failed with "${p.message}" in ${p.spawns} spawns across ${p.sessions} session(s) (${p.occurrences} times in all). Write the working form of the call, or the precondition it needs, where ${p.agent} reads it: ${LESSON_TARGET}.`,
1071
+ target: LESSON_TARGET,
1021
1072
  });
1022
1073
  });
1023
1074
  Object.entries(analysis.verdict_stats || {}).filter(([, s]) => s.fail >= 2).forEach(([agent, s]) => {
@@ -1025,7 +1076,7 @@ function deriveActionsFromAnalysis(analysis) {
1025
1076
  type: 'note',
1026
1077
  description: `Repeated judge failures: ${agent}`,
1027
1078
  content: `${agent} failed ${s.fail} time(s) (${s.retries} retr${s.retries === 1 ? 'y' : 'ies'}, ${s.resolved_by_retry} resolved by the retry). Read the recorded findings (\`pan-tools findings list --agent ${agent}\`) for the class that keeps recurring before the next phase.`,
1028
- target: '.planning/memory/',
1079
+ target: '.planning/findings.jsonl',
1029
1080
  });
1030
1081
  });
1031
1082
 
@@ -1034,8 +1085,8 @@ function deriveActionsFromAnalysis(analysis) {
1034
1085
  actions.push({
1035
1086
  type: 'note',
1036
1087
  description: `Knowledge gap: ${p.pattern}`,
1037
- content: `The agent had to infer this ${p.count} time(s). Research and cache the answer.`,
1038
- target: '.planning/memory/',
1088
+ content: `The agent had to infer this ${p.count} time(s). Research the answer and write it where the agents read: ${LESSON_TARGET}, or a comment at the code it concerns.`,
1089
+ target: LESSON_TARGET,
1039
1090
  });
1040
1091
  });
1041
1092
  }
@@ -1286,6 +1337,14 @@ function validatePromoteInputs(pattern, opts) {
1286
1337
  if (!pattern.id) return 'pattern.id is required';
1287
1338
  if (!pattern.summary) return 'pattern.summary is required';
1288
1339
  if (!pattern.rule) return 'pattern.rule is required';
1340
+ // A citation names the code the pattern rests on, relative to the tree that
1341
+ // holds the store; it must hold now, and learn lint L-007 keeps checking it.
1342
+ if (pattern.cites !== undefined && !Array.isArray(pattern.cites)) return 'pattern.cites must be a list';
1343
+ const { citationProblem } = require('./memory.cjs');
1344
+ for (const c of pattern.cites || []) {
1345
+ const problem = citationProblem(opts.sourceRoot, c);
1346
+ if (problem) return `citation ${c}: ${problem}`;
1347
+ }
1289
1348
  return null;
1290
1349
  }
1291
1350
 
@@ -1366,7 +1425,8 @@ function classifyPatternKind(pattern) {
1366
1425
  * always written by us — no general YAML dependency needed).
1367
1426
  */
1368
1427
  function readTopicFile(filePath) {
1369
- const content = fs.readFileSync(filePath, 'utf-8');
1428
+ // LF: an install committed with core.autocrlf=true holds these files as CRLF.
1429
+ const content = toLf(fs.readFileSync(filePath, 'utf-8'));
1370
1430
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
1371
1431
  if (!fmMatch) {
1372
1432
  return { frontmatter: { topic: '', patterns: [] }, body: content, _raw: content };
@@ -1429,6 +1489,13 @@ function serializeTopicFile(topic, patterns, body) {
1429
1489
  fm += ` promoted_at: ${p.promoted_at || ts}\n`;
1430
1490
  const srcExps = Array.isArray(p.source_experiments) ? p.source_experiments : [];
1431
1491
  fm += ` source_experiments: [${srcExps.join(', ')}]\n`;
1492
+ // Optional fields survive a re-serialisation: promoting into an existing topic
1493
+ // rewrites every pattern's frontmatter, and dropping these lost the supersession
1494
+ // learn lint L-005 reads, and the citations L-007 checks.
1495
+ const cites = Array.isArray(p.cites) ? p.cites : [];
1496
+ if (cites.length) fm += ` cites: [${cites.join(', ')}]\n`;
1497
+ if (p.superseded_by) fm += ` superseded_by: ${p.superseded_by}\n`;
1498
+ if (p.superseded_id) fm += ` superseded_id: ${p.superseded_id}\n`;
1432
1499
  }
1433
1500
  return `---\n${fm}---\n${body}`;
1434
1501
  }
@@ -1519,6 +1586,7 @@ function promotePattern(pattern, opts) {
1519
1586
  summary: pattern.summary,
1520
1587
  promoted_at: promotedAt,
1521
1588
  source_experiments: pattern.source_experiments || [],
1589
+ ...(pattern.cites && pattern.cites.length ? { cites: pattern.cites } : {}),
1522
1590
  });
1523
1591
 
1524
1592
  const serialized = serializeTopicFile(topic, frontmatter.patterns, body);
@@ -173,6 +173,74 @@ function renumberIntegerPhases(phasesDir, removedInt) {
173
173
  return { renamedDirs, renamedFiles };
174
174
  }
175
175
 
176
+ /**
177
+ * Remove one phase's section from roadmap text. The section runs from its `Phase N:` heading
178
+ * to the first line outside a code fence that is another phase heading, a heading at the
179
+ * section's own level or above (`## Progress`, a milestone group), a `</details>` that closes
180
+ * an enclosing block, or a collapsed milestone's `<details>` (its `<summary>` names a version
181
+ * or a phase range). Any other `<details>` block the section opens and closes itself is part
182
+ * of it. Running to the end of the file deleted `## Progress` with the last phase and the next
183
+ * milestone group's heading with a group's last phase; a line-blind bound stopped at a
184
+ * `# comment` or `## Usage` inside a fenced block.
185
+ * @param {string} content - roadmap.md text (LF or CRLF; line endings are kept)
186
+ * @param {string} phaseNum - the phase number as written in its heading
187
+ * @returns {string} the text without that section (unchanged when no heading matches)
188
+ */
189
+ function removePhaseSection(content, phaseNum) {
190
+ const lines = content.split('\n');
191
+ const fenced = new Array(lines.length).fill(false);
192
+ // A fence line with an info string opens a block, even inside another (a nested example);
193
+ // a bare one closes the innermost block it can close. A block never closed stays unfenced.
194
+ for (let i = 0, stack = []; i < lines.length; i++) {
195
+ const f = lines[i].match(/^\s{0,3}(`{3,}|~{3,})(.*)/);
196
+ if (!f) continue;
197
+ const top = stack[stack.length - 1];
198
+ if (top && !f[2].trim() && f[1][0] === top.fence[0] && f[1].length >= top.fence.length) {
199
+ stack.pop();
200
+ fenced.fill(true, top.open, i + 1);
201
+ } else if (!top || f[2].trim()) stack.push({ fence: f[1], open: i });
202
+ }
203
+ const headRe = new RegExp(`^(#{2,4})\\s*Phase\\s+${escapeRegex(phaseNum)}\\s*:`, 'i');
204
+ const start = lines.findIndex((l, i) => !fenced[i] && headRe.test(l));
205
+ if (start === -1) return content;
206
+ const levelEndRe = new RegExp(`^#{1,${lines[start].match(headRe)[1].length}}\\s`);
207
+ const endsSection = (i) => !fenced[i] && (/^#{2,4}\s*Phase\s+\d/i.test(lines[i]) || levelEndRe.test(lines[i]));
208
+ const opens = (i) => !fenced[i] && /^\s*<details\b/i.test(lines[i]);
209
+ const closes = (i) => !fenced[i] && /^\s*<\/details\b/i.test(lines[i]);
210
+ const oneLine = (i) => /<\/details\s*>/i.test(lines[i]);
211
+ // A collapsed milestone's <summary> names its version or phase range ("v1.0 MVP (Phases 1-4)").
212
+ const milestone = (i) => /<summary>[^<]*\b(?:v\d+\.\d+|phases\s+\d)/i.test(`${lines[i]} ${lines[i + 1] || ''}`);
213
+ // A <details> closed before anything that ends the section is the section's own block.
214
+ const ownBlock = (at) => {
215
+ for (let i = at + 1, d = 0; i < lines.length; i++) {
216
+ if (endsSection(i)) return false;
217
+ if (opens(i) && !oneLine(i)) d++;
218
+ else if (closes(i)) { if (d === 0) return true; d--; }
219
+ }
220
+ return false;
221
+ };
222
+ let end = start + 1;
223
+ for (let depth = 0; end < lines.length; end++) {
224
+ if (endsSection(end)) break;
225
+ if (opens(end)) {
226
+ if (depth === 0 && milestone(end)) break;
227
+ if (oneLine(end)) continue;
228
+ if (depth > 0 || ownBlock(end)) { depth++; continue; }
229
+ break;
230
+ }
231
+ if (closes(end)) { if (depth > 0) { depth--; continue; } break; }
232
+ }
233
+ const toEof = end === lines.length;
234
+ lines.splice(start, end - start);
235
+ let out = lines.join('\n');
236
+ // A section that ran to the end of the file took the final line ending with it.
237
+ if (toEof && content.endsWith('\n')) {
238
+ out = out.replace(/\r$/, '');
239
+ if (!out.endsWith('\n')) out += content.endsWith('\r\n') ? '\r\n' : '\n';
240
+ }
241
+ return out;
242
+ }
243
+
176
244
  /**
177
245
  * Rewrite roadmap.md after a phase is removed: delete the target section,
178
246
  * remove checkbox/table references, and renumber subsequent phase references.
@@ -189,14 +257,8 @@ function updateRoadmapAfterRemoval(cwd, phaseNum, isDecimal, normalized) {
189
257
  roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
190
258
  } catch { return; }
191
259
 
192
- // Remove the target phase section from roadmap.md.
193
- // Matches from the phase heading to the next phase heading (or end of file).
260
+ roadmapContent = removePhaseSection(roadmapContent, phaseNum);
194
261
  const targetEscaped = escapeRegex(phaseNum);
195
- const sectionPattern = new RegExp(
196
- `\\n?#{2,4}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`,
197
- 'i'
198
- );
199
- roadmapContent = roadmapContent.replace(sectionPattern, '');
200
262
 
201
263
  // Remove checkbox list items referencing this phase
202
264
  const checkboxPattern = new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${targetEscaped}[:\\s][^\\n]*`, 'gi');
@@ -207,8 +269,9 @@ function updateRoadmapAfterRemoval(cwd, phaseNum, isDecimal, normalized) {
207
269
  roadmapContent = roadmapContent.replace(tableRowPattern, '');
208
270
 
209
271
  // For integer phase removal, renumber all references to subsequent phases.
210
- // Walk from highest phase number down to removedInt+1, decrementing each by 1.
211
- // This avoids double-renaming (e.g. 8->7 then 7->6 would break if done ascending).
272
+ // Walk UP from removedInt+1, decrementing each by 1. A downward walk re-hits its
273
+ // own output (10->9, then that 9 and the real 9 ->8), so every later phase
274
+ // collapsed onto the removed number.
212
275
  if (!isDecimal) {
213
276
  const removedInt = parseInt(normalized, 10);
214
277
 
@@ -219,35 +282,35 @@ function updateRoadmapAfterRemoval(cwd, phaseNum, isDecimal, normalized) {
219
282
  // and an unbounded lower end is how `phase remove -1234567890` turned into
220
283
  // ~1.2 billion regex passes. The loop can now never exceed maxPhase steps.
221
284
  const stopAt = Math.max(removedInt, 0);
222
- for (let oldNum = maxPhase; oldNum > stopAt; oldNum--) {
285
+ for (let oldNum = stopAt + 1; oldNum <= maxPhase; oldNum++) {
223
286
  const newNum = oldNum - 1;
224
287
  const oldStr = String(oldNum);
225
288
  const newStr = String(newNum);
226
289
  const oldPad = oldStr.padStart(2, '0');
227
290
  const newPad = newStr.padStart(2, '0');
228
291
 
229
- // Phase headings: ## Phase 18: or ### Phase 18: -> ## Phase 17:
292
+ // Phase headings: ## Phase 18: or ### Phase 18.1: -> ## Phase 17: / ### Phase 17.1:
230
293
  roadmapContent = roadmapContent.replace(
231
- new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'),
294
+ new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}((?:\\.\\d+)?\\s*:)`, 'gi'),
232
295
  `$1${newStr}$2`
233
296
  );
234
297
 
235
- // Inline phase references: "Phase 18:" or "Phase 18 " -> "Phase 17:"
298
+ // Inline phase references: "Phase 18:", "Phase 18 " or "Phase 18.1:" -> "Phase 17..."
236
299
  roadmapContent = roadmapContent.replace(
237
- new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'),
300
+ new RegExp(`(Phase\\s+)${oldStr}((?:\\.\\d+)?[:\\s])`, 'g'),
238
301
  `$1${newStr}$2`
239
302
  );
240
303
 
241
- // Plan references in padded form: 18-01 -> 17-01
304
+ // Plan references in padded form: 18-01 / 18.1-01 -> 17-01 / 17.1-01, never inside a date (2026-10-01)
242
305
  roadmapContent = roadmapContent.replace(
243
- new RegExp(`${oldPad}-(\\d{2})`, 'g'),
244
- `${newPad}-$1`
306
+ new RegExp(`(?<![\\d.-])${oldPad}((?:\\.\\d+)?)-(\\d{2})(?!\\d)`, 'g'),
307
+ `${newPad}$1-$2`
245
308
  );
246
309
 
247
- // Progress table row numbers: | 18. -> | 17.
310
+ // Progress table row numbers: | 18. or | 18.1. -> | 17. / | 17.1.
248
311
  roadmapContent = roadmapContent.replace(
249
- new RegExp(`(\\|\\s*)${oldStr}\\.\\s`, 'g'),
250
- `$1${newStr}. `
312
+ new RegExp(`(\\|\\s*)${oldStr}((?:\\.\\d+)?)\\.\\s`, 'g'),
313
+ `$1${newStr}$2. `
251
314
  );
252
315
 
253
316
  // Depends-on references: "Depends on:** Phase 18" -> "Phase 17"
@@ -279,7 +342,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
279
342
  // PHASE_NUM_RE does not match — so a bad argument used to reach the renumber
280
343
  // logic and do real damage, in both cases reporting success and exiting 0:
281
344
  // `phase remove 0` renumbered EVERY roadmap heading down to "Phase 0"
282
- // (the descending renumber loop runs 99..1 and re-hits
345
+ // (the renumber loop then ran down from 99 and re-hit
283
346
  // its own output, collapsing 3->2->1->0) and renamed
284
347
  // every phase directory one lower.
285
348
  // `phase remove -1e9` spun that same loop ~1.2 billion times, each pass
@@ -425,6 +488,7 @@ module.exports = {
425
488
  collectDirsToRenumber,
426
489
  renamePhaseDir,
427
490
  renumberIntegerPhases,
491
+ removePhaseSection,
428
492
  updateRoadmapAfterRemoval,
429
493
  cmdPhaseRemove,
430
494
  updateStateAfterPhaseRemoval,
@@ -96,14 +96,20 @@ function resolvePhase(cwd, phaseNumber) {
96
96
  function extractPhaseGoal(cwd, phaseNumber) {
97
97
  const content = safeReadFile(path.join(planningPath(cwd), ROADMAP_FILE));
98
98
  if (!content) return { objective: null, success_criteria: [] };
99
- const re = new RegExp('#{2,4}\\s*Phase\\s+' + escapeRegex(String(phaseNumber)) + ':\\s*([^\\n]+)', 'i');
99
+ // Padded or not: the number comes from the zero-padded phase directory ("02"),
100
+ // while PAN writes `### Phase 2:` headings, so the report found no goal.
101
+ const unpadded = String(phaseNumber).trim().replace(/^0+(?=\d)/, '');
102
+ const re = new RegExp('#{2,4}\\s*Phase\\s+0*' + escapeRegex(unpadded) + ':\\s*([^\\n]+)', 'i');
100
103
  const m = content.match(re);
101
104
  if (!m) return { objective: null, success_criteria: [] };
102
105
  const start = m.index;
103
106
  const rest = content.slice(start + 1);
104
107
  const nextH = rest.match(/\n#{2,4}\s+Phase\s+\d/i);
105
108
  const end = nextH ? start + 1 + nextH.index : content.length;
106
- const section = content.slice(start, end);
109
+ let section = content.slice(start, end);
110
+ // A phase moved out by `roadmap compact` keeps a stub; its criteria are in the history.
111
+ const compacted = require('./roadmap-compact.cjs').compactedPhaseSection(cwd, phaseNumber, section);
112
+ if (compacted && compacted.section) section = compacted.section;
107
113
  const gm = section.match(/(?:\*\*Goal:\*\*|\*\*Goal\*\*:)\s*([^\n]+)/i);
108
114
  const objective = gm ? gm[1].trim() : null;
109
115
  const cm = section.match(/\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\s*\d+\.\s*[^\n]+\n?)+)/i);
@@ -4,10 +4,10 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, loadConfig, output, error, toPosix, isGitRepo, execGit } = require('./core.cjs');
7
+ const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, loadConfig, output, error, toPosix, isGitRepo, isGitIgnored, execGit } = require('./core.cjs');
8
8
  const { extractFrontmatter } = require('./frontmatter.cjs');
9
9
  const { writeStateMd, readStateSafe } = require('./state.cjs');
10
- const { enumerateRoadmapPhases } = require('./roadmap.cjs');
10
+ const { enumerateRoadmapPhases, roadmapRequirementIds } = require('./roadmap.cjs');
11
11
  const { PHASES_DIR, ROADMAP_FILE, REQUIREMENTS_FILE, STATE_FILE, isPlanFile, isSummaryFile, getPlanId, PHASE_DIR_RE, ARCHIVE_DIR_RE } = require('./constants.cjs');
12
12
  const { planningPath, phasesPath, filterPlanFiles, filterSummaryFiles, parsePhaseDir, fileAccessible, planningRel } = require('./utils.cjs');
13
13
  // Phase removal lives in phase-remove.cjs; re-exported below so consumers of
@@ -63,7 +63,7 @@ function cmdPhasesList(cwd, options, raw) {
63
63
  // If filtering by phase number
64
64
  if (phase) {
65
65
  const normalized = normalizePhaseName(phase);
66
- const match = dirs.find(dir => dir.startsWith(normalized));
66
+ const match = dirs.find(dir => dir === normalized || dir.startsWith(normalized + '-'));
67
67
  if (!match) {
68
68
  output({ files: [], count: 0, phase_dir: null, error: 'Phase not found' }, raw, '');
69
69
  return;
@@ -200,7 +200,7 @@ function cmdFindPhase(cwd, phase, raw) {
200
200
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
201
201
  const dirs = entries.filter(entry => entry.isDirectory()).map(entry => entry.name).sort((left, right) => comparePhaseNum(left, right));
202
202
 
203
- const match = dirs.find(dir => dir.startsWith(normalized));
203
+ const match = dirs.find(dir => dir === normalized || dir.startsWith(normalized + '-'));
204
204
  if (!match) {
205
205
  output(notFound, raw, '');
206
206
  return;
@@ -239,7 +239,7 @@ function cmdFindPhase(cwd, phase, raw) {
239
239
  * @param {boolean} raw - If true, output raw value instead of JSON
240
240
  * @returns {void}
241
241
  */
242
- function cmdPhasePlanIndex(cwd, phase, raw) {
242
+ function cmdPhasePlanIndex(cwd, phase, raw, opts = {}) {
243
243
  if (!phase) {
244
244
  error('phase required for phase-plan-index');
245
245
  }
@@ -253,7 +253,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
253
253
  try {
254
254
  const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
255
255
  const dirs = entries.filter(entry => entry.isDirectory()).map(entry => entry.name).sort((left, right) => comparePhaseNum(left, right));
256
- const match = dirs.find(dir => dir.startsWith(normalized));
256
+ const match = dirs.find(dir => dir === normalized || dir.startsWith(normalized + '-'));
257
257
  if (match) {
258
258
  phaseDir = path.join(phasesDir, match);
259
259
  phaseDirName = match;
@@ -280,7 +280,52 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
280
280
 
281
281
  const { plans, waves, incomplete, hasCheckpoints } = buildPlanIndex(phaseDir, planFiles, summaryFiles);
282
282
 
283
- output({ phase: normalized, plans, waves, incomplete, has_checkpoints: hasCheckpoints }, raw);
283
+ const result = { phase: normalized, plans, waves, incomplete, has_checkpoints: hasCheckpoints };
284
+ // `--failed <ids>` (market-ideas M28): the plans that must not run after those
285
+ // failed, because a dependency, direct or transitive, is among them.
286
+ if (opts.failed) {
287
+ const failed = String(opts.failed).split(',').map((s) => s.trim()).filter(Boolean);
288
+ result.failed = failed;
289
+ result.blocked = blockedPlans(plans, failed);
290
+ }
291
+ output(result, raw);
292
+ }
293
+
294
+ /** Whether a `depends_on` entry names this plan: its full id ("03-02") or its plan number ("02", "2"). */
295
+ function dependencyNames(dep, planId) {
296
+ const d = String(dep).trim();
297
+ if (d === planId) return true;
298
+ const own = planId.split('-').pop();
299
+ const strip = (s) => s.replace(/^0+(?=\d)/, '');
300
+ return /^\d+$/.test(d) && /^\d+$/.test(own) && strip(d) === strip(own);
301
+ }
302
+
303
+ /**
304
+ * The plans that must not run because a plan they depend on failed (market-ideas
305
+ * M28). Transitive: a plan waiting on a blocked plan is blocked too. Plans that
306
+ * already have a summary are done and never blocked.
307
+ * @param {Array<{id: string, depends_on: string[], has_summary: boolean}>} plans
308
+ * @param {string[]} failedIds
309
+ * @returns {Array<{id: string, blocked_by: string[]}>} in plan order; blocked_by names the failed plans
310
+ */
311
+ function blockedPlans(plans, failedIds) {
312
+ const failed = new Set(failedIds);
313
+ const blockedBy = new Map(); // plan id → Set of failed ids behind it
314
+ let changed = true;
315
+ while (changed) {
316
+ changed = false;
317
+ for (const p of plans) {
318
+ if (p.has_summary || failed.has(p.id)) continue;
319
+ const roots = blockedBy.get(p.id) || new Set();
320
+ const before = roots.size;
321
+ for (const dep of p.depends_on || []) {
322
+ for (const f of failed) if (dependencyNames(dep, f)) roots.add(f);
323
+ for (const [b, bRoots] of blockedBy) if (dependencyNames(dep, b)) for (const r of bRoots) roots.add(r);
324
+ }
325
+ if (roots.size > before) { blockedBy.set(p.id, roots); changed = true; }
326
+ }
327
+ }
328
+ return plans.filter((p) => blockedBy.has(p.id)).map((p) => ({ id: p.id, blocked_by: [...blockedBy.get(p.id)].sort() }));
284
329
  }
285
330
 
286
331
  /**
@@ -339,10 +384,14 @@ function buildPlanIndex(phaseDir, planFiles, summaryFiles) {
339
384
  const hasSummary = completedPlanIds.has(planId);
340
385
  if (!hasSummary) incomplete.push(planId);
341
386
 
387
+ const rawDeps = frontmatter.depends_on || frontmatter['depends-on'];
388
+ const dependsOn = (Array.isArray(rawDeps) ? rawDeps : (rawDeps ? [rawDeps] : [])).map(String).filter((d) => d.trim());
389
+
342
390
  plans.push({
343
391
  id: planId, wave, autonomous,
344
392
  objective,
345
393
  files_modified: filesModified,
394
+ depends_on: dependsOn,
346
395
  task_count: taskCount,
347
396
  has_summary: hasSummary,
348
397
  });
@@ -659,12 +708,10 @@ function markRequirementsCompleteForPhase(cwd, phaseNum, roadmapContent) {
659
708
  const reqPath = path.join(planningPath(cwd), REQUIREMENTS_FILE);
660
709
  try {
661
710
  const reqReadContent = fs.readFileSync(reqPath, 'utf-8');
662
- const reqMatch = roadmapContent.match(
663
- new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?(?:\\*\\*Requirements:\\*\\*|\\*\\*Requirements\\*\\*:)\\s*([^\\n]+)`, 'i')
664
- );
665
- if (!reqMatch) return;
666
-
667
- const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(id => id.trim()).filter(Boolean);
711
+ // The phase's own requirements, matched padded or not (roadmapRequirementIds): a
712
+ // bare `Phase N … Requirements` search ticked phase 1's when completing phase 5.
713
+ const reqIds = roadmapRequirementIds(roadmapContent, phaseNum);
714
+ if (reqIds.length === 0) return;
668
715
  let reqContent = reqReadContent;
669
716
 
670
717
  for (const reqId of reqIds) {
@@ -851,8 +898,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw, opts) {
851
898
  result.roadmap_updated = false;
852
899
  }
853
900
 
854
- // Auto-commit .planning/ metadata unless --no-commit or not a git repo
855
- const noCommit = opts && opts.noCommit;
901
+ // Auto-commit .planning/ metadata unless --no-commit, not a git repo, or the project
902
+ // keeps planning files out of git: `commit_docs: false` or a gitignored planning
903
+ // tree, the same rules `pan-tools commit` follows (this commit used to ignore both).
904
+ const noCommit = (opts && opts.noCommit)
905
+ || loadConfig(cwd).commit_docs === false
906
+ || isGitIgnored(cwd, planningRel());
856
907
  if (!noCommit && isGitRepo(cwd)) {
857
908
  const commitMsg = `docs(${normalized}): complete phase — ${phaseInfo.phase_name}`;
858
909
  execGit(cwd, ['add', planningRel() + '/']);
@@ -930,6 +981,7 @@ module.exports = {
930
981
  cmdPhaseNextDecimal,
931
982
  cmdFindPhase,
932
983
  cmdPhasePlanIndex,
984
+ blockedPlans,
933
985
  cmdPhaseAdd,
934
986
  cmdPhaseInsert,
935
987
  cmdPhaseRemove,
@@ -20,6 +20,7 @@ const {
20
20
  findPhaseInternal,
21
21
  getRoadmapPhaseInternal,
22
22
  toPosix,
23
+ toLf,
23
24
  } = require('./core.cjs');
24
25
  const {
25
26
  ROADMAP_FILE,
@@ -210,7 +211,8 @@ function buildPhaseDependencyGraph(cwd) {
210
211
  let files = [];
211
212
  try { files = fs.readdirSync(fullDir).filter(isPlanFile); } catch { continue; }
212
213
 
213
- const phaseText = files.map(f => safeReadFile(path.join(fullDir, f)) || '').join('\n');
214
+ // LF, so the frontmatter match below also finds a CRLF plan's depends_on.
215
+ const phaseText = toLf(files.map(f => safeReadFile(path.join(fullDir, f)) || '').join('\n'));
214
216
 
215
217
  // Explicit via frontmatter. Parse depends_on as freeform string (either
216
218
  // inline `[phase:1, phase:2]`, or block-list). Extract all digit runs.
@@ -19,7 +19,7 @@
19
19
 
20
20
  const fs = require('fs');
21
21
  const path = require('path');
22
- const { output, error, safeReadFile, toPosix } = require('./core.cjs');
22
+ const { output, error, safeReadFile, toPosix, escapeTableCell } = require('./core.cjs');
23
23
  const { planningPath } = require('./utils.cjs');
24
24
  const { publish } = require('./bus.cjs');
25
25
  const { findVerdictBlocks, validateVerdict } = require('./verdict.cjs');
@@ -235,7 +235,7 @@ function writeDeepReview(cwd, phaseNum, payload, opts) {
235
235
  const loc = f.file ? `\`${f.file}${f.line ? `:${f.line}` : ''}\`` : '—';
236
236
  // Neutralize markdown-table-breaking chars: escape pipes and flatten any
237
237
  // newlines so a finding description can't corrupt the table structure.
238
- const desc = String(f.description).replace(/\\/g, '\\\\').replace(/\|/g, '\\|').replace(/[\r\n]+/g, ' ');
238
+ const desc = escapeTableCell(f.description);
239
239
  lines.push(`| ${f.severity} | ${f.source} | ${f.category} | ${desc} | ${loc} |`);
240
240
  }
241
241
  lines.push('');