@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projitive/mcp",
3
- "version": "2.0.4",
3
+ "version": "2.1.1",
4
4
  "description": "Projitive MCP Server for project and task discovery/update",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ConfidenceScoreError, FileNotFoundError, FileReadError, FileWriteError, GovernanceRootNotFoundError, InvalidTaskIdError, MCPError, ProjectError, ProjectNotFoundError, PromptNotFoundError, ProjitiveError, ResourceNotFoundError, TaskError, TaskNotFoundError, TaskValidationError, ValidationError, } from './errors.js';
3
+ describe('errors module', () => {
4
+ it('constructs base and project/task hierarchy errors with metadata', () => {
5
+ const base = new ProjitiveError('base failure', 'BASE', { a: 1 });
6
+ const project = new ProjectError('project failure', 'PROJECT', { projectPath: '/tmp/project' });
7
+ const task = new TaskError('task failure', 'TASK', { taskId: 'TASK-0001' });
8
+ expect(base.name).toBe('ProjitiveError');
9
+ expect(base.code).toBe('BASE');
10
+ expect(base.details).toEqual({ a: 1 });
11
+ expect(project.name).toBe('ProjectError');
12
+ expect(project.code).toBe('PROJECT');
13
+ expect(task.name).toBe('TaskError');
14
+ expect(task.code).toBe('TASK');
15
+ });
16
+ it('constructs specific project and task lookup errors', () => {
17
+ const projectNotFound = new ProjectNotFoundError('/tmp/missing');
18
+ const governanceNotFound = new GovernanceRootNotFoundError('/tmp/project');
19
+ const taskNotFound = new TaskNotFoundError('TASK-0001');
20
+ const invalidTaskId = new InvalidTaskIdError('BAD');
21
+ const taskValidation = new TaskValidationError('TASK-0001', ['owner required', 'links required']);
22
+ expect(projectNotFound.code).toBe('PROJECT_NOT_FOUND');
23
+ expect(projectNotFound.message).toContain('/tmp/missing');
24
+ expect(governanceNotFound.code).toBe('GOVERNANCE_ROOT_NOT_FOUND');
25
+ expect(taskNotFound.code).toBe('TASK_NOT_FOUND');
26
+ expect(invalidTaskId.code).toBe('INVALID_TASK_ID');
27
+ expect(taskValidation.code).toBe('TASK_VALIDATION_FAILED');
28
+ expect(taskValidation.errors).toEqual(['owner required', 'links required']);
29
+ expect(taskValidation.message).toContain('owner required, links required');
30
+ });
31
+ it('constructs file and validation related errors', () => {
32
+ const cause = new Error('disk offline');
33
+ const notFound = new FileNotFoundError('/tmp/a.md');
34
+ const readError = new FileReadError('/tmp/b.md', cause);
35
+ const writeError = new FileWriteError('/tmp/c.md', cause);
36
+ const validation = new ValidationError('invalid input', ['field missing']);
37
+ const confidence = new ConfidenceScoreError('bad confidence', 1.5, ['must be <= 1']);
38
+ expect(notFound.name).toBe('FileError');
39
+ expect(notFound.code).toBe('FILE_NOT_FOUND');
40
+ expect(readError.details).toMatchObject({ filePath: '/tmp/b.md', cause: 'disk offline' });
41
+ expect(writeError.details).toMatchObject({ filePath: '/tmp/c.md', cause: 'disk offline' });
42
+ expect(validation.name).toBe('ValidationError');
43
+ expect(validation.code).toBe('VALIDATION_FAILED');
44
+ expect(validation.errors).toEqual(['field missing']);
45
+ expect(confidence.code).toBe('CONFIDENCE_SCORE_ERROR');
46
+ expect(confidence.score).toBe(1.5);
47
+ });
48
+ it('constructs MCP related not-found errors', () => {
49
+ const mcp = new MCPError('mcp failed', 'MCP_GENERIC', { retry: true });
50
+ const resource = new ResourceNotFoundError('projitive://missing');
51
+ const prompt = new PromptNotFoundError('taskExecution');
52
+ expect(mcp.name).toBe('MCPError');
53
+ expect(mcp.details).toEqual({ retry: true });
54
+ expect(resource.code).toBe('RESOURCE_NOT_FOUND');
55
+ expect(resource.message).toContain('projitive://missing');
56
+ expect(prompt.code).toBe('PROMPT_NOT_FOUND');
57
+ expect(prompt.message).toContain('taskExecution');
58
+ });
59
+ });
@@ -11,16 +11,27 @@ async function fileLineCount(filePath) {
11
11
  return content.split(/\r?\n/).length;
12
12
  }
13
13
  async function listMarkdownFiles(dirPath) {
14
- const entriesResult = await catchIt(fs.readdir(dirPath, { withFileTypes: true }));
15
- if (entriesResult.isError()) {
16
- return [];
17
- }
18
- const entries = entriesResult.value;
19
- const files = entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.md'));
20
14
  const result = [];
21
- for (const file of files) {
22
- const fullPath = path.join(dirPath, file.name);
23
- result.push({ path: fullPath, lineCount: await fileLineCount(fullPath) });
15
+ const stack = [dirPath];
16
+ while (stack.length > 0) {
17
+ const currentDir = stack.pop();
18
+ if (!currentDir) {
19
+ continue;
20
+ }
21
+ const entriesResult = await catchIt(fs.readdir(currentDir, { withFileTypes: true }));
22
+ if (entriesResult.isError()) {
23
+ continue;
24
+ }
25
+ for (const entry of entriesResult.value) {
26
+ const entryPath = path.join(currentDir, entry.name);
27
+ if (entry.isDirectory()) {
28
+ stack.push(entryPath);
29
+ continue;
30
+ }
31
+ if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
32
+ result.push({ path: entryPath, lineCount: await fileLineCount(entryPath) });
33
+ }
34
+ }
24
35
  }
25
36
  return result.sort((a, b) => a.path.localeCompare(b.path));
26
37
  }
@@ -8,3 +8,4 @@ export * from './catch.js';
8
8
  export * from './artifacts.js';
9
9
  export * from './linter.js';
10
10
  export * from './store.js';
11
+ export * from './tool.js';
@@ -12,7 +12,6 @@ export const TASK_LINT_CODES = {
12
12
  UPDATED_AT_INVALID: 'TASK_UPDATED_AT_INVALID',
13
13
  ROADMAP_REFS_EMPTY: 'TASK_ROADMAP_REFS_EMPTY',
14
14
  OUTSIDE_MARKER: 'TASK_OUTSIDE_MARKER',
15
- LINK_TARGET_MISSING: 'TASK_LINK_TARGET_MISSING',
16
15
  LINK_PATH_FORMAT_INVALID: 'TASK_LINK_PATH_FORMAT_INVALID',
17
16
  HOOK_FILE_MISSING: 'TASK_HOOK_FILE_MISSING',
18
17
  FILTER_EMPTY: 'TASK_FILTER_EMPTY',
@@ -25,6 +24,7 @@ export const TASK_LINT_CODES = {
25
24
  IN_PROGRESS_WITHOUT_SUBSTATE: 'TASK_IN_PROGRESS_WITHOUT_SUBSTATE',
26
25
  SUBSTATE_PHASE_INVALID: 'TASK_SUBSTATE_PHASE_INVALID',
27
26
  SUBSTATE_CONFIDENCE_INVALID: 'TASK_SUBSTATE_CONFIDENCE_INVALID',
27
+ RESEARCH_BRIEF_MISSING: 'TASK_RESEARCH_BRIEF_MISSING',
28
28
  };
29
29
  export const ROADMAP_LINT_CODES = {
30
30
  IDS_EMPTY: 'ROADMAP_IDS_EMPTY',
@@ -36,4 +36,6 @@ export const ROADMAP_LINT_CODES = {
36
36
  };
37
37
  export const PROJECT_LINT_CODES = {
38
38
  TASKS_FILE_MISSING: 'PROJECT_TASKS_FILE_MISSING',
39
+ ARCHITECTURE_DOC_MISSING: 'PROJECT_ARCHITECTURE_DOC_MISSING',
40
+ STYLE_DOC_MISSING: 'PROJECT_STYLE_DOC_MISSING',
39
41
  };
@@ -1,7 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  const MESSAGE_TEMPLATE_ENV = 'PROJITIVE_MESSAGE_TEMPLATE_PATH';
4
- const CONTENT_TEMPLATE_TOKEN = '{{content}}';
5
4
  function baseToolTemplateMarkdown() {
6
5
  return [
7
6
  '# {{tool_name}}',
@@ -15,58 +14,53 @@ function baseToolTemplateMarkdown() {
15
14
  '## Agent Guidance',
16
15
  '{{guidance}}',
17
16
  '',
17
+ '## Lint Suggestions',
18
+ '{{suggestions}}',
19
+ '',
18
20
  '## Next Call',
19
21
  '{{next_call}}',
20
- '',
21
- '## Raw Response',
22
- '{{content}}',
23
22
  ].join('\n');
24
23
  }
25
- function contextGuideTemplateExtra() {
26
- return [
27
- '',
28
- '## Common Tool Guides To Read First',
29
- '- ./CLAUDE.md',
30
- '- ./AGENTS.md',
31
- '- ./.github/copilot-instructions.md',
32
- '- ./.cursorrules',
33
- '- ./.github/instructions/*',
34
- '- ./.cursor/rules/*',
35
- ];
36
- }
37
- function idleDiscoveryTemplateExtra() {
38
- return [
39
- '',
40
- '## Idle Discovery Checklist (When No Actionable Task)',
41
- '- Scan backlog comments: TODO / FIXME / HACK / XXX.',
42
- '- Check lint gaps and create executable fix tasks.',
43
- '- Check test quality gaps (missing tests, flaky tests, low-value coverage).',
44
- '- Learn current project architecture and consolidate/update design docs in designs/.',
45
- '- Re-run {{tool_name}} after creating 1-3 focused TODO tasks.',
46
- ];
47
- }
48
- function commitReminderTemplateExtra() {
49
- return [
50
- '',
51
- '## Commit Reminder',
52
- '- After this update, create a commit to keep progress auditable.',
53
- '- Recommended format: type(scope): summary',
54
- '- Example: feat(task): complete TASK-0007 validation flow',
55
- '- Footer suggestion: Refs: TASK-0007, ROADMAP-0002',
56
- ];
57
- }
58
24
  export function getDefaultToolTemplateMarkdown(toolName) {
59
- const base = baseToolTemplateMarkdown().split('\n');
25
+ const base = baseToolTemplateMarkdown();
60
26
  if (toolName === 'taskNext') {
61
- return [...base, ...idleDiscoveryTemplateExtra()].join('\n');
27
+ return [
28
+ base,
29
+ '',
30
+ '## Idle Discovery Checklist (When No Actionable Task)',
31
+ '- Scan backlog comments: TODO / FIXME / HACK / XXX.',
32
+ '- Check lint gaps and create executable fix tasks.',
33
+ '- Check test quality gaps (missing tests, flaky tests, low-value coverage).',
34
+ '- Learn current project architecture and consolidate/update design docs in designs/.',
35
+ '- Review and update architecture docs under designs/core/ (architecture.md, style-guide.md) if missing or outdated.',
36
+ '- Re-run {{tool_name}} after creating 1-3 focused TODO tasks.',
37
+ ].join('\n');
62
38
  }
63
39
  if (toolName === 'projectContext' || toolName === 'taskContext' || toolName === 'roadmapContext') {
64
- return [...base, ...contextGuideTemplateExtra()].join('\n');
40
+ return [
41
+ base,
42
+ '',
43
+ '## Common Tool Guides To Read First',
44
+ '- ./CLAUDE.md',
45
+ '- ./AGENTS.md',
46
+ '- ./.github/copilot-instructions.md',
47
+ '- ./.cursorrules',
48
+ '- ./.github/instructions/*',
49
+ '- ./.cursor/rules/*',
50
+ ].join('\n');
65
51
  }
66
52
  if (toolName === 'taskUpdate' || toolName === 'roadmapUpdate') {
67
- return [...base, ...commitReminderTemplateExtra()].join('\n');
68
- }
69
- return base.join('\n');
53
+ return [
54
+ base,
55
+ '',
56
+ '## Commit Reminder',
57
+ '- After this update, create a commit to keep progress auditable.',
58
+ '- Recommended format: type(scope): summary',
59
+ '- Example: feat(task): complete TASK-0007 validation flow',
60
+ '- Footer suggestion: Refs: TASK-0007, ROADMAP-0002',
61
+ ].join('\n');
62
+ }
63
+ return base;
70
64
  }
71
65
  function loadTemplateFile(templatePath) {
72
66
  try {
@@ -172,13 +166,13 @@ function toSectionText(section) {
172
166
  function resolveSection(payload, title) {
173
167
  return payload.sections.find((item) => item.title === title);
174
168
  }
175
- function buildToolTemplateVariables(payload, classicMarkdown) {
169
+ function buildToolTemplateVariables(payload) {
176
170
  return {
177
171
  tool_name: payload.toolName,
178
- content: classicMarkdown,
179
172
  summary: toSectionText(resolveSection(payload, 'Summary')),
180
173
  evidence: toSectionText(resolveSection(payload, 'Evidence')),
181
174
  guidance: toSectionText(resolveSection(payload, 'Agent Guidance')),
175
+ suggestions: toSectionText(resolveSection(payload, 'Lint Suggestions')),
182
176
  next_call: toSectionText(resolveSection(payload, 'Next Call')),
183
177
  };
184
178
  }
@@ -187,33 +181,23 @@ function applyTemplateVariables(template, variables) {
187
181
  for (const [key, value] of Object.entries(variables)) {
188
182
  rendered = rendered.split(`{{${key}}}`).join(value);
189
183
  }
190
- if (!rendered.includes(variables.content) && !template.includes(CONTENT_TEMPLATE_TOKEN)) {
191
- rendered = `${rendered}\n\n${variables.content}`;
192
- }
193
184
  return rendered.trimEnd();
194
185
  }
195
186
  export function renderToolResponseMarkdown(payload) {
196
- const body = payload.sections.flatMap((section) => [
197
- `## ${section.title}`,
198
- ...withFallback(section.lines),
199
- '',
200
- ]);
201
- const classicMarkdown = [
202
- `# ${payload.toolName}`,
203
- '',
204
- ...body,
205
- ].join('\n').trimEnd();
206
187
  const template = loadMessageTemplate(payload.toolName);
207
- const variables = buildToolTemplateVariables(payload, classicMarkdown);
188
+ const variables = buildToolTemplateVariables(payload);
208
189
  return applyTemplateVariables(template, variables);
209
190
  }
210
191
  export function renderErrorMarkdown(toolName, cause, nextSteps, retryExample) {
211
- return renderToolResponseMarkdown({
212
- toolName,
213
- sections: [
214
- section('Error', [`cause: ${cause}`]),
215
- section('Next Step', nextSteps),
216
- section('Retry Example', [retryExample ?? '(none)']),
217
- ],
218
- });
192
+ const sections = [
193
+ section('Error', [`cause: ${cause}`]),
194
+ section('Next Step', nextSteps),
195
+ section('Retry Example', [retryExample ?? '(none)']),
196
+ ];
197
+ const body = sections.flatMap((sec) => [
198
+ `## ${sec.title}`,
199
+ ...withFallback(sec.lines),
200
+ '',
201
+ ]);
202
+ return [`# ${toolName}`, '', ...body].join('\n').trimEnd();
219
203
  }
@@ -0,0 +1,43 @@
1
+ import { asText, renderToolResponseMarkdown, renderErrorMarkdown, evidenceSection, guidanceSection, lintSection, nextCallSection, summarySection, } from './response.js';
2
+ export class ToolExecutionError extends Error {
3
+ nextSteps;
4
+ retryExample;
5
+ constructor(message, nextSteps = [], retryExample) {
6
+ super(message);
7
+ this.nextSteps = nextSteps;
8
+ this.retryExample = retryExample;
9
+ this.name = 'ToolExecutionError';
10
+ }
11
+ }
12
+ export function createGovernedTool(spec) {
13
+ const cb = async (input) => {
14
+ try {
15
+ const ctx = { now: new Date().toISOString() };
16
+ const data = await spec.execute(input, ctx);
17
+ const markdown = renderToolResponseMarkdown({
18
+ toolName: spec.name,
19
+ sections: [
20
+ summarySection(await spec.summary(data, ctx)),
21
+ evidenceSection(await (spec.evidence?.(data, ctx) ?? [])),
22
+ guidanceSection(await spec.guidance(data, ctx)),
23
+ lintSection(await (spec.suggestions?.(data, ctx) ?? [])),
24
+ nextCallSection(await spec.nextCall(data, ctx)),
25
+ ],
26
+ });
27
+ return asText(markdown);
28
+ }
29
+ catch (error) {
30
+ if (error instanceof ToolExecutionError) {
31
+ return {
32
+ ...asText(renderErrorMarkdown(spec.name, error.message, error.nextSteps, error.retryExample)),
33
+ isError: true,
34
+ };
35
+ }
36
+ return {
37
+ ...asText(renderErrorMarkdown(spec.name, String(error), [])),
38
+ isError: true,
39
+ };
40
+ }
41
+ };
42
+ return [spec.name, { title: spec.title, description: spec.description, inputSchema: spec.inputSchema }, cb];
43
+ }
@@ -0,0 +1,48 @@
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 { capitalizeFirstLetter, formatTitle, readMarkdownOrFallback } from './utils.js';
6
+ const tempPaths = [];
7
+ async function createTempDir() {
8
+ const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-utils-test-'));
9
+ tempPaths.push(dir);
10
+ return dir;
11
+ }
12
+ afterEach(async () => {
13
+ vi.restoreAllMocks();
14
+ await Promise.all(tempPaths.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
15
+ });
16
+ describe('utils module', () => {
17
+ it('reads markdown content when file exists and is non-empty', async () => {
18
+ const root = await createTempDir();
19
+ const filePath = path.join(root, 'README.md');
20
+ await fs.writeFile(filePath, '# Hello\n', 'utf-8');
21
+ const content = await readMarkdownOrFallback('README.md', 'Fallback', root);
22
+ expect(content).toBe('# Hello\n');
23
+ });
24
+ it('returns fallback markdown for missing or empty files', async () => {
25
+ const root = await createTempDir();
26
+ await fs.writeFile(path.join(root, 'empty.md'), ' ', 'utf-8');
27
+ const missing = await readMarkdownOrFallback('missing.md', 'Missing Title', root);
28
+ const empty = await readMarkdownOrFallback('empty.md', 'Empty Title', root);
29
+ expect(missing).toContain('# Missing Title');
30
+ expect(missing).toContain('- status: missing-or-empty');
31
+ expect(empty).toContain('# Empty Title');
32
+ expect(empty).toContain('- file: empty.md');
33
+ });
34
+ it('logs non-ENOENT read errors before falling back', async () => {
35
+ const root = await createTempDir();
36
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
37
+ vi.spyOn(fs, 'readFile').mockRejectedValueOnce({ code: 'EACCES' });
38
+ const result = await readMarkdownOrFallback('secret.md', 'Secret', root);
39
+ expect(result).toContain('# Secret');
40
+ expect(errorSpy).toHaveBeenCalledOnce();
41
+ });
42
+ it('formats title helpers consistently', () => {
43
+ expect(capitalizeFirstLetter('task_execution')).toBe('TaskExecution');
44
+ expect(capitalizeFirstLetter('roadmap-next')).toBe('RoadmapNext');
45
+ expect(formatTitle('task_execution')).toBe('Task Execution');
46
+ expect(formatTitle('roadmap-next')).toBe('Roadmap Next');
47
+ });
48
+ });
@@ -0,0 +1,57 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ const hoisted = vi.hoisted(() => {
3
+ const connectMock = vi.fn().mockResolvedValue(undefined);
4
+ const serverInstance = { connect: connectMock };
5
+ return {
6
+ connectMock,
7
+ serverInstance,
8
+ registerToolsMock: vi.fn(),
9
+ registerPromptsMock: vi.fn(),
10
+ registerResourcesMock: vi.fn(),
11
+ mcpServerMock: vi.fn().mockImplementation(() => serverInstance),
12
+ stdioTransportMock: vi.fn().mockImplementation(() => ({ kind: 'stdio' })),
13
+ };
14
+ });
15
+ vi.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({
16
+ McpServer: hoisted.mcpServerMock,
17
+ }));
18
+ vi.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({
19
+ StdioServerTransport: hoisted.stdioTransportMock,
20
+ }));
21
+ vi.mock('./tools/index.js', () => ({
22
+ registerTools: hoisted.registerToolsMock,
23
+ }));
24
+ vi.mock('./prompts/index.js', () => ({
25
+ registerPrompts: hoisted.registerPromptsMock,
26
+ }));
27
+ vi.mock('./resources/index.js', () => ({
28
+ registerResources: hoisted.registerResourcesMock,
29
+ }));
30
+ describe('index runtime module', () => {
31
+ afterEach(() => {
32
+ vi.restoreAllMocks();
33
+ vi.resetModules();
34
+ hoisted.connectMock.mockClear();
35
+ hoisted.registerToolsMock.mockClear();
36
+ hoisted.registerPromptsMock.mockClear();
37
+ hoisted.registerResourcesMock.mockClear();
38
+ hoisted.mcpServerMock.mockClear();
39
+ hoisted.stdioTransportMock.mockClear();
40
+ });
41
+ it('boots the MCP server, registers modules, and connects stdio transport', async () => {
42
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
43
+ await import('./index.js');
44
+ await Promise.resolve();
45
+ expect(hoisted.mcpServerMock).toHaveBeenCalledWith(expect.objectContaining({
46
+ name: 'projitive',
47
+ description: expect.stringContaining('governance-store-first outputs'),
48
+ }));
49
+ expect(hoisted.registerToolsMock).toHaveBeenCalledWith(hoisted.serverInstance);
50
+ expect(hoisted.registerPromptsMock).toHaveBeenCalledWith(hoisted.serverInstance);
51
+ expect(hoisted.registerResourcesMock).toHaveBeenCalledWith(hoisted.serverInstance, expect.any(String));
52
+ expect(hoisted.stdioTransportMock).toHaveBeenCalledOnce();
53
+ expect(hoisted.connectMock).toHaveBeenCalledOnce();
54
+ expect(errorSpy).toHaveBeenCalledWith('[projitive-mcp] starting server');
55
+ expect(errorSpy.mock.calls.some((call) => String(call[0]).includes('transport=stdio'))).toBe(true);
56
+ });
57
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ vi.mock('./quickStart.js', () => ({
3
+ registerQuickStartPrompt: vi.fn(),
4
+ }));
5
+ vi.mock('./taskDiscovery.js', () => ({
6
+ registerTaskDiscoveryPrompt: vi.fn(),
7
+ }));
8
+ vi.mock('./taskExecution.js', () => ({
9
+ registerTaskExecutionPrompt: vi.fn(),
10
+ }));
11
+ import { registerQuickStartPrompt } from './quickStart.js';
12
+ import { registerTaskDiscoveryPrompt } from './taskDiscovery.js';
13
+ import { registerTaskExecutionPrompt } from './taskExecution.js';
14
+ import { registerPrompts } from './index.js';
15
+ describe('prompts index module', () => {
16
+ it('registers all prompt categories', () => {
17
+ const server = {};
18
+ registerPrompts(server);
19
+ expect(registerQuickStartPrompt).toHaveBeenCalledWith(server);
20
+ expect(registerTaskDiscoveryPrompt).toHaveBeenCalledWith(server);
21
+ expect(registerTaskExecutionPrompt).toHaveBeenCalledWith(server);
22
+ });
23
+ });
@@ -78,34 +78,69 @@ export function registerQuickStartPrompt(server) {
78
78
  '',
79
79
  '## Autonomous Operating Loop',
80
80
  '',
81
- 'Keep this loop until no high-value actionable work remains:',
82
- '1. Discover: `taskNext()`',
83
- '2. Execute: update governance store + 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',
81
+ 'Keep this loop while high-value actionable tasks exist:',
82
+ '1. Discover: `taskNext()` - Select highest-priority task',
83
+ '2. Execute: perform work + update governance store (.projitive) + docs',
84
+ '3. Verify: `taskContext()` - Confirm changes and consistency',
85
+ '4. Re-prioritize: `taskNext()` - Update priority and select next',
86
+ '',
87
+ '**When to Stop or Pause:**',
88
+ '',
89
+ '**Stop Reason 1: Task is BLOCKED and needs external action**',
90
+ '- Blocker must have: type, description, blockingEntity (if applicable), unblockCondition (if applicable)',
91
+ '- Cannot continue work until blocker is actually resolved (not just "in progress")',
92
+ '- Examples:',
93
+ ' - internal_dependency: dependent TASK must be DONE first',
94
+ ' - external_dependency: external party must deliver (tracked via escalationPath)',
95
+ ' - resource: tool/access/budget must be allocated (tracked via escalationPath)',
96
+ ' - approval: decision-maker must sign off (tracked via blockingEntity)',
97
+ '- **What to do next:** Follow BLOCKED guidance to unblock, then re-run `taskNext()`',
98
+ '',
99
+ '**Stop Reason 2: Task is DONE and all acceptance criteria are met**',
100
+ '- Verify in `taskContext()` that all links/references are consistent',
101
+ '- Check that report/design/roadmap evidence is properly documented',
102
+ '- **What to do next:** Run `taskNext()` to pick the next task',
103
+ '',
104
+ '**Stop Reason 3: No actionable tasks (all TODO blocked or no tasks exist)**',
105
+ '- Means: all unblocked work is complete, only blocked tasks remain OR no tasks at all',
106
+ '- **What to do next:** Analyze blockers and roadmap, create 1-3 new TODO tasks via `taskCreate()`',
91
107
  '',
92
108
  '## Special Cases',
93
109
  '',
94
110
  '### Case 1: No .projitive directory',
95
111
  'Call `projectInit(projectPath="<project-dir>")` to initialize governance structure.',
96
112
  '',
97
- '### Case 2: No actionable 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 via `taskCreate()`',
113
+ '### Case 2: No actionable tasks (all TODO/IN_PROGRESS are blocked OR task count is zero)',
114
+ '',
115
+ '**If some tasks exist but all are BLOCKED:**',
116
+ '1. Run `taskList()` and filter by status = BLOCKED',
117
+ '2. For each BLOCKED task, check its blocker type:',
118
+ ' - **internal_dependency**: Is the blocking task listed? If no, create it',
119
+ ' - **external_dependency**: Is escalationPath defined? If no, add it',
120
+ ' - **resource**: Is escalationPath defined for requestor? If no, add it',
121
+ ' - **approval**: Is blockingEntity defined? If no, add it',
122
+ '3. For each blocker with missing metadata: call `taskUpdate()` to complete it',
123
+ '4. Select 1 BLOCKED task: follow its unblock guidance to take concrete action',
124
+ '5. Once blocker is resolved: call `taskUpdate(..., {status: "TODO"})` to unblock',
125
+ '6. Re-run `taskNext()` to see newly unblocked tasks',
126
+ '',
127
+ '**If no tasks exist at all (or only DONE tasks):**',
128
+ '1. Review `projectContext()` to confirm task count is 0',
129
+ '2. Read active roadmap milestones via `roadmapContext()`',
130
+ '3. For each active milestone, derive 1-3 executable TODO tasks:',
131
+ ' - **Clear outcome**: one-sentence done condition',
132
+ ' - **Evidence target**: at least one artifact (report, design, readme update)',
133
+ ' - **Small slice**: completable in one focused execution cycle',
134
+ ' - **Roadmap link**: link to roadmap via roadmapRefs',
135
+ ' - **Minimal dependencies**: avoid complex task chains at start',
136
+ '4. Call `taskCreate()` for each task with complete metadata',
137
+ '5. Re-run `taskNext()` to start executing the new tasks',
138
+ '',
139
+ '**If roadmap is also empty:**',
140
+ '1. Read design documents under `projitive://designs/`',
141
+ '2. Identify next architectural milestone or feature slice',
142
+ '3. Document in ROADMAP.md via `roadmapCreate()`',
143
+ '4. Create corresponding TODO tasks via `taskCreate()`',
109
144
  '',
110
145
  '## Hard Rules',
111
146
  '',
@@ -114,6 +149,7 @@ export function registerQuickStartPrompt(server) {
114
149
  '- **.projitive governance store is source of truth** - tasks.md/roadmap.md are generated views and may be overwritten',
115
150
  '- **Prefer tool writes over manual table/view edits** - Use taskCreate/taskUpdate/roadmapCreate/roadmapUpdate',
116
151
  '- **Always verify after updates** - Re-run taskContext() to confirm reference consistency',
152
+ '- **BLOCKED tasks require full blocker metadata** - type, description required; blockingEntity, unblockCondition, escalationPath as needed',
117
153
  ].join('\n');
118
154
  return asUserPrompt(text);
119
155
  });
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { registerQuickStartPrompt } from './quickStart.js';
3
+ describe('quickStart prompt', () => {
4
+ it('registers the prompt and renders known project guidance', async () => {
5
+ const server = { registerPrompt: vi.fn() };
6
+ registerQuickStartPrompt(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('quickStart');
11
+ expect(text).toContain('Known project path: "/workspace/app"');
12
+ expect(text).toContain('taskCreate');
13
+ expect(text).toContain('.projitive governance store is source of truth');
14
+ });
15
+ it('renders discovery workflow when project path is unknown', async () => {
16
+ const server = { registerPrompt: vi.fn() };
17
+ registerQuickStartPrompt(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 `projectLocate(inputPath="<selected-path>")` to lock governance root');
23
+ });
24
+ });
@@ -73,21 +73,30 @@ export function registerTaskDiscoveryPrompt(server) {
73
73
  ' - Tasks with roadmapRefs first',
74
74
  ' - Tasks with explicit owner first',
75
75
  '',
76
- '3. **BLOCKED** - Last consider blocked tasks',
77
- ' - Need to analyze blocker reason first',
78
- ' - May need to create sub-task to resolve blocker',
76
+ '3. **BLOCKED** - Last: unblock first before starting new work',
77
+ ' - Each BLOCKED task has blocker metadata with clear action steps',
78
+ ' - Check blocker type (internal_dependency, external_dependency, resource, approval)',
79
+ ' - Read taskContext to see blocker details and required action path',
80
+ ' - Follow the action path to unblock (may involve creating unblock task or reaching out)',
81
+ ' - Only move to TODO after unblock condition is actually met + documented',
79
82
  '',
80
83
  '### Discovery Methods',
81
84
  '',
82
85
  '#### Method A: Auto-select with taskNext() (Recommended)',
83
86
  '',
84
87
  'Call `taskNext()`, the tool will automatically:',
85
- '- Sort all tasks by priority',
88
+ '- Sort all tasks by priority (IN_PROGRESS > TODO, filter out pure BLOCKED)',
86
89
  '- Select highest-priority actionable task',
87
90
  '- Return task ID and summary',
88
91
  '',
89
92
  'Then call `taskContext(projectPath="...", taskId="<task-id>")` for task details.',
90
93
  '',
94
+ '**If taskNext returns a BLOCKED task:**',
95
+ '- This means all TODO/IN_PROGRESS options have been exhausted',
96
+ '- You MUST resolve the blocker before starting other work',
97
+ '- Read blocker metadata and follow action path',
98
+ '- Once blocker is resolved, re-run taskNext() to get next actionable task',
99
+ '',
91
100
  '### Discovery Quality Gate (before creating new tasks)',
92
101
  'Only create a TODO when all conditions are true:',
93
102
  '- It can be finished in one focused execution cycle',