@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.
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { TASKS_END, TASKS_START, collectTaskLintSuggestions, isValidTaskId, normalizeTask, parseTasksBlock, rankActionableTaskCandidates, resolveNoTaskDiscoveryGuidance, renderTaskSeedTemplate, renderTasksMarkdown, taskPriority, toTaskUpdatedAtMs, validateTransition, findTaskIdsOutsideMarkers, } from "./task.js";
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 lint lines with stable code prefix", () => {
134
- const task = normalizeTask({
135
- id: "TASK-0001",
136
- title: "lint",
137
- status: "IN_PROGRESS",
138
- owner: "",
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("parses empty task block correctly", async () => {
279
- const markdown = [
280
- "# Tasks",
281
- TASKS_START,
282
- "(no tasks)",
283
- TASKS_END,
284
- ].join("\n");
285
- const tasks = await parseTasksBlock(markdown);
286
- expect(tasks).toHaveLength(0);
287
- });
288
- it("parses task block without markers returns empty", async () => {
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("ranks candidates with same project score and priority by recency", () => {
384
- const older = buildCandidate({
95
+ it("collects blocker/substate lint rules", () => {
96
+ const blocked = normalizeTask({
385
97
  id: "TASK-0001",
386
- title: "Older Task",
387
- status: "TODO",
388
- projectScore: 2,
389
- taskUpdatedAtMs: toTaskUpdatedAtMs("2026-01-01T00:00:00.000Z")
98
+ title: "Blocked",
99
+ status: "BLOCKED",
100
+ summary: "blocked reason",
101
+ roadmapRefs: ["ROADMAP-0001"],
390
102
  });
391
- const newer = buildCandidate({
103
+ const inProgress = normalizeTask({
392
104
  id: "TASK-0002",
393
- title: "Newer Task",
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
- projectScore: 2,
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
- const ranked = rankActionableTaskCandidates([older, newer]);
399
- expect(ranked[0].task.id).toBe("TASK-0002");
400
- expect(ranked[1].task.id).toBe("TASK-0001");
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("renders tasks without subState or blocker when not applicable", () => {
127
+ it("lints invalid links path format", () => {
403
128
  const task = normalizeTask({
404
- id: "TASK-0001",
405
- title: "Simple TODO Task",
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 markdown = renderTasksMarkdown([task]);
409
- expect(markdown).not.toContain("subState:");
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("parses task with invalid subState phase gracefully", async () => {
413
- const markdown = [
414
- "# Tasks",
415
- TASKS_START,
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("parses task with invalid confidence score gracefully", async () => {
433
- const markdown = [
434
- "# Tasks",
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("parses task with invalid blocker type gracefully", async () => {
453
- const markdown = [
454
- "# Tasks",
455
- TASKS_START,
456
- "## TASK-0001 | BLOCKED | Invalid Blocker Type",
457
- "- owner: test",
458
- "- summary: test",
459
- "- updatedAt: 2026-02-22T00:00:00.000Z",
460
- "- roadmapRefs: ROADMAP-0001",
461
- "- links:",
462
- " - (none)",
463
- "- blocker:",
464
- " - type: invalid_type",
465
- " - description: test",
466
- TASKS_END,
467
- ].join("\n");
468
- const tasks = await parseTasksBlock(markdown);
469
- expect(tasks).toHaveLength(1);
470
- // Invalid blocker type should use default
471
- expect(tasks[0].blocker?.type).toBe("external_dependency");
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
  });
@@ -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
- HOOK_FILE_MISSING: "TASK_HOOK_FILE_MISSING",
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": "1.2.0",
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",