@projitive/mcp 1.0.0-beta.4 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -84
- package/output/index.js +183 -538
- package/output/projitive.js +259 -0
- package/output/prompts.js +87 -0
- package/output/resources.js +95 -0
- package/output/roadmap.js +135 -0
- package/output/tasks.js +322 -1
- package/package.json +5 -2
package/output/tasks.js
CHANGED
|
@@ -1,12 +1,118 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { discoverGovernanceArtifacts } from "./helpers/files/index.js";
|
|
5
|
+
import { findTextReferences } from "./helpers/markdown/index.js";
|
|
3
6
|
import { catchIt } from "./helpers/catch/index.js";
|
|
4
|
-
import { resolveGovernanceDir } from "./projitive.js";
|
|
7
|
+
import { resolveGovernanceDir, resolveScanDepth, resolveScanRoot, discoverProjects } from "./projitive.js";
|
|
5
8
|
import { isValidRoadmapId } from "./roadmap.js";
|
|
6
9
|
export const TASKS_START = "<!-- PROJITIVE:TASKS:START -->";
|
|
7
10
|
export const TASKS_END = "<!-- PROJITIVE:TASKS:END -->";
|
|
8
11
|
export const ALLOWED_STATUS = ["TODO", "IN_PROGRESS", "BLOCKED", "DONE"];
|
|
9
12
|
export const TASK_ID_REGEX = /^TASK-\d{4}$/;
|
|
13
|
+
function asText(markdown) {
|
|
14
|
+
return {
|
|
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");
|
|
31
|
+
}
|
|
32
|
+
function taskStatusGuidance(task) {
|
|
33
|
+
if (task.status === "TODO") {
|
|
34
|
+
return [
|
|
35
|
+
"- This task is TODO: confirm scope and set execution plan before edits.",
|
|
36
|
+
"- Move to IN_PROGRESS only after owner and initial evidence are ready.",
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
if (task.status === "IN_PROGRESS") {
|
|
40
|
+
return [
|
|
41
|
+
"- This task is IN_PROGRESS: prioritize finishing with report/design evidence updates.",
|
|
42
|
+
"- Verify references stay consistent before marking DONE.",
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
if (task.status === "BLOCKED") {
|
|
46
|
+
return [
|
|
47
|
+
"- This task is BLOCKED: identify blocker and required unblock condition first.",
|
|
48
|
+
"- Reopen only after blocker evidence is documented.",
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
return [
|
|
52
|
+
"- This task is DONE: only reopen when new requirement changes scope.",
|
|
53
|
+
"- Keep report evidence immutable unless correction is required.",
|
|
54
|
+
];
|
|
55
|
+
}
|
|
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
|
+
async function readOptionalMarkdown(filePath) {
|
|
67
|
+
const content = await fs.readFile(filePath, "utf-8").catch(() => undefined);
|
|
68
|
+
if (typeof content !== "string") {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const trimmed = content.trim();
|
|
72
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
73
|
+
}
|
|
74
|
+
async function readTaskContextHooks(governanceDir) {
|
|
75
|
+
const headPath = path.join(governanceDir, "hooks", "task_get_head.md");
|
|
76
|
+
const footerPath = path.join(governanceDir, "hooks", "task_get_footer.md");
|
|
77
|
+
const [head, footer] = await Promise.all([readOptionalMarkdown(headPath), readOptionalMarkdown(footerPath)]);
|
|
78
|
+
return { head, footer, headPath, footerPath };
|
|
79
|
+
}
|
|
80
|
+
function latestTaskUpdatedAt(tasks) {
|
|
81
|
+
const timestamps = tasks
|
|
82
|
+
.map((task) => new Date(task.updatedAt).getTime())
|
|
83
|
+
.filter((value) => Number.isFinite(value));
|
|
84
|
+
if (timestamps.length === 0) {
|
|
85
|
+
return "(unknown)";
|
|
86
|
+
}
|
|
87
|
+
return new Date(Math.max(...timestamps)).toISOString();
|
|
88
|
+
}
|
|
89
|
+
function actionableScore(tasks) {
|
|
90
|
+
return tasks.filter((task) => task.status === "IN_PROGRESS").length * 2
|
|
91
|
+
+ tasks.filter((task) => task.status === "TODO").length;
|
|
92
|
+
}
|
|
93
|
+
async function readActionableTaskCandidates(governanceDirs) {
|
|
94
|
+
const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
|
|
95
|
+
const snapshot = await loadTasks(governanceDir);
|
|
96
|
+
return {
|
|
97
|
+
governanceDir,
|
|
98
|
+
tasksPath: snapshot.tasksPath,
|
|
99
|
+
tasks: snapshot.tasks,
|
|
100
|
+
projectScore: actionableScore(snapshot.tasks),
|
|
101
|
+
projectLatestUpdatedAt: latestTaskUpdatedAt(snapshot.tasks),
|
|
102
|
+
};
|
|
103
|
+
}));
|
|
104
|
+
return snapshots.flatMap((item) => item.tasks
|
|
105
|
+
.filter((task) => task.status === "IN_PROGRESS" || task.status === "TODO")
|
|
106
|
+
.map((task) => ({
|
|
107
|
+
governanceDir: item.governanceDir,
|
|
108
|
+
tasksPath: item.tasksPath,
|
|
109
|
+
task,
|
|
110
|
+
projectScore: item.projectScore,
|
|
111
|
+
projectLatestUpdatedAt: item.projectLatestUpdatedAt,
|
|
112
|
+
taskUpdatedAtMs: toTaskUpdatedAtMs(task.updatedAt),
|
|
113
|
+
taskPriority: taskPriority(task.status),
|
|
114
|
+
})));
|
|
115
|
+
}
|
|
10
116
|
export function nowIso() {
|
|
11
117
|
return new Date().toISOString();
|
|
12
118
|
}
|
|
@@ -240,3 +346,218 @@ export function validateTransition(from, to) {
|
|
|
240
346
|
};
|
|
241
347
|
return allowed[from].has(to);
|
|
242
348
|
}
|
|
349
|
+
export function registerTaskTools(server) {
|
|
350
|
+
server.registerTool("taskList", {
|
|
351
|
+
title: "Task List",
|
|
352
|
+
description: "List project tasks with optional status filter for agent planning",
|
|
353
|
+
inputSchema: {
|
|
354
|
+
projectPath: z.string(),
|
|
355
|
+
status: z.enum(["TODO", "IN_PROGRESS", "BLOCKED", "DONE"]).optional(),
|
|
356
|
+
limit: z.number().int().min(1).max(200).optional(),
|
|
357
|
+
},
|
|
358
|
+
}, async ({ projectPath, status, limit }) => {
|
|
359
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
360
|
+
const { tasksPath, tasks } = await loadTasks(governanceDir);
|
|
361
|
+
const filtered = tasks
|
|
362
|
+
.filter((task) => (status ? task.status === status : true))
|
|
363
|
+
.slice(0, limit ?? 100);
|
|
364
|
+
const markdown = [
|
|
365
|
+
"# taskList",
|
|
366
|
+
"",
|
|
367
|
+
"## Summary",
|
|
368
|
+
`- governanceDir: ${governanceDir}`,
|
|
369
|
+
`- tasksPath: ${tasksPath}`,
|
|
370
|
+
`- filter.status: ${status ?? "(none)"}`,
|
|
371
|
+
`- returned: ${filtered.length}`,
|
|
372
|
+
"",
|
|
373
|
+
"## Evidence",
|
|
374
|
+
"- tasks:",
|
|
375
|
+
...(filtered.length > 0
|
|
376
|
+
? filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner || ""} | updatedAt=${task.updatedAt}`)
|
|
377
|
+
: ["- (none)"]),
|
|
378
|
+
"",
|
|
379
|
+
"## Agent Guidance",
|
|
380
|
+
"- Pick one task ID and call `taskContext`.",
|
|
381
|
+
"",
|
|
382
|
+
"## Next Call",
|
|
383
|
+
`- taskContext(projectPath=\"${governanceDir}\", taskId=\"TASK-0001\")`,
|
|
384
|
+
].join("\n");
|
|
385
|
+
return asText(markdown);
|
|
386
|
+
});
|
|
387
|
+
server.registerTool("taskNext", {
|
|
388
|
+
title: "Task Next",
|
|
389
|
+
description: "One-step discover and select the most actionable task with evidence and start guidance",
|
|
390
|
+
inputSchema: {
|
|
391
|
+
rootPath: z.string().optional(),
|
|
392
|
+
maxDepth: z.number().int().min(0).max(8).optional(),
|
|
393
|
+
topCandidates: z.number().int().min(1).max(20).optional(),
|
|
394
|
+
},
|
|
395
|
+
}, async ({ rootPath, maxDepth, topCandidates }) => {
|
|
396
|
+
const root = resolveScanRoot(rootPath);
|
|
397
|
+
const depth = resolveScanDepth(maxDepth);
|
|
398
|
+
const projects = await discoverProjects(root, depth);
|
|
399
|
+
const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
|
|
400
|
+
if (rankedCandidates.length === 0) {
|
|
401
|
+
const markdown = [
|
|
402
|
+
"# taskNext",
|
|
403
|
+
"",
|
|
404
|
+
"## Summary",
|
|
405
|
+
`- rootPath: ${root}`,
|
|
406
|
+
`- maxDepth: ${depth}`,
|
|
407
|
+
`- matchedProjects: ${projects.length}`,
|
|
408
|
+
"- actionableTasks: 0",
|
|
409
|
+
"",
|
|
410
|
+
"## Evidence",
|
|
411
|
+
"- candidates:",
|
|
412
|
+
"- (none)",
|
|
413
|
+
"",
|
|
414
|
+
"## Agent Guidance",
|
|
415
|
+
"- No TODO/IN_PROGRESS task is available.",
|
|
416
|
+
"- Create or reopen tasks in tasks.md, then rerun `taskNext`.",
|
|
417
|
+
"",
|
|
418
|
+
"## Next Call",
|
|
419
|
+
`- projectNext(rootPath=\"${root}\", maxDepth=${depth})`,
|
|
420
|
+
].join("\n");
|
|
421
|
+
return asText(markdown);
|
|
422
|
+
}
|
|
423
|
+
const selected = rankedCandidates[0];
|
|
424
|
+
const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
|
|
425
|
+
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
426
|
+
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
|
|
427
|
+
const taskLocation = (await findTextReferences(selected.tasksPath, selected.task.id))[0];
|
|
428
|
+
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
429
|
+
const suggestedReadOrder = [selected.tasksPath, ...relatedArtifacts.filter((item) => item !== selected.tasksPath)];
|
|
430
|
+
const candidateLimit = topCandidates ?? 5;
|
|
431
|
+
const markdown = [
|
|
432
|
+
"# taskNext",
|
|
433
|
+
"",
|
|
434
|
+
"## Summary",
|
|
435
|
+
`- rootPath: ${root}`,
|
|
436
|
+
`- maxDepth: ${depth}`,
|
|
437
|
+
`- matchedProjects: ${projects.length}`,
|
|
438
|
+
`- actionableTasks: ${rankedCandidates.length}`,
|
|
439
|
+
`- selectedProject: ${selected.governanceDir}`,
|
|
440
|
+
`- selectedTaskId: ${selected.task.id}`,
|
|
441
|
+
`- selectedTaskStatus: ${selected.task.status}`,
|
|
442
|
+
"",
|
|
443
|
+
"## Evidence",
|
|
444
|
+
"### Selected Task",
|
|
445
|
+
`- id: ${selected.task.id}`,
|
|
446
|
+
`- title: ${selected.task.title}`,
|
|
447
|
+
`- owner: ${selected.task.owner || "(none)"}`,
|
|
448
|
+
`- updatedAt: ${selected.task.updatedAt}`,
|
|
449
|
+
`- roadmapRefs: ${selected.task.roadmapRefs.join(", ") || "(none)"}`,
|
|
450
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selected.tasksPath}`,
|
|
451
|
+
"",
|
|
452
|
+
"### Top Candidates",
|
|
453
|
+
...rankedCandidates
|
|
454
|
+
.slice(0, candidateLimit)
|
|
455
|
+
.map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | project=${item.governanceDir} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
|
|
456
|
+
"",
|
|
457
|
+
"### Selection Reason",
|
|
458
|
+
"- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.",
|
|
459
|
+
`- Selected candidate scores: projectScore=${selected.projectScore}, taskPriority=${selected.taskPriority}, taskUpdatedAtMs=${selected.taskUpdatedAtMs}.`,
|
|
460
|
+
"",
|
|
461
|
+
"### Related Artifacts",
|
|
462
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
|
|
463
|
+
"",
|
|
464
|
+
"### Reference Locations",
|
|
465
|
+
...(referenceLocations.length > 0
|
|
466
|
+
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
467
|
+
: ["- (none)"]),
|
|
468
|
+
"",
|
|
469
|
+
"### Suggested Read Order",
|
|
470
|
+
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
471
|
+
"",
|
|
472
|
+
"## Agent Guidance",
|
|
473
|
+
"- Start immediately with Suggested Read Order and execute the selected task.",
|
|
474
|
+
"- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.",
|
|
475
|
+
"- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.",
|
|
476
|
+
"",
|
|
477
|
+
"## Next Call",
|
|
478
|
+
`- taskContext(projectPath=\"${selected.governanceDir}\", taskId=\"${selected.task.id}\")`,
|
|
479
|
+
].join("\n");
|
|
480
|
+
return asText(markdown);
|
|
481
|
+
});
|
|
482
|
+
server.registerTool("taskContext", {
|
|
483
|
+
title: "Task Context",
|
|
484
|
+
description: "Get one task with detail, evidence locations, and execution guidance",
|
|
485
|
+
inputSchema: {
|
|
486
|
+
projectPath: z.string(),
|
|
487
|
+
taskId: z.string(),
|
|
488
|
+
},
|
|
489
|
+
}, async ({ projectPath, taskId }) => {
|
|
490
|
+
if (!isValidTaskId(taskId)) {
|
|
491
|
+
return {
|
|
492
|
+
...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
|
+
isError: true,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
497
|
+
const { tasksPath, tasks } = await loadTasks(governanceDir);
|
|
498
|
+
const taskContextHooks = await readTaskContextHooks(governanceDir);
|
|
499
|
+
const task = tasks.find((item) => item.id === taskId);
|
|
500
|
+
if (!task) {
|
|
501
|
+
return {
|
|
502
|
+
...asText(renderErrorMarkdown("taskContext", `Task not found: ${taskId}`, ["- run `taskList` to discover available IDs", "- retry with an existing task ID"], `taskList(projectPath=\"${governanceDir}\")`)),
|
|
503
|
+
isError: true,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const taskLocation = (await findTextReferences(tasksPath, taskId))[0];
|
|
507
|
+
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
508
|
+
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
509
|
+
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
|
|
510
|
+
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
511
|
+
const suggestedReadOrder = [tasksPath, ...relatedArtifacts.filter((item) => item !== tasksPath)];
|
|
512
|
+
const hookPaths = Object.values(task.hooks)
|
|
513
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
514
|
+
.map((value) => path.resolve(governanceDir, value));
|
|
515
|
+
const hookStatus = `head=${taskContextHooks.head ? "loaded" : "missing"}, footer=${taskContextHooks.footer ? "loaded" : "missing"}`;
|
|
516
|
+
const coreMarkdown = [
|
|
517
|
+
"# taskContext",
|
|
518
|
+
"",
|
|
519
|
+
"## Summary",
|
|
520
|
+
`- governanceDir: ${governanceDir}`,
|
|
521
|
+
`- taskId: ${task.id}`,
|
|
522
|
+
`- title: ${task.title}`,
|
|
523
|
+
`- status: ${task.status}`,
|
|
524
|
+
`- owner: ${task.owner}`,
|
|
525
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
526
|
+
`- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
|
|
527
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : tasksPath}`,
|
|
528
|
+
`- hookStatus: ${hookStatus}`,
|
|
529
|
+
"",
|
|
530
|
+
"## Evidence",
|
|
531
|
+
"### Related Artifacts",
|
|
532
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
|
|
533
|
+
"",
|
|
534
|
+
"### Reference Locations",
|
|
535
|
+
...(referenceLocations.length > 0
|
|
536
|
+
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
537
|
+
: ["- (none)"]),
|
|
538
|
+
"",
|
|
539
|
+
"### Hook Paths",
|
|
540
|
+
...(hookPaths.length > 0 ? hookPaths.map((item) => `- ${item}`) : ["- (none)"]),
|
|
541
|
+
"",
|
|
542
|
+
"### Suggested Read Order",
|
|
543
|
+
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
544
|
+
"",
|
|
545
|
+
"## Agent Guidance",
|
|
546
|
+
"- Read the files in Suggested Read Order.",
|
|
547
|
+
"- Verify whether current status and evidence are consistent.",
|
|
548
|
+
...taskStatusGuidance(task),
|
|
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);
|
|
562
|
+
});
|
|
563
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@projitive/mcp",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Projitive MCP Server for project and task discovery/update",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"bin":
|
|
8
|
+
"bin": {
|
|
9
|
+
"mcp": "output/index.js"
|
|
10
|
+
},
|
|
9
11
|
"main": "./output/index.js",
|
|
10
12
|
"types": "./output/index.d.ts",
|
|
11
13
|
"publishConfig": {
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
},
|
|
14
16
|
"scripts": {
|
|
15
17
|
"test": "vitest run",
|
|
18
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
16
19
|
"build": "tsc -p tsconfig.json",
|
|
17
20
|
"prepublishOnly": "npm run build",
|
|
18
21
|
"dev": "tsc -p tsconfig.json --watch"
|