@projitive/mcp 2.0.4 → 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.
@@ -1,18 +1,18 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { z } from 'zod';
4
- import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, findTextReferences, ensureStore, loadActionableTasksFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, replaceTasksInStore, upsertTaskInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, } from '../common/index.js';
5
- import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from '../common/index.js';
6
- import { TASK_LINT_CODES, renderLintSuggestions } from '../common/index.js';
4
+ import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, findTextReferences, ensureStore, loadActionableTasksFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, replaceTasksInStore, upsertTaskInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, createGovernedTool, ToolExecutionError, PROJECT_LINT_CODES, TASK_LINT_CODES, renderLintSuggestions, } from '../common/index.js';
7
5
  import { resolveGovernanceDir, resolveScanDepth, resolveScanRoots, discoverProjectsAcrossRoots, toProjectPath } from './project.js';
8
6
  import { isValidRoadmapId } from './roadmap.js';
9
7
  import { SUB_STATE_PHASES, BLOCKER_TYPES } from '../types.js';
10
8
  export const ALLOWED_STATUS = ['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE'];
11
9
  export const TASK_ID_REGEX = /^TASK-(\d+)$/;
12
10
  export const TASKS_MARKDOWN_FILE = 'tasks.md';
13
- function appendLintSuggestions(target, suggestions) {
14
- target.push(...renderLintSuggestions(suggestions));
15
- }
11
+ export const TASK_RESEARCH_DIR = 'designs/research';
12
+ export const TASK_RESEARCH_FILE_SUFFIX = '.implementation-research.md';
13
+ export const CORE_DESIGN_DOCS_DIR = 'designs/core';
14
+ export const CORE_ARCHITECTURE_DOC_FILE = `${CORE_DESIGN_DOCS_DIR}/architecture.md`;
15
+ export const CORE_STYLE_DOC_FILE = `${CORE_DESIGN_DOCS_DIR}/style-guide.md`;
16
16
  function taskStatusGuidance(task) {
17
17
  if (task.status === 'TODO') {
18
18
  return [
@@ -46,6 +46,7 @@ const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
46
46
  '- Create TODO tasks only when evidence is clear: each new task must produce at least one report/designs/readme artifact update.',
47
47
  '- Skip duplicate scope: do not create tasks that overlap existing TODO/IN_PROGRESS/BLOCKED task intent.',
48
48
  '- Use quality gates for discovery candidates: user value, delivery risk reduction, or measurable throughput improvement.',
49
+ '- Review and update project architecture docs under designs/core/ (architecture.md, style-guide.md) if they are missing or outdated.',
49
50
  '- Keep each discovery round small (1-3 tasks), then rerun taskNext immediately for re-ranking and execution.',
50
51
  ];
51
52
  const DEFAULT_TASK_CONTEXT_READING_GUIDANCE = [
@@ -113,6 +114,87 @@ function normalizeTaskLink(link) {
113
114
  function resolveTaskLinkPath(projectPath, link) {
114
115
  return path.join(projectPath, link);
115
116
  }
117
+ function taskResearchBriefRelativePath(taskId) {
118
+ return `${TASK_RESEARCH_DIR}/${taskId}${TASK_RESEARCH_FILE_SUFFIX}`;
119
+ }
120
+ function renderTaskResearchBriefTemplate(task) {
121
+ return [
122
+ `# ${task.id} Implementation Research Brief`,
123
+ '',
124
+ `Task: ${task.title}`,
125
+ `Summary: ${task.summary || '(fill this with a short objective summary)'}`,
126
+ '',
127
+ '## Design Guidelines and Specs',
128
+ '- [ ] List relevant design/governance/spec files with line location',
129
+ '- Example: designs/ARCHITECTURE.md#L42-L76 - API boundary and constraints',
130
+ '- Example: roadmap.md#L18 - milestone acceptance criteria',
131
+ '',
132
+ '## Code Architecture and Implementation Findings',
133
+ '- [ ] Document current architecture and extension points with line location',
134
+ '- Example: packages/mcp/source/tools/task.ts#L1020-L1130 - taskContext response assembly',
135
+ '- Example: packages/mcp/source/prompts/taskExecution.ts#L25-L130 - execution workflow prompt',
136
+ '',
137
+ '## Implementation Plan',
138
+ '- [ ] Proposed change list with impacted modules',
139
+ '- [ ] Validation and regression test plan',
140
+ '',
141
+ '## Risks and Open Questions',
142
+ '- [ ] Known risks, assumptions, and unresolved questions',
143
+ ];
144
+ }
145
+ async function inspectTaskResearchBrief(governanceDir, task) {
146
+ const projectPath = toProjectPath(governanceDir);
147
+ const relativePath = taskResearchBriefRelativePath(task.id);
148
+ const absolutePath = resolveTaskLinkPath(projectPath, relativePath);
149
+ const exists = await fs.access(absolutePath).then(() => true).catch(() => false);
150
+ return { relativePath, absolutePath, exists, ready: exists };
151
+ }
152
+ function collectTaskResearchBriefLintSuggestions(state) {
153
+ if (!state.exists) {
154
+ return [{
155
+ code: TASK_LINT_CODES.RESEARCH_BRIEF_MISSING,
156
+ message: `Pre-execution research brief missing: ${state.relativePath}.`,
157
+ fixHint: 'Create the file and fill required sections before implementation.',
158
+ }];
159
+ }
160
+ return [];
161
+ }
162
+ function inspectProjectContextDocsFromArtifacts(files) {
163
+ const markdownFiles = files
164
+ .map((item) => item.replace(/\\/g, '/'))
165
+ .filter((item) => item.toLowerCase().endsWith('.md'));
166
+ const architectureDocSuffix = `/${CORE_ARCHITECTURE_DOC_FILE}`.toLowerCase();
167
+ const styleDocSuffix = `/${CORE_STYLE_DOC_FILE}`.toLowerCase();
168
+ const architectureDocs = markdownFiles.filter((item) => item.toLowerCase().endsWith(architectureDocSuffix));
169
+ const styleDocs = markdownFiles.filter((item) => item.toLowerCase().endsWith(styleDocSuffix));
170
+ const missingArchitectureDocs = architectureDocs.length === 0;
171
+ const missingStyleDocs = styleDocs.length === 0;
172
+ return {
173
+ architectureDocs,
174
+ styleDocs,
175
+ missingArchitectureDocs,
176
+ missingStyleDocs,
177
+ ready: !missingArchitectureDocs && !missingStyleDocs,
178
+ };
179
+ }
180
+ function collectProjectContextDocsLintSuggestions(state) {
181
+ const suggestions = [];
182
+ if (state.missingArchitectureDocs) {
183
+ suggestions.push({
184
+ code: PROJECT_LINT_CODES.ARCHITECTURE_DOC_MISSING,
185
+ message: 'Project context is missing architecture design documentation.',
186
+ fixHint: `Add required file: ${CORE_ARCHITECTURE_DOC_FILE}.`,
187
+ });
188
+ }
189
+ if (state.missingStyleDocs) {
190
+ suggestions.push({
191
+ code: PROJECT_LINT_CODES.STYLE_DOC_MISSING,
192
+ message: 'Project context is missing design style documentation.',
193
+ fixHint: `Add required file: ${CORE_STYLE_DOC_FILE}.`,
194
+ });
195
+ }
196
+ return suggestions;
197
+ }
116
198
  async function readActionableTaskCandidates(governanceDirs) {
117
199
  const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
118
200
  const tasksPath = path.join(governanceDir, '.projitive');
@@ -382,7 +464,7 @@ function collectTaskLintSuggestionItems(tasks) {
382
464
  export function collectTaskLintSuggestions(tasks) {
383
465
  return renderLintSuggestions(collectTaskLintSuggestionItems(tasks));
384
466
  }
385
- function collectSingleTaskLintSuggestions(task) {
467
+ function collectSingleTaskLintSuggestionItems(task) {
386
468
  const suggestions = [];
387
469
  if (task.status === 'IN_PROGRESS' && task.owner.trim().length === 0) {
388
470
  suggestions.push({
@@ -478,37 +560,21 @@ function collectSingleTaskLintSuggestions(task) {
478
560
  fixHint: 'Confidence must be between 0.0 and 1.0.',
479
561
  });
480
562
  }
481
- return renderLintSuggestions(suggestions);
563
+ return suggestions;
482
564
  }
483
- async function collectTaskFileLintSuggestions(governanceDir, task) {
484
- const suggestions = [];
485
- const projectPath = toProjectPath(governanceDir);
486
- for (const link of task.links) {
487
- const normalized = link.trim();
488
- if (normalized.length === 0) {
489
- continue;
490
- }
491
- if (/^https?:\/\//i.test(normalized)) {
492
- continue;
493
- }
494
- if (!isProjectRootRelativePath(normalized)) {
495
- suggestions.push({
496
- code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
497
- message: `Link path should be project-root-relative without leading slash: ${normalized}.`,
498
- fixHint: 'Use path/from/project/root format.',
499
- });
500
- continue;
501
- }
502
- const resolvedPath = resolveTaskLinkPath(projectPath, normalized);
503
- const exists = await fs.access(resolvedPath).then(() => true).catch(() => false);
504
- if (!exists) {
505
- suggestions.push({
506
- code: TASK_LINT_CODES.LINK_TARGET_MISSING,
507
- message: `Link target not found: ${normalized} (resolved: ${resolvedPath}).`,
508
- });
509
- }
510
- }
511
- return renderLintSuggestions(suggestions);
565
+ function collectSingleTaskLintSuggestions(task) {
566
+ return renderLintSuggestions(collectSingleTaskLintSuggestionItems(task));
567
+ }
568
+ async function collectDoneConformanceSuggestions(governanceDir, task) {
569
+ const researchBriefState = await inspectTaskResearchBrief(governanceDir, task);
570
+ const artifacts = await discoverGovernanceArtifacts(governanceDir);
571
+ const fileCandidates = candidateFilesFromArtifacts(artifacts);
572
+ const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
573
+ return [
574
+ ...collectSingleTaskLintSuggestionItems(task),
575
+ ...collectTaskResearchBriefLintSuggestions(researchBriefState),
576
+ ...collectProjectContextDocsLintSuggestions(projectContextDocsState),
577
+ ];
512
578
  }
513
579
  export function renderTasksMarkdown(tasks) {
514
580
  const sections = sortTasksNewestFirst(tasks).map((task) => {
@@ -610,7 +676,8 @@ export function validateTransition(from, to) {
610
676
  return allowed[from].has(to);
611
677
  }
612
678
  export function registerTaskTools(server) {
613
- server.registerTool('taskList', {
679
+ server.registerTool(...createGovernedTool({
680
+ name: 'taskList',
614
681
  title: 'Task List',
615
682
  description: 'List tasks for a known project and optionally filter by status',
616
683
  inputSchema: {
@@ -618,50 +685,48 @@ export function registerTaskTools(server) {
618
685
  status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
619
686
  limit: z.number().int().min(1).max(200).optional(),
620
687
  },
621
- }, async ({ projectPath, status, limit }) => {
622
- const governanceDir = await resolveGovernanceDir(projectPath);
623
- const normalizedProjectPath = toProjectPath(governanceDir);
624
- const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
625
- const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
626
- const filtered = tasks
627
- .filter((task) => (status ? task.status === status : true))
628
- .slice(0, limit ?? 100);
629
- const lintSuggestions = collectTaskLintSuggestions(filtered);
630
- if (status && filtered.length === 0) {
631
- appendLintSuggestions(lintSuggestions, [
632
- {
633
- code: TASK_LINT_CODES.FILTER_EMPTY,
634
- message: `No tasks matched status=${status}.`,
635
- fixHint: 'Confirm status values or update task states.',
636
- },
637
- ]);
638
- }
639
- const nextTaskId = filtered[0]?.id;
640
- const markdown = renderToolResponseMarkdown({
641
- toolName: 'taskList',
642
- sections: [
643
- summarySection([
644
- `- projectPath: ${normalizedProjectPath}`,
645
- `- governanceDir: ${governanceDir}`,
646
- `- tasksView: ${tasksViewPath}`,
647
- `- roadmapView: ${roadmapViewPath}`,
648
- `- filter.status: ${status ?? '(none)'}`,
649
- `- returned: ${filtered.length}`,
650
- ]),
651
- evidenceSection([
652
- '- tasks:',
653
- ...filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner || ''} | updatedAt=${task.updatedAt}`),
654
- ]),
655
- guidanceSection(['- Pick one task ID and call `taskContext`.']),
656
- lintSection(lintSuggestions),
657
- nextCallSection(nextTaskId
658
- ? `taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${nextTaskId}")`
659
- : undefined),
660
- ],
661
- });
662
- return asText(markdown);
663
- });
664
- server.registerTool('taskCreate', {
688
+ async execute({ projectPath, status, limit }) {
689
+ const governanceDir = await resolveGovernanceDir(projectPath);
690
+ const normalizedProjectPath = toProjectPath(governanceDir);
691
+ const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
692
+ const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
693
+ const filtered = tasks
694
+ .filter((task) => (status ? task.status === status : true))
695
+ .slice(0, limit ?? 100);
696
+ return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, filtered, status };
697
+ },
698
+ summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, filtered, status }) => [
699
+ `- projectPath: ${normalizedProjectPath}`,
700
+ `- governanceDir: ${governanceDir}`,
701
+ `- tasksView: ${tasksViewPath}`,
702
+ `- roadmapView: ${roadmapViewPath}`,
703
+ `- filter.status: ${status ?? '(none)'}`,
704
+ `- returned: ${filtered.length}`,
705
+ ],
706
+ evidence: ({ filtered }) => [
707
+ '- tasks:',
708
+ ...filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner || ''} | updatedAt=${task.updatedAt}`),
709
+ ],
710
+ guidance: () => ['- Pick one task ID and call `taskContext`.'],
711
+ suggestions: ({ filtered, status }) => {
712
+ const suggestions = collectTaskLintSuggestions(filtered);
713
+ if (status && filtered.length === 0) {
714
+ suggestions.push(...renderLintSuggestions([
715
+ {
716
+ code: TASK_LINT_CODES.FILTER_EMPTY,
717
+ message: `No tasks matched status=${status}.`,
718
+ fixHint: 'Confirm status values or update task states.',
719
+ },
720
+ ]));
721
+ }
722
+ return suggestions;
723
+ },
724
+ nextCall: ({ filtered, normalizedProjectPath }) => filtered[0]
725
+ ? `taskContext(projectPath="${normalizedProjectPath}", taskId="${filtered[0].id}")`
726
+ : undefined,
727
+ }));
728
+ server.registerTool(...createGovernedTool({
729
+ name: 'taskCreate',
665
730
  title: 'Task Create',
666
731
  description: 'Create a new task in governance store with stable TASK-<number> ID',
667
732
  inputSchema: {
@@ -686,322 +751,379 @@ export function registerTaskTools(server) {
686
751
  escalationPath: z.string().optional(),
687
752
  }).optional(),
688
753
  },
689
- }, async ({ projectPath, taskId, title, status, owner, summary, roadmapRefs, links, subState, blocker }) => {
690
- if (taskId && !isValidTaskId(taskId)) {
691
- return {
692
- ...asText(renderErrorMarkdown('taskCreate', `Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'omit taskId to auto-generate next ID'], `taskCreate(projectPath="${projectPath}", title="Define executable objective")`)),
693
- isError: true,
694
- };
695
- }
696
- const governanceDir = await resolveGovernanceDir(projectPath);
697
- const normalizedProjectPath = toProjectPath(governanceDir);
698
- const { tasksPath, tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
699
- const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
700
- const finalTaskId = taskId ?? nextTaskId(tasks);
701
- const duplicated = tasks.some((item) => item.id === finalTaskId);
702
- if (duplicated) {
703
- return {
704
- ...asText(renderErrorMarkdown('taskCreate', `Task already exists: ${finalTaskId}`, ['task IDs must be unique', 'use taskUpdate for existing tasks'], `taskUpdate(projectPath="${normalizedProjectPath}", taskId="${finalTaskId}", updates={...})`)),
705
- isError: true,
706
- };
707
- }
708
- const createdTask = normalizeTask({
709
- id: finalTaskId,
710
- title,
711
- status: status ?? 'TODO',
712
- owner,
713
- summary,
714
- roadmapRefs,
715
- links,
716
- subState,
717
- blocker,
718
- updatedAt: nowIso(),
719
- });
720
- await upsertTaskInStore(tasksPath, createdTask);
721
- await loadTasksDocumentWithOptions(governanceDir, true);
722
- const lintSuggestions = [
723
- ...collectSingleTaskLintSuggestions(createdTask),
724
- ...(await collectTaskFileLintSuggestions(governanceDir, createdTask)),
725
- ];
726
- const markdown = renderToolResponseMarkdown({
727
- toolName: 'taskCreate',
728
- sections: [
729
- summarySection([
730
- `- projectPath: ${normalizedProjectPath}`,
731
- `- governanceDir: ${governanceDir}`,
732
- `- tasksView: ${tasksViewPath}`,
733
- `- roadmapView: ${roadmapViewPath}`,
734
- `- taskId: ${createdTask.id}`,
735
- `- status: ${createdTask.status}`,
736
- `- owner: ${createdTask.owner || '(none)'}`,
737
- `- updatedAt: ${createdTask.updatedAt}`,
738
- ]),
739
- evidenceSection([
740
- '### Created Task',
741
- `- ${createdTask.id} | ${createdTask.status} | ${createdTask.title}`,
742
- `- summary: ${createdTask.summary || '(none)'}`,
743
- `- roadmapRefs: ${createdTask.roadmapRefs.join(', ') || '(none)'}`,
744
- `- links: ${createdTask.links.join(', ') || '(none)'}`,
745
- ]),
746
- guidanceSection([
747
- 'Task created in governance store successfully and tasks.md has been synced.',
748
- 'Run taskContext to verify references and lint guidance.',
749
- ]),
750
- lintSection(lintSuggestions),
751
- nextCallSection(`taskContext(projectPath="${normalizedProjectPath}", taskId="${createdTask.id}")`),
752
- ],
753
- });
754
- return asText(markdown);
755
- });
756
- server.registerTool('taskNext', {
754
+ async execute({ projectPath, taskId, title, status, owner, summary, roadmapRefs, links, subState, blocker }) {
755
+ if (taskId && !isValidTaskId(taskId)) {
756
+ throw new ToolExecutionError(`Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'omit taskId to auto-generate next ID'], `taskCreate(projectPath="${projectPath}", title="Define executable objective")`);
757
+ }
758
+ const governanceDir = await resolveGovernanceDir(projectPath);
759
+ const normalizedProjectPath = toProjectPath(governanceDir);
760
+ const { tasksPath, tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
761
+ const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
762
+ const finalTaskId = taskId ?? nextTaskId(tasks);
763
+ const duplicated = tasks.some((item) => item.id === finalTaskId);
764
+ if (duplicated) {
765
+ throw new ToolExecutionError(`Task already exists: ${finalTaskId}`, ['task IDs must be unique', 'use taskUpdate for existing tasks'], `taskUpdate(projectPath="${normalizedProjectPath}", taskId="${finalTaskId}", updates={...})`);
766
+ }
767
+ const createdTask = normalizeTask({
768
+ id: finalTaskId,
769
+ title,
770
+ status: status ?? 'TODO',
771
+ owner,
772
+ summary,
773
+ roadmapRefs,
774
+ links,
775
+ subState,
776
+ blocker,
777
+ updatedAt: nowIso(),
778
+ });
779
+ await upsertTaskInStore(tasksPath, createdTask);
780
+ await loadTasksDocumentWithOptions(governanceDir, true);
781
+ return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, createdTask };
782
+ },
783
+ summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, createdTask }) => [
784
+ `- projectPath: ${normalizedProjectPath}`,
785
+ `- governanceDir: ${governanceDir}`,
786
+ `- tasksView: ${tasksViewPath}`,
787
+ `- roadmapView: ${roadmapViewPath}`,
788
+ `- taskId: ${createdTask.id}`,
789
+ `- status: ${createdTask.status}`,
790
+ `- owner: ${createdTask.owner || '(none)'}`,
791
+ `- updatedAt: ${createdTask.updatedAt}`,
792
+ ],
793
+ evidence: ({ createdTask }) => [
794
+ '### Created Task',
795
+ `- ${createdTask.id} | ${createdTask.status} | ${createdTask.title}`,
796
+ `- summary: ${createdTask.summary || '(none)'}`,
797
+ `- roadmapRefs: ${createdTask.roadmapRefs.join(', ') || '(none)'}`,
798
+ `- links: ${createdTask.links.join(', ') || '(none)'}`,
799
+ ],
800
+ guidance: () => [
801
+ 'Task created in governance store successfully and tasks.md has been synced.',
802
+ 'Run taskContext to verify references and lint guidance.',
803
+ ],
804
+ suggestions: ({ createdTask }) => collectSingleTaskLintSuggestions(createdTask),
805
+ nextCall: ({ normalizedProjectPath, createdTask }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${createdTask.id}")`,
806
+ }));
807
+ server.registerTool(...createGovernedTool({
808
+ name: 'taskNext',
757
809
  title: 'Task Next',
758
810
  description: 'Start here to auto-select the highest-priority actionable task',
759
811
  inputSchema: {
760
812
  limit: z.number().int().min(1).max(20).optional(),
761
813
  },
762
- }, async ({ limit }) => {
763
- const roots = resolveScanRoots();
764
- const depth = resolveScanDepth();
765
- const projects = await discoverProjectsAcrossRoots(roots, depth);
766
- const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
767
- if (rankedCandidates.length === 0) {
768
- const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
769
- const tasksPath = path.join(governanceDir, '.projitive');
770
- await ensureStore(tasksPath);
771
- const stats = await loadTaskStatusStatsFromStore(tasksPath);
772
- const roadmapIds = await readRoadmapIds(governanceDir);
773
- return {
774
- governanceDir,
775
- roadmapIds,
776
- total: stats.total,
777
- todo: stats.todo,
778
- inProgress: stats.inProgress,
779
- blocked: stats.blocked,
780
- done: stats.done,
781
- };
782
- }));
783
- const preferredProject = projectSnapshots[0];
784
- const preferredRoadmapRef = preferredProject?.roadmapIds[0] ?? 'ROADMAP-0001';
785
- const noTaskDiscoveryGuidance = await resolveNoTaskDiscoveryGuidance(preferredProject?.governanceDir);
786
- const markdown = renderToolResponseMarkdown({
787
- toolName: 'taskNext',
788
- sections: [
789
- summarySection([
790
- `- rootPaths: ${roots.join(', ')}`,
791
- `- rootCount: ${roots.length}`,
792
- `- maxDepth: ${depth}`,
793
- `- matchedProjects: ${projects.length}`,
794
- '- actionableTasks: 0',
795
- ]),
796
- evidenceSection([
797
- '### Project Snapshots',
798
- ...(projectSnapshots.length > 0
799
- ? projectSnapshots.map((item, index) => `${index + 1}. ${toProjectPath(item.governanceDir)} | total=${item.total} | todo=${item.todo} | in_progress=${item.inProgress} | blocked=${item.blocked} | done=${item.done} | roadmapIds=${item.roadmapIds.join(', ') || '(none)'}`)
800
- : ['- (none)']),
801
- '',
802
- '### Seed Task Template',
803
- ...renderTaskSeedTemplate(preferredRoadmapRef),
804
- ]),
805
- guidanceSection([
806
- '- No TODO/IN_PROGRESS task is available.',
807
- '- Create 1-3 new TODO tasks using `taskCreate(...)` from active roadmap slices.',
808
- '- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.',
809
- '- If roadmap has active milestones, analyze milestone intent and split into 1-3 executable TODO tasks.',
810
- '',
811
- '### No-Task Discovery Checklist',
812
- ...noTaskDiscoveryGuidance,
813
- '',
814
- '- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.',
815
- '- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.',
816
- '- After creating tasks, rerun `taskNext` to re-rank actionable work.',
817
- ]),
818
- lintSection([
819
- '- No actionable tasks found. Verify task statuses and required fields in .projitive task table.',
820
- '- Ensure each new task has stable TASK-<number> ID and at least one roadmapRefs item.',
821
- ]),
822
- nextCallSection(preferredProject
823
- ? `taskCreate(projectPath="${toProjectPath(preferredProject.governanceDir)}", title="Create first executable slice", roadmapRefs=["${preferredRoadmapRef}"], summary="Derived from active roadmap milestone")`
824
- : 'projectScan()'),
825
- ],
826
- });
827
- return asText(markdown);
828
- }
829
- const selected = rankedCandidates[0];
830
- const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
831
- const lintSuggestions = collectTaskLintSuggestions(selectedTaskDocument.tasks);
832
- const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
833
- const fileCandidates = candidateFilesFromArtifacts(artifacts);
834
- const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
835
- const taskLocation = (await findTextReferences(selectedTaskDocument.markdownPath, selected.task.id))[0];
836
- const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
837
- const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
838
- const candidateLimit = limit ?? 5;
839
- const markdown = renderToolResponseMarkdown({
840
- toolName: 'taskNext',
841
- sections: [
842
- summarySection([
843
- `- rootPaths: ${roots.join(', ')}`,
844
- `- rootCount: ${roots.length}`,
845
- `- maxDepth: ${depth}`,
846
- `- matchedProjects: ${projects.length}`,
847
- `- actionableTasks: ${rankedCandidates.length}`,
848
- `- selectedProject: ${toProjectPath(selected.governanceDir)}`,
849
- `- selectedTaskId: ${selected.task.id}`,
850
- `- selectedTaskStatus: ${selected.task.status}`,
851
- ]),
852
- evidenceSection([
853
- '### Selected Task',
854
- `- id: ${selected.task.id}`,
855
- `- title: ${selected.task.title}`,
856
- `- owner: ${selected.task.owner || '(none)'}`,
857
- `- updatedAt: ${selected.task.updatedAt}`,
858
- `- roadmapRefs: ${selected.task.roadmapRefs.join(', ') || '(none)'}`,
859
- `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selectedTaskDocument.markdownPath}`,
860
- '',
861
- '### Top Candidates',
862
- ...rankedCandidates
863
- .slice(0, candidateLimit)
864
- .map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | projectPath=${toProjectPath(item.governanceDir)} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
865
- '',
866
- '### Selection Reason',
867
- '- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.',
868
- `- Selected candidate scores: projectScore=${selected.projectScore}, taskPriority=${selected.taskPriority}, taskUpdatedAtMs=${selected.taskUpdatedAtMs}.`,
814
+ async execute({ limit }) {
815
+ const roots = resolveScanRoots();
816
+ const depth = resolveScanDepth();
817
+ const projects = await discoverProjectsAcrossRoots(roots, depth);
818
+ const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
819
+ if (rankedCandidates.length === 0) {
820
+ const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
821
+ const tasksPath = path.join(governanceDir, '.projitive');
822
+ await ensureStore(tasksPath);
823
+ const stats = await loadTaskStatusStatsFromStore(tasksPath);
824
+ const roadmapIds = await readRoadmapIds(governanceDir);
825
+ return { governanceDir, roadmapIds, total: stats.total, todo: stats.todo, inProgress: stats.inProgress, blocked: stats.blocked, done: stats.done };
826
+ }));
827
+ const preferredProject = projectSnapshots[0];
828
+ const preferredRoadmapRef = preferredProject?.roadmapIds[0] ?? 'ROADMAP-0001';
829
+ const noTaskDiscoveryGuidance = await resolveNoTaskDiscoveryGuidance(preferredProject?.governanceDir);
830
+ return { isEmpty: true, roots, depth, projects, projectSnapshots, preferredProject, preferredRoadmapRef, noTaskDiscoveryGuidance };
831
+ }
832
+ const selected = rankedCandidates[0];
833
+ const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
834
+ const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
835
+ const fileCandidates = candidateFilesFromArtifacts(artifacts);
836
+ const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
837
+ const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
838
+ const taskLocation = (await findTextReferences(selectedTaskDocument.markdownPath, selected.task.id))[0];
839
+ const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
840
+ const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
841
+ const candidateLimit = limit ?? 5;
842
+ return {
843
+ isEmpty: false,
844
+ roots, depth, projects,
845
+ rankedCandidates, selected, selectedTaskDocument,
846
+ relatedArtifacts, referenceLocations,
847
+ suggestedReadOrder, projectContextDocsState, taskLocation, candidateLimit,
848
+ };
849
+ },
850
+ summary: (data) => {
851
+ if (data.isEmpty) {
852
+ return [
853
+ `- rootPaths: ${data.roots.join(', ')}`,
854
+ `- rootCount: ${data.roots.length}`,
855
+ `- maxDepth: ${data.depth}`,
856
+ `- matchedProjects: ${data.projects.length}`,
857
+ '- actionableTasks: 0',
858
+ ];
859
+ }
860
+ return [
861
+ `- rootPaths: ${data.roots.join(', ')}`,
862
+ `- rootCount: ${data.roots.length}`,
863
+ `- maxDepth: ${data.depth}`,
864
+ `- matchedProjects: ${data.projects.length}`,
865
+ `- actionableTasks: ${data.rankedCandidates.length}`,
866
+ `- selectedProject: ${toProjectPath(data.selected.governanceDir)}`,
867
+ `- selectedTaskId: ${data.selected.task.id}`,
868
+ `- selectedTaskStatus: ${data.selected.task.status}`,
869
+ ];
870
+ },
871
+ evidence: (data) => {
872
+ if (data.isEmpty) {
873
+ return [
874
+ '### Project Snapshots',
875
+ ...(data.projectSnapshots.length > 0
876
+ ? data.projectSnapshots.map((item, index) => `${index + 1}. ${toProjectPath(item.governanceDir)} | total=${item.total} | todo=${item.todo} | in_progress=${item.inProgress} | blocked=${item.blocked} | done=${item.done} | roadmapIds=${item.roadmapIds.join(', ') || '(none)'}`)
877
+ : ['- (none)']),
869
878
  '',
870
- '### Related Artifacts',
871
- ...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
879
+ '### Seed Task Template',
880
+ ...renderTaskSeedTemplate(data.preferredRoadmapRef),
881
+ ];
882
+ }
883
+ const { taskLocation, selectedTaskDocument, rankedCandidates, candidateLimit, relatedArtifacts, referenceLocations, suggestedReadOrder } = data;
884
+ const taskLocationStr = taskLocation
885
+ ? `${taskLocation.filePath}#L${taskLocation.line}`
886
+ : selectedTaskDocument.markdownPath;
887
+ return [
888
+ '### Selected Task',
889
+ `- id: ${data.selected.task.id}`,
890
+ `- title: ${data.selected.task.title}`,
891
+ `- owner: ${data.selected.task.owner || '(none)'}`,
892
+ `- updatedAt: ${data.selected.task.updatedAt}`,
893
+ `- roadmapRefs: ${data.selected.task.roadmapRefs.join(', ') || '(none)'}`,
894
+ `- taskLocation: ${taskLocationStr}`,
895
+ '',
896
+ '### Top Candidates',
897
+ ...rankedCandidates
898
+ .slice(0, candidateLimit)
899
+ .map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | projectPath=${toProjectPath(item.governanceDir)} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
900
+ '',
901
+ '### Selection Reason',
902
+ '- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.',
903
+ `- Selected candidate scores: projectScore=${data.selected.projectScore}, taskPriority=${data.selected.taskPriority}, taskUpdatedAtMs=${data.selected.taskUpdatedAtMs}.`,
904
+ '',
905
+ '### Related Artifacts',
906
+ ...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
907
+ '',
908
+ '### Reference Locations',
909
+ ...(referenceLocations.length > 0
910
+ ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
911
+ : ['- (none)']),
912
+ '',
913
+ '### Suggested Read Order',
914
+ ...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
915
+ ];
916
+ },
917
+ guidance: (data) => {
918
+ if (data.isEmpty) {
919
+ return [
920
+ '- No TODO/IN_PROGRESS task is available.',
921
+ '- Create 1-3 new TODO tasks using `taskCreate(...)` from active roadmap slices.',
922
+ '- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.',
923
+ '- If roadmap has active milestones, analyze milestone intent and split into 1-3 executable TODO tasks.',
872
924
  '',
873
- '### Reference Locations',
874
- ...(referenceLocations.length > 0
875
- ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
876
- : ['- (none)']),
925
+ '### No-Task Discovery Checklist',
926
+ ...data.noTaskDiscoveryGuidance,
877
927
  '',
878
- '### Suggested Read Order',
879
- ...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
880
- ]),
881
- guidanceSection([
882
- '- Start immediately with Suggested Read Order and execute the selected task.',
883
- '- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.',
884
- '- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.',
885
- ]),
886
- lintSection(lintSuggestions),
887
- nextCallSection(`taskContext(projectPath="${toProjectPath(selected.governanceDir)}", taskId="${selected.task.id}")`),
888
- ],
889
- });
890
- return asText(markdown);
891
- });
892
- server.registerTool('taskContext', {
928
+ '- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.',
929
+ '- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.',
930
+ '- After creating tasks, rerun `taskNext` to re-rank actionable work.',
931
+ ];
932
+ }
933
+ return [
934
+ ...(!data.projectContextDocsState.ready
935
+ ? [
936
+ '- Project context docs are incomplete. Complete missing project architecture/style docs before deep implementation.',
937
+ ...(data.projectContextDocsState.missingArchitectureDocs
938
+ ? [`- Missing architecture design doc: create required file ${CORE_ARCHITECTURE_DOC_FILE}.`]
939
+ : []),
940
+ ...(data.projectContextDocsState.missingStyleDocs
941
+ ? [`- Missing design style doc: create required file ${CORE_STYLE_DOC_FILE}.`]
942
+ : []),
943
+ ]
944
+ : []),
945
+ '- Start immediately with Suggested Read Order and execute the selected task.',
946
+ '- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.',
947
+ '- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.',
948
+ ];
949
+ },
950
+ suggestions: (data) => {
951
+ if (data.isEmpty) {
952
+ return [
953
+ '- No actionable tasks found. Verify task statuses and required fields in .projitive task table.',
954
+ '- Ensure each new task has stable TASK-<number> ID and at least one roadmapRefs item.',
955
+ ];
956
+ }
957
+ return [
958
+ ...collectTaskLintSuggestions(data.selectedTaskDocument.tasks),
959
+ ...renderLintSuggestions(collectProjectContextDocsLintSuggestions(data.projectContextDocsState)),
960
+ ];
961
+ },
962
+ nextCall: (data) => {
963
+ if (data.isEmpty) {
964
+ return data.preferredProject
965
+ ? `taskCreate(projectPath="${toProjectPath(data.preferredProject.governanceDir)}", title="Create first executable slice", roadmapRefs=["${data.preferredRoadmapRef}"], summary="Derived from active roadmap milestone")`
966
+ : 'projectScan()';
967
+ }
968
+ return `taskContext(projectPath="${toProjectPath(data.selected.governanceDir)}", taskId="${data.selected.task.id}")`;
969
+ },
970
+ }));
971
+ server.registerTool(...createGovernedTool({
972
+ name: 'taskContext',
893
973
  title: 'Task Context',
894
974
  description: 'Get deep context, evidence links, and read order for one task',
895
975
  inputSchema: {
896
976
  projectPath: z.string(),
897
977
  taskId: z.string(),
898
978
  },
899
- }, async ({ projectPath, taskId }) => {
900
- if (!isValidTaskId(taskId)) {
901
- return {
902
- ...asText(renderErrorMarkdown('taskContext', `Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'retry with a valid task ID'], `taskContext(projectPath="${projectPath}", taskId="TASK-0001")`)),
903
- isError: true,
904
- };
905
- }
906
- const governanceDir = await resolveGovernanceDir(projectPath);
907
- const normalizedProjectPath = toProjectPath(governanceDir);
908
- const { markdownPath, tasks } = await loadTasksDocument(governanceDir);
909
- const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
910
- const task = tasks.find((item) => item.id === taskId);
911
- if (!task) {
912
- return {
913
- ...asText(renderErrorMarkdown('taskContext', `Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`)),
914
- isError: true,
915
- };
916
- }
917
- const lintSuggestions = [
918
- ...collectSingleTaskLintSuggestions(task),
919
- ...(await collectTaskFileLintSuggestions(governanceDir, task)),
920
- ];
921
- const contextReadingGuidance = await resolveTaskContextReadingGuidance(governanceDir);
922
- const taskLocation = (await findTextReferences(markdownPath, taskId))[0];
923
- const artifacts = await discoverGovernanceArtifacts(governanceDir);
924
- const fileCandidates = candidateFilesFromArtifacts(artifacts);
925
- const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
926
- const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
927
- const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
928
- // Build summary with subState and blocker info (v1.1.0)
929
- const summaryLines = [
930
- `- projectPath: ${normalizedProjectPath}`,
931
- `- governanceDir: ${governanceDir}`,
932
- `- tasksView: ${markdownPath}`,
933
- `- roadmapView: ${roadmapViewPath}`,
934
- `- taskId: ${task.id}`,
935
- `- title: ${task.title}`,
936
- `- status: ${task.status}`,
937
- `- owner: ${task.owner}`,
938
- `- updatedAt: ${task.updatedAt}`,
939
- `- roadmapRefs: ${task.roadmapRefs.join(', ') || '(none)'}`,
940
- `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
941
- ];
942
- // Add subState info for IN_PROGRESS tasks (v1.1.0)
943
- if (task.subState && task.status === 'IN_PROGRESS') {
944
- summaryLines.push('- subState:');
945
- if (task.subState.phase) {
946
- summaryLines.push(` - phase: ${task.subState.phase}`);
979
+ async execute({ projectPath, taskId }) {
980
+ if (!isValidTaskId(taskId)) {
981
+ throw new ToolExecutionError(`Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'retry with a valid task ID'], `taskContext(projectPath="${projectPath}", taskId="TASK-0001")`);
947
982
  }
948
- if (typeof task.subState.confidence === 'number') {
949
- summaryLines.push(` - confidence: ${task.subState.confidence}`);
983
+ const governanceDir = await resolveGovernanceDir(projectPath);
984
+ const normalizedProjectPath = toProjectPath(governanceDir);
985
+ const { markdownPath, tasks } = await loadTasksDocument(governanceDir);
986
+ const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
987
+ const task = tasks.find((item) => item.id === taskId);
988
+ if (!task) {
989
+ throw new ToolExecutionError(`Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`);
950
990
  }
951
- if (task.subState.estimatedCompletion) {
952
- summaryLines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
953
- }
954
- }
955
- // Add blocker info for BLOCKED tasks (v1.1.0)
956
- if (task.blocker && task.status === 'BLOCKED') {
957
- summaryLines.push('- blocker:');
958
- summaryLines.push(` - type: ${task.blocker.type}`);
959
- summaryLines.push(` - description: ${task.blocker.description}`);
960
- if (task.blocker.blockingEntity) {
961
- summaryLines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
991
+ const researchBriefState = await inspectTaskResearchBrief(governanceDir, task);
992
+ const contextReadingGuidance = await resolveTaskContextReadingGuidance(governanceDir);
993
+ const taskLocation = (await findTextReferences(markdownPath, taskId))[0];
994
+ const artifacts = await discoverGovernanceArtifacts(governanceDir);
995
+ const fileCandidates = candidateFilesFromArtifacts(artifacts);
996
+ const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
997
+ const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
998
+ const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
999
+ const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
1000
+ return {
1001
+ normalizedProjectPath, governanceDir, markdownPath, roadmapViewPath,
1002
+ task, researchBriefState, contextReadingGuidance,
1003
+ taskLocation, referenceLocations, relatedArtifacts, suggestedReadOrder,
1004
+ projectContextDocsState,
1005
+ };
1006
+ },
1007
+ summary: ({ normalizedProjectPath, governanceDir, markdownPath, roadmapViewPath, task, researchBriefState, projectContextDocsState, taskLocation }) => {
1008
+ const lines = [
1009
+ `- projectPath: ${normalizedProjectPath}`,
1010
+ `- governanceDir: ${governanceDir}`,
1011
+ `- tasksView: ${markdownPath}`,
1012
+ `- roadmapView: ${roadmapViewPath}`,
1013
+ `- taskId: ${task.id}`,
1014
+ `- title: ${task.title}`,
1015
+ `- status: ${task.status}`,
1016
+ `- owner: ${task.owner}`,
1017
+ `- updatedAt: ${task.updatedAt}`,
1018
+ `- roadmapRefs: ${task.roadmapRefs.join(', ') || '(none)'}`,
1019
+ `- researchBriefPath: ${researchBriefState.relativePath}`,
1020
+ `- researchBriefStatus: ${researchBriefState.ready ? 'READY' : 'MISSING'}`,
1021
+ `- architectureDocsStatus: ${projectContextDocsState.missingArchitectureDocs ? 'MISSING' : 'READY'}`,
1022
+ `- styleDocsStatus: ${projectContextDocsState.missingStyleDocs ? 'MISSING' : 'READY'}`,
1023
+ `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
1024
+ ];
1025
+ if (task.subState && task.status === 'IN_PROGRESS') {
1026
+ lines.push('- subState:');
1027
+ if (task.subState.phase)
1028
+ lines.push(` - phase: ${task.subState.phase}`);
1029
+ if (typeof task.subState.confidence === 'number')
1030
+ lines.push(` - confidence: ${task.subState.confidence}`);
1031
+ if (task.subState.estimatedCompletion)
1032
+ lines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
962
1033
  }
963
- if (task.blocker.unblockCondition) {
964
- summaryLines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
1034
+ if (task.blocker && task.status === 'BLOCKED') {
1035
+ lines.push('- blocker:');
1036
+ lines.push(` - type: ${task.blocker.type}`);
1037
+ lines.push(` - description: ${task.blocker.description}`);
1038
+ if (task.blocker.blockingEntity)
1039
+ lines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
1040
+ if (task.blocker.unblockCondition)
1041
+ lines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
1042
+ if (task.blocker.escalationPath)
1043
+ lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
965
1044
  }
966
- if (task.blocker.escalationPath) {
967
- summaryLines.push(` - escalationPath: ${task.blocker.escalationPath}`);
968
- }
969
- }
970
- const coreMarkdown = renderToolResponseMarkdown({
971
- toolName: 'taskContext',
972
- sections: [
973
- summarySection(summaryLines),
974
- evidenceSection([
975
- '### Related Artifacts',
976
- ...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
977
- '',
978
- '### Reference Locations',
979
- ...(referenceLocations.length > 0
980
- ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
981
- : ['- (none)']),
1045
+ return lines;
1046
+ },
1047
+ evidence: ({ task, researchBriefState, projectContextDocsState, relatedArtifacts, referenceLocations, suggestedReadOrder }) => [
1048
+ '### Pre-Execution Research Brief',
1049
+ `- path: ${researchBriefState.relativePath}`,
1050
+ `- absolutePath: ${researchBriefState.absolutePath}`,
1051
+ `- status: ${researchBriefState.ready ? 'READY' : 'MISSING'}`,
1052
+ ...(!researchBriefState.ready
1053
+ ? [
982
1054
  '',
983
- '### Suggested Read Order',
984
- ...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
1055
+ '### Required Research Brief Template',
1056
+ ...renderTaskResearchBriefTemplate(task).map((line) => `- ${line}`),
1057
+ ]
1058
+ : []),
1059
+ '',
1060
+ '### Project Context Docs Check',
1061
+ `- architecture docs: ${projectContextDocsState.architectureDocs.length > 0 ? 'found' : 'missing'}`,
1062
+ ...(projectContextDocsState.architectureDocs.length > 0
1063
+ ? projectContextDocsState.architectureDocs.map((item) => `- architecture: ${item}`)
1064
+ : [`- architecture: add required file ${CORE_ARCHITECTURE_DOC_FILE}.`]),
1065
+ `- design style docs: ${projectContextDocsState.styleDocs.length > 0 ? 'found' : 'missing'}`,
1066
+ ...(projectContextDocsState.styleDocs.length > 0
1067
+ ? projectContextDocsState.styleDocs.map((item) => `- style: ${item}`)
1068
+ : [`- style: add required file ${CORE_STYLE_DOC_FILE}.`]),
1069
+ '',
1070
+ '### Related Artifacts',
1071
+ ...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
1072
+ '',
1073
+ '### Reference Locations',
1074
+ ...(referenceLocations.length > 0
1075
+ ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
1076
+ : ['- (none)']),
1077
+ '',
1078
+ '### Suggested Read Order',
1079
+ ...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
1080
+ ],
1081
+ guidance: ({ researchBriefState, projectContextDocsState, contextReadingGuidance, task }) => [
1082
+ ...(!researchBriefState.ready
1083
+ ? [
1084
+ '- Pre-execution gate is NOT satisfied. Complete research brief first, then proceed with implementation.',
1085
+ `- Create or update ${researchBriefState.relativePath} with design guidelines + code architecture findings before code changes.`,
1086
+ '- Include exact file/line locations in the brief (for example path/to/file.ts#L120).',
1087
+ '- Re-run taskContext after writing the brief and confirm researchBriefStatus becomes READY.',
1088
+ ]
1089
+ : [
1090
+ '- Pre-execution gate satisfied. Read the research brief first, then continue implementation.',
1091
+ `- Must read ${researchBriefState.relativePath} before any task execution changes.`,
985
1092
  ]),
986
- guidanceSection([
987
- '- Read the files in Suggested Read Order.',
988
- '',
989
- '### Recommended Context Reading',
990
- ...contextReadingGuidance,
991
- '',
992
- '- Verify whether current status and evidence are consistent.',
993
- ...taskStatusGuidance(task),
994
- '- If updates are needed, use tool writes for governance store (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.',
995
- '- After editing, re-run `taskContext` to verify references and context consistency.',
1093
+ ...(!projectContextDocsState.ready
1094
+ ? [
1095
+ '- Project context docs gate is NOT satisfied. Complete missing project architecture/style docs first.',
1096
+ ...(projectContextDocsState.missingArchitectureDocs
1097
+ ? [`- Missing architecture design doc. Add required file ${CORE_ARCHITECTURE_DOC_FILE} and include architecture boundaries and module responsibilities.`]
1098
+ : []),
1099
+ ...(projectContextDocsState.missingStyleDocs
1100
+ ? [`- Missing design style doc. Add required file ${CORE_STYLE_DOC_FILE} and include style language, tokens/themes, and UI consistency rules.`]
1101
+ : []),
1102
+ '- Re-run taskContext and confirm both architectureDocsStatus/styleDocsStatus are READY.',
1103
+ ]
1104
+ : [
1105
+ '- Project context docs gate satisfied. Architecture/style docs are available for execution alignment.',
996
1106
  ]),
997
- lintSection(lintSuggestions),
998
- nextCallSection(`taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${task.id}")`),
999
- ],
1000
- });
1001
- return asText(coreMarkdown);
1002
- });
1107
+ '- Read the files in Suggested Read Order.',
1108
+ '',
1109
+ '### Context Reading',
1110
+ ...contextReadingGuidance,
1111
+ '',
1112
+ '- Verify whether current status and evidence are consistent.',
1113
+ ...taskStatusGuidance(task),
1114
+ '- If updates are needed, use tool writes for governance store (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.',
1115
+ '- After editing, re-run `taskContext` to verify references and context consistency.',
1116
+ ],
1117
+ suggestions: ({ task, researchBriefState, projectContextDocsState }) => [
1118
+ ...collectSingleTaskLintSuggestions(task),
1119
+ ...renderLintSuggestions(collectTaskResearchBriefLintSuggestions(researchBriefState)),
1120
+ ...renderLintSuggestions(collectProjectContextDocsLintSuggestions(projectContextDocsState)),
1121
+ ],
1122
+ nextCall: ({ normalizedProjectPath, task }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${task.id}")`,
1123
+ }));
1003
1124
  // taskUpdate tool - Update task fields including subState and blocker (Spec v1.1.0)
1004
- server.registerTool('taskUpdate', {
1125
+ server.registerTool(...createGovernedTool({
1126
+ name: 'taskUpdate',
1005
1127
  title: 'Task Update',
1006
1128
  description: 'Update task fields including status, owner, summary, subState, and blocker metadata',
1007
1129
  inputSchema: {
@@ -1027,140 +1149,108 @@ export function registerTaskTools(server) {
1027
1149
  }).optional(),
1028
1150
  }),
1029
1151
  },
1030
- }, async ({ projectPath, taskId, updates }) => {
1031
- if (!isValidTaskId(taskId)) {
1032
- return {
1033
- ...asText(renderErrorMarkdown('taskUpdate', `Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'retry with a valid task ID'], `taskUpdate(projectPath="${projectPath}", taskId="TASK-0001", updates={...})`)),
1034
- isError: true,
1035
- };
1036
- }
1037
- const governanceDir = await resolveGovernanceDir(projectPath);
1038
- const normalizedProjectPath = toProjectPath(governanceDir);
1039
- const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
1040
- const tasksViewPath = path.join(governanceDir, TASKS_MARKDOWN_FILE);
1041
- const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
1042
- const taskIndex = tasks.findIndex((item) => item.id === taskId);
1043
- if (taskIndex === -1) {
1044
- return {
1045
- ...asText(renderErrorMarkdown('taskUpdate', `Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`)),
1046
- isError: true,
1047
- };
1048
- }
1049
- const task = tasks[taskIndex];
1050
- const originalStatus = task.status;
1051
- // Validate status transition
1052
- if (updates.status && !validateTransition(originalStatus, updates.status)) {
1053
- return {
1054
- ...asText(renderErrorMarkdown('taskUpdate', `Invalid status transition: ${originalStatus} -> ${updates.status}`, ['use `validateTransition` to check allowed transitions', 'provide evidence when transitioning to DONE'], `taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${taskId}")`)),
1055
- isError: true,
1056
- };
1057
- }
1058
- // Apply updates
1059
- if (updates.status)
1060
- task.status = updates.status;
1061
- if (updates.owner !== undefined)
1062
- task.owner = updates.owner;
1063
- if (updates.summary !== undefined)
1064
- task.summary = updates.summary;
1065
- if (updates.roadmapRefs)
1066
- task.roadmapRefs = updates.roadmapRefs;
1067
- if (updates.links)
1068
- task.links = updates.links;
1069
- // Handle subState (Spec v1.1.0)
1070
- if (updates.subState !== undefined) {
1071
- if (updates.subState === null) {
1072
- delete task.subState;
1152
+ async execute({ projectPath, taskId, updates }) {
1153
+ if (!isValidTaskId(taskId)) {
1154
+ throw new ToolExecutionError(`Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'retry with a valid task ID'], `taskUpdate(projectPath="${projectPath}", taskId="TASK-0001", updates={...})`);
1073
1155
  }
1074
- else {
1075
- task.subState = {
1076
- ...(task.subState || {}),
1077
- ...updates.subState,
1078
- };
1156
+ const governanceDir = await resolveGovernanceDir(projectPath);
1157
+ const normalizedProjectPath = toProjectPath(governanceDir);
1158
+ const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
1159
+ const tasksViewPath = path.join(governanceDir, TASKS_MARKDOWN_FILE);
1160
+ const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
1161
+ const taskIndex = tasks.findIndex((item) => item.id === taskId);
1162
+ if (taskIndex === -1) {
1163
+ throw new ToolExecutionError(`Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`);
1079
1164
  }
1080
- }
1081
- // Handle blocker (Spec v1.1.0)
1082
- if (updates.blocker !== undefined) {
1083
- if (updates.blocker === null) {
1084
- delete task.blocker;
1165
+ const task = tasks[taskIndex];
1166
+ const originalStatus = task.status;
1167
+ const previewTask = normalizeTask({ ...task, ...updates, updatedAt: nowIso() });
1168
+ if (updates.status && !validateTransition(originalStatus, updates.status)) {
1169
+ throw new ToolExecutionError(`Invalid status transition: ${originalStatus} -> ${updates.status}`, ['use `validateTransition` to check allowed transitions', 'provide evidence when transitioning to DONE'], `taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${taskId}")`);
1085
1170
  }
1086
- else {
1087
- task.blocker = updates.blocker;
1171
+ const updatedSubState = updates.subState === null ? undefined
1172
+ : updates.subState !== undefined ? { ...(task.subState ?? {}), ...updates.subState }
1173
+ : task.subState;
1174
+ const updatedBlocker = updates.blocker === null ? undefined
1175
+ : updates.blocker !== undefined ? updates.blocker
1176
+ : task.blocker;
1177
+ const normalizedTask = normalizeTask({
1178
+ ...task,
1179
+ ...(updates.status ? { status: updates.status } : {}),
1180
+ ...(updates.owner !== undefined ? { owner: updates.owner } : {}),
1181
+ ...(updates.summary !== undefined ? { summary: updates.summary } : {}),
1182
+ ...(updates.roadmapRefs ? { roadmapRefs: updates.roadmapRefs } : {}),
1183
+ ...(updates.links ? { links: updates.links } : {}),
1184
+ subState: updatedSubState,
1185
+ blocker: updatedBlocker,
1186
+ updatedAt: nowIso(),
1187
+ });
1188
+ await upsertTaskInStore(tasksPath, normalizedTask);
1189
+ await loadTasksDocumentWithOptions(governanceDir, true);
1190
+ return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, taskId, originalStatus, task: normalizedTask, previewTask, updates };
1191
+ },
1192
+ summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, taskId, originalStatus, task }) => {
1193
+ const lines = [
1194
+ `- projectPath: ${normalizedProjectPath}`,
1195
+ `- governanceDir: ${governanceDir}`,
1196
+ `- tasksView: ${tasksViewPath}`,
1197
+ `- roadmapView: ${roadmapViewPath}`,
1198
+ `- taskId: ${taskId}`,
1199
+ `- originalStatus: ${originalStatus}`,
1200
+ `- newStatus: ${task.status}`,
1201
+ `- updatedAt: ${task.updatedAt}`,
1202
+ ];
1203
+ if (task.subState) {
1204
+ lines.push('- subState:');
1205
+ if (task.subState.phase)
1206
+ lines.push(` - phase: ${task.subState.phase}`);
1207
+ if (typeof task.subState.confidence === 'number')
1208
+ lines.push(` - confidence: ${task.subState.confidence}`);
1209
+ if (task.subState.estimatedCompletion)
1210
+ lines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
1088
1211
  }
1089
- }
1090
- // Update updatedAt
1091
- task.updatedAt = nowIso();
1092
- const normalizedTask = normalizeTask(task);
1093
- // Save task incrementally
1094
- await upsertTaskInStore(tasksPath, normalizedTask);
1095
- await loadTasksDocumentWithOptions(governanceDir, true);
1096
- task.status = normalizedTask.status;
1097
- task.owner = normalizedTask.owner;
1098
- task.summary = normalizedTask.summary;
1099
- task.roadmapRefs = normalizedTask.roadmapRefs;
1100
- task.links = normalizedTask.links;
1101
- task.updatedAt = normalizedTask.updatedAt;
1102
- task.subState = normalizedTask.subState;
1103
- task.blocker = normalizedTask.blocker;
1104
- // Build response
1105
- const updateSummary = [
1106
- `- projectPath: ${normalizedProjectPath}`,
1107
- `- governanceDir: ${governanceDir}`,
1108
- `- tasksView: ${tasksViewPath}`,
1109
- `- roadmapView: ${roadmapViewPath}`,
1110
- `- taskId: ${taskId}`,
1111
- `- originalStatus: ${originalStatus}`,
1112
- `- newStatus: ${task.status}`,
1113
- `- updatedAt: ${task.updatedAt}`,
1114
- ];
1115
- if (task.subState) {
1116
- updateSummary.push('- subState:');
1117
- if (task.subState.phase)
1118
- updateSummary.push(` - phase: ${task.subState.phase}`);
1119
- if (typeof task.subState.confidence === 'number')
1120
- updateSummary.push(` - confidence: ${task.subState.confidence}`);
1121
- if (task.subState.estimatedCompletion)
1122
- updateSummary.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
1123
- }
1124
- if (task.blocker) {
1125
- updateSummary.push('- blocker:');
1126
- updateSummary.push(` - type: ${task.blocker.type}`);
1127
- updateSummary.push(` - description: ${task.blocker.description}`);
1128
- if (task.blocker.blockingEntity)
1129
- updateSummary.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
1130
- if (task.blocker.unblockCondition)
1131
- updateSummary.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
1132
- if (task.blocker.escalationPath)
1133
- updateSummary.push(` - escalationPath: ${task.blocker.escalationPath}`);
1134
- }
1135
- const markdown = renderToolResponseMarkdown({
1136
- toolName: 'taskUpdate',
1137
- sections: [
1138
- summarySection(updateSummary),
1139
- evidenceSection([
1140
- '### Updated Task',
1141
- `- ${task.id} | ${task.status} | ${task.title}`,
1142
- `- owner: ${task.owner || '(none)'}`,
1143
- `- summary: ${task.summary || '(none)'}`,
1144
- '',
1145
- '### Update Details',
1146
- ...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
1147
- ...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
1148
- ...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
1149
- ...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(', ')}`] : []),
1150
- ...(updates.links ? [`- links: ${updates.links.join(', ')}`] : []),
1151
- ...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
1152
- ...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
1153
- ]),
1154
- guidanceSection([
1155
- 'Task updated successfully and tasks.md has been synced. Run `taskContext` to verify the changes.',
1156
- 'If status changed to DONE, ensure evidence links are added.',
1157
- 'If subState or blocker were updated, verify the metadata is correct.',
1158
- '.projitive governance store is source of truth; tasks.md is a generated view and may be overwritten.',
1159
- ]),
1160
- lintSection([]),
1161
- nextCallSection(`taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${taskId}")`),
1162
- ],
1163
- });
1164
- return asText(markdown);
1165
- });
1212
+ if (task.blocker) {
1213
+ lines.push('- blocker:');
1214
+ lines.push(` - type: ${task.blocker.type}`);
1215
+ lines.push(` - description: ${task.blocker.description}`);
1216
+ if (task.blocker.blockingEntity)
1217
+ lines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
1218
+ if (task.blocker.unblockCondition)
1219
+ lines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
1220
+ if (task.blocker.escalationPath)
1221
+ lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
1222
+ }
1223
+ return lines;
1224
+ },
1225
+ evidence: ({ task, originalStatus, updates }) => [
1226
+ '### Updated Task',
1227
+ `- ${task.id} | ${task.status} | ${task.title}`,
1228
+ `- owner: ${task.owner || '(none)'}`,
1229
+ `- summary: ${task.summary || '(none)'}`,
1230
+ '',
1231
+ '### Update Details',
1232
+ ...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
1233
+ ...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
1234
+ ...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
1235
+ ...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(', ')}`] : []),
1236
+ ...(updates.links ? [`- links: ${updates.links.join(', ')}`] : []),
1237
+ ...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
1238
+ ...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
1239
+ ],
1240
+ guidance: ({ updates, originalStatus }) => [
1241
+ 'Task updated successfully and tasks.md has been synced. Run `taskContext` to verify the changes.',
1242
+ ...(updates.status === 'IN_PROGRESS' && originalStatus === 'TODO'
1243
+ ? ['- Ensure pre-execution research brief exists before deep implementation.']
1244
+ : []),
1245
+ ...(updates.status === 'DONE'
1246
+ ? ['- Verify evidence links are attached and reflect completed work.']
1247
+ : []),
1248
+ '.projitive governance store is source of truth; tasks.md is a generated view and may be overwritten.',
1249
+ ],
1250
+ suggestions: async ({ previewTask, governanceDir }) => [
1251
+ ...collectSingleTaskLintSuggestions(previewTask),
1252
+ ...renderLintSuggestions(await collectDoneConformanceSuggestions(governanceDir, previewTask)),
1253
+ ],
1254
+ nextCall: ({ normalizedProjectPath, taskId }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${taskId}")`,
1255
+ }));
1166
1256
  }