@projitive/mcp 1.0.2 → 1.0.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.
Files changed (40) hide show
  1. package/README.md +44 -20
  2. package/output/hooks.js +1 -14
  3. package/output/hooks.test.js +7 -18
  4. package/output/index.js +23 -5
  5. package/output/package.json +36 -0
  6. package/output/projitive.js +21 -2
  7. package/output/projitive.test.js +1 -0
  8. package/output/source/designs.js +38 -0
  9. package/output/source/helpers/artifacts/artifacts.js +10 -0
  10. package/output/source/helpers/artifacts/artifacts.test.js +18 -0
  11. package/output/source/helpers/artifacts/index.js +1 -0
  12. package/output/source/helpers/catch/catch.js +48 -0
  13. package/output/source/helpers/catch/catch.test.js +43 -0
  14. package/output/source/helpers/catch/index.js +1 -0
  15. package/output/source/helpers/files/files.js +62 -0
  16. package/output/source/helpers/files/files.test.js +32 -0
  17. package/output/source/helpers/files/index.js +1 -0
  18. package/output/source/helpers/index.js +6 -0
  19. package/output/source/helpers/linter/codes.js +25 -0
  20. package/output/source/helpers/linter/index.js +2 -0
  21. package/output/source/helpers/linter/linter.js +6 -0
  22. package/output/source/helpers/linter/linter.test.js +16 -0
  23. package/output/source/helpers/markdown/index.js +1 -0
  24. package/output/source/helpers/markdown/markdown.js +33 -0
  25. package/output/source/helpers/markdown/markdown.test.js +36 -0
  26. package/output/source/helpers/response/index.js +1 -0
  27. package/output/source/helpers/response/response.js +73 -0
  28. package/output/source/helpers/response/response.test.js +50 -0
  29. package/output/source/index.js +215 -0
  30. package/output/source/projitive.js +488 -0
  31. package/output/source/projitive.test.js +75 -0
  32. package/output/source/readme.js +26 -0
  33. package/output/source/reports.js +36 -0
  34. package/output/source/roadmap.js +165 -0
  35. package/output/source/roadmap.test.js +11 -0
  36. package/output/source/tasks.js +762 -0
  37. package/output/source/tasks.test.js +152 -0
  38. package/output/tasks.js +100 -80
  39. package/output/tasks.test.js +32 -8
  40. package/package.json +1 -1
@@ -0,0 +1,152 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { TASKS_END, TASKS_START, collectTaskLintSuggestions, isValidTaskId, normalizeTask, parseTasksBlock, rankActionableTaskCandidates, resolveNoTaskDiscoveryGuidance, renderTaskSeedTemplate, renderTasksMarkdown, taskPriority, toTaskUpdatedAtMs, validateTransition, } from "./tasks.js";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ function buildCandidate(partial) {
7
+ const task = normalizeTask({
8
+ id: partial.id,
9
+ title: partial.title,
10
+ status: partial.status,
11
+ updatedAt: partial.task?.updatedAt ?? "2026-01-01T00:00:00.000Z",
12
+ });
13
+ return {
14
+ governanceDir: partial.governanceDir ?? "/workspace/a",
15
+ tasksPath: partial.tasksPath ?? "/workspace/a/tasks.md",
16
+ task,
17
+ projectScore: partial.projectScore ?? 1,
18
+ projectLatestUpdatedAt: partial.projectLatestUpdatedAt ?? "2026-01-01T00:00:00.000Z",
19
+ taskUpdatedAtMs: partial.taskUpdatedAtMs ?? toTaskUpdatedAtMs(task.updatedAt),
20
+ taskPriority: partial.taskPriority ?? taskPriority(task.status),
21
+ };
22
+ }
23
+ 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("validates task IDs", () => {
52
+ expect(isValidTaskId("TASK-0001")).toBe(true);
53
+ expect(isValidTaskId("TASK-001")).toBe(false);
54
+ });
55
+ it("allows and rejects expected transitions", () => {
56
+ expect(validateTransition("TODO", "IN_PROGRESS")).toBe(true);
57
+ expect(validateTransition("IN_PROGRESS", "DONE")).toBe(true);
58
+ expect(validateTransition("DONE", "IN_PROGRESS")).toBe(false);
59
+ });
60
+ it("assigns priority for actionable statuses", () => {
61
+ expect(taskPriority("IN_PROGRESS")).toBe(2);
62
+ expect(taskPriority("TODO")).toBe(1);
63
+ expect(taskPriority("BLOCKED")).toBe(0);
64
+ });
65
+ it("returns zero timestamp for invalid date", () => {
66
+ expect(toTaskUpdatedAtMs("invalid")).toBe(0);
67
+ });
68
+ it("ranks by project score, then task priority, then recency", () => {
69
+ const candidates = [
70
+ buildCandidate({ id: "TASK-0001", title: "A", status: "TODO", projectScore: 2 }),
71
+ buildCandidate({ id: "TASK-0002", title: "B", status: "IN_PROGRESS", projectScore: 2 }),
72
+ buildCandidate({ id: "TASK-0003", title: "C", status: "IN_PROGRESS", projectScore: 3 }),
73
+ ];
74
+ const ranked = rankActionableTaskCandidates(candidates);
75
+ expect(ranked[0].task.id).toBe("TASK-0003");
76
+ expect(ranked[1].task.id).toBe("TASK-0002");
77
+ expect(ranked[2].task.id).toBe("TASK-0001");
78
+ });
79
+ it("renders lint lines with stable code prefix", () => {
80
+ const task = normalizeTask({
81
+ id: "TASK-0001",
82
+ title: "lint",
83
+ status: "IN_PROGRESS",
84
+ owner: "",
85
+ roadmapRefs: [],
86
+ });
87
+ const lint = collectTaskLintSuggestions([task]);
88
+ expect(lint.some((line) => line.startsWith("- [TASK_IN_PROGRESS_OWNER_EMPTY]"))).toBe(true);
89
+ expect(lint.some((line) => line.startsWith("- [TASK_ROADMAP_REFS_EMPTY]"))).toBe(true);
90
+ });
91
+ it("scopes outside-marker lint to provided task IDs", () => {
92
+ const tasks = [
93
+ normalizeTask({ id: "TASK-0001", title: "A", status: "TODO", roadmapRefs: ["ROADMAP-0001"] }),
94
+ normalizeTask({ id: "TASK-0002", title: "B", status: "TODO", roadmapRefs: ["ROADMAP-0001"] }),
95
+ ];
96
+ const markdown = [
97
+ "# Tasks",
98
+ "TASK-0002 outside",
99
+ "TASK-0003 outside",
100
+ TASKS_START,
101
+ "## TASK-0001 | TODO | A",
102
+ "- owner: (none)",
103
+ "- summary: (none)",
104
+ "- updatedAt: 2026-02-18T00:00:00.000Z",
105
+ "- roadmapRefs: ROADMAP-0001",
106
+ "- links:",
107
+ " - (none)",
108
+ "## TASK-0002 | TODO | B",
109
+ "- owner: (none)",
110
+ "- summary: (none)",
111
+ "- updatedAt: 2026-02-18T00:00:00.000Z",
112
+ "- roadmapRefs: ROADMAP-0001",
113
+ "- links:",
114
+ " - (none)",
115
+ TASKS_END,
116
+ ].join("\n");
117
+ const scoped = collectTaskLintSuggestions(tasks, markdown, new Set(["TASK-0001"]));
118
+ const scopedOutside = scoped.find((line) => line.includes("TASK IDs found outside marker block"));
119
+ expect(scopedOutside).toBeUndefined();
120
+ const all = collectTaskLintSuggestions(tasks, markdown);
121
+ const allOutside = all.find((line) => line.includes("TASK IDs found outside marker block"));
122
+ expect(allOutside).toContain("TASK-0002");
123
+ expect(allOutside).toContain("TASK-0003");
124
+ });
125
+ it("renders seed task template with provided roadmap ref", () => {
126
+ const lines = renderTaskSeedTemplate("ROADMAP-0099");
127
+ const markdown = lines.join("\n");
128
+ expect(markdown).toContain("## TASK-0001 | TODO | Define initial executable objective");
129
+ expect(markdown).toContain("- roadmapRefs: ROADMAP-0099");
130
+ expect(markdown).toContain("- links:");
131
+ expect(markdown).not.toContain("- hooks:");
132
+ });
133
+ it("uses default no-task guidance when hook file is absent", async () => {
134
+ const guidance = await resolveNoTaskDiscoveryGuidance("/path/that/does/not/exist");
135
+ expect(guidance.length).toBeGreaterThan(3);
136
+ expect(guidance.some((line) => line.includes("TODO/FIXME/HACK"))).toBe(true);
137
+ });
138
+ it("uses hook checklist when task_no_actionable hook exists", async () => {
139
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
140
+ const hooksDir = path.join(dir, "hooks");
141
+ await fs.mkdir(hooksDir, { recursive: true });
142
+ await fs.writeFile(path.join(hooksDir, "task_no_actionable.md"), [
143
+ "Objective:",
144
+ "- custom-item-1",
145
+ "- custom-item-2",
146
+ ].join("\n"), "utf-8");
147
+ const guidance = await resolveNoTaskDiscoveryGuidance(dir);
148
+ expect(guidance).toContain("- custom-item-1");
149
+ expect(guidance).toContain("- custom-item-2");
150
+ await fs.rm(dir, { recursive: true, force: true });
151
+ });
152
+ });
package/output/tasks.js CHANGED
@@ -48,11 +48,34 @@ async function readOptionalMarkdown(filePath) {
48
48
  const trimmed = content.trim();
49
49
  return trimmed.length > 0 ? trimmed : undefined;
50
50
  }
51
- async function readTaskContextHooks(governanceDir) {
52
- const headPath = path.join(governanceDir, "hooks", "task_get_head.md");
53
- const footerPath = path.join(governanceDir, "hooks", "task_get_footer.md");
54
- const [head, footer] = await Promise.all([readOptionalMarkdown(headPath), readOptionalMarkdown(footerPath)]);
55
- return { head, footer, headPath, footerPath };
51
+ const NO_TASK_DISCOVERY_HOOK_FILE = "task_no_actionable.md";
52
+ const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
53
+ "- Check whether current code violates project guide/spec conventions; create TODO tasks for each actionable gap.",
54
+ "- Check unit/integration test coverage and identify high-value missing tests; create TODO tasks for meaningful coverage improvements.",
55
+ "- Check development/testing workflow for bottlenecks (slow feedback, fragile scripts, unclear runbooks); create TODO tasks to improve reliability.",
56
+ "- Scan for TODO/FIXME/HACK comments and convert feasible items into governed TODO tasks with evidence links.",
57
+ "- Check dependency freshness and security advisories; create tasks for safe upgrades when needed.",
58
+ "- Check repeated manual operations that can be automated (lint/test/release checks); create tasks to reduce operational toil.",
59
+ ];
60
+ function parseHookChecklist(markdown) {
61
+ return markdown
62
+ .split(/\r?\n/)
63
+ .map((line) => line.trim())
64
+ .filter((line) => line.length > 0)
65
+ .filter((line) => /^[-*+]\s+/.test(line))
66
+ .map((line) => (line.startsWith("*") ? `-${line.slice(1)}` : line));
67
+ }
68
+ export async function resolveNoTaskDiscoveryGuidance(governanceDir) {
69
+ if (!governanceDir) {
70
+ return DEFAULT_NO_TASK_DISCOVERY_GUIDANCE;
71
+ }
72
+ const hookPath = path.join(governanceDir, "hooks", NO_TASK_DISCOVERY_HOOK_FILE);
73
+ const markdown = await readOptionalMarkdown(hookPath);
74
+ if (typeof markdown !== "string") {
75
+ return DEFAULT_NO_TASK_DISCOVERY_GUIDANCE;
76
+ }
77
+ const checklist = parseHookChecklist(markdown);
78
+ return checklist.length > 0 ? checklist : DEFAULT_NO_TASK_DISCOVERY_GUIDANCE;
56
79
  }
57
80
  function latestTaskUpdatedAt(tasks) {
58
81
  const timestamps = tasks
@@ -67,6 +90,31 @@ function actionableScore(tasks) {
67
90
  return tasks.filter((task) => task.status === "IN_PROGRESS").length * 2
68
91
  + tasks.filter((task) => task.status === "TODO").length;
69
92
  }
93
+ async function readRoadmapIds(governanceDir) {
94
+ const roadmapPath = path.join(governanceDir, "roadmap.md");
95
+ try {
96
+ const markdown = await fs.readFile(roadmapPath, "utf-8");
97
+ const matches = markdown.match(/ROADMAP-\d{4}/g) ?? [];
98
+ return Array.from(new Set(matches));
99
+ }
100
+ catch {
101
+ return [];
102
+ }
103
+ }
104
+ export function renderTaskSeedTemplate(roadmapRef) {
105
+ return [
106
+ "```markdown",
107
+ "## TASK-0001 | TODO | Define initial executable objective",
108
+ "- owner: ai-copilot",
109
+ "- summary: Convert one roadmap milestone or report gap into an actionable task.",
110
+ "- updatedAt: 2026-01-01T00:00:00.000Z",
111
+ `- roadmapRefs: ${roadmapRef}`,
112
+ "- links:",
113
+ " - ./README.md",
114
+ " - ./roadmap.md",
115
+ "```",
116
+ ];
117
+ }
70
118
  async function readActionableTaskCandidates(governanceDirs) {
71
119
  const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
72
120
  const snapshot = await loadTasks(governanceDir);
@@ -130,14 +178,6 @@ export function normalizeTask(task) {
130
178
  const normalizedRoadmapRefs = Array.isArray(task.roadmapRefs)
131
179
  ? task.roadmapRefs.map(String).filter((value) => isValidRoadmapId(value))
132
180
  : [];
133
- const inputHooks = task.hooks ?? {};
134
- const normalizedHooks = {};
135
- for (const key of ["onAssigned", "onCompleted", "onBlocked", "onReopened"]) {
136
- const value = inputHooks[key];
137
- if (typeof value === "string" && value.trim().length > 0) {
138
- normalizedHooks[key] = value;
139
- }
140
- }
141
181
  return {
142
182
  id: String(task.id),
143
183
  title: String(task.title),
@@ -147,7 +187,6 @@ export function normalizeTask(task) {
147
187
  updatedAt: task.updatedAt ? String(task.updatedAt) : nowIso(),
148
188
  links: Array.isArray(task.links) ? task.links.map(String) : [],
149
189
  roadmapRefs: Array.from(new Set(normalizedRoadmapRefs)),
150
- hooks: normalizedHooks,
151
190
  };
152
191
  }
153
192
  export async function parseTasksBlock(markdown) {
@@ -182,7 +221,6 @@ export async function parseTasksBlock(markdown) {
182
221
  updatedAt: nowIso(),
183
222
  links: [],
184
223
  roadmapRefs: [],
185
- hooks: {},
186
224
  };
187
225
  let inLinks = false;
188
226
  let inHooks = false;
@@ -245,14 +283,7 @@ export async function parseTasksBlock(markdown) {
245
283
  continue;
246
284
  }
247
285
  if (inHooks) {
248
- const hookMatch = nestedValue.match(/^(onAssigned|onCompleted|onBlocked|onReopened):\s+(.+)$/);
249
- if (hookMatch) {
250
- const [, hookKey, hookPath] = hookMatch;
251
- taskDraft.hooks = {
252
- ...(taskDraft.hooks ?? {}),
253
- [hookKey]: hookPath.trim(),
254
- };
255
- }
286
+ continue;
256
287
  }
257
288
  }
258
289
  tasks.push(normalizeTask(taskDraft));
@@ -399,18 +430,6 @@ async function collectTaskFileLintSuggestions(governanceDir, task) {
399
430
  });
400
431
  }
401
432
  }
402
- const hookEntries = Object.entries(task.hooks)
403
- .filter(([, value]) => typeof value === "string" && value.trim().length > 0);
404
- for (const [hookKey, hookPath] of hookEntries) {
405
- const resolvedPath = path.resolve(governanceDir, hookPath);
406
- const exists = await fs.access(resolvedPath).then(() => true).catch(() => false);
407
- if (!exists) {
408
- suggestions.push({
409
- code: TASK_LINT_CODES.HOOK_FILE_MISSING,
410
- message: `Hook file not found for ${hookKey}: ${hookPath} (resolved: ${resolvedPath}).`,
411
- });
412
- }
413
- }
414
433
  return renderLintSuggestions(suggestions);
415
434
  }
416
435
  export function renderTasksMarkdown(tasks) {
@@ -419,12 +438,6 @@ export function renderTasksMarkdown(tasks) {
419
438
  const links = task.links.length > 0
420
439
  ? ["- links:", ...task.links.map((link) => ` - ${link}`)]
421
440
  : ["- links:", " - (none)"];
422
- const hookEntries = Object.entries(task.hooks)
423
- .filter(([, value]) => typeof value === "string" && value.trim().length > 0)
424
- .map(([key, value]) => ` - ${key}: ${value}`);
425
- const hooks = hookEntries.length > 0
426
- ? ["- hooks:", ...hookEntries]
427
- : ["- hooks:", " - (none)"];
428
441
  return [
429
442
  `## ${task.id} | ${task.status} | ${task.title}`,
430
443
  `- owner: ${task.owner || "(none)"}`,
@@ -432,7 +445,6 @@ export function renderTasksMarkdown(tasks) {
432
445
  `- updatedAt: ${task.updatedAt}`,
433
446
  `- roadmapRefs: ${roadmapRefs}`,
434
447
  ...links,
435
- ...hooks,
436
448
  ].join("\n");
437
449
  });
438
450
  return [
@@ -543,6 +555,27 @@ export function registerTaskTools(server) {
543
555
  const projects = await discoverProjects(root, depth);
544
556
  const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
545
557
  if (rankedCandidates.length === 0) {
558
+ const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
559
+ const { tasksPath, tasks } = await loadTasks(governanceDir);
560
+ const roadmapIds = await readRoadmapIds(governanceDir);
561
+ const todo = tasks.filter((task) => task.status === "TODO").length;
562
+ const inProgress = tasks.filter((task) => task.status === "IN_PROGRESS").length;
563
+ const blocked = tasks.filter((task) => task.status === "BLOCKED").length;
564
+ const done = tasks.filter((task) => task.status === "DONE").length;
565
+ return {
566
+ governanceDir,
567
+ tasksPath,
568
+ roadmapIds,
569
+ total: tasks.length,
570
+ todo,
571
+ inProgress,
572
+ blocked,
573
+ done,
574
+ };
575
+ }));
576
+ const preferredProject = projectSnapshots[0];
577
+ const preferredRoadmapRef = preferredProject?.roadmapIds[0] ?? "ROADMAP-0001";
578
+ const noTaskDiscoveryGuidance = await resolveNoTaskDiscoveryGuidance(preferredProject?.governanceDir);
546
579
  const markdown = renderToolResponseMarkdown({
547
580
  toolName: "taskNext",
548
581
  sections: [
@@ -552,13 +585,33 @@ export function registerTaskTools(server) {
552
585
  `- matchedProjects: ${projects.length}`,
553
586
  "- actionableTasks: 0",
554
587
  ]),
555
- evidenceSection(["- candidates:", "- (none)"]),
588
+ evidenceSection([
589
+ "### Project Snapshots",
590
+ ...(projectSnapshots.length > 0
591
+ ? projectSnapshots.map((item, index) => `${index + 1}. ${item.governanceDir} | total=${item.total} | todo=${item.todo} | in_progress=${item.inProgress} | blocked=${item.blocked} | done=${item.done} | roadmapIds=${item.roadmapIds.join(", ") || "(none)"} | tasksPath=${item.tasksPath}`)
592
+ : ["- (none)"]),
593
+ "",
594
+ "### Seed Task Template",
595
+ ...renderTaskSeedTemplate(preferredRoadmapRef),
596
+ ]),
556
597
  guidanceSection([
557
598
  "- No TODO/IN_PROGRESS task is available.",
558
- "- Create or reopen tasks in tasks.md, then rerun `taskNext`.",
599
+ "- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.",
600
+ "",
601
+ "### No-Task Discovery Checklist",
602
+ ...noTaskDiscoveryGuidance,
603
+ "",
604
+ "- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.",
605
+ "- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.",
606
+ "- After adding tasks inside marker block, rerun `taskNext` to re-rank actionable work.",
607
+ ]),
608
+ lintSection([
609
+ "- No actionable tasks found. Verify task statuses and required fields in marker block.",
610
+ "- Ensure each new task has stable TASK-xxxx ID and at least one roadmapRefs item.",
559
611
  ]),
560
- lintSection(["- No actionable tasks found. Verify task statuses and required fields in marker block."]),
561
- nextCallSection(`projectNext(rootPath=\"${root}\", maxDepth=${depth})`),
612
+ nextCallSection(preferredProject
613
+ ? `projectContext(projectPath=\"${preferredProject.governanceDir}\")`
614
+ : `projectScan(rootPath=\"${root}\", maxDepth=${depth})`),
562
615
  ],
563
616
  });
564
617
  return asText(markdown);
@@ -641,7 +694,6 @@ export function registerTaskTools(server) {
641
694
  }
642
695
  const governanceDir = await resolveGovernanceDir(projectPath);
643
696
  const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
644
- const taskContextHooks = await readTaskContextHooks(governanceDir);
645
697
  const task = tasks.find((item) => item.id === taskId);
646
698
  if (!task) {
647
699
  return {
@@ -669,28 +721,6 @@ export function registerTaskTools(server) {
669
721
  const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
670
722
  const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
671
723
  const suggestedReadOrder = [tasksPath, ...relatedArtifacts.filter((item) => item !== tasksPath)];
672
- const hookPaths = Object.values(task.hooks)
673
- .filter((value) => typeof value === "string" && value.trim().length > 0)
674
- .map((value) => path.resolve(governanceDir, value));
675
- const hookStatus = `head=${taskContextHooks.head ? "loaded" : "missing"}, footer=${taskContextHooks.footer ? "loaded" : "missing"}`;
676
- if (!taskContextHooks.head) {
677
- appendLintSuggestions(lintSuggestions, [
678
- {
679
- code: TASK_LINT_CODES.CONTEXT_HOOK_HEAD_MISSING,
680
- message: `Missing ${taskContextHooks.headPath}.`,
681
- fixHint: "Add a task context head hook template if you need standardized preface.",
682
- },
683
- ]);
684
- }
685
- if (!taskContextHooks.footer) {
686
- appendLintSuggestions(lintSuggestions, [
687
- {
688
- code: TASK_LINT_CODES.CONTEXT_HOOK_FOOTER_MISSING,
689
- message: `Missing ${taskContextHooks.footerPath}.`,
690
- fixHint: "Add a task context footer hook template for standardized close-out checks.",
691
- },
692
- ]);
693
- }
694
724
  const coreMarkdown = renderToolResponseMarkdown({
695
725
  toolName: "taskContext",
696
726
  sections: [
@@ -703,7 +733,6 @@ export function registerTaskTools(server) {
703
733
  `- updatedAt: ${task.updatedAt}`,
704
734
  `- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
705
735
  `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : tasksPath}`,
706
- `- hookStatus: ${hookStatus}`,
707
736
  ]),
708
737
  evidenceSection([
709
738
  "### Related Artifacts",
@@ -714,9 +743,6 @@ export function registerTaskTools(server) {
714
743
  ? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
715
744
  : ["- (none)"]),
716
745
  "",
717
- "### Hook Paths",
718
- ...(hookPaths.length > 0 ? hookPaths.map((item) => `- ${item}`) : ["- (none)"]),
719
- "",
720
746
  "### Suggested Read Order",
721
747
  ...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
722
748
  ]),
@@ -731,12 +757,6 @@ export function registerTaskTools(server) {
731
757
  nextCallSection(`taskContext(projectPath=\"${governanceDir}\", taskId=\"${task.id}\")`),
732
758
  ],
733
759
  });
734
- const markdownParts = [
735
- taskContextHooks.head,
736
- coreMarkdown,
737
- taskContextHooks.footer,
738
- ].filter((value) => typeof value === "string" && value.trim().length > 0);
739
- const markdown = markdownParts.join("\n\n---\n\n");
740
- return asText(markdown);
760
+ return asText(coreMarkdown);
741
761
  });
742
762
  }
@@ -1,5 +1,8 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { TASKS_END, TASKS_START, collectTaskLintSuggestions, isValidTaskId, normalizeTask, parseTasksBlock, rankActionableTaskCandidates, renderTasksMarkdown, taskPriority, toTaskUpdatedAtMs, validateTransition, } from "./tasks.js";
2
+ import { TASKS_END, TASKS_START, collectTaskLintSuggestions, isValidTaskId, normalizeTask, parseTasksBlock, rankActionableTaskCandidates, resolveNoTaskDiscoveryGuidance, renderTaskSeedTemplate, renderTasksMarkdown, taskPriority, toTaskUpdatedAtMs, validateTransition, } from "./tasks.js";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
3
6
  function buildCandidate(partial) {
4
7
  const task = normalizeTask({
5
8
  id: partial.id,
@@ -29,8 +32,6 @@ describe("tasks module", () => {
29
32
  "- roadmapRefs: ROADMAP-0001",
30
33
  "- links:",
31
34
  " - ./designs/example.md",
32
- "- hooks:",
33
- " - onAssigned: ./hooks/on_task_assigned.md",
34
35
  TASKS_END,
35
36
  ].join("\n");
36
37
  const tasks = await parseTasksBlock(markdown);
@@ -38,7 +39,7 @@ describe("tasks module", () => {
38
39
  expect(tasks[0].id).toBe("TASK-0001");
39
40
  expect(tasks[0].status).toBe("TODO");
40
41
  expect(tasks[0].roadmapRefs).toEqual(["ROADMAP-0001"]);
41
- expect(tasks[0].hooks).toEqual({ onAssigned: "./hooks/on_task_assigned.md" });
42
+ expect(tasks[0].links).toEqual(["./designs/example.md"]);
42
43
  });
43
44
  it("renders markdown containing markers", () => {
44
45
  const task = normalizeTask({ id: "TASK-0002", title: "render", status: "IN_PROGRESS" });
@@ -104,8 +105,6 @@ describe("tasks module", () => {
104
105
  "- roadmapRefs: ROADMAP-0001",
105
106
  "- links:",
106
107
  " - (none)",
107
- "- hooks:",
108
- " - (none)",
109
108
  "## TASK-0002 | TODO | B",
110
109
  "- owner: (none)",
111
110
  "- summary: (none)",
@@ -113,8 +112,6 @@ describe("tasks module", () => {
113
112
  "- roadmapRefs: ROADMAP-0001",
114
113
  "- links:",
115
114
  " - (none)",
116
- "- hooks:",
117
- " - (none)",
118
115
  TASKS_END,
119
116
  ].join("\n");
120
117
  const scoped = collectTaskLintSuggestions(tasks, markdown, new Set(["TASK-0001"]));
@@ -125,4 +122,31 @@ describe("tasks module", () => {
125
122
  expect(allOutside).toContain("TASK-0002");
126
123
  expect(allOutside).toContain("TASK-0003");
127
124
  });
125
+ it("renders seed task template with provided roadmap ref", () => {
126
+ const lines = renderTaskSeedTemplate("ROADMAP-0099");
127
+ const markdown = lines.join("\n");
128
+ expect(markdown).toContain("## TASK-0001 | TODO | Define initial executable objective");
129
+ expect(markdown).toContain("- roadmapRefs: ROADMAP-0099");
130
+ expect(markdown).toContain("- links:");
131
+ expect(markdown).not.toContain("- hooks:");
132
+ });
133
+ it("uses default no-task guidance when hook file is absent", async () => {
134
+ const guidance = await resolveNoTaskDiscoveryGuidance("/path/that/does/not/exist");
135
+ expect(guidance.length).toBeGreaterThan(3);
136
+ expect(guidance.some((line) => line.includes("TODO/FIXME/HACK"))).toBe(true);
137
+ });
138
+ it("uses hook checklist when task_no_actionable hook exists", async () => {
139
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
140
+ const hooksDir = path.join(dir, "hooks");
141
+ await fs.mkdir(hooksDir, { recursive: true });
142
+ await fs.writeFile(path.join(hooksDir, "task_no_actionable.md"), [
143
+ "Objective:",
144
+ "- custom-item-1",
145
+ "- custom-item-2",
146
+ ].join("\n"), "utf-8");
147
+ const guidance = await resolveNoTaskDiscoveryGuidance(dir);
148
+ expect(guidance).toContain("- custom-item-1");
149
+ expect(guidance).toContain("- custom-item-2");
150
+ await fs.rm(dir, { recursive: true, force: true });
151
+ });
128
152
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projitive/mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Projitive MCP Server for project and task discovery/update",
5
5
  "license": "ISC",
6
6
  "author": "",