@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/projitive.js
CHANGED
|
@@ -4,19 +4,33 @@ 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";
|
|
11
|
+
const DEFAULT_GOVERNANCE_DIR = ".projitive";
|
|
9
12
|
const ignoreNames = new Set(["node_modules", ".git", ".next", "dist", "build"]);
|
|
10
13
|
const DEFAULT_SCAN_DEPTH = 3;
|
|
11
14
|
const MAX_SCAN_DEPTH = 8;
|
|
12
|
-
function asText(markdown) {
|
|
13
|
-
return {
|
|
14
|
-
content: [{ type: "text", text: markdown }],
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
15
|
function normalizePath(inputPath) {
|
|
18
16
|
return inputPath ? path.resolve(inputPath) : process.cwd();
|
|
19
17
|
}
|
|
18
|
+
function normalizeGovernanceDirName(input) {
|
|
19
|
+
const name = input?.trim() || DEFAULT_GOVERNANCE_DIR;
|
|
20
|
+
if (!name) {
|
|
21
|
+
throw new Error("governanceDir cannot be empty");
|
|
22
|
+
}
|
|
23
|
+
if (path.isAbsolute(name)) {
|
|
24
|
+
throw new Error("governanceDir must be a relative directory name");
|
|
25
|
+
}
|
|
26
|
+
if (name.includes("/") || name.includes("\\")) {
|
|
27
|
+
throw new Error("governanceDir must not contain path separators");
|
|
28
|
+
}
|
|
29
|
+
if (name === "." || name === "..") {
|
|
30
|
+
throw new Error("governanceDir must be a normal directory name");
|
|
31
|
+
}
|
|
32
|
+
return name;
|
|
33
|
+
}
|
|
20
34
|
function parseDepthFromEnv(rawDepth) {
|
|
21
35
|
if (typeof rawDepth !== "string" || rawDepth.trim().length === 0) {
|
|
22
36
|
return undefined;
|
|
@@ -54,11 +68,22 @@ async function readTasksSnapshot(governanceDir) {
|
|
|
54
68
|
const tasksPath = path.join(governanceDir, "tasks.md");
|
|
55
69
|
const markdown = await fs.readFile(tasksPath, "utf-8").catch(() => undefined);
|
|
56
70
|
if (typeof markdown !== "string") {
|
|
57
|
-
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
|
+
};
|
|
58
83
|
}
|
|
59
84
|
const { parseTasksBlock } = await import("./tasks.js");
|
|
60
85
|
const tasks = await parseTasksBlock(markdown);
|
|
61
|
-
return { tasksPath, exists: true, tasks };
|
|
86
|
+
return { tasksPath, exists: true, tasks, lintSuggestions: collectTaskLintSuggestions(tasks, markdown) };
|
|
62
87
|
}
|
|
63
88
|
function latestTaskUpdatedAt(tasks) {
|
|
64
89
|
const timestamps = tasks
|
|
@@ -134,7 +159,134 @@ export async function discoverProjects(rootPath, maxDepth) {
|
|
|
134
159
|
await walk(rootPath, 0);
|
|
135
160
|
return Array.from(new Set(results)).sort();
|
|
136
161
|
}
|
|
162
|
+
async function pathExists(targetPath) {
|
|
163
|
+
const accessResult = await catchIt(fs.access(targetPath));
|
|
164
|
+
return !accessResult.isError();
|
|
165
|
+
}
|
|
166
|
+
async function writeTextFile(targetPath, content, force) {
|
|
167
|
+
const exists = await pathExists(targetPath);
|
|
168
|
+
if (exists && !force) {
|
|
169
|
+
return { path: targetPath, action: "skipped" };
|
|
170
|
+
}
|
|
171
|
+
await fs.writeFile(targetPath, content, "utf-8");
|
|
172
|
+
return { path: targetPath, action: exists ? "updated" : "created" };
|
|
173
|
+
}
|
|
174
|
+
function defaultReadmeMarkdown(governanceDirName) {
|
|
175
|
+
return [
|
|
176
|
+
"# Projitive Governance Workspace",
|
|
177
|
+
"",
|
|
178
|
+
`This directory (\`${governanceDirName}/\`) is the governance root for this project.`,
|
|
179
|
+
"",
|
|
180
|
+
"## Conventions",
|
|
181
|
+
"- Keep roadmap/task/design/report files in markdown.",
|
|
182
|
+
"- Keep IDs stable (TASK-xxxx / ROADMAP-xxxx).",
|
|
183
|
+
"- Update report evidence before status transitions.",
|
|
184
|
+
].join("\n");
|
|
185
|
+
}
|
|
186
|
+
function defaultRoadmapMarkdown() {
|
|
187
|
+
return [
|
|
188
|
+
"# Roadmap",
|
|
189
|
+
"",
|
|
190
|
+
"## Active Milestones",
|
|
191
|
+
"- [ ] ROADMAP-0001: Bootstrap governance baseline (time: 2026-Q1)",
|
|
192
|
+
].join("\n");
|
|
193
|
+
}
|
|
194
|
+
function defaultTasksMarkdown() {
|
|
195
|
+
const updatedAt = new Date().toISOString();
|
|
196
|
+
return [
|
|
197
|
+
"# Tasks",
|
|
198
|
+
"",
|
|
199
|
+
"<!-- PROJITIVE:TASKS:START -->",
|
|
200
|
+
"## TASK-0001 | TODO | Bootstrap governance workspace",
|
|
201
|
+
"- owner: unassigned",
|
|
202
|
+
"- summary: Create initial governance artifacts and confirm task execution loop.",
|
|
203
|
+
`- updatedAt: ${updatedAt}`,
|
|
204
|
+
"- links:",
|
|
205
|
+
"- roadmapRefs: ROADMAP-0001",
|
|
206
|
+
"- hooks:",
|
|
207
|
+
"<!-- PROJITIVE:TASKS:END -->",
|
|
208
|
+
].join("\n");
|
|
209
|
+
}
|
|
210
|
+
export async function initializeProjectStructure(inputPath, governanceDir, force = false) {
|
|
211
|
+
const rootPath = normalizePath(inputPath);
|
|
212
|
+
const governanceDirName = normalizeGovernanceDirName(governanceDir);
|
|
213
|
+
const rootStat = await catchIt(fs.stat(rootPath));
|
|
214
|
+
if (rootStat.isError()) {
|
|
215
|
+
throw new Error(`Path not found: ${rootPath}`);
|
|
216
|
+
}
|
|
217
|
+
if (!rootStat.value.isDirectory()) {
|
|
218
|
+
throw new Error(`rootPath must be a directory: ${rootPath}`);
|
|
219
|
+
}
|
|
220
|
+
const governancePath = path.join(rootPath, governanceDirName);
|
|
221
|
+
const directories = [];
|
|
222
|
+
const requiredDirectories = [governancePath, path.join(governancePath, "designs"), path.join(governancePath, "reports"), path.join(governancePath, "hooks")];
|
|
223
|
+
for (const dirPath of requiredDirectories) {
|
|
224
|
+
const exists = await pathExists(dirPath);
|
|
225
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
226
|
+
directories.push({ path: dirPath, action: exists ? "skipped" : "created" });
|
|
227
|
+
}
|
|
228
|
+
const markerPath = path.join(governancePath, PROJECT_MARKER);
|
|
229
|
+
const files = await Promise.all([
|
|
230
|
+
writeTextFile(markerPath, "", force),
|
|
231
|
+
writeTextFile(path.join(governancePath, "README.md"), defaultReadmeMarkdown(governanceDirName), force),
|
|
232
|
+
writeTextFile(path.join(governancePath, "roadmap.md"), defaultRoadmapMarkdown(), force),
|
|
233
|
+
writeTextFile(path.join(governancePath, "tasks.md"), defaultTasksMarkdown(), force),
|
|
234
|
+
]);
|
|
235
|
+
return {
|
|
236
|
+
rootPath,
|
|
237
|
+
governanceDir: governancePath,
|
|
238
|
+
markerPath,
|
|
239
|
+
directories,
|
|
240
|
+
files,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
137
243
|
export function registerProjectTools(server) {
|
|
244
|
+
server.registerTool("projectInit", {
|
|
245
|
+
title: "Project Init",
|
|
246
|
+
description: "Initialize Projitive governance directory structure manually (default .projitive)",
|
|
247
|
+
inputSchema: {
|
|
248
|
+
rootPath: z.string().optional(),
|
|
249
|
+
governanceDir: z.string().optional(),
|
|
250
|
+
force: z.boolean().optional(),
|
|
251
|
+
},
|
|
252
|
+
}, async ({ rootPath, governanceDir, force }) => {
|
|
253
|
+
const initialized = await initializeProjectStructure(rootPath, governanceDir, force ?? false);
|
|
254
|
+
const filesByAction = {
|
|
255
|
+
created: initialized.files.filter((item) => item.action === "created"),
|
|
256
|
+
updated: initialized.files.filter((item) => item.action === "updated"),
|
|
257
|
+
skipped: initialized.files.filter((item) => item.action === "skipped"),
|
|
258
|
+
};
|
|
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
|
+
});
|
|
288
|
+
return asText(markdown);
|
|
289
|
+
});
|
|
138
290
|
server.registerTool("projectScan", {
|
|
139
291
|
title: "Project Scan",
|
|
140
292
|
description: "Scan filesystem and discover project governance roots marked by .projitive",
|
|
@@ -146,27 +298,30 @@ export function registerProjectTools(server) {
|
|
|
146
298
|
const root = resolveScanRoot(rootPath);
|
|
147
299
|
const depth = resolveScanDepth(maxDepth);
|
|
148
300
|
const projects = await discoverProjects(root, depth);
|
|
149
|
-
const markdown =
|
|
150
|
-
"
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
});
|
|
170
325
|
return asText(markdown);
|
|
171
326
|
});
|
|
172
327
|
server.registerTool("projectNext", {
|
|
@@ -192,7 +347,7 @@ export function registerProjectTools(server) {
|
|
|
192
347
|
governanceDir,
|
|
193
348
|
tasksPath: snapshot.tasksPath,
|
|
194
349
|
tasksExists: snapshot.exists,
|
|
195
|
-
|
|
350
|
+
lintSuggestions: snapshot.lintSuggestions,
|
|
196
351
|
inProgress,
|
|
197
352
|
todo,
|
|
198
353
|
blocked,
|
|
@@ -211,32 +366,31 @@ export function registerProjectTools(server) {
|
|
|
211
366
|
return b.latestUpdatedAt.localeCompare(a.latestUpdatedAt);
|
|
212
367
|
})
|
|
213
368
|
.slice(0, limit ?? 10);
|
|
214
|
-
const markdown =
|
|
215
|
-
"
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
].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
|
+
});
|
|
240
394
|
return asText(markdown);
|
|
241
395
|
});
|
|
242
396
|
server.registerTool("projectLocate", {
|
|
@@ -249,20 +403,19 @@ export function registerProjectTools(server) {
|
|
|
249
403
|
const resolvedFrom = normalizePath(inputPath);
|
|
250
404
|
const governanceDir = await resolveGovernanceDir(resolvedFrom);
|
|
251
405
|
const markerPath = path.join(governanceDir, ".projitive");
|
|
252
|
-
const markdown =
|
|
253
|
-
"
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
].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
|
+
});
|
|
266
419
|
return asText(markdown);
|
|
267
420
|
});
|
|
268
421
|
server.registerTool("projectContext", {
|
|
@@ -274,8 +427,9 @@ export function registerProjectTools(server) {
|
|
|
274
427
|
}, async ({ projectPath }) => {
|
|
275
428
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
276
429
|
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
277
|
-
const { tasksPath, tasks } = await
|
|
430
|
+
const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
|
|
278
431
|
const roadmapIds = await readRoadmapIds(governanceDir);
|
|
432
|
+
const lintSuggestions = collectTaskLintSuggestions(tasks, tasksMarkdown);
|
|
279
433
|
const taskSummary = {
|
|
280
434
|
total: tasks.length,
|
|
281
435
|
TODO: tasks.filter((task) => task.status === "TODO").length,
|
|
@@ -283,32 +437,33 @@ export function registerProjectTools(server) {
|
|
|
283
437
|
BLOCKED: tasks.filter((task) => task.status === "BLOCKED").length,
|
|
284
438
|
DONE: tasks.filter((task) => task.status === "DONE").length,
|
|
285
439
|
};
|
|
286
|
-
const markdown =
|
|
287
|
-
"
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
+
});
|
|
312
467
|
return asText(markdown);
|
|
313
468
|
});
|
|
314
469
|
}
|
package/output/projitive.test.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { discoverProjects, hasProjectMarker, resolveGovernanceDir } from "./projitive.js";
|
|
5
|
+
import { discoverProjects, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir } from "./projitive.js";
|
|
6
6
|
const tempPaths = [];
|
|
7
7
|
async function createTempDir() {
|
|
8
8
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
|
|
@@ -43,4 +43,32 @@ describe("projitive module", () => {
|
|
|
43
43
|
expect(projects).toContain(p1);
|
|
44
44
|
expect(projects).toContain(p2);
|
|
45
45
|
});
|
|
46
|
+
it("initializes governance structure under default .projitive directory", async () => {
|
|
47
|
+
const root = await createTempDir();
|
|
48
|
+
const initialized = await initializeProjectStructure(root);
|
|
49
|
+
expect(initialized.governanceDir).toBe(path.join(root, ".projitive"));
|
|
50
|
+
const expectedPaths = [
|
|
51
|
+
path.join(root, ".projitive", ".projitive"),
|
|
52
|
+
path.join(root, ".projitive", "README.md"),
|
|
53
|
+
path.join(root, ".projitive", "roadmap.md"),
|
|
54
|
+
path.join(root, ".projitive", "tasks.md"),
|
|
55
|
+
path.join(root, ".projitive", "designs"),
|
|
56
|
+
path.join(root, ".projitive", "reports"),
|
|
57
|
+
path.join(root, ".projitive", "hooks"),
|
|
58
|
+
];
|
|
59
|
+
await Promise.all(expectedPaths.map(async (targetPath) => {
|
|
60
|
+
await expect(fs.access(targetPath)).resolves.toBeUndefined();
|
|
61
|
+
}));
|
|
62
|
+
});
|
|
63
|
+
it("overwrites template files when force is enabled", async () => {
|
|
64
|
+
const root = await createTempDir();
|
|
65
|
+
const governanceDir = path.join(root, ".projitive");
|
|
66
|
+
const readmePath = path.join(governanceDir, "README.md");
|
|
67
|
+
await initializeProjectStructure(root);
|
|
68
|
+
await fs.writeFile(readmePath, "custom-content", "utf-8");
|
|
69
|
+
const initialized = await initializeProjectStructure(root, ".projitive", true);
|
|
70
|
+
const readmeContent = await fs.readFile(readmePath, "utf-8");
|
|
71
|
+
expect(readmeContent).toContain("Projitive Governance Workspace");
|
|
72
|
+
expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe("updated");
|
|
73
|
+
});
|
|
46
74
|
});
|
|
@@ -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
|
+
});
|