maxsimcli 4.0.2 → 4.2.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 (127) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/assets/CHANGELOG.md +16 -0
  3. package/dist/assets/dashboard/client/assets/{index-C_eAetZJ.js → index-BcRHShXD.js} +59 -59
  4. package/dist/assets/dashboard/client/assets/index-C199D4Eb.css +32 -0
  5. package/dist/assets/dashboard/client/index.html +2 -2
  6. package/dist/assets/dashboard/server.js +26 -11
  7. package/dist/assets/templates/agents/AGENTS.md +18 -69
  8. package/dist/assets/templates/agents/maxsim-code-reviewer.md +17 -92
  9. package/dist/assets/templates/agents/maxsim-codebase-mapper.md +57 -694
  10. package/dist/assets/templates/agents/maxsim-debugger.md +80 -925
  11. package/dist/assets/templates/agents/maxsim-executor.md +94 -431
  12. package/dist/assets/templates/agents/maxsim-integration-checker.md +51 -319
  13. package/dist/assets/templates/agents/maxsim-phase-researcher.md +63 -429
  14. package/dist/assets/templates/agents/maxsim-plan-checker.md +79 -568
  15. package/dist/assets/templates/agents/maxsim-planner.md +125 -855
  16. package/dist/assets/templates/agents/maxsim-project-researcher.md +32 -472
  17. package/dist/assets/templates/agents/maxsim-research-synthesizer.md +25 -134
  18. package/dist/assets/templates/agents/maxsim-roadmapper.md +66 -480
  19. package/dist/assets/templates/agents/maxsim-spec-reviewer.md +13 -55
  20. package/dist/assets/templates/agents/maxsim-verifier.md +95 -450
  21. package/dist/assets/templates/commands/maxsim/artefakte.md +122 -0
  22. package/dist/assets/templates/commands/maxsim/batch.md +42 -0
  23. package/dist/assets/templates/commands/maxsim/check-todos.md +1 -0
  24. package/dist/assets/templates/commands/maxsim/sdd.md +39 -0
  25. package/dist/assets/templates/references/thinking-partner.md +33 -0
  26. package/dist/assets/templates/workflows/batch.md +420 -0
  27. package/dist/assets/templates/workflows/check-todos.md +85 -1
  28. package/dist/assets/templates/workflows/discuss-phase.md +31 -0
  29. package/dist/assets/templates/workflows/execute-plan.md +96 -27
  30. package/dist/assets/templates/workflows/help.md +47 -0
  31. package/dist/assets/templates/workflows/sdd.md +426 -0
  32. package/dist/backend/index.d.ts +4 -0
  33. package/dist/backend/index.d.ts.map +1 -0
  34. package/dist/backend/index.js +12 -0
  35. package/dist/backend/index.js.map +1 -0
  36. package/dist/backend/lifecycle.d.ts +13 -0
  37. package/dist/backend/lifecycle.d.ts.map +1 -0
  38. package/dist/backend/lifecycle.js +168 -0
  39. package/dist/backend/lifecycle.js.map +1 -0
  40. package/dist/backend/server.d.ts +13 -0
  41. package/dist/backend/server.d.ts.map +1 -0
  42. package/dist/backend/server.js +1013 -0
  43. package/dist/backend/server.js.map +1 -0
  44. package/dist/backend/terminal.d.ts +49 -0
  45. package/dist/backend/terminal.d.ts.map +1 -0
  46. package/dist/backend/terminal.js +209 -0
  47. package/dist/backend/terminal.js.map +1 -0
  48. package/dist/backend/types.d.ts +77 -0
  49. package/dist/backend/types.d.ts.map +1 -0
  50. package/dist/backend/types.js +6 -0
  51. package/dist/backend/types.js.map +1 -0
  52. package/dist/backend-server.cjs +80636 -0
  53. package/dist/backend-server.cjs.map +1 -0
  54. package/dist/backend-server.d.cts +2 -0
  55. package/dist/backend-server.d.ts +11 -0
  56. package/dist/backend-server.d.ts.map +1 -0
  57. package/dist/backend-server.js +43 -0
  58. package/dist/backend-server.js.map +1 -0
  59. package/dist/cli.cjs +378 -172
  60. package/dist/cli.cjs.map +1 -1
  61. package/dist/cli.js +28 -8
  62. package/dist/cli.js.map +1 -1
  63. package/dist/core/artefakte.d.ts.map +1 -1
  64. package/dist/core/artefakte.js +16 -0
  65. package/dist/core/artefakte.js.map +1 -1
  66. package/dist/core/context-loader.d.ts +1 -0
  67. package/dist/core/context-loader.d.ts.map +1 -1
  68. package/dist/core/context-loader.js +58 -0
  69. package/dist/core/context-loader.js.map +1 -1
  70. package/dist/core/core.d.ts +6 -0
  71. package/dist/core/core.d.ts.map +1 -1
  72. package/dist/core/core.js +238 -0
  73. package/dist/core/core.js.map +1 -1
  74. package/dist/core/index.d.ts +1 -1
  75. package/dist/core/index.d.ts.map +1 -1
  76. package/dist/core/index.js +5 -3
  77. package/dist/core/index.js.map +1 -1
  78. package/dist/core/phase.d.ts +11 -11
  79. package/dist/core/phase.d.ts.map +1 -1
  80. package/dist/core/phase.js +88 -73
  81. package/dist/core/phase.js.map +1 -1
  82. package/dist/core/roadmap.d.ts +2 -2
  83. package/dist/core/roadmap.d.ts.map +1 -1
  84. package/dist/core/roadmap.js +11 -10
  85. package/dist/core/roadmap.js.map +1 -1
  86. package/dist/core/skills.d.ts +4 -3
  87. package/dist/core/skills.d.ts.map +1 -1
  88. package/dist/core/skills.js +14 -15
  89. package/dist/core/skills.js.map +1 -1
  90. package/dist/core/state.d.ts +11 -11
  91. package/dist/core/state.d.ts.map +1 -1
  92. package/dist/core/state.js +60 -54
  93. package/dist/core/state.js.map +1 -1
  94. package/dist/{core-TFSlUjV1.cjs → core-RRjCSt0G.cjs} +1 -9
  95. package/dist/{core-TFSlUjV1.cjs.map → core-RRjCSt0G.cjs.map} +1 -1
  96. package/dist/esm-iIOBzmdz.cjs +1561 -0
  97. package/dist/esm-iIOBzmdz.cjs.map +1 -0
  98. package/dist/install.cjs +2 -2
  99. package/dist/lifecycle-0M4VqOMm.cjs +136 -0
  100. package/dist/lifecycle-0M4VqOMm.cjs.map +1 -0
  101. package/dist/mcp/config-tools.d.ts +13 -0
  102. package/dist/mcp/config-tools.d.ts.map +1 -0
  103. package/dist/mcp/config-tools.js +66 -0
  104. package/dist/mcp/config-tools.js.map +1 -0
  105. package/dist/mcp/context-tools.d.ts +13 -0
  106. package/dist/mcp/context-tools.d.ts.map +1 -0
  107. package/dist/mcp/context-tools.js +176 -0
  108. package/dist/mcp/context-tools.js.map +1 -0
  109. package/dist/mcp/index.d.ts +0 -1
  110. package/dist/mcp/index.d.ts.map +1 -1
  111. package/dist/mcp/index.js +6 -1
  112. package/dist/mcp/index.js.map +1 -1
  113. package/dist/mcp/phase-tools.js +3 -3
  114. package/dist/mcp/phase-tools.js.map +1 -1
  115. package/dist/mcp/roadmap-tools.d.ts +13 -0
  116. package/dist/mcp/roadmap-tools.d.ts.map +1 -0
  117. package/dist/mcp/roadmap-tools.js +79 -0
  118. package/dist/mcp/roadmap-tools.js.map +1 -0
  119. package/dist/mcp-server.cjs +799 -38
  120. package/dist/mcp-server.cjs.map +1 -1
  121. package/dist/server-G1MIg_Oe.cjs +2980 -0
  122. package/dist/server-G1MIg_Oe.cjs.map +1 -0
  123. package/dist/{skills-BOSxYUzf.cjs → skills-MYlMkYNt.cjs} +41 -29
  124. package/dist/skills-MYlMkYNt.cjs.map +1 -0
  125. package/package.json +7 -1
  126. package/dist/assets/dashboard/client/assets/index-CmiJKqOU.css +0 -32
  127. package/dist/skills-BOSxYUzf.cjs.map +0 -1
@@ -23028,7 +23028,7 @@ var require_has_flag = /* @__PURE__ */ __commonJSMin(((exports, module) => {
23028
23028
  //#endregion
23029
23029
  //#region ../../node_modules/supports-color/index.js
23030
23030
  var require_supports_color = /* @__PURE__ */ __commonJSMin(((exports, module) => {
23031
- const os$1 = require("os");
23031
+ const os$2 = require("os");
23032
23032
  const tty$1 = require("tty");
23033
23033
  const hasFlag = require_has_flag();
23034
23034
  const { env } = process;
@@ -23055,7 +23055,7 @@ var require_supports_color = /* @__PURE__ */ __commonJSMin(((exports, module) =>
23055
23055
  const min = forceColor || 0;
23056
23056
  if (env.TERM === "dumb") return min;
23057
23057
  if (process.platform === "win32") {
23058
- const osRelease = os$1.release().split(".");
23058
+ const osRelease = os$2.release().split(".");
23059
23059
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
23060
23060
  return 1;
23061
23061
  }
@@ -26756,6 +26756,9 @@ function statePath(cwd) {
26756
26756
  function roadmapPath(cwd) {
26757
26757
  return planningPath(cwd, "ROADMAP.md");
26758
26758
  }
26759
+ function configPath(cwd) {
26760
+ return planningPath(cwd, "config.json");
26761
+ }
26759
26762
  function phasesPath(cwd) {
26760
26763
  return planningPath(cwd, "phases");
26761
26764
  }
@@ -26770,6 +26773,19 @@ function listSubDirs(dir, sortByPhase = false) {
26770
26773
  const dirs = node_fs.default.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
26771
26774
  return sortByPhase ? dirs.sort((a, b) => comparePhaseNum(a, b)) : dirs;
26772
26775
  }
26776
+ /** Async version of listSubDirs using fs.promises. */
26777
+ async function listSubDirsAsync(dir, sortByPhase = false) {
26778
+ const dirs = (await node_fs.promises.readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
26779
+ return sortByPhase ? dirs.sort((a, b) => comparePhaseNum(a, b)) : dirs;
26780
+ }
26781
+ /** Async version of safeReadFile using fs.promises. */
26782
+ async function safeReadFileAsync(filePath) {
26783
+ try {
26784
+ return await node_fs.promises.readFile(filePath, "utf-8");
26785
+ } catch {
26786
+ return null;
26787
+ }
26788
+ }
26773
26789
  /** Extract a human-readable message from an unknown thrown value. */
26774
26790
  function errorMsg(e) {
26775
26791
  return e instanceof Error ? e.message : String(e);
@@ -26784,6 +26800,104 @@ function debugLog(contextOrError, error) {
26784
26800
  function escapePhaseNum(phase) {
26785
26801
  return String(phase).replace(/\./g, "\\.");
26786
26802
  }
26803
+ function safeReadFile(filePath) {
26804
+ try {
26805
+ return node_fs.default.readFileSync(filePath, "utf-8");
26806
+ } catch {
26807
+ return null;
26808
+ }
26809
+ }
26810
+ let _configCache = null;
26811
+ function loadConfig(cwd) {
26812
+ if (_configCache && _configCache.cwd === cwd) return _configCache.config;
26813
+ const cfgPath = configPath(cwd);
26814
+ const defaults = {
26815
+ model_profile: "balanced",
26816
+ commit_docs: true,
26817
+ search_gitignored: false,
26818
+ branching_strategy: "none",
26819
+ phase_branch_template: "maxsim/phase-{phase}-{slug}",
26820
+ milestone_branch_template: "maxsim/{milestone}-{slug}",
26821
+ research: true,
26822
+ plan_checker: true,
26823
+ verifier: true,
26824
+ parallelization: true,
26825
+ brave_search: false
26826
+ };
26827
+ try {
26828
+ const raw = node_fs.default.readFileSync(cfgPath, "utf-8");
26829
+ const parsed = JSON.parse(raw);
26830
+ const get = (key, nested) => {
26831
+ if (parsed[key] !== void 0) return parsed[key];
26832
+ if (nested) {
26833
+ const section = parsed[nested.section];
26834
+ if (section && typeof section === "object" && section !== null && nested.field in section) return section[nested.field];
26835
+ }
26836
+ };
26837
+ const parallelization = (() => {
26838
+ const val = get("parallelization");
26839
+ if (typeof val === "boolean") return val;
26840
+ if (typeof val === "object" && val !== null && "enabled" in val) return val.enabled;
26841
+ return defaults.parallelization;
26842
+ })();
26843
+ const result = {
26844
+ model_profile: get("model_profile") ?? defaults.model_profile,
26845
+ commit_docs: get("commit_docs", {
26846
+ section: "planning",
26847
+ field: "commit_docs"
26848
+ }) ?? defaults.commit_docs,
26849
+ search_gitignored: get("search_gitignored", {
26850
+ section: "planning",
26851
+ field: "search_gitignored"
26852
+ }) ?? defaults.search_gitignored,
26853
+ branching_strategy: get("branching_strategy", {
26854
+ section: "git",
26855
+ field: "branching_strategy"
26856
+ }) ?? defaults.branching_strategy,
26857
+ phase_branch_template: get("phase_branch_template", {
26858
+ section: "git",
26859
+ field: "phase_branch_template"
26860
+ }) ?? defaults.phase_branch_template,
26861
+ milestone_branch_template: get("milestone_branch_template", {
26862
+ section: "git",
26863
+ field: "milestone_branch_template"
26864
+ }) ?? defaults.milestone_branch_template,
26865
+ research: get("research", {
26866
+ section: "workflow",
26867
+ field: "research"
26868
+ }) ?? defaults.research,
26869
+ plan_checker: get("plan_checker", {
26870
+ section: "workflow",
26871
+ field: "plan_checker"
26872
+ }) ?? get("plan_checker", {
26873
+ section: "workflow",
26874
+ field: "plan_check"
26875
+ }) ?? defaults.plan_checker,
26876
+ verifier: get("verifier", {
26877
+ section: "workflow",
26878
+ field: "verifier"
26879
+ }) ?? defaults.verifier,
26880
+ parallelization,
26881
+ brave_search: get("brave_search") ?? defaults.brave_search,
26882
+ model_overrides: parsed["model_overrides"]
26883
+ };
26884
+ _configCache = {
26885
+ cwd,
26886
+ config: result
26887
+ };
26888
+ return result;
26889
+ } catch (e) {
26890
+ if (node_fs.default.existsSync(cfgPath)) {
26891
+ console.warn(`[maxsim] Warning: config.json exists but could not be parsed — using defaults.`);
26892
+ debugLog("config-load-failed", e);
26893
+ }
26894
+ _configCache = {
26895
+ cwd,
26896
+ config: defaults
26897
+ };
26898
+ return defaults;
26899
+ }
26900
+ }
26787
26901
  function normalizePhaseName(phase) {
26788
26902
  const match = phase.match(/^(\d+)([A-Z])?(\.\d+)?/i);
26789
26903
  if (!match) return phase;
@@ -26923,6 +27037,77 @@ function generateSlugInternal(text) {
26923
27037
  strict: true
26924
27038
  });
26925
27039
  }
27040
+ async function pathExistsAsync(p) {
27041
+ try {
27042
+ await node_fs.promises.access(p);
27043
+ return true;
27044
+ } catch {
27045
+ return false;
27046
+ }
27047
+ }
27048
+ async function searchPhaseInDirAsync(baseDir, relBase, normalized) {
27049
+ try {
27050
+ const match = (await listSubDirsAsync(baseDir, true)).find((d) => d.startsWith(normalized));
27051
+ if (!match) return null;
27052
+ const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
27053
+ const phaseNumber = dirMatch ? dirMatch[1] : normalized;
27054
+ const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
27055
+ const phaseDir = node_path.default.join(baseDir, match);
27056
+ const phaseFiles = await node_fs.promises.readdir(phaseDir);
27057
+ const plans = phaseFiles.filter(isPlanFile).sort();
27058
+ const summaries = phaseFiles.filter(isSummaryFile).sort();
27059
+ const hasResearch = phaseFiles.some((f) => f.endsWith("-RESEARCH.md") || f === "RESEARCH.md");
27060
+ const hasContext = phaseFiles.some((f) => f.endsWith("-CONTEXT.md") || f === "CONTEXT.md");
27061
+ const hasVerification = phaseFiles.some((f) => f.endsWith("-VERIFICATION.md") || f === "VERIFICATION.md");
27062
+ const completedPlanIds = new Set(summaries.map(summaryId));
27063
+ const incompletePlans = plans.filter((p) => !completedPlanIds.has(planId(p)));
27064
+ return {
27065
+ found: true,
27066
+ directory: node_path.default.join(relBase, match),
27067
+ phase_number: phaseNumber,
27068
+ phase_name: phaseName,
27069
+ phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") : null,
27070
+ plans,
27071
+ summaries,
27072
+ incomplete_plans: incompletePlans,
27073
+ has_research: hasResearch,
27074
+ has_context: hasContext,
27075
+ has_verification: hasVerification
27076
+ };
27077
+ } catch (e) {
27078
+ debugLog("search-phase-in-dir-async-failed", {
27079
+ dir: baseDir,
27080
+ phase: normalized,
27081
+ error: errorMsg(e)
27082
+ });
27083
+ return null;
27084
+ }
27085
+ }
27086
+ async function findPhaseInternalAsync(cwd, phase) {
27087
+ if (!phase) return null;
27088
+ const pd = phasesPath(cwd);
27089
+ const normalized = normalizePhaseName(phase);
27090
+ const current = await searchPhaseInDirAsync(pd, node_path.default.join(".planning", "phases"), normalized);
27091
+ if (current) return current;
27092
+ const milestonesDir = planningPath(cwd, "milestones");
27093
+ if (!await pathExistsAsync(milestonesDir)) return null;
27094
+ try {
27095
+ const archiveDirs = (await node_fs.promises.readdir(milestonesDir, { withFileTypes: true })).filter((e) => e.isDirectory() && /^v[\d.]+-phases$/.test(e.name)).map((e) => e.name).sort().reverse();
27096
+ for (const archiveName of archiveDirs) {
27097
+ const versionMatch = archiveName.match(/^(v[\d.]+)-phases$/);
27098
+ if (!versionMatch) continue;
27099
+ const version = versionMatch[1];
27100
+ const result = await searchPhaseInDirAsync(node_path.default.join(milestonesDir, archiveName), node_path.default.join(".planning", "milestones", archiveName), normalized);
27101
+ if (result) {
27102
+ result.archived = version;
27103
+ return result;
27104
+ }
27105
+ }
27106
+ } catch (e) {
27107
+ debugLog("find-phase-async-milestone-search-failed", e);
27108
+ }
27109
+ return null;
27110
+ }
26926
27111
 
26927
27112
  //#endregion
26928
27113
  //#region ../../node_modules/yaml/dist/nodes/identity.js
@@ -33574,6 +33759,23 @@ var require_dist = /* @__PURE__ */ __commonJSMin(((exports) => {
33574
33759
  exports.visitAsync = visit.visitAsync;
33575
33760
  }));
33576
33761
 
33762
+ //#endregion
33763
+ //#region src/core/types.ts
33764
+ var import_dist = /* @__PURE__ */ __toESM(require_dist());
33765
+ function cmdOk(result, rawValue) {
33766
+ return {
33767
+ ok: true,
33768
+ result,
33769
+ rawValue
33770
+ };
33771
+ }
33772
+ function cmdErr(error) {
33773
+ return {
33774
+ ok: false,
33775
+ error
33776
+ };
33777
+ }
33778
+
33577
33779
  //#endregion
33578
33780
  //#region src/core/frontmatter.ts
33579
33781
  /**
@@ -33581,7 +33783,6 @@ var require_dist = /* @__PURE__ */ __commonJSMin(((exports) => {
33581
33783
  *
33582
33784
  * Uses the `yaml` npm package instead of a hand-rolled parser.
33583
33785
  */
33584
- var import_dist = /* @__PURE__ */ __toESM(require_dist());
33585
33786
 
33586
33787
  //#endregion
33587
33788
  //#region src/core/phase.ts
@@ -33590,15 +33791,18 @@ var import_dist = /* @__PURE__ */ __toESM(require_dist());
33590
33791
  *
33591
33792
  * Ported from maxsim/bin/lib/phase.cjs
33592
33793
  */
33593
- function scaffoldPhaseStubs(dirPath, phaseId, name) {
33794
+ async function scaffoldPhaseStubs(dirPath, phaseId, name) {
33594
33795
  const today = todayISO();
33595
- 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`);
33596
- 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`);
33796
+ await Promise.all([node_fs.promises.writeFile(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`), node_fs.promises.writeFile(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`)]);
33597
33797
  }
33598
- function phaseAddCore(cwd, description, options) {
33798
+ async function phaseAddCore(cwd, description, options) {
33599
33799
  const rmPath = roadmapPath(cwd);
33600
- if (!node_fs.default.existsSync(rmPath)) throw new Error("ROADMAP.md not found");
33601
- const content = node_fs.default.readFileSync(rmPath, "utf-8");
33800
+ let content;
33801
+ try {
33802
+ content = await node_fs.promises.readFile(rmPath, "utf-8");
33803
+ } catch {
33804
+ throw new Error("ROADMAP.md not found");
33805
+ }
33602
33806
  const slug = generateSlugInternal(description);
33603
33807
  const phasePattern = getPhasePattern();
33604
33808
  let maxPhase = 0;
@@ -33611,15 +33815,15 @@ function phaseAddCore(cwd, description, options) {
33611
33815
  const paddedNum = String(newPhaseNum).padStart(2, "0");
33612
33816
  const dirName = `${paddedNum}-${slug}`;
33613
33817
  const dirPath = planningPath(cwd, "phases", dirName);
33614
- node_fs.default.mkdirSync(dirPath, { recursive: true });
33615
- node_fs.default.writeFileSync(node_path.default.join(dirPath, ".gitkeep"), "");
33616
- if (options?.includeStubs) scaffoldPhaseStubs(dirPath, paddedNum, description);
33818
+ await node_fs.promises.mkdir(dirPath, { recursive: true });
33819
+ await node_fs.promises.writeFile(node_path.default.join(dirPath, ".gitkeep"), "");
33820
+ if (options?.includeStubs) await scaffoldPhaseStubs(dirPath, paddedNum, description);
33617
33821
  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`;
33618
33822
  let updatedContent;
33619
33823
  const lastSeparator = content.lastIndexOf("\n---");
33620
33824
  if (lastSeparator > 0) updatedContent = content.slice(0, lastSeparator) + phaseEntry + content.slice(lastSeparator);
33621
33825
  else updatedContent = content + phaseEntry;
33622
- node_fs.default.writeFileSync(rmPath, updatedContent, "utf-8");
33826
+ await node_fs.promises.writeFile(rmPath, updatedContent, "utf-8");
33623
33827
  return {
33624
33828
  phase_number: newPhaseNum,
33625
33829
  padded: paddedNum,
@@ -33628,10 +33832,14 @@ function phaseAddCore(cwd, description, options) {
33628
33832
  description
33629
33833
  };
33630
33834
  }
33631
- function phaseInsertCore(cwd, afterPhase, description, options) {
33835
+ async function phaseInsertCore(cwd, afterPhase, description, options) {
33632
33836
  const rmPath = roadmapPath(cwd);
33633
- if (!node_fs.default.existsSync(rmPath)) throw new Error("ROADMAP.md not found");
33634
- const content = node_fs.default.readFileSync(rmPath, "utf-8");
33837
+ let content;
33838
+ try {
33839
+ content = await node_fs.promises.readFile(rmPath, "utf-8");
33840
+ } catch {
33841
+ throw new Error("ROADMAP.md not found");
33842
+ }
33635
33843
  const slug = generateSlugInternal(description);
33636
33844
  const afterPhaseEscaped = "0*" + normalizePhaseName(afterPhase).replace(/^0+/, "").replace(/\./g, "\\.");
33637
33845
  if (!getPhasePattern(afterPhaseEscaped, "i").test(content)) throw new Error(`Phase ${afterPhase} not found in ROADMAP.md`);
@@ -33639,7 +33847,7 @@ function phaseInsertCore(cwd, afterPhase, description, options) {
33639
33847
  const normalizedBase = normalizePhaseName(afterPhase);
33640
33848
  const existingDecimals = [];
33641
33849
  try {
33642
- const dirs = listSubDirs(phasesDirPath);
33850
+ const dirs = await listSubDirsAsync(phasesDirPath);
33643
33851
  const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
33644
33852
  for (const dir of dirs) {
33645
33853
  const dm = dir.match(decimalPattern);
@@ -33651,9 +33859,9 @@ function phaseInsertCore(cwd, afterPhase, description, options) {
33651
33859
  const decimalPhase = `${normalizedBase}.${existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1}`;
33652
33860
  const dirName = `${decimalPhase}-${slug}`;
33653
33861
  const dirPath = planningPath(cwd, "phases", dirName);
33654
- node_fs.default.mkdirSync(dirPath, { recursive: true });
33655
- node_fs.default.writeFileSync(node_path.default.join(dirPath, ".gitkeep"), "");
33656
- if (options?.includeStubs) scaffoldPhaseStubs(dirPath, decimalPhase, description);
33862
+ await node_fs.promises.mkdir(dirPath, { recursive: true });
33863
+ await node_fs.promises.writeFile(node_path.default.join(dirPath, ".gitkeep"), "");
33864
+ if (options?.includeStubs) await scaffoldPhaseStubs(dirPath, decimalPhase, description);
33657
33865
  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`;
33658
33866
  const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, "i");
33659
33867
  const headerMatch = content.match(headerPattern);
@@ -33664,7 +33872,7 @@ function phaseInsertCore(cwd, afterPhase, description, options) {
33664
33872
  if (nextPhaseMatch) insertIdx = headerIdx + headerMatch[0].length + nextPhaseMatch.index;
33665
33873
  else insertIdx = content.length;
33666
33874
  const updatedContent = content.slice(0, insertIdx) + phaseEntry + content.slice(insertIdx);
33667
- node_fs.default.writeFileSync(rmPath, updatedContent, "utf-8");
33875
+ await node_fs.promises.writeFile(rmPath, updatedContent, "utf-8");
33668
33876
  return {
33669
33877
  phase_number: decimalPhase,
33670
33878
  after_phase: afterPhase,
@@ -33673,18 +33881,19 @@ function phaseInsertCore(cwd, afterPhase, description, options) {
33673
33881
  description
33674
33882
  };
33675
33883
  }
33676
- function phaseCompleteCore(cwd, phaseNum) {
33884
+ async function phaseCompleteCore(cwd, phaseNum) {
33677
33885
  const rmPath = roadmapPath(cwd);
33678
33886
  const stPath = statePath(cwd);
33679
33887
  const phasesDirPath = phasesPath(cwd);
33680
33888
  const today = todayISO();
33681
- const phaseInfo = findPhaseInternal(cwd, phaseNum);
33889
+ const phaseInfo = await findPhaseInternalAsync(cwd, phaseNum);
33682
33890
  if (!phaseInfo) throw new Error(`Phase ${phaseNum} not found`);
33683
33891
  const planCount = phaseInfo.plans.length;
33684
33892
  const summaryCount = phaseInfo.summaries.length;
33685
33893
  let requirementsUpdated = false;
33686
- if (node_fs.default.existsSync(rmPath)) {
33687
- let roadmapContent = node_fs.default.readFileSync(rmPath, "utf-8");
33894
+ const rmExists = await pathExistsAsync(rmPath);
33895
+ if (rmExists) {
33896
+ let roadmapContent = await node_fs.promises.readFile(rmPath, "utf-8");
33688
33897
  const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapePhaseNum(phaseNum)}[:\\s][^\\n]*)`, "i");
33689
33898
  roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
33690
33899
  const phaseEscaped = escapePhaseNum(phaseNum);
@@ -33693,20 +33902,20 @@ function phaseCompleteCore(cwd, phaseNum) {
33693
33902
  const planCountPattern = new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`, "i");
33694
33903
  roadmapContent = roadmapContent.replace(planCountPattern, `$1${summaryCount}/${planCount} plans complete`);
33695
33904
  debugLog("phase-complete-write", `writing ROADMAP.md for phase ${phaseNum}`);
33696
- node_fs.default.writeFileSync(rmPath, roadmapContent, "utf-8");
33905
+ await node_fs.promises.writeFile(rmPath, roadmapContent, "utf-8");
33697
33906
  debugLog("phase-complete-write", `ROADMAP.md updated for phase ${phaseNum}`);
33698
33907
  const reqPath = planningPath(cwd, "REQUIREMENTS.md");
33699
- if (node_fs.default.existsSync(reqPath)) {
33908
+ if (await pathExistsAsync(reqPath)) {
33700
33909
  const reqMatch = roadmapContent.match(new RegExp(`Phase\\s+${escapePhaseNum(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, "i"));
33701
33910
  if (reqMatch) {
33702
33911
  const reqIds = reqMatch[1].replace(/[\[\]]/g, "").split(/[,\s]+/).map((r) => r.trim()).filter(Boolean);
33703
- let reqContent = node_fs.default.readFileSync(reqPath, "utf-8");
33912
+ let reqContent = await node_fs.promises.readFile(reqPath, "utf-8");
33704
33913
  for (const reqId of reqIds) {
33705
33914
  reqContent = reqContent.replace(new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqId}\\*\\*)`, "gi"), "$1x$2");
33706
33915
  reqContent = reqContent.replace(new RegExp(`(\\|\\s*${reqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, "gi"), "$1 Complete $2");
33707
33916
  }
33708
33917
  debugLog("phase-complete-write", `writing REQUIREMENTS.md for phase ${phaseNum}`);
33709
- node_fs.default.writeFileSync(reqPath, reqContent, "utf-8");
33918
+ await node_fs.promises.writeFile(reqPath, reqContent, "utf-8");
33710
33919
  debugLog("phase-complete-write", `REQUIREMENTS.md updated for phase ${phaseNum}`);
33711
33920
  requirementsUpdated = true;
33712
33921
  }
@@ -33716,7 +33925,7 @@ function phaseCompleteCore(cwd, phaseNum) {
33716
33925
  let nextPhaseName = null;
33717
33926
  let isLastPhase = true;
33718
33927
  try {
33719
- const dirs = listSubDirs(phasesDirPath, true);
33928
+ const dirs = await listSubDirsAsync(phasesDirPath, true);
33720
33929
  for (const dir of dirs) {
33721
33930
  const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)?)-?(.*)/i);
33722
33931
  if (dm) {
@@ -33731,8 +33940,9 @@ function phaseCompleteCore(cwd, phaseNum) {
33731
33940
  } catch (e) {
33732
33941
  debugLog("phase-complete-next-phase-scan-failed", e);
33733
33942
  }
33734
- if (node_fs.default.existsSync(stPath)) {
33735
- let stateContent = node_fs.default.readFileSync(stPath, "utf-8");
33943
+ const stExists = await pathExistsAsync(stPath);
33944
+ if (stExists) {
33945
+ let stateContent = await node_fs.promises.readFile(stPath, "utf-8");
33736
33946
  stateContent = stateContent.replace(/(\*\*Current Phase:\*\*\s*).*/, `$1${nextPhaseNum || phaseNum}`);
33737
33947
  if (nextPhaseName) stateContent = stateContent.replace(/(\*\*Current Phase Name:\*\*\s*).*/, `$1${nextPhaseName.replace(/-/g, " ")}`);
33738
33948
  stateContent = stateContent.replace(/(\*\*Status:\*\*\s*).*/, `$1${isLastPhase ? "Milestone complete" : "Ready to plan"}`);
@@ -33740,7 +33950,7 @@ function phaseCompleteCore(cwd, phaseNum) {
33740
33950
  stateContent = stateContent.replace(/(\*\*Last Activity:\*\*\s*).*/, `$1${today}`);
33741
33951
  stateContent = stateContent.replace(/(\*\*Last Activity Description:\*\*\s*).*/, `$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ""}`);
33742
33952
  debugLog("phase-complete-write", `writing STATE.md for phase ${phaseNum}`);
33743
- node_fs.default.writeFileSync(stPath, stateContent, "utf-8");
33953
+ await node_fs.promises.writeFile(stPath, stateContent, "utf-8");
33744
33954
  debugLog("phase-complete-write", `STATE.md updated for phase ${phaseNum}`);
33745
33955
  }
33746
33956
  return {
@@ -33751,8 +33961,8 @@ function phaseCompleteCore(cwd, phaseNum) {
33751
33961
  next_phase_name: nextPhaseName,
33752
33962
  is_last_phase: isLastPhase,
33753
33963
  date: today,
33754
- roadmap_updated: node_fs.default.existsSync(rmPath),
33755
- state_updated: node_fs.default.existsSync(stPath),
33964
+ roadmap_updated: rmExists,
33965
+ state_updated: stExists,
33756
33966
  requirements_updated: requirementsUpdated
33757
33967
  };
33758
33968
  }
@@ -33886,7 +34096,7 @@ function registerPhaseTools(server) {
33886
34096
  const cwd = detectProjectRoot();
33887
34097
  if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
33888
34098
  if (!name || !name.trim()) return mcpError("Phase name must not be empty", "Validation failed");
33889
- const result = phaseAddCore(cwd, name, { includeStubs: true });
34099
+ const result = await phaseAddCore(cwd, name, { includeStubs: true });
33890
34100
  return mcpSuccess({
33891
34101
  phase_number: result.phase_number,
33892
34102
  padded: result.padded,
@@ -33906,7 +34116,7 @@ function registerPhaseTools(server) {
33906
34116
  const cwd = detectProjectRoot();
33907
34117
  if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
33908
34118
  if (!name || !name.trim()) return mcpError("Phase name must not be empty", "Validation failed");
33909
- const result = phaseInsertCore(cwd, after, name, { includeStubs: true });
34119
+ const result = await phaseInsertCore(cwd, after, name, { includeStubs: true });
33910
34120
  return mcpSuccess({
33911
34121
  phase_number: result.phase_number,
33912
34122
  after_phase: result.after_phase,
@@ -33922,7 +34132,7 @@ function registerPhaseTools(server) {
33922
34132
  try {
33923
34133
  const cwd = detectProjectRoot();
33924
34134
  if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
33925
- const result = phaseCompleteCore(cwd, phase);
34135
+ const result = await phaseCompleteCore(cwd, phase);
33926
34136
  return mcpSuccess({
33927
34137
  completed_phase: result.completed_phase,
33928
34138
  phase_name: result.phase_name,
@@ -34245,6 +34455,554 @@ function registerStateTools(server) {
34245
34455
  });
34246
34456
  }
34247
34457
 
34458
+ //#endregion
34459
+ //#region src/core/roadmap.ts
34460
+ /**
34461
+ * Roadmap — Roadmap parsing and update operations
34462
+ *
34463
+ * Ported from maxsim/bin/lib/roadmap.cjs
34464
+ */
34465
+ async function cmdRoadmapAnalyze(cwd) {
34466
+ const content = await safeReadFileAsync(roadmapPath(cwd));
34467
+ if (!content) return cmdOk({
34468
+ error: "ROADMAP.md not found",
34469
+ milestones: [],
34470
+ phases: [],
34471
+ current_phase: null
34472
+ });
34473
+ const phasesDir = phasesPath(cwd);
34474
+ const phasePattern = getPhasePattern();
34475
+ const parsedPhases = [];
34476
+ let match;
34477
+ while ((match = phasePattern.exec(content)) !== null) {
34478
+ const phaseNum = match[1];
34479
+ const phaseName = match[2].replace(/\(INSERTED\)/i, "").trim();
34480
+ const sectionStart = match.index;
34481
+ const nextHeader = content.slice(sectionStart).match(/\n#{2,4}\s+Phase\s+\d/i);
34482
+ const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
34483
+ const section = content.slice(sectionStart, sectionEnd);
34484
+ const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
34485
+ const goal = goalMatch ? goalMatch[1].trim() : null;
34486
+ const dependsMatch = section.match(/\*\*Depends on:\*\*\s*([^\n]+)/i);
34487
+ const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
34488
+ parsedPhases.push({
34489
+ phaseNum,
34490
+ phaseName,
34491
+ goal,
34492
+ depends_on,
34493
+ normalized: normalizePhaseName(phaseNum),
34494
+ checkboxPattern: new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace(".", "\\.")}`, "i")
34495
+ });
34496
+ }
34497
+ let allDirs = [];
34498
+ try {
34499
+ allDirs = await listSubDirsAsync(phasesDir);
34500
+ } catch {}
34501
+ const phases = await Promise.all(parsedPhases.map(async (p) => {
34502
+ let diskStatus = "no_directory";
34503
+ let planCount = 0;
34504
+ let summaryCount = 0;
34505
+ let hasContext = false;
34506
+ let hasResearch = false;
34507
+ try {
34508
+ const dirMatch = allDirs.find((d) => d.startsWith(p.normalized + "-") || d === p.normalized);
34509
+ if (dirMatch) {
34510
+ const phaseFiles = await node_fs.promises.readdir(node_path.default.join(phasesDir, dirMatch));
34511
+ planCount = phaseFiles.filter((f) => isPlanFile(f)).length;
34512
+ summaryCount = phaseFiles.filter((f) => isSummaryFile(f)).length;
34513
+ hasContext = phaseFiles.some((f) => f.endsWith("-CONTEXT.md") || f === "CONTEXT.md");
34514
+ hasResearch = phaseFiles.some((f) => f.endsWith("-RESEARCH.md") || f === "RESEARCH.md");
34515
+ if (summaryCount >= planCount && planCount > 0) diskStatus = "complete";
34516
+ else if (summaryCount > 0) diskStatus = "partial";
34517
+ else if (planCount > 0) diskStatus = "planned";
34518
+ else if (hasResearch) diskStatus = "researched";
34519
+ else if (hasContext) diskStatus = "discussed";
34520
+ else diskStatus = "empty";
34521
+ }
34522
+ } catch (e) {
34523
+ debugLog(e);
34524
+ }
34525
+ const checkboxMatch = content.match(p.checkboxPattern);
34526
+ const roadmapComplete = checkboxMatch ? checkboxMatch[1] === "x" : false;
34527
+ return {
34528
+ number: p.phaseNum,
34529
+ name: p.phaseName,
34530
+ goal: p.goal,
34531
+ depends_on: p.depends_on,
34532
+ plan_count: planCount,
34533
+ summary_count: summaryCount,
34534
+ has_context: hasContext,
34535
+ has_research: hasResearch,
34536
+ disk_status: diskStatus,
34537
+ roadmap_complete: roadmapComplete
34538
+ };
34539
+ }));
34540
+ const milestones = [];
34541
+ const milestonePattern = /##\s*(.*v(\d+\.\d+)[^(\n]*)/gi;
34542
+ let mMatch;
34543
+ while ((mMatch = milestonePattern.exec(content)) !== null) milestones.push({
34544
+ heading: mMatch[1].trim(),
34545
+ version: "v" + mMatch[2]
34546
+ });
34547
+ const currentPhase = phases.find((p) => p.disk_status === "planned" || p.disk_status === "partial") || null;
34548
+ const nextPhase = phases.find((p) => p.disk_status === "empty" || p.disk_status === "no_directory" || p.disk_status === "discussed" || p.disk_status === "researched") || null;
34549
+ const totalPlans = phases.reduce((sum, p) => sum + p.plan_count, 0);
34550
+ const totalSummaries = phases.reduce((sum, p) => sum + p.summary_count, 0);
34551
+ const completedPhases = phases.filter((p) => p.disk_status === "complete").length;
34552
+ const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:\.\d+)?)/gi;
34553
+ const checklistPhases = /* @__PURE__ */ new Set();
34554
+ let checklistMatch;
34555
+ while ((checklistMatch = checklistPattern.exec(content)) !== null) checklistPhases.add(checklistMatch[1]);
34556
+ const detailPhases = new Set(phases.map((p) => p.number));
34557
+ const missingDetails = [...checklistPhases].filter((p) => !detailPhases.has(p));
34558
+ return cmdOk({
34559
+ milestones,
34560
+ phases,
34561
+ phase_count: phases.length,
34562
+ completed_phases: completedPhases,
34563
+ total_plans: totalPlans,
34564
+ total_summaries: totalSummaries,
34565
+ progress_percent: totalPlans > 0 ? Math.min(100, Math.round(totalSummaries / totalPlans * 100)) : 0,
34566
+ current_phase: currentPhase ? currentPhase.number : null,
34567
+ next_phase: nextPhase ? nextPhase.number : null,
34568
+ missing_phase_details: missingDetails.length > 0 ? missingDetails : null
34569
+ });
34570
+ }
34571
+
34572
+ //#endregion
34573
+ //#region src/core/context-loader.ts
34574
+ /**
34575
+ * Context Loader — Intelligent file selection for workflow context assembly
34576
+ *
34577
+ * Selects relevant planning files based on the current task/phase domain,
34578
+ * preventing context overload by loading only what matters.
34579
+ */
34580
+ function fileEntry(cwd, relPath, role) {
34581
+ const fullPath = node_path.default.join(cwd, relPath);
34582
+ try {
34583
+ return {
34584
+ path: relPath,
34585
+ role,
34586
+ size: node_fs.default.statSync(fullPath).size
34587
+ };
34588
+ } catch {
34589
+ return null;
34590
+ }
34591
+ }
34592
+ function addIfExists(files, cwd, relPath, role) {
34593
+ const entry = fileEntry(cwd, relPath, role);
34594
+ if (entry) files.push(entry);
34595
+ }
34596
+ const TOPIC_TO_CODEBASE_DOCS = {
34597
+ ui: ["CONVENTIONS.md", "STRUCTURE.md"],
34598
+ frontend: ["CONVENTIONS.md", "STRUCTURE.md"],
34599
+ component: ["CONVENTIONS.md", "STRUCTURE.md"],
34600
+ api: ["ARCHITECTURE.md", "CONVENTIONS.md"],
34601
+ backend: ["ARCHITECTURE.md", "CONVENTIONS.md"],
34602
+ server: ["ARCHITECTURE.md", "CONVENTIONS.md"],
34603
+ database: ["ARCHITECTURE.md", "STACK.md"],
34604
+ schema: ["ARCHITECTURE.md", "STACK.md"],
34605
+ data: ["ARCHITECTURE.md", "STACK.md"],
34606
+ testing: ["TESTING.md", "CONVENTIONS.md"],
34607
+ test: ["TESTING.md", "CONVENTIONS.md"],
34608
+ integration: ["INTEGRATIONS.md", "STACK.md"],
34609
+ deploy: ["INTEGRATIONS.md", "STACK.md"],
34610
+ refactor: ["CONCERNS.md", "ARCHITECTURE.md"],
34611
+ cleanup: ["CONCERNS.md", "ARCHITECTURE.md"],
34612
+ setup: ["STACK.md", "STRUCTURE.md"],
34613
+ config: ["STACK.md", "STRUCTURE.md"],
34614
+ auth: ["ARCHITECTURE.md", "INTEGRATIONS.md"],
34615
+ performance: ["ARCHITECTURE.md", "STACK.md"],
34616
+ install: ["STACK.md", "STRUCTURE.md"]
34617
+ };
34618
+ const DEFAULT_CODEBASE_DOCS = ["STACK.md", "ARCHITECTURE.md"];
34619
+ function selectCodebaseDocs(topic) {
34620
+ if (!topic) return DEFAULT_CODEBASE_DOCS;
34621
+ const topicLower = topic.toLowerCase();
34622
+ const matched = /* @__PURE__ */ new Set();
34623
+ for (const [keyword, docs] of Object.entries(TOPIC_TO_CODEBASE_DOCS)) if (topicLower.includes(keyword)) for (const doc of docs) matched.add(doc);
34624
+ return matched.size > 0 ? Array.from(matched) : DEFAULT_CODEBASE_DOCS;
34625
+ }
34626
+ function loadProjectContext(cwd) {
34627
+ const files = [];
34628
+ addIfExists(files, cwd, ".planning/PROJECT.md", "project-vision");
34629
+ addIfExists(files, cwd, ".planning/REQUIREMENTS.md", "requirements");
34630
+ addIfExists(files, cwd, ".planning/STATE.md", "state");
34631
+ addIfExists(files, cwd, ".planning/config.json", "config");
34632
+ return files;
34633
+ }
34634
+ function loadRoadmapContext(cwd) {
34635
+ const files = [];
34636
+ addIfExists(files, cwd, ".planning/ROADMAP.md", "roadmap");
34637
+ return files;
34638
+ }
34639
+ function loadPhaseContext(cwd, phase) {
34640
+ const files = [];
34641
+ const phaseInfo = findPhaseInternal(cwd, phase);
34642
+ if (!phaseInfo?.directory) return files;
34643
+ const phaseDir = phaseInfo.directory;
34644
+ try {
34645
+ const phaseFiles = node_fs.default.readdirSync(node_path.default.join(cwd, phaseDir));
34646
+ for (const f of phaseFiles) {
34647
+ const relPath = node_path.default.join(phaseDir, f);
34648
+ if (f.endsWith("-CONTEXT.md") || f === "CONTEXT.md") addIfExists(files, cwd, relPath, "phase-context");
34649
+ else if (f.endsWith("-RESEARCH.md") || f === "RESEARCH.md") addIfExists(files, cwd, relPath, "phase-research");
34650
+ else if (f.endsWith("-PLAN.md")) addIfExists(files, cwd, relPath, "phase-plan");
34651
+ else if (f.endsWith("-SUMMARY.md")) addIfExists(files, cwd, relPath, "phase-summary");
34652
+ else if (f.endsWith("-VERIFICATION.md") || f === "VERIFICATION.md") addIfExists(files, cwd, relPath, "phase-verification");
34653
+ }
34654
+ } catch (e) {
34655
+ debugLog("context-loader-phase-files-failed", e);
34656
+ }
34657
+ return files;
34658
+ }
34659
+ function loadArtefakteContext(cwd, phase) {
34660
+ const files = [];
34661
+ for (const filename of [
34662
+ "DECISIONS.md",
34663
+ "ACCEPTANCE-CRITERIA.md",
34664
+ "NO-GOS.md"
34665
+ ]) {
34666
+ if (phase) {
34667
+ const phaseInfo = findPhaseInternal(cwd, phase);
34668
+ if (phaseInfo?.directory) addIfExists(files, cwd, node_path.default.join(phaseInfo.directory, filename), `artefakt-${filename.toLowerCase()}`);
34669
+ }
34670
+ addIfExists(files, cwd, `.planning/${filename}`, `artefakt-${filename.toLowerCase()}`);
34671
+ }
34672
+ return files;
34673
+ }
34674
+ function loadCodebaseContext(cwd, topic) {
34675
+ const files = [];
34676
+ const codebaseDir = planningPath(cwd, "codebase");
34677
+ try {
34678
+ const existing = node_fs.default.readdirSync(codebaseDir).filter((f) => f.endsWith(".md"));
34679
+ const selected = selectCodebaseDocs(topic);
34680
+ for (const filename of selected) if (existing.includes(filename)) addIfExists(files, cwd, `.planning/codebase/${filename}`, `codebase-${filename.replace(".md", "").toLowerCase()}`);
34681
+ } catch {}
34682
+ return files;
34683
+ }
34684
+ function loadHistoryContext(cwd, currentPhase) {
34685
+ const files = [];
34686
+ const pd = phasesPath(cwd);
34687
+ try {
34688
+ const dirs = listSubDirs(pd, true);
34689
+ for (const dir of dirs) {
34690
+ if (currentPhase) {
34691
+ if (dir.match(/^(\d+[A-Z]?(?:\.\d+)?)/i)?.[1] === currentPhase) continue;
34692
+ }
34693
+ const dirPath = node_path.default.join(pd, dir);
34694
+ const summaries = node_fs.default.readdirSync(dirPath).filter((f) => isSummaryFile(f));
34695
+ for (const s of summaries) addIfExists(files, cwd, node_path.default.join(".planning", "phases", dir, s), "history-summary");
34696
+ }
34697
+ } catch (e) {
34698
+ debugLog("context-loader-history-failed", e);
34699
+ }
34700
+ return files;
34701
+ }
34702
+ function cmdContextLoad(cwd, phase, topic, includeHistory) {
34703
+ const allFiles = [];
34704
+ allFiles.push(...loadProjectContext(cwd));
34705
+ allFiles.push(...loadRoadmapContext(cwd));
34706
+ allFiles.push(...loadArtefakteContext(cwd, phase));
34707
+ const selectedDocs = selectCodebaseDocs(topic);
34708
+ allFiles.push(...loadCodebaseContext(cwd, topic));
34709
+ if (phase) allFiles.push(...loadPhaseContext(cwd, phase));
34710
+ if (includeHistory) allFiles.push(...loadHistoryContext(cwd, phase));
34711
+ const seen = /* @__PURE__ */ new Set();
34712
+ const deduped = allFiles.filter((f) => {
34713
+ if (seen.has(f.path)) return false;
34714
+ seen.add(f.path);
34715
+ return true;
34716
+ });
34717
+ return cmdOk({
34718
+ files: deduped,
34719
+ total_size: deduped.reduce((sum, f) => sum + f.size, 0),
34720
+ phase: phase ?? null,
34721
+ topic: topic ?? null,
34722
+ codebase_docs_selected: selectedDocs
34723
+ });
34724
+ }
34725
+
34726
+ //#endregion
34727
+ //#region src/mcp/context-tools.ts
34728
+ /**
34729
+ * Context Query MCP Tools — Project context exposed as MCP tools
34730
+ *
34731
+ * CRITICAL: Never import output() or error() from core — they call process.exit().
34732
+ * CRITICAL: Never write to stdout — it is reserved for MCP JSON-RPC protocol.
34733
+ * CRITICAL: Never call process.exit() — the server must stay alive after every tool call.
34734
+ */
34735
+ /**
34736
+ * Register all context query tools on the MCP server.
34737
+ */
34738
+ function registerContextTools(server) {
34739
+ server.tool("mcp_get_active_phase", "Get the currently active phase and next phase from roadmap analysis and STATE.md.", {}, async () => {
34740
+ try {
34741
+ const cwd = detectProjectRoot();
34742
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34743
+ const roadmapResult = await cmdRoadmapAnalyze(cwd);
34744
+ let current_phase = null;
34745
+ let next_phase = null;
34746
+ let phase_name = null;
34747
+ let status = null;
34748
+ if (roadmapResult.ok) {
34749
+ const data = roadmapResult.result;
34750
+ current_phase = data.current_phase ?? null;
34751
+ next_phase = data.next_phase ?? null;
34752
+ }
34753
+ const stateContent = safeReadFile(planningPath(cwd, "STATE.md"));
34754
+ if (stateContent) {
34755
+ const statePhase = stateExtractField(stateContent, "Current Phase");
34756
+ if (statePhase) phase_name = statePhase;
34757
+ const stateStatus = stateExtractField(stateContent, "Status");
34758
+ if (stateStatus) status = stateStatus;
34759
+ }
34760
+ return mcpSuccess({
34761
+ current_phase,
34762
+ next_phase,
34763
+ phase_name,
34764
+ status
34765
+ }, `Active phase: ${phase_name ?? current_phase ?? "unknown"}`);
34766
+ } catch (e) {
34767
+ return mcpError("Failed: " + e.message, "Error occurred");
34768
+ }
34769
+ });
34770
+ server.tool("mcp_get_guidelines", "Get project guidelines: PROJECT.md vision, config, and optionally phase-specific context.", { phase: stringType().optional().describe("Optional phase number to include phase-specific context") }, async ({ phase }) => {
34771
+ try {
34772
+ const cwd = detectProjectRoot();
34773
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34774
+ const project_vision = safeReadFile(planningPath(cwd, "PROJECT.md"));
34775
+ const config = loadConfig(cwd);
34776
+ let phase_context = null;
34777
+ if (phase) {
34778
+ const phaseInfo = findPhaseInternal(cwd, phase);
34779
+ if (phaseInfo) phase_context = safeReadFile(node_path.default.join(phaseInfo.directory, `${phaseInfo.phase_number}-CONTEXT.md`));
34780
+ }
34781
+ return mcpSuccess({
34782
+ project_vision,
34783
+ config,
34784
+ phase_context
34785
+ }, `Guidelines loaded${phase ? ` with phase ${phase} context` : ""}`);
34786
+ } catch (e) {
34787
+ return mcpError("Failed: " + e.message, "Error occurred");
34788
+ }
34789
+ });
34790
+ server.tool("mcp_get_context_for_task", "Load context files for a task. Includes project context, roadmap, artefakte, and codebase docs filtered by topic. Topic keywords select relevant codebase docs: \"ui/frontend\" loads CONVENTIONS+STRUCTURE, \"api/backend\" loads ARCHITECTURE+CONVENTIONS, \"testing\" loads TESTING+CONVENTIONS, \"database\" loads ARCHITECTURE+STACK, \"refactor\" loads CONCERNS+ARCHITECTURE. Without topic, defaults to STACK+ARCHITECTURE.", {
34791
+ phase: stringType().optional().describe("Phase number to scope context to"),
34792
+ topic: stringType().optional().describe("Topic keywords to filter codebase docs (e.g. \"frontend\", \"api\", \"testing\", \"database\", \"refactor\")")
34793
+ }, async ({ phase, topic }) => {
34794
+ try {
34795
+ const cwd = detectProjectRoot();
34796
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34797
+ const result = cmdContextLoad(cwd, phase, topic, true);
34798
+ if (!result.ok) return mcpError(result.error, "Context load failed");
34799
+ return mcpSuccess({ context: result.result }, `Context loaded${phase ? ` for phase ${phase}` : ""}${topic ? ` topic "${topic}"` : ""}`);
34800
+ } catch (e) {
34801
+ return mcpError("Failed: " + e.message, "Error occurred");
34802
+ }
34803
+ });
34804
+ server.tool("mcp_get_project_overview", "Get a high-level project overview: PROJECT.md, REQUIREMENTS.md, and STATE.md contents.", {}, async () => {
34805
+ try {
34806
+ const cwd = detectProjectRoot();
34807
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34808
+ return mcpSuccess({
34809
+ project: safeReadFile(planningPath(cwd, "PROJECT.md")),
34810
+ requirements: safeReadFile(planningPath(cwd, "REQUIREMENTS.md")),
34811
+ state: safeReadFile(planningPath(cwd, "STATE.md"))
34812
+ }, "Project overview loaded");
34813
+ } catch (e) {
34814
+ return mcpError("Failed: " + e.message, "Error occurred");
34815
+ }
34816
+ });
34817
+ server.tool("mcp_get_phase_detail", "Get detailed information about a specific phase including all its files (plans, summaries, context, research, verification).", { phase: stringType().describe("Phase number or name (e.g. \"01\", \"1\", \"01A\")") }, async ({ phase }) => {
34818
+ try {
34819
+ const cwd = detectProjectRoot();
34820
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34821
+ const phaseInfo = findPhaseInternal(cwd, phase);
34822
+ if (!phaseInfo) return mcpError(`Phase ${phase} not found`, "Phase not found");
34823
+ const files = [];
34824
+ try {
34825
+ const entries = node_fs.default.readdirSync(phaseInfo.directory);
34826
+ for (const entry of entries) {
34827
+ const fullPath = node_path.default.join(phaseInfo.directory, entry);
34828
+ if (node_fs.default.statSync(fullPath).isFile()) files.push({
34829
+ name: entry,
34830
+ content: safeReadFile(fullPath)
34831
+ });
34832
+ }
34833
+ } catch {}
34834
+ return mcpSuccess({
34835
+ phase_number: phaseInfo.phase_number,
34836
+ phase_name: phaseInfo.phase_name,
34837
+ directory: phaseInfo.directory,
34838
+ files
34839
+ }, `Phase ${phaseInfo.phase_number} detail: ${files.length} file(s)`);
34840
+ } catch (e) {
34841
+ return mcpError("Failed: " + e.message, "Error occurred");
34842
+ }
34843
+ });
34844
+ }
34845
+
34846
+ //#endregion
34847
+ //#region src/mcp/roadmap-tools.ts
34848
+ /**
34849
+ * Register all roadmap query tools on the MCP server.
34850
+ */
34851
+ function registerRoadmapTools(server) {
34852
+ server.tool("mcp_get_roadmap", "Get the full roadmap analysis including all phases, their status, and progress.", {}, async () => {
34853
+ try {
34854
+ const cwd = detectProjectRoot();
34855
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34856
+ const result = await cmdRoadmapAnalyze(cwd);
34857
+ if (!result.ok) return mcpError(result.error, "Roadmap analysis failed");
34858
+ return mcpSuccess({ roadmap: result.result }, "Roadmap analysis complete");
34859
+ } catch (e) {
34860
+ return mcpError("Failed: " + e.message, "Error occurred");
34861
+ }
34862
+ });
34863
+ server.tool("mcp_get_roadmap_progress", "Get a focused progress summary: total phases, completed, in-progress, not started, and progress percentage.", {}, async () => {
34864
+ try {
34865
+ const cwd = detectProjectRoot();
34866
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34867
+ const result = await cmdRoadmapAnalyze(cwd);
34868
+ if (!result.ok) return mcpError(result.error, "Roadmap analysis failed");
34869
+ const data = result.result;
34870
+ const phases = data.phases ?? [];
34871
+ const total_phases = phases.length;
34872
+ let completed = 0;
34873
+ let in_progress = 0;
34874
+ let not_started = 0;
34875
+ for (const p of phases) {
34876
+ const status = String(p.status ?? "").toLowerCase();
34877
+ if (status === "completed" || status === "done") completed++;
34878
+ else if (status === "in-progress" || status === "in_progress" || status === "active") in_progress++;
34879
+ else not_started++;
34880
+ }
34881
+ const progress_percent = total_phases > 0 ? Math.round(completed / total_phases * 100) : 0;
34882
+ return mcpSuccess({
34883
+ total_phases,
34884
+ completed,
34885
+ in_progress,
34886
+ not_started,
34887
+ progress_percent,
34888
+ current_phase: data.current_phase ?? null,
34889
+ next_phase: data.next_phase ?? null
34890
+ }, `Progress: ${completed}/${total_phases} phases complete (${progress_percent}%)`);
34891
+ } catch (e) {
34892
+ return mcpError("Failed: " + e.message, "Error occurred");
34893
+ }
34894
+ });
34895
+ }
34896
+
34897
+ //#endregion
34898
+ //#region src/core/config.ts
34899
+ /**
34900
+ * Config — Planning config CRUD operations
34901
+ *
34902
+ * Ported from maxsim/bin/lib/config.cjs
34903
+ */
34904
+ function cmdConfigSet(cwd, keyPath, value, raw) {
34905
+ const configPath = node_path.default.join(cwd, ".planning", "config.json");
34906
+ if (!keyPath) return cmdErr("Usage: config-set <key.path> <value>");
34907
+ let parsedValue = value;
34908
+ if (value === "true") parsedValue = true;
34909
+ else if (value === "false") parsedValue = false;
34910
+ else if (value !== void 0 && !isNaN(Number(value)) && value !== "") parsedValue = Number(value);
34911
+ let config = {};
34912
+ try {
34913
+ if (node_fs.default.existsSync(configPath)) config = JSON.parse(node_fs.default.readFileSync(configPath, "utf-8"));
34914
+ } catch (err) {
34915
+ return cmdErr("Failed to read config.json: " + err.message);
34916
+ }
34917
+ const keys = keyPath.split(".");
34918
+ let current = config;
34919
+ for (let i = 0; i < keys.length - 1; i++) {
34920
+ const key = keys[i];
34921
+ if (current[key] === void 0 || typeof current[key] !== "object") current[key] = {};
34922
+ current = current[key];
34923
+ }
34924
+ current[keys[keys.length - 1]] = parsedValue;
34925
+ try {
34926
+ node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
34927
+ return cmdOk({
34928
+ updated: true,
34929
+ key: keyPath,
34930
+ value: parsedValue
34931
+ }, raw ? `${keyPath}=${parsedValue}` : void 0);
34932
+ } catch (err) {
34933
+ return cmdErr("Failed to write config.json: " + err.message);
34934
+ }
34935
+ }
34936
+ function cmdConfigGet(cwd, keyPath, raw) {
34937
+ const configPath = node_path.default.join(cwd, ".planning", "config.json");
34938
+ if (!keyPath) return cmdErr("Usage: config-get <key.path>");
34939
+ let config = {};
34940
+ try {
34941
+ if (node_fs.default.existsSync(configPath)) config = JSON.parse(node_fs.default.readFileSync(configPath, "utf-8"));
34942
+ else return cmdErr("No config.json found at " + configPath);
34943
+ } catch (err) {
34944
+ return cmdErr("Failed to read config.json: " + err.message);
34945
+ }
34946
+ const keys = keyPath.split(".");
34947
+ let current = config;
34948
+ for (const key of keys) {
34949
+ if (current === void 0 || current === null || typeof current !== "object") return cmdErr(`Key not found: ${keyPath}`);
34950
+ current = current[key];
34951
+ }
34952
+ if (current === void 0) return cmdErr(`Key not found: ${keyPath}`);
34953
+ return cmdOk(current, raw ? String(current) : void 0);
34954
+ }
34955
+
34956
+ //#endregion
34957
+ //#region src/mcp/config-tools.ts
34958
+ /**
34959
+ * Config Query MCP Tools — Project configuration exposed as MCP tools
34960
+ *
34961
+ * CRITICAL: Never import output() or error() from core — they call process.exit().
34962
+ * CRITICAL: Never write to stdout — it is reserved for MCP JSON-RPC protocol.
34963
+ * CRITICAL: Never call process.exit() — the server must stay alive after every tool call.
34964
+ */
34965
+ /**
34966
+ * Register all config query tools on the MCP server.
34967
+ */
34968
+ function registerConfigTools(server) {
34969
+ server.tool("mcp_get_config", "Get project configuration. Optionally provide a key path to get a specific value.", { key: stringType().optional().describe("Optional dot-separated key path (e.g. \"model_profile\", \"branching.strategy\")") }, async ({ key }) => {
34970
+ try {
34971
+ const cwd = detectProjectRoot();
34972
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34973
+ if (key) {
34974
+ const result = cmdConfigGet(cwd, key, true);
34975
+ if (!result.ok) return mcpError(result.error, "Config get failed");
34976
+ return mcpSuccess({
34977
+ key,
34978
+ value: result.rawValue ?? result.result
34979
+ }, `Config value for "${key}"`);
34980
+ }
34981
+ return mcpSuccess({ config: loadConfig(cwd) }, "Full configuration loaded");
34982
+ } catch (e) {
34983
+ return mcpError("Failed: " + e.message, "Error occurred");
34984
+ }
34985
+ });
34986
+ server.tool("mcp_update_config", "Update a project configuration value by key path.", {
34987
+ key: stringType().describe("Dot-separated key path (e.g. \"model_profile\", \"branching.strategy\")"),
34988
+ value: stringType().describe("New value to set")
34989
+ }, async ({ key, value }) => {
34990
+ try {
34991
+ const cwd = detectProjectRoot();
34992
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34993
+ const result = cmdConfigSet(cwd, key, value, true);
34994
+ if (!result.ok) return mcpError(result.error, "Config update failed");
34995
+ return mcpSuccess({
34996
+ updated: true,
34997
+ key,
34998
+ value
34999
+ }, `Config "${key}" updated to "${value}"`);
35000
+ } catch (e) {
35001
+ return mcpError("Failed: " + e.message, "Error occurred");
35002
+ }
35003
+ });
35004
+ }
35005
+
34248
35006
  //#endregion
34249
35007
  //#region src/mcp/index.ts
34250
35008
  /**
@@ -34254,6 +35012,9 @@ function registerAllTools(server) {
34254
35012
  registerPhaseTools(server);
34255
35013
  registerTodoTools(server);
34256
35014
  registerStateTools(server);
35015
+ registerContextTools(server);
35016
+ registerRoadmapTools(server);
35017
+ registerConfigTools(server);
34257
35018
  }
34258
35019
 
34259
35020
  //#endregion