@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.
- package/output/package.json +8 -2
- package/output/source/common/artifacts.js +1 -1
- package/output/source/common/artifacts.test.js +11 -11
- package/output/source/common/errors.js +19 -19
- package/output/source/common/errors.test.js +59 -0
- package/output/source/common/files.js +30 -19
- package/output/source/common/files.test.js +14 -14
- package/output/source/common/index.js +11 -10
- package/output/source/common/linter.js +29 -27
- package/output/source/common/linter.test.js +9 -9
- package/output/source/common/markdown.js +3 -3
- package/output/source/common/markdown.test.js +15 -15
- package/output/source/common/response.js +91 -107
- package/output/source/common/response.test.js +30 -30
- package/output/source/common/store.js +40 -40
- package/output/source/common/store.test.js +72 -72
- package/output/source/common/tool.js +43 -0
- package/output/source/common/types.js +3 -3
- package/output/source/common/utils.js +8 -8
- package/output/source/common/utils.test.js +48 -0
- package/output/source/index.js +16 -16
- package/output/source/index.runtime.test.js +57 -0
- package/output/source/index.test.js +64 -64
- package/output/source/prompts/index.js +3 -3
- package/output/source/prompts/index.test.js +23 -0
- package/output/source/prompts/quickStart.js +96 -96
- package/output/source/prompts/quickStart.test.js +24 -0
- package/output/source/prompts/taskDiscovery.js +184 -184
- package/output/source/prompts/taskDiscovery.test.js +24 -0
- package/output/source/prompts/taskExecution.js +164 -148
- package/output/source/prompts/taskExecution.test.js +27 -0
- package/output/source/resources/designs.js +26 -26
- package/output/source/resources/designs.resources.test.js +52 -0
- package/output/source/resources/designs.test.js +88 -88
- package/output/source/resources/governance.js +19 -19
- package/output/source/resources/governance.test.js +35 -0
- package/output/source/resources/index.js +2 -2
- package/output/source/resources/index.test.js +18 -0
- package/output/source/resources/readme.js +7 -7
- package/output/source/resources/readme.test.js +113 -113
- package/output/source/resources/reports.js +10 -10
- package/output/source/resources/reports.test.js +83 -83
- package/output/source/tools/index.js +3 -3
- package/output/source/tools/index.test.js +23 -0
- package/output/source/tools/project.js +330 -377
- package/output/source/tools/project.test.js +308 -175
- package/output/source/tools/roadmap.js +236 -255
- package/output/source/tools/roadmap.test.js +241 -46
- package/output/source/tools/task.js +770 -652
- package/output/source/tools/task.test.js +433 -105
- package/output/source/types.js +28 -22
- package/package.json +8 -2
|
@@ -1,32 +1,30 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { z } from
|
|
4
|
-
import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, ROADMAP_LINT_CODES, renderLintSuggestions, findTextReferences, ensureStore, loadRoadmapsFromStore, upsertRoadmapInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
export const
|
|
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) =>
|
|
15
|
+
.filter((value) => value > 0)
|
|
17
16
|
.reduce((max, value) => Math.max(max, value), 0);
|
|
18
17
|
const next = maxSuffix + 1;
|
|
19
|
-
|
|
20
|
-
|
|
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(
|
|
22
|
+
const match = roadmapId.match(ROADMAP_ID_REGEX);
|
|
26
23
|
if (!match) {
|
|
27
24
|
return -1;
|
|
28
25
|
}
|
|
29
|
-
|
|
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
|
-
|
|
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 ===
|
|
49
|
-
time: typeof raw.time ===
|
|
50
|
-
updatedAt: typeof raw.updatedAt ===
|
|
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 ===
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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,
|
|
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,
|
|
82
|
-
const viewState = await getMarkdownViewState(roadmapPath,
|
|
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,
|
|
92
|
-
await markMarkdownViewBuilt(roadmapPath,
|
|
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
|
|
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:
|
|
122
|
-
fixHint:
|
|
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:
|
|
129
|
-
fixHint:
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
161
|
+
return toRoadmapIdNumericSuffix(id) > 0;
|
|
165
162
|
}
|
|
166
163
|
export function registerRoadmapTools(server) {
|
|
167
|
-
server.registerTool(
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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([
|
|
264
|
+
status: z.enum(['active', 'done']).optional(),
|
|
270
265
|
time: z.string().optional(),
|
|
271
266
|
},
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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([
|
|
324
|
+
status: z.enum(['active', 'done']).optional(),
|
|
337
325
|
time: z.string().optional(),
|
|
338
326
|
}),
|
|
339
327
|
},
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
...
|
|
354
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
}
|