@projitive/mcp 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,15 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { z } from "zod";
4
- import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, findTextReferences } from "../common/index.js";
4
+ import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, findTextReferences, ensureStore, loadActionableTasksFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, replaceTasksInStore, upsertTaskInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, } from "../common/index.js";
5
5
  import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "../common/index.js";
6
- import { catchIt, TASK_LINT_CODES, renderLintSuggestions } from "../common/index.js";
6
+ import { TASK_LINT_CODES, renderLintSuggestions } from "../common/index.js";
7
7
  import { resolveGovernanceDir, resolveScanDepth, resolveScanRoots, discoverProjectsAcrossRoots, toProjectPath } from "./project.js";
8
8
  import { isValidRoadmapId } from "./roadmap.js";
9
9
  import { SUB_STATE_PHASES, BLOCKER_TYPES } from "../types.js";
10
- export const TASKS_START = "<!-- PROJITIVE:TASKS:START -->";
11
- export const TASKS_END = "<!-- PROJITIVE:TASKS:END -->";
12
10
  export const ALLOWED_STATUS = ["TODO", "IN_PROGRESS", "BLOCKED", "DONE"];
13
11
  export const TASK_ID_REGEX = /^TASK-\d{4}$/;
12
+ export const TASKS_MARKDOWN_FILE = "tasks.md";
14
13
  function appendLintSuggestions(target, suggestions) {
15
14
  target.push(...renderLintSuggestions(suggestions));
16
15
  }
@@ -38,62 +37,40 @@ function taskStatusGuidance(task) {
38
37
  "- Keep report evidence immutable unless correction is required.",
39
38
  ];
40
39
  }
41
- async function readOptionalMarkdown(filePath) {
42
- const content = await fs.readFile(filePath, "utf-8").catch(() => undefined);
43
- if (typeof content !== "string") {
44
- return undefined;
45
- }
46
- const trimmed = content.trim();
47
- return trimmed.length > 0 ? trimmed : undefined;
48
- }
49
- const NO_TASK_DISCOVERY_HOOK_FILE = "task_no_actionable.md";
50
40
  const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
51
- "- Check whether current code violates project guide/spec conventions; create TODO tasks for each actionable gap.",
52
- "- Check unit/integration test coverage and identify high-value missing tests; create TODO tasks for meaningful coverage improvements.",
53
- "- Check development/testing workflow for bottlenecks (slow feedback, fragile scripts, unclear runbooks); create tasks to improve reliability.",
54
- "- Scan for TODO/FIXME/HACK comments and convert feasible items into governed TODO tasks with evidence links.",
55
- "- Check dependency freshness and security advisories; create tasks for safe upgrades when needed.",
56
- "- Check repeated manual operations that can be automated (lint/test/release checks); create tasks to reduce operational toil.",
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/design/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.",
49
+ ];
50
+ 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.",
57
58
  ];
58
- function parseHookChecklist(markdown) {
59
- return markdown
60
- .split(/\r?\n/)
61
- .map((line) => line.trim())
62
- .filter((line) => line.length > 0)
63
- .filter((line) => /^[-*+]\s+/.test(line))
64
- .map((line) => (line.startsWith("*") ? `-${line.slice(1)}` : line));
65
- }
66
59
  export async function resolveNoTaskDiscoveryGuidance(governanceDir) {
67
- if (!governanceDir) {
68
- return DEFAULT_NO_TASK_DISCOVERY_GUIDANCE;
69
- }
70
- const hookPath = path.join(governanceDir, "hooks", NO_TASK_DISCOVERY_HOOK_FILE);
71
- const markdown = await readOptionalMarkdown(hookPath);
72
- if (typeof markdown !== "string") {
73
- return DEFAULT_NO_TASK_DISCOVERY_GUIDANCE;
74
- }
75
- const checklist = parseHookChecklist(markdown);
76
- return checklist.length > 0 ? checklist : DEFAULT_NO_TASK_DISCOVERY_GUIDANCE;
60
+ void governanceDir;
61
+ return DEFAULT_NO_TASK_DISCOVERY_GUIDANCE;
77
62
  }
78
- function latestTaskUpdatedAt(tasks) {
79
- const timestamps = tasks
80
- .map((task) => new Date(task.updatedAt).getTime())
81
- .filter((value) => Number.isFinite(value));
82
- if (timestamps.length === 0) {
83
- return "(unknown)";
84
- }
85
- return new Date(Math.max(...timestamps)).toISOString();
86
- }
87
- function actionableScore(tasks) {
88
- return tasks.filter((task) => task.status === "IN_PROGRESS").length * 2
89
- + tasks.filter((task) => task.status === "TODO").length;
63
+ export async function resolveTaskContextReadingGuidance(governanceDir) {
64
+ void governanceDir;
65
+ return DEFAULT_TASK_CONTEXT_READING_GUIDANCE;
90
66
  }
91
67
  async function readRoadmapIds(governanceDir) {
92
- const roadmapPath = path.join(governanceDir, "roadmap.md");
68
+ const dbPath = path.join(governanceDir, ".projitive");
93
69
  try {
94
- const markdown = await fs.readFile(roadmapPath, "utf-8");
95
- const matches = markdown.match(/ROADMAP-\d{4}/g) ?? [];
96
- return Array.from(new Set(matches));
70
+ await ensureStore(dbPath);
71
+ const milestones = await loadRoadmapsFromStore(dbPath);
72
+ const ids = milestones.map((item) => item.id).filter((item) => isValidRoadmapId(item));
73
+ return Array.from(new Set(ids));
97
74
  }
98
75
  catch {
99
76
  return [];
@@ -115,20 +92,23 @@ export function renderTaskSeedTemplate(roadmapRef) {
115
92
  }
116
93
  async function readActionableTaskCandidates(governanceDirs) {
117
94
  const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
118
- const snapshot = await loadTasks(governanceDir);
95
+ const tasksPath = path.join(governanceDir, ".projitive");
96
+ await ensureStore(tasksPath);
97
+ const [stats, actionableTasks] = await Promise.all([
98
+ loadTaskStatusStatsFromStore(tasksPath),
99
+ loadActionableTasksFromStore(tasksPath),
100
+ ]);
119
101
  return {
120
102
  governanceDir,
121
- tasksPath: snapshot.tasksPath,
122
- tasks: snapshot.tasks,
123
- projectScore: actionableScore(snapshot.tasks),
124
- projectLatestUpdatedAt: latestTaskUpdatedAt(snapshot.tasks),
103
+ tasks: actionableTasks,
104
+ projectScore: stats.inProgress * 2 + stats.todo,
105
+ projectLatestUpdatedAt: stats.latestUpdatedAt || "(unknown)",
125
106
  };
126
107
  }));
127
108
  return snapshots.flatMap((item) => item.tasks
128
109
  .filter((task) => task.status === "IN_PROGRESS" || task.status === "TODO")
129
110
  .map((task) => ({
130
111
  governanceDir: item.governanceDir,
131
- tasksPath: item.tasksPath,
132
112
  task,
133
113
  projectScore: item.projectScore,
134
114
  projectLatestUpdatedAt: item.projectLatestUpdatedAt,
@@ -155,6 +135,49 @@ export function toTaskUpdatedAtMs(updatedAt) {
155
135
  const timestamp = new Date(updatedAt).getTime();
156
136
  return Number.isFinite(timestamp) ? timestamp : 0;
157
137
  }
138
+ function toTaskIdNumericSuffix(taskId) {
139
+ const match = taskId.match(/^(?:TASK-)(\d{4})$/);
140
+ if (!match) {
141
+ return -1;
142
+ }
143
+ return Number.parseInt(match[1], 10);
144
+ }
145
+ export function sortTasksNewestFirst(tasks) {
146
+ return [...tasks].sort((a, b) => {
147
+ const updatedAtDelta = toTaskUpdatedAtMs(b.updatedAt) - toTaskUpdatedAtMs(a.updatedAt);
148
+ if (updatedAtDelta !== 0) {
149
+ return updatedAtDelta;
150
+ }
151
+ const idDelta = toTaskIdNumericSuffix(b.id) - toTaskIdNumericSuffix(a.id);
152
+ if (idDelta !== 0) {
153
+ return idDelta;
154
+ }
155
+ return b.id.localeCompare(a.id);
156
+ });
157
+ }
158
+ function normalizeAndSortTasks(tasks) {
159
+ return sortTasksNewestFirst(tasks.map((task) => normalizeTask(task)));
160
+ }
161
+ function resolveTaskArtifactPaths(governanceDir) {
162
+ return {
163
+ tasksPath: path.join(governanceDir, ".projitive"),
164
+ markdownPath: path.join(governanceDir, TASKS_MARKDOWN_FILE),
165
+ };
166
+ }
167
+ async function syncTasksMarkdownView(tasksPath, markdownPath, markdown, force = false) {
168
+ const sourceVersion = await getStoreVersion(tasksPath, "tasks");
169
+ const viewState = await getMarkdownViewState(tasksPath, "tasks_markdown");
170
+ const markdownExists = await fs.access(markdownPath).then(() => true).catch(() => false);
171
+ const shouldWrite = force
172
+ || !markdownExists
173
+ || viewState.dirty
174
+ || viewState.lastSourceVersion !== sourceVersion;
175
+ if (!shouldWrite) {
176
+ return;
177
+ }
178
+ await fs.writeFile(markdownPath, markdown, "utf-8");
179
+ await markMarkdownViewBuilt(tasksPath, "tasks_markdown", sourceVersion);
180
+ }
158
181
  export function rankActionableTaskCandidates(candidates) {
159
182
  return [...candidates].sort((a, b) => {
160
183
  if (b.projectScore !== a.projectScore) {
@@ -172,114 +195,6 @@ export function rankActionableTaskCandidates(candidates) {
172
195
  return a.task.id.localeCompare(b.task.id);
173
196
  });
174
197
  }
175
- // Helper function to check if a line is a top-level task field
176
- function isTopLevelField(line) {
177
- const topLevelFields = [
178
- "- owner:",
179
- "- summary:",
180
- "- updatedAt:",
181
- "- roadmapRefs:",
182
- "- links:",
183
- "- hooks:",
184
- "- subState:",
185
- "- blocker:",
186
- ];
187
- return topLevelFields.some((field) => line.startsWith(field));
188
- }
189
- // Parse subState nested field
190
- function parseSubState(lines, startIndex) {
191
- const subState = {};
192
- let index = startIndex + 1;
193
- while (index < lines.length) {
194
- const line = lines[index];
195
- const trimmed = line.trim();
196
- // Check if we've reached the end of subState (new top-level field or new task)
197
- if (trimmed.startsWith("- ") && isTopLevelField(trimmed)) {
198
- break;
199
- }
200
- // Check if we've reached a new task
201
- if (trimmed.startsWith("## TASK-")) {
202
- break;
203
- }
204
- // Parse nested fields (2-space indentation expected)
205
- if (trimmed.startsWith("- phase:")) {
206
- const phase = trimmed.replace("- phase:", "").trim();
207
- if (SUB_STATE_PHASES.includes(phase)) {
208
- subState.phase = phase;
209
- }
210
- }
211
- else if (trimmed.startsWith("- confidence:")) {
212
- const confidenceStr = trimmed.replace("- confidence:", "").trim();
213
- const confidence = Number.parseFloat(confidenceStr);
214
- if (!Number.isNaN(confidence) && confidence >= 0 && confidence <= 1) {
215
- subState.confidence = confidence;
216
- }
217
- }
218
- else if (trimmed.startsWith("- estimatedCompletion:")) {
219
- const estimatedCompletion = trimmed.replace("- estimatedCompletion:", "").trim();
220
- if (estimatedCompletion && estimatedCompletion !== "(none)") {
221
- subState.estimatedCompletion = estimatedCompletion;
222
- }
223
- }
224
- index++;
225
- }
226
- return { subState, endIndex: index - 1 };
227
- }
228
- // Parse blocker nested field
229
- function parseBlocker(lines, startIndex) {
230
- const blocker = {};
231
- let index = startIndex + 1;
232
- while (index < lines.length) {
233
- const line = lines[index];
234
- const trimmed = line.trim();
235
- // Check if we've reached the end of blocker (new top-level field or new task)
236
- if (trimmed.startsWith("- ") && isTopLevelField(trimmed)) {
237
- break;
238
- }
239
- // Check if we've reached a new task
240
- if (trimmed.startsWith("## TASK-")) {
241
- break;
242
- }
243
- // Parse nested fields (2-space indentation expected)
244
- if (trimmed.startsWith("- type:")) {
245
- const type = trimmed.replace("- type:", "").trim();
246
- if (BLOCKER_TYPES.includes(type)) {
247
- blocker.type = type;
248
- }
249
- }
250
- else if (trimmed.startsWith("- description:")) {
251
- const description = trimmed.replace("- description:", "").trim();
252
- if (description && description !== "(none)") {
253
- blocker.description = description;
254
- }
255
- }
256
- else if (trimmed.startsWith("- blockingEntity:")) {
257
- const blockingEntity = trimmed.replace("- blockingEntity:", "").trim();
258
- if (blockingEntity && blockingEntity !== "(none)") {
259
- blocker.blockingEntity = blockingEntity;
260
- }
261
- }
262
- else if (trimmed.startsWith("- unblockCondition:")) {
263
- const unblockCondition = trimmed.replace("- unblockCondition:", "").trim();
264
- if (unblockCondition && unblockCondition !== "(none)") {
265
- blocker.unblockCondition = unblockCondition;
266
- }
267
- }
268
- else if (trimmed.startsWith("- escalationPath:")) {
269
- const escalationPath = trimmed.replace("- escalationPath:", "").trim();
270
- if (escalationPath && escalationPath !== "(none)") {
271
- blocker.escalationPath = escalationPath;
272
- }
273
- }
274
- index++;
275
- }
276
- // Validate required fields
277
- if (!blocker.type || !blocker.description) {
278
- // Return empty blocker if required fields are missing
279
- return { blocker: { type: "external_dependency", description: "Unknown blocker" }, endIndex: index - 1 };
280
- }
281
- return { blocker: blocker, endIndex: index - 1 };
282
- }
283
198
  export function normalizeTask(task) {
284
199
  const normalizedRoadmapRefs = Array.isArray(task.roadmapRefs)
285
200
  ? task.roadmapRefs.map(String).filter((value) => isValidRoadmapId(value))
@@ -303,146 +218,8 @@ export function normalizeTask(task) {
303
218
  }
304
219
  return normalized;
305
220
  }
306
- export async function parseTasksBlock(markdown) {
307
- const start = markdown.indexOf(TASKS_START);
308
- const end = markdown.indexOf(TASKS_END);
309
- if (start === -1 || end === -1 || end <= start) {
310
- return [];
311
- }
312
- const body = markdown.slice(start + TASKS_START.length, end).trim();
313
- if (!body || body === "(no tasks)") {
314
- return [];
315
- }
316
- const sections = body
317
- .split(/\n(?=##\s+TASK-\d{4}\s+\|\s+(?:TODO|IN_PROGRESS|BLOCKED|DONE)\s+\|)/g)
318
- .map((section) => section.trim())
319
- .filter((section) => section.startsWith("## TASK-"));
320
- const tasks = [];
321
- for (const section of sections) {
322
- const lines = section.split(/\r?\n/);
323
- const header = lines[0]?.match(/^##\s+(TASK-\d{4})\s+\|\s+(TODO|IN_PROGRESS|BLOCKED|DONE)\s+\|\s+(.+)$/);
324
- if (!header) {
325
- continue;
326
- }
327
- const [, id, statusRaw, title] = header;
328
- const status = statusRaw;
329
- const taskDraft = {
330
- id,
331
- title: title.trim(),
332
- status,
333
- owner: "",
334
- summary: "",
335
- updatedAt: nowIso(),
336
- links: [],
337
- roadmapRefs: [],
338
- };
339
- let inLinks = false;
340
- let inHooks = false;
341
- // Convert to indexed for loop to allow skipping lines when parsing subState/blocker
342
- const sectionLines = lines.slice(1);
343
- for (let lineIndex = 0; lineIndex < sectionLines.length; lineIndex++) {
344
- const line = sectionLines[lineIndex];
345
- const trimmed = line.trim();
346
- if (!trimmed) {
347
- continue;
348
- }
349
- if (trimmed.startsWith("- owner:")) {
350
- taskDraft.owner = trimmed.replace("- owner:", "").trim();
351
- inLinks = false;
352
- inHooks = false;
353
- continue;
354
- }
355
- if (trimmed.startsWith("- summary:")) {
356
- taskDraft.summary = trimmed.replace("- summary:", "").trim();
357
- inLinks = false;
358
- inHooks = false;
359
- continue;
360
- }
361
- if (trimmed.startsWith("- updatedAt:")) {
362
- taskDraft.updatedAt = trimmed.replace("- updatedAt:", "").trim();
363
- inLinks = false;
364
- inHooks = false;
365
- continue;
366
- }
367
- if (trimmed.startsWith("- roadmapRefs:")) {
368
- const payload = trimmed.replace("- roadmapRefs:", "").trim();
369
- const refs = payload === "(none)"
370
- ? []
371
- : payload
372
- .split(",")
373
- .map((value) => value.trim())
374
- .filter((value) => value.length > 0);
375
- taskDraft.roadmapRefs = refs;
376
- inLinks = false;
377
- inHooks = false;
378
- continue;
379
- }
380
- if (trimmed === "- links:") {
381
- inLinks = true;
382
- inHooks = false;
383
- continue;
384
- }
385
- if (trimmed === "- hooks:") {
386
- inLinks = false;
387
- inHooks = true;
388
- continue;
389
- }
390
- // Handle subState nested field (Spec v1.1.0)
391
- if (trimmed.startsWith("- subState:")) {
392
- const { subState, endIndex } = parseSubState(sectionLines, lineIndex);
393
- if (Object.keys(subState).length > 0) {
394
- taskDraft.subState = subState;
395
- }
396
- // Skip to the end of subState parsing
397
- lineIndex = endIndex;
398
- inLinks = false;
399
- inHooks = false;
400
- continue;
401
- }
402
- // Handle blocker nested field (Spec v1.1.0)
403
- if (trimmed.startsWith("- blocker:")) {
404
- const { blocker, endIndex } = parseBlocker(sectionLines, lineIndex);
405
- if (blocker.type && blocker.description) {
406
- taskDraft.blocker = blocker;
407
- }
408
- // Skip to the end of blocker parsing
409
- lineIndex = endIndex;
410
- inLinks = false;
411
- inHooks = false;
412
- continue;
413
- }
414
- const nestedItem = trimmed.match(/^-\s+(.+)$/);
415
- if (!nestedItem) {
416
- continue;
417
- }
418
- const nestedValue = nestedItem[1].trim();
419
- if (nestedValue === "(none)") {
420
- continue;
421
- }
422
- if (inLinks) {
423
- taskDraft.links = [...(taskDraft.links ?? []), nestedValue];
424
- continue;
425
- }
426
- if (inHooks) {
427
- continue;
428
- }
429
- }
430
- tasks.push(normalizeTask(taskDraft));
431
- }
432
- return tasks;
433
- }
434
- export function findTaskIdsOutsideMarkers(markdown) {
435
- const start = markdown.indexOf(TASKS_START);
436
- const end = markdown.indexOf(TASKS_END);
437
- const outsideText = (start !== -1 && end !== -1 && end > start)
438
- ? `${markdown.slice(0, start)}\n${markdown.slice(end + TASKS_END.length)}`
439
- : markdown;
440
- const ids = outsideText.match(/TASK-\d{4}/g) ?? [];
441
- return Array.from(new Set(ids));
442
- }
443
- function collectTaskLintSuggestionItems(tasks, options = {}) {
221
+ function collectTaskLintSuggestionItems(tasks) {
444
222
  const suggestions = [];
445
- const { markdown, outsideMarkerScopeIds } = options;
446
223
  const duplicateIds = Array.from(tasks.reduce((counter, task) => {
447
224
  counter.set(task.id, (counter.get(task.id) ?? 0) + 1);
448
225
  return counter;
@@ -497,17 +274,6 @@ function collectTaskLintSuggestionItems(tasks, options = {}) {
497
274
  fixHint: "Bind at least one ROADMAP-xxxx when applicable.",
498
275
  });
499
276
  }
500
- if (typeof markdown === "string") {
501
- const outsideMarkerTaskIds = findTaskIdsOutsideMarkers(markdown)
502
- .filter((taskId) => (outsideMarkerScopeIds ? outsideMarkerScopeIds.has(taskId) : true));
503
- if (outsideMarkerTaskIds.length > 0) {
504
- suggestions.push({
505
- code: TASK_LINT_CODES.OUTSIDE_MARKER,
506
- message: `TASK IDs found outside marker block: ${outsideMarkerTaskIds.join(", ")}.`,
507
- fixHint: "Keep task source of truth inside marker region only.",
508
- });
509
- }
510
- }
511
277
  // ============================================================================
512
278
  // Spec v1.1.0 - Blocker Categorization Validation
513
279
  // ============================================================================
@@ -564,8 +330,8 @@ function collectTaskLintSuggestionItems(tasks, options = {}) {
564
330
  }
565
331
  return suggestions;
566
332
  }
567
- export function collectTaskLintSuggestions(tasks, markdown, outsideMarkerScopeIds) {
568
- return renderLintSuggestions(collectTaskLintSuggestionItems(tasks, { markdown, outsideMarkerScopeIds }));
333
+ export function collectTaskLintSuggestions(tasks) {
334
+ return renderLintSuggestions(collectTaskLintSuggestionItems(tasks));
569
335
  }
570
336
  function collectSingleTaskLintSuggestions(task) {
571
337
  const suggestions = [];
@@ -676,7 +442,7 @@ async function collectTaskFileLintSuggestions(governanceDir, task) {
676
442
  return renderLintSuggestions(suggestions);
677
443
  }
678
444
  export function renderTasksMarkdown(tasks) {
679
- const sections = tasks.map((task) => {
445
+ const sections = sortTasksNewestFirst(tasks).map((task) => {
680
446
  const roadmapRefs = task.roadmapRefs.length > 0 ? task.roadmapRefs.join(", ") : "(none)";
681
447
  const links = task.links.length > 0
682
448
  ? ["- links:", ...task.links.map((link) => ` - ${link}`)]
@@ -722,22 +488,19 @@ export function renderTasksMarkdown(tasks) {
722
488
  return [
723
489
  "# Tasks",
724
490
  "",
725
- "This file is maintained by Projitive MCP. If editing manually, please keep the Markdown structure valid.",
491
+ "This file is generated from .projitive sqlite tables by Projitive MCP. Manual edits will be overwritten.",
726
492
  "",
727
- TASKS_START,
728
493
  ...(sections.length > 0 ? sections : ["(no tasks)"]),
729
- TASKS_END,
730
494
  "",
731
495
  ].join("\n");
732
496
  }
733
497
  export async function ensureTasksFile(inputPath) {
734
498
  const governanceDir = await resolveGovernanceDir(inputPath);
735
- const tasksPath = path.join(governanceDir, "tasks.md");
499
+ const { tasksPath, markdownPath } = resolveTaskArtifactPaths(governanceDir);
736
500
  await fs.mkdir(governanceDir, { recursive: true });
737
- const accessResult = await catchIt(fs.access(tasksPath));
738
- if (accessResult.isError()) {
739
- await fs.writeFile(tasksPath, renderTasksMarkdown([]), "utf-8");
740
- }
501
+ await ensureStore(tasksPath);
502
+ const tasks = normalizeAndSortTasks(await loadTasksFromStore(tasksPath));
503
+ await syncTasksMarkdownView(tasksPath, markdownPath, renderTasksMarkdown(tasks));
741
504
  return tasksPath;
742
505
  }
743
506
  export async function loadTasks(inputPath) {
@@ -745,13 +508,21 @@ export async function loadTasks(inputPath) {
745
508
  return { tasksPath, tasks };
746
509
  }
747
510
  export async function loadTasksDocument(inputPath) {
511
+ return loadTasksDocumentWithOptions(inputPath, false);
512
+ }
513
+ export async function loadTasksDocumentWithOptions(inputPath, forceViewSync) {
748
514
  const tasksPath = await ensureTasksFile(inputPath);
749
- const markdown = await fs.readFile(tasksPath, "utf-8");
750
- return { tasksPath, markdown, tasks: await parseTasksBlock(markdown) };
515
+ const tasks = normalizeAndSortTasks(await loadTasksFromStore(tasksPath));
516
+ const markdown = renderTasksMarkdown(tasks);
517
+ const markdownPath = path.join(path.dirname(tasksPath), TASKS_MARKDOWN_FILE);
518
+ await syncTasksMarkdownView(tasksPath, markdownPath, markdown, forceViewSync);
519
+ return { tasksPath, markdownPath, markdown, tasks };
751
520
  }
752
521
  export async function saveTasks(tasksPath, tasks) {
753
- const normalized = tasks.map((task) => normalizeTask(task));
754
- await fs.writeFile(tasksPath, renderTasksMarkdown(normalized), "utf-8");
522
+ const normalized = normalizeAndSortTasks(tasks);
523
+ const markdownPath = path.join(path.dirname(tasksPath), TASKS_MARKDOWN_FILE);
524
+ await replaceTasksInStore(tasksPath, normalized);
525
+ await syncTasksMarkdownView(tasksPath, markdownPath, renderTasksMarkdown(normalized));
755
526
  }
756
527
  export function validateTransition(from, to) {
757
528
  if (from === to) {
@@ -776,11 +547,11 @@ export function registerTaskTools(server) {
776
547
  },
777
548
  }, async ({ projectPath, status, limit }) => {
778
549
  const governanceDir = await resolveGovernanceDir(projectPath);
779
- const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
550
+ const { tasks } = await loadTasksDocument(governanceDir);
780
551
  const filtered = tasks
781
552
  .filter((task) => (status ? task.status === status : true))
782
553
  .slice(0, limit ?? 100);
783
- const lintSuggestions = collectTaskLintSuggestions(filtered, tasksMarkdown, new Set(filtered.map((task) => task.id)));
554
+ const lintSuggestions = collectTaskLintSuggestions(filtered);
784
555
  if (status && filtered.length === 0) {
785
556
  appendLintSuggestions(lintSuggestions, [
786
557
  {
@@ -796,7 +567,6 @@ export function registerTaskTools(server) {
796
567
  sections: [
797
568
  summarySection([
798
569
  `- governanceDir: ${governanceDir}`,
799
- `- tasksPath: ${tasksPath}`,
800
570
  `- filter.status: ${status ?? "(none)"}`,
801
571
  `- returned: ${filtered.length}`,
802
572
  ]),
@@ -826,21 +596,18 @@ export function registerTaskTools(server) {
826
596
  const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
827
597
  if (rankedCandidates.length === 0) {
828
598
  const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
829
- const { tasksPath, tasks } = await loadTasks(governanceDir);
599
+ const tasksPath = path.join(governanceDir, ".projitive");
600
+ await ensureStore(tasksPath);
601
+ const stats = await loadTaskStatusStatsFromStore(tasksPath);
830
602
  const roadmapIds = await readRoadmapIds(governanceDir);
831
- const todo = tasks.filter((task) => task.status === "TODO").length;
832
- const inProgress = tasks.filter((task) => task.status === "IN_PROGRESS").length;
833
- const blocked = tasks.filter((task) => task.status === "BLOCKED").length;
834
- const done = tasks.filter((task) => task.status === "DONE").length;
835
603
  return {
836
604
  governanceDir,
837
- tasksPath,
838
605
  roadmapIds,
839
- total: tasks.length,
840
- todo,
841
- inProgress,
842
- blocked,
843
- done,
606
+ total: stats.total,
607
+ todo: stats.todo,
608
+ inProgress: stats.inProgress,
609
+ blocked: stats.blocked,
610
+ done: stats.done,
844
611
  };
845
612
  }));
846
613
  const preferredProject = projectSnapshots[0];
@@ -859,7 +626,7 @@ export function registerTaskTools(server) {
859
626
  evidenceSection([
860
627
  "### Project Snapshots",
861
628
  ...(projectSnapshots.length > 0
862
- ? 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)"} | tasksPath=${item.tasksPath}`)
629
+ ? 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)"}`)
863
630
  : ["- (none)"]),
864
631
  "",
865
632
  "### Seed Task Template",
@@ -868,16 +635,17 @@ export function registerTaskTools(server) {
868
635
  guidanceSection([
869
636
  "- No TODO/IN_PROGRESS task is available.",
870
637
  "- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.",
638
+ "- If roadmap has active milestones, analyze milestone intent and split into 1-3 executable TODO tasks.",
871
639
  "",
872
640
  "### No-Task Discovery Checklist",
873
641
  ...noTaskDiscoveryGuidance,
874
642
  "",
875
643
  "- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.",
876
644
  "- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.",
877
- "- After adding tasks inside marker block, rerun `taskNext` to re-rank actionable work.",
645
+ "- After creating tasks, rerun `taskNext` to re-rank actionable work.",
878
646
  ]),
879
647
  lintSection([
880
- "- No actionable tasks found. Verify task statuses and required fields in marker block.",
648
+ "- No actionable tasks found. Verify task statuses and required fields in .projitive task table.",
881
649
  "- Ensure each new task has stable TASK-xxxx ID and at least one roadmapRefs item.",
882
650
  ]),
883
651
  nextCallSection(preferredProject
@@ -889,13 +657,13 @@ export function registerTaskTools(server) {
889
657
  }
890
658
  const selected = rankedCandidates[0];
891
659
  const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
892
- const lintSuggestions = collectTaskLintSuggestions(selectedTaskDocument.tasks, selectedTaskDocument.markdown);
660
+ const lintSuggestions = collectTaskLintSuggestions(selectedTaskDocument.tasks);
893
661
  const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
894
662
  const fileCandidates = candidateFilesFromArtifacts(artifacts);
895
663
  const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
896
- const taskLocation = (await findTextReferences(selected.tasksPath, selected.task.id))[0];
664
+ const taskLocation = (await findTextReferences(selectedTaskDocument.markdownPath, selected.task.id))[0];
897
665
  const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
898
- const suggestedReadOrder = [selected.tasksPath, ...relatedArtifacts.filter((item) => item !== selected.tasksPath)];
666
+ const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
899
667
  const candidateLimit = limit ?? 5;
900
668
  const markdown = renderToolResponseMarkdown({
901
669
  toolName: "taskNext",
@@ -917,7 +685,7 @@ export function registerTaskTools(server) {
917
685
  `- owner: ${selected.task.owner || "(none)"}`,
918
686
  `- updatedAt: ${selected.task.updatedAt}`,
919
687
  `- roadmapRefs: ${selected.task.roadmapRefs.join(", ") || "(none)"}`,
920
- `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selected.tasksPath}`,
688
+ `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selectedTaskDocument.markdownPath}`,
921
689
  "",
922
690
  "### Top Candidates",
923
691
  ...rankedCandidates
@@ -965,7 +733,7 @@ export function registerTaskTools(server) {
965
733
  };
966
734
  }
967
735
  const governanceDir = await resolveGovernanceDir(projectPath);
968
- const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
736
+ const { markdownPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
969
737
  const task = tasks.find((item) => item.id === taskId);
970
738
  if (!task) {
971
739
  return {
@@ -977,22 +745,13 @@ export function registerTaskTools(server) {
977
745
  ...collectSingleTaskLintSuggestions(task),
978
746
  ...(await collectTaskFileLintSuggestions(governanceDir, task)),
979
747
  ];
980
- const outsideMarkerTaskIds = findTaskIdsOutsideMarkers(tasksMarkdown);
981
- if (outsideMarkerTaskIds.includes(task.id)) {
982
- appendLintSuggestions(lintSuggestions, [
983
- {
984
- code: TASK_LINT_CODES.OUTSIDE_MARKER,
985
- message: `Current task ID appears outside marker block (${task.id}).`,
986
- fixHint: "Keep task source of truth inside marker region.",
987
- },
988
- ]);
989
- }
990
- const taskLocation = (await findTextReferences(tasksPath, taskId))[0];
748
+ const contextReadingGuidance = await resolveTaskContextReadingGuidance(governanceDir);
749
+ const taskLocation = (await findTextReferences(markdownPath, taskId))[0];
991
750
  const artifacts = await discoverGovernanceArtifacts(governanceDir);
992
751
  const fileCandidates = candidateFilesFromArtifacts(artifacts);
993
752
  const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
994
753
  const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
995
- const suggestedReadOrder = [tasksPath, ...relatedArtifacts.filter((item) => item !== tasksPath)];
754
+ const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
996
755
  // Build summary with subState and blocker info (v1.1.0)
997
756
  const summaryLines = [
998
757
  `- governanceDir: ${governanceDir}`,
@@ -1002,7 +761,7 @@ export function registerTaskTools(server) {
1002
761
  `- owner: ${task.owner}`,
1003
762
  `- updatedAt: ${task.updatedAt}`,
1004
763
  `- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
1005
- `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : tasksPath}`,
764
+ `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
1006
765
  ];
1007
766
  // Add subState info for IN_PROGRESS tasks (v1.1.0)
1008
767
  if (task.subState && task.status === "IN_PROGRESS") {
@@ -1050,9 +809,13 @@ export function registerTaskTools(server) {
1050
809
  ]),
1051
810
  guidanceSection([
1052
811
  "- Read the files in Suggested Read Order.",
812
+ "",
813
+ "### Recommended Context Reading",
814
+ ...contextReadingGuidance,
815
+ "",
1053
816
  "- Verify whether current status and evidence are consistent.",
1054
817
  ...taskStatusGuidance(task),
1055
- "- If updates are needed, edit tasks/designs/reports markdown directly and keep TASK IDs unchanged.",
818
+ "- If updates are needed, use tool writes for sqlite source (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.",
1056
819
  "- After editing, re-run `taskContext` to verify references and context consistency.",
1057
820
  ]),
1058
821
  lintSection(lintSuggestions),
@@ -1096,7 +859,7 @@ export function registerTaskTools(server) {
1096
859
  };
1097
860
  }
1098
861
  const governanceDir = await resolveGovernanceDir(projectPath);
1099
- const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
862
+ const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
1100
863
  const taskIndex = tasks.findIndex((item) => item.id === taskId);
1101
864
  if (taskIndex === -1) {
1102
865
  return {
@@ -1147,8 +910,17 @@ export function registerTaskTools(server) {
1147
910
  }
1148
911
  // Update updatedAt
1149
912
  task.updatedAt = nowIso();
1150
- // Save tasks
1151
- await saveTasks(tasksPath, tasks);
913
+ const normalizedTask = normalizeTask(task);
914
+ // Save task incrementally
915
+ await upsertTaskInStore(tasksPath, normalizedTask);
916
+ task.status = normalizedTask.status;
917
+ task.owner = normalizedTask.owner;
918
+ task.summary = normalizedTask.summary;
919
+ task.roadmapRefs = normalizedTask.roadmapRefs;
920
+ task.links = normalizedTask.links;
921
+ task.updatedAt = normalizedTask.updatedAt;
922
+ task.subState = normalizedTask.subState;
923
+ task.blocker = normalizedTask.blocker;
1152
924
  // Build response
1153
925
  const updateSummary = [
1154
926
  `- taskId: ${taskId}`,
@@ -1199,6 +971,8 @@ export function registerTaskTools(server) {
1199
971
  "Task updated successfully. Run `taskContext` to verify the changes.",
1200
972
  "If status changed to DONE, ensure evidence links are added.",
1201
973
  "If subState or blocker were updated, verify the metadata is correct.",
974
+ "SQLite is source of truth; tasks.md is a generated view and may be overwritten.",
975
+ "Call `syncViews(projectPath=..., views=[\"tasks\"], force=true)` when immediate markdown materialization is required.",
1202
976
  ]),
1203
977
  lintSection([]),
1204
978
  nextCallSection(`taskContext(projectPath=\"${toProjectPath(governanceDir)}\", taskId=\"${taskId}\")`),