@projitive/mcp 2.0.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 CHANGED
@@ -5,7 +5,7 @@ Language: English | [简体中文](README_CN.md)
5
5
  ## Version
6
6
 
7
7
  - Current Spec Version: projitive-spec v1.0.0
8
- - MCP Version: 2.0.0
8
+ - MCP Version: 2.0.1
9
9
 
10
10
  ## 60-Second Start
11
11
 
@@ -157,7 +157,7 @@ sequenceDiagram
157
157
 
158
158
  ### 3. Evidence-First Execution
159
159
 
160
- - State changes should be backed by report/design/readme evidence.
160
+ - State changes should be backed by report/designs/readme evidence.
161
161
  - Tool output format is agent-friendly markdown for chained execution.
162
162
 
163
163
  ### 4. Deterministic Multi-Agent Workflow
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projitive/mcp",
3
- "version": "2.0.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": "",
@@ -13,6 +13,7 @@ export const TASK_LINT_CODES = {
13
13
  ROADMAP_REFS_EMPTY: "TASK_ROADMAP_REFS_EMPTY",
14
14
  OUTSIDE_MARKER: "TASK_OUTSIDE_MARKER",
15
15
  LINK_TARGET_MISSING: "TASK_LINK_TARGET_MISSING",
16
+ LINK_PATH_FORMAT_INVALID: "TASK_LINK_PATH_FORMAT_INVALID",
16
17
  HOOK_FILE_MISSING: "TASK_HOOK_FILE_MISSING",
17
18
  FILTER_EMPTY: "TASK_FILTER_EMPTY",
18
19
  CONTEXT_HOOK_HEAD_MISSING: "TASK_CONTEXT_HOOK_HEAD_MISSING",
@@ -99,7 +99,7 @@ export function registerQuickStartPrompt(server) {
99
99
  "2. If roadmap has active goals, split milestones into 1-3 executable TODO tasks",
100
100
  "3. Apply task creation gate before adding each task:",
101
101
  " - Clear outcome: one-sentence done condition",
102
- " - Verifiable evidence: at least one report/design/readme link target",
102
+ " - Verifiable evidence: at least one report/designs/readme link target",
103
103
  " - Small slice: should be completable in one focused execution cycle",
104
104
  " - Traceability: include at least one roadmapRefs item when applicable",
105
105
  " - Distinct scope: avoid overlap with existing DONE/BLOCKED tasks",
@@ -10,7 +10,7 @@ describe("readme module", () => {
10
10
  "",
11
11
  "## Required Reading for Agents",
12
12
  "",
13
- "- Local: ./design/README.md",
13
+ "- Local: ./designs/README.md",
14
14
  "- Local: .projitive/tasks.md",
15
15
  "- External: https://example.com/docs",
16
16
  "",
@@ -22,7 +22,7 @@ describe("readme module", () => {
22
22
  expect(result.length).toBe(3);
23
23
  expect(result[0]).toEqual({
24
24
  source: "Local",
25
- value: "./design/README.md",
25
+ value: "./designs/README.md",
26
26
  });
27
27
  expect(result[1]).toEqual({
28
28
  source: "Local",
@@ -42,7 +42,7 @@ const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
42
42
  "- If all remaining tasks are BLOCKED, create one unblock task with explicit unblock condition and dependency owner.",
43
43
  "- Start from active roadmap milestones and split into the smallest executable slices with a single done condition each.",
44
44
  "- Prefer slices that unlock multiple downstream tasks before isolated refactors or low-impact cleanups.",
45
- "- Create TODO tasks only when evidence is clear: each new task must produce at least one report/design/readme artifact update.",
45
+ "- Create TODO tasks only when evidence is clear: each new task must produce at least one report/designs/readme artifact update.",
46
46
  "- Skip duplicate scope: do not create tasks that overlap existing TODO/IN_PROGRESS/BLOCKED task intent.",
47
47
  "- Use quality gates for discovery candidates: user value, delivery risk reduction, or measurable throughput improvement.",
48
48
  "- Keep each discovery round small (1-3 tasks), then rerun taskNext immediately for re-ranking and execution.",
@@ -85,11 +85,33 @@ export function renderTaskSeedTemplate(roadmapRef) {
85
85
  "- updatedAt: 2026-01-01T00:00:00.000Z",
86
86
  `- roadmapRefs: ${roadmapRef}`,
87
87
  "- links:",
88
- " - ./README.md",
89
- " - ./roadmap.md",
88
+ " - README.md",
89
+ " - .projitive/roadmap.md",
90
90
  "```",
91
91
  ];
92
92
  }
93
+ function isHttpUrl(value) {
94
+ return /^https?:\/\//i.test(value);
95
+ }
96
+ function isProjectRootRelativePath(value) {
97
+ return value.length > 0
98
+ && !value.startsWith("/")
99
+ && !value.startsWith("./")
100
+ && !value.startsWith("../")
101
+ && !/^[A-Za-z]:\//.test(value);
102
+ }
103
+ function normalizeTaskLink(link) {
104
+ const trimmed = link.trim();
105
+ if (trimmed.length === 0 || isHttpUrl(trimmed)) {
106
+ return trimmed;
107
+ }
108
+ const slashNormalized = trimmed.replace(/\\/g, "/");
109
+ const withoutDotPrefix = slashNormalized.replace(/^\.\//, "");
110
+ return withoutDotPrefix.replace(/^\/+/, "");
111
+ }
112
+ function resolveTaskLinkPath(projectPath, link) {
113
+ return path.join(projectPath, link);
114
+ }
93
115
  async function readActionableTaskCandidates(governanceDirs) {
94
116
  const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
95
117
  const tasksPath = path.join(governanceDir, ".projitive");
@@ -206,7 +228,12 @@ export function normalizeTask(task) {
206
228
  owner: task.owner ? String(task.owner) : "",
207
229
  summary: task.summary ? String(task.summary) : "",
208
230
  updatedAt: task.updatedAt ? String(task.updatedAt) : nowIso(),
209
- links: Array.isArray(task.links) ? task.links.map(String) : [],
231
+ links: Array.isArray(task.links)
232
+ ? Array.from(new Set(task.links
233
+ .map(String)
234
+ .map((value) => normalizeTaskLink(value))
235
+ .filter((value) => value.length > 0)))
236
+ : [],
210
237
  roadmapRefs: Array.from(new Set(normalizedRoadmapRefs)),
211
238
  };
212
239
  // Include optional v1.1.0 fields if present
@@ -274,6 +301,17 @@ function collectTaskLintSuggestionItems(tasks) {
274
301
  fixHint: "Bind at least one ROADMAP-xxxx when applicable.",
275
302
  });
276
303
  }
304
+ const invalidLinkPathFormat = tasks.filter((task) => task.links.some((link) => {
305
+ const normalized = link.trim();
306
+ return normalized.length > 0 && !isHttpUrl(normalized) && !isProjectRootRelativePath(normalized);
307
+ }));
308
+ if (invalidLinkPathFormat.length > 0) {
309
+ suggestions.push({
310
+ code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
311
+ message: `${invalidLinkPathFormat.length} task(s) contain invalid links path format.`,
312
+ fixHint: "Use project-root-relative paths without leading slash (for example reports/task-0001.md) or http(s) URL.",
313
+ });
314
+ }
277
315
  // ============================================================================
278
316
  // Spec v1.1.0 - Blocker Categorization Validation
279
317
  // ============================================================================
@@ -349,6 +387,17 @@ function collectSingleTaskLintSuggestions(task) {
349
387
  fixHint: "Add at least one evidence link.",
350
388
  });
351
389
  }
390
+ const invalidLinkPathFormat = task.links.some((link) => {
391
+ const normalized = link.trim();
392
+ return normalized.length > 0 && !isHttpUrl(normalized) && !isProjectRootRelativePath(normalized);
393
+ });
394
+ if (invalidLinkPathFormat) {
395
+ suggestions.push({
396
+ code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
397
+ message: "Current task has invalid links path format.",
398
+ fixHint: "Use project-root-relative paths without leading slash (for example reports/task-0001.md) or http(s) URL.",
399
+ });
400
+ }
352
401
  if (task.status === "BLOCKED" && task.summary.trim().length === 0) {
353
402
  suggestions.push({
354
403
  code: TASK_LINT_CODES.BLOCKED_SUMMARY_EMPTY,
@@ -422,6 +471,7 @@ function collectSingleTaskLintSuggestions(task) {
422
471
  }
423
472
  async function collectTaskFileLintSuggestions(governanceDir, task) {
424
473
  const suggestions = [];
474
+ const projectPath = toProjectPath(governanceDir);
425
475
  for (const link of task.links) {
426
476
  const normalized = link.trim();
427
477
  if (normalized.length === 0) {
@@ -430,7 +480,15 @@ async function collectTaskFileLintSuggestions(governanceDir, task) {
430
480
  if (/^https?:\/\//i.test(normalized)) {
431
481
  continue;
432
482
  }
433
- const resolvedPath = path.resolve(governanceDir, normalized);
483
+ if (!isProjectRootRelativePath(normalized)) {
484
+ suggestions.push({
485
+ code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
486
+ message: `Link path should be project-root-relative without leading slash: ${normalized}.`,
487
+ fixHint: "Use path/from/project/root format.",
488
+ });
489
+ continue;
490
+ }
491
+ const resolvedPath = resolveTaskLinkPath(projectPath, normalized);
434
492
  const exists = await fs.access(resolvedPath).then(() => true).catch(() => false);
435
493
  if (!exists) {
436
494
  suggestions.push({
@@ -111,6 +111,30 @@ describe("tasks module", () => {
111
111
  expect(lint.some((line) => line.includes("BLOCKED_WITHOUT_BLOCKER"))).toBe(true);
112
112
  expect(lint.some((line) => line.includes("IN_PROGRESS_WITHOUT_SUBSTATE"))).toBe(true);
113
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",
118
+ status: "TODO",
119
+ links: ["/reports/a.md", "./designs/b.md", "reports/c.md", "https://example.com/evidence"],
120
+ });
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);
126
+ });
127
+ it("lints invalid links path format", () => {
128
+ const task = normalizeTask({
129
+ id: "TASK-0004",
130
+ title: "invalid link",
131
+ status: "TODO",
132
+ links: ["../outside.md"],
133
+ roadmapRefs: ["ROADMAP-0001"],
134
+ });
135
+ const lint = collectTaskLintSuggestions([task]);
136
+ expect(lint.some((line) => line.includes("TASK_LINK_PATH_FORMAT_INVALID"))).toBe(true);
137
+ });
114
138
  it("renders seed task template with provided roadmap ref", () => {
115
139
  const lines = renderTaskSeedTemplate("ROADMAP-0099");
116
140
  const markdown = lines.join("\n");
@@ -36,6 +36,7 @@ export const TASK_LINT_CODES = {
36
36
  OUTSIDE_MARKER: "TASK_OUTSIDE_MARKER",
37
37
  FILTER_EMPTY: "TASK_FILTER_EMPTY",
38
38
  LINK_TARGET_MISSING: "TASK_LINK_TARGET_MISSING",
39
+ LINK_PATH_FORMAT_INVALID: "TASK_LINK_PATH_FORMAT_INVALID",
39
40
  // Spec v1.1.0 - Blocker Categorization
40
41
  BLOCKED_WITHOUT_BLOCKER: "TASK_BLOCKED_WITHOUT_BLOCKER",
41
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": "2.0.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": "",