@projitive/mcp 1.0.0-beta.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 +318 -0
- package/output/designs.js +38 -0
- package/output/helpers/catch/catch.js +48 -0
- package/output/helpers/catch/catch.test.js +43 -0
- package/output/helpers/catch/index.js +1 -0
- package/output/helpers/files/files.js +62 -0
- package/output/helpers/files/files.test.js +32 -0
- package/output/helpers/files/index.js +1 -0
- package/output/helpers/index.js +3 -0
- package/output/helpers/markdown/index.js +1 -0
- package/output/helpers/markdown/markdown.js +33 -0
- package/output/helpers/markdown/markdown.test.js +36 -0
- package/output/hooks.js +62 -0
- package/output/hooks.test.js +51 -0
- package/output/index.js +562 -0
- package/output/projitive.js +55 -0
- package/output/projitive.test.js +46 -0
- package/output/readme.js +26 -0
- package/output/reports.js +36 -0
- package/output/roadmap.js +4 -0
- package/output/tasks.js +242 -0
- package/output/tasks.test.js +78 -0
- package/package.json +29 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { discoverProjects, hasProjectMarker, resolveGovernanceDir } from "./projitive.js";
|
|
6
|
+
const tempPaths = [];
|
|
7
|
+
async function createTempDir() {
|
|
8
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-test-"));
|
|
9
|
+
tempPaths.push(dir);
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await Promise.all(tempPaths.splice(0).map(async (dir) => {
|
|
14
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
15
|
+
}));
|
|
16
|
+
});
|
|
17
|
+
describe("projitive module", () => {
|
|
18
|
+
it("does not treat marker directory as a valid project marker", async () => {
|
|
19
|
+
const root = await createTempDir();
|
|
20
|
+
const dirMarkerPath = path.join(root, ".projitive");
|
|
21
|
+
await fs.mkdir(dirMarkerPath, { recursive: true });
|
|
22
|
+
const hasMarker = await hasProjectMarker(root);
|
|
23
|
+
expect(hasMarker).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
it("resolves governance dir by walking upwards for .projitive", async () => {
|
|
26
|
+
const root = await createTempDir();
|
|
27
|
+
const governanceDir = path.join(root, "repo", "governance");
|
|
28
|
+
const deepDir = path.join(governanceDir, "nested", "module");
|
|
29
|
+
await fs.mkdir(deepDir, { recursive: true });
|
|
30
|
+
await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
|
|
31
|
+
const resolved = await resolveGovernanceDir(deepDir);
|
|
32
|
+
expect(resolved).toBe(governanceDir);
|
|
33
|
+
});
|
|
34
|
+
it("discovers projects by marker file", async () => {
|
|
35
|
+
const root = await createTempDir();
|
|
36
|
+
const p1 = path.join(root, "a");
|
|
37
|
+
const p2 = path.join(root, "b", "c");
|
|
38
|
+
await fs.mkdir(p1, { recursive: true });
|
|
39
|
+
await fs.mkdir(p2, { recursive: true });
|
|
40
|
+
await fs.writeFile(path.join(p1, ".projitive"), "", "utf-8");
|
|
41
|
+
await fs.writeFile(path.join(p2, ".projitive"), "", "utf-8");
|
|
42
|
+
const projects = await discoverProjects(root, 4);
|
|
43
|
+
expect(projects).toContain(p1);
|
|
44
|
+
expect(projects).toContain(p2);
|
|
45
|
+
});
|
|
46
|
+
});
|
package/output/readme.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function parseRequiredReading(markdown) {
|
|
2
|
+
const lines = markdown.split(/\r?\n/);
|
|
3
|
+
const result = [];
|
|
4
|
+
let inSection = false;
|
|
5
|
+
for (const line of lines) {
|
|
6
|
+
const trimmed = line.trim();
|
|
7
|
+
if (/^##\s+(Required Reading for Agents|Agent 必读)$/i.test(trimmed)) {
|
|
8
|
+
inSection = true;
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (inSection && trimmed.startsWith("## ")) {
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
if (!inSection || !trimmed.startsWith("- ")) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const payload = trimmed.replace(/^-\s+/, "");
|
|
18
|
+
if (payload.startsWith("Local:")) {
|
|
19
|
+
result.push({ source: "Local", value: payload.replace("Local:", "").trim() });
|
|
20
|
+
}
|
|
21
|
+
if (payload.startsWith("External:")) {
|
|
22
|
+
result.push({ source: "External", value: payload.replace("External:", "").trim() });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isValidRoadmapId } from "./roadmap.js";
|
|
2
|
+
import { isValidTaskId } from "./tasks.js";
|
|
3
|
+
export function parseReportMetadata(markdown) {
|
|
4
|
+
const lines = markdown.split(/\r?\n/);
|
|
5
|
+
const metadata = {};
|
|
6
|
+
for (const line of lines) {
|
|
7
|
+
const [rawKey, ...rawValue] = line.split(":");
|
|
8
|
+
if (!rawKey || rawValue.length === 0) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const key = rawKey.trim().toLowerCase();
|
|
12
|
+
const value = rawValue.join(":").trim();
|
|
13
|
+
if (key === "task")
|
|
14
|
+
metadata.task = value;
|
|
15
|
+
if (key === "roadmap")
|
|
16
|
+
metadata.roadmap = value;
|
|
17
|
+
if (key === "owner")
|
|
18
|
+
metadata.owner = value;
|
|
19
|
+
if (key === "date")
|
|
20
|
+
metadata.date = value;
|
|
21
|
+
}
|
|
22
|
+
return metadata;
|
|
23
|
+
}
|
|
24
|
+
export function validateReportMetadata(metadata) {
|
|
25
|
+
const errors = [];
|
|
26
|
+
if (!metadata.task) {
|
|
27
|
+
errors.push("Missing Task metadata");
|
|
28
|
+
}
|
|
29
|
+
else if (!isValidTaskId(metadata.task)) {
|
|
30
|
+
errors.push(`Invalid Task metadata format: ${metadata.task}`);
|
|
31
|
+
}
|
|
32
|
+
if (metadata.roadmap && !isValidRoadmapId(metadata.roadmap)) {
|
|
33
|
+
errors.push(`Invalid Roadmap metadata format: ${metadata.roadmap}`);
|
|
34
|
+
}
|
|
35
|
+
return { ok: errors.length === 0, errors };
|
|
36
|
+
}
|
package/output/tasks.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { catchIt } from "./helpers/catch/index.js";
|
|
4
|
+
import { resolveGovernanceDir } from "./projitive.js";
|
|
5
|
+
import { isValidRoadmapId } from "./roadmap.js";
|
|
6
|
+
export const TASKS_START = "<!-- PROJITIVE:TASKS:START -->";
|
|
7
|
+
export const TASKS_END = "<!-- PROJITIVE:TASKS:END -->";
|
|
8
|
+
export const ALLOWED_STATUS = ["TODO", "IN_PROGRESS", "BLOCKED", "DONE"];
|
|
9
|
+
export const TASK_ID_REGEX = /^TASK-\d{4}$/;
|
|
10
|
+
export function nowIso() {
|
|
11
|
+
return new Date().toISOString();
|
|
12
|
+
}
|
|
13
|
+
export function isValidTaskId(id) {
|
|
14
|
+
return TASK_ID_REGEX.test(id);
|
|
15
|
+
}
|
|
16
|
+
export function taskPriority(status) {
|
|
17
|
+
if (status === "IN_PROGRESS") {
|
|
18
|
+
return 2;
|
|
19
|
+
}
|
|
20
|
+
if (status === "TODO") {
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
export function toTaskUpdatedAtMs(updatedAt) {
|
|
26
|
+
const timestamp = new Date(updatedAt).getTime();
|
|
27
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
28
|
+
}
|
|
29
|
+
export function rankActionableTaskCandidates(candidates) {
|
|
30
|
+
return [...candidates].sort((a, b) => {
|
|
31
|
+
if (b.projectScore !== a.projectScore) {
|
|
32
|
+
return b.projectScore - a.projectScore;
|
|
33
|
+
}
|
|
34
|
+
if (b.taskPriority !== a.taskPriority) {
|
|
35
|
+
return b.taskPriority - a.taskPriority;
|
|
36
|
+
}
|
|
37
|
+
if (b.taskUpdatedAtMs !== a.taskUpdatedAtMs) {
|
|
38
|
+
return b.taskUpdatedAtMs - a.taskUpdatedAtMs;
|
|
39
|
+
}
|
|
40
|
+
if (a.governanceDir !== b.governanceDir) {
|
|
41
|
+
return a.governanceDir.localeCompare(b.governanceDir);
|
|
42
|
+
}
|
|
43
|
+
return a.task.id.localeCompare(b.task.id);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function normalizeTask(task) {
|
|
47
|
+
const normalizedRoadmapRefs = Array.isArray(task.roadmapRefs)
|
|
48
|
+
? task.roadmapRefs.map(String).filter((value) => isValidRoadmapId(value))
|
|
49
|
+
: [];
|
|
50
|
+
const inputHooks = task.hooks ?? {};
|
|
51
|
+
const normalizedHooks = {};
|
|
52
|
+
for (const key of ["onAssigned", "onCompleted", "onBlocked", "onReopened"]) {
|
|
53
|
+
const value = inputHooks[key];
|
|
54
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
55
|
+
normalizedHooks[key] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
id: String(task.id),
|
|
60
|
+
title: String(task.title),
|
|
61
|
+
status: ALLOWED_STATUS.includes(task.status) ? task.status : "TODO",
|
|
62
|
+
owner: task.owner ? String(task.owner) : "",
|
|
63
|
+
summary: task.summary ? String(task.summary) : "",
|
|
64
|
+
updatedAt: task.updatedAt ? String(task.updatedAt) : nowIso(),
|
|
65
|
+
links: Array.isArray(task.links) ? task.links.map(String) : [],
|
|
66
|
+
roadmapRefs: Array.from(new Set(normalizedRoadmapRefs)),
|
|
67
|
+
hooks: normalizedHooks,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export async function parseTasksBlock(markdown) {
|
|
71
|
+
const start = markdown.indexOf(TASKS_START);
|
|
72
|
+
const end = markdown.indexOf(TASKS_END);
|
|
73
|
+
if (start === -1 || end === -1 || end <= start) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const body = markdown.slice(start + TASKS_START.length, end).trim();
|
|
77
|
+
if (!body || body === "(no tasks)") {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const sections = body
|
|
81
|
+
.split(/\n(?=##\s+TASK-\d{4}\s+\|\s+(?:TODO|IN_PROGRESS|BLOCKED|DONE)\s+\|)/g)
|
|
82
|
+
.map((section) => section.trim())
|
|
83
|
+
.filter((section) => section.startsWith("## TASK-"));
|
|
84
|
+
const tasks = [];
|
|
85
|
+
for (const section of sections) {
|
|
86
|
+
const lines = section.split(/\r?\n/);
|
|
87
|
+
const header = lines[0]?.match(/^##\s+(TASK-\d{4})\s+\|\s+(TODO|IN_PROGRESS|BLOCKED|DONE)\s+\|\s+(.+)$/);
|
|
88
|
+
if (!header) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const [, id, statusRaw, title] = header;
|
|
92
|
+
const status = statusRaw;
|
|
93
|
+
const taskDraft = {
|
|
94
|
+
id,
|
|
95
|
+
title: title.trim(),
|
|
96
|
+
status,
|
|
97
|
+
owner: "",
|
|
98
|
+
summary: "",
|
|
99
|
+
updatedAt: nowIso(),
|
|
100
|
+
links: [],
|
|
101
|
+
roadmapRefs: [],
|
|
102
|
+
hooks: {},
|
|
103
|
+
};
|
|
104
|
+
let inLinks = false;
|
|
105
|
+
let inHooks = false;
|
|
106
|
+
for (const line of lines.slice(1)) {
|
|
107
|
+
const trimmed = line.trim();
|
|
108
|
+
if (!trimmed) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (trimmed.startsWith("- owner:")) {
|
|
112
|
+
taskDraft.owner = trimmed.replace("- owner:", "").trim();
|
|
113
|
+
inLinks = false;
|
|
114
|
+
inHooks = false;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (trimmed.startsWith("- summary:")) {
|
|
118
|
+
taskDraft.summary = trimmed.replace("- summary:", "").trim();
|
|
119
|
+
inLinks = false;
|
|
120
|
+
inHooks = false;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (trimmed.startsWith("- updatedAt:")) {
|
|
124
|
+
taskDraft.updatedAt = trimmed.replace("- updatedAt:", "").trim();
|
|
125
|
+
inLinks = false;
|
|
126
|
+
inHooks = false;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (trimmed.startsWith("- roadmapRefs:")) {
|
|
130
|
+
const payload = trimmed.replace("- roadmapRefs:", "").trim();
|
|
131
|
+
const refs = payload === "(none)"
|
|
132
|
+
? []
|
|
133
|
+
: payload
|
|
134
|
+
.split(",")
|
|
135
|
+
.map((value) => value.trim())
|
|
136
|
+
.filter((value) => value.length > 0);
|
|
137
|
+
taskDraft.roadmapRefs = refs;
|
|
138
|
+
inLinks = false;
|
|
139
|
+
inHooks = false;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (trimmed === "- links:") {
|
|
143
|
+
inLinks = true;
|
|
144
|
+
inHooks = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (trimmed === "- hooks:") {
|
|
148
|
+
inLinks = false;
|
|
149
|
+
inHooks = true;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const nestedItem = trimmed.match(/^-\s+(.+)$/);
|
|
153
|
+
if (!nestedItem) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const nestedValue = nestedItem[1].trim();
|
|
157
|
+
if (nestedValue === "(none)") {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (inLinks) {
|
|
161
|
+
taskDraft.links = [...(taskDraft.links ?? []), nestedValue];
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (inHooks) {
|
|
165
|
+
const hookMatch = nestedValue.match(/^(onAssigned|onCompleted|onBlocked|onReopened):\s+(.+)$/);
|
|
166
|
+
if (hookMatch) {
|
|
167
|
+
const [, hookKey, hookPath] = hookMatch;
|
|
168
|
+
taskDraft.hooks = {
|
|
169
|
+
...(taskDraft.hooks ?? {}),
|
|
170
|
+
[hookKey]: hookPath.trim(),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
tasks.push(normalizeTask(taskDraft));
|
|
176
|
+
}
|
|
177
|
+
return tasks;
|
|
178
|
+
}
|
|
179
|
+
export function renderTasksMarkdown(tasks) {
|
|
180
|
+
const sections = tasks.map((task) => {
|
|
181
|
+
const roadmapRefs = task.roadmapRefs.length > 0 ? task.roadmapRefs.join(", ") : "(none)";
|
|
182
|
+
const links = task.links.length > 0
|
|
183
|
+
? ["- links:", ...task.links.map((link) => ` - ${link}`)]
|
|
184
|
+
: ["- links:", " - (none)"];
|
|
185
|
+
const hookEntries = Object.entries(task.hooks)
|
|
186
|
+
.filter(([, value]) => typeof value === "string" && value.trim().length > 0)
|
|
187
|
+
.map(([key, value]) => ` - ${key}: ${value}`);
|
|
188
|
+
const hooks = hookEntries.length > 0
|
|
189
|
+
? ["- hooks:", ...hookEntries]
|
|
190
|
+
: ["- hooks:", " - (none)"];
|
|
191
|
+
return [
|
|
192
|
+
`## ${task.id} | ${task.status} | ${task.title}`,
|
|
193
|
+
`- owner: ${task.owner || "(none)"}`,
|
|
194
|
+
`- summary: ${task.summary || "(none)"}`,
|
|
195
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
196
|
+
`- roadmapRefs: ${roadmapRefs}`,
|
|
197
|
+
...links,
|
|
198
|
+
...hooks,
|
|
199
|
+
].join("\n");
|
|
200
|
+
});
|
|
201
|
+
return [
|
|
202
|
+
"# Tasks",
|
|
203
|
+
"",
|
|
204
|
+
"本文件由 Projitive MCP 维护,手动编辑请保持 Markdown 结构合法。",
|
|
205
|
+
"",
|
|
206
|
+
TASKS_START,
|
|
207
|
+
...(sections.length > 0 ? sections : ["(no tasks)"]),
|
|
208
|
+
TASKS_END,
|
|
209
|
+
"",
|
|
210
|
+
].join("\n");
|
|
211
|
+
}
|
|
212
|
+
export async function ensureTasksFile(inputPath) {
|
|
213
|
+
const governanceDir = await resolveGovernanceDir(inputPath);
|
|
214
|
+
const tasksPath = path.join(governanceDir, "tasks.md");
|
|
215
|
+
await fs.mkdir(governanceDir, { recursive: true });
|
|
216
|
+
const accessResult = await catchIt(fs.access(tasksPath));
|
|
217
|
+
if (accessResult.isError()) {
|
|
218
|
+
await fs.writeFile(tasksPath, renderTasksMarkdown([]), "utf-8");
|
|
219
|
+
}
|
|
220
|
+
return tasksPath;
|
|
221
|
+
}
|
|
222
|
+
export async function loadTasks(inputPath) {
|
|
223
|
+
const tasksPath = await ensureTasksFile(inputPath);
|
|
224
|
+
const markdown = await fs.readFile(tasksPath, "utf-8");
|
|
225
|
+
return { tasksPath, tasks: await parseTasksBlock(markdown) };
|
|
226
|
+
}
|
|
227
|
+
export async function saveTasks(tasksPath, tasks) {
|
|
228
|
+
const normalized = tasks.map((task) => normalizeTask(task));
|
|
229
|
+
await fs.writeFile(tasksPath, renderTasksMarkdown(normalized), "utf-8");
|
|
230
|
+
}
|
|
231
|
+
export function validateTransition(from, to) {
|
|
232
|
+
if (from === to) {
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
const allowed = {
|
|
236
|
+
TODO: new Set(["IN_PROGRESS", "BLOCKED"]),
|
|
237
|
+
IN_PROGRESS: new Set(["BLOCKED", "DONE"]),
|
|
238
|
+
BLOCKED: new Set(["IN_PROGRESS", "TODO"]),
|
|
239
|
+
DONE: new Set(),
|
|
240
|
+
};
|
|
241
|
+
return allowed[from].has(to);
|
|
242
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { TASKS_END, TASKS_START, isValidTaskId, normalizeTask, parseTasksBlock, rankActionableTaskCandidates, renderTasksMarkdown, taskPriority, toTaskUpdatedAtMs, validateTransition, } from "./tasks.js";
|
|
3
|
+
function buildCandidate(partial) {
|
|
4
|
+
const task = normalizeTask({
|
|
5
|
+
id: partial.id,
|
|
6
|
+
title: partial.title,
|
|
7
|
+
status: partial.status,
|
|
8
|
+
updatedAt: partial.task?.updatedAt ?? "2026-01-01T00:00:00.000Z",
|
|
9
|
+
});
|
|
10
|
+
return {
|
|
11
|
+
governanceDir: partial.governanceDir ?? "/workspace/a",
|
|
12
|
+
tasksPath: partial.tasksPath ?? "/workspace/a/tasks.md",
|
|
13
|
+
task,
|
|
14
|
+
projectScore: partial.projectScore ?? 1,
|
|
15
|
+
projectLatestUpdatedAt: partial.projectLatestUpdatedAt ?? "2026-01-01T00:00:00.000Z",
|
|
16
|
+
taskUpdatedAtMs: partial.taskUpdatedAtMs ?? toTaskUpdatedAtMs(task.updatedAt),
|
|
17
|
+
taskPriority: partial.taskPriority ?? taskPriority(task.status),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
describe("tasks module", () => {
|
|
21
|
+
it("parses markdown task block and normalizes task fields", async () => {
|
|
22
|
+
const markdown = [
|
|
23
|
+
"# Tasks",
|
|
24
|
+
TASKS_START,
|
|
25
|
+
"## TASK-0001 | TODO | hello",
|
|
26
|
+
"- owner: alice",
|
|
27
|
+
"- summary: first task",
|
|
28
|
+
"- updatedAt: 2026-02-17T00:00:00.000Z",
|
|
29
|
+
"- roadmapRefs: ROADMAP-0001",
|
|
30
|
+
"- links:",
|
|
31
|
+
" - ./designs/example.md",
|
|
32
|
+
"- hooks:",
|
|
33
|
+
" - onAssigned: ./hooks/on_task_assigned.md",
|
|
34
|
+
TASKS_END,
|
|
35
|
+
].join("\n");
|
|
36
|
+
const tasks = await parseTasksBlock(markdown);
|
|
37
|
+
expect(tasks).toHaveLength(1);
|
|
38
|
+
expect(tasks[0].id).toBe("TASK-0001");
|
|
39
|
+
expect(tasks[0].status).toBe("TODO");
|
|
40
|
+
expect(tasks[0].roadmapRefs).toEqual(["ROADMAP-0001"]);
|
|
41
|
+
expect(tasks[0].hooks).toEqual({ onAssigned: "./hooks/on_task_assigned.md" });
|
|
42
|
+
});
|
|
43
|
+
it("renders markdown containing markers", () => {
|
|
44
|
+
const task = normalizeTask({ id: "TASK-0002", title: "render", status: "IN_PROGRESS" });
|
|
45
|
+
const markdown = renderTasksMarkdown([task]);
|
|
46
|
+
expect(markdown.includes(TASKS_START)).toBe(true);
|
|
47
|
+
expect(markdown.includes(TASKS_END)).toBe(true);
|
|
48
|
+
expect(markdown.includes("## TASK-0002 | IN_PROGRESS | render")).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it("validates task IDs", () => {
|
|
51
|
+
expect(isValidTaskId("TASK-0001")).toBe(true);
|
|
52
|
+
expect(isValidTaskId("TASK-001")).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
it("allows and rejects expected transitions", () => {
|
|
55
|
+
expect(validateTransition("TODO", "IN_PROGRESS")).toBe(true);
|
|
56
|
+
expect(validateTransition("IN_PROGRESS", "DONE")).toBe(true);
|
|
57
|
+
expect(validateTransition("DONE", "IN_PROGRESS")).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it("assigns priority for actionable statuses", () => {
|
|
60
|
+
expect(taskPriority("IN_PROGRESS")).toBe(2);
|
|
61
|
+
expect(taskPriority("TODO")).toBe(1);
|
|
62
|
+
expect(taskPriority("BLOCKED")).toBe(0);
|
|
63
|
+
});
|
|
64
|
+
it("returns zero timestamp for invalid date", () => {
|
|
65
|
+
expect(toTaskUpdatedAtMs("invalid")).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
it("ranks by project score, then task priority, then recency", () => {
|
|
68
|
+
const candidates = [
|
|
69
|
+
buildCandidate({ id: "TASK-0001", title: "A", status: "TODO", projectScore: 2 }),
|
|
70
|
+
buildCandidate({ id: "TASK-0002", title: "B", status: "IN_PROGRESS", projectScore: 2 }),
|
|
71
|
+
buildCandidate({ id: "TASK-0003", title: "C", status: "IN_PROGRESS", projectScore: 3 }),
|
|
72
|
+
];
|
|
73
|
+
const ranked = rankActionableTaskCandidates(candidates);
|
|
74
|
+
expect(ranked[0].task.id).toBe("TASK-0003");
|
|
75
|
+
expect(ranked[1].task.id).toBe("TASK-0002");
|
|
76
|
+
expect(ranked[2].task.id).toBe("TASK-0001");
|
|
77
|
+
});
|
|
78
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@projitive/mcp",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "Projitive MCP Server for project and task discovery/update",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./output/index.js",
|
|
9
|
+
"types": "./output/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"build": "tsc -p tsconfig.json",
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"dev": "tsc -p tsconfig.json --watch"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"output"
|
|
18
|
+
],
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.17.5",
|
|
21
|
+
"zod": "^3.23.8"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24.3.0",
|
|
25
|
+
"tsx": "^4.20.5",
|
|
26
|
+
"typescript": "^5.9.2",
|
|
27
|
+
"vitest": "^3.2.4"
|
|
28
|
+
}
|
|
29
|
+
}
|