@projitive/mcp 1.0.0 → 1.0.2
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 +29 -1
- 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/index.js +1 -0
- package/output/projitive.js +252 -97
- package/output/projitive.test.js +29 -1
- 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/tasks.js +341 -162
- package/output/tasks.test.js +51 -1
- 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") {
|
|
@@ -282,6 +259,160 @@ export async function parseTasksBlock(markdown) {
|
|
|
282
259
|
}
|
|
283
260
|
return tasks;
|
|
284
261
|
}
|
|
262
|
+
export function findTaskIdsOutsideMarkers(markdown) {
|
|
263
|
+
const start = markdown.indexOf(TASKS_START);
|
|
264
|
+
const end = markdown.indexOf(TASKS_END);
|
|
265
|
+
const outsideText = (start !== -1 && end !== -1 && end > start)
|
|
266
|
+
? `${markdown.slice(0, start)}\n${markdown.slice(end + TASKS_END.length)}`
|
|
267
|
+
: markdown;
|
|
268
|
+
const ids = outsideText.match(/TASK-\d{4}/g) ?? [];
|
|
269
|
+
return Array.from(new Set(ids));
|
|
270
|
+
}
|
|
271
|
+
function collectTaskLintSuggestionItems(tasks, options = {}) {
|
|
272
|
+
const suggestions = [];
|
|
273
|
+
const { markdown, outsideMarkerScopeIds } = options;
|
|
274
|
+
const duplicateIds = Array.from(tasks.reduce((counter, task) => {
|
|
275
|
+
counter.set(task.id, (counter.get(task.id) ?? 0) + 1);
|
|
276
|
+
return counter;
|
|
277
|
+
}, new Map())
|
|
278
|
+
.entries())
|
|
279
|
+
.filter(([, count]) => count > 1)
|
|
280
|
+
.map(([id]) => id);
|
|
281
|
+
if (duplicateIds.length > 0) {
|
|
282
|
+
suggestions.push({
|
|
283
|
+
code: TASK_LINT_CODES.DUPLICATE_ID,
|
|
284
|
+
message: `Duplicate task IDs detected: ${duplicateIds.join(", ")}.`,
|
|
285
|
+
fixHint: "Keep task IDs unique in marker block.",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
const inProgressWithoutOwner = tasks.filter((task) => task.status === "IN_PROGRESS" && task.owner.trim().length === 0);
|
|
289
|
+
if (inProgressWithoutOwner.length > 0) {
|
|
290
|
+
suggestions.push({
|
|
291
|
+
code: TASK_LINT_CODES.IN_PROGRESS_OWNER_EMPTY,
|
|
292
|
+
message: `${inProgressWithoutOwner.length} IN_PROGRESS task(s) have empty owner.`,
|
|
293
|
+
fixHint: "Set owner before continuing execution.",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
const doneWithoutLinks = tasks.filter((task) => task.status === "DONE" && task.links.length === 0);
|
|
297
|
+
if (doneWithoutLinks.length > 0) {
|
|
298
|
+
suggestions.push({
|
|
299
|
+
code: TASK_LINT_CODES.DONE_LINKS_MISSING,
|
|
300
|
+
message: `${doneWithoutLinks.length} DONE task(s) have no links evidence.`,
|
|
301
|
+
fixHint: "Add at least one evidence link before keeping DONE.",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const blockedWithoutReason = tasks.filter((task) => task.status === "BLOCKED" && task.summary.trim().length === 0);
|
|
305
|
+
if (blockedWithoutReason.length > 0) {
|
|
306
|
+
suggestions.push({
|
|
307
|
+
code: TASK_LINT_CODES.BLOCKED_SUMMARY_EMPTY,
|
|
308
|
+
message: `${blockedWithoutReason.length} BLOCKED task(s) have empty summary.`,
|
|
309
|
+
fixHint: "Add blocker reason and unblock condition.",
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const invalidUpdatedAt = tasks.filter((task) => !Number.isFinite(new Date(task.updatedAt).getTime()));
|
|
313
|
+
if (invalidUpdatedAt.length > 0) {
|
|
314
|
+
suggestions.push({
|
|
315
|
+
code: TASK_LINT_CODES.UPDATED_AT_INVALID,
|
|
316
|
+
message: `${invalidUpdatedAt.length} task(s) have invalid updatedAt format.`,
|
|
317
|
+
fixHint: "Use ISO8601 UTC timestamp.",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const missingRoadmapRefs = tasks.filter((task) => task.roadmapRefs.length === 0);
|
|
321
|
+
if (missingRoadmapRefs.length > 0) {
|
|
322
|
+
suggestions.push({
|
|
323
|
+
code: TASK_LINT_CODES.ROADMAP_REFS_EMPTY,
|
|
324
|
+
message: `${missingRoadmapRefs.length} task(s) have empty roadmapRefs.`,
|
|
325
|
+
fixHint: "Bind at least one ROADMAP-xxxx when applicable.",
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
if (typeof markdown === "string") {
|
|
329
|
+
const outsideMarkerTaskIds = findTaskIdsOutsideMarkers(markdown)
|
|
330
|
+
.filter((taskId) => (outsideMarkerScopeIds ? outsideMarkerScopeIds.has(taskId) : true));
|
|
331
|
+
if (outsideMarkerTaskIds.length > 0) {
|
|
332
|
+
suggestions.push({
|
|
333
|
+
code: TASK_LINT_CODES.OUTSIDE_MARKER,
|
|
334
|
+
message: `TASK IDs found outside marker block: ${outsideMarkerTaskIds.join(", ")}.`,
|
|
335
|
+
fixHint: "Keep task source of truth inside marker region only.",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return suggestions;
|
|
340
|
+
}
|
|
341
|
+
export function collectTaskLintSuggestions(tasks, markdown, outsideMarkerScopeIds) {
|
|
342
|
+
return renderLintSuggestions(collectTaskLintSuggestionItems(tasks, { markdown, outsideMarkerScopeIds }));
|
|
343
|
+
}
|
|
344
|
+
function collectSingleTaskLintSuggestions(task) {
|
|
345
|
+
const suggestions = [];
|
|
346
|
+
if (task.status === "IN_PROGRESS" && task.owner.trim().length === 0) {
|
|
347
|
+
suggestions.push({
|
|
348
|
+
code: TASK_LINT_CODES.IN_PROGRESS_OWNER_EMPTY,
|
|
349
|
+
message: "Current task is IN_PROGRESS but owner is empty.",
|
|
350
|
+
fixHint: "Set owner before continuing execution.",
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (task.status === "DONE" && task.links.length === 0) {
|
|
354
|
+
suggestions.push({
|
|
355
|
+
code: TASK_LINT_CODES.DONE_LINKS_MISSING,
|
|
356
|
+
message: "Current task is DONE but has no links evidence.",
|
|
357
|
+
fixHint: "Add at least one evidence link.",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
if (task.status === "BLOCKED" && task.summary.trim().length === 0) {
|
|
361
|
+
suggestions.push({
|
|
362
|
+
code: TASK_LINT_CODES.BLOCKED_SUMMARY_EMPTY,
|
|
363
|
+
message: "Current task is BLOCKED but summary is empty.",
|
|
364
|
+
fixHint: "Add blocker reason and unblock condition.",
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (!Number.isFinite(new Date(task.updatedAt).getTime())) {
|
|
368
|
+
suggestions.push({
|
|
369
|
+
code: TASK_LINT_CODES.UPDATED_AT_INVALID,
|
|
370
|
+
message: "Current task updatedAt is invalid.",
|
|
371
|
+
fixHint: "Use ISO8601 UTC timestamp.",
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (task.roadmapRefs.length === 0) {
|
|
375
|
+
suggestions.push({
|
|
376
|
+
code: TASK_LINT_CODES.ROADMAP_REFS_EMPTY,
|
|
377
|
+
message: "Current task has empty roadmapRefs.",
|
|
378
|
+
fixHint: "Bind ROADMAP-xxxx where applicable.",
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return renderLintSuggestions(suggestions);
|
|
382
|
+
}
|
|
383
|
+
async function collectTaskFileLintSuggestions(governanceDir, task) {
|
|
384
|
+
const suggestions = [];
|
|
385
|
+
for (const link of task.links) {
|
|
386
|
+
const normalized = link.trim();
|
|
387
|
+
if (normalized.length === 0) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (/^https?:\/\//i.test(normalized)) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
const resolvedPath = path.resolve(governanceDir, normalized);
|
|
394
|
+
const exists = await fs.access(resolvedPath).then(() => true).catch(() => false);
|
|
395
|
+
if (!exists) {
|
|
396
|
+
suggestions.push({
|
|
397
|
+
code: TASK_LINT_CODES.LINK_TARGET_MISSING,
|
|
398
|
+
message: `Link target not found: ${normalized} (resolved: ${resolvedPath}).`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const hookEntries = Object.entries(task.hooks)
|
|
403
|
+
.filter(([, value]) => typeof value === "string" && value.trim().length > 0);
|
|
404
|
+
for (const [hookKey, hookPath] of hookEntries) {
|
|
405
|
+
const resolvedPath = path.resolve(governanceDir, hookPath);
|
|
406
|
+
const exists = await fs.access(resolvedPath).then(() => true).catch(() => false);
|
|
407
|
+
if (!exists) {
|
|
408
|
+
suggestions.push({
|
|
409
|
+
code: TASK_LINT_CODES.HOOK_FILE_MISSING,
|
|
410
|
+
message: `Hook file not found for ${hookKey}: ${hookPath} (resolved: ${resolvedPath}).`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return renderLintSuggestions(suggestions);
|
|
415
|
+
}
|
|
285
416
|
export function renderTasksMarkdown(tasks) {
|
|
286
417
|
const sections = tasks.map((task) => {
|
|
287
418
|
const roadmapRefs = task.roadmapRefs.length > 0 ? task.roadmapRefs.join(", ") : "(none)";
|
|
@@ -326,9 +457,13 @@ export async function ensureTasksFile(inputPath) {
|
|
|
326
457
|
return tasksPath;
|
|
327
458
|
}
|
|
328
459
|
export async function loadTasks(inputPath) {
|
|
460
|
+
const { tasksPath, tasks } = await loadTasksDocument(inputPath);
|
|
461
|
+
return { tasksPath, tasks };
|
|
462
|
+
}
|
|
463
|
+
export async function loadTasksDocument(inputPath) {
|
|
329
464
|
const tasksPath = await ensureTasksFile(inputPath);
|
|
330
465
|
const markdown = await fs.readFile(tasksPath, "utf-8");
|
|
331
|
-
return { tasksPath, tasks: await parseTasksBlock(markdown) };
|
|
466
|
+
return { tasksPath, markdown, tasks: await parseTasksBlock(markdown) };
|
|
332
467
|
}
|
|
333
468
|
export async function saveTasks(tasksPath, tasks) {
|
|
334
469
|
const normalized = tasks.map((task) => normalizeTask(task));
|
|
@@ -357,31 +492,41 @@ export function registerTaskTools(server) {
|
|
|
357
492
|
},
|
|
358
493
|
}, async ({ projectPath, status, limit }) => {
|
|
359
494
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
360
|
-
const { tasksPath, tasks } = await
|
|
495
|
+
const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
|
|
361
496
|
const filtered = tasks
|
|
362
497
|
.filter((task) => (status ? task.status === status : true))
|
|
363
498
|
.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
|
-
|
|
499
|
+
const lintSuggestions = collectTaskLintSuggestions(filtered, tasksMarkdown, new Set(filtered.map((task) => task.id)));
|
|
500
|
+
if (status && filtered.length === 0) {
|
|
501
|
+
appendLintSuggestions(lintSuggestions, [
|
|
502
|
+
{
|
|
503
|
+
code: TASK_LINT_CODES.FILTER_EMPTY,
|
|
504
|
+
message: `No tasks matched status=${status}.`,
|
|
505
|
+
fixHint: "Confirm status values or update task states.",
|
|
506
|
+
},
|
|
507
|
+
]);
|
|
508
|
+
}
|
|
509
|
+
const nextTaskId = filtered[0]?.id;
|
|
510
|
+
const markdown = renderToolResponseMarkdown({
|
|
511
|
+
toolName: "taskList",
|
|
512
|
+
sections: [
|
|
513
|
+
summarySection([
|
|
514
|
+
`- governanceDir: ${governanceDir}`,
|
|
515
|
+
`- tasksPath: ${tasksPath}`,
|
|
516
|
+
`- filter.status: ${status ?? "(none)"}`,
|
|
517
|
+
`- returned: ${filtered.length}`,
|
|
518
|
+
]),
|
|
519
|
+
evidenceSection([
|
|
520
|
+
"- tasks:",
|
|
521
|
+
...filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner || ""} | updatedAt=${task.updatedAt}`),
|
|
522
|
+
]),
|
|
523
|
+
guidanceSection(["- Pick one task ID and call `taskContext`."]),
|
|
524
|
+
lintSection(lintSuggestions),
|
|
525
|
+
nextCallSection(nextTaskId
|
|
526
|
+
? `taskContext(projectPath=\"${governanceDir}\", taskId=\"${nextTaskId}\")`
|
|
527
|
+
: undefined),
|
|
528
|
+
],
|
|
529
|
+
});
|
|
385
530
|
return asText(markdown);
|
|
386
531
|
});
|
|
387
532
|
server.registerTool("taskNext", {
|
|
@@ -398,29 +543,29 @@ export function registerTaskTools(server) {
|
|
|
398
543
|
const projects = await discoverProjects(root, depth);
|
|
399
544
|
const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
|
|
400
545
|
if (rankedCandidates.length === 0) {
|
|
401
|
-
const markdown =
|
|
402
|
-
"
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
`- projectNext(rootPath=\"${root}\", maxDepth=${depth})`,
|
|
420
|
-
].join("\n");
|
|
546
|
+
const markdown = renderToolResponseMarkdown({
|
|
547
|
+
toolName: "taskNext",
|
|
548
|
+
sections: [
|
|
549
|
+
summarySection([
|
|
550
|
+
`- rootPath: ${root}`,
|
|
551
|
+
`- maxDepth: ${depth}`,
|
|
552
|
+
`- matchedProjects: ${projects.length}`,
|
|
553
|
+
"- actionableTasks: 0",
|
|
554
|
+
]),
|
|
555
|
+
evidenceSection(["- candidates:", "- (none)"]),
|
|
556
|
+
guidanceSection([
|
|
557
|
+
"- No TODO/IN_PROGRESS task is available.",
|
|
558
|
+
"- Create or reopen tasks in tasks.md, then rerun `taskNext`.",
|
|
559
|
+
]),
|
|
560
|
+
lintSection(["- No actionable tasks found. Verify task statuses and required fields in marker block."]),
|
|
561
|
+
nextCallSection(`projectNext(rootPath=\"${root}\", maxDepth=${depth})`),
|
|
562
|
+
],
|
|
563
|
+
});
|
|
421
564
|
return asText(markdown);
|
|
422
565
|
}
|
|
423
566
|
const selected = rankedCandidates[0];
|
|
567
|
+
const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
|
|
568
|
+
const lintSuggestions = collectTaskLintSuggestions(selectedTaskDocument.tasks, selectedTaskDocument.markdown);
|
|
424
569
|
const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
|
|
425
570
|
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
426
571
|
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
|
|
@@ -428,55 +573,56 @@ export function registerTaskTools(server) {
|
|
|
428
573
|
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
429
574
|
const suggestedReadOrder = [selected.tasksPath, ...relatedArtifacts.filter((item) => item !== selected.tasksPath)];
|
|
430
575
|
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
|
-
|
|
576
|
+
const markdown = renderToolResponseMarkdown({
|
|
577
|
+
toolName: "taskNext",
|
|
578
|
+
sections: [
|
|
579
|
+
summarySection([
|
|
580
|
+
`- rootPath: ${root}`,
|
|
581
|
+
`- maxDepth: ${depth}`,
|
|
582
|
+
`- matchedProjects: ${projects.length}`,
|
|
583
|
+
`- actionableTasks: ${rankedCandidates.length}`,
|
|
584
|
+
`- selectedProject: ${selected.governanceDir}`,
|
|
585
|
+
`- selectedTaskId: ${selected.task.id}`,
|
|
586
|
+
`- selectedTaskStatus: ${selected.task.status}`,
|
|
587
|
+
]),
|
|
588
|
+
evidenceSection([
|
|
589
|
+
"### Selected Task",
|
|
590
|
+
`- id: ${selected.task.id}`,
|
|
591
|
+
`- title: ${selected.task.title}`,
|
|
592
|
+
`- owner: ${selected.task.owner || "(none)"}`,
|
|
593
|
+
`- updatedAt: ${selected.task.updatedAt}`,
|
|
594
|
+
`- roadmapRefs: ${selected.task.roadmapRefs.join(", ") || "(none)"}`,
|
|
595
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selected.tasksPath}`,
|
|
596
|
+
"",
|
|
597
|
+
"### Top Candidates",
|
|
598
|
+
...rankedCandidates
|
|
599
|
+
.slice(0, candidateLimit)
|
|
600
|
+
.map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | project=${item.governanceDir} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
|
|
601
|
+
"",
|
|
602
|
+
"### Selection Reason",
|
|
603
|
+
"- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.",
|
|
604
|
+
`- Selected candidate scores: projectScore=${selected.projectScore}, taskPriority=${selected.taskPriority}, taskUpdatedAtMs=${selected.taskUpdatedAtMs}.`,
|
|
605
|
+
"",
|
|
606
|
+
"### Related Artifacts",
|
|
607
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
|
|
608
|
+
"",
|
|
609
|
+
"### Reference Locations",
|
|
610
|
+
...(referenceLocations.length > 0
|
|
611
|
+
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
612
|
+
: ["- (none)"]),
|
|
613
|
+
"",
|
|
614
|
+
"### Suggested Read Order",
|
|
615
|
+
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
616
|
+
]),
|
|
617
|
+
guidanceSection([
|
|
618
|
+
"- Start immediately with Suggested Read Order and execute the selected task.",
|
|
619
|
+
"- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.",
|
|
620
|
+
"- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.",
|
|
621
|
+
]),
|
|
622
|
+
lintSection(lintSuggestions),
|
|
623
|
+
nextCallSection(`taskContext(projectPath=\"${selected.governanceDir}\", taskId=\"${selected.task.id}\")`),
|
|
624
|
+
],
|
|
625
|
+
});
|
|
480
626
|
return asText(markdown);
|
|
481
627
|
});
|
|
482
628
|
server.registerTool("taskContext", {
|
|
@@ -489,20 +635,34 @@ export function registerTaskTools(server) {
|
|
|
489
635
|
}, async ({ projectPath, taskId }) => {
|
|
490
636
|
if (!isValidTaskId(taskId)) {
|
|
491
637
|
return {
|
|
492
|
-
...asText(renderErrorMarkdown("taskContext", `Invalid task ID format: ${taskId}`, ["
|
|
638
|
+
...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
639
|
isError: true,
|
|
494
640
|
};
|
|
495
641
|
}
|
|
496
642
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
497
|
-
const { tasksPath, tasks } = await
|
|
643
|
+
const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
|
|
498
644
|
const taskContextHooks = await readTaskContextHooks(governanceDir);
|
|
499
645
|
const task = tasks.find((item) => item.id === taskId);
|
|
500
646
|
if (!task) {
|
|
501
647
|
return {
|
|
502
|
-
...asText(renderErrorMarkdown("taskContext", `Task not found: ${taskId}`, ["
|
|
648
|
+
...asText(renderErrorMarkdown("taskContext", `Task not found: ${taskId}`, ["run `taskList` to discover available IDs", "retry with an existing task ID"], `taskList(projectPath=\"${governanceDir}\")`)),
|
|
503
649
|
isError: true,
|
|
504
650
|
};
|
|
505
651
|
}
|
|
652
|
+
const lintSuggestions = [
|
|
653
|
+
...collectSingleTaskLintSuggestions(task),
|
|
654
|
+
...(await collectTaskFileLintSuggestions(governanceDir, task)),
|
|
655
|
+
];
|
|
656
|
+
const outsideMarkerTaskIds = findTaskIdsOutsideMarkers(tasksMarkdown);
|
|
657
|
+
if (outsideMarkerTaskIds.includes(task.id)) {
|
|
658
|
+
appendLintSuggestions(lintSuggestions, [
|
|
659
|
+
{
|
|
660
|
+
code: TASK_LINT_CODES.OUTSIDE_MARKER,
|
|
661
|
+
message: `Current task ID appears outside marker block (${task.id}).`,
|
|
662
|
+
fixHint: "Keep task source of truth inside marker region.",
|
|
663
|
+
},
|
|
664
|
+
]);
|
|
665
|
+
}
|
|
506
666
|
const taskLocation = (await findTextReferences(tasksPath, taskId))[0];
|
|
507
667
|
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
508
668
|
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
@@ -513,45 +673,64 @@ export function registerTaskTools(server) {
|
|
|
513
673
|
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
514
674
|
.map((value) => path.resolve(governanceDir, value));
|
|
515
675
|
const hookStatus = `head=${taskContextHooks.head ? "loaded" : "missing"}, footer=${taskContextHooks.footer ? "loaded" : "missing"}`;
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
676
|
+
if (!taskContextHooks.head) {
|
|
677
|
+
appendLintSuggestions(lintSuggestions, [
|
|
678
|
+
{
|
|
679
|
+
code: TASK_LINT_CODES.CONTEXT_HOOK_HEAD_MISSING,
|
|
680
|
+
message: `Missing ${taskContextHooks.headPath}.`,
|
|
681
|
+
fixHint: "Add a task context head hook template if you need standardized preface.",
|
|
682
|
+
},
|
|
683
|
+
]);
|
|
684
|
+
}
|
|
685
|
+
if (!taskContextHooks.footer) {
|
|
686
|
+
appendLintSuggestions(lintSuggestions, [
|
|
687
|
+
{
|
|
688
|
+
code: TASK_LINT_CODES.CONTEXT_HOOK_FOOTER_MISSING,
|
|
689
|
+
message: `Missing ${taskContextHooks.footerPath}.`,
|
|
690
|
+
fixHint: "Add a task context footer hook template for standardized close-out checks.",
|
|
691
|
+
},
|
|
692
|
+
]);
|
|
693
|
+
}
|
|
694
|
+
const coreMarkdown = renderToolResponseMarkdown({
|
|
695
|
+
toolName: "taskContext",
|
|
696
|
+
sections: [
|
|
697
|
+
summarySection([
|
|
698
|
+
`- governanceDir: ${governanceDir}`,
|
|
699
|
+
`- taskId: ${task.id}`,
|
|
700
|
+
`- title: ${task.title}`,
|
|
701
|
+
`- status: ${task.status}`,
|
|
702
|
+
`- owner: ${task.owner}`,
|
|
703
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
704
|
+
`- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
|
|
705
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : tasksPath}`,
|
|
706
|
+
`- hookStatus: ${hookStatus}`,
|
|
707
|
+
]),
|
|
708
|
+
evidenceSection([
|
|
709
|
+
"### Related Artifacts",
|
|
710
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
|
|
711
|
+
"",
|
|
712
|
+
"### Reference Locations",
|
|
713
|
+
...(referenceLocations.length > 0
|
|
714
|
+
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
715
|
+
: ["- (none)"]),
|
|
716
|
+
"",
|
|
717
|
+
"### Hook Paths",
|
|
718
|
+
...(hookPaths.length > 0 ? hookPaths.map((item) => `- ${item}`) : ["- (none)"]),
|
|
719
|
+
"",
|
|
720
|
+
"### Suggested Read Order",
|
|
721
|
+
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
722
|
+
]),
|
|
723
|
+
guidanceSection([
|
|
724
|
+
"- Read the files in Suggested Read Order.",
|
|
725
|
+
"- Verify whether current status and evidence are consistent.",
|
|
726
|
+
...taskStatusGuidance(task),
|
|
727
|
+
"- If updates are needed, edit tasks/designs/reports markdown directly and keep TASK IDs unchanged.",
|
|
728
|
+
"- After editing, re-run `taskContext` to verify references and context consistency.",
|
|
729
|
+
]),
|
|
730
|
+
lintSection(lintSuggestions),
|
|
731
|
+
nextCallSection(`taskContext(projectPath=\"${governanceDir}\", taskId=\"${task.id}\")`),
|
|
732
|
+
],
|
|
733
|
+
});
|
|
555
734
|
const markdownParts = [
|
|
556
735
|
taskContextHooks.head,
|
|
557
736
|
coreMarkdown,
|