@projitive/mcp 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -423
- 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/linter.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/resources/readme.test.js +2 -2
- package/output/source/tools/project.js +206 -111
- package/output/source/tools/project.test.js +6 -3
- package/output/source/tools/roadmap.js +166 -16
- package/output/source/tools/roadmap.test.js +19 -55
- package/output/source/tools/task.js +206 -374
- package/output/source/tools/task.test.js +84 -388
- package/output/source/types.js +1 -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 {
|
|
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
|
-
"-
|
|
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/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.",
|
|
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 [];
|
|
@@ -108,27 +85,52 @@ export function renderTaskSeedTemplate(roadmapRef) {
|
|
|
108
85
|
"- updatedAt: 2026-01-01T00:00:00.000Z",
|
|
109
86
|
`- roadmapRefs: ${roadmapRef}`,
|
|
110
87
|
"- links:",
|
|
111
|
-
" -
|
|
112
|
-
" -
|
|
88
|
+
" - README.md",
|
|
89
|
+
" - .projitive/roadmap.md",
|
|
113
90
|
"```",
|
|
114
91
|
];
|
|
115
92
|
}
|
|
93
|
+
function isHttpUrl(value) {
|
|
94
|
+
return /^https?:\/\//i.test(value);
|
|
95
|
+
}
|
|
96
|
+
function isProjectRootRelativePath(value) {
|
|
97
|
+
return value.length > 0
|
|
98
|
+
&& !value.startsWith("/")
|
|
99
|
+
&& !value.startsWith("./")
|
|
100
|
+
&& !value.startsWith("../")
|
|
101
|
+
&& !/^[A-Za-z]:\//.test(value);
|
|
102
|
+
}
|
|
103
|
+
function normalizeTaskLink(link) {
|
|
104
|
+
const trimmed = link.trim();
|
|
105
|
+
if (trimmed.length === 0 || isHttpUrl(trimmed)) {
|
|
106
|
+
return trimmed;
|
|
107
|
+
}
|
|
108
|
+
const slashNormalized = trimmed.replace(/\\/g, "/");
|
|
109
|
+
const withoutDotPrefix = slashNormalized.replace(/^\.\//, "");
|
|
110
|
+
return withoutDotPrefix.replace(/^\/+/, "");
|
|
111
|
+
}
|
|
112
|
+
function resolveTaskLinkPath(projectPath, link) {
|
|
113
|
+
return path.join(projectPath, link);
|
|
114
|
+
}
|
|
116
115
|
async function readActionableTaskCandidates(governanceDirs) {
|
|
117
116
|
const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
|
|
118
|
-
const
|
|
117
|
+
const tasksPath = path.join(governanceDir, ".projitive");
|
|
118
|
+
await ensureStore(tasksPath);
|
|
119
|
+
const [stats, actionableTasks] = await Promise.all([
|
|
120
|
+
loadTaskStatusStatsFromStore(tasksPath),
|
|
121
|
+
loadActionableTasksFromStore(tasksPath),
|
|
122
|
+
]);
|
|
119
123
|
return {
|
|
120
124
|
governanceDir,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
projectLatestUpdatedAt: latestTaskUpdatedAt(snapshot.tasks),
|
|
125
|
+
tasks: actionableTasks,
|
|
126
|
+
projectScore: stats.inProgress * 2 + stats.todo,
|
|
127
|
+
projectLatestUpdatedAt: stats.latestUpdatedAt || "(unknown)",
|
|
125
128
|
};
|
|
126
129
|
}));
|
|
127
130
|
return snapshots.flatMap((item) => item.tasks
|
|
128
131
|
.filter((task) => task.status === "IN_PROGRESS" || task.status === "TODO")
|
|
129
132
|
.map((task) => ({
|
|
130
133
|
governanceDir: item.governanceDir,
|
|
131
|
-
tasksPath: item.tasksPath,
|
|
132
134
|
task,
|
|
133
135
|
projectScore: item.projectScore,
|
|
134
136
|
projectLatestUpdatedAt: item.projectLatestUpdatedAt,
|
|
@@ -155,6 +157,49 @@ export function toTaskUpdatedAtMs(updatedAt) {
|
|
|
155
157
|
const timestamp = new Date(updatedAt).getTime();
|
|
156
158
|
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
157
159
|
}
|
|
160
|
+
function toTaskIdNumericSuffix(taskId) {
|
|
161
|
+
const match = taskId.match(/^(?:TASK-)(\d{4})$/);
|
|
162
|
+
if (!match) {
|
|
163
|
+
return -1;
|
|
164
|
+
}
|
|
165
|
+
return Number.parseInt(match[1], 10);
|
|
166
|
+
}
|
|
167
|
+
export function sortTasksNewestFirst(tasks) {
|
|
168
|
+
return [...tasks].sort((a, b) => {
|
|
169
|
+
const updatedAtDelta = toTaskUpdatedAtMs(b.updatedAt) - toTaskUpdatedAtMs(a.updatedAt);
|
|
170
|
+
if (updatedAtDelta !== 0) {
|
|
171
|
+
return updatedAtDelta;
|
|
172
|
+
}
|
|
173
|
+
const idDelta = toTaskIdNumericSuffix(b.id) - toTaskIdNumericSuffix(a.id);
|
|
174
|
+
if (idDelta !== 0) {
|
|
175
|
+
return idDelta;
|
|
176
|
+
}
|
|
177
|
+
return b.id.localeCompare(a.id);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
function normalizeAndSortTasks(tasks) {
|
|
181
|
+
return sortTasksNewestFirst(tasks.map((task) => normalizeTask(task)));
|
|
182
|
+
}
|
|
183
|
+
function resolveTaskArtifactPaths(governanceDir) {
|
|
184
|
+
return {
|
|
185
|
+
tasksPath: path.join(governanceDir, ".projitive"),
|
|
186
|
+
markdownPath: path.join(governanceDir, TASKS_MARKDOWN_FILE),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
async function syncTasksMarkdownView(tasksPath, markdownPath, markdown, force = false) {
|
|
190
|
+
const sourceVersion = await getStoreVersion(tasksPath, "tasks");
|
|
191
|
+
const viewState = await getMarkdownViewState(tasksPath, "tasks_markdown");
|
|
192
|
+
const markdownExists = await fs.access(markdownPath).then(() => true).catch(() => false);
|
|
193
|
+
const shouldWrite = force
|
|
194
|
+
|| !markdownExists
|
|
195
|
+
|| viewState.dirty
|
|
196
|
+
|| viewState.lastSourceVersion !== sourceVersion;
|
|
197
|
+
if (!shouldWrite) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
await fs.writeFile(markdownPath, markdown, "utf-8");
|
|
201
|
+
await markMarkdownViewBuilt(tasksPath, "tasks_markdown", sourceVersion);
|
|
202
|
+
}
|
|
158
203
|
export function rankActionableTaskCandidates(candidates) {
|
|
159
204
|
return [...candidates].sort((a, b) => {
|
|
160
205
|
if (b.projectScore !== a.projectScore) {
|
|
@@ -172,114 +217,6 @@ export function rankActionableTaskCandidates(candidates) {
|
|
|
172
217
|
return a.task.id.localeCompare(b.task.id);
|
|
173
218
|
});
|
|
174
219
|
}
|
|
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
220
|
export function normalizeTask(task) {
|
|
284
221
|
const normalizedRoadmapRefs = Array.isArray(task.roadmapRefs)
|
|
285
222
|
? task.roadmapRefs.map(String).filter((value) => isValidRoadmapId(value))
|
|
@@ -291,7 +228,12 @@ export function normalizeTask(task) {
|
|
|
291
228
|
owner: task.owner ? String(task.owner) : "",
|
|
292
229
|
summary: task.summary ? String(task.summary) : "",
|
|
293
230
|
updatedAt: task.updatedAt ? String(task.updatedAt) : nowIso(),
|
|
294
|
-
links: Array.isArray(task.links)
|
|
231
|
+
links: Array.isArray(task.links)
|
|
232
|
+
? Array.from(new Set(task.links
|
|
233
|
+
.map(String)
|
|
234
|
+
.map((value) => normalizeTaskLink(value))
|
|
235
|
+
.filter((value) => value.length > 0)))
|
|
236
|
+
: [],
|
|
295
237
|
roadmapRefs: Array.from(new Set(normalizedRoadmapRefs)),
|
|
296
238
|
};
|
|
297
239
|
// Include optional v1.1.0 fields if present
|
|
@@ -303,146 +245,8 @@ export function normalizeTask(task) {
|
|
|
303
245
|
}
|
|
304
246
|
return normalized;
|
|
305
247
|
}
|
|
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 = {}) {
|
|
248
|
+
function collectTaskLintSuggestionItems(tasks) {
|
|
444
249
|
const suggestions = [];
|
|
445
|
-
const { markdown, outsideMarkerScopeIds } = options;
|
|
446
250
|
const duplicateIds = Array.from(tasks.reduce((counter, task) => {
|
|
447
251
|
counter.set(task.id, (counter.get(task.id) ?? 0) + 1);
|
|
448
252
|
return counter;
|
|
@@ -497,16 +301,16 @@ function collectTaskLintSuggestionItems(tasks, options = {}) {
|
|
|
497
301
|
fixHint: "Bind at least one ROADMAP-xxxx when applicable.",
|
|
498
302
|
});
|
|
499
303
|
}
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
304
|
+
const invalidLinkPathFormat = tasks.filter((task) => task.links.some((link) => {
|
|
305
|
+
const normalized = link.trim();
|
|
306
|
+
return normalized.length > 0 && !isHttpUrl(normalized) && !isProjectRootRelativePath(normalized);
|
|
307
|
+
}));
|
|
308
|
+
if (invalidLinkPathFormat.length > 0) {
|
|
309
|
+
suggestions.push({
|
|
310
|
+
code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
|
|
311
|
+
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.",
|
|
313
|
+
});
|
|
510
314
|
}
|
|
511
315
|
// ============================================================================
|
|
512
316
|
// Spec v1.1.0 - Blocker Categorization Validation
|
|
@@ -564,8 +368,8 @@ function collectTaskLintSuggestionItems(tasks, options = {}) {
|
|
|
564
368
|
}
|
|
565
369
|
return suggestions;
|
|
566
370
|
}
|
|
567
|
-
export function collectTaskLintSuggestions(tasks
|
|
568
|
-
return renderLintSuggestions(collectTaskLintSuggestionItems(tasks
|
|
371
|
+
export function collectTaskLintSuggestions(tasks) {
|
|
372
|
+
return renderLintSuggestions(collectTaskLintSuggestionItems(tasks));
|
|
569
373
|
}
|
|
570
374
|
function collectSingleTaskLintSuggestions(task) {
|
|
571
375
|
const suggestions = [];
|
|
@@ -583,6 +387,17 @@ function collectSingleTaskLintSuggestions(task) {
|
|
|
583
387
|
fixHint: "Add at least one evidence link.",
|
|
584
388
|
});
|
|
585
389
|
}
|
|
390
|
+
const invalidLinkPathFormat = task.links.some((link) => {
|
|
391
|
+
const normalized = link.trim();
|
|
392
|
+
return normalized.length > 0 && !isHttpUrl(normalized) && !isProjectRootRelativePath(normalized);
|
|
393
|
+
});
|
|
394
|
+
if (invalidLinkPathFormat) {
|
|
395
|
+
suggestions.push({
|
|
396
|
+
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.",
|
|
399
|
+
});
|
|
400
|
+
}
|
|
586
401
|
if (task.status === "BLOCKED" && task.summary.trim().length === 0) {
|
|
587
402
|
suggestions.push({
|
|
588
403
|
code: TASK_LINT_CODES.BLOCKED_SUMMARY_EMPTY,
|
|
@@ -656,6 +471,7 @@ function collectSingleTaskLintSuggestions(task) {
|
|
|
656
471
|
}
|
|
657
472
|
async function collectTaskFileLintSuggestions(governanceDir, task) {
|
|
658
473
|
const suggestions = [];
|
|
474
|
+
const projectPath = toProjectPath(governanceDir);
|
|
659
475
|
for (const link of task.links) {
|
|
660
476
|
const normalized = link.trim();
|
|
661
477
|
if (normalized.length === 0) {
|
|
@@ -664,7 +480,15 @@ async function collectTaskFileLintSuggestions(governanceDir, task) {
|
|
|
664
480
|
if (/^https?:\/\//i.test(normalized)) {
|
|
665
481
|
continue;
|
|
666
482
|
}
|
|
667
|
-
|
|
483
|
+
if (!isProjectRootRelativePath(normalized)) {
|
|
484
|
+
suggestions.push({
|
|
485
|
+
code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
|
|
486
|
+
message: `Link path should be project-root-relative without leading slash: ${normalized}.`,
|
|
487
|
+
fixHint: "Use path/from/project/root format.",
|
|
488
|
+
});
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const resolvedPath = resolveTaskLinkPath(projectPath, normalized);
|
|
668
492
|
const exists = await fs.access(resolvedPath).then(() => true).catch(() => false);
|
|
669
493
|
if (!exists) {
|
|
670
494
|
suggestions.push({
|
|
@@ -676,7 +500,7 @@ async function collectTaskFileLintSuggestions(governanceDir, task) {
|
|
|
676
500
|
return renderLintSuggestions(suggestions);
|
|
677
501
|
}
|
|
678
502
|
export function renderTasksMarkdown(tasks) {
|
|
679
|
-
const sections = tasks.map((task) => {
|
|
503
|
+
const sections = sortTasksNewestFirst(tasks).map((task) => {
|
|
680
504
|
const roadmapRefs = task.roadmapRefs.length > 0 ? task.roadmapRefs.join(", ") : "(none)";
|
|
681
505
|
const links = task.links.length > 0
|
|
682
506
|
? ["- links:", ...task.links.map((link) => ` - ${link}`)]
|
|
@@ -722,22 +546,19 @@ export function renderTasksMarkdown(tasks) {
|
|
|
722
546
|
return [
|
|
723
547
|
"# Tasks",
|
|
724
548
|
"",
|
|
725
|
-
"This file is
|
|
549
|
+
"This file is generated from .projitive sqlite tables by Projitive MCP. Manual edits will be overwritten.",
|
|
726
550
|
"",
|
|
727
|
-
TASKS_START,
|
|
728
551
|
...(sections.length > 0 ? sections : ["(no tasks)"]),
|
|
729
|
-
TASKS_END,
|
|
730
552
|
"",
|
|
731
553
|
].join("\n");
|
|
732
554
|
}
|
|
733
555
|
export async function ensureTasksFile(inputPath) {
|
|
734
556
|
const governanceDir = await resolveGovernanceDir(inputPath);
|
|
735
|
-
const tasksPath =
|
|
557
|
+
const { tasksPath, markdownPath } = resolveTaskArtifactPaths(governanceDir);
|
|
736
558
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
}
|
|
559
|
+
await ensureStore(tasksPath);
|
|
560
|
+
const tasks = normalizeAndSortTasks(await loadTasksFromStore(tasksPath));
|
|
561
|
+
await syncTasksMarkdownView(tasksPath, markdownPath, renderTasksMarkdown(tasks));
|
|
741
562
|
return tasksPath;
|
|
742
563
|
}
|
|
743
564
|
export async function loadTasks(inputPath) {
|
|
@@ -745,13 +566,21 @@ export async function loadTasks(inputPath) {
|
|
|
745
566
|
return { tasksPath, tasks };
|
|
746
567
|
}
|
|
747
568
|
export async function loadTasksDocument(inputPath) {
|
|
569
|
+
return loadTasksDocumentWithOptions(inputPath, false);
|
|
570
|
+
}
|
|
571
|
+
export async function loadTasksDocumentWithOptions(inputPath, forceViewSync) {
|
|
748
572
|
const tasksPath = await ensureTasksFile(inputPath);
|
|
749
|
-
const
|
|
750
|
-
|
|
573
|
+
const tasks = normalizeAndSortTasks(await loadTasksFromStore(tasksPath));
|
|
574
|
+
const markdown = renderTasksMarkdown(tasks);
|
|
575
|
+
const markdownPath = path.join(path.dirname(tasksPath), TASKS_MARKDOWN_FILE);
|
|
576
|
+
await syncTasksMarkdownView(tasksPath, markdownPath, markdown, forceViewSync);
|
|
577
|
+
return { tasksPath, markdownPath, markdown, tasks };
|
|
751
578
|
}
|
|
752
579
|
export async function saveTasks(tasksPath, tasks) {
|
|
753
|
-
const normalized = tasks
|
|
754
|
-
|
|
580
|
+
const normalized = normalizeAndSortTasks(tasks);
|
|
581
|
+
const markdownPath = path.join(path.dirname(tasksPath), TASKS_MARKDOWN_FILE);
|
|
582
|
+
await replaceTasksInStore(tasksPath, normalized);
|
|
583
|
+
await syncTasksMarkdownView(tasksPath, markdownPath, renderTasksMarkdown(normalized));
|
|
755
584
|
}
|
|
756
585
|
export function validateTransition(from, to) {
|
|
757
586
|
if (from === to) {
|
|
@@ -776,11 +605,11 @@ export function registerTaskTools(server) {
|
|
|
776
605
|
},
|
|
777
606
|
}, async ({ projectPath, status, limit }) => {
|
|
778
607
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
779
|
-
const {
|
|
608
|
+
const { tasks } = await loadTasksDocument(governanceDir);
|
|
780
609
|
const filtered = tasks
|
|
781
610
|
.filter((task) => (status ? task.status === status : true))
|
|
782
611
|
.slice(0, limit ?? 100);
|
|
783
|
-
const lintSuggestions = collectTaskLintSuggestions(filtered
|
|
612
|
+
const lintSuggestions = collectTaskLintSuggestions(filtered);
|
|
784
613
|
if (status && filtered.length === 0) {
|
|
785
614
|
appendLintSuggestions(lintSuggestions, [
|
|
786
615
|
{
|
|
@@ -796,7 +625,6 @@ export function registerTaskTools(server) {
|
|
|
796
625
|
sections: [
|
|
797
626
|
summarySection([
|
|
798
627
|
`- governanceDir: ${governanceDir}`,
|
|
799
|
-
`- tasksPath: ${tasksPath}`,
|
|
800
628
|
`- filter.status: ${status ?? "(none)"}`,
|
|
801
629
|
`- returned: ${filtered.length}`,
|
|
802
630
|
]),
|
|
@@ -826,21 +654,18 @@ export function registerTaskTools(server) {
|
|
|
826
654
|
const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
|
|
827
655
|
if (rankedCandidates.length === 0) {
|
|
828
656
|
const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
|
|
829
|
-
const
|
|
657
|
+
const tasksPath = path.join(governanceDir, ".projitive");
|
|
658
|
+
await ensureStore(tasksPath);
|
|
659
|
+
const stats = await loadTaskStatusStatsFromStore(tasksPath);
|
|
830
660
|
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
661
|
return {
|
|
836
662
|
governanceDir,
|
|
837
|
-
tasksPath,
|
|
838
663
|
roadmapIds,
|
|
839
|
-
total:
|
|
840
|
-
todo,
|
|
841
|
-
inProgress,
|
|
842
|
-
blocked,
|
|
843
|
-
done,
|
|
664
|
+
total: stats.total,
|
|
665
|
+
todo: stats.todo,
|
|
666
|
+
inProgress: stats.inProgress,
|
|
667
|
+
blocked: stats.blocked,
|
|
668
|
+
done: stats.done,
|
|
844
669
|
};
|
|
845
670
|
}));
|
|
846
671
|
const preferredProject = projectSnapshots[0];
|
|
@@ -859,7 +684,7 @@ export function registerTaskTools(server) {
|
|
|
859
684
|
evidenceSection([
|
|
860
685
|
"### Project Snapshots",
|
|
861
686
|
...(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)"}
|
|
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)"}`)
|
|
863
688
|
: ["- (none)"]),
|
|
864
689
|
"",
|
|
865
690
|
"### Seed Task Template",
|
|
@@ -868,16 +693,17 @@ export function registerTaskTools(server) {
|
|
|
868
693
|
guidanceSection([
|
|
869
694
|
"- No TODO/IN_PROGRESS task is available.",
|
|
870
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.",
|
|
871
697
|
"",
|
|
872
698
|
"### No-Task Discovery Checklist",
|
|
873
699
|
...noTaskDiscoveryGuidance,
|
|
874
700
|
"",
|
|
875
701
|
"- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.",
|
|
876
702
|
"- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.",
|
|
877
|
-
"- After
|
|
703
|
+
"- After creating tasks, rerun `taskNext` to re-rank actionable work.",
|
|
878
704
|
]),
|
|
879
705
|
lintSection([
|
|
880
|
-
"- No actionable tasks found. Verify task statuses and required fields in
|
|
706
|
+
"- No actionable tasks found. Verify task statuses and required fields in .projitive task table.",
|
|
881
707
|
"- Ensure each new task has stable TASK-xxxx ID and at least one roadmapRefs item.",
|
|
882
708
|
]),
|
|
883
709
|
nextCallSection(preferredProject
|
|
@@ -889,13 +715,13 @@ export function registerTaskTools(server) {
|
|
|
889
715
|
}
|
|
890
716
|
const selected = rankedCandidates[0];
|
|
891
717
|
const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
|
|
892
|
-
const lintSuggestions = collectTaskLintSuggestions(selectedTaskDocument.tasks
|
|
718
|
+
const lintSuggestions = collectTaskLintSuggestions(selectedTaskDocument.tasks);
|
|
893
719
|
const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
|
|
894
720
|
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
895
721
|
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
|
|
896
|
-
const taskLocation = (await findTextReferences(
|
|
722
|
+
const taskLocation = (await findTextReferences(selectedTaskDocument.markdownPath, selected.task.id))[0];
|
|
897
723
|
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
898
|
-
const suggestedReadOrder = [
|
|
724
|
+
const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
|
|
899
725
|
const candidateLimit = limit ?? 5;
|
|
900
726
|
const markdown = renderToolResponseMarkdown({
|
|
901
727
|
toolName: "taskNext",
|
|
@@ -917,7 +743,7 @@ export function registerTaskTools(server) {
|
|
|
917
743
|
`- owner: ${selected.task.owner || "(none)"}`,
|
|
918
744
|
`- updatedAt: ${selected.task.updatedAt}`,
|
|
919
745
|
`- roadmapRefs: ${selected.task.roadmapRefs.join(", ") || "(none)"}`,
|
|
920
|
-
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` :
|
|
746
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selectedTaskDocument.markdownPath}`,
|
|
921
747
|
"",
|
|
922
748
|
"### Top Candidates",
|
|
923
749
|
...rankedCandidates
|
|
@@ -965,7 +791,7 @@ export function registerTaskTools(server) {
|
|
|
965
791
|
};
|
|
966
792
|
}
|
|
967
793
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
968
|
-
const {
|
|
794
|
+
const { markdownPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
|
|
969
795
|
const task = tasks.find((item) => item.id === taskId);
|
|
970
796
|
if (!task) {
|
|
971
797
|
return {
|
|
@@ -977,22 +803,13 @@ export function registerTaskTools(server) {
|
|
|
977
803
|
...collectSingleTaskLintSuggestions(task),
|
|
978
804
|
...(await collectTaskFileLintSuggestions(governanceDir, task)),
|
|
979
805
|
];
|
|
980
|
-
const
|
|
981
|
-
|
|
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];
|
|
806
|
+
const contextReadingGuidance = await resolveTaskContextReadingGuidance(governanceDir);
|
|
807
|
+
const taskLocation = (await findTextReferences(markdownPath, taskId))[0];
|
|
991
808
|
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
992
809
|
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
993
810
|
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
|
|
994
811
|
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
995
|
-
const suggestedReadOrder = [
|
|
812
|
+
const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
|
|
996
813
|
// Build summary with subState and blocker info (v1.1.0)
|
|
997
814
|
const summaryLines = [
|
|
998
815
|
`- governanceDir: ${governanceDir}`,
|
|
@@ -1002,7 +819,7 @@ export function registerTaskTools(server) {
|
|
|
1002
819
|
`- owner: ${task.owner}`,
|
|
1003
820
|
`- updatedAt: ${task.updatedAt}`,
|
|
1004
821
|
`- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
|
|
1005
|
-
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` :
|
|
822
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
|
|
1006
823
|
];
|
|
1007
824
|
// Add subState info for IN_PROGRESS tasks (v1.1.0)
|
|
1008
825
|
if (task.subState && task.status === "IN_PROGRESS") {
|
|
@@ -1050,9 +867,13 @@ export function registerTaskTools(server) {
|
|
|
1050
867
|
]),
|
|
1051
868
|
guidanceSection([
|
|
1052
869
|
"- Read the files in Suggested Read Order.",
|
|
870
|
+
"",
|
|
871
|
+
"### Recommended Context Reading",
|
|
872
|
+
...contextReadingGuidance,
|
|
873
|
+
"",
|
|
1053
874
|
"- Verify whether current status and evidence are consistent.",
|
|
1054
875
|
...taskStatusGuidance(task),
|
|
1055
|
-
"- If updates are needed,
|
|
876
|
+
"- If updates are needed, use tool writes for sqlite source (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.",
|
|
1056
877
|
"- After editing, re-run `taskContext` to verify references and context consistency.",
|
|
1057
878
|
]),
|
|
1058
879
|
lintSection(lintSuggestions),
|
|
@@ -1096,7 +917,7 @@ export function registerTaskTools(server) {
|
|
|
1096
917
|
};
|
|
1097
918
|
}
|
|
1098
919
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1099
|
-
const { tasksPath, tasks
|
|
920
|
+
const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
|
|
1100
921
|
const taskIndex = tasks.findIndex((item) => item.id === taskId);
|
|
1101
922
|
if (taskIndex === -1) {
|
|
1102
923
|
return {
|
|
@@ -1147,8 +968,17 @@ export function registerTaskTools(server) {
|
|
|
1147
968
|
}
|
|
1148
969
|
// Update updatedAt
|
|
1149
970
|
task.updatedAt = nowIso();
|
|
1150
|
-
|
|
1151
|
-
|
|
971
|
+
const normalizedTask = normalizeTask(task);
|
|
972
|
+
// Save task incrementally
|
|
973
|
+
await upsertTaskInStore(tasksPath, normalizedTask);
|
|
974
|
+
task.status = normalizedTask.status;
|
|
975
|
+
task.owner = normalizedTask.owner;
|
|
976
|
+
task.summary = normalizedTask.summary;
|
|
977
|
+
task.roadmapRefs = normalizedTask.roadmapRefs;
|
|
978
|
+
task.links = normalizedTask.links;
|
|
979
|
+
task.updatedAt = normalizedTask.updatedAt;
|
|
980
|
+
task.subState = normalizedTask.subState;
|
|
981
|
+
task.blocker = normalizedTask.blocker;
|
|
1152
982
|
// Build response
|
|
1153
983
|
const updateSummary = [
|
|
1154
984
|
`- taskId: ${taskId}`,
|
|
@@ -1199,6 +1029,8 @@ export function registerTaskTools(server) {
|
|
|
1199
1029
|
"Task updated successfully. Run `taskContext` to verify the changes.",
|
|
1200
1030
|
"If status changed to DONE, ensure evidence links are added.",
|
|
1201
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.",
|
|
1202
1034
|
]),
|
|
1203
1035
|
lintSection([]),
|
|
1204
1036
|
nextCallSection(`taskContext(projectPath=\"${toProjectPath(governanceDir)}\", taskId=\"${taskId}\")`),
|