@projitive/mcp 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/output/package.json +8 -2
  2. package/output/source/common/artifacts.js +1 -1
  3. package/output/source/common/artifacts.test.js +11 -11
  4. package/output/source/common/errors.js +19 -19
  5. package/output/source/common/errors.test.js +59 -0
  6. package/output/source/common/files.js +30 -19
  7. package/output/source/common/files.test.js +14 -14
  8. package/output/source/common/index.js +11 -10
  9. package/output/source/common/linter.js +29 -27
  10. package/output/source/common/linter.test.js +9 -9
  11. package/output/source/common/markdown.js +3 -3
  12. package/output/source/common/markdown.test.js +15 -15
  13. package/output/source/common/response.js +91 -107
  14. package/output/source/common/response.test.js +30 -30
  15. package/output/source/common/store.js +40 -40
  16. package/output/source/common/store.test.js +72 -72
  17. package/output/source/common/tool.js +43 -0
  18. package/output/source/common/types.js +3 -3
  19. package/output/source/common/utils.js +8 -8
  20. package/output/source/common/utils.test.js +48 -0
  21. package/output/source/index.js +16 -16
  22. package/output/source/index.runtime.test.js +57 -0
  23. package/output/source/index.test.js +64 -64
  24. package/output/source/prompts/index.js +3 -3
  25. package/output/source/prompts/index.test.js +23 -0
  26. package/output/source/prompts/quickStart.js +96 -96
  27. package/output/source/prompts/quickStart.test.js +24 -0
  28. package/output/source/prompts/taskDiscovery.js +184 -184
  29. package/output/source/prompts/taskDiscovery.test.js +24 -0
  30. package/output/source/prompts/taskExecution.js +164 -148
  31. package/output/source/prompts/taskExecution.test.js +27 -0
  32. package/output/source/resources/designs.js +26 -26
  33. package/output/source/resources/designs.resources.test.js +52 -0
  34. package/output/source/resources/designs.test.js +88 -88
  35. package/output/source/resources/governance.js +19 -19
  36. package/output/source/resources/governance.test.js +35 -0
  37. package/output/source/resources/index.js +2 -2
  38. package/output/source/resources/index.test.js +18 -0
  39. package/output/source/resources/readme.js +7 -7
  40. package/output/source/resources/readme.test.js +113 -113
  41. package/output/source/resources/reports.js +10 -10
  42. package/output/source/resources/reports.test.js +83 -83
  43. package/output/source/tools/index.js +3 -3
  44. package/output/source/tools/index.test.js +23 -0
  45. package/output/source/tools/project.js +330 -377
  46. package/output/source/tools/project.test.js +308 -175
  47. package/output/source/tools/roadmap.js +236 -255
  48. package/output/source/tools/roadmap.test.js +241 -46
  49. package/output/source/tools/task.js +770 -652
  50. package/output/source/tools/task.test.js +433 -105
  51. package/output/source/types.js +28 -22
  52. package/package.json +8 -2
@@ -1,61 +1,62 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
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";
7
- import { resolveGovernanceDir, resolveScanDepth, resolveScanRoots, discoverProjectsAcrossRoots, toProjectPath } from "./project.js";
8
- import { isValidRoadmapId } from "./roadmap.js";
9
- import { SUB_STATE_PHASES, BLOCKER_TYPES } from "../types.js";
10
- export const ALLOWED_STATUS = ["TODO", "IN_PROGRESS", "BLOCKED", "DONE"];
11
- export const TASK_ID_REGEX = /^TASK-\d{4}$/;
12
- export const TASKS_MARKDOWN_FILE = "tasks.md";
13
- function appendLintSuggestions(target, suggestions) {
14
- target.push(...renderLintSuggestions(suggestions));
15
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { z } from 'zod';
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';
5
+ import { resolveGovernanceDir, resolveScanDepth, resolveScanRoots, discoverProjectsAcrossRoots, toProjectPath } from './project.js';
6
+ import { isValidRoadmapId } from './roadmap.js';
7
+ import { SUB_STATE_PHASES, BLOCKER_TYPES } from '../types.js';
8
+ export const ALLOWED_STATUS = ['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE'];
9
+ export const TASK_ID_REGEX = /^TASK-(\d+)$/;
10
+ export const TASKS_MARKDOWN_FILE = 'tasks.md';
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
- if (task.status === "TODO") {
17
+ if (task.status === 'TODO') {
18
18
  return [
19
- "- This task is TODO: confirm scope and set execution plan before edits.",
20
- "- Move to IN_PROGRESS only after owner and initial evidence are ready.",
19
+ '- This task is TODO: confirm scope and set execution plan before edits.',
20
+ '- Move to IN_PROGRESS only after owner and initial evidence are ready.',
21
21
  ];
22
22
  }
23
- if (task.status === "IN_PROGRESS") {
23
+ if (task.status === 'IN_PROGRESS') {
24
24
  return [
25
- "- This task is IN_PROGRESS: prioritize finishing with report/design evidence updates.",
26
- "- Verify references stay consistent before marking DONE.",
25
+ '- This task is IN_PROGRESS: prioritize finishing with report/design evidence updates.',
26
+ '- Verify references stay consistent before marking DONE.',
27
27
  ];
28
28
  }
29
- if (task.status === "BLOCKED") {
29
+ if (task.status === 'BLOCKED') {
30
30
  return [
31
- "- This task is BLOCKED: identify blocker and required unblock condition first.",
32
- "- Reopen only after blocker evidence is documented.",
31
+ '- This task is BLOCKED: identify blocker and required unblock condition first.',
32
+ '- Reopen only after blocker evidence is documented.',
33
33
  ];
34
34
  }
35
35
  return [
36
- "- This task is DONE: only reopen when new requirement changes scope.",
37
- "- Keep report evidence immutable unless correction is required.",
36
+ '- This task is DONE: only reopen when new requirement changes scope.',
37
+ '- Keep report evidence immutable unless correction is required.',
38
38
  ];
39
39
  }
40
40
  const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
41
- "- 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.",
46
- "- Create TODO tasks only when evidence is clear: each new task must produce at least one report/designs/readme artifact update.",
47
- "- Skip duplicate scope: do not create tasks that overlap existing TODO/IN_PROGRESS/BLOCKED task intent.",
48
- "- Use quality gates for discovery candidates: user value, delivery risk reduction, or measurable throughput improvement.",
49
- "- Keep each discovery round small (1-3 tasks), then rerun taskNext immediately for re-ranking and execution.",
41
+ '- 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.',
46
+ '- Create TODO tasks only when evidence is clear: each new task must produce at least one report/designs/readme artifact update.',
47
+ '- Skip duplicate scope: do not create tasks that overlap existing TODO/IN_PROGRESS/BLOCKED task intent.',
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.',
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 = [
52
- "- Read governance workspace overview first (README.md / projitive://governance/workspace).",
53
- "- Read roadmap and active milestones (roadmap.md / projitive://governance/roadmap).",
54
- "- Read task view and related task cards (tasks.md / projitive://governance/tasks).",
55
- "- Read design specs and technical decisions under designs/ (architecture, API contracts, constraints).",
56
- "- Read reports/ for latest execution evidence, regressions, and unresolved risks.",
57
- "- Read process guides under templates/docs/project guidelines to align with local governance rules.",
58
- "- If available, read docs/ architecture or migration guides before major structural changes.",
53
+ '- Read governance workspace overview first (README.md / projitive://governance/workspace).',
54
+ '- Read roadmap and active milestones (roadmap.md / projitive://governance/roadmap).',
55
+ '- Read task view and related task cards (tasks.md / projitive://governance/tasks).',
56
+ '- Read design specs and technical decisions under designs/ (architecture, API contracts, constraints).',
57
+ '- Read reports/ for latest execution evidence, regressions, and unresolved risks.',
58
+ '- Read process guides under templates/docs/project guidelines to align with local governance rules.',
59
+ '- If available, read docs/ architecture or migration guides before major structural changes.',
59
60
  ];
60
61
  export async function resolveNoTaskDiscoveryGuidance(governanceDir) {
61
62
  void governanceDir;
@@ -66,7 +67,7 @@ export async function resolveTaskContextReadingGuidance(governanceDir) {
66
67
  return DEFAULT_TASK_CONTEXT_READING_GUIDANCE;
67
68
  }
68
69
  async function readRoadmapIds(governanceDir) {
69
- const dbPath = path.join(governanceDir, ".projitive");
70
+ const dbPath = path.join(governanceDir, '.projitive');
70
71
  try {
71
72
  await ensureStore(dbPath);
72
73
  const milestones = await loadRoadmapsFromStore(dbPath);
@@ -79,16 +80,16 @@ async function readRoadmapIds(governanceDir) {
79
80
  }
80
81
  export function renderTaskSeedTemplate(roadmapRef) {
81
82
  return [
82
- "```markdown",
83
- "## TASK-0001 | TODO | Define initial executable objective",
84
- "- owner: ai-copilot",
85
- "- summary: Convert one roadmap milestone or report gap into an actionable task.",
86
- "- updatedAt: 2026-01-01T00:00:00.000Z",
83
+ '```markdown',
84
+ '## TASK-0001 | TODO | Define initial executable objective',
85
+ '- owner: ai-copilot',
86
+ '- summary: Convert one roadmap milestone or report gap into an actionable task.',
87
+ '- updatedAt: 2026-01-01T00:00:00.000Z',
87
88
  `- roadmapRefs: ${roadmapRef}`,
88
- "- links:",
89
- " - README.md",
90
- " - .projitive/roadmap.md",
91
- "```",
89
+ '- links:',
90
+ ' - README.md',
91
+ ' - .projitive/roadmap.md',
92
+ '```',
92
93
  ];
93
94
  }
94
95
  function isHttpUrl(value) {
@@ -96,9 +97,9 @@ function isHttpUrl(value) {
96
97
  }
97
98
  function isProjectRootRelativePath(value) {
98
99
  return value.length > 0
99
- && !value.startsWith("/")
100
- && !value.startsWith("./")
101
- && !value.startsWith("../")
100
+ && !value.startsWith('/')
101
+ && !value.startsWith('./')
102
+ && !value.startsWith('../')
102
103
  && !/^[A-Za-z]:\//.test(value);
103
104
  }
104
105
  function normalizeTaskLink(link) {
@@ -106,16 +107,97 @@ function normalizeTaskLink(link) {
106
107
  if (trimmed.length === 0 || isHttpUrl(trimmed)) {
107
108
  return trimmed;
108
109
  }
109
- const slashNormalized = trimmed.replace(/\\/g, "/");
110
- const withoutDotPrefix = slashNormalized.replace(/^\.\//, "");
111
- return withoutDotPrefix.replace(/^\/+/, "");
110
+ const slashNormalized = trimmed.replace(/\\/g, '/');
111
+ const withoutDotPrefix = slashNormalized.replace(/^\.\//, '');
112
+ return withoutDotPrefix.replace(/^\/+/, '');
112
113
  }
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
- const tasksPath = path.join(governanceDir, ".projitive");
200
+ const tasksPath = path.join(governanceDir, '.projitive');
119
201
  await ensureStore(tasksPath);
120
202
  const [stats, actionableTasks] = await Promise.all([
121
203
  loadTaskStatusStatsFromStore(tasksPath),
@@ -125,11 +207,11 @@ async function readActionableTaskCandidates(governanceDirs) {
125
207
  governanceDir,
126
208
  tasks: actionableTasks,
127
209
  projectScore: stats.inProgress * 2 + stats.todo,
128
- projectLatestUpdatedAt: stats.latestUpdatedAt || "(unknown)",
210
+ projectLatestUpdatedAt: stats.latestUpdatedAt || '(unknown)',
129
211
  };
130
212
  }));
131
213
  return snapshots.flatMap((item) => item.tasks
132
- .filter((task) => task.status === "IN_PROGRESS" || task.status === "TODO")
214
+ .filter((task) => task.status === 'IN_PROGRESS' || task.status === 'TODO')
133
215
  .map((task) => ({
134
216
  governanceDir: item.governanceDir,
135
217
  task,
@@ -143,13 +225,13 @@ export function nowIso() {
143
225
  return new Date().toISOString();
144
226
  }
145
227
  export function isValidTaskId(id) {
146
- return TASK_ID_REGEX.test(id);
228
+ return toTaskIdNumericSuffix(id) > 0;
147
229
  }
148
230
  export function taskPriority(status) {
149
- if (status === "IN_PROGRESS") {
231
+ if (status === 'IN_PROGRESS') {
150
232
  return 2;
151
233
  }
152
- if (status === "TODO") {
234
+ if (status === 'TODO') {
153
235
  return 1;
154
236
  }
155
237
  return 0;
@@ -159,11 +241,21 @@ export function toTaskUpdatedAtMs(updatedAt) {
159
241
  return Number.isFinite(timestamp) ? timestamp : 0;
160
242
  }
161
243
  function toTaskIdNumericSuffix(taskId) {
162
- const match = taskId.match(/^(?:TASK-)(\d{4})$/);
244
+ const match = taskId.match(TASK_ID_REGEX);
163
245
  if (!match) {
164
246
  return -1;
165
247
  }
166
- return Number.parseInt(match[1], 10);
248
+ const suffix = Number.parseInt(match[1], 10);
249
+ return Number.isFinite(suffix) ? suffix : -1;
250
+ }
251
+ function nextTaskId(tasks) {
252
+ const maxSuffix = tasks
253
+ .map((item) => toTaskIdNumericSuffix(item.id))
254
+ .filter((value) => value > 0)
255
+ .reduce((max, value) => Math.max(max, value), 0);
256
+ const next = maxSuffix + 1;
257
+ const minWidth = Math.max(4, String(next).length);
258
+ return `TASK-${String(next).padStart(minWidth, '0')}`;
167
259
  }
168
260
  export function sortTasksNewestFirst(tasks) {
169
261
  return [...tasks].sort((a, b) => {
@@ -183,13 +275,13 @@ function normalizeAndSortTasks(tasks) {
183
275
  }
184
276
  function resolveTaskArtifactPaths(governanceDir) {
185
277
  return {
186
- tasksPath: path.join(governanceDir, ".projitive"),
278
+ tasksPath: path.join(governanceDir, '.projitive'),
187
279
  markdownPath: path.join(governanceDir, TASKS_MARKDOWN_FILE),
188
280
  };
189
281
  }
190
282
  async function syncTasksMarkdownView(tasksPath, markdownPath, markdown, force = false) {
191
- const sourceVersion = await getStoreVersion(tasksPath, "tasks");
192
- const viewState = await getMarkdownViewState(tasksPath, "tasks_markdown");
283
+ const sourceVersion = await getStoreVersion(tasksPath, 'tasks');
284
+ const viewState = await getMarkdownViewState(tasksPath, 'tasks_markdown');
193
285
  const markdownExists = await fs.access(markdownPath).then(() => true).catch(() => false);
194
286
  const shouldWrite = force
195
287
  || !markdownExists
@@ -198,8 +290,8 @@ async function syncTasksMarkdownView(tasksPath, markdownPath, markdown, force =
198
290
  if (!shouldWrite) {
199
291
  return;
200
292
  }
201
- await fs.writeFile(markdownPath, markdown, "utf-8");
202
- await markMarkdownViewBuilt(tasksPath, "tasks_markdown", sourceVersion);
293
+ await fs.writeFile(markdownPath, markdown, 'utf-8');
294
+ await markMarkdownViewBuilt(tasksPath, 'tasks_markdown', sourceVersion);
203
295
  }
204
296
  export function rankActionableTaskCandidates(candidates) {
205
297
  return [...candidates].sort((a, b) => {
@@ -225,9 +317,9 @@ export function normalizeTask(task) {
225
317
  const normalized = {
226
318
  id: String(task.id),
227
319
  title: String(task.title),
228
- status: ALLOWED_STATUS.includes(task.status) ? task.status : "TODO",
229
- owner: task.owner ? String(task.owner) : "",
230
- summary: task.summary ? String(task.summary) : "",
320
+ status: ALLOWED_STATUS.includes(task.status) ? task.status : 'TODO',
321
+ owner: task.owner ? String(task.owner) : '',
322
+ summary: task.summary ? String(task.summary) : '',
231
323
  updatedAt: task.updatedAt ? String(task.updatedAt) : nowIso(),
232
324
  links: Array.isArray(task.links)
233
325
  ? Array.from(new Set(task.links
@@ -258,32 +350,32 @@ function collectTaskLintSuggestionItems(tasks) {
258
350
  if (duplicateIds.length > 0) {
259
351
  suggestions.push({
260
352
  code: TASK_LINT_CODES.DUPLICATE_ID,
261
- message: `Duplicate task IDs detected: ${duplicateIds.join(", ")}.`,
262
- fixHint: "Keep task IDs unique in marker block.",
353
+ message: `Duplicate task IDs detected: ${duplicateIds.join(', ')}.`,
354
+ fixHint: 'Keep task IDs unique in marker block.',
263
355
  });
264
356
  }
265
- const inProgressWithoutOwner = tasks.filter((task) => task.status === "IN_PROGRESS" && task.owner.trim().length === 0);
357
+ const inProgressWithoutOwner = tasks.filter((task) => task.status === 'IN_PROGRESS' && task.owner.trim().length === 0);
266
358
  if (inProgressWithoutOwner.length > 0) {
267
359
  suggestions.push({
268
360
  code: TASK_LINT_CODES.IN_PROGRESS_OWNER_EMPTY,
269
361
  message: `${inProgressWithoutOwner.length} IN_PROGRESS task(s) have empty owner.`,
270
- fixHint: "Set owner before continuing execution.",
362
+ fixHint: 'Set owner before continuing execution.',
271
363
  });
272
364
  }
273
- const doneWithoutLinks = tasks.filter((task) => task.status === "DONE" && task.links.length === 0);
365
+ const doneWithoutLinks = tasks.filter((task) => task.status === 'DONE' && task.links.length === 0);
274
366
  if (doneWithoutLinks.length > 0) {
275
367
  suggestions.push({
276
368
  code: TASK_LINT_CODES.DONE_LINKS_MISSING,
277
369
  message: `${doneWithoutLinks.length} DONE task(s) have no links evidence.`,
278
- fixHint: "Add at least one evidence link before keeping DONE.",
370
+ fixHint: 'Add at least one evidence link before keeping DONE.',
279
371
  });
280
372
  }
281
- const blockedWithoutReason = tasks.filter((task) => task.status === "BLOCKED" && task.summary.trim().length === 0);
373
+ const blockedWithoutReason = tasks.filter((task) => task.status === 'BLOCKED' && task.summary.trim().length === 0);
282
374
  if (blockedWithoutReason.length > 0) {
283
375
  suggestions.push({
284
376
  code: TASK_LINT_CODES.BLOCKED_SUMMARY_EMPTY,
285
377
  message: `${blockedWithoutReason.length} BLOCKED task(s) have empty summary.`,
286
- fixHint: "Add blocker reason and unblock condition.",
378
+ fixHint: 'Add blocker reason and unblock condition.',
287
379
  });
288
380
  }
289
381
  const invalidUpdatedAt = tasks.filter((task) => !Number.isFinite(new Date(task.updatedAt).getTime()));
@@ -291,7 +383,7 @@ function collectTaskLintSuggestionItems(tasks) {
291
383
  suggestions.push({
292
384
  code: TASK_LINT_CODES.UPDATED_AT_INVALID,
293
385
  message: `${invalidUpdatedAt.length} task(s) have invalid updatedAt format.`,
294
- fixHint: "Use ISO8601 UTC timestamp.",
386
+ fixHint: 'Use ISO8601 UTC timestamp.',
295
387
  });
296
388
  }
297
389
  const missingRoadmapRefs = tasks.filter((task) => task.roadmapRefs.length === 0);
@@ -299,7 +391,7 @@ function collectTaskLintSuggestionItems(tasks) {
299
391
  suggestions.push({
300
392
  code: TASK_LINT_CODES.ROADMAP_REFS_EMPTY,
301
393
  message: `${missingRoadmapRefs.length} task(s) have empty roadmapRefs.`,
302
- fixHint: "Bind at least one ROADMAP-xxxx when applicable.",
394
+ fixHint: 'Bind at least one ROADMAP-xxxx when applicable.',
303
395
  });
304
396
  }
305
397
  const invalidLinkPathFormat = tasks.filter((task) => task.links.some((link) => {
@@ -310,18 +402,18 @@ function collectTaskLintSuggestionItems(tasks) {
310
402
  suggestions.push({
311
403
  code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
312
404
  message: `${invalidLinkPathFormat.length} task(s) contain invalid links path format.`,
313
- fixHint: "Use project-root-relative paths without leading slash (for example reports/task-0001.md) or http(s) URL.",
405
+ fixHint: 'Use project-root-relative paths without leading slash (for example reports/task-0001.md) or http(s) URL.',
314
406
  });
315
407
  }
316
408
  // ============================================================================
317
409
  // Spec v1.1.0 - Blocker Categorization Validation
318
410
  // ============================================================================
319
- const blockedWithoutBlocker = tasks.filter((task) => task.status === "BLOCKED" && !task.blocker);
411
+ const blockedWithoutBlocker = tasks.filter((task) => task.status === 'BLOCKED' && !task.blocker);
320
412
  if (blockedWithoutBlocker.length > 0) {
321
413
  suggestions.push({
322
414
  code: TASK_LINT_CODES.BLOCKED_WITHOUT_BLOCKER,
323
415
  message: `${blockedWithoutBlocker.length} BLOCKED task(s) have no blocker metadata.`,
324
- fixHint: "Add structured blocker metadata with type and description.",
416
+ fixHint: 'Add structured blocker metadata with type and description.',
325
417
  });
326
418
  }
327
419
  const blockerTypeInvalid = tasks.filter((task) => task.blocker && !BLOCKER_TYPES.includes(task.blocker.type));
@@ -329,7 +421,7 @@ function collectTaskLintSuggestionItems(tasks) {
329
421
  suggestions.push({
330
422
  code: TASK_LINT_CODES.BLOCKER_TYPE_INVALID,
331
423
  message: `${blockerTypeInvalid.length} task(s) have invalid blocker type.`,
332
- fixHint: `Use one of: ${BLOCKER_TYPES.join(", ")}.`,
424
+ fixHint: `Use one of: ${BLOCKER_TYPES.join(', ')}.`,
333
425
  });
334
426
  }
335
427
  const blockerDescriptionEmpty = tasks.filter((task) => task.blocker && !task.blocker.description?.trim());
@@ -337,18 +429,18 @@ function collectTaskLintSuggestionItems(tasks) {
337
429
  suggestions.push({
338
430
  code: TASK_LINT_CODES.BLOCKER_DESCRIPTION_EMPTY,
339
431
  message: `${blockerDescriptionEmpty.length} task(s) have empty blocker description.`,
340
- fixHint: "Provide a clear description of why the task is blocked.",
432
+ fixHint: 'Provide a clear description of why the task is blocked.',
341
433
  });
342
434
  }
343
435
  // ============================================================================
344
436
  // Spec v1.1.0 - Sub-state Metadata Validation (Optional but Recommended)
345
437
  // ============================================================================
346
- const inProgressWithoutSubState = tasks.filter((task) => task.status === "IN_PROGRESS" && !task.subState);
438
+ const inProgressWithoutSubState = tasks.filter((task) => task.status === 'IN_PROGRESS' && !task.subState);
347
439
  if (inProgressWithoutSubState.length > 0) {
348
440
  suggestions.push({
349
441
  code: TASK_LINT_CODES.IN_PROGRESS_WITHOUT_SUBSTATE,
350
442
  message: `${inProgressWithoutSubState.length} IN_PROGRESS task(s) have no subState metadata.`,
351
- fixHint: "Add optional subState metadata for better progress tracking.",
443
+ fixHint: 'Add optional subState metadata for better progress tracking.',
352
444
  });
353
445
  }
354
446
  const subStatePhaseInvalid = tasks.filter((task) => task.subState?.phase && !SUB_STATE_PHASES.includes(task.subState.phase));
@@ -356,15 +448,15 @@ function collectTaskLintSuggestionItems(tasks) {
356
448
  suggestions.push({
357
449
  code: TASK_LINT_CODES.SUBSTATE_PHASE_INVALID,
358
450
  message: `${subStatePhaseInvalid.length} task(s) have invalid subState phase.`,
359
- fixHint: `Use one of: ${SUB_STATE_PHASES.join(", ")}.`,
451
+ fixHint: `Use one of: ${SUB_STATE_PHASES.join(', ')}.`,
360
452
  });
361
453
  }
362
- const subStateConfidenceInvalid = tasks.filter((task) => typeof task.subState?.confidence === "number" && (task.subState.confidence < 0 || task.subState.confidence > 1));
454
+ const subStateConfidenceInvalid = tasks.filter((task) => typeof task.subState?.confidence === 'number' && (task.subState.confidence < 0 || task.subState.confidence > 1));
363
455
  if (subStateConfidenceInvalid.length > 0) {
364
456
  suggestions.push({
365
457
  code: TASK_LINT_CODES.SUBSTATE_CONFIDENCE_INVALID,
366
458
  message: `${subStateConfidenceInvalid.length} task(s) have invalid confidence score.`,
367
- fixHint: "Confidence must be between 0.0 and 1.0.",
459
+ fixHint: 'Confidence must be between 0.0 and 1.0.',
368
460
  });
369
461
  }
370
462
  return suggestions;
@@ -372,20 +464,20 @@ function collectTaskLintSuggestionItems(tasks) {
372
464
  export function collectTaskLintSuggestions(tasks) {
373
465
  return renderLintSuggestions(collectTaskLintSuggestionItems(tasks));
374
466
  }
375
- function collectSingleTaskLintSuggestions(task) {
467
+ function collectSingleTaskLintSuggestionItems(task) {
376
468
  const suggestions = [];
377
- if (task.status === "IN_PROGRESS" && task.owner.trim().length === 0) {
469
+ if (task.status === 'IN_PROGRESS' && task.owner.trim().length === 0) {
378
470
  suggestions.push({
379
471
  code: TASK_LINT_CODES.IN_PROGRESS_OWNER_EMPTY,
380
- message: "Current task is IN_PROGRESS but owner is empty.",
381
- fixHint: "Set owner before continuing execution.",
472
+ message: 'Current task is IN_PROGRESS but owner is empty.',
473
+ fixHint: 'Set owner before continuing execution.',
382
474
  });
383
475
  }
384
- if (task.status === "DONE" && task.links.length === 0) {
476
+ if (task.status === 'DONE' && task.links.length === 0) {
385
477
  suggestions.push({
386
478
  code: TASK_LINT_CODES.DONE_LINKS_MISSING,
387
- message: "Current task is DONE but has no links evidence.",
388
- fixHint: "Add at least one evidence link.",
479
+ message: 'Current task is DONE but has no links evidence.',
480
+ fixHint: 'Add at least one evidence link.',
389
481
  });
390
482
  }
391
483
  const invalidLinkPathFormat = task.links.some((link) => {
@@ -395,132 +487,116 @@ function collectSingleTaskLintSuggestions(task) {
395
487
  if (invalidLinkPathFormat) {
396
488
  suggestions.push({
397
489
  code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
398
- message: "Current task has invalid links path format.",
399
- fixHint: "Use project-root-relative paths without leading slash (for example reports/task-0001.md) or http(s) URL.",
490
+ message: 'Current task has invalid links path format.',
491
+ fixHint: 'Use project-root-relative paths without leading slash (for example reports/task-0001.md) or http(s) URL.',
400
492
  });
401
493
  }
402
- if (task.status === "BLOCKED" && task.summary.trim().length === 0) {
494
+ if (task.status === 'BLOCKED' && task.summary.trim().length === 0) {
403
495
  suggestions.push({
404
496
  code: TASK_LINT_CODES.BLOCKED_SUMMARY_EMPTY,
405
- message: "Current task is BLOCKED but summary is empty.",
406
- fixHint: "Add blocker reason and unblock condition.",
497
+ message: 'Current task is BLOCKED but summary is empty.',
498
+ fixHint: 'Add blocker reason and unblock condition.',
407
499
  });
408
500
  }
409
501
  if (!Number.isFinite(new Date(task.updatedAt).getTime())) {
410
502
  suggestions.push({
411
503
  code: TASK_LINT_CODES.UPDATED_AT_INVALID,
412
- message: "Current task updatedAt is invalid.",
413
- fixHint: "Use ISO8601 UTC timestamp.",
504
+ message: 'Current task updatedAt is invalid.',
505
+ fixHint: 'Use ISO8601 UTC timestamp.',
414
506
  });
415
507
  }
416
508
  if (task.roadmapRefs.length === 0) {
417
509
  suggestions.push({
418
510
  code: TASK_LINT_CODES.ROADMAP_REFS_EMPTY,
419
- message: "Current task has empty roadmapRefs.",
420
- fixHint: "Bind ROADMAP-xxxx where applicable.",
511
+ message: 'Current task has empty roadmapRefs.',
512
+ fixHint: 'Bind ROADMAP-xxxx where applicable.',
421
513
  });
422
514
  }
423
515
  // ============================================================================
424
516
  // Spec v1.1.0 - Blocker Categorization Validation (Single Task)
425
517
  // ============================================================================
426
- if (task.status === "BLOCKED" && !task.blocker) {
518
+ if (task.status === 'BLOCKED' && !task.blocker) {
427
519
  suggestions.push({
428
520
  code: TASK_LINT_CODES.BLOCKED_WITHOUT_BLOCKER,
429
- message: "Current task is BLOCKED but has no blocker metadata.",
430
- fixHint: "Add structured blocker metadata with type and description.",
521
+ message: 'Current task is BLOCKED but has no blocker metadata.',
522
+ fixHint: 'Add structured blocker metadata with type and description.',
431
523
  });
432
524
  }
433
525
  if (task.blocker && !BLOCKER_TYPES.includes(task.blocker.type)) {
434
526
  suggestions.push({
435
527
  code: TASK_LINT_CODES.BLOCKER_TYPE_INVALID,
436
528
  message: `Current task has invalid blocker type: ${task.blocker.type}.`,
437
- fixHint: `Use one of: ${BLOCKER_TYPES.join(", ")}.`,
529
+ fixHint: `Use one of: ${BLOCKER_TYPES.join(', ')}.`,
438
530
  });
439
531
  }
440
532
  if (task.blocker && !task.blocker.description?.trim()) {
441
533
  suggestions.push({
442
534
  code: TASK_LINT_CODES.BLOCKER_DESCRIPTION_EMPTY,
443
- message: "Current task has empty blocker description.",
444
- fixHint: "Provide a clear description of why the task is blocked.",
535
+ message: 'Current task has empty blocker description.',
536
+ fixHint: 'Provide a clear description of why the task is blocked.',
445
537
  });
446
538
  }
447
539
  // ============================================================================
448
540
  // Spec v1.1.0 - Sub-state Metadata Validation (Single Task, Optional)
449
541
  // ============================================================================
450
- if (task.status === "IN_PROGRESS" && !task.subState) {
542
+ if (task.status === 'IN_PROGRESS' && !task.subState) {
451
543
  suggestions.push({
452
544
  code: TASK_LINT_CODES.IN_PROGRESS_WITHOUT_SUBSTATE,
453
- message: "Current task is IN_PROGRESS but has no subState metadata.",
454
- fixHint: "Add optional subState metadata for better progress tracking.",
545
+ message: 'Current task is IN_PROGRESS but has no subState metadata.',
546
+ fixHint: 'Add optional subState metadata for better progress tracking.',
455
547
  });
456
548
  }
457
549
  if (task.subState?.phase && !SUB_STATE_PHASES.includes(task.subState.phase)) {
458
550
  suggestions.push({
459
551
  code: TASK_LINT_CODES.SUBSTATE_PHASE_INVALID,
460
552
  message: `Current task has invalid subState phase: ${task.subState.phase}.`,
461
- fixHint: `Use one of: ${SUB_STATE_PHASES.join(", ")}.`,
553
+ fixHint: `Use one of: ${SUB_STATE_PHASES.join(', ')}.`,
462
554
  });
463
555
  }
464
- if (typeof task.subState?.confidence === "number" && (task.subState.confidence < 0 || task.subState.confidence > 1)) {
556
+ if (typeof task.subState?.confidence === 'number' && (task.subState.confidence < 0 || task.subState.confidence > 1)) {
465
557
  suggestions.push({
466
558
  code: TASK_LINT_CODES.SUBSTATE_CONFIDENCE_INVALID,
467
559
  message: `Current task has invalid confidence score: ${task.subState.confidence}.`,
468
- fixHint: "Confidence must be between 0.0 and 1.0.",
560
+ fixHint: 'Confidence must be between 0.0 and 1.0.',
469
561
  });
470
562
  }
471
- return renderLintSuggestions(suggestions);
563
+ return suggestions;
472
564
  }
473
- async function collectTaskFileLintSuggestions(governanceDir, task) {
474
- const suggestions = [];
475
- const projectPath = toProjectPath(governanceDir);
476
- for (const link of task.links) {
477
- const normalized = link.trim();
478
- if (normalized.length === 0) {
479
- continue;
480
- }
481
- if (/^https?:\/\//i.test(normalized)) {
482
- continue;
483
- }
484
- if (!isProjectRootRelativePath(normalized)) {
485
- suggestions.push({
486
- code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
487
- message: `Link path should be project-root-relative without leading slash: ${normalized}.`,
488
- fixHint: "Use path/from/project/root format.",
489
- });
490
- continue;
491
- }
492
- const resolvedPath = resolveTaskLinkPath(projectPath, normalized);
493
- const exists = await fs.access(resolvedPath).then(() => true).catch(() => false);
494
- if (!exists) {
495
- suggestions.push({
496
- code: TASK_LINT_CODES.LINK_TARGET_MISSING,
497
- message: `Link target not found: ${normalized} (resolved: ${resolvedPath}).`,
498
- });
499
- }
500
- }
501
- 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
+ ];
502
578
  }
503
579
  export function renderTasksMarkdown(tasks) {
504
580
  const sections = sortTasksNewestFirst(tasks).map((task) => {
505
- const roadmapRefs = task.roadmapRefs.length > 0 ? task.roadmapRefs.join(", ") : "(none)";
581
+ const roadmapRefs = task.roadmapRefs.length > 0 ? task.roadmapRefs.join(', ') : '(none)';
506
582
  const links = task.links.length > 0
507
- ? ["- links:", ...task.links.map((link) => ` - ${link}`)]
508
- : ["- links:", " - (none)"];
583
+ ? ['- links:', ...task.links.map((link) => ` - ${link}`)]
584
+ : ['- links:', ' - (none)'];
509
585
  const lines = [
510
586
  `## ${task.id} | ${task.status} | ${task.title}`,
511
- `- owner: ${task.owner || "(none)"}`,
512
- `- summary: ${task.summary || "(none)"}`,
587
+ `- owner: ${task.owner || '(none)'}`,
588
+ `- summary: ${task.summary || '(none)'}`,
513
589
  `- updatedAt: ${task.updatedAt}`,
514
590
  `- roadmapRefs: ${roadmapRefs}`,
515
591
  ...links,
516
592
  ];
517
593
  // Add subState for IN_PROGRESS tasks (Spec v1.1.0)
518
- if (task.subState && task.status === "IN_PROGRESS") {
519
- lines.push(`- subState:`);
594
+ if (task.subState && task.status === 'IN_PROGRESS') {
595
+ lines.push('- subState:');
520
596
  if (task.subState.phase) {
521
597
  lines.push(` - phase: ${task.subState.phase}`);
522
598
  }
523
- if (typeof task.subState.confidence === "number") {
599
+ if (typeof task.subState.confidence === 'number') {
524
600
  lines.push(` - confidence: ${task.subState.confidence}`);
525
601
  }
526
602
  if (task.subState.estimatedCompletion) {
@@ -528,8 +604,8 @@ export function renderTasksMarkdown(tasks) {
528
604
  }
529
605
  }
530
606
  // Add blocker for BLOCKED tasks (Spec v1.1.0)
531
- if (task.blocker && task.status === "BLOCKED") {
532
- lines.push(`- blocker:`);
607
+ if (task.blocker && task.status === 'BLOCKED') {
608
+ lines.push('- blocker:');
533
609
  lines.push(` - type: ${task.blocker.type}`);
534
610
  lines.push(` - description: ${task.blocker.description}`);
535
611
  if (task.blocker.blockingEntity) {
@@ -542,16 +618,20 @@ export function renderTasksMarkdown(tasks) {
542
618
  lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
543
619
  }
544
620
  }
545
- return lines.join("\n");
621
+ return lines.join('\n');
546
622
  });
547
623
  return [
548
- "# Tasks",
549
- "",
550
- "This file is generated from .projitive governance store by Projitive MCP. Manual edits will be overwritten.",
551
- "",
552
- ...(sections.length > 0 ? sections : ["(no tasks)"]),
553
- "",
554
- ].join("\n");
624
+ '# Tasks',
625
+ '',
626
+ 'Projitive is an AI-first project governance framework for tasks, roadmaps, reports, and designs.',
627
+ 'Author: yinxulai',
628
+ 'Repository: https://github.com/yinxulai/projitive',
629
+ 'Do not edit this file manually. This file is automatically generated by Projitive.',
630
+ 'This file is generated from .projitive governance store by Projitive MCP. Manual edits will be overwritten.',
631
+ '',
632
+ ...(sections.length > 0 ? sections : ['(no tasks)']),
633
+ '',
634
+ ].join('\n');
555
635
  }
556
636
  export async function ensureTasksFile(inputPath) {
557
637
  const governanceDir = await resolveGovernanceDir(inputPath);
@@ -588,414 +668,480 @@ export function validateTransition(from, to) {
588
668
  return true;
589
669
  }
590
670
  const allowed = {
591
- TODO: new Set(["IN_PROGRESS", "BLOCKED"]),
592
- IN_PROGRESS: new Set(["BLOCKED", "DONE"]),
593
- BLOCKED: new Set(["IN_PROGRESS", "TODO"]),
671
+ TODO: new Set(['IN_PROGRESS', 'BLOCKED']),
672
+ IN_PROGRESS: new Set(['BLOCKED', 'DONE']),
673
+ BLOCKED: new Set(['IN_PROGRESS', 'TODO']),
594
674
  DONE: new Set(),
595
675
  };
596
676
  return allowed[from].has(to);
597
677
  }
598
678
  export function registerTaskTools(server) {
599
- server.registerTool("taskList", {
600
- title: "Task List",
601
- description: "List tasks for a known project and optionally filter by status",
679
+ server.registerTool(...createGovernedTool({
680
+ name: 'taskList',
681
+ title: 'Task List',
682
+ description: 'List tasks for a known project and optionally filter by status',
602
683
  inputSchema: {
603
684
  projectPath: z.string(),
604
- status: z.enum(["TODO", "IN_PROGRESS", "BLOCKED", "DONE"]).optional(),
685
+ status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
605
686
  limit: z.number().int().min(1).max(200).optional(),
606
687
  },
607
- }, async ({ projectPath, status, limit }) => {
608
- const governanceDir = await resolveGovernanceDir(projectPath);
609
- const normalizedProjectPath = toProjectPath(governanceDir);
610
- const { tasks } = await loadTasksDocument(governanceDir);
611
- const filtered = tasks
612
- .filter((task) => (status ? task.status === status : true))
613
- .slice(0, limit ?? 100);
614
- const lintSuggestions = collectTaskLintSuggestions(filtered);
615
- if (status && filtered.length === 0) {
616
- appendLintSuggestions(lintSuggestions, [
617
- {
618
- code: TASK_LINT_CODES.FILTER_EMPTY,
619
- message: `No tasks matched status=${status}.`,
620
- fixHint: "Confirm status values or update task states.",
621
- },
622
- ]);
623
- }
624
- const nextTaskId = filtered[0]?.id;
625
- const markdown = renderToolResponseMarkdown({
626
- toolName: "taskList",
627
- sections: [
628
- summarySection([
629
- `- projectPath: ${normalizedProjectPath}`,
630
- `- governanceDir: ${governanceDir}`,
631
- `- filter.status: ${status ?? "(none)"}`,
632
- `- returned: ${filtered.length}`,
633
- ]),
634
- evidenceSection([
635
- "- tasks:",
636
- ...filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner || ""} | updatedAt=${task.updatedAt}`),
637
- ]),
638
- guidanceSection(["- Pick one task ID and call `taskContext`."]),
639
- lintSection(lintSuggestions),
640
- nextCallSection(nextTaskId
641
- ? `taskContext(projectPath=\"${toProjectPath(governanceDir)}\", taskId=\"${nextTaskId}\")`
642
- : undefined),
643
- ],
644
- });
645
- return asText(markdown);
646
- });
647
- server.registerTool("taskCreate", {
648
- title: "Task Create",
649
- description: "Create a new task in governance store with stable TASK-xxxx ID",
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',
730
+ title: 'Task Create',
731
+ description: 'Create a new task in governance store with stable TASK-<number> ID',
650
732
  inputSchema: {
651
733
  projectPath: z.string(),
652
- taskId: z.string(),
734
+ taskId: z.string().optional(),
653
735
  title: z.string(),
654
- status: z.enum(["TODO", "IN_PROGRESS", "BLOCKED", "DONE"]).optional(),
736
+ status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
655
737
  owner: z.string().optional(),
656
738
  summary: z.string().optional(),
657
739
  roadmapRefs: z.array(z.string()).optional(),
658
740
  links: z.array(z.string()).optional(),
659
741
  subState: z.object({
660
- phase: z.enum(["discovery", "design", "implementation", "testing"]).optional(),
742
+ phase: z.enum(['discovery', 'design', 'implementation', 'testing']).optional(),
661
743
  confidence: z.number().min(0).max(1).optional(),
662
744
  estimatedCompletion: z.string().optional(),
663
745
  }).optional(),
664
746
  blocker: z.object({
665
- type: z.enum(["internal_dependency", "external_dependency", "resource", "approval"]),
747
+ type: z.enum(['internal_dependency', 'external_dependency', 'resource', 'approval']),
666
748
  description: z.string(),
667
749
  blockingEntity: z.string().optional(),
668
750
  unblockCondition: z.string().optional(),
669
751
  escalationPath: z.string().optional(),
670
752
  }).optional(),
671
753
  },
672
- }, async ({ projectPath, taskId, title, status, owner, summary, roadmapRefs, links, subState, blocker }) => {
673
- if (!isValidTaskId(taskId)) {
674
- return {
675
- ...asText(renderErrorMarkdown("taskCreate", `Invalid task ID format: ${taskId}`, ["expected format: TASK-0001", "retry with a valid task ID"], `taskCreate(projectPath=\"${projectPath}\", taskId=\"TASK-0001\", title=\"Define executable objective\")`)),
676
- isError: true,
677
- };
678
- }
679
- const governanceDir = await resolveGovernanceDir(projectPath);
680
- const normalizedProjectPath = toProjectPath(governanceDir);
681
- const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
682
- const duplicated = tasks.some((item) => item.id === taskId);
683
- if (duplicated) {
684
- return {
685
- ...asText(renderErrorMarkdown("taskCreate", `Task already exists: ${taskId}`, ["task IDs must be unique", "use taskUpdate for existing tasks"], `taskUpdate(projectPath=\"${normalizedProjectPath}\", taskId=\"${taskId}\", updates={...})`)),
686
- isError: true,
687
- };
688
- }
689
- const createdTask = normalizeTask({
690
- id: taskId,
691
- title,
692
- status: status ?? "TODO",
693
- owner,
694
- summary,
695
- roadmapRefs,
696
- links,
697
- subState,
698
- blocker,
699
- updatedAt: nowIso(),
700
- });
701
- await upsertTaskInStore(tasksPath, createdTask);
702
- await loadTasksDocumentWithOptions(governanceDir, true);
703
- const lintSuggestions = [
704
- ...collectSingleTaskLintSuggestions(createdTask),
705
- ...(await collectTaskFileLintSuggestions(governanceDir, createdTask)),
706
- ];
707
- const markdown = renderToolResponseMarkdown({
708
- toolName: "taskCreate",
709
- sections: [
710
- summarySection([
711
- `- projectPath: ${normalizedProjectPath}`,
712
- `- governanceDir: ${governanceDir}`,
713
- `- taskId: ${createdTask.id}`,
714
- `- status: ${createdTask.status}`,
715
- `- owner: ${createdTask.owner || "(none)"}`,
716
- `- updatedAt: ${createdTask.updatedAt}`,
717
- ]),
718
- evidenceSection([
719
- "### Created Task",
720
- `- ${createdTask.id} | ${createdTask.status} | ${createdTask.title}`,
721
- `- summary: ${createdTask.summary || "(none)"}`,
722
- `- roadmapRefs: ${createdTask.roadmapRefs.join(", ") || "(none)"}`,
723
- `- links: ${createdTask.links.join(", ") || "(none)"}`,
724
- ]),
725
- guidanceSection([
726
- "Task created in governance store successfully and tasks.md has been synced.",
727
- "Run taskContext to verify references and lint guidance.",
728
- ]),
729
- lintSection(lintSuggestions),
730
- nextCallSection(`taskContext(projectPath=\"${normalizedProjectPath}\", taskId=\"${createdTask.id}\")`),
731
- ],
732
- });
733
- return asText(markdown);
734
- });
735
- server.registerTool("taskNext", {
736
- title: "Task Next",
737
- description: "Start here to auto-select the highest-priority actionable task",
738
- inputSchema: {
739
- limit: z.number().int().min(1).max(20).optional(),
740
- },
741
- }, async ({ limit }) => {
742
- const roots = resolveScanRoots();
743
- const depth = resolveScanDepth();
744
- const projects = await discoverProjectsAcrossRoots(roots, depth);
745
- const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
746
- if (rankedCandidates.length === 0) {
747
- const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
748
- const tasksPath = path.join(governanceDir, ".projitive");
749
- await ensureStore(tasksPath);
750
- const stats = await loadTaskStatusStatsFromStore(tasksPath);
751
- const roadmapIds = await readRoadmapIds(governanceDir);
752
- return {
753
- governanceDir,
754
- roadmapIds,
755
- total: stats.total,
756
- todo: stats.todo,
757
- inProgress: stats.inProgress,
758
- blocked: stats.blocked,
759
- done: stats.done,
760
- };
761
- }));
762
- const preferredProject = projectSnapshots[0];
763
- const preferredRoadmapRef = preferredProject?.roadmapIds[0] ?? "ROADMAP-0001";
764
- const noTaskDiscoveryGuidance = await resolveNoTaskDiscoveryGuidance(preferredProject?.governanceDir);
765
- const markdown = renderToolResponseMarkdown({
766
- toolName: "taskNext",
767
- sections: [
768
- summarySection([
769
- `- rootPaths: ${roots.join(", ")}`,
770
- `- rootCount: ${roots.length}`,
771
- `- maxDepth: ${depth}`,
772
- `- matchedProjects: ${projects.length}`,
773
- "- actionableTasks: 0",
774
- ]),
775
- evidenceSection([
776
- "### Project Snapshots",
777
- ...(projectSnapshots.length > 0
778
- ? 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)"}`)
779
- : ["- (none)"]),
780
- "",
781
- "### Seed Task Template",
782
- ...renderTaskSeedTemplate(preferredRoadmapRef),
783
- ]),
784
- guidanceSection([
785
- "- No TODO/IN_PROGRESS task is available.",
786
- "- Create 1-3 new TODO tasks using `taskCreate(...)` from active roadmap slices.",
787
- "- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.",
788
- "- If roadmap has active milestones, analyze milestone intent and split into 1-3 executable TODO tasks.",
789
- "",
790
- "### No-Task Discovery Checklist",
791
- ...noTaskDiscoveryGuidance,
792
- "",
793
- "- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.",
794
- "- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.",
795
- "- After creating tasks, rerun `taskNext` to re-rank actionable work.",
796
- ]),
797
- lintSection([
798
- "- No actionable tasks found. Verify task statuses and required fields in .projitive task table.",
799
- "- Ensure each new task has stable TASK-xxxx ID and at least one roadmapRefs item.",
800
- ]),
801
- nextCallSection(preferredProject
802
- ? `taskCreate(projectPath=\"${toProjectPath(preferredProject.governanceDir)}\", taskId=\"TASK-0001\", title=\"Create first executable slice\", roadmapRefs=[\"${preferredRoadmapRef}\"], summary=\"Derived from active roadmap milestone\")`
803
- : "projectScan()"),
804
- ],
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(),
805
778
  });
806
- return asText(markdown);
807
- }
808
- const selected = rankedCandidates[0];
809
- const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
810
- const lintSuggestions = collectTaskLintSuggestions(selectedTaskDocument.tasks);
811
- const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
812
- const fileCandidates = candidateFilesFromArtifacts(artifacts);
813
- const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
814
- const taskLocation = (await findTextReferences(selectedTaskDocument.markdownPath, selected.task.id))[0];
815
- const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
816
- const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
817
- const candidateLimit = limit ?? 5;
818
- const markdown = renderToolResponseMarkdown({
819
- toolName: "taskNext",
820
- sections: [
821
- summarySection([
822
- `- rootPaths: ${roots.join(", ")}`,
823
- `- rootCount: ${roots.length}`,
824
- `- maxDepth: ${depth}`,
825
- `- matchedProjects: ${projects.length}`,
826
- `- actionableTasks: ${rankedCandidates.length}`,
827
- `- selectedProject: ${toProjectPath(selected.governanceDir)}`,
828
- `- selectedTaskId: ${selected.task.id}`,
829
- `- selectedTaskStatus: ${selected.task.status}`,
830
- ]),
831
- evidenceSection([
832
- "### Selected Task",
833
- `- id: ${selected.task.id}`,
834
- `- title: ${selected.task.title}`,
835
- `- owner: ${selected.task.owner || "(none)"}`,
836
- `- updatedAt: ${selected.task.updatedAt}`,
837
- `- roadmapRefs: ${selected.task.roadmapRefs.join(", ") || "(none)"}`,
838
- `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selectedTaskDocument.markdownPath}`,
839
- "",
840
- "### Top Candidates",
841
- ...rankedCandidates
842
- .slice(0, candidateLimit)
843
- .map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | projectPath=${toProjectPath(item.governanceDir)} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
844
- "",
845
- "### Selection Reason",
846
- "- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.",
847
- `- Selected candidate scores: projectScore=${selected.projectScore}, taskPriority=${selected.taskPriority}, taskUpdatedAtMs=${selected.taskUpdatedAtMs}.`,
848
- "",
849
- "### Related Artifacts",
850
- ...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
851
- "",
852
- "### Reference Locations",
853
- ...(referenceLocations.length > 0
854
- ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
855
- : ["- (none)"]),
856
- "",
857
- "### Suggested Read Order",
858
- ...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
859
- ]),
860
- guidanceSection([
861
- "- Start immediately with Suggested Read Order and execute the selected task.",
862
- "- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.",
863
- "- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.",
864
- ]),
865
- lintSection(lintSuggestions),
866
- nextCallSection(`taskContext(projectPath=\"${toProjectPath(selected.governanceDir)}\", taskId=\"${selected.task.id}\")`),
867
- ],
868
- });
869
- return asText(markdown);
870
- });
871
- server.registerTool("taskContext", {
872
- title: "Task Context",
873
- description: "Get deep context, evidence links, and read order for one task",
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',
809
+ title: 'Task Next',
810
+ description: 'Start here to auto-select the highest-priority actionable task',
874
811
  inputSchema: {
875
- projectPath: z.string(),
876
- taskId: z.string(),
812
+ limit: z.number().int().min(1).max(20).optional(),
877
813
  },
878
- }, async ({ projectPath, taskId }) => {
879
- if (!isValidTaskId(taskId)) {
880
- return {
881
- ...asText(renderErrorMarkdown("taskContext", `Invalid task ID format: ${taskId}`, ["expected format: TASK-0001", "retry with a valid task ID"], `taskContext(projectPath=\"${projectPath}\", taskId=\"TASK-0001\")`)),
882
- isError: true,
883
- };
884
- }
885
- const governanceDir = await resolveGovernanceDir(projectPath);
886
- const normalizedProjectPath = toProjectPath(governanceDir);
887
- const { markdownPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
888
- const task = tasks.find((item) => item.id === taskId);
889
- if (!task) {
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;
890
842
  return {
891
- ...asText(renderErrorMarkdown("taskContext", `Task not found: ${taskId}`, ["run `taskList` to discover available IDs", "retry with an existing task ID"], `taskList(projectPath=\"${toProjectPath(governanceDir)}\")`)),
892
- isError: true,
843
+ isEmpty: false,
844
+ roots, depth, projects,
845
+ rankedCandidates, selected, selectedTaskDocument,
846
+ relatedArtifacts, referenceLocations,
847
+ suggestedReadOrder, projectContextDocsState, taskLocation, candidateLimit,
893
848
  };
894
- }
895
- const lintSuggestions = [
896
- ...collectSingleTaskLintSuggestions(task),
897
- ...(await collectTaskFileLintSuggestions(governanceDir, task)),
898
- ];
899
- const contextReadingGuidance = await resolveTaskContextReadingGuidance(governanceDir);
900
- const taskLocation = (await findTextReferences(markdownPath, taskId))[0];
901
- const artifacts = await discoverGovernanceArtifacts(governanceDir);
902
- const fileCandidates = candidateFilesFromArtifacts(artifacts);
903
- const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
904
- const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
905
- const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
906
- // Build summary with subState and blocker info (v1.1.0)
907
- const summaryLines = [
908
- `- projectPath: ${normalizedProjectPath}`,
909
- `- governanceDir: ${governanceDir}`,
910
- `- taskId: ${task.id}`,
911
- `- title: ${task.title}`,
912
- `- status: ${task.status}`,
913
- `- owner: ${task.owner}`,
914
- `- updatedAt: ${task.updatedAt}`,
915
- `- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
916
- `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
917
- ];
918
- // Add subState info for IN_PROGRESS tasks (v1.1.0)
919
- if (task.subState && task.status === "IN_PROGRESS") {
920
- summaryLines.push(`- subState:`);
921
- if (task.subState.phase) {
922
- summaryLines.push(` - phase: ${task.subState.phase}`);
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
+ ];
923
859
  }
924
- if (typeof task.subState.confidence === "number") {
925
- summaryLines.push(` - confidence: ${task.subState.confidence}`);
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)']),
878
+ '',
879
+ '### Seed Task Template',
880
+ ...renderTaskSeedTemplate(data.preferredRoadmapRef),
881
+ ];
926
882
  }
927
- if (task.subState.estimatedCompletion) {
928
- summaryLines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
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.',
924
+ '',
925
+ '### No-Task Discovery Checklist',
926
+ ...data.noTaskDiscoveryGuidance,
927
+ '',
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
+ ];
929
932
  }
930
- }
931
- // Add blocker info for BLOCKED tasks (v1.1.0)
932
- if (task.blocker && task.status === "BLOCKED") {
933
- summaryLines.push(`- blocker:`);
934
- summaryLines.push(` - type: ${task.blocker.type}`);
935
- summaryLines.push(` - description: ${task.blocker.description}`);
936
- if (task.blocker.blockingEntity) {
937
- summaryLines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
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
+ ];
938
956
  }
939
- if (task.blocker.unblockCondition) {
940
- summaryLines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
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()';
941
967
  }
942
- if (task.blocker.escalationPath) {
943
- summaryLines.push(` - escalationPath: ${task.blocker.escalationPath}`);
968
+ return `taskContext(projectPath="${toProjectPath(data.selected.governanceDir)}", taskId="${data.selected.task.id}")`;
969
+ },
970
+ }));
971
+ server.registerTool(...createGovernedTool({
972
+ name: 'taskContext',
973
+ title: 'Task Context',
974
+ description: 'Get deep context, evidence links, and read order for one task',
975
+ inputSchema: {
976
+ projectPath: z.string(),
977
+ taskId: z.string(),
978
+ },
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")`);
944
982
  }
945
- }
946
- const coreMarkdown = renderToolResponseMarkdown({
947
- toolName: "taskContext",
948
- sections: [
949
- summarySection(summaryLines),
950
- evidenceSection([
951
- "### Related Artifacts",
952
- ...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
953
- "",
954
- "### Reference Locations",
955
- ...(referenceLocations.length > 0
956
- ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
957
- : ["- (none)"]),
958
- "",
959
- "### Suggested Read Order",
960
- ...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
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)}")`);
990
+ }
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}`);
1033
+ }
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}`);
1044
+ }
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
+ ? [
1054
+ '',
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.`,
961
1092
  ]),
962
- guidanceSection([
963
- "- Read the files in Suggested Read Order.",
964
- "",
965
- "### Recommended Context Reading",
966
- ...contextReadingGuidance,
967
- "",
968
- "- Verify whether current status and evidence are consistent.",
969
- ...taskStatusGuidance(task),
970
- "- If updates are needed, use tool writes for governance store (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.",
971
- "- 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.',
972
1106
  ]),
973
- lintSection(lintSuggestions),
974
- nextCallSection(`taskContext(projectPath=\"${toProjectPath(governanceDir)}\", taskId=\"${task.id}\")`),
975
- ],
976
- });
977
- return asText(coreMarkdown);
978
- });
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
+ }));
979
1124
  // taskUpdate tool - Update task fields including subState and blocker (Spec v1.1.0)
980
- server.registerTool("taskUpdate", {
981
- title: "Task Update",
982
- description: "Update task fields including status, owner, summary, subState, and blocker metadata",
1125
+ server.registerTool(...createGovernedTool({
1126
+ name: 'taskUpdate',
1127
+ title: 'Task Update',
1128
+ description: 'Update task fields including status, owner, summary, subState, and blocker metadata',
983
1129
  inputSchema: {
984
1130
  projectPath: z.string(),
985
1131
  taskId: z.string(),
986
1132
  updates: z.object({
987
- status: z.enum(["TODO", "IN_PROGRESS", "BLOCKED", "DONE"]).optional(),
1133
+ status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
988
1134
  owner: z.string().optional(),
989
1135
  summary: z.string().optional(),
990
1136
  roadmapRefs: z.array(z.string()).optional(),
991
1137
  links: z.array(z.string()).optional(),
992
1138
  subState: z.object({
993
- phase: z.enum(["discovery", "design", "implementation", "testing"]).optional(),
1139
+ phase: z.enum(['discovery', 'design', 'implementation', 'testing']).optional(),
994
1140
  confidence: z.number().min(0).max(1).optional(),
995
1141
  estimatedCompletion: z.string().optional(),
996
1142
  }).optional(),
997
1143
  blocker: z.object({
998
- type: z.enum(["internal_dependency", "external_dependency", "resource", "approval"]),
1144
+ type: z.enum(['internal_dependency', 'external_dependency', 'resource', 'approval']),
999
1145
  description: z.string(),
1000
1146
  blockingEntity: z.string().optional(),
1001
1147
  unblockCondition: z.string().optional(),
@@ -1003,136 +1149,108 @@ export function registerTaskTools(server) {
1003
1149
  }).optional(),
1004
1150
  }),
1005
1151
  },
1006
- }, async ({ projectPath, taskId, updates }) => {
1007
- if (!isValidTaskId(taskId)) {
1008
- return {
1009
- ...asText(renderErrorMarkdown("taskUpdate", `Invalid task ID format: ${taskId}`, ["expected format: TASK-0001", "retry with a valid task ID"], `taskUpdate(projectPath=\"${projectPath}\", taskId=\"TASK-0001\", updates={...})`)),
1010
- isError: true,
1011
- };
1012
- }
1013
- const governanceDir = await resolveGovernanceDir(projectPath);
1014
- const normalizedProjectPath = toProjectPath(governanceDir);
1015
- const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
1016
- const taskIndex = tasks.findIndex((item) => item.id === taskId);
1017
- if (taskIndex === -1) {
1018
- return {
1019
- ...asText(renderErrorMarkdown("taskUpdate", `Task not found: ${taskId}`, ["run `taskList` to discover available IDs", "retry with an existing task ID"], `taskList(projectPath=\"${toProjectPath(governanceDir)}\")`)),
1020
- isError: true,
1021
- };
1022
- }
1023
- const task = tasks[taskIndex];
1024
- const originalStatus = task.status;
1025
- // Validate status transition
1026
- if (updates.status && !validateTransition(originalStatus, updates.status)) {
1027
- return {
1028
- ...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}\")`)),
1029
- isError: true,
1030
- };
1031
- }
1032
- // Apply updates
1033
- if (updates.status)
1034
- task.status = updates.status;
1035
- if (updates.owner !== undefined)
1036
- task.owner = updates.owner;
1037
- if (updates.summary !== undefined)
1038
- task.summary = updates.summary;
1039
- if (updates.roadmapRefs)
1040
- task.roadmapRefs = updates.roadmapRefs;
1041
- if (updates.links)
1042
- task.links = updates.links;
1043
- // Handle subState (Spec v1.1.0)
1044
- if (updates.subState !== undefined) {
1045
- if (updates.subState === null) {
1046
- 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={...})`);
1047
1155
  }
1048
- else {
1049
- task.subState = {
1050
- ...(task.subState || {}),
1051
- ...updates.subState,
1052
- };
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)}")`);
1053
1164
  }
1054
- }
1055
- // Handle blocker (Spec v1.1.0)
1056
- if (updates.blocker !== undefined) {
1057
- if (updates.blocker === null) {
1058
- 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}")`);
1059
1170
  }
1060
- else {
1061
- 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}`);
1062
1211
  }
1063
- }
1064
- // Update updatedAt
1065
- task.updatedAt = nowIso();
1066
- const normalizedTask = normalizeTask(task);
1067
- // Save task incrementally
1068
- await upsertTaskInStore(tasksPath, normalizedTask);
1069
- await loadTasksDocumentWithOptions(governanceDir, true);
1070
- task.status = normalizedTask.status;
1071
- task.owner = normalizedTask.owner;
1072
- task.summary = normalizedTask.summary;
1073
- task.roadmapRefs = normalizedTask.roadmapRefs;
1074
- task.links = normalizedTask.links;
1075
- task.updatedAt = normalizedTask.updatedAt;
1076
- task.subState = normalizedTask.subState;
1077
- task.blocker = normalizedTask.blocker;
1078
- // Build response
1079
- const updateSummary = [
1080
- `- projectPath: ${normalizedProjectPath}`,
1081
- `- governanceDir: ${governanceDir}`,
1082
- `- taskId: ${taskId}`,
1083
- `- originalStatus: ${originalStatus}`,
1084
- `- newStatus: ${task.status}`,
1085
- `- updatedAt: ${task.updatedAt}`,
1086
- ];
1087
- if (task.subState) {
1088
- updateSummary.push(`- subState:`);
1089
- if (task.subState.phase)
1090
- updateSummary.push(` - phase: ${task.subState.phase}`);
1091
- if (typeof task.subState.confidence === "number")
1092
- updateSummary.push(` - confidence: ${task.subState.confidence}`);
1093
- if (task.subState.estimatedCompletion)
1094
- updateSummary.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
1095
- }
1096
- if (task.blocker) {
1097
- updateSummary.push(`- blocker:`);
1098
- updateSummary.push(` - type: ${task.blocker.type}`);
1099
- updateSummary.push(` - description: ${task.blocker.description}`);
1100
- if (task.blocker.blockingEntity)
1101
- updateSummary.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
1102
- if (task.blocker.unblockCondition)
1103
- updateSummary.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
1104
- if (task.blocker.escalationPath)
1105
- updateSummary.push(` - escalationPath: ${task.blocker.escalationPath}`);
1106
- }
1107
- const markdown = renderToolResponseMarkdown({
1108
- toolName: "taskUpdate",
1109
- sections: [
1110
- summarySection(updateSummary),
1111
- evidenceSection([
1112
- "### Updated Task",
1113
- `- ${task.id} | ${task.status} | ${task.title}`,
1114
- `- owner: ${task.owner || "(none)"}`,
1115
- `- summary: ${task.summary || "(none)"}`,
1116
- "",
1117
- "### Update Details",
1118
- ...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
1119
- ...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
1120
- ...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
1121
- ...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(", ")}`] : []),
1122
- ...(updates.links ? [`- links: ${updates.links.join(", ")}`] : []),
1123
- ...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
1124
- ...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
1125
- ]),
1126
- guidanceSection([
1127
- "Task updated successfully and tasks.md has been synced. Run `taskContext` to verify the changes.",
1128
- "If status changed to DONE, ensure evidence links are added.",
1129
- "If subState or blocker were updated, verify the metadata is correct.",
1130
- ".projitive governance store is source of truth; tasks.md is a generated view and may be overwritten.",
1131
- ]),
1132
- lintSection([]),
1133
- nextCallSection(`taskContext(projectPath=\"${toProjectPath(governanceDir)}\", taskId=\"${taskId}\")`),
1134
- ],
1135
- });
1136
- return asText(markdown);
1137
- });
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
+ }));
1138
1256
  }