@projitive/mcp 1.0.1 → 1.0.3
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 +44 -20
- package/output/helpers/artifacts/artifacts.js +10 -0
- package/output/helpers/artifacts/artifacts.test.js +18 -0
- package/output/helpers/artifacts/index.js +1 -0
- package/output/helpers/index.js +3 -0
- package/output/helpers/linter/codes.js +25 -0
- package/output/helpers/linter/index.js +2 -0
- package/output/helpers/linter/linter.js +6 -0
- package/output/helpers/linter/linter.test.js +16 -0
- package/output/helpers/response/index.js +1 -0
- package/output/helpers/response/response.js +73 -0
- package/output/helpers/response/response.test.js +50 -0
- package/output/hooks.js +1 -14
- package/output/hooks.test.js +7 -18
- package/output/index.js +23 -5
- package/output/package.json +36 -0
- package/output/projitive.js +158 -124
- package/output/projitive.test.js +1 -0
- package/output/rendering-input-guard.test.js +20 -0
- package/output/roadmap.js +106 -80
- package/output/roadmap.test.js +11 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectContext.md +48 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectInit.md +40 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectLocate.md +22 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectNext.md +31 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/projectScan.md +28 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/roadmapContext.md +33 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/roadmapList.md +25 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/summary.json +90 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/summary.md +17 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/taskContext.md +47 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/taskList.md +27 -0
- package/output/smoke-reports/2026-02-18T13-18-19-740Z/taskNext.md +64 -0
- package/output/source/designs.js +38 -0
- package/output/source/helpers/artifacts/artifacts.js +10 -0
- package/output/source/helpers/artifacts/artifacts.test.js +18 -0
- package/output/source/helpers/artifacts/index.js +1 -0
- package/output/source/helpers/catch/catch.js +48 -0
- package/output/source/helpers/catch/catch.test.js +43 -0
- package/output/source/helpers/catch/index.js +1 -0
- package/output/source/helpers/files/files.js +62 -0
- package/output/source/helpers/files/files.test.js +32 -0
- package/output/source/helpers/files/index.js +1 -0
- package/output/source/helpers/index.js +6 -0
- package/output/source/helpers/linter/codes.js +25 -0
- package/output/source/helpers/linter/index.js +2 -0
- package/output/source/helpers/linter/linter.js +6 -0
- package/output/source/helpers/linter/linter.test.js +16 -0
- package/output/source/helpers/markdown/index.js +1 -0
- package/output/source/helpers/markdown/markdown.js +33 -0
- package/output/source/helpers/markdown/markdown.test.js +36 -0
- package/output/source/helpers/response/index.js +1 -0
- package/output/source/helpers/response/response.js +73 -0
- package/output/source/helpers/response/response.test.js +50 -0
- package/output/source/index.js +215 -0
- package/output/source/projitive.js +488 -0
- package/output/source/projitive.test.js +75 -0
- package/output/source/readme.js +26 -0
- package/output/source/reports.js +36 -0
- package/output/source/roadmap.js +165 -0
- package/output/source/roadmap.test.js +11 -0
- package/output/source/tasks.js +762 -0
- package/output/source/tasks.test.js +152 -0
- package/output/tasks.js +403 -204
- package/output/tasks.test.js +78 -4
- package/package.json +1 -1
package/output/tasks.js
CHANGED
|
@@ -1,33 +1,20 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { candidateFilesFromArtifacts } from "./helpers/artifacts/index.js";
|
|
4
5
|
import { discoverGovernanceArtifacts } from "./helpers/files/index.js";
|
|
5
6
|
import { findTextReferences } from "./helpers/markdown/index.js";
|
|
7
|
+
import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "./helpers/response/index.js";
|
|
6
8
|
import { catchIt } from "./helpers/catch/index.js";
|
|
9
|
+
import { TASK_LINT_CODES, renderLintSuggestions } from "./helpers/linter/index.js";
|
|
7
10
|
import { resolveGovernanceDir, resolveScanDepth, resolveScanRoot, discoverProjects } from "./projitive.js";
|
|
8
11
|
import { isValidRoadmapId } from "./roadmap.js";
|
|
9
12
|
export const TASKS_START = "<!-- PROJITIVE:TASKS:START -->";
|
|
10
13
|
export const TASKS_END = "<!-- PROJITIVE:TASKS:END -->";
|
|
11
14
|
export const ALLOWED_STATUS = ["TODO", "IN_PROGRESS", "BLOCKED", "DONE"];
|
|
12
15
|
export const TASK_ID_REGEX = /^TASK-\d{4}$/;
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
content: [{ type: "text", text: markdown }],
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
function renderErrorMarkdown(toolName, cause, nextSteps, retryExample) {
|
|
19
|
-
return [
|
|
20
|
-
`# ${toolName}`,
|
|
21
|
-
"",
|
|
22
|
-
"## Error",
|
|
23
|
-
`- cause: ${cause}`,
|
|
24
|
-
"",
|
|
25
|
-
"## Next Step",
|
|
26
|
-
...(nextSteps.length > 0 ? nextSteps : ["- (none)"]),
|
|
27
|
-
"",
|
|
28
|
-
"## Retry Example",
|
|
29
|
-
`- ${retryExample ?? "(none)"}`,
|
|
30
|
-
].join("\n");
|
|
16
|
+
function appendLintSuggestions(target, suggestions) {
|
|
17
|
+
target.push(...renderLintSuggestions(suggestions));
|
|
31
18
|
}
|
|
32
19
|
function taskStatusGuidance(task) {
|
|
33
20
|
if (task.status === "TODO") {
|
|
@@ -53,16 +40,6 @@ function taskStatusGuidance(task) {
|
|
|
53
40
|
"- Keep report evidence immutable unless correction is required.",
|
|
54
41
|
];
|
|
55
42
|
}
|
|
56
|
-
function candidateFilesFromArtifacts(artifacts) {
|
|
57
|
-
return artifacts
|
|
58
|
-
.filter((item) => item.exists)
|
|
59
|
-
.flatMap((item) => {
|
|
60
|
-
if (item.kind === "file") {
|
|
61
|
-
return [item.path];
|
|
62
|
-
}
|
|
63
|
-
return (item.markdownFiles ?? []).map((entry) => entry.path);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
43
|
async function readOptionalMarkdown(filePath) {
|
|
67
44
|
const content = await fs.readFile(filePath, "utf-8").catch(() => undefined);
|
|
68
45
|
if (typeof content !== "string") {
|
|
@@ -71,11 +48,34 @@ async function readOptionalMarkdown(filePath) {
|
|
|
71
48
|
const trimmed = content.trim();
|
|
72
49
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
73
50
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
51
|
+
const NO_TASK_DISCOVERY_HOOK_FILE = "task_no_actionable.md";
|
|
52
|
+
const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
|
|
53
|
+
"- Check whether current code violates project guide/spec conventions; create TODO tasks for each actionable gap.",
|
|
54
|
+
"- Check unit/integration test coverage and identify high-value missing tests; create TODO tasks for meaningful coverage improvements.",
|
|
55
|
+
"- Check development/testing workflow for bottlenecks (slow feedback, fragile scripts, unclear runbooks); create TODO tasks to improve reliability.",
|
|
56
|
+
"- Scan for TODO/FIXME/HACK comments and convert feasible items into governed TODO tasks with evidence links.",
|
|
57
|
+
"- Check dependency freshness and security advisories; create tasks for safe upgrades when needed.",
|
|
58
|
+
"- Check repeated manual operations that can be automated (lint/test/release checks); create tasks to reduce operational toil.",
|
|
59
|
+
];
|
|
60
|
+
function parseHookChecklist(markdown) {
|
|
61
|
+
return markdown
|
|
62
|
+
.split(/\r?\n/)
|
|
63
|
+
.map((line) => line.trim())
|
|
64
|
+
.filter((line) => line.length > 0)
|
|
65
|
+
.filter((line) => /^[-*+]\s+/.test(line))
|
|
66
|
+
.map((line) => (line.startsWith("*") ? `-${line.slice(1)}` : line));
|
|
67
|
+
}
|
|
68
|
+
export async function resolveNoTaskDiscoveryGuidance(governanceDir) {
|
|
69
|
+
if (!governanceDir) {
|
|
70
|
+
return DEFAULT_NO_TASK_DISCOVERY_GUIDANCE;
|
|
71
|
+
}
|
|
72
|
+
const hookPath = path.join(governanceDir, "hooks", NO_TASK_DISCOVERY_HOOK_FILE);
|
|
73
|
+
const markdown = await readOptionalMarkdown(hookPath);
|
|
74
|
+
if (typeof markdown !== "string") {
|
|
75
|
+
return DEFAULT_NO_TASK_DISCOVERY_GUIDANCE;
|
|
76
|
+
}
|
|
77
|
+
const checklist = parseHookChecklist(markdown);
|
|
78
|
+
return checklist.length > 0 ? checklist : DEFAULT_NO_TASK_DISCOVERY_GUIDANCE;
|
|
79
79
|
}
|
|
80
80
|
function latestTaskUpdatedAt(tasks) {
|
|
81
81
|
const timestamps = tasks
|
|
@@ -90,6 +90,31 @@ function actionableScore(tasks) {
|
|
|
90
90
|
return tasks.filter((task) => task.status === "IN_PROGRESS").length * 2
|
|
91
91
|
+ tasks.filter((task) => task.status === "TODO").length;
|
|
92
92
|
}
|
|
93
|
+
async function readRoadmapIds(governanceDir) {
|
|
94
|
+
const roadmapPath = path.join(governanceDir, "roadmap.md");
|
|
95
|
+
try {
|
|
96
|
+
const markdown = await fs.readFile(roadmapPath, "utf-8");
|
|
97
|
+
const matches = markdown.match(/ROADMAP-\d{4}/g) ?? [];
|
|
98
|
+
return Array.from(new Set(matches));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function renderTaskSeedTemplate(roadmapRef) {
|
|
105
|
+
return [
|
|
106
|
+
"```markdown",
|
|
107
|
+
"## TASK-0001 | TODO | Define initial executable objective",
|
|
108
|
+
"- owner: ai-copilot",
|
|
109
|
+
"- summary: Convert one roadmap milestone or report gap into an actionable task.",
|
|
110
|
+
"- updatedAt: 2026-01-01T00:00:00.000Z",
|
|
111
|
+
`- roadmapRefs: ${roadmapRef}`,
|
|
112
|
+
"- links:",
|
|
113
|
+
" - ./README.md",
|
|
114
|
+
" - ./roadmap.md",
|
|
115
|
+
"```",
|
|
116
|
+
];
|
|
117
|
+
}
|
|
93
118
|
async function readActionableTaskCandidates(governanceDirs) {
|
|
94
119
|
const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
|
|
95
120
|
const snapshot = await loadTasks(governanceDir);
|
|
@@ -153,14 +178,6 @@ export function normalizeTask(task) {
|
|
|
153
178
|
const normalizedRoadmapRefs = Array.isArray(task.roadmapRefs)
|
|
154
179
|
? task.roadmapRefs.map(String).filter((value) => isValidRoadmapId(value))
|
|
155
180
|
: [];
|
|
156
|
-
const inputHooks = task.hooks ?? {};
|
|
157
|
-
const normalizedHooks = {};
|
|
158
|
-
for (const key of ["onAssigned", "onCompleted", "onBlocked", "onReopened"]) {
|
|
159
|
-
const value = inputHooks[key];
|
|
160
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
161
|
-
normalizedHooks[key] = value;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
181
|
return {
|
|
165
182
|
id: String(task.id),
|
|
166
183
|
title: String(task.title),
|
|
@@ -170,7 +187,6 @@ export function normalizeTask(task) {
|
|
|
170
187
|
updatedAt: task.updatedAt ? String(task.updatedAt) : nowIso(),
|
|
171
188
|
links: Array.isArray(task.links) ? task.links.map(String) : [],
|
|
172
189
|
roadmapRefs: Array.from(new Set(normalizedRoadmapRefs)),
|
|
173
|
-
hooks: normalizedHooks,
|
|
174
190
|
};
|
|
175
191
|
}
|
|
176
192
|
export async function parseTasksBlock(markdown) {
|
|
@@ -205,7 +221,6 @@ export async function parseTasksBlock(markdown) {
|
|
|
205
221
|
updatedAt: nowIso(),
|
|
206
222
|
links: [],
|
|
207
223
|
roadmapRefs: [],
|
|
208
|
-
hooks: {},
|
|
209
224
|
};
|
|
210
225
|
let inLinks = false;
|
|
211
226
|
let inHooks = false;
|
|
@@ -268,32 +283,161 @@ export async function parseTasksBlock(markdown) {
|
|
|
268
283
|
continue;
|
|
269
284
|
}
|
|
270
285
|
if (inHooks) {
|
|
271
|
-
|
|
272
|
-
if (hookMatch) {
|
|
273
|
-
const [, hookKey, hookPath] = hookMatch;
|
|
274
|
-
taskDraft.hooks = {
|
|
275
|
-
...(taskDraft.hooks ?? {}),
|
|
276
|
-
[hookKey]: hookPath.trim(),
|
|
277
|
-
};
|
|
278
|
-
}
|
|
286
|
+
continue;
|
|
279
287
|
}
|
|
280
288
|
}
|
|
281
289
|
tasks.push(normalizeTask(taskDraft));
|
|
282
290
|
}
|
|
283
291
|
return tasks;
|
|
284
292
|
}
|
|
293
|
+
export function findTaskIdsOutsideMarkers(markdown) {
|
|
294
|
+
const start = markdown.indexOf(TASKS_START);
|
|
295
|
+
const end = markdown.indexOf(TASKS_END);
|
|
296
|
+
const outsideText = (start !== -1 && end !== -1 && end > start)
|
|
297
|
+
? `${markdown.slice(0, start)}\n${markdown.slice(end + TASKS_END.length)}`
|
|
298
|
+
: markdown;
|
|
299
|
+
const ids = outsideText.match(/TASK-\d{4}/g) ?? [];
|
|
300
|
+
return Array.from(new Set(ids));
|
|
301
|
+
}
|
|
302
|
+
function collectTaskLintSuggestionItems(tasks, options = {}) {
|
|
303
|
+
const suggestions = [];
|
|
304
|
+
const { markdown, outsideMarkerScopeIds } = options;
|
|
305
|
+
const duplicateIds = Array.from(tasks.reduce((counter, task) => {
|
|
306
|
+
counter.set(task.id, (counter.get(task.id) ?? 0) + 1);
|
|
307
|
+
return counter;
|
|
308
|
+
}, new Map())
|
|
309
|
+
.entries())
|
|
310
|
+
.filter(([, count]) => count > 1)
|
|
311
|
+
.map(([id]) => id);
|
|
312
|
+
if (duplicateIds.length > 0) {
|
|
313
|
+
suggestions.push({
|
|
314
|
+
code: TASK_LINT_CODES.DUPLICATE_ID,
|
|
315
|
+
message: `Duplicate task IDs detected: ${duplicateIds.join(", ")}.`,
|
|
316
|
+
fixHint: "Keep task IDs unique in marker block.",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
const inProgressWithoutOwner = tasks.filter((task) => task.status === "IN_PROGRESS" && task.owner.trim().length === 0);
|
|
320
|
+
if (inProgressWithoutOwner.length > 0) {
|
|
321
|
+
suggestions.push({
|
|
322
|
+
code: TASK_LINT_CODES.IN_PROGRESS_OWNER_EMPTY,
|
|
323
|
+
message: `${inProgressWithoutOwner.length} IN_PROGRESS task(s) have empty owner.`,
|
|
324
|
+
fixHint: "Set owner before continuing execution.",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const doneWithoutLinks = tasks.filter((task) => task.status === "DONE" && task.links.length === 0);
|
|
328
|
+
if (doneWithoutLinks.length > 0) {
|
|
329
|
+
suggestions.push({
|
|
330
|
+
code: TASK_LINT_CODES.DONE_LINKS_MISSING,
|
|
331
|
+
message: `${doneWithoutLinks.length} DONE task(s) have no links evidence.`,
|
|
332
|
+
fixHint: "Add at least one evidence link before keeping DONE.",
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const blockedWithoutReason = tasks.filter((task) => task.status === "BLOCKED" && task.summary.trim().length === 0);
|
|
336
|
+
if (blockedWithoutReason.length > 0) {
|
|
337
|
+
suggestions.push({
|
|
338
|
+
code: TASK_LINT_CODES.BLOCKED_SUMMARY_EMPTY,
|
|
339
|
+
message: `${blockedWithoutReason.length} BLOCKED task(s) have empty summary.`,
|
|
340
|
+
fixHint: "Add blocker reason and unblock condition.",
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const invalidUpdatedAt = tasks.filter((task) => !Number.isFinite(new Date(task.updatedAt).getTime()));
|
|
344
|
+
if (invalidUpdatedAt.length > 0) {
|
|
345
|
+
suggestions.push({
|
|
346
|
+
code: TASK_LINT_CODES.UPDATED_AT_INVALID,
|
|
347
|
+
message: `${invalidUpdatedAt.length} task(s) have invalid updatedAt format.`,
|
|
348
|
+
fixHint: "Use ISO8601 UTC timestamp.",
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
const missingRoadmapRefs = tasks.filter((task) => task.roadmapRefs.length === 0);
|
|
352
|
+
if (missingRoadmapRefs.length > 0) {
|
|
353
|
+
suggestions.push({
|
|
354
|
+
code: TASK_LINT_CODES.ROADMAP_REFS_EMPTY,
|
|
355
|
+
message: `${missingRoadmapRefs.length} task(s) have empty roadmapRefs.`,
|
|
356
|
+
fixHint: "Bind at least one ROADMAP-xxxx when applicable.",
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (typeof markdown === "string") {
|
|
360
|
+
const outsideMarkerTaskIds = findTaskIdsOutsideMarkers(markdown)
|
|
361
|
+
.filter((taskId) => (outsideMarkerScopeIds ? outsideMarkerScopeIds.has(taskId) : true));
|
|
362
|
+
if (outsideMarkerTaskIds.length > 0) {
|
|
363
|
+
suggestions.push({
|
|
364
|
+
code: TASK_LINT_CODES.OUTSIDE_MARKER,
|
|
365
|
+
message: `TASK IDs found outside marker block: ${outsideMarkerTaskIds.join(", ")}.`,
|
|
366
|
+
fixHint: "Keep task source of truth inside marker region only.",
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return suggestions;
|
|
371
|
+
}
|
|
372
|
+
export function collectTaskLintSuggestions(tasks, markdown, outsideMarkerScopeIds) {
|
|
373
|
+
return renderLintSuggestions(collectTaskLintSuggestionItems(tasks, { markdown, outsideMarkerScopeIds }));
|
|
374
|
+
}
|
|
375
|
+
function collectSingleTaskLintSuggestions(task) {
|
|
376
|
+
const suggestions = [];
|
|
377
|
+
if (task.status === "IN_PROGRESS" && task.owner.trim().length === 0) {
|
|
378
|
+
suggestions.push({
|
|
379
|
+
code: TASK_LINT_CODES.IN_PROGRESS_OWNER_EMPTY,
|
|
380
|
+
message: "Current task is IN_PROGRESS but owner is empty.",
|
|
381
|
+
fixHint: "Set owner before continuing execution.",
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
if (task.status === "DONE" && task.links.length === 0) {
|
|
385
|
+
suggestions.push({
|
|
386
|
+
code: TASK_LINT_CODES.DONE_LINKS_MISSING,
|
|
387
|
+
message: "Current task is DONE but has no links evidence.",
|
|
388
|
+
fixHint: "Add at least one evidence link.",
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (task.status === "BLOCKED" && task.summary.trim().length === 0) {
|
|
392
|
+
suggestions.push({
|
|
393
|
+
code: TASK_LINT_CODES.BLOCKED_SUMMARY_EMPTY,
|
|
394
|
+
message: "Current task is BLOCKED but summary is empty.",
|
|
395
|
+
fixHint: "Add blocker reason and unblock condition.",
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (!Number.isFinite(new Date(task.updatedAt).getTime())) {
|
|
399
|
+
suggestions.push({
|
|
400
|
+
code: TASK_LINT_CODES.UPDATED_AT_INVALID,
|
|
401
|
+
message: "Current task updatedAt is invalid.",
|
|
402
|
+
fixHint: "Use ISO8601 UTC timestamp.",
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (task.roadmapRefs.length === 0) {
|
|
406
|
+
suggestions.push({
|
|
407
|
+
code: TASK_LINT_CODES.ROADMAP_REFS_EMPTY,
|
|
408
|
+
message: "Current task has empty roadmapRefs.",
|
|
409
|
+
fixHint: "Bind ROADMAP-xxxx where applicable.",
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return renderLintSuggestions(suggestions);
|
|
413
|
+
}
|
|
414
|
+
async function collectTaskFileLintSuggestions(governanceDir, task) {
|
|
415
|
+
const suggestions = [];
|
|
416
|
+
for (const link of task.links) {
|
|
417
|
+
const normalized = link.trim();
|
|
418
|
+
if (normalized.length === 0) {
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (/^https?:\/\//i.test(normalized)) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const resolvedPath = path.resolve(governanceDir, normalized);
|
|
425
|
+
const exists = await fs.access(resolvedPath).then(() => true).catch(() => false);
|
|
426
|
+
if (!exists) {
|
|
427
|
+
suggestions.push({
|
|
428
|
+
code: TASK_LINT_CODES.LINK_TARGET_MISSING,
|
|
429
|
+
message: `Link target not found: ${normalized} (resolved: ${resolvedPath}).`,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return renderLintSuggestions(suggestions);
|
|
434
|
+
}
|
|
285
435
|
export function renderTasksMarkdown(tasks) {
|
|
286
436
|
const sections = tasks.map((task) => {
|
|
287
437
|
const roadmapRefs = task.roadmapRefs.length > 0 ? task.roadmapRefs.join(", ") : "(none)";
|
|
288
438
|
const links = task.links.length > 0
|
|
289
439
|
? ["- links:", ...task.links.map((link) => ` - ${link}`)]
|
|
290
440
|
: ["- links:", " - (none)"];
|
|
291
|
-
const hookEntries = Object.entries(task.hooks)
|
|
292
|
-
.filter(([, value]) => typeof value === "string" && value.trim().length > 0)
|
|
293
|
-
.map(([key, value]) => ` - ${key}: ${value}`);
|
|
294
|
-
const hooks = hookEntries.length > 0
|
|
295
|
-
? ["- hooks:", ...hookEntries]
|
|
296
|
-
: ["- hooks:", " - (none)"];
|
|
297
441
|
return [
|
|
298
442
|
`## ${task.id} | ${task.status} | ${task.title}`,
|
|
299
443
|
`- owner: ${task.owner || "(none)"}`,
|
|
@@ -301,7 +445,6 @@ export function renderTasksMarkdown(tasks) {
|
|
|
301
445
|
`- updatedAt: ${task.updatedAt}`,
|
|
302
446
|
`- roadmapRefs: ${roadmapRefs}`,
|
|
303
447
|
...links,
|
|
304
|
-
...hooks,
|
|
305
448
|
].join("\n");
|
|
306
449
|
});
|
|
307
450
|
return [
|
|
@@ -326,9 +469,13 @@ export async function ensureTasksFile(inputPath) {
|
|
|
326
469
|
return tasksPath;
|
|
327
470
|
}
|
|
328
471
|
export async function loadTasks(inputPath) {
|
|
472
|
+
const { tasksPath, tasks } = await loadTasksDocument(inputPath);
|
|
473
|
+
return { tasksPath, tasks };
|
|
474
|
+
}
|
|
475
|
+
export async function loadTasksDocument(inputPath) {
|
|
329
476
|
const tasksPath = await ensureTasksFile(inputPath);
|
|
330
477
|
const markdown = await fs.readFile(tasksPath, "utf-8");
|
|
331
|
-
return { tasksPath, tasks: await parseTasksBlock(markdown) };
|
|
478
|
+
return { tasksPath, markdown, tasks: await parseTasksBlock(markdown) };
|
|
332
479
|
}
|
|
333
480
|
export async function saveTasks(tasksPath, tasks) {
|
|
334
481
|
const normalized = tasks.map((task) => normalizeTask(task));
|
|
@@ -357,31 +504,41 @@ export function registerTaskTools(server) {
|
|
|
357
504
|
},
|
|
358
505
|
}, async ({ projectPath, status, limit }) => {
|
|
359
506
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
360
|
-
const { tasksPath, tasks } = await
|
|
507
|
+
const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
|
|
361
508
|
const filtered = tasks
|
|
362
509
|
.filter((task) => (status ? task.status === status : true))
|
|
363
510
|
.slice(0, limit ?? 100);
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
511
|
+
const lintSuggestions = collectTaskLintSuggestions(filtered, tasksMarkdown, new Set(filtered.map((task) => task.id)));
|
|
512
|
+
if (status && filtered.length === 0) {
|
|
513
|
+
appendLintSuggestions(lintSuggestions, [
|
|
514
|
+
{
|
|
515
|
+
code: TASK_LINT_CODES.FILTER_EMPTY,
|
|
516
|
+
message: `No tasks matched status=${status}.`,
|
|
517
|
+
fixHint: "Confirm status values or update task states.",
|
|
518
|
+
},
|
|
519
|
+
]);
|
|
520
|
+
}
|
|
521
|
+
const nextTaskId = filtered[0]?.id;
|
|
522
|
+
const markdown = renderToolResponseMarkdown({
|
|
523
|
+
toolName: "taskList",
|
|
524
|
+
sections: [
|
|
525
|
+
summarySection([
|
|
526
|
+
`- governanceDir: ${governanceDir}`,
|
|
527
|
+
`- tasksPath: ${tasksPath}`,
|
|
528
|
+
`- filter.status: ${status ?? "(none)"}`,
|
|
529
|
+
`- returned: ${filtered.length}`,
|
|
530
|
+
]),
|
|
531
|
+
evidenceSection([
|
|
532
|
+
"- tasks:",
|
|
533
|
+
...filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner || ""} | updatedAt=${task.updatedAt}`),
|
|
534
|
+
]),
|
|
535
|
+
guidanceSection(["- Pick one task ID and call `taskContext`."]),
|
|
536
|
+
lintSection(lintSuggestions),
|
|
537
|
+
nextCallSection(nextTaskId
|
|
538
|
+
? `taskContext(projectPath=\"${governanceDir}\", taskId=\"${nextTaskId}\")`
|
|
539
|
+
: undefined),
|
|
540
|
+
],
|
|
541
|
+
});
|
|
385
542
|
return asText(markdown);
|
|
386
543
|
});
|
|
387
544
|
server.registerTool("taskNext", {
|
|
@@ -398,29 +555,70 @@ export function registerTaskTools(server) {
|
|
|
398
555
|
const projects = await discoverProjects(root, depth);
|
|
399
556
|
const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
|
|
400
557
|
if (rankedCandidates.length === 0) {
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
]
|
|
558
|
+
const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
|
|
559
|
+
const { tasksPath, tasks } = await loadTasks(governanceDir);
|
|
560
|
+
const roadmapIds = await readRoadmapIds(governanceDir);
|
|
561
|
+
const todo = tasks.filter((task) => task.status === "TODO").length;
|
|
562
|
+
const inProgress = tasks.filter((task) => task.status === "IN_PROGRESS").length;
|
|
563
|
+
const blocked = tasks.filter((task) => task.status === "BLOCKED").length;
|
|
564
|
+
const done = tasks.filter((task) => task.status === "DONE").length;
|
|
565
|
+
return {
|
|
566
|
+
governanceDir,
|
|
567
|
+
tasksPath,
|
|
568
|
+
roadmapIds,
|
|
569
|
+
total: tasks.length,
|
|
570
|
+
todo,
|
|
571
|
+
inProgress,
|
|
572
|
+
blocked,
|
|
573
|
+
done,
|
|
574
|
+
};
|
|
575
|
+
}));
|
|
576
|
+
const preferredProject = projectSnapshots[0];
|
|
577
|
+
const preferredRoadmapRef = preferredProject?.roadmapIds[0] ?? "ROADMAP-0001";
|
|
578
|
+
const noTaskDiscoveryGuidance = await resolveNoTaskDiscoveryGuidance(preferredProject?.governanceDir);
|
|
579
|
+
const markdown = renderToolResponseMarkdown({
|
|
580
|
+
toolName: "taskNext",
|
|
581
|
+
sections: [
|
|
582
|
+
summarySection([
|
|
583
|
+
`- rootPath: ${root}`,
|
|
584
|
+
`- maxDepth: ${depth}`,
|
|
585
|
+
`- matchedProjects: ${projects.length}`,
|
|
586
|
+
"- actionableTasks: 0",
|
|
587
|
+
]),
|
|
588
|
+
evidenceSection([
|
|
589
|
+
"### Project Snapshots",
|
|
590
|
+
...(projectSnapshots.length > 0
|
|
591
|
+
? 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}`)
|
|
592
|
+
: ["- (none)"]),
|
|
593
|
+
"",
|
|
594
|
+
"### Seed Task Template",
|
|
595
|
+
...renderTaskSeedTemplate(preferredRoadmapRef),
|
|
596
|
+
]),
|
|
597
|
+
guidanceSection([
|
|
598
|
+
"- No TODO/IN_PROGRESS task is available.",
|
|
599
|
+
"- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.",
|
|
600
|
+
"",
|
|
601
|
+
"### No-Task Discovery Checklist",
|
|
602
|
+
...noTaskDiscoveryGuidance,
|
|
603
|
+
"",
|
|
604
|
+
"- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.",
|
|
605
|
+
"- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.",
|
|
606
|
+
"- After adding tasks inside marker block, rerun `taskNext` to re-rank actionable work.",
|
|
607
|
+
]),
|
|
608
|
+
lintSection([
|
|
609
|
+
"- No actionable tasks found. Verify task statuses and required fields in marker block.",
|
|
610
|
+
"- Ensure each new task has stable TASK-xxxx ID and at least one roadmapRefs item.",
|
|
611
|
+
]),
|
|
612
|
+
nextCallSection(preferredProject
|
|
613
|
+
? `projectContext(projectPath=\"${preferredProject.governanceDir}\")`
|
|
614
|
+
: `projectScan(rootPath=\"${root}\", maxDepth=${depth})`),
|
|
615
|
+
],
|
|
616
|
+
});
|
|
421
617
|
return asText(markdown);
|
|
422
618
|
}
|
|
423
619
|
const selected = rankedCandidates[0];
|
|
620
|
+
const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
|
|
621
|
+
const lintSuggestions = collectTaskLintSuggestions(selectedTaskDocument.tasks, selectedTaskDocument.markdown);
|
|
424
622
|
const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
|
|
425
623
|
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
426
624
|
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
|
|
@@ -428,55 +626,56 @@ export function registerTaskTools(server) {
|
|
|
428
626
|
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
429
627
|
const suggestedReadOrder = [selected.tasksPath, ...relatedArtifacts.filter((item) => item !== selected.tasksPath)];
|
|
430
628
|
const candidateLimit = topCandidates ?? 5;
|
|
431
|
-
const markdown =
|
|
432
|
-
"
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
629
|
+
const markdown = renderToolResponseMarkdown({
|
|
630
|
+
toolName: "taskNext",
|
|
631
|
+
sections: [
|
|
632
|
+
summarySection([
|
|
633
|
+
`- rootPath: ${root}`,
|
|
634
|
+
`- maxDepth: ${depth}`,
|
|
635
|
+
`- matchedProjects: ${projects.length}`,
|
|
636
|
+
`- actionableTasks: ${rankedCandidates.length}`,
|
|
637
|
+
`- selectedProject: ${selected.governanceDir}`,
|
|
638
|
+
`- selectedTaskId: ${selected.task.id}`,
|
|
639
|
+
`- selectedTaskStatus: ${selected.task.status}`,
|
|
640
|
+
]),
|
|
641
|
+
evidenceSection([
|
|
642
|
+
"### Selected Task",
|
|
643
|
+
`- id: ${selected.task.id}`,
|
|
644
|
+
`- title: ${selected.task.title}`,
|
|
645
|
+
`- owner: ${selected.task.owner || "(none)"}`,
|
|
646
|
+
`- updatedAt: ${selected.task.updatedAt}`,
|
|
647
|
+
`- roadmapRefs: ${selected.task.roadmapRefs.join(", ") || "(none)"}`,
|
|
648
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selected.tasksPath}`,
|
|
649
|
+
"",
|
|
650
|
+
"### Top Candidates",
|
|
651
|
+
...rankedCandidates
|
|
652
|
+
.slice(0, candidateLimit)
|
|
653
|
+
.map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | project=${item.governanceDir} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
|
|
654
|
+
"",
|
|
655
|
+
"### Selection Reason",
|
|
656
|
+
"- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.",
|
|
657
|
+
`- Selected candidate scores: projectScore=${selected.projectScore}, taskPriority=${selected.taskPriority}, taskUpdatedAtMs=${selected.taskUpdatedAtMs}.`,
|
|
658
|
+
"",
|
|
659
|
+
"### Related Artifacts",
|
|
660
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
|
|
661
|
+
"",
|
|
662
|
+
"### Reference Locations",
|
|
663
|
+
...(referenceLocations.length > 0
|
|
664
|
+
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
665
|
+
: ["- (none)"]),
|
|
666
|
+
"",
|
|
667
|
+
"### Suggested Read Order",
|
|
668
|
+
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
669
|
+
]),
|
|
670
|
+
guidanceSection([
|
|
671
|
+
"- Start immediately with Suggested Read Order and execute the selected task.",
|
|
672
|
+
"- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.",
|
|
673
|
+
"- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.",
|
|
674
|
+
]),
|
|
675
|
+
lintSection(lintSuggestions),
|
|
676
|
+
nextCallSection(`taskContext(projectPath=\"${selected.governanceDir}\", taskId=\"${selected.task.id}\")`),
|
|
677
|
+
],
|
|
678
|
+
});
|
|
480
679
|
return asText(markdown);
|
|
481
680
|
});
|
|
482
681
|
server.registerTool("taskContext", {
|
|
@@ -489,75 +688,75 @@ export function registerTaskTools(server) {
|
|
|
489
688
|
}, async ({ projectPath, taskId }) => {
|
|
490
689
|
if (!isValidTaskId(taskId)) {
|
|
491
690
|
return {
|
|
492
|
-
...asText(renderErrorMarkdown("taskContext", `Invalid task ID format: ${taskId}`, ["
|
|
691
|
+
...asText(renderErrorMarkdown("taskContext", `Invalid task ID format: ${taskId}`, ["expected format: TASK-0001", "retry with a valid task ID"], `taskContext(projectPath=\"${projectPath}\", taskId=\"TASK-0001\")`)),
|
|
493
692
|
isError: true,
|
|
494
693
|
};
|
|
495
694
|
}
|
|
496
695
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
497
|
-
const { tasksPath, tasks } = await
|
|
498
|
-
const taskContextHooks = await readTaskContextHooks(governanceDir);
|
|
696
|
+
const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
|
|
499
697
|
const task = tasks.find((item) => item.id === taskId);
|
|
500
698
|
if (!task) {
|
|
501
699
|
return {
|
|
502
|
-
...asText(renderErrorMarkdown("taskContext", `Task not found: ${taskId}`, ["
|
|
700
|
+
...asText(renderErrorMarkdown("taskContext", `Task not found: ${taskId}`, ["run `taskList` to discover available IDs", "retry with an existing task ID"], `taskList(projectPath=\"${governanceDir}\")`)),
|
|
503
701
|
isError: true,
|
|
504
702
|
};
|
|
505
703
|
}
|
|
704
|
+
const lintSuggestions = [
|
|
705
|
+
...collectSingleTaskLintSuggestions(task),
|
|
706
|
+
...(await collectTaskFileLintSuggestions(governanceDir, task)),
|
|
707
|
+
];
|
|
708
|
+
const outsideMarkerTaskIds = findTaskIdsOutsideMarkers(tasksMarkdown);
|
|
709
|
+
if (outsideMarkerTaskIds.includes(task.id)) {
|
|
710
|
+
appendLintSuggestions(lintSuggestions, [
|
|
711
|
+
{
|
|
712
|
+
code: TASK_LINT_CODES.OUTSIDE_MARKER,
|
|
713
|
+
message: `Current task ID appears outside marker block (${task.id}).`,
|
|
714
|
+
fixHint: "Keep task source of truth inside marker region.",
|
|
715
|
+
},
|
|
716
|
+
]);
|
|
717
|
+
}
|
|
506
718
|
const taskLocation = (await findTextReferences(tasksPath, taskId))[0];
|
|
507
719
|
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
508
720
|
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
509
721
|
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
|
|
510
722
|
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
511
723
|
const suggestedReadOrder = [tasksPath, ...relatedArtifacts.filter((item) => item !== tasksPath)];
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
"- If updates are needed, edit tasks/designs/reports markdown directly and keep TASK IDs unchanged.",
|
|
550
|
-
"- After editing, re-run `taskContext` to verify references and context consistency.",
|
|
551
|
-
"",
|
|
552
|
-
"## Next Call",
|
|
553
|
-
`- taskContext(projectPath=\"${governanceDir}\", taskId=\"${task.id}\")`,
|
|
554
|
-
].join("\n");
|
|
555
|
-
const markdownParts = [
|
|
556
|
-
taskContextHooks.head,
|
|
557
|
-
coreMarkdown,
|
|
558
|
-
taskContextHooks.footer,
|
|
559
|
-
].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
560
|
-
const markdown = markdownParts.join("\n\n---\n\n");
|
|
561
|
-
return asText(markdown);
|
|
724
|
+
const coreMarkdown = renderToolResponseMarkdown({
|
|
725
|
+
toolName: "taskContext",
|
|
726
|
+
sections: [
|
|
727
|
+
summarySection([
|
|
728
|
+
`- governanceDir: ${governanceDir}`,
|
|
729
|
+
`- taskId: ${task.id}`,
|
|
730
|
+
`- title: ${task.title}`,
|
|
731
|
+
`- status: ${task.status}`,
|
|
732
|
+
`- owner: ${task.owner}`,
|
|
733
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
734
|
+
`- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
|
|
735
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : tasksPath}`,
|
|
736
|
+
]),
|
|
737
|
+
evidenceSection([
|
|
738
|
+
"### Related Artifacts",
|
|
739
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
|
|
740
|
+
"",
|
|
741
|
+
"### Reference Locations",
|
|
742
|
+
...(referenceLocations.length > 0
|
|
743
|
+
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
744
|
+
: ["- (none)"]),
|
|
745
|
+
"",
|
|
746
|
+
"### Suggested Read Order",
|
|
747
|
+
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
748
|
+
]),
|
|
749
|
+
guidanceSection([
|
|
750
|
+
"- Read the files in Suggested Read Order.",
|
|
751
|
+
"- Verify whether current status and evidence are consistent.",
|
|
752
|
+
...taskStatusGuidance(task),
|
|
753
|
+
"- If updates are needed, edit tasks/designs/reports markdown directly and keep TASK IDs unchanged.",
|
|
754
|
+
"- After editing, re-run `taskContext` to verify references and context consistency.",
|
|
755
|
+
]),
|
|
756
|
+
lintSection(lintSuggestions),
|
|
757
|
+
nextCallSection(`taskContext(projectPath=\"${governanceDir}\", taskId=\"${task.id}\")`),
|
|
758
|
+
],
|
|
759
|
+
});
|
|
760
|
+
return asText(coreMarkdown);
|
|
562
761
|
});
|
|
563
762
|
}
|