@projitive/mcp 1.1.2 → 2.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 +115 -422
- package/output/package.json +4 -1
- package/output/source/common/files.js +1 -1
- package/output/source/common/index.js +1 -0
- package/output/source/common/migrations/runner.js +68 -0
- package/output/source/common/migrations/steps.js +55 -0
- package/output/source/common/migrations/types.js +1 -0
- package/output/source/common/response.js +147 -1
- package/output/source/common/store.js +623 -0
- package/output/source/common/store.test.js +164 -0
- package/output/source/index.js +1 -1
- package/output/source/prompts/quickStart.js +33 -7
- package/output/source/prompts/taskDiscovery.js +23 -9
- package/output/source/prompts/taskExecution.js +18 -8
- package/output/source/resources/governance.js +2 -2
- package/output/source/tools/project.js +254 -119
- package/output/source/tools/project.test.js +33 -11
- package/output/source/tools/roadmap.js +166 -16
- package/output/source/tools/roadmap.test.js +19 -55
- package/output/source/tools/task.js +152 -376
- package/output/source/tools/task.test.js +64 -392
- package/output/source/types.js +0 -9
- package/package.json +4 -1
|
@@ -2,9 +2,10 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import { discoverGovernanceArtifacts, catchIt, PROJECT_LINT_CODES, renderLintSuggestions } from "../common/index.js";
|
|
6
|
-
import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderToolResponseMarkdown, summarySection, } from "../common/index.js";
|
|
7
|
-
import { collectTaskLintSuggestions, loadTasksDocument } from "./task.js";
|
|
5
|
+
import { discoverGovernanceArtifacts, catchIt, PROJECT_LINT_CODES, renderLintSuggestions, ensureStore, replaceRoadmapsInStore, replaceTasksInStore, markMarkdownViewDirty, loadTaskStatusStatsFromStore, loadRoadmapIdsFromStore, } from "../common/index.js";
|
|
6
|
+
import { asText, evidenceSection, getDefaultToolTemplateMarkdown, guidanceSection, lintSection, nextCallSection, renderToolResponseMarkdown, summarySection, } from "../common/index.js";
|
|
7
|
+
import { collectTaskLintSuggestions, loadTasksDocument, loadTasksDocumentWithOptions, renderTasksMarkdown } from "./task.js";
|
|
8
|
+
import { loadRoadmapDocumentWithOptions, renderRoadmapMarkdown } from "./roadmap.js";
|
|
8
9
|
export const PROJECT_MARKER = ".projitive";
|
|
9
10
|
const DEFAULT_GOVERNANCE_DIR = ".projitive";
|
|
10
11
|
const ignoreNames = new Set(["node_modules", ".git", ".next", "dist", "build"]);
|
|
@@ -46,9 +47,43 @@ function requireEnvVar(name) {
|
|
|
46
47
|
}
|
|
47
48
|
return value.trim();
|
|
48
49
|
}
|
|
50
|
+
function normalizeScanRoots(rootPaths) {
|
|
51
|
+
const normalized = rootPaths
|
|
52
|
+
.map((entry) => entry.trim())
|
|
53
|
+
.filter((entry) => entry.length > 0)
|
|
54
|
+
.map((entry) => normalizePath(entry));
|
|
55
|
+
return Array.from(new Set(normalized));
|
|
56
|
+
}
|
|
57
|
+
function parseScanRoots(rawValue) {
|
|
58
|
+
const trimmed = rawValue.trim();
|
|
59
|
+
if (trimmed.length === 0) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
return trimmed.split(path.delimiter);
|
|
63
|
+
}
|
|
64
|
+
export function resolveScanRoots(inputPaths) {
|
|
65
|
+
const normalizedInputPaths = normalizeScanRoots(inputPaths ?? []);
|
|
66
|
+
if (normalizedInputPaths.length > 0) {
|
|
67
|
+
return normalizedInputPaths;
|
|
68
|
+
}
|
|
69
|
+
const configuredRoots = process.env.PROJITIVE_SCAN_ROOT_PATHS;
|
|
70
|
+
const rootsFromMultiEnv = typeof configuredRoots === "string"
|
|
71
|
+
? normalizeScanRoots(parseScanRoots(configuredRoots))
|
|
72
|
+
: [];
|
|
73
|
+
if (rootsFromMultiEnv.length > 0) {
|
|
74
|
+
return rootsFromMultiEnv;
|
|
75
|
+
}
|
|
76
|
+
const legacyRoot = process.env.PROJITIVE_SCAN_ROOT_PATH;
|
|
77
|
+
const rootsFromLegacyEnv = typeof legacyRoot === "string"
|
|
78
|
+
? normalizeScanRoots([legacyRoot])
|
|
79
|
+
: [];
|
|
80
|
+
if (rootsFromLegacyEnv.length > 0) {
|
|
81
|
+
return rootsFromLegacyEnv;
|
|
82
|
+
}
|
|
83
|
+
throw new Error("Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS (or legacy PROJITIVE_SCAN_ROOT_PATH)");
|
|
84
|
+
}
|
|
49
85
|
export function resolveScanRoot(inputPath) {
|
|
50
|
-
|
|
51
|
-
return normalizePath(inputPath ?? configuredRoot);
|
|
86
|
+
return resolveScanRoots(inputPath ? [inputPath] : undefined)[0];
|
|
52
87
|
}
|
|
53
88
|
export function resolveScanDepth(inputDepth) {
|
|
54
89
|
const configuredDepthRaw = requireEnvVar("PROJITIVE_SCAN_MAX_DEPTH");
|
|
@@ -75,49 +110,45 @@ function renderArtifactsMarkdown(artifacts) {
|
|
|
75
110
|
return rows.join("\n");
|
|
76
111
|
}
|
|
77
112
|
async function readTasksSnapshot(governanceDir) {
|
|
78
|
-
const tasksPath = path.join(governanceDir,
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
113
|
+
const tasksPath = path.join(governanceDir, PROJECT_MARKER);
|
|
114
|
+
const exists = await fs.access(tasksPath).then(() => true).catch(() => false);
|
|
115
|
+
if (!exists) {
|
|
81
116
|
return {
|
|
82
|
-
tasksPath,
|
|
83
117
|
exists: false,
|
|
84
|
-
tasks: [],
|
|
85
118
|
lintSuggestions: renderLintSuggestions([
|
|
86
119
|
{
|
|
87
120
|
code: PROJECT_LINT_CODES.TASKS_FILE_MISSING,
|
|
88
|
-
message: "
|
|
121
|
+
message: "governance store is missing.",
|
|
89
122
|
fixHint: "Initialize governance tasks structure first.",
|
|
90
123
|
},
|
|
91
124
|
]),
|
|
125
|
+
todo: 0,
|
|
126
|
+
inProgress: 0,
|
|
127
|
+
blocked: 0,
|
|
128
|
+
done: 0,
|
|
129
|
+
total: 0,
|
|
130
|
+
latestUpdatedAt: "(unknown)",
|
|
131
|
+
score: 0,
|
|
92
132
|
};
|
|
93
133
|
}
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
return {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
function actionableScore(tasks) {
|
|
108
|
-
return tasks.filter((task) => task.status === "IN_PROGRESS").length * 2
|
|
109
|
-
+ tasks.filter((task) => task.status === "TODO").length;
|
|
134
|
+
await ensureStore(tasksPath);
|
|
135
|
+
const stats = await loadTaskStatusStatsFromStore(tasksPath);
|
|
136
|
+
return {
|
|
137
|
+
exists: true,
|
|
138
|
+
lintSuggestions: [],
|
|
139
|
+
todo: stats.todo,
|
|
140
|
+
inProgress: stats.inProgress,
|
|
141
|
+
blocked: stats.blocked,
|
|
142
|
+
done: stats.done,
|
|
143
|
+
total: stats.total,
|
|
144
|
+
latestUpdatedAt: stats.latestUpdatedAt || "(unknown)",
|
|
145
|
+
score: stats.inProgress * 2 + stats.todo,
|
|
146
|
+
};
|
|
110
147
|
}
|
|
111
148
|
async function readRoadmapIds(governanceDir) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const matches = markdown.match(/ROADMAP-\d{4}/g) ?? [];
|
|
116
|
-
return Array.from(new Set(matches));
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return [];
|
|
120
|
-
}
|
|
149
|
+
const dbPath = path.join(governanceDir, PROJECT_MARKER);
|
|
150
|
+
await ensureStore(dbPath);
|
|
151
|
+
return loadRoadmapIdsFromStore(dbPath);
|
|
121
152
|
}
|
|
122
153
|
export async function hasProjectMarker(dirPath) {
|
|
123
154
|
const markerPath = path.join(dirPath, PROJECT_MARKER);
|
|
@@ -210,6 +241,25 @@ export async function discoverProjects(rootPath, maxDepth) {
|
|
|
210
241
|
await walk(rootPath, 0);
|
|
211
242
|
return Array.from(new Set(results)).sort();
|
|
212
243
|
}
|
|
244
|
+
export async function discoverProjectsAcrossRoots(rootPaths, maxDepth) {
|
|
245
|
+
const perRootResults = await Promise.all(rootPaths.map((rootPath) => discoverProjects(rootPath, maxDepth)));
|
|
246
|
+
return Array.from(new Set(perRootResults.flat())).sort();
|
|
247
|
+
}
|
|
248
|
+
const DEFAULT_TOOL_TEMPLATE_NAMES = [
|
|
249
|
+
"projectInit",
|
|
250
|
+
"projectScan",
|
|
251
|
+
"projectNext",
|
|
252
|
+
"projectLocate",
|
|
253
|
+
"projectContext",
|
|
254
|
+
"syncViews",
|
|
255
|
+
"taskList",
|
|
256
|
+
"taskNext",
|
|
257
|
+
"taskContext",
|
|
258
|
+
"taskUpdate",
|
|
259
|
+
"roadmapList",
|
|
260
|
+
"roadmapContext",
|
|
261
|
+
"roadmapUpdate",
|
|
262
|
+
];
|
|
213
263
|
async function pathExists(targetPath) {
|
|
214
264
|
const accessResult = await catchIt(fs.access(targetPath));
|
|
215
265
|
return !accessResult.isError();
|
|
@@ -229,51 +279,62 @@ function defaultReadmeMarkdown(governanceDirName) {
|
|
|
229
279
|
`This directory (\`${governanceDirName}/\`) is the governance root for this project.`,
|
|
230
280
|
"",
|
|
231
281
|
"## Conventions",
|
|
232
|
-
"- Keep roadmap/task
|
|
282
|
+
"- Keep roadmap/task source of truth in .projitive sqlite tables.",
|
|
283
|
+
"- Treat roadmap.md/tasks.md as generated views from sqlite.",
|
|
233
284
|
"- Keep IDs stable (TASK-xxxx / ROADMAP-xxxx).",
|
|
234
285
|
"- Update report evidence before status transitions.",
|
|
235
286
|
].join("\n");
|
|
236
287
|
}
|
|
237
|
-
function defaultRoadmapMarkdown() {
|
|
238
|
-
return
|
|
239
|
-
"# Roadmap",
|
|
240
|
-
"",
|
|
241
|
-
"## Active Milestones",
|
|
242
|
-
"- [ ] ROADMAP-0001: Bootstrap governance baseline (time: 2026-Q1)",
|
|
243
|
-
].join("\n");
|
|
288
|
+
function defaultRoadmapMarkdown(milestones = defaultRoadmapMilestones()) {
|
|
289
|
+
return renderRoadmapMarkdown(milestones);
|
|
244
290
|
}
|
|
245
|
-
function defaultTasksMarkdown() {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
291
|
+
function defaultTasksMarkdown(updatedAt = new Date().toISOString()) {
|
|
292
|
+
return renderTasksMarkdown([
|
|
293
|
+
{
|
|
294
|
+
id: "TASK-0001",
|
|
295
|
+
title: "Bootstrap governance workspace",
|
|
296
|
+
status: "TODO",
|
|
297
|
+
owner: "unassigned",
|
|
298
|
+
summary: "Create initial governance artifacts and confirm task execution loop.",
|
|
299
|
+
updatedAt,
|
|
300
|
+
links: [],
|
|
301
|
+
roadmapRefs: ["ROADMAP-0001"],
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
}
|
|
305
|
+
function defaultRoadmapMilestones() {
|
|
306
|
+
return [{
|
|
307
|
+
id: "ROADMAP-0001",
|
|
308
|
+
title: "Bootstrap governance baseline",
|
|
309
|
+
status: "active",
|
|
310
|
+
time: "2026-Q1",
|
|
311
|
+
updatedAt: new Date().toISOString(),
|
|
312
|
+
}];
|
|
260
313
|
}
|
|
261
|
-
function
|
|
314
|
+
function defaultTemplateReadmeMarkdown() {
|
|
262
315
|
return [
|
|
263
|
-
"
|
|
264
|
-
"
|
|
316
|
+
"# Template Guide",
|
|
317
|
+
"",
|
|
318
|
+
"This directory stores response templates (one file per tool).",
|
|
265
319
|
"",
|
|
266
|
-
"
|
|
267
|
-
"-
|
|
268
|
-
"-
|
|
269
|
-
"- Check development/testing workflow bottlenecks; create tasks for reliability and speed improvements.",
|
|
270
|
-
"- Check TODO/FIXME/HACK comments; turn feasible items into governed tasks.",
|
|
271
|
-
"- Check dependency/security hygiene and stale tooling; create tasks where upgrades are justified.",
|
|
320
|
+
"How to enable:",
|
|
321
|
+
"- Set env `PROJITIVE_MESSAGE_TEMPLATE_PATH` to a template directory.",
|
|
322
|
+
"- Example: .projitive/templates/tools",
|
|
272
323
|
"",
|
|
273
|
-
"
|
|
274
|
-
"-
|
|
275
|
-
"-
|
|
276
|
-
"-
|
|
324
|
+
"Rule:",
|
|
325
|
+
"- Prefer one template per tool: <toolName>.md (e.g. taskNext.md).",
|
|
326
|
+
"- Template directory mode only loads <toolName>.md files.",
|
|
327
|
+
"- If a tool template file is missing, Projitive will auto-generate that file before rendering.",
|
|
328
|
+
"- Include {{content}} to render original tool output.",
|
|
329
|
+
"- If {{content}} is missing, original output is appended after template text.",
|
|
330
|
+
"",
|
|
331
|
+
"Basic Variables:",
|
|
332
|
+
"- {{tool_name}}",
|
|
333
|
+
"- {{summary}}",
|
|
334
|
+
"- {{evidence}}",
|
|
335
|
+
"- {{guidance}}",
|
|
336
|
+
"- {{next_call}}",
|
|
337
|
+
"- {{content}}",
|
|
277
338
|
].join("\n");
|
|
278
339
|
}
|
|
279
340
|
export async function initializeProjectStructure(inputPath, governanceDir, force = false) {
|
|
@@ -288,24 +349,49 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
|
|
|
288
349
|
}
|
|
289
350
|
const governancePath = path.join(projectPath, governanceDirName);
|
|
290
351
|
const directories = [];
|
|
291
|
-
const requiredDirectories = [
|
|
352
|
+
const requiredDirectories = [
|
|
353
|
+
governancePath,
|
|
354
|
+
path.join(governancePath, "designs"),
|
|
355
|
+
path.join(governancePath, "reports"),
|
|
356
|
+
path.join(governancePath, "templates"),
|
|
357
|
+
path.join(governancePath, "templates", "tools"),
|
|
358
|
+
];
|
|
292
359
|
for (const dirPath of requiredDirectories) {
|
|
293
360
|
const exists = await pathExists(dirPath);
|
|
294
361
|
await fs.mkdir(dirPath, { recursive: true });
|
|
295
362
|
directories.push({ path: dirPath, action: exists ? "skipped" : "created" });
|
|
296
363
|
}
|
|
297
364
|
const markerPath = path.join(governancePath, PROJECT_MARKER);
|
|
298
|
-
const
|
|
299
|
-
|
|
365
|
+
const defaultRoadmapData = defaultRoadmapMilestones();
|
|
366
|
+
const defaultTaskUpdatedAt = new Date().toISOString();
|
|
367
|
+
const markerExists = await pathExists(markerPath);
|
|
368
|
+
await ensureStore(markerPath);
|
|
369
|
+
if (force || !markerExists) {
|
|
370
|
+
await replaceRoadmapsInStore(markerPath, defaultRoadmapData);
|
|
371
|
+
await replaceTasksInStore(markerPath, [
|
|
372
|
+
{
|
|
373
|
+
id: "TASK-0001",
|
|
374
|
+
title: "Bootstrap governance workspace",
|
|
375
|
+
status: "TODO",
|
|
376
|
+
owner: "unassigned",
|
|
377
|
+
summary: "Create initial governance artifacts and confirm task execution loop.",
|
|
378
|
+
updatedAt: defaultTaskUpdatedAt,
|
|
379
|
+
links: [],
|
|
380
|
+
roadmapRefs: ["ROADMAP-0001"],
|
|
381
|
+
},
|
|
382
|
+
]);
|
|
383
|
+
}
|
|
384
|
+
const baseFiles = await Promise.all([
|
|
300
385
|
writeTextFile(path.join(governancePath, "README.md"), defaultReadmeMarkdown(governanceDirName), force),
|
|
301
|
-
writeTextFile(path.join(governancePath, "roadmap.md"), defaultRoadmapMarkdown(), force),
|
|
302
|
-
writeTextFile(path.join(governancePath, "tasks.md"), defaultTasksMarkdown(), force),
|
|
303
|
-
writeTextFile(path.join(governancePath, "
|
|
386
|
+
writeTextFile(path.join(governancePath, "roadmap.md"), defaultRoadmapMarkdown(defaultRoadmapData), force),
|
|
387
|
+
writeTextFile(path.join(governancePath, "tasks.md"), defaultTasksMarkdown(defaultTaskUpdatedAt), force),
|
|
388
|
+
writeTextFile(path.join(governancePath, "templates", "README.md"), defaultTemplateReadmeMarkdown(), force),
|
|
304
389
|
]);
|
|
390
|
+
const toolTemplateFiles = await Promise.all(DEFAULT_TOOL_TEMPLATE_NAMES.map((toolName) => writeTextFile(path.join(governancePath, "templates", "tools", `${toolName}.md`), getDefaultToolTemplateMarkdown(toolName), force)));
|
|
391
|
+
const files = [...baseFiles, ...toolTemplateFiles];
|
|
305
392
|
return {
|
|
306
393
|
projectPath,
|
|
307
394
|
governanceDir: governancePath,
|
|
308
|
-
markerPath,
|
|
309
395
|
directories,
|
|
310
396
|
files,
|
|
311
397
|
};
|
|
@@ -332,7 +418,6 @@ export function registerProjectTools(server) {
|
|
|
332
418
|
summarySection([
|
|
333
419
|
`- projectPath: ${initialized.projectPath}`,
|
|
334
420
|
`- governanceDir: ${initialized.governanceDir}`,
|
|
335
|
-
`- markerPath: ${initialized.markerPath}`,
|
|
336
421
|
`- force: ${force === true ? "true" : "false"}`,
|
|
337
422
|
]),
|
|
338
423
|
evidenceSection([
|
|
@@ -349,8 +434,8 @@ export function registerProjectTools(server) {
|
|
|
349
434
|
"- Continue with projectContext and taskList for execution.",
|
|
350
435
|
]),
|
|
351
436
|
lintSection([
|
|
352
|
-
"- After init, fill owner/roadmapRefs/links in
|
|
353
|
-
"- Keep task source-of-truth inside
|
|
437
|
+
"- After init, fill owner/roadmapRefs/links in .projitive task table before marking DONE.",
|
|
438
|
+
"- Keep task source-of-truth inside sqlite tables.",
|
|
354
439
|
]),
|
|
355
440
|
nextCallSection(`projectContext(projectPath=\"${initialized.projectPath}\")`),
|
|
356
441
|
],
|
|
@@ -362,14 +447,15 @@ export function registerProjectTools(server) {
|
|
|
362
447
|
description: "Start here when project path is unknown; discover all governance roots",
|
|
363
448
|
inputSchema: {},
|
|
364
449
|
}, async () => {
|
|
365
|
-
const
|
|
450
|
+
const roots = resolveScanRoots();
|
|
366
451
|
const depth = resolveScanDepth();
|
|
367
|
-
const projects = await
|
|
452
|
+
const projects = await discoverProjectsAcrossRoots(roots, depth);
|
|
368
453
|
const markdown = renderToolResponseMarkdown({
|
|
369
454
|
toolName: "projectScan",
|
|
370
455
|
sections: [
|
|
371
456
|
summarySection([
|
|
372
|
-
`-
|
|
457
|
+
`- rootPaths: ${roots.join(", ")}`,
|
|
458
|
+
`- rootCount: ${roots.length}`,
|
|
373
459
|
`- maxDepth: ${depth}`,
|
|
374
460
|
`- discoveredCount: ${projects.length}`,
|
|
375
461
|
]),
|
|
@@ -398,28 +484,23 @@ export function registerProjectTools(server) {
|
|
|
398
484
|
limit: z.number().int().min(1).max(50).optional(),
|
|
399
485
|
},
|
|
400
486
|
}, async ({ limit }) => {
|
|
401
|
-
const
|
|
487
|
+
const roots = resolveScanRoots();
|
|
402
488
|
const depth = resolveScanDepth();
|
|
403
|
-
const projects = await
|
|
489
|
+
const projects = await discoverProjectsAcrossRoots(roots, depth);
|
|
404
490
|
const snapshots = await Promise.all(projects.map(async (governanceDir) => {
|
|
405
491
|
const snapshot = await readTasksSnapshot(governanceDir);
|
|
406
|
-
const
|
|
407
|
-
const todo = snapshot.tasks.filter((task) => task.status === "TODO").length;
|
|
408
|
-
const blocked = snapshot.tasks.filter((task) => task.status === "BLOCKED").length;
|
|
409
|
-
const done = snapshot.tasks.filter((task) => task.status === "DONE").length;
|
|
410
|
-
const actionable = inProgress + todo;
|
|
492
|
+
const actionable = snapshot.inProgress + snapshot.todo;
|
|
411
493
|
return {
|
|
412
494
|
governanceDir,
|
|
413
|
-
tasksPath: snapshot.tasksPath,
|
|
414
495
|
tasksExists: snapshot.exists,
|
|
415
496
|
lintSuggestions: snapshot.lintSuggestions,
|
|
416
|
-
inProgress,
|
|
417
|
-
todo,
|
|
418
|
-
blocked,
|
|
419
|
-
done,
|
|
497
|
+
inProgress: snapshot.inProgress,
|
|
498
|
+
todo: snapshot.todo,
|
|
499
|
+
blocked: snapshot.blocked,
|
|
500
|
+
done: snapshot.done,
|
|
420
501
|
actionable,
|
|
421
|
-
latestUpdatedAt:
|
|
422
|
-
score:
|
|
502
|
+
latestUpdatedAt: snapshot.latestUpdatedAt,
|
|
503
|
+
score: snapshot.score,
|
|
423
504
|
};
|
|
424
505
|
}));
|
|
425
506
|
const ranked = snapshots
|
|
@@ -431,11 +512,16 @@ export function registerProjectTools(server) {
|
|
|
431
512
|
return b.latestUpdatedAt.localeCompare(a.latestUpdatedAt);
|
|
432
513
|
})
|
|
433
514
|
.slice(0, limit ?? 10);
|
|
515
|
+
if (ranked[0]) {
|
|
516
|
+
const topDoc = await loadTasksDocument(ranked[0].governanceDir);
|
|
517
|
+
ranked[0].lintSuggestions = collectTaskLintSuggestions(topDoc.tasks);
|
|
518
|
+
}
|
|
434
519
|
const markdown = renderToolResponseMarkdown({
|
|
435
520
|
toolName: "projectNext",
|
|
436
521
|
sections: [
|
|
437
522
|
summarySection([
|
|
438
|
-
`-
|
|
523
|
+
`- rootPaths: ${roots.join(", ")}`,
|
|
524
|
+
`- rootCount: ${roots.length}`,
|
|
439
525
|
`- maxDepth: ${depth}`,
|
|
440
526
|
`- matchedProjects: ${projects.length}`,
|
|
441
527
|
`- actionableProjects: ${ranked.length}`,
|
|
@@ -443,12 +529,12 @@ export function registerProjectTools(server) {
|
|
|
443
529
|
]),
|
|
444
530
|
evidenceSection([
|
|
445
531
|
"- rankedProjects:",
|
|
446
|
-
...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}
|
|
532
|
+
...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}${item.tasksExists ? "" : " | store=missing"}`),
|
|
447
533
|
]),
|
|
448
534
|
guidanceSection([
|
|
449
535
|
"- Pick top 1 project and call `projectContext` with its projectPath.",
|
|
450
536
|
"- Then call `taskList` and `taskContext` to continue execution.",
|
|
451
|
-
"- If
|
|
537
|
+
"- If governance store is missing, initialize governance before task-level operations.",
|
|
452
538
|
]),
|
|
453
539
|
lintSection(ranked[0]?.lintSuggestions ?? []),
|
|
454
540
|
nextCallSection(ranked[0]
|
|
@@ -468,7 +554,6 @@ export function registerProjectTools(server) {
|
|
|
468
554
|
const resolvedFrom = normalizePath(inputPath);
|
|
469
555
|
const governanceDir = await resolveGovernanceDir(resolvedFrom);
|
|
470
556
|
const projectPath = toProjectPath(governanceDir);
|
|
471
|
-
const markerPath = path.join(governanceDir, ".projitive");
|
|
472
557
|
const markdown = renderToolResponseMarkdown({
|
|
473
558
|
toolName: "projectLocate",
|
|
474
559
|
sections: [
|
|
@@ -476,7 +561,6 @@ export function registerProjectTools(server) {
|
|
|
476
561
|
`- resolvedFrom: ${resolvedFrom}`,
|
|
477
562
|
`- projectPath: ${projectPath}`,
|
|
478
563
|
`- governanceDir: ${governanceDir}`,
|
|
479
|
-
`- markerPath: ${markerPath}`,
|
|
480
564
|
]),
|
|
481
565
|
guidanceSection(["- Call `projectContext` with this projectPath to get task and roadmap summaries."]),
|
|
482
566
|
lintSection(["- Run `projectContext` to get governance/module lint suggestions for this project."]),
|
|
@@ -485,6 +569,61 @@ export function registerProjectTools(server) {
|
|
|
485
569
|
});
|
|
486
570
|
return asText(markdown);
|
|
487
571
|
});
|
|
572
|
+
server.registerTool("syncViews", {
|
|
573
|
+
title: "Sync Views",
|
|
574
|
+
description: "Materialize markdown views from .projitive sqlite tables (tasks.md / roadmap.md)",
|
|
575
|
+
inputSchema: {
|
|
576
|
+
projectPath: z.string(),
|
|
577
|
+
views: z.array(z.enum(["tasks", "roadmap"])).optional(),
|
|
578
|
+
force: z.boolean().optional(),
|
|
579
|
+
},
|
|
580
|
+
}, async ({ projectPath, views, force }) => {
|
|
581
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
582
|
+
const dbPath = path.join(governanceDir, PROJECT_MARKER);
|
|
583
|
+
const selectedViews = views && views.length > 0
|
|
584
|
+
? Array.from(new Set(views))
|
|
585
|
+
: ["tasks", "roadmap"];
|
|
586
|
+
const forceSync = force === true;
|
|
587
|
+
if (forceSync) {
|
|
588
|
+
if (selectedViews.includes("tasks")) {
|
|
589
|
+
await markMarkdownViewDirty(dbPath, "tasks_markdown");
|
|
590
|
+
}
|
|
591
|
+
if (selectedViews.includes("roadmap")) {
|
|
592
|
+
await markMarkdownViewDirty(dbPath, "roadmaps_markdown");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
let taskCount;
|
|
596
|
+
let roadmapCount;
|
|
597
|
+
if (selectedViews.includes("tasks")) {
|
|
598
|
+
const taskDoc = await loadTasksDocumentWithOptions(governanceDir, forceSync);
|
|
599
|
+
taskCount = taskDoc.tasks.length;
|
|
600
|
+
}
|
|
601
|
+
if (selectedViews.includes("roadmap")) {
|
|
602
|
+
const roadmapDoc = await loadRoadmapDocumentWithOptions(governanceDir, forceSync);
|
|
603
|
+
roadmapCount = roadmapDoc.milestones.length;
|
|
604
|
+
}
|
|
605
|
+
const markdown = renderToolResponseMarkdown({
|
|
606
|
+
toolName: "syncViews",
|
|
607
|
+
sections: [
|
|
608
|
+
summarySection([
|
|
609
|
+
`- governanceDir: ${governanceDir}`,
|
|
610
|
+
`- views: ${selectedViews.join(", ")}`,
|
|
611
|
+
`- force: ${forceSync ? "true" : "false"}`,
|
|
612
|
+
]),
|
|
613
|
+
evidenceSection([
|
|
614
|
+
...(typeof taskCount === "number" ? [`- tasks.md synced | taskCount=${taskCount}`] : []),
|
|
615
|
+
...(typeof roadmapCount === "number" ? [`- roadmap.md synced | roadmapCount=${roadmapCount}`] : []),
|
|
616
|
+
]),
|
|
617
|
+
guidanceSection([
|
|
618
|
+
"Use this tool after batch updates when you need immediate markdown materialization.",
|
|
619
|
+
"Routine workflows can rely on lazy sync and usually do not require force=true.",
|
|
620
|
+
]),
|
|
621
|
+
lintSection([]),
|
|
622
|
+
nextCallSection(`projectContext(projectPath="${toProjectPath(governanceDir)}")`),
|
|
623
|
+
],
|
|
624
|
+
});
|
|
625
|
+
return asText(markdown);
|
|
626
|
+
});
|
|
488
627
|
server.registerTool("projectContext", {
|
|
489
628
|
title: "Project Context",
|
|
490
629
|
description: "Get project-level summary before selecting or executing a task",
|
|
@@ -495,32 +634,28 @@ export function registerProjectTools(server) {
|
|
|
495
634
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
496
635
|
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
497
636
|
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
498
|
-
const
|
|
637
|
+
const dbPath = path.join(governanceDir, PROJECT_MARKER);
|
|
638
|
+
await ensureStore(dbPath);
|
|
639
|
+
const taskStats = await loadTaskStatusStatsFromStore(dbPath);
|
|
640
|
+
const { markdownPath: tasksMarkdownPath, tasks } = await loadTasksDocument(governanceDir);
|
|
499
641
|
const roadmapIds = await readRoadmapIds(governanceDir);
|
|
500
|
-
const lintSuggestions = collectTaskLintSuggestions(tasks
|
|
501
|
-
const taskSummary = {
|
|
502
|
-
total: tasks.length,
|
|
503
|
-
TODO: tasks.filter((task) => task.status === "TODO").length,
|
|
504
|
-
IN_PROGRESS: tasks.filter((task) => task.status === "IN_PROGRESS").length,
|
|
505
|
-
BLOCKED: tasks.filter((task) => task.status === "BLOCKED").length,
|
|
506
|
-
DONE: tasks.filter((task) => task.status === "DONE").length,
|
|
507
|
-
};
|
|
642
|
+
const lintSuggestions = collectTaskLintSuggestions(tasks);
|
|
508
643
|
const markdown = renderToolResponseMarkdown({
|
|
509
644
|
toolName: "projectContext",
|
|
510
645
|
sections: [
|
|
511
646
|
summarySection([
|
|
512
647
|
`- projectPath: ${normalizedProjectPath}`,
|
|
513
648
|
`- governanceDir: ${governanceDir}`,
|
|
514
|
-
`-
|
|
649
|
+
`- tasksView: ${tasksMarkdownPath}`,
|
|
515
650
|
`- roadmapIds: ${roadmapIds.length}`,
|
|
516
651
|
]),
|
|
517
652
|
evidenceSection([
|
|
518
653
|
"### Task Summary",
|
|
519
|
-
`- total: ${
|
|
520
|
-
`- TODO: ${
|
|
521
|
-
`- IN_PROGRESS: ${
|
|
522
|
-
`- BLOCKED: ${
|
|
523
|
-
`- DONE: ${
|
|
654
|
+
`- total: ${taskStats.total}`,
|
|
655
|
+
`- TODO: ${taskStats.todo}`,
|
|
656
|
+
`- IN_PROGRESS: ${taskStats.inProgress}`,
|
|
657
|
+
`- BLOCKED: ${taskStats.blocked}`,
|
|
658
|
+
`- DONE: ${taskStats.done}`,
|
|
524
659
|
"",
|
|
525
660
|
"### Artifacts",
|
|
526
661
|
renderArtifactsMarkdown(artifacts),
|
|
@@ -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, vi } from "vitest";
|
|
5
|
-
import { discoverProjects, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir,
|
|
5
|
+
import { discoverProjects, discoverProjectsAcrossRoots, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir, resolveScanRoots, resolveScanDepth, toProjectPath, registerProjectTools } from "./project.js";
|
|
6
6
|
const tempPaths = [];
|
|
7
7
|
async function createTempDir() {
|
|
8
8
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
|
|
@@ -186,6 +186,15 @@ describe("projitive module", () => {
|
|
|
186
186
|
const projects = await discoverProjects(root, 3);
|
|
187
187
|
expect(projects).toEqual([]);
|
|
188
188
|
});
|
|
189
|
+
it("ignores non-existent roots when scanning across multiple roots", async () => {
|
|
190
|
+
const validRoot = await createTempDir();
|
|
191
|
+
const validProject = path.join(validRoot, "project-a");
|
|
192
|
+
const missingRoot = path.join(validRoot, "__missing_root__");
|
|
193
|
+
await fs.mkdir(validProject, { recursive: true });
|
|
194
|
+
await fs.writeFile(path.join(validProject, ".projitive"), "", "utf-8");
|
|
195
|
+
const projects = await discoverProjectsAcrossRoots([missingRoot, validRoot], 3);
|
|
196
|
+
expect(projects).toContain(validProject);
|
|
197
|
+
});
|
|
189
198
|
});
|
|
190
199
|
describe("initializeProjectStructure", () => {
|
|
191
200
|
it("initializes governance structure under default .projitive directory", async () => {
|
|
@@ -197,10 +206,13 @@ describe("projitive module", () => {
|
|
|
197
206
|
path.join(root, ".projitive", "README.md"),
|
|
198
207
|
path.join(root, ".projitive", "roadmap.md"),
|
|
199
208
|
path.join(root, ".projitive", "tasks.md"),
|
|
200
|
-
path.join(root, ".projitive", "
|
|
209
|
+
path.join(root, ".projitive", "templates", "README.md"),
|
|
210
|
+
path.join(root, ".projitive", "templates", "tools", "taskNext.md"),
|
|
211
|
+
path.join(root, ".projitive", "templates", "tools", "taskUpdate.md"),
|
|
201
212
|
path.join(root, ".projitive", "designs"),
|
|
202
213
|
path.join(root, ".projitive", "reports"),
|
|
203
|
-
path.join(root, ".projitive", "
|
|
214
|
+
path.join(root, ".projitive", "templates"),
|
|
215
|
+
path.join(root, ".projitive", "templates", "tools"),
|
|
204
216
|
];
|
|
205
217
|
await Promise.all(expectedPaths.map(async (targetPath) => {
|
|
206
218
|
await expect(fs.access(targetPath)).resolves.toBeUndefined();
|
|
@@ -257,7 +269,7 @@ describe("projitive module", () => {
|
|
|
257
269
|
const initialized = await initializeProjectStructure(root);
|
|
258
270
|
expect(initialized.directories.some(d => d.path.includes("designs"))).toBe(true);
|
|
259
271
|
expect(initialized.directories.some(d => d.path.includes("reports"))).toBe(true);
|
|
260
|
-
expect(initialized.directories.some(d => d.path.includes("
|
|
272
|
+
expect(initialized.directories.some(d => d.path.includes("templates"))).toBe(true);
|
|
261
273
|
});
|
|
262
274
|
});
|
|
263
275
|
describe("utility functions", () => {
|
|
@@ -267,20 +279,30 @@ describe("projitive module", () => {
|
|
|
267
279
|
expect(toProjectPath("/a/b/c")).toBe("/a/b");
|
|
268
280
|
});
|
|
269
281
|
});
|
|
270
|
-
describe("
|
|
271
|
-
it("uses environment variable when no
|
|
282
|
+
describe("resolveScanRoots", () => {
|
|
283
|
+
it("uses legacy environment variable when no multi-root env is provided", () => {
|
|
272
284
|
vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
|
|
273
|
-
expect(
|
|
285
|
+
expect(resolveScanRoots()).toEqual(["/test/root"]);
|
|
274
286
|
vi.unstubAllEnvs();
|
|
275
287
|
});
|
|
276
|
-
it("uses input
|
|
288
|
+
it("uses input paths when provided", () => {
|
|
277
289
|
vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
|
|
278
|
-
expect(
|
|
290
|
+
expect(resolveScanRoots(["/custom/path", " /custom/path ", "/second/path"])).toEqual(["/custom/path", "/second/path"]);
|
|
291
|
+
vi.unstubAllEnvs();
|
|
292
|
+
});
|
|
293
|
+
it("uses PROJITIVE_SCAN_ROOT_PATHS with platform delimiter", () => {
|
|
294
|
+
vi.stubEnv("PROJITIVE_SCAN_ROOT_PATHS", ["/root/a", "/root/b", "", " /root/c "].join(path.delimiter));
|
|
295
|
+
expect(resolveScanRoots()).toEqual(["/root/a", "/root/b", "/root/c"]);
|
|
296
|
+
vi.unstubAllEnvs();
|
|
297
|
+
});
|
|
298
|
+
it("treats JSON-like string as plain delimiter input", () => {
|
|
299
|
+
vi.stubEnv("PROJITIVE_SCAN_ROOT_PATHS", JSON.stringify(["/json/a", "/json/b"]));
|
|
300
|
+
expect(resolveScanRoots()).toHaveLength(1);
|
|
279
301
|
vi.unstubAllEnvs();
|
|
280
302
|
});
|
|
281
|
-
it("throws error when
|
|
303
|
+
it("throws error when no root environment variables are configured", () => {
|
|
282
304
|
vi.unstubAllEnvs();
|
|
283
|
-
expect(() =>
|
|
305
|
+
expect(() => resolveScanRoots()).toThrow("Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS");
|
|
284
306
|
});
|
|
285
307
|
});
|
|
286
308
|
describe("resolveScanDepth", () => {
|