@projitive/mcp 1.2.0 → 2.0.1
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 +114 -423
- 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/linter.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/resources/readme.test.js +2 -2
- package/output/source/tools/project.js +206 -111
- package/output/source/tools/project.test.js +6 -3
- package/output/source/tools/roadmap.js +166 -16
- package/output/source/tools/roadmap.test.js +19 -55
- package/output/source/tools/task.js +206 -374
- package/output/source/tools/task.test.js +84 -388
- package/output/source/types.js +1 -9
- package/package.json +4 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import { collectTaskLintSuggestions, isValidTaskId, normalizeTask, rankActionableTaskCandidates, resolveNoTaskDiscoveryGuidance, renderTaskSeedTemplate, renderTasksMarkdown, loadTasksDocument, saveTasks, taskPriority, toTaskUpdatedAtMs, validateTransition, } from "./task.js";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
@@ -12,7 +12,6 @@ function buildCandidate(partial) {
|
|
|
12
12
|
});
|
|
13
13
|
return {
|
|
14
14
|
governanceDir: partial.governanceDir ?? "/workspace/a",
|
|
15
|
-
tasksPath: partial.tasksPath ?? "/workspace/a/tasks.md",
|
|
16
15
|
task,
|
|
17
16
|
projectScore: partial.projectScore ?? 1,
|
|
18
17
|
projectLatestUpdatedAt: partial.projectLatestUpdatedAt ?? "2026-01-01T00:00:00.000Z",
|
|
@@ -21,87 +20,6 @@ function buildCandidate(partial) {
|
|
|
21
20
|
};
|
|
22
21
|
}
|
|
23
22
|
describe("tasks module", () => {
|
|
24
|
-
it("parses markdown task block and normalizes task fields", async () => {
|
|
25
|
-
const markdown = [
|
|
26
|
-
"# Tasks",
|
|
27
|
-
TASKS_START,
|
|
28
|
-
"## TASK-0001 | TODO | hello",
|
|
29
|
-
"- owner: alice",
|
|
30
|
-
"- summary: first task",
|
|
31
|
-
"- updatedAt: 2026-02-17T00:00:00.000Z",
|
|
32
|
-
"- roadmapRefs: ROADMAP-0001",
|
|
33
|
-
"- links:",
|
|
34
|
-
" - ./designs/example.md",
|
|
35
|
-
TASKS_END,
|
|
36
|
-
].join("\n");
|
|
37
|
-
const tasks = await parseTasksBlock(markdown);
|
|
38
|
-
expect(tasks).toHaveLength(1);
|
|
39
|
-
expect(tasks[0].id).toBe("TASK-0001");
|
|
40
|
-
expect(tasks[0].status).toBe("TODO");
|
|
41
|
-
expect(tasks[0].roadmapRefs).toEqual(["ROADMAP-0001"]);
|
|
42
|
-
expect(tasks[0].links).toEqual(["./designs/example.md"]);
|
|
43
|
-
});
|
|
44
|
-
it("renders markdown containing markers", () => {
|
|
45
|
-
const task = normalizeTask({ id: "TASK-0002", title: "render", status: "IN_PROGRESS" });
|
|
46
|
-
const markdown = renderTasksMarkdown([task]);
|
|
47
|
-
expect(markdown.includes(TASKS_START)).toBe(true);
|
|
48
|
-
expect(markdown.includes(TASKS_END)).toBe(true);
|
|
49
|
-
expect(markdown.includes("## TASK-0002 | IN_PROGRESS | render")).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
it("parses task with subState metadata (Spec v1.1.0)", async () => {
|
|
52
|
-
const markdown = [
|
|
53
|
-
"# Tasks",
|
|
54
|
-
TASKS_START,
|
|
55
|
-
"## TASK-0003 | IN_PROGRESS | feature with substate",
|
|
56
|
-
"- owner: bob",
|
|
57
|
-
"- summary: implementing feature",
|
|
58
|
-
"- updatedAt: 2026-02-20T00:00:00.000Z",
|
|
59
|
-
"- roadmapRefs: ROADMAP-0001",
|
|
60
|
-
"- links:",
|
|
61
|
-
" - ./designs/feature.md",
|
|
62
|
-
"- subState:",
|
|
63
|
-
" - phase: implementation",
|
|
64
|
-
" - confidence: 0.85",
|
|
65
|
-
" - estimatedCompletion: 2026-02-25T15:00:00Z",
|
|
66
|
-
TASKS_END,
|
|
67
|
-
].join("\n");
|
|
68
|
-
const tasks = await parseTasksBlock(markdown);
|
|
69
|
-
expect(tasks).toHaveLength(1);
|
|
70
|
-
expect(tasks[0].id).toBe("TASK-0003");
|
|
71
|
-
expect(tasks[0].subState).toBeDefined();
|
|
72
|
-
expect(tasks[0].subState?.phase).toBe("implementation");
|
|
73
|
-
expect(tasks[0].subState?.confidence).toBe(0.85);
|
|
74
|
-
expect(tasks[0].subState?.estimatedCompletion).toBe("2026-02-25T15:00:00Z");
|
|
75
|
-
});
|
|
76
|
-
it("parses task with blocker metadata (Spec v1.1.0)", async () => {
|
|
77
|
-
const markdown = [
|
|
78
|
-
"# Tasks",
|
|
79
|
-
TASKS_START,
|
|
80
|
-
"## TASK-0004 | BLOCKED | waiting for api",
|
|
81
|
-
"- owner: charlie",
|
|
82
|
-
"- summary: Waiting for payment API v2.0",
|
|
83
|
-
"- updatedAt: 2026-02-20T00:00:00.000Z",
|
|
84
|
-
"- roadmapRefs: ROADMAP-0001",
|
|
85
|
-
"- links:",
|
|
86
|
-
" - ./docs/api-waiting.md",
|
|
87
|
-
"- blocker:",
|
|
88
|
-
" - type: external_dependency",
|
|
89
|
-
" - description: Waiting for payment API v2.0",
|
|
90
|
-
" - blockingEntity: third-party/payment-provider",
|
|
91
|
-
" - unblockCondition: API v2.0 GA announced",
|
|
92
|
-
" - escalationPath: contact-pm-for-workaround",
|
|
93
|
-
TASKS_END,
|
|
94
|
-
].join("\n");
|
|
95
|
-
const tasks = await parseTasksBlock(markdown);
|
|
96
|
-
expect(tasks).toHaveLength(1);
|
|
97
|
-
expect(tasks[0].id).toBe("TASK-0004");
|
|
98
|
-
expect(tasks[0].blocker).toBeDefined();
|
|
99
|
-
expect(tasks[0].blocker?.type).toBe("external_dependency");
|
|
100
|
-
expect(tasks[0].blocker?.description).toBe("Waiting for payment API v2.0");
|
|
101
|
-
expect(tasks[0].blocker?.blockingEntity).toBe("third-party/payment-provider");
|
|
102
|
-
expect(tasks[0].blocker?.unblockCondition).toBe("API v2.0 GA announced");
|
|
103
|
-
expect(tasks[0].blocker?.escalationPath).toBe("contact-pm-for-workaround");
|
|
104
|
-
});
|
|
105
23
|
it("validates task IDs", () => {
|
|
106
24
|
expect(isValidTaskId("TASK-0001")).toBe(true);
|
|
107
25
|
expect(isValidTaskId("TASK-001")).toBe(false);
|
|
@@ -130,125 +48,12 @@ describe("tasks module", () => {
|
|
|
130
48
|
expect(ranked[1].task.id).toBe("TASK-0002");
|
|
131
49
|
expect(ranked[2].task.id).toBe("TASK-0001");
|
|
132
50
|
});
|
|
133
|
-
it("renders
|
|
134
|
-
const task = normalizeTask({
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
roadmapRefs: [],
|
|
140
|
-
});
|
|
141
|
-
const lint = collectTaskLintSuggestions([task]);
|
|
142
|
-
expect(lint.some((line) => line.startsWith("- [TASK_IN_PROGRESS_OWNER_EMPTY]"))).toBe(true);
|
|
143
|
-
expect(lint.some((line) => line.startsWith("- [TASK_ROADMAP_REFS_EMPTY]"))).toBe(true);
|
|
144
|
-
});
|
|
145
|
-
it("scopes outside-marker lint to provided task IDs", () => {
|
|
146
|
-
const tasks = [
|
|
147
|
-
normalizeTask({ id: "TASK-0001", title: "A", status: "TODO", roadmapRefs: ["ROADMAP-0001"] }),
|
|
148
|
-
normalizeTask({ id: "TASK-0002", title: "B", status: "TODO", roadmapRefs: ["ROADMAP-0001"] }),
|
|
149
|
-
];
|
|
150
|
-
const markdown = [
|
|
151
|
-
"# Tasks",
|
|
152
|
-
"TASK-0002 outside",
|
|
153
|
-
"TASK-0003 outside",
|
|
154
|
-
TASKS_START,
|
|
155
|
-
"## TASK-0001 | TODO | A",
|
|
156
|
-
"- owner: (none)",
|
|
157
|
-
"- summary: (none)",
|
|
158
|
-
"- updatedAt: 2026-02-18T00:00:00.000Z",
|
|
159
|
-
"- roadmapRefs: ROADMAP-0001",
|
|
160
|
-
"- links:",
|
|
161
|
-
" - (none)",
|
|
162
|
-
"## TASK-0002 | TODO | B",
|
|
163
|
-
"- owner: (none)",
|
|
164
|
-
"- summary: (none)",
|
|
165
|
-
"- updatedAt: 2026-02-18T00:00:00.000Z",
|
|
166
|
-
"- roadmapRefs: ROADMAP-0001",
|
|
167
|
-
"- links:",
|
|
168
|
-
" - (none)",
|
|
169
|
-
TASKS_END,
|
|
170
|
-
].join("\n");
|
|
171
|
-
const scoped = collectTaskLintSuggestions(tasks, markdown, new Set(["TASK-0001"]));
|
|
172
|
-
const scopedOutside = scoped.find((line) => line.includes("TASK IDs found outside marker block"));
|
|
173
|
-
expect(scopedOutside).toBeUndefined();
|
|
174
|
-
const all = collectTaskLintSuggestions(tasks, markdown);
|
|
175
|
-
const allOutside = all.find((line) => line.includes("TASK IDs found outside marker block"));
|
|
176
|
-
expect(allOutside).toContain("TASK-0002");
|
|
177
|
-
expect(allOutside).toContain("TASK-0003");
|
|
178
|
-
});
|
|
179
|
-
it("renders seed task template with provided roadmap ref", () => {
|
|
180
|
-
const lines = renderTaskSeedTemplate("ROADMAP-0099");
|
|
181
|
-
const markdown = lines.join("\n");
|
|
182
|
-
expect(markdown).toContain("## TASK-0001 | TODO | Define initial executable objective");
|
|
183
|
-
expect(markdown).toContain("- roadmapRefs: ROADMAP-0099");
|
|
184
|
-
expect(markdown).toContain("- links:");
|
|
185
|
-
expect(markdown).not.toContain("- hooks:");
|
|
186
|
-
});
|
|
187
|
-
it("uses default no-task guidance when hook file is absent", async () => {
|
|
188
|
-
const guidance = await resolveNoTaskDiscoveryGuidance("/path/that/does/not/exist");
|
|
189
|
-
expect(guidance.length).toBeGreaterThan(3);
|
|
190
|
-
expect(guidance.some((line) => line.includes("TODO/FIXME/HACK"))).toBe(true);
|
|
191
|
-
});
|
|
192
|
-
it("uses hook checklist when task_no_actionable hook exists", async () => {
|
|
193
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
|
|
194
|
-
const hooksDir = path.join(dir, "hooks");
|
|
195
|
-
await fs.mkdir(hooksDir, { recursive: true });
|
|
196
|
-
await fs.writeFile(path.join(hooksDir, "task_no_actionable.md"), [
|
|
197
|
-
"Objective:",
|
|
198
|
-
"- custom-item-1",
|
|
199
|
-
"- custom-item-2",
|
|
200
|
-
].join("\n"), "utf-8");
|
|
201
|
-
const guidance = await resolveNoTaskDiscoveryGuidance(dir);
|
|
202
|
-
expect(guidance).toContain("- custom-item-1");
|
|
203
|
-
expect(guidance).toContain("- custom-item-2");
|
|
204
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
205
|
-
});
|
|
206
|
-
// Additional tests for improved coverage
|
|
207
|
-
it("finds task IDs outside marker blocks", () => {
|
|
208
|
-
const markdown = [
|
|
209
|
-
"# Tasks",
|
|
210
|
-
"Reference to TASK-0001 outside",
|
|
211
|
-
"Another reference to TASK-0002",
|
|
212
|
-
TASKS_START,
|
|
213
|
-
"## TASK-0001 | TODO | A",
|
|
214
|
-
"- owner: (none)",
|
|
215
|
-
"- summary: (none)",
|
|
216
|
-
"- updatedAt: 2026-02-18T00:00:00.000Z",
|
|
217
|
-
"- roadmapRefs: ROADMAP-0001",
|
|
218
|
-
"- links:",
|
|
219
|
-
" - (none)",
|
|
220
|
-
TASKS_END,
|
|
221
|
-
"Postscript with TASK-0003",
|
|
222
|
-
].join("\n");
|
|
223
|
-
const ids = findTaskIdsOutsideMarkers(markdown);
|
|
224
|
-
expect(ids).toContain("TASK-0001");
|
|
225
|
-
expect(ids).toContain("TASK-0002");
|
|
226
|
-
expect(ids).toContain("TASK-0003");
|
|
227
|
-
expect(ids).toHaveLength(3);
|
|
228
|
-
});
|
|
229
|
-
it("normalizes task with optional fields", () => {
|
|
230
|
-
const task = normalizeTask({
|
|
231
|
-
id: "TASK-0001",
|
|
232
|
-
title: "Test Task",
|
|
233
|
-
status: "TODO",
|
|
234
|
-
subState: {
|
|
235
|
-
phase: "discovery",
|
|
236
|
-
confidence: 0.75,
|
|
237
|
-
estimatedCompletion: "2026-03-01T00:00:00Z"
|
|
238
|
-
},
|
|
239
|
-
blocker: {
|
|
240
|
-
type: "internal_dependency",
|
|
241
|
-
description: "Waiting for team review"
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
expect(task.id).toBe("TASK-0001");
|
|
245
|
-
expect(task.title).toBe("Test Task");
|
|
246
|
-
expect(task.status).toBe("TODO");
|
|
247
|
-
expect(task.subState).toBeDefined();
|
|
248
|
-
expect(task.subState?.phase).toBe("discovery");
|
|
249
|
-
expect(task.subState?.confidence).toBe(0.75);
|
|
250
|
-
expect(task.blocker).toBeDefined();
|
|
251
|
-
expect(task.blocker?.type).toBe("internal_dependency");
|
|
51
|
+
it("renders task markdown without legacy markers", () => {
|
|
52
|
+
const task = normalizeTask({ id: "TASK-0002", title: "render", status: "IN_PROGRESS" });
|
|
53
|
+
const markdown = renderTasksMarkdown([task]);
|
|
54
|
+
expect(markdown.includes("PROJITIVE:TASKS:START")).toBe(false);
|
|
55
|
+
expect(markdown.includes("PROJITIVE:TASKS:END")).toBe(false);
|
|
56
|
+
expect(markdown.includes("## TASK-0002 | IN_PROGRESS | render")).toBe(true);
|
|
252
57
|
});
|
|
253
58
|
it("renders tasks with subState and blocker metadata", () => {
|
|
254
59
|
const task1 = normalizeTask({
|
|
@@ -257,8 +62,8 @@ describe("tasks module", () => {
|
|
|
257
62
|
status: "IN_PROGRESS",
|
|
258
63
|
subState: {
|
|
259
64
|
phase: "implementation",
|
|
260
|
-
confidence: 0.85
|
|
261
|
-
}
|
|
65
|
+
confidence: 0.85,
|
|
66
|
+
},
|
|
262
67
|
});
|
|
263
68
|
const task2 = normalizeTask({
|
|
264
69
|
id: "TASK-0002",
|
|
@@ -266,8 +71,8 @@ describe("tasks module", () => {
|
|
|
266
71
|
status: "BLOCKED",
|
|
267
72
|
blocker: {
|
|
268
73
|
type: "external_dependency",
|
|
269
|
-
description: "Waiting for API"
|
|
270
|
-
}
|
|
74
|
+
description: "Waiting for API",
|
|
75
|
+
},
|
|
271
76
|
});
|
|
272
77
|
const markdown = renderTasksMarkdown([task1, task2]);
|
|
273
78
|
expect(markdown).toContain("phase: implementation");
|
|
@@ -275,199 +80,90 @@ describe("tasks module", () => {
|
|
|
275
80
|
expect(markdown).toContain("type: external_dependency");
|
|
276
81
|
expect(markdown).toContain("description: Waiting for API");
|
|
277
82
|
});
|
|
278
|
-
it("
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const markdown = [
|
|
290
|
-
"# Tasks",
|
|
291
|
-
"## TASK-0001 | TODO | No Markers",
|
|
292
|
-
"- owner: alice",
|
|
293
|
-
].join("\n");
|
|
294
|
-
const tasks = await parseTasksBlock(markdown);
|
|
295
|
-
expect(tasks).toHaveLength(0);
|
|
296
|
-
});
|
|
297
|
-
it("collects lint suggestions for duplicate IDs", () => {
|
|
298
|
-
const tasks = [
|
|
299
|
-
normalizeTask({ id: "TASK-0001", title: "A", status: "TODO", roadmapRefs: ["ROADMAP-0001"] }),
|
|
300
|
-
normalizeTask({ id: "TASK-0001", title: "B", status: "TODO", roadmapRefs: ["ROADMAP-0001"] }),
|
|
301
|
-
];
|
|
302
|
-
const lint = collectTaskLintSuggestions(tasks);
|
|
303
|
-
expect(lint.some((line) => line.includes("Duplicate task IDs"))).toBe(true);
|
|
304
|
-
});
|
|
305
|
-
it("collects lint suggestions for DONE without links", () => {
|
|
306
|
-
const tasks = [
|
|
307
|
-
normalizeTask({ id: "TASK-0001", title: "Done Task", status: "DONE", roadmapRefs: ["ROADMAP-0001"], links: [] }),
|
|
308
|
-
];
|
|
309
|
-
const lint = collectTaskLintSuggestions(tasks);
|
|
310
|
-
expect(lint.some((line) => line.includes("DONE task(s) have no links evidence"))).toBe(true);
|
|
311
|
-
});
|
|
312
|
-
it("collects lint suggestions for BLOCKED without summary", () => {
|
|
313
|
-
const tasks = [
|
|
314
|
-
normalizeTask({ id: "TASK-0001", title: "Blocked", status: "BLOCKED", summary: "", roadmapRefs: ["ROADMAP-0001"] }),
|
|
315
|
-
];
|
|
316
|
-
const lint = collectTaskLintSuggestions(tasks);
|
|
317
|
-
expect(lint.some((line) => line.includes("BLOCKED task(s) have empty summary"))).toBe(true);
|
|
318
|
-
});
|
|
319
|
-
it("collects lint suggestions for invalid updatedAt", () => {
|
|
320
|
-
const tasks = [
|
|
321
|
-
normalizeTask({ id: "TASK-0001", title: "Invalid Date", status: "TODO", updatedAt: "not-a-date", roadmapRefs: ["ROADMAP-0001"] }),
|
|
322
|
-
];
|
|
323
|
-
const lint = collectTaskLintSuggestions(tasks);
|
|
324
|
-
expect(lint.some((line) => line.includes("invalid updatedAt format"))).toBe(true);
|
|
325
|
-
});
|
|
326
|
-
it("collects lint suggestions for Spec v1.1.0 blocker validation", () => {
|
|
327
|
-
const tasks = [
|
|
328
|
-
normalizeTask({
|
|
329
|
-
id: "TASK-0001",
|
|
330
|
-
title: "Blocked Without Metadata",
|
|
331
|
-
status: "BLOCKED",
|
|
332
|
-
summary: "Blocked but no metadata",
|
|
333
|
-
roadmapRefs: ["ROADMAP-0001"]
|
|
334
|
-
}),
|
|
335
|
-
];
|
|
336
|
-
const lint = collectTaskLintSuggestions(tasks);
|
|
337
|
-
expect(lint.some((line) => line.includes("BLOCKED task(s) have no blocker metadata"))).toBe(true);
|
|
338
|
-
});
|
|
339
|
-
it("collects lint suggestions for Spec v1.1.0 subState validation", () => {
|
|
340
|
-
const tasks = [
|
|
341
|
-
normalizeTask({
|
|
342
|
-
id: "TASK-0001",
|
|
343
|
-
title: "In Progress Without SubState",
|
|
344
|
-
status: "IN_PROGRESS",
|
|
345
|
-
owner: "ai-copilot",
|
|
346
|
-
roadmapRefs: ["ROADMAP-0001"]
|
|
347
|
-
}),
|
|
348
|
-
];
|
|
349
|
-
const lint = collectTaskLintSuggestions(tasks);
|
|
350
|
-
expect(lint.some((line) => line.includes("IN_PROGRESS task(s) have no subState metadata"))).toBe(true);
|
|
351
|
-
});
|
|
352
|
-
it("validates all status transitions correctly", () => {
|
|
353
|
-
// TODO transitions
|
|
354
|
-
expect(validateTransition("TODO", "TODO")).toBe(true);
|
|
355
|
-
expect(validateTransition("TODO", "IN_PROGRESS")).toBe(true);
|
|
356
|
-
expect(validateTransition("TODO", "BLOCKED")).toBe(true);
|
|
357
|
-
expect(validateTransition("TODO", "DONE")).toBe(false);
|
|
358
|
-
// IN_PROGRESS transitions
|
|
359
|
-
expect(validateTransition("IN_PROGRESS", "IN_PROGRESS")).toBe(true);
|
|
360
|
-
expect(validateTransition("IN_PROGRESS", "BLOCKED")).toBe(true);
|
|
361
|
-
expect(validateTransition("IN_PROGRESS", "DONE")).toBe(true);
|
|
362
|
-
expect(validateTransition("IN_PROGRESS", "TODO")).toBe(false);
|
|
363
|
-
// BLOCKED transitions
|
|
364
|
-
expect(validateTransition("BLOCKED", "BLOCKED")).toBe(true);
|
|
365
|
-
expect(validateTransition("BLOCKED", "IN_PROGRESS")).toBe(true);
|
|
366
|
-
expect(validateTransition("BLOCKED", "TODO")).toBe(true);
|
|
367
|
-
expect(validateTransition("BLOCKED", "DONE")).toBe(false);
|
|
368
|
-
// DONE transitions
|
|
369
|
-
expect(validateTransition("DONE", "DONE")).toBe(true);
|
|
370
|
-
expect(validateTransition("DONE", "TODO")).toBe(false);
|
|
371
|
-
expect(validateTransition("DONE", "IN_PROGRESS")).toBe(false);
|
|
372
|
-
expect(validateTransition("DONE", "BLOCKED")).toBe(false);
|
|
373
|
-
});
|
|
374
|
-
it("ranks candidates with same project score by task priority", () => {
|
|
375
|
-
const candidates = [
|
|
376
|
-
buildCandidate({ id: "TASK-0001", title: "TODO Task", status: "TODO", projectScore: 2 }),
|
|
377
|
-
buildCandidate({ id: "TASK-0002", title: "IN_PROGRESS Task", status: "IN_PROGRESS", projectScore: 2 }),
|
|
378
|
-
];
|
|
379
|
-
const ranked = rankActionableTaskCandidates(candidates);
|
|
380
|
-
expect(ranked[0].task.id).toBe("TASK-0002");
|
|
381
|
-
expect(ranked[1].task.id).toBe("TASK-0001");
|
|
83
|
+
it("collects lint lines with stable code prefix", () => {
|
|
84
|
+
const task = normalizeTask({
|
|
85
|
+
id: "TASK-0001",
|
|
86
|
+
title: "lint",
|
|
87
|
+
status: "IN_PROGRESS",
|
|
88
|
+
owner: "",
|
|
89
|
+
roadmapRefs: [],
|
|
90
|
+
});
|
|
91
|
+
const lint = collectTaskLintSuggestions([task]);
|
|
92
|
+
expect(lint.some((line) => line.startsWith("- [TASK_IN_PROGRESS_OWNER_EMPTY]"))).toBe(true);
|
|
93
|
+
expect(lint.some((line) => line.startsWith("- [TASK_ROADMAP_REFS_EMPTY]"))).toBe(true);
|
|
382
94
|
});
|
|
383
|
-
it("
|
|
384
|
-
const
|
|
95
|
+
it("collects blocker/substate lint rules", () => {
|
|
96
|
+
const blocked = normalizeTask({
|
|
385
97
|
id: "TASK-0001",
|
|
386
|
-
title: "
|
|
387
|
-
status: "
|
|
388
|
-
|
|
389
|
-
|
|
98
|
+
title: "Blocked",
|
|
99
|
+
status: "BLOCKED",
|
|
100
|
+
summary: "blocked reason",
|
|
101
|
+
roadmapRefs: ["ROADMAP-0001"],
|
|
390
102
|
});
|
|
391
|
-
const
|
|
103
|
+
const inProgress = normalizeTask({
|
|
392
104
|
id: "TASK-0002",
|
|
393
|
-
title: "
|
|
105
|
+
title: "In Progress",
|
|
106
|
+
status: "IN_PROGRESS",
|
|
107
|
+
owner: "ai-copilot",
|
|
108
|
+
roadmapRefs: ["ROADMAP-0001"],
|
|
109
|
+
});
|
|
110
|
+
const lint = collectTaskLintSuggestions([blocked, inProgress]);
|
|
111
|
+
expect(lint.some((line) => line.includes("BLOCKED_WITHOUT_BLOCKER"))).toBe(true);
|
|
112
|
+
expect(lint.some((line) => line.includes("IN_PROGRESS_WITHOUT_SUBSTATE"))).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
it("normalizes links to project-root-relative format without leading slash", () => {
|
|
115
|
+
const task = normalizeTask({
|
|
116
|
+
id: "TASK-0003",
|
|
117
|
+
title: "link normalize",
|
|
394
118
|
status: "TODO",
|
|
395
|
-
|
|
396
|
-
taskUpdatedAtMs: toTaskUpdatedAtMs("2026-02-01T00:00:00.000Z")
|
|
119
|
+
links: ["/reports/a.md", "./designs/b.md", "reports/c.md", "https://example.com/evidence"],
|
|
397
120
|
});
|
|
398
|
-
|
|
399
|
-
expect(
|
|
400
|
-
expect(
|
|
121
|
+
expect(task.links).toContain("reports/a.md");
|
|
122
|
+
expect(task.links).toContain("designs/b.md");
|
|
123
|
+
expect(task.links).toContain("reports/c.md");
|
|
124
|
+
expect(task.links).toContain("https://example.com/evidence");
|
|
125
|
+
expect(task.links.some((item) => item.startsWith("/"))).toBe(false);
|
|
401
126
|
});
|
|
402
|
-
it("
|
|
127
|
+
it("lints invalid links path format", () => {
|
|
403
128
|
const task = normalizeTask({
|
|
404
|
-
id: "TASK-
|
|
405
|
-
title: "
|
|
406
|
-
status: "TODO"
|
|
129
|
+
id: "TASK-0004",
|
|
130
|
+
title: "invalid link",
|
|
131
|
+
status: "TODO",
|
|
132
|
+
links: ["../outside.md"],
|
|
133
|
+
roadmapRefs: ["ROADMAP-0001"],
|
|
407
134
|
});
|
|
408
|
-
const
|
|
409
|
-
expect(
|
|
410
|
-
expect(markdown).not.toContain("blocker:");
|
|
135
|
+
const lint = collectTaskLintSuggestions([task]);
|
|
136
|
+
expect(lint.some((line) => line.includes("TASK_LINK_PATH_FORMAT_INVALID"))).toBe(true);
|
|
411
137
|
});
|
|
412
|
-
it("
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
"## TASK-0001 | IN_PROGRESS | Invalid Phase",
|
|
417
|
-
"- owner: test",
|
|
418
|
-
"- summary: test",
|
|
419
|
-
"- updatedAt: 2026-02-22T00:00:00.000Z",
|
|
420
|
-
"- roadmapRefs: ROADMAP-0001",
|
|
421
|
-
"- links:",
|
|
422
|
-
" - (none)",
|
|
423
|
-
"- subState:",
|
|
424
|
-
" - phase: invalid_phase",
|
|
425
|
-
TASKS_END,
|
|
426
|
-
].join("\n");
|
|
427
|
-
const tasks = await parseTasksBlock(markdown);
|
|
428
|
-
expect(tasks).toHaveLength(1);
|
|
429
|
-
// Invalid phase should be ignored
|
|
430
|
-
expect(tasks[0].subState?.phase).toBeUndefined();
|
|
138
|
+
it("renders seed task template with provided roadmap ref", () => {
|
|
139
|
+
const lines = renderTaskSeedTemplate("ROADMAP-0099");
|
|
140
|
+
const markdown = lines.join("\n");
|
|
141
|
+
expect(markdown).toContain("- roadmapRefs: ROADMAP-0099");
|
|
431
142
|
});
|
|
432
|
-
it("
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
TASKS_START,
|
|
436
|
-
"## TASK-0001 | IN_PROGRESS | Invalid Confidence",
|
|
437
|
-
"- owner: test",
|
|
438
|
-
"- summary: test",
|
|
439
|
-
"- updatedAt: 2026-02-22T00:00:00.000Z",
|
|
440
|
-
"- roadmapRefs: ROADMAP-0001",
|
|
441
|
-
"- links:",
|
|
442
|
-
" - (none)",
|
|
443
|
-
"- subState:",
|
|
444
|
-
" - confidence: 2.5",
|
|
445
|
-
TASKS_END,
|
|
446
|
-
].join("\n");
|
|
447
|
-
const tasks = await parseTasksBlock(markdown);
|
|
448
|
-
expect(tasks).toHaveLength(1);
|
|
449
|
-
// Invalid confidence should be ignored
|
|
450
|
-
expect(tasks[0].subState?.confidence).toBeUndefined();
|
|
143
|
+
it("uses default no-task guidance when hook file is absent", async () => {
|
|
144
|
+
const guidance = await resolveNoTaskDiscoveryGuidance("/path/that/does/not/exist");
|
|
145
|
+
expect(guidance.length).toBeGreaterThan(3);
|
|
451
146
|
});
|
|
452
|
-
it("
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
"
|
|
465
|
-
"
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
expect(tasks).
|
|
470
|
-
|
|
471
|
-
expect(
|
|
147
|
+
it("returns same default no-task guidance regardless of path", async () => {
|
|
148
|
+
const guidanceA = await resolveNoTaskDiscoveryGuidance("/path/that/does/not/exist");
|
|
149
|
+
const guidanceB = await resolveNoTaskDiscoveryGuidance("/another/path");
|
|
150
|
+
expect(guidanceA).toEqual(guidanceB);
|
|
151
|
+
});
|
|
152
|
+
it("loads and saves tasks from sqlite and keeps newest-first order", async () => {
|
|
153
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-task-"));
|
|
154
|
+
const governanceDir = path.join(root, ".projitive");
|
|
155
|
+
await fs.mkdir(governanceDir, { recursive: true });
|
|
156
|
+
await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
|
|
157
|
+
const tasksPath = path.join(governanceDir, ".projitive");
|
|
158
|
+
await saveTasks(tasksPath, [
|
|
159
|
+
normalizeTask({ id: "TASK-0001", title: "older", status: "TODO", updatedAt: "2026-01-01T00:00:00.000Z" }),
|
|
160
|
+
normalizeTask({ id: "TASK-0002", title: "newer", status: "TODO", updatedAt: "2026-02-01T00:00:00.000Z" }),
|
|
161
|
+
]);
|
|
162
|
+
const loaded = await loadTasksDocument(governanceDir);
|
|
163
|
+
expect(loaded.tasks[0].id).toBe("TASK-0002");
|
|
164
|
+
expect(loaded.tasks[1].id).toBe("TASK-0001");
|
|
165
|
+
const markdown = await fs.readFile(path.join(governanceDir, "tasks.md"), "utf-8");
|
|
166
|
+
expect(markdown).toContain("generated from .projitive sqlite tables");
|
|
167
|
+
await fs.rm(root, { recursive: true, force: true });
|
|
472
168
|
});
|
|
473
169
|
});
|
package/output/source/types.js
CHANGED
|
@@ -26,12 +26,6 @@ export const BLOCKER_TYPES = [
|
|
|
26
26
|
"resource",
|
|
27
27
|
"approval"
|
|
28
28
|
];
|
|
29
|
-
// ============================================================================
|
|
30
|
-
// Parser Constants
|
|
31
|
-
// ============================================================================
|
|
32
|
-
export const TASKS_START = "<!-- PROJITIVE:TASKS:START -->";
|
|
33
|
-
export const TASKS_END = "<!-- PROJITIVE:TASKS:END -->";
|
|
34
|
-
export const TASK_ID_REGEX = /^TASK-\d{4}$/;
|
|
35
29
|
export const TASK_LINT_CODES = {
|
|
36
30
|
DUPLICATE_ID: "TASK_DUPLICATE_ID",
|
|
37
31
|
IN_PROGRESS_OWNER_EMPTY: "TASK_IN_PROGRESS_OWNER_EMPTY",
|
|
@@ -42,9 +36,7 @@ export const TASK_LINT_CODES = {
|
|
|
42
36
|
OUTSIDE_MARKER: "TASK_OUTSIDE_MARKER",
|
|
43
37
|
FILTER_EMPTY: "TASK_FILTER_EMPTY",
|
|
44
38
|
LINK_TARGET_MISSING: "TASK_LINK_TARGET_MISSING",
|
|
45
|
-
|
|
46
|
-
CONTEXT_HOOK_HEAD_MISSING: "TASK_CONTEXT_HOOK_HEAD_MISSING",
|
|
47
|
-
CONTEXT_HOOK_FOOTER_MISSING: "TASK_CONTEXT_HOOK_FOOTER_MISSING",
|
|
39
|
+
LINK_PATH_FORMAT_INVALID: "TASK_LINK_PATH_FORMAT_INVALID",
|
|
48
40
|
// Spec v1.1.0 - Blocker Categorization
|
|
49
41
|
BLOCKED_WITHOUT_BLOCKER: "TASK_BLOCKED_WITHOUT_BLOCKER",
|
|
50
42
|
BLOCKER_TYPE_INVALID: "TASK_BLOCKER_TYPE_INVALID",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@projitive/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Projitive MCP Server for project and task discovery/update",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"author": "",
|
|
@@ -25,11 +25,14 @@
|
|
|
25
25
|
"output"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"@duckdb/node-api": "1.5.0-r.1",
|
|
28
29
|
"@modelcontextprotocol/sdk": "^1.17.5",
|
|
30
|
+
"sql.js": "^1.14.1",
|
|
29
31
|
"zod": "^3.23.8"
|
|
30
32
|
},
|
|
31
33
|
"devDependencies": {
|
|
32
34
|
"@types/node": "^24.3.0",
|
|
35
|
+
"@types/sql.js": "^1.4.9",
|
|
33
36
|
"@vitest/coverage-v8": "^3.2.4",
|
|
34
37
|
"tsx": "^4.20.5",
|
|
35
38
|
"typescript": "^5.9.2",
|