maxsimcli 3.10.3 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/assets/CHANGELOG.md +26 -0
  3. package/dist/assets/dashboard/client/assets/index-CxFKStBk.css +32 -0
  4. package/dist/assets/dashboard/client/assets/{index-CZ8WC97G.js → index-wtQDvXzr.js} +64 -64
  5. package/dist/assets/dashboard/client/index.html +2 -2
  6. package/dist/assets/dashboard/server.js +5 -1
  7. package/dist/assets/templates/agents/AGENTS.md +82 -0
  8. package/dist/assets/templates/commands/maxsim/settings.md +1 -1
  9. package/dist/assets/templates/skills/code-review/SKILL.md +151 -0
  10. package/dist/assets/templates/skills/memory-management/SKILL.md +174 -0
  11. package/dist/assets/templates/skills/simplify/SKILL.md +137 -0
  12. package/dist/assets/templates/skills/using-maxsim/SKILL.md +115 -0
  13. package/dist/assets/templates/templates/config.json +1 -1
  14. package/dist/assets/templates/workflows/add-tests.md +3 -3
  15. package/dist/assets/templates/workflows/complete-milestone.md +1 -1
  16. package/dist/assets/templates/workflows/execute-phase.md +4 -14
  17. package/dist/assets/templates/workflows/execute-plan.md +10 -0
  18. package/dist/assets/templates/workflows/init-existing.md +7 -3
  19. package/dist/assets/templates/workflows/new-milestone.md +4 -0
  20. package/dist/assets/templates/workflows/new-project.md +6 -2
  21. package/dist/assets/templates/workflows/plan-phase.md +2 -2
  22. package/dist/assets/templates/workflows/settings.md +8 -4
  23. package/dist/assets/templates/workflows/verify-work.md +1 -1
  24. package/dist/cli.cjs +818 -599
  25. package/dist/cli.cjs.map +1 -1
  26. package/dist/cli.js +78 -204
  27. package/dist/cli.js.map +1 -1
  28. package/dist/core/commands.d.ts +7 -0
  29. package/dist/core/commands.d.ts.map +1 -1
  30. package/dist/core/commands.js +40 -35
  31. package/dist/core/commands.js.map +1 -1
  32. package/dist/core/core.d.ts +39 -1
  33. package/dist/core/core.d.ts.map +1 -1
  34. package/dist/core/core.js +122 -47
  35. package/dist/core/core.js.map +1 -1
  36. package/dist/core/dashboard-launcher.d.ts +56 -0
  37. package/dist/core/dashboard-launcher.d.ts.map +1 -0
  38. package/dist/core/dashboard-launcher.js +243 -0
  39. package/dist/core/dashboard-launcher.js.map +1 -0
  40. package/dist/core/index.d.ts +4 -2
  41. package/dist/core/index.d.ts.map +1 -1
  42. package/dist/core/index.js +20 -2
  43. package/dist/core/index.js.map +1 -1
  44. package/dist/core/init.d.ts +2 -3
  45. package/dist/core/init.d.ts.map +1 -1
  46. package/dist/core/init.js +33 -52
  47. package/dist/core/init.js.map +1 -1
  48. package/dist/core/milestone.d.ts.map +1 -1
  49. package/dist/core/milestone.js +15 -20
  50. package/dist/core/milestone.js.map +1 -1
  51. package/dist/core/phase.d.ts +33 -0
  52. package/dist/core/phase.d.ts.map +1 -1
  53. package/dist/core/phase.js +282 -225
  54. package/dist/core/phase.js.map +1 -1
  55. package/dist/core/roadmap.d.ts.map +1 -1
  56. package/dist/core/roadmap.js +17 -18
  57. package/dist/core/roadmap.js.map +1 -1
  58. package/dist/core/state.d.ts +5 -0
  59. package/dist/core/state.d.ts.map +1 -1
  60. package/dist/core/state.js +51 -42
  61. package/dist/core/state.js.map +1 -1
  62. package/dist/core/template.d.ts.map +1 -1
  63. package/dist/core/template.js +1 -1
  64. package/dist/core/template.js.map +1 -1
  65. package/dist/core/types.d.ts +4 -4
  66. package/dist/core/types.d.ts.map +1 -1
  67. package/dist/core/types.js +1 -2
  68. package/dist/core/types.js.map +1 -1
  69. package/dist/core/verify.d.ts.map +1 -1
  70. package/dist/core/verify.js +61 -80
  71. package/dist/core/verify.js.map +1 -1
  72. package/dist/install/adapters.d.ts +15 -0
  73. package/dist/install/adapters.d.ts.map +1 -0
  74. package/dist/install/adapters.js +203 -0
  75. package/dist/install/adapters.js.map +1 -0
  76. package/dist/install/copy.d.ts +15 -0
  77. package/dist/install/copy.d.ts.map +1 -0
  78. package/dist/install/copy.js +191 -0
  79. package/dist/install/copy.js.map +1 -0
  80. package/dist/install/dashboard.d.ts +16 -0
  81. package/dist/install/dashboard.d.ts.map +1 -0
  82. package/dist/install/dashboard.js +273 -0
  83. package/dist/install/dashboard.js.map +1 -0
  84. package/dist/install/hooks.d.ts +32 -0
  85. package/dist/install/hooks.d.ts.map +1 -0
  86. package/dist/install/hooks.js +285 -0
  87. package/dist/install/hooks.js.map +1 -0
  88. package/dist/install/index.d.ts +2 -0
  89. package/dist/install/index.d.ts.map +1 -0
  90. package/dist/install/index.js +598 -0
  91. package/dist/install/index.js.map +1 -0
  92. package/dist/install/manifest.d.ts +20 -0
  93. package/dist/install/manifest.d.ts.map +1 -0
  94. package/dist/install/manifest.js +135 -0
  95. package/dist/install/manifest.js.map +1 -0
  96. package/dist/install/patches.d.ts +11 -0
  97. package/dist/install/patches.d.ts.map +1 -0
  98. package/dist/install/patches.js +136 -0
  99. package/dist/install/patches.js.map +1 -0
  100. package/dist/install/shared.d.ts +50 -0
  101. package/dist/install/shared.d.ts.map +1 -0
  102. package/dist/install/shared.js +142 -0
  103. package/dist/install/shared.js.map +1 -0
  104. package/dist/install/uninstall.d.ts +6 -0
  105. package/dist/install/uninstall.d.ts.map +1 -0
  106. package/dist/install/uninstall.js +280 -0
  107. package/dist/install/uninstall.js.map +1 -0
  108. package/dist/install.cjs +782 -705
  109. package/dist/install.cjs.map +1 -1
  110. package/dist/mcp/index.d.ts +12 -0
  111. package/dist/mcp/index.d.ts.map +1 -0
  112. package/dist/mcp/index.js +21 -0
  113. package/dist/mcp/index.js.map +1 -0
  114. package/dist/mcp/phase-tools.d.ts +13 -0
  115. package/dist/mcp/phase-tools.d.ts.map +1 -0
  116. package/dist/mcp/phase-tools.js +164 -0
  117. package/dist/mcp/phase-tools.js.map +1 -0
  118. package/dist/mcp/state-tools.d.ts +13 -0
  119. package/dist/mcp/state-tools.d.ts.map +1 -0
  120. package/dist/mcp/state-tools.js +185 -0
  121. package/dist/mcp/state-tools.js.map +1 -0
  122. package/dist/mcp/todo-tools.d.ts +13 -0
  123. package/dist/mcp/todo-tools.d.ts.map +1 -0
  124. package/dist/mcp/todo-tools.js +143 -0
  125. package/dist/mcp/todo-tools.js.map +1 -0
  126. package/dist/mcp/utils.d.ts +27 -0
  127. package/dist/mcp/utils.d.ts.map +1 -0
  128. package/dist/mcp/utils.js +82 -0
  129. package/dist/mcp/utils.js.map +1 -0
  130. package/dist/mcp-server.cjs +11806 -0
  131. package/dist/mcp-server.cjs.map +1 -0
  132. package/dist/mcp-server.d.cts +2 -0
  133. package/dist/mcp-server.d.ts +12 -0
  134. package/dist/mcp-server.d.ts.map +1 -0
  135. package/dist/mcp-server.js +31 -0
  136. package/dist/mcp-server.js.map +1 -0
  137. package/package.json +5 -3
  138. package/dist/assets/dashboard/client/assets/index-DzJChB-D.css +0 -32
  139. package/dist/install.d.ts +0 -2
  140. package/dist/install.d.ts.map +0 -1
  141. package/dist/install.js +0 -1804
  142. package/dist/install.js.map +0 -1
package/dist/cli.cjs CHANGED
@@ -31,10 +31,9 @@ let node_fs = require("node:fs");
31
31
  node_fs = __toESM(node_fs);
32
32
  let node_path = require("node:path");
33
33
  node_path = __toESM(node_path);
34
+ let node_child_process = require("node:child_process");
34
35
  let node_os = require("node:os");
35
36
  node_os = __toESM(node_os);
36
- let node_child_process = require("node:child_process");
37
- let node_module = require("node:module");
38
37
  let node_buffer = require("node:buffer");
39
38
  let child_process = require("child_process");
40
39
  let node_events = require("node:events");
@@ -42,6 +41,7 @@ let node_process = require("node:process");
42
41
  node_process = __toESM(node_process);
43
42
  let node_tty = require("node:tty");
44
43
  node_tty = __toESM(node_tty);
44
+ let node_module = require("node:module");
45
45
 
46
46
  //#region src/core/types.ts
47
47
  const PLANNING_CONFIG_DEFAULTS = {
@@ -53,9 +53,8 @@ const PLANNING_CONFIG_DEFAULTS = {
53
53
  milestone_branch_template: "maxsim/{milestone}-{slug}",
54
54
  workflow: {
55
55
  research: true,
56
- plan_check: true,
57
- verifier: true,
58
- nyquist_validation: false
56
+ plan_checker: true,
57
+ verifier: true
59
58
  },
60
59
  parallelization: true,
61
60
  brave_search: false
@@ -594,7 +593,7 @@ var require_has_flag = /* @__PURE__ */ __commonJSMin(((exports, module) => {
594
593
  //#endregion
595
594
  //#region ../../node_modules/supports-color/index.js
596
595
  var require_supports_color = /* @__PURE__ */ __commonJSMin(((exports, module) => {
597
- const os$4 = require("os");
596
+ const os$5 = require("os");
598
597
  const tty$2 = require("tty");
599
598
  const hasFlag = require_has_flag();
600
599
  const { env } = process;
@@ -621,7 +620,7 @@ var require_supports_color = /* @__PURE__ */ __commonJSMin(((exports, module) =>
621
620
  const min = forceColor || 0;
622
621
  if (env.TERM === "dumb") return min;
623
622
  if (process.platform === "win32") {
624
- const osRelease = os$4.release().split(".");
623
+ const osRelease = os$5.release().split(".");
625
624
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
626
625
  return 1;
627
626
  }
@@ -4711,21 +4710,86 @@ const MODEL_PROFILES = {
4711
4710
  tokenburner: "opus"
4712
4711
  }
4713
4712
  };
4713
+ /** Thrown by output() to signal successful command completion. */
4714
+ var CliOutput = class {
4715
+ result;
4716
+ raw;
4717
+ rawValue;
4718
+ constructor(result, raw, rawValue) {
4719
+ this.result = result;
4720
+ this.raw = raw ?? false;
4721
+ this.rawValue = rawValue;
4722
+ }
4723
+ };
4724
+ /** Thrown by error() to signal a command error. */
4725
+ var CliError = class {
4726
+ message;
4727
+ constructor(message) {
4728
+ this.message = message;
4729
+ }
4730
+ };
4714
4731
  function output(result, raw, rawValue) {
4715
- if (raw && rawValue !== void 0) process.stdout.write(String(rawValue));
4732
+ throw new CliOutput(result, raw, rawValue);
4733
+ }
4734
+ function error(message) {
4735
+ throw new CliError(message);
4736
+ }
4737
+ /** Re-throw CliOutput/CliError signals so catch blocks don't intercept them */
4738
+ function rethrowCliSignals(e) {
4739
+ if (e instanceof CliOutput || e instanceof CliError) throw e;
4740
+ }
4741
+ /**
4742
+ * Handle a CliOutput by writing to stdout. Extracted so cli.ts can use it.
4743
+ */
4744
+ function writeOutput(out) {
4745
+ if (out.raw && out.rawValue !== void 0) process.stdout.write(String(out.rawValue));
4716
4746
  else {
4717
- const json = JSON.stringify(result, null, 2);
4747
+ const json = JSON.stringify(out.result, null, 2);
4718
4748
  if (json.length > 5e4) {
4719
4749
  const tmpPath = node_path.default.join(node_os.default.tmpdir(), `maxsim-${Date.now()}.json`);
4720
4750
  node_fs.default.writeFileSync(tmpPath, json, "utf-8");
4721
4751
  process.stdout.write("@file:" + tmpPath);
4722
4752
  } else process.stdout.write(json);
4723
4753
  }
4724
- process.exit(0);
4725
4754
  }
4726
- function error(message) {
4727
- process.stderr.write("Error: " + message + "\n");
4728
- process.exit(1);
4755
+ /** Today's date as YYYY-MM-DD. */
4756
+ function todayISO() {
4757
+ return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4758
+ }
4759
+ /** Canonical .planning/ sub-paths. */
4760
+ function planningPath(cwd, ...segments) {
4761
+ return node_path.default.join(cwd, ".planning", ...segments);
4762
+ }
4763
+ function statePath(cwd) {
4764
+ return planningPath(cwd, "STATE.md");
4765
+ }
4766
+ function roadmapPath(cwd) {
4767
+ return planningPath(cwd, "ROADMAP.md");
4768
+ }
4769
+ function configPath(cwd) {
4770
+ return planningPath(cwd, "config.json");
4771
+ }
4772
+ function phasesPath(cwd) {
4773
+ return planningPath(cwd, "phases");
4774
+ }
4775
+ /** Phase-file predicates. */
4776
+ const isPlanFile = (f) => f.endsWith("-PLAN.md") || f === "PLAN.md";
4777
+ const isSummaryFile = (f) => f.endsWith("-SUMMARY.md") || f === "SUMMARY.md";
4778
+ /** Strip suffix to get plan/summary ID. */
4779
+ const planId = (f) => f.replace("-PLAN.md", "").replace("PLAN.md", "");
4780
+ const summaryId = (f) => f.replace("-SUMMARY.md", "").replace("SUMMARY.md", "");
4781
+ /** List subdirectory names, optionally sorted by phase number. */
4782
+ function listSubDirs(dir, sortByPhase = false) {
4783
+ const dirs = node_fs.default.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
4784
+ return sortByPhase ? dirs.sort((a, b) => comparePhaseNum(a, b)) : dirs;
4785
+ }
4786
+ /** Log only when MAXSIM_DEBUG is set. */
4787
+ function debugLog(e) {
4788
+ if (process.env.MAXSIM_DEBUG) console.error(e);
4789
+ }
4790
+ /** Escape a phase number for use in regex. */
4791
+ function escapePhaseNum(phase) {
4792
+ return String(phase).replace(/\./g, "\\.");
4729
4793
  }
4730
4794
  function safeReadFile(filePath) {
4731
4795
  try {
@@ -4734,8 +4798,10 @@ function safeReadFile(filePath) {
4734
4798
  return null;
4735
4799
  }
4736
4800
  }
4801
+ let _configCache = null;
4737
4802
  function loadConfig(cwd) {
4738
- const configPath = node_path.default.join(cwd, ".planning", "config.json");
4803
+ if (_configCache && _configCache.cwd === cwd) return _configCache.config;
4804
+ const cfgPath = configPath(cwd);
4739
4805
  const defaults = {
4740
4806
  model_profile: "balanced",
4741
4807
  commit_docs: true,
@@ -4750,7 +4816,7 @@ function loadConfig(cwd) {
4750
4816
  brave_search: false
4751
4817
  };
4752
4818
  try {
4753
- const raw = node_fs.default.readFileSync(configPath, "utf-8");
4819
+ const raw = node_fs.default.readFileSync(cfgPath, "utf-8");
4754
4820
  const parsed = JSON.parse(raw);
4755
4821
  const get = (key, nested) => {
4756
4822
  if (parsed[key] !== void 0) return parsed[key];
@@ -4765,7 +4831,7 @@ function loadConfig(cwd) {
4765
4831
  if (typeof val === "object" && val !== null && "enabled" in val) return val.enabled;
4766
4832
  return defaults.parallelization;
4767
4833
  })();
4768
- return {
4834
+ const result = {
4769
4835
  model_profile: get("model_profile") ?? defaults.model_profile,
4770
4836
  commit_docs: get("commit_docs", {
4771
4837
  section: "planning",
@@ -4792,6 +4858,9 @@ function loadConfig(cwd) {
4792
4858
  field: "research"
4793
4859
  }) ?? defaults.research,
4794
4860
  plan_checker: get("plan_checker", {
4861
+ section: "workflow",
4862
+ field: "plan_checker"
4863
+ }) ?? get("plan_checker", {
4795
4864
  section: "workflow",
4796
4865
  field: "plan_check"
4797
4866
  }) ?? defaults.plan_checker,
@@ -4803,7 +4872,16 @@ function loadConfig(cwd) {
4803
4872
  brave_search: get("brave_search") ?? defaults.brave_search,
4804
4873
  model_overrides: parsed["model_overrides"]
4805
4874
  };
4875
+ _configCache = {
4876
+ cwd,
4877
+ config: result
4878
+ };
4879
+ return result;
4806
4880
  } catch {
4881
+ _configCache = {
4882
+ cwd,
4883
+ config: defaults
4884
+ };
4807
4885
  return defaults;
4808
4886
  }
4809
4887
  }
@@ -4873,23 +4951,20 @@ function getPhasePattern(escapedPhaseNum, flags = "gim") {
4873
4951
  }
4874
4952
  function searchPhaseInDir(baseDir, relBase, normalized) {
4875
4953
  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));
4954
+ const match = listSubDirs(baseDir, true).find((d) => d.startsWith(normalized));
4877
4955
  if (!match) return null;
4878
4956
  const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
4879
4957
  const phaseNumber = dirMatch ? dirMatch[1] : normalized;
4880
4958
  const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
4881
4959
  const phaseDir = node_path.default.join(baseDir, match);
4882
4960
  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();
4961
+ const plans = phaseFiles.filter(isPlanFile).sort();
4962
+ const summaries = phaseFiles.filter(isSummaryFile).sort();
4885
4963
  const hasResearch = phaseFiles.some((f) => f.endsWith("-RESEARCH.md") || f === "RESEARCH.md");
4886
4964
  const hasContext = phaseFiles.some((f) => f.endsWith("-CONTEXT.md") || f === "CONTEXT.md");
4887
4965
  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
- });
4966
+ const completedPlanIds = new Set(summaries.map(summaryId));
4967
+ const incompletePlans = plans.filter((p) => !completedPlanIds.has(planId(p)));
4893
4968
  return {
4894
4969
  found: true,
4895
4970
  directory: node_path.default.join(relBase, match),
@@ -4909,12 +4984,16 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
4909
4984
  }
4910
4985
  function findPhaseInternal(cwd, phase) {
4911
4986
  if (!phase) return null;
4912
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
4987
+ const pd = phasesPath(cwd);
4913
4988
  const normalized = normalizePhaseName(phase);
4914
- const current = searchPhaseInDir(phasesDir, node_path.default.join(".planning", "phases"), normalized);
4989
+ const current = searchPhaseInDir(pd, node_path.default.join(".planning", "phases"), normalized);
4915
4990
  if (current) return current;
4916
- const milestonesDir = node_path.default.join(cwd, ".planning", "milestones");
4917
- if (!node_fs.default.existsSync(milestonesDir)) return null;
4991
+ const milestonesDir = planningPath(cwd, "milestones");
4992
+ try {
4993
+ node_fs.default.statSync(milestonesDir);
4994
+ } catch {
4995
+ return null;
4996
+ }
4918
4997
  try {
4919
4998
  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
4999
  for (const archiveName of archiveDirs) {
@@ -4928,14 +5007,13 @@ function findPhaseInternal(cwd, phase) {
4928
5007
  }
4929
5008
  }
4930
5009
  } catch (e) {
4931
- if (process.env.MAXSIM_DEBUG) console.error(e);
5010
+ debugLog(e);
4932
5011
  }
4933
5012
  return null;
4934
5013
  }
4935
5014
  function getArchivedPhaseDirs(cwd) {
4936
- const milestonesDir = node_path.default.join(cwd, ".planning", "milestones");
5015
+ const milestonesDir = planningPath(cwd, "milestones");
4937
5016
  const results = [];
4938
- if (!node_fs.default.existsSync(milestonesDir)) return results;
4939
5017
  try {
4940
5018
  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
5019
  for (const archiveName of phaseDirs) {
@@ -4943,7 +5021,7 @@ function getArchivedPhaseDirs(cwd) {
4943
5021
  if (!versionMatch) continue;
4944
5022
  const version = versionMatch[1];
4945
5023
  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));
5024
+ const dirs = listSubDirs(archivePath, true);
4947
5025
  for (const dir of dirs) results.push({
4948
5026
  name: dir,
4949
5027
  milestone: version,
@@ -4952,17 +5030,16 @@ function getArchivedPhaseDirs(cwd) {
4952
5030
  });
4953
5031
  }
4954
5032
  } catch (e) {
4955
- if (process.env.MAXSIM_DEBUG) console.error(e);
5033
+ debugLog(e);
4956
5034
  }
4957
5035
  return results;
4958
5036
  }
4959
5037
  function getRoadmapPhaseInternal(cwd, phaseNum) {
4960
5038
  if (!phaseNum) return null;
4961
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
4962
- if (!node_fs.default.existsSync(roadmapPath)) return null;
5039
+ const rp = roadmapPath(cwd);
4963
5040
  try {
4964
- const content = node_fs.default.readFileSync(roadmapPath, "utf-8");
4965
- const phasePattern = getPhasePattern(phaseNum.toString().replace(/\./g, "\\."), "i");
5041
+ const content = node_fs.default.readFileSync(rp, "utf-8");
5042
+ const phasePattern = getPhasePattern(escapePhaseNum(phaseNum), "i");
4966
5043
  const headerMatch = content.match(phasePattern);
4967
5044
  if (!headerMatch) return null;
4968
5045
  const phaseName = headerMatch[1].trim();
@@ -4983,8 +5060,8 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
4983
5060
  return null;
4984
5061
  }
4985
5062
  }
4986
- function resolveModelInternal(cwd, agentType) {
4987
- const config = loadConfig(cwd);
5063
+ function resolveModelInternal(cwd, agentType, config) {
5064
+ config = config ?? loadConfig(cwd);
4988
5065
  const override = config.model_overrides?.[agentType];
4989
5066
  if (override) return override === "opus" ? "inherit" : override;
4990
5067
  const profile = config.model_profile || "balanced";
@@ -5011,7 +5088,7 @@ function generateSlugInternal(text) {
5011
5088
  }
5012
5089
  function getMilestoneInfo(cwd) {
5013
5090
  try {
5014
- const roadmap = node_fs.default.readFileSync(node_path.default.join(cwd, ".planning", "ROADMAP.md"), "utf-8");
5091
+ const roadmap = node_fs.default.readFileSync(roadmapPath(cwd), "utf-8");
5015
5092
  const versionMatch = roadmap.match(/v(\d+\.\d+)/);
5016
5093
  const nameMatch = roadmap.match(/## .*v\d+\.\d+[:\s]+([^\n(]+)/);
5017
5094
  return {
@@ -11994,17 +12071,32 @@ function readTextArgOrFile(cwd, value, filePath, label) {
11994
12071
  throw new Error(`${label} file not found: ${filePath}`);
11995
12072
  }
11996
12073
  }
12074
+ /**
12075
+ * Append an entry to a section in STATE.md content, removing placeholder text.
12076
+ * Returns updated content or null if section not found.
12077
+ */
12078
+ function appendToStateSection(content, sectionPattern, entry, placeholderPatterns) {
12079
+ const match = content.match(sectionPattern);
12080
+ if (!match) return null;
12081
+ let sectionBody = match[2];
12082
+ for (const pat of placeholderPatterns || [
12083
+ /None yet\.?\s*\n?/gi,
12084
+ /No decisions yet\.?\s*\n?/gi,
12085
+ /None\.?\s*\n?/gi
12086
+ ]) sectionBody = sectionBody.replace(pat, "");
12087
+ sectionBody = sectionBody.trimEnd() + "\n" + entry + "\n";
12088
+ return content.replace(sectionPattern, (_m, header) => `${header}${sectionBody}`);
12089
+ }
11997
12090
  function cmdStateLoad(cwd, raw) {
11998
12091
  const config = loadConfig(cwd);
11999
- const planningDir = node_path.default.join(cwd, ".planning");
12000
12092
  let stateRaw = "";
12001
12093
  try {
12002
- stateRaw = node_fs.default.readFileSync(node_path.default.join(planningDir, "STATE.md"), "utf-8");
12094
+ stateRaw = node_fs.default.readFileSync(statePath(cwd), "utf-8");
12003
12095
  } catch (e) {
12004
- if (process.env.MAXSIM_DEBUG) console.error(e);
12096
+ debugLog(e);
12005
12097
  }
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"));
12098
+ const configExists = node_fs.default.existsSync(configPath(cwd));
12099
+ const roadmapExists = node_fs.default.existsSync(roadmapPath(cwd));
12008
12100
  const stateExists = stateRaw.length > 0;
12009
12101
  const result = {
12010
12102
  config,
@@ -12015,7 +12107,7 @@ function cmdStateLoad(cwd, raw) {
12015
12107
  };
12016
12108
  if (raw) {
12017
12109
  const c = config;
12018
- const lines = [
12110
+ output(result, true, [
12019
12111
  `model_profile=${c.model_profile}`,
12020
12112
  `commit_docs=${c.commit_docs}`,
12021
12113
  `branching_strategy=${c.branching_strategy}`,
@@ -12028,16 +12120,14 @@ function cmdStateLoad(cwd, raw) {
12028
12120
  `config_exists=${configExists}`,
12029
12121
  `roadmap_exists=${roadmapExists}`,
12030
12122
  `state_exists=${stateExists}`
12031
- ];
12032
- process.stdout.write(lines.join("\n"));
12033
- process.exit(0);
12123
+ ].join("\n"));
12034
12124
  }
12035
12125
  output(result);
12036
12126
  }
12037
12127
  function cmdStateGet(cwd, section, raw) {
12038
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12128
+ const statePath$2 = statePath(cwd);
12039
12129
  try {
12040
- const content = node_fs.default.readFileSync(statePath, "utf-8");
12130
+ const content = node_fs.default.readFileSync(statePath$2, "utf-8");
12041
12131
  if (!section) {
12042
12132
  output({ content }, raw, content);
12043
12133
  return;
@@ -12056,14 +12146,15 @@ function cmdStateGet(cwd, section, raw) {
12056
12146
  return;
12057
12147
  }
12058
12148
  output({ error: `Section or field "${section}" not found` }, raw, "");
12059
- } catch {
12149
+ } catch (e) {
12150
+ rethrowCliSignals(e);
12060
12151
  error("STATE.md not found");
12061
12152
  }
12062
12153
  }
12063
12154
  function cmdStatePatch(cwd, patches, raw) {
12064
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12155
+ const statePath$3 = statePath(cwd);
12065
12156
  try {
12066
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12157
+ let content = node_fs.default.readFileSync(statePath$3, "utf-8");
12067
12158
  const results = {
12068
12159
  updated: [],
12069
12160
  failed: []
@@ -12076,28 +12167,30 @@ function cmdStatePatch(cwd, patches, raw) {
12076
12167
  results.updated.push(field);
12077
12168
  } else results.failed.push(field);
12078
12169
  }
12079
- if (results.updated.length > 0) node_fs.default.writeFileSync(statePath, content, "utf-8");
12170
+ if (results.updated.length > 0) node_fs.default.writeFileSync(statePath$3, content, "utf-8");
12080
12171
  output(results, raw, results.updated.length > 0 ? "true" : "false");
12081
- } catch {
12172
+ } catch (e) {
12173
+ rethrowCliSignals(e);
12082
12174
  error("STATE.md not found");
12083
12175
  }
12084
12176
  }
12085
12177
  function cmdStateUpdate(cwd, field, value) {
12086
12178
  if (!field || value === void 0) error("field and value required for state update");
12087
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12179
+ const statePath$4 = statePath(cwd);
12088
12180
  try {
12089
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12181
+ let content = node_fs.default.readFileSync(statePath$4, "utf-8");
12090
12182
  const fieldEscaped = escapeStringRegexp(field);
12091
12183
  const pattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, "i");
12092
12184
  if (pattern.test(content)) {
12093
12185
  content = content.replace(pattern, (_match, prefix) => `${prefix}${value}`);
12094
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12186
+ node_fs.default.writeFileSync(statePath$4, content, "utf-8");
12095
12187
  output({ updated: true });
12096
12188
  } else output({
12097
12189
  updated: false,
12098
12190
  reason: `Field "${field}" not found in STATE.md`
12099
12191
  });
12100
- } catch {
12192
+ } catch (e) {
12193
+ rethrowCliSignals(e);
12101
12194
  output({
12102
12195
  updated: false,
12103
12196
  reason: "STATE.md not found"
@@ -12105,15 +12198,15 @@ function cmdStateUpdate(cwd, field, value) {
12105
12198
  }
12106
12199
  }
12107
12200
  function cmdStateAdvancePlan(cwd, raw) {
12108
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12109
- if (!node_fs.default.existsSync(statePath)) {
12201
+ const statePath$5 = statePath(cwd);
12202
+ if (!node_fs.default.existsSync(statePath$5)) {
12110
12203
  output({ error: "STATE.md not found" }, raw);
12111
12204
  return;
12112
12205
  }
12113
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12206
+ let content = node_fs.default.readFileSync(statePath$5, "utf-8");
12114
12207
  const currentPlan = parseInt(stateExtractField(content, "Current Plan") ?? "", 10);
12115
12208
  const totalPlans = parseInt(stateExtractField(content, "Total Plans in Phase") ?? "", 10);
12116
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
12209
+ const today = todayISO();
12117
12210
  if (isNaN(currentPlan) || isNaN(totalPlans)) {
12118
12211
  output({ error: "Cannot parse Current Plan or Total Plans in Phase from STATE.md" }, raw);
12119
12212
  return;
@@ -12121,7 +12214,7 @@ function cmdStateAdvancePlan(cwd, raw) {
12121
12214
  if (currentPlan >= totalPlans) {
12122
12215
  content = stateReplaceField(content, "Status", "Phase complete — ready for verification") || content;
12123
12216
  content = stateReplaceField(content, "Last Activity", today) || content;
12124
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12217
+ node_fs.default.writeFileSync(statePath$5, content, "utf-8");
12125
12218
  output({
12126
12219
  advanced: false,
12127
12220
  reason: "last_plan",
@@ -12134,7 +12227,7 @@ function cmdStateAdvancePlan(cwd, raw) {
12134
12227
  content = stateReplaceField(content, "Current Plan", String(newPlan)) || content;
12135
12228
  content = stateReplaceField(content, "Status", "Ready to execute") || content;
12136
12229
  content = stateReplaceField(content, "Last Activity", today) || content;
12137
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12230
+ node_fs.default.writeFileSync(statePath$5, content, "utf-8");
12138
12231
  output({
12139
12232
  advanced: true,
12140
12233
  previous_plan: currentPlan,
@@ -12144,12 +12237,12 @@ function cmdStateAdvancePlan(cwd, raw) {
12144
12237
  }
12145
12238
  }
12146
12239
  function cmdStateRecordMetric(cwd, options, raw) {
12147
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12148
- if (!node_fs.default.existsSync(statePath)) {
12240
+ const statePath$6 = statePath(cwd);
12241
+ if (!node_fs.default.existsSync(statePath$6)) {
12149
12242
  output({ error: "STATE.md not found" }, raw);
12150
12243
  return;
12151
12244
  }
12152
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12245
+ let content = node_fs.default.readFileSync(statePath$6, "utf-8");
12153
12246
  const { phase, plan, duration, tasks, files } = options;
12154
12247
  if (!phase || !plan || !duration) {
12155
12248
  output({ error: "phase, plan, and duration required" }, raw);
@@ -12163,7 +12256,7 @@ function cmdStateRecordMetric(cwd, options, raw) {
12163
12256
  if (tableBody.trim() === "" || tableBody.includes("None yet")) tableBody = newRow;
12164
12257
  else tableBody = tableBody + "\n" + newRow;
12165
12258
  content = content.replace(metricsPattern, (_match, header) => `${header}${tableBody}\n`);
12166
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12259
+ node_fs.default.writeFileSync(statePath$6, content, "utf-8");
12167
12260
  output({
12168
12261
  recorded: true,
12169
12262
  phase,
@@ -12176,21 +12269,21 @@ function cmdStateRecordMetric(cwd, options, raw) {
12176
12269
  }, raw, "false");
12177
12270
  }
12178
12271
  function cmdStateUpdateProgress(cwd, raw) {
12179
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12180
- if (!node_fs.default.existsSync(statePath)) {
12272
+ const statePath$7 = statePath(cwd);
12273
+ if (!node_fs.default.existsSync(statePath$7)) {
12181
12274
  output({ error: "STATE.md not found" }, raw);
12182
12275
  return;
12183
12276
  }
12184
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12185
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
12277
+ let content = node_fs.default.readFileSync(statePath$7, "utf-8");
12278
+ const phasesDir = phasesPath(cwd);
12186
12279
  let totalPlans = 0;
12187
12280
  let totalSummaries = 0;
12188
12281
  if (node_fs.default.existsSync(phasesDir)) {
12189
12282
  const phaseDirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
12190
12283
  for (const dir of phaseDirs) {
12191
12284
  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;
12285
+ totalPlans += files.filter((f) => isPlanFile(f)).length;
12286
+ totalSummaries += files.filter((f) => isSummaryFile(f)).length;
12194
12287
  }
12195
12288
  }
12196
12289
  const percent = totalPlans > 0 ? Math.min(100, Math.round(totalSummaries / totalPlans * 100)) : 0;
@@ -12200,7 +12293,7 @@ function cmdStateUpdateProgress(cwd, raw) {
12200
12293
  const progressPattern = /(\*\*Progress:\*\*\s*).*/i;
12201
12294
  if (progressPattern.test(content)) {
12202
12295
  content = content.replace(progressPattern, (_match, prefix) => `${prefix}${progressStr}`);
12203
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12296
+ node_fs.default.writeFileSync(statePath$7, content, "utf-8");
12204
12297
  output({
12205
12298
  updated: true,
12206
12299
  percent,
@@ -12214,8 +12307,8 @@ function cmdStateUpdateProgress(cwd, raw) {
12214
12307
  }, raw, "false");
12215
12308
  }
12216
12309
  function cmdStateAddDecision(cwd, options, raw) {
12217
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12218
- if (!node_fs.default.existsSync(statePath)) {
12310
+ const statePath$8 = statePath(cwd);
12311
+ if (!node_fs.default.existsSync(statePath$8)) {
12219
12312
  output({ error: "STATE.md not found" }, raw);
12220
12313
  return;
12221
12314
  }
@@ -12236,16 +12329,11 @@ function cmdStateAddDecision(cwd, options, raw) {
12236
12329
  output({ error: "summary required" }, raw);
12237
12330
  return;
12238
12331
  }
12239
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12332
+ const content = node_fs.default.readFileSync(statePath$8, "utf-8");
12240
12333
  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");
12334
+ 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]);
12335
+ if (updated) {
12336
+ node_fs.default.writeFileSync(statePath$8, updated, "utf-8");
12249
12337
  output({
12250
12338
  added: true,
12251
12339
  decision: entry
@@ -12256,8 +12344,8 @@ function cmdStateAddDecision(cwd, options, raw) {
12256
12344
  }, raw, "false");
12257
12345
  }
12258
12346
  function cmdStateAddBlocker(cwd, text, raw) {
12259
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12260
- if (!node_fs.default.existsSync(statePath)) {
12347
+ const statePath$9 = statePath(cwd);
12348
+ if (!node_fs.default.existsSync(statePath$9)) {
12261
12349
  output({ error: "STATE.md not found" }, raw);
12262
12350
  return;
12263
12351
  }
@@ -12276,16 +12364,9 @@ function cmdStateAddBlocker(cwd, text, raw) {
12276
12364
  output({ error: "text required" }, raw);
12277
12365
  return;
12278
12366
  }
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");
12367
+ 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]);
12368
+ if (updated) {
12369
+ node_fs.default.writeFileSync(statePath$9, updated, "utf-8");
12289
12370
  output({
12290
12371
  added: true,
12291
12372
  blocker: blockerText
@@ -12296,8 +12377,8 @@ function cmdStateAddBlocker(cwd, text, raw) {
12296
12377
  }, raw, "false");
12297
12378
  }
12298
12379
  function cmdStateResolveBlocker(cwd, text, raw) {
12299
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12300
- if (!node_fs.default.existsSync(statePath)) {
12380
+ const statePath$10 = statePath(cwd);
12381
+ if (!node_fs.default.existsSync(statePath$10)) {
12301
12382
  output({ error: "STATE.md not found" }, raw);
12302
12383
  return;
12303
12384
  }
@@ -12305,7 +12386,7 @@ function cmdStateResolveBlocker(cwd, text, raw) {
12305
12386
  output({ error: "text required" }, raw);
12306
12387
  return;
12307
12388
  }
12308
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12389
+ let content = node_fs.default.readFileSync(statePath$10, "utf-8");
12309
12390
  const sectionPattern = /(###?\s*(?:Blockers|Blockers\/Concerns|Concerns)\s*\n)([\s\S]*?)(?=\n###?|\n##[^#]|$)/i;
12310
12391
  const match = content.match(sectionPattern);
12311
12392
  if (match) {
@@ -12315,7 +12396,7 @@ function cmdStateResolveBlocker(cwd, text, raw) {
12315
12396
  }).join("\n");
12316
12397
  if (!newBody.trim() || !newBody.includes("- ")) newBody = "None\n";
12317
12398
  content = content.replace(sectionPattern, (_match, header) => `${header}${newBody}`);
12318
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12399
+ node_fs.default.writeFileSync(statePath$10, content, "utf-8");
12319
12400
  output({
12320
12401
  resolved: true,
12321
12402
  blocker: text
@@ -12326,12 +12407,12 @@ function cmdStateResolveBlocker(cwd, text, raw) {
12326
12407
  }, raw, "false");
12327
12408
  }
12328
12409
  function cmdStateRecordSession(cwd, options, raw) {
12329
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12330
- if (!node_fs.default.existsSync(statePath)) {
12410
+ const statePath$11 = statePath(cwd);
12411
+ if (!node_fs.default.existsSync(statePath$11)) {
12331
12412
  output({ error: "STATE.md not found" }, raw);
12332
12413
  return;
12333
12414
  }
12334
- let content = node_fs.default.readFileSync(statePath, "utf-8");
12415
+ let content = node_fs.default.readFileSync(statePath$11, "utf-8");
12335
12416
  const now = (/* @__PURE__ */ new Date()).toISOString();
12336
12417
  const updated = [];
12337
12418
  let result = stateReplaceField(content, "Last session", now);
@@ -12360,7 +12441,7 @@ function cmdStateRecordSession(cwd, options, raw) {
12360
12441
  updated.push("Resume File");
12361
12442
  }
12362
12443
  if (updated.length > 0) {
12363
- node_fs.default.writeFileSync(statePath, content, "utf-8");
12444
+ node_fs.default.writeFileSync(statePath$11, content, "utf-8");
12364
12445
  output({
12365
12446
  recorded: true,
12366
12447
  updated
@@ -12371,12 +12452,12 @@ function cmdStateRecordSession(cwd, options, raw) {
12371
12452
  }, raw, "false");
12372
12453
  }
12373
12454
  function cmdStateSnapshot(cwd, raw) {
12374
- const statePath = node_path.default.join(cwd, ".planning", "STATE.md");
12375
- if (!node_fs.default.existsSync(statePath)) {
12455
+ const statePath$12 = statePath(cwd);
12456
+ if (!node_fs.default.existsSync(statePath$12)) {
12376
12457
  output({ error: "STATE.md not found" }, raw);
12377
12458
  return;
12378
12459
  }
12379
- const content = node_fs.default.readFileSync(statePath, "utf-8");
12460
+ const content = node_fs.default.readFileSync(statePath$12, "utf-8");
12380
12461
  const extractField = (fieldName) => {
12381
12462
  const pattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*(.+)`, "i");
12382
12463
  const match = content.match(pattern);
@@ -12454,8 +12535,8 @@ function cmdStateSnapshot(cwd, raw) {
12454
12535
  * Ported from maxsim/bin/lib/roadmap.cjs
12455
12536
  */
12456
12537
  function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
12457
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
12458
- if (!node_fs.default.existsSync(roadmapPath)) {
12538
+ const rmPath = roadmapPath(cwd);
12539
+ if (!node_fs.default.existsSync(rmPath)) {
12459
12540
  output({
12460
12541
  found: false,
12461
12542
  error: "ROADMAP.md not found"
@@ -12463,7 +12544,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
12463
12544
  return;
12464
12545
  }
12465
12546
  try {
12466
- const content = node_fs.default.readFileSync(roadmapPath, "utf-8");
12547
+ const content = node_fs.default.readFileSync(rmPath, "utf-8");
12467
12548
  const escapedPhase = phaseNum.replace(/\./g, "\\.");
12468
12549
  const phasePattern = getPhasePattern(escapedPhase, "i");
12469
12550
  const headerMatch = content.match(phasePattern);
@@ -12503,12 +12584,13 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
12503
12584
  section
12504
12585
  }, raw, section);
12505
12586
  } catch (e) {
12587
+ rethrowCliSignals(e);
12506
12588
  error("Failed to read ROADMAP.md: " + e.message);
12507
12589
  }
12508
12590
  }
12509
12591
  function cmdRoadmapAnalyze(cwd, raw) {
12510
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
12511
- if (!node_fs.default.existsSync(roadmapPath)) {
12592
+ const rmPath = roadmapPath(cwd);
12593
+ if (!node_fs.default.existsSync(rmPath)) {
12512
12594
  output({
12513
12595
  error: "ROADMAP.md not found",
12514
12596
  milestones: [],
@@ -12517,8 +12599,8 @@ function cmdRoadmapAnalyze(cwd, raw) {
12517
12599
  }, raw);
12518
12600
  return;
12519
12601
  }
12520
- const content = node_fs.default.readFileSync(roadmapPath, "utf-8");
12521
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
12602
+ const content = node_fs.default.readFileSync(rmPath, "utf-8");
12603
+ const phasesDir = phasesPath(cwd);
12522
12604
  const phasePattern = getPhasePattern();
12523
12605
  const phases = [];
12524
12606
  let match;
@@ -12540,11 +12622,11 @@ function cmdRoadmapAnalyze(cwd, raw) {
12540
12622
  let hasContext = false;
12541
12623
  let hasResearch = false;
12542
12624
  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);
12625
+ const dirMatch = listSubDirs(phasesDir).find((d) => d.startsWith(normalized + "-") || d === normalized);
12544
12626
  if (dirMatch) {
12545
12627
  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;
12628
+ planCount = phaseFiles.filter((f) => isPlanFile(f)).length;
12629
+ summaryCount = phaseFiles.filter((f) => isSummaryFile(f)).length;
12548
12630
  hasContext = phaseFiles.some((f) => f.endsWith("-CONTEXT.md") || f === "CONTEXT.md");
12549
12631
  hasResearch = phaseFiles.some((f) => f.endsWith("-RESEARCH.md") || f === "RESEARCH.md");
12550
12632
  if (summaryCount >= planCount && planCount > 0) diskStatus = "complete";
@@ -12555,7 +12637,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
12555
12637
  else diskStatus = "empty";
12556
12638
  }
12557
12639
  } catch (e) {
12558
- if (process.env.MAXSIM_DEBUG) console.error(e);
12640
+ debugLog(e);
12559
12641
  }
12560
12642
  const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace(".", "\\.")}`, "i");
12561
12643
  const checkboxMatch = content.match(checkboxPattern);
@@ -12606,7 +12688,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
12606
12688
  }
12607
12689
  function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
12608
12690
  if (!phaseNum) error("phase number required for roadmap update-plan-progress");
12609
- const roadmapPath = node_path.default.join(cwd, ".planning", "ROADMAP.md");
12691
+ const rmPath = roadmapPath(cwd);
12610
12692
  const phaseInfo = findPhaseInternal(cwd, phaseNum);
12611
12693
  if (!phaseInfo) error(`Phase ${phaseNum} not found`);
12612
12694
  const planCount = phaseInfo.plans.length;
@@ -12622,8 +12704,8 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
12622
12704
  }
12623
12705
  const isComplete = summaryCount >= planCount;
12624
12706
  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)) {
12707
+ const today = todayISO();
12708
+ if (!node_fs.default.existsSync(rmPath)) {
12627
12709
  output({
12628
12710
  updated: false,
12629
12711
  reason: "ROADMAP.md not found",
@@ -12632,7 +12714,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
12632
12714
  }, raw, "no roadmap");
12633
12715
  return;
12634
12716
  }
12635
- let roadmapContent = node_fs.default.readFileSync(roadmapPath, "utf-8");
12717
+ let roadmapContent = node_fs.default.readFileSync(rmPath, "utf-8");
12636
12718
  const phaseEscaped = phaseNum.replace(".", "\\.");
12637
12719
  const tablePattern = new RegExp(`(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|)[^|]*(\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`, "i");
12638
12720
  const dateField = isComplete ? ` ${today} ` : " ";
@@ -12644,7 +12726,7 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
12644
12726
  const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`, "i");
12645
12727
  roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
12646
12728
  }
12647
- node_fs.default.writeFileSync(roadmapPath, roadmapContent, "utf-8");
12729
+ node_fs.default.writeFileSync(rmPath, roadmapContent, "utf-8");
12648
12730
  output({
12649
12731
  updated: true,
12650
12732
  phase: phaseNum,
@@ -12666,7 +12748,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
12666
12748
  if (!reqIdsRaw || reqIdsRaw.length === 0) error("requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02");
12667
12749
  const reqIds = reqIdsRaw.join(" ").replace(/[\[\]]/g, "").split(/[,\s]+/).map((r) => r.trim()).filter(Boolean);
12668
12750
  if (reqIds.length === 0) error("no valid requirement IDs found");
12669
- const reqPath = node_path.default.join(cwd, ".planning", "REQUIREMENTS.md");
12751
+ const reqPath = planningPath(cwd, "REQUIREMENTS.md");
12670
12752
  if (!node_fs.default.existsSync(reqPath)) {
12671
12753
  output({
12672
12754
  updated: false,
@@ -12702,13 +12784,13 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
12702
12784
  }
12703
12785
  function cmdMilestoneComplete(cwd, version, options, raw) {
12704
12786
  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];
12787
+ const roadmapPath$1 = roadmapPath(cwd);
12788
+ const reqPath = planningPath(cwd, "REQUIREMENTS.md");
12789
+ const statePath$1 = statePath(cwd);
12790
+ const milestonesPath = planningPath(cwd, "MILESTONES.md");
12791
+ const archiveDir = planningPath(cwd, "milestones");
12792
+ const phasesDir = phasesPath(cwd);
12793
+ const today = todayISO();
12712
12794
  const milestoneName = options.name || version;
12713
12795
  node_fs.default.mkdirSync(archiveDir, { recursive: true });
12714
12796
  let phaseCount = 0;
@@ -12716,12 +12798,12 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
12716
12798
  let totalTasks = 0;
12717
12799
  const accomplishments = [];
12718
12800
  try {
12719
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
12801
+ const dirs = listSubDirs(phasesDir, true);
12720
12802
  for (const dir of dirs) {
12721
12803
  phaseCount++;
12722
12804
  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");
12805
+ const plans = phaseFiles.filter(isPlanFile);
12806
+ const summaries = phaseFiles.filter(isSummaryFile);
12725
12807
  totalPlans += plans.length;
12726
12808
  for (const s of summaries) try {
12727
12809
  const content = node_fs.default.readFileSync(node_path.default.join(phasesDir, dir, s), "utf-8");
@@ -12730,14 +12812,14 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
12730
12812
  const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
12731
12813
  totalTasks += taskMatches.length;
12732
12814
  } catch (e) {
12733
- if (process.env.MAXSIM_DEBUG) console.error(e);
12815
+ debugLog(e);
12734
12816
  }
12735
12817
  }
12736
12818
  } catch (e) {
12737
- if (process.env.MAXSIM_DEBUG) console.error(e);
12819
+ debugLog(e);
12738
12820
  }
12739
- if (node_fs.default.existsSync(roadmapPath)) {
12740
- const roadmapContent = node_fs.default.readFileSync(roadmapPath, "utf-8");
12821
+ if (node_fs.default.existsSync(roadmapPath$1)) {
12822
+ const roadmapContent = node_fs.default.readFileSync(roadmapPath$1, "utf-8");
12741
12823
  node_fs.default.writeFileSync(node_path.default.join(archiveDir, `${version}-ROADMAP.md`), roadmapContent, "utf-8");
12742
12824
  }
12743
12825
  if (node_fs.default.existsSync(reqPath)) {
@@ -12753,22 +12835,22 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
12753
12835
  const existing = node_fs.default.readFileSync(milestonesPath, "utf-8");
12754
12836
  node_fs.default.writeFileSync(milestonesPath, existing + "\n" + milestoneEntry, "utf-8");
12755
12837
  } 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");
12838
+ if (node_fs.default.existsSync(statePath$1)) {
12839
+ let stateContent = node_fs.default.readFileSync(statePath$1, "utf-8");
12758
12840
  stateContent = stateContent.replace(/(\*\*Status:\*\*\s*).*/, `$1${version} milestone complete`);
12759
12841
  stateContent = stateContent.replace(/(\*\*Last Activity:\*\*\s*).*/, `$1${today}`);
12760
12842
  stateContent = stateContent.replace(/(\*\*Last Activity Description:\*\*\s*).*/, `$1${version} milestone completed and archived`);
12761
- node_fs.default.writeFileSync(statePath, stateContent, "utf-8");
12843
+ node_fs.default.writeFileSync(statePath$1, stateContent, "utf-8");
12762
12844
  }
12763
12845
  let phasesArchived = false;
12764
12846
  if (options.archivePhases) try {
12765
12847
  const phaseArchiveDir = node_path.default.join(archiveDir, `${version}-phases`);
12766
12848
  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);
12849
+ const phaseDirNames = listSubDirs(phasesDir);
12768
12850
  for (const dir of phaseDirNames) node_fs.default.renameSync(node_path.default.join(phasesDir, dir), node_path.default.join(phaseArchiveDir, dir));
12769
12851
  phasesArchived = phaseDirNames.length > 0;
12770
12852
  } catch (e) {
12771
- if (process.env.MAXSIM_DEBUG) console.error(e);
12853
+ debugLog(e);
12772
12854
  }
12773
12855
  output({
12774
12856
  version,
@@ -12785,7 +12867,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
12785
12867
  phases: phasesArchived
12786
12868
  },
12787
12869
  milestones_updated: true,
12788
- state_updated: node_fs.default.existsSync(statePath)
12870
+ state_updated: node_fs.default.existsSync(statePath$1)
12789
12871
  }, raw);
12790
12872
  }
12791
12873
 
@@ -13202,6 +13284,18 @@ const chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
13202
13284
  *
13203
13285
  * Ported from maxsim/bin/lib/commands.cjs
13204
13286
  */
13287
+ function parseTodoFrontmatter(content) {
13288
+ const createdMatch = content.match(/^created:\s*(.+)$/m);
13289
+ const titleMatch = content.match(/^title:\s*(.+)$/m);
13290
+ const areaMatch = content.match(/^area:\s*(.+)$/m);
13291
+ const completedMatch = content.match(/^completed:\s*(.+)$/m);
13292
+ return {
13293
+ created: createdMatch ? createdMatch[1].trim() : "unknown",
13294
+ title: titleMatch ? titleMatch[1].trim() : "Untitled",
13295
+ area: areaMatch ? areaMatch[1].trim() : "general",
13296
+ ...completedMatch && { completed: completedMatch[1].trim() }
13297
+ };
13298
+ }
13205
13299
  function cmdGenerateSlug(text, raw) {
13206
13300
  if (!text) error("text required for slug generation");
13207
13301
  const slug = (0, import_slugify.default)(text, {
@@ -13215,7 +13309,7 @@ function cmdCurrentTimestamp(format, raw) {
13215
13309
  let result;
13216
13310
  switch (format) {
13217
13311
  case "date":
13218
- result = now.toISOString().split("T")[0];
13312
+ result = todayISO();
13219
13313
  break;
13220
13314
  case "filename":
13221
13315
  result = now.toISOString().replace(/:/g, "-").replace(/\..+/, "");
@@ -13227,31 +13321,27 @@ function cmdCurrentTimestamp(format, raw) {
13227
13321
  output({ timestamp: result }, raw, result);
13228
13322
  }
13229
13323
  function cmdListTodos(cwd, area, raw) {
13230
- const pendingDir = node_path.default.join(cwd, ".planning", "todos", "pending");
13324
+ const pendingDir = planningPath(cwd, "todos", "pending");
13231
13325
  let count = 0;
13232
13326
  const todos = [];
13233
13327
  try {
13234
13328
  const files = node_fs.default.readdirSync(pendingDir).filter((f) => f.endsWith(".md"));
13235
13329
  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;
13330
+ const fm = parseTodoFrontmatter(node_fs.default.readFileSync(node_path.default.join(pendingDir, file), "utf-8"));
13331
+ if (area && fm.area !== area) continue;
13242
13332
  count++;
13243
13333
  todos.push({
13244
13334
  file,
13245
- created: createdMatch ? createdMatch[1].trim() : "unknown",
13246
- title: titleMatch ? titleMatch[1].trim() : "Untitled",
13247
- area: todoArea,
13335
+ created: fm.created,
13336
+ title: fm.title,
13337
+ area: fm.area,
13248
13338
  path: node_path.default.join(".planning", "todos", "pending", file)
13249
13339
  });
13250
13340
  } catch (e) {
13251
- if (process.env.MAXSIM_DEBUG) console.error(e);
13341
+ debugLog(e);
13252
13342
  }
13253
13343
  } catch (e) {
13254
- if (process.env.MAXSIM_DEBUG) console.error(e);
13344
+ debugLog(e);
13255
13345
  }
13256
13346
  output({
13257
13347
  count,
@@ -13267,7 +13357,8 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
13267
13357
  exists: true,
13268
13358
  type: stats.isDirectory() ? "directory" : stats.isFile() ? "file" : "other"
13269
13359
  }, raw, "true");
13270
- } catch {
13360
+ } catch (e) {
13361
+ rethrowCliSignals(e);
13271
13362
  output({
13272
13363
  exists: false,
13273
13364
  type: null
@@ -13275,7 +13366,7 @@ function cmdVerifyPathExists(cwd, targetPath, raw) {
13275
13366
  }
13276
13367
  }
13277
13368
  function cmdHistoryDigest(cwd, raw) {
13278
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
13369
+ const phasesDir = phasesPath(cwd);
13279
13370
  const digest = {
13280
13371
  phases: {},
13281
13372
  decisions: [],
@@ -13289,14 +13380,14 @@ function cmdHistoryDigest(cwd, raw) {
13289
13380
  milestone: a.milestone
13290
13381
  });
13291
13382
  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();
13383
+ const currentDirs = listSubDirs(phasesDir, true);
13293
13384
  for (const dir of currentDirs) allPhaseDirs.push({
13294
13385
  name: dir,
13295
13386
  fullPath: node_path.default.join(phasesDir, dir),
13296
13387
  milestone: null
13297
13388
  });
13298
13389
  } catch (e) {
13299
- if (process.env.MAXSIM_DEBUG) console.error(e);
13390
+ debugLog(e);
13300
13391
  }
13301
13392
  if (allPhaseDirs.length === 0) {
13302
13393
  output({
@@ -13308,7 +13399,7 @@ function cmdHistoryDigest(cwd, raw) {
13308
13399
  }
13309
13400
  try {
13310
13401
  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");
13402
+ const summaries = node_fs.default.readdirSync(dirPath).filter((f) => isSummaryFile(f));
13312
13403
  for (const summary of summaries) try {
13313
13404
  const fm = extractFrontmatter(node_fs.default.readFileSync(node_path.default.join(dirPath, summary), "utf-8"));
13314
13405
  const phaseNum = fm.phase || dir.split("-")[0];
@@ -13332,7 +13423,7 @@ function cmdHistoryDigest(cwd, raw) {
13332
13423
  const techStack = fm["tech-stack"];
13333
13424
  if (techStack && techStack.added) techStack.added.forEach((t) => digest.tech_stack.add(typeof t === "string" ? t : t.name));
13334
13425
  } catch (e) {
13335
- if (process.env.MAXSIM_DEBUG) console.error(e);
13426
+ debugLog(e);
13336
13427
  }
13337
13428
  }
13338
13429
  const outputDigest = {
@@ -13348,6 +13439,7 @@ function cmdHistoryDigest(cwd, raw) {
13348
13439
  };
13349
13440
  output(outputDigest, raw);
13350
13441
  } catch (e) {
13442
+ rethrowCliSignals(e);
13351
13443
  error("Failed to generate history digest: " + e.message);
13352
13444
  }
13353
13445
  }
@@ -13520,6 +13612,7 @@ async function cmdWebsearch(query, options, raw) {
13520
13612
  results
13521
13613
  }, raw, results.map((r) => `${r.title}\n${r.url}\n${r.description}`).join("\n\n"));
13522
13614
  } catch (err) {
13615
+ rethrowCliSignals(err);
13523
13616
  output({
13524
13617
  available: false,
13525
13618
  error: err.message
@@ -13527,7 +13620,7 @@ async function cmdWebsearch(query, options, raw) {
13527
13620
  }
13528
13621
  }
13529
13622
  function cmdProgressRender(cwd, format, raw) {
13530
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
13623
+ const phasesDir = phasesPath(cwd);
13531
13624
  const milestone = getMilestoneInfo(cwd);
13532
13625
  const phases = [];
13533
13626
  let totalPlans = 0;
@@ -13541,8 +13634,8 @@ function cmdProgressRender(cwd, format, raw) {
13541
13634
  const phaseNum = dm ? dm[1] : dir;
13542
13635
  const phaseName = dm && dm[2] ? dm[2].replace(/-/g, " ") : "";
13543
13636
  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;
13637
+ const planCount = phaseFiles.filter((f) => isPlanFile(f)).length;
13638
+ const summaryCount = phaseFiles.filter((f) => isSummaryFile(f)).length;
13546
13639
  totalPlans += planCount;
13547
13640
  totalSummaries += summaryCount;
13548
13641
  let status;
@@ -13559,7 +13652,7 @@ function cmdProgressRender(cwd, format, raw) {
13559
13652
  });
13560
13653
  }
13561
13654
  } catch (e) {
13562
- if (process.env.MAXSIM_DEBUG) console.error(e);
13655
+ debugLog(e);
13563
13656
  }
13564
13657
  const percent = totalPlans > 0 ? Math.min(100, Math.round(totalSummaries / totalPlans * 100)) : 0;
13565
13658
  if (format === "table") {
@@ -13619,13 +13712,13 @@ function cmdProgressRender(cwd, format, raw) {
13619
13712
  }
13620
13713
  function cmdTodoComplete(cwd, filename, raw) {
13621
13714
  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");
13715
+ const pendingDir = planningPath(cwd, "todos", "pending");
13716
+ const completedDir = planningPath(cwd, "todos", "completed");
13624
13717
  const sourcePath = node_path.default.join(pendingDir, filename);
13625
13718
  if (!node_fs.default.existsSync(sourcePath)) error(`Todo not found: ${filename}`);
13626
13719
  node_fs.default.mkdirSync(completedDir, { recursive: true });
13627
13720
  let content = node_fs.default.readFileSync(sourcePath, "utf-8");
13628
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13721
+ const today = todayISO();
13629
13722
  content = `completed: ${today}\n` + content;
13630
13723
  node_fs.default.writeFileSync(node_path.default.join(completedDir, filename), content, "utf-8");
13631
13724
  node_fs.default.unlinkSync(sourcePath);
@@ -13638,7 +13731,7 @@ function cmdTodoComplete(cwd, filename, raw) {
13638
13731
  function cmdScaffold(cwd, type, options, raw) {
13639
13732
  const { phase, name } = options;
13640
13733
  const padded = phase ? normalizePhaseName(phase) : "00";
13641
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
13734
+ const today = todayISO();
13642
13735
  const phaseInfo = phase ? findPhaseInternal(cwd, phase) : null;
13643
13736
  const phaseDir = phaseInfo ? node_path.default.join(cwd, phaseInfo.directory) : null;
13644
13737
  if (phase && !phaseDir && type !== "phase-dir") error(`Phase ${phase} directory not found`);
@@ -13660,7 +13753,7 @@ function cmdScaffold(cwd, type, options, raw) {
13660
13753
  case "phase-dir": {
13661
13754
  if (!phase || !name) error("phase and name required for phase-dir scaffold");
13662
13755
  const dirName = `${padded}-${generateSlugInternal(name)}`;
13663
- const phasesParent = node_path.default.join(cwd, ".planning", "phases");
13756
+ const phasesParent = phasesPath(cwd);
13664
13757
  node_fs.default.mkdirSync(phasesParent, { recursive: true });
13665
13758
  const dirPath = node_path.default.join(phasesParent, dirName);
13666
13759
  node_fs.default.mkdirSync(dirPath, { recursive: true });
@@ -13853,10 +13946,10 @@ function cmdVerifyPhaseCompleteness(cwd, phase, raw) {
13853
13946
  output({ error: "Cannot read phase directory" }, raw);
13854
13947
  return;
13855
13948
  }
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, "")));
13949
+ const plans = files.filter((f) => isPlanFile(f));
13950
+ const summaries = files.filter((f) => isSummaryFile(f));
13951
+ const planIds = new Set(plans.map((p) => planId(p)));
13952
+ const summaryIds = new Set(summaries.map((s) => summaryId(s)));
13860
13953
  const incompletePlans = [...planIds].filter((id) => !summaryIds.has(id));
13861
13954
  if (incompletePlans.length > 0) errors.push(`Plans without summaries: ${incompletePlans.join(", ")}`);
13862
13955
  const orphanSummaries = [...summaryIds].filter((id) => !planIds.has(id));
@@ -14041,11 +14134,11 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
14041
14134
  }, raw, verified === results.length ? "valid" : "invalid");
14042
14135
  }
14043
14136
  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");
14137
+ const rmPath = roadmapPath(cwd);
14138
+ const phasesDir = phasesPath(cwd);
14046
14139
  const errors = [];
14047
14140
  const warnings = [];
14048
- if (!node_fs.default.existsSync(roadmapPath)) {
14141
+ if (!node_fs.default.existsSync(rmPath)) {
14049
14142
  errors.push("ROADMAP.md not found");
14050
14143
  output({
14051
14144
  passed: false,
@@ -14054,20 +14147,20 @@ function cmdValidateConsistency(cwd, raw) {
14054
14147
  }, raw, "failed");
14055
14148
  return;
14056
14149
  }
14057
- const roadmapContent = node_fs.default.readFileSync(roadmapPath, "utf-8");
14150
+ const roadmapContent = node_fs.default.readFileSync(rmPath, "utf-8");
14058
14151
  const roadmapPhases = /* @__PURE__ */ new Set();
14059
14152
  const phasePattern = getPhasePattern();
14060
14153
  let m;
14061
14154
  while ((m = phasePattern.exec(roadmapContent)) !== null) roadmapPhases.add(m[1]);
14062
14155
  const diskPhases = /* @__PURE__ */ new Set();
14063
14156
  try {
14064
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
14157
+ const dirs = listSubDirs(phasesDir);
14065
14158
  for (const dir of dirs) {
14066
14159
  const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)?)/i);
14067
14160
  if (dm) diskPhases.add(dm[1]);
14068
14161
  }
14069
14162
  } catch (e) {
14070
- if (process.env.MAXSIM_DEBUG) console.error(e);
14163
+ debugLog(e);
14071
14164
  }
14072
14165
  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
14166
  for (const p of diskPhases) {
@@ -14077,31 +14170,31 @@ function cmdValidateConsistency(cwd, raw) {
14077
14170
  const integerPhases = [...diskPhases].filter((p) => !p.includes(".")).map((p) => parseInt(p, 10)).sort((a, b) => a - b);
14078
14171
  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
14172
  try {
14080
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
14173
+ const dirs = listSubDirs(phasesDir, true);
14081
14174
  for (const dir of dirs) {
14082
14175
  const phaseFiles = node_fs.default.readdirSync(node_path.default.join(phasesDir, dir));
14083
- const plans = phaseFiles.filter((f) => f.endsWith("-PLAN.md")).sort();
14176
+ const plans = phaseFiles.filter((f) => isPlanFile(f)).sort();
14084
14177
  const planNums = plans.map((p) => {
14085
14178
  const pm = p.match(/-(\d{2})-PLAN\.md$/);
14086
14179
  return pm ? parseInt(pm[1], 10) : null;
14087
14180
  }).filter((n) => n !== null);
14088
14181
  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", "")));
14182
+ const summaries = phaseFiles.filter((f) => isSummaryFile(f));
14183
+ const planIdsSet = new Set(plans.map((p) => planId(p)));
14184
+ const summaryIdsSet = new Set(summaries.map((s) => summaryId(s)));
14092
14185
  for (const sid of summaryIdsSet) if (!planIdsSet.has(sid)) warnings.push(`Summary ${sid}-SUMMARY.md in ${dir} has no matching PLAN.md`);
14093
14186
  }
14094
14187
  } catch (e) {
14095
- if (process.env.MAXSIM_DEBUG) console.error(e);
14188
+ debugLog(e);
14096
14189
  }
14097
14190
  try {
14098
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
14191
+ const dirs = listSubDirs(phasesDir);
14099
14192
  for (const dir of dirs) {
14100
- const plans = node_fs.default.readdirSync(node_path.default.join(phasesDir, dir)).filter((f) => f.endsWith("-PLAN.md"));
14193
+ const plans = node_fs.default.readdirSync(node_path.default.join(phasesDir, dir)).filter((f) => isPlanFile(f));
14101
14194
  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
14195
  }
14103
14196
  } catch (e) {
14104
- if (process.env.MAXSIM_DEBUG) console.error(e);
14197
+ debugLog(e);
14105
14198
  }
14106
14199
  const passed = errors.length === 0;
14107
14200
  output({
@@ -14112,12 +14205,12 @@ function cmdValidateConsistency(cwd, raw) {
14112
14205
  }, raw, passed ? "passed" : "failed");
14113
14206
  }
14114
14207
  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");
14208
+ const planningDir = planningPath(cwd);
14209
+ const projectPath = planningPath(cwd, "PROJECT.md");
14210
+ const rmPath = roadmapPath(cwd);
14211
+ const stPath = statePath(cwd);
14212
+ const cfgPath = configPath(cwd);
14213
+ const phasesDir = phasesPath(cwd);
14121
14214
  const errors = [];
14122
14215
  const warnings = [];
14123
14216
  const info = [];
@@ -14153,21 +14246,20 @@ function cmdValidateHealth(cwd, options, raw) {
14153
14246
  "## Requirements"
14154
14247
  ]) if (!content.includes(section)) addIssue("warning", "W001", `PROJECT.md missing section: ${section}`, "Add section manually");
14155
14248
  }
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)) {
14249
+ if (!node_fs.default.existsSync(rmPath)) addIssue("error", "E003", "ROADMAP.md not found", "Run /maxsim:new-milestone to create roadmap");
14250
+ if (!node_fs.default.existsSync(stPath)) {
14158
14251
  addIssue("error", "E004", "STATE.md not found", "Run /maxsim:health --repair to regenerate", true);
14159
14252
  repairs.push("regenerateState");
14160
14253
  } else {
14161
- const phaseRefs = [...node_fs.default.readFileSync(statePath, "utf-8").matchAll(/[Pp]hase\s+(\d+(?:\.\d+)?)/g)].map((m) => m[1]);
14254
+ const phaseRefs = [...node_fs.default.readFileSync(stPath, "utf-8").matchAll(/[Pp]hase\s+(\d+(?:\.\d+)?)/g)].map((m) => m[1]);
14162
14255
  const diskPhases = /* @__PURE__ */ new Set();
14163
14256
  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+)?)/);
14257
+ for (const dir of listSubDirs(phasesDir)) {
14258
+ const dm = dir.match(/^(\d+(?:\.\d+)?)/);
14167
14259
  if (dm) diskPhases.add(dm[1]);
14168
14260
  }
14169
14261
  } catch (e) {
14170
- if (process.env.MAXSIM_DEBUG) console.error(e);
14262
+ debugLog(e);
14171
14263
  }
14172
14264
  for (const ref of phaseRefs) {
14173
14265
  const normalizedRef = String(parseInt(ref, 10)).padStart(2, "0");
@@ -14179,11 +14271,11 @@ function cmdValidateHealth(cwd, options, raw) {
14179
14271
  }
14180
14272
  }
14181
14273
  }
14182
- if (!node_fs.default.existsSync(configPath)) {
14274
+ if (!node_fs.default.existsSync(cfgPath)) {
14183
14275
  addIssue("warning", "W003", "config.json not found", "Run /maxsim:health --repair to create with defaults", true);
14184
14276
  repairs.push("createConfig");
14185
14277
  } else try {
14186
- const rawContent = node_fs.default.readFileSync(configPath, "utf-8");
14278
+ const rawContent = node_fs.default.readFileSync(cfgPath, "utf-8");
14187
14279
  const parsed = JSON.parse(rawContent);
14188
14280
  const validProfiles = [
14189
14281
  "quality",
@@ -14197,42 +14289,39 @@ function cmdValidateHealth(cwd, options, raw) {
14197
14289
  repairs.push("resetConfig");
14198
14290
  }
14199
14291
  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)");
14292
+ 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
14293
  } catch (e) {
14203
- if (process.env.MAXSIM_DEBUG) console.error(e);
14294
+ debugLog(e);
14204
14295
  }
14205
14296
  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", "")));
14297
+ const orphanDirs = listSubDirs(phasesDir);
14298
+ for (const dirName of orphanDirs) {
14299
+ const phaseFiles = node_fs.default.readdirSync(node_path.default.join(phasesDir, dirName));
14300
+ const plans = phaseFiles.filter((f) => isPlanFile(f));
14301
+ const summaries = phaseFiles.filter((f) => isSummaryFile(f));
14302
+ const summaryBases = new Set(summaries.map((s) => summaryId(s)));
14213
14303
  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");
14304
+ const planBase = planId(plan);
14305
+ if (!summaryBases.has(planBase)) addIssue("info", "I001", `${dirName}/${plan} has no SUMMARY.md`, "May be in progress");
14216
14306
  }
14217
14307
  }
14218
14308
  } catch (e) {
14219
- if (process.env.MAXSIM_DEBUG) console.error(e);
14309
+ debugLog(e);
14220
14310
  }
14221
- if (node_fs.default.existsSync(roadmapPath)) {
14222
- const roadmapContent = node_fs.default.readFileSync(roadmapPath, "utf-8");
14311
+ if (node_fs.default.existsSync(rmPath)) {
14312
+ const roadmapContent = node_fs.default.readFileSync(rmPath, "utf-8");
14223
14313
  const roadmapPhases = /* @__PURE__ */ new Set();
14224
14314
  const phasePattern = getPhasePattern();
14225
14315
  let m;
14226
14316
  while ((m = phasePattern.exec(roadmapContent)) !== null) roadmapPhases.add(m[1]);
14227
14317
  const diskPhases = /* @__PURE__ */ new Set();
14228
14318
  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);
14319
+ for (const dir of listSubDirs(phasesDir)) {
14320
+ const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)?)/i);
14232
14321
  if (dm) diskPhases.add(dm[1]);
14233
14322
  }
14234
14323
  } catch (e) {
14235
- if (process.env.MAXSIM_DEBUG) console.error(e);
14324
+ debugLog(e);
14236
14325
  }
14237
14326
  for (const p of roadmapPhases) {
14238
14327
  const padded = String(parseInt(p, 10)).padStart(2, "0");
@@ -14248,7 +14337,7 @@ function cmdValidateHealth(cwd, options, raw) {
14248
14337
  switch (repair) {
14249
14338
  case "createConfig":
14250
14339
  case "resetConfig":
14251
- node_fs.default.writeFileSync(configPath, JSON.stringify({
14340
+ node_fs.default.writeFileSync(cfgPath, JSON.stringify({
14252
14341
  model_profile: "balanced",
14253
14342
  commit_docs: true,
14254
14343
  search_gitignored: false,
@@ -14265,9 +14354,9 @@ function cmdValidateHealth(cwd, options, raw) {
14265
14354
  });
14266
14355
  break;
14267
14356
  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);
14357
+ if (node_fs.default.existsSync(stPath)) {
14358
+ const backupPath = `${stPath}.bak-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
14359
+ node_fs.default.copyFileSync(stPath, backupPath);
14271
14360
  repairActions.push({
14272
14361
  action: "backupState",
14273
14362
  success: true,
@@ -14283,8 +14372,8 @@ function cmdValidateHealth(cwd, options, raw) {
14283
14372
  stateContent += `**Current phase:** (determining...)\n`;
14284
14373
  stateContent += `**Status:** Resuming\n\n`;
14285
14374
  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");
14375
+ stateContent += `- ${todayISO()}: STATE.md regenerated by /maxsim:health --repair\n`;
14376
+ node_fs.default.writeFileSync(stPath, stateContent, "utf-8");
14288
14377
  repairActions.push({
14289
14378
  action: repair,
14290
14379
  success: true,
@@ -14323,10 +14412,170 @@ function cmdValidateHealth(cwd, options, raw) {
14323
14412
  *
14324
14413
  * Ported from maxsim/bin/lib/phase.cjs
14325
14414
  */
14415
+ function scaffoldPhaseStubs(dirPath, phaseId, name) {
14416
+ const today = todayISO();
14417
+ 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`);
14418
+ 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`);
14419
+ }
14420
+ function phaseAddCore(cwd, 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 phasePattern = getPhasePattern();
14426
+ let maxPhase = 0;
14427
+ let m;
14428
+ while ((m = phasePattern.exec(content)) !== null) {
14429
+ const num = parseInt(m[1], 10);
14430
+ if (num > maxPhase) maxPhase = num;
14431
+ }
14432
+ const newPhaseNum = maxPhase + 1;
14433
+ const paddedNum = String(newPhaseNum).padStart(2, "0");
14434
+ const dirName = `${paddedNum}-${slug}`;
14435
+ const dirPath = planningPath(cwd, "phases", dirName);
14436
+ node_fs.default.mkdirSync(dirPath, { recursive: true });
14437
+ node_fs.default.writeFileSync(node_path.default.join(dirPath, ".gitkeep"), "");
14438
+ if (options?.includeStubs) scaffoldPhaseStubs(dirPath, paddedNum, description);
14439
+ 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`;
14440
+ let updatedContent;
14441
+ const lastSeparator = content.lastIndexOf("\n---");
14442
+ if (lastSeparator > 0) updatedContent = content.slice(0, lastSeparator) + phaseEntry + content.slice(lastSeparator);
14443
+ else updatedContent = content + phaseEntry;
14444
+ node_fs.default.writeFileSync(rmPath, updatedContent, "utf-8");
14445
+ return {
14446
+ phase_number: newPhaseNum,
14447
+ padded: paddedNum,
14448
+ slug,
14449
+ directory: `.planning/phases/${dirName}`,
14450
+ description
14451
+ };
14452
+ }
14453
+ function phaseInsertCore(cwd, afterPhase, description, options) {
14454
+ const rmPath = roadmapPath(cwd);
14455
+ if (!node_fs.default.existsSync(rmPath)) throw new Error("ROADMAP.md not found");
14456
+ const content = node_fs.default.readFileSync(rmPath, "utf-8");
14457
+ const slug = generateSlugInternal(description);
14458
+ const afterPhaseEscaped = "0*" + normalizePhaseName(afterPhase).replace(/^0+/, "").replace(/\./g, "\\.");
14459
+ if (!getPhasePattern(afterPhaseEscaped, "i").test(content)) throw new Error(`Phase ${afterPhase} not found in ROADMAP.md`);
14460
+ const phasesDirPath = phasesPath(cwd);
14461
+ const normalizedBase = normalizePhaseName(afterPhase);
14462
+ const existingDecimals = [];
14463
+ try {
14464
+ const dirs = listSubDirs(phasesDirPath);
14465
+ const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
14466
+ for (const dir of dirs) {
14467
+ const dm = dir.match(decimalPattern);
14468
+ if (dm) existingDecimals.push(parseInt(dm[1], 10));
14469
+ }
14470
+ } catch (e) {
14471
+ debugLog(e);
14472
+ }
14473
+ const decimalPhase = `${normalizedBase}.${existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1}`;
14474
+ const dirName = `${decimalPhase}-${slug}`;
14475
+ const dirPath = planningPath(cwd, "phases", dirName);
14476
+ node_fs.default.mkdirSync(dirPath, { recursive: true });
14477
+ node_fs.default.writeFileSync(node_path.default.join(dirPath, ".gitkeep"), "");
14478
+ if (options?.includeStubs) scaffoldPhaseStubs(dirPath, decimalPhase, description);
14479
+ 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`;
14480
+ const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, "i");
14481
+ const headerMatch = content.match(headerPattern);
14482
+ if (!headerMatch) throw new Error(`Could not find Phase ${afterPhase} header`);
14483
+ const headerIdx = content.indexOf(headerMatch[0]);
14484
+ const nextPhaseMatch = content.slice(headerIdx + headerMatch[0].length).match(/\n#{2,4}\s+Phase\s+\d/i);
14485
+ let insertIdx;
14486
+ if (nextPhaseMatch) insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
14487
+ else insertIdx = content.length;
14488
+ const updatedContent = content.slice(0, insertIdx) + phaseEntry + content.slice(insertIdx);
14489
+ node_fs.default.writeFileSync(rmPath, updatedContent, "utf-8");
14490
+ return {
14491
+ phase_number: decimalPhase,
14492
+ after_phase: afterPhase,
14493
+ slug,
14494
+ directory: `.planning/phases/${dirName}`,
14495
+ description
14496
+ };
14497
+ }
14498
+ function phaseCompleteCore(cwd, phaseNum) {
14499
+ const rmPath = roadmapPath(cwd);
14500
+ const stPath = statePath(cwd);
14501
+ const phasesDirPath = phasesPath(cwd);
14502
+ const today = todayISO();
14503
+ const phaseInfo = findPhaseInternal(cwd, phaseNum);
14504
+ if (!phaseInfo) throw new Error(`Phase ${phaseNum} not found`);
14505
+ const planCount = phaseInfo.plans.length;
14506
+ const summaryCount = phaseInfo.summaries.length;
14507
+ let requirementsUpdated = false;
14508
+ if (node_fs.default.existsSync(rmPath)) {
14509
+ let roadmapContent = node_fs.default.readFileSync(rmPath, "utf-8");
14510
+ const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapePhaseNum(phaseNum)}[:\\s][^\\n]*)`, "i");
14511
+ roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
14512
+ const phaseEscaped = escapePhaseNum(phaseNum);
14513
+ const tablePattern = new RegExp(`(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|[^|]*\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`, "i");
14514
+ roadmapContent = roadmapContent.replace(tablePattern, `$1 Complete $2 ${today} $3`);
14515
+ const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`, "i");
14516
+ roadmapContent = roadmapContent.replace(planCountPattern, `$1${summaryCount}/${planCount} plans complete`);
14517
+ node_fs.default.writeFileSync(rmPath, roadmapContent, "utf-8");
14518
+ const reqPath = planningPath(cwd, "REQUIREMENTS.md");
14519
+ if (node_fs.default.existsSync(reqPath)) {
14520
+ const reqMatch = roadmapContent.match(new RegExp(`Phase\\s+${escapePhaseNum(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, "i"));
14521
+ if (reqMatch) {
14522
+ const reqIds = reqMatch[1].replace(/[\[\]]/g, "").split(/[,\s]+/).map((r) => r.trim()).filter(Boolean);
14523
+ let reqContent = node_fs.default.readFileSync(reqPath, "utf-8");
14524
+ for (const reqId of reqIds) {
14525
+ reqContent = reqContent.replace(new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqId}\\*\\*)`, "gi"), "$1x$2");
14526
+ reqContent = reqContent.replace(new RegExp(`(\\|\\s*${reqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, "gi"), "$1 Complete $2");
14527
+ }
14528
+ node_fs.default.writeFileSync(reqPath, reqContent, "utf-8");
14529
+ requirementsUpdated = true;
14530
+ }
14531
+ }
14532
+ }
14533
+ let nextPhaseNum = null;
14534
+ let nextPhaseName = null;
14535
+ let isLastPhase = true;
14536
+ try {
14537
+ const dirs = listSubDirs(phasesDirPath, true);
14538
+ for (const dir of dirs) {
14539
+ const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
14540
+ if (dm) {
14541
+ if (comparePhaseNum(dm[1], phaseNum) > 0) {
14542
+ nextPhaseNum = dm[1];
14543
+ nextPhaseName = dm[2] || null;
14544
+ isLastPhase = false;
14545
+ break;
14546
+ }
14547
+ }
14548
+ }
14549
+ } catch (e) {
14550
+ debugLog(e);
14551
+ }
14552
+ if (node_fs.default.existsSync(stPath)) {
14553
+ let stateContent = node_fs.default.readFileSync(stPath, "utf-8");
14554
+ stateContent = stateContent.replace(/(\*\*Current Phase:\*\*\s*).*/, `$1${nextPhaseNum || phaseNum}`);
14555
+ if (nextPhaseName) stateContent = stateContent.replace(/(\*\*Current Phase Name:\*\*\s*).*/, `$1${nextPhaseName.replace(/-/g, " ")}`);
14556
+ stateContent = stateContent.replace(/(\*\*Status:\*\*\s*).*/, `$1${isLastPhase ? "Milestone complete" : "Ready to plan"}`);
14557
+ stateContent = stateContent.replace(/(\*\*Current Plan:\*\*\s*).*/, `$1Not started`);
14558
+ stateContent = stateContent.replace(/(\*\*Last Activity:\*\*\s*).*/, `$1${today}`);
14559
+ stateContent = stateContent.replace(/(\*\*Last Activity Description:\*\*\s*).*/, `$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ""}`);
14560
+ node_fs.default.writeFileSync(stPath, stateContent, "utf-8");
14561
+ }
14562
+ return {
14563
+ completed_phase: phaseNum,
14564
+ phase_name: phaseInfo.phase_name,
14565
+ plans_executed: `${summaryCount}/${planCount}`,
14566
+ next_phase: nextPhaseNum,
14567
+ next_phase_name: nextPhaseName,
14568
+ is_last_phase: isLastPhase,
14569
+ date: today,
14570
+ roadmap_updated: node_fs.default.existsSync(rmPath),
14571
+ state_updated: node_fs.default.existsSync(stPath),
14572
+ requirements_updated: requirementsUpdated
14573
+ };
14574
+ }
14326
14575
  function cmdPhasesList(cwd, options, raw) {
14327
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
14576
+ const phasesDirPath = phasesPath(cwd);
14328
14577
  const { type, phase, includeArchived } = options;
14329
- if (!node_fs.default.existsSync(phasesDir)) {
14578
+ if (!node_fs.default.existsSync(phasesDirPath)) {
14330
14579
  if (type) output({
14331
14580
  files: [],
14332
14581
  count: 0
@@ -14338,7 +14587,7 @@ function cmdPhasesList(cwd, options, raw) {
14338
14587
  return;
14339
14588
  }
14340
14589
  try {
14341
- let dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
14590
+ let dirs = listSubDirs(phasesDirPath);
14342
14591
  if (includeArchived) {
14343
14592
  const archived = getArchivedPhaseDirs(cwd);
14344
14593
  for (const a of archived) dirs.push(`${a.name} [${a.milestone}]`);
@@ -14361,11 +14610,11 @@ function cmdPhasesList(cwd, options, raw) {
14361
14610
  if (type) {
14362
14611
  const files = [];
14363
14612
  for (const dir of dirs) {
14364
- const dirPath = node_path.default.join(phasesDir, dir);
14613
+ const dirPath = node_path.default.join(phasesDirPath, dir);
14365
14614
  const dirFiles = node_fs.default.readdirSync(dirPath);
14366
14615
  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");
14616
+ if (type === "plans") filtered = dirFiles.filter(isPlanFile);
14617
+ else if (type === "summaries") filtered = dirFiles.filter(isSummaryFile);
14369
14618
  else filtered = dirFiles;
14370
14619
  files.push(...filtered.sort());
14371
14620
  }
@@ -14381,13 +14630,14 @@ function cmdPhasesList(cwd, options, raw) {
14381
14630
  count: dirs.length
14382
14631
  }, raw, dirs.join("\n"));
14383
14632
  } catch (e) {
14633
+ rethrowCliSignals(e);
14384
14634
  error("Failed to list phases: " + e.message);
14385
14635
  }
14386
14636
  }
14387
14637
  function cmdPhaseNextDecimal(cwd, basePhase, raw) {
14388
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
14638
+ const phasesDirPath = phasesPath(cwd);
14389
14639
  const normalized = normalizePhaseName(basePhase);
14390
- if (!node_fs.default.existsSync(phasesDir)) {
14640
+ if (!node_fs.default.existsSync(phasesDirPath)) {
14391
14641
  output({
14392
14642
  found: false,
14393
14643
  base_phase: normalized,
@@ -14397,7 +14647,7 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
14397
14647
  return;
14398
14648
  }
14399
14649
  try {
14400
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
14650
+ const dirs = listSubDirs(phasesDirPath);
14401
14651
  const baseExists = dirs.some((d) => d.startsWith(normalized + "-") || d === normalized);
14402
14652
  const decimalPattern = new RegExp(`^${normalized}\\.(\\d+)`);
14403
14653
  const existingDecimals = [];
@@ -14421,12 +14671,13 @@ function cmdPhaseNextDecimal(cwd, basePhase, raw) {
14421
14671
  existing: existingDecimals
14422
14672
  }, raw, nextDecimal);
14423
14673
  } catch (e) {
14674
+ rethrowCliSignals(e);
14424
14675
  error("Failed to calculate next decimal phase: " + e.message);
14425
14676
  }
14426
14677
  }
14427
14678
  function cmdFindPhase(cwd, phase, raw) {
14428
14679
  if (!phase) error("phase identifier required");
14429
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
14680
+ const phasesDirPath = phasesPath(cwd);
14430
14681
  const normalized = normalizePhaseName(phase);
14431
14682
  const notFound = {
14432
14683
  found: false,
@@ -14437,7 +14688,7 @@ function cmdFindPhase(cwd, phase, raw) {
14437
14688
  summaries: []
14438
14689
  };
14439
14690
  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));
14691
+ const match = listSubDirs(phasesDirPath, true).find((d) => d.startsWith(normalized));
14441
14692
  if (!match) {
14442
14693
  output(notFound, raw, "");
14443
14694
  return;
@@ -14445,10 +14696,10 @@ function cmdFindPhase(cwd, phase, raw) {
14445
14696
  const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
14446
14697
  const phaseNumber = dirMatch ? dirMatch[1] : normalized;
14447
14698
  const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
14448
- const phaseDir = node_path.default.join(phasesDir, match);
14699
+ const phaseDir = node_path.default.join(phasesDirPath, match);
14449
14700
  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();
14701
+ const plans = phaseFiles.filter(isPlanFile).sort();
14702
+ const summaries = phaseFiles.filter(isSummaryFile).sort();
14452
14703
  const result = {
14453
14704
  found: true,
14454
14705
  directory: node_path.default.join(".planning", "phases", match),
@@ -14458,20 +14709,21 @@ function cmdFindPhase(cwd, phase, raw) {
14458
14709
  summaries
14459
14710
  };
14460
14711
  output(result, raw, result.directory);
14461
- } catch {
14712
+ } catch (e) {
14713
+ rethrowCliSignals(e);
14462
14714
  output(notFound, raw, "");
14463
14715
  }
14464
14716
  }
14465
14717
  function cmdPhasePlanIndex(cwd, phase, raw) {
14466
14718
  if (!phase) error("phase required for phase-plan-index");
14467
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
14719
+ const phasesDirPath = phasesPath(cwd);
14468
14720
  const normalized = normalizePhaseName(phase);
14469
14721
  let phaseDir = null;
14470
14722
  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);
14723
+ const match = listSubDirs(phasesDirPath, true).find((d) => d.startsWith(normalized));
14724
+ if (match) phaseDir = node_path.default.join(phasesDirPath, match);
14473
14725
  } catch (e) {
14474
- if (process.env.MAXSIM_DEBUG) console.error(e);
14726
+ debugLog(e);
14475
14727
  }
14476
14728
  if (!phaseDir) {
14477
14729
  output({
@@ -14485,15 +14737,15 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
14485
14737
  return;
14486
14738
  }
14487
14739
  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", "")));
14740
+ const planFiles = phaseFiles.filter(isPlanFile).sort();
14741
+ const summaryFiles = phaseFiles.filter(isSummaryFile);
14742
+ const completedPlanIds = new Set(summaryFiles.map(summaryId));
14491
14743
  const plans = [];
14492
14744
  const waves = {};
14493
14745
  const incomplete = [];
14494
14746
  let hasCheckpoints = false;
14495
14747
  for (const planFile of planFiles) {
14496
- const planId = planFile.replace("-PLAN.md", "").replace("PLAN.md", "");
14748
+ const id = planId(planFile);
14497
14749
  const planPath = node_path.default.join(phaseDir, planFile);
14498
14750
  const content = node_fs.default.readFileSync(planPath, "utf-8");
14499
14751
  const fm = extractFrontmatter(content);
@@ -14504,10 +14756,10 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
14504
14756
  if (!autonomous) hasCheckpoints = true;
14505
14757
  let filesModified = [];
14506
14758
  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);
14759
+ const hasSummary = completedPlanIds.has(id);
14760
+ if (!hasSummary) incomplete.push(id);
14509
14761
  const plan = {
14510
- id: planId,
14762
+ id,
14511
14763
  wave,
14512
14764
  autonomous,
14513
14765
  objective: fm.objective || null,
@@ -14518,7 +14770,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
14518
14770
  plans.push(plan);
14519
14771
  const waveKey = String(wave);
14520
14772
  if (!waves[waveKey]) waves[waveKey] = [];
14521
- waves[waveKey].push(planId);
14773
+ waves[waveKey].push(id);
14522
14774
  }
14523
14775
  output({
14524
14776
  phase: normalized,
@@ -14530,102 +14782,56 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
14530
14782
  }
14531
14783
  function cmdPhaseAdd(cwd, description, raw) {
14532
14784
  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;
14785
+ try {
14786
+ const result = phaseAddCore(cwd, description, { includeStubs: false });
14787
+ output({
14788
+ phase_number: result.phase_number,
14789
+ padded: result.padded,
14790
+ name: result.description,
14791
+ slug: result.slug,
14792
+ directory: result.directory
14793
+ }, raw, result.padded);
14794
+ } catch (e) {
14795
+ rethrowCliSignals(e);
14796
+ error(e.message);
14543
14797
  }
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
14798
  }
14564
14799
  function cmdPhaseInsert(cwd, afterPhase, description, raw) {
14565
14800
  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
14801
  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
- }
14802
+ const result = phaseInsertCore(cwd, afterPhase, description, { includeStubs: false });
14803
+ output({
14804
+ phase_number: result.phase_number,
14805
+ after_phase: result.after_phase,
14806
+ name: result.description,
14807
+ slug: result.slug,
14808
+ directory: result.directory
14809
+ }, raw, result.phase_number);
14582
14810
  } catch (e) {
14583
- if (process.env.MAXSIM_DEBUG) console.error(e);
14811
+ rethrowCliSignals(e);
14812
+ error(e.message);
14584
14813
  }
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
14814
  }
14609
14815
  function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14610
14816
  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");
14817
+ const rmPath = roadmapPath(cwd);
14818
+ const phasesDirPath = phasesPath(cwd);
14613
14819
  const force = options.force || false;
14614
- if (!node_fs.default.existsSync(roadmapPath)) error("ROADMAP.md not found");
14820
+ if (!node_fs.default.existsSync(rmPath)) error("ROADMAP.md not found");
14615
14821
  const normalized = normalizePhaseName(targetPhase);
14616
14822
  const isDecimal = targetPhase.includes(".");
14617
14823
  let targetDir = null;
14618
14824
  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;
14825
+ targetDir = listSubDirs(phasesDirPath, true).find((d) => d.startsWith(normalized + "-") || d === normalized) || null;
14620
14826
  } catch (e) {
14621
- if (process.env.MAXSIM_DEBUG) console.error(e);
14827
+ debugLog(e);
14622
14828
  }
14623
14829
  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");
14830
+ const targetPath = node_path.default.join(phasesDirPath, targetDir);
14831
+ const summaries = node_fs.default.readdirSync(targetPath).filter(isSummaryFile);
14626
14832
  if (summaries.length > 0) error(`Phase ${targetPhase} has ${summaries.length} executed plan(s). Use --force to remove anyway.`);
14627
14833
  }
14628
- if (targetDir) node_fs.default.rmSync(node_path.default.join(phasesDir, targetDir), {
14834
+ if (targetDir) node_fs.default.rmSync(node_path.default.join(phasesDirPath, targetDir), {
14629
14835
  recursive: true,
14630
14836
  force: true
14631
14837
  });
@@ -14636,7 +14842,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14636
14842
  const baseInt = baseParts[0];
14637
14843
  const removedDecimal = parseInt(baseParts[1], 10);
14638
14844
  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));
14845
+ const dirs = listSubDirs(phasesDirPath, true);
14640
14846
  const decPattern = new RegExp(`^${baseInt}\\.(\\d+)-(.+)$`);
14641
14847
  const toRename = [];
14642
14848
  for (const dir of dirs) {
@@ -14653,15 +14859,15 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14653
14859
  const oldPhaseId = `${baseInt}.${item.oldDecimal}`;
14654
14860
  const newPhaseId = `${baseInt}.${newDecimal}`;
14655
14861
  const newDirName = `${baseInt}.${newDecimal}-${item.slug}`;
14656
- node_fs.default.renameSync(node_path.default.join(phasesDir, item.dir), node_path.default.join(phasesDir, newDirName));
14862
+ node_fs.default.renameSync(node_path.default.join(phasesDirPath, item.dir), node_path.default.join(phasesDirPath, newDirName));
14657
14863
  renamedDirs.push({
14658
14864
  from: item.dir,
14659
14865
  to: newDirName
14660
14866
  });
14661
- const dirFiles = node_fs.default.readdirSync(node_path.default.join(phasesDir, newDirName));
14867
+ const dirFiles = node_fs.default.readdirSync(node_path.default.join(phasesDirPath, newDirName));
14662
14868
  for (const f of dirFiles) if (f.includes(oldPhaseId)) {
14663
14869
  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));
14870
+ node_fs.default.renameSync(node_path.default.join(phasesDirPath, newDirName, f), node_path.default.join(phasesDirPath, newDirName, newFileName));
14665
14871
  renamedFiles.push({
14666
14872
  from: f,
14667
14873
  to: newFileName
@@ -14669,12 +14875,12 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14669
14875
  }
14670
14876
  }
14671
14877
  } catch (e) {
14672
- if (process.env.MAXSIM_DEBUG) console.error(e);
14878
+ debugLog(e);
14673
14879
  }
14674
14880
  } else {
14675
14881
  const removedInt = parseInt(normalized, 10);
14676
14882
  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));
14883
+ const dirs = listSubDirs(phasesDirPath, true);
14678
14884
  const toRename = [];
14679
14885
  for (const dir of dirs) {
14680
14886
  const dm = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i);
@@ -14701,15 +14907,15 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14701
14907
  const oldPrefix = `${oldPadded}${letterSuffix}${decimalSuffix}`;
14702
14908
  const newPrefix = `${newPadded}${letterSuffix}${decimalSuffix}`;
14703
14909
  const newDirName = `${newPrefix}-${item.slug}`;
14704
- node_fs.default.renameSync(node_path.default.join(phasesDir, item.dir), node_path.default.join(phasesDir, newDirName));
14910
+ node_fs.default.renameSync(node_path.default.join(phasesDirPath, item.dir), node_path.default.join(phasesDirPath, newDirName));
14705
14911
  renamedDirs.push({
14706
14912
  from: item.dir,
14707
14913
  to: newDirName
14708
14914
  });
14709
- const dirFiles = node_fs.default.readdirSync(node_path.default.join(phasesDir, newDirName));
14915
+ const dirFiles = node_fs.default.readdirSync(node_path.default.join(phasesDirPath, newDirName));
14710
14916
  for (const f of dirFiles) if (f.startsWith(oldPrefix)) {
14711
14917
  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));
14918
+ node_fs.default.renameSync(node_path.default.join(phasesDirPath, newDirName, f), node_path.default.join(phasesDirPath, newDirName, newFileName));
14713
14919
  renamedFiles.push({
14714
14920
  from: f,
14715
14921
  to: newFileName
@@ -14717,11 +14923,11 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14717
14923
  }
14718
14924
  }
14719
14925
  } catch (e) {
14720
- if (process.env.MAXSIM_DEBUG) console.error(e);
14926
+ debugLog(e);
14721
14927
  }
14722
14928
  }
14723
- let roadmapContent = node_fs.default.readFileSync(roadmapPath, "utf-8");
14724
- const targetEscaped = targetPhase.replace(/\./g, "\\.");
14929
+ let roadmapContent = node_fs.default.readFileSync(rmPath, "utf-8");
14930
+ const targetEscaped = escapePhaseNum(targetPhase);
14725
14931
  const sectionPattern = new RegExp(`\\n?#{2,4}\\s*Phase\\s+${targetEscaped}\\s*:[\\s\\S]*?(?=\\n#{2,4}\\s+Phase\\s+\\d|$)`, "i");
14726
14932
  roadmapContent = roadmapContent.replace(sectionPattern, "");
14727
14933
  const checkboxPattern = new RegExp(`\\n?-\\s*\\[[ x]\\]\\s*.*Phase\\s+${targetEscaped}[:\\s][^\\n]*`, "gi");
@@ -14743,10 +14949,10 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14743
14949
  roadmapContent = roadmapContent.replace(new RegExp(`(Depends on:\\*\\*\\s*Phase\\s+)${oldStr}\\b`, "gi"), `$1${newStr}`);
14744
14950
  }
14745
14951
  }
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");
14952
+ node_fs.default.writeFileSync(rmPath, roadmapContent, "utf-8");
14953
+ const stPath = statePath(cwd);
14954
+ if (node_fs.default.existsSync(stPath)) {
14955
+ let stateContent = node_fs.default.readFileSync(stPath, "utf-8");
14750
14956
  const totalPattern = /(\*\*Total Phases:\*\*\s*)(\d+)/;
14751
14957
  const totalMatch = stateContent.match(totalPattern);
14752
14958
  if (totalMatch) {
@@ -14759,7 +14965,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14759
14965
  const oldTotal = parseInt(ofMatch[2], 10);
14760
14966
  stateContent = stateContent.replace(ofPattern, `$1${oldTotal - 1}$3`);
14761
14967
  }
14762
- node_fs.default.writeFileSync(statePath, stateContent, "utf-8");
14968
+ node_fs.default.writeFileSync(stPath, stateContent, "utf-8");
14763
14969
  }
14764
14970
  output({
14765
14971
  removed: targetPhase,
@@ -14767,84 +14973,28 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
14767
14973
  renamed_directories: renamedDirs,
14768
14974
  renamed_files: renamedFiles,
14769
14975
  roadmap_updated: true,
14770
- state_updated: node_fs.default.existsSync(statePath)
14976
+ state_updated: node_fs.default.existsSync(stPath)
14771
14977
  }, raw);
14772
14978
  }
14773
14979
  function cmdPhaseComplete(cwd, phaseNum, raw) {
14774
14980
  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
14981
  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
- }
14982
+ const result = phaseCompleteCore(cwd, phaseNum);
14983
+ output({
14984
+ completed_phase: result.completed_phase,
14985
+ phase_name: result.phase_name,
14986
+ plans_executed: result.plans_executed,
14987
+ next_phase: result.next_phase,
14988
+ next_phase_name: result.next_phase_name,
14989
+ is_last_phase: result.is_last_phase,
14990
+ date: result.date,
14991
+ roadmap_updated: result.roadmap_updated,
14992
+ state_updated: result.state_updated
14993
+ }, raw);
14824
14994
  } 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");
14995
+ rethrowCliSignals(e);
14996
+ error(e.message);
14836
14997
  }
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
14998
  }
14849
14999
 
14850
15000
  //#endregion
@@ -14902,7 +15052,7 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
14902
15052
  return;
14903
15053
  }
14904
15054
  const padded = normalizePhaseName(options.phase);
14905
- const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
15055
+ const today = todayISO();
14906
15056
  const phaseName = options.name || phaseInfo.phase_name || "Unnamed";
14907
15057
  const phaseId = `${padded}-${phaseInfo.phase_slug || generateSlugInternal(phaseName)}`;
14908
15058
  const planNum = (options.plan || "01").padStart(2, "0");
@@ -15068,6 +15218,155 @@ function cmdTemplateFill(cwd, templateType, options, raw) {
15068
15218
  }, raw, relPath);
15069
15219
  }
15070
15220
 
15221
+ //#endregion
15222
+ //#region src/core/dashboard-launcher.ts
15223
+ /**
15224
+ * Dashboard Launcher — Shared dashboard lifecycle utilities
15225
+ *
15226
+ * Used by both cli.ts (tool-router) and install.ts (npx entry point).
15227
+ */
15228
+ const DEFAULT_PORT = 3333;
15229
+ const PORT_RANGE_END = 3343;
15230
+ const HEALTH_TIMEOUT_MS = 1500;
15231
+ /**
15232
+ * Check if a dashboard health endpoint is responding on the given port.
15233
+ */
15234
+ async function checkHealth(port, timeoutMs = HEALTH_TIMEOUT_MS) {
15235
+ try {
15236
+ const controller = new AbortController();
15237
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
15238
+ const res = await fetch(`http://localhost:${port}/api/health`, { signal: controller.signal });
15239
+ clearTimeout(timer);
15240
+ if (res.ok) return (await res.json()).status === "ok";
15241
+ return false;
15242
+ } catch {
15243
+ return false;
15244
+ }
15245
+ }
15246
+ /**
15247
+ * Scan the port range for a running dashboard instance.
15248
+ * Returns the port number if found, null otherwise.
15249
+ */
15250
+ async function findRunningDashboard(timeoutMs = HEALTH_TIMEOUT_MS) {
15251
+ for (let port = DEFAULT_PORT; port <= PORT_RANGE_END; port++) if (await checkHealth(port, timeoutMs)) return port;
15252
+ return null;
15253
+ }
15254
+ /**
15255
+ * Kill processes listening on the given port. Cross-platform.
15256
+ */
15257
+ function killProcessOnPort(port) {
15258
+ if (process.platform === "win32") try {
15259
+ const lines = (0, node_child_process.execSync)(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: "utf-8" }).trim().split("\n");
15260
+ const pids = /* @__PURE__ */ new Set();
15261
+ for (const line of lines) {
15262
+ const parts = line.trim().split(/\s+/);
15263
+ const pid = parts[parts.length - 1];
15264
+ if (pid && pid !== "0") pids.add(pid);
15265
+ }
15266
+ for (const pid of pids) try {
15267
+ (0, node_child_process.execSync)(`taskkill /PID ${pid} /F`, { stdio: "ignore" });
15268
+ } catch {}
15269
+ } catch {}
15270
+ else try {
15271
+ (0, node_child_process.execSync)(`lsof -i :${port} -t | xargs kill -SIGTERM 2>/dev/null`, { stdio: "ignore" });
15272
+ } catch {}
15273
+ }
15274
+ /**
15275
+ * Resolve the dashboard server entry point path.
15276
+ * Tries: local project install, global install, @maxsim/dashboard package, monorepo walk.
15277
+ */
15278
+ function resolveDashboardServer() {
15279
+ const localDashboard = node_path.default.join(process.cwd(), ".claude", "dashboard", "server.js");
15280
+ if (node_fs.default.existsSync(localDashboard)) return localDashboard;
15281
+ const globalDashboard = node_path.default.join(node_os.default.homedir(), ".claude", "dashboard", "server.js");
15282
+ if (node_fs.default.existsSync(globalDashboard)) return globalDashboard;
15283
+ try {
15284
+ const pkgPath = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href).resolve("@maxsim/dashboard/package.json");
15285
+ const pkgDir = node_path.default.dirname(pkgPath);
15286
+ const serverJs = node_path.default.join(pkgDir, "server.js");
15287
+ if (node_fs.default.existsSync(serverJs)) return serverJs;
15288
+ const serverTs = node_path.default.join(pkgDir, "server.ts");
15289
+ if (node_fs.default.existsSync(serverTs)) return serverTs;
15290
+ } catch {}
15291
+ try {
15292
+ let dir = node_path.default.dirname(new URL(require("url").pathToFileURL(__filename).href).pathname);
15293
+ if (process.platform === "win32" && dir.startsWith("/")) dir = dir.slice(1);
15294
+ for (let i = 0; i < 5; i++) {
15295
+ const candidate = node_path.default.join(dir, "packages", "dashboard", "server.ts");
15296
+ if (node_fs.default.existsSync(candidate)) return candidate;
15297
+ const candidateJs = node_path.default.join(dir, "packages", "dashboard", "server.js");
15298
+ if (node_fs.default.existsSync(candidateJs)) return candidateJs;
15299
+ dir = node_path.default.dirname(dir);
15300
+ }
15301
+ } catch {}
15302
+ return null;
15303
+ }
15304
+ /**
15305
+ * Ensure node-pty is installed in the dashboard directory.
15306
+ * Returns true if node-pty is available after this call.
15307
+ */
15308
+ function ensureNodePty(serverDir) {
15309
+ const ptyModulePath = node_path.default.join(serverDir, "node_modules", "node-pty");
15310
+ if (node_fs.default.existsSync(ptyModulePath)) return true;
15311
+ const dashPkgPath = node_path.default.join(serverDir, "package.json");
15312
+ if (!node_fs.default.existsSync(dashPkgPath)) node_fs.default.writeFileSync(dashPkgPath, "{\"private\":true}\n");
15313
+ try {
15314
+ (0, node_child_process.execSync)("npm install node-pty --save-optional --no-audit --no-fund --loglevel=error", {
15315
+ cwd: serverDir,
15316
+ stdio: "inherit",
15317
+ timeout: 12e4
15318
+ });
15319
+ return true;
15320
+ } catch {
15321
+ return false;
15322
+ }
15323
+ }
15324
+ /**
15325
+ * Read dashboard.json config from the parent directory of the dashboard dir.
15326
+ */
15327
+ function readDashboardConfig(serverPath) {
15328
+ const dashboardDir = node_path.default.dirname(serverPath);
15329
+ const dashboardConfigPath = node_path.default.join(node_path.default.dirname(dashboardDir), "dashboard.json");
15330
+ let projectCwd = process.cwd();
15331
+ let networkMode = false;
15332
+ if (node_fs.default.existsSync(dashboardConfigPath)) try {
15333
+ const config = JSON.parse(node_fs.default.readFileSync(dashboardConfigPath, "utf8"));
15334
+ if (config.projectCwd) projectCwd = config.projectCwd;
15335
+ networkMode = config.networkMode ?? false;
15336
+ } catch {}
15337
+ return {
15338
+ projectCwd,
15339
+ networkMode
15340
+ };
15341
+ }
15342
+ /**
15343
+ * Spawn the dashboard server as a detached background process.
15344
+ * Returns the child process PID, or null if spawn failed.
15345
+ */
15346
+ function spawnDashboard(options) {
15347
+ const { serverPath, projectCwd, networkMode = false, nodeEnv = "production" } = options;
15348
+ const serverDir = node_path.default.dirname(serverPath);
15349
+ const isTsFile = serverPath.endsWith(".ts");
15350
+ const child = (0, node_child_process.spawn)("node", isTsFile ? [
15351
+ "--import",
15352
+ "tsx",
15353
+ serverPath
15354
+ ] : [serverPath], {
15355
+ cwd: serverDir,
15356
+ detached: true,
15357
+ stdio: "ignore",
15358
+ env: {
15359
+ ...process.env,
15360
+ MAXSIM_PROJECT_CWD: projectCwd,
15361
+ MAXSIM_NETWORK_MODE: networkMode ? "1" : "0",
15362
+ NODE_ENV: isTsFile ? "development" : nodeEnv
15363
+ },
15364
+ ...process.platform === "win32" ? { shell: true } : {}
15365
+ });
15366
+ child.unref();
15367
+ return child.pid ?? null;
15368
+ }
15369
+
15071
15370
  //#endregion
15072
15371
  //#region src/core/init.ts
15073
15372
  /**
@@ -15094,7 +15393,7 @@ function scanPhaseArtifacts(cwd, phaseDirectory) {
15094
15393
  const uatFile = files.find((f) => f.endsWith("-UAT.md") || f === "UAT.md");
15095
15394
  if (uatFile) result.uat_path = node_path.default.join(phaseDirectory, uatFile);
15096
15395
  } catch (e) {
15097
- if (process.env.MAXSIM_DEBUG) console.error(e);
15396
+ debugLog(e);
15098
15397
  }
15099
15398
  return result;
15100
15399
  }
@@ -15147,7 +15446,6 @@ function cmdInitPlanPhase(cwd, phase, raw) {
15147
15446
  checker_model: resolveModelInternal(cwd, "maxsim-plan-checker"),
15148
15447
  research_enabled: config.research,
15149
15448
  plan_checker_enabled: config.plan_checker,
15150
- nyquist_validation_enabled: false,
15151
15449
  commit_docs: config.commit_docs,
15152
15450
  phase_found: !!phaseInfo,
15153
15451
  phase_dir: phaseInfo?.directory ?? null,
@@ -15193,7 +15491,7 @@ function cmdInitNewProject(cwd, raw) {
15193
15491
  ]
15194
15492
  }).trim().length > 0;
15195
15493
  } catch (e) {
15196
- if (process.env.MAXSIM_DEBUG) console.error(e);
15494
+ debugLog(e);
15197
15495
  }
15198
15496
  hasPackageFile = pathExistsInternal(cwd, "package.json") || pathExistsInternal(cwd, "requirements.txt") || pathExistsInternal(cwd, "Cargo.toml") || pathExistsInternal(cwd, "go.mod") || pathExistsInternal(cwd, "Package.swift");
15199
15497
  output({
@@ -15236,13 +15534,13 @@ function cmdInitQuick(cwd, description, raw) {
15236
15534
  const config = loadConfig(cwd);
15237
15535
  const now = /* @__PURE__ */ new Date();
15238
15536
  const slug = description ? generateSlugInternal(description)?.substring(0, 40) ?? null : null;
15239
- const quickDir = node_path.default.join(cwd, ".planning", "quick");
15537
+ const quickDir = planningPath(cwd, "quick");
15240
15538
  let nextNum = 1;
15241
15539
  try {
15242
15540
  const existing = node_fs.default.readdirSync(quickDir).filter((f) => /^\d+-/.test(f)).map((f) => parseInt(f.split("-")[0], 10)).filter((n) => !isNaN(n));
15243
15541
  if (existing.length > 0) nextNum = Math.max(...existing) + 1;
15244
15542
  } catch (e) {
15245
- if (process.env.MAXSIM_DEBUG) console.error(e);
15543
+ debugLog(e);
15246
15544
  }
15247
15545
  output({
15248
15546
  planner_model: resolveModelInternal(cwd, "maxsim-planner"),
@@ -15253,7 +15551,7 @@ function cmdInitQuick(cwd, description, raw) {
15253
15551
  next_num: nextNum,
15254
15552
  slug,
15255
15553
  description: description ?? null,
15256
- date: now.toISOString().split("T")[0],
15554
+ date: todayISO(),
15257
15555
  timestamp: now.toISOString(),
15258
15556
  quick_dir: ".planning/quick",
15259
15557
  task_dir: slug ? `.planning/quick/${nextNum}-${slug}` : null,
@@ -15265,9 +15563,9 @@ function cmdInitResume(cwd, raw) {
15265
15563
  const config = loadConfig(cwd);
15266
15564
  let interruptedAgentId = null;
15267
15565
  try {
15268
- interruptedAgentId = node_fs.default.readFileSync(node_path.default.join(cwd, ".planning", "current-agent-id.txt"), "utf-8").trim();
15566
+ interruptedAgentId = node_fs.default.readFileSync(planningPath(cwd, "current-agent-id.txt"), "utf-8").trim();
15269
15567
  } catch (e) {
15270
- if (process.env.MAXSIM_DEBUG) console.error(e);
15568
+ debugLog(e);
15271
15569
  }
15272
15570
  output({
15273
15571
  state_exists: pathExistsInternal(cwd, ".planning/STATE.md"),
@@ -15351,7 +15649,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
15351
15649
  function cmdInitTodos(cwd, area, raw) {
15352
15650
  const config = loadConfig(cwd);
15353
15651
  const now = /* @__PURE__ */ new Date();
15354
- const pendingDir = node_path.default.join(cwd, ".planning", "todos", "pending");
15652
+ const pendingDir = planningPath(cwd, "todos", "pending");
15355
15653
  let count = 0;
15356
15654
  const todos = [];
15357
15655
  try {
@@ -15372,14 +15670,14 @@ function cmdInitTodos(cwd, area, raw) {
15372
15670
  path: node_path.default.join(".planning", "todos", "pending", file)
15373
15671
  });
15374
15672
  } catch (e) {
15375
- if (process.env.MAXSIM_DEBUG) console.error(e);
15673
+ debugLog(e);
15376
15674
  }
15377
15675
  } catch (e) {
15378
- if (process.env.MAXSIM_DEBUG) console.error(e);
15676
+ debugLog(e);
15379
15677
  }
15380
15678
  output({
15381
15679
  commit_docs: config.commit_docs,
15382
- date: now.toISOString().split("T")[0],
15680
+ date: todayISO(),
15383
15681
  timestamp: now.toISOString(),
15384
15682
  todo_count: count,
15385
15683
  todos,
@@ -15396,24 +15694,24 @@ function cmdInitMilestoneOp(cwd, raw) {
15396
15694
  const milestone = getMilestoneInfo(cwd);
15397
15695
  let phaseCount = 0;
15398
15696
  let completedPhases = 0;
15399
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
15697
+ const phasesDir = phasesPath(cwd);
15400
15698
  try {
15401
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
15699
+ const dirs = listSubDirs(phasesDir);
15402
15700
  phaseCount = dirs.length;
15403
15701
  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++;
15702
+ if (node_fs.default.readdirSync(node_path.default.join(phasesDir, dir)).some((f) => isSummaryFile(f))) completedPhases++;
15405
15703
  } catch (e) {
15406
- if (process.env.MAXSIM_DEBUG) console.error(e);
15704
+ debugLog(e);
15407
15705
  }
15408
15706
  } catch (e) {
15409
- if (process.env.MAXSIM_DEBUG) console.error(e);
15707
+ debugLog(e);
15410
15708
  }
15411
- const archiveDir = node_path.default.join(cwd, ".planning", "archive");
15709
+ const archiveDir = planningPath(cwd, "archive");
15412
15710
  let archivedMilestones = [];
15413
15711
  try {
15414
- archivedMilestones = node_fs.default.readdirSync(archiveDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
15712
+ archivedMilestones = listSubDirs(archiveDir);
15415
15713
  } catch (e) {
15416
- if (process.env.MAXSIM_DEBUG) console.error(e);
15714
+ debugLog(e);
15417
15715
  }
15418
15716
  output({
15419
15717
  commit_docs: config.commit_docs,
@@ -15434,12 +15732,12 @@ function cmdInitMilestoneOp(cwd, raw) {
15434
15732
  }
15435
15733
  function cmdInitMapCodebase(cwd, raw) {
15436
15734
  const config = loadConfig(cwd);
15437
- const codebaseDir = node_path.default.join(cwd, ".planning", "codebase");
15735
+ const codebaseDir = planningPath(cwd, "codebase");
15438
15736
  let existingMaps = [];
15439
15737
  try {
15440
15738
  existingMaps = node_fs.default.readdirSync(codebaseDir).filter((f) => f.endsWith(".md"));
15441
15739
  } catch (e) {
15442
- if (process.env.MAXSIM_DEBUG) console.error(e);
15740
+ debugLog(e);
15443
15741
  }
15444
15742
  output({
15445
15743
  mapper_model: resolveModelInternal(cwd, "maxsim-codebase-mapper"),
@@ -15471,15 +15769,15 @@ function cmdInitExisting(cwd, raw) {
15471
15769
  ]
15472
15770
  }).trim().length > 0;
15473
15771
  } catch (e) {
15474
- if (process.env.MAXSIM_DEBUG) console.error(e);
15772
+ debugLog(e);
15475
15773
  }
15476
15774
  hasPackageFile = pathExistsInternal(cwd, "package.json") || pathExistsInternal(cwd, "requirements.txt") || pathExistsInternal(cwd, "Cargo.toml") || pathExistsInternal(cwd, "go.mod") || pathExistsInternal(cwd, "Package.swift");
15477
15775
  let planningFiles = [];
15478
15776
  try {
15479
- const planDir = node_path.default.join(cwd, ".planning");
15777
+ const planDir = planningPath(cwd);
15480
15778
  if (node_fs.default.existsSync(planDir)) planningFiles = node_fs.default.readdirSync(planDir, { recursive: true }).map((f) => String(f)).filter((f) => !f.startsWith("."));
15481
15779
  } catch (e) {
15482
- if (process.env.MAXSIM_DEBUG) console.error(e);
15780
+ debugLog(e);
15483
15781
  }
15484
15782
  output({
15485
15783
  researcher_model: resolveModelInternal(cwd, "maxsim-project-researcher"),
@@ -15506,20 +15804,20 @@ function cmdInitExisting(cwd, raw) {
15506
15804
  function cmdInitProgress(cwd, raw) {
15507
15805
  const config = loadConfig(cwd);
15508
15806
  const milestone = getMilestoneInfo(cwd);
15509
- const phasesDir = node_path.default.join(cwd, ".planning", "phases");
15807
+ const progressPhasesDir = phasesPath(cwd);
15510
15808
  const phases = [];
15511
15809
  let currentPhase = null;
15512
15810
  let nextPhase = null;
15513
15811
  try {
15514
- const dirs = node_fs.default.readdirSync(phasesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
15812
+ const dirs = listSubDirs(progressPhasesDir, true);
15515
15813
  for (const dir of dirs) {
15516
15814
  const match = dir.match(/^(\d+(?:\.\d+)?)-?(.*)/);
15517
15815
  const phaseNumber = match ? match[1] : dir;
15518
15816
  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");
15817
+ const phaseDirPath = node_path.default.join(progressPhasesDir, dir);
15818
+ const phaseFiles = node_fs.default.readdirSync(phaseDirPath);
15819
+ const plans = phaseFiles.filter((f) => isPlanFile(f));
15820
+ const summaries = phaseFiles.filter((f) => isSummaryFile(f));
15523
15821
  const hasResearch = phaseFiles.some((f) => f.endsWith("-RESEARCH.md") || f === "RESEARCH.md");
15524
15822
  const status = summaries.length >= plans.length && plans.length > 0 ? "complete" : plans.length > 0 ? "in_progress" : hasResearch ? "researched" : "pending";
15525
15823
  const phaseInfo = {
@@ -15536,14 +15834,14 @@ function cmdInitProgress(cwd, raw) {
15536
15834
  if (!nextPhase && status === "pending") nextPhase = phaseInfo;
15537
15835
  }
15538
15836
  } catch (e) {
15539
- if (process.env.MAXSIM_DEBUG) console.error(e);
15837
+ debugLog(e);
15540
15838
  }
15541
15839
  let pausedAt = null;
15542
15840
  try {
15543
- const pauseMatch = node_fs.default.readFileSync(node_path.default.join(cwd, ".planning", "STATE.md"), "utf-8").match(/\*\*Paused At:\*\*\s*(.+)/);
15841
+ const pauseMatch = node_fs.default.readFileSync(planningPath(cwd, "STATE.md"), "utf-8").match(/\*\*Paused At:\*\*\s*(.+)/);
15544
15842
  if (pauseMatch) pausedAt = pauseMatch[1].trim();
15545
15843
  } catch (e) {
15546
- if (process.env.MAXSIM_DEBUG) console.error(e);
15844
+ debugLog(e);
15547
15845
  }
15548
15846
  output({
15549
15847
  executor_model: resolveModelInternal(cwd, "maxsim-executor"),
@@ -15828,33 +16126,49 @@ const COMMANDS = {
15828
16126
  freshness: f.freshness ?? void 0
15829
16127
  }, raw);
15830
16128
  },
15831
- "dashboard": (args) => handleDashboard(args.slice(1))
16129
+ "dashboard": (args) => handleDashboard(args.slice(1)),
16130
+ "start-server": async () => {
16131
+ const serverPath = node_path.join(__dirname, "mcp-server.cjs");
16132
+ (0, node_child_process.spawn)(process.execPath, [serverPath], { stdio: "inherit" }).on("exit", (code) => process.exit(code ?? 0));
16133
+ }
15832
16134
  };
15833
16135
  async function main() {
15834
- const args = process.argv.slice(2);
15835
- let cwd = process.cwd();
15836
- const cwdEqArg = args.find((arg) => arg.startsWith("--cwd="));
15837
- const cwdIdx = args.indexOf("--cwd");
15838
- if (cwdEqArg) {
15839
- const value = cwdEqArg.slice(6).trim();
15840
- if (!value) error("Missing value for --cwd");
15841
- args.splice(args.indexOf(cwdEqArg), 1);
15842
- cwd = node_path.resolve(value);
15843
- } else if (cwdIdx !== -1) {
15844
- const value = args[cwdIdx + 1];
15845
- if (!value || value.startsWith("--")) error("Missing value for --cwd");
15846
- args.splice(cwdIdx, 2);
15847
- cwd = node_path.resolve(value);
15848
- }
15849
- if (!node_fs.existsSync(cwd) || !node_fs.statSync(cwd).isDirectory()) error(`Invalid --cwd: ${cwd}`);
15850
- const rawIndex = args.indexOf("--raw");
15851
- const raw = rawIndex !== -1;
15852
- if (rawIndex !== -1) args.splice(rawIndex, 1);
15853
- const command = args[0];
15854
- if (!command) error(`Usage: maxsim-tools <command> [args] [--raw] [--cwd <path>]\nCommands: ${Object.keys(COMMANDS).join(", ")}`);
15855
- const handler = COMMANDS[command];
15856
- if (!handler) error(`Unknown command: ${command}`);
15857
- await handler(args, cwd, raw);
16136
+ try {
16137
+ const args = process.argv.slice(2);
16138
+ let cwd = process.cwd();
16139
+ const cwdEqArg = args.find((arg) => arg.startsWith("--cwd="));
16140
+ const cwdIdx = args.indexOf("--cwd");
16141
+ if (cwdEqArg) {
16142
+ const value = cwdEqArg.slice(6).trim();
16143
+ if (!value) error("Missing value for --cwd");
16144
+ args.splice(args.indexOf(cwdEqArg), 1);
16145
+ cwd = node_path.resolve(value);
16146
+ } else if (cwdIdx !== -1) {
16147
+ const value = args[cwdIdx + 1];
16148
+ if (!value || value.startsWith("--")) error("Missing value for --cwd");
16149
+ args.splice(cwdIdx, 2);
16150
+ cwd = node_path.resolve(value);
16151
+ }
16152
+ if (!node_fs.existsSync(cwd) || !node_fs.statSync(cwd).isDirectory()) error(`Invalid --cwd: ${cwd}`);
16153
+ const rawIndex = args.indexOf("--raw");
16154
+ const raw = rawIndex !== -1;
16155
+ if (rawIndex !== -1) args.splice(rawIndex, 1);
16156
+ const command = args[0];
16157
+ if (!command) error(`Usage: maxsim-tools <command> [args] [--raw] [--cwd <path>]\nCommands: ${Object.keys(COMMANDS).join(", ")}`);
16158
+ const handler = COMMANDS[command];
16159
+ if (!handler) error(`Unknown command: ${command}`);
16160
+ await handler(args, cwd, raw);
16161
+ } catch (thrown) {
16162
+ if (thrown instanceof CliOutput) {
16163
+ writeOutput(thrown);
16164
+ process.exit(0);
16165
+ }
16166
+ if (thrown instanceof CliError) {
16167
+ process.stderr.write("Error: " + thrown.message + "\n");
16168
+ process.exit(1);
16169
+ }
16170
+ throw thrown;
16171
+ }
15858
16172
  }
15859
16173
  /**
15860
16174
  * Dashboard launch command.
@@ -15864,40 +16178,20 @@ async function main() {
15864
16178
  * Supports --stop to kill a running instance.
15865
16179
  */
15866
16180
  async function handleDashboard(args) {
15867
- const DEFAULT_PORT = 3333;
15868
- const PORT_RANGE_END = 3343;
15869
- const HEALTH_TIMEOUT_MS = 1500;
15870
16181
  const networkMode = args.includes("--network");
15871
16182
  if (args.includes("--stop")) {
15872
- for (let port = DEFAULT_PORT; port <= PORT_RANGE_END; port++) if (await checkHealth(port, HEALTH_TIMEOUT_MS)) {
15873
- console.log(`Dashboard found on port ${port} — sending shutdown...`);
15874
- console.log(`Dashboard at http://localhost:${port} is running.`);
15875
- console.log(`To stop it, close the browser tab or kill the process on port ${port}.`);
15876
- try {
15877
- if (process.platform === "win32") {
15878
- const lines = (0, node_child_process.execSync)(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: "utf-8" }).trim().split("\n");
15879
- const pids = /* @__PURE__ */ new Set();
15880
- for (const line of lines) {
15881
- const parts = line.trim().split(/\s+/);
15882
- const pid = parts[parts.length - 1];
15883
- if (pid && pid !== "0") pids.add(pid);
15884
- }
15885
- for (const pid of pids) try {
15886
- (0, node_child_process.execSync)(`taskkill /PID ${pid} /F`, { stdio: "ignore" });
15887
- console.log(`Killed process ${pid}`);
15888
- } catch {}
15889
- } else (0, node_child_process.execSync)(`lsof -i :${port} -t | xargs kill -SIGTERM 2>/dev/null`, { stdio: "ignore" });
15890
- console.log("Dashboard stopped.");
15891
- } catch {
15892
- console.log("Could not automatically stop the dashboard. Kill the process manually.");
15893
- }
16183
+ for (let port = DEFAULT_PORT; port <= PORT_RANGE_END; port++) if (await checkHealth(port)) {
16184
+ console.log(`Dashboard found on port ${port} — stopping...`);
16185
+ killProcessOnPort(port);
16186
+ console.log("Dashboard stopped.");
15894
16187
  return;
15895
16188
  }
15896
16189
  console.log("No running dashboard found.");
15897
16190
  return;
15898
16191
  }
15899
- for (let port = DEFAULT_PORT; port <= PORT_RANGE_END; port++) if (await checkHealth(port, HEALTH_TIMEOUT_MS)) {
15900
- console.log(`Dashboard already running at http://localhost:${port}`);
16192
+ const runningPort = await findRunningDashboard();
16193
+ if (runningPort) {
16194
+ console.log(`Dashboard already running at http://localhost:${runningPort}`);
15901
16195
  return;
15902
16196
  }
15903
16197
  const serverPath = resolveDashboardServer();
@@ -15906,100 +16200,25 @@ async function handleDashboard(args) {
15906
16200
  console.error("Ensure @maxsim/dashboard is installed and built.");
15907
16201
  process.exit(1);
15908
16202
  }
15909
- const isTsFile = serverPath.endsWith(".ts");
15910
- const runner = "node";
15911
- const runnerArgs = isTsFile ? [
15912
- "--import",
15913
- "tsx",
15914
- serverPath
15915
- ] : [serverPath];
15916
16203
  const serverDir = node_path.dirname(serverPath);
15917
- let projectCwd = process.cwd();
15918
- const dashboardConfigPath = node_path.join(node_path.dirname(serverDir), "dashboard.json");
15919
- if (node_fs.existsSync(dashboardConfigPath)) try {
15920
- const config = JSON.parse(node_fs.readFileSync(dashboardConfigPath, "utf8"));
15921
- if (config.projectCwd) projectCwd = config.projectCwd;
15922
- } catch {}
15923
- const ptyModulePath = node_path.join(serverDir, "node_modules", "node-pty");
15924
- if (!node_fs.existsSync(ptyModulePath)) {
15925
- console.log("Installing node-pty for terminal support...");
15926
- try {
15927
- (0, node_child_process.execSync)("npm install node-pty --save-optional --no-audit --no-fund --loglevel=error", {
15928
- cwd: serverDir,
15929
- stdio: "inherit",
15930
- timeout: 12e4
15931
- });
15932
- } catch {
15933
- console.warn("node-pty installation failed — terminal will be unavailable.");
15934
- }
15935
- }
16204
+ const dashConfig = readDashboardConfig(serverPath);
16205
+ console.log("Installing node-pty for terminal support...");
16206
+ if (!ensureNodePty(serverDir)) console.warn("node-pty installation failed — terminal will be unavailable.");
15936
16207
  console.log("Dashboard starting...");
15937
- const child = (0, node_child_process.spawn)(runner, runnerArgs, {
15938
- cwd: serverDir,
15939
- detached: true,
15940
- stdio: "ignore",
15941
- env: {
15942
- ...process.env,
15943
- MAXSIM_PROJECT_CWD: projectCwd,
15944
- NODE_ENV: isTsFile ? "development" : "production",
15945
- ...networkMode ? { MAXSIM_NETWORK_MODE: "1" } : {}
15946
- },
15947
- ...process.platform === "win32" ? { shell: true } : {}
16208
+ const pid = spawnDashboard({
16209
+ serverPath,
16210
+ projectCwd: dashConfig.projectCwd,
16211
+ networkMode
15948
16212
  });
15949
- child.unref();
15950
16213
  await new Promise((resolve) => setTimeout(resolve, 3e3));
15951
- for (let port = DEFAULT_PORT; port <= PORT_RANGE_END; port++) if (await checkHealth(port, HEALTH_TIMEOUT_MS)) {
15952
- console.log(`Dashboard ready at http://localhost:${port}`);
16214
+ const readyPort = await findRunningDashboard();
16215
+ if (readyPort) {
16216
+ console.log(`Dashboard ready at http://localhost:${readyPort}`);
15953
16217
  return;
15954
16218
  }
15955
- console.log(`Dashboard spawned (PID ${child.pid}). It may take a moment to start.`);
16219
+ console.log(`Dashboard spawned (PID ${pid}). It may take a moment to start.`);
15956
16220
  console.log(`Check http://localhost:${DEFAULT_PORT} once ready.`);
15957
16221
  }
15958
- /**
15959
- * Check if a dashboard health endpoint is responding on the given port.
15960
- */
15961
- async function checkHealth(port, timeoutMs) {
15962
- try {
15963
- const controller = new AbortController();
15964
- const timer = setTimeout(() => controller.abort(), timeoutMs);
15965
- const res = await fetch(`http://localhost:${port}/api/health`, { signal: controller.signal });
15966
- clearTimeout(timer);
15967
- if (res.ok) return (await res.json()).status === "ok";
15968
- return false;
15969
- } catch {
15970
- return false;
15971
- }
15972
- }
15973
- /**
15974
- * Resolve the dashboard server entry point path.
15975
- * Tries: built server.js first, then source server.ts for dev mode.
15976
- */
15977
- function resolveDashboardServer() {
15978
- const localDashboard = node_path.join(process.cwd(), ".claude", "dashboard", "server.js");
15979
- if (node_fs.existsSync(localDashboard)) return localDashboard;
15980
- const globalDashboard = node_path.join(node_os.homedir(), ".claude", "dashboard", "server.js");
15981
- if (node_fs.existsSync(globalDashboard)) return globalDashboard;
15982
- try {
15983
- const pkgPath = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href).resolve("@maxsim/dashboard/package.json");
15984
- const pkgDir = node_path.dirname(pkgPath);
15985
- const serverJs = node_path.join(pkgDir, "server.js");
15986
- if (node_fs.existsSync(serverJs)) return serverJs;
15987
- const serverTs = node_path.join(pkgDir, "server.ts");
15988
- if (node_fs.existsSync(serverTs)) return serverTs;
15989
- } catch {}
15990
- try {
15991
- let dir = node_path.dirname(new URL(require("url").pathToFileURL(__filename).href).pathname);
15992
- if (process.platform === "win32" && dir.startsWith("/")) dir = dir.slice(1);
15993
- for (let i = 0; i < 5; i++) {
15994
- const candidate = node_path.join(dir, "packages", "dashboard", "server.ts");
15995
- if (node_fs.existsSync(candidate)) return candidate;
15996
- const candidateJs = node_path.join(dir, "packages", "dashboard", "server.js");
15997
- if (node_fs.existsSync(candidateJs)) return candidateJs;
15998
- dir = node_path.dirname(dir);
15999
- }
16000
- } catch {}
16001
- return null;
16002
- }
16003
16222
  main();
16004
16223
 
16005
16224
  //#endregion