@projitive/mcp 2.0.2 → 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 (41) hide show
  1. package/README.md +14 -1
  2. package/output/package.json +8 -2
  3. package/output/source/common/artifacts.js +1 -1
  4. package/output/source/common/artifacts.test.js +11 -11
  5. package/output/source/common/errors.js +19 -19
  6. package/output/source/common/files.js +11 -11
  7. package/output/source/common/files.test.js +14 -14
  8. package/output/source/common/index.js +10 -10
  9. package/output/source/common/linter.js +27 -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 +74 -74
  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/types.js +3 -3
  18. package/output/source/common/utils.js +8 -8
  19. package/output/source/index.js +16 -16
  20. package/output/source/index.test.js +64 -64
  21. package/output/source/prompts/index.js +3 -3
  22. package/output/source/prompts/quickStart.js +96 -96
  23. package/output/source/prompts/taskDiscovery.js +184 -180
  24. package/output/source/prompts/taskExecution.js +148 -147
  25. package/output/source/resources/designs.js +26 -26
  26. package/output/source/resources/designs.test.js +88 -88
  27. package/output/source/resources/governance.js +19 -19
  28. package/output/source/resources/index.js +2 -2
  29. package/output/source/resources/readme.js +7 -7
  30. package/output/source/resources/readme.test.js +113 -113
  31. package/output/source/resources/reports.js +10 -10
  32. package/output/source/resources/reports.test.js +83 -83
  33. package/output/source/tools/index.js +3 -3
  34. package/output/source/tools/project.js +196 -191
  35. package/output/source/tools/project.test.js +187 -164
  36. package/output/source/tools/roadmap.js +173 -76
  37. package/output/source/tools/roadmap.test.js +58 -42
  38. package/output/source/tools/task.js +380 -255
  39. package/output/source/tools/task.test.js +117 -110
  40. package/output/source/types.js +22 -22
  41. 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,19 +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
- "taskUpdate",
259
- "roadmapList",
260
- "roadmapContext",
261
- "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',
262
258
  ];
263
259
  async function pathExists(targetPath) {
264
260
  const accessResult = await catchIt(fs.access(targetPath));
@@ -267,23 +263,23 @@ async function pathExists(targetPath) {
267
263
  async function writeTextFile(targetPath, content, force) {
268
264
  const exists = await pathExists(targetPath);
269
265
  if (exists && !force) {
270
- return { path: targetPath, action: "skipped" };
266
+ return { path: targetPath, action: 'skipped' };
271
267
  }
272
- await fs.writeFile(targetPath, content, "utf-8");
273
- return { path: targetPath, action: exists ? "updated" : "created" };
268
+ await fs.writeFile(targetPath, content, 'utf-8');
269
+ return { path: targetPath, action: exists ? 'updated' : 'created' };
274
270
  }
275
271
  function defaultReadmeMarkdown(governanceDirName) {
276
272
  return [
277
- "# Projitive Governance Workspace",
278
- "",
273
+ '# Projitive Governance Workspace',
274
+ '',
279
275
  `This directory (\`${governanceDirName}/\`) is the governance root for this project.`,
280
- "",
281
- "## Conventions",
282
- "- Keep roadmap/task source of truth in .projitive sqlite tables.",
283
- "- Treat roadmap.md/tasks.md as generated views from sqlite.",
284
- "- Keep IDs stable (TASK-xxxx / ROADMAP-xxxx).",
285
- "- Update report evidence before status transitions.",
286
- ].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');
287
283
  }
288
284
  function defaultRoadmapMarkdown(milestones = defaultRoadmapMilestones()) {
289
285
  return renderRoadmapMarkdown(milestones);
@@ -291,51 +287,51 @@ function defaultRoadmapMarkdown(milestones = defaultRoadmapMilestones()) {
291
287
  function defaultTasksMarkdown(updatedAt = new Date().toISOString()) {
292
288
  return renderTasksMarkdown([
293
289
  {
294
- id: "TASK-0001",
295
- title: "Bootstrap governance workspace",
296
- status: "TODO",
297
- owner: "unassigned",
298
- 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.',
299
295
  updatedAt,
300
296
  links: [],
301
- roadmapRefs: ["ROADMAP-0001"],
297
+ roadmapRefs: ['ROADMAP-0001'],
302
298
  },
303
299
  ]);
304
300
  }
305
301
  function defaultRoadmapMilestones() {
306
302
  return [{
307
- id: "ROADMAP-0001",
308
- title: "Bootstrap governance baseline",
309
- status: "active",
310
- time: "2026-Q1",
303
+ id: 'ROADMAP-0001',
304
+ title: 'Bootstrap governance baseline',
305
+ status: 'active',
306
+ time: '2026-Q1',
311
307
  updatedAt: new Date().toISOString(),
312
308
  }];
313
309
  }
314
310
  function defaultTemplateReadmeMarkdown() {
315
311
  return [
316
- "# Template Guide",
317
- "",
318
- "This directory stores response templates (one file per tool).",
319
- "",
320
- "How to enable:",
321
- "- Set env `PROJITIVE_MESSAGE_TEMPLATE_PATH` to a template directory.",
322
- "- Example: .projitive/templates/tools",
323
- "",
324
- "Rule:",
325
- "- Prefer one template per tool: <toolName>.md (e.g. taskNext.md).",
326
- "- Template directory mode only loads <toolName>.md files.",
327
- "- If a tool template file is missing, Projitive will auto-generate that file before rendering.",
328
- "- Include {{content}} to render original tool output.",
329
- "- If {{content}} is missing, original output is appended after template text.",
330
- "",
331
- "Basic Variables:",
332
- "- {{tool_name}}",
333
- "- {{summary}}",
334
- "- {{evidence}}",
335
- "- {{guidance}}",
336
- "- {{next_call}}",
337
- "- {{content}}",
338
- ].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');
339
335
  }
340
336
  export async function initializeProjectStructure(inputPath, governanceDir, force = false) {
341
337
  const projectPath = normalizePath(inputPath);
@@ -351,15 +347,15 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
351
347
  const directories = [];
352
348
  const requiredDirectories = [
353
349
  governancePath,
354
- path.join(governancePath, "designs"),
355
- path.join(governancePath, "reports"),
356
- path.join(governancePath, "templates"),
357
- 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'),
358
354
  ];
359
355
  for (const dirPath of requiredDirectories) {
360
356
  const exists = await pathExists(dirPath);
361
357
  await fs.mkdir(dirPath, { recursive: true });
362
- directories.push({ path: dirPath, action: exists ? "skipped" : "created" });
358
+ directories.push({ path: dirPath, action: exists ? 'skipped' : 'created' });
363
359
  }
364
360
  const markerPath = path.join(governancePath, PROJECT_MARKER);
365
361
  const defaultRoadmapData = defaultRoadmapMilestones();
@@ -370,24 +366,24 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
370
366
  await replaceRoadmapsInStore(markerPath, defaultRoadmapData);
371
367
  await replaceTasksInStore(markerPath, [
372
368
  {
373
- id: "TASK-0001",
374
- title: "Bootstrap governance workspace",
375
- status: "TODO",
376
- owner: "unassigned",
377
- 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.',
378
374
  updatedAt: defaultTaskUpdatedAt,
379
375
  links: [],
380
- roadmapRefs: ["ROADMAP-0001"],
376
+ roadmapRefs: ['ROADMAP-0001'],
381
377
  },
382
378
  ]);
383
379
  }
384
380
  const baseFiles = await Promise.all([
385
- writeTextFile(path.join(governancePath, "README.md"), defaultReadmeMarkdown(governanceDirName), force),
386
- writeTextFile(path.join(governancePath, "roadmap.md"), defaultRoadmapMarkdown(defaultRoadmapData), force),
387
- writeTextFile(path.join(governancePath, "tasks.md"), defaultTasksMarkdown(defaultTaskUpdatedAt), force),
388
- 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),
389
385
  ]);
390
- 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)));
391
387
  const files = [...baseFiles, ...toolTemplateFiles];
392
388
  return {
393
389
  projectPath,
@@ -397,9 +393,9 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
397
393
  };
398
394
  }
399
395
  export function registerProjectTools(server) {
400
- server.registerTool("projectInit", {
401
- title: "Project Init",
402
- 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)',
403
399
  inputSchema: {
404
400
  projectPath: z.string(),
405
401
  governanceDir: z.string().optional(),
@@ -408,78 +404,79 @@ export function registerProjectTools(server) {
408
404
  }, async ({ projectPath, governanceDir, force }) => {
409
405
  const initialized = await initializeProjectStructure(projectPath, governanceDir, force ?? false);
410
406
  const filesByAction = {
411
- created: initialized.files.filter((item) => item.action === "created"),
412
- updated: initialized.files.filter((item) => item.action === "updated"),
413
- 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'),
414
410
  };
415
411
  const markdown = renderToolResponseMarkdown({
416
- toolName: "projectInit",
412
+ toolName: 'projectInit',
417
413
  sections: [
418
414
  summarySection([
419
415
  `- projectPath: ${initialized.projectPath}`,
420
416
  `- governanceDir: ${initialized.governanceDir}`,
421
- `- force: ${force === true ? "true" : "false"}`,
417
+ `- force: ${force === true ? 'true' : 'false'}`,
422
418
  ]),
423
419
  evidenceSection([
424
420
  `- createdFiles: ${filesByAction.created.length}`,
425
421
  `- updatedFiles: ${filesByAction.updated.length}`,
426
422
  `- skippedFiles: ${filesByAction.skipped.length}`,
427
- "- directories:",
423
+ '- directories:',
428
424
  ...initialized.directories.map((item) => ` - ${item.action}: ${item.path}`),
429
- "- files:",
425
+ '- files:',
430
426
  ...initialized.files.map((item) => ` - ${item.action}: ${item.path}`),
431
427
  ]),
432
428
  guidanceSection([
433
- "- If files were skipped and you want to overwrite templates, rerun with force=true.",
434
- "- 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.',
435
431
  ]),
436
432
  lintSection([
437
- "- After init, fill owner/roadmapRefs/links in .projitive task table before marking DONE.",
438
- "- Keep task source-of-truth inside sqlite tables.",
433
+ '- After init, fill owner/roadmapRefs/links in .projitive task table before marking DONE.',
434
+ '- Keep task source-of-truth inside .projitive governance store.',
439
435
  ]),
440
- nextCallSection(`projectContext(projectPath=\"${initialized.projectPath}\")`),
436
+ nextCallSection(`projectContext(projectPath="${initialized.projectPath}")`),
441
437
  ],
442
438
  });
443
439
  return asText(markdown);
444
440
  });
445
- server.registerTool("projectScan", {
446
- title: "Project Scan",
447
- 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',
448
444
  inputSchema: {},
449
445
  }, async () => {
450
446
  const roots = resolveScanRoots();
451
447
  const depth = resolveScanDepth();
452
- const projects = await discoverProjectsAcrossRoots(roots, depth);
448
+ const governanceDirs = await discoverProjectsAcrossRoots(roots, depth);
449
+ const projects = Array.from(new Set(governanceDirs.map((governanceDir) => toProjectPath(governanceDir)))).sort();
453
450
  const markdown = renderToolResponseMarkdown({
454
- toolName: "projectScan",
451
+ toolName: 'projectScan',
455
452
  sections: [
456
453
  summarySection([
457
- `- rootPaths: ${roots.join(", ")}`,
454
+ `- rootPaths: ${roots.join(', ')}`,
458
455
  `- rootCount: ${roots.length}`,
459
456
  `- maxDepth: ${depth}`,
460
457
  `- discoveredCount: ${projects.length}`,
461
458
  ]),
462
459
  evidenceSection([
463
- "- projects:",
460
+ '- projects:',
464
461
  ...projects.map((project, index) => `${index + 1}. ${project}`),
465
462
  ]),
466
463
  guidanceSection([
467
- "- Use one discovered project path and call `projectLocate` to lock governance root.",
468
- "- 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.',
469
466
  ]),
470
467
  lintSection(projects.length === 0
471
- ? ["- No governance root discovered. Add `.projitive` marker and baseline artifacts before execution."]
472
- : ["- 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.']),
473
470
  nextCallSection(projects[0]
474
- ? `projectLocate(inputPath=\"${projects[0]}\")`
471
+ ? `projectLocate(inputPath="${projects[0]}")`
475
472
  : undefined),
476
473
  ],
477
474
  });
478
475
  return asText(markdown);
479
476
  });
480
- server.registerTool("projectNext", {
481
- title: "Project Next",
482
- 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',
483
480
  inputSchema: {
484
481
  limit: z.number().int().min(1).max(50).optional(),
485
482
  },
@@ -517,10 +514,10 @@ export function registerProjectTools(server) {
517
514
  ranked[0].lintSuggestions = collectTaskLintSuggestions(topDoc.tasks);
518
515
  }
519
516
  const markdown = renderToolResponseMarkdown({
520
- toolName: "projectNext",
517
+ toolName: 'projectNext',
521
518
  sections: [
522
519
  summarySection([
523
- `- rootPaths: ${roots.join(", ")}`,
520
+ `- rootPaths: ${roots.join(', ')}`,
524
521
  `- rootCount: ${roots.length}`,
525
522
  `- maxDepth: ${depth}`,
526
523
  `- matchedProjects: ${projects.length}`,
@@ -528,25 +525,25 @@ export function registerProjectTools(server) {
528
525
  `- limit: ${limit ?? 10}`,
529
526
  ]),
530
527
  evidenceSection([
531
- "- rankedProjects:",
532
- ...ranked.map((item, index) => `${index + 1}. ${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'}`),
533
530
  ]),
534
531
  guidanceSection([
535
- "- Pick top 1 project and call `projectContext` with its projectPath.",
536
- "- Then call `taskList` and `taskContext` to continue execution.",
537
- "- 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.',
538
535
  ]),
539
536
  lintSection(ranked[0]?.lintSuggestions ?? []),
540
537
  nextCallSection(ranked[0]
541
- ? `projectContext(projectPath=\"${toProjectPath(ranked[0].governanceDir)}\")`
538
+ ? `projectContext(projectPath="${toProjectPath(ranked[0].governanceDir)}")`
542
539
  : undefined),
543
540
  ],
544
541
  });
545
542
  return asText(markdown);
546
543
  });
547
- server.registerTool("projectLocate", {
548
- title: "Project Locate",
549
- 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',
550
547
  inputSchema: {
551
548
  inputPath: z.string(),
552
549
  },
@@ -555,78 +552,84 @@ export function registerProjectTools(server) {
555
552
  const governanceDir = await resolveGovernanceDir(resolvedFrom);
556
553
  const projectPath = toProjectPath(governanceDir);
557
554
  const markdown = renderToolResponseMarkdown({
558
- toolName: "projectLocate",
555
+ toolName: 'projectLocate',
559
556
  sections: [
560
557
  summarySection([
561
558
  `- resolvedFrom: ${resolvedFrom}`,
562
559
  `- projectPath: ${projectPath}`,
563
560
  `- governanceDir: ${governanceDir}`,
564
561
  ]),
565
- guidanceSection(["- Call `projectContext` with this projectPath to get task and roadmap summaries."]),
566
- lintSection(["- Run `projectContext` to get governance/module lint suggestions for this project."]),
567
- 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}")`),
568
565
  ],
569
566
  });
570
567
  return asText(markdown);
571
568
  });
572
- server.registerTool("syncViews", {
573
- title: "Sync Views",
574
- description: "Materialize markdown views from .projitive sqlite tables (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)',
575
572
  inputSchema: {
576
573
  projectPath: z.string(),
577
- views: z.array(z.enum(["tasks", "roadmap"])).optional(),
574
+ views: z.array(z.enum(['tasks', 'roadmap'])).optional(),
578
575
  force: z.boolean().optional(),
579
576
  },
580
577
  }, async ({ projectPath, views, force }) => {
581
578
  const governanceDir = await resolveGovernanceDir(projectPath);
579
+ const normalizedProjectPath = toProjectPath(governanceDir);
580
+ const tasksViewPath = path.join(governanceDir, 'tasks.md');
581
+ const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
582
582
  const dbPath = path.join(governanceDir, PROJECT_MARKER);
583
583
  const selectedViews = views && views.length > 0
584
584
  ? Array.from(new Set(views))
585
- : ["tasks", "roadmap"];
585
+ : ['tasks', 'roadmap'];
586
586
  const forceSync = force === true;
587
587
  if (forceSync) {
588
- if (selectedViews.includes("tasks")) {
589
- await markMarkdownViewDirty(dbPath, "tasks_markdown");
588
+ if (selectedViews.includes('tasks')) {
589
+ await markMarkdownViewDirty(dbPath, 'tasks_markdown');
590
590
  }
591
- if (selectedViews.includes("roadmap")) {
592
- await markMarkdownViewDirty(dbPath, "roadmaps_markdown");
591
+ if (selectedViews.includes('roadmap')) {
592
+ await markMarkdownViewDirty(dbPath, 'roadmaps_markdown');
593
593
  }
594
594
  }
595
595
  let taskCount;
596
596
  let roadmapCount;
597
- if (selectedViews.includes("tasks")) {
597
+ if (selectedViews.includes('tasks')) {
598
598
  const taskDoc = await loadTasksDocumentWithOptions(governanceDir, forceSync);
599
599
  taskCount = taskDoc.tasks.length;
600
600
  }
601
- if (selectedViews.includes("roadmap")) {
601
+ if (selectedViews.includes('roadmap')) {
602
602
  const roadmapDoc = await loadRoadmapDocumentWithOptions(governanceDir, forceSync);
603
603
  roadmapCount = roadmapDoc.milestones.length;
604
604
  }
605
605
  const markdown = renderToolResponseMarkdown({
606
- toolName: "syncViews",
606
+ toolName: 'syncViews',
607
607
  sections: [
608
608
  summarySection([
609
+ `- projectPath: ${normalizedProjectPath}`,
609
610
  `- governanceDir: ${governanceDir}`,
610
- `- views: ${selectedViews.join(", ")}`,
611
- `- force: ${forceSync ? "true" : "false"}`,
611
+ `- tasksView: ${tasksViewPath}`,
612
+ `- roadmapView: ${roadmapViewPath}`,
613
+ `- views: ${selectedViews.join(', ')}`,
614
+ `- force: ${forceSync ? 'true' : 'false'}`,
612
615
  ]),
613
616
  evidenceSection([
614
- ...(typeof taskCount === "number" ? [`- tasks.md synced | taskCount=${taskCount}`] : []),
615
- ...(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}`] : []),
616
619
  ]),
617
620
  guidanceSection([
618
- "Use this tool after batch updates when you need immediate markdown materialization.",
619
- "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.',
620
623
  ]),
621
624
  lintSection([]),
622
- nextCallSection(`projectContext(projectPath="${toProjectPath(governanceDir)}")`),
625
+ nextCallSection(`projectContext(projectPath="${normalizedProjectPath}")`),
623
626
  ],
624
627
  });
625
628
  return asText(markdown);
626
629
  });
627
- server.registerTool("projectContext", {
628
- title: "Project Context",
629
- 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',
630
633
  inputSchema: {
631
634
  projectPath: z.string(),
632
635
  },
@@ -638,34 +641,36 @@ export function registerProjectTools(server) {
638
641
  await ensureStore(dbPath);
639
642
  const taskStats = await loadTaskStatusStatsFromStore(dbPath);
640
643
  const { markdownPath: tasksMarkdownPath, tasks } = await loadTasksDocument(governanceDir);
641
- const roadmapIds = await readRoadmapIds(governanceDir);
644
+ const { markdownPath: roadmapMarkdownPath, milestones } = await loadRoadmapDocumentWithOptions(governanceDir, false);
645
+ const roadmapIds = milestones.map((item) => item.id);
642
646
  const lintSuggestions = collectTaskLintSuggestions(tasks);
643
647
  const markdown = renderToolResponseMarkdown({
644
- toolName: "projectContext",
648
+ toolName: 'projectContext',
645
649
  sections: [
646
650
  summarySection([
647
651
  `- projectPath: ${normalizedProjectPath}`,
648
652
  `- governanceDir: ${governanceDir}`,
649
653
  `- tasksView: ${tasksMarkdownPath}`,
654
+ `- roadmapView: ${roadmapMarkdownPath}`,
650
655
  `- roadmapIds: ${roadmapIds.length}`,
651
656
  ]),
652
657
  evidenceSection([
653
- "### Task Summary",
658
+ '### Task Summary',
654
659
  `- total: ${taskStats.total}`,
655
660
  `- TODO: ${taskStats.todo}`,
656
661
  `- IN_PROGRESS: ${taskStats.inProgress}`,
657
662
  `- BLOCKED: ${taskStats.blocked}`,
658
663
  `- DONE: ${taskStats.done}`,
659
- "",
660
- "### Artifacts",
664
+ '',
665
+ '### Artifacts',
661
666
  renderArtifactsMarkdown(artifacts),
662
667
  ]),
663
668
  guidanceSection([
664
- "- Start from `taskList` to choose a target task.",
665
- "- 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.',
666
671
  ]),
667
672
  lintSection(lintSuggestions),
668
- nextCallSection(`taskList(projectPath=\"${normalizedProjectPath}\")`),
673
+ nextCallSection(`taskList(projectPath="${normalizedProjectPath}")`),
669
674
  ],
670
675
  });
671
676
  return asText(markdown);