@projitive/mcp 2.0.1 → 2.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.
package/README.md CHANGED
@@ -21,6 +21,17 @@ Why teams use it:
21
21
  - Better evidence traceability
22
22
  - More predictable multi-agent delivery
23
23
 
24
+ ## Outcomes You Can Expect
25
+
26
+ After onboarding Projitive MCP, teams typically get these outcomes quickly:
27
+
28
+ - Faster execution bootstrapping: create missing work items with taskCreate/roadmapCreate.
29
+ - Better state integrity: task and roadmap transitions remain traceable and verifiable.
30
+ - Stronger delivery continuity: discover -> execute -> verify -> reprioritize loops stay stable.
31
+ - Easier adoption: new contributors follow a deterministic call sequence.
32
+
33
+ Key point: best results come from autonomous execution agents such as OpenClaw.
34
+
24
35
  ## What It Is Useful For
25
36
 
26
37
  Projitive MCP helps agents move work forward in governed projects without losing traceability.
@@ -82,7 +93,7 @@ Recommended minimal sequence:
82
93
 
83
94
  1. taskNext
84
95
  2. taskContext
85
- 3. taskUpdate and/or roadmapUpdate
96
+ 3. taskCreate/taskUpdate and/or roadmapCreate/roadmapUpdate
86
97
  4. taskContext
87
98
  5. taskNext
88
99
 
@@ -124,9 +135,11 @@ sequenceDiagram
124
135
  | Task | taskList | List tasks |
125
136
  | Task | taskNext | Select best actionable task |
126
137
  | Task | taskContext | Get task evidence and reading order |
138
+ | Task | taskCreate | Create task |
127
139
  | Task | taskUpdate | Update task state and metadata |
128
140
  | Roadmap | roadmapList | List roadmaps and linked tasks |
129
141
  | Roadmap | roadmapContext | Get roadmap context |
142
+ | Roadmap | roadmapCreate | Create roadmap milestone |
130
143
  | Roadmap | roadmapUpdate | Update roadmap milestone fields |
131
144
 
132
145
  ### Resources
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projitive/mcp",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Projitive MCP Server for project and task discovery/update",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -25,14 +25,11 @@
25
25
  "output"
26
26
  ],
27
27
  "dependencies": {
28
- "@duckdb/node-api": "1.5.0-r.1",
29
28
  "@modelcontextprotocol/sdk": "^1.17.5",
30
- "sql.js": "^1.14.1",
31
29
  "zod": "^3.23.8"
32
30
  },
33
31
  "devDependencies": {
34
32
  "@types/node": "^24.3.0",
35
- "@types/sql.js": "^1.4.9",
36
33
  "@vitest/coverage-v8": "^3.2.4",
37
34
  "tsx": "^4.20.5",
38
35
  "typescript": "^5.9.2",
@@ -1,13 +1,8 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import duckdb from "@duckdb/node-api";
4
- import initSqlJs from "sql.js";
5
3
  const STORE_SCHEMA_VERSION = 3;
6
- const SQL_HEADER = Buffer.from("SQLite format 3\0", "utf8");
7
- const sqlRuntimePromise = initSqlJs();
8
4
  const storeCache = new Map();
9
5
  const storeLocks = new Map();
10
- let duckdbConnectionPromise;
11
6
  function defaultViewState(name) {
12
7
  return {
13
8
  name,
@@ -37,17 +32,6 @@ function defaultStore() {
37
32
  function nowIso() {
38
33
  return new Date().toISOString();
39
34
  }
40
- function parseJsonOr(raw, fallback) {
41
- if (typeof raw !== "string" || raw.trim().length === 0) {
42
- return fallback;
43
- }
44
- try {
45
- return JSON.parse(raw);
46
- }
47
- catch {
48
- return fallback;
49
- }
50
- }
51
35
  function normalizeTaskStatus(status) {
52
36
  if (status === "IN_PROGRESS" || status === "BLOCKED" || status === "DONE") {
53
37
  return status;
@@ -57,12 +41,6 @@ function normalizeTaskStatus(status) {
57
41
  function normalizeRoadmapStatus(status) {
58
42
  return status === "done" ? "done" : "active";
59
43
  }
60
- function isSqliteBuffer(data) {
61
- if (data.length < SQL_HEADER.length) {
62
- return false;
63
- }
64
- return data.subarray(0, SQL_HEADER.length).equals(SQL_HEADER);
65
- }
66
44
  function normalizeStore(input) {
67
45
  const base = defaultStore();
68
46
  const meta = input.meta ?? {};
@@ -129,106 +107,6 @@ function normalizeStore(input) {
129
107
  migration_history: Array.isArray(input.migration_history) ? input.migration_history : [],
130
108
  };
131
109
  }
132
- async function migrateSqliteToJson(data) {
133
- const SQL = await sqlRuntimePromise;
134
- const db = new SQL.Database(new Uint8Array(data));
135
- try {
136
- const tasksResult = db.exec(`
137
- SELECT id, title, status, owner, summary, updated_at, links_json, roadmap_refs_json, sub_state_json, blocker_json, COALESCE(record_version, 1)
138
- FROM tasks
139
- `);
140
- const roadmapsResult = db.exec(`
141
- SELECT id, title, status, time, updated_at, COALESCE(record_version, 1)
142
- FROM roadmaps
143
- `);
144
- const metaResult = db.exec(`
145
- SELECT key, value
146
- FROM meta
147
- WHERE key IN ('tasks_version', 'roadmaps_version', 'store_schema_version')
148
- `);
149
- const viewStateResult = db.exec(`
150
- SELECT name, dirty, last_source_version, last_built_at, COALESCE(record_version, 1)
151
- FROM view_state
152
- WHERE name IN ('tasks_markdown', 'roadmaps_markdown')
153
- `);
154
- const tasks = tasksResult[0]?.values?.map((row) => ({
155
- id: String(row[0]),
156
- title: String(row[1]),
157
- status: normalizeTaskStatus(String(row[2])),
158
- owner: String(row[3]),
159
- summary: String(row[4]),
160
- updatedAt: String(row[5]),
161
- links: parseJsonOr(row[6], []),
162
- roadmapRefs: parseJsonOr(row[7], []),
163
- subState: parseJsonOr(row[8], undefined),
164
- blocker: parseJsonOr(row[9], undefined),
165
- recordVersion: Number(row[10]) || 1,
166
- })) ?? [];
167
- const roadmaps = roadmapsResult[0]?.values?.map((row) => ({
168
- id: String(row[0]),
169
- title: String(row[1]),
170
- status: normalizeRoadmapStatus(String(row[2])),
171
- time: row[3] == null ? undefined : String(row[3]),
172
- updatedAt: String(row[4]),
173
- recordVersion: Number(row[5]) || 1,
174
- })) ?? [];
175
- const meta = defaultStore().meta;
176
- const metaRows = metaResult[0]?.values ?? [];
177
- for (const row of metaRows) {
178
- const key = String(row[0]);
179
- const value = Number.parseInt(String(row[1]), 10);
180
- if (!Number.isFinite(value)) {
181
- continue;
182
- }
183
- if (key === "tasks_version")
184
- meta.tasks_version = value;
185
- if (key === "roadmaps_version")
186
- meta.roadmaps_version = value;
187
- if (key === "store_schema_version")
188
- meta.store_schema_version = value;
189
- }
190
- const tasksView = defaultViewState("tasks_markdown");
191
- const roadmapsView = defaultViewState("roadmaps_markdown");
192
- const viewRows = viewStateResult[0]?.values ?? [];
193
- for (const row of viewRows) {
194
- const name = String(row[0]);
195
- const dirty = Number(row[1]) === 1;
196
- const lastSourceVersion = Number(row[2]) || 0;
197
- const lastBuiltAt = String(row[3] ?? "");
198
- const recordVersion = Number(row[4]) || 1;
199
- if (name === "tasks_markdown") {
200
- tasksView.dirty = dirty;
201
- tasksView.lastSourceVersion = lastSourceVersion;
202
- tasksView.lastBuiltAt = lastBuiltAt;
203
- tasksView.recordVersion = recordVersion;
204
- }
205
- if (name === "roadmaps_markdown") {
206
- roadmapsView.dirty = dirty;
207
- roadmapsView.lastSourceVersion = lastSourceVersion;
208
- roadmapsView.lastBuiltAt = lastBuiltAt;
209
- roadmapsView.recordVersion = recordVersion;
210
- }
211
- }
212
- return normalizeStore({
213
- schema: "projitive-json-store",
214
- tasks,
215
- roadmaps,
216
- meta: {
217
- tasks_version: meta.tasks_version,
218
- roadmaps_version: meta.roadmaps_version,
219
- store_schema_version: STORE_SCHEMA_VERSION,
220
- },
221
- view_state: {
222
- tasks_markdown: tasksView,
223
- roadmaps_markdown: roadmapsView,
224
- },
225
- migration_history: [],
226
- });
227
- }
228
- finally {
229
- db.close();
230
- }
231
- }
232
110
  async function persistStore(dbPath, store) {
233
111
  await fs.mkdir(path.dirname(dbPath), { recursive: true });
234
112
  const tempPath = `${dbPath}.tmp-${process.pid}-${Date.now()}`;
@@ -241,10 +119,6 @@ async function loadStoreFromDisk(dbPath) {
241
119
  if (!file || file.length === 0) {
242
120
  return { store: defaultStore(), shouldPersist: true };
243
121
  }
244
- if (isSqliteBuffer(file)) {
245
- const migrated = await migrateSqliteToJson(file);
246
- return { store: migrated, shouldPersist: true };
247
- }
248
122
  const text = file.toString("utf8").trim();
249
123
  if (text.length === 0) {
250
124
  return { store: defaultStore(), shouldPersist: true };
@@ -337,119 +211,6 @@ function normalizeStatusForSort(status) {
337
211
  return 1;
338
212
  return 0;
339
213
  }
340
- async function getDuckdbConnection() {
341
- if (!duckdbConnectionPromise) {
342
- duckdbConnectionPromise = (async () => {
343
- const instance = await duckdb.DuckDBInstance.create(":memory:");
344
- return instance.connect();
345
- })();
346
- }
347
- return duckdbConnectionPromise;
348
- }
349
- function normalizeStoredTaskLike(raw) {
350
- if (!raw || typeof raw !== "object") {
351
- return null;
352
- }
353
- const value = raw;
354
- const id = value.id;
355
- const title = value.title;
356
- if (typeof id !== "string" || typeof title !== "string") {
357
- return null;
358
- }
359
- const statusRaw = typeof value.status === "string" ? value.status : "TODO";
360
- const owner = typeof value.owner === "string" ? value.owner : "";
361
- const summary = typeof value.summary === "string" ? value.summary : "";
362
- const updatedAt = typeof value.updatedAt === "string"
363
- ? value.updatedAt
364
- : (typeof value.updated_at === "string" ? value.updated_at : nowIso());
365
- const links = Array.isArray(value.links) ? value.links.map((item) => String(item)) : [];
366
- const roadmapRefs = Array.isArray(value.roadmapRefs)
367
- ? value.roadmapRefs.map((item) => String(item))
368
- : (Array.isArray(value.roadmap_refs) ? value.roadmap_refs.map((item) => String(item)) : []);
369
- const recordVersionRaw = value.recordVersion ?? value.record_version;
370
- const recordVersion = Number.isFinite(Number(recordVersionRaw)) ? Number(recordVersionRaw) : 1;
371
- return {
372
- id,
373
- title,
374
- status: normalizeTaskStatus(statusRaw),
375
- owner,
376
- summary,
377
- updatedAt,
378
- links,
379
- roadmapRefs,
380
- subState: value.subState,
381
- blocker: value.blocker,
382
- recordVersion,
383
- };
384
- }
385
- function normalizeStoredRoadmapLike(raw) {
386
- if (!raw || typeof raw !== "object") {
387
- return null;
388
- }
389
- const value = raw;
390
- const id = value.id;
391
- const title = value.title;
392
- if (typeof id !== "string" || typeof title !== "string") {
393
- return null;
394
- }
395
- const statusRaw = typeof value.status === "string" ? value.status : "active";
396
- const time = typeof value.time === "string" ? value.time : undefined;
397
- const updatedAt = typeof value.updatedAt === "string"
398
- ? value.updatedAt
399
- : (typeof value.updated_at === "string" ? value.updated_at : nowIso());
400
- const recordVersionRaw = value.recordVersion ?? value.record_version;
401
- const recordVersion = Number.isFinite(Number(recordVersionRaw)) ? Number(recordVersionRaw) : 1;
402
- return {
403
- id,
404
- title,
405
- status: normalizeRoadmapStatus(statusRaw),
406
- time,
407
- updatedAt,
408
- recordVersion,
409
- };
410
- }
411
- async function runDuckdbQuery(sql) {
412
- try {
413
- const connection = await getDuckdbConnection();
414
- const result = await connection.run(sql);
415
- const rows = await result.getRowObjectsJS();
416
- if (!rows || rows.length === 0) {
417
- return undefined;
418
- }
419
- return rows;
420
- }
421
- catch {
422
- return undefined;
423
- }
424
- }
425
- async function loadTasksFromDuckdb(dbPath) {
426
- const sql = `SELECT tasks FROM read_json_auto('${dbPath.replace(/'/g, "''")}') LIMIT 1;`;
427
- const rows = await runDuckdbQuery(sql);
428
- if (!rows || rows.length === 0) {
429
- return undefined;
430
- }
431
- const rawTasks = rows[0]?.tasks;
432
- if (!Array.isArray(rawTasks)) {
433
- return undefined;
434
- }
435
- return rawTasks
436
- .map((item) => normalizeStoredTaskLike(item))
437
- .filter((item) => item != null);
438
- }
439
- async function loadRoadmapsFromDuckdb(dbPath) {
440
- const sql = `SELECT roadmaps FROM read_json_auto('${dbPath.replace(/'/g, "''")}') LIMIT 1;`;
441
- const rows = await runDuckdbQuery(sql);
442
- if (!rows || rows.length === 0) {
443
- return undefined;
444
- }
445
- const rawRoadmaps = rows[0]?.roadmaps;
446
- if (!Array.isArray(rawRoadmaps)) {
447
- return undefined;
448
- }
449
- return rawRoadmaps
450
- .map((item) => normalizeStoredRoadmapLike(item))
451
- .filter((item) => item != null);
452
- }
453
214
  export async function ensureStore(dbPath) {
454
215
  await openStore(dbPath);
455
216
  }
@@ -487,11 +248,8 @@ export async function markMarkdownViewDirty(dbPath, viewName) {
487
248
  });
488
249
  }
489
250
  export async function loadTasksFromStore(dbPath) {
490
- const tasksFromDuckdb = await loadTasksFromDuckdb(dbPath);
491
- if (!tasksFromDuckdb) {
492
- throw new Error("DuckDB task query failed");
493
- }
494
- return tasksFromDuckdb.map(toPublicTask);
251
+ const store = await openStore(dbPath);
252
+ return store.tasks.map(toPublicTask);
495
253
  }
496
254
  export async function loadTaskStatusStatsFromStore(dbPath) {
497
255
  const tasks = await loadTasksFromStore(dbPath);
@@ -568,11 +326,8 @@ export async function replaceTasksInStore(dbPath, tasks) {
568
326
  });
569
327
  }
570
328
  export async function loadRoadmapsFromStore(dbPath) {
571
- const roadmapsFromDuckdb = await loadRoadmapsFromDuckdb(dbPath);
572
- if (!roadmapsFromDuckdb) {
573
- throw new Error("DuckDB roadmap query failed");
574
- }
575
- return roadmapsFromDuckdb.map(toPublicRoadmap);
329
+ const store = await openStore(dbPath);
330
+ return store.roadmaps.map(toPublicRoadmap);
576
331
  }
577
332
  export async function loadRoadmapIdsFromStore(dbPath) {
578
333
  const roadmaps = await loadRoadmapsFromStore(dbPath);
@@ -18,7 +18,7 @@ const MCP_RUNTIME_VERSION = typeof packageJson.version === "string" && packageJs
18
18
  const server = new McpServer({
19
19
  name: "projitive",
20
20
  version: MCP_RUNTIME_VERSION,
21
- description: "Semantic Projitive MCP for project/task discovery and agent guidance with sqlite-first governance outputs",
21
+ description: "Semantic Projitive MCP for project/task discovery and agent guidance with governance-store-first outputs",
22
22
  });
23
23
  // 注册所有模块
24
24
  registerTools(server);
@@ -54,7 +54,7 @@ export function registerQuickStartPrompt(server) {
54
54
  "",
55
55
  "### Option A: Auto-select (Recommended)",
56
56
  "Call `taskNext()` to get highest-priority actionable task.",
57
- "If no actionable tasks are returned and roadmap has active goals, analyze roadmap context and create 1-3 TODO tasks manually.",
57
+ "If no actionable tasks are returned and roadmap has active goals, analyze roadmap context and create 1-3 TODO tasks via `taskCreate()`.",
58
58
  "Then call `taskNext()` again to re-rank.",
59
59
  "",
60
60
  "### Option B: Manual select",
@@ -67,9 +67,9 @@ export function registerQuickStartPrompt(server) {
67
67
  "After getting task context:",
68
68
  "1. Read evidence links in Suggested Read Order",
69
69
  "2. Understand task requirements and acceptance criteria",
70
- "3. Write governance source via tools (`taskUpdate` / `roadmapUpdate`) instead of editing tasks.md/roadmap.md directly",
70
+ "3. Write governance source via tools (`taskCreate` / `taskUpdate` / `roadmapCreate` / `roadmapUpdate`) instead of editing tasks.md/roadmap.md directly",
71
71
  "4. Update docs (`designs/` / `reports/`) as required by evidence",
72
- "5. If immediate markdown snapshots are needed, call `syncViews(projectPath=..., force=true)`",
72
+ "5. taskCreate/taskUpdate/roadmapCreate/roadmapUpdate will auto-sync corresponding markdown views",
73
73
  "6. Update task status:",
74
74
  " - TODO → IN_PROGRESS (when starting execution)",
75
75
  " - IN_PROGRESS → DONE (when completed)",
@@ -80,7 +80,7 @@ export function registerQuickStartPrompt(server) {
80
80
  "",
81
81
  "Keep this loop until no high-value actionable work remains:",
82
82
  "1. Discover: `taskNext()`",
83
- "2. Execute: update sqlite + docs + report evidence",
83
+ "2. Execute: update governance store + docs + report evidence",
84
84
  "3. Verify: `taskContext()`",
85
85
  "4. Re-prioritize: `taskNext()`",
86
86
  "",
@@ -105,14 +105,14 @@ export function registerQuickStartPrompt(server) {
105
105
  " - Distinct scope: avoid overlap with existing DONE/BLOCKED tasks",
106
106
  "4. Prefer unblocking tasks that unlock multiple follow-up tasks",
107
107
  "5. Re-run `taskNext()` to pick the new tasks",
108
- "6. If still no tasks, read design documents in projitive://designs/ and create TODO tasks manually",
108
+ "6. If still no tasks, read design documents in projitive://designs/ and create TODO tasks via `taskCreate()`",
109
109
  "",
110
110
  "## Hard Rules",
111
111
  "",
112
112
  "- **NEVER modify TASK/ROADMAP IDs** - Keep them immutable once assigned",
113
113
  "- **Every status transition must have report evidence** - Create execution reports in reports/ directory",
114
- "- **SQLite is source of truth** - tasks.md/roadmap.md are generated views and may be overwritten",
115
- "- **Prefer tool writes over manual table/view edits** - Use taskUpdate/roadmapUpdate/syncViews",
114
+ "- **.projitive governance store is source of truth** - tasks.md/roadmap.md are generated views and may be overwritten",
115
+ "- **Prefer tool writes over manual table/view edits** - Use taskCreate/taskUpdate/roadmapCreate/roadmapUpdate",
116
116
  "- **Always verify after updates** - Re-run taskContext() to confirm reference consistency",
117
117
  ].join("\n");
118
118
  return asUserPrompt(text);
@@ -53,7 +53,7 @@ export function registerTaskDiscoveryPrompt(server) {
53
53
  "",
54
54
  "### Artifacts",
55
55
  "Check if these files exist:",
56
- "- \u2705 .projitive - SQLite governance database (required)",
56
+ "- \u2705 .projitive - Governance store file (required)",
57
57
  "- \u2705 tasks.md - Generated task view",
58
58
  "- \u2705 roadmap.md - Generated roadmap view",
59
59
  "- \u2705 README.md - Project description",
@@ -145,7 +145,7 @@ export function registerTaskDiscoveryPrompt(server) {
145
145
  " - Reference history reports in reports/",
146
146
  "",
147
147
  "3. **Execute task content**",
148
- " - Update sqlite task/roadmap tables via MCP tools; edit designs/*.md and reports/*.md as needed",
148
+ " - Update governance store via MCP tools; edit designs/*.md and reports/*.md as needed",
149
149
  " - If immediate markdown snapshots are required by another agent/session, call syncViews(force=true)",
150
150
  " - Create execution report (reports/ directory)",
151
151
  " - Update task status to DONE",
@@ -163,11 +163,13 @@ export function registerTaskDiscoveryPrompt(server) {
163
163
  "1. Check if .projitive exists",
164
164
  "2. Analyze active roadmap milestones and derive 1-3 executable TODO tasks",
165
165
  "3. Prioritize milestone slices that unblock multiple downstream tasks",
166
- "4. Re-run taskNext() and continue execution if tasks are available",
167
- "5. If still empty, read design documents in designs/ directory",
168
- "6. Read roadmap.md to understand project goals",
169
- "7. Create 1-3 new TODO tasks",
170
- "8. Each task must have:",
166
+ "3.5 If roadmap milestone is missing, create it first via `roadmapCreate()`",
167
+ "4. Create tasks via `taskCreate(projectPath=\"...\", taskId=\"TASK-xxxx\", title=\"...\", roadmapRefs=[\"ROADMAP-xxxx\"], summary=\"...\")`",
168
+ "5. Re-run taskNext() and continue execution if tasks are available",
169
+ "6. If still empty, read design documents in designs/ directory",
170
+ "7. Read roadmap.md to understand project goals",
171
+ "8. Create 1-3 new TODO tasks",
172
+ "9. Each task must have:",
171
173
  " - Unique TASK-xxxx ID",
172
174
  " - Clear title",
173
175
  " - Detailed summary",
@@ -177,7 +179,7 @@ export function registerTaskDiscoveryPrompt(server) {
177
179
  "",
178
180
  "1. Select one blocked task and call `taskContext()`",
179
181
  "2. Understand blocker reason (check blocker field, Spec v1.1.0)",
180
- "3. Create a new TODO task to resolve blocker",
182
+ "3. Create a new TODO task via `taskCreate()` to resolve blocker",
181
183
  "4. Execute this new task first",
182
184
  "",
183
185
  "### Case 3: Missing required governance files",
@@ -196,8 +198,10 @@ export function registerTaskDiscoveryPrompt(server) {
196
198
  "| `taskNext()` | Auto-select best task |",
197
199
  "| `taskList()` | List all tasks |",
198
200
  "| `taskContext()` | Get task details |",
201
+ "| `taskCreate()` | Create a new task |",
199
202
  "| `taskUpdate()` | Update task status |",
200
- "| `syncViews()` | Force materialize tasks.md/roadmap.md from sqlite |",
203
+ "| `roadmapCreate()` | Create a new roadmap milestone |",
204
+ "| `syncViews()` | Force materialize tasks.md/roadmap.md from governance store |",
201
205
  ].join("\n");
202
206
  return asUserPrompt(text);
203
207
  });
@@ -70,12 +70,12 @@ export function registerTaskExecutionPrompt(server) {
70
70
  " - Fill subState (optional, Spec v1.1.0)",
71
71
  "",
72
72
  "2. **Execute task content**",
73
- " - Prefer MCP tool writes for sqlite source (`taskUpdate` / `roadmapUpdate`)",
73
+ " - Prefer MCP tool writes for governance store (`taskUpdate` / `roadmapUpdate`)",
74
74
  " - .projitive (task table) - Update task status and metadata",
75
75
  " - designs/*.md - Add or update design documents",
76
76
  " - reports/*.md - Create execution reports",
77
77
  " - .projitive (roadmap table) - Update roadmap (if needed)",
78
- " - If downstream consumers require immediate markdown snapshots, call `syncViews(..., force=true)`",
78
+ " - taskCreate/taskUpdate/roadmapCreate/roadmapUpdate auto-sync corresponding markdown views",
79
79
  "",
80
80
  "3. **Create execution report**",
81
81
  " - Create new report file in reports/ directory",
@@ -125,15 +125,16 @@ export function registerTaskExecutionPrompt(server) {
125
125
  " - type: Blocker type (dependency/missing-info/technical-debt/other)",
126
126
  " - description: Blocker description",
127
127
  " - relatedLinks: Related links (optional)",
128
- "3. Create a new TODO task to resolve blocker",
128
+ "3. Create a new TODO task via `taskCreate()` to resolve blocker",
129
129
  "",
130
130
  "### Case 2: No actionable tasks",
131
131
  "",
132
132
  "If taskNext() returns empty:",
133
133
  "1. Call `projectContext()` to recheck project state",
134
134
  "2. Analyze active roadmap milestones and find smallest executable slices",
135
+ "2.5 If roadmap milestones are missing, create them via `roadmapCreate()`",
135
136
  "3. Read design documents in designs/ directory to find delivery gaps",
136
- "4. Create 1-3 new TODO tasks only if each task has clear done condition and evidence output",
137
+ "4. Create 1-3 new TODO tasks via `taskCreate()` only if each task has clear done condition and evidence output",
137
138
  "5. Re-run `taskNext()` and continue",
138
139
  "",
139
140
  "### Case 3: Need to initialize governance",
@@ -153,7 +154,7 @@ export function registerTaskExecutionPrompt(server) {
153
154
  " - IN_PROGRESS \u2192 DONE: Report REQUIRED",
154
155
  " - IN_PROGRESS \u2192 BLOCKED: Report recommended to explain blocker",
155
156
  "",
156
- "3. **SQLite-first writes only**",
157
+ "3. **Governance-store-first writes only**",
157
158
  " - tasks.md/roadmap.md are generated views, not authoritative source",
158
159
  " - Use taskUpdate/roadmapUpdate as primary write path",
159
160
  "",
@@ -15,7 +15,7 @@ export function registerGovernanceResources(server, repoRoot) {
15
15
  }));
16
16
  server.registerResource("governanceTasks", "projitive://governance/tasks", {
17
17
  title: "Governance Tasks",
18
- description: "Current task pool markdown view generated from .projitive sqlite task table",
18
+ description: "Current task pool markdown view generated from .projitive governance store",
19
19
  mimeType: "text/markdown",
20
20
  }, async () => ({
21
21
  contents: [
@@ -27,7 +27,7 @@ export function registerGovernanceResources(server, repoRoot) {
27
27
  }));
28
28
  server.registerResource("governanceRoadmap", "projitive://governance/roadmap", {
29
29
  title: "Governance Roadmap",
30
- description: "Current roadmap markdown view generated from .projitive sqlite roadmap table",
30
+ description: "Current roadmap markdown view generated from .projitive governance store",
31
31
  mimeType: "text/markdown",
32
32
  }, async () => ({
33
33
  contents: [
@@ -255,9 +255,11 @@ const DEFAULT_TOOL_TEMPLATE_NAMES = [
255
255
  "taskList",
256
256
  "taskNext",
257
257
  "taskContext",
258
+ "taskCreate",
258
259
  "taskUpdate",
259
260
  "roadmapList",
260
261
  "roadmapContext",
262
+ "roadmapCreate",
261
263
  "roadmapUpdate",
262
264
  ];
263
265
  async function pathExists(targetPath) {
@@ -279,8 +281,8 @@ function defaultReadmeMarkdown(governanceDirName) {
279
281
  `This directory (\`${governanceDirName}/\`) is the governance root for this project.`,
280
282
  "",
281
283
  "## Conventions",
282
- "- Keep roadmap/task source of truth in .projitive sqlite tables.",
283
- "- Treat roadmap.md/tasks.md as generated views from sqlite.",
284
+ "- Keep roadmap/task source of truth in .projitive governance store.",
285
+ "- Treat roadmap.md/tasks.md as generated views from governance store.",
284
286
  "- Keep IDs stable (TASK-xxxx / ROADMAP-xxxx).",
285
287
  "- Update report evidence before status transitions.",
286
288
  ].join("\n");
@@ -435,7 +437,7 @@ export function registerProjectTools(server) {
435
437
  ]),
436
438
  lintSection([
437
439
  "- After init, fill owner/roadmapRefs/links in .projitive task table before marking DONE.",
438
- "- Keep task source-of-truth inside sqlite tables.",
440
+ "- Keep task source-of-truth inside .projitive governance store.",
439
441
  ]),
440
442
  nextCallSection(`projectContext(projectPath=\"${initialized.projectPath}\")`),
441
443
  ],
@@ -449,7 +451,8 @@ export function registerProjectTools(server) {
449
451
  }, async () => {
450
452
  const roots = resolveScanRoots();
451
453
  const depth = resolveScanDepth();
452
- const projects = await discoverProjectsAcrossRoots(roots, depth);
454
+ const governanceDirs = await discoverProjectsAcrossRoots(roots, depth);
455
+ const projects = Array.from(new Set(governanceDirs.map((governanceDir) => toProjectPath(governanceDir)))).sort();
453
456
  const markdown = renderToolResponseMarkdown({
454
457
  toolName: "projectScan",
455
458
  sections: [
@@ -529,7 +532,7 @@ export function registerProjectTools(server) {
529
532
  ]),
530
533
  evidenceSection([
531
534
  "- rankedProjects:",
532
- ...ranked.map((item, index) => `${index + 1}. ${item.governanceDir} | actionable=${item.actionable} | in_progress=${item.inProgress} | todo=${item.todo} | blocked=${item.blocked} | done=${item.done} | latest=${item.latestUpdatedAt}${item.tasksExists ? "" : " | store=missing"}`),
535
+ ...ranked.map((item, index) => `${index + 1}. ${toProjectPath(item.governanceDir)} | actionable=${item.actionable} | in_progress=${item.inProgress} | todo=${item.todo} | blocked=${item.blocked} | done=${item.done} | latest=${item.latestUpdatedAt}${item.tasksExists ? "" : " | store=missing"}`),
533
536
  ]),
534
537
  guidanceSection([
535
538
  "- Pick top 1 project and call `projectContext` with its projectPath.",
@@ -571,7 +574,7 @@ export function registerProjectTools(server) {
571
574
  });
572
575
  server.registerTool("syncViews", {
573
576
  title: "Sync Views",
574
- description: "Materialize markdown views from .projitive sqlite tables (tasks.md / roadmap.md)",
577
+ description: "Materialize markdown views from .projitive governance store (tasks.md / roadmap.md)",
575
578
  inputSchema: {
576
579
  projectPath: z.string(),
577
580
  views: z.array(z.enum(["tasks", "roadmap"])).optional(),
@@ -579,6 +582,7 @@ export function registerProjectTools(server) {
579
582
  },
580
583
  }, async ({ projectPath, views, force }) => {
581
584
  const governanceDir = await resolveGovernanceDir(projectPath);
585
+ const normalizedProjectPath = toProjectPath(governanceDir);
582
586
  const dbPath = path.join(governanceDir, PROJECT_MARKER);
583
587
  const selectedViews = views && views.length > 0
584
588
  ? Array.from(new Set(views))
@@ -606,6 +610,7 @@ export function registerProjectTools(server) {
606
610
  toolName: "syncViews",
607
611
  sections: [
608
612
  summarySection([
613
+ `- projectPath: ${normalizedProjectPath}`,
609
614
  `- governanceDir: ${governanceDir}`,
610
615
  `- views: ${selectedViews.join(", ")}`,
611
616
  `- force: ${forceSync ? "true" : "false"}`,
@@ -619,7 +624,7 @@ export function registerProjectTools(server) {
619
624
  "Routine workflows can rely on lazy sync and usually do not require force=true.",
620
625
  ]),
621
626
  lintSection([]),
622
- nextCallSection(`projectContext(projectPath="${toProjectPath(governanceDir)}")`),
627
+ nextCallSection(`projectContext(projectPath="${normalizedProjectPath}")`),
623
628
  ],
624
629
  });
625
630
  return asText(markdown);
@@ -340,5 +340,27 @@ describe("projitive module", () => {
340
340
  expect(() => registerProjectTools(mockServer)).not.toThrow();
341
341
  expect(mockServer.registerTool).toHaveBeenCalled();
342
342
  });
343
+ it("projectScan lists project root paths instead of governance directories", async () => {
344
+ const root = await createTempDir();
345
+ const projectRoot = path.join(root, "app");
346
+ const governanceDir = path.join(projectRoot, ".projitive");
347
+ const templateDir = await createTempDir();
348
+ await fs.mkdir(governanceDir, { recursive: true });
349
+ await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
350
+ vi.stubEnv("PROJITIVE_SCAN_ROOT_PATHS", root);
351
+ vi.stubEnv("PROJITIVE_SCAN_MAX_DEPTH", "3");
352
+ vi.stubEnv("PROJITIVE_MESSAGE_TEMPLATE_PATH", templateDir);
353
+ const mockServer = {
354
+ registerTool: vi.fn(),
355
+ };
356
+ registerProjectTools(mockServer);
357
+ const projectScanCall = mockServer.registerTool.mock.calls.find((call) => call[0] === "projectScan");
358
+ expect(projectScanCall).toBeTruthy();
359
+ const projectScanHandler = projectScanCall?.[2];
360
+ const result = await projectScanHandler();
361
+ const markdown = result.content[0]?.text ?? "";
362
+ expect(markdown).toContain(`1. ${projectRoot}`);
363
+ expect(markdown).not.toContain(`1. ${governanceDir}`);
364
+ });
343
365
  });
344
366
  });
@@ -10,6 +10,17 @@ export const ROADMAP_MARKDOWN_FILE = "roadmap.md";
10
10
  function nowIso() {
11
11
  return new Date().toISOString();
12
12
  }
13
+ function nextRoadmapId(milestones) {
14
+ const maxSuffix = milestones
15
+ .map((item) => toRoadmapIdNumericSuffix(item.id))
16
+ .filter((value) => Number.isFinite(value) && value >= 0)
17
+ .reduce((max, value) => Math.max(max, value), 0);
18
+ const next = maxSuffix + 1;
19
+ if (next > 9999) {
20
+ throw new Error("ROADMAP ID overflow: maximum supported ID is ROADMAP-9999");
21
+ }
22
+ return `ROADMAP-${String(next).padStart(4, "0")}`;
23
+ }
13
24
  function toRoadmapIdNumericSuffix(roadmapId) {
14
25
  const match = roadmapId.match(/^(?:ROADMAP-)(\d{4})$/);
15
26
  if (!match) {
@@ -53,7 +64,7 @@ export function renderRoadmapMarkdown(milestones) {
53
64
  return [
54
65
  "# Roadmap",
55
66
  "",
56
- "This file is generated from .projitive sqlite tables by Projitive MCP. Manual edits will be overwritten.",
67
+ "This file is generated from .projitive governance store by Projitive MCP. Manual edits will be overwritten.",
57
68
  "",
58
69
  "## Active Milestones",
59
70
  ...(lines.length > 0 ? lines : ["- (no milestones)"]),
@@ -161,6 +172,7 @@ export function registerRoadmapTools(server) {
161
172
  },
162
173
  }, async ({ projectPath }) => {
163
174
  const governanceDir = await resolveGovernanceDir(projectPath);
175
+ const normalizedProjectPath = toProjectPath(governanceDir);
164
176
  const roadmapIds = await loadRoadmapIds(governanceDir);
165
177
  const { tasks } = await loadTasks(governanceDir);
166
178
  const lintSuggestions = collectRoadmapLintSuggestions(roadmapIds, tasks);
@@ -168,6 +180,7 @@ export function registerRoadmapTools(server) {
168
180
  toolName: "roadmapList",
169
181
  sections: [
170
182
  summarySection([
183
+ `- projectPath: ${normalizedProjectPath}`,
171
184
  `- governanceDir: ${governanceDir}`,
172
185
  `- roadmapCount: ${roadmapIds.length}`,
173
186
  ]),
@@ -202,6 +215,7 @@ export function registerRoadmapTools(server) {
202
215
  };
203
216
  }
204
217
  const governanceDir = await resolveGovernanceDir(projectPath);
218
+ const normalizedProjectPath = toProjectPath(governanceDir);
205
219
  const artifacts = await discoverGovernanceArtifacts(governanceDir);
206
220
  const fileCandidates = candidateFilesFromArtifacts(artifacts);
207
221
  const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, roadmapId)))).flat();
@@ -221,6 +235,7 @@ export function registerRoadmapTools(server) {
221
235
  toolName: "roadmapContext",
222
236
  sections: [
223
237
  summarySection([
238
+ `- projectPath: ${normalizedProjectPath}`,
224
239
  `- governanceDir: ${governanceDir}`,
225
240
  `- roadmapId: ${roadmapId}`,
226
241
  `- relatedTasks: ${relatedTasks.length}`,
@@ -244,9 +259,75 @@ export function registerRoadmapTools(server) {
244
259
  });
245
260
  return asText(markdown);
246
261
  });
262
+ server.registerTool("roadmapCreate", {
263
+ title: "Roadmap Create",
264
+ description: "Create one roadmap milestone in governance store",
265
+ inputSchema: {
266
+ projectPath: z.string(),
267
+ roadmapId: z.string().optional(),
268
+ title: z.string(),
269
+ status: z.enum(["active", "done"]).optional(),
270
+ time: z.string().optional(),
271
+ },
272
+ }, async ({ projectPath, roadmapId, title, status, time }) => {
273
+ if (roadmapId && !isValidRoadmapId(roadmapId)) {
274
+ return {
275
+ ...asText(renderErrorMarkdown("roadmapCreate", `Invalid roadmap ID format: ${roadmapId}`, ["expected format: ROADMAP-0001", "omit roadmapId to auto-generate next ID"], `roadmapCreate(projectPath=\"${projectPath}\", title=\"Define milestone\", time=\"2026-Q2\")`)),
276
+ isError: true,
277
+ };
278
+ }
279
+ const governanceDir = await resolveGovernanceDir(projectPath);
280
+ const normalizedProjectPath = toProjectPath(governanceDir);
281
+ const doc = await loadRoadmapDocument(governanceDir);
282
+ const finalRoadmapId = roadmapId ?? nextRoadmapId(doc.milestones);
283
+ const duplicated = doc.milestones.some((item) => item.id === finalRoadmapId);
284
+ if (duplicated) {
285
+ return {
286
+ ...asText(renderErrorMarkdown("roadmapCreate", `Roadmap milestone already exists: ${finalRoadmapId}`, ["roadmap IDs must be unique", "use roadmapUpdate for existing milestone"], `roadmapUpdate(projectPath=\"${normalizedProjectPath}\", roadmapId=\"${finalRoadmapId}\", updates={...})`)),
287
+ isError: true,
288
+ };
289
+ }
290
+ const created = normalizeMilestone({
291
+ id: finalRoadmapId,
292
+ title,
293
+ status: status ?? "active",
294
+ time,
295
+ updatedAt: nowIso(),
296
+ });
297
+ await upsertRoadmapInStore(doc.roadmapPath, created);
298
+ const refreshed = await loadRoadmapDocumentWithOptions(governanceDir, true);
299
+ const { tasks } = await loadTasks(governanceDir);
300
+ const lintSuggestions = collectRoadmapLintSuggestions(refreshed.milestones.map((item) => item.id), tasks);
301
+ const markdown = renderToolResponseMarkdown({
302
+ toolName: "roadmapCreate",
303
+ sections: [
304
+ summarySection([
305
+ `- projectPath: ${normalizedProjectPath}`,
306
+ `- governanceDir: ${governanceDir}`,
307
+ `- roadmapId: ${created.id}`,
308
+ `- status: ${created.status}`,
309
+ `- updatedAt: ${created.updatedAt}`,
310
+ ]),
311
+ evidenceSection([
312
+ "### Created Milestone",
313
+ `- ${created.id} | ${created.status} | ${created.title}${created.time ? ` | time=${created.time}` : ""}`,
314
+ "",
315
+ "### Roadmap Count",
316
+ `- total: ${refreshed.milestones.length}`,
317
+ ]),
318
+ guidanceSection([
319
+ "Milestone created successfully and roadmap.md has been synced.",
320
+ "Re-run roadmapContext to verify linked task traceability.",
321
+ ]),
322
+ lintSection(lintSuggestions),
323
+ nextCallSection(`roadmapContext(projectPath=\"${normalizedProjectPath}\", roadmapId=\"${created.id}\")`),
324
+ ],
325
+ });
326
+ return asText(markdown);
327
+ });
247
328
  server.registerTool("roadmapUpdate", {
248
329
  title: "Roadmap Update",
249
- description: "Update one roadmap milestone fields incrementally in sqlite table",
330
+ description: "Update one roadmap milestone fields incrementally in governance store",
250
331
  inputSchema: {
251
332
  projectPath: z.string(),
252
333
  roadmapId: z.string(),
@@ -264,6 +345,7 @@ export function registerRoadmapTools(server) {
264
345
  };
265
346
  }
266
347
  const governanceDir = await resolveGovernanceDir(projectPath);
348
+ const normalizedProjectPath = toProjectPath(governanceDir);
267
349
  const doc = await loadRoadmapDocument(governanceDir);
268
350
  const existing = doc.milestones.find((item) => item.id === roadmapId);
269
351
  if (!existing) {
@@ -285,6 +367,7 @@ export function registerRoadmapTools(server) {
285
367
  toolName: "roadmapUpdate",
286
368
  sections: [
287
369
  summarySection([
370
+ `- projectPath: ${normalizedProjectPath}`,
288
371
  `- governanceDir: ${governanceDir}`,
289
372
  `- roadmapId: ${roadmapId}`,
290
373
  `- newStatus: ${updated.status}`,
@@ -298,10 +381,9 @@ export function registerRoadmapTools(server) {
298
381
  `- total: ${refreshed.milestones.length}`,
299
382
  ]),
300
383
  guidanceSection([
301
- "Milestone updated successfully.",
384
+ "Milestone updated successfully and roadmap.md has been synced.",
302
385
  "Re-run roadmapContext to verify linked task traceability.",
303
- "SQLite is source of truth; roadmap.md is a generated view and may be overwritten.",
304
- "Call `syncViews(projectPath=..., views=[\"roadmap\"], force=true)` when immediate markdown materialization is required.",
386
+ ".projitive governance store is source of truth; roadmap.md is a generated view and may be overwritten.",
305
387
  ]),
306
388
  lintSection([]),
307
389
  nextCallSection(`roadmapContext(projectPath="${toProjectPath(governanceDir)}", roadmapId="${roadmapId}")`),
@@ -1,8 +1,8 @@
1
- import { describe, it, expect, beforeAll, afterAll } from "vitest";
1
+ import { describe, it, expect, beforeAll, afterAll, vi } from "vitest";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import os from "node:os";
5
- import { isValidRoadmapId, collectRoadmapLintSuggestions, loadRoadmapDocument, renderRoadmapMarkdown } from "./roadmap.js";
5
+ import { isValidRoadmapId, collectRoadmapLintSuggestions, loadRoadmapDocument, renderRoadmapMarkdown, registerRoadmapTools } from "./roadmap.js";
6
6
  describe("roadmap module", () => {
7
7
  let tempDir;
8
8
  beforeAll(async () => {
@@ -45,7 +45,7 @@ describe("roadmap module", () => {
45
45
  const suggestions = collectRoadmapLintSuggestions(["ROADMAP-0001"], tasks);
46
46
  expect(suggestions.some(s => s.includes("TASK_REFS_EMPTY"))).toBe(true);
47
47
  });
48
- it("loads from sqlite and rewrites roadmap markdown view", async () => {
48
+ it("loads from governance store and rewrites roadmap markdown view", async () => {
49
49
  const governanceDir = path.join(tempDir, ".projitive-db");
50
50
  await fs.mkdir(governanceDir, { recursive: true });
51
51
  await fs.writeFile(path.join(governanceDir, ".projitive"), "", "utf-8");
@@ -53,7 +53,7 @@ describe("roadmap module", () => {
53
53
  expect(doc.roadmapPath.endsWith(".projitive")).toBe(true);
54
54
  expect(doc.markdownPath.endsWith("roadmap.md")).toBe(true);
55
55
  const markdown = await fs.readFile(path.join(governanceDir, "roadmap.md"), "utf-8");
56
- expect(markdown).toContain("generated from .projitive sqlite tables");
56
+ expect(markdown).toContain("generated from .projitive governance store");
57
57
  });
58
58
  it("renders milestones in newest-first order", () => {
59
59
  const markdown = renderRoadmapMarkdown([
@@ -63,5 +63,13 @@ describe("roadmap module", () => {
63
63
  expect(markdown.indexOf("ROADMAP-0002")).toBeLessThan(markdown.indexOf("ROADMAP-0001"));
64
64
  expect(markdown).toContain("[x] ROADMAP-0002");
65
65
  });
66
+ it("registers roadmapCreate tool", () => {
67
+ const mockServer = {
68
+ registerTool: (..._args) => undefined,
69
+ };
70
+ const spy = vi.spyOn(mockServer, "registerTool");
71
+ registerRoadmapTools(mockServer);
72
+ expect(spy.mock.calls.some((call) => call[0] === "roadmapCreate")).toBe(true);
73
+ });
66
74
  });
67
75
  });
@@ -39,6 +39,7 @@ function taskStatusGuidance(task) {
39
39
  }
40
40
  const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
41
41
  "- Recheck project state first: run projectContext and confirm there is truly no TODO/IN_PROGRESS task to execute.",
42
+ "- Create new tasks via `taskCreate(...)` (do not edit tasks.md directly).",
42
43
  "- If all remaining tasks are BLOCKED, create one unblock task with explicit unblock condition and dependency owner.",
43
44
  "- Start from active roadmap milestones and split into the smallest executable slices with a single done condition each.",
44
45
  "- Prefer slices that unlock multiple downstream tasks before isolated refactors or low-impact cleanups.",
@@ -546,7 +547,7 @@ export function renderTasksMarkdown(tasks) {
546
547
  return [
547
548
  "# Tasks",
548
549
  "",
549
- "This file is generated from .projitive sqlite tables by Projitive MCP. Manual edits will be overwritten.",
550
+ "This file is generated from .projitive governance store by Projitive MCP. Manual edits will be overwritten.",
550
551
  "",
551
552
  ...(sections.length > 0 ? sections : ["(no tasks)"]),
552
553
  "",
@@ -605,6 +606,7 @@ export function registerTaskTools(server) {
605
606
  },
606
607
  }, async ({ projectPath, status, limit }) => {
607
608
  const governanceDir = await resolveGovernanceDir(projectPath);
609
+ const normalizedProjectPath = toProjectPath(governanceDir);
608
610
  const { tasks } = await loadTasksDocument(governanceDir);
609
611
  const filtered = tasks
610
612
  .filter((task) => (status ? task.status === status : true))
@@ -624,6 +626,7 @@ export function registerTaskTools(server) {
624
626
  toolName: "taskList",
625
627
  sections: [
626
628
  summarySection([
629
+ `- projectPath: ${normalizedProjectPath}`,
627
630
  `- governanceDir: ${governanceDir}`,
628
631
  `- filter.status: ${status ?? "(none)"}`,
629
632
  `- returned: ${filtered.length}`,
@@ -641,6 +644,94 @@ export function registerTaskTools(server) {
641
644
  });
642
645
  return asText(markdown);
643
646
  });
647
+ server.registerTool("taskCreate", {
648
+ title: "Task Create",
649
+ description: "Create a new task in governance store with stable TASK-xxxx ID",
650
+ inputSchema: {
651
+ projectPath: z.string(),
652
+ taskId: z.string(),
653
+ title: z.string(),
654
+ status: z.enum(["TODO", "IN_PROGRESS", "BLOCKED", "DONE"]).optional(),
655
+ owner: z.string().optional(),
656
+ summary: z.string().optional(),
657
+ roadmapRefs: z.array(z.string()).optional(),
658
+ links: z.array(z.string()).optional(),
659
+ subState: z.object({
660
+ phase: z.enum(["discovery", "design", "implementation", "testing"]).optional(),
661
+ confidence: z.number().min(0).max(1).optional(),
662
+ estimatedCompletion: z.string().optional(),
663
+ }).optional(),
664
+ blocker: z.object({
665
+ type: z.enum(["internal_dependency", "external_dependency", "resource", "approval"]),
666
+ description: z.string(),
667
+ blockingEntity: z.string().optional(),
668
+ unblockCondition: z.string().optional(),
669
+ escalationPath: z.string().optional(),
670
+ }).optional(),
671
+ },
672
+ }, async ({ projectPath, taskId, title, status, owner, summary, roadmapRefs, links, subState, blocker }) => {
673
+ if (!isValidTaskId(taskId)) {
674
+ return {
675
+ ...asText(renderErrorMarkdown("taskCreate", `Invalid task ID format: ${taskId}`, ["expected format: TASK-0001", "retry with a valid task ID"], `taskCreate(projectPath=\"${projectPath}\", taskId=\"TASK-0001\", title=\"Define executable objective\")`)),
676
+ isError: true,
677
+ };
678
+ }
679
+ const governanceDir = await resolveGovernanceDir(projectPath);
680
+ const normalizedProjectPath = toProjectPath(governanceDir);
681
+ const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
682
+ const duplicated = tasks.some((item) => item.id === taskId);
683
+ if (duplicated) {
684
+ return {
685
+ ...asText(renderErrorMarkdown("taskCreate", `Task already exists: ${taskId}`, ["task IDs must be unique", "use taskUpdate for existing tasks"], `taskUpdate(projectPath=\"${normalizedProjectPath}\", taskId=\"${taskId}\", updates={...})`)),
686
+ isError: true,
687
+ };
688
+ }
689
+ const createdTask = normalizeTask({
690
+ id: taskId,
691
+ title,
692
+ status: status ?? "TODO",
693
+ owner,
694
+ summary,
695
+ roadmapRefs,
696
+ links,
697
+ subState,
698
+ blocker,
699
+ updatedAt: nowIso(),
700
+ });
701
+ await upsertTaskInStore(tasksPath, createdTask);
702
+ await loadTasksDocumentWithOptions(governanceDir, true);
703
+ const lintSuggestions = [
704
+ ...collectSingleTaskLintSuggestions(createdTask),
705
+ ...(await collectTaskFileLintSuggestions(governanceDir, createdTask)),
706
+ ];
707
+ const markdown = renderToolResponseMarkdown({
708
+ toolName: "taskCreate",
709
+ sections: [
710
+ summarySection([
711
+ `- projectPath: ${normalizedProjectPath}`,
712
+ `- governanceDir: ${governanceDir}`,
713
+ `- taskId: ${createdTask.id}`,
714
+ `- status: ${createdTask.status}`,
715
+ `- owner: ${createdTask.owner || "(none)"}`,
716
+ `- updatedAt: ${createdTask.updatedAt}`,
717
+ ]),
718
+ evidenceSection([
719
+ "### Created Task",
720
+ `- ${createdTask.id} | ${createdTask.status} | ${createdTask.title}`,
721
+ `- summary: ${createdTask.summary || "(none)"}`,
722
+ `- roadmapRefs: ${createdTask.roadmapRefs.join(", ") || "(none)"}`,
723
+ `- links: ${createdTask.links.join(", ") || "(none)"}`,
724
+ ]),
725
+ guidanceSection([
726
+ "Task created in governance store successfully and tasks.md has been synced.",
727
+ "Run taskContext to verify references and lint guidance.",
728
+ ]),
729
+ lintSection(lintSuggestions),
730
+ nextCallSection(`taskContext(projectPath=\"${normalizedProjectPath}\", taskId=\"${createdTask.id}\")`),
731
+ ],
732
+ });
733
+ return asText(markdown);
734
+ });
644
735
  server.registerTool("taskNext", {
645
736
  title: "Task Next",
646
737
  description: "Start here to auto-select the highest-priority actionable task",
@@ -684,7 +775,7 @@ export function registerTaskTools(server) {
684
775
  evidenceSection([
685
776
  "### Project Snapshots",
686
777
  ...(projectSnapshots.length > 0
687
- ? 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)"}`)
778
+ ? projectSnapshots.map((item, index) => `${index + 1}. ${toProjectPath(item.governanceDir)} | total=${item.total} | todo=${item.todo} | in_progress=${item.inProgress} | blocked=${item.blocked} | done=${item.done} | roadmapIds=${item.roadmapIds.join(", ") || "(none)"}`)
688
779
  : ["- (none)"]),
689
780
  "",
690
781
  "### Seed Task Template",
@@ -692,6 +783,7 @@ export function registerTaskTools(server) {
692
783
  ]),
693
784
  guidanceSection([
694
785
  "- No TODO/IN_PROGRESS task is available.",
786
+ "- Create 1-3 new TODO tasks using `taskCreate(...)` from active roadmap slices.",
695
787
  "- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.",
696
788
  "- If roadmap has active milestones, analyze milestone intent and split into 1-3 executable TODO tasks.",
697
789
  "",
@@ -707,7 +799,7 @@ export function registerTaskTools(server) {
707
799
  "- Ensure each new task has stable TASK-xxxx ID and at least one roadmapRefs item.",
708
800
  ]),
709
801
  nextCallSection(preferredProject
710
- ? `projectContext(projectPath=\"${toProjectPath(preferredProject.governanceDir)}\")`
802
+ ? `taskCreate(projectPath=\"${toProjectPath(preferredProject.governanceDir)}\", taskId=\"TASK-0001\", title=\"Create first executable slice\", roadmapRefs=[\"${preferredRoadmapRef}\"], summary=\"Derived from active roadmap milestone\")`
711
803
  : "projectScan()"),
712
804
  ],
713
805
  });
@@ -732,7 +824,7 @@ export function registerTaskTools(server) {
732
824
  `- maxDepth: ${depth}`,
733
825
  `- matchedProjects: ${projects.length}`,
734
826
  `- actionableTasks: ${rankedCandidates.length}`,
735
- `- selectedProject: ${selected.governanceDir}`,
827
+ `- selectedProject: ${toProjectPath(selected.governanceDir)}`,
736
828
  `- selectedTaskId: ${selected.task.id}`,
737
829
  `- selectedTaskStatus: ${selected.task.status}`,
738
830
  ]),
@@ -748,7 +840,7 @@ export function registerTaskTools(server) {
748
840
  "### Top Candidates",
749
841
  ...rankedCandidates
750
842
  .slice(0, candidateLimit)
751
- .map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | project=${item.governanceDir} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
843
+ .map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | projectPath=${toProjectPath(item.governanceDir)} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
752
844
  "",
753
845
  "### Selection Reason",
754
846
  "- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.",
@@ -791,6 +883,7 @@ export function registerTaskTools(server) {
791
883
  };
792
884
  }
793
885
  const governanceDir = await resolveGovernanceDir(projectPath);
886
+ const normalizedProjectPath = toProjectPath(governanceDir);
794
887
  const { markdownPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
795
888
  const task = tasks.find((item) => item.id === taskId);
796
889
  if (!task) {
@@ -812,6 +905,7 @@ export function registerTaskTools(server) {
812
905
  const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
813
906
  // Build summary with subState and blocker info (v1.1.0)
814
907
  const summaryLines = [
908
+ `- projectPath: ${normalizedProjectPath}`,
815
909
  `- governanceDir: ${governanceDir}`,
816
910
  `- taskId: ${task.id}`,
817
911
  `- title: ${task.title}`,
@@ -873,7 +967,7 @@ export function registerTaskTools(server) {
873
967
  "",
874
968
  "- Verify whether current status and evidence are consistent.",
875
969
  ...taskStatusGuidance(task),
876
- "- If updates are needed, use tool writes for sqlite source (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.",
970
+ "- If updates are needed, use tool writes for governance store (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.",
877
971
  "- After editing, re-run `taskContext` to verify references and context consistency.",
878
972
  ]),
879
973
  lintSection(lintSuggestions),
@@ -917,6 +1011,7 @@ export function registerTaskTools(server) {
917
1011
  };
918
1012
  }
919
1013
  const governanceDir = await resolveGovernanceDir(projectPath);
1014
+ const normalizedProjectPath = toProjectPath(governanceDir);
920
1015
  const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
921
1016
  const taskIndex = tasks.findIndex((item) => item.id === taskId);
922
1017
  if (taskIndex === -1) {
@@ -971,6 +1066,7 @@ export function registerTaskTools(server) {
971
1066
  const normalizedTask = normalizeTask(task);
972
1067
  // Save task incrementally
973
1068
  await upsertTaskInStore(tasksPath, normalizedTask);
1069
+ await loadTasksDocumentWithOptions(governanceDir, true);
974
1070
  task.status = normalizedTask.status;
975
1071
  task.owner = normalizedTask.owner;
976
1072
  task.summary = normalizedTask.summary;
@@ -981,6 +1077,8 @@ export function registerTaskTools(server) {
981
1077
  task.blocker = normalizedTask.blocker;
982
1078
  // Build response
983
1079
  const updateSummary = [
1080
+ `- projectPath: ${normalizedProjectPath}`,
1081
+ `- governanceDir: ${governanceDir}`,
984
1082
  `- taskId: ${taskId}`,
985
1083
  `- originalStatus: ${originalStatus}`,
986
1084
  `- newStatus: ${task.status}`,
@@ -1026,11 +1124,10 @@ export function registerTaskTools(server) {
1026
1124
  ...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
1027
1125
  ]),
1028
1126
  guidanceSection([
1029
- "Task updated successfully. Run `taskContext` to verify the changes.",
1127
+ "Task updated successfully and tasks.md has been synced. Run `taskContext` to verify the changes.",
1030
1128
  "If status changed to DONE, ensure evidence links are added.",
1031
1129
  "If subState or blocker were updated, verify the metadata is correct.",
1032
- "SQLite is source of truth; tasks.md is a generated view and may be overwritten.",
1033
- "Call `syncViews(projectPath=..., views=[\"tasks\"], force=true)` when immediate markdown materialization is required.",
1130
+ ".projitive governance store is source of truth; tasks.md is a generated view and may be overwritten.",
1034
1131
  ]),
1035
1132
  lintSection([]),
1036
1133
  nextCallSection(`taskContext(projectPath=\"${toProjectPath(governanceDir)}\", taskId=\"${taskId}\")`),
@@ -149,7 +149,7 @@ describe("tasks module", () => {
149
149
  const guidanceB = await resolveNoTaskDiscoveryGuidance("/another/path");
150
150
  expect(guidanceA).toEqual(guidanceB);
151
151
  });
152
- it("loads and saves tasks from sqlite and keeps newest-first order", async () => {
152
+ it("loads and saves tasks from governance store and keeps newest-first order", async () => {
153
153
  const root = await fs.mkdtemp(path.join(os.tmpdir(), "projitive-mcp-task-"));
154
154
  const governanceDir = path.join(root, ".projitive");
155
155
  await fs.mkdir(governanceDir, { recursive: true });
@@ -163,7 +163,7 @@ describe("tasks module", () => {
163
163
  expect(loaded.tasks[0].id).toBe("TASK-0002");
164
164
  expect(loaded.tasks[1].id).toBe("TASK-0001");
165
165
  const markdown = await fs.readFile(path.join(governanceDir, "tasks.md"), "utf-8");
166
- expect(markdown).toContain("generated from .projitive sqlite tables");
166
+ expect(markdown).toContain("generated from .projitive governance store");
167
167
  await fs.rm(root, { recursive: true, force: true });
168
168
  });
169
169
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projitive/mcp",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Projitive MCP Server for project and task discovery/update",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -25,14 +25,11 @@
25
25
  "output"
26
26
  ],
27
27
  "dependencies": {
28
- "@duckdb/node-api": "1.5.0-r.1",
29
28
  "@modelcontextprotocol/sdk": "^1.17.5",
30
- "sql.js": "^1.14.1",
31
29
  "zod": "^3.23.8"
32
30
  },
33
31
  "devDependencies": {
34
32
  "@types/node": "^24.3.0",
35
- "@types/sql.js": "^1.4.9",
36
33
  "@vitest/coverage-v8": "^3.2.4",
37
34
  "tsx": "^4.20.5",
38
35
  "typescript": "^5.9.2",
@@ -1,68 +0,0 @@
1
- import { MIGRATION_STEPS } from "./steps.js";
2
- function setStoreSchemaVersion(db, nextVersion) {
3
- const statement = db.prepare(`
4
- INSERT INTO meta (key, value)
5
- VALUES (?, ?)
6
- ON CONFLICT(key) DO UPDATE SET value=excluded.value
7
- `);
8
- statement.run(["store_schema_version", String(nextVersion)]);
9
- statement.free();
10
- }
11
- function writeMigrationHistory(db, migrationId, fromVersion, toVersion, checksum, status, startedAt, finishedAt, errorMessage) {
12
- const statement = db.prepare(`
13
- INSERT INTO migration_history (
14
- id,
15
- from_version,
16
- to_version,
17
- checksum,
18
- started_at,
19
- finished_at,
20
- status,
21
- error_message
22
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
23
- `);
24
- statement.run([
25
- migrationId,
26
- fromVersion,
27
- toVersion,
28
- checksum,
29
- startedAt,
30
- finishedAt,
31
- status,
32
- errorMessage,
33
- ]);
34
- statement.free();
35
- }
36
- export function runPendingMigrations(db, currentVersion, targetVersion) {
37
- let versionCursor = currentVersion;
38
- if (versionCursor >= targetVersion) {
39
- return;
40
- }
41
- const steps = MIGRATION_STEPS
42
- .filter((item) => item.fromVersion >= currentVersion && item.toVersion <= targetVersion)
43
- .sort((a, b) => a.fromVersion - b.fromVersion);
44
- for (const step of steps) {
45
- if (step.fromVersion !== versionCursor) {
46
- throw new Error(`Migration chain broken at version ${versionCursor}. Missing step ${versionCursor} -> ${step.toVersion}.`);
47
- }
48
- const startedAt = new Date().toISOString();
49
- try {
50
- db.exec("BEGIN TRANSACTION;");
51
- step.up(db);
52
- setStoreSchemaVersion(db, step.toVersion);
53
- const finishedAt = new Date().toISOString();
54
- writeMigrationHistory(db, step.id, step.fromVersion, step.toVersion, step.checksum, "SUCCESS", startedAt, finishedAt, null);
55
- db.exec("COMMIT;");
56
- versionCursor = step.toVersion;
57
- }
58
- catch (error) {
59
- db.exec("ROLLBACK;");
60
- const finishedAt = new Date().toISOString();
61
- writeMigrationHistory(db, step.id, step.fromVersion, step.toVersion, step.checksum, "FAILED", startedAt, finishedAt, error instanceof Error ? error.message : String(error));
62
- throw error;
63
- }
64
- }
65
- if (versionCursor !== targetVersion) {
66
- throw new Error(`Migration target not reached. expected=${targetVersion}, actual=${versionCursor}`);
67
- }
68
- }
@@ -1,55 +0,0 @@
1
- function hasColumn(db, tableName, columnName) {
2
- const result = db.exec(`PRAGMA table_info(${tableName});`);
3
- if (result.length === 0) {
4
- return false;
5
- }
6
- const rows = result[0].values;
7
- return rows.some((row) => String(row[1]) === columnName);
8
- }
9
- function ensureRecordVersionColumns(db) {
10
- const tables = ["tasks", "roadmaps", "meta", "view_state"];
11
- for (const tableName of tables) {
12
- if (!hasColumn(db, tableName, "record_version")) {
13
- db.exec(`ALTER TABLE ${tableName} ADD COLUMN record_version INTEGER NOT NULL DEFAULT 1;`);
14
- }
15
- }
16
- }
17
- function ensurePerformanceIndexes(db) {
18
- db.exec(`
19
- CREATE INDEX IF NOT EXISTS idx_tasks_status_updated
20
- ON tasks(status, updated_at DESC);
21
- CREATE INDEX IF NOT EXISTS idx_tasks_updated
22
- ON tasks(updated_at DESC);
23
- CREATE INDEX IF NOT EXISTS idx_roadmaps_updated
24
- ON roadmaps(updated_at DESC);
25
- `);
26
- }
27
- export const MIGRATION_STEPS = [
28
- {
29
- id: "20260313_baseline_v1",
30
- fromVersion: 0,
31
- toVersion: 1,
32
- checksum: "baseline-v1",
33
- up: () => {
34
- // Baseline marker for pre-versioned stores.
35
- },
36
- },
37
- {
38
- id: "20260313_add_record_version_v2",
39
- fromVersion: 1,
40
- toVersion: 2,
41
- checksum: "add-record-version-v2",
42
- up: (db) => {
43
- ensureRecordVersionColumns(db);
44
- },
45
- },
46
- {
47
- id: "20260313_add_indexes_v3",
48
- fromVersion: 2,
49
- toVersion: 3,
50
- checksum: "add-indexes-v3",
51
- up: (db) => {
52
- ensurePerformanceIndexes(db);
53
- },
54
- },
55
- ];
@@ -1 +0,0 @@
1
- export {};