@projitive/mcp 2.1.1 → 2.1.3
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/package.json +5 -1
- package/output/source/common/linter.js +2 -4
- package/output/source/common/response.js +27 -2
- package/output/source/common/response.test.js +13 -1
- package/output/source/prompts/quickStart.js +8 -3
- package/output/source/prompts/taskDiscovery.js +17 -2
- package/output/source/prompts/taskDiscovery.test.js +4 -0
- package/output/source/prompts/taskExecution.js +19 -5
- package/output/source/prompts/taskExecution.test.js +6 -1
- package/output/source/tools/project.js +316 -45
- package/output/source/tools/project.test.js +50 -5
- package/output/source/tools/task.js +83 -55
- package/output/source/tools/task.test.js +42 -32
- package/output/source/types.js +2 -1
- package/package.json +5 -1
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import process from 'node:process';
|
|
4
5
|
import { z } from 'zod';
|
|
5
|
-
import { discoverGovernanceArtifacts, catchIt, PROJECT_LINT_CODES, renderLintSuggestions, ensureStore, replaceRoadmapsInStore, replaceTasksInStore, loadTaskStatusStatsFromStore, createGovernedTool, getDefaultToolTemplateMarkdown, } from '../common/index.js';
|
|
6
|
-
import { collectTaskLintSuggestions, loadTasksDocument, loadTasksDocumentWithOptions, renderTasksMarkdown } from './task.js';
|
|
6
|
+
import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, catchIt, PROJECT_LINT_CODES, renderLintSuggestions, ensureStore, loadRoadmapsFromStore, loadTasksFromStore, replaceRoadmapsInStore, replaceTasksInStore, loadTaskStatusStatsFromStore, upsertRoadmapInStore, upsertTaskInStore, createGovernedTool, getDefaultToolTemplateMarkdown, } from '../common/index.js';
|
|
7
|
+
import { collectProjectContextDocsLintSuggestions, collectTaskLintSuggestions, CORE_ARCHITECTURE_DOC_FILE, CORE_CODE_STYLE_DOC_FILE, CORE_UI_STYLE_DOC_FILE, inspectProjectContextDocsFromArtifacts, loadTasksDocument, loadTasksDocumentWithOptions, renderTasksMarkdown, } from './task.js';
|
|
7
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']);
|
|
11
12
|
const MAX_SCAN_DEPTH = 8;
|
|
13
|
+
const DEFAULT_SCAN_DEPTH = 3;
|
|
12
14
|
function normalizePath(inputPath) {
|
|
13
15
|
return inputPath ? path.resolve(inputPath) : process.cwd();
|
|
14
16
|
}
|
|
@@ -32,13 +34,6 @@ function parseDepthFromEnv(rawDepth) {
|
|
|
32
34
|
}
|
|
33
35
|
return Math.min(MAX_SCAN_DEPTH, Math.max(0, parsed));
|
|
34
36
|
}
|
|
35
|
-
function requireEnvVar(name) {
|
|
36
|
-
const value = process.env[name];
|
|
37
|
-
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
38
|
-
throw new Error(`Missing required environment variable: ${name}`);
|
|
39
|
-
}
|
|
40
|
-
return value.trim();
|
|
41
|
-
}
|
|
42
37
|
function normalizeScanRoots(rootPaths) {
|
|
43
38
|
const normalized = rootPaths
|
|
44
39
|
.map((entry) => entry.trim())
|
|
@@ -72,21 +67,24 @@ export function resolveScanRoots(inputPaths) {
|
|
|
72
67
|
if (rootsFromLegacyEnv.length > 0) {
|
|
73
68
|
return rootsFromLegacyEnv;
|
|
74
69
|
}
|
|
75
|
-
|
|
70
|
+
return [os.homedir()];
|
|
76
71
|
}
|
|
77
72
|
export function resolveScanRoot(inputPath) {
|
|
78
73
|
return resolveScanRoots(inputPath ? [inputPath] : undefined)[0];
|
|
79
74
|
}
|
|
80
75
|
export function resolveScanDepth(inputDepth) {
|
|
81
|
-
const configuredDepthRaw = requireEnvVar('PROJITIVE_SCAN_MAX_DEPTH');
|
|
82
|
-
const configuredDepth = parseDepthFromEnv(configuredDepthRaw);
|
|
83
|
-
if (typeof configuredDepth !== 'number') {
|
|
84
|
-
throw new Error('Invalid PROJITIVE_SCAN_MAX_DEPTH: expected integer in range 0-8');
|
|
85
|
-
}
|
|
86
76
|
if (typeof inputDepth === 'number') {
|
|
87
77
|
return inputDepth;
|
|
88
78
|
}
|
|
89
|
-
|
|
79
|
+
const configuredDepthRaw = process.env.PROJITIVE_SCAN_MAX_DEPTH;
|
|
80
|
+
if (typeof configuredDepthRaw === 'string' && configuredDepthRaw.trim().length > 0) {
|
|
81
|
+
const configuredDepth = parseDepthFromEnv(configuredDepthRaw);
|
|
82
|
+
if (typeof configuredDepth !== 'number') {
|
|
83
|
+
throw new Error('Invalid PROJITIVE_SCAN_MAX_DEPTH: expected integer in range 0-8');
|
|
84
|
+
}
|
|
85
|
+
return configuredDepth;
|
|
86
|
+
}
|
|
87
|
+
return DEFAULT_SCAN_DEPTH;
|
|
90
88
|
}
|
|
91
89
|
function renderArtifactsMarkdown(artifacts) {
|
|
92
90
|
const rows = artifacts.map((item) => {
|
|
@@ -272,34 +270,261 @@ function defaultReadmeMarkdown(governanceDirName) {
|
|
|
272
270
|
'- Treat roadmap.md/tasks.md as generated views from governance store.',
|
|
273
271
|
'- Keep IDs stable (TASK-xxxx / ROADMAP-xxxx).',
|
|
274
272
|
'- Update report evidence before status transitions.',
|
|
273
|
+
'- Maintain core docs under designs/core/: architecture.md, code-style.md, ui-style.md.',
|
|
274
|
+
'- After each task completion, review whether architecture, code conventions, or UI rules changed and update the matching core docs.',
|
|
275
275
|
].join('\n');
|
|
276
276
|
}
|
|
277
277
|
function defaultRoadmapMarkdown(milestones = defaultRoadmapMilestones()) {
|
|
278
278
|
return renderRoadmapMarkdown(milestones);
|
|
279
279
|
}
|
|
280
280
|
function defaultTasksMarkdown(updatedAt = new Date().toISOString()) {
|
|
281
|
-
return renderTasksMarkdown(
|
|
281
|
+
return renderTasksMarkdown(defaultBootstrapTasks(updatedAt));
|
|
282
|
+
}
|
|
283
|
+
function defaultBootstrapTaskBlueprints() {
|
|
284
|
+
return [
|
|
282
285
|
{
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
owner: 'unassigned',
|
|
287
|
-
summary: 'Create initial governance artifacts and confirm task execution loop.',
|
|
288
|
-
updatedAt,
|
|
289
|
-
links: [],
|
|
290
|
-
roadmapRefs: ['ROADMAP-0001'],
|
|
286
|
+
title: 'Initialize project architecture document',
|
|
287
|
+
summary: `Establish system context, boundaries, modules, and integration flows in ${CORE_ARCHITECTURE_DOC_FILE}.`,
|
|
288
|
+
links: [CORE_ARCHITECTURE_DOC_FILE],
|
|
291
289
|
},
|
|
292
|
-
|
|
290
|
+
{
|
|
291
|
+
title: 'Initialize code style document',
|
|
292
|
+
summary: `Capture naming, structure, testing, and review conventions in ${CORE_CODE_STYLE_DOC_FILE}.`,
|
|
293
|
+
links: [CORE_CODE_STYLE_DOC_FILE],
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
title: 'Initialize UI style document',
|
|
297
|
+
summary: `Capture visual language, tokens, accessibility, and interaction rules in ${CORE_UI_STYLE_DOC_FILE}.`,
|
|
298
|
+
links: [CORE_UI_STYLE_DOC_FILE],
|
|
299
|
+
},
|
|
300
|
+
];
|
|
293
301
|
}
|
|
294
302
|
function defaultRoadmapMilestones() {
|
|
295
303
|
return [{
|
|
296
304
|
id: 'ROADMAP-0001',
|
|
297
|
-
title: 'Bootstrap governance baseline',
|
|
305
|
+
title: 'Bootstrap governance and core docs baseline',
|
|
298
306
|
status: 'active',
|
|
299
307
|
time: '2026-Q1',
|
|
300
308
|
updatedAt: new Date().toISOString(),
|
|
301
309
|
}];
|
|
302
310
|
}
|
|
311
|
+
function defaultBootstrapTasks(updatedAt = new Date().toISOString()) {
|
|
312
|
+
return defaultBootstrapTaskBlueprints().map((blueprint, index) => ({
|
|
313
|
+
id: `TASK-${String(index + 1).padStart(4, '0')}`,
|
|
314
|
+
title: blueprint.title,
|
|
315
|
+
status: 'TODO',
|
|
316
|
+
owner: 'unassigned',
|
|
317
|
+
summary: blueprint.summary,
|
|
318
|
+
updatedAt,
|
|
319
|
+
links: blueprint.links,
|
|
320
|
+
roadmapRefs: ['ROADMAP-0001'],
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
function nextGeneratedTaskId(taskIds) {
|
|
324
|
+
const maxSuffix = taskIds
|
|
325
|
+
.map((taskId) => /^TASK-(\d+)$/.exec(taskId)?.[1])
|
|
326
|
+
.map((value) => Number.parseInt(value ?? '-1', 10))
|
|
327
|
+
.filter((value) => Number.isFinite(value) && value > 0)
|
|
328
|
+
.reduce((max, value) => Math.max(max, value), 0);
|
|
329
|
+
const next = maxSuffix + 1;
|
|
330
|
+
return `TASK-${String(next).padStart(Math.max(4, String(next).length), '0')}`;
|
|
331
|
+
}
|
|
332
|
+
function buildBackfillTask(blueprint, taskId, roadmapRef) {
|
|
333
|
+
return {
|
|
334
|
+
id: taskId,
|
|
335
|
+
title: blueprint.title,
|
|
336
|
+
status: 'TODO',
|
|
337
|
+
owner: 'unassigned',
|
|
338
|
+
summary: blueprint.summary,
|
|
339
|
+
updatedAt: new Date().toISOString(),
|
|
340
|
+
links: blueprint.links,
|
|
341
|
+
roadmapRefs: [roadmapRef],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function inspectProjectInitMissingState(governancePath) {
|
|
345
|
+
const requiredDirectories = [
|
|
346
|
+
governancePath,
|
|
347
|
+
path.join(governancePath, 'designs'),
|
|
348
|
+
path.join(governancePath, 'designs', 'core'),
|
|
349
|
+
path.join(governancePath, 'designs', 'research'),
|
|
350
|
+
path.join(governancePath, 'reports'),
|
|
351
|
+
path.join(governancePath, 'templates'),
|
|
352
|
+
path.join(governancePath, 'templates', 'tools'),
|
|
353
|
+
];
|
|
354
|
+
const requiredFiles = [
|
|
355
|
+
path.join(governancePath, 'README.md'),
|
|
356
|
+
path.join(governancePath, 'roadmap.md'),
|
|
357
|
+
path.join(governancePath, 'tasks.md'),
|
|
358
|
+
path.join(governancePath, CORE_ARCHITECTURE_DOC_FILE),
|
|
359
|
+
path.join(governancePath, CORE_CODE_STYLE_DOC_FILE),
|
|
360
|
+
path.join(governancePath, CORE_UI_STYLE_DOC_FILE),
|
|
361
|
+
path.join(governancePath, 'templates', 'README.md'),
|
|
362
|
+
...DEFAULT_TOOL_TEMPLATE_NAMES.map((toolName) => path.join(governancePath, 'templates', 'tools', `${toolName}.md`)),
|
|
363
|
+
];
|
|
364
|
+
const missingDirectories = (await Promise.all(requiredDirectories.map(async (dirPath) => (await pathExists(dirPath)) ? undefined : dirPath)))
|
|
365
|
+
.filter((item) => item != null);
|
|
366
|
+
const missingFiles = (await Promise.all(requiredFiles.map(async (filePath) => (await pathExists(filePath)) ? undefined : filePath)))
|
|
367
|
+
.filter((item) => item != null);
|
|
368
|
+
const markerPath = path.join(governancePath, PROJECT_MARKER);
|
|
369
|
+
const markerMissing = !(await pathExists(markerPath));
|
|
370
|
+
if (markerMissing) {
|
|
371
|
+
return {
|
|
372
|
+
markerMissing,
|
|
373
|
+
missingDirectories,
|
|
374
|
+
missingFiles,
|
|
375
|
+
missingBootstrapTaskTitles: defaultBootstrapTaskBlueprints().map((item) => item.title),
|
|
376
|
+
missingBootstrapRoadmap: true,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
await ensureStore(markerPath);
|
|
380
|
+
const [tasks, roadmaps] = await Promise.all([
|
|
381
|
+
loadTasksFromStore(markerPath),
|
|
382
|
+
loadRoadmapsFromStore(markerPath),
|
|
383
|
+
]);
|
|
384
|
+
const missingBootstrapTaskTitles = defaultBootstrapTaskBlueprints()
|
|
385
|
+
.filter((blueprint) => !tasks.some((task) => task.title === blueprint.title || blueprint.links.some((link) => task.links.includes(link))))
|
|
386
|
+
.map((item) => item.title);
|
|
387
|
+
return {
|
|
388
|
+
markerMissing,
|
|
389
|
+
missingDirectories,
|
|
390
|
+
missingFiles,
|
|
391
|
+
missingBootstrapTaskTitles,
|
|
392
|
+
missingBootstrapRoadmap: roadmaps.length === 0,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
async function backfillBootstrapTasks(markerPath) {
|
|
396
|
+
await ensureStore(markerPath);
|
|
397
|
+
const [tasks, roadmaps] = await Promise.all([
|
|
398
|
+
loadTasksFromStore(markerPath),
|
|
399
|
+
loadRoadmapsFromStore(markerPath),
|
|
400
|
+
]);
|
|
401
|
+
let createdBootstrapRoadmapId;
|
|
402
|
+
let roadmapRef = roadmaps[0]?.id;
|
|
403
|
+
if (!roadmapRef) {
|
|
404
|
+
const milestone = defaultRoadmapMilestones()[0];
|
|
405
|
+
await upsertRoadmapInStore(markerPath, milestone);
|
|
406
|
+
createdBootstrapRoadmapId = milestone.id;
|
|
407
|
+
roadmapRef = milestone.id;
|
|
408
|
+
}
|
|
409
|
+
const mutableTasks = [...tasks];
|
|
410
|
+
const createdBootstrapTaskIds = [];
|
|
411
|
+
for (const blueprint of defaultBootstrapTaskBlueprints()) {
|
|
412
|
+
const exists = mutableTasks.some((task) => task.title === blueprint.title || blueprint.links.some((link) => task.links.includes(link)));
|
|
413
|
+
if (exists) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const nextTaskId = nextGeneratedTaskId(mutableTasks.map((task) => task.id));
|
|
417
|
+
const task = buildBackfillTask(blueprint, nextTaskId, roadmapRef);
|
|
418
|
+
await upsertTaskInStore(markerPath, task);
|
|
419
|
+
mutableTasks.push(task);
|
|
420
|
+
createdBootstrapTaskIds.push(nextTaskId);
|
|
421
|
+
}
|
|
422
|
+
return { createdBootstrapTaskIds, createdBootstrapRoadmapId };
|
|
423
|
+
}
|
|
424
|
+
function defaultProjectArchitectureMarkdown() {
|
|
425
|
+
return [
|
|
426
|
+
'# Project Architecture',
|
|
427
|
+
'',
|
|
428
|
+
'## Mission and Scope',
|
|
429
|
+
'- Describe the product or repository purpose.',
|
|
430
|
+
'- Define the operational boundary of this project.',
|
|
431
|
+
'',
|
|
432
|
+
'## System Boundaries',
|
|
433
|
+
'- List primary inputs, outputs, external integrations, and ownership boundaries.',
|
|
434
|
+
'',
|
|
435
|
+
'## Modules and Responsibilities',
|
|
436
|
+
'- Document the major modules, packages, or services and their responsibilities.',
|
|
437
|
+
'',
|
|
438
|
+
'## Key Flows',
|
|
439
|
+
'- Summarize the highest-value runtime and maintenance flows.',
|
|
440
|
+
'',
|
|
441
|
+
'## Change Triggers',
|
|
442
|
+
'- Update this document when tasks change architecture boundaries, data flow, or integration contracts.',
|
|
443
|
+
].join('\n');
|
|
444
|
+
}
|
|
445
|
+
function defaultCodeStyleMarkdown() {
|
|
446
|
+
return [
|
|
447
|
+
'# Code Style',
|
|
448
|
+
'',
|
|
449
|
+
'## Core Principles',
|
|
450
|
+
'- Document the repository coding principles and non-negotiable constraints.',
|
|
451
|
+
'',
|
|
452
|
+
'## Naming and Structure',
|
|
453
|
+
'- Define naming rules, module boundaries, and file organization expectations.',
|
|
454
|
+
'',
|
|
455
|
+
'## Testing and Validation',
|
|
456
|
+
'- Record expectations for unit tests, integration tests, and verification commands.',
|
|
457
|
+
'',
|
|
458
|
+
'## Review Checklist',
|
|
459
|
+
'- List the code quality checks every completed task should re-evaluate.',
|
|
460
|
+
'',
|
|
461
|
+
'## Change Triggers',
|
|
462
|
+
'- Update this document when tasks establish or revise reusable engineering conventions.',
|
|
463
|
+
].join('\n');
|
|
464
|
+
}
|
|
465
|
+
function defaultUiStyleMarkdown() {
|
|
466
|
+
return [
|
|
467
|
+
'# UI Style',
|
|
468
|
+
'',
|
|
469
|
+
'## Visual Language',
|
|
470
|
+
'- Describe the intended product tone, typography, spacing, and visual hierarchy.',
|
|
471
|
+
'',
|
|
472
|
+
'## Components and Patterns',
|
|
473
|
+
'- Record reusable UI patterns, interaction rules, and component expectations.',
|
|
474
|
+
'',
|
|
475
|
+
'## Accessibility and Quality',
|
|
476
|
+
'- Document accessibility expectations, responsiveness rules, and UX quality bars.',
|
|
477
|
+
'',
|
|
478
|
+
'## Design Tokens',
|
|
479
|
+
'- Capture colors, spacing, motion, and other token-level guidance if applicable.',
|
|
480
|
+
'',
|
|
481
|
+
'## Change Triggers',
|
|
482
|
+
'- Update this document when tasks change UI behavior, interaction rules, or visual consistency guidance.',
|
|
483
|
+
].join('\n');
|
|
484
|
+
}
|
|
485
|
+
function toRelativeGovernancePath(governanceDir, targetPath) {
|
|
486
|
+
const relative = path.relative(governanceDir, targetPath).replace(/\\/g, '/');
|
|
487
|
+
return relative.length > 0 ? relative : '.';
|
|
488
|
+
}
|
|
489
|
+
function classifyProjectInitMissing(initialized) {
|
|
490
|
+
const coreDocFiles = new Set([
|
|
491
|
+
CORE_ARCHITECTURE_DOC_FILE,
|
|
492
|
+
CORE_CODE_STYLE_DOC_FILE,
|
|
493
|
+
CORE_UI_STYLE_DOC_FILE,
|
|
494
|
+
]);
|
|
495
|
+
const relativeMissingFiles = initialized.missingBeforeInit.missingFiles
|
|
496
|
+
.map((item) => toRelativeGovernancePath(initialized.governanceDir, item));
|
|
497
|
+
const coreDocs = relativeMissingFiles.filter((item) => coreDocFiles.has(item));
|
|
498
|
+
const templates = relativeMissingFiles.filter((item) => item === 'templates/README.md' || item.startsWith('templates/tools/'));
|
|
499
|
+
const otherFiles = relativeMissingFiles.filter((item) => !coreDocFiles.has(item) && !(item === 'templates/README.md' || item.startsWith('templates/tools/')));
|
|
500
|
+
return {
|
|
501
|
+
coreDocs,
|
|
502
|
+
templates,
|
|
503
|
+
bootstrapTasks: initialized.missingBeforeInit.missingBootstrapTaskTitles,
|
|
504
|
+
otherFiles,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
function renderProjectInitRepairSummary(initialized) {
|
|
508
|
+
const classified = classifyProjectInitMissing(initialized);
|
|
509
|
+
return [
|
|
510
|
+
'### Repair Summary (Missing Before Init)',
|
|
511
|
+
'- core docs:',
|
|
512
|
+
` - count: ${classified.coreDocs.length}`,
|
|
513
|
+
...(classified.coreDocs.length > 0 ? classified.coreDocs.map((item) => ` - ${item}`) : [' - (none)']),
|
|
514
|
+
'- templates:',
|
|
515
|
+
` - count: ${classified.templates.length}`,
|
|
516
|
+
...(classified.templates.length > 0 ? classified.templates.map((item) => ` - ${item}`) : [' - (none)']),
|
|
517
|
+
'- bootstrap tasks:',
|
|
518
|
+
` - count: ${classified.bootstrapTasks.length}`,
|
|
519
|
+
...(classified.bootstrapTasks.length > 0 ? classified.bootstrapTasks.map((item) => ` - ${item}`) : [' - (none)']),
|
|
520
|
+
'- other required files:',
|
|
521
|
+
` - count: ${classified.otherFiles.length}`,
|
|
522
|
+
...(classified.otherFiles.length > 0 ? classified.otherFiles.map((item) => ` - ${item}`) : [' - (none)']),
|
|
523
|
+
'- remediation actions:',
|
|
524
|
+
` - created bootstrap tasks: ${initialized.remediation.createdBootstrapTaskIds.length}`,
|
|
525
|
+
` - created bootstrap roadmap: ${initialized.remediation.createdBootstrapRoadmapId ?? '(none)'}`,
|
|
526
|
+
];
|
|
527
|
+
}
|
|
303
528
|
function defaultTemplateReadmeMarkdown() {
|
|
304
529
|
return [
|
|
305
530
|
'# Template Guide',
|
|
@@ -335,10 +560,13 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
|
|
|
335
560
|
throw new Error(`projectPath must be a directory: ${projectPath}`);
|
|
336
561
|
}
|
|
337
562
|
const governancePath = path.join(projectPath, governanceDirName);
|
|
563
|
+
const missingBeforeInit = await inspectProjectInitMissingState(governancePath);
|
|
338
564
|
const directories = [];
|
|
339
565
|
const requiredDirectories = [
|
|
340
566
|
governancePath,
|
|
341
567
|
path.join(governancePath, 'designs'),
|
|
568
|
+
path.join(governancePath, 'designs', 'core'),
|
|
569
|
+
path.join(governancePath, 'designs', 'research'),
|
|
342
570
|
path.join(governancePath, 'reports'),
|
|
343
571
|
path.join(governancePath, 'templates'),
|
|
344
572
|
path.join(governancePath, 'templates', 'tools'),
|
|
@@ -353,25 +581,21 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
|
|
|
353
581
|
const defaultTaskUpdatedAt = new Date().toISOString();
|
|
354
582
|
const markerExists = await pathExists(markerPath);
|
|
355
583
|
await ensureStore(markerPath);
|
|
584
|
+
let remediation = { createdBootstrapTaskIds: [] };
|
|
356
585
|
if (force || !markerExists) {
|
|
357
586
|
await replaceRoadmapsInStore(markerPath, defaultRoadmapData);
|
|
358
|
-
await replaceTasksInStore(markerPath,
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
status: 'TODO',
|
|
363
|
-
owner: 'unassigned',
|
|
364
|
-
summary: 'Create initial governance artifacts and confirm task execution loop.',
|
|
365
|
-
updatedAt: defaultTaskUpdatedAt,
|
|
366
|
-
links: [],
|
|
367
|
-
roadmapRefs: ['ROADMAP-0001'],
|
|
368
|
-
},
|
|
369
|
-
]);
|
|
587
|
+
await replaceTasksInStore(markerPath, defaultBootstrapTasks(defaultTaskUpdatedAt));
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
remediation = await backfillBootstrapTasks(markerPath);
|
|
370
591
|
}
|
|
371
592
|
const baseFiles = await Promise.all([
|
|
372
593
|
writeTextFile(path.join(governancePath, 'README.md'), defaultReadmeMarkdown(governanceDirName), force),
|
|
373
594
|
writeTextFile(path.join(governancePath, 'roadmap.md'), defaultRoadmapMarkdown(defaultRoadmapData), force),
|
|
374
595
|
writeTextFile(path.join(governancePath, 'tasks.md'), defaultTasksMarkdown(defaultTaskUpdatedAt), force),
|
|
596
|
+
writeTextFile(path.join(governancePath, CORE_ARCHITECTURE_DOC_FILE), defaultProjectArchitectureMarkdown(), force),
|
|
597
|
+
writeTextFile(path.join(governancePath, CORE_CODE_STYLE_DOC_FILE), defaultCodeStyleMarkdown(), force),
|
|
598
|
+
writeTextFile(path.join(governancePath, CORE_UI_STYLE_DOC_FILE), defaultUiStyleMarkdown(), force),
|
|
375
599
|
writeTextFile(path.join(governancePath, 'templates', 'README.md'), defaultTemplateReadmeMarkdown(), force),
|
|
376
600
|
]);
|
|
377
601
|
const toolTemplateFiles = await Promise.all(DEFAULT_TOOL_TEMPLATE_NAMES.map((toolName) => writeTextFile(path.join(governancePath, 'templates', 'tools', `${toolName}.md`), getDefaultToolTemplateMarkdown(toolName), force)));
|
|
@@ -381,6 +605,8 @@ export async function initializeProjectStructure(inputPath, governanceDir, force
|
|
|
381
605
|
governanceDir: governancePath,
|
|
382
606
|
directories,
|
|
383
607
|
files,
|
|
608
|
+
missingBeforeInit,
|
|
609
|
+
remediation,
|
|
384
610
|
};
|
|
385
611
|
}
|
|
386
612
|
export function registerProjectTools(server) {
|
|
@@ -411,17 +637,45 @@ export function registerProjectTools(server) {
|
|
|
411
637
|
`- createdFiles: ${filesByAction.created.length}`,
|
|
412
638
|
`- updatedFiles: ${filesByAction.updated.length}`,
|
|
413
639
|
`- skippedFiles: ${filesByAction.skipped.length}`,
|
|
640
|
+
`- missingDirectoriesBeforeInit: ${initialized.missingBeforeInit.missingDirectories.length}`,
|
|
641
|
+
`- missingFilesBeforeInit: ${initialized.missingBeforeInit.missingFiles.length}`,
|
|
642
|
+
`- missingBootstrapTasksBeforeInit: ${initialized.missingBeforeInit.missingBootstrapTaskTitles.length}`,
|
|
643
|
+
`- missingBootstrapRoadmapBeforeInit: ${initialized.missingBeforeInit.missingBootstrapRoadmap ? 'true' : 'false'}`,
|
|
644
|
+
`- createdBootstrapTasks: ${initialized.remediation.createdBootstrapTaskIds.length}`,
|
|
645
|
+
`- createdBootstrapRoadmap: ${initialized.remediation.createdBootstrapRoadmapId ?? '(none)'}`,
|
|
414
646
|
'- directories:',
|
|
415
647
|
...initialized.directories.map((item) => ` - ${item.action}: ${item.path}`),
|
|
416
648
|
'- files:',
|
|
417
649
|
...initialized.files.map((item) => ` - ${item.action}: ${item.path}`),
|
|
650
|
+
...(initialized.missingBeforeInit.missingDirectories.length > 0
|
|
651
|
+
? ['- missingDirectoriesBeforeInit.list:', ...initialized.missingBeforeInit.missingDirectories.map((item) => ` - ${item}`)]
|
|
652
|
+
: []),
|
|
653
|
+
...(initialized.missingBeforeInit.missingFiles.length > 0
|
|
654
|
+
? ['- missingFilesBeforeInit.list:', ...initialized.missingBeforeInit.missingFiles.map((item) => ` - ${item}`)]
|
|
655
|
+
: []),
|
|
656
|
+
...(initialized.missingBeforeInit.missingBootstrapTaskTitles.length > 0
|
|
657
|
+
? ['- missingBootstrapTasksBeforeInit.list:', ...initialized.missingBeforeInit.missingBootstrapTaskTitles.map((item) => ` - ${item}`)]
|
|
658
|
+
: []),
|
|
659
|
+
...(initialized.remediation.createdBootstrapTaskIds.length > 0
|
|
660
|
+
? ['- createdBootstrapTaskIds.list:', ...initialized.remediation.createdBootstrapTaskIds.map((item) => ` - ${item}`)]
|
|
661
|
+
: []),
|
|
662
|
+
'',
|
|
663
|
+
...renderProjectInitRepairSummary(initialized),
|
|
418
664
|
],
|
|
419
|
-
guidance: () => [
|
|
665
|
+
guidance: ({ initialized }) => [
|
|
666
|
+
...(initialized.missingBeforeInit.markerMissing
|
|
667
|
+
? ['- Governance root was missing before init. This call performed a full bootstrap.']
|
|
668
|
+
: ['- Governance root already existed. This call inspected the current initialization state and backfilled missing artifacts where possible.']),
|
|
669
|
+
...(initialized.missingBeforeInit.missingFiles.length > 0 || initialized.missingBeforeInit.missingDirectories.length > 0 || initialized.missingBeforeInit.missingBootstrapTaskTitles.length > 0 || initialized.missingBeforeInit.missingBootstrapRoadmap
|
|
670
|
+
? ['- This project was partially initialized. Review the created files and bootstrap tasks, then complete any placeholder content.']
|
|
671
|
+
: ['- No initialization gaps were detected. Use force=true only when you intentionally want to overwrite templates/files.']),
|
|
420
672
|
'- If files were skipped and you want to overwrite templates, rerun with force=true.',
|
|
421
673
|
'- Continue with projectContext and taskList for execution.',
|
|
674
|
+
'- Start with the three bootstrap TODO tasks for architecture, code style, and UI style docs.',
|
|
422
675
|
],
|
|
423
676
|
suggestions: () => [
|
|
424
677
|
'- After init, fill owner/roadmapRefs/links in .projitive task table before marking DONE.',
|
|
678
|
+
'- Keep designs/core/architecture.md, designs/core/code-style.md, and designs/core/ui-style.md in sync with completed work.',
|
|
425
679
|
'- Keep task source-of-truth inside .projitive governance store.',
|
|
426
680
|
],
|
|
427
681
|
nextCall: ({ initialized }) => `projectContext(projectPath="${initialized.projectPath}")`,
|
|
@@ -595,20 +849,24 @@ export function registerProjectTools(server) {
|
|
|
595
849
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
596
850
|
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
597
851
|
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
852
|
+
const projectContextDocsState = inspectProjectContextDocsFromArtifacts(candidateFilesFromArtifacts(artifacts));
|
|
598
853
|
const dbPath = path.join(governanceDir, PROJECT_MARKER);
|
|
599
854
|
await ensureStore(dbPath);
|
|
600
855
|
const taskStats = await loadTaskStatusStatsFromStore(dbPath);
|
|
601
856
|
const { markdownPath: tasksMarkdownPath, tasks } = await loadTasksDocument(governanceDir);
|
|
602
857
|
const { markdownPath: roadmapMarkdownPath, milestones } = await loadRoadmapDocumentWithOptions(governanceDir, false);
|
|
603
858
|
const roadmapIds = milestones.map((item) => item.id);
|
|
604
|
-
return { normalizedProjectPath, governanceDir, tasksMarkdownPath, roadmapMarkdownPath, roadmapIds, taskStats, artifacts, tasks };
|
|
859
|
+
return { normalizedProjectPath, governanceDir, tasksMarkdownPath, roadmapMarkdownPath, roadmapIds, taskStats, artifacts, tasks, projectContextDocsState };
|
|
605
860
|
},
|
|
606
|
-
summary: ({ normalizedProjectPath, governanceDir, tasksMarkdownPath, roadmapMarkdownPath, roadmapIds }) => [
|
|
861
|
+
summary: ({ normalizedProjectPath, governanceDir, tasksMarkdownPath, roadmapMarkdownPath, roadmapIds, projectContextDocsState }) => [
|
|
607
862
|
`- projectPath: ${normalizedProjectPath}`,
|
|
608
863
|
`- governanceDir: ${governanceDir}`,
|
|
609
864
|
`- tasksView: ${tasksMarkdownPath}`,
|
|
610
865
|
`- roadmapView: ${roadmapMarkdownPath}`,
|
|
611
866
|
`- roadmapIds: ${roadmapIds.length}`,
|
|
867
|
+
`- projectArchitectureDocsStatus: ${projectContextDocsState.missingArchitectureDocs ? 'MISSING' : 'READY'}`,
|
|
868
|
+
`- codeStyleDocsStatus: ${projectContextDocsState.missingCodeStyleDocs ? 'MISSING' : 'READY'}`,
|
|
869
|
+
`- uiStyleDocsStatus: ${projectContextDocsState.missingUiStyleDocs ? 'MISSING' : 'READY'}`,
|
|
612
870
|
],
|
|
613
871
|
evidence: ({ taskStats, artifacts }) => [
|
|
614
872
|
'### Task Summary',
|
|
@@ -621,11 +879,24 @@ export function registerProjectTools(server) {
|
|
|
621
879
|
'### Artifacts',
|
|
622
880
|
renderArtifactsMarkdown(artifacts),
|
|
623
881
|
],
|
|
624
|
-
guidance: () => [
|
|
882
|
+
guidance: ({ normalizedProjectPath, projectContextDocsState }) => [
|
|
883
|
+
...(!projectContextDocsState.ready
|
|
884
|
+
? [
|
|
885
|
+
'- Project-level governance gate is NOT satisfied because required core docs are missing.',
|
|
886
|
+
`- Rerun projectInit to repair missing governance artifacts and bootstrap tasks: projectInit(projectPath="${normalizedProjectPath}")`,
|
|
887
|
+
'- After projectInit backfills missing files/tasks, update any placeholder content in the created docs.',
|
|
888
|
+
]
|
|
889
|
+
: []),
|
|
890
|
+
'- Governance state must be changed via tools; do not directly edit tasks.md/roadmap.md generated views.',
|
|
625
891
|
'- Start from `taskList` to choose a target task.',
|
|
626
892
|
'- Then call `taskContext` with a task ID to retrieve evidence locations and reading order.',
|
|
627
893
|
],
|
|
628
|
-
suggestions: ({ tasks }) =>
|
|
629
|
-
|
|
894
|
+
suggestions: ({ tasks, projectContextDocsState }) => [
|
|
895
|
+
...collectTaskLintSuggestions(tasks),
|
|
896
|
+
...renderLintSuggestions(collectProjectContextDocsLintSuggestions(projectContextDocsState)),
|
|
897
|
+
],
|
|
898
|
+
nextCall: ({ normalizedProjectPath, projectContextDocsState }) => projectContextDocsState.ready
|
|
899
|
+
? `taskList(projectPath="${normalizedProjectPath}")`
|
|
900
|
+
: `projectInit(projectPath="${normalizedProjectPath}")`,
|
|
630
901
|
}));
|
|
631
902
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import os from 'node:os';
|
|
2
|
+
import os, { homedir } from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { discoverProjects, discoverProjectsAcrossRoots, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir, resolveScanRoots, resolveScanRoot, resolveScanDepth, toProjectPath, registerProjectTools } from './project.js';
|
|
6
|
+
import { loadTasksFromStore, replaceTasksInStore } from '../common/store.js';
|
|
6
7
|
const tempPaths = [];
|
|
7
8
|
async function createTempDir() {
|
|
8
9
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-mcp-test-'));
|
|
@@ -222,10 +223,15 @@ describe('projitive module', () => {
|
|
|
222
223
|
path.join(root, '.projitive', 'README.md'),
|
|
223
224
|
path.join(root, '.projitive', 'roadmap.md'),
|
|
224
225
|
path.join(root, '.projitive', 'tasks.md'),
|
|
226
|
+
path.join(root, '.projitive', 'designs', 'core', 'architecture.md'),
|
|
227
|
+
path.join(root, '.projitive', 'designs', 'core', 'code-style.md'),
|
|
228
|
+
path.join(root, '.projitive', 'designs', 'core', 'ui-style.md'),
|
|
225
229
|
path.join(root, '.projitive', 'templates', 'README.md'),
|
|
226
230
|
path.join(root, '.projitive', 'templates', 'tools', 'taskNext.md'),
|
|
227
231
|
path.join(root, '.projitive', 'templates', 'tools', 'taskUpdate.md'),
|
|
228
232
|
path.join(root, '.projitive', 'designs'),
|
|
233
|
+
path.join(root, '.projitive', 'designs', 'core'),
|
|
234
|
+
path.join(root, '.projitive', 'designs', 'research'),
|
|
229
235
|
path.join(root, '.projitive', 'reports'),
|
|
230
236
|
path.join(root, '.projitive', 'templates'),
|
|
231
237
|
path.join(root, '.projitive', 'templates', 'tools'),
|
|
@@ -278,6 +284,25 @@ describe('projitive module', () => {
|
|
|
278
284
|
expect(readmeContent).toBe('custom-content');
|
|
279
285
|
expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe('skipped');
|
|
280
286
|
});
|
|
287
|
+
it('backfills missing core docs and bootstrap tasks when rerunning projectInit on partial governance', async () => {
|
|
288
|
+
const root = await createTempDir();
|
|
289
|
+
const governanceDir = path.join(root, '.projitive');
|
|
290
|
+
await initializeProjectStructure(root);
|
|
291
|
+
await fs.rm(path.join(governanceDir, 'designs', 'core', 'ui-style.md'));
|
|
292
|
+
const dbPath = path.join(governanceDir, '.projitive');
|
|
293
|
+
const existingTasks = await loadTasksFromStore(dbPath);
|
|
294
|
+
const uiTask = existingTasks.find((task) => task.title === 'Initialize UI style document');
|
|
295
|
+
expect(uiTask).toBeTruthy();
|
|
296
|
+
const filteredTasks = existingTasks.filter((task) => task.title !== 'Initialize UI style document');
|
|
297
|
+
await replaceTasksInStore(dbPath, filteredTasks);
|
|
298
|
+
const initialized = await initializeProjectStructure(root);
|
|
299
|
+
expect(await fs.access(path.join(governanceDir, 'designs', 'core', 'ui-style.md')).then(() => true).catch(() => false)).toBe(true);
|
|
300
|
+
expect(initialized.missingBeforeInit.missingFiles.some((item) => item.endsWith('designs/core/ui-style.md'))).toBe(true);
|
|
301
|
+
expect(initialized.missingBeforeInit.missingBootstrapTaskTitles).toContain('Initialize UI style document');
|
|
302
|
+
expect(initialized.remediation.createdBootstrapTaskIds).toHaveLength(1);
|
|
303
|
+
const reloadedTasks = await loadTasksFromStore(dbPath);
|
|
304
|
+
expect(reloadedTasks.some((task) => task.title === 'Initialize UI style document')).toBe(true);
|
|
305
|
+
});
|
|
281
306
|
it('creates all required subdirectories', async () => {
|
|
282
307
|
const root = await createTempDir();
|
|
283
308
|
const initialized = await initializeProjectStructure(root);
|
|
@@ -327,9 +352,9 @@ describe('projitive module', () => {
|
|
|
327
352
|
expect(resolveScanRoots()).toHaveLength(1);
|
|
328
353
|
vi.unstubAllEnvs();
|
|
329
354
|
});
|
|
330
|
-
it('
|
|
355
|
+
it('falls back to home directory when no env vars configured', () => {
|
|
331
356
|
vi.unstubAllEnvs();
|
|
332
|
-
expect(
|
|
357
|
+
expect(resolveScanRoots()).toEqual([homedir()]);
|
|
333
358
|
});
|
|
334
359
|
});
|
|
335
360
|
describe('resolveScanDepth', () => {
|
|
@@ -357,9 +382,9 @@ describe('projitive module', () => {
|
|
|
357
382
|
expect(() => resolveScanDepth()).toThrow('Invalid PROJITIVE_SCAN_MAX_DEPTH');
|
|
358
383
|
vi.unstubAllEnvs();
|
|
359
384
|
});
|
|
360
|
-
it('
|
|
385
|
+
it('falls back to default depth 3 when env var is missing', () => {
|
|
361
386
|
vi.unstubAllEnvs();
|
|
362
|
-
expect(
|
|
387
|
+
expect(resolveScanDepth()).toBe(3);
|
|
363
388
|
});
|
|
364
389
|
});
|
|
365
390
|
describe('resolveScanRoot', () => {
|
|
@@ -443,6 +468,13 @@ describe('projitive module', () => {
|
|
|
443
468
|
expect(result.isError).toBeUndefined();
|
|
444
469
|
expect(result.content[0].text).toContain('governanceDir:');
|
|
445
470
|
expect(result.content[0].text).toContain('createdFiles:');
|
|
471
|
+
expect(result.content[0].text).toContain('designs/core/architecture.md');
|
|
472
|
+
expect(result.content[0].text).toContain('designs/core/code-style.md');
|
|
473
|
+
expect(result.content[0].text).toContain('designs/core/ui-style.md');
|
|
474
|
+
expect(result.content[0].text).toContain('Repair Summary (Missing Before Init)');
|
|
475
|
+
expect(result.content[0].text).toContain('- core docs:');
|
|
476
|
+
expect(result.content[0].text).toContain('- templates:');
|
|
477
|
+
expect(result.content[0].text).toContain('- bootstrap tasks:');
|
|
446
478
|
});
|
|
447
479
|
it('projectLocate resolves governance dir from any inner path', async () => {
|
|
448
480
|
const root = await createTempDir();
|
|
@@ -484,6 +516,19 @@ describe('projitive module', () => {
|
|
|
484
516
|
expect(result.content[0].text).toContain('Task Summary');
|
|
485
517
|
expect(result.content[0].text).toContain('Artifacts');
|
|
486
518
|
});
|
|
519
|
+
it('projectContext suggests rerunning projectInit when required core docs are missing', async () => {
|
|
520
|
+
const root = await createTempDir();
|
|
521
|
+
await initializeProjectStructure(root);
|
|
522
|
+
await fs.rm(path.join(root, '.projitive', 'designs', 'core', 'code-style.md'));
|
|
523
|
+
const mockServer = { registerTool: vi.fn() };
|
|
524
|
+
registerProjectTools(mockServer);
|
|
525
|
+
const projectContext = getProjectToolHandler(mockServer, 'projectContext');
|
|
526
|
+
const result = await projectContext({ projectPath: root });
|
|
527
|
+
expect(result.isError).toBeUndefined();
|
|
528
|
+
expect(result.content[0].text).toContain('codeStyleDocsStatus: MISSING');
|
|
529
|
+
expect(result.content[0].text).toContain('projectInit(projectPath="');
|
|
530
|
+
expect(result.content[0].text).toContain('PROJECT_CODE_STYLE_DOC_MISSING');
|
|
531
|
+
});
|
|
487
532
|
it('syncViews materializes both tasks and roadmap markdown views', async () => {
|
|
488
533
|
const root = await createTempDir();
|
|
489
534
|
await initializeProjectStructure(root);
|