@projitive/mcp 2.0.3 → 2.0.4

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 (40) 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/files.js +11 -11
  6. package/output/source/common/files.test.js +14 -14
  7. package/output/source/common/index.js +10 -10
  8. package/output/source/common/linter.js +27 -27
  9. package/output/source/common/linter.test.js +9 -9
  10. package/output/source/common/markdown.js +3 -3
  11. package/output/source/common/markdown.test.js +15 -15
  12. package/output/source/common/response.js +74 -74
  13. package/output/source/common/response.test.js +30 -30
  14. package/output/source/common/store.js +40 -40
  15. package/output/source/common/store.test.js +72 -72
  16. package/output/source/common/types.js +3 -3
  17. package/output/source/common/utils.js +8 -8
  18. package/output/source/index.js +16 -16
  19. package/output/source/index.test.js +64 -64
  20. package/output/source/prompts/index.js +3 -3
  21. package/output/source/prompts/quickStart.js +96 -96
  22. package/output/source/prompts/taskDiscovery.js +184 -184
  23. package/output/source/prompts/taskExecution.js +148 -148
  24. package/output/source/resources/designs.js +26 -26
  25. package/output/source/resources/designs.test.js +88 -88
  26. package/output/source/resources/governance.js +19 -19
  27. package/output/source/resources/index.js +2 -2
  28. package/output/source/resources/readme.js +7 -7
  29. package/output/source/resources/readme.test.js +113 -113
  30. package/output/source/resources/reports.js +10 -10
  31. package/output/source/resources/reports.test.js +83 -83
  32. package/output/source/tools/index.js +3 -3
  33. package/output/source/tools/project.js +191 -191
  34. package/output/source/tools/project.test.js +174 -173
  35. package/output/source/tools/roadmap.js +110 -95
  36. package/output/source/tools/roadmap.test.js +54 -46
  37. package/output/source/tools/task.js +305 -277
  38. package/output/source/tools/task.test.js +117 -110
  39. package/output/source/types.js +22 -22
  40. package/package.json +8 -2
@@ -1,15 +1,14 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import process from "node:process";
4
- import { z } from "zod";
5
- import { discoverGovernanceArtifacts, catchIt, PROJECT_LINT_CODES, renderLintSuggestions, ensureStore, replaceRoadmapsInStore, replaceTasksInStore, markMarkdownViewDirty, loadTaskStatusStatsFromStore, loadRoadmapIdsFromStore, } from "../common/index.js";
6
- import { asText, evidenceSection, getDefaultToolTemplateMarkdown, guidanceSection, lintSection, nextCallSection, renderToolResponseMarkdown, summarySection, } from "../common/index.js";
7
- import { collectTaskLintSuggestions, loadTasksDocument, loadTasksDocumentWithOptions, renderTasksMarkdown } from "./task.js";
8
- import { loadRoadmapDocumentWithOptions, renderRoadmapMarkdown } from "./roadmap.js";
9
- export const PROJECT_MARKER = ".projitive";
10
- const DEFAULT_GOVERNANCE_DIR = ".projitive";
11
- const ignoreNames = new Set(["node_modules", ".git", ".next", "dist", "build"]);
12
- const DEFAULT_SCAN_DEPTH = 3;
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { z } from 'zod';
5
+ import { discoverGovernanceArtifacts, catchIt, PROJECT_LINT_CODES, renderLintSuggestions, ensureStore, replaceRoadmapsInStore, replaceTasksInStore, markMarkdownViewDirty, loadTaskStatusStatsFromStore, } from '../common/index.js';
6
+ import { asText, evidenceSection, getDefaultToolTemplateMarkdown, guidanceSection, lintSection, nextCallSection, renderToolResponseMarkdown, summarySection, } from '../common/index.js';
7
+ import { collectTaskLintSuggestions, loadTasksDocument, loadTasksDocumentWithOptions, renderTasksMarkdown } from './task.js';
8
+ import { loadRoadmapDocumentWithOptions, renderRoadmapMarkdown } from './roadmap.js';
9
+ export const PROJECT_MARKER = '.projitive';
10
+ const DEFAULT_GOVERNANCE_DIR = '.projitive';
11
+ const ignoreNames = new Set(['node_modules', '.git', '.next', 'dist', 'build']);
13
12
  const MAX_SCAN_DEPTH = 8;
14
13
  function normalizePath(inputPath) {
15
14
  return inputPath ? path.resolve(inputPath) : process.cwd();
@@ -17,21 +16,21 @@ function normalizePath(inputPath) {
17
16
  function normalizeGovernanceDirName(input) {
18
17
  const name = input?.trim() || DEFAULT_GOVERNANCE_DIR;
19
18
  if (!name) {
20
- throw new Error("governanceDir cannot be empty");
19
+ throw new Error('governanceDir cannot be empty');
21
20
  }
22
21
  if (path.isAbsolute(name)) {
23
- throw new Error("governanceDir must be a relative directory name");
22
+ throw new Error('governanceDir must be a relative directory name');
24
23
  }
25
- if (name.includes("/") || name.includes("\\")) {
26
- throw new Error("governanceDir must not contain path separators");
24
+ if (name.includes('/') || name.includes('\\')) {
25
+ throw new Error('governanceDir must not contain path separators');
27
26
  }
28
- if (name === "." || name === "..") {
29
- throw new Error("governanceDir must be a normal directory name");
27
+ if (name === '.' || name === '..') {
28
+ throw new Error('governanceDir must be a normal directory name');
30
29
  }
31
30
  return name;
32
31
  }
33
32
  function parseDepthFromEnv(rawDepth) {
34
- if (typeof rawDepth !== "string" || rawDepth.trim().length === 0) {
33
+ if (typeof rawDepth !== 'string' || rawDepth.trim().length === 0) {
35
34
  return undefined;
36
35
  }
37
36
  const parsed = Number.parseInt(rawDepth, 10);
@@ -42,7 +41,7 @@ function parseDepthFromEnv(rawDepth) {
42
41
  }
43
42
  function requireEnvVar(name) {
44
43
  const value = process.env[name];
45
- if (typeof value !== "string" || value.trim().length === 0) {
44
+ if (typeof value !== 'string' || value.trim().length === 0) {
46
45
  throw new Error(`Missing required environment variable: ${name}`);
47
46
  }
48
47
  return value.trim();
@@ -67,47 +66,47 @@ export function resolveScanRoots(inputPaths) {
67
66
  return normalizedInputPaths;
68
67
  }
69
68
  const configuredRoots = process.env.PROJITIVE_SCAN_ROOT_PATHS;
70
- const rootsFromMultiEnv = typeof configuredRoots === "string"
69
+ const rootsFromMultiEnv = typeof configuredRoots === 'string'
71
70
  ? normalizeScanRoots(parseScanRoots(configuredRoots))
72
71
  : [];
73
72
  if (rootsFromMultiEnv.length > 0) {
74
73
  return rootsFromMultiEnv;
75
74
  }
76
75
  const legacyRoot = process.env.PROJITIVE_SCAN_ROOT_PATH;
77
- const rootsFromLegacyEnv = typeof legacyRoot === "string"
76
+ const rootsFromLegacyEnv = typeof legacyRoot === 'string'
78
77
  ? normalizeScanRoots([legacyRoot])
79
78
  : [];
80
79
  if (rootsFromLegacyEnv.length > 0) {
81
80
  return rootsFromLegacyEnv;
82
81
  }
83
- throw new Error("Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS (or legacy PROJITIVE_SCAN_ROOT_PATH)");
82
+ throw new Error('Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS (or legacy PROJITIVE_SCAN_ROOT_PATH)');
84
83
  }
85
84
  export function resolveScanRoot(inputPath) {
86
85
  return resolveScanRoots(inputPath ? [inputPath] : undefined)[0];
87
86
  }
88
87
  export function resolveScanDepth(inputDepth) {
89
- const configuredDepthRaw = requireEnvVar("PROJITIVE_SCAN_MAX_DEPTH");
88
+ const configuredDepthRaw = requireEnvVar('PROJITIVE_SCAN_MAX_DEPTH');
90
89
  const configuredDepth = parseDepthFromEnv(configuredDepthRaw);
91
- if (typeof configuredDepth !== "number") {
92
- throw new Error("Invalid PROJITIVE_SCAN_MAX_DEPTH: expected integer in range 0-8");
90
+ if (typeof configuredDepth !== 'number') {
91
+ throw new Error('Invalid PROJITIVE_SCAN_MAX_DEPTH: expected integer in range 0-8');
93
92
  }
94
- if (typeof inputDepth === "number") {
93
+ if (typeof inputDepth === 'number') {
95
94
  return inputDepth;
96
95
  }
97
96
  return configuredDepth;
98
97
  }
99
98
  function renderArtifactsMarkdown(artifacts) {
100
99
  const rows = artifacts.map((item) => {
101
- if (item.kind === "file") {
102
- const lineText = item.lineCount == null ? "-" : String(item.lineCount);
103
- return `- ${item.exists ? "" : ""} ${item.name} \n path: ${item.path} \n lineCount: ${lineText}`;
100
+ if (item.kind === 'file') {
101
+ const lineText = item.lineCount == null ? '-' : String(item.lineCount);
102
+ return `- ${item.exists ? '' : ''} ${item.name} \n path: ${item.path} \n lineCount: ${lineText}`;
104
103
  }
105
104
  const nested = (item.markdownFiles ?? [])
106
105
  .map((entry) => ` - ${entry.path} (lines: ${entry.lineCount})`)
107
- .join("\n");
108
- return `- ${item.exists ? "" : ""} ${item.name}/ \n path: ${item.path}${nested ? `\n markdownFiles:\n${nested}` : ""}`;
106
+ .join('\n');
107
+ return `- ${item.exists ? '' : ''} ${item.name}/ \n path: ${item.path}${nested ? `\n markdownFiles:\n${nested}` : ''}`;
109
108
  });
110
- return rows.join("\n");
109
+ return rows.join('\n');
111
110
  }
112
111
  async function readTasksSnapshot(governanceDir) {
113
112
  const tasksPath = path.join(governanceDir, PROJECT_MARKER);
@@ -118,8 +117,8 @@ async function readTasksSnapshot(governanceDir) {
118
117
  lintSuggestions: renderLintSuggestions([
119
118
  {
120
119
  code: PROJECT_LINT_CODES.TASKS_FILE_MISSING,
121
- message: "governance store is missing.",
122
- fixHint: "Initialize governance tasks structure first.",
120
+ message: 'governance store is missing.',
121
+ fixHint: 'Initialize governance tasks structure first.',
123
122
  },
124
123
  ]),
125
124
  todo: 0,
@@ -127,7 +126,7 @@ async function readTasksSnapshot(governanceDir) {
127
126
  blocked: 0,
128
127
  done: 0,
129
128
  total: 0,
130
- latestUpdatedAt: "(unknown)",
129
+ latestUpdatedAt: '(unknown)',
131
130
  score: 0,
132
131
  };
133
132
  }
@@ -141,15 +140,10 @@ async function readTasksSnapshot(governanceDir) {
141
140
  blocked: stats.blocked,
142
141
  done: stats.done,
143
142
  total: stats.total,
144
- latestUpdatedAt: stats.latestUpdatedAt || "(unknown)",
143
+ latestUpdatedAt: stats.latestUpdatedAt || '(unknown)',
145
144
  score: stats.inProgress * 2 + stats.todo,
146
145
  };
147
146
  }
148
- async function readRoadmapIds(governanceDir) {
149
- const dbPath = path.join(governanceDir, PROJECT_MARKER);
150
- await ensureStore(dbPath);
151
- return loadRoadmapIdsFromStore(dbPath);
152
- }
153
147
  export async function hasProjectMarker(dirPath) {
154
148
  const markerPath = path.join(dirPath, PROJECT_MARKER);
155
149
  const statResult = await catchIt(fs.stat(markerPath));
@@ -246,21 +240,21 @@ export async function discoverProjectsAcrossRoots(rootPaths, maxDepth) {
246
240
  return Array.from(new Set(perRootResults.flat())).sort();
247
241
  }
248
242
  const DEFAULT_TOOL_TEMPLATE_NAMES = [
249
- "projectInit",
250
- "projectScan",
251
- "projectNext",
252
- "projectLocate",
253
- "projectContext",
254
- "syncViews",
255
- "taskList",
256
- "taskNext",
257
- "taskContext",
258
- "taskCreate",
259
- "taskUpdate",
260
- "roadmapList",
261
- "roadmapContext",
262
- "roadmapCreate",
263
- "roadmapUpdate",
243
+ 'projectInit',
244
+ 'projectScan',
245
+ 'projectNext',
246
+ 'projectLocate',
247
+ 'projectContext',
248
+ 'syncViews',
249
+ 'taskList',
250
+ 'taskNext',
251
+ 'taskContext',
252
+ 'taskCreate',
253
+ 'taskUpdate',
254
+ 'roadmapList',
255
+ 'roadmapContext',
256
+ 'roadmapCreate',
257
+ 'roadmapUpdate',
264
258
  ];
265
259
  async function pathExists(targetPath) {
266
260
  const accessResult = await catchIt(fs.access(targetPath));
@@ -269,23 +263,23 @@ async function pathExists(targetPath) {
269
263
  async function writeTextFile(targetPath, content, force) {
270
264
  const exists = await pathExists(targetPath);
271
265
  if (exists && !force) {
272
- return { path: targetPath, action: "skipped" };
266
+ return { path: targetPath, action: 'skipped' };
273
267
  }
274
- await fs.writeFile(targetPath, content, "utf-8");
275
- return { path: targetPath, action: exists ? "updated" : "created" };
268
+ await fs.writeFile(targetPath, content, 'utf-8');
269
+ return { path: targetPath, action: exists ? 'updated' : 'created' };
276
270
  }
277
271
  function defaultReadmeMarkdown(governanceDirName) {
278
272
  return [
279
- "# Projitive Governance Workspace",
280
- "",
273
+ '# Projitive Governance Workspace',
274
+ '',
281
275
  `This directory (\`${governanceDirName}/\`) is the governance root for this project.`,
282
- "",
283
- "## Conventions",
284
- "- Keep roadmap/task source of truth in .projitive governance store.",
285
- "- Treat roadmap.md/tasks.md as generated views from governance store.",
286
- "- Keep IDs stable (TASK-xxxx / ROADMAP-xxxx).",
287
- "- Update report evidence before status transitions.",
288
- ].join("\n");
276
+ '',
277
+ '## Conventions',
278
+ '- Keep roadmap/task source of truth in .projitive governance store.',
279
+ '- Treat roadmap.md/tasks.md as generated views from governance store.',
280
+ '- Keep IDs stable (TASK-xxxx / ROADMAP-xxxx).',
281
+ '- Update report evidence before status transitions.',
282
+ ].join('\n');
289
283
  }
290
284
  function defaultRoadmapMarkdown(milestones = defaultRoadmapMilestones()) {
291
285
  return renderRoadmapMarkdown(milestones);
@@ -293,51 +287,51 @@ function defaultRoadmapMarkdown(milestones = defaultRoadmapMilestones()) {
293
287
  function defaultTasksMarkdown(updatedAt = new Date().toISOString()) {
294
288
  return renderTasksMarkdown([
295
289
  {
296
- id: "TASK-0001",
297
- title: "Bootstrap governance workspace",
298
- status: "TODO",
299
- owner: "unassigned",
300
- summary: "Create initial governance artifacts and confirm task execution loop.",
290
+ id: 'TASK-0001',
291
+ title: 'Bootstrap governance workspace',
292
+ status: 'TODO',
293
+ owner: 'unassigned',
294
+ summary: 'Create initial governance artifacts and confirm task execution loop.',
301
295
  updatedAt,
302
296
  links: [],
303
- roadmapRefs: ["ROADMAP-0001"],
297
+ roadmapRefs: ['ROADMAP-0001'],
304
298
  },
305
299
  ]);
306
300
  }
307
301
  function defaultRoadmapMilestones() {
308
302
  return [{
309
- id: "ROADMAP-0001",
310
- title: "Bootstrap governance baseline",
311
- status: "active",
312
- time: "2026-Q1",
303
+ id: 'ROADMAP-0001',
304
+ title: 'Bootstrap governance baseline',
305
+ status: 'active',
306
+ time: '2026-Q1',
313
307
  updatedAt: new Date().toISOString(),
314
308
  }];
315
309
  }
316
310
  function defaultTemplateReadmeMarkdown() {
317
311
  return [
318
- "# Template Guide",
319
- "",
320
- "This directory stores response templates (one file per tool).",
321
- "",
322
- "How to enable:",
323
- "- Set env `PROJITIVE_MESSAGE_TEMPLATE_PATH` to a template directory.",
324
- "- Example: .projitive/templates/tools",
325
- "",
326
- "Rule:",
327
- "- Prefer one template per tool: <toolName>.md (e.g. taskNext.md).",
328
- "- Template directory mode only loads <toolName>.md files.",
329
- "- If a tool template file is missing, Projitive will auto-generate that file before rendering.",
330
- "- Include {{content}} to render original tool output.",
331
- "- If {{content}} is missing, original output is appended after template text.",
332
- "",
333
- "Basic Variables:",
334
- "- {{tool_name}}",
335
- "- {{summary}}",
336
- "- {{evidence}}",
337
- "- {{guidance}}",
338
- "- {{next_call}}",
339
- "- {{content}}",
340
- ].join("\n");
312
+ '# Template Guide',
313
+ '',
314
+ 'This directory stores response templates (one file per tool).',
315
+ '',
316
+ 'How to enable:',
317
+ '- Set env `PROJITIVE_MESSAGE_TEMPLATE_PATH` to a template directory.',
318
+ '- Example: .projitive/templates/tools',
319
+ '',
320
+ 'Rule:',
321
+ '- Prefer one template per tool: <toolName>.md (e.g. taskNext.md).',
322
+ '- Template directory mode only loads <toolName>.md files.',
323
+ '- If a tool template file is missing, Projitive will auto-generate that file before rendering.',
324
+ '- Include {{content}} to render original tool output.',
325
+ '- If {{content}} is missing, original output is appended after template text.',
326
+ '',
327
+ 'Basic Variables:',
328
+ '- {{tool_name}}',
329
+ '- {{summary}}',
330
+ '- {{evidence}}',
331
+ '- {{guidance}}',
332
+ '- {{next_call}}',
333
+ '- {{content}}',
334
+ ].join('\n');
341
335
  }
342
336
  export async function initializeProjectStructure(inputPath, governanceDir, force = false) {
343
337
  const projectPath = normalizePath(inputPath);
@@ -353,15 +347,15 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
353
347
  const directories = [];
354
348
  const requiredDirectories = [
355
349
  governancePath,
356
- path.join(governancePath, "designs"),
357
- path.join(governancePath, "reports"),
358
- path.join(governancePath, "templates"),
359
- path.join(governancePath, "templates", "tools"),
350
+ path.join(governancePath, 'designs'),
351
+ path.join(governancePath, 'reports'),
352
+ path.join(governancePath, 'templates'),
353
+ path.join(governancePath, 'templates', 'tools'),
360
354
  ];
361
355
  for (const dirPath of requiredDirectories) {
362
356
  const exists = await pathExists(dirPath);
363
357
  await fs.mkdir(dirPath, { recursive: true });
364
- directories.push({ path: dirPath, action: exists ? "skipped" : "created" });
358
+ directories.push({ path: dirPath, action: exists ? 'skipped' : 'created' });
365
359
  }
366
360
  const markerPath = path.join(governancePath, PROJECT_MARKER);
367
361
  const defaultRoadmapData = defaultRoadmapMilestones();
@@ -372,24 +366,24 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
372
366
  await replaceRoadmapsInStore(markerPath, defaultRoadmapData);
373
367
  await replaceTasksInStore(markerPath, [
374
368
  {
375
- id: "TASK-0001",
376
- title: "Bootstrap governance workspace",
377
- status: "TODO",
378
- owner: "unassigned",
379
- summary: "Create initial governance artifacts and confirm task execution loop.",
369
+ id: 'TASK-0001',
370
+ title: 'Bootstrap governance workspace',
371
+ status: 'TODO',
372
+ owner: 'unassigned',
373
+ summary: 'Create initial governance artifacts and confirm task execution loop.',
380
374
  updatedAt: defaultTaskUpdatedAt,
381
375
  links: [],
382
- roadmapRefs: ["ROADMAP-0001"],
376
+ roadmapRefs: ['ROADMAP-0001'],
383
377
  },
384
378
  ]);
385
379
  }
386
380
  const baseFiles = await Promise.all([
387
- writeTextFile(path.join(governancePath, "README.md"), defaultReadmeMarkdown(governanceDirName), force),
388
- writeTextFile(path.join(governancePath, "roadmap.md"), defaultRoadmapMarkdown(defaultRoadmapData), force),
389
- writeTextFile(path.join(governancePath, "tasks.md"), defaultTasksMarkdown(defaultTaskUpdatedAt), force),
390
- writeTextFile(path.join(governancePath, "templates", "README.md"), defaultTemplateReadmeMarkdown(), force),
381
+ writeTextFile(path.join(governancePath, 'README.md'), defaultReadmeMarkdown(governanceDirName), force),
382
+ writeTextFile(path.join(governancePath, 'roadmap.md'), defaultRoadmapMarkdown(defaultRoadmapData), force),
383
+ writeTextFile(path.join(governancePath, 'tasks.md'), defaultTasksMarkdown(defaultTaskUpdatedAt), force),
384
+ writeTextFile(path.join(governancePath, 'templates', 'README.md'), defaultTemplateReadmeMarkdown(), force),
391
385
  ]);
392
- const toolTemplateFiles = await Promise.all(DEFAULT_TOOL_TEMPLATE_NAMES.map((toolName) => writeTextFile(path.join(governancePath, "templates", "tools", `${toolName}.md`), getDefaultToolTemplateMarkdown(toolName), force)));
386
+ const toolTemplateFiles = await Promise.all(DEFAULT_TOOL_TEMPLATE_NAMES.map((toolName) => writeTextFile(path.join(governancePath, 'templates', 'tools', `${toolName}.md`), getDefaultToolTemplateMarkdown(toolName), force)));
393
387
  const files = [...baseFiles, ...toolTemplateFiles];
394
388
  return {
395
389
  projectPath,
@@ -399,9 +393,9 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
399
393
  };
400
394
  }
401
395
  export function registerProjectTools(server) {
402
- server.registerTool("projectInit", {
403
- title: "Project Init",
404
- description: "Bootstrap governance files when a project has no .projitive yet (requires projectPath)",
396
+ server.registerTool('projectInit', {
397
+ title: 'Project Init',
398
+ description: 'Bootstrap governance files when a project has no .projitive yet (requires projectPath)',
405
399
  inputSchema: {
406
400
  projectPath: z.string(),
407
401
  governanceDir: z.string().optional(),
@@ -410,43 +404,43 @@ export function registerProjectTools(server) {
410
404
  }, async ({ projectPath, governanceDir, force }) => {
411
405
  const initialized = await initializeProjectStructure(projectPath, governanceDir, force ?? false);
412
406
  const filesByAction = {
413
- created: initialized.files.filter((item) => item.action === "created"),
414
- updated: initialized.files.filter((item) => item.action === "updated"),
415
- skipped: initialized.files.filter((item) => item.action === "skipped"),
407
+ created: initialized.files.filter((item) => item.action === 'created'),
408
+ updated: initialized.files.filter((item) => item.action === 'updated'),
409
+ skipped: initialized.files.filter((item) => item.action === 'skipped'),
416
410
  };
417
411
  const markdown = renderToolResponseMarkdown({
418
- toolName: "projectInit",
412
+ toolName: 'projectInit',
419
413
  sections: [
420
414
  summarySection([
421
415
  `- projectPath: ${initialized.projectPath}`,
422
416
  `- governanceDir: ${initialized.governanceDir}`,
423
- `- force: ${force === true ? "true" : "false"}`,
417
+ `- force: ${force === true ? 'true' : 'false'}`,
424
418
  ]),
425
419
  evidenceSection([
426
420
  `- createdFiles: ${filesByAction.created.length}`,
427
421
  `- updatedFiles: ${filesByAction.updated.length}`,
428
422
  `- skippedFiles: ${filesByAction.skipped.length}`,
429
- "- directories:",
423
+ '- directories:',
430
424
  ...initialized.directories.map((item) => ` - ${item.action}: ${item.path}`),
431
- "- files:",
425
+ '- files:',
432
426
  ...initialized.files.map((item) => ` - ${item.action}: ${item.path}`),
433
427
  ]),
434
428
  guidanceSection([
435
- "- If files were skipped and you want to overwrite templates, rerun with force=true.",
436
- "- Continue with projectContext and taskList for execution.",
429
+ '- If files were skipped and you want to overwrite templates, rerun with force=true.',
430
+ '- Continue with projectContext and taskList for execution.',
437
431
  ]),
438
432
  lintSection([
439
- "- After init, fill owner/roadmapRefs/links in .projitive task table before marking DONE.",
440
- "- Keep task source-of-truth inside .projitive governance store.",
433
+ '- After init, fill owner/roadmapRefs/links in .projitive task table before marking DONE.',
434
+ '- Keep task source-of-truth inside .projitive governance store.',
441
435
  ]),
442
- nextCallSection(`projectContext(projectPath=\"${initialized.projectPath}\")`),
436
+ nextCallSection(`projectContext(projectPath="${initialized.projectPath}")`),
443
437
  ],
444
438
  });
445
439
  return asText(markdown);
446
440
  });
447
- server.registerTool("projectScan", {
448
- title: "Project Scan",
449
- description: "Start here when project path is unknown; discover all governance roots",
441
+ server.registerTool('projectScan', {
442
+ title: 'Project Scan',
443
+ description: 'Start here when project path is unknown; discover all governance roots',
450
444
  inputSchema: {},
451
445
  }, async () => {
452
446
  const roots = resolveScanRoots();
@@ -454,35 +448,35 @@ export function registerProjectTools(server) {
454
448
  const governanceDirs = await discoverProjectsAcrossRoots(roots, depth);
455
449
  const projects = Array.from(new Set(governanceDirs.map((governanceDir) => toProjectPath(governanceDir)))).sort();
456
450
  const markdown = renderToolResponseMarkdown({
457
- toolName: "projectScan",
451
+ toolName: 'projectScan',
458
452
  sections: [
459
453
  summarySection([
460
- `- rootPaths: ${roots.join(", ")}`,
454
+ `- rootPaths: ${roots.join(', ')}`,
461
455
  `- rootCount: ${roots.length}`,
462
456
  `- maxDepth: ${depth}`,
463
457
  `- discoveredCount: ${projects.length}`,
464
458
  ]),
465
459
  evidenceSection([
466
- "- projects:",
460
+ '- projects:',
467
461
  ...projects.map((project, index) => `${index + 1}. ${project}`),
468
462
  ]),
469
463
  guidanceSection([
470
- "- Use one discovered project path and call `projectLocate` to lock governance root.",
471
- "- Then call `projectContext` to inspect current governance state.",
464
+ '- Use one discovered project path and call `projectLocate` to lock governance root.',
465
+ '- Then call `projectContext` to inspect current governance state.',
472
466
  ]),
473
467
  lintSection(projects.length === 0
474
- ? ["- No governance root discovered. Add `.projitive` marker and baseline artifacts before execution."]
475
- : ["- Run `projectContext` on a discovered project to receive module-level lint suggestions."]),
468
+ ? ['- No governance root discovered. Add `.projitive` marker and baseline artifacts before execution.']
469
+ : ['- Run `projectContext` on a discovered project to receive module-level lint suggestions.']),
476
470
  nextCallSection(projects[0]
477
- ? `projectLocate(inputPath=\"${projects[0]}\")`
471
+ ? `projectLocate(inputPath="${projects[0]}")`
478
472
  : undefined),
479
473
  ],
480
474
  });
481
475
  return asText(markdown);
482
476
  });
483
- server.registerTool("projectNext", {
484
- title: "Project Next",
485
- description: "Rank actionable projects and return the best execution target",
477
+ server.registerTool('projectNext', {
478
+ title: 'Project Next',
479
+ description: 'Rank actionable projects and return the best execution target',
486
480
  inputSchema: {
487
481
  limit: z.number().int().min(1).max(50).optional(),
488
482
  },
@@ -520,10 +514,10 @@ export function registerProjectTools(server) {
520
514
  ranked[0].lintSuggestions = collectTaskLintSuggestions(topDoc.tasks);
521
515
  }
522
516
  const markdown = renderToolResponseMarkdown({
523
- toolName: "projectNext",
517
+ toolName: 'projectNext',
524
518
  sections: [
525
519
  summarySection([
526
- `- rootPaths: ${roots.join(", ")}`,
520
+ `- rootPaths: ${roots.join(', ')}`,
527
521
  `- rootCount: ${roots.length}`,
528
522
  `- maxDepth: ${depth}`,
529
523
  `- matchedProjects: ${projects.length}`,
@@ -531,25 +525,25 @@ export function registerProjectTools(server) {
531
525
  `- limit: ${limit ?? 10}`,
532
526
  ]),
533
527
  evidenceSection([
534
- "- rankedProjects:",
535
- ...ranked.map((item, index) => `${index + 1}. ${toProjectPath(item.governanceDir)} | actionable=${item.actionable} | in_progress=${item.inProgress} | todo=${item.todo} | blocked=${item.blocked} | done=${item.done} | latest=${item.latestUpdatedAt}${item.tasksExists ? "" : " | store=missing"}`),
528
+ '- rankedProjects:',
529
+ ...ranked.map((item, index) => `${index + 1}. ${toProjectPath(item.governanceDir)} | actionable=${item.actionable} | in_progress=${item.inProgress} | todo=${item.todo} | blocked=${item.blocked} | done=${item.done} | latest=${item.latestUpdatedAt}${item.tasksExists ? '' : ' | store=missing'}`),
536
530
  ]),
537
531
  guidanceSection([
538
- "- Pick top 1 project and call `projectContext` with its projectPath.",
539
- "- Then call `taskList` and `taskContext` to continue execution.",
540
- "- If governance store is missing, initialize governance before task-level operations.",
532
+ '- Pick top 1 project and call `projectContext` with its projectPath.',
533
+ '- Then call `taskList` and `taskContext` to continue execution.',
534
+ '- If governance store is missing, initialize governance before task-level operations.',
541
535
  ]),
542
536
  lintSection(ranked[0]?.lintSuggestions ?? []),
543
537
  nextCallSection(ranked[0]
544
- ? `projectContext(projectPath=\"${toProjectPath(ranked[0].governanceDir)}\")`
538
+ ? `projectContext(projectPath="${toProjectPath(ranked[0].governanceDir)}")`
545
539
  : undefined),
546
540
  ],
547
541
  });
548
542
  return asText(markdown);
549
543
  });
550
- server.registerTool("projectLocate", {
551
- title: "Project Locate",
552
- description: "Resolve the nearest governance root from any in-project path",
544
+ server.registerTool('projectLocate', {
545
+ title: 'Project Locate',
546
+ description: 'Resolve the nearest governance root from any in-project path',
553
547
  inputSchema: {
554
548
  inputPath: z.string(),
555
549
  },
@@ -558,70 +552,74 @@ export function registerProjectTools(server) {
558
552
  const governanceDir = await resolveGovernanceDir(resolvedFrom);
559
553
  const projectPath = toProjectPath(governanceDir);
560
554
  const markdown = renderToolResponseMarkdown({
561
- toolName: "projectLocate",
555
+ toolName: 'projectLocate',
562
556
  sections: [
563
557
  summarySection([
564
558
  `- resolvedFrom: ${resolvedFrom}`,
565
559
  `- projectPath: ${projectPath}`,
566
560
  `- governanceDir: ${governanceDir}`,
567
561
  ]),
568
- guidanceSection(["- Call `projectContext` with this projectPath to get task and roadmap summaries."]),
569
- lintSection(["- Run `projectContext` to get governance/module lint suggestions for this project."]),
570
- nextCallSection(`projectContext(projectPath=\"${projectPath}\")`),
562
+ guidanceSection(['- Call `projectContext` with this projectPath to get task and roadmap summaries.']),
563
+ lintSection(['- Run `projectContext` to get governance/module lint suggestions for this project.']),
564
+ nextCallSection(`projectContext(projectPath="${projectPath}")`),
571
565
  ],
572
566
  });
573
567
  return asText(markdown);
574
568
  });
575
- server.registerTool("syncViews", {
576
- title: "Sync Views",
577
- description: "Materialize markdown views from .projitive governance store (tasks.md / roadmap.md)",
569
+ server.registerTool('syncViews', {
570
+ title: 'Sync Views',
571
+ description: 'Materialize markdown views from .projitive governance store (tasks.md / roadmap.md)',
578
572
  inputSchema: {
579
573
  projectPath: z.string(),
580
- views: z.array(z.enum(["tasks", "roadmap"])).optional(),
574
+ views: z.array(z.enum(['tasks', 'roadmap'])).optional(),
581
575
  force: z.boolean().optional(),
582
576
  },
583
577
  }, async ({ projectPath, views, force }) => {
584
578
  const governanceDir = await resolveGovernanceDir(projectPath);
585
579
  const normalizedProjectPath = toProjectPath(governanceDir);
580
+ const tasksViewPath = path.join(governanceDir, 'tasks.md');
581
+ const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
586
582
  const dbPath = path.join(governanceDir, PROJECT_MARKER);
587
583
  const selectedViews = views && views.length > 0
588
584
  ? Array.from(new Set(views))
589
- : ["tasks", "roadmap"];
585
+ : ['tasks', 'roadmap'];
590
586
  const forceSync = force === true;
591
587
  if (forceSync) {
592
- if (selectedViews.includes("tasks")) {
593
- await markMarkdownViewDirty(dbPath, "tasks_markdown");
588
+ if (selectedViews.includes('tasks')) {
589
+ await markMarkdownViewDirty(dbPath, 'tasks_markdown');
594
590
  }
595
- if (selectedViews.includes("roadmap")) {
596
- await markMarkdownViewDirty(dbPath, "roadmaps_markdown");
591
+ if (selectedViews.includes('roadmap')) {
592
+ await markMarkdownViewDirty(dbPath, 'roadmaps_markdown');
597
593
  }
598
594
  }
599
595
  let taskCount;
600
596
  let roadmapCount;
601
- if (selectedViews.includes("tasks")) {
597
+ if (selectedViews.includes('tasks')) {
602
598
  const taskDoc = await loadTasksDocumentWithOptions(governanceDir, forceSync);
603
599
  taskCount = taskDoc.tasks.length;
604
600
  }
605
- if (selectedViews.includes("roadmap")) {
601
+ if (selectedViews.includes('roadmap')) {
606
602
  const roadmapDoc = await loadRoadmapDocumentWithOptions(governanceDir, forceSync);
607
603
  roadmapCount = roadmapDoc.milestones.length;
608
604
  }
609
605
  const markdown = renderToolResponseMarkdown({
610
- toolName: "syncViews",
606
+ toolName: 'syncViews',
611
607
  sections: [
612
608
  summarySection([
613
609
  `- projectPath: ${normalizedProjectPath}`,
614
610
  `- governanceDir: ${governanceDir}`,
615
- `- views: ${selectedViews.join(", ")}`,
616
- `- force: ${forceSync ? "true" : "false"}`,
611
+ `- tasksView: ${tasksViewPath}`,
612
+ `- roadmapView: ${roadmapViewPath}`,
613
+ `- views: ${selectedViews.join(', ')}`,
614
+ `- force: ${forceSync ? 'true' : 'false'}`,
617
615
  ]),
618
616
  evidenceSection([
619
- ...(typeof taskCount === "number" ? [`- tasks.md synced | taskCount=${taskCount}`] : []),
620
- ...(typeof roadmapCount === "number" ? [`- roadmap.md synced | roadmapCount=${roadmapCount}`] : []),
617
+ ...(typeof taskCount === 'number' ? [`- tasks.md synced | taskCount=${taskCount}`] : []),
618
+ ...(typeof roadmapCount === 'number' ? [`- roadmap.md synced | roadmapCount=${roadmapCount}`] : []),
621
619
  ]),
622
620
  guidanceSection([
623
- "Use this tool after batch updates when you need immediate markdown materialization.",
624
- "Routine workflows can rely on lazy sync and usually do not require force=true.",
621
+ 'Use this tool after batch updates when you need immediate markdown materialization.',
622
+ 'Routine workflows can rely on lazy sync and usually do not require force=true.',
625
623
  ]),
626
624
  lintSection([]),
627
625
  nextCallSection(`projectContext(projectPath="${normalizedProjectPath}")`),
@@ -629,9 +627,9 @@ export function registerProjectTools(server) {
629
627
  });
630
628
  return asText(markdown);
631
629
  });
632
- server.registerTool("projectContext", {
633
- title: "Project Context",
634
- description: "Get project-level summary before selecting or executing a task",
630
+ server.registerTool('projectContext', {
631
+ title: 'Project Context',
632
+ description: 'Get project-level summary before selecting or executing a task',
635
633
  inputSchema: {
636
634
  projectPath: z.string(),
637
635
  },
@@ -643,34 +641,36 @@ export function registerProjectTools(server) {
643
641
  await ensureStore(dbPath);
644
642
  const taskStats = await loadTaskStatusStatsFromStore(dbPath);
645
643
  const { markdownPath: tasksMarkdownPath, tasks } = await loadTasksDocument(governanceDir);
646
- const roadmapIds = await readRoadmapIds(governanceDir);
644
+ const { markdownPath: roadmapMarkdownPath, milestones } = await loadRoadmapDocumentWithOptions(governanceDir, false);
645
+ const roadmapIds = milestones.map((item) => item.id);
647
646
  const lintSuggestions = collectTaskLintSuggestions(tasks);
648
647
  const markdown = renderToolResponseMarkdown({
649
- toolName: "projectContext",
648
+ toolName: 'projectContext',
650
649
  sections: [
651
650
  summarySection([
652
651
  `- projectPath: ${normalizedProjectPath}`,
653
652
  `- governanceDir: ${governanceDir}`,
654
653
  `- tasksView: ${tasksMarkdownPath}`,
654
+ `- roadmapView: ${roadmapMarkdownPath}`,
655
655
  `- roadmapIds: ${roadmapIds.length}`,
656
656
  ]),
657
657
  evidenceSection([
658
- "### Task Summary",
658
+ '### Task Summary',
659
659
  `- total: ${taskStats.total}`,
660
660
  `- TODO: ${taskStats.todo}`,
661
661
  `- IN_PROGRESS: ${taskStats.inProgress}`,
662
662
  `- BLOCKED: ${taskStats.blocked}`,
663
663
  `- DONE: ${taskStats.done}`,
664
- "",
665
- "### Artifacts",
664
+ '',
665
+ '### Artifacts',
666
666
  renderArtifactsMarkdown(artifacts),
667
667
  ]),
668
668
  guidanceSection([
669
- "- Start from `taskList` to choose a target task.",
670
- "- Then call `taskContext` with a task ID to retrieve evidence locations and reading order.",
669
+ '- Start from `taskList` to choose a target task.',
670
+ '- Then call `taskContext` with a task ID to retrieve evidence locations and reading order.',
671
671
  ]),
672
672
  lintSection(lintSuggestions),
673
- nextCallSection(`taskList(projectPath=\"${normalizedProjectPath}\")`),
673
+ nextCallSection(`taskList(projectPath="${normalizedProjectPath}")`),
674
674
  ],
675
675
  });
676
676
  return asText(markdown);