@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.
@@ -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 [
@@ -27,10 +27,180 @@ function taskStatusGuidance(task) {
27
27
  ];
28
28
  }
29
29
  if (task.status === 'BLOCKED') {
30
- return [
31
- '- This task is BLOCKED: identify blocker and required unblock condition first.',
32
- '- Reopen only after blocker evidence is documented.',
30
+ const guidance = [
31
+ '## BLOCKED Task - Structured Unblocking Path',
32
+ '',
33
33
  ];
34
+ // ===== LAYER 1: CRITICAL VALIDATION =====
35
+ guidance.push('### 🔴 CRITICAL - Validate Blocker Metadata First');
36
+ guidance.push('');
37
+ if (!task.blocker) {
38
+ guidance.push('⚠️ **BLOCKER MISSING** - Task cannot be truly BLOCKED without blocker metadata.');
39
+ guidance.push('');
40
+ guidance.push('**Required Action:**');
41
+ guidance.push('```');
42
+ guidance.push('taskUpdate(projectPath="...", taskId="' + task.id + '", {');
43
+ guidance.push(' blocker: {');
44
+ guidance.push(' type: "internal_dependency|external_dependency|resource|approval",');
45
+ guidance.push(' description: "Specific reason for block",');
46
+ guidance.push(' blockingEntity: "Optional: who/what is blocking",');
47
+ guidance.push(' unblockCondition: "Optional: exact condition to unblock"');
48
+ guidance.push(' }');
49
+ guidance.push('})');
50
+ guidance.push('```');
51
+ guidance.push('');
52
+ guidance.push('**Then re-run taskContext() to see type-specific guidance.**');
53
+ return guidance;
54
+ }
55
+ const { type, description, blockingEntity, unblockCondition, escalationPath } = task.blocker;
56
+ guidance.push('**Blocker Summary:**');
57
+ guidance.push(`- Type: **${type}**`);
58
+ guidance.push(`- Issue: ${description}`);
59
+ if (blockingEntity)
60
+ guidance.push(`- Blocking Entity: ${blockingEntity}`);
61
+ if (unblockCondition)
62
+ guidance.push(`- Unblock Condition: ${unblockCondition}`);
63
+ guidance.push('');
64
+ // ===== LAYER 2: HOW TO UNBLOCK =====
65
+ guidance.push('### 🟠 HOW TO UNBLOCK - Type-Specific Steps');
66
+ guidance.push('');
67
+ if (type === 'internal_dependency') {
68
+ guidance.push('**This task is blocked by another task (internal dependency).**');
69
+ guidance.push('');
70
+ guidance.push('**Step 1: Identify the Blocking Task**');
71
+ guidance.push('- Call `taskList()` and search for a task matching this description:');
72
+ guidance.push(` "${unblockCondition || description}"`);
73
+ guidance.push('- Or ask: who owns completing this blocker?');
74
+ guidance.push('');
75
+ guidance.push('**Step 2: Check Blocking Task Status**');
76
+ guidance.push('- If DONE: Proceed to Step 4');
77
+ guidance.push('- If TODO/IN_PROGRESS: Coordinate with owner → proceed to Step 4 when complete');
78
+ guidance.push('- If NOT FOUND: Proceed to Step 3');
79
+ guidance.push('');
80
+ guidance.push('**Step 3: Create the Missing Blocking Task (if needed)**');
81
+ guidance.push('```');
82
+ guidance.push('taskCreate(projectPath="...", {');
83
+ guidance.push(' title: "Unblock ' + task.id + ': [specific outcome]",');
84
+ guidance.push(' status: "TODO",');
85
+ guidance.push(' summary: "Required to unblock ' + task.id + '"');
86
+ guidance.push('})');
87
+ guidance.push('```');
88
+ guidance.push('');
89
+ guidance.push('**Step 4: After Blocker is Resolved - Unblock This Task**');
90
+ guidance.push('- Verify the blocker is actually DONE (not just in progress)');
91
+ guidance.push('- Call `taskUpdate()` to move back to TODO:');
92
+ guidance.push('```');
93
+ guidance.push('taskUpdate(projectPath="...", taskId="' + task.id + '", {status: "TODO"})');
94
+ guidance.push('```');
95
+ guidance.push('- This removes the BLOCKED state and allows execution to continue');
96
+ }
97
+ else if (type === 'external_dependency') {
98
+ guidance.push('**This task is blocked by an external party/service/event.**');
99
+ guidance.push('');
100
+ guidance.push('**Step 1: Understand What is Needed**');
101
+ guidance.push(`- Blocking Entity: ${blockingEntity || '(not documented)'}`);
102
+ guidance.push(`- Description: ${description}`);
103
+ guidance.push('- Contact method: (verify in your knowledge base)');
104
+ guidance.push('');
105
+ guidance.push('**Step 2: Reach Out or Escalate**');
106
+ if (escalationPath) {
107
+ guidance.push(`- Use escalation path: ${escalationPath}`);
108
+ }
109
+ else {
110
+ guidance.push('- If escalation path missing: call `taskUpdate()` to add it');
111
+ }
112
+ guidance.push('- Send request specifying: what, by when, and why (link this TASK ID)');
113
+ guidance.push('');
114
+ guidance.push('**Step 3: Track External Progress**');
115
+ guidance.push('- Check status periodically');
116
+ guidance.push('- Update task with any new information via `taskUpdate()`');
117
+ guidance.push('- If blocked for too long: follow escalation path');
118
+ guidance.push('');
119
+ guidance.push('**Step 4: After External Delivery - Unblock This Task**');
120
+ guidance.push('- Verify delivery is complete and acceptable');
121
+ guidance.push('- Call `taskUpdate()` to move back to TODO:');
122
+ guidance.push('```');
123
+ guidance.push('taskUpdate(projectPath="...", taskId="' + task.id + '", {status: "TODO"})');
124
+ guidance.push('```');
125
+ }
126
+ else if (type === 'resource') {
127
+ guidance.push('**This task is blocked by missing resource (tools, access, personnel, budget, etc.).**');
128
+ guidance.push('');
129
+ guidance.push('**Step 1: Clarify the Missing Resource**');
130
+ guidance.push(`- Need: ${description}`);
131
+ guidance.push('- Who can allocate? (should be in escalationPath)');
132
+ guidance.push('');
133
+ guidance.push('**Step 2: Request or Allocate**');
134
+ if (escalationPath) {
135
+ guidance.push(`- Contact: ${escalationPath}`);
136
+ }
137
+ else {
138
+ guidance.push('- FIX: Call `taskUpdate()` to add escalationPath for resource owner');
139
+ }
140
+ guidance.push('- Provide justification (link this TASK ID and explain why needed)');
141
+ guidance.push('');
142
+ guidance.push('**Step 3: Wait for Approval and Setup**');
143
+ guidance.push('- Track approval and allocation status');
144
+ guidance.push('- Once allocated and available: proceed to Step 4');
145
+ guidance.push('');
146
+ guidance.push('**Step 4: After Resource Secured - Unblock This Task**');
147
+ guidance.push('- Confirm resource is ready to use');
148
+ guidance.push('- Call `taskUpdate()` to move back to TODO:');
149
+ guidance.push('```');
150
+ guidance.push('taskUpdate(projectPath="...", taskId="' + task.id + '", {status: "TODO"})');
151
+ guidance.push('```');
152
+ }
153
+ else if (type === 'approval') {
154
+ guidance.push('**This task is blocked by pending approval from decision maker.**');
155
+ guidance.push('');
156
+ guidance.push('**Step 1: Identify Approver**');
157
+ guidance.push(`- Approver: ${blockingEntity || '(not documented)'}`);
158
+ guidance.push('- Approval criteria: (ensure clear in description)');
159
+ guidance.push('');
160
+ guidance.push('**Step 2: Prepare and Submit Approval Request**');
161
+ guidance.push('- What are you asking approval for? (clear one-sentence request)');
162
+ guidance.push('- Why? (link this TASK ID and provide context)');
163
+ guidance.push('- By when? (deadline)');
164
+ if (blockingEntity) {
165
+ guidance.push(`- Send to: ${blockingEntity}`);
166
+ }
167
+ guidance.push('');
168
+ guidance.push('**Step 3: Track Approval Process**');
169
+ guidance.push('- Follow up if no response by deadline');
170
+ if (escalationPath) {
171
+ guidance.push(`- If denied or stalled: use escalation path: ${escalationPath}`);
172
+ }
173
+ else {
174
+ guidance.push('- If escalation needed: call `taskUpdate()` to add escalationPath');
175
+ }
176
+ guidance.push('');
177
+ guidance.push('**Step 4: After Approval Granted - Unblock This Task**');
178
+ guidance.push('- Confirm approval is in writing (link to approval evidence)');
179
+ guidance.push('- Call `taskUpdate()` to move back to TODO:');
180
+ guidance.push('```');
181
+ guidance.push('taskUpdate(projectPath="...", taskId="' + task.id + '", {status: "TODO"})');
182
+ guidance.push('```');
183
+ }
184
+ guidance.push('');
185
+ // ===== LAYER 3: REFERENCE INFORMATION =====
186
+ guidance.push('### ℹ️ REFERENCE - System-Wide Information');
187
+ guidance.push('');
188
+ guidance.push('**All Blocker Types:**');
189
+ guidance.push('- `internal_dependency` — Blocked by another task that must complete first');
190
+ guidance.push('- `external_dependency` — Blocked by external party/service/event');
191
+ guidance.push('- `resource` — Blocked by missing resource (tool, access, personnel, budget)');
192
+ guidance.push('- `approval` — Blocked by pending decision/sign-off');
193
+ guidance.push('');
194
+ guidance.push('**Unblock Verification Checklist:**');
195
+ guidance.push('- ✓ Blocker condition is actually met (not just "almost done")');
196
+ guidance.push('- ✓ Evidence is documented (link to TASK/report/email/etc)');
197
+ guidance.push('- ✓ Task status can be safely moved back to TODO');
198
+ guidance.push('');
199
+ guidance.push('**After Unblocking - Next Steps:**');
200
+ guidance.push('1. Call `taskUpdate(..., {status: "TODO"})` to unblock');
201
+ guidance.push('2. Call `taskContext()` to see task in unblocked state');
202
+ guidance.push('3. Call `taskNext()` to resume execution flow');
203
+ return guidance;
34
204
  }
35
205
  return [
36
206
  '- This task is DONE: only reopen when new requirement changes scope.',
@@ -39,13 +209,18 @@ function taskStatusGuidance(task) {
39
209
  }
40
210
  const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
41
211
  '- Recheck project state first: run projectContext and confirm there is truly no TODO/IN_PROGRESS task to execute.',
42
- '- Create new tasks via `taskCreate(...)` (do not edit tasks.md directly).',
43
- '- If all remaining tasks are BLOCKED, create one unblock task with explicit unblock condition and dependency owner.',
44
- '- Start from active roadmap milestones and split into the smallest executable slices with a single done condition each.',
45
- '- Prefer slices that unlock multiple downstream tasks before isolated refactors or low-impact cleanups.',
212
+ '- Check BLOCKED tasks: if BLOCKED tasks exist, read their blocker metadata and take unblock action before creating new tasks.',
213
+ ' - internal_dependency: create/track the blocking task, coordinate with owner',
214
+ ' - external_dependency: reach out to blocking entity or escalate',
215
+ ' - resource: request/allocate the missing resource',
216
+ ' - approval: follow escalation path to expedite approval',
217
+ '- Only after all BLOCKED tasks are unblocked (moved back to TODO/IN_PROGRESS), then create new tasks via `taskCreate(...)`.',
46
218
  '- Create TODO tasks only when evidence is clear: each new task must produce at least one report/designs/readme artifact update.',
219
+ '- Start from active roadmap milestones and split into smallest executable slices with single done condition each.',
220
+ '- Prefer slices that unlock multiple downstream tasks before isolated refactors or low-impact cleanups.',
47
221
  '- Skip duplicate scope: do not create tasks that overlap existing TODO/IN_PROGRESS/BLOCKED task intent.',
48
222
  '- Use quality gates for discovery candidates: user value, delivery risk reduction, or measurable throughput improvement.',
223
+ '- Review and update project architecture docs under designs/core/ (architecture.md, style-guide.md) if they are missing or outdated.',
49
224
  '- Keep each discovery round small (1-3 tasks), then rerun taskNext immediately for re-ranking and execution.',
50
225
  ];
51
226
  const DEFAULT_TASK_CONTEXT_READING_GUIDANCE = [
@@ -113,6 +288,87 @@ function normalizeTaskLink(link) {
113
288
  function resolveTaskLinkPath(projectPath, link) {
114
289
  return path.join(projectPath, link);
115
290
  }
291
+ function taskResearchBriefRelativePath(taskId) {
292
+ return `${TASK_RESEARCH_DIR}/${taskId}${TASK_RESEARCH_FILE_SUFFIX}`;
293
+ }
294
+ function renderTaskResearchBriefTemplate(task) {
295
+ return [
296
+ `# ${task.id} Implementation Research Brief`,
297
+ '',
298
+ `Task: ${task.title}`,
299
+ `Summary: ${task.summary || '(fill this with a short objective summary)'}`,
300
+ '',
301
+ '## Design Guidelines and Specs',
302
+ '- [ ] List relevant design/governance/spec files with line location',
303
+ '- Example: designs/ARCHITECTURE.md#L42-L76 - API boundary and constraints',
304
+ '- Example: roadmap.md#L18 - milestone acceptance criteria',
305
+ '',
306
+ '## Code Architecture and Implementation Findings',
307
+ '- [ ] Document current architecture and extension points with line location',
308
+ '- Example: packages/mcp/source/tools/task.ts#L1020-L1130 - taskContext response assembly',
309
+ '- Example: packages/mcp/source/prompts/taskExecution.ts#L25-L130 - execution workflow prompt',
310
+ '',
311
+ '## Implementation Plan',
312
+ '- [ ] Proposed change list with impacted modules',
313
+ '- [ ] Validation and regression test plan',
314
+ '',
315
+ '## Risks and Open Questions',
316
+ '- [ ] Known risks, assumptions, and unresolved questions',
317
+ ];
318
+ }
319
+ async function inspectTaskResearchBrief(governanceDir, task) {
320
+ const projectPath = toProjectPath(governanceDir);
321
+ const relativePath = taskResearchBriefRelativePath(task.id);
322
+ const absolutePath = resolveTaskLinkPath(projectPath, relativePath);
323
+ const exists = await fs.access(absolutePath).then(() => true).catch(() => false);
324
+ return { relativePath, absolutePath, exists, ready: exists };
325
+ }
326
+ function collectTaskResearchBriefLintSuggestions(state) {
327
+ if (!state.exists) {
328
+ return [{
329
+ code: TASK_LINT_CODES.RESEARCH_BRIEF_MISSING,
330
+ message: `Pre-execution research brief missing: ${state.relativePath}.`,
331
+ fixHint: 'Create the file and fill required sections before implementation.',
332
+ }];
333
+ }
334
+ return [];
335
+ }
336
+ function inspectProjectContextDocsFromArtifacts(files) {
337
+ const markdownFiles = files
338
+ .map((item) => item.replace(/\\/g, '/'))
339
+ .filter((item) => item.toLowerCase().endsWith('.md'));
340
+ const architectureDocSuffix = `/${CORE_ARCHITECTURE_DOC_FILE}`.toLowerCase();
341
+ const styleDocSuffix = `/${CORE_STYLE_DOC_FILE}`.toLowerCase();
342
+ const architectureDocs = markdownFiles.filter((item) => item.toLowerCase().endsWith(architectureDocSuffix));
343
+ const styleDocs = markdownFiles.filter((item) => item.toLowerCase().endsWith(styleDocSuffix));
344
+ const missingArchitectureDocs = architectureDocs.length === 0;
345
+ const missingStyleDocs = styleDocs.length === 0;
346
+ return {
347
+ architectureDocs,
348
+ styleDocs,
349
+ missingArchitectureDocs,
350
+ missingStyleDocs,
351
+ ready: !missingArchitectureDocs && !missingStyleDocs,
352
+ };
353
+ }
354
+ function collectProjectContextDocsLintSuggestions(state) {
355
+ const suggestions = [];
356
+ if (state.missingArchitectureDocs) {
357
+ suggestions.push({
358
+ code: PROJECT_LINT_CODES.ARCHITECTURE_DOC_MISSING,
359
+ message: 'Project context is missing architecture design documentation.',
360
+ fixHint: `Add required file: ${CORE_ARCHITECTURE_DOC_FILE}.`,
361
+ });
362
+ }
363
+ if (state.missingStyleDocs) {
364
+ suggestions.push({
365
+ code: PROJECT_LINT_CODES.STYLE_DOC_MISSING,
366
+ message: 'Project context is missing design style documentation.',
367
+ fixHint: `Add required file: ${CORE_STYLE_DOC_FILE}.`,
368
+ });
369
+ }
370
+ return suggestions;
371
+ }
116
372
  async function readActionableTaskCandidates(governanceDirs) {
117
373
  const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
118
374
  const tasksPath = path.join(governanceDir, '.projitive');
@@ -382,7 +638,7 @@ function collectTaskLintSuggestionItems(tasks) {
382
638
  export function collectTaskLintSuggestions(tasks) {
383
639
  return renderLintSuggestions(collectTaskLintSuggestionItems(tasks));
384
640
  }
385
- function collectSingleTaskLintSuggestions(task) {
641
+ function collectSingleTaskLintSuggestionItems(task) {
386
642
  const suggestions = [];
387
643
  if (task.status === 'IN_PROGRESS' && task.owner.trim().length === 0) {
388
644
  suggestions.push({
@@ -478,37 +734,21 @@ function collectSingleTaskLintSuggestions(task) {
478
734
  fixHint: 'Confidence must be between 0.0 and 1.0.',
479
735
  });
480
736
  }
481
- return renderLintSuggestions(suggestions);
737
+ return suggestions;
482
738
  }
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);
739
+ function collectSingleTaskLintSuggestions(task) {
740
+ return renderLintSuggestions(collectSingleTaskLintSuggestionItems(task));
741
+ }
742
+ async function collectDoneConformanceSuggestions(governanceDir, task) {
743
+ const researchBriefState = await inspectTaskResearchBrief(governanceDir, task);
744
+ const artifacts = await discoverGovernanceArtifacts(governanceDir);
745
+ const fileCandidates = candidateFilesFromArtifacts(artifacts);
746
+ const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
747
+ return [
748
+ ...collectSingleTaskLintSuggestionItems(task),
749
+ ...collectTaskResearchBriefLintSuggestions(researchBriefState),
750
+ ...collectProjectContextDocsLintSuggestions(projectContextDocsState),
751
+ ];
512
752
  }
513
753
  export function renderTasksMarkdown(tasks) {
514
754
  const sections = sortTasksNewestFirst(tasks).map((task) => {
@@ -610,7 +850,8 @@ export function validateTransition(from, to) {
610
850
  return allowed[from].has(to);
611
851
  }
612
852
  export function registerTaskTools(server) {
613
- server.registerTool('taskList', {
853
+ server.registerTool(...createGovernedTool({
854
+ name: 'taskList',
614
855
  title: 'Task List',
615
856
  description: 'List tasks for a known project and optionally filter by status',
616
857
  inputSchema: {
@@ -618,50 +859,48 @@ export function registerTaskTools(server) {
618
859
  status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
619
860
  limit: z.number().int().min(1).max(200).optional(),
620
861
  },
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', {
862
+ async execute({ projectPath, status, limit }) {
863
+ const governanceDir = await resolveGovernanceDir(projectPath);
864
+ const normalizedProjectPath = toProjectPath(governanceDir);
865
+ const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
866
+ const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
867
+ const filtered = tasks
868
+ .filter((task) => (status ? task.status === status : true))
869
+ .slice(0, limit ?? 100);
870
+ return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, filtered, status };
871
+ },
872
+ summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, filtered, status }) => [
873
+ `- projectPath: ${normalizedProjectPath}`,
874
+ `- governanceDir: ${governanceDir}`,
875
+ `- tasksView: ${tasksViewPath}`,
876
+ `- roadmapView: ${roadmapViewPath}`,
877
+ `- filter.status: ${status ?? '(none)'}`,
878
+ `- returned: ${filtered.length}`,
879
+ ],
880
+ evidence: ({ filtered }) => [
881
+ '- tasks:',
882
+ ...filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner || ''} | updatedAt=${task.updatedAt}`),
883
+ ],
884
+ guidance: () => ['- Pick one task ID and call `taskContext`.'],
885
+ suggestions: ({ filtered, status }) => {
886
+ const suggestions = collectTaskLintSuggestions(filtered);
887
+ if (status && filtered.length === 0) {
888
+ suggestions.push(...renderLintSuggestions([
889
+ {
890
+ code: TASK_LINT_CODES.FILTER_EMPTY,
891
+ message: `No tasks matched status=${status}.`,
892
+ fixHint: 'Confirm status values or update task states.',
893
+ },
894
+ ]));
895
+ }
896
+ return suggestions;
897
+ },
898
+ nextCall: ({ filtered, normalizedProjectPath }) => filtered[0]
899
+ ? `taskContext(projectPath="${normalizedProjectPath}", taskId="${filtered[0].id}")`
900
+ : undefined,
901
+ }));
902
+ server.registerTool(...createGovernedTool({
903
+ name: 'taskCreate',
665
904
  title: 'Task Create',
666
905
  description: 'Create a new task in governance store with stable TASK-<number> ID',
667
906
  inputSchema: {
@@ -686,322 +925,379 @@ export function registerTaskTools(server) {
686
925
  escalationPath: z.string().optional(),
687
926
  }).optional(),
688
927
  },
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', {
928
+ async execute({ projectPath, taskId, title, status, owner, summary, roadmapRefs, links, subState, blocker }) {
929
+ if (taskId && !isValidTaskId(taskId)) {
930
+ 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")`);
931
+ }
932
+ const governanceDir = await resolveGovernanceDir(projectPath);
933
+ const normalizedProjectPath = toProjectPath(governanceDir);
934
+ const { tasksPath, tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
935
+ const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
936
+ const finalTaskId = taskId ?? nextTaskId(tasks);
937
+ const duplicated = tasks.some((item) => item.id === finalTaskId);
938
+ if (duplicated) {
939
+ throw new ToolExecutionError(`Task already exists: ${finalTaskId}`, ['task IDs must be unique', 'use taskUpdate for existing tasks'], `taskUpdate(projectPath="${normalizedProjectPath}", taskId="${finalTaskId}", updates={...})`);
940
+ }
941
+ const createdTask = normalizeTask({
942
+ id: finalTaskId,
943
+ title,
944
+ status: status ?? 'TODO',
945
+ owner,
946
+ summary,
947
+ roadmapRefs,
948
+ links,
949
+ subState,
950
+ blocker,
951
+ updatedAt: nowIso(),
952
+ });
953
+ await upsertTaskInStore(tasksPath, createdTask);
954
+ await loadTasksDocumentWithOptions(governanceDir, true);
955
+ return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, createdTask };
956
+ },
957
+ summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, createdTask }) => [
958
+ `- projectPath: ${normalizedProjectPath}`,
959
+ `- governanceDir: ${governanceDir}`,
960
+ `- tasksView: ${tasksViewPath}`,
961
+ `- roadmapView: ${roadmapViewPath}`,
962
+ `- taskId: ${createdTask.id}`,
963
+ `- status: ${createdTask.status}`,
964
+ `- owner: ${createdTask.owner || '(none)'}`,
965
+ `- updatedAt: ${createdTask.updatedAt}`,
966
+ ],
967
+ evidence: ({ createdTask }) => [
968
+ '### Created Task',
969
+ `- ${createdTask.id} | ${createdTask.status} | ${createdTask.title}`,
970
+ `- summary: ${createdTask.summary || '(none)'}`,
971
+ `- roadmapRefs: ${createdTask.roadmapRefs.join(', ') || '(none)'}`,
972
+ `- links: ${createdTask.links.join(', ') || '(none)'}`,
973
+ ],
974
+ guidance: () => [
975
+ 'Task created in governance store successfully and tasks.md has been synced.',
976
+ 'Run taskContext to verify references and lint guidance.',
977
+ ],
978
+ suggestions: ({ createdTask }) => collectSingleTaskLintSuggestions(createdTask),
979
+ nextCall: ({ normalizedProjectPath, createdTask }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${createdTask.id}")`,
980
+ }));
981
+ server.registerTool(...createGovernedTool({
982
+ name: 'taskNext',
757
983
  title: 'Task Next',
758
984
  description: 'Start here to auto-select the highest-priority actionable task',
759
985
  inputSchema: {
760
986
  limit: z.number().int().min(1).max(20).optional(),
761
987
  },
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}.`,
988
+ async execute({ limit }) {
989
+ const roots = resolveScanRoots();
990
+ const depth = resolveScanDepth();
991
+ const projects = await discoverProjectsAcrossRoots(roots, depth);
992
+ const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
993
+ if (rankedCandidates.length === 0) {
994
+ const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
995
+ const tasksPath = path.join(governanceDir, '.projitive');
996
+ await ensureStore(tasksPath);
997
+ const stats = await loadTaskStatusStatsFromStore(tasksPath);
998
+ const roadmapIds = await readRoadmapIds(governanceDir);
999
+ return { governanceDir, roadmapIds, total: stats.total, todo: stats.todo, inProgress: stats.inProgress, blocked: stats.blocked, done: stats.done };
1000
+ }));
1001
+ const preferredProject = projectSnapshots[0];
1002
+ const preferredRoadmapRef = preferredProject?.roadmapIds[0] ?? 'ROADMAP-0001';
1003
+ const noTaskDiscoveryGuidance = await resolveNoTaskDiscoveryGuidance(preferredProject?.governanceDir);
1004
+ return { isEmpty: true, roots, depth, projects, projectSnapshots, preferredProject, preferredRoadmapRef, noTaskDiscoveryGuidance };
1005
+ }
1006
+ const selected = rankedCandidates[0];
1007
+ const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
1008
+ const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
1009
+ const fileCandidates = candidateFilesFromArtifacts(artifacts);
1010
+ const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
1011
+ const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
1012
+ const taskLocation = (await findTextReferences(selectedTaskDocument.markdownPath, selected.task.id))[0];
1013
+ const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
1014
+ const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
1015
+ const candidateLimit = limit ?? 5;
1016
+ return {
1017
+ isEmpty: false,
1018
+ roots, depth, projects,
1019
+ rankedCandidates, selected, selectedTaskDocument,
1020
+ relatedArtifacts, referenceLocations,
1021
+ suggestedReadOrder, projectContextDocsState, taskLocation, candidateLimit,
1022
+ };
1023
+ },
1024
+ summary: (data) => {
1025
+ if (data.isEmpty) {
1026
+ return [
1027
+ `- rootPaths: ${data.roots.join(', ')}`,
1028
+ `- rootCount: ${data.roots.length}`,
1029
+ `- maxDepth: ${data.depth}`,
1030
+ `- matchedProjects: ${data.projects.length}`,
1031
+ '- actionableTasks: 0',
1032
+ ];
1033
+ }
1034
+ return [
1035
+ `- rootPaths: ${data.roots.join(', ')}`,
1036
+ `- rootCount: ${data.roots.length}`,
1037
+ `- maxDepth: ${data.depth}`,
1038
+ `- matchedProjects: ${data.projects.length}`,
1039
+ `- actionableTasks: ${data.rankedCandidates.length}`,
1040
+ `- selectedProject: ${toProjectPath(data.selected.governanceDir)}`,
1041
+ `- selectedTaskId: ${data.selected.task.id}`,
1042
+ `- selectedTaskStatus: ${data.selected.task.status}`,
1043
+ ];
1044
+ },
1045
+ evidence: (data) => {
1046
+ if (data.isEmpty) {
1047
+ return [
1048
+ '### Project Snapshots',
1049
+ ...(data.projectSnapshots.length > 0
1050
+ ? 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)'}`)
1051
+ : ['- (none)']),
869
1052
  '',
870
- '### Related Artifacts',
871
- ...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
1053
+ '### Seed Task Template',
1054
+ ...renderTaskSeedTemplate(data.preferredRoadmapRef),
1055
+ ];
1056
+ }
1057
+ const { taskLocation, selectedTaskDocument, rankedCandidates, candidateLimit, relatedArtifacts, referenceLocations, suggestedReadOrder } = data;
1058
+ const taskLocationStr = taskLocation
1059
+ ? `${taskLocation.filePath}#L${taskLocation.line}`
1060
+ : selectedTaskDocument.markdownPath;
1061
+ return [
1062
+ '### Selected Task',
1063
+ `- id: ${data.selected.task.id}`,
1064
+ `- title: ${data.selected.task.title}`,
1065
+ `- owner: ${data.selected.task.owner || '(none)'}`,
1066
+ `- updatedAt: ${data.selected.task.updatedAt}`,
1067
+ `- roadmapRefs: ${data.selected.task.roadmapRefs.join(', ') || '(none)'}`,
1068
+ `- taskLocation: ${taskLocationStr}`,
1069
+ '',
1070
+ '### Top Candidates',
1071
+ ...rankedCandidates
1072
+ .slice(0, candidateLimit)
1073
+ .map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | projectPath=${toProjectPath(item.governanceDir)} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
1074
+ '',
1075
+ '### Selection Reason',
1076
+ '- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.',
1077
+ `- Selected candidate scores: projectScore=${data.selected.projectScore}, taskPriority=${data.selected.taskPriority}, taskUpdatedAtMs=${data.selected.taskUpdatedAtMs}.`,
1078
+ '',
1079
+ '### Related Artifacts',
1080
+ ...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
1081
+ '',
1082
+ '### Reference Locations',
1083
+ ...(referenceLocations.length > 0
1084
+ ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
1085
+ : ['- (none)']),
1086
+ '',
1087
+ '### Suggested Read Order',
1088
+ ...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
1089
+ ];
1090
+ },
1091
+ guidance: (data) => {
1092
+ if (data.isEmpty) {
1093
+ return [
1094
+ '- No TODO/IN_PROGRESS task is available.',
1095
+ '- Create 1-3 new TODO tasks using `taskCreate(...)` from active roadmap slices.',
1096
+ '- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.',
1097
+ '- If roadmap has active milestones, analyze milestone intent and split into 1-3 executable TODO tasks.',
872
1098
  '',
873
- '### Reference Locations',
874
- ...(referenceLocations.length > 0
875
- ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
876
- : ['- (none)']),
1099
+ '### No-Task Discovery Checklist',
1100
+ ...data.noTaskDiscoveryGuidance,
877
1101
  '',
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', {
1102
+ '- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.',
1103
+ '- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.',
1104
+ '- After creating tasks, rerun `taskNext` to re-rank actionable work.',
1105
+ ];
1106
+ }
1107
+ return [
1108
+ ...(!data.projectContextDocsState.ready
1109
+ ? [
1110
+ '- Project context docs are incomplete. Complete missing project architecture/style docs before deep implementation.',
1111
+ ...(data.projectContextDocsState.missingArchitectureDocs
1112
+ ? [`- Missing architecture design doc: create required file ${CORE_ARCHITECTURE_DOC_FILE}.`]
1113
+ : []),
1114
+ ...(data.projectContextDocsState.missingStyleDocs
1115
+ ? [`- Missing design style doc: create required file ${CORE_STYLE_DOC_FILE}.`]
1116
+ : []),
1117
+ ]
1118
+ : []),
1119
+ '- Start immediately with Suggested Read Order and execute the selected task.',
1120
+ '- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.',
1121
+ '- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.',
1122
+ ];
1123
+ },
1124
+ suggestions: (data) => {
1125
+ if (data.isEmpty) {
1126
+ return [
1127
+ '- No actionable tasks found. Verify task statuses and required fields in .projitive task table.',
1128
+ '- Ensure each new task has stable TASK-<number> ID and at least one roadmapRefs item.',
1129
+ ];
1130
+ }
1131
+ return [
1132
+ ...collectTaskLintSuggestions(data.selectedTaskDocument.tasks),
1133
+ ...renderLintSuggestions(collectProjectContextDocsLintSuggestions(data.projectContextDocsState)),
1134
+ ];
1135
+ },
1136
+ nextCall: (data) => {
1137
+ if (data.isEmpty) {
1138
+ return data.preferredProject
1139
+ ? `taskCreate(projectPath="${toProjectPath(data.preferredProject.governanceDir)}", title="Create first executable slice", roadmapRefs=["${data.preferredRoadmapRef}"], summary="Derived from active roadmap milestone")`
1140
+ : 'projectScan()';
1141
+ }
1142
+ return `taskContext(projectPath="${toProjectPath(data.selected.governanceDir)}", taskId="${data.selected.task.id}")`;
1143
+ },
1144
+ }));
1145
+ server.registerTool(...createGovernedTool({
1146
+ name: 'taskContext',
893
1147
  title: 'Task Context',
894
1148
  description: 'Get deep context, evidence links, and read order for one task',
895
1149
  inputSchema: {
896
1150
  projectPath: z.string(),
897
1151
  taskId: z.string(),
898
1152
  },
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}`);
947
- }
948
- if (typeof task.subState.confidence === 'number') {
949
- summaryLines.push(` - confidence: ${task.subState.confidence}`);
1153
+ async execute({ projectPath, taskId }) {
1154
+ if (!isValidTaskId(taskId)) {
1155
+ 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")`);
950
1156
  }
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}`);
1157
+ const governanceDir = await resolveGovernanceDir(projectPath);
1158
+ const normalizedProjectPath = toProjectPath(governanceDir);
1159
+ const { markdownPath, tasks } = await loadTasksDocument(governanceDir);
1160
+ const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
1161
+ const task = tasks.find((item) => item.id === taskId);
1162
+ if (!task) {
1163
+ throw new ToolExecutionError(`Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`);
962
1164
  }
963
- if (task.blocker.unblockCondition) {
964
- summaryLines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
1165
+ const researchBriefState = await inspectTaskResearchBrief(governanceDir, task);
1166
+ const contextReadingGuidance = await resolveTaskContextReadingGuidance(governanceDir);
1167
+ const taskLocation = (await findTextReferences(markdownPath, taskId))[0];
1168
+ const artifacts = await discoverGovernanceArtifacts(governanceDir);
1169
+ const fileCandidates = candidateFilesFromArtifacts(artifacts);
1170
+ const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
1171
+ const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
1172
+ const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
1173
+ const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
1174
+ return {
1175
+ normalizedProjectPath, governanceDir, markdownPath, roadmapViewPath,
1176
+ task, researchBriefState, contextReadingGuidance,
1177
+ taskLocation, referenceLocations, relatedArtifacts, suggestedReadOrder,
1178
+ projectContextDocsState,
1179
+ };
1180
+ },
1181
+ summary: ({ normalizedProjectPath, governanceDir, markdownPath, roadmapViewPath, task, researchBriefState, projectContextDocsState, taskLocation }) => {
1182
+ const lines = [
1183
+ `- projectPath: ${normalizedProjectPath}`,
1184
+ `- governanceDir: ${governanceDir}`,
1185
+ `- tasksView: ${markdownPath}`,
1186
+ `- roadmapView: ${roadmapViewPath}`,
1187
+ `- taskId: ${task.id}`,
1188
+ `- title: ${task.title}`,
1189
+ `- status: ${task.status}`,
1190
+ `- owner: ${task.owner}`,
1191
+ `- updatedAt: ${task.updatedAt}`,
1192
+ `- roadmapRefs: ${task.roadmapRefs.join(', ') || '(none)'}`,
1193
+ `- researchBriefPath: ${researchBriefState.relativePath}`,
1194
+ `- researchBriefStatus: ${researchBriefState.ready ? 'READY' : 'MISSING'}`,
1195
+ `- architectureDocsStatus: ${projectContextDocsState.missingArchitectureDocs ? 'MISSING' : 'READY'}`,
1196
+ `- styleDocsStatus: ${projectContextDocsState.missingStyleDocs ? 'MISSING' : 'READY'}`,
1197
+ `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
1198
+ ];
1199
+ if (task.subState && task.status === 'IN_PROGRESS') {
1200
+ lines.push('- subState:');
1201
+ if (task.subState.phase)
1202
+ lines.push(` - phase: ${task.subState.phase}`);
1203
+ if (typeof task.subState.confidence === 'number')
1204
+ lines.push(` - confidence: ${task.subState.confidence}`);
1205
+ if (task.subState.estimatedCompletion)
1206
+ lines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
965
1207
  }
966
- if (task.blocker.escalationPath) {
967
- summaryLines.push(` - escalationPath: ${task.blocker.escalationPath}`);
1208
+ if (task.blocker && task.status === 'BLOCKED') {
1209
+ lines.push('- blocker:');
1210
+ lines.push(` - type: ${task.blocker.type}`);
1211
+ lines.push(` - description: ${task.blocker.description}`);
1212
+ if (task.blocker.blockingEntity)
1213
+ lines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
1214
+ if (task.blocker.unblockCondition)
1215
+ lines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
1216
+ if (task.blocker.escalationPath)
1217
+ lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
968
1218
  }
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)']),
1219
+ return lines;
1220
+ },
1221
+ evidence: ({ task, researchBriefState, projectContextDocsState, relatedArtifacts, referenceLocations, suggestedReadOrder }) => [
1222
+ '### Pre-Execution Research Brief',
1223
+ `- path: ${researchBriefState.relativePath}`,
1224
+ `- absolutePath: ${researchBriefState.absolutePath}`,
1225
+ `- status: ${researchBriefState.ready ? 'READY' : 'MISSING'}`,
1226
+ ...(!researchBriefState.ready
1227
+ ? [
982
1228
  '',
983
- '### Suggested Read Order',
984
- ...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
1229
+ '### Required Research Brief Template',
1230
+ ...renderTaskResearchBriefTemplate(task).map((line) => `- ${line}`),
1231
+ ]
1232
+ : []),
1233
+ '',
1234
+ '### Project Context Docs Check',
1235
+ `- architecture docs: ${projectContextDocsState.architectureDocs.length > 0 ? 'found' : 'missing'}`,
1236
+ ...(projectContextDocsState.architectureDocs.length > 0
1237
+ ? projectContextDocsState.architectureDocs.map((item) => `- architecture: ${item}`)
1238
+ : [`- architecture: add required file ${CORE_ARCHITECTURE_DOC_FILE}.`]),
1239
+ `- design style docs: ${projectContextDocsState.styleDocs.length > 0 ? 'found' : 'missing'}`,
1240
+ ...(projectContextDocsState.styleDocs.length > 0
1241
+ ? projectContextDocsState.styleDocs.map((item) => `- style: ${item}`)
1242
+ : [`- style: add required file ${CORE_STYLE_DOC_FILE}.`]),
1243
+ '',
1244
+ '### Related Artifacts',
1245
+ ...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
1246
+ '',
1247
+ '### Reference Locations',
1248
+ ...(referenceLocations.length > 0
1249
+ ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
1250
+ : ['- (none)']),
1251
+ '',
1252
+ '### Suggested Read Order',
1253
+ ...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
1254
+ ],
1255
+ guidance: ({ researchBriefState, projectContextDocsState, contextReadingGuidance, task }) => [
1256
+ ...(!researchBriefState.ready
1257
+ ? [
1258
+ '- Pre-execution gate is NOT satisfied. Complete research brief first, then proceed with implementation.',
1259
+ `- Create or update ${researchBriefState.relativePath} with design guidelines + code architecture findings before code changes.`,
1260
+ '- Include exact file/line locations in the brief (for example path/to/file.ts#L120).',
1261
+ '- Re-run taskContext after writing the brief and confirm researchBriefStatus becomes READY.',
1262
+ ]
1263
+ : [
1264
+ '- Pre-execution gate satisfied. Read the research brief first, then continue implementation.',
1265
+ `- Must read ${researchBriefState.relativePath} before any task execution changes.`,
985
1266
  ]),
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.',
1267
+ ...(!projectContextDocsState.ready
1268
+ ? [
1269
+ '- Project context docs gate is NOT satisfied. Complete missing project architecture/style docs first.',
1270
+ ...(projectContextDocsState.missingArchitectureDocs
1271
+ ? [`- Missing architecture design doc. Add required file ${CORE_ARCHITECTURE_DOC_FILE} and include architecture boundaries and module responsibilities.`]
1272
+ : []),
1273
+ ...(projectContextDocsState.missingStyleDocs
1274
+ ? [`- Missing design style doc. Add required file ${CORE_STYLE_DOC_FILE} and include style language, tokens/themes, and UI consistency rules.`]
1275
+ : []),
1276
+ '- Re-run taskContext and confirm both architectureDocsStatus/styleDocsStatus are READY.',
1277
+ ]
1278
+ : [
1279
+ '- Project context docs gate satisfied. Architecture/style docs are available for execution alignment.',
996
1280
  ]),
997
- lintSection(lintSuggestions),
998
- nextCallSection(`taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${task.id}")`),
999
- ],
1000
- });
1001
- return asText(coreMarkdown);
1002
- });
1281
+ '- Read the files in Suggested Read Order.',
1282
+ '',
1283
+ '### Context Reading',
1284
+ ...contextReadingGuidance,
1285
+ '',
1286
+ '- Verify whether current status and evidence are consistent.',
1287
+ ...taskStatusGuidance(task),
1288
+ '- If updates are needed, use tool writes for governance store (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.',
1289
+ '- After editing, re-run `taskContext` to verify references and context consistency.',
1290
+ ],
1291
+ suggestions: ({ task, researchBriefState, projectContextDocsState }) => [
1292
+ ...collectSingleTaskLintSuggestions(task),
1293
+ ...renderLintSuggestions(collectTaskResearchBriefLintSuggestions(researchBriefState)),
1294
+ ...renderLintSuggestions(collectProjectContextDocsLintSuggestions(projectContextDocsState)),
1295
+ ],
1296
+ nextCall: ({ normalizedProjectPath, task }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${task.id}")`,
1297
+ }));
1003
1298
  // taskUpdate tool - Update task fields including subState and blocker (Spec v1.1.0)
1004
- server.registerTool('taskUpdate', {
1299
+ server.registerTool(...createGovernedTool({
1300
+ name: 'taskUpdate',
1005
1301
  title: 'Task Update',
1006
1302
  description: 'Update task fields including status, owner, summary, subState, and blocker metadata',
1007
1303
  inputSchema: {
@@ -1027,140 +1323,108 @@ export function registerTaskTools(server) {
1027
1323
  }).optional(),
1028
1324
  }),
1029
1325
  },
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;
1326
+ async execute({ projectPath, taskId, updates }) {
1327
+ if (!isValidTaskId(taskId)) {
1328
+ 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
1329
  }
1074
- else {
1075
- task.subState = {
1076
- ...(task.subState || {}),
1077
- ...updates.subState,
1078
- };
1330
+ const governanceDir = await resolveGovernanceDir(projectPath);
1331
+ const normalizedProjectPath = toProjectPath(governanceDir);
1332
+ const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
1333
+ const tasksViewPath = path.join(governanceDir, TASKS_MARKDOWN_FILE);
1334
+ const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
1335
+ const taskIndex = tasks.findIndex((item) => item.id === taskId);
1336
+ if (taskIndex === -1) {
1337
+ throw new ToolExecutionError(`Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`);
1079
1338
  }
1080
- }
1081
- // Handle blocker (Spec v1.1.0)
1082
- if (updates.blocker !== undefined) {
1083
- if (updates.blocker === null) {
1084
- delete task.blocker;
1339
+ const task = tasks[taskIndex];
1340
+ const originalStatus = task.status;
1341
+ const previewTask = normalizeTask({ ...task, ...updates, updatedAt: nowIso() });
1342
+ if (updates.status && !validateTransition(originalStatus, updates.status)) {
1343
+ 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
1344
  }
1086
- else {
1087
- task.blocker = updates.blocker;
1345
+ const updatedSubState = updates.subState === null ? undefined
1346
+ : updates.subState !== undefined ? { ...(task.subState ?? {}), ...updates.subState }
1347
+ : task.subState;
1348
+ const updatedBlocker = updates.blocker === null ? undefined
1349
+ : updates.blocker !== undefined ? updates.blocker
1350
+ : task.blocker;
1351
+ const normalizedTask = normalizeTask({
1352
+ ...task,
1353
+ ...(updates.status ? { status: updates.status } : {}),
1354
+ ...(updates.owner !== undefined ? { owner: updates.owner } : {}),
1355
+ ...(updates.summary !== undefined ? { summary: updates.summary } : {}),
1356
+ ...(updates.roadmapRefs ? { roadmapRefs: updates.roadmapRefs } : {}),
1357
+ ...(updates.links ? { links: updates.links } : {}),
1358
+ subState: updatedSubState,
1359
+ blocker: updatedBlocker,
1360
+ updatedAt: nowIso(),
1361
+ });
1362
+ await upsertTaskInStore(tasksPath, normalizedTask);
1363
+ await loadTasksDocumentWithOptions(governanceDir, true);
1364
+ return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, taskId, originalStatus, task: normalizedTask, previewTask, updates };
1365
+ },
1366
+ summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, taskId, originalStatus, task }) => {
1367
+ const lines = [
1368
+ `- projectPath: ${normalizedProjectPath}`,
1369
+ `- governanceDir: ${governanceDir}`,
1370
+ `- tasksView: ${tasksViewPath}`,
1371
+ `- roadmapView: ${roadmapViewPath}`,
1372
+ `- taskId: ${taskId}`,
1373
+ `- originalStatus: ${originalStatus}`,
1374
+ `- newStatus: ${task.status}`,
1375
+ `- updatedAt: ${task.updatedAt}`,
1376
+ ];
1377
+ if (task.subState) {
1378
+ lines.push('- subState:');
1379
+ if (task.subState.phase)
1380
+ lines.push(` - phase: ${task.subState.phase}`);
1381
+ if (typeof task.subState.confidence === 'number')
1382
+ lines.push(` - confidence: ${task.subState.confidence}`);
1383
+ if (task.subState.estimatedCompletion)
1384
+ lines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
1088
1385
  }
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
- });
1386
+ if (task.blocker) {
1387
+ lines.push('- blocker:');
1388
+ lines.push(` - type: ${task.blocker.type}`);
1389
+ lines.push(` - description: ${task.blocker.description}`);
1390
+ if (task.blocker.blockingEntity)
1391
+ lines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
1392
+ if (task.blocker.unblockCondition)
1393
+ lines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
1394
+ if (task.blocker.escalationPath)
1395
+ lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
1396
+ }
1397
+ return lines;
1398
+ },
1399
+ evidence: ({ task, originalStatus, updates }) => [
1400
+ '### Updated Task',
1401
+ `- ${task.id} | ${task.status} | ${task.title}`,
1402
+ `- owner: ${task.owner || '(none)'}`,
1403
+ `- summary: ${task.summary || '(none)'}`,
1404
+ '',
1405
+ '### Update Details',
1406
+ ...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
1407
+ ...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
1408
+ ...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
1409
+ ...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(', ')}`] : []),
1410
+ ...(updates.links ? [`- links: ${updates.links.join(', ')}`] : []),
1411
+ ...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
1412
+ ...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
1413
+ ],
1414
+ guidance: ({ updates, originalStatus }) => [
1415
+ 'Task updated successfully and tasks.md has been synced. Run `taskContext` to verify the changes.',
1416
+ ...(updates.status === 'IN_PROGRESS' && originalStatus === 'TODO'
1417
+ ? ['- Ensure pre-execution research brief exists before deep implementation.']
1418
+ : []),
1419
+ ...(updates.status === 'DONE'
1420
+ ? ['- Verify evidence links are attached and reflect completed work.']
1421
+ : []),
1422
+ '.projitive governance store is source of truth; tasks.md is a generated view and may be overwritten.',
1423
+ ],
1424
+ suggestions: async ({ previewTask, governanceDir }) => [
1425
+ ...collectSingleTaskLintSuggestions(previewTask),
1426
+ ...renderLintSuggestions(await collectDoneConformanceSuggestions(governanceDir, previewTask)),
1427
+ ],
1428
+ nextCall: ({ normalizedProjectPath, taskId }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${taskId}")`,
1429
+ }));
1166
1430
  }