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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/assets/CHANGELOG.md +16 -0
- package/dist/assets/dashboard/client/assets/{index-C_eAetZJ.js → index-BcRHShXD.js} +59 -59
- package/dist/assets/dashboard/client/assets/index-C199D4Eb.css +32 -0
- package/dist/assets/dashboard/client/index.html +2 -2
- package/dist/assets/dashboard/server.js +26 -11
- package/dist/assets/templates/agents/AGENTS.md +18 -69
- package/dist/assets/templates/agents/maxsim-code-reviewer.md +17 -92
- package/dist/assets/templates/agents/maxsim-codebase-mapper.md +57 -694
- package/dist/assets/templates/agents/maxsim-debugger.md +80 -925
- package/dist/assets/templates/agents/maxsim-executor.md +94 -431
- package/dist/assets/templates/agents/maxsim-integration-checker.md +51 -319
- package/dist/assets/templates/agents/maxsim-phase-researcher.md +63 -429
- package/dist/assets/templates/agents/maxsim-plan-checker.md +79 -568
- package/dist/assets/templates/agents/maxsim-planner.md +125 -855
- package/dist/assets/templates/agents/maxsim-project-researcher.md +32 -472
- package/dist/assets/templates/agents/maxsim-research-synthesizer.md +25 -134
- package/dist/assets/templates/agents/maxsim-roadmapper.md +66 -480
- package/dist/assets/templates/agents/maxsim-spec-reviewer.md +13 -55
- package/dist/assets/templates/agents/maxsim-verifier.md +95 -450
- package/dist/assets/templates/commands/maxsim/artefakte.md +122 -0
- package/dist/assets/templates/commands/maxsim/batch.md +42 -0
- package/dist/assets/templates/commands/maxsim/check-todos.md +1 -0
- package/dist/assets/templates/commands/maxsim/sdd.md +39 -0
- package/dist/assets/templates/references/thinking-partner.md +33 -0
- package/dist/assets/templates/workflows/batch.md +420 -0
- package/dist/assets/templates/workflows/check-todos.md +85 -1
- package/dist/assets/templates/workflows/discuss-phase.md +31 -0
- package/dist/assets/templates/workflows/execute-plan.md +96 -27
- package/dist/assets/templates/workflows/help.md +47 -0
- package/dist/assets/templates/workflows/sdd.md +426 -0
- package/dist/backend/index.d.ts +4 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +12 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/lifecycle.d.ts +13 -0
- package/dist/backend/lifecycle.d.ts.map +1 -0
- package/dist/backend/lifecycle.js +168 -0
- package/dist/backend/lifecycle.js.map +1 -0
- package/dist/backend/server.d.ts +13 -0
- package/dist/backend/server.d.ts.map +1 -0
- package/dist/backend/server.js +1013 -0
- package/dist/backend/server.js.map +1 -0
- package/dist/backend/terminal.d.ts +49 -0
- package/dist/backend/terminal.d.ts.map +1 -0
- package/dist/backend/terminal.js +209 -0
- package/dist/backend/terminal.js.map +1 -0
- package/dist/backend/types.d.ts +77 -0
- package/dist/backend/types.d.ts.map +1 -0
- package/dist/backend/types.js +6 -0
- package/dist/backend/types.js.map +1 -0
- package/dist/backend-server.cjs +80636 -0
- package/dist/backend-server.cjs.map +1 -0
- package/dist/backend-server.d.cts +2 -0
- package/dist/backend-server.d.ts +11 -0
- package/dist/backend-server.d.ts.map +1 -0
- package/dist/backend-server.js +43 -0
- package/dist/backend-server.js.map +1 -0
- package/dist/cli.cjs +378 -172
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +28 -8
- package/dist/cli.js.map +1 -1
- package/dist/core/artefakte.d.ts.map +1 -1
- package/dist/core/artefakte.js +16 -0
- package/dist/core/artefakte.js.map +1 -1
- package/dist/core/context-loader.d.ts +1 -0
- package/dist/core/context-loader.d.ts.map +1 -1
- package/dist/core/context-loader.js +58 -0
- package/dist/core/context-loader.js.map +1 -1
- package/dist/core/core.d.ts +6 -0
- package/dist/core/core.d.ts.map +1 -1
- package/dist/core/core.js +238 -0
- package/dist/core/core.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/phase.d.ts +11 -11
- package/dist/core/phase.d.ts.map +1 -1
- package/dist/core/phase.js +88 -73
- package/dist/core/phase.js.map +1 -1
- package/dist/core/roadmap.d.ts +2 -2
- package/dist/core/roadmap.d.ts.map +1 -1
- package/dist/core/roadmap.js +11 -10
- package/dist/core/roadmap.js.map +1 -1
- package/dist/core/skills.d.ts +4 -3
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +14 -15
- package/dist/core/skills.js.map +1 -1
- package/dist/core/state.d.ts +11 -11
- package/dist/core/state.d.ts.map +1 -1
- package/dist/core/state.js +60 -54
- package/dist/core/state.js.map +1 -1
- package/dist/{core-TFSlUjV1.cjs → core-RRjCSt0G.cjs} +1 -9
- package/dist/{core-TFSlUjV1.cjs.map → core-RRjCSt0G.cjs.map} +1 -1
- package/dist/esm-iIOBzmdz.cjs +1561 -0
- package/dist/esm-iIOBzmdz.cjs.map +1 -0
- package/dist/install.cjs +2 -2
- package/dist/lifecycle-0M4VqOMm.cjs +136 -0
- package/dist/lifecycle-0M4VqOMm.cjs.map +1 -0
- package/dist/mcp/config-tools.d.ts +13 -0
- package/dist/mcp/config-tools.d.ts.map +1 -0
- package/dist/mcp/config-tools.js +66 -0
- package/dist/mcp/config-tools.js.map +1 -0
- package/dist/mcp/context-tools.d.ts +13 -0
- package/dist/mcp/context-tools.d.ts.map +1 -0
- package/dist/mcp/context-tools.js +176 -0
- package/dist/mcp/context-tools.js.map +1 -0
- package/dist/mcp/index.d.ts +0 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +6 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/phase-tools.js +3 -3
- package/dist/mcp/phase-tools.js.map +1 -1
- package/dist/mcp/roadmap-tools.d.ts +13 -0
- package/dist/mcp/roadmap-tools.d.ts.map +1 -0
- package/dist/mcp/roadmap-tools.js +79 -0
- package/dist/mcp/roadmap-tools.js.map +1 -0
- package/dist/mcp-server.cjs +799 -38
- package/dist/mcp-server.cjs.map +1 -1
- package/dist/server-G1MIg_Oe.cjs +2980 -0
- package/dist/server-G1MIg_Oe.cjs.map +1 -0
- package/dist/{skills-BOSxYUzf.cjs → skills-MYlMkYNt.cjs} +41 -29
- package/dist/skills-MYlMkYNt.cjs.map +1 -0
- package/package.json +7 -1
- package/dist/assets/dashboard/client/assets/index-CmiJKqOU.css +0 -32
- package/dist/skills-BOSxYUzf.cjs.map +0 -1
package/dist/mcp-server.cjs
CHANGED
|
@@ -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$
|
|
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$
|
|
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.
|
|
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
|
-
|
|
33601
|
-
|
|
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.
|
|
33615
|
-
node_fs.
|
|
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.
|
|
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
|
-
|
|
33634
|
-
|
|
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 =
|
|
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.
|
|
33655
|
-
node_fs.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
33687
|
-
|
|
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.
|
|
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 (
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
33735
|
-
|
|
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.
|
|
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:
|
|
33755
|
-
state_updated:
|
|
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
|