maxsimcli 3.10.3 → 3.11.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 (78) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/assets/CHANGELOG.md +7 -0
  3. package/dist/assets/dashboard/server.js +5 -1
  4. package/dist/assets/templates/workflows/execute-plan.md +10 -0
  5. package/dist/cli.cjs +554 -439
  6. package/dist/cli.cjs.map +1 -1
  7. package/dist/cli.js +5 -0
  8. package/dist/cli.js.map +1 -1
  9. package/dist/core/commands.d.ts +7 -0
  10. package/dist/core/commands.d.ts.map +1 -1
  11. package/dist/core/commands.js +36 -34
  12. package/dist/core/commands.js.map +1 -1
  13. package/dist/core/core.d.ts +21 -1
  14. package/dist/core/core.d.ts.map +1 -1
  15. package/dist/core/core.js +81 -36
  16. package/dist/core/core.js.map +1 -1
  17. package/dist/core/index.d.ts +1 -1
  18. package/dist/core/index.d.ts.map +1 -1
  19. package/dist/core/index.js.map +1 -1
  20. package/dist/core/init.d.ts +2 -2
  21. package/dist/core/init.d.ts.map +1 -1
  22. package/dist/core/init.js +33 -51
  23. package/dist/core/init.js.map +1 -1
  24. package/dist/core/milestone.d.ts.map +1 -1
  25. package/dist/core/milestone.js +15 -20
  26. package/dist/core/milestone.js.map +1 -1
  27. package/dist/core/phase.d.ts +33 -0
  28. package/dist/core/phase.d.ts.map +1 -1
  29. package/dist/core/phase.js +275 -224
  30. package/dist/core/phase.js.map +1 -1
  31. package/dist/core/roadmap.d.ts.map +1 -1
  32. package/dist/core/roadmap.js +16 -18
  33. package/dist/core/roadmap.js.map +1 -1
  34. package/dist/core/state.d.ts +5 -0
  35. package/dist/core/state.d.ts.map +1 -1
  36. package/dist/core/state.js +44 -37
  37. package/dist/core/state.js.map +1 -1
  38. package/dist/core/template.d.ts.map +1 -1
  39. package/dist/core/template.js +1 -1
  40. package/dist/core/template.js.map +1 -1
  41. package/dist/core/types.d.ts +3 -2
  42. package/dist/core/types.d.ts.map +1 -1
  43. package/dist/core/types.js.map +1 -1
  44. package/dist/core/verify.d.ts.map +1 -1
  45. package/dist/core/verify.js +61 -80
  46. package/dist/core/verify.js.map +1 -1
  47. package/dist/install.cjs +23 -0
  48. package/dist/install.cjs.map +1 -1
  49. package/dist/install.js +37 -0
  50. package/dist/install.js.map +1 -1
  51. package/dist/mcp/index.d.ts +12 -0
  52. package/dist/mcp/index.d.ts.map +1 -0
  53. package/dist/mcp/index.js +21 -0
  54. package/dist/mcp/index.js.map +1 -0
  55. package/dist/mcp/phase-tools.d.ts +13 -0
  56. package/dist/mcp/phase-tools.d.ts.map +1 -0
  57. package/dist/mcp/phase-tools.js +164 -0
  58. package/dist/mcp/phase-tools.js.map +1 -0
  59. package/dist/mcp/state-tools.d.ts +13 -0
  60. package/dist/mcp/state-tools.d.ts.map +1 -0
  61. package/dist/mcp/state-tools.js +185 -0
  62. package/dist/mcp/state-tools.js.map +1 -0
  63. package/dist/mcp/todo-tools.d.ts +13 -0
  64. package/dist/mcp/todo-tools.d.ts.map +1 -0
  65. package/dist/mcp/todo-tools.js +143 -0
  66. package/dist/mcp/todo-tools.js.map +1 -0
  67. package/dist/mcp/utils.d.ts +27 -0
  68. package/dist/mcp/utils.d.ts.map +1 -0
  69. package/dist/mcp/utils.js +82 -0
  70. package/dist/mcp/utils.js.map +1 -0
  71. package/dist/mcp-server.cjs +11806 -0
  72. package/dist/mcp-server.cjs.map +1 -0
  73. package/dist/mcp-server.d.cts +2 -0
  74. package/dist/mcp-server.d.ts +12 -0
  75. package/dist/mcp-server.d.ts.map +1 -0
  76. package/dist/mcp-server.js +31 -0
  77. package/dist/mcp-server.js.map +1 -0
  78. package/package.json +5 -3
package/dist/cli.cjs CHANGED
@@ -4727,6 +4727,45 @@ function error(message) {
4727
4727
  process.stderr.write("Error: " + message + "\n");
4728
4728
  process.exit(1);
4729
4729
  }
4730
+ /** Today's date as YYYY-MM-DD. */
4731
+ function todayISO() {
4732
+ return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4733
+ }
4734
+ /** Canonical .planning/ sub-paths. */
4735
+ function planningPath(cwd, ...segments) {
4736
+ return node_path.default.join(cwd, ".planning", ...segments);
4737
+ }
4738
+ function statePath(cwd) {
4739
+ return planningPath(cwd, "STATE.md");
4740
+ }
4741
+ function roadmapPath(cwd) {
4742
+ return planningPath(cwd, "ROADMAP.md");
4743
+ }
4744
+ function configPath(cwd) {
4745
+ return planningPath(cwd, "config.json");
4746
+ }
4747
+ function phasesPath(cwd) {
4748
+ return planningPath(cwd, "phases");
4749
+ }
4750
+ /** Phase-file predicates. */
4751
+ const isPlanFile = (f) => f.endsWith("-PLAN.md") || f === "PLAN.md";
4752
+ const isSummaryFile = (f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md";
4753
+ /** Strip suffix to get plan/summary ID. */
4754
+ const planId = (f) => f.replace("-PLAN.md", "").replace("PLAN.md", "");
4755
+ const summaryId = (f) => f.replace("-SUMMARY.md", "").replace("SUMMARY.md", "");
4756
+ /** List subdirectory names, optionally sorted by phase number. */
4757
+ function listSubDirs(dir, sortByPhase = false) {
4758
+ const dirs = node_fs.default.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4759
+ return sortByPhase ? dirs.sort((a, b) => comparePhaseNum(a, b)) : dirs;
4760
+ }
4761
+ /** Log only when MAXSIM_DEBUG is set. */
4762
+ function debugLog(e) {
4763
+ if (process.env.MAXSIM_DEBUG) console.error(e);
4764
+ }
4765
+ /** Escape a phase number for use in regex. */
4766
+ function escapePhaseNum(phase) {
4767
+ return String(phase).replace(/\./g, "\\.");
4768
+ }
4730
4769
  function safeReadFile(filePath) {
4731
4770
  try {
4732
4771
  return node_fs.default.readFileSync(filePath, "utf-8");
@@ -4734,8 +4773,10 @@ function safeReadFile(filePath) {
4734
4773
  return null;
4735
4774
  }
4736
4775
  }
4776
+ let _configCache = null;
4737
4777
  function loadConfig(cwd) {
4738
- const configPath = node_path.default.join(cwd, ".planning", "config.json");
4778
+ if (_configCache && _configCache.cwd === cwd) return _configCache.config;
4779
+ const cfgPath = configPath(cwd);
4739
4780
  const defaults = {
4740
4781
  model_profile: "balanced",
4741
4782
  commit_docs: true,
@@ -4750,7 +4791,7 @@ function loadConfig(cwd) {
4750
4791
  brave_search: false
4751
4792
  };
4752
4793
  try {
4753
- const raw = node_fs.default.readFileSync(configPath, "utf-8");
4794
+ const raw = node_fs.default.readFileSync(cfgPath, "utf-8");
4754
4795
  const parsed = JSON.parse(raw);
4755
4796
  const get = (key, nested) => {
4756
4797
  if (parsed[key] !== void 0) return parsed[key];
@@ -4765,7 +4806,7 @@ function loadConfig(cwd) {
4765
4806
  if (typeof val === "object" && val !== null && "enabled" in val) return val.enabled;
4766
4807
  return defaults.parallelization;
4767
4808
  })();
4768
- return {
4809
+ const result = {
4769
4810
  model_profile: get("model_profile") ?? defaults.model_profile,
4770
4811
  commit_docs: get("commit_docs", {
4771
4812
  section: "planning",
@@ -4803,7 +4844,16 @@ function loadConfig(cwd) {
4803
4844
  brave_search: get("brave_search") ?? defaults.brave_search,
4804
4845
  model_overrides: parsed["model_overrides"]
4805
4846
  };
4847
+ _configCache = {
4848
+ cwd,
4849
+ config: result
4850
+ };
4851
+ return result;
4806
4852
  } catch {
4853
+ _configCache = {
4854
+ cwd,
4855
+ config: defaults
4856
+ };
4807
4857
  return defaults;
4808
4858
  }
4809
4859
  }
@@ -4873,23 +4923,20 @@ function getPhasePattern(escapedPhaseNum, flags = "gim") {
4873
4923
  }
4874
4924
  function searchPhaseInDir(baseDir, relBase, normalized) {
4875
4925
  try {
4876
- const match = node_fs.default.readdirSync(baseDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => comparePhaseNum(a, b)).find((d) => d.startsWith(normalized));
4926
+ const match = listSubDirs(baseDir, true).find((d) => d.startsWith(normalized));
4877
4927
  if (!match) return null;
4878
4928
  const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
4879
4929
  const phaseNumber = dirMatch ? dirMatch[1] : normalized;
4880
4930
  const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
4881
4931
  const phaseDir = node_path.default.join(baseDir, match);
4882
4932
  const phaseFiles = node_fs.default.readdirSync(phaseDir);
4883
- const plans = phaseFiles.filter((f) => f.endsWith("-PLAN.md") || f === "PLAN.md").sort();
4884
- const summaries = phaseFiles.filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md").sort();
4933
+ const plans = phaseFiles.filter(isPlanFile).sort();
4934
+ const summaries = phaseFiles.filter(isSummaryFile).sort();
4885
4935
  const hasResearch = phaseFiles.some((f) => f.endsWith("-RESEARCH.md") || f === "RESEARCH.md");
4886
4936
  const hasContext = phaseFiles.some((f) => f.endsWith("-CONTEXT.md") || f === "CONTEXT.md");
4887
4937
  const hasVerification = phaseFiles.some((f) => f.endsWith("-VERIFICATION.md") || f === "VERIFICATION.md");
4888
- const completedPlanIds = new Set(summaries.map((s) => s.replace("-SUMMARY.md", "").replace("SUMMARY.md", "")));
4889
- const incompletePlans = plans.filter((p) => {
4890
- const planId = p.replace("-PLAN.md", "").replace("PLAN.md", "");
4891
- return !completedPlanIds.has(planId);
4892
- });
4938
+ const completedPlanIds = new Set(summaries.map(summaryId));
4939
+ const incompletePlans = plans.filter((p) => !completedPlanIds.has(planId(p)));
4893
4940
  return {
4894
4941
  found: true,
4895
4942
  directory: node_path.default.join(relBase, match),
@@ -4909,12 +4956,16 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
4909
4956
  }
4910
4957
  function findPhaseInternal(cwd, phase) {
4911
4958
  if (!phase) return null;
4912
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
4959
+ const pd = phasesPath(cwd);
4913
4960
  const normalized = normalizePhaseName(phase);
4914
- const current = searchPhaseInDir(phasesDir, node_path.default.join(".planning", "phases"), normalized);
4961
+ const current = searchPhaseInDir(pd, node_path.default.join(".planning", "phases"), normalized);
4915
4962
  if (current) return current;
4916
- const milestonesDir = node_path.default.join(cwd, ".planning", "milestones");
4917
- if (!node_fs.default.existsSync(milestonesDir)) return null;
4963
+ const milestonesDir = planningPath(cwd, "milestones");
4964
+ try {
4965
+ node_fs.default.statSync(milestonesDir);
4966
+ } catch {
4967
+ return null;
4968
+ }
4918
4969
  try {
4919
4970
  const archiveDirs = node_fs.default.readdirSync(milestonesDir, { withFileTypes: true }).filter((e) => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name)).map((e) => e.name).sort().reverse();
4920
4971
  for (const archiveName of archiveDirs) {
@@ -4928,14 +4979,13 @@ function findPhaseInternal(cwd, phase) {
4928
4979
  }
4929
4980
  }
4930
4981
  } catch (e) {
4931
- if (process.env.MAXSIM_DEBUG) console.error(e);
4982
+ debugLog(e);
4932
4983
  }
4933
4984
  return null;
4934
4985
  }
4935
4986
  function getArchivedPhaseDirs(cwd) {
4936
- const milestonesDir = node_path.default.join(cwd, ".planning", "milestones");
4987
+ const milestonesDir = planningPath(cwd, "milestones");
4937
4988
  const results = [];
4938
- if (!node_fs.default.existsSync(milestonesDir)) return results;
4939
4989
  try {
4940
4990
  const phaseDirs = node_fs.default.readdirSync(milestonesDir, { withFileTypes: true }).filter((e) => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name)).map((e) => e.name).sort().reverse();
4941
4991
  for (const archiveName of phaseDirs) {
@@ -4943,7 +4993,7 @@ function getArchivedPhaseDirs(cwd) {
4943
4993
  if (!versionMatch) continue;
4944
4994
  const version = versionMatch[1];
4945
4995
  const archivePath = node_path.default.join(milestonesDir, archiveName);
4946
- const dirs = node_fs.default.readdirSync(archivePath, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => comparePhaseNum(a, b));
4996
+ const dirs = listSubDirs(archivePath, true);
4947
4997
  for (const dir of dirs) results.push({
4948
4998
  name: dir,
4949
4999
  milestone: version,
@@ -4952,17 +5002,16 @@ function getArchivedPhaseDirs(cwd) {
4952
5002
  });
4953
5003
  }
4954
5004
  } catch (e) {
4955
- if (process.env.MAXSIM_DEBUG) console.error(e);
5005
+ debugLog(e);
4956
5006
  }
4957
5007
  return results;
4958
5008
  }
4959
5009
  function getRoadmapPhaseInternal(cwd, phaseNum) {
4960
5010
  if (!phaseNum) return null;
4961
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
4962
- if (!node_fs.default.existsSync(roadmapPath)) return null;
5011
+ const rp = roadmapPath(cwd);
4963
5012
  try {
4964
- const content = node_fs.default.readFileSync(roadmapPath, "utf-8");
4965
- const phasePattern = getPhasePattern(phaseNum.toString().replace(/\./g, "\\."), "i");
5013
+ const content = node_fs.default.readFileSync(rp, "utf-8");
5014
+ const phasePattern = getPhasePattern(escapePhaseNum(phaseNum), "i");
4966
5015
  const headerMatch = content.match(phasePattern);
4967
5016
  if (!headerMatch) return null;
4968
5017
  const phaseName = headerMatch[1].trim();
@@ -4983,8 +5032,8 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
4983
5032
  return null;
4984
5033
  }
4985
5034
  }
4986
- function resolveModelInternal(cwd, agentType) {
4987
- const config = loadConfig(cwd);
5035
+ function resolveModelInternal(cwd, agentType, config) {
5036
+ config = config ?? loadConfig(cwd);
4988
5037
  const override = config.model_overrides?.[agentType];
4989
5038
  if (override) return override === "opus" ? "inherit" : override;
4990
5039
  const profile = config.model_profile || "balanced";
@@ -5011,7 +5060,7 @@ function generateSlugInternal(text) {
5011
5060
  }
5012
5061
  function getMilestoneInfo(cwd) {
5013
5062
  try {
5014
- const roadmap = node_fs.default.readFileSync(node_path.default.join(cwd, ".planning", "ROADMAP.md"), "utf-8");
5063
+ const roadmap = node_fs.default.readFileSync(roadmapPath(cwd), "utf-8");
5015
5064
  const versionMatch = roadmap.match(/v(\d+\.\d+)/);
5016
5065
  const nameMatch = roadmap.match(/## .*v\d+\.\d+[:\s]+([^\n(]+)/);
5017
5066
  return {
@@ -11994,17 +12043,32 @@ function readTextArgOrFile(cwd, value, filePath, label) {
11994
12043
  throw new Error(`${label} file not found: ${filePath}`);
11995
12044
  }
11996
12045
  }
12046
+ /**
12047
+ * Append an entry to a section in STATE.md content, removing placeholder text.
12048
+ * Returns updated content or null if section not found.
12049
+ */
12050
+ function appendToStateSection(content, sectionPattern, entry, placeholderPatterns) {
12051
+ const match = content.match(sectionPattern);
12052
+ if (!match) return null;
12053
+ let sectionBody = match[2];
12054
+ for (const pat of placeholderPatterns || [
12055
+ /None yet\.?\s*\n?/gi,
12056
+ /No decisions yet\.?\s*\n?/gi,
12057
+ /None\.?\s*\n?/gi
12058
+ ]) sectionBody = sectionBody.replace(pat, "");
12059
+ sectionBody = sectionBody.trimEnd() + "\n" + entry + "\n";
12060
+ return content.replace(sectionPattern, (_m, header) => `${header}${sectionBody}`);
12061
+ }
11997
12062
  function cmdStateLoad(cwd, raw) {
11998
12063
  const config = loadConfig(cwd);
11999
- const planningDir = node_path.default.join(cwd, ".planning");
12000
12064
  let stateRaw = "";
12001
12065
  try {
12002
- stateRaw = node_fs.default.readFileSync(node_path.default.join(planningDir, "STATE.md"), "utf-8");
12066
+ stateRaw = node_fs.default.readFileSync(statePath(cwd), "utf-8");
12003
12067
  } catch (e) {
12004
- if (process.env.MAXSIM_DEBUG) console.error(e);
12068
+ debugLog(e);
12005
12069
  }
12006
- const configExists = node_fs.default.existsSync(node_path.default.join(planningDir, "config.json"));
12007
- const roadmapExists = node_fs.default.existsSync(node_path.default.join(planningDir, "ROADMAP.md"));
12070
+ const configExists = node_fs.default.existsSync(configPath(cwd));
12071
+ const roadmapExists = node_fs.default.existsSync(roadmapPath(cwd));
12008
12072
  const stateExists = stateRaw.length > 0;
12009
12073
  const result = {
12010
12074
  config,
@@ -12035,9 +12099,9 @@ function cmdStateLoad(cwd, raw) {
12035
12099
  output(result);
12036
12100
  }
12037
12101
  function cmdStateGet(cwd, section, raw) {
12038
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12102
+ const statePath$2 = statePath(cwd);
12039
12103
  try {
12040
- const content = node_fs.default.readFileSync(statePath, "utf-8");
12104
+ const content = node_fs.default.readFileSync(statePath$2, "utf-8");
12041
12105
  if (!section) {
12042
12106
  output({ content }, raw, content);
12043
12107
  return;
@@ -12061,9 +12125,9 @@ function cmdStateGet(cwd, section, raw) {
12061
12125
  }
12062
12126
  }
12063
12127
  function cmdStatePatch(cwd, patches, raw) {
12064
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12128
+ const statePath$3 = statePath(cwd);
12065
12129
  try {
12066
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12130
+ let content = node_fs.default.readFileSync(statePath$3, "utf-8");
12067
12131
  const results = {
12068
12132
  updated: [],
12069
12133
  failed: []
@@ -12076,7 +12140,7 @@ function cmdStatePatch(cwd, patches, raw) {
12076
12140
  results.updated.push(field);
12077
12141
  } else results.failed.push(field);
12078
12142
  }
12079
- if (results.updated.length > 0) node_fs.default.writeFileSync(statePath, content, "utf-8");
12143
+ if (results.updated.length > 0) node_fs.default.writeFileSync(statePath$3, content, "utf-8");
12080
12144
  output(results, raw, results.updated.length > 0 ? "true" : "false");
12081
12145
  } catch {
12082
12146
  error("STATE.md not found");
@@ -12084,14 +12148,14 @@ function cmdStatePatch(cwd, patches, raw) {
12084
12148
  }
12085
12149
  function cmdStateUpdate(cwd, field, value) {
12086
12150
  if (!field || value === void 0) error("field and value required for state update");
12087
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12151
+ const statePath$4 = statePath(cwd);
12088
12152
  try {
12089
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12153
+ let content = node_fs.default.readFileSync(statePath$4, "utf-8");
12090
12154
  const fieldEscaped = escapeStringRegexp(field);
12091
12155
  const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, "i");
12092
12156
  if (pattern.test(content)) {
12093
12157
  content = content.replace(pattern, (_match, prefix) => `${prefix}${value}`);
12094
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12158
+ node_fs.default.writeFileSync(statePath$4, content, "utf-8");
12095
12159
  output({ updated: true });
12096
12160
  } else output({
12097
12161
  updated: false,
@@ -12105,15 +12169,15 @@ function cmdStateUpdate(cwd, field, value) {
12105
12169
  }
12106
12170
  }
12107
12171
  function cmdStateAdvancePlan(cwd, raw) {
12108
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12109
- if (!node_fs.default.existsSync(statePath)) {
12172
+ const statePath$5 = statePath(cwd);
12173
+ if (!node_fs.default.existsSync(statePath$5)) {
12110
12174
  output({ error: "STATE.md not found" }, raw);
12111
12175
  return;
12112
12176
  }
12113
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12177
+ let content = node_fs.default.readFileSync(statePath$5, "utf-8");
12114
12178
  const currentPlan = parseInt(stateExtractField(content, "Current Plan") ?? "", 10);
12115
12179
  const totalPlans = parseInt(stateExtractField(content, "Total Plans in Phase") ?? "", 10);
12116
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
12180
+ const today = todayISO();
12117
12181
  if (isNaN(currentPlan) || isNaN(totalPlans)) {
12118
12182
  output({ error: "Cannot parse Current Plan or Total Plans in Phase from STATE.md" }, raw);
12119
12183
  return;
@@ -12121,7 +12185,7 @@ function cmdStateAdvancePlan(cwd, raw) {
12121
12185
  if (currentPlan >= totalPlans) {
12122
12186
  content = stateReplaceField(content, "Status", "Phase complete — ready for verification") || content;
12123
12187
  content = stateReplaceField(content, "Last Activity", today) || content;
12124
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12188
+ node_fs.default.writeFileSync(statePath$5, content, "utf-8");
12125
12189
  output({
12126
12190
  advanced: false,
12127
12191
  reason: "last_plan",
@@ -12134,7 +12198,7 @@ function cmdStateAdvancePlan(cwd, raw) {
12134
12198
  content = stateReplaceField(content, "Current Plan", String(newPlan)) || content;
12135
12199
  content = stateReplaceField(content, "Status", "Ready to execute") || content;
12136
12200
  content = stateReplaceField(content, "Last Activity", today) || content;
12137
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12201
+ node_fs.default.writeFileSync(statePath$5, content, "utf-8");
12138
12202
  output({
12139
12203
  advanced: true,
12140
12204
  previous_plan: currentPlan,
@@ -12144,12 +12208,12 @@ function cmdStateAdvancePlan(cwd, raw) {
12144
12208
  }
12145
12209
  }
12146
12210
  function cmdStateRecordMetric(cwd, options, raw) {
12147
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12148
- if (!node_fs.default.existsSync(statePath)) {
12211
+ const statePath$6 = statePath(cwd);
12212
+ if (!node_fs.default.existsSync(statePath$6)) {
12149
12213
  output({ error: "STATE.md not found" }, raw);
12150
12214
  return;
12151
12215
  }
12152
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12216
+ let content = node_fs.default.readFileSync(statePath$6, "utf-8");
12153
12217
  const { phase, plan, duration, tasks, files } = options;
12154
12218
  if (!phase || !plan || !duration) {
12155
12219
  output({ error: "phase, plan, and duration required" }, raw);
@@ -12163,7 +12227,7 @@ function cmdStateRecordMetric(cwd, options, raw) {
12163
12227
  if (tableBody.trim() === "" || tableBody.includes("None yet")) tableBody = newRow;
12164
12228
  else tableBody = tableBody + "\n" + newRow;
12165
12229
  content = content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
12166
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12230
+ node_fs.default.writeFileSync(statePath$6, content, "utf-8");
12167
12231
  output({
12168
12232
  recorded: true,
12169
12233
  phase,
@@ -12176,21 +12240,21 @@ function cmdStateRecordMetric(cwd, options, raw) {
12176
12240
  }, raw, "false");
12177
12241
  }
12178
12242
  function cmdStateUpdateProgress(cwd, raw) {
12179
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12180
- if (!node_fs.default.existsSync(statePath)) {
12243
+ const statePath$7 = statePath(cwd);
12244
+ if (!node_fs.default.existsSync(statePath$7)) {
12181
12245
  output({ error: "STATE.md not found" }, raw);
12182
12246
  return;
12183
12247
  }
12184
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12185
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
12248
+ let content = node_fs.default.readFileSync(statePath$7, "utf-8");
12249
+ const phasesDir = phasesPath(cwd);
12186
12250
  let totalPlans = 0;
12187
12251
  let totalSummaries = 0;
12188
12252
  if (node_fs.default.existsSync(phasesDir)) {
12189
12253
  const phaseDirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
12190
12254
  for (const dir of phaseDirs) {
12191
12255
  const files = node_fs.default.readdirSync(node_path.default.join(phasesDir, dir));
12192
- totalPlans += files.filter((f) => f.match(/-PLAN\.md$/i)).length;
12193
- totalSummaries += files.filter((f) => f.match(/-SUMMARY\.md$/i)).length;
12256
+ totalPlans += files.filter((f) => isPlanFile(f)).length;
12257
+ totalSummaries += files.filter((f) => isSummaryFile(f)).length;
12194
12258
  }
12195
12259
  }
12196
12260
  const percent = totalPlans > 0 ? Math.min(100, Math.round(totalSummaries / totalPlans * 100)) : 0;
@@ -12200,7 +12264,7 @@ function cmdStateUpdateProgress(cwd, raw) {
12200
12264
  const progressPattern = /(\*\*Progress:\*\*\s*).*/i;
12201
12265
  if (progressPattern.test(content)) {
12202
12266
  content = content.replace(progressPattern, (_match, prefix) => `${prefix}${progressStr}`);
12203
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12267
+ node_fs.default.writeFileSync(statePath$7, content, "utf-8");
12204
12268
  output({
12205
12269
  updated: true,
12206
12270
  percent,
@@ -12214,8 +12278,8 @@ function cmdStateUpdateProgress(cwd, raw) {
12214
12278
  }, raw, "false");
12215
12279
  }
12216
12280
  function cmdStateAddDecision(cwd, options, raw) {
12217
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12218
- if (!node_fs.default.existsSync(statePath)) {
12281
+ const statePath$8 = statePath(cwd);
12282
+ if (!node_fs.default.existsSync(statePath$8)) {
12219
12283
  output({ error: "STATE.md not found" }, raw);
12220
12284
  return;
12221
12285
  }
@@ -12236,16 +12300,11 @@ function cmdStateAddDecision(cwd, options, raw) {
12236
12300
  output({ error: "summary required" }, raw);
12237
12301
  return;
12238
12302
  }
12239
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12303
+ const content = node_fs.default.readFileSync(statePath$8, "utf-8");
12240
12304
  const entry = `- [Phase ${phase || "?"}]: ${summaryText}${rationaleText ? ` — ${rationaleText}` : ""}`;
12241
- const sectionPattern = /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
12242
- const match = content.match(sectionPattern);
12243
- if (match) {
12244
- let sectionBody = match[2];
12245
- sectionBody = sectionBody.replace(/None yet\.?\s*\n?/gi, "").replace(/No decisions yet\.?\s*\n?/gi, "");
12246
- sectionBody = sectionBody.trimEnd() + "\n" + entry + "\n";
12247
- content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
12248
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12305
+ const updated = appendToStateSection(content, /(###?\s*(?:Decisions|Decisions Made|Accumulated.*Decisions)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i, entry, [/None yet\.?\s*\n?/gi, /No decisions yet\.?\s*\n?/gi]);
12306
+ if (updated) {
12307
+ node_fs.default.writeFileSync(statePath$8, updated, "utf-8");
12249
12308
  output({
12250
12309
  added: true,
12251
12310
  decision: entry
@@ -12256,8 +12315,8 @@ function cmdStateAddDecision(cwd, options, raw) {
12256
12315
  }, raw, "false");
12257
12316
  }
12258
12317
  function cmdStateAddBlocker(cwd, text, raw) {
12259
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12260
- if (!node_fs.default.existsSync(statePath)) {
12318
+ const statePath$9 = statePath(cwd);
12319
+ if (!node_fs.default.existsSync(statePath$9)) {
12261
12320
  output({ error: "STATE.md not found" }, raw);
12262
12321
  return;
12263
12322
  }
@@ -12276,16 +12335,9 @@ function cmdStateAddBlocker(cwd, text, raw) {
12276
12335
  output({ error: "text required" }, raw);
12277
12336
  return;
12278
12337
  }
12279
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12280
- const entry = `- ${blockerText}`;
12281
- const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
12282
- const match = content.match(sectionPattern);
12283
- if (match) {
12284
- let sectionBody = match[2];
12285
- sectionBody = sectionBody.replace(/None\.?\s*\n?/gi, "").replace(/None yet\.?\s*\n?/gi, "");
12286
- sectionBody = sectionBody.trimEnd() + "\n" + entry + "\n";
12287
- content = content.replace(sectionPattern, (_match, header) => `${header}${sectionBody}`);
12288
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12338
+ const updated = appendToStateSection(node_fs.default.readFileSync(statePath$9, "utf-8"), /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i, `- ${blockerText}`, [/None\.?\s*\n?/gi, /None yet\.?\s*\n?/gi]);
12339
+ if (updated) {
12340
+ node_fs.default.writeFileSync(statePath$9, updated, "utf-8");
12289
12341
  output({
12290
12342
  added: true,
12291
12343
  blocker: blockerText
@@ -12296,8 +12348,8 @@ function cmdStateAddBlocker(cwd, text, raw) {
12296
12348
  }, raw, "false");
12297
12349
  }
12298
12350
  function cmdStateResolveBlocker(cwd, text, raw) {
12299
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12300
- if (!node_fs.default.existsSync(statePath)) {
12351
+ const statePath$10 = statePath(cwd);
12352
+ if (!node_fs.default.existsSync(statePath$10)) {
12301
12353
  output({ error: "STATE.md not found" }, raw);
12302
12354
  return;
12303
12355
  }
@@ -12305,7 +12357,7 @@ function cmdStateResolveBlocker(cwd, text, raw) {
12305
12357
  output({ error: "text required" }, raw);
12306
12358
  return;
12307
12359
  }
12308
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12360
+ let content = node_fs.default.readFileSync(statePath$10, "utf-8");
12309
12361
  const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
12310
12362
  const match = content.match(sectionPattern);
12311
12363
  if (match) {
@@ -12315,7 +12367,7 @@ function cmdStateResolveBlocker(cwd, text, raw) {
12315
12367
  }).join("\n");
12316
12368
  if (!newBody.trim() || !newBody.includes("- ")) newBody = "None\n";
12317
12369
  content = content.replace(sectionPattern, (_match, header) => `${header}${newBody}`);
12318
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12370
+ node_fs.default.writeFileSync(statePath$10, content, "utf-8");
12319
12371
  output({
12320
12372
  resolved: true,
12321
12373
  blocker: text
@@ -12326,12 +12378,12 @@ function cmdStateResolveBlocker(cwd, text, raw) {
12326
12378
  }, raw, "false");
12327
12379
  }
12328
12380
  function cmdStateRecordSession(cwd, options, raw) {
12329
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12330
- if (!node_fs.default.existsSync(statePath)) {
12381
+ const statePath$11 = statePath(cwd);
12382
+ if (!node_fs.default.existsSync(statePath$11)) {
12331
12383
  output({ error: "STATE.md not found" }, raw);
12332
12384
  return;
12333
12385
  }
12334
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12386
+ let content = node_fs.default.readFileSync(statePath$11, "utf-8");
12335
12387
  const now = (/* @__PURE__ */ new Date()).toISOString();
12336
12388
  const updated = [];
12337
12389
  let result = stateReplaceField(content, "Last session", now);
@@ -12360,7 +12412,7 @@ function cmdStateRecordSession(cwd, options, raw) {
12360
12412
  updated.push("Resume File");
12361
12413
  }
12362
12414
  if (updated.length > 0) {
12363
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12415
+ node_fs.default.writeFileSync(statePath$11, content, "utf-8");
12364
12416
  output({
12365
12417
  recorded: true,
12366
12418
  updated
@@ -12371,12 +12423,12 @@ function cmdStateRecordSession(cwd, options, raw) {
12371
12423
  }, raw, "false");
12372
12424
  }
12373
12425
  function cmdStateSnapshot(cwd, raw) {
12374
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12375
- if (!node_fs.default.existsSync(statePath)) {
12426
+ const statePath$12 = statePath(cwd);
12427
+ if (!node_fs.default.existsSync(statePath$12)) {
12376
12428
  output({ error: "STATE.md not found" }, raw);
12377
12429
  return;
12378
12430
  }
12379
- const content = node_fs.default.readFileSync(statePath, "utf-8");
12431
+ const content = node_fs.default.readFileSync(statePath$12, "utf-8");
12380
12432
  const extractField = (fieldName) => {
12381
12433
  const pattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*(.+)`, "i");
12382
12434
  const match = content.match(pattern);
@@ -12454,8 +12506,8 @@ function cmdStateSnapshot(cwd, raw) {
12454
12506
  * Ported from maxsim/bin/lib/roadmap.cjs
12455
12507
  */
12456
12508
  function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
12457
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
12458
- if (!node_fs.default.existsSync(roadmapPath)) {
12509
+ const rmPath = roadmapPath(cwd);
12510
+ if (!node_fs.default.existsSync(rmPath)) {
12459
12511
  output({
12460
12512
  found: false,
12461
12513
  error: "ROADMAP.md not found"
@@ -12463,7 +12515,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
12463
12515
  return;
12464
12516
  }
12465
12517
  try {
12466
- const content = node_fs.default.readFileSync(roadmapPath, "utf-8");
12518
+ const content = node_fs.default.readFileSync(rmPath, "utf-8");
12467
12519
  const escapedPhase = phaseNum.replace(/\./g, "\\.");
12468
12520
  const phasePattern = getPhasePattern(escapedPhase, "i");
12469
12521
  const headerMatch = content.match(phasePattern);
@@ -12507,8 +12559,8 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
12507
12559
  }
12508
12560
  }
12509
12561
  function cmdRoadmapAnalyze(cwd, raw) {
12510
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
12511
- if (!node_fs.default.existsSync(roadmapPath)) {
12562
+ const rmPath = roadmapPath(cwd);
12563
+ if (!node_fs.default.existsSync(rmPath)) {
12512
12564
  output({
12513
12565
  error: "ROADMAP.md not found",
12514
12566
  milestones: [],
@@ -12517,8 +12569,8 @@ function cmdRoadmapAnalyze(cwd, raw) {
12517
12569
  }, raw);
12518
12570
  return;
12519
12571
  }
12520
- const content = node_fs.default.readFileSync(roadmapPath, "utf-8");
12521
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
12572
+ const content = node_fs.default.readFileSync(rmPath, "utf-8");
12573
+ const phasesDir = phasesPath(cwd);
12522
12574
  const phasePattern = getPhasePattern();
12523
12575
  const phases = [];
12524
12576
  let match;
@@ -12540,11 +12592,11 @@ function cmdRoadmapAnalyze(cwd, raw) {
12540
12592
  let hasContext = false;
12541
12593
  let hasResearch = false;
12542
12594
  try {
12543
- const dirMatch = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).find((d) => d.startsWith(normalized + "-") || d === normalized);
12595
+ const dirMatch = listSubDirs(phasesDir).find((d) => d.startsWith(normalized + "-") || d === normalized);
12544
12596
  if (dirMatch) {
12545
12597
  const phaseFiles = node_fs.default.readdirSync(node_path.default.join(phasesDir, dirMatch));
12546
- planCount = phaseFiles.filter((f) => f.endsWith("-PLAN.md") || f === "PLAN.md").length;
12547
- summaryCount = phaseFiles.filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md").length;
12598
+ planCount = phaseFiles.filter((f) => isPlanFile(f)).length;
12599
+ summaryCount = phaseFiles.filter((f) => isSummaryFile(f)).length;
12548
12600
  hasContext = phaseFiles.some((f) => f.endsWith("-CONTEXT.md") || f === "CONTEXT.md");
12549
12601
  hasResearch = phaseFiles.some((f) => f.endsWith("-RESEARCH.md") || f === "RESEARCH.md");
12550
12602
  if (summaryCount >= planCount && planCount > 0) diskStatus = "complete";
@@ -12555,7 +12607,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
12555
12607
  else diskStatus = "empty";
12556
12608
  }
12557
12609
  } catch (e) {
12558
- if (process.env.MAXSIM_DEBUG) console.error(e);
12610
+ debugLog(e);
12559
12611
  }
12560
12612
  const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace(".", "\\.")}`, "i");
12561
12613
  const checkboxMatch = content.match(checkboxPattern);
@@ -12606,7 +12658,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
12606
12658
  }
12607
12659
  function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
12608
12660
  if (!phaseNum) error("phase number required for roadmap update-plan-progress");
12609
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
12661
+ const rmPath = roadmapPath(cwd);
12610
12662
  const phaseInfo = findPhaseInternal(cwd, phaseNum);
12611
12663
  if (!phaseInfo) error(`Phase ${phaseNum} not found`);
12612
12664
  const planCount = phaseInfo.plans.length;
@@ -12622,8 +12674,8 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
12622
12674
  }
12623
12675
  const isComplete = summaryCount >= planCount;
12624
12676
  const status = isComplete ? "Complete" : summaryCount > 0 ? "In Progress" : "Planned";
12625
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
12626
- if (!node_fs.default.existsSync(roadmapPath)) {
12677
+ const today = todayISO();
12678
+ if (!node_fs.default.existsSync(rmPath)) {
12627
12679
  output({
12628
12680
  updated: false,
12629
12681
  reason: "ROADMAP.md not found",
@@ -12632,7 +12684,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
12632
12684
  }, raw, "no roadmap");
12633
12685
  return;
12634
12686
  }
12635
- let roadmapContent = node_fs.default.readFileSync(roadmapPath, "utf-8");
12687
+ let roadmapContent = node_fs.default.readFileSync(rmPath, "utf-8");
12636
12688
  const phaseEscaped = phaseNum.replace(".", "\\.");
12637
12689
  const tablePattern = new RegExp(`(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|)[^|]*(\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`, "i");
12638
12690
  const dateField = isComplete ? ` ${today} ` : " ";
@@ -12644,7 +12696,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
12644
12696
  const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`, "i");
12645
12697
  roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
12646
12698
  }
12647
- node_fs.default.writeFileSync(roadmapPath, roadmapContent, "utf-8");
12699
+ node_fs.default.writeFileSync(rmPath, roadmapContent, "utf-8");
12648
12700
  output({
12649
12701
  updated: true,
12650
12702
  phase: phaseNum,
@@ -12666,7 +12718,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
12666
12718
  if (!reqIdsRaw || reqIdsRaw.length === 0) error("requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02");
12667
12719
  const reqIds = reqIdsRaw.join(" ").replace(/[\[\]]/g, "").split(/[,\s]+/).map((r) => r.trim()).filter(Boolean);
12668
12720
  if (reqIds.length === 0) error("no valid requirement IDs found");
12669
- const reqPath = node_path.default.join(cwd, ".planning", "REQUIREMENTS.md");
12721
+ const reqPath = planningPath(cwd, "REQUIREMENTS.md");
12670
12722
  if (!node_fs.default.existsSync(reqPath)) {
12671
12723
  output({
12672
12724
  updated: false,
@@ -12702,13 +12754,13 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
12702
12754
  }
12703
12755
  function cmdMilestoneComplete(cwd, version, options, raw) {
12704
12756
  if (!version) error("version required for milestone complete (e.g., v1.0)");
12705
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
12706
- const reqPath = node_path.default.join(cwd, ".planning", "REQUIREMENTS.md");
12707
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12708
- const milestonesPath = node_path.default.join(cwd, ".planning", "MILESTONES.md");
12709
- const archiveDir = node_path.default.join(cwd, ".planning", "milestones");
12710
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
12711
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
12757
+ const roadmapPath$1 = roadmapPath(cwd);
12758
+ const reqPath = planningPath(cwd, "REQUIREMENTS.md");
12759
+ const statePath$1 = statePath(cwd);
12760
+ const milestonesPath = planningPath(cwd, "MILESTONES.md");
12761
+ const archiveDir = planningPath(cwd, "milestones");
12762
+ const phasesDir = phasesPath(cwd);
12763
+ const today = todayISO();
12712
12764
  const milestoneName = options.name || version;
12713
12765
  node_fs.default.mkdirSync(archiveDir, { recursive: true });
12714
12766
  let phaseCount = 0;
@@ -12716,12 +12768,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
12716
12768
  let totalTasks = 0;
12717
12769
  const accomplishments = [];
12718
12770
  try {
12719
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
12771
+ const dirs = listSubDirs(phasesDir, true);
12720
12772
  for (const dir of dirs) {
12721
12773
  phaseCount++;
12722
12774
  const phaseFiles = node_fs.default.readdirSync(node_path.default.join(phasesDir, dir));
12723
- const plans = phaseFiles.filter((f) => f.endsWith("-PLAN.md") || f === "PLAN.md");
12724
- const summaries = phaseFiles.filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md");
12775
+ const plans = phaseFiles.filter(isPlanFile);
12776
+ const summaries = phaseFiles.filter(isSummaryFile);
12725
12777
  totalPlans += plans.length;
12726
12778
  for (const s of summaries) try {
12727
12779
  const content = node_fs.default.readFileSync(node_path.default.join(phasesDir, dir, s), "utf-8");
@@ -12730,14 +12782,14 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
12730
12782
  const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
12731
12783
  totalTasks += taskMatches.length;
12732
12784
  } catch (e) {
12733
- if (process.env.MAXSIM_DEBUG) console.error(e);
12785
+ debugLog(e);
12734
12786
  }
12735
12787
  }
12736
12788
  } catch (e) {
12737
- if (process.env.MAXSIM_DEBUG) console.error(e);
12789
+ debugLog(e);
12738
12790
  }
12739
- if (node_fs.default.existsSync(roadmapPath)) {
12740
- const roadmapContent = node_fs.default.readFileSync(roadmapPath, "utf-8");
12791
+ if (node_fs.default.existsSync(roadmapPath$1)) {
12792
+ const roadmapContent = node_fs.default.readFileSync(roadmapPath$1, "utf-8");
12741
12793
  node_fs.default.writeFileSync(node_path.default.join(archiveDir, `${version}-ROADMAP.md`), roadmapContent, "utf-8");
12742
12794
  }
12743
12795
  if (node_fs.default.existsSync(reqPath)) {
@@ -12753,22 +12805,22 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
12753
12805
  const existing = node_fs.default.readFileSync(milestonesPath, "utf-8");
12754
12806
  node_fs.default.writeFileSync(milestonesPath, existing + "\n" + milestoneEntry, "utf-8");
12755
12807
  } else node_fs.default.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, "utf-8");
12756
- if (node_fs.default.existsSync(statePath)) {
12757
- let stateContent = node_fs.default.readFileSync(statePath, "utf-8");
12808
+ if (node_fs.default.existsSync(statePath$1)) {
12809
+ let stateContent = node_fs.default.readFileSync(statePath$1, "utf-8");
12758
12810
  stateContent = stateContent.replace(/(\*\*Status:\*\*\s*).*/, `$1${version} milestone complete`);
12759
12811
  stateContent = stateContent.replace(/(\*\*Last Activity:\*\*\s*).*/, `$1${today}`);
12760
12812
  stateContent = stateContent.replace(/(\*\*Last Activity Description:\*\*\s*).*/, `$1${version} milestone completed and archived`);
12761
- node_fs.default.writeFileSync(statePath, stateContent, "utf-8");
12813
+ node_fs.default.writeFileSync(statePath$1, stateContent, "utf-8");
12762
12814
  }
12763
12815
  let phasesArchived = false;
12764
12816
  if (options.archivePhases) try {
12765
12817
  const phaseArchiveDir = node_path.default.join(archiveDir, `${version}-phases`);
12766
12818
  node_fs.default.mkdirSync(phaseArchiveDir, { recursive: true });
12767
- const phaseDirNames = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
12819
+ const phaseDirNames = listSubDirs(phasesDir);
12768
12820
  for (const dir of phaseDirNames) node_fs.default.renameSync(node_path.default.join(phasesDir, dir), node_path.default.join(phaseArchiveDir, dir));
12769
12821
  phasesArchived = phaseDirNames.length > 0;
12770
12822
  } catch (e) {
12771
- if (process.env.MAXSIM_DEBUG) console.error(e);
12823
+ debugLog(e);
12772
12824
  }
12773
12825
  output({
12774
12826
  version,
@@ -12785,7 +12837,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
12785
12837
  phases: phasesArchived
12786
12838
  },
12787
12839
  milestones_updated: true,
12788
- state_updated: node_fs.default.existsSync(statePath)
12840
+ state_updated: node_fs.default.existsSync(statePath$1)
12789
12841
  }, raw);
12790
12842
  }
12791
12843
 
@@ -13202,6 +13254,18 @@ const chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
13202
13254
  *
13203
13255
  * Ported from maxsim/bin/lib/commands.cjs
13204
13256
  */
13257
+ function parseTodoFrontmatter(content) {
13258
+ const createdMatch = content.match(/^created:\s*(.+)$/m);
13259
+ const titleMatch = content.match(/^title:\s*(.+)$/m);
13260
+ const areaMatch = content.match(/^area:\s*(.+)$/m);
13261
+ const completedMatch = content.match(/^completed:\s*(.+)$/m);
13262
+ return {
13263
+ created: createdMatch ? createdMatch[1].trim() : "unknown",
13264
+ title: titleMatch ? titleMatch[1].trim() : "Untitled",
13265
+ area: areaMatch ? areaMatch[1].trim() : "general",
13266
+ ...completedMatch && { completed: completedMatch[1].trim() }
13267
+ };
13268
+ }
13205
13269
  function cmdGenerateSlug(text, raw) {
13206
13270
  if (!text) error("text required for slug generation");
13207
13271
  const slug = (0, import_slugify.default)(text, {
@@ -13215,7 +13279,7 @@ function cmdCurrentTimestamp(format, raw) {
13215
13279
  let result;
13216
13280
  switch (format) {
13217
13281
  case "date":
13218
- result = now.toISOString().split("T")[0];
13282
+ result = todayISO();
13219
13283
  break;
13220
13284
  case "filename":
13221
13285
  result = now.toISOString().replace(/:/g, "-").replace(/\..+/, "");
@@ -13227,31 +13291,27 @@ function cmdCurrentTimestamp(format, raw) {
13227
13291
  output({ timestamp: result }, raw, result);
13228
13292
  }
13229
13293
  function cmdListTodos(cwd, area, raw) {
13230
- const pendingDir = node_path.default.join(cwd, ".planning", "todos", "pending");
13294
+ const pendingDir = planningPath(cwd, "todos", "pending");
13231
13295
  let count = 0;
13232
13296
  const todos = [];
13233
13297
  try {
13234
13298
  const files = node_fs.default.readdirSync(pendingDir).filter((f) => f.endsWith(".md"));
13235
13299
  for (const file of files) try {
13236
- const content = node_fs.default.readFileSync(node_path.default.join(pendingDir, file), "utf-8");
13237
- const createdMatch = content.match(/^created:\s*(.+)$/m);
13238
- const titleMatch = content.match(/^title:\s*(.+)$/m);
13239
- const areaMatch = content.match(/^area:\s*(.+)$/m);
13240
- const todoArea = areaMatch ? areaMatch[1].trim() : "general";
13241
- if (area && todoArea !== area) continue;
13300
+ const fm = parseTodoFrontmatter(node_fs.default.readFileSync(node_path.default.join(pendingDir, file), "utf-8"));
13301
+ if (area && fm.area !== area) continue;
13242
13302
  count++;
13243
13303
  todos.push({
13244
13304
  file,
13245
- created: createdMatch ? createdMatch[1].trim() : "unknown",
13246
- title: titleMatch ? titleMatch[1].trim() : "Untitled",
13247
- area: todoArea,
13305
+ created: fm.created,
13306
+ title: fm.title,
13307
+ area: fm.area,
13248
13308
  path: node_path.default.join(".planning", "todos", "pending", file)
13249
13309
  });
13250
13310
  } catch (e) {
13251
- if (process.env.MAXSIM_DEBUG) console.error(e);
13311
+ debugLog(e);
13252
13312
  }
13253
13313
  } catch (e) {
13254
- if (process.env.MAXSIM_DEBUG) console.error(e);
13314
+ debugLog(e);
13255
13315
  }
13256
13316
  output({
13257
13317
  count,
@@ -13275,7 +13335,7 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
13275
13335
  }
13276
13336
  }
13277
13337
  function cmdHistoryDigest(cwd, raw) {
13278
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
13338
+ const phasesDir = phasesPath(cwd);
13279
13339
  const digest = {
13280
13340
  phases: {},
13281
13341
  decisions: [],
@@ -13289,14 +13349,14 @@ function cmdHistoryDigest(cwd, raw) {
13289
13349
  milestone: a.milestone
13290
13350
  });
13291
13351
  if (node_fs.default.existsSync(phasesDir)) try {
13292
- const currentDirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
13352
+ const currentDirs = listSubDirs(phasesDir, true);
13293
13353
  for (const dir of currentDirs) allPhaseDirs.push({
13294
13354
  name: dir,
13295
13355
  fullPath: node_path.default.join(phasesDir, dir),
13296
13356
  milestone: null
13297
13357
  });
13298
13358
  } catch (e) {
13299
- if (process.env.MAXSIM_DEBUG) console.error(e);
13359
+ debugLog(e);
13300
13360
  }
13301
13361
  if (allPhaseDirs.length === 0) {
13302
13362
  output({
@@ -13308,7 +13368,7 @@ function cmdHistoryDigest(cwd, raw) {
13308
13368
  }
13309
13369
  try {
13310
13370
  for (const { name: dir, fullPath: dirPath } of allPhaseDirs) {
13311
- const summaries = node_fs.default.readdirSync(dirPath).filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md");
13371
+ const summaries = node_fs.default.readdirSync(dirPath).filter((f) => isSummaryFile(f));
13312
13372
  for (const summary of summaries) try {
13313
13373
  const fm = extractFrontmatter(node_fs.default.readFileSync(node_path.default.join(dirPath, summary), "utf-8"));
13314
13374
  const phaseNum = fm.phase || dir.split("-")[0];
@@ -13332,7 +13392,7 @@ function cmdHistoryDigest(cwd, raw) {
13332
13392
  const techStack = fm["tech-stack"];
13333
13393
  if (techStack && techStack.added) techStack.added.forEach((t) => digest.tech_stack.add(typeof t === "string" ? t : t.name));
13334
13394
  } catch (e) {
13335
- if (process.env.MAXSIM_DEBUG) console.error(e);
13395
+ debugLog(e);
13336
13396
  }
13337
13397
  }
13338
13398
  const outputDigest = {
@@ -13527,7 +13587,7 @@ async function cmdWebsearch(query, options, raw) {
13527
13587
  }
13528
13588
  }
13529
13589
  function cmdProgressRender(cwd, format, raw) {
13530
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
13590
+ const phasesDir = phasesPath(cwd);
13531
13591
  const milestone = getMilestoneInfo(cwd);
13532
13592
  const phases = [];
13533
13593
  let totalPlans = 0;
@@ -13541,8 +13601,8 @@ function cmdProgressRender(cwd, format, raw) {
13541
13601
  const phaseNum = dm ? dm[1] : dir;
13542
13602
  const phaseName = dm && dm[2] ? dm[2].replace(/-/g, " ") : "";
13543
13603
  const phaseFiles = node_fs.default.readdirSync(node_path.default.join(phasesDir, dir));
13544
- const planCount = phaseFiles.filter((f) => f.endsWith("-PLAN.md") || f === "PLAN.md").length;
13545
- const summaryCount = phaseFiles.filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md").length;
13604
+ const planCount = phaseFiles.filter((f) => isPlanFile(f)).length;
13605
+ const summaryCount = phaseFiles.filter((f) => isSummaryFile(f)).length;
13546
13606
  totalPlans += planCount;
13547
13607
  totalSummaries += summaryCount;
13548
13608
  let status;
@@ -13559,7 +13619,7 @@ function cmdProgressRender(cwd, format, raw) {
13559
13619
  });
13560
13620
  }
13561
13621
  } catch (e) {
13562
- if (process.env.MAXSIM_DEBUG) console.error(e);
13622
+ debugLog(e);
13563
13623
  }
13564
13624
  const percent = totalPlans > 0 ? Math.min(100, Math.round(totalSummaries / totalPlans * 100)) : 0;
13565
13625
  if (format === "table") {
@@ -13619,13 +13679,13 @@ function cmdProgressRender(cwd, format, raw) {
13619
13679
  }
13620
13680
  function cmdTodoComplete(cwd, filename, raw) {
13621
13681
  if (!filename) error("filename required for todo complete");
13622
- const pendingDir = node_path.default.join(cwd, ".planning", "todos", "pending");
13623
- const completedDir = node_path.default.join(cwd, ".planning", "todos", "completed");
13682
+ const pendingDir = planningPath(cwd, "todos", "pending");
13683
+ const completedDir = planningPath(cwd, "todos", "completed");
13624
13684
  const sourcePath = node_path.default.join(pendingDir, filename);
13625
13685
  if (!node_fs.default.existsSync(sourcePath)) error(`Todo not found: ${filename}`);
13626
13686
  node_fs.default.mkdirSync(completedDir, { recursive: true });
13627
13687
  let content = node_fs.default.readFileSync(sourcePath, "utf-8");
13628
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13688
+ const today = todayISO();
13629
13689
  content = `completed: ${today}\n` + content;
13630
13690
  node_fs.default.writeFileSync(node_path.default.join(completedDir, filename), content, "utf-8");
13631
13691
  node_fs.default.unlinkSync(sourcePath);
@@ -13638,7 +13698,7 @@ function cmdTodoComplete(cwd, filename, raw) {
13638
13698
  function cmdScaffold(cwd, type, options, raw) {
13639
13699
  const { phase, name } = options;
13640
13700
  const padded = phase ? normalizePhaseName(phase) : "00";
13641
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13701
+ const today = todayISO();
13642
13702
  const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
13643
13703
  const phaseDir = phaseInfo ? node_path.default.join(cwd, phaseInfo.directory) : null;
13644
13704
  if (phase && !phaseDir && type !== "phase-dir") error(`Phase ${phase} directory not found`);
@@ -13660,7 +13720,7 @@ function cmdScaffold(cwd, type, options, raw) {
13660
13720
  case "phase-dir": {
13661
13721
  if (!phase || !name) error("phase and name required for phase-dir scaffold");
13662
13722
  const dirName = `${padded}-${generateSlugInternal(name)}`;
13663
- const phasesParent = node_path.default.join(cwd, ".planning", "phases");
13723
+ const phasesParent = phasesPath(cwd);
13664
13724
  node_fs.default.mkdirSync(phasesParent, { recursive: true });
13665
13725
  const dirPath = node_path.default.join(phasesParent, dirName);
13666
13726
  node_fs.default.mkdirSync(dirPath, { recursive: true });
@@ -13853,10 +13913,10 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
13853
13913
  output({ error: "Cannot read phase directory" }, raw);
13854
13914
  return;
13855
13915
  }
13856
- const plans = files.filter((f) => /-PLAN\.md$/i.test(f));
13857
- const summaries = files.filter((f) => /-SUMMARY\.md$/i.test(f));
13858
- const planIds = new Set(plans.map((p) => p.replace(/-PLAN\.md$/i, "")));
13859
- const summaryIds = new Set(summaries.map((s) => s.replace(/-SUMMARY\.md$/i, "")));
13916
+ const plans = files.filter((f) => isPlanFile(f));
13917
+ const summaries = files.filter((f) => isSummaryFile(f));
13918
+ const planIds = new Set(plans.map((p) => planId(p)));
13919
+ const summaryIds = new Set(summaries.map((s) => summaryId(s)));
13860
13920
  const incompletePlans = [...planIds].filter((id) => !summaryIds.has(id));
13861
13921
  if (incompletePlans.length > 0) errors.push(`Plans without summaries: ${incompletePlans.join(", ")}`);
13862
13922
  const orphanSummaries = [...summaryIds].filter((id) => !planIds.has(id));
@@ -14041,11 +14101,11 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
14041
14101
  }, raw, verified === results.length ? "valid" : "invalid");
14042
14102
  }
14043
14103
  function cmdValidateConsistency(cwd, raw) {
14044
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
14045
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
14104
+ const rmPath = roadmapPath(cwd);
14105
+ const phasesDir = phasesPath(cwd);
14046
14106
  const errors = [];
14047
14107
  const warnings = [];
14048
- if (!node_fs.default.existsSync(roadmapPath)) {
14108
+ if (!node_fs.default.existsSync(rmPath)) {
14049
14109
  errors.push("ROADMAP.md not found");
14050
14110
  output({
14051
14111
  passed: false,
@@ -14054,20 +14114,20 @@ function cmdValidateConsistency(cwd, raw) {
14054
14114
  }, raw, "failed");
14055
14115
  return;
14056
14116
  }
14057
- const roadmapContent = node_fs.default.readFileSync(roadmapPath, "utf-8");
14117
+ const roadmapContent = node_fs.default.readFileSync(rmPath, "utf-8");
14058
14118
  const roadmapPhases = /* @__PURE__ */ new Set();
14059
14119
  const phasePattern = getPhasePattern();
14060
14120
  let m;
14061
14121
  while ((m = phasePattern.exec(roadmapContent)) !== null) roadmapPhases.add(m[1]);
14062
14122
  const diskPhases = /* @__PURE__ */ new Set();
14063
14123
  try {
14064
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
14124
+ const dirs = listSubDirs(phasesDir);
14065
14125
  for (const dir of dirs) {
14066
14126
  const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)?)/i);
14067
14127
  if (dm) diskPhases.add(dm[1]);
14068
14128
  }
14069
14129
  } catch (e) {
14070
- if (process.env.MAXSIM_DEBUG) console.error(e);
14130
+ debugLog(e);
14071
14131
  }
14072
14132
  for (const p of roadmapPhases) if (!diskPhases.has(p) && !diskPhases.has(normalizePhaseName(p))) warnings.push(`Phase ${p} in ROADMAP.md but no directory on disk`);
14073
14133
  for (const p of diskPhases) {
@@ -14077,31 +14137,31 @@ function cmdValidateConsistency(cwd, raw) {
14077
14137
  const integerPhases = [...diskPhases].filter((p) => !p.includes(".")).map((p) => parseInt(p, 10)).sort((a, b) => a - b);
14078
14138
  for (let i = 1; i < integerPhases.length; i++) if (integerPhases[i] !== integerPhases[i - 1] + 1) warnings.push(`Gap in phase numbering: ${integerPhases[i - 1]} → ${integerPhases[i]}`);
14079
14139
  try {
14080
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
14140
+ const dirs = listSubDirs(phasesDir, true);
14081
14141
  for (const dir of dirs) {
14082
14142
  const phaseFiles = node_fs.default.readdirSync(node_path.default.join(phasesDir, dir));
14083
- const plans = phaseFiles.filter((f) => f.endsWith("-PLAN.md")).sort();
14143
+ const plans = phaseFiles.filter((f) => isPlanFile(f)).sort();
14084
14144
  const planNums = plans.map((p) => {
14085
14145
  const pm = p.match(/-(\d{2})-PLAN\.md$/);
14086
14146
  return pm ? parseInt(pm[1], 10) : null;
14087
14147
  }).filter((n) => n !== null);
14088
14148
  for (let i = 1; i < planNums.length; i++) if (planNums[i] !== planNums[i - 1] + 1) warnings.push(`Gap in plan numbering in ${dir}: plan ${planNums[i - 1]} → ${planNums[i]}`);
14089
- const summaries = phaseFiles.filter((f) => f.endsWith("-SUMMARY.md"));
14090
- const planIdsSet = new Set(plans.map((p) => p.replace("-PLAN.md", "")));
14091
- const summaryIdsSet = new Set(summaries.map((s) => s.replace("-SUMMARY.md", "")));
14149
+ const summaries = phaseFiles.filter((f) => isSummaryFile(f));
14150
+ const planIdsSet = new Set(plans.map((p) => planId(p)));
14151
+ const summaryIdsSet = new Set(summaries.map((s) => summaryId(s)));
14092
14152
  for (const sid of summaryIdsSet) if (!planIdsSet.has(sid)) warnings.push(`Summary ${sid}-SUMMARY.md in ${dir} has no matching PLAN.md`);
14093
14153
  }
14094
14154
  } catch (e) {
14095
- if (process.env.MAXSIM_DEBUG) console.error(e);
14155
+ debugLog(e);
14096
14156
  }
14097
14157
  try {
14098
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
14158
+ const dirs = listSubDirs(phasesDir);
14099
14159
  for (const dir of dirs) {
14100
- const plans = node_fs.default.readdirSync(node_path.default.join(phasesDir, dir)).filter((f) => f.endsWith("-PLAN.md"));
14160
+ const plans = node_fs.default.readdirSync(node_path.default.join(phasesDir, dir)).filter((f) => isPlanFile(f));
14101
14161
  for (const plan of plans) if (!extractFrontmatter(node_fs.default.readFileSync(node_path.default.join(phasesDir, dir, plan), "utf-8")).wave) warnings.push(`${dir}/${plan}: missing 'wave' in frontmatter`);
14102
14162
  }
14103
14163
  } catch (e) {
14104
- if (process.env.MAXSIM_DEBUG) console.error(e);
14164
+ debugLog(e);
14105
14165
  }
14106
14166
  const passed = errors.length === 0;
14107
14167
  output({
@@ -14112,12 +14172,12 @@ function cmdValidateConsistency(cwd, raw) {
14112
14172
  }, raw, passed ? "passed" : "failed");
14113
14173
  }
14114
14174
  function cmdValidateHealth(cwd, options, raw) {
14115
- const planningDir = node_path.default.join(cwd, ".planning");
14116
- const projectPath = node_path.default.join(planningDir, "PROJECT.md");
14117
- const roadmapPath = node_path.default.join(planningDir, "ROADMAP.md");
14118
- const statePath = node_path.default.join(planningDir, "STATE.md");
14119
- const configPath = node_path.default.join(planningDir, "config.json");
14120
- const phasesDir = node_path.default.join(planningDir, "phases");
14175
+ const planningDir = planningPath(cwd);
14176
+ const projectPath = planningPath(cwd, "PROJECT.md");
14177
+ const rmPath = roadmapPath(cwd);
14178
+ const stPath = statePath(cwd);
14179
+ const cfgPath = configPath(cwd);
14180
+ const phasesDir = phasesPath(cwd);
14121
14181
  const errors = [];
14122
14182
  const warnings = [];
14123
14183
  const info = [];
@@ -14153,21 +14213,20 @@ function cmdValidateHealth(cwd, options, raw) {
14153
14213
  "## Requirements"
14154
14214
  ]) if (!content.includes(section)) addIssue("warning", "W001", `PROJECT.md missing section: ${section}`, "Add section manually");
14155
14215
  }
14156
- if (!node_fs.default.existsSync(roadmapPath)) addIssue("error", "E003", "ROADMAP.md not found", "Run /maxsim:new-milestone to create roadmap");
14157
- if (!node_fs.default.existsSync(statePath)) {
14216
+ if (!node_fs.default.existsSync(rmPath)) addIssue("error", "E003", "ROADMAP.md not found", "Run /maxsim:new-milestone to create roadmap");
14217
+ if (!node_fs.default.existsSync(stPath)) {
14158
14218
  addIssue("error", "E004", "STATE.md not found", "Run /maxsim:health --repair to regenerate", true);
14159
14219
  repairs.push("regenerateState");
14160
14220
  } else {
14161
- const phaseRefs = [...node_fs.default.readFileSync(statePath, "utf-8").matchAll(/[Pp]hase\s+(\d+(?:\.\d+)?)/g)].map((m) => m[1]);
14221
+ const phaseRefs = [...node_fs.default.readFileSync(stPath, "utf-8").matchAll(/[Pp]hase\s+(\d+(?:\.\d+)?)/g)].map((m) => m[1]);
14162
14222
  const diskPhases = /* @__PURE__ */ new Set();
14163
14223
  try {
14164
- const entries = node_fs.default.readdirSync(phasesDir, { withFileTypes: true });
14165
- for (const e of entries) if (e.isDirectory()) {
14166
- const dm = e.name.match(/^(\d+(?:\.\d+)?)/);
14224
+ for (const dir of listSubDirs(phasesDir)) {
14225
+ const dm = dir.match(/^(\d+(?:\.\d+)?)/);
14167
14226
  if (dm) diskPhases.add(dm[1]);
14168
14227
  }
14169
14228
  } catch (e) {
14170
- if (process.env.MAXSIM_DEBUG) console.error(e);
14229
+ debugLog(e);
14171
14230
  }
14172
14231
  for (const ref of phaseRefs) {
14173
14232
  const normalizedRef = String(parseInt(ref, 10)).padStart(2, "0");
@@ -14179,11 +14238,11 @@ function cmdValidateHealth(cwd, options, raw) {
14179
14238
  }
14180
14239
  }
14181
14240
  }
14182
- if (!node_fs.default.existsSync(configPath)) {
14241
+ if (!node_fs.default.existsSync(cfgPath)) {
14183
14242
  addIssue("warning", "W003", "config.json not found", "Run /maxsim:health --repair to create with defaults", true);
14184
14243
  repairs.push("createConfig");
14185
14244
  } else try {
14186
- const rawContent = node_fs.default.readFileSync(configPath, "utf-8");
14245
+ const rawContent = node_fs.default.readFileSync(cfgPath, "utf-8");
14187
14246
  const parsed = JSON.parse(rawContent);
14188
14247
  const validProfiles = [
14189
14248
  "quality",
@@ -14197,42 +14256,39 @@ function cmdValidateHealth(cwd, options, raw) {
14197
14256
  repairs.push("resetConfig");
14198
14257
  }
14199
14258
  try {
14200
- const entries = node_fs.default.readdirSync(phasesDir, { withFileTypes: true });
14201
- for (const e of entries) if (e.isDirectory() && !e.name.match(/^\d{2}(?:\.\d+)?-[\w-]+$/)) addIssue("warning", "W005", `Phase directory "${e.name}" doesn't follow NN-name format`, "Rename to match pattern (e.g., 01-setup)");
14259
+ for (const dirName of listSubDirs(phasesDir)) if (!dirName.match(/^\d{2}(?:\.\d+)?-[\w-]+$/)) addIssue("warning", "W005", `Phase directory "${dirName}" doesn't follow NN-name format`, "Rename to match pattern (e.g., 01-setup)");
14202
14260
  } catch (e) {
14203
- if (process.env.MAXSIM_DEBUG) console.error(e);
14261
+ debugLog(e);
14204
14262
  }
14205
14263
  try {
14206
- const entries = node_fs.default.readdirSync(phasesDir, { withFileTypes: true });
14207
- for (const e of entries) {
14208
- if (!e.isDirectory()) continue;
14209
- const phaseFiles = node_fs.default.readdirSync(node_path.default.join(phasesDir, e.name));
14210
- const plans = phaseFiles.filter((f) => f.endsWith("-PLAN.md") || f === "PLAN.md");
14211
- const summaries = phaseFiles.filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md");
14212
- const summaryBases = new Set(summaries.map((s) => s.replace("-SUMMARY.md", "").replace("SUMMARY.md", "")));
14264
+ const orphanDirs = listSubDirs(phasesDir);
14265
+ for (const dirName of orphanDirs) {
14266
+ const phaseFiles = node_fs.default.readdirSync(node_path.default.join(phasesDir, dirName));
14267
+ const plans = phaseFiles.filter((f) => isPlanFile(f));
14268
+ const summaries = phaseFiles.filter((f) => isSummaryFile(f));
14269
+ const summaryBases = new Set(summaries.map((s) => summaryId(s)));
14213
14270
  for (const plan of plans) {
14214
- const planBase = plan.replace("-PLAN.md", "").replace("PLAN.md", "");
14215
- if (!summaryBases.has(planBase)) addIssue("info", "I001", `${e.name}/${plan} has no SUMMARY.md`, "May be in progress");
14271
+ const planBase = planId(plan);
14272
+ if (!summaryBases.has(planBase)) addIssue("info", "I001", `${dirName}/${plan} has no SUMMARY.md`, "May be in progress");
14216
14273
  }
14217
14274
  }
14218
14275
  } catch (e) {
14219
- if (process.env.MAXSIM_DEBUG) console.error(e);
14276
+ debugLog(e);
14220
14277
  }
14221
- if (node_fs.default.existsSync(roadmapPath)) {
14222
- const roadmapContent = node_fs.default.readFileSync(roadmapPath, "utf-8");
14278
+ if (node_fs.default.existsSync(rmPath)) {
14279
+ const roadmapContent = node_fs.default.readFileSync(rmPath, "utf-8");
14223
14280
  const roadmapPhases = /* @__PURE__ */ new Set();
14224
14281
  const phasePattern = getPhasePattern();
14225
14282
  let m;
14226
14283
  while ((m = phasePattern.exec(roadmapContent)) !== null) roadmapPhases.add(m[1]);
14227
14284
  const diskPhases = /* @__PURE__ */ new Set();
14228
14285
  try {
14229
- const entries = node_fs.default.readdirSync(phasesDir, { withFileTypes: true });
14230
- for (const e of entries) if (e.isDirectory()) {
14231
- const dm = e.name.match(/^(\d+[A-Z]?(?:\.\d+)?)/i);
14286
+ for (const dir of listSubDirs(phasesDir)) {
14287
+ const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)?)/i);
14232
14288
  if (dm) diskPhases.add(dm[1]);
14233
14289
  }
14234
14290
  } catch (e) {
14235
- if (process.env.MAXSIM_DEBUG) console.error(e);
14291
+ debugLog(e);
14236
14292
  }
14237
14293
  for (const p of roadmapPhases) {
14238
14294
  const padded = String(parseInt(p, 10)).padStart(2, "0");
@@ -14248,7 +14304,7 @@ function cmdValidateHealth(cwd, options, raw) {
14248
14304
  switch (repair) {
14249
14305
  case "createConfig":
14250
14306
  case "resetConfig":
14251
- node_fs.default.writeFileSync(configPath, JSON.stringify({
14307
+ node_fs.default.writeFileSync(cfgPath, JSON.stringify({
14252
14308
  model_profile: "balanced",
14253
14309
  commit_docs: true,
14254
14310
  search_gitignored: false,
@@ -14265,9 +14321,9 @@ function cmdValidateHealth(cwd, options, raw) {
14265
14321
  });
14266
14322
  break;
14267
14323
  case "regenerateState": {
14268
- if (node_fs.default.existsSync(statePath)) {
14269
- const backupPath = `${statePath}.bak-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
14270
- node_fs.default.copyFileSync(statePath, backupPath);
14324
+ if (node_fs.default.existsSync(stPath)) {
14325
+ const backupPath = `${stPath}.bak-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
14326
+ node_fs.default.copyFileSync(stPath, backupPath);
14271
14327
  repairActions.push({
14272
14328
  action: "backupState",
14273
14329
  success: true,
@@ -14283,8 +14339,8 @@ function cmdValidateHealth(cwd, options, raw) {
14283
14339
  stateContent += `**Current phase:** (determining...)\n`;
14284
14340
  stateContent += `**Status:** Resuming\n\n`;
14285
14341
  stateContent += `## Session Log\n\n`;
14286
- stateContent += `- ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}: STATE.md regenerated by /maxsim:health --repair\n`;
14287
- node_fs.default.writeFileSync(statePath, stateContent, "utf-8");
14342
+ stateContent += `- ${todayISO()}: STATE.md regenerated by /maxsim:health --repair\n`;
14343
+ node_fs.default.writeFileSync(stPath, stateContent, "utf-8");
14288
14344
  repairActions.push({
14289
14345
  action: repair,
14290
14346
  success: true,
@@ -14323,10 +14379,170 @@ function cmdValidateHealth(cwd, options, raw) {
14323
14379
  *
14324
14380
  * Ported from maxsim/bin/lib/phase.cjs
14325
14381
  */
14382
+ function scaffoldPhaseStubs(dirPath, phaseId, name) {
14383
+ const today = todayISO();
14384
+ node_fs.default.writeFileSync(node_path.default.join(dirPath, `${phaseId}-CONTEXT.md`), `# Phase ${phaseId} Context: ${name}\n\n**Created:** ${today}\n**Phase goal:** [To be defined during /maxsim:discuss-phase]\n\n---\n\n_Context will be populated by /maxsim:discuss-phase_\n`);
14385
+ node_fs.default.writeFileSync(node_path.default.join(dirPath, `${phaseId}-RESEARCH.md`), `# Phase ${phaseId}: ${name} - Research\n\n**Researched:** Not yet\n**Domain:** TBD\n**Confidence:** TBD\n\n---\n\n_Research will be populated by /maxsim:research-phase_\n`);
14386
+ }
14387
+ function phaseAddCore(cwd, description, options) {
14388
+ const rmPath = roadmapPath(cwd);
14389
+ if (!node_fs.default.existsSync(rmPath)) throw new Error("ROADMAP.md not found");
14390
+ const content = node_fs.default.readFileSync(rmPath, "utf-8");
14391
+ const slug = generateSlugInternal(description);
14392
+ const phasePattern = getPhasePattern();
14393
+ let maxPhase = 0;
14394
+ let m;
14395
+ while ((m = phasePattern.exec(content)) !== null) {
14396
+ const num = parseInt(m[1], 10);
14397
+ if (num > maxPhase) maxPhase = num;
14398
+ }
14399
+ const newPhaseNum = maxPhase + 1;
14400
+ const paddedNum = String(newPhaseNum).padStart(2, "0");
14401
+ const dirName = `${paddedNum}-${slug}`;
14402
+ const dirPath = planningPath(cwd, "phases", dirName);
14403
+ node_fs.default.mkdirSync(dirPath, { recursive: true });
14404
+ node_fs.default.writeFileSync(node_path.default.join(dirPath, ".gitkeep"), "");
14405
+ if (options?.includeStubs) scaffoldPhaseStubs(dirPath, paddedNum, description);
14406
+ const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /maxsim:plan-phase ${newPhaseNum} to break down)\n`;
14407
+ let updatedContent;
14408
+ const lastSeparator = content.lastIndexOf("\n---");
14409
+ if (lastSeparator > 0) updatedContent = content.slice(0, lastSeparator) + phaseEntry + content.slice(lastSeparator);
14410
+ else updatedContent = content + phaseEntry;
14411
+ node_fs.default.writeFileSync(rmPath, updatedContent, "utf-8");
14412
+ return {
14413
+ phase_number: newPhaseNum,
14414
+ padded: paddedNum,
14415
+ slug,
14416
+ directory: `.planning/phases/${dirName}`,
14417
+ description
14418
+ };
14419
+ }
14420
+ function phaseInsertCore(cwd, afterPhase, description, options) {
14421
+ const rmPath = roadmapPath(cwd);
14422
+ if (!node_fs.default.existsSync(rmPath)) throw new Error("ROADMAP.md not found");
14423
+ const content = node_fs.default.readFileSync(rmPath, "utf-8");
14424
+ const slug = generateSlugInternal(description);
14425
+ const afterPhaseEscaped = "0*" + normalizePhaseName(afterPhase).replace(/^0+/, "").replace(/\./g, "\\.");
14426
+ if (!getPhasePattern(afterPhaseEscaped, "i").test(content)) throw new Error(`Phase ${afterPhase} not found in ROADMAP.md`);
14427
+ const phasesDirPath = phasesPath(cwd);
14428
+ const normalizedBase = normalizePhaseName(afterPhase);
14429
+ const existingDecimals = [];
14430
+ try {
14431
+ const dirs = listSubDirs(phasesDirPath);
14432
+ const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
14433
+ for (const dir of dirs) {
14434
+ const dm = dir.match(decimalPattern);
14435
+ if (dm) existingDecimals.push(parseInt(dm[1], 10));
14436
+ }
14437
+ } catch (e) {
14438
+ debugLog(e);
14439
+ }
14440
+ const decimalPhase = `${normalizedBase}.${existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1}`;
14441
+ const dirName = `${decimalPhase}-${slug}`;
14442
+ const dirPath = planningPath(cwd, "phases", dirName);
14443
+ node_fs.default.mkdirSync(dirPath, { recursive: true });
14444
+ node_fs.default.writeFileSync(node_path.default.join(dirPath, ".gitkeep"), "");
14445
+ if (options?.includeStubs) scaffoldPhaseStubs(dirPath, decimalPhase, description);
14446
+ const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /maxsim:plan-phase ${decimalPhase} to break down)\n`;
14447
+ const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, "i");
14448
+ const headerMatch = content.match(headerPattern);
14449
+ if (!headerMatch) throw new Error(`Could not find Phase ${afterPhase} header`);
14450
+ const headerIdx = content.indexOf(headerMatch[0]);
14451
+ const nextPhaseMatch = content.slice(headerIdx + headerMatch[0].length).match(/\n#{2,4}\s+Phase\s+\d/i);
14452
+ let insertIdx;
14453
+ if (nextPhaseMatch) insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
14454
+ else insertIdx = content.length;
14455
+ const updatedContent = content.slice(0, insertIdx) + phaseEntry + content.slice(insertIdx);
14456
+ node_fs.default.writeFileSync(rmPath, updatedContent, "utf-8");
14457
+ return {
14458
+ phase_number: decimalPhase,
14459
+ after_phase: afterPhase,
14460
+ slug,
14461
+ directory: `.planning/phases/${dirName}`,
14462
+ description
14463
+ };
14464
+ }
14465
+ function phaseCompleteCore(cwd, phaseNum) {
14466
+ const rmPath = roadmapPath(cwd);
14467
+ const stPath = statePath(cwd);
14468
+ const phasesDirPath = phasesPath(cwd);
14469
+ const today = todayISO();
14470
+ const phaseInfo = findPhaseInternal(cwd, phaseNum);
14471
+ if (!phaseInfo) throw new Error(`Phase ${phaseNum} not found`);
14472
+ const planCount = phaseInfo.plans.length;
14473
+ const summaryCount = phaseInfo.summaries.length;
14474
+ let requirementsUpdated = false;
14475
+ if (node_fs.default.existsSync(rmPath)) {
14476
+ let roadmapContent = node_fs.default.readFileSync(rmPath, "utf-8");
14477
+ const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapePhaseNum(phaseNum)}[:\\s][^\\n]*)`, "i");
14478
+ roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
14479
+ const phaseEscaped = escapePhaseNum(phaseNum);
14480
+ const tablePattern = new RegExp(`(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`, "i");
14481
+ roadmapContent = roadmapContent.replace(tablePattern, `$1 Complete $2 ${today} $3`);
14482
+ const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`, "i");
14483
+ roadmapContent = roadmapContent.replace(planCountPattern, `$1${summaryCount}/${planCount} plans complete`);
14484
+ node_fs.default.writeFileSync(rmPath, roadmapContent, "utf-8");
14485
+ const reqPath = planningPath(cwd, "REQUIREMENTS.md");
14486
+ if (node_fs.default.existsSync(reqPath)) {
14487
+ const reqMatch = roadmapContent.match(new RegExp(`Phase\\s+${escapePhaseNum(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, "i"));
14488
+ if (reqMatch) {
14489
+ const reqIds = reqMatch[1].replace(/[\[\]]/g, "").split(/[,\s]+/).map((r) => r.trim()).filter(Boolean);
14490
+ let reqContent = node_fs.default.readFileSync(reqPath, "utf-8");
14491
+ for (const reqId of reqIds) {
14492
+ reqContent = reqContent.replace(new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqId}\\*\\*)`, "gi"), "$1x$2");
14493
+ reqContent = reqContent.replace(new RegExp(`(\\|\\s*${reqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, "gi"), "$1 Complete $2");
14494
+ }
14495
+ node_fs.default.writeFileSync(reqPath, reqContent, "utf-8");
14496
+ requirementsUpdated = true;
14497
+ }
14498
+ }
14499
+ }
14500
+ let nextPhaseNum = null;
14501
+ let nextPhaseName = null;
14502
+ let isLastPhase = true;
14503
+ try {
14504
+ const dirs = listSubDirs(phasesDirPath, true);
14505
+ for (const dir of dirs) {
14506
+ const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
14507
+ if (dm) {
14508
+ if (comparePhaseNum(dm[1], phaseNum) > 0) {
14509
+ nextPhaseNum = dm[1];
14510
+ nextPhaseName = dm[2] || null;
14511
+ isLastPhase = false;
14512
+ break;
14513
+ }
14514
+ }
14515
+ }
14516
+ } catch (e) {
14517
+ debugLog(e);
14518
+ }
14519
+ if (node_fs.default.existsSync(stPath)) {
14520
+ let stateContent = node_fs.default.readFileSync(stPath, "utf-8");
14521
+ stateContent = stateContent.replace(/(\*\*Current Phase:\*\*\s*).*/, `$1${nextPhaseNum || phaseNum}`);
14522
+ if (nextPhaseName) stateContent = stateContent.replace(/(\*\*Current Phase Name:\*\*\s*).*/, `$1${nextPhaseName.replace(/-/g, " ")}`);
14523
+ stateContent = stateContent.replace(/(\*\*Status:\*\*\s*).*/, `$1${isLastPhase ? "Milestone complete" : "Ready to plan"}`);
14524
+ stateContent = stateContent.replace(/(\*\*Current Plan:\*\*\s*).*/, `$1Not started`);
14525
+ stateContent = stateContent.replace(/(\*\*Last Activity:\*\*\s*).*/, `$1${today}`);
14526
+ stateContent = stateContent.replace(/(\*\*Last Activity Description:\*\*\s*).*/, `$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ""}`);
14527
+ node_fs.default.writeFileSync(stPath, stateContent, "utf-8");
14528
+ }
14529
+ return {
14530
+ completed_phase: phaseNum,
14531
+ phase_name: phaseInfo.phase_name,
14532
+ plans_executed: `${summaryCount}/${planCount}`,
14533
+ next_phase: nextPhaseNum,
14534
+ next_phase_name: nextPhaseName,
14535
+ is_last_phase: isLastPhase,
14536
+ date: today,
14537
+ roadmap_updated: node_fs.default.existsSync(rmPath),
14538
+ state_updated: node_fs.default.existsSync(stPath),
14539
+ requirements_updated: requirementsUpdated
14540
+ };
14541
+ }
14326
14542
  function cmdPhasesList(cwd, options, raw) {
14327
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
14543
+ const phasesDirPath = phasesPath(cwd);
14328
14544
  const { type, phase, includeArchived } = options;
14329
- if (!node_fs.default.existsSync(phasesDir)) {
14545
+ if (!node_fs.default.existsSync(phasesDirPath)) {
14330
14546
  if (type) output({
14331
14547
  files: [],
14332
14548
  count: 0
@@ -14338,7 +14554,7 @@ function cmdPhasesList(cwd, options, raw) {
14338
14554
  return;
14339
14555
  }
14340
14556
  try {
14341
- let dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
14557
+ let dirs = listSubDirs(phasesDirPath);
14342
14558
  if (includeArchived) {
14343
14559
  const archived = getArchivedPhaseDirs(cwd);
14344
14560
  for (const a of archived) dirs.push(`${a.name} [${a.milestone}]`);
@@ -14361,11 +14577,11 @@ function cmdPhasesList(cwd, options, raw) {
14361
14577
  if (type) {
14362
14578
  const files = [];
14363
14579
  for (const dir of dirs) {
14364
- const dirPath = node_path.default.join(phasesDir, dir);
14580
+ const dirPath = node_path.default.join(phasesDirPath, dir);
14365
14581
  const dirFiles = node_fs.default.readdirSync(dirPath);
14366
14582
  let filtered;
14367
- if (type === "plans") filtered = dirFiles.filter((f) => f.endsWith("-PLAN.md") || f === "PLAN.md");
14368
- else if (type === "summaries") filtered = dirFiles.filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md");
14583
+ if (type === "plans") filtered = dirFiles.filter(isPlanFile);
14584
+ else if (type === "summaries") filtered = dirFiles.filter(isSummaryFile);
14369
14585
  else filtered = dirFiles;
14370
14586
  files.push(...filtered.sort());
14371
14587
  }
@@ -14385,9 +14601,9 @@ function cmdPhasesList(cwd, options, raw) {
14385
14601
  }
14386
14602
  }
14387
14603
  function cmdPhaseNextDecimal(cwd, basePhase, raw) {
14388
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
14604
+ const phasesDirPath = phasesPath(cwd);
14389
14605
  const normalized = normalizePhaseName(basePhase);
14390
- if (!node_fs.default.existsSync(phasesDir)) {
14606
+ if (!node_fs.default.existsSync(phasesDirPath)) {
14391
14607
  output({
14392
14608
  found: false,
14393
14609
  base_phase: normalized,
@@ -14397,7 +14613,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
14397
14613
  return;
14398
14614
  }
14399
14615
  try {
14400
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
14616
+ const dirs = listSubDirs(phasesDirPath);
14401
14617
  const baseExists = dirs.some((d) => d.startsWith(normalized + "-") || d === normalized);
14402
14618
  const decimalPattern = new RegExp(`^${normalized}\\.(\\d+)`);
14403
14619
  const existingDecimals = [];
@@ -14426,7 +14642,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
14426
14642
  }
14427
14643
  function cmdFindPhase(cwd, phase, raw) {
14428
14644
  if (!phase) error("phase identifier required");
14429
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
14645
+ const phasesDirPath = phasesPath(cwd);
14430
14646
  const normalized = normalizePhaseName(phase);
14431
14647
  const notFound = {
14432
14648
  found: false,
@@ -14437,7 +14653,7 @@ function cmdFindPhase(cwd, phase, raw) {
14437
14653
  summaries: []
14438
14654
  };
14439
14655
  try {
14440
- const match = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => comparePhaseNum(a, b)).find((d) => d.startsWith(normalized));
14656
+ const match = listSubDirs(phasesDirPath, true).find((d) => d.startsWith(normalized));
14441
14657
  if (!match) {
14442
14658
  output(notFound, raw, "");
14443
14659
  return;
@@ -14445,10 +14661,10 @@ function cmdFindPhase(cwd, phase, raw) {
14445
14661
  const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
14446
14662
  const phaseNumber = dirMatch ? dirMatch[1] : normalized;
14447
14663
  const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
14448
- const phaseDir = node_path.default.join(phasesDir, match);
14664
+ const phaseDir = node_path.default.join(phasesDirPath, match);
14449
14665
  const phaseFiles = node_fs.default.readdirSync(phaseDir);
14450
- const plans = phaseFiles.filter((f) => f.endsWith("-PLAN.md") || f === "PLAN.md").sort();
14451
- const summaries = phaseFiles.filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md").sort();
14666
+ const plans = phaseFiles.filter(isPlanFile).sort();
14667
+ const summaries = phaseFiles.filter(isSummaryFile).sort();
14452
14668
  const result = {
14453
14669
  found: true,
14454
14670
  directory: node_path.default.join(".planning", "phases", match),
@@ -14464,14 +14680,14 @@ function cmdFindPhase(cwd, phase, raw) {
14464
14680
  }
14465
14681
  function cmdPhasePlanIndex(cwd, phase, raw) {
14466
14682
  if (!phase) error("phase required for phase-plan-index");
14467
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
14683
+ const phasesDirPath = phasesPath(cwd);
14468
14684
  const normalized = normalizePhaseName(phase);
14469
14685
  let phaseDir = null;
14470
14686
  try {
14471
- const match = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => comparePhaseNum(a, b)).find((d) => d.startsWith(normalized));
14472
- if (match) phaseDir = node_path.default.join(phasesDir, match);
14687
+ const match = listSubDirs(phasesDirPath, true).find((d) => d.startsWith(normalized));
14688
+ if (match) phaseDir = node_path.default.join(phasesDirPath, match);
14473
14689
  } catch (e) {
14474
- if (process.env.MAXSIM_DEBUG) console.error(e);
14690
+ debugLog(e);
14475
14691
  }
14476
14692
  if (!phaseDir) {
14477
14693
  output({
@@ -14485,15 +14701,15 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
14485
14701
  return;
14486
14702
  }
14487
14703
  const phaseFiles = node_fs.default.readdirSync(phaseDir);
14488
- const planFiles = phaseFiles.filter((f) => f.endsWith("-PLAN.md") || f === "PLAN.md").sort();
14489
- const summaryFiles = phaseFiles.filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md");
14490
- const completedPlanIds = new Set(summaryFiles.map((s) => s.replace("-SUMMARY.md", "").replace("SUMMARY.md", "")));
14704
+ const planFiles = phaseFiles.filter(isPlanFile).sort();
14705
+ const summaryFiles = phaseFiles.filter(isSummaryFile);
14706
+ const completedPlanIds = new Set(summaryFiles.map(summaryId));
14491
14707
  const plans = [];
14492
14708
  const waves = {};
14493
14709
  const incomplete = [];
14494
14710
  let hasCheckpoints = false;
14495
14711
  for (const planFile of planFiles) {
14496
- const planId = planFile.replace("-PLAN.md", "").replace("PLAN.md", "");
14712
+ const id = planId(planFile);
14497
14713
  const planPath = node_path.default.join(phaseDir, planFile);
14498
14714
  const content = node_fs.default.readFileSync(planPath, "utf-8");
14499
14715
  const fm = extractFrontmatter(content);
@@ -14504,10 +14720,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
14504
14720
  if (!autonomous) hasCheckpoints = true;
14505
14721
  let filesModified = [];
14506
14722
  if (fm["files-modified"]) filesModified = Array.isArray(fm["files-modified"]) ? fm["files-modified"] : [fm["files-modified"]];
14507
- const hasSummary = completedPlanIds.has(planId);
14508
- if (!hasSummary) incomplete.push(planId);
14723
+ const hasSummary = completedPlanIds.has(id);
14724
+ if (!hasSummary) incomplete.push(id);
14509
14725
  const plan = {
14510
- id: planId,
14726
+ id,
14511
14727
  wave,
14512
14728
  autonomous,
14513
14729
  objective: fm.objective || null,
@@ -14518,7 +14734,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
14518
14734
  plans.push(plan);
14519
14735
  const waveKey = String(wave);
14520
14736
  if (!waves[waveKey]) waves[waveKey] = [];
14521
- waves[waveKey].push(planId);
14737
+ waves[waveKey].push(id);
14522
14738
  }
14523
14739
  output({
14524
14740
  phase: normalized,
@@ -14530,102 +14746,54 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
14530
14746
  }
14531
14747
  function cmdPhaseAdd(cwd, description, raw) {
14532
14748
  if (!description) error("description required for phase add");
14533
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
14534
- if (!node_fs.default.existsSync(roadmapPath)) error("ROADMAP.md not found");
14535
- const content = node_fs.default.readFileSync(roadmapPath, "utf-8");
14536
- const slug = generateSlugInternal(description);
14537
- const phasePattern = getPhasePattern();
14538
- let maxPhase = 0;
14539
- let m;
14540
- while ((m = phasePattern.exec(content)) !== null) {
14541
- const num = parseInt(m[1], 10);
14542
- if (num > maxPhase) maxPhase = num;
14749
+ try {
14750
+ const result = phaseAddCore(cwd, description, { includeStubs: false });
14751
+ output({
14752
+ phase_number: result.phase_number,
14753
+ padded: result.padded,
14754
+ name: result.description,
14755
+ slug: result.slug,
14756
+ directory: result.directory
14757
+ }, raw, result.padded);
14758
+ } catch (e) {
14759
+ error(e.message);
14543
14760
  }
14544
- const newPhaseNum = maxPhase + 1;
14545
- const paddedNum = String(newPhaseNum).padStart(2, "0");
14546
- const dirName = `${paddedNum}-${slug}`;
14547
- const dirPath = node_path.default.join(cwd, ".planning", "phases", dirName);
14548
- node_fs.default.mkdirSync(dirPath, { recursive: true });
14549
- node_fs.default.writeFileSync(node_path.default.join(dirPath, ".gitkeep"), "");
14550
- const phaseEntry = `\n### Phase ${newPhaseNum}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${maxPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /maxsim:plan-phase ${newPhaseNum} to break down)\n`;
14551
- let updatedContent;
14552
- const lastSeparator = content.lastIndexOf("\n---");
14553
- if (lastSeparator > 0) updatedContent = content.slice(0, lastSeparator) + phaseEntry + content.slice(lastSeparator);
14554
- else updatedContent = content + phaseEntry;
14555
- node_fs.default.writeFileSync(roadmapPath, updatedContent, "utf-8");
14556
- output({
14557
- phase_number: newPhaseNum,
14558
- padded: paddedNum,
14559
- name: description,
14560
- slug,
14561
- directory: `.planning/phases/${dirName}`
14562
- }, raw, paddedNum);
14563
14761
  }
14564
14762
  function cmdPhaseInsert(cwd, afterPhase, description, raw) {
14565
14763
  if (!afterPhase || !description) error("after-phase and description required for phase insert");
14566
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
14567
- if (!node_fs.default.existsSync(roadmapPath)) error("ROADMAP.md not found");
14568
- const content = node_fs.default.readFileSync(roadmapPath, "utf-8");
14569
- const slug = generateSlugInternal(description);
14570
- const afterPhaseEscaped = "0*" + normalizePhaseName(afterPhase).replace(/^0+/, "").replace(/\./g, "\\.");
14571
- if (!getPhasePattern(afterPhaseEscaped, "i").test(content)) error(`Phase ${afterPhase} not found in ROADMAP.md`);
14572
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
14573
- const normalizedBase = normalizePhaseName(afterPhase);
14574
- const existingDecimals = [];
14575
14764
  try {
14576
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
14577
- const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
14578
- for (const dir of dirs) {
14579
- const dm = dir.match(decimalPattern);
14580
- if (dm) existingDecimals.push(parseInt(dm[1], 10));
14581
- }
14765
+ const result = phaseInsertCore(cwd, afterPhase, description, { includeStubs: false });
14766
+ output({
14767
+ phase_number: result.phase_number,
14768
+ after_phase: result.after_phase,
14769
+ name: result.description,
14770
+ slug: result.slug,
14771
+ directory: result.directory
14772
+ }, raw, result.phase_number);
14582
14773
  } catch (e) {
14583
- if (process.env.MAXSIM_DEBUG) console.error(e);
14774
+ error(e.message);
14584
14775
  }
14585
- const decimalPhase = `${normalizedBase}.${existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1}`;
14586
- const dirName = `${decimalPhase}-${slug}`;
14587
- const dirPath = node_path.default.join(cwd, ".planning", "phases", dirName);
14588
- node_fs.default.mkdirSync(dirPath, { recursive: true });
14589
- node_fs.default.writeFileSync(node_path.default.join(dirPath, ".gitkeep"), "");
14590
- const phaseEntry = `\n### Phase ${decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /maxsim:plan-phase ${decimalPhase} to break down)\n`;
14591
- const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, "i");
14592
- const headerMatch = content.match(headerPattern);
14593
- if (!headerMatch) error(`Could not find Phase ${afterPhase} header`);
14594
- const headerIdx = content.indexOf(headerMatch[0]);
14595
- const nextPhaseMatch = content.slice(headerIdx + headerMatch[0].length).match(/\n#{2,4}\s+Phase\s+\d/i);
14596
- let insertIdx;
14597
- if (nextPhaseMatch) insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
14598
- else insertIdx = content.length;
14599
- const updatedContent = content.slice(0, insertIdx) + phaseEntry + content.slice(insertIdx);
14600
- node_fs.default.writeFileSync(roadmapPath, updatedContent, "utf-8");
14601
- output({
14602
- phase_number: decimalPhase,
14603
- after_phase: afterPhase,
14604
- name: description,
14605
- slug,
14606
- directory: `.planning/phases/${dirName}`
14607
- }, raw, decimalPhase);
14608
14776
  }
14609
14777
  function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14610
14778
  if (!targetPhase) error("phase number required for phase remove");
14611
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
14612
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
14779
+ const rmPath = roadmapPath(cwd);
14780
+ const phasesDirPath = phasesPath(cwd);
14613
14781
  const force = options.force || false;
14614
- if (!node_fs.default.existsSync(roadmapPath)) error("ROADMAP.md not found");
14782
+ if (!node_fs.default.existsSync(rmPath)) error("ROADMAP.md not found");
14615
14783
  const normalized = normalizePhaseName(targetPhase);
14616
14784
  const isDecimal = targetPhase.includes(".");
14617
14785
  let targetDir = null;
14618
14786
  try {
14619
- targetDir = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => comparePhaseNum(a, b)).find((d) => d.startsWith(normalized + "-") || d === normalized) || null;
14787
+ targetDir = listSubDirs(phasesDirPath, true).find((d) => d.startsWith(normalized + "-") || d === normalized) || null;
14620
14788
  } catch (e) {
14621
- if (process.env.MAXSIM_DEBUG) console.error(e);
14789
+ debugLog(e);
14622
14790
  }
14623
14791
  if (targetDir && !force) {
14624
- const targetPath = node_path.default.join(phasesDir, targetDir);
14625
- const summaries = node_fs.default.readdirSync(targetPath).filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md");
14792
+ const targetPath = node_path.default.join(phasesDirPath, targetDir);
14793
+ const summaries = node_fs.default.readdirSync(targetPath).filter(isSummaryFile);
14626
14794
  if (summaries.length > 0) error(`Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`);
14627
14795
  }
14628
- if (targetDir) node_fs.default.rmSync(node_path.default.join(phasesDir, targetDir), {
14796
+ if (targetDir) node_fs.default.rmSync(node_path.default.join(phasesDirPath, targetDir), {
14629
14797
  recursive: true,
14630
14798
  force: true
14631
14799
  });
@@ -14636,7 +14804,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14636
14804
  const baseInt = baseParts[0];
14637
14805
  const removedDecimal = parseInt(baseParts[1], 10);
14638
14806
  try {
14639
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => comparePhaseNum(a, b));
14807
+ const dirs = listSubDirs(phasesDirPath, true);
14640
14808
  const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
14641
14809
  const toRename = [];
14642
14810
  for (const dir of dirs) {
@@ -14653,15 +14821,15 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14653
14821
  const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
14654
14822
  const newPhaseId = `${baseInt}.${newDecimal}`;
14655
14823
  const newDirName = `${baseInt}.${newDecimal}-${item.slug}`;
14656
- node_fs.default.renameSync(node_path.default.join(phasesDir, item.dir), node_path.default.join(phasesDir, newDirName));
14824
+ node_fs.default.renameSync(node_path.default.join(phasesDirPath, item.dir), node_path.default.join(phasesDirPath, newDirName));
14657
14825
  renamedDirs.push({
14658
14826
  from: item.dir,
14659
14827
  to: newDirName
14660
14828
  });
14661
- const dirFiles = node_fs.default.readdirSync(node_path.default.join(phasesDir, newDirName));
14829
+ const dirFiles = node_fs.default.readdirSync(node_path.default.join(phasesDirPath, newDirName));
14662
14830
  for (const f of dirFiles) if (f.includes(oldPhaseId)) {
14663
14831
  const newFileName = f.replace(oldPhaseId, newPhaseId);
14664
- node_fs.default.renameSync(node_path.default.join(phasesDir, newDirName, f), node_path.default.join(phasesDir, newDirName, newFileName));
14832
+ node_fs.default.renameSync(node_path.default.join(phasesDirPath, newDirName, f), node_path.default.join(phasesDirPath, newDirName, newFileName));
14665
14833
  renamedFiles.push({
14666
14834
  from: f,
14667
14835
  to: newFileName
@@ -14669,12 +14837,12 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14669
14837
  }
14670
14838
  }
14671
14839
  } catch (e) {
14672
- if (process.env.MAXSIM_DEBUG) console.error(e);
14840
+ debugLog(e);
14673
14841
  }
14674
14842
  } else {
14675
14843
  const removedInt = parseInt(normalized, 10);
14676
14844
  try {
14677
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => comparePhaseNum(a, b));
14845
+ const dirs = listSubDirs(phasesDirPath, true);
14678
14846
  const toRename = [];
14679
14847
  for (const dir of dirs) {
14680
14848
  const dm = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
@@ -14701,15 +14869,15 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14701
14869
  const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
14702
14870
  const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
14703
14871
  const newDirName = `${newPrefix}-${item.slug}`;
14704
- node_fs.default.renameSync(node_path.default.join(phasesDir, item.dir), node_path.default.join(phasesDir, newDirName));
14872
+ node_fs.default.renameSync(node_path.default.join(phasesDirPath, item.dir), node_path.default.join(phasesDirPath, newDirName));
14705
14873
  renamedDirs.push({
14706
14874
  from: item.dir,
14707
14875
  to: newDirName
14708
14876
  });
14709
- const dirFiles = node_fs.default.readdirSync(node_path.default.join(phasesDir, newDirName));
14877
+ const dirFiles = node_fs.default.readdirSync(node_path.default.join(phasesDirPath, newDirName));
14710
14878
  for (const f of dirFiles) if (f.startsWith(oldPrefix)) {
14711
14879
  const newFileName = newPrefix + f.slice(oldPrefix.length);
14712
- node_fs.default.renameSync(node_path.default.join(phasesDir, newDirName, f), node_path.default.join(phasesDir, newDirName, newFileName));
14880
+ node_fs.default.renameSync(node_path.default.join(phasesDirPath, newDirName, f), node_path.default.join(phasesDirPath, newDirName, newFileName));
14713
14881
  renamedFiles.push({
14714
14882
  from: f,
14715
14883
  to: newFileName
@@ -14717,11 +14885,11 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14717
14885
  }
14718
14886
  }
14719
14887
  } catch (e) {
14720
- if (process.env.MAXSIM_DEBUG) console.error(e);
14888
+ debugLog(e);
14721
14889
  }
14722
14890
  }
14723
- let roadmapContent = node_fs.default.readFileSync(roadmapPath, "utf-8");
14724
- const targetEscaped = targetPhase.replace(/\./g, "\\.");
14891
+ let roadmapContent = node_fs.default.readFileSync(rmPath, "utf-8");
14892
+ const targetEscaped = escapePhaseNum(targetPhase);
14725
14893
  const sectionPattern = new RegExp(`\\n?#{2,4}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`, "i");
14726
14894
  roadmapContent = roadmapContent.replace(sectionPattern, "");
14727
14895
  const checkboxPattern = new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${targetEscaped}[:\\s][^\\n]*`, "gi");
@@ -14743,10 +14911,10 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14743
14911
  roadmapContent = roadmapContent.replace(new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, "gi"), `$1${newStr}`);
14744
14912
  }
14745
14913
  }
14746
- node_fs.default.writeFileSync(roadmapPath, roadmapContent, "utf-8");
14747
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
14748
- if (node_fs.default.existsSync(statePath)) {
14749
- let stateContent = node_fs.default.readFileSync(statePath, "utf-8");
14914
+ node_fs.default.writeFileSync(rmPath, roadmapContent, "utf-8");
14915
+ const stPath = statePath(cwd);
14916
+ if (node_fs.default.existsSync(stPath)) {
14917
+ let stateContent = node_fs.default.readFileSync(stPath, "utf-8");
14750
14918
  const totalPattern = /(\*\*Total Phases:\*\*\s*)(\d+)/;
14751
14919
  const totalMatch = stateContent.match(totalPattern);
14752
14920
  if (totalMatch) {
@@ -14759,7 +14927,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14759
14927
  const oldTotal = parseInt(ofMatch[2], 10);
14760
14928
  stateContent = stateContent.replace(ofPattern, `$1${oldTotal - 1}$3`);
14761
14929
  }
14762
- node_fs.default.writeFileSync(statePath, stateContent, "utf-8");
14930
+ node_fs.default.writeFileSync(stPath, stateContent, "utf-8");
14763
14931
  }
14764
14932
  output({
14765
14933
  removed: targetPhase,
@@ -14767,84 +14935,27 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14767
14935
  renamed_directories: renamedDirs,
14768
14936
  renamed_files: renamedFiles,
14769
14937
  roadmap_updated: true,
14770
- state_updated: node_fs.default.existsSync(statePath)
14938
+ state_updated: node_fs.default.existsSync(stPath)
14771
14939
  }, raw);
14772
14940
  }
14773
14941
  function cmdPhaseComplete(cwd, phaseNum, raw) {
14774
14942
  if (!phaseNum) error("phase number required for phase complete");
14775
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
14776
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
14777
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
14778
- normalizePhaseName(phaseNum);
14779
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
14780
- const phaseInfo = findPhaseInternal(cwd, phaseNum);
14781
- if (!phaseInfo) error(`Phase ${phaseNum} not found`);
14782
- const planCount = phaseInfo.plans.length;
14783
- const summaryCount = phaseInfo.summaries.length;
14784
- if (node_fs.default.existsSync(roadmapPath)) {
14785
- let roadmapContent = node_fs.default.readFileSync(roadmapPath, "utf-8");
14786
- const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseNum.replace(".", "\\.")}[:\\s][^\\n]*)`, "i");
14787
- roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
14788
- const phaseEscaped = phaseNum.replace(".", "\\.");
14789
- const tablePattern = new RegExp(`(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`, "i");
14790
- roadmapContent = roadmapContent.replace(tablePattern, `$1 Complete $2 ${today} $3`);
14791
- const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`, "i");
14792
- roadmapContent = roadmapContent.replace(planCountPattern, `$1${summaryCount}/${planCount} plans complete`);
14793
- node_fs.default.writeFileSync(roadmapPath, roadmapContent, "utf-8");
14794
- const reqPath = node_path.default.join(cwd, ".planning", "REQUIREMENTS.md");
14795
- if (node_fs.default.existsSync(reqPath)) {
14796
- const reqMatch = roadmapContent.match(new RegExp(`Phase\\s+${phaseNum.replace(".", "\\.")}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, "i"));
14797
- if (reqMatch) {
14798
- const reqIds = reqMatch[1].replace(/[\[\]]/g, "").split(/[,\s]+/).map((r) => r.trim()).filter(Boolean);
14799
- let reqContent = node_fs.default.readFileSync(reqPath, "utf-8");
14800
- for (const reqId of reqIds) {
14801
- reqContent = reqContent.replace(new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqId}\\*\\*)`, "gi"), "$1x$2");
14802
- reqContent = reqContent.replace(new RegExp(`(\\|\\s*${reqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, "gi"), "$1 Complete $2");
14803
- }
14804
- node_fs.default.writeFileSync(reqPath, reqContent, "utf-8");
14805
- }
14806
- }
14807
- }
14808
- let nextPhaseNum = null;
14809
- let nextPhaseName = null;
14810
- let isLastPhase = true;
14811
14943
  try {
14812
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort((a, b) => comparePhaseNum(a, b));
14813
- for (const dir of dirs) {
14814
- const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
14815
- if (dm) {
14816
- if (comparePhaseNum(dm[1], phaseNum) > 0) {
14817
- nextPhaseNum = dm[1];
14818
- nextPhaseName = dm[2] || null;
14819
- isLastPhase = false;
14820
- break;
14821
- }
14822
- }
14823
- }
14944
+ const result = phaseCompleteCore(cwd, phaseNum);
14945
+ output({
14946
+ completed_phase: result.completed_phase,
14947
+ phase_name: result.phase_name,
14948
+ plans_executed: result.plans_executed,
14949
+ next_phase: result.next_phase,
14950
+ next_phase_name: result.next_phase_name,
14951
+ is_last_phase: result.is_last_phase,
14952
+ date: result.date,
14953
+ roadmap_updated: result.roadmap_updated,
14954
+ state_updated: result.state_updated
14955
+ }, raw);
14824
14956
  } catch (e) {
14825
- if (process.env.MAXSIM_DEBUG) console.error(e);
14826
- }
14827
- if (node_fs.default.existsSync(statePath)) {
14828
- let stateContent = node_fs.default.readFileSync(statePath, "utf-8");
14829
- stateContent = stateContent.replace(/(\*\*Current Phase:\*\*\s*).*/, `$1${nextPhaseNum || phaseNum}`);
14830
- if (nextPhaseName) stateContent = stateContent.replace(/(\*\*Current Phase Name:\*\*\s*).*/, `$1${nextPhaseName.replace(/-/g, " ")}`);
14831
- stateContent = stateContent.replace(/(\*\*Status:\*\*\s*).*/, `$1${isLastPhase ? "Milestone complete" : "Ready to plan"}`);
14832
- stateContent = stateContent.replace(/(\*\*Current Plan:\*\*\s*).*/, `$1Not started`);
14833
- stateContent = stateContent.replace(/(\*\*Last Activity:\*\*\s*).*/, `$1${today}`);
14834
- stateContent = stateContent.replace(/(\*\*Last Activity Description:\*\*\s*).*/, `$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ""}`);
14835
- node_fs.default.writeFileSync(statePath, stateContent, "utf-8");
14957
+ error(e.message);
14836
14958
  }
14837
- output({
14838
- completed_phase: phaseNum,
14839
- phase_name: phaseInfo.phase_name,
14840
- plans_executed: `${summaryCount}/${planCount}`,
14841
- next_phase: nextPhaseNum,
14842
- next_phase_name: nextPhaseName,
14843
- is_last_phase: isLastPhase,
14844
- date: today,
14845
- roadmap_updated: node_fs.default.existsSync(roadmapPath),
14846
- state_updated: node_fs.default.existsSync(statePath)
14847
- }, raw);
14848
14959
  }
14849
14960
 
14850
14961
  //#endregion
@@ -14902,7 +15013,7 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
14902
15013
  return;
14903
15014
  }
14904
15015
  const padded = normalizePhaseName(options.phase);
14905
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
15016
+ const today = todayISO();
14906
15017
  const phaseName = options.name || phaseInfo.phase_name || "Unnamed";
14907
15018
  const phaseId = `${padded}-${phaseInfo.phase_slug || generateSlugInternal(phaseName)}`;
14908
15019
  const planNum = (options.plan || "01").padStart(2, "0");
@@ -15094,7 +15205,7 @@ function scanPhaseArtifacts(cwd, phaseDirectory) {
15094
15205
  const uatFile = files.find((f) => f.endsWith("-UAT.md") || f === "UAT.md");
15095
15206
  if (uatFile) result.uat_path = node_path.default.join(phaseDirectory, uatFile);
15096
15207
  } catch (e) {
15097
- if (process.env.MAXSIM_DEBUG) console.error(e);
15208
+ debugLog(e);
15098
15209
  }
15099
15210
  return result;
15100
15211
  }
@@ -15193,7 +15304,7 @@ function cmdInitNewProject(cwd, raw) {
15193
15304
  ]
15194
15305
  }).trim().length > 0;
15195
15306
  } catch (e) {
15196
- if (process.env.MAXSIM_DEBUG) console.error(e);
15307
+ debugLog(e);
15197
15308
  }
15198
15309
  hasPackageFile = pathExistsInternal(cwd, "package.json") || pathExistsInternal(cwd, "requirements.txt") || pathExistsInternal(cwd, "Cargo.toml") || pathExistsInternal(cwd, "go.mod") || pathExistsInternal(cwd, "Package.swift");
15199
15310
  output({
@@ -15236,13 +15347,13 @@ function cmdInitQuick(cwd, description, raw) {
15236
15347
  const config = loadConfig(cwd);
15237
15348
  const now = /* @__PURE__ */ new Date();
15238
15349
  const slug = description ? generateSlugInternal(description)?.substring(0, 40) ?? null : null;
15239
- const quickDir = node_path.default.join(cwd, ".planning", "quick");
15350
+ const quickDir = planningPath(cwd, "quick");
15240
15351
  let nextNum = 1;
15241
15352
  try {
15242
15353
  const existing = node_fs.default.readdirSync(quickDir).filter((f) => /^\d+-/.test(f)).map((f) => parseInt(f.split("-")[0], 10)).filter((n) => !isNaN(n));
15243
15354
  if (existing.length > 0) nextNum = Math.max(...existing) + 1;
15244
15355
  } catch (e) {
15245
- if (process.env.MAXSIM_DEBUG) console.error(e);
15356
+ debugLog(e);
15246
15357
  }
15247
15358
  output({
15248
15359
  planner_model: resolveModelInternal(cwd, "maxsim-planner"),
@@ -15253,7 +15364,7 @@ function cmdInitQuick(cwd, description, raw) {
15253
15364
  next_num: nextNum,
15254
15365
  slug,
15255
15366
  description: description ?? null,
15256
- date: now.toISOString().split("T")[0],
15367
+ date: todayISO(),
15257
15368
  timestamp: now.toISOString(),
15258
15369
  quick_dir: ".planning/quick",
15259
15370
  task_dir: slug ? `.planning/quick/${nextNum}-${slug}` : null,
@@ -15265,9 +15376,9 @@ function cmdInitResume(cwd, raw) {
15265
15376
  const config = loadConfig(cwd);
15266
15377
  let interruptedAgentId = null;
15267
15378
  try {
15268
- interruptedAgentId = node_fs.default.readFileSync(node_path.default.join(cwd, ".planning", "current-agent-id.txt"), "utf-8").trim();
15379
+ interruptedAgentId = node_fs.default.readFileSync(planningPath(cwd, "current-agent-id.txt"), "utf-8").trim();
15269
15380
  } catch (e) {
15270
- if (process.env.MAXSIM_DEBUG) console.error(e);
15381
+ debugLog(e);
15271
15382
  }
15272
15383
  output({
15273
15384
  state_exists: pathExistsInternal(cwd, ".planning/STATE.md"),
@@ -15351,7 +15462,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
15351
15462
  function cmdInitTodos(cwd, area, raw) {
15352
15463
  const config = loadConfig(cwd);
15353
15464
  const now = /* @__PURE__ */ new Date();
15354
- const pendingDir = node_path.default.join(cwd, ".planning", "todos", "pending");
15465
+ const pendingDir = planningPath(cwd, "todos", "pending");
15355
15466
  let count = 0;
15356
15467
  const todos = [];
15357
15468
  try {
@@ -15372,14 +15483,14 @@ function cmdInitTodos(cwd, area, raw) {
15372
15483
  path: node_path.default.join(".planning", "todos", "pending", file)
15373
15484
  });
15374
15485
  } catch (e) {
15375
- if (process.env.MAXSIM_DEBUG) console.error(e);
15486
+ debugLog(e);
15376
15487
  }
15377
15488
  } catch (e) {
15378
- if (process.env.MAXSIM_DEBUG) console.error(e);
15489
+ debugLog(e);
15379
15490
  }
15380
15491
  output({
15381
15492
  commit_docs: config.commit_docs,
15382
- date: now.toISOString().split("T")[0],
15493
+ date: todayISO(),
15383
15494
  timestamp: now.toISOString(),
15384
15495
  todo_count: count,
15385
15496
  todos,
@@ -15396,24 +15507,24 @@ function cmdInitMilestoneOp(cwd, raw) {
15396
15507
  const milestone = getMilestoneInfo(cwd);
15397
15508
  let phaseCount = 0;
15398
15509
  let completedPhases = 0;
15399
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
15510
+ const phasesDir = phasesPath(cwd);
15400
15511
  try {
15401
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
15512
+ const dirs = listSubDirs(phasesDir);
15402
15513
  phaseCount = dirs.length;
15403
15514
  for (const dir of dirs) try {
15404
- if (node_fs.default.readdirSync(node_path.default.join(phasesDir, dir)).some((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md")) completedPhases++;
15515
+ if (node_fs.default.readdirSync(node_path.default.join(phasesDir, dir)).some((f) => isSummaryFile(f))) completedPhases++;
15405
15516
  } catch (e) {
15406
- if (process.env.MAXSIM_DEBUG) console.error(e);
15517
+ debugLog(e);
15407
15518
  }
15408
15519
  } catch (e) {
15409
- if (process.env.MAXSIM_DEBUG) console.error(e);
15520
+ debugLog(e);
15410
15521
  }
15411
- const archiveDir = node_path.default.join(cwd, ".planning", "archive");
15522
+ const archiveDir = planningPath(cwd, "archive");
15412
15523
  let archivedMilestones = [];
15413
15524
  try {
15414
- archivedMilestones = node_fs.default.readdirSync(archiveDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
15525
+ archivedMilestones = listSubDirs(archiveDir);
15415
15526
  } catch (e) {
15416
- if (process.env.MAXSIM_DEBUG) console.error(e);
15527
+ debugLog(e);
15417
15528
  }
15418
15529
  output({
15419
15530
  commit_docs: config.commit_docs,
@@ -15434,12 +15545,12 @@ function cmdInitMilestoneOp(cwd, raw) {
15434
15545
  }
15435
15546
  function cmdInitMapCodebase(cwd, raw) {
15436
15547
  const config = loadConfig(cwd);
15437
- const codebaseDir = node_path.default.join(cwd, ".planning", "codebase");
15548
+ const codebaseDir = planningPath(cwd, "codebase");
15438
15549
  let existingMaps = [];
15439
15550
  try {
15440
15551
  existingMaps = node_fs.default.readdirSync(codebaseDir).filter((f) => f.endsWith(".md"));
15441
15552
  } catch (e) {
15442
- if (process.env.MAXSIM_DEBUG) console.error(e);
15553
+ debugLog(e);
15443
15554
  }
15444
15555
  output({
15445
15556
  mapper_model: resolveModelInternal(cwd, "maxsim-codebase-mapper"),
@@ -15471,15 +15582,15 @@ function cmdInitExisting(cwd, raw) {
15471
15582
  ]
15472
15583
  }).trim().length > 0;
15473
15584
  } catch (e) {
15474
- if (process.env.MAXSIM_DEBUG) console.error(e);
15585
+ debugLog(e);
15475
15586
  }
15476
15587
  hasPackageFile = pathExistsInternal(cwd, "package.json") || pathExistsInternal(cwd, "requirements.txt") || pathExistsInternal(cwd, "Cargo.toml") || pathExistsInternal(cwd, "go.mod") || pathExistsInternal(cwd, "Package.swift");
15477
15588
  let planningFiles = [];
15478
15589
  try {
15479
- const planDir = node_path.default.join(cwd, ".planning");
15590
+ const planDir = planningPath(cwd);
15480
15591
  if (node_fs.default.existsSync(planDir)) planningFiles = node_fs.default.readdirSync(planDir, { recursive: true }).map((f) => String(f)).filter((f) => !f.startsWith("."));
15481
15592
  } catch (e) {
15482
- if (process.env.MAXSIM_DEBUG) console.error(e);
15593
+ debugLog(e);
15483
15594
  }
15484
15595
  output({
15485
15596
  researcher_model: resolveModelInternal(cwd, "maxsim-project-researcher"),
@@ -15506,20 +15617,20 @@ function cmdInitExisting(cwd, raw) {
15506
15617
  function cmdInitProgress(cwd, raw) {
15507
15618
  const config = loadConfig(cwd);
15508
15619
  const milestone = getMilestoneInfo(cwd);
15509
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
15620
+ const progressPhasesDir = phasesPath(cwd);
15510
15621
  const phases = [];
15511
15622
  let currentPhase = null;
15512
15623
  let nextPhase = null;
15513
15624
  try {
15514
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
15625
+ const dirs = listSubDirs(progressPhasesDir, true);
15515
15626
  for (const dir of dirs) {
15516
15627
  const match = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
15517
15628
  const phaseNumber = match ? match[1] : dir;
15518
15629
  const phaseName = match && match[2] ? match[2] : null;
15519
- const phasePath = node_path.default.join(phasesDir, dir);
15520
- const phaseFiles = node_fs.default.readdirSync(phasePath);
15521
- const plans = phaseFiles.filter((f) => f.endsWith("-PLAN.md") || f === "PLAN.md");
15522
- const summaries = phaseFiles.filter((f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md");
15630
+ const phaseDirPath = node_path.default.join(progressPhasesDir, dir);
15631
+ const phaseFiles = node_fs.default.readdirSync(phaseDirPath);
15632
+ const plans = phaseFiles.filter((f) => isPlanFile(f));
15633
+ const summaries = phaseFiles.filter((f) => isSummaryFile(f));
15523
15634
  const hasResearch = phaseFiles.some((f) => f.endsWith("-RESEARCH.md") || f === "RESEARCH.md");
15524
15635
  const status = summaries.length >= plans.length && plans.length > 0 ? "complete" : plans.length > 0 ? "in_progress" : hasResearch ? "researched" : "pending";
15525
15636
  const phaseInfo = {
@@ -15536,14 +15647,14 @@ function cmdInitProgress(cwd, raw) {
15536
15647
  if (!nextPhase && status === "pending") nextPhase = phaseInfo;
15537
15648
  }
15538
15649
  } catch (e) {
15539
- if (process.env.MAXSIM_DEBUG) console.error(e);
15650
+ debugLog(e);
15540
15651
  }
15541
15652
  let pausedAt = null;
15542
15653
  try {
15543
- const pauseMatch = node_fs.default.readFileSync(node_path.default.join(cwd, ".planning", "STATE.md"), "utf-8").match(/\*\*Paused At:\*\*\s*(.+)/);
15654
+ const pauseMatch = node_fs.default.readFileSync(planningPath(cwd, "STATE.md"), "utf-8").match(/\*\*Paused At:\*\*\s*(.+)/);
15544
15655
  if (pauseMatch) pausedAt = pauseMatch[1].trim();
15545
15656
  } catch (e) {
15546
- if (process.env.MAXSIM_DEBUG) console.error(e);
15657
+ debugLog(e);
15547
15658
  }
15548
15659
  output({
15549
15660
  executor_model: resolveModelInternal(cwd, "maxsim-executor"),
@@ -15828,7 +15939,11 @@ const COMMANDS = {
15828
15939
  freshness: f.freshness ?? void 0
15829
15940
  }, raw);
15830
15941
  },
15831
- "dashboard": (args) => handleDashboard(args.slice(1))
15942
+ "dashboard": (args) => handleDashboard(args.slice(1)),
15943
+ "start-server": async () => {
15944
+ const serverPath = node_path.join(__dirname, "mcp-server.cjs");
15945
+ (0, node_child_process.spawn)(process.execPath, [serverPath], { stdio: "inherit" }).on("exit", (code) => process.exit(code ?? 0));
15946
+ }
15832
15947
  };
15833
15948
  async function main() {
15834
15949
  const args = process.argv.slice(2);