@projitive/mcp 2.0.4 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { registerTaskDiscoveryPrompt } from './taskDiscovery.js';
3
+ describe('taskDiscovery prompt', () => {
4
+ it('registers the prompt and renders project-specific discovery guidance', async () => {
5
+ const server = { registerPrompt: vi.fn() };
6
+ registerTaskDiscoveryPrompt(server);
7
+ const handler = server.registerPrompt.mock.calls[0][2];
8
+ const result = await handler({ projectPath: '/workspace/app' });
9
+ const text = result.messages[0].content.text;
10
+ expect(server.registerPrompt.mock.calls[0][0]).toBe('taskDiscovery');
11
+ expect(text).toContain('Known project path: "/workspace/app"');
12
+ expect(text).toContain('Method A: Auto-select with taskNext()');
13
+ expect(text).toContain('roadmapCreate');
14
+ });
15
+ it('renders unknown-project discovery steps', async () => {
16
+ const server = { registerPrompt: vi.fn() };
17
+ registerTaskDiscoveryPrompt(server);
18
+ const handler = server.registerPrompt.mock.calls[0][2];
19
+ const result = await handler({});
20
+ const text = result.messages[0].content.text;
21
+ expect(text).toContain('Call `projectScan()` to discover all governance roots');
22
+ expect(text).toContain('Call `projectContext(projectPath="<project-path>")` to load project context');
23
+ });
24
+ });
@@ -43,6 +43,16 @@ export function registerTaskExecutionPrompt(server) {
43
43
  '- Understand task background and motivation',
44
44
  '- Identify task acceptance criteria',
45
45
  '- Look for related design decisions',
46
+ '- Check Pre-Execution Research Brief status in taskContext output',
47
+ '- If status is MISSING/INCOMPLETE, complete it before any implementation',
48
+ '',
49
+ '### Pre-Execution Research Brief (Mandatory Gate)',
50
+ '- Fixed file name: `designs/research/<TASK-ID>.implementation-research.md`',
51
+ '- Required sections:',
52
+ ' - `## Design Guidelines and Specs`',
53
+ ' - `## Code Architecture and Implementation Findings`',
54
+ '- Must include line locations (for example `path/to/file.ts#L42`)',
55
+ '- You MUST read this file before progressing implementation',
46
56
  '',
47
57
  '### Evidence',
48
58
  '- Candidate Files - Related file list',
@@ -59,12 +69,15 @@ export function registerTaskExecutionPrompt(server) {
59
69
  '|----------------|-----------|-------------|',
60
70
  '| TODO | \u2192 IN_PROGRESS | When starting execution |',
61
71
  '| IN_PROGRESS | \u2192 DONE | When task is complete |',
62
- '| IN_PROGRESS | \u2192 BLOCKED | When blocked |',
63
- '| BLOCKED | \u2192 TODO | When blocker is resolved |',
72
+ '| IN_PROGRESS | \u2192 BLOCKED | When production is halted by external factor |',
73
+ '| BLOCKED | \u2192 TODO | When blocker condition is resolved + evidence is documented |',
74
+ '| BLOCKED | \u2192 UNBLOCK TASK (new) | Optional: create a new TODO task if blocker requires work (e.g., someone else must do something) |',
64
75
  '',
65
76
  '### Execution Steps',
66
77
  '',
67
78
  '1. **Prepare (if status is TODO)',
79
+ ' - Ensure Pre-Execution Research Brief status is READY in `taskContext()`',
80
+ ' - If not READY, complete research brief first and re-run `taskContext()`',
68
81
  ' - Call `taskUpdate()` to change status to IN_PROGRESS',
69
82
  ' - Set owner (if empty)',
70
83
  ' - Fill subState (optional, Spec v1.1.0)',
@@ -117,17 +130,59 @@ export function registerTaskExecutionPrompt(server) {
117
130
  '',
118
131
  '## Special Cases',
119
132
  '',
120
- '### Case 1: Encountered a blocker',
133
+ '### Case 1: Encountered a blocker during IN_PROGRESS',
121
134
  '',
122
- 'If unable to continue task execution:',
123
- '1. Call `taskUpdate()` to change status to BLOCKED',
124
- '2. Fill blocker field (Spec v1.1.0):',
125
- ' - type: Blocker type (dependency/missing-info/technical-debt/other)',
126
- ' - description: Blocker description',
127
- ' - relatedLinks: Related links (optional)',
128
- '3. Create a new TODO task via `taskCreate()` to resolve blocker',
135
+ 'If unable to continue task execution due to external/blocking factors:',
129
136
  '',
130
- '### Case 2: No actionable tasks',
137
+ '**Always do these steps:**',
138
+ '1. Call `taskUpdate()` to change status to BLOCKED',
139
+ '2. Fill blocker metadata (REQUIRED - Spec v1.1.0):',
140
+ ' - **type**: One of: `internal_dependency`, `external_dependency`, `resource`, `approval`',
141
+ ' - **description**: Clear description of what is blocking and why',
142
+ ' - **blockingEntity** (optional): Who/what is causing block (person, team, external service)',
143
+ ' - **unblockCondition** (optional): Specific condition needed to unblock (e.g., "TASK-42 must be completed")',
144
+ ' - **escalationPath** (optional): Path to escalate if blocker persists',
145
+ '',
146
+ '**Then choose ONE of these paths based on blocker type:**',
147
+ '',
148
+ '#### If `internal_dependency` (another task must complete first)',
149
+ '- Check if the blocking task exists and has owner assigned.',
150
+ '- If exists: coordinate with owner and recheck after their task completes.',
151
+ '- If missing: create new TASK via taskCreate() with explicit unblock condition.',
152
+ '',
153
+ '#### If `external_dependency` (external party/service/approval needed)',
154
+ '- Document blockingEntity clearly.',
155
+ '- If unclear how to reach them, add escalationPath.',
156
+ '- Wait for external delivery or take action per escalationPath.',
157
+ '',
158
+ '#### If `resource` (missing tool, access, budget, personnel)',
159
+ '- Document what resource is needed (in description).',
160
+ '- If owner unknown, set escalationPath.',
161
+ '- Take action to request/allocate resource.',
162
+ '',
163
+ '#### If `approval` (needs approver sign-off)',
164
+ '- Document who must approve (blockingEntity).',
165
+ '- Prepare approval request with clear criteria.',
166
+ '- Track approval status and escalate if delayed.',
167
+ '',
168
+ '**What NOT to do:**',
169
+ '- Do NOT leave BLOCKED task without blocker metadata.',
170
+ '- Do NOT mark BLOCKED if it\'s actually just "waiting for me to finish" (keep IN_PROGRESS).',
171
+ '- Do NOT flip BLOCKED → TODO unless actual unblock condition is met + documented.',
172
+ '',
173
+ '### Case 2: Found a BLOCKED task in taskNext()',
174
+ '',
175
+ 'If taskNext() returns a BLOCKED task instead of TODO/IN_PROGRESS:',
176
+ '1. Read blocker metadata carefully (shown in taskContext output).',
177
+ '2. Follow blocker-specific action path in taskStatusGuidance output.',
178
+ '3. Take concrete steps to unblock:',
179
+ ' - Create/track dependency task',
180
+ ' - Reach out to blocking entity',
181
+ ' - Request missing resource',
182
+ ' - Follow escalation path if applicable',
183
+ '4. Once unblock condition is met: call `taskUpdate(taskId, {status: \'TODO\'})` and re-run taskNext().',
184
+ '',
185
+ '### Case 3: No actionable tasks (all TODO are blocked)',
131
186
  '',
132
187
  'If taskNext() returns empty:',
133
188
  '1. Call `projectContext()` to recheck project state',
@@ -152,7 +207,7 @@ export function registerTaskExecutionPrompt(server) {
152
207
  '2. **Every status transition must have report evidence**',
153
208
  ' - TODO \u2192 IN_PROGRESS: Report not required',
154
209
  ' - IN_PROGRESS \u2192 DONE: Report REQUIRED',
155
- ' - IN_PROGRESS \u2192 BLOCKED: Report recommended to explain blocker',
210
+ ' - IN_PROGRESS \u2192 BLOCKED: Include report to explain blocker',
156
211
  '',
157
212
  '3. **Governance-store-first writes only**',
158
213
  ' - tasks.md/roadmap.md are generated views, not authoritative source',
@@ -166,6 +221,10 @@ export function registerTaskExecutionPrompt(server) {
166
221
  '5. **Keep updatedAt in sync**',
167
222
  ' - Every time you update a task',
168
223
  ' - Must update updatedAt to current time (ISO 8601 format)',
224
+ '',
225
+ '6. **Pre-execution research brief is mandatory**',
226
+ ' - `TODO -> IN_PROGRESS` is not allowed until research brief is READY',
227
+ ' - Always read `designs/research/<TASK-ID>.implementation-research.md` before implementation',
169
228
  ].join('\n');
170
229
  return asUserPrompt(text);
171
230
  });
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { registerTaskExecutionPrompt } from './taskExecution.js';
3
+ describe('taskExecution prompt', () => {
4
+ it('registers the prompt and renders direct taskContext entry when task is known', async () => {
5
+ const server = { registerPrompt: vi.fn() };
6
+ registerTaskExecutionPrompt(server);
7
+ const handler = server.registerPrompt.mock.calls[0][2];
8
+ const result = await handler({ projectPath: '/workspace/app', taskId: 'TASK-0007' });
9
+ const text = result.messages[0].content.text;
10
+ expect(server.registerPrompt.mock.calls[0][0]).toBe('taskExecution');
11
+ expect(text).toContain('1) Run taskContext(projectPath="/workspace/app", taskId="TASK-0007").');
12
+ expect(text).toContain('Every status transition must have report evidence');
13
+ expect(text).toContain('Pre-Execution Research Brief (Mandatory Gate)');
14
+ expect(text).toContain('designs/research/<TASK-ID>.implementation-research.md');
15
+ });
16
+ it('falls back to taskNext when task is not provided', async () => {
17
+ const server = { registerPrompt: vi.fn() };
18
+ registerTaskExecutionPrompt(server);
19
+ const handler = server.registerPrompt.mock.calls[0][2];
20
+ const result = await handler({});
21
+ const text = result.messages[0].content.text;
22
+ expect(text).toContain('1) Run taskNext().');
23
+ expect(text).toContain('Governance-store-first writes only');
24
+ expect(text).toContain('TODO -> IN_PROGRESS');
25
+ expect(text).toContain('research brief is READY');
26
+ });
27
+ });
@@ -0,0 +1,52 @@
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, vi } from 'vitest';
5
+ import { registerDesignFilesResources } from './designs.js';
6
+ const tempPaths = [];
7
+ async function createTempDir() {
8
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-design-resource-test-'));
9
+ tempPaths.push(dir);
10
+ return dir;
11
+ }
12
+ afterEach(async () => {
13
+ await Promise.all(tempPaths.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
14
+ vi.restoreAllMocks();
15
+ });
16
+ describe('design resources registration', () => {
17
+ it('registers discovered markdown design resources recursively', async () => {
18
+ const root = await createTempDir();
19
+ const designsDir = path.join(root, '.projitive', 'designs', 'mobile');
20
+ await fs.mkdir(designsDir, { recursive: true });
21
+ await fs.writeFile(path.join(root, '.projitive', 'designs', 'architecture.md'), '# Architecture\n', 'utf-8');
22
+ await fs.writeFile(path.join(designsDir, 'screen.md'), '# Screen\n', 'utf-8');
23
+ const server = { registerResource: vi.fn() };
24
+ await registerDesignFilesResources(server, root);
25
+ const calls = server.registerResource.mock.calls;
26
+ expect(calls).toHaveLength(2);
27
+ expect(calls.map((call) => call[0])).toEqual(expect.arrayContaining(['design-architecture', 'design-mobile-screen']));
28
+ const mobileHandler = calls.find((call) => call[0] === 'design-mobile-screen')?.[3];
29
+ const result = await mobileHandler();
30
+ expect(result.contents[0]).toEqual({
31
+ uri: 'projitive://designs/mobile-screen',
32
+ text: '# Screen\n',
33
+ });
34
+ });
35
+ it('registers default fallback resource when designs directory is missing', async () => {
36
+ const root = await createTempDir();
37
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
38
+ const server = { registerResource: vi.fn() };
39
+ await registerDesignFilesResources(server, root);
40
+ const calls = server.registerResource.mock.calls;
41
+ expect(calls).toHaveLength(1);
42
+ expect(calls[0][0]).toBe('designs');
43
+ expect(warnSpy).toHaveBeenCalledOnce();
44
+ const handler = calls[0][3];
45
+ await expect(handler()).resolves.toMatchObject({
46
+ contents: [{
47
+ uri: 'projitive://designs',
48
+ text: '# Designs Directory\n\nDesign documents not found. Please create design files in .projitive/designs/ directory.',
49
+ }],
50
+ });
51
+ });
52
+ });
@@ -0,0 +1,35 @@
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, vi } from 'vitest';
5
+ import { registerGovernanceResources } from './governance.js';
6
+ const tempPaths = [];
7
+ async function createTempDir() {
8
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-governance-resource-test-'));
9
+ tempPaths.push(dir);
10
+ return dir;
11
+ }
12
+ afterEach(async () => {
13
+ await Promise.all(tempPaths.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
14
+ vi.restoreAllMocks();
15
+ });
16
+ describe('governance resources', () => {
17
+ it('registers workspace, tasks, and roadmap resources from markdown files', async () => {
18
+ const root = await createTempDir();
19
+ const governanceDir = path.join(root, '.projitive');
20
+ await fs.mkdir(governanceDir, { recursive: true });
21
+ await fs.writeFile(path.join(governanceDir, 'README.md'), '# Workspace\n', 'utf-8');
22
+ await fs.writeFile(path.join(governanceDir, 'tasks.md'), '# Tasks\n', 'utf-8');
23
+ await fs.writeFile(path.join(governanceDir, 'roadmap.md'), '# Roadmap\n', 'utf-8');
24
+ const server = { registerResource: vi.fn() };
25
+ registerGovernanceResources(server, root);
26
+ const calls = server.registerResource.mock.calls;
27
+ expect(calls).toHaveLength(3);
28
+ const workspaceHandler = calls[0][3];
29
+ const tasksHandler = calls[1][3];
30
+ const roadmapHandler = calls[2][3];
31
+ await expect(workspaceHandler()).resolves.toMatchObject({ contents: [{ uri: 'projitive://governance/workspace', text: '# Workspace\n' }] });
32
+ await expect(tasksHandler()).resolves.toMatchObject({ contents: [{ uri: 'projitive://governance/tasks', text: '# Tasks\n' }] });
33
+ await expect(roadmapHandler()).resolves.toMatchObject({ contents: [{ uri: 'projitive://governance/roadmap', text: '# Roadmap\n' }] });
34
+ });
35
+ });
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ vi.mock('./governance.js', () => ({
3
+ registerGovernanceResources: vi.fn(),
4
+ }));
5
+ vi.mock('./designs.js', () => ({
6
+ registerDesignFilesResources: vi.fn(),
7
+ }));
8
+ import { registerGovernanceResources } from './governance.js';
9
+ import { registerDesignFilesResources } from './designs.js';
10
+ import { registerResources } from './index.js';
11
+ describe('resources index module', () => {
12
+ it('registers governance and design resources', () => {
13
+ const server = {};
14
+ registerResources(server, '/workspace/repo');
15
+ expect(registerGovernanceResources).toHaveBeenCalledWith(server, '/workspace/repo');
16
+ expect(registerDesignFilesResources).toHaveBeenCalledWith(server, '/workspace/repo');
17
+ });
18
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ vi.mock('./project.js', () => ({
3
+ registerProjectTools: vi.fn(),
4
+ }));
5
+ vi.mock('./task.js', () => ({
6
+ registerTaskTools: vi.fn(),
7
+ }));
8
+ vi.mock('./roadmap.js', () => ({
9
+ registerRoadmapTools: vi.fn(),
10
+ }));
11
+ import { registerProjectTools } from './project.js';
12
+ import { registerTaskTools } from './task.js';
13
+ import { registerRoadmapTools } from './roadmap.js';
14
+ import { registerTools } from './index.js';
15
+ describe('tools index module', () => {
16
+ it('registers all tool groups', () => {
17
+ const server = {};
18
+ registerTools(server);
19
+ expect(registerProjectTools).toHaveBeenCalledWith(server);
20
+ expect(registerTaskTools).toHaveBeenCalledWith(server);
21
+ expect(registerRoadmapTools).toHaveBeenCalledWith(server);
22
+ });
23
+ });