@projitive/mcp 2.0.3 → 2.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 (52) hide show
  1. package/output/package.json +8 -2
  2. package/output/source/common/artifacts.js +1 -1
  3. package/output/source/common/artifacts.test.js +11 -11
  4. package/output/source/common/errors.js +19 -19
  5. package/output/source/common/errors.test.js +59 -0
  6. package/output/source/common/files.js +30 -19
  7. package/output/source/common/files.test.js +14 -14
  8. package/output/source/common/index.js +11 -10
  9. package/output/source/common/linter.js +29 -27
  10. package/output/source/common/linter.test.js +9 -9
  11. package/output/source/common/markdown.js +3 -3
  12. package/output/source/common/markdown.test.js +15 -15
  13. package/output/source/common/response.js +91 -107
  14. package/output/source/common/response.test.js +30 -30
  15. package/output/source/common/store.js +40 -40
  16. package/output/source/common/store.test.js +72 -72
  17. package/output/source/common/tool.js +43 -0
  18. package/output/source/common/types.js +3 -3
  19. package/output/source/common/utils.js +8 -8
  20. package/output/source/common/utils.test.js +48 -0
  21. package/output/source/index.js +16 -16
  22. package/output/source/index.runtime.test.js +57 -0
  23. package/output/source/index.test.js +64 -64
  24. package/output/source/prompts/index.js +3 -3
  25. package/output/source/prompts/index.test.js +23 -0
  26. package/output/source/prompts/quickStart.js +96 -96
  27. package/output/source/prompts/quickStart.test.js +24 -0
  28. package/output/source/prompts/taskDiscovery.js +184 -184
  29. package/output/source/prompts/taskDiscovery.test.js +24 -0
  30. package/output/source/prompts/taskExecution.js +164 -148
  31. package/output/source/prompts/taskExecution.test.js +27 -0
  32. package/output/source/resources/designs.js +26 -26
  33. package/output/source/resources/designs.resources.test.js +52 -0
  34. package/output/source/resources/designs.test.js +88 -88
  35. package/output/source/resources/governance.js +19 -19
  36. package/output/source/resources/governance.test.js +35 -0
  37. package/output/source/resources/index.js +2 -2
  38. package/output/source/resources/index.test.js +18 -0
  39. package/output/source/resources/readme.js +7 -7
  40. package/output/source/resources/readme.test.js +113 -113
  41. package/output/source/resources/reports.js +10 -10
  42. package/output/source/resources/reports.test.js +83 -83
  43. package/output/source/tools/index.js +3 -3
  44. package/output/source/tools/index.test.js +23 -0
  45. package/output/source/tools/project.js +330 -377
  46. package/output/source/tools/project.test.js +308 -175
  47. package/output/source/tools/roadmap.js +236 -255
  48. package/output/source/tools/roadmap.test.js +241 -46
  49. package/output/source/tools/task.js +770 -652
  50. package/output/source/tools/task.test.js +433 -105
  51. package/output/source/types.js +28 -22
  52. package/package.json +8 -2
@@ -1,32 +1,30 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { z } from "zod";
4
- import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, ROADMAP_LINT_CODES, renderLintSuggestions, findTextReferences, ensureStore, loadRoadmapsFromStore, upsertRoadmapInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, } from "../common/index.js";
5
- import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "../common/index.js";
6
- import { resolveGovernanceDir, toProjectPath } from "./project.js";
7
- import { loadTasks } from "./task.js";
8
- export const ROADMAP_ID_REGEX = /^ROADMAP-\d{4}$/;
9
- export const ROADMAP_MARKDOWN_FILE = "roadmap.md";
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
4
+ import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, ROADMAP_LINT_CODES, renderLintSuggestions, findTextReferences, ensureStore, loadRoadmapsFromStore, upsertRoadmapInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, createGovernedTool, ToolExecutionError, } from '../common/index.js';
5
+ import { resolveGovernanceDir, toProjectPath } from './project.js';
6
+ import { loadTasks, loadTasksDocument } from './task.js';
7
+ export const ROADMAP_ID_REGEX = /^ROADMAP-(\d+)$/;
8
+ export const ROADMAP_MARKDOWN_FILE = 'roadmap.md';
10
9
  function nowIso() {
11
10
  return new Date().toISOString();
12
11
  }
13
12
  function nextRoadmapId(milestones) {
14
13
  const maxSuffix = milestones
15
14
  .map((item) => toRoadmapIdNumericSuffix(item.id))
16
- .filter((value) => Number.isFinite(value) && value >= 0)
15
+ .filter((value) => value > 0)
17
16
  .reduce((max, value) => Math.max(max, value), 0);
18
17
  const next = maxSuffix + 1;
19
- if (next > 9999) {
20
- throw new Error("ROADMAP ID overflow: maximum supported ID is ROADMAP-9999");
21
- }
22
- return `ROADMAP-${String(next).padStart(4, "0")}`;
18
+ const minWidth = Math.max(4, String(next).length);
19
+ return `ROADMAP-${String(next).padStart(minWidth, '0')}`;
23
20
  }
24
21
  function toRoadmapIdNumericSuffix(roadmapId) {
25
- const match = roadmapId.match(/^(?:ROADMAP-)(\d{4})$/);
22
+ const match = roadmapId.match(ROADMAP_ID_REGEX);
26
23
  if (!match) {
27
24
  return -1;
28
25
  }
29
- return Number.parseInt(match[1], 10);
26
+ const suffix = Number.parseInt(match[1], 10);
27
+ return Number.isFinite(suffix) ? suffix : -1;
30
28
  }
31
29
  function sortMilestonesNewestFirst(milestones) {
32
30
  return [...milestones].sort((a, b) => {
@@ -34,20 +32,16 @@ function sortMilestonesNewestFirst(milestones) {
34
32
  if (Number.isFinite(updatedAtDelta) && updatedAtDelta !== 0) {
35
33
  return updatedAtDelta;
36
34
  }
37
- const idDelta = toRoadmapIdNumericSuffix(b.id) - toRoadmapIdNumericSuffix(a.id);
38
- if (idDelta !== 0) {
39
- return idDelta;
40
- }
41
- return b.id.localeCompare(a.id);
35
+ return toRoadmapIdNumericSuffix(b.id) - toRoadmapIdNumericSuffix(a.id);
42
36
  });
43
37
  }
44
38
  function normalizeMilestone(raw) {
45
39
  return {
46
40
  id: String(raw.id),
47
41
  title: String(raw.title),
48
- status: raw.status === "done" ? "done" : "active",
49
- time: typeof raw.time === "string" && raw.time.trim().length > 0 ? raw.time.trim() : undefined,
50
- updatedAt: typeof raw.updatedAt === "string" && Number.isFinite(new Date(raw.updatedAt).getTime()) ? raw.updatedAt : nowIso(),
42
+ status: raw.status === 'done' ? 'done' : 'active',
43
+ time: typeof raw.time === 'string' && raw.time.trim().length > 0 ? raw.time.trim() : undefined,
44
+ updatedAt: typeof raw.updatedAt === 'string' && Number.isFinite(new Date(raw.updatedAt).getTime()) ? raw.updatedAt : nowIso(),
51
45
  };
52
46
  }
53
47
  function normalizeAndSortMilestones(milestones) {
@@ -57,29 +51,33 @@ function normalizeAndSortMilestones(milestones) {
57
51
  }
58
52
  export function renderRoadmapMarkdown(milestones) {
59
53
  const lines = sortMilestonesNewestFirst(milestones).map((item) => {
60
- const checkbox = item.status === "done" ? "x" : " ";
61
- const timeText = item.time ? ` (time: ${item.time})` : "";
54
+ const checkbox = item.status === 'done' ? 'x' : ' ';
55
+ const timeText = item.time ? ` (time: ${item.time})` : '';
62
56
  return `- [${checkbox}] ${item.id}: ${item.title}${timeText}`;
63
57
  });
64
58
  return [
65
- "# Roadmap",
66
- "",
67
- "This file is generated from .projitive governance store by Projitive MCP. Manual edits will be overwritten.",
68
- "",
69
- "## Active Milestones",
70
- ...(lines.length > 0 ? lines : ["- (no milestones)"]),
71
- "",
72
- ].join("\n");
59
+ '# Roadmap',
60
+ '',
61
+ 'Projitive is an AI-first project governance framework for tasks, roadmaps, reports, and designs.',
62
+ 'Author: yinxulai',
63
+ 'Repository: https://github.com/yinxulai/projitive',
64
+ 'Do not edit this file manually. This file is automatically generated by Projitive.',
65
+ 'This file is generated from .projitive governance store by Projitive MCP. Manual edits will be overwritten.',
66
+ '',
67
+ '## Active Milestones',
68
+ ...(lines.length > 0 ? lines : ['- (no milestones)']),
69
+ '',
70
+ ].join('\n');
73
71
  }
74
72
  function resolveRoadmapArtifactPaths(governanceDir) {
75
73
  return {
76
- roadmapPath: path.join(governanceDir, ".projitive"),
74
+ roadmapPath: path.join(governanceDir, '.projitive'),
77
75
  markdownPath: path.join(governanceDir, ROADMAP_MARKDOWN_FILE),
78
76
  };
79
77
  }
80
78
  async function syncRoadmapMarkdownView(roadmapPath, markdownPath, markdown, force = false) {
81
- const sourceVersion = await getStoreVersion(roadmapPath, "roadmaps");
82
- const viewState = await getMarkdownViewState(roadmapPath, "roadmaps_markdown");
79
+ const sourceVersion = await getStoreVersion(roadmapPath, 'roadmaps');
80
+ const viewState = await getMarkdownViewState(roadmapPath, 'roadmaps_markdown');
83
81
  const markdownExists = await fs.access(markdownPath).then(() => true).catch(() => false);
84
82
  const shouldWrite = force
85
83
  || !markdownExists
@@ -88,8 +86,8 @@ async function syncRoadmapMarkdownView(roadmapPath, markdownPath, markdown, forc
88
86
  if (!shouldWrite) {
89
87
  return;
90
88
  }
91
- await fs.writeFile(markdownPath, markdown, "utf-8");
92
- await markMarkdownViewBuilt(roadmapPath, "roadmaps_markdown", sourceVersion);
89
+ await fs.writeFile(markdownPath, markdown, 'utf-8');
90
+ await markMarkdownViewBuilt(roadmapPath, 'roadmaps_markdown', sourceVersion);
93
91
  }
94
92
  export async function loadRoadmapDocument(inputPath) {
95
93
  return loadRoadmapDocumentWithOptions(inputPath, false);
@@ -98,8 +96,7 @@ export async function loadRoadmapDocumentWithOptions(inputPath, forceViewSync) {
98
96
  const governanceDir = await resolveGovernanceDir(inputPath);
99
97
  const { roadmapPath, markdownPath } = resolveRoadmapArtifactPaths(governanceDir);
100
98
  await ensureStore(roadmapPath);
101
- const milestones = normalizeAndSortMilestones(await loadRoadmapsFromStore(roadmapPath));
102
- const normalizedMilestones = normalizeAndSortMilestones(milestones);
99
+ const normalizedMilestones = normalizeAndSortMilestones(await loadRoadmapsFromStore(roadmapPath));
103
100
  const markdown = renderRoadmapMarkdown(normalizedMilestones);
104
101
  await syncRoadmapMarkdownView(roadmapPath, markdownPath, markdown, forceViewSync);
105
102
  return {
@@ -118,15 +115,15 @@ function collectRoadmapLintSuggestionItems(roadmapIds, tasks) {
118
115
  if (roadmapIds.length === 0) {
119
116
  suggestions.push({
120
117
  code: ROADMAP_LINT_CODES.IDS_EMPTY,
121
- message: "No roadmap IDs found in .projitive roadmap table.",
122
- fixHint: "Add at least one ROADMAP-xxxx milestone.",
118
+ message: 'No roadmap IDs found in .projitive roadmap table.',
119
+ fixHint: 'Add at least one ROADMAP-xxxx milestone.',
123
120
  });
124
121
  }
125
122
  if (tasks.length === 0) {
126
123
  suggestions.push({
127
124
  code: ROADMAP_LINT_CODES.TASKS_EMPTY,
128
- message: "No tasks found in .projitive task table.",
129
- fixHint: "Add task cards and bind roadmapRefs for traceability.",
125
+ message: 'No tasks found in .projitive task table.',
126
+ fixHint: 'Add task cards and bind roadmapRefs for traceability.',
130
127
  });
131
128
  return suggestions;
132
129
  }
@@ -136,15 +133,15 @@ function collectRoadmapLintSuggestionItems(roadmapIds, tasks) {
136
133
  suggestions.push({
137
134
  code: ROADMAP_LINT_CODES.TASK_REFS_EMPTY,
138
135
  message: `${unboundTasks.length} task(s) have empty roadmapRefs.`,
139
- fixHint: "Bind ROADMAP-xxxx where applicable.",
136
+ fixHint: 'Bind ROADMAP-xxxx where applicable.',
140
137
  });
141
138
  }
142
139
  const unknownRefs = Array.from(new Set(tasks.flatMap((task) => task.roadmapRefs).filter((id) => !roadmapSet.has(id))));
143
140
  if (unknownRefs.length > 0) {
144
141
  suggestions.push({
145
142
  code: ROADMAP_LINT_CODES.UNKNOWN_REFS,
146
- message: `Unknown roadmapRefs detected: ${unknownRefs.join(", ")}.`,
147
- fixHint: "Add missing roadmap IDs or fix task references.",
143
+ message: `Unknown roadmapRefs detected: ${unknownRefs.join(', ')}.`,
144
+ fixHint: 'Add missing roadmap IDs or fix task references.',
148
145
  });
149
146
  }
150
147
  const noLinkedRoadmaps = roadmapIds.filter((id) => !tasks.some((task) => task.roadmapRefs.includes(id)));
@@ -152,7 +149,7 @@ function collectRoadmapLintSuggestionItems(roadmapIds, tasks) {
152
149
  suggestions.push({
153
150
  code: ROADMAP_LINT_CODES.ZERO_LINKED_TASKS,
154
151
  message: `${noLinkedRoadmaps.length} roadmap ID(s) have zero linked tasks.`,
155
- fixHint: `Consider binding tasks to: ${noLinkedRoadmaps.slice(0, 3).join(", ")}${noLinkedRoadmaps.length > 3 ? ", ..." : ""}.`,
152
+ fixHint: `Consider binding tasks to: ${noLinkedRoadmaps.slice(0, 3).join(', ')}${noLinkedRoadmaps.length > 3 ? ', ...' : ''}.`,
156
153
  });
157
154
  }
158
155
  return suggestions;
@@ -161,234 +158,218 @@ export function collectRoadmapLintSuggestions(roadmapIds, tasks) {
161
158
  return renderLintSuggestions(collectRoadmapLintSuggestionItems(roadmapIds, tasks));
162
159
  }
163
160
  export function isValidRoadmapId(id) {
164
- return ROADMAP_ID_REGEX.test(id);
161
+ return toRoadmapIdNumericSuffix(id) > 0;
165
162
  }
166
163
  export function registerRoadmapTools(server) {
167
- server.registerTool("roadmapList", {
168
- title: "Roadmap List",
169
- description: "List roadmap IDs and task linkage for planning or traceability",
164
+ server.registerTool(...createGovernedTool({
165
+ name: 'roadmapList',
166
+ title: 'Roadmap List',
167
+ description: 'List roadmap IDs and task linkage for planning or traceability',
170
168
  inputSchema: {
171
169
  projectPath: z.string(),
172
170
  },
173
- }, async ({ projectPath }) => {
174
- const governanceDir = await resolveGovernanceDir(projectPath);
175
- const normalizedProjectPath = toProjectPath(governanceDir);
176
- const roadmapIds = await loadRoadmapIds(governanceDir);
177
- const { tasks } = await loadTasks(governanceDir);
178
- const lintSuggestions = collectRoadmapLintSuggestions(roadmapIds, tasks);
179
- const markdown = renderToolResponseMarkdown({
180
- toolName: "roadmapList",
181
- sections: [
182
- summarySection([
183
- `- projectPath: ${normalizedProjectPath}`,
184
- `- governanceDir: ${governanceDir}`,
185
- `- roadmapCount: ${roadmapIds.length}`,
186
- ]),
187
- evidenceSection([
188
- "- roadmaps:",
189
- ...roadmapIds.map((id) => {
190
- const linkedTasks = tasks.filter((task) => task.roadmapRefs.includes(id));
191
- return `- ${id} | linkedTasks=${linkedTasks.length}`;
192
- }),
193
- ]),
194
- guidanceSection(["- Pick one roadmap ID and call `roadmapContext`."]),
195
- lintSection(lintSuggestions),
196
- nextCallSection(roadmapIds[0]
197
- ? `roadmapContext(projectPath=\"${toProjectPath(governanceDir)}\", roadmapId=\"${roadmapIds[0]}\")`
198
- : undefined),
199
- ],
200
- });
201
- return asText(markdown);
202
- });
203
- server.registerTool("roadmapContext", {
204
- title: "Roadmap Context",
205
- description: "Inspect one roadmap with linked tasks and reference locations",
171
+ async execute({ projectPath }) {
172
+ const governanceDir = await resolveGovernanceDir(projectPath);
173
+ const normalizedProjectPath = toProjectPath(governanceDir);
174
+ const { milestones, markdownPath: roadmapViewPath } = await loadRoadmapDocument(governanceDir);
175
+ const roadmapIds = milestones.map((item) => item.id);
176
+ const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
177
+ return { normalizedProjectPath, governanceDir, roadmapIds, roadmapViewPath, tasksViewPath, tasks };
178
+ },
179
+ summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, roadmapIds }) => [
180
+ `- projectPath: ${normalizedProjectPath}`,
181
+ `- governanceDir: ${governanceDir}`,
182
+ `- tasksView: ${tasksViewPath}`,
183
+ `- roadmapView: ${roadmapViewPath}`,
184
+ `- roadmapCount: ${roadmapIds.length}`,
185
+ ],
186
+ evidence: ({ roadmapIds, tasks }) => [
187
+ '- roadmaps:',
188
+ ...roadmapIds.map((id) => {
189
+ const linkedTasks = tasks.filter((task) => task.roadmapRefs.includes(id));
190
+ return `- ${id} | linkedTasks=${linkedTasks.length}`;
191
+ }),
192
+ ],
193
+ guidance: () => ['- Pick one roadmap ID and call `roadmapContext`.'],
194
+ suggestions: ({ roadmapIds, tasks }) => collectRoadmapLintSuggestions(roadmapIds, tasks),
195
+ nextCall: ({ roadmapIds, normalizedProjectPath }) => roadmapIds[0]
196
+ ? `roadmapContext(projectPath="${normalizedProjectPath}", roadmapId="${roadmapIds[0]}")`
197
+ : undefined,
198
+ }));
199
+ server.registerTool(...createGovernedTool({
200
+ name: 'roadmapContext',
201
+ title: 'Roadmap Context',
202
+ description: 'Inspect one roadmap with linked tasks and reference locations',
206
203
  inputSchema: {
207
204
  projectPath: z.string(),
208
205
  roadmapId: z.string(),
209
206
  },
210
- }, async ({ projectPath, roadmapId }) => {
211
- if (!isValidRoadmapId(roadmapId)) {
212
- return {
213
- ...asText(renderErrorMarkdown("roadmapContext", `Invalid roadmap ID format: ${roadmapId}`, ["expected format: ROADMAP-0001", "retry with a valid roadmap ID"], `roadmapContext(projectPath=\"${projectPath}\", roadmapId=\"ROADMAP-0001\")`)),
214
- isError: true,
215
- };
216
- }
217
- const governanceDir = await resolveGovernanceDir(projectPath);
218
- const normalizedProjectPath = toProjectPath(governanceDir);
219
- const artifacts = await discoverGovernanceArtifacts(governanceDir);
220
- const fileCandidates = candidateFilesFromArtifacts(artifacts);
221
- const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, roadmapId)))).flat();
222
- const { tasks } = await loadTasks(governanceDir);
223
- const relatedTasks = tasks.filter((task) => task.roadmapRefs.includes(roadmapId));
224
- const roadmapIds = await loadRoadmapIds(governanceDir);
225
- const lintSuggestionItems = collectRoadmapLintSuggestionItems(roadmapIds, tasks);
226
- if (relatedTasks.length === 0) {
227
- lintSuggestionItems.push({
228
- code: ROADMAP_LINT_CODES.CONTEXT_RELATED_TASKS_EMPTY,
229
- message: `relatedTasks=0 for ${roadmapId}.`,
230
- fixHint: "Batch bind task roadmapRefs to improve execution traceability.",
231
- });
232
- }
233
- const lintSuggestions = renderLintSuggestions(lintSuggestionItems);
234
- const markdown = renderToolResponseMarkdown({
235
- toolName: "roadmapContext",
236
- sections: [
237
- summarySection([
238
- `- projectPath: ${normalizedProjectPath}`,
239
- `- governanceDir: ${governanceDir}`,
240
- `- roadmapId: ${roadmapId}`,
241
- `- relatedTasks: ${relatedTasks.length}`,
242
- `- references: ${referenceLocations.length}`,
243
- ]),
244
- evidenceSection([
245
- "### Related Tasks",
246
- ...relatedTasks.map((task) => `- ${task.id} | ${task.status} | ${task.title}`),
247
- "",
248
- "### Reference Locations",
249
- ...referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`),
250
- ]),
251
- guidanceSection([
252
- "- Read roadmap references first, then related tasks.",
253
- "- Keep ROADMAP/TASK IDs unchanged while updating markdown files.",
254
- "- Re-run `roadmapContext` after edits to confirm references remain consistent.",
255
- ]),
256
- lintSection(lintSuggestions),
257
- nextCallSection(`roadmapContext(projectPath=\"${toProjectPath(governanceDir)}\", roadmapId=\"${roadmapId}\")`),
258
- ],
259
- });
260
- return asText(markdown);
261
- });
262
- server.registerTool("roadmapCreate", {
263
- title: "Roadmap Create",
264
- description: "Create one roadmap milestone in governance store",
207
+ async execute({ projectPath, roadmapId }) {
208
+ if (!isValidRoadmapId(roadmapId)) {
209
+ throw new ToolExecutionError(`Invalid roadmap ID format: ${roadmapId}`, ['expected format: ROADMAP-1 or ROADMAP-0001', 'retry with a valid roadmap ID'], `roadmapContext(projectPath="${projectPath}", roadmapId="ROADMAP-0001")`);
210
+ }
211
+ const governanceDir = await resolveGovernanceDir(projectPath);
212
+ const normalizedProjectPath = toProjectPath(governanceDir);
213
+ const { markdownPath: roadmapViewPath } = resolveRoadmapArtifactPaths(governanceDir);
214
+ const artifacts = await discoverGovernanceArtifacts(governanceDir);
215
+ const fileCandidates = candidateFilesFromArtifacts(artifacts);
216
+ const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, roadmapId)))).flat();
217
+ const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
218
+ const relatedTasks = tasks.filter((task) => task.roadmapRefs.includes(roadmapId));
219
+ const roadmapIds = await loadRoadmapIds(governanceDir);
220
+ return { normalizedProjectPath, governanceDir, roadmapId, roadmapViewPath, tasksViewPath, relatedTasks, referenceLocations, roadmapIds, tasks };
221
+ },
222
+ summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, roadmapId, relatedTasks, referenceLocations }) => [
223
+ `- projectPath: ${normalizedProjectPath}`,
224
+ `- governanceDir: ${governanceDir}`,
225
+ `- tasksView: ${tasksViewPath}`,
226
+ `- roadmapView: ${roadmapViewPath}`,
227
+ `- roadmapId: ${roadmapId}`,
228
+ `- relatedTasks: ${relatedTasks.length}`,
229
+ `- references: ${referenceLocations.length}`,
230
+ ],
231
+ evidence: ({ relatedTasks, referenceLocations }) => [
232
+ '### Related Tasks',
233
+ ...relatedTasks.map((task) => `- ${task.id} | ${task.status} | ${task.title}`),
234
+ '',
235
+ '### Reference Locations',
236
+ ...referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`),
237
+ ],
238
+ guidance: () => [
239
+ '- Read roadmap references first, then related tasks.',
240
+ '- Keep ROADMAP/TASK IDs unchanged while updating markdown files.',
241
+ '- Re-run `roadmapContext` after edits to confirm references remain consistent.',
242
+ ],
243
+ suggestions: ({ roadmapIds, tasks, relatedTasks, roadmapId }) => {
244
+ const items = collectRoadmapLintSuggestionItems(roadmapIds, tasks);
245
+ if (relatedTasks.length === 0) {
246
+ items.push({
247
+ code: ROADMAP_LINT_CODES.CONTEXT_RELATED_TASKS_EMPTY,
248
+ message: `relatedTasks=0 for ${roadmapId}.`,
249
+ fixHint: 'Batch bind task roadmapRefs to improve execution traceability.',
250
+ });
251
+ }
252
+ return renderLintSuggestions(items);
253
+ },
254
+ nextCall: ({ normalizedProjectPath, roadmapId }) => `roadmapContext(projectPath="${normalizedProjectPath}", roadmapId="${roadmapId}")`,
255
+ }));
256
+ server.registerTool(...createGovernedTool({
257
+ name: 'roadmapCreate',
258
+ title: 'Roadmap Create',
259
+ description: 'Create one roadmap milestone in governance store',
265
260
  inputSchema: {
266
261
  projectPath: z.string(),
267
262
  roadmapId: z.string().optional(),
268
263
  title: z.string(),
269
- status: z.enum(["active", "done"]).optional(),
264
+ status: z.enum(['active', 'done']).optional(),
270
265
  time: z.string().optional(),
271
266
  },
272
- }, async ({ projectPath, roadmapId, title, status, time }) => {
273
- if (roadmapId && !isValidRoadmapId(roadmapId)) {
274
- return {
275
- ...asText(renderErrorMarkdown("roadmapCreate", `Invalid roadmap ID format: ${roadmapId}`, ["expected format: ROADMAP-0001", "omit roadmapId to auto-generate next ID"], `roadmapCreate(projectPath=\"${projectPath}\", title=\"Define milestone\", time=\"2026-Q2\")`)),
276
- isError: true,
277
- };
278
- }
279
- const governanceDir = await resolveGovernanceDir(projectPath);
280
- const normalizedProjectPath = toProjectPath(governanceDir);
281
- const doc = await loadRoadmapDocument(governanceDir);
282
- const finalRoadmapId = roadmapId ?? nextRoadmapId(doc.milestones);
283
- const duplicated = doc.milestones.some((item) => item.id === finalRoadmapId);
284
- if (duplicated) {
285
- return {
286
- ...asText(renderErrorMarkdown("roadmapCreate", `Roadmap milestone already exists: ${finalRoadmapId}`, ["roadmap IDs must be unique", "use roadmapUpdate for existing milestone"], `roadmapUpdate(projectPath=\"${normalizedProjectPath}\", roadmapId=\"${finalRoadmapId}\", updates={...})`)),
287
- isError: true,
288
- };
289
- }
290
- const created = normalizeMilestone({
291
- id: finalRoadmapId,
292
- title,
293
- status: status ?? "active",
294
- time,
295
- updatedAt: nowIso(),
296
- });
297
- await upsertRoadmapInStore(doc.roadmapPath, created);
298
- const refreshed = await loadRoadmapDocumentWithOptions(governanceDir, true);
299
- const { tasks } = await loadTasks(governanceDir);
300
- const lintSuggestions = collectRoadmapLintSuggestions(refreshed.milestones.map((item) => item.id), tasks);
301
- const markdown = renderToolResponseMarkdown({
302
- toolName: "roadmapCreate",
303
- sections: [
304
- summarySection([
305
- `- projectPath: ${normalizedProjectPath}`,
306
- `- governanceDir: ${governanceDir}`,
307
- `- roadmapId: ${created.id}`,
308
- `- status: ${created.status}`,
309
- `- updatedAt: ${created.updatedAt}`,
310
- ]),
311
- evidenceSection([
312
- "### Created Milestone",
313
- `- ${created.id} | ${created.status} | ${created.title}${created.time ? ` | time=${created.time}` : ""}`,
314
- "",
315
- "### Roadmap Count",
316
- `- total: ${refreshed.milestones.length}`,
317
- ]),
318
- guidanceSection([
319
- "Milestone created successfully and roadmap.md has been synced.",
320
- "Re-run roadmapContext to verify linked task traceability.",
321
- ]),
322
- lintSection(lintSuggestions),
323
- nextCallSection(`roadmapContext(projectPath=\"${normalizedProjectPath}\", roadmapId=\"${created.id}\")`),
324
- ],
325
- });
326
- return asText(markdown);
327
- });
328
- server.registerTool("roadmapUpdate", {
329
- title: "Roadmap Update",
330
- description: "Update one roadmap milestone fields incrementally in governance store",
267
+ async execute({ projectPath, roadmapId, title, status, time }) {
268
+ if (roadmapId && !isValidRoadmapId(roadmapId)) {
269
+ throw new ToolExecutionError(`Invalid roadmap ID format: ${roadmapId}`, ['expected format: ROADMAP-1 or ROADMAP-0001', 'omit roadmapId to auto-generate next ID'], `roadmapCreate(projectPath="${projectPath}", title="Define milestone", time="2026-Q2")`);
270
+ }
271
+ const governanceDir = await resolveGovernanceDir(projectPath);
272
+ const normalizedProjectPath = toProjectPath(governanceDir);
273
+ const doc = await loadRoadmapDocument(governanceDir);
274
+ const { markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
275
+ const finalRoadmapId = roadmapId ?? nextRoadmapId(doc.milestones);
276
+ const duplicated = doc.milestones.some((item) => item.id === finalRoadmapId);
277
+ if (duplicated) {
278
+ throw new ToolExecutionError(`Roadmap milestone already exists: ${finalRoadmapId}`, ['roadmap IDs must be unique', 'use roadmapUpdate for existing milestone'], `roadmapUpdate(projectPath="${normalizedProjectPath}", roadmapId="${finalRoadmapId}", updates={...})`);
279
+ }
280
+ const created = normalizeMilestone({
281
+ id: finalRoadmapId,
282
+ title,
283
+ status: status ?? 'active',
284
+ time,
285
+ updatedAt: nowIso(),
286
+ });
287
+ await upsertRoadmapInStore(doc.roadmapPath, created);
288
+ const refreshed = await loadRoadmapDocumentWithOptions(governanceDir, true);
289
+ const { tasks } = await loadTasks(governanceDir);
290
+ return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath: refreshed.markdownPath, created, refreshed, tasks };
291
+ },
292
+ summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, created }) => [
293
+ `- projectPath: ${normalizedProjectPath}`,
294
+ `- governanceDir: ${governanceDir}`,
295
+ `- tasksView: ${tasksViewPath}`,
296
+ `- roadmapView: ${roadmapViewPath}`,
297
+ `- roadmapId: ${created.id}`,
298
+ `- status: ${created.status}`,
299
+ `- updatedAt: ${created.updatedAt}`,
300
+ ],
301
+ evidence: ({ created, refreshed }) => [
302
+ '### Created Milestone',
303
+ `- ${created.id} | ${created.status} | ${created.title}${created.time ? ` | time=${created.time}` : ''}`,
304
+ '',
305
+ '### Roadmap Count',
306
+ `- total: ${refreshed.milestones.length}`,
307
+ ],
308
+ guidance: () => [
309
+ 'Milestone created successfully and roadmap.md has been synced.',
310
+ 'Re-run roadmapContext to verify linked task traceability.',
311
+ ],
312
+ suggestions: ({ refreshed, tasks }) => collectRoadmapLintSuggestions(refreshed.milestones.map((item) => item.id), tasks),
313
+ nextCall: ({ normalizedProjectPath, created }) => `roadmapContext(projectPath="${normalizedProjectPath}", roadmapId="${created.id}")`,
314
+ }));
315
+ server.registerTool(...createGovernedTool({
316
+ name: 'roadmapUpdate',
317
+ title: 'Roadmap Update',
318
+ description: 'Update one roadmap milestone fields incrementally in governance store',
331
319
  inputSchema: {
332
320
  projectPath: z.string(),
333
321
  roadmapId: z.string(),
334
322
  updates: z.object({
335
323
  title: z.string().optional(),
336
- status: z.enum(["active", "done"]).optional(),
324
+ status: z.enum(['active', 'done']).optional(),
337
325
  time: z.string().optional(),
338
326
  }),
339
327
  },
340
- }, async ({ projectPath, roadmapId, updates }) => {
341
- if (!isValidRoadmapId(roadmapId)) {
342
- return {
343
- ...asText(renderErrorMarkdown("roadmapUpdate", `Invalid roadmap ID format: ${roadmapId}`, ["expected format: ROADMAP-0001", "retry with a valid roadmap ID"], `roadmapUpdate(projectPath="${projectPath}", roadmapId="ROADMAP-0001", updates={...})`)),
344
- isError: true,
345
- };
346
- }
347
- const governanceDir = await resolveGovernanceDir(projectPath);
348
- const normalizedProjectPath = toProjectPath(governanceDir);
349
- const doc = await loadRoadmapDocument(governanceDir);
350
- const existing = doc.milestones.find((item) => item.id === roadmapId);
351
- if (!existing) {
352
- return {
353
- ...asText(renderErrorMarkdown("roadmapUpdate", `Roadmap milestone not found: ${roadmapId}`, ["run roadmapList to discover existing roadmap IDs", "retry with an existing roadmap ID"], `roadmapList(projectPath="${toProjectPath(governanceDir)}")`)),
354
- isError: true,
328
+ async execute({ projectPath, roadmapId, updates }) {
329
+ if (!isValidRoadmapId(roadmapId)) {
330
+ throw new ToolExecutionError(`Invalid roadmap ID format: ${roadmapId}`, ['expected format: ROADMAP-1 or ROADMAP-0001', 'retry with a valid roadmap ID'], `roadmapUpdate(projectPath="${projectPath}", roadmapId="ROADMAP-0001", updates={...})`);
331
+ }
332
+ const governanceDir = await resolveGovernanceDir(projectPath);
333
+ const normalizedProjectPath = toProjectPath(governanceDir);
334
+ const doc = await loadRoadmapDocument(governanceDir);
335
+ const { markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
336
+ const existing = doc.milestones.find((item) => item.id === roadmapId);
337
+ if (!existing) {
338
+ throw new ToolExecutionError(`Roadmap milestone not found: ${roadmapId}`, ['run roadmapList to discover existing roadmap IDs', 'retry with an existing roadmap ID'], `roadmapList(projectPath="${toProjectPath(governanceDir)}")`);
339
+ }
340
+ const updated = {
341
+ ...existing,
342
+ title: updates.title ?? existing.title,
343
+ status: updates.status ?? existing.status,
344
+ time: updates.time ?? existing.time,
345
+ updatedAt: nowIso(),
355
346
  };
356
- }
357
- const updated = {
358
- ...existing,
359
- title: updates.title ?? existing.title,
360
- status: updates.status ?? existing.status,
361
- time: updates.time ?? existing.time,
362
- updatedAt: nowIso(),
363
- };
364
- await upsertRoadmapInStore(doc.roadmapPath, updated);
365
- const refreshed = await loadRoadmapDocumentWithOptions(governanceDir, true);
366
- const markdown = renderToolResponseMarkdown({
367
- toolName: "roadmapUpdate",
368
- sections: [
369
- summarySection([
370
- `- projectPath: ${normalizedProjectPath}`,
371
- `- governanceDir: ${governanceDir}`,
372
- `- roadmapId: ${roadmapId}`,
373
- `- newStatus: ${updated.status}`,
374
- `- updatedAt: ${updated.updatedAt}`,
375
- ]),
376
- evidenceSection([
377
- "### Updated Milestone",
378
- `- ${updated.id} | ${updated.status} | ${updated.title}${updated.time ? ` | time=${updated.time}` : ""}`,
379
- "",
380
- `### Roadmap Count`,
381
- `- total: ${refreshed.milestones.length}`,
382
- ]),
383
- guidanceSection([
384
- "Milestone updated successfully and roadmap.md has been synced.",
385
- "Re-run roadmapContext to verify linked task traceability.",
386
- ".projitive governance store is source of truth; roadmap.md is a generated view and may be overwritten.",
387
- ]),
388
- lintSection([]),
389
- nextCallSection(`roadmapContext(projectPath="${toProjectPath(governanceDir)}", roadmapId="${roadmapId}")`),
390
- ],
391
- });
392
- return asText(markdown);
393
- });
347
+ await upsertRoadmapInStore(doc.roadmapPath, updated);
348
+ const refreshed = await loadRoadmapDocumentWithOptions(governanceDir, true);
349
+ return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath: refreshed.markdownPath, roadmapId, updated, refreshed };
350
+ },
351
+ summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, roadmapId, updated }) => [
352
+ `- projectPath: ${normalizedProjectPath}`,
353
+ `- governanceDir: ${governanceDir}`,
354
+ `- tasksView: ${tasksViewPath}`,
355
+ `- roadmapView: ${roadmapViewPath}`,
356
+ `- roadmapId: ${roadmapId}`,
357
+ `- newStatus: ${updated.status}`,
358
+ `- updatedAt: ${updated.updatedAt}`,
359
+ ],
360
+ evidence: ({ updated, refreshed }) => [
361
+ '### Updated Milestone',
362
+ `- ${updated.id} | ${updated.status} | ${updated.title}${updated.time ? ` | time=${updated.time}` : ''}`,
363
+ '',
364
+ '### Roadmap Count',
365
+ `- total: ${refreshed.milestones.length}`,
366
+ ],
367
+ guidance: () => [
368
+ 'Milestone updated successfully and roadmap.md has been synced.',
369
+ 'Re-run roadmapContext to verify linked task traceability.',
370
+ '.projitive governance store is source of truth; roadmap.md is a generated view and may be overwritten.',
371
+ ],
372
+ suggestions: () => [],
373
+ nextCall: ({ normalizedProjectPath, roadmapId }) => `roadmapContext(projectPath="${normalizedProjectPath}", roadmapId="${roadmapId}")`,
374
+ }));
394
375
  }