@projitive/mcp 2.0.4 → 2.1.1

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.
@@ -1,8 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
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';
4
+ import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, ROADMAP_LINT_CODES, renderLintSuggestions, findTextReferences, ensureStore, loadRoadmapsFromStore, upsertRoadmapInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, createGovernedTool, ToolExecutionError, } from '../common/index.js';
6
5
  import { resolveGovernanceDir, toProjectPath } from './project.js';
7
6
  import { loadTasks, loadTasksDocument } from './task.js';
8
7
  export const ROADMAP_ID_REGEX = /^ROADMAP-(\d+)$/;
@@ -33,11 +32,7 @@ function sortMilestonesNewestFirst(milestones) {
33
32
  if (Number.isFinite(updatedAtDelta) && updatedAtDelta !== 0) {
34
33
  return updatedAtDelta;
35
34
  }
36
- const idDelta = toRoadmapIdNumericSuffix(b.id) - toRoadmapIdNumericSuffix(a.id);
37
- if (idDelta !== 0) {
38
- return idDelta;
39
- }
40
- return b.id.localeCompare(a.id);
35
+ return toRoadmapIdNumericSuffix(b.id) - toRoadmapIdNumericSuffix(a.id);
41
36
  });
42
37
  }
43
38
  function normalizeMilestone(raw) {
@@ -101,8 +96,7 @@ export async function loadRoadmapDocumentWithOptions(inputPath, forceViewSync) {
101
96
  const governanceDir = await resolveGovernanceDir(inputPath);
102
97
  const { roadmapPath, markdownPath } = resolveRoadmapArtifactPaths(governanceDir);
103
98
  await ensureStore(roadmapPath);
104
- const milestones = normalizeAndSortMilestones(await loadRoadmapsFromStore(roadmapPath));
105
- const normalizedMilestones = normalizeAndSortMilestones(milestones);
99
+ const normalizedMilestones = normalizeAndSortMilestones(await loadRoadmapsFromStore(roadmapPath));
106
100
  const markdown = renderRoadmapMarkdown(normalizedMilestones);
107
101
  await syncRoadmapMarkdownView(roadmapPath, markdownPath, markdown, forceViewSync);
108
102
  return {
@@ -167,108 +161,100 @@ export function isValidRoadmapId(id) {
167
161
  return toRoadmapIdNumericSuffix(id) > 0;
168
162
  }
169
163
  export function registerRoadmapTools(server) {
170
- server.registerTool('roadmapList', {
164
+ server.registerTool(...createGovernedTool({
165
+ name: 'roadmapList',
171
166
  title: 'Roadmap List',
172
167
  description: 'List roadmap IDs and task linkage for planning or traceability',
173
168
  inputSchema: {
174
169
  projectPath: z.string(),
175
170
  },
176
- }, async ({ projectPath }) => {
177
- const governanceDir = await resolveGovernanceDir(projectPath);
178
- const normalizedProjectPath = toProjectPath(governanceDir);
179
- const { milestones, markdownPath: roadmapViewPath } = await loadRoadmapDocument(governanceDir);
180
- const roadmapIds = milestones.map((item) => item.id);
181
- const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
182
- const lintSuggestions = collectRoadmapLintSuggestions(roadmapIds, tasks);
183
- const markdown = renderToolResponseMarkdown({
184
- toolName: 'roadmapList',
185
- sections: [
186
- summarySection([
187
- `- projectPath: ${normalizedProjectPath}`,
188
- `- governanceDir: ${governanceDir}`,
189
- `- tasksView: ${tasksViewPath}`,
190
- `- roadmapView: ${roadmapViewPath}`,
191
- `- roadmapCount: ${roadmapIds.length}`,
192
- ]),
193
- evidenceSection([
194
- '- roadmaps:',
195
- ...roadmapIds.map((id) => {
196
- const linkedTasks = tasks.filter((task) => task.roadmapRefs.includes(id));
197
- return `- ${id} | linkedTasks=${linkedTasks.length}`;
198
- }),
199
- ]),
200
- guidanceSection(['- Pick one roadmap ID and call `roadmapContext`.']),
201
- lintSection(lintSuggestions),
202
- nextCallSection(roadmapIds[0]
203
- ? `roadmapContext(projectPath="${toProjectPath(governanceDir)}", roadmapId="${roadmapIds[0]}")`
204
- : undefined),
205
- ],
206
- });
207
- return asText(markdown);
208
- });
209
- server.registerTool('roadmapContext', {
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',
210
201
  title: 'Roadmap Context',
211
202
  description: 'Inspect one roadmap with linked tasks and reference locations',
212
203
  inputSchema: {
213
204
  projectPath: z.string(),
214
205
  roadmapId: z.string(),
215
206
  },
216
- }, async ({ projectPath, roadmapId }) => {
217
- if (!isValidRoadmapId(roadmapId)) {
218
- return {
219
- ...asText(renderErrorMarkdown('roadmapContext', `Invalid roadmap ID format: ${roadmapId}`, ['expected format: ROADMAP-1 or ROADMAP-0001', 'retry with a valid roadmap ID'], `roadmapContext(projectPath="${projectPath}", roadmapId="ROADMAP-0001")`)),
220
- isError: true,
221
- };
222
- }
223
- const governanceDir = await resolveGovernanceDir(projectPath);
224
- const normalizedProjectPath = toProjectPath(governanceDir);
225
- const { markdownPath: roadmapViewPath } = resolveRoadmapArtifactPaths(governanceDir);
226
- const artifacts = await discoverGovernanceArtifacts(governanceDir);
227
- const fileCandidates = candidateFilesFromArtifacts(artifacts);
228
- const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, roadmapId)))).flat();
229
- const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
230
- const relatedTasks = tasks.filter((task) => task.roadmapRefs.includes(roadmapId));
231
- const roadmapIds = await loadRoadmapIds(governanceDir);
232
- const lintSuggestionItems = collectRoadmapLintSuggestionItems(roadmapIds, tasks);
233
- if (relatedTasks.length === 0) {
234
- lintSuggestionItems.push({
235
- code: ROADMAP_LINT_CODES.CONTEXT_RELATED_TASKS_EMPTY,
236
- message: `relatedTasks=0 for ${roadmapId}.`,
237
- fixHint: 'Batch bind task roadmapRefs to improve execution traceability.',
238
- });
239
- }
240
- const lintSuggestions = renderLintSuggestions(lintSuggestionItems);
241
- const markdown = renderToolResponseMarkdown({
242
- toolName: 'roadmapContext',
243
- sections: [
244
- summarySection([
245
- `- projectPath: ${normalizedProjectPath}`,
246
- `- governanceDir: ${governanceDir}`,
247
- `- tasksView: ${tasksViewPath}`,
248
- `- roadmapView: ${roadmapViewPath}`,
249
- `- roadmapId: ${roadmapId}`,
250
- `- relatedTasks: ${relatedTasks.length}`,
251
- `- references: ${referenceLocations.length}`,
252
- ]),
253
- evidenceSection([
254
- '### Related Tasks',
255
- ...relatedTasks.map((task) => `- ${task.id} | ${task.status} | ${task.title}`),
256
- '',
257
- '### Reference Locations',
258
- ...referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`),
259
- ]),
260
- guidanceSection([
261
- '- Read roadmap references first, then related tasks.',
262
- '- Keep ROADMAP/TASK IDs unchanged while updating markdown files.',
263
- '- Re-run `roadmapContext` after edits to confirm references remain consistent.',
264
- ]),
265
- lintSection(lintSuggestions),
266
- nextCallSection(`roadmapContext(projectPath="${toProjectPath(governanceDir)}", roadmapId="${roadmapId}")`),
267
- ],
268
- });
269
- return asText(markdown);
270
- });
271
- server.registerTool('roadmapCreate', {
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',
272
258
  title: 'Roadmap Create',
273
259
  description: 'Create one roadmap milestone in governance store',
274
260
  inputSchema: {
@@ -278,66 +264,56 @@ export function registerRoadmapTools(server) {
278
264
  status: z.enum(['active', 'done']).optional(),
279
265
  time: z.string().optional(),
280
266
  },
281
- }, async ({ projectPath, roadmapId, title, status, time }) => {
282
- if (roadmapId && !isValidRoadmapId(roadmapId)) {
283
- return {
284
- ...asText(renderErrorMarkdown('roadmapCreate', `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")`)),
285
- isError: true,
286
- };
287
- }
288
- const governanceDir = await resolveGovernanceDir(projectPath);
289
- const normalizedProjectPath = toProjectPath(governanceDir);
290
- const doc = await loadRoadmapDocument(governanceDir);
291
- const { markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
292
- const finalRoadmapId = roadmapId ?? nextRoadmapId(doc.milestones);
293
- const duplicated = doc.milestones.some((item) => item.id === finalRoadmapId);
294
- if (duplicated) {
295
- return {
296
- ...asText(renderErrorMarkdown('roadmapCreate', `Roadmap milestone already exists: ${finalRoadmapId}`, ['roadmap IDs must be unique', 'use roadmapUpdate for existing milestone'], `roadmapUpdate(projectPath="${normalizedProjectPath}", roadmapId="${finalRoadmapId}", updates={...})`)),
297
- isError: true,
298
- };
299
- }
300
- const created = normalizeMilestone({
301
- id: finalRoadmapId,
302
- title,
303
- status: status ?? 'active',
304
- time,
305
- updatedAt: nowIso(),
306
- });
307
- await upsertRoadmapInStore(doc.roadmapPath, created);
308
- const refreshed = await loadRoadmapDocumentWithOptions(governanceDir, true);
309
- const { tasks } = await loadTasks(governanceDir);
310
- const lintSuggestions = collectRoadmapLintSuggestions(refreshed.milestones.map((item) => item.id), tasks);
311
- const markdown = renderToolResponseMarkdown({
312
- toolName: 'roadmapCreate',
313
- sections: [
314
- summarySection([
315
- `- projectPath: ${normalizedProjectPath}`,
316
- `- governanceDir: ${governanceDir}`,
317
- `- tasksView: ${tasksViewPath}`,
318
- `- roadmapView: ${refreshed.markdownPath}`,
319
- `- roadmapId: ${created.id}`,
320
- `- status: ${created.status}`,
321
- `- updatedAt: ${created.updatedAt}`,
322
- ]),
323
- evidenceSection([
324
- '### Created Milestone',
325
- `- ${created.id} | ${created.status} | ${created.title}${created.time ? ` | time=${created.time}` : ''}`,
326
- '',
327
- '### Roadmap Count',
328
- `- total: ${refreshed.milestones.length}`,
329
- ]),
330
- guidanceSection([
331
- 'Milestone created successfully and roadmap.md has been synced.',
332
- 'Re-run roadmapContext to verify linked task traceability.',
333
- ]),
334
- lintSection(lintSuggestions),
335
- nextCallSection(`roadmapContext(projectPath="${normalizedProjectPath}", roadmapId="${created.id}")`),
336
- ],
337
- });
338
- return asText(markdown);
339
- });
340
- server.registerTool('roadmapUpdate', {
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',
341
317
  title: 'Roadmap Update',
342
318
  description: 'Update one roadmap milestone fields incrementally in governance store',
343
319
  inputSchema: {
@@ -349,61 +325,51 @@ export function registerRoadmapTools(server) {
349
325
  time: z.string().optional(),
350
326
  }),
351
327
  },
352
- }, async ({ projectPath, roadmapId, updates }) => {
353
- if (!isValidRoadmapId(roadmapId)) {
354
- return {
355
- ...asText(renderErrorMarkdown('roadmapUpdate', `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={...})`)),
356
- isError: true,
357
- };
358
- }
359
- const governanceDir = await resolveGovernanceDir(projectPath);
360
- const normalizedProjectPath = toProjectPath(governanceDir);
361
- const doc = await loadRoadmapDocument(governanceDir);
362
- const { markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
363
- const existing = doc.milestones.find((item) => item.id === roadmapId);
364
- if (!existing) {
365
- return {
366
- ...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)}")`)),
367
- 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(),
368
346
  };
369
- }
370
- const updated = {
371
- ...existing,
372
- title: updates.title ?? existing.title,
373
- status: updates.status ?? existing.status,
374
- time: updates.time ?? existing.time,
375
- updatedAt: nowIso(),
376
- };
377
- await upsertRoadmapInStore(doc.roadmapPath, updated);
378
- const refreshed = await loadRoadmapDocumentWithOptions(governanceDir, true);
379
- const markdown = renderToolResponseMarkdown({
380
- toolName: 'roadmapUpdate',
381
- sections: [
382
- summarySection([
383
- `- projectPath: ${normalizedProjectPath}`,
384
- `- governanceDir: ${governanceDir}`,
385
- `- tasksView: ${tasksViewPath}`,
386
- `- roadmapView: ${refreshed.markdownPath}`,
387
- `- roadmapId: ${roadmapId}`,
388
- `- newStatus: ${updated.status}`,
389
- `- updatedAt: ${updated.updatedAt}`,
390
- ]),
391
- evidenceSection([
392
- '### Updated Milestone',
393
- `- ${updated.id} | ${updated.status} | ${updated.title}${updated.time ? ` | time=${updated.time}` : ''}`,
394
- '',
395
- '### Roadmap Count',
396
- `- total: ${refreshed.milestones.length}`,
397
- ]),
398
- guidanceSection([
399
- 'Milestone updated successfully and roadmap.md has been synced.',
400
- 'Re-run roadmapContext to verify linked task traceability.',
401
- '.projitive governance store is source of truth; roadmap.md is a generated view and may be overwritten.',
402
- ]),
403
- lintSection([]),
404
- nextCallSection(`roadmapContext(projectPath="${toProjectPath(governanceDir)}", roadmapId="${roadmapId}")`),
405
- ],
406
- });
407
- return asText(markdown);
408
- });
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
+ }));
409
375
  }