@projitive/mcp 1.0.7 → 1.1.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.
Files changed (56) hide show
  1. package/README.md +3 -3
  2. package/output/package.json +4 -1
  3. package/output/source/{helpers/catch → common}/catch.js +6 -6
  4. package/output/source/{helpers/catch → common}/catch.test.js +6 -6
  5. package/output/source/common/confidence.js +231 -0
  6. package/output/source/common/confidence.test.js +205 -0
  7. package/output/source/common/errors.js +120 -0
  8. package/output/source/{helpers/files → common}/files.js +1 -1
  9. package/output/source/{helpers/files → common}/files.test.js +1 -1
  10. package/output/source/common/index.js +10 -0
  11. package/output/source/{helpers/linter/codes.js → common/linter.js} +13 -0
  12. package/output/source/{helpers/markdown → common}/markdown.test.js +1 -1
  13. package/output/source/{helpers/response → common}/response.test.js +1 -1
  14. package/output/source/common/types.js +7 -0
  15. package/output/source/common/utils.js +39 -0
  16. package/output/source/design-context.js +51 -500
  17. package/output/source/index.js +8 -193
  18. package/output/source/index.test.js +116 -0
  19. package/output/source/prompts/index.js +9 -0
  20. package/output/source/prompts/quickStart.js +94 -0
  21. package/output/source/prompts/taskDiscovery.js +190 -0
  22. package/output/source/prompts/taskExecution.js +161 -0
  23. package/output/source/resources/designs.js +108 -0
  24. package/output/source/resources/designs.test.js +154 -0
  25. package/output/source/resources/governance.js +40 -0
  26. package/output/source/resources/index.js +6 -0
  27. package/output/source/resources/readme.test.js +167 -0
  28. package/output/source/{reports.js → resources/reports.js} +5 -3
  29. package/output/source/resources/reports.test.js +149 -0
  30. package/output/source/tools/index.js +8 -0
  31. package/output/source/{projitive.js → tools/project.js} +6 -9
  32. package/output/source/tools/project.test.js +322 -0
  33. package/output/source/{roadmap.js → tools/roadmap.js} +4 -7
  34. package/output/source/tools/roadmap.test.js +103 -0
  35. package/output/source/{tasks.js → tools/task.js} +581 -27
  36. package/output/source/tools/task.test.js +473 -0
  37. package/output/source/types.js +67 -0
  38. package/package.json +4 -1
  39. package/output/source/designs.js +0 -38
  40. package/output/source/helpers/artifacts/index.js +0 -1
  41. package/output/source/helpers/catch/index.js +0 -1
  42. package/output/source/helpers/files/index.js +0 -1
  43. package/output/source/helpers/index.js +0 -6
  44. package/output/source/helpers/linter/index.js +0 -2
  45. package/output/source/helpers/linter/linter.js +0 -6
  46. package/output/source/helpers/markdown/index.js +0 -1
  47. package/output/source/helpers/response/index.js +0 -1
  48. package/output/source/projitive.test.js +0 -111
  49. package/output/source/roadmap.test.js +0 -11
  50. package/output/source/tasks.test.js +0 -152
  51. /package/output/source/{helpers/artifacts → common}/artifacts.js +0 -0
  52. /package/output/source/{helpers/artifacts → common}/artifacts.test.js +0 -0
  53. /package/output/source/{helpers/linter → common}/linter.test.js +0 -0
  54. /package/output/source/{helpers/markdown → common}/markdown.js +0 -0
  55. /package/output/source/{helpers/response → common}/response.js +0 -0
  56. /package/output/source/{readme.js → resources/readme.js} +0 -0
@@ -0,0 +1,161 @@
1
+ import { z } from "zod";
2
+ function asUserPrompt(text) {
3
+ return {
4
+ messages: [
5
+ {
6
+ role: "user",
7
+ content: {
8
+ type: "text",
9
+ text,
10
+ },
11
+ },
12
+ ],
13
+ };
14
+ }
15
+ export function registerTaskExecutionPrompt(server) {
16
+ server.registerPrompt("taskExecution", {
17
+ title: "Task Execution",
18
+ description: "Primary execution prompt: select one task, execute, and verify evidence consistency",
19
+ argsSchema: {
20
+ projectPath: z.string().optional(),
21
+ taskId: z.string().optional(),
22
+ },
23
+ }, async ({ projectPath, taskId }) => {
24
+ const taskEntry = taskId && projectPath
25
+ ? `1) Run taskContext(projectPath="${projectPath}", taskId="${taskId}").`
26
+ : "1) Run taskNext().";
27
+ const text = [
28
+ "# Execute Task Workflow",
29
+ "",
30
+ "You are a Projitive task execution assistant. Here is the complete workflow:",
31
+ "",
32
+ "## Step 1: Get the Task",
33
+ "",
34
+ taskEntry,
35
+ "",
36
+ "## Step 2: Understand the Task",
37
+ "",
38
+ "After getting task context, read carefully:",
39
+ "",
40
+ "### Suggested Read Order",
41
+ "Read each file in order:",
42
+ "- Note content and location of each file",
43
+ "- Understand task background and motivation",
44
+ "- Identify task acceptance criteria",
45
+ "- Look for related design decisions",
46
+ "",
47
+ "### Evidence",
48
+ "- Candidate Files - Related file list",
49
+ "- Reference Locations - Where TASK/ROADMAP IDs appear",
50
+ "",
51
+ "### Lint Suggestions",
52
+ "Resolve any warnings before starting task execution.",
53
+ "",
54
+ "## Step 3: Execute the Task",
55
+ "",
56
+ "### Status Management",
57
+ "",
58
+ "| Current Status | Next Step | Description |",
59
+ "|----------------|-----------|-------------|",
60
+ "| TODO | \u2192 IN_PROGRESS | When starting execution |",
61
+ "| IN_PROGRESS | \u2192 DONE | When task is complete |",
62
+ "| IN_PROGRESS | \u2192 BLOCKED | When blocked |",
63
+ "| BLOCKED | \u2192 TODO | When blocker is resolved |",
64
+ "",
65
+ "### Execution Steps",
66
+ "",
67
+ "1. **Prepare (if status is TODO)",
68
+ " - Call `taskUpdate()` to change status to IN_PROGRESS",
69
+ " - Set owner (if empty)",
70
+ " - Fill subState (optional, Spec v1.1.0)",
71
+ "",
72
+ "2. **Execute task content**",
73
+ " - Only edit files under .projitive/",
74
+ " - tasks.md - Update task status and metadata",
75
+ " - designs/*.md - Add or update design documents",
76
+ " - reports/*.md - Create execution reports",
77
+ " - roadmap.md - Update roadmap (if needed)",
78
+ "",
79
+ "3. **Create execution report**",
80
+ " - Create new report file in reports/ directory",
81
+ " - Record what changes were made",
82
+ " - Record why changes were made",
83
+ " - Record verification results",
84
+ "",
85
+ "4. **Complete task (if status is IN_PROGRESS)",
86
+ " - Confirm all acceptance criteria are met",
87
+ " - Call `taskUpdate()` to change status to DONE",
88
+ " - Update updatedAt timestamp",
89
+ "",
90
+ "## Step 4: Verify Changes",
91
+ "",
92
+ "After execution:",
93
+ "",
94
+ "1. **Re-run taskContext()",
95
+ " - Confirm task status updated correctly",
96
+ " - Confirm updatedAt timestamp updated",
97
+ "",
98
+ "2. **Check reference consistency",
99
+ " - Confirm all TASK/ROADMAP IDs still valid",
100
+ " - Confirm no broken links in Reference Locations",
101
+ "",
102
+ "3. **Check Lint Suggestions",
103
+ " - Confirm no new warnings",
104
+ " - If there are warnings, resolve them first",
105
+ "",
106
+ "4. **Continue to next task (optional)",
107
+ " - Call `taskNext()` for next task",
108
+ " - Repeat above workflow",
109
+ "",
110
+ "## Special Cases",
111
+ "",
112
+ "### Case 1: Encountered a blocker",
113
+ "",
114
+ "If unable to continue task execution:",
115
+ "1. Call `taskUpdate()` to change status to BLOCKED",
116
+ "2. Fill blocker field (Spec v1.1.0):",
117
+ " - type: Blocker type (dependency/missing-info/technical-debt/other)",
118
+ " - description: Blocker description",
119
+ " - relatedLinks: Related links (optional)",
120
+ "3. Create a new TODO task to resolve blocker",
121
+ "",
122
+ "### Case 2: No actionable tasks",
123
+ "",
124
+ "If taskNext() returns empty:",
125
+ "1. Call `projectContext()` to recheck project state",
126
+ "2. Read design documents in designs/ directory",
127
+ "3. Create 1-3 new TODO tasks",
128
+ "",
129
+ "### Case 3: Need to initialize governance",
130
+ "",
131
+ "If .projitive directory does not exist:",
132
+ "1. Call `projectInit(projectPath=\"<project-dir>\")`",
133
+ "2. Then restart",
134
+ "",
135
+ "## Hard Rules (NEVER violate)",
136
+ "",
137
+ "1. **NEVER modify TASK/ROADMAP IDs**",
138
+ " - Keep them immutable once assigned",
139
+ " - If you need to reference, keep them as-is",
140
+ "",
141
+ "2. **Every status transition must have report evidence**",
142
+ " - TODO \u2192 IN_PROGRESS: Report not required",
143
+ " - IN_PROGRESS \u2192 DONE: Report REQUIRED",
144
+ " - IN_PROGRESS \u2192 BLOCKED: Report recommended to explain blocker",
145
+ "",
146
+ "3. **Only edit files under .projitive/**",
147
+ " - Unless task scope explicitly requires modifying other files",
148
+ " - Any non-governance file edits must be explained in task summary",
149
+ "",
150
+ "4. **Always verify after updates**",
151
+ " - After every taskUpdate() call",
152
+ " - Re-run taskContext()",
153
+ " - Confirm reference consistency",
154
+ "",
155
+ "5. **Keep updatedAt in sync**",
156
+ " - Every time you update a task",
157
+ " - Must update updatedAt to current time (ISO 8601 format)",
158
+ ].join("\n");
159
+ return asUserPrompt(text);
160
+ });
161
+ }
@@ -0,0 +1,108 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { isValidRoadmapId } from "../tools/roadmap.js";
4
+ import { isValidTaskId } from "../tools/task.js";
5
+ export function parseDesignMetadata(markdown) {
6
+ const lines = markdown.split(/\r?\n/);
7
+ const metadata = {};
8
+ for (const line of lines) {
9
+ // Remove markdown bold markers (**)
10
+ const cleanLine = line.replace(/\*\*/g, "");
11
+ const [rawKey, ...rawValue] = cleanLine.split(":");
12
+ if (!rawKey || rawValue.length === 0) {
13
+ continue;
14
+ }
15
+ const key = rawKey.trim().toLowerCase();
16
+ const value = rawValue.join(":").trim();
17
+ if (key === "task")
18
+ metadata.task = value;
19
+ if (key === "roadmap")
20
+ metadata.roadmap = value;
21
+ if (key === "owner")
22
+ metadata.owner = value;
23
+ if (key === "status")
24
+ metadata.status = value;
25
+ if (key === "last updated")
26
+ metadata.lastUpdated = value;
27
+ }
28
+ return metadata;
29
+ }
30
+ export function validateDesignMetadata(metadata) {
31
+ const errors = [];
32
+ if (!metadata.task) {
33
+ errors.push("Missing Task metadata");
34
+ }
35
+ else if (!isValidTaskId(metadata.task)) {
36
+ errors.push(`Invalid Task metadata format: ${metadata.task}`);
37
+ }
38
+ if (metadata.roadmap && !isValidRoadmapId(metadata.roadmap)) {
39
+ errors.push(`Invalid Roadmap metadata format: ${metadata.roadmap}`);
40
+ }
41
+ return { ok: errors.length === 0, errors };
42
+ }
43
+ async function findAllMarkdownFiles(dir) {
44
+ const result = [];
45
+ async function walk(currentDir, relativeBase) {
46
+ const entries = await fs.readdir(currentDir, { withFileTypes: true });
47
+ for (const entry of entries) {
48
+ const fullPath = path.join(currentDir, entry.name);
49
+ if (entry.isDirectory()) {
50
+ await walk(fullPath, path.join(relativeBase, entry.name));
51
+ }
52
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
53
+ result.push({
54
+ filePath: fullPath,
55
+ relativePath: path.join(relativeBase, entry.name),
56
+ });
57
+ }
58
+ }
59
+ }
60
+ await walk(dir, "");
61
+ return result;
62
+ }
63
+ export async function registerDesignFilesResources(server, repoRoot) {
64
+ const designsDir = path.join(repoRoot, ".projitive", "designs");
65
+ try {
66
+ // Check if design documents directory exists
67
+ await fs.access(designsDir);
68
+ // Recursively read all .md files (including subdirectories)
69
+ const markdownFiles = await findAllMarkdownFiles(designsDir);
70
+ // Register each design file as a resource
71
+ for (const { filePath, relativePath } of markdownFiles) {
72
+ // Generate designId from relative path, replace path separators with '-'
73
+ const designId = relativePath
74
+ .slice(0, -3) // Remove .md suffix
75
+ .replace(/[\\/]/g, "-"); // Replace path separators with '-'
76
+ const content = await fs.readFile(filePath, "utf-8");
77
+ // Register resource
78
+ server.registerResource(`design-${designId}`, `projitive://designs/${designId}`, {
79
+ title: designId,
80
+ description: `Design document: ${relativePath}`,
81
+ mimeType: "text/markdown",
82
+ }, async () => ({
83
+ contents: [
84
+ {
85
+ uri: `projitive://designs/${designId}`,
86
+ text: content,
87
+ },
88
+ ],
89
+ }));
90
+ }
91
+ }
92
+ catch (error) {
93
+ // If design documents directory doesn't exist, register a default resource
94
+ console.warn(`Designs directory not found at ${designsDir}, registering default design resource`);
95
+ server.registerResource("designs", "projitive://designs", {
96
+ title: "Designs",
97
+ description: "Design documents directory",
98
+ mimeType: "text/markdown",
99
+ }, async () => ({
100
+ contents: [
101
+ {
102
+ uri: "projitive://designs",
103
+ text: `# Designs Directory\n\nDesign documents not found. Please create design files in .projitive/designs/ directory.`,
104
+ },
105
+ ],
106
+ }));
107
+ }
108
+ }
@@ -0,0 +1,154 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseDesignMetadata, validateDesignMetadata } from "./designs.js";
3
+ describe("designs module", () => {
4
+ describe("parseDesignMetadata", () => {
5
+ it("parses task metadata from markdown", () => {
6
+ const markdown = [
7
+ "# Design Document",
8
+ "",
9
+ "**Task:** TASK-0001",
10
+ "**Owner:** ai-copilot",
11
+ "**Status:** Draft",
12
+ "**Last Updated:** 2026-02-22",
13
+ "",
14
+ "Some content here",
15
+ ].join("\n");
16
+ const metadata = parseDesignMetadata(markdown);
17
+ expect(metadata.task).toBe("TASK-0001");
18
+ expect(metadata.owner).toBe("ai-copilot");
19
+ expect(metadata.status).toBe("Draft");
20
+ expect(metadata.lastUpdated).toBe("2026-02-22");
21
+ });
22
+ it("parses roadmap metadata from markdown", () => {
23
+ const markdown = [
24
+ "# Design Document",
25
+ "",
26
+ "**Roadmap:** ROADMAP-0001",
27
+ "",
28
+ "Some content here",
29
+ ].join("\n");
30
+ const metadata = parseDesignMetadata(markdown);
31
+ expect(metadata.roadmap).toBe("ROADMAP-0001");
32
+ });
33
+ it("returns empty object for markdown without metadata", () => {
34
+ const markdown = [
35
+ "# Simple Design",
36
+ "",
37
+ "No metadata here",
38
+ ].join("\n");
39
+ const metadata = parseDesignMetadata(markdown);
40
+ expect(metadata).toEqual({});
41
+ });
42
+ it("handles empty string", () => {
43
+ const metadata = parseDesignMetadata("");
44
+ expect(metadata).toEqual({});
45
+ });
46
+ it("handles malformed metadata lines", () => {
47
+ const markdown = [
48
+ "# Report",
49
+ "",
50
+ "Task without colon",
51
+ "Not a metadata line",
52
+ ":",
53
+ "Task:",
54
+ ].join("\n");
55
+ const metadata = parseDesignMetadata(markdown);
56
+ expect(metadata).toBeDefined();
57
+ });
58
+ it("parses metadata in different formats", () => {
59
+ const markdown = [
60
+ "Task: TASK-0001",
61
+ "task: TASK-0002",
62
+ "TASK: TASK-0003",
63
+ " task : TASK-0004 ",
64
+ ].join("\n");
65
+ const metadata = parseDesignMetadata(markdown);
66
+ expect(metadata.task).toBeDefined();
67
+ });
68
+ });
69
+ describe("validateDesignMetadata", () => {
70
+ it("validates correct task metadata", () => {
71
+ const metadata = {
72
+ task: "TASK-0001",
73
+ owner: "ai-copilot",
74
+ status: "Draft",
75
+ lastUpdated: "2026-02-22",
76
+ };
77
+ const result = validateDesignMetadata(metadata);
78
+ expect(result.ok).toBe(true);
79
+ expect(result.errors).toEqual([]);
80
+ });
81
+ it("rejects missing task metadata", () => {
82
+ const metadata = {
83
+ owner: "ai-copilot",
84
+ };
85
+ const result = validateDesignMetadata(metadata);
86
+ expect(result.ok).toBe(false);
87
+ expect(result.errors).toContain("Missing Task metadata");
88
+ });
89
+ it("rejects invalid task ID format", () => {
90
+ const metadata = {
91
+ task: "invalid-format",
92
+ };
93
+ const result = validateDesignMetadata(metadata);
94
+ expect(result.ok).toBe(false);
95
+ expect(result.errors.some((e) => e.includes("Invalid Task"))).toBe(true);
96
+ });
97
+ it("validates optional roadmap metadata", () => {
98
+ const metadata = {
99
+ task: "TASK-0001",
100
+ roadmap: "ROADMAP-0001",
101
+ };
102
+ const result = validateDesignMetadata(metadata);
103
+ expect(result.ok).toBe(true);
104
+ });
105
+ it("rejects invalid roadmap ID format", () => {
106
+ const metadata = {
107
+ task: "TASK-0001",
108
+ roadmap: "invalid-roadmap",
109
+ };
110
+ const result = validateDesignMetadata(metadata);
111
+ expect(result.ok).toBe(false);
112
+ expect(result.errors.some((e) => e.includes("Invalid Roadmap"))).toBe(true);
113
+ });
114
+ it("handles empty metadata object", () => {
115
+ const metadata = {};
116
+ const result = validateDesignMetadata(metadata);
117
+ expect(result.ok).toBe(false);
118
+ expect(result.errors.length).toBeGreaterThan(0);
119
+ });
120
+ it("collects multiple validation errors", () => {
121
+ const metadata = {
122
+ task: "invalid-task",
123
+ roadmap: "invalid-roadmap",
124
+ };
125
+ const result = validateDesignMetadata(metadata);
126
+ expect(result.ok).toBe(false);
127
+ expect(result.errors.length).toBeGreaterThan(1);
128
+ });
129
+ });
130
+ describe("integration", () => {
131
+ it("parses and validates complete design metadata", () => {
132
+ const markdown = [
133
+ "# Design Completion Report",
134
+ "",
135
+ "**Task:** TASK-0001",
136
+ "**Roadmap:** ROADMAP-0002",
137
+ "**Owner:** ai-copilot",
138
+ "**Status:** Completed",
139
+ "**Last Updated:** 2026-02-22",
140
+ "",
141
+ "## Summary",
142
+ "Design completed successfully",
143
+ ].join("\n");
144
+ const metadata = parseDesignMetadata(markdown);
145
+ const validation = validateDesignMetadata(metadata);
146
+ expect(metadata.task).toBe("TASK-0001");
147
+ expect(metadata.roadmap).toBe("ROADMAP-0002");
148
+ expect(metadata.owner).toBe("ai-copilot");
149
+ expect(metadata.status).toBe("Completed");
150
+ expect(metadata.lastUpdated).toBe("2026-02-22");
151
+ expect(validation.ok).toBe(true);
152
+ });
153
+ });
154
+ });
@@ -0,0 +1,40 @@
1
+ // Governance resource management
2
+ import { readMarkdownOrFallback } from "../common/utils.js";
3
+ export function registerGovernanceResources(server, repoRoot) {
4
+ server.registerResource("governanceWorkspace", "projitive://governance/workspace", {
5
+ title: "Governance Workspace",
6
+ description: "Primary governance README under .projitive",
7
+ mimeType: "text/markdown",
8
+ }, async () => ({
9
+ contents: [
10
+ {
11
+ uri: "projitive://governance/workspace",
12
+ text: await readMarkdownOrFallback(".projitive/README.md", "Governance Workspace", repoRoot),
13
+ },
14
+ ],
15
+ }));
16
+ server.registerResource("governanceTasks", "projitive://governance/tasks", {
17
+ title: "Governance Tasks",
18
+ description: "Current task pool and status under .projitive/tasks.md",
19
+ mimeType: "text/markdown",
20
+ }, async () => ({
21
+ contents: [
22
+ {
23
+ uri: "projitive://governance/tasks",
24
+ text: await readMarkdownOrFallback(".projitive/tasks.md", "Governance Tasks", repoRoot),
25
+ },
26
+ ],
27
+ }));
28
+ server.registerResource("governanceRoadmap", "projitive://governance/roadmap", {
29
+ title: "Governance Roadmap",
30
+ description: "Current roadmap under .projitive/roadmap.md",
31
+ mimeType: "text/markdown",
32
+ }, async () => ({
33
+ contents: [
34
+ {
35
+ uri: "projitive://governance/roadmap",
36
+ text: await readMarkdownOrFallback(".projitive/roadmap.md", "Governance Roadmap", repoRoot),
37
+ },
38
+ ],
39
+ }));
40
+ }
@@ -0,0 +1,6 @@
1
+ import { registerGovernanceResources } from "./governance.js";
2
+ import { registerDesignFilesResources } from "./designs.js";
3
+ export function registerResources(server, repoRoot) {
4
+ registerGovernanceResources(server, repoRoot);
5
+ registerDesignFilesResources(server, repoRoot);
6
+ }
@@ -0,0 +1,167 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseRequiredReading, } from "./readme.js";
3
+ describe("readme module", () => {
4
+ describe("parseRequiredReading", () => {
5
+ it("parses local and external required reading items", () => {
6
+ const markdown = [
7
+ "# Project README",
8
+ "",
9
+ "Some intro content",
10
+ "",
11
+ "## Required Reading for Agents",
12
+ "",
13
+ "- Local: ./design/README.md",
14
+ "- Local: .projitive/tasks.md",
15
+ "- External: https://example.com/docs",
16
+ "",
17
+ "## Other Section",
18
+ "",
19
+ "More content here",
20
+ ].join("\n");
21
+ const result = parseRequiredReading(markdown);
22
+ expect(result.length).toBe(3);
23
+ expect(result[0]).toEqual({
24
+ source: "Local",
25
+ value: "./design/README.md",
26
+ });
27
+ expect(result[1]).toEqual({
28
+ source: "Local",
29
+ value: ".projitive/tasks.md",
30
+ });
31
+ expect(result[2]).toEqual({
32
+ source: "External",
33
+ value: "https://example.com/docs",
34
+ });
35
+ });
36
+ it("parses Chinese section header", () => {
37
+ const markdown = [
38
+ "# 项目 README",
39
+ "",
40
+ "## Agent 必读",
41
+ "",
42
+ "- Local: ./docs/guide.md",
43
+ "",
44
+ "## 其他部分",
45
+ ].join("\n");
46
+ const result = parseRequiredReading(markdown);
47
+ expect(result.length).toBe(1);
48
+ expect(result[0].source).toBe("Local");
49
+ expect(result[0].value).toBe("./docs/guide.md");
50
+ });
51
+ it("returns empty array when no required reading section", () => {
52
+ const markdown = [
53
+ "# Simple README",
54
+ "",
55
+ "No required reading here",
56
+ "",
57
+ "## Other Section",
58
+ ].join("\n");
59
+ const result = parseRequiredReading(markdown);
60
+ expect(result).toEqual([]);
61
+ });
62
+ it("returns empty array for empty string", () => {
63
+ const result = parseRequiredReading("");
64
+ expect(result).toEqual([]);
65
+ });
66
+ it("ignores non-list items in required reading section", () => {
67
+ const markdown = [
68
+ "## Required Reading for Agents",
69
+ "",
70
+ "Some paragraph text here",
71
+ "- Local: valid-item.md",
72
+ "Another paragraph",
73
+ "- External: https://valid.com",
74
+ "",
75
+ "## Next Section",
76
+ ].join("\n");
77
+ const result = parseRequiredReading(markdown);
78
+ expect(result.length).toBe(2);
79
+ expect(result[0].value).toBe("valid-item.md");
80
+ expect(result[1].value).toBe("https://valid.com");
81
+ });
82
+ it("handles items with whitespace variations", () => {
83
+ const markdown = [
84
+ "## Required Reading for Agents",
85
+ "",
86
+ "- Local: ./path/with/spaces.md ",
87
+ "- External: https://example.com ",
88
+ ].join("\n");
89
+ const result = parseRequiredReading(markdown);
90
+ expect(result.length).toBe(2);
91
+ expect(result[0].value).toBe("./path/with/spaces.md");
92
+ expect(result[1].value).toBe("https://example.com");
93
+ });
94
+ it("ignores items without Local: or External: prefix", () => {
95
+ const markdown = [
96
+ "## Required Reading for Agents",
97
+ "",
98
+ "- Local: valid.md",
99
+ "- Invalid: this is ignored",
100
+ "- Just a plain list item",
101
+ "- External: https://valid.com",
102
+ ].join("\n");
103
+ const result = parseRequiredReading(markdown);
104
+ expect(result.length).toBe(2);
105
+ expect(result.every((item) => item.source === "Local" || item.source === "External")).toBe(true);
106
+ });
107
+ it("stops at next section header", () => {
108
+ const markdown = [
109
+ "## Required Reading for Agents",
110
+ "",
111
+ "- Local: before.md",
112
+ "",
113
+ "## Another Section",
114
+ "",
115
+ "- Local: after.md",
116
+ ].join("\n");
117
+ const result = parseRequiredReading(markdown);
118
+ expect(result.length).toBe(1);
119
+ expect(result[0].value).toBe("before.md");
120
+ });
121
+ it("handles multiple required reading sections (takes first one)", () => {
122
+ const markdown = [
123
+ "## Required Reading for Agents",
124
+ "",
125
+ "- Local: first.md",
126
+ "",
127
+ "## Other Section",
128
+ "",
129
+ "## Required Reading for Agents",
130
+ "",
131
+ "- Local: second.md",
132
+ ].join("\n");
133
+ const result = parseRequiredReading(markdown);
134
+ expect(result.length).toBe(1);
135
+ expect(result[0].value).toBe("first.md");
136
+ });
137
+ });
138
+ describe("edge cases", () => {
139
+ it("handles markdown with just the section", () => {
140
+ const markdown = "## Required Reading for Agents";
141
+ const result = parseRequiredReading(markdown);
142
+ expect(result).toEqual([]);
143
+ });
144
+ it("handles section with empty lines", () => {
145
+ const markdown = [
146
+ "## Required Reading for Agents",
147
+ "",
148
+ "",
149
+ "- Local: item.md",
150
+ "",
151
+ "",
152
+ ].join("\n");
153
+ const result = parseRequiredReading(markdown);
154
+ expect(result.length).toBe(1);
155
+ expect(result[0].value).toBe("item.md");
156
+ });
157
+ it("handles mixed case section headers", () => {
158
+ const markdown = [
159
+ "## required reading for agents",
160
+ "",
161
+ "- Local: lowercase.md",
162
+ ].join("\n");
163
+ const result = parseRequiredReading(markdown);
164
+ expect(result.length).toBe(1);
165
+ });
166
+ });
167
+ });
@@ -1,10 +1,12 @@
1
- import { isValidRoadmapId } from "./roadmap.js";
2
- import { isValidTaskId } from "./tasks.js";
1
+ import { isValidRoadmapId } from "../tools/roadmap.js";
2
+ import { isValidTaskId } from "../tools/task.js";
3
3
  export function parseReportMetadata(markdown) {
4
4
  const lines = markdown.split(/\r?\n/);
5
5
  const metadata = {};
6
6
  for (const line of lines) {
7
- const [rawKey, ...rawValue] = line.split(":");
7
+ // Remove markdown bold markers (**)
8
+ const cleanLine = line.replace(/\*\*/g, "");
9
+ const [rawKey, ...rawValue] = cleanLine.split(":");
8
10
  if (!rawKey || rawValue.length === 0) {
9
11
  continue;
10
12
  }