@projitive/mcp 2.0.3 → 2.1.0

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