maxsimcli 4.0.2 → 4.1.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 (68) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/assets/CHANGELOG.md +8 -0
  3. package/dist/backend/index.d.ts +4 -0
  4. package/dist/backend/index.d.ts.map +1 -0
  5. package/dist/backend/index.js +12 -0
  6. package/dist/backend/index.js.map +1 -0
  7. package/dist/backend/lifecycle.d.ts +13 -0
  8. package/dist/backend/lifecycle.d.ts.map +1 -0
  9. package/dist/backend/lifecycle.js +168 -0
  10. package/dist/backend/lifecycle.js.map +1 -0
  11. package/dist/backend/server.d.ts +13 -0
  12. package/dist/backend/server.d.ts.map +1 -0
  13. package/dist/backend/server.js +1013 -0
  14. package/dist/backend/server.js.map +1 -0
  15. package/dist/backend/terminal.d.ts +49 -0
  16. package/dist/backend/terminal.d.ts.map +1 -0
  17. package/dist/backend/terminal.js +209 -0
  18. package/dist/backend/terminal.js.map +1 -0
  19. package/dist/backend/types.d.ts +77 -0
  20. package/dist/backend/types.d.ts.map +1 -0
  21. package/dist/backend/types.js +6 -0
  22. package/dist/backend/types.js.map +1 -0
  23. package/dist/backend-server.cjs +80513 -0
  24. package/dist/backend-server.cjs.map +1 -0
  25. package/dist/backend-server.d.cts +2 -0
  26. package/dist/backend-server.d.ts +11 -0
  27. package/dist/backend-server.d.ts.map +1 -0
  28. package/dist/backend-server.js +43 -0
  29. package/dist/backend-server.js.map +1 -0
  30. package/dist/cli.cjs +71 -29
  31. package/dist/cli.cjs.map +1 -1
  32. package/dist/cli.js +23 -3
  33. package/dist/cli.js.map +1 -1
  34. package/dist/core/skills.d.ts +4 -3
  35. package/dist/core/skills.d.ts.map +1 -1
  36. package/dist/core/skills.js +14 -15
  37. package/dist/core/skills.js.map +1 -1
  38. package/dist/{core-TFSlUjV1.cjs → core-RRjCSt0G.cjs} +1 -9
  39. package/dist/{core-TFSlUjV1.cjs.map → core-RRjCSt0G.cjs.map} +1 -1
  40. package/dist/esm-iIOBzmdz.cjs +1561 -0
  41. package/dist/esm-iIOBzmdz.cjs.map +1 -0
  42. package/dist/install.cjs +2 -2
  43. package/dist/lifecycle-D4E9yP6E.cjs +136 -0
  44. package/dist/lifecycle-D4E9yP6E.cjs.map +1 -0
  45. package/dist/mcp/config-tools.d.ts +13 -0
  46. package/dist/mcp/config-tools.d.ts.map +1 -0
  47. package/dist/mcp/config-tools.js +66 -0
  48. package/dist/mcp/config-tools.js.map +1 -0
  49. package/dist/mcp/context-tools.d.ts +13 -0
  50. package/dist/mcp/context-tools.d.ts.map +1 -0
  51. package/dist/mcp/context-tools.js +172 -0
  52. package/dist/mcp/context-tools.js.map +1 -0
  53. package/dist/mcp/index.d.ts +0 -1
  54. package/dist/mcp/index.d.ts.map +1 -1
  55. package/dist/mcp/index.js +6 -1
  56. package/dist/mcp/index.js.map +1 -1
  57. package/dist/mcp/roadmap-tools.d.ts +13 -0
  58. package/dist/mcp/roadmap-tools.d.ts.map +1 -0
  59. package/dist/mcp/roadmap-tools.js +79 -0
  60. package/dist/mcp/roadmap-tools.js.map +1 -0
  61. package/dist/mcp-server.cjs +641 -3
  62. package/dist/mcp-server.cjs.map +1 -1
  63. package/dist/server-pvY2WbKj.cjs +2980 -0
  64. package/dist/server-pvY2WbKj.cjs.map +1 -0
  65. package/dist/{skills-BOSxYUzf.cjs → skills-MYlMkYNt.cjs} +41 -29
  66. package/dist/skills-MYlMkYNt.cjs.map +1 -0
  67. package/package.json +7 -1
  68. 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;
@@ -33574,6 +33688,23 @@ var require_dist = /* @__PURE__ */ __commonJSMin(((exports) => {
33574
33688
  exports.visitAsync = visit.visitAsync;
33575
33689
  }));
33576
33690
 
33691
+ //#endregion
33692
+ //#region src/core/types.ts
33693
+ var import_dist = /* @__PURE__ */ __toESM(require_dist());
33694
+ function cmdOk(result, rawValue) {
33695
+ return {
33696
+ ok: true,
33697
+ result,
33698
+ rawValue
33699
+ };
33700
+ }
33701
+ function cmdErr(error) {
33702
+ return {
33703
+ ok: false,
33704
+ error
33705
+ };
33706
+ }
33707
+
33577
33708
  //#endregion
33578
33709
  //#region src/core/frontmatter.ts
33579
33710
  /**
@@ -33581,7 +33712,6 @@ var require_dist = /* @__PURE__ */ __commonJSMin(((exports) => {
33581
33712
  *
33582
33713
  * Uses the `yaml` npm package instead of a hand-rolled parser.
33583
33714
  */
33584
- var import_dist = /* @__PURE__ */ __toESM(require_dist());
33585
33715
 
33586
33716
  //#endregion
33587
33717
  //#region src/core/phase.ts
@@ -34245,6 +34375,511 @@ function registerStateTools(server) {
34245
34375
  });
34246
34376
  }
34247
34377
 
34378
+ //#endregion
34379
+ //#region src/core/roadmap.ts
34380
+ /**
34381
+ * Roadmap — Roadmap parsing and update operations
34382
+ *
34383
+ * Ported from maxsim/bin/lib/roadmap.cjs
34384
+ */
34385
+ async function cmdRoadmapAnalyze(cwd) {
34386
+ const content = await safeReadFileAsync(roadmapPath(cwd));
34387
+ if (!content) return cmdOk({
34388
+ error: "ROADMAP.md not found",
34389
+ milestones: [],
34390
+ phases: [],
34391
+ current_phase: null
34392
+ });
34393
+ const phasesDir = phasesPath(cwd);
34394
+ const phasePattern = getPhasePattern();
34395
+ const parsedPhases = [];
34396
+ let match;
34397
+ while ((match = phasePattern.exec(content)) !== null) {
34398
+ const phaseNum = match[1];
34399
+ const phaseName = match[2].replace(/\(INSERTED\)/i, "").trim();
34400
+ const sectionStart = match.index;
34401
+ const nextHeader = content.slice(sectionStart).match(/\n#{2,4}\s+Phase\s+\d/i);
34402
+ const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
34403
+ const section = content.slice(sectionStart, sectionEnd);
34404
+ const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
34405
+ const goal = goalMatch ? goalMatch[1].trim() : null;
34406
+ const dependsMatch = section.match(/\*\*Depends on:\*\*\s*([^\n]+)/i);
34407
+ const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
34408
+ parsedPhases.push({
34409
+ phaseNum,
34410
+ phaseName,
34411
+ goal,
34412
+ depends_on,
34413
+ normalized: normalizePhaseName(phaseNum),
34414
+ checkboxPattern: new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${phaseNum.replace(".", "\\.")}`, "i")
34415
+ });
34416
+ }
34417
+ let allDirs = [];
34418
+ try {
34419
+ allDirs = await listSubDirsAsync(phasesDir);
34420
+ } catch {}
34421
+ const phases = await Promise.all(parsedPhases.map(async (p) => {
34422
+ let diskStatus = "no_directory";
34423
+ let planCount = 0;
34424
+ let summaryCount = 0;
34425
+ let hasContext = false;
34426
+ let hasResearch = false;
34427
+ try {
34428
+ const dirMatch = allDirs.find((d) => d.startsWith(p.normalized + "-") || d === p.normalized);
34429
+ if (dirMatch) {
34430
+ const phaseFiles = await node_fs.default.promises.readdir(node_path.default.join(phasesDir, dirMatch));
34431
+ planCount = phaseFiles.filter((f) => isPlanFile(f)).length;
34432
+ summaryCount = phaseFiles.filter((f) => isSummaryFile(f)).length;
34433
+ hasContext = phaseFiles.some((f) => f.endsWith("-CONTEXT.md") || f === "CONTEXT.md");
34434
+ hasResearch = phaseFiles.some((f) => f.endsWith("-RESEARCH.md") || f === "RESEARCH.md");
34435
+ if (summaryCount >= planCount && planCount > 0) diskStatus = "complete";
34436
+ else if (summaryCount > 0) diskStatus = "partial";
34437
+ else if (planCount > 0) diskStatus = "planned";
34438
+ else if (hasResearch) diskStatus = "researched";
34439
+ else if (hasContext) diskStatus = "discussed";
34440
+ else diskStatus = "empty";
34441
+ }
34442
+ } catch (e) {
34443
+ debugLog(e);
34444
+ }
34445
+ const checkboxMatch = content.match(p.checkboxPattern);
34446
+ const roadmapComplete = checkboxMatch ? checkboxMatch[1] === "x" : false;
34447
+ return {
34448
+ number: p.phaseNum,
34449
+ name: p.phaseName,
34450
+ goal: p.goal,
34451
+ depends_on: p.depends_on,
34452
+ plan_count: planCount,
34453
+ summary_count: summaryCount,
34454
+ has_context: hasContext,
34455
+ has_research: hasResearch,
34456
+ disk_status: diskStatus,
34457
+ roadmap_complete: roadmapComplete
34458
+ };
34459
+ }));
34460
+ const milestones = [];
34461
+ const milestonePattern = /##\s*(.*v(\d+\.\d+)[^(\n]*)/gi;
34462
+ let mMatch;
34463
+ while ((mMatch = milestonePattern.exec(content)) !== null) milestones.push({
34464
+ heading: mMatch[1].trim(),
34465
+ version: "v" + mMatch[2]
34466
+ });
34467
+ const currentPhase = phases.find((p) => p.disk_status === "planned" || p.disk_status === "partial") || null;
34468
+ const nextPhase = phases.find((p) => p.disk_status === "empty" || p.disk_status === "no_directory" || p.disk_status === "discussed" || p.disk_status === "researched") || null;
34469
+ const totalPlans = phases.reduce((sum, p) => sum + p.plan_count, 0);
34470
+ const totalSummaries = phases.reduce((sum, p) => sum + p.summary_count, 0);
34471
+ const completedPhases = phases.filter((p) => p.disk_status === "complete").length;
34472
+ const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:\.\d+)?)/gi;
34473
+ const checklistPhases = /* @__PURE__ */ new Set();
34474
+ let checklistMatch;
34475
+ while ((checklistMatch = checklistPattern.exec(content)) !== null) checklistPhases.add(checklistMatch[1]);
34476
+ const detailPhases = new Set(phases.map((p) => p.number));
34477
+ const missingDetails = [...checklistPhases].filter((p) => !detailPhases.has(p));
34478
+ return cmdOk({
34479
+ milestones,
34480
+ phases,
34481
+ phase_count: phases.length,
34482
+ completed_phases: completedPhases,
34483
+ total_plans: totalPlans,
34484
+ total_summaries: totalSummaries,
34485
+ progress_percent: totalPlans > 0 ? Math.min(100, Math.round(totalSummaries / totalPlans * 100)) : 0,
34486
+ current_phase: currentPhase ? currentPhase.number : null,
34487
+ next_phase: nextPhase ? nextPhase.number : null,
34488
+ missing_phase_details: missingDetails.length > 0 ? missingDetails : null
34489
+ });
34490
+ }
34491
+
34492
+ //#endregion
34493
+ //#region src/core/context-loader.ts
34494
+ /**
34495
+ * Context Loader — Intelligent file selection for workflow context assembly
34496
+ *
34497
+ * Selects relevant planning files based on the current task/phase domain,
34498
+ * preventing context overload by loading only what matters.
34499
+ */
34500
+ function fileEntry(cwd, relPath, role) {
34501
+ const fullPath = node_path.default.join(cwd, relPath);
34502
+ try {
34503
+ return {
34504
+ path: relPath,
34505
+ role,
34506
+ size: node_fs.default.statSync(fullPath).size
34507
+ };
34508
+ } catch {
34509
+ return null;
34510
+ }
34511
+ }
34512
+ function addIfExists(files, cwd, relPath, role) {
34513
+ const entry = fileEntry(cwd, relPath, role);
34514
+ if (entry) files.push(entry);
34515
+ }
34516
+ function loadProjectContext(cwd) {
34517
+ const files = [];
34518
+ addIfExists(files, cwd, ".planning/PROJECT.md", "project-vision");
34519
+ addIfExists(files, cwd, ".planning/REQUIREMENTS.md", "requirements");
34520
+ addIfExists(files, cwd, ".planning/STATE.md", "state");
34521
+ addIfExists(files, cwd, ".planning/config.json", "config");
34522
+ return files;
34523
+ }
34524
+ function loadRoadmapContext(cwd) {
34525
+ const files = [];
34526
+ addIfExists(files, cwd, ".planning/ROADMAP.md", "roadmap");
34527
+ return files;
34528
+ }
34529
+ function loadPhaseContext(cwd, phase) {
34530
+ const files = [];
34531
+ const phaseInfo = findPhaseInternal(cwd, phase);
34532
+ if (!phaseInfo?.directory) return files;
34533
+ const phaseDir = phaseInfo.directory;
34534
+ try {
34535
+ const phaseFiles = node_fs.default.readdirSync(node_path.default.join(cwd, phaseDir));
34536
+ for (const f of phaseFiles) {
34537
+ const relPath = node_path.default.join(phaseDir, f);
34538
+ if (f.endsWith("-CONTEXT.md") || f === "CONTEXT.md") addIfExists(files, cwd, relPath, "phase-context");
34539
+ else if (f.endsWith("-RESEARCH.md") || f === "RESEARCH.md") addIfExists(files, cwd, relPath, "phase-research");
34540
+ else if (f.endsWith("-PLAN.md")) addIfExists(files, cwd, relPath, "phase-plan");
34541
+ else if (f.endsWith("-SUMMARY.md")) addIfExists(files, cwd, relPath, "phase-summary");
34542
+ else if (f.endsWith("-VERIFICATION.md") || f === "VERIFICATION.md") addIfExists(files, cwd, relPath, "phase-verification");
34543
+ }
34544
+ } catch (e) {
34545
+ debugLog("context-loader-phase-files-failed", e);
34546
+ }
34547
+ return files;
34548
+ }
34549
+ function loadArtefakteContext(cwd, phase) {
34550
+ const files = [];
34551
+ for (const filename of [
34552
+ "DECISIONS.md",
34553
+ "ACCEPTANCE-CRITERIA.md",
34554
+ "NO-GOS.md"
34555
+ ]) {
34556
+ if (phase) {
34557
+ const phaseInfo = findPhaseInternal(cwd, phase);
34558
+ if (phaseInfo?.directory) addIfExists(files, cwd, node_path.default.join(phaseInfo.directory, filename), `artefakt-${filename.toLowerCase()}`);
34559
+ }
34560
+ addIfExists(files, cwd, `.planning/${filename}`, `artefakt-${filename.toLowerCase()}`);
34561
+ }
34562
+ return files;
34563
+ }
34564
+ function loadHistoryContext(cwd, currentPhase) {
34565
+ const files = [];
34566
+ const pd = phasesPath(cwd);
34567
+ try {
34568
+ const dirs = listSubDirs(pd, true);
34569
+ for (const dir of dirs) {
34570
+ if (currentPhase) {
34571
+ if (dir.match(/^(\d+[A-Z]?(?:\.\d+)?)/i)?.[1] === currentPhase) continue;
34572
+ }
34573
+ const dirPath = node_path.default.join(pd, dir);
34574
+ const summaries = node_fs.default.readdirSync(dirPath).filter((f) => isSummaryFile(f));
34575
+ for (const s of summaries) addIfExists(files, cwd, node_path.default.join(".planning", "phases", dir, s), "history-summary");
34576
+ }
34577
+ } catch (e) {
34578
+ debugLog("context-loader-history-failed", e);
34579
+ }
34580
+ return files;
34581
+ }
34582
+ function cmdContextLoad(cwd, phase, topic, includeHistory) {
34583
+ const allFiles = [];
34584
+ allFiles.push(...loadProjectContext(cwd));
34585
+ allFiles.push(...loadRoadmapContext(cwd));
34586
+ allFiles.push(...loadArtefakteContext(cwd, phase));
34587
+ if (phase) allFiles.push(...loadPhaseContext(cwd, phase));
34588
+ if (includeHistory) allFiles.push(...loadHistoryContext(cwd, phase));
34589
+ const seen = /* @__PURE__ */ new Set();
34590
+ const deduped = allFiles.filter((f) => {
34591
+ if (seen.has(f.path)) return false;
34592
+ seen.add(f.path);
34593
+ return true;
34594
+ });
34595
+ return cmdOk({
34596
+ files: deduped,
34597
+ total_size: deduped.reduce((sum, f) => sum + f.size, 0),
34598
+ phase: phase ?? null,
34599
+ topic: topic ?? null
34600
+ });
34601
+ }
34602
+
34603
+ //#endregion
34604
+ //#region src/mcp/context-tools.ts
34605
+ /**
34606
+ * Context Query MCP Tools — Project context exposed as MCP tools
34607
+ *
34608
+ * CRITICAL: Never import output() or error() from core — they call process.exit().
34609
+ * CRITICAL: Never write to stdout — it is reserved for MCP JSON-RPC protocol.
34610
+ * CRITICAL: Never call process.exit() — the server must stay alive after every tool call.
34611
+ */
34612
+ /**
34613
+ * Register all context query tools on the MCP server.
34614
+ */
34615
+ function registerContextTools(server) {
34616
+ server.tool("mcp_get_active_phase", "Get the currently active phase and next phase from roadmap analysis and STATE.md.", {}, async () => {
34617
+ try {
34618
+ const cwd = detectProjectRoot();
34619
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34620
+ const roadmapResult = await cmdRoadmapAnalyze(cwd);
34621
+ let current_phase = null;
34622
+ let next_phase = null;
34623
+ let phase_name = null;
34624
+ let status = null;
34625
+ if (roadmapResult.ok) {
34626
+ const data = roadmapResult.result;
34627
+ current_phase = data.current_phase ?? null;
34628
+ next_phase = data.next_phase ?? null;
34629
+ }
34630
+ const stateContent = safeReadFile(planningPath(cwd, "STATE.md"));
34631
+ if (stateContent) {
34632
+ const statePhase = stateExtractField(stateContent, "Current Phase");
34633
+ if (statePhase) phase_name = statePhase;
34634
+ const stateStatus = stateExtractField(stateContent, "Status");
34635
+ if (stateStatus) status = stateStatus;
34636
+ }
34637
+ return mcpSuccess({
34638
+ current_phase,
34639
+ next_phase,
34640
+ phase_name,
34641
+ status
34642
+ }, `Active phase: ${phase_name ?? current_phase ?? "unknown"}`);
34643
+ } catch (e) {
34644
+ return mcpError("Failed: " + e.message, "Error occurred");
34645
+ }
34646
+ });
34647
+ 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 }) => {
34648
+ try {
34649
+ const cwd = detectProjectRoot();
34650
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34651
+ const project_vision = safeReadFile(planningPath(cwd, "PROJECT.md"));
34652
+ const config = loadConfig(cwd);
34653
+ let phase_context = null;
34654
+ if (phase) {
34655
+ const phaseInfo = findPhaseInternal(cwd, phase);
34656
+ if (phaseInfo) phase_context = safeReadFile(node_path.default.join(phaseInfo.directory, `${phaseInfo.phase_number}-CONTEXT.md`));
34657
+ }
34658
+ return mcpSuccess({
34659
+ project_vision,
34660
+ config,
34661
+ phase_context
34662
+ }, `Guidelines loaded${phase ? ` with phase ${phase} context` : ""}`);
34663
+ } catch (e) {
34664
+ return mcpError("Failed: " + e.message, "Error occurred");
34665
+ }
34666
+ });
34667
+ server.tool("mcp_get_context_for_task", "Load context files for a task, including project context, roadmap, and optionally phase-specific and topic-specific files.", {
34668
+ phase: stringType().optional().describe("Optional phase number to scope context to"),
34669
+ topic: stringType().optional().describe("Optional topic to filter context by")
34670
+ }, async ({ phase, topic }) => {
34671
+ try {
34672
+ const cwd = detectProjectRoot();
34673
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34674
+ const result = cmdContextLoad(cwd, phase, topic, true);
34675
+ if (!result.ok) return mcpError(result.error, "Context load failed");
34676
+ return mcpSuccess({ context: result.result }, `Context loaded${phase ? ` for phase ${phase}` : ""}${topic ? ` topic "${topic}"` : ""}`);
34677
+ } catch (e) {
34678
+ return mcpError("Failed: " + e.message, "Error occurred");
34679
+ }
34680
+ });
34681
+ server.tool("mcp_get_project_overview", "Get a high-level project overview: PROJECT.md, REQUIREMENTS.md, and STATE.md contents.", {}, async () => {
34682
+ try {
34683
+ const cwd = detectProjectRoot();
34684
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34685
+ return mcpSuccess({
34686
+ project: safeReadFile(planningPath(cwd, "PROJECT.md")),
34687
+ requirements: safeReadFile(planningPath(cwd, "REQUIREMENTS.md")),
34688
+ state: safeReadFile(planningPath(cwd, "STATE.md"))
34689
+ }, "Project overview loaded");
34690
+ } catch (e) {
34691
+ return mcpError("Failed: " + e.message, "Error occurred");
34692
+ }
34693
+ });
34694
+ 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 }) => {
34695
+ try {
34696
+ const cwd = detectProjectRoot();
34697
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34698
+ const phaseInfo = findPhaseInternal(cwd, phase);
34699
+ if (!phaseInfo) return mcpError(`Phase ${phase} not found`, "Phase not found");
34700
+ const files = [];
34701
+ try {
34702
+ const entries = node_fs.default.readdirSync(phaseInfo.directory);
34703
+ for (const entry of entries) {
34704
+ const fullPath = node_path.default.join(phaseInfo.directory, entry);
34705
+ if (node_fs.default.statSync(fullPath).isFile()) files.push({
34706
+ name: entry,
34707
+ content: safeReadFile(fullPath)
34708
+ });
34709
+ }
34710
+ } catch {}
34711
+ return mcpSuccess({
34712
+ phase_number: phaseInfo.phase_number,
34713
+ phase_name: phaseInfo.phase_name,
34714
+ directory: phaseInfo.directory,
34715
+ files
34716
+ }, `Phase ${phaseInfo.phase_number} detail: ${files.length} file(s)`);
34717
+ } catch (e) {
34718
+ return mcpError("Failed: " + e.message, "Error occurred");
34719
+ }
34720
+ });
34721
+ }
34722
+
34723
+ //#endregion
34724
+ //#region src/mcp/roadmap-tools.ts
34725
+ /**
34726
+ * Register all roadmap query tools on the MCP server.
34727
+ */
34728
+ function registerRoadmapTools(server) {
34729
+ server.tool("mcp_get_roadmap", "Get the full roadmap analysis including all phases, their status, and progress.", {}, async () => {
34730
+ try {
34731
+ const cwd = detectProjectRoot();
34732
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34733
+ const result = await cmdRoadmapAnalyze(cwd);
34734
+ if (!result.ok) return mcpError(result.error, "Roadmap analysis failed");
34735
+ return mcpSuccess({ roadmap: result.result }, "Roadmap analysis complete");
34736
+ } catch (e) {
34737
+ return mcpError("Failed: " + e.message, "Error occurred");
34738
+ }
34739
+ });
34740
+ server.tool("mcp_get_roadmap_progress", "Get a focused progress summary: total phases, completed, in-progress, not started, and progress percentage.", {}, async () => {
34741
+ try {
34742
+ const cwd = detectProjectRoot();
34743
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34744
+ const result = await cmdRoadmapAnalyze(cwd);
34745
+ if (!result.ok) return mcpError(result.error, "Roadmap analysis failed");
34746
+ const data = result.result;
34747
+ const phases = data.phases ?? [];
34748
+ const total_phases = phases.length;
34749
+ let completed = 0;
34750
+ let in_progress = 0;
34751
+ let not_started = 0;
34752
+ for (const p of phases) {
34753
+ const status = String(p.status ?? "").toLowerCase();
34754
+ if (status === "completed" || status === "done") completed++;
34755
+ else if (status === "in-progress" || status === "in_progress" || status === "active") in_progress++;
34756
+ else not_started++;
34757
+ }
34758
+ const progress_percent = total_phases > 0 ? Math.round(completed / total_phases * 100) : 0;
34759
+ return mcpSuccess({
34760
+ total_phases,
34761
+ completed,
34762
+ in_progress,
34763
+ not_started,
34764
+ progress_percent,
34765
+ current_phase: data.current_phase ?? null,
34766
+ next_phase: data.next_phase ?? null
34767
+ }, `Progress: ${completed}/${total_phases} phases complete (${progress_percent}%)`);
34768
+ } catch (e) {
34769
+ return mcpError("Failed: " + e.message, "Error occurred");
34770
+ }
34771
+ });
34772
+ }
34773
+
34774
+ //#endregion
34775
+ //#region src/core/config.ts
34776
+ /**
34777
+ * Config — Planning config CRUD operations
34778
+ *
34779
+ * Ported from maxsim/bin/lib/config.cjs
34780
+ */
34781
+ function cmdConfigSet(cwd, keyPath, value, raw) {
34782
+ const configPath = node_path.default.join(cwd, ".planning", "config.json");
34783
+ if (!keyPath) return cmdErr("Usage: config-set <key.path> <value>");
34784
+ let parsedValue = value;
34785
+ if (value === "true") parsedValue = true;
34786
+ else if (value === "false") parsedValue = false;
34787
+ else if (value !== void 0 && !isNaN(Number(value)) && value !== "") parsedValue = Number(value);
34788
+ let config = {};
34789
+ try {
34790
+ if (node_fs.default.existsSync(configPath)) config = JSON.parse(node_fs.default.readFileSync(configPath, "utf-8"));
34791
+ } catch (err) {
34792
+ return cmdErr("Failed to read config.json: " + err.message);
34793
+ }
34794
+ const keys = keyPath.split(".");
34795
+ let current = config;
34796
+ for (let i = 0; i < keys.length - 1; i++) {
34797
+ const key = keys[i];
34798
+ if (current[key] === void 0 || typeof current[key] !== "object") current[key] = {};
34799
+ current = current[key];
34800
+ }
34801
+ current[keys[keys.length - 1]] = parsedValue;
34802
+ try {
34803
+ node_fs.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
34804
+ return cmdOk({
34805
+ updated: true,
34806
+ key: keyPath,
34807
+ value: parsedValue
34808
+ }, raw ? `${keyPath}=${parsedValue}` : void 0);
34809
+ } catch (err) {
34810
+ return cmdErr("Failed to write config.json: " + err.message);
34811
+ }
34812
+ }
34813
+ function cmdConfigGet(cwd, keyPath, raw) {
34814
+ const configPath = node_path.default.join(cwd, ".planning", "config.json");
34815
+ if (!keyPath) return cmdErr("Usage: config-get <key.path>");
34816
+ let config = {};
34817
+ try {
34818
+ if (node_fs.default.existsSync(configPath)) config = JSON.parse(node_fs.default.readFileSync(configPath, "utf-8"));
34819
+ else return cmdErr("No config.json found at " + configPath);
34820
+ } catch (err) {
34821
+ return cmdErr("Failed to read config.json: " + err.message);
34822
+ }
34823
+ const keys = keyPath.split(".");
34824
+ let current = config;
34825
+ for (const key of keys) {
34826
+ if (current === void 0 || current === null || typeof current !== "object") return cmdErr(`Key not found: ${keyPath}`);
34827
+ current = current[key];
34828
+ }
34829
+ if (current === void 0) return cmdErr(`Key not found: ${keyPath}`);
34830
+ return cmdOk(current, raw ? String(current) : void 0);
34831
+ }
34832
+
34833
+ //#endregion
34834
+ //#region src/mcp/config-tools.ts
34835
+ /**
34836
+ * Config Query MCP Tools — Project configuration exposed as MCP tools
34837
+ *
34838
+ * CRITICAL: Never import output() or error() from core — they call process.exit().
34839
+ * CRITICAL: Never write to stdout — it is reserved for MCP JSON-RPC protocol.
34840
+ * CRITICAL: Never call process.exit() — the server must stay alive after every tool call.
34841
+ */
34842
+ /**
34843
+ * Register all config query tools on the MCP server.
34844
+ */
34845
+ function registerConfigTools(server) {
34846
+ 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 }) => {
34847
+ try {
34848
+ const cwd = detectProjectRoot();
34849
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34850
+ if (key) {
34851
+ const result = cmdConfigGet(cwd, key, true);
34852
+ if (!result.ok) return mcpError(result.error, "Config get failed");
34853
+ return mcpSuccess({
34854
+ key,
34855
+ value: result.rawValue ?? result.result
34856
+ }, `Config value for "${key}"`);
34857
+ }
34858
+ return mcpSuccess({ config: loadConfig(cwd) }, "Full configuration loaded");
34859
+ } catch (e) {
34860
+ return mcpError("Failed: " + e.message, "Error occurred");
34861
+ }
34862
+ });
34863
+ server.tool("mcp_update_config", "Update a project configuration value by key path.", {
34864
+ key: stringType().describe("Dot-separated key path (e.g. \"model_profile\", \"branching.strategy\")"),
34865
+ value: stringType().describe("New value to set")
34866
+ }, async ({ key, value }) => {
34867
+ try {
34868
+ const cwd = detectProjectRoot();
34869
+ if (!cwd) return mcpError("No .planning/ directory found", "Project not detected");
34870
+ const result = cmdConfigSet(cwd, key, value, true);
34871
+ if (!result.ok) return mcpError(result.error, "Config update failed");
34872
+ return mcpSuccess({
34873
+ updated: true,
34874
+ key,
34875
+ value
34876
+ }, `Config "${key}" updated to "${value}"`);
34877
+ } catch (e) {
34878
+ return mcpError("Failed: " + e.message, "Error occurred");
34879
+ }
34880
+ });
34881
+ }
34882
+
34248
34883
  //#endregion
34249
34884
  //#region src/mcp/index.ts
34250
34885
  /**
@@ -34254,6 +34889,9 @@ function registerAllTools(server) {
34254
34889
  registerPhaseTools(server);
34255
34890
  registerTodoTools(server);
34256
34891
  registerStateTools(server);
34892
+ registerContextTools(server);
34893
+ registerRoadmapTools(server);
34894
+ registerConfigTools(server);
34257
34895
  }
34258
34896
 
34259
34897
  //#endregion