@projitive/mcp 2.0.2 → 2.0.4
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 +14 -1
- package/output/package.json +8 -2
- package/output/source/common/artifacts.js +1 -1
- package/output/source/common/artifacts.test.js +11 -11
- package/output/source/common/errors.js +19 -19
- package/output/source/common/files.js +11 -11
- package/output/source/common/files.test.js +14 -14
- package/output/source/common/index.js +10 -10
- package/output/source/common/linter.js +27 -27
- package/output/source/common/linter.test.js +9 -9
- package/output/source/common/markdown.js +3 -3
- package/output/source/common/markdown.test.js +15 -15
- package/output/source/common/response.js +74 -74
- package/output/source/common/response.test.js +30 -30
- package/output/source/common/store.js +40 -40
- package/output/source/common/store.test.js +72 -72
- package/output/source/common/types.js +3 -3
- package/output/source/common/utils.js +8 -8
- package/output/source/index.js +16 -16
- package/output/source/index.test.js +64 -64
- package/output/source/prompts/index.js +3 -3
- package/output/source/prompts/quickStart.js +96 -96
- package/output/source/prompts/taskDiscovery.js +184 -180
- package/output/source/prompts/taskExecution.js +148 -147
- package/output/source/resources/designs.js +26 -26
- package/output/source/resources/designs.test.js +88 -88
- package/output/source/resources/governance.js +19 -19
- package/output/source/resources/index.js +2 -2
- package/output/source/resources/readme.js +7 -7
- package/output/source/resources/readme.test.js +113 -113
- package/output/source/resources/reports.js +10 -10
- package/output/source/resources/reports.test.js +83 -83
- package/output/source/tools/index.js +3 -3
- package/output/source/tools/project.js +196 -191
- package/output/source/tools/project.test.js +187 -164
- package/output/source/tools/roadmap.js +173 -76
- package/output/source/tools/roadmap.test.js +58 -42
- package/output/source/tools/task.js +380 -255
- package/output/source/tools/task.test.js +117 -110
- package/output/source/types.js +22 -22
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -21,6 +21,17 @@ Why teams use it:
|
|
|
21
21
|
- Better evidence traceability
|
|
22
22
|
- More predictable multi-agent delivery
|
|
23
23
|
|
|
24
|
+
## Outcomes You Can Expect
|
|
25
|
+
|
|
26
|
+
After onboarding Projitive MCP, teams typically get these outcomes quickly:
|
|
27
|
+
|
|
28
|
+
- Faster execution bootstrapping: create missing work items with taskCreate/roadmapCreate.
|
|
29
|
+
- Better state integrity: task and roadmap transitions remain traceable and verifiable.
|
|
30
|
+
- Stronger delivery continuity: discover -> execute -> verify -> reprioritize loops stay stable.
|
|
31
|
+
- Easier adoption: new contributors follow a deterministic call sequence.
|
|
32
|
+
|
|
33
|
+
Key point: best results come from autonomous execution agents such as OpenClaw.
|
|
34
|
+
|
|
24
35
|
## What It Is Useful For
|
|
25
36
|
|
|
26
37
|
Projitive MCP helps agents move work forward in governed projects without losing traceability.
|
|
@@ -82,7 +93,7 @@ Recommended minimal sequence:
|
|
|
82
93
|
|
|
83
94
|
1. taskNext
|
|
84
95
|
2. taskContext
|
|
85
|
-
3. taskUpdate and/or roadmapUpdate
|
|
96
|
+
3. taskCreate/taskUpdate and/or roadmapCreate/roadmapUpdate
|
|
86
97
|
4. taskContext
|
|
87
98
|
5. taskNext
|
|
88
99
|
|
|
@@ -124,9 +135,11 @@ sequenceDiagram
|
|
|
124
135
|
| Task | taskList | List tasks |
|
|
125
136
|
| Task | taskNext | Select best actionable task |
|
|
126
137
|
| Task | taskContext | Get task evidence and reading order |
|
|
138
|
+
| Task | taskCreate | Create task |
|
|
127
139
|
| Task | taskUpdate | Update task state and metadata |
|
|
128
140
|
| Roadmap | roadmapList | List roadmaps and linked tasks |
|
|
129
141
|
| Roadmap | roadmapContext | Get roadmap context |
|
|
142
|
+
| Roadmap | roadmapCreate | Create roadmap milestone |
|
|
130
143
|
| Roadmap | roadmapUpdate | Update roadmap milestone fields |
|
|
131
144
|
|
|
132
145
|
### Resources
|
package/output/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@projitive/mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "Projitive MCP Server for project and task discovery/update",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"test": "vitest run",
|
|
17
17
|
"test:coverage": "vitest run --coverage",
|
|
18
18
|
"benchmark": "vitest bench --run",
|
|
19
|
-
"lint": "
|
|
19
|
+
"lint": "npm run lint:types && npm run lint:eslint",
|
|
20
|
+
"lint:types": "tsc -p tsconfig.json --noEmit",
|
|
21
|
+
"lint:eslint": "eslint .",
|
|
20
22
|
"build": "rm -rf output && tsc -p tsconfig.json",
|
|
21
23
|
"prepublishOnly": "npm run build",
|
|
22
24
|
"dev": "tsc -p tsconfig.json --watch"
|
|
@@ -31,8 +33,12 @@
|
|
|
31
33
|
"devDependencies": {
|
|
32
34
|
"@types/node": "^24.3.0",
|
|
33
35
|
"@vitest/coverage-v8": "^3.2.4",
|
|
36
|
+
"@eslint/js": "^9.37.0",
|
|
37
|
+
"eslint": "^9.37.0",
|
|
38
|
+
"globals": "^16.4.0",
|
|
34
39
|
"tsx": "^4.20.5",
|
|
35
40
|
"typescript": "^5.9.2",
|
|
41
|
+
"typescript-eslint": "^8.45.0",
|
|
36
42
|
"vitest": "^3.2.4"
|
|
37
43
|
}
|
|
38
44
|
}
|
|
@@ -2,7 +2,7 @@ export function candidateFilesFromArtifacts(artifacts) {
|
|
|
2
2
|
return artifacts
|
|
3
3
|
.filter((item) => item.exists)
|
|
4
4
|
.flatMap((item) => {
|
|
5
|
-
if (item.kind ===
|
|
5
|
+
if (item.kind === 'file') {
|
|
6
6
|
return [item.path];
|
|
7
7
|
}
|
|
8
8
|
return (item.markdownFiles ?? []).map((entry) => entry.path);
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import { describe, expect, it } from
|
|
2
|
-
import { candidateFilesFromArtifacts } from
|
|
3
|
-
describe(
|
|
4
|
-
it(
|
|
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
5
|
const candidates = candidateFilesFromArtifacts([
|
|
6
|
-
{ name:
|
|
7
|
-
{ name:
|
|
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
8
|
{
|
|
9
|
-
name:
|
|
10
|
-
kind:
|
|
11
|
-
path:
|
|
9
|
+
name: 'designs',
|
|
10
|
+
kind: 'directory',
|
|
11
|
+
path: '/a/designs',
|
|
12
12
|
exists: true,
|
|
13
|
-
markdownFiles: [{ path:
|
|
13
|
+
markdownFiles: [{ path: '/a/designs/d1.md', lineCount: 10 }],
|
|
14
14
|
},
|
|
15
15
|
]);
|
|
16
|
-
expect(candidates).toEqual([
|
|
16
|
+
expect(candidates).toEqual(['/a/README.md', '/a/designs/d1.md']);
|
|
17
17
|
});
|
|
18
18
|
});
|
|
@@ -6,49 +6,49 @@ export class ProjitiveError extends Error {
|
|
|
6
6
|
super(message);
|
|
7
7
|
this.code = code;
|
|
8
8
|
this.details = details;
|
|
9
|
-
this.name =
|
|
9
|
+
this.name = 'ProjitiveError';
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
// Project related errors
|
|
13
13
|
export class ProjectError extends ProjitiveError {
|
|
14
14
|
constructor(message, code, details) {
|
|
15
15
|
super(message, code, details);
|
|
16
|
-
this.name =
|
|
16
|
+
this.name = 'ProjectError';
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
export class ProjectNotFoundError extends ProjectError {
|
|
20
20
|
constructor(inputPath) {
|
|
21
|
-
super(`Project not found at path: ${inputPath}`,
|
|
21
|
+
super(`Project not found at path: ${inputPath}`, 'PROJECT_NOT_FOUND', {
|
|
22
22
|
inputPath,
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
export class GovernanceRootNotFoundError extends ProjectError {
|
|
27
27
|
constructor(projectPath) {
|
|
28
|
-
super(`Governance root not found for project: ${projectPath}`,
|
|
28
|
+
super(`Governance root not found for project: ${projectPath}`, 'GOVERNANCE_ROOT_NOT_FOUND', { projectPath });
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
// Task related errors
|
|
32
32
|
export class TaskError extends ProjitiveError {
|
|
33
33
|
constructor(message, code, details) {
|
|
34
34
|
super(message, code, details);
|
|
35
|
-
this.name =
|
|
35
|
+
this.name = 'TaskError';
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
export class TaskNotFoundError extends TaskError {
|
|
39
39
|
constructor(taskId) {
|
|
40
|
-
super(`Task not found: ${taskId}`,
|
|
40
|
+
super(`Task not found: ${taskId}`, 'TASK_NOT_FOUND', { taskId });
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
export class InvalidTaskIdError extends TaskError {
|
|
44
44
|
constructor(taskId) {
|
|
45
|
-
super(`Invalid task ID: ${taskId}`,
|
|
45
|
+
super(`Invalid task ID: ${taskId}`, 'INVALID_TASK_ID', { taskId });
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
export class TaskValidationError extends TaskError {
|
|
49
49
|
errors;
|
|
50
50
|
constructor(taskId, errors) {
|
|
51
|
-
super(`Task validation failed for ${taskId}: ${errors.join(
|
|
51
|
+
super(`Task validation failed for ${taskId}: ${errors.join(', ')}`, 'TASK_VALIDATION_FAILED', { taskId, errors });
|
|
52
52
|
this.errors = errors;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -56,26 +56,26 @@ export class TaskValidationError extends TaskError {
|
|
|
56
56
|
export class FileError extends ProjitiveError {
|
|
57
57
|
filePath;
|
|
58
58
|
constructor(message, filePath, code, details) {
|
|
59
|
-
super(message, code ||
|
|
59
|
+
super(message, code || 'FILE_ERROR', { filePath, ...details });
|
|
60
60
|
this.filePath = filePath;
|
|
61
|
-
this.name =
|
|
61
|
+
this.name = 'FileError';
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
export class FileNotFoundError extends FileError {
|
|
65
65
|
constructor(filePath) {
|
|
66
|
-
super(`File not found: ${filePath}`, filePath,
|
|
66
|
+
super(`File not found: ${filePath}`, filePath, 'FILE_NOT_FOUND');
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
export class FileReadError extends FileError {
|
|
70
70
|
constructor(filePath, cause) {
|
|
71
|
-
super(`Failed to read file: ${filePath}`, filePath,
|
|
71
|
+
super(`Failed to read file: ${filePath}`, filePath, 'FILE_READ_ERROR', {
|
|
72
72
|
cause: cause?.message,
|
|
73
73
|
});
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
export class FileWriteError extends FileError {
|
|
77
77
|
constructor(filePath, cause) {
|
|
78
|
-
super(`Failed to write file: ${filePath}`, filePath,
|
|
78
|
+
super(`Failed to write file: ${filePath}`, filePath, 'FILE_WRITE_ERROR', {
|
|
79
79
|
cause: cause?.message,
|
|
80
80
|
});
|
|
81
81
|
}
|
|
@@ -84,15 +84,15 @@ export class FileWriteError extends FileError {
|
|
|
84
84
|
export class ValidationError extends ProjitiveError {
|
|
85
85
|
errors;
|
|
86
86
|
constructor(message, errors = [], code) {
|
|
87
|
-
super(message, code ||
|
|
87
|
+
super(message, code || 'VALIDATION_FAILED', { errors });
|
|
88
88
|
this.errors = errors;
|
|
89
|
-
this.name =
|
|
89
|
+
this.name = 'ValidationError';
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
export class ConfidenceScoreError extends ValidationError {
|
|
93
93
|
score;
|
|
94
94
|
constructor(message, score, errors = []) {
|
|
95
|
-
super(message, errors,
|
|
95
|
+
super(message, errors, 'CONFIDENCE_SCORE_ERROR');
|
|
96
96
|
this.score = score;
|
|
97
97
|
this.score = score;
|
|
98
98
|
}
|
|
@@ -101,19 +101,19 @@ export class ConfidenceScoreError extends ValidationError {
|
|
|
101
101
|
export class MCPError extends ProjitiveError {
|
|
102
102
|
constructor(message, code, details) {
|
|
103
103
|
super(message, code, details);
|
|
104
|
-
this.name =
|
|
104
|
+
this.name = 'MCPError';
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
export class ResourceNotFoundError extends MCPError {
|
|
108
108
|
constructor(resourceUri) {
|
|
109
|
-
super(`Resource not found: ${resourceUri}`,
|
|
109
|
+
super(`Resource not found: ${resourceUri}`, 'RESOURCE_NOT_FOUND', {
|
|
110
110
|
resourceUri,
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
export class PromptNotFoundError extends MCPError {
|
|
115
115
|
constructor(promptName) {
|
|
116
|
-
super(`Prompt not found: ${promptName}`,
|
|
116
|
+
super(`Prompt not found: ${promptName}`, 'PROMPT_NOT_FOUND', {
|
|
117
117
|
promptName,
|
|
118
118
|
});
|
|
119
119
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { catchIt } from
|
|
4
|
-
const FILE_ARTIFACTS = [
|
|
5
|
-
const DIRECTORY_ARTIFACTS = [
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { catchIt } from './catch.js';
|
|
4
|
+
const FILE_ARTIFACTS = ['README.md', 'roadmap.md', 'tasks.md'];
|
|
5
|
+
const DIRECTORY_ARTIFACTS = ['designs', 'reports', 'templates'];
|
|
6
6
|
async function fileLineCount(filePath) {
|
|
7
|
-
const content = await fs.readFile(filePath,
|
|
7
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
8
8
|
if (!content) {
|
|
9
9
|
return 0;
|
|
10
10
|
}
|
|
@@ -16,7 +16,7 @@ async function listMarkdownFiles(dirPath) {
|
|
|
16
16
|
return [];
|
|
17
17
|
}
|
|
18
18
|
const entries = entriesResult.value;
|
|
19
|
-
const files = entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(
|
|
19
|
+
const files = entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md'));
|
|
20
20
|
const result = [];
|
|
21
21
|
for (const file of files) {
|
|
22
22
|
const fullPath = path.join(dirPath, file.name);
|
|
@@ -32,14 +32,14 @@ export async function discoverGovernanceArtifacts(governanceDir) {
|
|
|
32
32
|
if (!accessResult.isError()) {
|
|
33
33
|
result.push({
|
|
34
34
|
name: artifact,
|
|
35
|
-
kind:
|
|
35
|
+
kind: 'file',
|
|
36
36
|
path: artifactPath,
|
|
37
37
|
exists: true,
|
|
38
38
|
lineCount: await fileLineCount(artifactPath),
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
else {
|
|
42
|
-
result.push({ name: artifact, kind:
|
|
42
|
+
result.push({ name: artifact, kind: 'file', path: artifactPath, exists: false });
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
for (const artifact of DIRECTORY_ARTIFACTS) {
|
|
@@ -48,14 +48,14 @@ export async function discoverGovernanceArtifacts(governanceDir) {
|
|
|
48
48
|
if (!accessResult.isError()) {
|
|
49
49
|
result.push({
|
|
50
50
|
name: artifact,
|
|
51
|
-
kind:
|
|
51
|
+
kind: 'directory',
|
|
52
52
|
path: artifactPath,
|
|
53
53
|
exists: true,
|
|
54
54
|
markdownFiles: await listMarkdownFiles(artifactPath),
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
else {
|
|
58
|
-
result.push({ name: artifact, kind:
|
|
58
|
+
result.push({ name: artifact, kind: 'directory', path: artifactPath, exists: false, markdownFiles: [] });
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
return result;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import os from
|
|
3
|
-
import path from
|
|
4
|
-
import { afterEach, describe, expect, it } from
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
5
|
import { discoverGovernanceArtifacts } from './files.js';
|
|
6
6
|
const tempPaths = [];
|
|
7
7
|
async function createTempDir() {
|
|
8
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(),
|
|
8
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-mcp-test-'));
|
|
9
9
|
tempPaths.push(dir);
|
|
10
10
|
return dir;
|
|
11
11
|
}
|
|
@@ -14,19 +14,19 @@ afterEach(async () => {
|
|
|
14
14
|
await fs.rm(dir, { recursive: true, force: true });
|
|
15
15
|
}));
|
|
16
16
|
});
|
|
17
|
-
describe(
|
|
18
|
-
it(
|
|
17
|
+
describe('files module', () => {
|
|
18
|
+
it('discovers governance artifacts with paths and line counts', async () => {
|
|
19
19
|
const root = await createTempDir();
|
|
20
|
-
await fs.writeFile(path.join(root,
|
|
21
|
-
await fs.writeFile(path.join(root,
|
|
22
|
-
await fs.mkdir(path.join(root,
|
|
23
|
-
await fs.writeFile(path.join(root,
|
|
20
|
+
await fs.writeFile(path.join(root, 'README.md'), '# Readme\n', 'utf-8');
|
|
21
|
+
await fs.writeFile(path.join(root, 'tasks.md'), '# Tasks\n## TODO\n', 'utf-8');
|
|
22
|
+
await fs.mkdir(path.join(root, 'designs'), { recursive: true });
|
|
23
|
+
await fs.writeFile(path.join(root, 'designs', 'feature-design.md'), '# Design\n', 'utf-8');
|
|
24
24
|
const artifacts = await discoverGovernanceArtifacts(root);
|
|
25
|
-
const readme = artifacts.find((item) => item.name ===
|
|
26
|
-
const designs = artifacts.find((item) => item.name ===
|
|
25
|
+
const readme = artifacts.find((item) => item.name === 'README.md');
|
|
26
|
+
const designs = artifacts.find((item) => item.name === 'designs');
|
|
27
27
|
expect(readme?.exists).toBe(true);
|
|
28
28
|
expect(readme?.lineCount).toBe(2);
|
|
29
29
|
expect(designs?.exists).toBe(true);
|
|
30
|
-
expect(designs?.markdownFiles?.[0].path.endsWith(
|
|
30
|
+
expect(designs?.markdownFiles?.[0].path.endsWith('feature-design.md')).toBe(true);
|
|
31
31
|
});
|
|
32
32
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
7
|
-
export * from
|
|
8
|
-
export * from
|
|
9
|
-
export * from
|
|
10
|
-
export * from
|
|
1
|
+
export * from './errors.js';
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
export * from './utils.js';
|
|
4
|
+
export * from './markdown.js';
|
|
5
|
+
export * from './files.js';
|
|
6
|
+
export * from './response.js';
|
|
7
|
+
export * from './catch.js';
|
|
8
|
+
export * from './artifacts.js';
|
|
9
|
+
export * from './linter.js';
|
|
10
|
+
export * from './store.js';
|
|
@@ -1,39 +1,39 @@
|
|
|
1
1
|
export function renderLintSuggestions(suggestions) {
|
|
2
2
|
return suggestions.map((item) => {
|
|
3
|
-
const suffix = item.fixHint ? ` ${item.fixHint}` :
|
|
3
|
+
const suffix = item.fixHint ? ` ${item.fixHint}` : '';
|
|
4
4
|
return `- [${item.code}] ${item.message}${suffix}`;
|
|
5
5
|
});
|
|
6
6
|
}
|
|
7
7
|
export const TASK_LINT_CODES = {
|
|
8
|
-
DUPLICATE_ID:
|
|
9
|
-
IN_PROGRESS_OWNER_EMPTY:
|
|
10
|
-
DONE_LINKS_MISSING:
|
|
11
|
-
BLOCKED_SUMMARY_EMPTY:
|
|
12
|
-
UPDATED_AT_INVALID:
|
|
13
|
-
ROADMAP_REFS_EMPTY:
|
|
14
|
-
OUTSIDE_MARKER:
|
|
15
|
-
LINK_TARGET_MISSING:
|
|
16
|
-
LINK_PATH_FORMAT_INVALID:
|
|
17
|
-
HOOK_FILE_MISSING:
|
|
18
|
-
FILTER_EMPTY:
|
|
19
|
-
CONTEXT_HOOK_HEAD_MISSING:
|
|
20
|
-
CONTEXT_HOOK_FOOTER_MISSING:
|
|
8
|
+
DUPLICATE_ID: 'TASK_DUPLICATE_ID',
|
|
9
|
+
IN_PROGRESS_OWNER_EMPTY: 'TASK_IN_PROGRESS_OWNER_EMPTY',
|
|
10
|
+
DONE_LINKS_MISSING: 'TASK_DONE_LINKS_MISSING',
|
|
11
|
+
BLOCKED_SUMMARY_EMPTY: 'TASK_BLOCKED_SUMMARY_EMPTY',
|
|
12
|
+
UPDATED_AT_INVALID: 'TASK_UPDATED_AT_INVALID',
|
|
13
|
+
ROADMAP_REFS_EMPTY: 'TASK_ROADMAP_REFS_EMPTY',
|
|
14
|
+
OUTSIDE_MARKER: 'TASK_OUTSIDE_MARKER',
|
|
15
|
+
LINK_TARGET_MISSING: 'TASK_LINK_TARGET_MISSING',
|
|
16
|
+
LINK_PATH_FORMAT_INVALID: 'TASK_LINK_PATH_FORMAT_INVALID',
|
|
17
|
+
HOOK_FILE_MISSING: 'TASK_HOOK_FILE_MISSING',
|
|
18
|
+
FILTER_EMPTY: 'TASK_FILTER_EMPTY',
|
|
19
|
+
CONTEXT_HOOK_HEAD_MISSING: 'TASK_CONTEXT_HOOK_HEAD_MISSING',
|
|
20
|
+
CONTEXT_HOOK_FOOTER_MISSING: 'TASK_CONTEXT_HOOK_FOOTER_MISSING',
|
|
21
21
|
// Spec v1.1.0 - Blocker Categorization
|
|
22
|
-
BLOCKED_WITHOUT_BLOCKER:
|
|
23
|
-
BLOCKER_TYPE_INVALID:
|
|
24
|
-
BLOCKER_DESCRIPTION_EMPTY:
|
|
25
|
-
IN_PROGRESS_WITHOUT_SUBSTATE:
|
|
26
|
-
SUBSTATE_PHASE_INVALID:
|
|
27
|
-
SUBSTATE_CONFIDENCE_INVALID:
|
|
22
|
+
BLOCKED_WITHOUT_BLOCKER: 'TASK_BLOCKED_WITHOUT_BLOCKER',
|
|
23
|
+
BLOCKER_TYPE_INVALID: 'TASK_BLOCKER_TYPE_INVALID',
|
|
24
|
+
BLOCKER_DESCRIPTION_EMPTY: 'TASK_BLOCKER_DESCRIPTION_EMPTY',
|
|
25
|
+
IN_PROGRESS_WITHOUT_SUBSTATE: 'TASK_IN_PROGRESS_WITHOUT_SUBSTATE',
|
|
26
|
+
SUBSTATE_PHASE_INVALID: 'TASK_SUBSTATE_PHASE_INVALID',
|
|
27
|
+
SUBSTATE_CONFIDENCE_INVALID: 'TASK_SUBSTATE_CONFIDENCE_INVALID',
|
|
28
28
|
};
|
|
29
29
|
export const ROADMAP_LINT_CODES = {
|
|
30
|
-
IDS_EMPTY:
|
|
31
|
-
TASKS_EMPTY:
|
|
32
|
-
TASK_REFS_EMPTY:
|
|
33
|
-
UNKNOWN_REFS:
|
|
34
|
-
ZERO_LINKED_TASKS:
|
|
35
|
-
CONTEXT_RELATED_TASKS_EMPTY:
|
|
30
|
+
IDS_EMPTY: 'ROADMAP_IDS_EMPTY',
|
|
31
|
+
TASKS_EMPTY: 'ROADMAP_TASKS_EMPTY',
|
|
32
|
+
TASK_REFS_EMPTY: 'ROADMAP_TASK_REFS_EMPTY',
|
|
33
|
+
UNKNOWN_REFS: 'ROADMAP_UNKNOWN_REFS',
|
|
34
|
+
ZERO_LINKED_TASKS: 'ROADMAP_ZERO_LINKED_TASKS',
|
|
35
|
+
CONTEXT_RELATED_TASKS_EMPTY: 'ROADMAP_CONTEXT_RELATED_TASKS_EMPTY',
|
|
36
36
|
};
|
|
37
37
|
export const PROJECT_LINT_CODES = {
|
|
38
|
-
TASKS_FILE_MISSING:
|
|
38
|
+
TASKS_FILE_MISSING: 'PROJECT_TASKS_FILE_MISSING',
|
|
39
39
|
};
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { describe, expect, it } from
|
|
2
|
-
import { renderLintSuggestions } from
|
|
3
|
-
describe(
|
|
4
|
-
it(
|
|
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
5
|
const lines = renderLintSuggestions([
|
|
6
|
-
{ code:
|
|
6
|
+
{ code: 'TASK_001', message: 'Example lint' },
|
|
7
7
|
]);
|
|
8
|
-
expect(lines).toEqual([
|
|
8
|
+
expect(lines).toEqual(['- [TASK_001] Example lint']);
|
|
9
9
|
});
|
|
10
|
-
it(
|
|
10
|
+
it('appends fixHint when provided', () => {
|
|
11
11
|
const lines = renderLintSuggestions([
|
|
12
|
-
{ code:
|
|
12
|
+
{ code: 'TASK_002', message: 'Missing field.', fixHint: 'Set owner.' },
|
|
13
13
|
]);
|
|
14
|
-
expect(lines).toEqual([
|
|
14
|
+
expect(lines).toEqual(['- [TASK_002] Missing field. Set owner.']);
|
|
15
15
|
});
|
|
16
16
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import fs from
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
2
|
export async function readMarkdownSections(filePath) {
|
|
3
|
-
const content = await fs.readFile(filePath,
|
|
3
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
4
4
|
const lines = content.split(/\r?\n/);
|
|
5
5
|
const headers = [];
|
|
6
6
|
lines.forEach((line, index) => {
|
|
@@ -21,7 +21,7 @@ export async function readMarkdownSections(filePath) {
|
|
|
21
21
|
return { filePath, lineCount: lines.length, sections };
|
|
22
22
|
}
|
|
23
23
|
export async function findTextReferences(filePath, needle) {
|
|
24
|
-
const content = await fs.readFile(filePath,
|
|
24
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
25
25
|
const lines = content.split(/\r?\n/);
|
|
26
26
|
const result = [];
|
|
27
27
|
lines.forEach((line, index) => {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import os from
|
|
3
|
-
import path from
|
|
4
|
-
import { afterEach, describe, expect, it } from
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
5
|
import { findTextReferences, readMarkdownSections } from './markdown.js';
|
|
6
6
|
const tempPaths = [];
|
|
7
7
|
async function createTempDir() {
|
|
8
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(),
|
|
8
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-mcp-test-'));
|
|
9
9
|
tempPaths.push(dir);
|
|
10
10
|
return dir;
|
|
11
11
|
}
|
|
@@ -14,22 +14,22 @@ afterEach(async () => {
|
|
|
14
14
|
await fs.rm(dir, { recursive: true, force: true });
|
|
15
15
|
}));
|
|
16
16
|
});
|
|
17
|
-
describe(
|
|
18
|
-
it(
|
|
17
|
+
describe('markdown module', () => {
|
|
18
|
+
it('locates markdown sections with line ranges', async () => {
|
|
19
19
|
const root = await createTempDir();
|
|
20
|
-
const file = path.join(root,
|
|
21
|
-
await fs.writeFile(file, [
|
|
20
|
+
const file = path.join(root, 'tasks.md');
|
|
21
|
+
await fs.writeFile(file, ['# Tasks', '', '## TODO', '- TASK-0001', '## DONE', '- TASK-0002'].join('\n'), 'utf-8');
|
|
22
22
|
const located = await readMarkdownSections(file);
|
|
23
23
|
expect(located.lineCount).toBe(6);
|
|
24
|
-
expect(located.sections[0].heading).toBe(
|
|
25
|
-
expect(located.sections[1].heading).toBe(
|
|
24
|
+
expect(located.sections[0].heading).toBe('Tasks');
|
|
25
|
+
expect(located.sections[1].heading).toBe('TODO');
|
|
26
26
|
expect(located.sections[1].startLine).toBe(3);
|
|
27
27
|
});
|
|
28
|
-
it(
|
|
28
|
+
it('finds ID references with exact line number', async () => {
|
|
29
29
|
const root = await createTempDir();
|
|
30
|
-
const file = path.join(root,
|
|
31
|
-
await fs.writeFile(file, [
|
|
32
|
-
const refs = await findTextReferences(file,
|
|
30
|
+
const file = path.join(root, 'reports.md');
|
|
31
|
+
await fs.writeFile(file, ['Task: TASK-0001', 'Roadmap: ROADMAP-0001'].join('\n'), 'utf-8');
|
|
32
|
+
const refs = await findTextReferences(file, 'TASK-0001');
|
|
33
33
|
expect(refs).toHaveLength(1);
|
|
34
34
|
expect(refs[0].line).toBe(1);
|
|
35
35
|
});
|