@projitive/mcp 2.0.2 → 2.0.4

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