@projitive/mcp 1.2.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.
@@ -0,0 +1,164 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { afterEach, describe, expect, it } from "vitest";
4
+ import { ensureStore, getMarkdownViewState, getStoreVersion, loadActionableTasksFromStore, loadRoadmapIdsFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, markMarkdownViewBuilt, markMarkdownViewDirty, replaceRoadmapsInStore, replaceTasksInStore, upsertRoadmapInStore, upsertTaskInStore, } from "./store.js";
5
+ const tempPaths = [];
6
+ async function createTempDbPath() {
7
+ const sandboxRoot = path.join(process.cwd(), ".tmp", "store-tests");
8
+ await fs.mkdir(sandboxRoot, { recursive: true });
9
+ const dir = await fs.mkdtemp(path.join(sandboxRoot, "case-"));
10
+ tempPaths.push(dir);
11
+ return path.join(dir, ".projitive");
12
+ }
13
+ async function readRawStore(dbPath) {
14
+ const content = await fs.readFile(dbPath, "utf8");
15
+ return JSON.parse(content);
16
+ }
17
+ function task(input) {
18
+ return {
19
+ id: input.id,
20
+ title: input.title,
21
+ status: input.status ?? "TODO",
22
+ owner: input.owner ?? "",
23
+ summary: input.summary ?? "",
24
+ updatedAt: input.updatedAt ?? new Date().toISOString(),
25
+ links: input.links ?? [],
26
+ roadmapRefs: input.roadmapRefs ?? [],
27
+ subState: input.subState,
28
+ blocker: input.blocker,
29
+ };
30
+ }
31
+ function milestone(input) {
32
+ return {
33
+ id: input.id,
34
+ title: input.title,
35
+ status: input.status ?? "active",
36
+ time: input.time,
37
+ updatedAt: input.updatedAt ?? new Date().toISOString(),
38
+ };
39
+ }
40
+ afterEach(async () => {
41
+ await Promise.all(tempPaths.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
42
+ });
43
+ describe("store", () => {
44
+ it("initializes JSON store with default meta/view state", async () => {
45
+ const dbPath = await createTempDbPath();
46
+ await ensureStore(dbPath);
47
+ const store = await readRawStore(dbPath);
48
+ expect(store.schema).toBe("projitive-json-store");
49
+ expect(store.meta.store_schema_version).toBe(3);
50
+ expect(store.meta.tasks_version).toBe(0);
51
+ expect(store.meta.roadmaps_version).toBe(0);
52
+ expect(store.view_state.tasks_markdown.dirty).toBe(true);
53
+ expect(store.view_state.tasks_markdown.lastSourceVersion).toBe(0);
54
+ expect(store.view_state.roadmaps_markdown.dirty).toBe(true);
55
+ expect(store.view_state.roadmaps_markdown.lastSourceVersion).toBe(0);
56
+ expect(Array.isArray(store.migration_history)).toBe(true);
57
+ });
58
+ it("tracks view state dirty/build transitions", async () => {
59
+ const dbPath = await createTempDbPath();
60
+ await ensureStore(dbPath);
61
+ const initial = await getMarkdownViewState(dbPath, "tasks_markdown");
62
+ expect(initial.dirty).toBe(true);
63
+ await markMarkdownViewBuilt(dbPath, "tasks_markdown", 7, "2026-03-13T00:00:00.000Z");
64
+ const built = await getMarkdownViewState(dbPath, "tasks_markdown");
65
+ expect(built.dirty).toBe(false);
66
+ expect(built.lastSourceVersion).toBe(7);
67
+ expect(built.lastBuiltAt).toBe("2026-03-13T00:00:00.000Z");
68
+ await markMarkdownViewDirty(dbPath, "tasks_markdown");
69
+ const dirtyAgain = await getMarkdownViewState(dbPath, "tasks_markdown");
70
+ expect(dirtyAgain.dirty).toBe(true);
71
+ expect(dirtyAgain.lastSourceVersion).toBe(7);
72
+ });
73
+ it("upserts tasks and updates source/view versions", async () => {
74
+ const dbPath = await createTempDbPath();
75
+ await ensureStore(dbPath);
76
+ await upsertTaskInStore(dbPath, task({
77
+ id: "TASK-0001",
78
+ title: "First",
79
+ status: "TODO",
80
+ owner: "alice",
81
+ summary: "first",
82
+ updatedAt: "2026-03-13T00:00:00.000Z",
83
+ roadmapRefs: ["ROADMAP-0001"],
84
+ }));
85
+ await upsertTaskInStore(dbPath, task({
86
+ id: "TASK-0001",
87
+ title: "First Updated",
88
+ status: "IN_PROGRESS",
89
+ owner: "alice",
90
+ summary: "updated",
91
+ updatedAt: "2026-03-13T01:00:00.000Z",
92
+ roadmapRefs: ["ROADMAP-0001"],
93
+ links: ["reports/r1.md"],
94
+ }));
95
+ const tasks = await loadTasksFromStore(dbPath);
96
+ expect(tasks).toHaveLength(1);
97
+ expect(tasks[0].title).toBe("First Updated");
98
+ expect(tasks[0].status).toBe("IN_PROGRESS");
99
+ expect(tasks[0].links).toEqual(["reports/r1.md"]);
100
+ const tasksVersion = await getStoreVersion(dbPath, "tasks");
101
+ expect(tasksVersion).toBe(2);
102
+ const viewState = await getMarkdownViewState(dbPath, "tasks_markdown");
103
+ expect(viewState.dirty).toBe(true);
104
+ const store = await readRawStore(dbPath);
105
+ const updated = store.tasks.find((item) => item.id === "TASK-0001");
106
+ expect(updated?.recordVersion).toBe(2);
107
+ });
108
+ it("replaces task set and computes actionable ranking/stats", async () => {
109
+ const dbPath = await createTempDbPath();
110
+ await ensureStore(dbPath);
111
+ await replaceTasksInStore(dbPath, [
112
+ task({ id: "TASK-0001", title: "Todo older", status: "TODO", updatedAt: "2026-03-10T00:00:00.000Z" }),
113
+ task({ id: "TASK-0002", title: "In progress", status: "IN_PROGRESS", updatedAt: "2026-03-12T00:00:00.000Z" }),
114
+ task({ id: "TASK-0003", title: "Todo newer", status: "TODO", updatedAt: "2026-03-13T00:00:00.000Z" }),
115
+ task({ id: "TASK-0004", title: "Blocked", status: "BLOCKED", updatedAt: "2026-03-11T00:00:00.000Z" }),
116
+ task({ id: "TASK-0005", title: "Done", status: "DONE", updatedAt: "2026-03-09T00:00:00.000Z" }),
117
+ ]);
118
+ const stats = await loadTaskStatusStatsFromStore(dbPath);
119
+ expect(stats.todo).toBe(2);
120
+ expect(stats.inProgress).toBe(1);
121
+ expect(stats.blocked).toBe(1);
122
+ expect(stats.done).toBe(1);
123
+ expect(stats.total).toBe(5);
124
+ expect(stats.latestUpdatedAt).toBe("2026-03-13T00:00:00.000Z");
125
+ const actionable = await loadActionableTasksFromStore(dbPath, 2);
126
+ expect(actionable).toHaveLength(2);
127
+ expect(actionable[0].id).toBe("TASK-0002");
128
+ expect(actionable[1].id).toBe("TASK-0003");
129
+ const tasksVersion = await getStoreVersion(dbPath, "tasks");
130
+ expect(tasksVersion).toBe(1);
131
+ });
132
+ it("upserts/replaces roadmaps and updates versions", async () => {
133
+ const dbPath = await createTempDbPath();
134
+ await ensureStore(dbPath);
135
+ await upsertRoadmapInStore(dbPath, milestone({
136
+ id: "ROADMAP-0001",
137
+ title: "Phase 1",
138
+ status: "active",
139
+ updatedAt: "2026-03-10T00:00:00.000Z",
140
+ }));
141
+ await upsertRoadmapInStore(dbPath, milestone({
142
+ id: "ROADMAP-0001",
143
+ title: "Phase 1 done",
144
+ status: "done",
145
+ time: "2026-Q1",
146
+ updatedAt: "2026-03-11T00:00:00.000Z",
147
+ }));
148
+ const list1 = await loadRoadmapsFromStore(dbPath);
149
+ expect(list1).toHaveLength(1);
150
+ expect(list1[0].title).toBe("Phase 1 done");
151
+ expect(list1[0].status).toBe("done");
152
+ expect(list1[0].time).toBe("2026-Q1");
153
+ await replaceRoadmapsInStore(dbPath, [
154
+ milestone({ id: "ROADMAP-0002", title: "Phase 2", status: "active", updatedAt: "2026-03-12T00:00:00.000Z" }),
155
+ milestone({ id: "ROADMAP-0003", title: "Phase 3", status: "done", updatedAt: "2026-03-13T00:00:00.000Z" }),
156
+ ]);
157
+ const ids = await loadRoadmapIdsFromStore(dbPath);
158
+ expect(ids).toEqual(["ROADMAP-0003", "ROADMAP-0002"]);
159
+ const roadmapsVersion = await getStoreVersion(dbPath, "roadmaps");
160
+ expect(roadmapsVersion).toBe(3);
161
+ const viewState = await getMarkdownViewState(dbPath, "roadmaps_markdown");
162
+ expect(viewState.dirty).toBe(true);
163
+ });
164
+ });
@@ -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 markdown-first outputs",
21
+ description: "Semantic Projitive MCP for project/task discovery and agent guidance with sqlite-first governance outputs",
22
22
  });
23
23
  // 注册所有模块
24
24
  registerTools(server);
@@ -54,6 +54,8 @@ 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.",
58
+ "Then call `taskNext()` again to re-rank.",
57
59
  "",
58
60
  "### Option B: Manual select",
59
61
  "1. Call `taskList()` to list all tasks",
@@ -65,12 +67,27 @@ export function registerQuickStartPrompt(server) {
65
67
  "After getting task context:",
66
68
  "1. Read evidence links in Suggested Read Order",
67
69
  "2. Understand task requirements and acceptance criteria",
68
- "3. Edit governance markdown only (tasks/designs/reports/roadmap)",
69
- "4. Update task status:",
70
+ "3. Write governance source via tools (`taskUpdate` / `roadmapUpdate`) instead of editing tasks.md/roadmap.md directly",
71
+ "4. Update docs (`designs/` / `reports/`) as required by evidence",
72
+ "5. If immediate markdown snapshots are needed, call `syncViews(projectPath=..., force=true)`",
73
+ "6. Update task status:",
70
74
  " - TODO → IN_PROGRESS (when starting execution)",
71
75
  " - IN_PROGRESS → DONE (when completed)",
72
76
  " - IN_PROGRESS → BLOCKED (when blocked)",
73
- "5. Re-run taskContext() to verify changes",
77
+ "7. Re-run taskContext() to verify changes",
78
+ "",
79
+ "## Autonomous Operating Loop",
80
+ "",
81
+ "Keep this loop until no high-value actionable work remains:",
82
+ "1. Discover: `taskNext()`",
83
+ "2. Execute: update sqlite + docs + report evidence",
84
+ "3. Verify: `taskContext()`",
85
+ "4. Re-prioritize: `taskNext()`",
86
+ "",
87
+ "Stop and re-discover only when:",
88
+ "- Current task is BLOCKED with a clear blocker description and unblock condition",
89
+ "- Acceptance criteria are met and status is DONE",
90
+ "- Project has no actionable tasks and requires roadmap-driven task creation",
74
91
  "",
75
92
  "## Special Cases",
76
93
  "",
@@ -78,15 +95,24 @@ export function registerQuickStartPrompt(server) {
78
95
  "Call `projectInit(projectPath=\"<project-dir>\")` to initialize governance structure.",
79
96
  "",
80
97
  "### Case 2: No actionable tasks",
81
- "1. Check if tasks.md is missing",
82
- "2. Read design documents in projitive://designs/",
83
- "3. Create 1-3 new TODO tasks",
98
+ "1. Check if .projitive database is missing",
99
+ "2. If roadmap has active goals, split milestones into 1-3 executable TODO tasks",
100
+ "3. Apply task creation gate before adding each task:",
101
+ " - Clear outcome: one-sentence done condition",
102
+ " - Verifiable evidence: at least one report/designs/readme link target",
103
+ " - Small slice: should be completable in one focused execution cycle",
104
+ " - Traceability: include at least one roadmapRefs item when applicable",
105
+ " - Distinct scope: avoid overlap with existing DONE/BLOCKED tasks",
106
+ "4. Prefer unblocking tasks that unlock multiple follow-up tasks",
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",
84
109
  "",
85
110
  "## Hard Rules",
86
111
  "",
87
112
  "- **NEVER modify TASK/ROADMAP IDs** - Keep them immutable once assigned",
88
113
  "- **Every status transition must have report evidence** - Create execution reports in reports/ directory",
89
- "- **Only edit files under .projitive/** - Unless task scope explicitly requires otherwise",
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",
90
116
  "- **Always verify after updates** - Re-run taskContext() to confirm reference consistency",
91
117
  ].join("\n");
92
118
  return asUserPrompt(text);
@@ -53,8 +53,9 @@ export function registerTaskDiscoveryPrompt(server) {
53
53
  "",
54
54
  "### Artifacts",
55
55
  "Check if these files exist:",
56
- "- \u2705 tasks.md - Task list (required)",
57
- "- \u2705 roadmap.md - Roadmap",
56
+ "- \u2705 .projitive - SQLite governance database (required)",
57
+ "- \u2705 tasks.md - Generated task view",
58
+ "- \u2705 roadmap.md - Generated roadmap view",
58
59
  "- \u2705 README.md - Project description",
59
60
  "- \u2705 designs/ - Design documents directory",
60
61
  "- \u2705 reports/ - Reports directory",
@@ -87,6 +88,14 @@ export function registerTaskDiscoveryPrompt(server) {
87
88
  "",
88
89
  "Then call `taskContext(projectPath=\"...\", taskId=\"<task-id>\")` for task details.",
89
90
  "",
91
+ "### Discovery Quality Gate (before creating new tasks)",
92
+ "Only create a TODO when all conditions are true:",
93
+ "- It can be finished in one focused execution cycle",
94
+ "- It has explicit done condition and measurable evidence output",
95
+ "- It links to roadmap/readme/report/design context",
96
+ "- It does not duplicate an existing TODO/IN_PROGRESS/BLOCKED task",
97
+ "- It improves project throughput (unblocks or delivers user-visible value)",
98
+ "",
90
99
  "#### Method B: Manual select with taskList()",
91
100
  "",
92
101
  "1. Call `taskList()` to get all tasks",
@@ -136,7 +145,8 @@ export function registerTaskDiscoveryPrompt(server) {
136
145
  " - Reference history reports in reports/",
137
146
  "",
138
147
  "3. **Execute task content**",
139
- " - Edit governance files (tasks.md / designs/*.md / reports/*.md / roadmap.md)",
148
+ " - Update sqlite task/roadmap tables via MCP tools; edit designs/*.md and reports/*.md as needed",
149
+ " - If immediate markdown snapshots are required by another agent/session, call syncViews(force=true)",
140
150
  " - Create execution report (reports/ directory)",
141
151
  " - Update task status to DONE",
142
152
  "",
@@ -150,11 +160,14 @@ export function registerTaskDiscoveryPrompt(server) {
150
160
  "### Case 1: No tasks at all",
151
161
  "",
152
162
  "If taskNext() returns empty:",
153
- "1. Check if tasks.md exists",
154
- "2. Read design documents in designs/ directory",
155
- "3. Read roadmap.md to understand project goals",
156
- "4. Create 1-3 new TODO tasks",
157
- "5. Each task must have:",
163
+ "1. Check if .projitive exists",
164
+ "2. Analyze active roadmap milestones and derive 1-3 executable TODO tasks",
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:",
158
171
  " - Unique TASK-xxxx ID",
159
172
  " - Clear title",
160
173
  " - Detailed summary",
@@ -169,7 +182,7 @@ export function registerTaskDiscoveryPrompt(server) {
169
182
  "",
170
183
  "### Case 3: Missing required governance files",
171
184
  "",
172
- "If tasks.md does not exist:",
185
+ "If .projitive does not exist:",
173
186
  "1. Call `projectInit(projectPath=\"...\")` to initialize governance structure",
174
187
  "2. Then restart discovery flow",
175
188
  "",
@@ -184,6 +197,7 @@ export function registerTaskDiscoveryPrompt(server) {
184
197
  "| `taskList()` | List all tasks |",
185
198
  "| `taskContext()` | Get task details |",
186
199
  "| `taskUpdate()` | Update task status |",
200
+ "| `syncViews()` | Force materialize tasks.md/roadmap.md from sqlite |",
187
201
  ].join("\n");
188
202
  return asUserPrompt(text);
189
203
  });
@@ -70,11 +70,12 @@ export function registerTaskExecutionPrompt(server) {
70
70
  " - Fill subState (optional, Spec v1.1.0)",
71
71
  "",
72
72
  "2. **Execute task content**",
73
- " - Only edit files under .projitive/",
74
- " - tasks.md - Update task status and metadata",
73
+ " - Prefer MCP tool writes for sqlite source (`taskUpdate` / `roadmapUpdate`)",
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
- " - roadmap.md - Update roadmap (if needed)",
77
+ " - .projitive (roadmap table) - Update roadmap (if needed)",
78
+ " - If downstream consumers require immediate markdown snapshots, call `syncViews(..., force=true)`",
78
79
  "",
79
80
  "3. **Create execution report**",
80
81
  " - Create new report file in reports/ directory",
@@ -107,6 +108,13 @@ export function registerTaskExecutionPrompt(server) {
107
108
  " - Call `taskNext()` for next task",
108
109
  " - Repeat above workflow",
109
110
  "",
111
+ "## Autonomous Progress Rules",
112
+ "",
113
+ "- Keep WIP small: drive one task to DONE before starting another unless blocker requires branching.",
114
+ "- Prefer unblock-first execution: if one small task unlocks multiple tasks, do it first.",
115
+ "- Avoid churn: do not repeatedly flip status without producing new evidence.",
116
+ "- Every loop should leave one durable artifact update (task status, report, or roadmap progress).",
117
+ "",
110
118
  "## Special Cases",
111
119
  "",
112
120
  "### Case 1: Encountered a blocker",
@@ -123,8 +131,10 @@ export function registerTaskExecutionPrompt(server) {
123
131
  "",
124
132
  "If taskNext() returns empty:",
125
133
  "1. Call `projectContext()` to recheck project state",
126
- "2. Read design documents in designs/ directory",
127
- "3. Create 1-3 new TODO tasks",
134
+ "2. Analyze active roadmap milestones and find smallest executable slices",
135
+ "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
+ "5. Re-run `taskNext()` and continue",
128
138
  "",
129
139
  "### Case 3: Need to initialize governance",
130
140
  "",
@@ -143,9 +153,9 @@ export function registerTaskExecutionPrompt(server) {
143
153
  " - IN_PROGRESS \u2192 DONE: Report REQUIRED",
144
154
  " - IN_PROGRESS \u2192 BLOCKED: Report recommended to explain blocker",
145
155
  "",
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",
156
+ "3. **SQLite-first writes only**",
157
+ " - tasks.md/roadmap.md are generated views, not authoritative source",
158
+ " - Use taskUpdate/roadmapUpdate as primary write path",
149
159
  "",
150
160
  "4. **Always verify after updates**",
151
161
  " - After every taskUpdate() call",
@@ -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 and status under .projitive/tasks.md",
18
+ description: "Current task pool markdown view generated from .projitive sqlite task table",
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 under .projitive/roadmap.md",
30
+ description: "Current roadmap markdown view generated from .projitive sqlite roadmap table",
31
31
  mimeType: "text/markdown",
32
32
  }, async () => ({
33
33
  contents: [
@@ -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",