@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/output/index.js CHANGED
@@ -1,563 +1,208 @@
1
1
  #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import process from "node:process";
4
- import fs from "node:fs/promises";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
8
  import { z } from "zod";
8
- import { discoverGovernanceArtifacts } from "./helpers/files/index.js";
9
- import { findTextReferences } from "./helpers/markdown/index.js";
10
- import { discoverProjects, resolveGovernanceDir } from "./projitive.js";
11
- import { isValidRoadmapId } from "./roadmap.js";
12
- import { isValidTaskId, loadTasks, parseTasksBlock, rankActionableTaskCandidates, taskPriority, toTaskUpdatedAtMs, } from "./tasks.js";
9
+ import { registerProjectTools } from "./projitive.js";
10
+ import { registerTaskTools } from "./tasks.js";
11
+ import { registerRoadmapTools } from "./roadmap.js";
13
12
  const PROJITIVE_SPEC_VERSION = "1.0.0";
14
13
  const server = new McpServer({
15
- name: "projitive-mcp",
14
+ name: "projitive",
16
15
  version: PROJITIVE_SPEC_VERSION,
17
16
  description: "Semantic Projitive MCP for project/task discovery and agent guidance with markdown-first outputs",
18
17
  });
19
- function asText(markdown) {
20
- return {
21
- content: [{ type: "text", text: markdown }],
22
- };
18
+ const currentFilePath = fileURLToPath(import.meta.url);
19
+ const sourceDir = path.dirname(currentFilePath);
20
+ const repoRoot = path.resolve(sourceDir, "..", "..", "..");
21
+ function resolveRepoFile(relativePath) {
22
+ return path.join(repoRoot, relativePath);
23
23
  }
24
- function renderErrorMarkdown(toolName, cause, nextSteps) {
24
+ async function readMarkdownOrFallback(relativePath, fallbackTitle) {
25
+ const absolutePath = resolveRepoFile(relativePath);
26
+ const content = await fs.readFile(absolutePath, "utf-8").catch(() => undefined);
27
+ if (typeof content === "string" && content.trim().length > 0) {
28
+ return content;
29
+ }
25
30
  return [
26
- `# ${toolName}`,
27
- "",
28
- "## Error",
29
- `- cause: ${cause}`,
31
+ `# ${fallbackTitle}`,
30
32
  "",
31
- "## Next Step",
32
- ...(nextSteps.length > 0 ? nextSteps : ["- (none)"]),
33
+ `- file: ${relativePath}`,
34
+ "- status: missing-or-empty",
35
+ "- next: create this file or ensure it has readable markdown content",
33
36
  ].join("\n");
34
37
  }
35
- function normalizePath(inputPath) {
36
- return inputPath ? path.resolve(inputPath) : process.cwd();
37
- }
38
- function candidateFilesFromArtifacts(artifacts) {
39
- return artifacts
40
- .filter((item) => item.exists)
41
- .flatMap((item) => {
42
- if (item.kind === "file") {
43
- return [item.path];
44
- }
45
- return (item.markdownFiles ?? []).map((entry) => entry.path);
46
- });
47
- }
48
- async function readOptionalMarkdown(filePath) {
49
- const content = await fs.readFile(filePath, "utf-8").catch(() => undefined);
50
- if (typeof content !== "string") {
51
- return undefined;
52
- }
53
- const trimmed = content.trim();
54
- return trimmed.length > 0 ? trimmed : undefined;
55
- }
56
- async function readTaskGetHooks(governanceDir) {
57
- const headPath = path.join(governanceDir, "hooks", "task_get_head.md");
58
- const footerPath = path.join(governanceDir, "hooks", "task_get_footer.md");
59
- const [head, footer] = await Promise.all([readOptionalMarkdown(headPath), readOptionalMarkdown(footerPath)]);
60
- return { head, footer, headPath, footerPath };
61
- }
62
- async function readRoadmapIds(governanceDir) {
63
- const roadmapPath = path.join(governanceDir, "roadmap.md");
64
- try {
65
- const markdown = await fs.readFile(roadmapPath, "utf-8");
66
- const matches = markdown.match(/ROADMAP-\d{4}/g) ?? [];
67
- return Array.from(new Set(matches));
68
- }
69
- catch {
70
- return [];
71
- }
72
- }
73
- function renderArtifactsMarkdown(artifacts) {
74
- const rows = artifacts.map((item) => {
75
- if (item.kind === "file") {
76
- const lineText = item.lineCount == null ? "-" : String(item.lineCount);
77
- return `- ${item.exists ? "✅" : "❌"} ${item.name} \n path: ${item.path} \n lineCount: ${lineText}`;
78
- }
79
- const nested = (item.markdownFiles ?? [])
80
- .map((entry) => ` - ${entry.path} (lines: ${entry.lineCount})`)
81
- .join("\n");
82
- return `- ${item.exists ? "✅" : "❌"} ${item.name}/ \n path: ${item.path}${nested ? `\n markdownFiles:\n${nested}` : ""}`;
83
- });
84
- return rows.join("\n");
85
- }
86
- async function readTasksSnapshot(governanceDir) {
87
- const tasksPath = path.join(governanceDir, "tasks.md");
88
- const markdown = await fs.readFile(tasksPath, "utf-8").catch(() => undefined);
89
- if (typeof markdown !== "string") {
90
- return { tasksPath, exists: false, tasks: [] };
91
- }
92
- const tasks = await parseTasksBlock(markdown);
93
- return { tasksPath, exists: true, tasks };
94
- }
95
- function latestTaskUpdatedAt(tasks) {
96
- const timestamps = tasks
97
- .map((task) => new Date(task.updatedAt).getTime())
98
- .filter((value) => Number.isFinite(value));
99
- if (timestamps.length === 0) {
100
- return "(unknown)";
101
- }
102
- return new Date(Math.max(...timestamps)).toISOString();
103
- }
104
- function actionableScore(tasks) {
105
- return tasks.filter((task) => task.status === "IN_PROGRESS").length * 2
106
- + tasks.filter((task) => task.status === "TODO").length;
38
+ function renderMethodCatalogMarkdown() {
39
+ return [
40
+ "# MCP Method Catalog",
41
+ "",
42
+ "## Core Pattern",
43
+ "- Prefer List/Context for primary discovery/detail flows.",
44
+ "- Use Next/Scan/Locate for acceleration and bootstrapping.",
45
+ "",
46
+ "## Methods",
47
+ "| Group | Method | Role |",
48
+ "|---|---|---|",
49
+ "| Project | projectScan | discover governance projects by marker |",
50
+ "| Project | projectNext | rank actionable projects |",
51
+ "| Project | projectLocate | resolve nearest governance root |",
52
+ "| Project | projectContext | summarize project governance context |",
53
+ "| Task | taskList | list tasks with optional filters |",
54
+ "| Task | taskNext | select top actionable task |",
55
+ "| Task | taskContext | inspect one task with references |",
56
+ "| Roadmap | roadmapList | list roadmap IDs and linked tasks |",
57
+ "| Roadmap | roadmapContext | inspect one roadmap with references |",
58
+ ].join("\n");
107
59
  }
108
- async function readActionableTaskCandidates(governanceDirs) {
109
- const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
110
- const snapshot = await readTasksSnapshot(governanceDir);
111
- return {
112
- governanceDir,
113
- tasksPath: snapshot.tasksPath,
114
- tasks: snapshot.tasks,
115
- projectScore: actionableScore(snapshot.tasks),
116
- projectLatestUpdatedAt: latestTaskUpdatedAt(snapshot.tasks),
117
- };
60
+ function registerGovernanceResources() {
61
+ server.registerResource("governanceWorkspace", "projitive://governance/workspace", {
62
+ title: "Governance Workspace",
63
+ description: "Primary governance README under .projitive",
64
+ mimeType: "text/markdown",
65
+ }, async () => ({
66
+ contents: [
67
+ {
68
+ uri: "projitive://governance/workspace",
69
+ text: await readMarkdownOrFallback(".projitive/README.md", "Governance Workspace"),
70
+ },
71
+ ],
118
72
  }));
119
- return snapshots.flatMap((item) => item.tasks
120
- .filter((task) => task.status === "IN_PROGRESS" || task.status === "TODO")
121
- .map((task) => ({
122
- governanceDir: item.governanceDir,
123
- tasksPath: item.tasksPath,
124
- task,
125
- projectScore: item.projectScore,
126
- projectLatestUpdatedAt: item.projectLatestUpdatedAt,
127
- taskUpdatedAtMs: toTaskUpdatedAtMs(task.updatedAt),
128
- taskPriority: taskPriority(task.status),
129
- })));
130
- }
131
- server.registerTool("project.scan", {
132
- title: "Project Scan",
133
- description: "Scan filesystem and discover project governance roots marked by .projitive",
134
- inputSchema: {
135
- rootPath: z.string().optional(),
136
- maxDepth: z.number().int().min(0).max(8).optional(),
137
- },
138
- }, async ({ rootPath, maxDepth }) => {
139
- const root = normalizePath(rootPath);
140
- const projects = await discoverProjects(root, maxDepth ?? 3);
141
- const markdown = [
142
- "# project.scan",
143
- "",
144
- "## Summary",
145
- `- rootPath: ${root}`,
146
- `- maxDepth: ${maxDepth ?? 3}`,
147
- `- discoveredCount: ${projects.length}`,
148
- "",
149
- "## Evidence",
150
- "- projects:",
151
- ...(projects.length > 0 ? projects.map((project, index) => `${index + 1}. ${project}`) : ["- (none)"]),
152
- "",
153
- "## Agent Guidance",
154
- "- Next: call `project.locate` with one target path to lock the active governance root.",
155
- "- Then: call `project.overview` to view artifact and task status.",
156
- ].join("\n");
157
- return asText(markdown);
158
- });
159
- server.registerTool("project.next", {
160
- title: "Project Next",
161
- description: "Directly list recently actionable projects for immediate agent progression",
162
- inputSchema: {
163
- rootPath: z.string().optional(),
164
- maxDepth: z.number().int().min(0).max(8).optional(),
165
- limit: z.number().int().min(1).max(50).optional(),
166
- },
167
- }, async ({ rootPath, maxDepth, limit }) => {
168
- const root = normalizePath(rootPath);
169
- const projects = await discoverProjects(root, maxDepth ?? 3);
170
- const snapshots = await Promise.all(projects.map(async (governanceDir) => {
171
- const snapshot = await readTasksSnapshot(governanceDir);
172
- const inProgress = snapshot.tasks.filter((task) => task.status === "IN_PROGRESS").length;
173
- const todo = snapshot.tasks.filter((task) => task.status === "TODO").length;
174
- const blocked = snapshot.tasks.filter((task) => task.status === "BLOCKED").length;
175
- const done = snapshot.tasks.filter((task) => task.status === "DONE").length;
176
- const actionable = inProgress + todo;
177
- return {
178
- governanceDir,
179
- tasksPath: snapshot.tasksPath,
180
- tasksExists: snapshot.exists,
181
- total: snapshot.tasks.length,
182
- inProgress,
183
- todo,
184
- blocked,
185
- done,
186
- actionable,
187
- latestUpdatedAt: latestTaskUpdatedAt(snapshot.tasks),
188
- score: actionableScore(snapshot.tasks),
189
- };
73
+ server.registerResource("governanceTasks", "projitive://governance/tasks", {
74
+ title: "Governance Tasks",
75
+ description: "Current task pool and status under .projitive/tasks.md",
76
+ mimeType: "text/markdown",
77
+ }, async () => ({
78
+ contents: [
79
+ {
80
+ uri: "projitive://governance/tasks",
81
+ text: await readMarkdownOrFallback(".projitive/tasks.md", "Governance Tasks"),
82
+ },
83
+ ],
190
84
  }));
191
- const ranked = snapshots
192
- .filter((item) => item.actionable > 0)
193
- .sort((a, b) => {
194
- if (b.score !== a.score) {
195
- return b.score - a.score;
196
- }
197
- return b.latestUpdatedAt.localeCompare(a.latestUpdatedAt);
198
- })
199
- .slice(0, limit ?? 10);
200
- const markdown = [
201
- "# project.next",
202
- "",
203
- "## Summary",
204
- `- rootPath: ${root}`,
205
- `- maxDepth: ${maxDepth ?? 3}`,
206
- `- matchedProjects: ${projects.length}`,
207
- `- actionableProjects: ${ranked.length}`,
208
- `- limit: ${limit ?? 10}`,
209
- "",
210
- "## Evidence",
211
- "- rankedProjects:",
212
- ...(ranked.length > 0
213
- ? ranked.map((item, index) => `${index + 1}. ${item.governanceDir} | actionable=${item.actionable} | in_progress=${item.inProgress} | todo=${item.todo} | blocked=${item.blocked} | done=${item.done} | latest=${item.latestUpdatedAt} | tasksPath=${item.tasksPath}${item.tasksExists ? "" : " (missing)"}`)
214
- : ["- (none)"]),
215
- "",
216
- "## Agent Guidance",
217
- "- Pick top 1 project and call `project.overview` with its governanceDir.",
218
- "- Then call `task.list` and `task.get` to continue execution.",
219
- "- If `tasksPath` is missing, create tasks.md using project convention before task-level operations.",
220
- ].join("\n");
221
- return asText(markdown);
222
- });
223
- server.registerTool("project.locate", {
224
- title: "Project Locate",
225
- description: "Resolve current project governance root from an in-project path by finding the nearest .projitive marker",
226
- inputSchema: {
227
- inputPath: z.string(),
228
- },
229
- }, async ({ inputPath }) => {
230
- const resolvedFrom = normalizePath(inputPath);
231
- const governanceDir = await resolveGovernanceDir(resolvedFrom);
232
- const markerPath = path.join(governanceDir, ".projitive");
233
- const markdown = [
234
- "# project.locate",
235
- "",
236
- "## Summary",
237
- `- resolvedFrom: ${resolvedFrom}`,
238
- `- governanceDir: ${governanceDir}`,
239
- `- markerPath: ${markerPath}`,
240
- "",
241
- "## Agent Guidance",
242
- "- Next: call `project.overview` with this governanceDir to get task and roadmap summaries.",
243
- ].join("\n");
244
- return asText(markdown);
245
- });
246
- server.registerTool("project.overview", {
247
- title: "Project Overview",
248
- description: "Summarize governance artifacts and task/roadmap status for agent planning",
249
- inputSchema: {
250
- projectPath: z.string(),
251
- },
252
- }, async ({ projectPath }) => {
253
- const governanceDir = await resolveGovernanceDir(projectPath);
254
- const artifacts = await discoverGovernanceArtifacts(governanceDir);
255
- const { tasksPath, tasks } = await loadTasks(governanceDir);
256
- const roadmapIds = await readRoadmapIds(governanceDir);
257
- const taskSummary = {
258
- total: tasks.length,
259
- TODO: tasks.filter((task) => task.status === "TODO").length,
260
- IN_PROGRESS: tasks.filter((task) => task.status === "IN_PROGRESS").length,
261
- BLOCKED: tasks.filter((task) => task.status === "BLOCKED").length,
262
- DONE: tasks.filter((task) => task.status === "DONE").length,
85
+ server.registerResource("governanceRoadmap", "projitive://governance/roadmap", {
86
+ title: "Governance Roadmap",
87
+ description: "Current roadmap under .projitive/roadmap.md",
88
+ mimeType: "text/markdown",
89
+ }, async () => ({
90
+ contents: [
91
+ {
92
+ uri: "projitive://governance/roadmap",
93
+ text: await readMarkdownOrFallback(".projitive/roadmap.md", "Governance Roadmap"),
94
+ },
95
+ ],
96
+ }));
97
+ server.registerResource("mcpMethodCatalog", "projitive://mcp/method-catalog", {
98
+ title: "MCP Method Catalog",
99
+ description: "Method naming and purpose map for agent routing",
100
+ mimeType: "text/markdown",
101
+ }, async () => ({
102
+ contents: [
103
+ {
104
+ uri: "projitive://mcp/method-catalog",
105
+ text: renderMethodCatalogMarkdown(),
106
+ },
107
+ ],
108
+ }));
109
+ }
110
+ function asUserPrompt(text) {
111
+ return {
112
+ messages: [
113
+ {
114
+ role: "user",
115
+ content: {
116
+ type: "text",
117
+ text,
118
+ },
119
+ },
120
+ ],
263
121
  };
264
- const markdown = [
265
- "# project.overview",
266
- "",
267
- "## Summary",
268
- `- governanceDir: ${governanceDir}`,
269
- `- tasksFile: ${tasksPath}`,
270
- `- roadmapIds: ${roadmapIds.length}`,
271
- "",
272
- "## Evidence",
273
- "### Task Summary",
274
- `- total: ${taskSummary.total}`,
275
- `- TODO: ${taskSummary.TODO}`,
276
- `- IN_PROGRESS: ${taskSummary.IN_PROGRESS}`,
277
- `- BLOCKED: ${taskSummary.BLOCKED}`,
278
- `- DONE: ${taskSummary.DONE}`,
279
- "",
280
- "### Artifacts",
281
- renderArtifactsMarkdown(artifacts),
282
- "",
283
- "## Agent Guidance",
284
- "- Next: call `task.list` to choose a target task.",
285
- "- Then: call `task.get` with a task ID to retrieve evidence locations and reading order.",
286
- ].join("\n");
287
- return asText(markdown);
288
- });
289
- server.registerTool("task.list", {
290
- title: "Task List",
291
- description: "List project tasks with optional status filter for agent planning",
292
- inputSchema: {
293
- projectPath: z.string(),
294
- status: z.enum(["TODO", "IN_PROGRESS", "BLOCKED", "DONE"]).optional(),
295
- limit: z.number().int().min(1).max(200).optional(),
296
- },
297
- }, async ({ projectPath, status, limit }) => {
298
- const governanceDir = await resolveGovernanceDir(projectPath);
299
- const { tasksPath, tasks } = await loadTasks(governanceDir);
300
- const filtered = tasks
301
- .filter((task) => (status ? task.status === status : true))
302
- .slice(0, limit ?? 100);
303
- const markdown = [
304
- "# task.list",
305
- "",
306
- "## Summary",
307
- `- governanceDir: ${governanceDir}`,
308
- `- tasksPath: ${tasksPath}`,
309
- `- filter.status: ${status ?? "(none)"}`,
310
- `- returned: ${filtered.length}`,
311
- "",
312
- "## Evidence",
313
- "- tasks:",
314
- ...(filtered.length > 0
315
- ? filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner || ""} | updatedAt=${task.updatedAt}`)
316
- : ["- (none)"]),
317
- "",
318
- "## Agent Guidance",
319
- "- Next: pick one task ID and call `task.get`.",
320
- ].join("\n");
321
- return asText(markdown);
322
- });
323
- server.registerTool("task.next", {
324
- title: "Task Next",
325
- description: "One-step discover and select the most actionable task with evidence and start guidance",
326
- inputSchema: {
327
- rootPath: z.string().optional(),
328
- maxDepth: z.number().int().min(0).max(8).optional(),
329
- topCandidates: z.number().int().min(1).max(20).optional(),
330
- },
331
- }, async ({ rootPath, maxDepth, topCandidates }) => {
332
- const root = normalizePath(rootPath);
333
- const projects = await discoverProjects(root, maxDepth ?? 3);
334
- const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
335
- if (rankedCandidates.length === 0) {
336
- const markdown = [
337
- "# task.next",
122
+ }
123
+ function registerGovernancePrompts() {
124
+ server.registerPrompt("executeTaskWorkflow", {
125
+ title: "Execute Task Workflow",
126
+ description: "Guide an agent through taskNext -> taskContext -> artifact update -> verification",
127
+ argsSchema: {
128
+ rootPath: z.string().optional(),
129
+ projectPath: z.string().optional(),
130
+ taskId: z.string().optional(),
131
+ },
132
+ }, async ({ rootPath, projectPath, taskId }) => {
133
+ const text = [
134
+ "You are executing Projitive governance workflow.",
135
+ "",
136
+ "Execution order:",
137
+ taskId && projectPath
138
+ ? `1) Run taskContext(projectPath=\"${projectPath}\", taskId=\"${taskId}\").`
139
+ : `1) Run taskNext(${rootPath ? `rootPath=\"${rootPath}\"` : ""}).`,
140
+ "2) Read Suggested Read Order and collect blocking gaps.",
141
+ "3) Update markdown artifacts only (tasks/designs/reports/roadmap as needed).",
142
+ "4) Re-run taskContext for the selected task and verify references are consistent.",
338
143
  "",
339
- "## Summary",
340
- `- rootPath: ${root}`,
341
- `- maxDepth: ${maxDepth ?? 3}`,
342
- `- matchedProjects: ${projects.length}`,
343
- "- actionableTasks: 0",
144
+ "Hard rules:",
145
+ "- Keep TASK/ROADMAP IDs immutable.",
146
+ "- Every status transition must have report evidence.",
147
+ "- Do not introduce non-governance file edits unless task scope requires.",
148
+ ].join("\n");
149
+ return asUserPrompt(text);
150
+ });
151
+ server.registerPrompt("updateTaskStatusWithEvidence", {
152
+ title: "Update Task Status With Evidence",
153
+ description: "Template for safe task status transitions and evidence alignment",
154
+ argsSchema: {
155
+ projectPath: z.string(),
156
+ taskId: z.string(),
157
+ targetStatus: z.enum(["TODO", "IN_PROGRESS", "BLOCKED", "DONE"]),
158
+ },
159
+ }, async ({ projectPath, taskId, targetStatus }) => {
160
+ const text = [
161
+ "Perform a safe task status update using Projitive rules.",
344
162
  "",
345
- "## Evidence",
346
- "- candidates:",
347
- "- (none)",
163
+ `1) Run taskContext(projectPath=\"${projectPath}\", taskId=\"${taskId}\").`,
164
+ `2) Plan status transition toward ${targetStatus}.`,
165
+ "3) Update tasks.md status and updatedAt.",
166
+ "4) Add or update a report under reports/ with concrete evidence.",
167
+ "5) Re-run taskContext and confirm status/evidence/reference consistency.",
348
168
  "",
349
- "## Agent Guidance",
350
- "- No TODO/IN_PROGRESS task is available.",
351
- "- Create or reopen tasks in tasks.md, then rerun `task.next`.",
169
+ "Checklist:",
170
+ "- Transition is valid per status machine.",
171
+ "- links/roadmapRefs remain parseable and consistent.",
172
+ "- Hook paths (if any) still resolve.",
352
173
  ].join("\n");
353
- return asText(markdown);
354
- }
355
- const selected = rankedCandidates[0];
356
- const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
357
- const fileCandidates = candidateFilesFromArtifacts(artifacts);
358
- const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
359
- const taskLocation = (await findTextReferences(selected.tasksPath, selected.task.id))[0];
360
- const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
361
- const suggestedReadOrder = [selected.tasksPath, ...relatedArtifacts.filter((item) => item !== selected.tasksPath)];
362
- const candidateLimit = topCandidates ?? 5;
363
- const markdown = [
364
- "# task.next",
365
- "",
366
- "## Summary",
367
- `- rootPath: ${root}`,
368
- `- maxDepth: ${maxDepth ?? 3}`,
369
- `- matchedProjects: ${projects.length}`,
370
- `- actionableTasks: ${rankedCandidates.length}`,
371
- `- selectedProject: ${selected.governanceDir}`,
372
- `- selectedTaskId: ${selected.task.id}`,
373
- `- selectedTaskStatus: ${selected.task.status}`,
374
- "",
375
- "## Evidence",
376
- "### Selected Task",
377
- `- id: ${selected.task.id}`,
378
- `- title: ${selected.task.title}`,
379
- `- owner: ${selected.task.owner || "(none)"}`,
380
- `- updatedAt: ${selected.task.updatedAt}`,
381
- `- roadmapRefs: ${selected.task.roadmapRefs.join(", ") || "(none)"}`,
382
- `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selected.tasksPath}`,
383
- "",
384
- "### Top Candidates",
385
- ...rankedCandidates
386
- .slice(0, candidateLimit)
387
- .map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | project=${item.governanceDir} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
388
- "",
389
- "### Related Artifacts",
390
- ...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
391
- "",
392
- "### Reference Locations",
393
- ...(referenceLocations.length > 0
394
- ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
395
- : ["- (none)"]),
396
- "",
397
- "### Suggested Read Order",
398
- ...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
399
- "",
400
- "## Agent Guidance",
401
- "- Start immediately with Suggested Read Order and execute the selected task.",
402
- "- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.",
403
- "- Re-run `task.get` for the selectedTaskId after edits to verify evidence consistency.",
404
- ].join("\n");
405
- return asText(markdown);
406
- });
407
- server.registerTool("task.get", {
408
- title: "Task Get",
409
- description: "Get one task with related evidence locations and a guidance prompt for the agent",
410
- inputSchema: {
411
- projectPath: z.string(),
412
- taskId: z.string(),
413
- },
414
- }, async ({ projectPath, taskId }) => {
415
- if (!isValidTaskId(taskId)) {
416
- return {
417
- ...asText(renderErrorMarkdown("task.get", `Invalid task ID format: ${taskId}`, ["- expected format: TASK-0001", "- retry with a valid task ID"])),
418
- isError: true,
419
- };
420
- }
421
- const governanceDir = await resolveGovernanceDir(projectPath);
422
- const { tasksPath, tasks } = await loadTasks(governanceDir);
423
- const taskGetHooks = await readTaskGetHooks(governanceDir);
424
- const task = tasks.find((item) => item.id === taskId);
425
- if (!task) {
426
- return {
427
- ...asText(renderErrorMarkdown("task.get", `Task not found: ${taskId}`, ["- run `task.list` to discover available IDs", "- retry with an existing task ID"])),
428
- isError: true,
429
- };
430
- }
431
- const taskLocation = (await findTextReferences(tasksPath, taskId))[0];
432
- const artifacts = await discoverGovernanceArtifacts(governanceDir);
433
- const fileCandidates = candidateFilesFromArtifacts(artifacts);
434
- const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
435
- const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
436
- const suggestedReadOrder = [tasksPath, ...relatedArtifacts.filter((item) => item !== tasksPath)];
437
- const hookPaths = Object.values(task.hooks)
438
- .filter((value) => typeof value === "string" && value.trim().length > 0)
439
- .map((value) => path.resolve(governanceDir, value));
440
- const hookStatus = `head=${taskGetHooks.head ? "loaded" : "missing"}, footer=${taskGetHooks.footer ? "loaded" : "missing"}`;
441
- const coreMarkdown = [
442
- "# task.get",
443
- "",
444
- "## Summary",
445
- `- governanceDir: ${governanceDir}`,
446
- `- taskId: ${task.id}`,
447
- `- title: ${task.title}`,
448
- `- status: ${task.status}`,
449
- `- owner: ${task.owner}`,
450
- `- updatedAt: ${task.updatedAt}`,
451
- `- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
452
- `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : tasksPath}`,
453
- `- hookStatus: ${hookStatus}`,
454
- "",
455
- "## Evidence",
456
- "### Related Artifacts",
457
- ...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
458
- "",
459
- "### Reference Locations",
460
- ...(referenceLocations.length > 0
461
- ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
462
- : ["- (none)"]),
463
- "",
464
- "### Hook Paths",
465
- ...(hookPaths.length > 0 ? hookPaths.map((item) => `- ${item}`) : ["- (none)"]),
466
- "",
467
- "### Suggested Read Order",
468
- ...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
469
- "",
470
- "## Agent Guidance",
471
- "- Read the files in Suggested Read Order.",
472
- "- Verify whether current status and evidence are consistent.",
473
- "- If updates are needed, edit tasks/designs/reports markdown directly and keep TASK IDs unchanged.",
474
- "- After editing, re-run `task.get` to verify references and context consistency.",
475
- ].join("\n");
476
- const markdownParts = [
477
- taskGetHooks.head,
478
- coreMarkdown,
479
- taskGetHooks.footer,
480
- ].filter((value) => typeof value === "string" && value.trim().length > 0);
481
- const markdown = markdownParts.join("\n\n---\n\n");
482
- return asText(markdown);
483
- });
484
- server.registerTool("roadmap.list", {
485
- title: "Roadmap List",
486
- description: "List roadmap IDs and related tasks for project planning",
487
- inputSchema: {
488
- projectPath: z.string(),
489
- },
490
- }, async ({ projectPath }) => {
491
- const governanceDir = await resolveGovernanceDir(projectPath);
492
- const roadmapIds = await readRoadmapIds(governanceDir);
493
- const { tasks } = await loadTasks(governanceDir);
494
- const markdown = [
495
- "# roadmap.list",
496
- "",
497
- "## Summary",
498
- `- governanceDir: ${governanceDir}`,
499
- `- roadmapCount: ${roadmapIds.length}`,
500
- "",
501
- "## Evidence",
502
- "- roadmaps:",
503
- ...(roadmapIds.length > 0
504
- ? roadmapIds.map((id) => {
505
- const linkedTasks = tasks.filter((task) => task.roadmapRefs.includes(id));
506
- return `- ${id} | linkedTasks=${linkedTasks.length}`;
507
- })
508
- : ["- (none)"]),
509
- "",
510
- "## Agent Guidance",
511
- "- Next: call `roadmap.get` with a roadmap ID to inspect references and related tasks.",
512
- ].join("\n");
513
- return asText(markdown);
514
- });
515
- server.registerTool("roadmap.get", {
516
- title: "Roadmap Get",
517
- description: "Get one roadmap with related task and evidence locations for agent guidance",
518
- inputSchema: {
519
- projectPath: z.string(),
520
- roadmapId: z.string(),
521
- },
522
- }, async ({ projectPath, roadmapId }) => {
523
- if (!isValidRoadmapId(roadmapId)) {
524
- return {
525
- ...asText(renderErrorMarkdown("roadmap.get", `Invalid roadmap ID format: ${roadmapId}`, ["- expected format: ROADMAP-0001", "- retry with a valid roadmap ID"])),
526
- isError: true,
527
- };
528
- }
529
- const governanceDir = await resolveGovernanceDir(projectPath);
530
- const artifacts = await discoverGovernanceArtifacts(governanceDir);
531
- const fileCandidates = candidateFilesFromArtifacts(artifacts);
532
- const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, roadmapId)))).flat();
533
- const { tasks } = await loadTasks(governanceDir);
534
- const relatedTasks = tasks.filter((task) => task.roadmapRefs.includes(roadmapId));
535
- const markdown = [
536
- "# roadmap.get",
537
- "",
538
- "## Summary",
539
- `- governanceDir: ${governanceDir}`,
540
- `- roadmapId: ${roadmapId}`,
541
- `- relatedTasks: ${relatedTasks.length}`,
542
- `- references: ${referenceLocations.length}`,
543
- "",
544
- "## Evidence",
545
- "### Related Tasks",
546
- ...(relatedTasks.length > 0
547
- ? relatedTasks.map((task) => `- ${task.id} | ${task.status} | ${task.title}`)
548
- : ["- (none)"]),
549
- "",
550
- "### Reference Locations",
551
- ...(referenceLocations.length > 0
552
- ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
553
- : ["- (none)"]),
554
- "",
555
- "## Agent Guidance",
556
- "- Read roadmap references first, then related tasks.",
557
- "- Keep ROADMAP/TASK IDs unchanged while updating markdown files.",
558
- "- Re-run `roadmap.get` after edits to confirm references remain consistent.",
559
- ].join("\n");
560
- return asText(markdown);
174
+ return asUserPrompt(text);
175
+ });
176
+ server.registerPrompt("triageProjectGovernance", {
177
+ title: "Triage Project Governance",
178
+ description: "Template to inspect a project and select next actionable governance task",
179
+ argsSchema: {
180
+ rootPath: z.string().optional(),
181
+ },
182
+ }, async ({ rootPath }) => {
183
+ const text = [
184
+ "Triage governance across projects and pick execution target.",
185
+ "",
186
+ `1) Run projectNext(${rootPath ? `rootPath=\"${rootPath}\"` : ""}).`,
187
+ "2) Select top ranked project.",
188
+ "3) Run projectContext(projectPath=<selectedProject>).",
189
+ "4) Run taskList(projectPath=<selectedProject>, status=IN_PROGRESS).",
190
+ "5) If none, run taskNext to select TODO/IN_PROGRESS candidate.",
191
+ "6) Continue with taskContext for detailed evidence mapping.",
192
+ ].join("\n");
193
+ return asUserPrompt(text);
194
+ });
195
+ }
196
+ registerTaskTools(server);
197
+ registerProjectTools(server);
198
+ registerRoadmapTools(server);
199
+ registerGovernanceResources();
200
+ registerGovernancePrompts();
201
+ async function main() {
202
+ const transport = new StdioServerTransport();
203
+ await server.connect(transport);
204
+ }
205
+ void main().catch((error) => {
206
+ console.error("Server error:", error);
207
+ process.exit(1);
561
208
  });
562
- const transport = new StdioServerTransport();
563
- await server.connect(transport);