@projitive/mcp 1.1.2 → 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.
- package/README.md +115 -422
- package/output/package.json +4 -1
- package/output/source/common/files.js +1 -1
- package/output/source/common/index.js +1 -0
- package/output/source/common/migrations/runner.js +68 -0
- package/output/source/common/migrations/steps.js +55 -0
- package/output/source/common/migrations/types.js +1 -0
- package/output/source/common/response.js +147 -1
- package/output/source/common/store.js +623 -0
- package/output/source/common/store.test.js +164 -0
- package/output/source/index.js +1 -1
- package/output/source/prompts/quickStart.js +33 -7
- package/output/source/prompts/taskDiscovery.js +23 -9
- package/output/source/prompts/taskExecution.js +18 -8
- package/output/source/resources/governance.js +2 -2
- package/output/source/tools/project.js +254 -119
- package/output/source/tools/project.test.js +33 -11
- package/output/source/tools/roadmap.js +166 -16
- package/output/source/tools/roadmap.test.js +19 -55
- package/output/source/tools/task.js +152 -376
- package/output/source/tools/task.test.js +64 -392
- package/output/source/types.js +0 -9
- package/package.json +4 -1
|
@@ -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 {
|
|
7
|
-
import { resolveGovernanceDir, resolveScanDepth,
|
|
6
|
+
import { TASK_LINT_CODES, renderLintSuggestions } from "../common/index.js";
|
|
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
|
-
"-
|
|
52
|
-
"-
|
|
53
|
-
"-
|
|
54
|
-
"-
|
|
55
|
-
"-
|
|
56
|
-
"-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
68
|
+
const dbPath = path.join(governanceDir, ".projitive");
|
|
93
69
|
try {
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
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
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
|
568
|
-
return renderLintSuggestions(collectTaskLintSuggestionItems(tasks
|
|
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
|
|
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 =
|
|
499
|
+
const { tasksPath, markdownPath } = resolveTaskArtifactPaths(governanceDir);
|
|
736
500
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
|
750
|
-
|
|
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
|
|
754
|
-
|
|
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 {
|
|
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
|
|
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
|
]),
|
|
@@ -820,27 +590,24 @@ export function registerTaskTools(server) {
|
|
|
820
590
|
limit: z.number().int().min(1).max(20).optional(),
|
|
821
591
|
},
|
|
822
592
|
}, async ({ limit }) => {
|
|
823
|
-
const
|
|
593
|
+
const roots = resolveScanRoots();
|
|
824
594
|
const depth = resolveScanDepth();
|
|
825
|
-
const projects = await
|
|
595
|
+
const projects = await discoverProjectsAcrossRoots(roots, depth);
|
|
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
|
|
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:
|
|
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];
|
|
@@ -850,7 +617,8 @@ export function registerTaskTools(server) {
|
|
|
850
617
|
toolName: "taskNext",
|
|
851
618
|
sections: [
|
|
852
619
|
summarySection([
|
|
853
|
-
`-
|
|
620
|
+
`- rootPaths: ${roots.join(", ")}`,
|
|
621
|
+
`- rootCount: ${roots.length}`,
|
|
854
622
|
`- maxDepth: ${depth}`,
|
|
855
623
|
`- matchedProjects: ${projects.length}`,
|
|
856
624
|
"- actionableTasks: 0",
|
|
@@ -858,7 +626,7 @@ export function registerTaskTools(server) {
|
|
|
858
626
|
evidenceSection([
|
|
859
627
|
"### Project Snapshots",
|
|
860
628
|
...(projectSnapshots.length > 0
|
|
861
|
-
? 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)"}
|
|
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)"}`)
|
|
862
630
|
: ["- (none)"]),
|
|
863
631
|
"",
|
|
864
632
|
"### Seed Task Template",
|
|
@@ -867,16 +635,17 @@ export function registerTaskTools(server) {
|
|
|
867
635
|
guidanceSection([
|
|
868
636
|
"- No TODO/IN_PROGRESS task is available.",
|
|
869
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.",
|
|
870
639
|
"",
|
|
871
640
|
"### No-Task Discovery Checklist",
|
|
872
641
|
...noTaskDiscoveryGuidance,
|
|
873
642
|
"",
|
|
874
643
|
"- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.",
|
|
875
644
|
"- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.",
|
|
876
|
-
"- After
|
|
645
|
+
"- After creating tasks, rerun `taskNext` to re-rank actionable work.",
|
|
877
646
|
]),
|
|
878
647
|
lintSection([
|
|
879
|
-
"- No actionable tasks found. Verify task statuses and required fields in
|
|
648
|
+
"- No actionable tasks found. Verify task statuses and required fields in .projitive task table.",
|
|
880
649
|
"- Ensure each new task has stable TASK-xxxx ID and at least one roadmapRefs item.",
|
|
881
650
|
]),
|
|
882
651
|
nextCallSection(preferredProject
|
|
@@ -888,19 +657,20 @@ export function registerTaskTools(server) {
|
|
|
888
657
|
}
|
|
889
658
|
const selected = rankedCandidates[0];
|
|
890
659
|
const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
|
|
891
|
-
const lintSuggestions = collectTaskLintSuggestions(selectedTaskDocument.tasks
|
|
660
|
+
const lintSuggestions = collectTaskLintSuggestions(selectedTaskDocument.tasks);
|
|
892
661
|
const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
|
|
893
662
|
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
894
663
|
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
|
|
895
|
-
const taskLocation = (await findTextReferences(
|
|
664
|
+
const taskLocation = (await findTextReferences(selectedTaskDocument.markdownPath, selected.task.id))[0];
|
|
896
665
|
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
897
|
-
const suggestedReadOrder = [
|
|
666
|
+
const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
|
|
898
667
|
const candidateLimit = limit ?? 5;
|
|
899
668
|
const markdown = renderToolResponseMarkdown({
|
|
900
669
|
toolName: "taskNext",
|
|
901
670
|
sections: [
|
|
902
671
|
summarySection([
|
|
903
|
-
`-
|
|
672
|
+
`- rootPaths: ${roots.join(", ")}`,
|
|
673
|
+
`- rootCount: ${roots.length}`,
|
|
904
674
|
`- maxDepth: ${depth}`,
|
|
905
675
|
`- matchedProjects: ${projects.length}`,
|
|
906
676
|
`- actionableTasks: ${rankedCandidates.length}`,
|
|
@@ -915,7 +685,7 @@ export function registerTaskTools(server) {
|
|
|
915
685
|
`- owner: ${selected.task.owner || "(none)"}`,
|
|
916
686
|
`- updatedAt: ${selected.task.updatedAt}`,
|
|
917
687
|
`- roadmapRefs: ${selected.task.roadmapRefs.join(", ") || "(none)"}`,
|
|
918
|
-
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` :
|
|
688
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selectedTaskDocument.markdownPath}`,
|
|
919
689
|
"",
|
|
920
690
|
"### Top Candidates",
|
|
921
691
|
...rankedCandidates
|
|
@@ -963,7 +733,7 @@ export function registerTaskTools(server) {
|
|
|
963
733
|
};
|
|
964
734
|
}
|
|
965
735
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
966
|
-
const {
|
|
736
|
+
const { markdownPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
|
|
967
737
|
const task = tasks.find((item) => item.id === taskId);
|
|
968
738
|
if (!task) {
|
|
969
739
|
return {
|
|
@@ -975,22 +745,13 @@ export function registerTaskTools(server) {
|
|
|
975
745
|
...collectSingleTaskLintSuggestions(task),
|
|
976
746
|
...(await collectTaskFileLintSuggestions(governanceDir, task)),
|
|
977
747
|
];
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
appendLintSuggestions(lintSuggestions, [
|
|
981
|
-
{
|
|
982
|
-
code: TASK_LINT_CODES.OUTSIDE_MARKER,
|
|
983
|
-
message: `Current task ID appears outside marker block (${task.id}).`,
|
|
984
|
-
fixHint: "Keep task source of truth inside marker region.",
|
|
985
|
-
},
|
|
986
|
-
]);
|
|
987
|
-
}
|
|
988
|
-
const taskLocation = (await findTextReferences(tasksPath, taskId))[0];
|
|
748
|
+
const contextReadingGuidance = await resolveTaskContextReadingGuidance(governanceDir);
|
|
749
|
+
const taskLocation = (await findTextReferences(markdownPath, taskId))[0];
|
|
989
750
|
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
990
751
|
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
991
752
|
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
|
|
992
753
|
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
993
|
-
const suggestedReadOrder = [
|
|
754
|
+
const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
|
|
994
755
|
// Build summary with subState and blocker info (v1.1.0)
|
|
995
756
|
const summaryLines = [
|
|
996
757
|
`- governanceDir: ${governanceDir}`,
|
|
@@ -1000,7 +761,7 @@ export function registerTaskTools(server) {
|
|
|
1000
761
|
`- owner: ${task.owner}`,
|
|
1001
762
|
`- updatedAt: ${task.updatedAt}`,
|
|
1002
763
|
`- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
|
|
1003
|
-
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` :
|
|
764
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
|
|
1004
765
|
];
|
|
1005
766
|
// Add subState info for IN_PROGRESS tasks (v1.1.0)
|
|
1006
767
|
if (task.subState && task.status === "IN_PROGRESS") {
|
|
@@ -1048,9 +809,13 @@ export function registerTaskTools(server) {
|
|
|
1048
809
|
]),
|
|
1049
810
|
guidanceSection([
|
|
1050
811
|
"- Read the files in Suggested Read Order.",
|
|
812
|
+
"",
|
|
813
|
+
"### Recommended Context Reading",
|
|
814
|
+
...contextReadingGuidance,
|
|
815
|
+
"",
|
|
1051
816
|
"- Verify whether current status and evidence are consistent.",
|
|
1052
817
|
...taskStatusGuidance(task),
|
|
1053
|
-
"- If updates are needed,
|
|
818
|
+
"- If updates are needed, use tool writes for sqlite source (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.",
|
|
1054
819
|
"- After editing, re-run `taskContext` to verify references and context consistency.",
|
|
1055
820
|
]),
|
|
1056
821
|
lintSection(lintSuggestions),
|
|
@@ -1094,7 +859,7 @@ export function registerTaskTools(server) {
|
|
|
1094
859
|
};
|
|
1095
860
|
}
|
|
1096
861
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1097
|
-
const { tasksPath, tasks
|
|
862
|
+
const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
|
|
1098
863
|
const taskIndex = tasks.findIndex((item) => item.id === taskId);
|
|
1099
864
|
if (taskIndex === -1) {
|
|
1100
865
|
return {
|
|
@@ -1145,8 +910,17 @@ export function registerTaskTools(server) {
|
|
|
1145
910
|
}
|
|
1146
911
|
// Update updatedAt
|
|
1147
912
|
task.updatedAt = nowIso();
|
|
1148
|
-
|
|
1149
|
-
|
|
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;
|
|
1150
924
|
// Build response
|
|
1151
925
|
const updateSummary = [
|
|
1152
926
|
`- taskId: ${taskId}`,
|
|
@@ -1197,6 +971,8 @@ export function registerTaskTools(server) {
|
|
|
1197
971
|
"Task updated successfully. Run `taskContext` to verify the changes.",
|
|
1198
972
|
"If status changed to DONE, ensure evidence links are added.",
|
|
1199
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.",
|
|
1200
976
|
]),
|
|
1201
977
|
lintSection([]),
|
|
1202
978
|
nextCallSection(`taskContext(projectPath=\"${toProjectPath(governanceDir)}\", taskId=\"${taskId}\")`),
|