@projitive/mcp 1.0.1 → 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 +1 -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/projitive.js +137 -122
- 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/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Language: English | [简体中文](README_CN.md)
|
|
4
4
|
|
|
5
|
-
**Current Spec Version: projitive-spec v1.0.0 | MCP Version: 1.0.
|
|
5
|
+
**Current Spec Version: projitive-spec v1.0.0 | MCP Version: 1.0.2**
|
|
6
6
|
|
|
7
7
|
Projitive MCP server (semantic interface edition) helps agents discover projects, select tasks, locate evidence, and execute under governance workflows.
|
|
8
8
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function candidateFilesFromArtifacts(artifacts) {
|
|
2
|
+
return artifacts
|
|
3
|
+
.filter((item) => item.exists)
|
|
4
|
+
.flatMap((item) => {
|
|
5
|
+
if (item.kind === "file") {
|
|
6
|
+
return [item.path];
|
|
7
|
+
}
|
|
8
|
+
return (item.markdownFiles ?? []).map((entry) => entry.path);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { candidateFilesFromArtifacts } from "./artifacts.js";
|
|
3
|
+
describe("candidateFilesFromArtifacts", () => {
|
|
4
|
+
it("collects existing file artifacts and markdown files from existing directories", () => {
|
|
5
|
+
const candidates = candidateFilesFromArtifacts([
|
|
6
|
+
{ name: "README.md", kind: "file", path: "/a/README.md", exists: true, lineCount: 3 },
|
|
7
|
+
{ name: "tasks.md", kind: "file", path: "/a/tasks.md", exists: false },
|
|
8
|
+
{
|
|
9
|
+
name: "designs",
|
|
10
|
+
kind: "directory",
|
|
11
|
+
path: "/a/designs",
|
|
12
|
+
exists: true,
|
|
13
|
+
markdownFiles: [{ path: "/a/designs/d1.md", lineCount: 10 }],
|
|
14
|
+
},
|
|
15
|
+
]);
|
|
16
|
+
expect(candidates).toEqual(["/a/README.md", "/a/designs/d1.md"]);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./artifacts.js";
|
package/output/helpers/index.js
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const TASK_LINT_CODES = {
|
|
2
|
+
DUPLICATE_ID: "TASK_DUPLICATE_ID",
|
|
3
|
+
IN_PROGRESS_OWNER_EMPTY: "TASK_IN_PROGRESS_OWNER_EMPTY",
|
|
4
|
+
DONE_LINKS_MISSING: "TASK_DONE_LINKS_MISSING",
|
|
5
|
+
BLOCKED_SUMMARY_EMPTY: "TASK_BLOCKED_SUMMARY_EMPTY",
|
|
6
|
+
UPDATED_AT_INVALID: "TASK_UPDATED_AT_INVALID",
|
|
7
|
+
ROADMAP_REFS_EMPTY: "TASK_ROADMAP_REFS_EMPTY",
|
|
8
|
+
OUTSIDE_MARKER: "TASK_OUTSIDE_MARKER",
|
|
9
|
+
LINK_TARGET_MISSING: "TASK_LINK_TARGET_MISSING",
|
|
10
|
+
HOOK_FILE_MISSING: "TASK_HOOK_FILE_MISSING",
|
|
11
|
+
FILTER_EMPTY: "TASK_FILTER_EMPTY",
|
|
12
|
+
CONTEXT_HOOK_HEAD_MISSING: "TASK_CONTEXT_HOOK_HEAD_MISSING",
|
|
13
|
+
CONTEXT_HOOK_FOOTER_MISSING: "TASK_CONTEXT_HOOK_FOOTER_MISSING",
|
|
14
|
+
};
|
|
15
|
+
export const ROADMAP_LINT_CODES = {
|
|
16
|
+
IDS_EMPTY: "ROADMAP_IDS_EMPTY",
|
|
17
|
+
TASKS_EMPTY: "ROADMAP_TASKS_EMPTY",
|
|
18
|
+
TASK_REFS_EMPTY: "ROADMAP_TASK_REFS_EMPTY",
|
|
19
|
+
UNKNOWN_REFS: "ROADMAP_UNKNOWN_REFS",
|
|
20
|
+
ZERO_LINKED_TASKS: "ROADMAP_ZERO_LINKED_TASKS",
|
|
21
|
+
CONTEXT_RELATED_TASKS_EMPTY: "ROADMAP_CONTEXT_RELATED_TASKS_EMPTY",
|
|
22
|
+
};
|
|
23
|
+
export const PROJECT_LINT_CODES = {
|
|
24
|
+
TASKS_FILE_MISSING: "PROJECT_TASKS_FILE_MISSING",
|
|
25
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderLintSuggestions } from "./linter.js";
|
|
3
|
+
describe("renderLintSuggestions", () => {
|
|
4
|
+
it("renders lint lines with code and message", () => {
|
|
5
|
+
const lines = renderLintSuggestions([
|
|
6
|
+
{ code: "TASK_001", message: "Example lint" },
|
|
7
|
+
]);
|
|
8
|
+
expect(lines).toEqual(["- [TASK_001] Example lint"]);
|
|
9
|
+
});
|
|
10
|
+
it("appends fixHint when provided", () => {
|
|
11
|
+
const lines = renderLintSuggestions([
|
|
12
|
+
{ code: "TASK_002", message: "Missing field.", fixHint: "Set owner." },
|
|
13
|
+
]);
|
|
14
|
+
expect(lines).toEqual(["- [TASK_002] Missing field. Set owner."]);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./response.js";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function asText(markdown) {
|
|
2
|
+
return {
|
|
3
|
+
content: [{ type: "text", text: markdown }],
|
|
4
|
+
};
|
|
5
|
+
}
|
|
6
|
+
function withFallback(lines) {
|
|
7
|
+
return lines.length > 0 ? lines : ["- (none)"];
|
|
8
|
+
}
|
|
9
|
+
function shouldKeepRawLine(trimmed) {
|
|
10
|
+
if (trimmed.length === 0) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
if (trimmed.startsWith("#") || trimmed.startsWith(">") || trimmed.startsWith("```")) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (/^[-*+]\s/.test(trimmed)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (/^\d+\.\s/.test(trimmed)) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function normalizeLine(line) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (shouldKeepRawLine(trimmed)) {
|
|
27
|
+
return line;
|
|
28
|
+
}
|
|
29
|
+
return `- ${trimmed}`;
|
|
30
|
+
}
|
|
31
|
+
function normalizeLines(lines) {
|
|
32
|
+
return lines.map((line) => normalizeLine(line));
|
|
33
|
+
}
|
|
34
|
+
export function section(title, lines) {
|
|
35
|
+
return { title, lines: normalizeLines(lines) };
|
|
36
|
+
}
|
|
37
|
+
export function summarySection(lines) {
|
|
38
|
+
return section("Summary", lines);
|
|
39
|
+
}
|
|
40
|
+
export function evidenceSection(lines) {
|
|
41
|
+
return section("Evidence", lines);
|
|
42
|
+
}
|
|
43
|
+
export function guidanceSection(lines) {
|
|
44
|
+
return section("Agent Guidance", lines);
|
|
45
|
+
}
|
|
46
|
+
export function lintSection(lines) {
|
|
47
|
+
return section("Lint Suggestions", lines);
|
|
48
|
+
}
|
|
49
|
+
export function nextCallSection(nextCall) {
|
|
50
|
+
return section("Next Call", nextCall ? [nextCall] : []);
|
|
51
|
+
}
|
|
52
|
+
export function renderToolResponseMarkdown(payload) {
|
|
53
|
+
const body = payload.sections.flatMap((section) => [
|
|
54
|
+
`## ${section.title}`,
|
|
55
|
+
...withFallback(section.lines),
|
|
56
|
+
"",
|
|
57
|
+
]);
|
|
58
|
+
return [
|
|
59
|
+
`# ${payload.toolName}`,
|
|
60
|
+
"",
|
|
61
|
+
...body,
|
|
62
|
+
].join("\n").trimEnd();
|
|
63
|
+
}
|
|
64
|
+
export function renderErrorMarkdown(toolName, cause, nextSteps, retryExample) {
|
|
65
|
+
return renderToolResponseMarkdown({
|
|
66
|
+
toolName,
|
|
67
|
+
sections: [
|
|
68
|
+
section("Error", [`cause: ${cause}`]),
|
|
69
|
+
section("Next Step", nextSteps),
|
|
70
|
+
section("Retry Example", [retryExample ?? "(none)"]),
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { asText, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "./response.js";
|
|
3
|
+
describe("response helpers", () => {
|
|
4
|
+
it("wraps markdown text as MCP text content", () => {
|
|
5
|
+
const result = asText("# hello");
|
|
6
|
+
expect(result.content).toEqual([{ type: "text", text: "# hello" }]);
|
|
7
|
+
});
|
|
8
|
+
it("renders error markdown sections", () => {
|
|
9
|
+
const markdown = renderErrorMarkdown("taskContext", "bad id", ["retry"], "taskContext(...)");
|
|
10
|
+
expect(markdown).toContain("# taskContext");
|
|
11
|
+
expect(markdown).toContain("## Error");
|
|
12
|
+
expect(markdown).toContain("- cause: bad id");
|
|
13
|
+
expect(markdown).toContain("- retry");
|
|
14
|
+
expect(markdown).toContain("## Retry Example");
|
|
15
|
+
});
|
|
16
|
+
it("renders standard tool response sections with fallback", () => {
|
|
17
|
+
const markdown = renderToolResponseMarkdown({
|
|
18
|
+
toolName: "taskList",
|
|
19
|
+
sections: [
|
|
20
|
+
{ title: "Summary", lines: ["- governanceDir: /tmp/.projitive"] },
|
|
21
|
+
{ title: "Evidence", lines: [] },
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
expect(markdown).toContain("# taskList");
|
|
25
|
+
expect(markdown).toContain("## Summary");
|
|
26
|
+
expect(markdown).toContain("## Evidence");
|
|
27
|
+
expect(markdown).toContain("- (none)");
|
|
28
|
+
});
|
|
29
|
+
it("auto-prefixes plain lines in section helpers", () => {
|
|
30
|
+
const markdown = renderToolResponseMarkdown({
|
|
31
|
+
toolName: "taskList",
|
|
32
|
+
sections: [
|
|
33
|
+
summarySection(["governanceDir: /tmp/.projitive"]),
|
|
34
|
+
],
|
|
35
|
+
});
|
|
36
|
+
expect(markdown).toContain("- governanceDir: /tmp/.projitive");
|
|
37
|
+
});
|
|
38
|
+
it("nextCallSection accepts optional call and falls back when missing", () => {
|
|
39
|
+
const withCall = renderToolResponseMarkdown({
|
|
40
|
+
toolName: "taskList",
|
|
41
|
+
sections: [nextCallSection("taskContext(projectPath=\"/tmp\", taskId=\"TASK-0001\")")],
|
|
42
|
+
});
|
|
43
|
+
expect(withCall).toContain("- taskContext(projectPath=\"/tmp\", taskId=\"TASK-0001\")");
|
|
44
|
+
const withoutCall = renderToolResponseMarkdown({
|
|
45
|
+
toolName: "taskList",
|
|
46
|
+
sections: [nextCallSection(undefined)],
|
|
47
|
+
});
|
|
48
|
+
expect(withoutCall).toContain("- (none)");
|
|
49
|
+
});
|
|
50
|
+
});
|
package/output/projitive.js
CHANGED
|
@@ -4,17 +4,14 @@ import process from "node:process";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { discoverGovernanceArtifacts } from "./helpers/files/index.js";
|
|
6
6
|
import { catchIt } from "./helpers/catch/index.js";
|
|
7
|
-
import {
|
|
7
|
+
import { PROJECT_LINT_CODES, renderLintSuggestions } from "./helpers/linter/index.js";
|
|
8
|
+
import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderToolResponseMarkdown, summarySection, } from "./helpers/response/index.js";
|
|
9
|
+
import { collectTaskLintSuggestions, loadTasksDocument } from "./tasks.js";
|
|
8
10
|
export const PROJECT_MARKER = ".projitive";
|
|
9
11
|
const DEFAULT_GOVERNANCE_DIR = ".projitive";
|
|
10
12
|
const ignoreNames = new Set(["node_modules", ".git", ".next", "dist", "build"]);
|
|
11
13
|
const DEFAULT_SCAN_DEPTH = 3;
|
|
12
14
|
const MAX_SCAN_DEPTH = 8;
|
|
13
|
-
function asText(markdown) {
|
|
14
|
-
return {
|
|
15
|
-
content: [{ type: "text", text: markdown }],
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
15
|
function normalizePath(inputPath) {
|
|
19
16
|
return inputPath ? path.resolve(inputPath) : process.cwd();
|
|
20
17
|
}
|
|
@@ -71,11 +68,22 @@ async function readTasksSnapshot(governanceDir) {
|
|
|
71
68
|
const tasksPath = path.join(governanceDir, "tasks.md");
|
|
72
69
|
const markdown = await fs.readFile(tasksPath, "utf-8").catch(() => undefined);
|
|
73
70
|
if (typeof markdown !== "string") {
|
|
74
|
-
return {
|
|
71
|
+
return {
|
|
72
|
+
tasksPath,
|
|
73
|
+
exists: false,
|
|
74
|
+
tasks: [],
|
|
75
|
+
lintSuggestions: renderLintSuggestions([
|
|
76
|
+
{
|
|
77
|
+
code: PROJECT_LINT_CODES.TASKS_FILE_MISSING,
|
|
78
|
+
message: "tasks.md is missing.",
|
|
79
|
+
fixHint: "Initialize governance tasks structure first.",
|
|
80
|
+
},
|
|
81
|
+
]),
|
|
82
|
+
};
|
|
75
83
|
}
|
|
76
84
|
const { parseTasksBlock } = await import("./tasks.js");
|
|
77
85
|
const tasks = await parseTasksBlock(markdown);
|
|
78
|
-
return { tasksPath, exists: true, tasks };
|
|
86
|
+
return { tasksPath, exists: true, tasks, lintSuggestions: collectTaskLintSuggestions(tasks, markdown) };
|
|
79
87
|
}
|
|
80
88
|
function latestTaskUpdatedAt(tasks) {
|
|
81
89
|
const timestamps = tasks
|
|
@@ -248,31 +256,35 @@ export function registerProjectTools(server) {
|
|
|
248
256
|
updated: initialized.files.filter((item) => item.action === "updated"),
|
|
249
257
|
skipped: initialized.files.filter((item) => item.action === "skipped"),
|
|
250
258
|
};
|
|
251
|
-
const markdown =
|
|
252
|
-
"
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
259
|
+
const markdown = renderToolResponseMarkdown({
|
|
260
|
+
toolName: "projectInit",
|
|
261
|
+
sections: [
|
|
262
|
+
summarySection([
|
|
263
|
+
`- rootPath: ${initialized.rootPath}`,
|
|
264
|
+
`- governanceDir: ${initialized.governanceDir}`,
|
|
265
|
+
`- markerPath: ${initialized.markerPath}`,
|
|
266
|
+
`- force: ${force === true ? "true" : "false"}`,
|
|
267
|
+
]),
|
|
268
|
+
evidenceSection([
|
|
269
|
+
`- createdFiles: ${filesByAction.created.length}`,
|
|
270
|
+
`- updatedFiles: ${filesByAction.updated.length}`,
|
|
271
|
+
`- skippedFiles: ${filesByAction.skipped.length}`,
|
|
272
|
+
"- directories:",
|
|
273
|
+
...initialized.directories.map((item) => ` - ${item.action}: ${item.path}`),
|
|
274
|
+
"- files:",
|
|
275
|
+
...initialized.files.map((item) => ` - ${item.action}: ${item.path}`),
|
|
276
|
+
]),
|
|
277
|
+
guidanceSection([
|
|
278
|
+
"- If files were skipped and you want to overwrite templates, rerun with force=true.",
|
|
279
|
+
"- Continue with projectContext and taskList for execution.",
|
|
280
|
+
]),
|
|
281
|
+
lintSection([
|
|
282
|
+
"- After init, fill owner/roadmapRefs/links in tasks.md before marking DONE.",
|
|
283
|
+
"- Keep task source-of-truth inside marker block only.",
|
|
284
|
+
]),
|
|
285
|
+
nextCallSection(`projectContext(projectPath=\"${initialized.governanceDir}\")`),
|
|
286
|
+
],
|
|
287
|
+
});
|
|
276
288
|
return asText(markdown);
|
|
277
289
|
});
|
|
278
290
|
server.registerTool("projectScan", {
|
|
@@ -286,27 +298,30 @@ export function registerProjectTools(server) {
|
|
|
286
298
|
const root = resolveScanRoot(rootPath);
|
|
287
299
|
const depth = resolveScanDepth(maxDepth);
|
|
288
300
|
const projects = await discoverProjects(root, depth);
|
|
289
|
-
const markdown =
|
|
290
|
-
"
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
301
|
+
const markdown = renderToolResponseMarkdown({
|
|
302
|
+
toolName: "projectScan",
|
|
303
|
+
sections: [
|
|
304
|
+
summarySection([
|
|
305
|
+
`- rootPath: ${root}`,
|
|
306
|
+
`- maxDepth: ${depth}`,
|
|
307
|
+
`- discoveredCount: ${projects.length}`,
|
|
308
|
+
]),
|
|
309
|
+
evidenceSection([
|
|
310
|
+
"- projects:",
|
|
311
|
+
...projects.map((project, index) => `${index + 1}. ${project}`),
|
|
312
|
+
]),
|
|
313
|
+
guidanceSection([
|
|
314
|
+
"- Use one discovered project path and call `projectLocate` to lock governance root.",
|
|
315
|
+
"- Then call `projectContext` to inspect current governance state.",
|
|
316
|
+
]),
|
|
317
|
+
lintSection(projects.length === 0
|
|
318
|
+
? ["- No governance root discovered. Add `.projitive` marker and baseline artifacts before execution."]
|
|
319
|
+
: ["- Run `projectContext` on a discovered project to receive module-level lint suggestions."]),
|
|
320
|
+
nextCallSection(projects[0]
|
|
321
|
+
? `projectLocate(inputPath=\"${projects[0]}\")`
|
|
322
|
+
: undefined),
|
|
323
|
+
],
|
|
324
|
+
});
|
|
310
325
|
return asText(markdown);
|
|
311
326
|
});
|
|
312
327
|
server.registerTool("projectNext", {
|
|
@@ -332,7 +347,7 @@ export function registerProjectTools(server) {
|
|
|
332
347
|
governanceDir,
|
|
333
348
|
tasksPath: snapshot.tasksPath,
|
|
334
349
|
tasksExists: snapshot.exists,
|
|
335
|
-
|
|
350
|
+
lintSuggestions: snapshot.lintSuggestions,
|
|
336
351
|
inProgress,
|
|
337
352
|
todo,
|
|
338
353
|
blocked,
|
|
@@ -351,32 +366,31 @@ export function registerProjectTools(server) {
|
|
|
351
366
|
return b.latestUpdatedAt.localeCompare(a.latestUpdatedAt);
|
|
352
367
|
})
|
|
353
368
|
.slice(0, limit ?? 10);
|
|
354
|
-
const markdown =
|
|
355
|
-
"
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
].join("\n");
|
|
369
|
+
const markdown = renderToolResponseMarkdown({
|
|
370
|
+
toolName: "projectNext",
|
|
371
|
+
sections: [
|
|
372
|
+
summarySection([
|
|
373
|
+
`- rootPath: ${root}`,
|
|
374
|
+
`- maxDepth: ${depth}`,
|
|
375
|
+
`- matchedProjects: ${projects.length}`,
|
|
376
|
+
`- actionableProjects: ${ranked.length}`,
|
|
377
|
+
`- limit: ${limit ?? 10}`,
|
|
378
|
+
]),
|
|
379
|
+
evidenceSection([
|
|
380
|
+
"- rankedProjects:",
|
|
381
|
+
...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)"}`),
|
|
382
|
+
]),
|
|
383
|
+
guidanceSection([
|
|
384
|
+
"- Pick top 1 project and call `projectContext` with its governanceDir.",
|
|
385
|
+
"- Then call `taskList` and `taskContext` to continue execution.",
|
|
386
|
+
"- If `tasksPath` is missing, create tasks.md using project convention before task-level operations.",
|
|
387
|
+
]),
|
|
388
|
+
lintSection(ranked[0]?.lintSuggestions ?? []),
|
|
389
|
+
nextCallSection(ranked[0]
|
|
390
|
+
? `projectContext(projectPath=\"${ranked[0].governanceDir}\")`
|
|
391
|
+
: undefined),
|
|
392
|
+
],
|
|
393
|
+
});
|
|
380
394
|
return asText(markdown);
|
|
381
395
|
});
|
|
382
396
|
server.registerTool("projectLocate", {
|
|
@@ -389,20 +403,19 @@ export function registerProjectTools(server) {
|
|
|
389
403
|
const resolvedFrom = normalizePath(inputPath);
|
|
390
404
|
const governanceDir = await resolveGovernanceDir(resolvedFrom);
|
|
391
405
|
const markerPath = path.join(governanceDir, ".projitive");
|
|
392
|
-
const markdown =
|
|
393
|
-
"
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
].join("\n");
|
|
406
|
+
const markdown = renderToolResponseMarkdown({
|
|
407
|
+
toolName: "projectLocate",
|
|
408
|
+
sections: [
|
|
409
|
+
summarySection([
|
|
410
|
+
`- resolvedFrom: ${resolvedFrom}`,
|
|
411
|
+
`- governanceDir: ${governanceDir}`,
|
|
412
|
+
`- markerPath: ${markerPath}`,
|
|
413
|
+
]),
|
|
414
|
+
guidanceSection(["- Call `projectContext` with this governanceDir to get task and roadmap summaries."]),
|
|
415
|
+
lintSection(["- Run `projectContext` to get governance/module lint suggestions for this project."]),
|
|
416
|
+
nextCallSection(`projectContext(projectPath=\"${governanceDir}\")`),
|
|
417
|
+
],
|
|
418
|
+
});
|
|
406
419
|
return asText(markdown);
|
|
407
420
|
});
|
|
408
421
|
server.registerTool("projectContext", {
|
|
@@ -414,8 +427,9 @@ export function registerProjectTools(server) {
|
|
|
414
427
|
}, async ({ projectPath }) => {
|
|
415
428
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
416
429
|
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
417
|
-
const { tasksPath, tasks } = await
|
|
430
|
+
const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
|
|
418
431
|
const roadmapIds = await readRoadmapIds(governanceDir);
|
|
432
|
+
const lintSuggestions = collectTaskLintSuggestions(tasks, tasksMarkdown);
|
|
419
433
|
const taskSummary = {
|
|
420
434
|
total: tasks.length,
|
|
421
435
|
TODO: tasks.filter((task) => task.status === "TODO").length,
|
|
@@ -423,32 +437,33 @@ export function registerProjectTools(server) {
|
|
|
423
437
|
BLOCKED: tasks.filter((task) => task.status === "BLOCKED").length,
|
|
424
438
|
DONE: tasks.filter((task) => task.status === "DONE").length,
|
|
425
439
|
};
|
|
426
|
-
const markdown =
|
|
427
|
-
"
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
440
|
+
const markdown = renderToolResponseMarkdown({
|
|
441
|
+
toolName: "projectContext",
|
|
442
|
+
sections: [
|
|
443
|
+
summarySection([
|
|
444
|
+
`- governanceDir: ${governanceDir}`,
|
|
445
|
+
`- tasksFile: ${tasksPath}`,
|
|
446
|
+
`- roadmapIds: ${roadmapIds.length}`,
|
|
447
|
+
]),
|
|
448
|
+
evidenceSection([
|
|
449
|
+
"### Task Summary",
|
|
450
|
+
`- total: ${taskSummary.total}`,
|
|
451
|
+
`- TODO: ${taskSummary.TODO}`,
|
|
452
|
+
`- IN_PROGRESS: ${taskSummary.IN_PROGRESS}`,
|
|
453
|
+
`- BLOCKED: ${taskSummary.BLOCKED}`,
|
|
454
|
+
`- DONE: ${taskSummary.DONE}`,
|
|
455
|
+
"",
|
|
456
|
+
"### Artifacts",
|
|
457
|
+
renderArtifactsMarkdown(artifacts),
|
|
458
|
+
]),
|
|
459
|
+
guidanceSection([
|
|
460
|
+
"- Start from `taskList` to choose a target task.",
|
|
461
|
+
"- Then call `taskContext` with a task ID to retrieve evidence locations and reading order.",
|
|
462
|
+
]),
|
|
463
|
+
lintSection(lintSuggestions),
|
|
464
|
+
nextCallSection(`taskList(projectPath=\"${governanceDir}\")`),
|
|
465
|
+
],
|
|
466
|
+
});
|
|
452
467
|
return asText(markdown);
|
|
453
468
|
});
|
|
454
469
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
const MODULE_FILES = [
|
|
5
|
+
"tasks.ts",
|
|
6
|
+
"roadmap.ts",
|
|
7
|
+
"projitive.ts",
|
|
8
|
+
];
|
|
9
|
+
const INVALID_LITERAL_PATTERN = /["'`]\s*-\s+(#{1,6}\s|>\s*|```)/g;
|
|
10
|
+
describe("rendering input guard", () => {
|
|
11
|
+
it("does not contain accidental bullet-prefixed markdown literals in module outputs", async () => {
|
|
12
|
+
const sourceDir = path.resolve(import.meta.dirname);
|
|
13
|
+
for (const file of MODULE_FILES) {
|
|
14
|
+
const filePath = path.join(sourceDir, file);
|
|
15
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
16
|
+
const matches = content.match(INVALID_LITERAL_PATTERN) ?? [];
|
|
17
|
+
expect(matches, `invalid literals in ${filePath}`).toHaveLength(0);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|