@projitive/mcp 2.0.4 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,8 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import process from 'node:process';
4
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';
5
+ import { discoverGovernanceArtifacts, catchIt, PROJECT_LINT_CODES, renderLintSuggestions, ensureStore, replaceRoadmapsInStore, replaceTasksInStore, loadTaskStatusStatsFromStore, createGovernedTool, getDefaultToolTemplateMarkdown, } from '../common/index.js';
7
6
  import { collectTaskLintSuggestions, loadTasksDocument, loadTasksDocumentWithOptions, renderTasksMarkdown } from './task.js';
8
7
  import { loadRoadmapDocumentWithOptions, renderRoadmapMarkdown } from './roadmap.js';
9
8
  export const PROJECT_MARKER = '.projitive';
@@ -15,9 +14,6 @@ function normalizePath(inputPath) {
15
14
  }
16
15
  function normalizeGovernanceDirName(input) {
17
16
  const name = input?.trim() || DEFAULT_GOVERNANCE_DIR;
18
- if (!name) {
19
- throw new Error('governanceDir cannot be empty');
20
- }
21
17
  if (path.isAbsolute(name)) {
22
18
  throw new Error('governanceDir must be a relative directory name');
23
19
  }
@@ -30,9 +26,6 @@ function normalizeGovernanceDirName(input) {
30
26
  return name;
31
27
  }
32
28
  function parseDepthFromEnv(rawDepth) {
33
- if (typeof rawDepth !== 'string' || rawDepth.trim().length === 0) {
34
- return undefined;
35
- }
36
29
  const parsed = Number.parseInt(rawDepth, 10);
37
30
  if (!Number.isFinite(parsed)) {
38
31
  return undefined;
@@ -321,16 +314,14 @@ function defaultTemplateReadmeMarkdown() {
321
314
  '- Prefer one template per tool: <toolName>.md (e.g. taskNext.md).',
322
315
  '- Template directory mode only loads <toolName>.md files.',
323
316
  '- 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
317
  '',
327
318
  'Basic Variables:',
328
319
  '- {{tool_name}}',
329
320
  '- {{summary}}',
330
321
  '- {{evidence}}',
331
322
  '- {{guidance}}',
323
+ '- {{lint_suggestions}}',
332
324
  '- {{next_call}}',
333
- '- {{content}}',
334
325
  ].join('\n');
335
326
  }
336
327
  export async function initializeProjectStructure(inputPath, governanceDir, force = false) {
@@ -393,7 +384,8 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
393
384
  };
394
385
  }
395
386
  export function registerProjectTools(server) {
396
- server.registerTool('projectInit', {
387
+ server.registerTool(...createGovernedTool({
388
+ name: 'projectInit',
397
389
  title: 'Project Init',
398
390
  description: 'Bootstrap governance files when a project has no .projitive yet (requires projectPath)',
399
391
  inputSchema: {
@@ -401,172 +393,152 @@ export function registerProjectTools(server) {
401
393
  governanceDir: z.string().optional(),
402
394
  force: z.boolean().optional(),
403
395
  },
404
- }, async ({ projectPath, governanceDir, force }) => {
405
- const initialized = await initializeProjectStructure(projectPath, governanceDir, force ?? false);
406
- const filesByAction = {
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'),
410
- };
411
- const markdown = renderToolResponseMarkdown({
412
- toolName: 'projectInit',
413
- sections: [
414
- summarySection([
415
- `- projectPath: ${initialized.projectPath}`,
416
- `- governanceDir: ${initialized.governanceDir}`,
417
- `- force: ${force === true ? 'true' : 'false'}`,
418
- ]),
419
- evidenceSection([
420
- `- createdFiles: ${filesByAction.created.length}`,
421
- `- updatedFiles: ${filesByAction.updated.length}`,
422
- `- skippedFiles: ${filesByAction.skipped.length}`,
423
- '- directories:',
424
- ...initialized.directories.map((item) => ` - ${item.action}: ${item.path}`),
425
- '- files:',
426
- ...initialized.files.map((item) => ` - ${item.action}: ${item.path}`),
427
- ]),
428
- guidanceSection([
429
- '- If files were skipped and you want to overwrite templates, rerun with force=true.',
430
- '- Continue with projectContext and taskList for execution.',
431
- ]),
432
- lintSection([
433
- '- After init, fill owner/roadmapRefs/links in .projitive task table before marking DONE.',
434
- '- Keep task source-of-truth inside .projitive governance store.',
435
- ]),
436
- nextCallSection(`projectContext(projectPath="${initialized.projectPath}")`),
437
- ],
438
- });
439
- return asText(markdown);
440
- });
441
- server.registerTool('projectScan', {
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',
442
431
  title: 'Project Scan',
443
432
  description: 'Start here when project path is unknown; discover all governance roots',
444
433
  inputSchema: {},
445
- }, async () => {
446
- const roots = resolveScanRoots();
447
- const depth = resolveScanDepth();
448
- const governanceDirs = await discoverProjectsAcrossRoots(roots, depth);
449
- const projects = Array.from(new Set(governanceDirs.map((governanceDir) => toProjectPath(governanceDir)))).sort();
450
- const markdown = renderToolResponseMarkdown({
451
- toolName: 'projectScan',
452
- sections: [
453
- summarySection([
454
- `- rootPaths: ${roots.join(', ')}`,
455
- `- rootCount: ${roots.length}`,
456
- `- maxDepth: ${depth}`,
457
- `- discoveredCount: ${projects.length}`,
458
- ]),
459
- evidenceSection([
460
- '- projects:',
461
- ...projects.map((project, index) => `${index + 1}. ${project}`),
462
- ]),
463
- guidanceSection([
464
- '- Use one discovered project path and call `projectLocate` to lock governance root.',
465
- '- Then call `projectContext` to inspect current governance state.',
466
- ]),
467
- lintSection(projects.length === 0
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.']),
470
- nextCallSection(projects[0]
471
- ? `projectLocate(inputPath="${projects[0]}")`
472
- : undefined),
473
- ],
474
- });
475
- return asText(markdown);
476
- });
477
- server.registerTool('projectNext', {
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',
478
462
  title: 'Project Next',
479
463
  description: 'Rank actionable projects and return the best execution target',
480
464
  inputSchema: {
481
465
  limit: z.number().int().min(1).max(50).optional(),
482
466
  },
483
- }, async ({ limit }) => {
484
- const roots = resolveScanRoots();
485
- const depth = resolveScanDepth();
486
- const projects = await discoverProjectsAcrossRoots(roots, depth);
487
- const snapshots = await Promise.all(projects.map(async (governanceDir) => {
488
- const snapshot = await readTasksSnapshot(governanceDir);
489
- const actionable = snapshot.inProgress + snapshot.todo;
490
- return {
491
- governanceDir,
492
- tasksExists: snapshot.exists,
493
- lintSuggestions: snapshot.lintSuggestions,
494
- inProgress: snapshot.inProgress,
495
- todo: snapshot.todo,
496
- blocked: snapshot.blocked,
497
- done: snapshot.done,
498
- actionable,
499
- latestUpdatedAt: snapshot.latestUpdatedAt,
500
- score: snapshot.score,
501
- };
502
- }));
503
- const ranked = snapshots
504
- .filter((item) => item.actionable > 0)
505
- .sort((a, b) => {
506
- if (b.score !== a.score) {
507
- return b.score - a.score;
508
- }
509
- return b.latestUpdatedAt.localeCompare(a.latestUpdatedAt);
510
- })
511
- .slice(0, limit ?? 10);
512
- if (ranked[0]) {
513
- const topDoc = await loadTasksDocument(ranked[0].governanceDir);
514
- ranked[0].lintSuggestions = collectTaskLintSuggestions(topDoc.tasks);
515
- }
516
- const markdown = renderToolResponseMarkdown({
517
- toolName: 'projectNext',
518
- sections: [
519
- summarySection([
520
- `- rootPaths: ${roots.join(', ')}`,
521
- `- rootCount: ${roots.length}`,
522
- `- maxDepth: ${depth}`,
523
- `- matchedProjects: ${projects.length}`,
524
- `- actionableProjects: ${ranked.length}`,
525
- `- limit: ${limit ?? 10}`,
526
- ]),
527
- evidenceSection([
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'}`),
530
- ]),
531
- guidanceSection([
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.',
535
- ]),
536
- lintSection(ranked[0]?.lintSuggestions ?? []),
537
- nextCallSection(ranked[0]
538
- ? `projectContext(projectPath="${toProjectPath(ranked[0].governanceDir)}")`
539
- : undefined),
540
- ],
541
- });
542
- return asText(markdown);
543
- });
544
- server.registerTool('projectLocate', {
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',
545
520
  title: 'Project Locate',
546
521
  description: 'Resolve the nearest governance root from any in-project path',
547
522
  inputSchema: {
548
523
  inputPath: z.string(),
549
524
  },
550
- }, async ({ inputPath }) => {
551
- const resolvedFrom = normalizePath(inputPath);
552
- const governanceDir = await resolveGovernanceDir(resolvedFrom);
553
- const projectPath = toProjectPath(governanceDir);
554
- const markdown = renderToolResponseMarkdown({
555
- toolName: 'projectLocate',
556
- sections: [
557
- summarySection([
558
- `- resolvedFrom: ${resolvedFrom}`,
559
- `- projectPath: ${projectPath}`,
560
- `- governanceDir: ${governanceDir}`,
561
- ]),
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}")`),
565
- ],
566
- });
567
- return asText(markdown);
568
- });
569
- server.registerTool('syncViews', {
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',
570
542
  title: 'Sync Views',
571
543
  description: 'Materialize markdown views from .projitive governance store (tasks.md / roadmap.md)',
572
544
  inputSchema: {
@@ -574,105 +546,86 @@ export function registerProjectTools(server) {
574
546
  views: z.array(z.enum(['tasks', 'roadmap'])).optional(),
575
547
  force: z.boolean().optional(),
576
548
  },
577
- }, async ({ projectPath, views, force }) => {
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
- const dbPath = path.join(governanceDir, PROJECT_MARKER);
583
- const selectedViews = views && views.length > 0
584
- ? Array.from(new Set(views))
585
- : ['tasks', 'roadmap'];
586
- const forceSync = force === true;
587
- if (forceSync) {
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;
588
558
  if (selectedViews.includes('tasks')) {
589
- await markMarkdownViewDirty(dbPath, 'tasks_markdown');
559
+ const taskDoc = await loadTasksDocumentWithOptions(governanceDir, forceSync);
560
+ taskCount = taskDoc.tasks.length;
590
561
  }
591
562
  if (selectedViews.includes('roadmap')) {
592
- await markMarkdownViewDirty(dbPath, 'roadmaps_markdown');
563
+ const roadmapDoc = await loadRoadmapDocumentWithOptions(governanceDir, forceSync);
564
+ roadmapCount = roadmapDoc.milestones.length;
593
565
  }
594
- }
595
- let taskCount;
596
- let roadmapCount;
597
- if (selectedViews.includes('tasks')) {
598
- const taskDoc = await loadTasksDocumentWithOptions(governanceDir, forceSync);
599
- taskCount = taskDoc.tasks.length;
600
- }
601
- if (selectedViews.includes('roadmap')) {
602
- const roadmapDoc = await loadRoadmapDocumentWithOptions(governanceDir, forceSync);
603
- roadmapCount = roadmapDoc.milestones.length;
604
- }
605
- const markdown = renderToolResponseMarkdown({
606
- toolName: 'syncViews',
607
- sections: [
608
- summarySection([
609
- `- projectPath: ${normalizedProjectPath}`,
610
- `- governanceDir: ${governanceDir}`,
611
- `- tasksView: ${tasksViewPath}`,
612
- `- roadmapView: ${roadmapViewPath}`,
613
- `- views: ${selectedViews.join(', ')}`,
614
- `- force: ${forceSync ? 'true' : 'false'}`,
615
- ]),
616
- evidenceSection([
617
- ...(typeof taskCount === 'number' ? [`- tasks.md synced | taskCount=${taskCount}`] : []),
618
- ...(typeof roadmapCount === 'number' ? [`- roadmap.md synced | roadmapCount=${roadmapCount}`] : []),
619
- ]),
620
- guidanceSection([
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.',
623
- ]),
624
- lintSection([]),
625
- nextCallSection(`projectContext(projectPath="${normalizedProjectPath}")`),
626
- ],
627
- });
628
- return asText(markdown);
629
- });
630
- server.registerTool('projectContext', {
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',
631
589
  title: 'Project Context',
632
590
  description: 'Get project-level summary before selecting or executing a task',
633
591
  inputSchema: {
634
592
  projectPath: z.string(),
635
593
  },
636
- }, async ({ projectPath }) => {
637
- const governanceDir = await resolveGovernanceDir(projectPath);
638
- const normalizedProjectPath = toProjectPath(governanceDir);
639
- const artifacts = await discoverGovernanceArtifacts(governanceDir);
640
- const dbPath = path.join(governanceDir, PROJECT_MARKER);
641
- await ensureStore(dbPath);
642
- const taskStats = await loadTaskStatusStatsFromStore(dbPath);
643
- const { markdownPath: tasksMarkdownPath, tasks } = await loadTasksDocument(governanceDir);
644
- const { markdownPath: roadmapMarkdownPath, milestones } = await loadRoadmapDocumentWithOptions(governanceDir, false);
645
- const roadmapIds = milestones.map((item) => item.id);
646
- const lintSuggestions = collectTaskLintSuggestions(tasks);
647
- const markdown = renderToolResponseMarkdown({
648
- toolName: 'projectContext',
649
- sections: [
650
- summarySection([
651
- `- projectPath: ${normalizedProjectPath}`,
652
- `- governanceDir: ${governanceDir}`,
653
- `- tasksView: ${tasksMarkdownPath}`,
654
- `- roadmapView: ${roadmapMarkdownPath}`,
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
  }