@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.
- package/output/package.json +1 -1
- package/output/source/common/errors.test.js +59 -0
- package/output/source/common/files.js +20 -9
- package/output/source/common/index.js +1 -0
- package/output/source/common/linter.js +3 -1
- package/output/source/common/response.js +51 -67
- package/output/source/common/tool.js +43 -0
- package/output/source/common/utils.test.js +48 -0
- package/output/source/index.runtime.test.js +57 -0
- package/output/source/prompts/index.test.js +23 -0
- package/output/source/prompts/quickStart.js +58 -22
- package/output/source/prompts/quickStart.test.js +24 -0
- package/output/source/prompts/taskDiscovery.js +13 -4
- package/output/source/prompts/taskDiscovery.test.js +24 -0
- package/output/source/prompts/taskExecution.js +71 -12
- package/output/source/prompts/taskExecution.test.js +27 -0
- package/output/source/resources/designs.resources.test.js +52 -0
- package/output/source/resources/governance.test.js +35 -0
- package/output/source/resources/index.test.js +18 -0
- package/output/source/tools/index.test.js +23 -0
- package/output/source/tools/project.js +210 -257
- package/output/source/tools/project.test.js +136 -4
- package/output/source/tools/roadmap.js +182 -216
- package/output/source/tools/roadmap.test.js +187 -0
- package/output/source/tools/task.js +779 -515
- package/output/source/tools/task.test.js +323 -2
- package/output/source/types.js +6 -0
- package/package.json +1 -1
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, findTextReferences, ensureStore, loadActionableTasksFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, replaceTasksInStore, upsertTaskInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, } from '../common/index.js';
|
|
5
|
-
import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from '../common/index.js';
|
|
6
|
-
import { TASK_LINT_CODES, renderLintSuggestions } from '../common/index.js';
|
|
4
|
+
import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, findTextReferences, ensureStore, loadActionableTasksFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, replaceTasksInStore, upsertTaskInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, createGovernedTool, ToolExecutionError, PROJECT_LINT_CODES, TASK_LINT_CODES, renderLintSuggestions, } from '../common/index.js';
|
|
7
5
|
import { resolveGovernanceDir, resolveScanDepth, resolveScanRoots, discoverProjectsAcrossRoots, toProjectPath } from './project.js';
|
|
8
6
|
import { isValidRoadmapId } from './roadmap.js';
|
|
9
7
|
import { SUB_STATE_PHASES, BLOCKER_TYPES } from '../types.js';
|
|
10
8
|
export const ALLOWED_STATUS = ['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE'];
|
|
11
9
|
export const TASK_ID_REGEX = /^TASK-(\d+)$/;
|
|
12
10
|
export const TASKS_MARKDOWN_FILE = 'tasks.md';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
export const TASK_RESEARCH_DIR = 'designs/research';
|
|
12
|
+
export const TASK_RESEARCH_FILE_SUFFIX = '.implementation-research.md';
|
|
13
|
+
export const CORE_DESIGN_DOCS_DIR = 'designs/core';
|
|
14
|
+
export const CORE_ARCHITECTURE_DOC_FILE = `${CORE_DESIGN_DOCS_DIR}/architecture.md`;
|
|
15
|
+
export const CORE_STYLE_DOC_FILE = `${CORE_DESIGN_DOCS_DIR}/style-guide.md`;
|
|
16
16
|
function taskStatusGuidance(task) {
|
|
17
17
|
if (task.status === 'TODO') {
|
|
18
18
|
return [
|
|
@@ -27,10 +27,180 @@ function taskStatusGuidance(task) {
|
|
|
27
27
|
];
|
|
28
28
|
}
|
|
29
29
|
if (task.status === 'BLOCKED') {
|
|
30
|
-
|
|
31
|
-
'
|
|
32
|
-
'
|
|
30
|
+
const guidance = [
|
|
31
|
+
'## BLOCKED Task - Structured Unblocking Path',
|
|
32
|
+
'',
|
|
33
33
|
];
|
|
34
|
+
// ===== LAYER 1: CRITICAL VALIDATION =====
|
|
35
|
+
guidance.push('### 🔴 CRITICAL - Validate Blocker Metadata First');
|
|
36
|
+
guidance.push('');
|
|
37
|
+
if (!task.blocker) {
|
|
38
|
+
guidance.push('⚠️ **BLOCKER MISSING** - Task cannot be truly BLOCKED without blocker metadata.');
|
|
39
|
+
guidance.push('');
|
|
40
|
+
guidance.push('**Required Action:**');
|
|
41
|
+
guidance.push('```');
|
|
42
|
+
guidance.push('taskUpdate(projectPath="...", taskId="' + task.id + '", {');
|
|
43
|
+
guidance.push(' blocker: {');
|
|
44
|
+
guidance.push(' type: "internal_dependency|external_dependency|resource|approval",');
|
|
45
|
+
guidance.push(' description: "Specific reason for block",');
|
|
46
|
+
guidance.push(' blockingEntity: "Optional: who/what is blocking",');
|
|
47
|
+
guidance.push(' unblockCondition: "Optional: exact condition to unblock"');
|
|
48
|
+
guidance.push(' }');
|
|
49
|
+
guidance.push('})');
|
|
50
|
+
guidance.push('```');
|
|
51
|
+
guidance.push('');
|
|
52
|
+
guidance.push('**Then re-run taskContext() to see type-specific guidance.**');
|
|
53
|
+
return guidance;
|
|
54
|
+
}
|
|
55
|
+
const { type, description, blockingEntity, unblockCondition, escalationPath } = task.blocker;
|
|
56
|
+
guidance.push('**Blocker Summary:**');
|
|
57
|
+
guidance.push(`- Type: **${type}**`);
|
|
58
|
+
guidance.push(`- Issue: ${description}`);
|
|
59
|
+
if (blockingEntity)
|
|
60
|
+
guidance.push(`- Blocking Entity: ${blockingEntity}`);
|
|
61
|
+
if (unblockCondition)
|
|
62
|
+
guidance.push(`- Unblock Condition: ${unblockCondition}`);
|
|
63
|
+
guidance.push('');
|
|
64
|
+
// ===== LAYER 2: HOW TO UNBLOCK =====
|
|
65
|
+
guidance.push('### 🟠 HOW TO UNBLOCK - Type-Specific Steps');
|
|
66
|
+
guidance.push('');
|
|
67
|
+
if (type === 'internal_dependency') {
|
|
68
|
+
guidance.push('**This task is blocked by another task (internal dependency).**');
|
|
69
|
+
guidance.push('');
|
|
70
|
+
guidance.push('**Step 1: Identify the Blocking Task**');
|
|
71
|
+
guidance.push('- Call `taskList()` and search for a task matching this description:');
|
|
72
|
+
guidance.push(` "${unblockCondition || description}"`);
|
|
73
|
+
guidance.push('- Or ask: who owns completing this blocker?');
|
|
74
|
+
guidance.push('');
|
|
75
|
+
guidance.push('**Step 2: Check Blocking Task Status**');
|
|
76
|
+
guidance.push('- If DONE: Proceed to Step 4');
|
|
77
|
+
guidance.push('- If TODO/IN_PROGRESS: Coordinate with owner → proceed to Step 4 when complete');
|
|
78
|
+
guidance.push('- If NOT FOUND: Proceed to Step 3');
|
|
79
|
+
guidance.push('');
|
|
80
|
+
guidance.push('**Step 3: Create the Missing Blocking Task (if needed)**');
|
|
81
|
+
guidance.push('```');
|
|
82
|
+
guidance.push('taskCreate(projectPath="...", {');
|
|
83
|
+
guidance.push(' title: "Unblock ' + task.id + ': [specific outcome]",');
|
|
84
|
+
guidance.push(' status: "TODO",');
|
|
85
|
+
guidance.push(' summary: "Required to unblock ' + task.id + '"');
|
|
86
|
+
guidance.push('})');
|
|
87
|
+
guidance.push('```');
|
|
88
|
+
guidance.push('');
|
|
89
|
+
guidance.push('**Step 4: After Blocker is Resolved - Unblock This Task**');
|
|
90
|
+
guidance.push('- Verify the blocker is actually DONE (not just in progress)');
|
|
91
|
+
guidance.push('- Call `taskUpdate()` to move back to TODO:');
|
|
92
|
+
guidance.push('```');
|
|
93
|
+
guidance.push('taskUpdate(projectPath="...", taskId="' + task.id + '", {status: "TODO"})');
|
|
94
|
+
guidance.push('```');
|
|
95
|
+
guidance.push('- This removes the BLOCKED state and allows execution to continue');
|
|
96
|
+
}
|
|
97
|
+
else if (type === 'external_dependency') {
|
|
98
|
+
guidance.push('**This task is blocked by an external party/service/event.**');
|
|
99
|
+
guidance.push('');
|
|
100
|
+
guidance.push('**Step 1: Understand What is Needed**');
|
|
101
|
+
guidance.push(`- Blocking Entity: ${blockingEntity || '(not documented)'}`);
|
|
102
|
+
guidance.push(`- Description: ${description}`);
|
|
103
|
+
guidance.push('- Contact method: (verify in your knowledge base)');
|
|
104
|
+
guidance.push('');
|
|
105
|
+
guidance.push('**Step 2: Reach Out or Escalate**');
|
|
106
|
+
if (escalationPath) {
|
|
107
|
+
guidance.push(`- Use escalation path: ${escalationPath}`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
guidance.push('- If escalation path missing: call `taskUpdate()` to add it');
|
|
111
|
+
}
|
|
112
|
+
guidance.push('- Send request specifying: what, by when, and why (link this TASK ID)');
|
|
113
|
+
guidance.push('');
|
|
114
|
+
guidance.push('**Step 3: Track External Progress**');
|
|
115
|
+
guidance.push('- Check status periodically');
|
|
116
|
+
guidance.push('- Update task with any new information via `taskUpdate()`');
|
|
117
|
+
guidance.push('- If blocked for too long: follow escalation path');
|
|
118
|
+
guidance.push('');
|
|
119
|
+
guidance.push('**Step 4: After External Delivery - Unblock This Task**');
|
|
120
|
+
guidance.push('- Verify delivery is complete and acceptable');
|
|
121
|
+
guidance.push('- Call `taskUpdate()` to move back to TODO:');
|
|
122
|
+
guidance.push('```');
|
|
123
|
+
guidance.push('taskUpdate(projectPath="...", taskId="' + task.id + '", {status: "TODO"})');
|
|
124
|
+
guidance.push('```');
|
|
125
|
+
}
|
|
126
|
+
else if (type === 'resource') {
|
|
127
|
+
guidance.push('**This task is blocked by missing resource (tools, access, personnel, budget, etc.).**');
|
|
128
|
+
guidance.push('');
|
|
129
|
+
guidance.push('**Step 1: Clarify the Missing Resource**');
|
|
130
|
+
guidance.push(`- Need: ${description}`);
|
|
131
|
+
guidance.push('- Who can allocate? (should be in escalationPath)');
|
|
132
|
+
guidance.push('');
|
|
133
|
+
guidance.push('**Step 2: Request or Allocate**');
|
|
134
|
+
if (escalationPath) {
|
|
135
|
+
guidance.push(`- Contact: ${escalationPath}`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
guidance.push('- FIX: Call `taskUpdate()` to add escalationPath for resource owner');
|
|
139
|
+
}
|
|
140
|
+
guidance.push('- Provide justification (link this TASK ID and explain why needed)');
|
|
141
|
+
guidance.push('');
|
|
142
|
+
guidance.push('**Step 3: Wait for Approval and Setup**');
|
|
143
|
+
guidance.push('- Track approval and allocation status');
|
|
144
|
+
guidance.push('- Once allocated and available: proceed to Step 4');
|
|
145
|
+
guidance.push('');
|
|
146
|
+
guidance.push('**Step 4: After Resource Secured - Unblock This Task**');
|
|
147
|
+
guidance.push('- Confirm resource is ready to use');
|
|
148
|
+
guidance.push('- Call `taskUpdate()` to move back to TODO:');
|
|
149
|
+
guidance.push('```');
|
|
150
|
+
guidance.push('taskUpdate(projectPath="...", taskId="' + task.id + '", {status: "TODO"})');
|
|
151
|
+
guidance.push('```');
|
|
152
|
+
}
|
|
153
|
+
else if (type === 'approval') {
|
|
154
|
+
guidance.push('**This task is blocked by pending approval from decision maker.**');
|
|
155
|
+
guidance.push('');
|
|
156
|
+
guidance.push('**Step 1: Identify Approver**');
|
|
157
|
+
guidance.push(`- Approver: ${blockingEntity || '(not documented)'}`);
|
|
158
|
+
guidance.push('- Approval criteria: (ensure clear in description)');
|
|
159
|
+
guidance.push('');
|
|
160
|
+
guidance.push('**Step 2: Prepare and Submit Approval Request**');
|
|
161
|
+
guidance.push('- What are you asking approval for? (clear one-sentence request)');
|
|
162
|
+
guidance.push('- Why? (link this TASK ID and provide context)');
|
|
163
|
+
guidance.push('- By when? (deadline)');
|
|
164
|
+
if (blockingEntity) {
|
|
165
|
+
guidance.push(`- Send to: ${blockingEntity}`);
|
|
166
|
+
}
|
|
167
|
+
guidance.push('');
|
|
168
|
+
guidance.push('**Step 3: Track Approval Process**');
|
|
169
|
+
guidance.push('- Follow up if no response by deadline');
|
|
170
|
+
if (escalationPath) {
|
|
171
|
+
guidance.push(`- If denied or stalled: use escalation path: ${escalationPath}`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
guidance.push('- If escalation needed: call `taskUpdate()` to add escalationPath');
|
|
175
|
+
}
|
|
176
|
+
guidance.push('');
|
|
177
|
+
guidance.push('**Step 4: After Approval Granted - Unblock This Task**');
|
|
178
|
+
guidance.push('- Confirm approval is in writing (link to approval evidence)');
|
|
179
|
+
guidance.push('- Call `taskUpdate()` to move back to TODO:');
|
|
180
|
+
guidance.push('```');
|
|
181
|
+
guidance.push('taskUpdate(projectPath="...", taskId="' + task.id + '", {status: "TODO"})');
|
|
182
|
+
guidance.push('```');
|
|
183
|
+
}
|
|
184
|
+
guidance.push('');
|
|
185
|
+
// ===== LAYER 3: REFERENCE INFORMATION =====
|
|
186
|
+
guidance.push('### ℹ️ REFERENCE - System-Wide Information');
|
|
187
|
+
guidance.push('');
|
|
188
|
+
guidance.push('**All Blocker Types:**');
|
|
189
|
+
guidance.push('- `internal_dependency` — Blocked by another task that must complete first');
|
|
190
|
+
guidance.push('- `external_dependency` — Blocked by external party/service/event');
|
|
191
|
+
guidance.push('- `resource` — Blocked by missing resource (tool, access, personnel, budget)');
|
|
192
|
+
guidance.push('- `approval` — Blocked by pending decision/sign-off');
|
|
193
|
+
guidance.push('');
|
|
194
|
+
guidance.push('**Unblock Verification Checklist:**');
|
|
195
|
+
guidance.push('- ✓ Blocker condition is actually met (not just "almost done")');
|
|
196
|
+
guidance.push('- ✓ Evidence is documented (link to TASK/report/email/etc)');
|
|
197
|
+
guidance.push('- ✓ Task status can be safely moved back to TODO');
|
|
198
|
+
guidance.push('');
|
|
199
|
+
guidance.push('**After Unblocking - Next Steps:**');
|
|
200
|
+
guidance.push('1. Call `taskUpdate(..., {status: "TODO"})` to unblock');
|
|
201
|
+
guidance.push('2. Call `taskContext()` to see task in unblocked state');
|
|
202
|
+
guidance.push('3. Call `taskNext()` to resume execution flow');
|
|
203
|
+
return guidance;
|
|
34
204
|
}
|
|
35
205
|
return [
|
|
36
206
|
'- This task is DONE: only reopen when new requirement changes scope.',
|
|
@@ -39,13 +209,18 @@ function taskStatusGuidance(task) {
|
|
|
39
209
|
}
|
|
40
210
|
const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
|
|
41
211
|
'- Recheck project state first: run projectContext and confirm there is truly no TODO/IN_PROGRESS task to execute.',
|
|
42
|
-
'-
|
|
43
|
-
'-
|
|
44
|
-
'-
|
|
45
|
-
'-
|
|
212
|
+
'- Check BLOCKED tasks: if BLOCKED tasks exist, read their blocker metadata and take unblock action before creating new tasks.',
|
|
213
|
+
' - internal_dependency: create/track the blocking task, coordinate with owner',
|
|
214
|
+
' - external_dependency: reach out to blocking entity or escalate',
|
|
215
|
+
' - resource: request/allocate the missing resource',
|
|
216
|
+
' - approval: follow escalation path to expedite approval',
|
|
217
|
+
'- Only after all BLOCKED tasks are unblocked (moved back to TODO/IN_PROGRESS), then create new tasks via `taskCreate(...)`.',
|
|
46
218
|
'- Create TODO tasks only when evidence is clear: each new task must produce at least one report/designs/readme artifact update.',
|
|
219
|
+
'- Start from active roadmap milestones and split into smallest executable slices with single done condition each.',
|
|
220
|
+
'- Prefer slices that unlock multiple downstream tasks before isolated refactors or low-impact cleanups.',
|
|
47
221
|
'- Skip duplicate scope: do not create tasks that overlap existing TODO/IN_PROGRESS/BLOCKED task intent.',
|
|
48
222
|
'- Use quality gates for discovery candidates: user value, delivery risk reduction, or measurable throughput improvement.',
|
|
223
|
+
'- Review and update project architecture docs under designs/core/ (architecture.md, style-guide.md) if they are missing or outdated.',
|
|
49
224
|
'- Keep each discovery round small (1-3 tasks), then rerun taskNext immediately for re-ranking and execution.',
|
|
50
225
|
];
|
|
51
226
|
const DEFAULT_TASK_CONTEXT_READING_GUIDANCE = [
|
|
@@ -113,6 +288,87 @@ function normalizeTaskLink(link) {
|
|
|
113
288
|
function resolveTaskLinkPath(projectPath, link) {
|
|
114
289
|
return path.join(projectPath, link);
|
|
115
290
|
}
|
|
291
|
+
function taskResearchBriefRelativePath(taskId) {
|
|
292
|
+
return `${TASK_RESEARCH_DIR}/${taskId}${TASK_RESEARCH_FILE_SUFFIX}`;
|
|
293
|
+
}
|
|
294
|
+
function renderTaskResearchBriefTemplate(task) {
|
|
295
|
+
return [
|
|
296
|
+
`# ${task.id} Implementation Research Brief`,
|
|
297
|
+
'',
|
|
298
|
+
`Task: ${task.title}`,
|
|
299
|
+
`Summary: ${task.summary || '(fill this with a short objective summary)'}`,
|
|
300
|
+
'',
|
|
301
|
+
'## Design Guidelines and Specs',
|
|
302
|
+
'- [ ] List relevant design/governance/spec files with line location',
|
|
303
|
+
'- Example: designs/ARCHITECTURE.md#L42-L76 - API boundary and constraints',
|
|
304
|
+
'- Example: roadmap.md#L18 - milestone acceptance criteria',
|
|
305
|
+
'',
|
|
306
|
+
'## Code Architecture and Implementation Findings',
|
|
307
|
+
'- [ ] Document current architecture and extension points with line location',
|
|
308
|
+
'- Example: packages/mcp/source/tools/task.ts#L1020-L1130 - taskContext response assembly',
|
|
309
|
+
'- Example: packages/mcp/source/prompts/taskExecution.ts#L25-L130 - execution workflow prompt',
|
|
310
|
+
'',
|
|
311
|
+
'## Implementation Plan',
|
|
312
|
+
'- [ ] Proposed change list with impacted modules',
|
|
313
|
+
'- [ ] Validation and regression test plan',
|
|
314
|
+
'',
|
|
315
|
+
'## Risks and Open Questions',
|
|
316
|
+
'- [ ] Known risks, assumptions, and unresolved questions',
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
async function inspectTaskResearchBrief(governanceDir, task) {
|
|
320
|
+
const projectPath = toProjectPath(governanceDir);
|
|
321
|
+
const relativePath = taskResearchBriefRelativePath(task.id);
|
|
322
|
+
const absolutePath = resolveTaskLinkPath(projectPath, relativePath);
|
|
323
|
+
const exists = await fs.access(absolutePath).then(() => true).catch(() => false);
|
|
324
|
+
return { relativePath, absolutePath, exists, ready: exists };
|
|
325
|
+
}
|
|
326
|
+
function collectTaskResearchBriefLintSuggestions(state) {
|
|
327
|
+
if (!state.exists) {
|
|
328
|
+
return [{
|
|
329
|
+
code: TASK_LINT_CODES.RESEARCH_BRIEF_MISSING,
|
|
330
|
+
message: `Pre-execution research brief missing: ${state.relativePath}.`,
|
|
331
|
+
fixHint: 'Create the file and fill required sections before implementation.',
|
|
332
|
+
}];
|
|
333
|
+
}
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
function inspectProjectContextDocsFromArtifacts(files) {
|
|
337
|
+
const markdownFiles = files
|
|
338
|
+
.map((item) => item.replace(/\\/g, '/'))
|
|
339
|
+
.filter((item) => item.toLowerCase().endsWith('.md'));
|
|
340
|
+
const architectureDocSuffix = `/${CORE_ARCHITECTURE_DOC_FILE}`.toLowerCase();
|
|
341
|
+
const styleDocSuffix = `/${CORE_STYLE_DOC_FILE}`.toLowerCase();
|
|
342
|
+
const architectureDocs = markdownFiles.filter((item) => item.toLowerCase().endsWith(architectureDocSuffix));
|
|
343
|
+
const styleDocs = markdownFiles.filter((item) => item.toLowerCase().endsWith(styleDocSuffix));
|
|
344
|
+
const missingArchitectureDocs = architectureDocs.length === 0;
|
|
345
|
+
const missingStyleDocs = styleDocs.length === 0;
|
|
346
|
+
return {
|
|
347
|
+
architectureDocs,
|
|
348
|
+
styleDocs,
|
|
349
|
+
missingArchitectureDocs,
|
|
350
|
+
missingStyleDocs,
|
|
351
|
+
ready: !missingArchitectureDocs && !missingStyleDocs,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function collectProjectContextDocsLintSuggestions(state) {
|
|
355
|
+
const suggestions = [];
|
|
356
|
+
if (state.missingArchitectureDocs) {
|
|
357
|
+
suggestions.push({
|
|
358
|
+
code: PROJECT_LINT_CODES.ARCHITECTURE_DOC_MISSING,
|
|
359
|
+
message: 'Project context is missing architecture design documentation.',
|
|
360
|
+
fixHint: `Add required file: ${CORE_ARCHITECTURE_DOC_FILE}.`,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
if (state.missingStyleDocs) {
|
|
364
|
+
suggestions.push({
|
|
365
|
+
code: PROJECT_LINT_CODES.STYLE_DOC_MISSING,
|
|
366
|
+
message: 'Project context is missing design style documentation.',
|
|
367
|
+
fixHint: `Add required file: ${CORE_STYLE_DOC_FILE}.`,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return suggestions;
|
|
371
|
+
}
|
|
116
372
|
async function readActionableTaskCandidates(governanceDirs) {
|
|
117
373
|
const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
|
|
118
374
|
const tasksPath = path.join(governanceDir, '.projitive');
|
|
@@ -382,7 +638,7 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
382
638
|
export function collectTaskLintSuggestions(tasks) {
|
|
383
639
|
return renderLintSuggestions(collectTaskLintSuggestionItems(tasks));
|
|
384
640
|
}
|
|
385
|
-
function
|
|
641
|
+
function collectSingleTaskLintSuggestionItems(task) {
|
|
386
642
|
const suggestions = [];
|
|
387
643
|
if (task.status === 'IN_PROGRESS' && task.owner.trim().length === 0) {
|
|
388
644
|
suggestions.push({
|
|
@@ -478,37 +734,21 @@ function collectSingleTaskLintSuggestions(task) {
|
|
|
478
734
|
fixHint: 'Confidence must be between 0.0 and 1.0.',
|
|
479
735
|
});
|
|
480
736
|
}
|
|
481
|
-
return
|
|
737
|
+
return suggestions;
|
|
482
738
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
|
|
497
|
-
message: `Link path should be project-root-relative without leading slash: ${normalized}.`,
|
|
498
|
-
fixHint: 'Use path/from/project/root format.',
|
|
499
|
-
});
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
const resolvedPath = resolveTaskLinkPath(projectPath, normalized);
|
|
503
|
-
const exists = await fs.access(resolvedPath).then(() => true).catch(() => false);
|
|
504
|
-
if (!exists) {
|
|
505
|
-
suggestions.push({
|
|
506
|
-
code: TASK_LINT_CODES.LINK_TARGET_MISSING,
|
|
507
|
-
message: `Link target not found: ${normalized} (resolved: ${resolvedPath}).`,
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
return renderLintSuggestions(suggestions);
|
|
739
|
+
function collectSingleTaskLintSuggestions(task) {
|
|
740
|
+
return renderLintSuggestions(collectSingleTaskLintSuggestionItems(task));
|
|
741
|
+
}
|
|
742
|
+
async function collectDoneConformanceSuggestions(governanceDir, task) {
|
|
743
|
+
const researchBriefState = await inspectTaskResearchBrief(governanceDir, task);
|
|
744
|
+
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
745
|
+
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
746
|
+
const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
|
|
747
|
+
return [
|
|
748
|
+
...collectSingleTaskLintSuggestionItems(task),
|
|
749
|
+
...collectTaskResearchBriefLintSuggestions(researchBriefState),
|
|
750
|
+
...collectProjectContextDocsLintSuggestions(projectContextDocsState),
|
|
751
|
+
];
|
|
512
752
|
}
|
|
513
753
|
export function renderTasksMarkdown(tasks) {
|
|
514
754
|
const sections = sortTasksNewestFirst(tasks).map((task) => {
|
|
@@ -610,7 +850,8 @@ export function validateTransition(from, to) {
|
|
|
610
850
|
return allowed[from].has(to);
|
|
611
851
|
}
|
|
612
852
|
export function registerTaskTools(server) {
|
|
613
|
-
server.registerTool(
|
|
853
|
+
server.registerTool(...createGovernedTool({
|
|
854
|
+
name: 'taskList',
|
|
614
855
|
title: 'Task List',
|
|
615
856
|
description: 'List tasks for a known project and optionally filter by status',
|
|
616
857
|
inputSchema: {
|
|
@@ -618,50 +859,48 @@ export function registerTaskTools(server) {
|
|
|
618
859
|
status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
|
|
619
860
|
limit: z.number().int().min(1).max(200).optional(),
|
|
620
861
|
},
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
});
|
|
664
|
-
server.registerTool('taskCreate', {
|
|
862
|
+
async execute({ projectPath, status, limit }) {
|
|
863
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
864
|
+
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
865
|
+
const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
|
|
866
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
867
|
+
const filtered = tasks
|
|
868
|
+
.filter((task) => (status ? task.status === status : true))
|
|
869
|
+
.slice(0, limit ?? 100);
|
|
870
|
+
return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, filtered, status };
|
|
871
|
+
},
|
|
872
|
+
summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, filtered, status }) => [
|
|
873
|
+
`- projectPath: ${normalizedProjectPath}`,
|
|
874
|
+
`- governanceDir: ${governanceDir}`,
|
|
875
|
+
`- tasksView: ${tasksViewPath}`,
|
|
876
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
877
|
+
`- filter.status: ${status ?? '(none)'}`,
|
|
878
|
+
`- returned: ${filtered.length}`,
|
|
879
|
+
],
|
|
880
|
+
evidence: ({ filtered }) => [
|
|
881
|
+
'- tasks:',
|
|
882
|
+
...filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner || ''} | updatedAt=${task.updatedAt}`),
|
|
883
|
+
],
|
|
884
|
+
guidance: () => ['- Pick one task ID and call `taskContext`.'],
|
|
885
|
+
suggestions: ({ filtered, status }) => {
|
|
886
|
+
const suggestions = collectTaskLintSuggestions(filtered);
|
|
887
|
+
if (status && filtered.length === 0) {
|
|
888
|
+
suggestions.push(...renderLintSuggestions([
|
|
889
|
+
{
|
|
890
|
+
code: TASK_LINT_CODES.FILTER_EMPTY,
|
|
891
|
+
message: `No tasks matched status=${status}.`,
|
|
892
|
+
fixHint: 'Confirm status values or update task states.',
|
|
893
|
+
},
|
|
894
|
+
]));
|
|
895
|
+
}
|
|
896
|
+
return suggestions;
|
|
897
|
+
},
|
|
898
|
+
nextCall: ({ filtered, normalizedProjectPath }) => filtered[0]
|
|
899
|
+
? `taskContext(projectPath="${normalizedProjectPath}", taskId="${filtered[0].id}")`
|
|
900
|
+
: undefined,
|
|
901
|
+
}));
|
|
902
|
+
server.registerTool(...createGovernedTool({
|
|
903
|
+
name: 'taskCreate',
|
|
665
904
|
title: 'Task Create',
|
|
666
905
|
description: 'Create a new task in governance store with stable TASK-<number> ID',
|
|
667
906
|
inputSchema: {
|
|
@@ -686,322 +925,379 @@ export function registerTaskTools(server) {
|
|
|
686
925
|
escalationPath: z.string().optional(),
|
|
687
926
|
}).optional(),
|
|
688
927
|
},
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
`- links: ${createdTask.links.join(', ') || '(none)'}`,
|
|
745
|
-
]),
|
|
746
|
-
guidanceSection([
|
|
747
|
-
'Task created in governance store successfully and tasks.md has been synced.',
|
|
748
|
-
'Run taskContext to verify references and lint guidance.',
|
|
749
|
-
]),
|
|
750
|
-
lintSection(lintSuggestions),
|
|
751
|
-
nextCallSection(`taskContext(projectPath="${normalizedProjectPath}", taskId="${createdTask.id}")`),
|
|
752
|
-
],
|
|
753
|
-
});
|
|
754
|
-
return asText(markdown);
|
|
755
|
-
});
|
|
756
|
-
server.registerTool('taskNext', {
|
|
928
|
+
async execute({ projectPath, taskId, title, status, owner, summary, roadmapRefs, links, subState, blocker }) {
|
|
929
|
+
if (taskId && !isValidTaskId(taskId)) {
|
|
930
|
+
throw new ToolExecutionError(`Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'omit taskId to auto-generate next ID'], `taskCreate(projectPath="${projectPath}", title="Define executable objective")`);
|
|
931
|
+
}
|
|
932
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
933
|
+
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
934
|
+
const { tasksPath, tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
|
|
935
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
936
|
+
const finalTaskId = taskId ?? nextTaskId(tasks);
|
|
937
|
+
const duplicated = tasks.some((item) => item.id === finalTaskId);
|
|
938
|
+
if (duplicated) {
|
|
939
|
+
throw new ToolExecutionError(`Task already exists: ${finalTaskId}`, ['task IDs must be unique', 'use taskUpdate for existing tasks'], `taskUpdate(projectPath="${normalizedProjectPath}", taskId="${finalTaskId}", updates={...})`);
|
|
940
|
+
}
|
|
941
|
+
const createdTask = normalizeTask({
|
|
942
|
+
id: finalTaskId,
|
|
943
|
+
title,
|
|
944
|
+
status: status ?? 'TODO',
|
|
945
|
+
owner,
|
|
946
|
+
summary,
|
|
947
|
+
roadmapRefs,
|
|
948
|
+
links,
|
|
949
|
+
subState,
|
|
950
|
+
blocker,
|
|
951
|
+
updatedAt: nowIso(),
|
|
952
|
+
});
|
|
953
|
+
await upsertTaskInStore(tasksPath, createdTask);
|
|
954
|
+
await loadTasksDocumentWithOptions(governanceDir, true);
|
|
955
|
+
return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, createdTask };
|
|
956
|
+
},
|
|
957
|
+
summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, createdTask }) => [
|
|
958
|
+
`- projectPath: ${normalizedProjectPath}`,
|
|
959
|
+
`- governanceDir: ${governanceDir}`,
|
|
960
|
+
`- tasksView: ${tasksViewPath}`,
|
|
961
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
962
|
+
`- taskId: ${createdTask.id}`,
|
|
963
|
+
`- status: ${createdTask.status}`,
|
|
964
|
+
`- owner: ${createdTask.owner || '(none)'}`,
|
|
965
|
+
`- updatedAt: ${createdTask.updatedAt}`,
|
|
966
|
+
],
|
|
967
|
+
evidence: ({ createdTask }) => [
|
|
968
|
+
'### Created Task',
|
|
969
|
+
`- ${createdTask.id} | ${createdTask.status} | ${createdTask.title}`,
|
|
970
|
+
`- summary: ${createdTask.summary || '(none)'}`,
|
|
971
|
+
`- roadmapRefs: ${createdTask.roadmapRefs.join(', ') || '(none)'}`,
|
|
972
|
+
`- links: ${createdTask.links.join(', ') || '(none)'}`,
|
|
973
|
+
],
|
|
974
|
+
guidance: () => [
|
|
975
|
+
'Task created in governance store successfully and tasks.md has been synced.',
|
|
976
|
+
'Run taskContext to verify references and lint guidance.',
|
|
977
|
+
],
|
|
978
|
+
suggestions: ({ createdTask }) => collectSingleTaskLintSuggestions(createdTask),
|
|
979
|
+
nextCall: ({ normalizedProjectPath, createdTask }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${createdTask.id}")`,
|
|
980
|
+
}));
|
|
981
|
+
server.registerTool(...createGovernedTool({
|
|
982
|
+
name: 'taskNext',
|
|
757
983
|
title: 'Task Next',
|
|
758
984
|
description: 'Start here to auto-select the highest-priority actionable task',
|
|
759
985
|
inputSchema: {
|
|
760
986
|
limit: z.number().int().min(1).max(20).optional(),
|
|
761
987
|
},
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
const
|
|
785
|
-
const
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
});
|
|
827
|
-
return asText(markdown);
|
|
828
|
-
}
|
|
829
|
-
const selected = rankedCandidates[0];
|
|
830
|
-
const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
|
|
831
|
-
const lintSuggestions = collectTaskLintSuggestions(selectedTaskDocument.tasks);
|
|
832
|
-
const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
|
|
833
|
-
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
834
|
-
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
|
|
835
|
-
const taskLocation = (await findTextReferences(selectedTaskDocument.markdownPath, selected.task.id))[0];
|
|
836
|
-
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
837
|
-
const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
|
|
838
|
-
const candidateLimit = limit ?? 5;
|
|
839
|
-
const markdown = renderToolResponseMarkdown({
|
|
840
|
-
toolName: 'taskNext',
|
|
841
|
-
sections: [
|
|
842
|
-
summarySection([
|
|
843
|
-
`- rootPaths: ${roots.join(', ')}`,
|
|
844
|
-
`- rootCount: ${roots.length}`,
|
|
845
|
-
`- maxDepth: ${depth}`,
|
|
846
|
-
`- matchedProjects: ${projects.length}`,
|
|
847
|
-
`- actionableTasks: ${rankedCandidates.length}`,
|
|
848
|
-
`- selectedProject: ${toProjectPath(selected.governanceDir)}`,
|
|
849
|
-
`- selectedTaskId: ${selected.task.id}`,
|
|
850
|
-
`- selectedTaskStatus: ${selected.task.status}`,
|
|
851
|
-
]),
|
|
852
|
-
evidenceSection([
|
|
853
|
-
'### Selected Task',
|
|
854
|
-
`- id: ${selected.task.id}`,
|
|
855
|
-
`- title: ${selected.task.title}`,
|
|
856
|
-
`- owner: ${selected.task.owner || '(none)'}`,
|
|
857
|
-
`- updatedAt: ${selected.task.updatedAt}`,
|
|
858
|
-
`- roadmapRefs: ${selected.task.roadmapRefs.join(', ') || '(none)'}`,
|
|
859
|
-
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selectedTaskDocument.markdownPath}`,
|
|
860
|
-
'',
|
|
861
|
-
'### Top Candidates',
|
|
862
|
-
...rankedCandidates
|
|
863
|
-
.slice(0, candidateLimit)
|
|
864
|
-
.map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | projectPath=${toProjectPath(item.governanceDir)} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
|
|
865
|
-
'',
|
|
866
|
-
'### Selection Reason',
|
|
867
|
-
'- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.',
|
|
868
|
-
`- Selected candidate scores: projectScore=${selected.projectScore}, taskPriority=${selected.taskPriority}, taskUpdatedAtMs=${selected.taskUpdatedAtMs}.`,
|
|
988
|
+
async execute({ limit }) {
|
|
989
|
+
const roots = resolveScanRoots();
|
|
990
|
+
const depth = resolveScanDepth();
|
|
991
|
+
const projects = await discoverProjectsAcrossRoots(roots, depth);
|
|
992
|
+
const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
|
|
993
|
+
if (rankedCandidates.length === 0) {
|
|
994
|
+
const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
|
|
995
|
+
const tasksPath = path.join(governanceDir, '.projitive');
|
|
996
|
+
await ensureStore(tasksPath);
|
|
997
|
+
const stats = await loadTaskStatusStatsFromStore(tasksPath);
|
|
998
|
+
const roadmapIds = await readRoadmapIds(governanceDir);
|
|
999
|
+
return { governanceDir, roadmapIds, total: stats.total, todo: stats.todo, inProgress: stats.inProgress, blocked: stats.blocked, done: stats.done };
|
|
1000
|
+
}));
|
|
1001
|
+
const preferredProject = projectSnapshots[0];
|
|
1002
|
+
const preferredRoadmapRef = preferredProject?.roadmapIds[0] ?? 'ROADMAP-0001';
|
|
1003
|
+
const noTaskDiscoveryGuidance = await resolveNoTaskDiscoveryGuidance(preferredProject?.governanceDir);
|
|
1004
|
+
return { isEmpty: true, roots, depth, projects, projectSnapshots, preferredProject, preferredRoadmapRef, noTaskDiscoveryGuidance };
|
|
1005
|
+
}
|
|
1006
|
+
const selected = rankedCandidates[0];
|
|
1007
|
+
const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
|
|
1008
|
+
const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
|
|
1009
|
+
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
1010
|
+
const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
|
|
1011
|
+
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
|
|
1012
|
+
const taskLocation = (await findTextReferences(selectedTaskDocument.markdownPath, selected.task.id))[0];
|
|
1013
|
+
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
1014
|
+
const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
|
|
1015
|
+
const candidateLimit = limit ?? 5;
|
|
1016
|
+
return {
|
|
1017
|
+
isEmpty: false,
|
|
1018
|
+
roots, depth, projects,
|
|
1019
|
+
rankedCandidates, selected, selectedTaskDocument,
|
|
1020
|
+
relatedArtifacts, referenceLocations,
|
|
1021
|
+
suggestedReadOrder, projectContextDocsState, taskLocation, candidateLimit,
|
|
1022
|
+
};
|
|
1023
|
+
},
|
|
1024
|
+
summary: (data) => {
|
|
1025
|
+
if (data.isEmpty) {
|
|
1026
|
+
return [
|
|
1027
|
+
`- rootPaths: ${data.roots.join(', ')}`,
|
|
1028
|
+
`- rootCount: ${data.roots.length}`,
|
|
1029
|
+
`- maxDepth: ${data.depth}`,
|
|
1030
|
+
`- matchedProjects: ${data.projects.length}`,
|
|
1031
|
+
'- actionableTasks: 0',
|
|
1032
|
+
];
|
|
1033
|
+
}
|
|
1034
|
+
return [
|
|
1035
|
+
`- rootPaths: ${data.roots.join(', ')}`,
|
|
1036
|
+
`- rootCount: ${data.roots.length}`,
|
|
1037
|
+
`- maxDepth: ${data.depth}`,
|
|
1038
|
+
`- matchedProjects: ${data.projects.length}`,
|
|
1039
|
+
`- actionableTasks: ${data.rankedCandidates.length}`,
|
|
1040
|
+
`- selectedProject: ${toProjectPath(data.selected.governanceDir)}`,
|
|
1041
|
+
`- selectedTaskId: ${data.selected.task.id}`,
|
|
1042
|
+
`- selectedTaskStatus: ${data.selected.task.status}`,
|
|
1043
|
+
];
|
|
1044
|
+
},
|
|
1045
|
+
evidence: (data) => {
|
|
1046
|
+
if (data.isEmpty) {
|
|
1047
|
+
return [
|
|
1048
|
+
'### Project Snapshots',
|
|
1049
|
+
...(data.projectSnapshots.length > 0
|
|
1050
|
+
? data.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)'}`)
|
|
1051
|
+
: ['- (none)']),
|
|
869
1052
|
'',
|
|
870
|
-
'###
|
|
871
|
-
...(
|
|
1053
|
+
'### Seed Task Template',
|
|
1054
|
+
...renderTaskSeedTemplate(data.preferredRoadmapRef),
|
|
1055
|
+
];
|
|
1056
|
+
}
|
|
1057
|
+
const { taskLocation, selectedTaskDocument, rankedCandidates, candidateLimit, relatedArtifacts, referenceLocations, suggestedReadOrder } = data;
|
|
1058
|
+
const taskLocationStr = taskLocation
|
|
1059
|
+
? `${taskLocation.filePath}#L${taskLocation.line}`
|
|
1060
|
+
: selectedTaskDocument.markdownPath;
|
|
1061
|
+
return [
|
|
1062
|
+
'### Selected Task',
|
|
1063
|
+
`- id: ${data.selected.task.id}`,
|
|
1064
|
+
`- title: ${data.selected.task.title}`,
|
|
1065
|
+
`- owner: ${data.selected.task.owner || '(none)'}`,
|
|
1066
|
+
`- updatedAt: ${data.selected.task.updatedAt}`,
|
|
1067
|
+
`- roadmapRefs: ${data.selected.task.roadmapRefs.join(', ') || '(none)'}`,
|
|
1068
|
+
`- taskLocation: ${taskLocationStr}`,
|
|
1069
|
+
'',
|
|
1070
|
+
'### Top Candidates',
|
|
1071
|
+
...rankedCandidates
|
|
1072
|
+
.slice(0, candidateLimit)
|
|
1073
|
+
.map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | projectPath=${toProjectPath(item.governanceDir)} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
|
|
1074
|
+
'',
|
|
1075
|
+
'### Selection Reason',
|
|
1076
|
+
'- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.',
|
|
1077
|
+
`- Selected candidate scores: projectScore=${data.selected.projectScore}, taskPriority=${data.selected.taskPriority}, taskUpdatedAtMs=${data.selected.taskUpdatedAtMs}.`,
|
|
1078
|
+
'',
|
|
1079
|
+
'### Related Artifacts',
|
|
1080
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
|
|
1081
|
+
'',
|
|
1082
|
+
'### Reference Locations',
|
|
1083
|
+
...(referenceLocations.length > 0
|
|
1084
|
+
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
1085
|
+
: ['- (none)']),
|
|
1086
|
+
'',
|
|
1087
|
+
'### Suggested Read Order',
|
|
1088
|
+
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
1089
|
+
];
|
|
1090
|
+
},
|
|
1091
|
+
guidance: (data) => {
|
|
1092
|
+
if (data.isEmpty) {
|
|
1093
|
+
return [
|
|
1094
|
+
'- No TODO/IN_PROGRESS task is available.',
|
|
1095
|
+
'- Create 1-3 new TODO tasks using `taskCreate(...)` from active roadmap slices.',
|
|
1096
|
+
'- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.',
|
|
1097
|
+
'- If roadmap has active milestones, analyze milestone intent and split into 1-3 executable TODO tasks.',
|
|
872
1098
|
'',
|
|
873
|
-
'###
|
|
874
|
-
...
|
|
875
|
-
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
876
|
-
: ['- (none)']),
|
|
1099
|
+
'### No-Task Discovery Checklist',
|
|
1100
|
+
...data.noTaskDiscoveryGuidance,
|
|
877
1101
|
'',
|
|
878
|
-
'
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1102
|
+
'- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.',
|
|
1103
|
+
'- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.',
|
|
1104
|
+
'- After creating tasks, rerun `taskNext` to re-rank actionable work.',
|
|
1105
|
+
];
|
|
1106
|
+
}
|
|
1107
|
+
return [
|
|
1108
|
+
...(!data.projectContextDocsState.ready
|
|
1109
|
+
? [
|
|
1110
|
+
'- Project context docs are incomplete. Complete missing project architecture/style docs before deep implementation.',
|
|
1111
|
+
...(data.projectContextDocsState.missingArchitectureDocs
|
|
1112
|
+
? [`- Missing architecture design doc: create required file ${CORE_ARCHITECTURE_DOC_FILE}.`]
|
|
1113
|
+
: []),
|
|
1114
|
+
...(data.projectContextDocsState.missingStyleDocs
|
|
1115
|
+
? [`- Missing design style doc: create required file ${CORE_STYLE_DOC_FILE}.`]
|
|
1116
|
+
: []),
|
|
1117
|
+
]
|
|
1118
|
+
: []),
|
|
1119
|
+
'- Start immediately with Suggested Read Order and execute the selected task.',
|
|
1120
|
+
'- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.',
|
|
1121
|
+
'- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.',
|
|
1122
|
+
];
|
|
1123
|
+
},
|
|
1124
|
+
suggestions: (data) => {
|
|
1125
|
+
if (data.isEmpty) {
|
|
1126
|
+
return [
|
|
1127
|
+
'- No actionable tasks found. Verify task statuses and required fields in .projitive task table.',
|
|
1128
|
+
'- Ensure each new task has stable TASK-<number> ID and at least one roadmapRefs item.',
|
|
1129
|
+
];
|
|
1130
|
+
}
|
|
1131
|
+
return [
|
|
1132
|
+
...collectTaskLintSuggestions(data.selectedTaskDocument.tasks),
|
|
1133
|
+
...renderLintSuggestions(collectProjectContextDocsLintSuggestions(data.projectContextDocsState)),
|
|
1134
|
+
];
|
|
1135
|
+
},
|
|
1136
|
+
nextCall: (data) => {
|
|
1137
|
+
if (data.isEmpty) {
|
|
1138
|
+
return data.preferredProject
|
|
1139
|
+
? `taskCreate(projectPath="${toProjectPath(data.preferredProject.governanceDir)}", title="Create first executable slice", roadmapRefs=["${data.preferredRoadmapRef}"], summary="Derived from active roadmap milestone")`
|
|
1140
|
+
: 'projectScan()';
|
|
1141
|
+
}
|
|
1142
|
+
return `taskContext(projectPath="${toProjectPath(data.selected.governanceDir)}", taskId="${data.selected.task.id}")`;
|
|
1143
|
+
},
|
|
1144
|
+
}));
|
|
1145
|
+
server.registerTool(...createGovernedTool({
|
|
1146
|
+
name: 'taskContext',
|
|
893
1147
|
title: 'Task Context',
|
|
894
1148
|
description: 'Get deep context, evidence links, and read order for one task',
|
|
895
1149
|
inputSchema: {
|
|
896
1150
|
projectPath: z.string(),
|
|
897
1151
|
taskId: z.string(),
|
|
898
1152
|
},
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
...asText(renderErrorMarkdown('taskContext', `Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'retry with a valid task ID'], `taskContext(projectPath="${projectPath}", taskId="TASK-0001")`)),
|
|
903
|
-
isError: true,
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
907
|
-
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
908
|
-
const { markdownPath, tasks } = await loadTasksDocument(governanceDir);
|
|
909
|
-
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
910
|
-
const task = tasks.find((item) => item.id === taskId);
|
|
911
|
-
if (!task) {
|
|
912
|
-
return {
|
|
913
|
-
...asText(renderErrorMarkdown('taskContext', `Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`)),
|
|
914
|
-
isError: true,
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
const lintSuggestions = [
|
|
918
|
-
...collectSingleTaskLintSuggestions(task),
|
|
919
|
-
...(await collectTaskFileLintSuggestions(governanceDir, task)),
|
|
920
|
-
];
|
|
921
|
-
const contextReadingGuidance = await resolveTaskContextReadingGuidance(governanceDir);
|
|
922
|
-
const taskLocation = (await findTextReferences(markdownPath, taskId))[0];
|
|
923
|
-
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
924
|
-
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
925
|
-
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
|
|
926
|
-
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
927
|
-
const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
|
|
928
|
-
// Build summary with subState and blocker info (v1.1.0)
|
|
929
|
-
const summaryLines = [
|
|
930
|
-
`- projectPath: ${normalizedProjectPath}`,
|
|
931
|
-
`- governanceDir: ${governanceDir}`,
|
|
932
|
-
`- tasksView: ${markdownPath}`,
|
|
933
|
-
`- roadmapView: ${roadmapViewPath}`,
|
|
934
|
-
`- taskId: ${task.id}`,
|
|
935
|
-
`- title: ${task.title}`,
|
|
936
|
-
`- status: ${task.status}`,
|
|
937
|
-
`- owner: ${task.owner}`,
|
|
938
|
-
`- updatedAt: ${task.updatedAt}`,
|
|
939
|
-
`- roadmapRefs: ${task.roadmapRefs.join(', ') || '(none)'}`,
|
|
940
|
-
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
|
|
941
|
-
];
|
|
942
|
-
// Add subState info for IN_PROGRESS tasks (v1.1.0)
|
|
943
|
-
if (task.subState && task.status === 'IN_PROGRESS') {
|
|
944
|
-
summaryLines.push('- subState:');
|
|
945
|
-
if (task.subState.phase) {
|
|
946
|
-
summaryLines.push(` - phase: ${task.subState.phase}`);
|
|
947
|
-
}
|
|
948
|
-
if (typeof task.subState.confidence === 'number') {
|
|
949
|
-
summaryLines.push(` - confidence: ${task.subState.confidence}`);
|
|
1153
|
+
async execute({ projectPath, taskId }) {
|
|
1154
|
+
if (!isValidTaskId(taskId)) {
|
|
1155
|
+
throw new ToolExecutionError(`Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'retry with a valid task ID'], `taskContext(projectPath="${projectPath}", taskId="TASK-0001")`);
|
|
950
1156
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
summaryLines.push(` - type: ${task.blocker.type}`);
|
|
959
|
-
summaryLines.push(` - description: ${task.blocker.description}`);
|
|
960
|
-
if (task.blocker.blockingEntity) {
|
|
961
|
-
summaryLines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
1157
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1158
|
+
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
1159
|
+
const { markdownPath, tasks } = await loadTasksDocument(governanceDir);
|
|
1160
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
1161
|
+
const task = tasks.find((item) => item.id === taskId);
|
|
1162
|
+
if (!task) {
|
|
1163
|
+
throw new ToolExecutionError(`Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`);
|
|
962
1164
|
}
|
|
963
|
-
|
|
964
|
-
|
|
1165
|
+
const researchBriefState = await inspectTaskResearchBrief(governanceDir, task);
|
|
1166
|
+
const contextReadingGuidance = await resolveTaskContextReadingGuidance(governanceDir);
|
|
1167
|
+
const taskLocation = (await findTextReferences(markdownPath, taskId))[0];
|
|
1168
|
+
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
1169
|
+
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
1170
|
+
const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
|
|
1171
|
+
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
|
|
1172
|
+
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
1173
|
+
const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
|
|
1174
|
+
return {
|
|
1175
|
+
normalizedProjectPath, governanceDir, markdownPath, roadmapViewPath,
|
|
1176
|
+
task, researchBriefState, contextReadingGuidance,
|
|
1177
|
+
taskLocation, referenceLocations, relatedArtifacts, suggestedReadOrder,
|
|
1178
|
+
projectContextDocsState,
|
|
1179
|
+
};
|
|
1180
|
+
},
|
|
1181
|
+
summary: ({ normalizedProjectPath, governanceDir, markdownPath, roadmapViewPath, task, researchBriefState, projectContextDocsState, taskLocation }) => {
|
|
1182
|
+
const lines = [
|
|
1183
|
+
`- projectPath: ${normalizedProjectPath}`,
|
|
1184
|
+
`- governanceDir: ${governanceDir}`,
|
|
1185
|
+
`- tasksView: ${markdownPath}`,
|
|
1186
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
1187
|
+
`- taskId: ${task.id}`,
|
|
1188
|
+
`- title: ${task.title}`,
|
|
1189
|
+
`- status: ${task.status}`,
|
|
1190
|
+
`- owner: ${task.owner}`,
|
|
1191
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
1192
|
+
`- roadmapRefs: ${task.roadmapRefs.join(', ') || '(none)'}`,
|
|
1193
|
+
`- researchBriefPath: ${researchBriefState.relativePath}`,
|
|
1194
|
+
`- researchBriefStatus: ${researchBriefState.ready ? 'READY' : 'MISSING'}`,
|
|
1195
|
+
`- architectureDocsStatus: ${projectContextDocsState.missingArchitectureDocs ? 'MISSING' : 'READY'}`,
|
|
1196
|
+
`- styleDocsStatus: ${projectContextDocsState.missingStyleDocs ? 'MISSING' : 'READY'}`,
|
|
1197
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
|
|
1198
|
+
];
|
|
1199
|
+
if (task.subState && task.status === 'IN_PROGRESS') {
|
|
1200
|
+
lines.push('- subState:');
|
|
1201
|
+
if (task.subState.phase)
|
|
1202
|
+
lines.push(` - phase: ${task.subState.phase}`);
|
|
1203
|
+
if (typeof task.subState.confidence === 'number')
|
|
1204
|
+
lines.push(` - confidence: ${task.subState.confidence}`);
|
|
1205
|
+
if (task.subState.estimatedCompletion)
|
|
1206
|
+
lines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
965
1207
|
}
|
|
966
|
-
if (task.blocker.
|
|
967
|
-
|
|
1208
|
+
if (task.blocker && task.status === 'BLOCKED') {
|
|
1209
|
+
lines.push('- blocker:');
|
|
1210
|
+
lines.push(` - type: ${task.blocker.type}`);
|
|
1211
|
+
lines.push(` - description: ${task.blocker.description}`);
|
|
1212
|
+
if (task.blocker.blockingEntity)
|
|
1213
|
+
lines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
1214
|
+
if (task.blocker.unblockCondition)
|
|
1215
|
+
lines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
|
|
1216
|
+
if (task.blocker.escalationPath)
|
|
1217
|
+
lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
968
1218
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
'### Reference Locations',
|
|
979
|
-
...(referenceLocations.length > 0
|
|
980
|
-
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
981
|
-
: ['- (none)']),
|
|
1219
|
+
return lines;
|
|
1220
|
+
},
|
|
1221
|
+
evidence: ({ task, researchBriefState, projectContextDocsState, relatedArtifacts, referenceLocations, suggestedReadOrder }) => [
|
|
1222
|
+
'### Pre-Execution Research Brief',
|
|
1223
|
+
`- path: ${researchBriefState.relativePath}`,
|
|
1224
|
+
`- absolutePath: ${researchBriefState.absolutePath}`,
|
|
1225
|
+
`- status: ${researchBriefState.ready ? 'READY' : 'MISSING'}`,
|
|
1226
|
+
...(!researchBriefState.ready
|
|
1227
|
+
? [
|
|
982
1228
|
'',
|
|
983
|
-
'###
|
|
984
|
-
...
|
|
1229
|
+
'### Required Research Brief Template',
|
|
1230
|
+
...renderTaskResearchBriefTemplate(task).map((line) => `- ${line}`),
|
|
1231
|
+
]
|
|
1232
|
+
: []),
|
|
1233
|
+
'',
|
|
1234
|
+
'### Project Context Docs Check',
|
|
1235
|
+
`- architecture docs: ${projectContextDocsState.architectureDocs.length > 0 ? 'found' : 'missing'}`,
|
|
1236
|
+
...(projectContextDocsState.architectureDocs.length > 0
|
|
1237
|
+
? projectContextDocsState.architectureDocs.map((item) => `- architecture: ${item}`)
|
|
1238
|
+
: [`- architecture: add required file ${CORE_ARCHITECTURE_DOC_FILE}.`]),
|
|
1239
|
+
`- design style docs: ${projectContextDocsState.styleDocs.length > 0 ? 'found' : 'missing'}`,
|
|
1240
|
+
...(projectContextDocsState.styleDocs.length > 0
|
|
1241
|
+
? projectContextDocsState.styleDocs.map((item) => `- style: ${item}`)
|
|
1242
|
+
: [`- style: add required file ${CORE_STYLE_DOC_FILE}.`]),
|
|
1243
|
+
'',
|
|
1244
|
+
'### Related Artifacts',
|
|
1245
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
|
|
1246
|
+
'',
|
|
1247
|
+
'### Reference Locations',
|
|
1248
|
+
...(referenceLocations.length > 0
|
|
1249
|
+
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
1250
|
+
: ['- (none)']),
|
|
1251
|
+
'',
|
|
1252
|
+
'### Suggested Read Order',
|
|
1253
|
+
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
1254
|
+
],
|
|
1255
|
+
guidance: ({ researchBriefState, projectContextDocsState, contextReadingGuidance, task }) => [
|
|
1256
|
+
...(!researchBriefState.ready
|
|
1257
|
+
? [
|
|
1258
|
+
'- Pre-execution gate is NOT satisfied. Complete research brief first, then proceed with implementation.',
|
|
1259
|
+
`- Create or update ${researchBriefState.relativePath} with design guidelines + code architecture findings before code changes.`,
|
|
1260
|
+
'- Include exact file/line locations in the brief (for example path/to/file.ts#L120).',
|
|
1261
|
+
'- Re-run taskContext after writing the brief and confirm researchBriefStatus becomes READY.',
|
|
1262
|
+
]
|
|
1263
|
+
: [
|
|
1264
|
+
'- Pre-execution gate satisfied. Read the research brief first, then continue implementation.',
|
|
1265
|
+
`- Must read ${researchBriefState.relativePath} before any task execution changes.`,
|
|
985
1266
|
]),
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
'',
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
'-
|
|
1267
|
+
...(!projectContextDocsState.ready
|
|
1268
|
+
? [
|
|
1269
|
+
'- Project context docs gate is NOT satisfied. Complete missing project architecture/style docs first.',
|
|
1270
|
+
...(projectContextDocsState.missingArchitectureDocs
|
|
1271
|
+
? [`- Missing architecture design doc. Add required file ${CORE_ARCHITECTURE_DOC_FILE} and include architecture boundaries and module responsibilities.`]
|
|
1272
|
+
: []),
|
|
1273
|
+
...(projectContextDocsState.missingStyleDocs
|
|
1274
|
+
? [`- Missing design style doc. Add required file ${CORE_STYLE_DOC_FILE} and include style language, tokens/themes, and UI consistency rules.`]
|
|
1275
|
+
: []),
|
|
1276
|
+
'- Re-run taskContext and confirm both architectureDocsStatus/styleDocsStatus are READY.',
|
|
1277
|
+
]
|
|
1278
|
+
: [
|
|
1279
|
+
'- Project context docs gate satisfied. Architecture/style docs are available for execution alignment.',
|
|
996
1280
|
]),
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1281
|
+
'- Read the files in Suggested Read Order.',
|
|
1282
|
+
'',
|
|
1283
|
+
'### Context Reading',
|
|
1284
|
+
...contextReadingGuidance,
|
|
1285
|
+
'',
|
|
1286
|
+
'- Verify whether current status and evidence are consistent.',
|
|
1287
|
+
...taskStatusGuidance(task),
|
|
1288
|
+
'- If updates are needed, use tool writes for governance store (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.',
|
|
1289
|
+
'- After editing, re-run `taskContext` to verify references and context consistency.',
|
|
1290
|
+
],
|
|
1291
|
+
suggestions: ({ task, researchBriefState, projectContextDocsState }) => [
|
|
1292
|
+
...collectSingleTaskLintSuggestions(task),
|
|
1293
|
+
...renderLintSuggestions(collectTaskResearchBriefLintSuggestions(researchBriefState)),
|
|
1294
|
+
...renderLintSuggestions(collectProjectContextDocsLintSuggestions(projectContextDocsState)),
|
|
1295
|
+
],
|
|
1296
|
+
nextCall: ({ normalizedProjectPath, task }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${task.id}")`,
|
|
1297
|
+
}));
|
|
1003
1298
|
// taskUpdate tool - Update task fields including subState and blocker (Spec v1.1.0)
|
|
1004
|
-
server.registerTool(
|
|
1299
|
+
server.registerTool(...createGovernedTool({
|
|
1300
|
+
name: 'taskUpdate',
|
|
1005
1301
|
title: 'Task Update',
|
|
1006
1302
|
description: 'Update task fields including status, owner, summary, subState, and blocker metadata',
|
|
1007
1303
|
inputSchema: {
|
|
@@ -1027,140 +1323,108 @@ export function registerTaskTools(server) {
|
|
|
1027
1323
|
}).optional(),
|
|
1028
1324
|
}),
|
|
1029
1325
|
},
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
...asText(renderErrorMarkdown('taskUpdate', `Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'retry with a valid task ID'], `taskUpdate(projectPath="${projectPath}", taskId="TASK-0001", updates={...})`)),
|
|
1034
|
-
isError: true,
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1038
|
-
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
1039
|
-
const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
|
|
1040
|
-
const tasksViewPath = path.join(governanceDir, TASKS_MARKDOWN_FILE);
|
|
1041
|
-
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
1042
|
-
const taskIndex = tasks.findIndex((item) => item.id === taskId);
|
|
1043
|
-
if (taskIndex === -1) {
|
|
1044
|
-
return {
|
|
1045
|
-
...asText(renderErrorMarkdown('taskUpdate', `Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`)),
|
|
1046
|
-
isError: true,
|
|
1047
|
-
};
|
|
1048
|
-
}
|
|
1049
|
-
const task = tasks[taskIndex];
|
|
1050
|
-
const originalStatus = task.status;
|
|
1051
|
-
// Validate status transition
|
|
1052
|
-
if (updates.status && !validateTransition(originalStatus, updates.status)) {
|
|
1053
|
-
return {
|
|
1054
|
-
...asText(renderErrorMarkdown('taskUpdate', `Invalid status transition: ${originalStatus} -> ${updates.status}`, ['use `validateTransition` to check allowed transitions', 'provide evidence when transitioning to DONE'], `taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${taskId}")`)),
|
|
1055
|
-
isError: true,
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
|
-
// Apply updates
|
|
1059
|
-
if (updates.status)
|
|
1060
|
-
task.status = updates.status;
|
|
1061
|
-
if (updates.owner !== undefined)
|
|
1062
|
-
task.owner = updates.owner;
|
|
1063
|
-
if (updates.summary !== undefined)
|
|
1064
|
-
task.summary = updates.summary;
|
|
1065
|
-
if (updates.roadmapRefs)
|
|
1066
|
-
task.roadmapRefs = updates.roadmapRefs;
|
|
1067
|
-
if (updates.links)
|
|
1068
|
-
task.links = updates.links;
|
|
1069
|
-
// Handle subState (Spec v1.1.0)
|
|
1070
|
-
if (updates.subState !== undefined) {
|
|
1071
|
-
if (updates.subState === null) {
|
|
1072
|
-
delete task.subState;
|
|
1326
|
+
async execute({ projectPath, taskId, updates }) {
|
|
1327
|
+
if (!isValidTaskId(taskId)) {
|
|
1328
|
+
throw new ToolExecutionError(`Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'retry with a valid task ID'], `taskUpdate(projectPath="${projectPath}", taskId="TASK-0001", updates={...})`);
|
|
1073
1329
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1330
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1331
|
+
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
1332
|
+
const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
|
|
1333
|
+
const tasksViewPath = path.join(governanceDir, TASKS_MARKDOWN_FILE);
|
|
1334
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
1335
|
+
const taskIndex = tasks.findIndex((item) => item.id === taskId);
|
|
1336
|
+
if (taskIndex === -1) {
|
|
1337
|
+
throw new ToolExecutionError(`Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`);
|
|
1079
1338
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
if (updates.
|
|
1084
|
-
|
|
1339
|
+
const task = tasks[taskIndex];
|
|
1340
|
+
const originalStatus = task.status;
|
|
1341
|
+
const previewTask = normalizeTask({ ...task, ...updates, updatedAt: nowIso() });
|
|
1342
|
+
if (updates.status && !validateTransition(originalStatus, updates.status)) {
|
|
1343
|
+
throw new ToolExecutionError(`Invalid status transition: ${originalStatus} -> ${updates.status}`, ['use `validateTransition` to check allowed transitions', 'provide evidence when transitioning to DONE'], `taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${taskId}")`);
|
|
1085
1344
|
}
|
|
1086
|
-
|
|
1087
|
-
task.
|
|
1345
|
+
const updatedSubState = updates.subState === null ? undefined
|
|
1346
|
+
: updates.subState !== undefined ? { ...(task.subState ?? {}), ...updates.subState }
|
|
1347
|
+
: task.subState;
|
|
1348
|
+
const updatedBlocker = updates.blocker === null ? undefined
|
|
1349
|
+
: updates.blocker !== undefined ? updates.blocker
|
|
1350
|
+
: task.blocker;
|
|
1351
|
+
const normalizedTask = normalizeTask({
|
|
1352
|
+
...task,
|
|
1353
|
+
...(updates.status ? { status: updates.status } : {}),
|
|
1354
|
+
...(updates.owner !== undefined ? { owner: updates.owner } : {}),
|
|
1355
|
+
...(updates.summary !== undefined ? { summary: updates.summary } : {}),
|
|
1356
|
+
...(updates.roadmapRefs ? { roadmapRefs: updates.roadmapRefs } : {}),
|
|
1357
|
+
...(updates.links ? { links: updates.links } : {}),
|
|
1358
|
+
subState: updatedSubState,
|
|
1359
|
+
blocker: updatedBlocker,
|
|
1360
|
+
updatedAt: nowIso(),
|
|
1361
|
+
});
|
|
1362
|
+
await upsertTaskInStore(tasksPath, normalizedTask);
|
|
1363
|
+
await loadTasksDocumentWithOptions(governanceDir, true);
|
|
1364
|
+
return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, taskId, originalStatus, task: normalizedTask, previewTask, updates };
|
|
1365
|
+
},
|
|
1366
|
+
summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, taskId, originalStatus, task }) => {
|
|
1367
|
+
const lines = [
|
|
1368
|
+
`- projectPath: ${normalizedProjectPath}`,
|
|
1369
|
+
`- governanceDir: ${governanceDir}`,
|
|
1370
|
+
`- tasksView: ${tasksViewPath}`,
|
|
1371
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
1372
|
+
`- taskId: ${taskId}`,
|
|
1373
|
+
`- originalStatus: ${originalStatus}`,
|
|
1374
|
+
`- newStatus: ${task.status}`,
|
|
1375
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
1376
|
+
];
|
|
1377
|
+
if (task.subState) {
|
|
1378
|
+
lines.push('- subState:');
|
|
1379
|
+
if (task.subState.phase)
|
|
1380
|
+
lines.push(` - phase: ${task.subState.phase}`);
|
|
1381
|
+
if (typeof task.subState.confidence === 'number')
|
|
1382
|
+
lines.push(` - confidence: ${task.subState.confidence}`);
|
|
1383
|
+
if (task.subState.estimatedCompletion)
|
|
1384
|
+
lines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
1088
1385
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
task
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
`-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
`-
|
|
1110
|
-
`-
|
|
1111
|
-
`-
|
|
1112
|
-
`-
|
|
1113
|
-
`-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
updateSummary.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
1134
|
-
}
|
|
1135
|
-
const markdown = renderToolResponseMarkdown({
|
|
1136
|
-
toolName: 'taskUpdate',
|
|
1137
|
-
sections: [
|
|
1138
|
-
summarySection(updateSummary),
|
|
1139
|
-
evidenceSection([
|
|
1140
|
-
'### Updated Task',
|
|
1141
|
-
`- ${task.id} | ${task.status} | ${task.title}`,
|
|
1142
|
-
`- owner: ${task.owner || '(none)'}`,
|
|
1143
|
-
`- summary: ${task.summary || '(none)'}`,
|
|
1144
|
-
'',
|
|
1145
|
-
'### Update Details',
|
|
1146
|
-
...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
|
|
1147
|
-
...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
|
|
1148
|
-
...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
|
|
1149
|
-
...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(', ')}`] : []),
|
|
1150
|
-
...(updates.links ? [`- links: ${updates.links.join(', ')}`] : []),
|
|
1151
|
-
...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
|
|
1152
|
-
...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
|
|
1153
|
-
]),
|
|
1154
|
-
guidanceSection([
|
|
1155
|
-
'Task updated successfully and tasks.md has been synced. Run `taskContext` to verify the changes.',
|
|
1156
|
-
'If status changed to DONE, ensure evidence links are added.',
|
|
1157
|
-
'If subState or blocker were updated, verify the metadata is correct.',
|
|
1158
|
-
'.projitive governance store is source of truth; tasks.md is a generated view and may be overwritten.',
|
|
1159
|
-
]),
|
|
1160
|
-
lintSection([]),
|
|
1161
|
-
nextCallSection(`taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${taskId}")`),
|
|
1162
|
-
],
|
|
1163
|
-
});
|
|
1164
|
-
return asText(markdown);
|
|
1165
|
-
});
|
|
1386
|
+
if (task.blocker) {
|
|
1387
|
+
lines.push('- blocker:');
|
|
1388
|
+
lines.push(` - type: ${task.blocker.type}`);
|
|
1389
|
+
lines.push(` - description: ${task.blocker.description}`);
|
|
1390
|
+
if (task.blocker.blockingEntity)
|
|
1391
|
+
lines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
1392
|
+
if (task.blocker.unblockCondition)
|
|
1393
|
+
lines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
|
|
1394
|
+
if (task.blocker.escalationPath)
|
|
1395
|
+
lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
1396
|
+
}
|
|
1397
|
+
return lines;
|
|
1398
|
+
},
|
|
1399
|
+
evidence: ({ task, originalStatus, updates }) => [
|
|
1400
|
+
'### Updated Task',
|
|
1401
|
+
`- ${task.id} | ${task.status} | ${task.title}`,
|
|
1402
|
+
`- owner: ${task.owner || '(none)'}`,
|
|
1403
|
+
`- summary: ${task.summary || '(none)'}`,
|
|
1404
|
+
'',
|
|
1405
|
+
'### Update Details',
|
|
1406
|
+
...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
|
|
1407
|
+
...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
|
|
1408
|
+
...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
|
|
1409
|
+
...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(', ')}`] : []),
|
|
1410
|
+
...(updates.links ? [`- links: ${updates.links.join(', ')}`] : []),
|
|
1411
|
+
...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
|
|
1412
|
+
...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
|
|
1413
|
+
],
|
|
1414
|
+
guidance: ({ updates, originalStatus }) => [
|
|
1415
|
+
'Task updated successfully and tasks.md has been synced. Run `taskContext` to verify the changes.',
|
|
1416
|
+
...(updates.status === 'IN_PROGRESS' && originalStatus === 'TODO'
|
|
1417
|
+
? ['- Ensure pre-execution research brief exists before deep implementation.']
|
|
1418
|
+
: []),
|
|
1419
|
+
...(updates.status === 'DONE'
|
|
1420
|
+
? ['- Verify evidence links are attached and reflect completed work.']
|
|
1421
|
+
: []),
|
|
1422
|
+
'.projitive governance store is source of truth; tasks.md is a generated view and may be overwritten.',
|
|
1423
|
+
],
|
|
1424
|
+
suggestions: async ({ previewTask, governanceDir }) => [
|
|
1425
|
+
...collectSingleTaskLintSuggestions(previewTask),
|
|
1426
|
+
...renderLintSuggestions(await collectDoneConformanceSuggestions(governanceDir, previewTask)),
|
|
1427
|
+
],
|
|
1428
|
+
nextCall: ({ normalizedProjectPath, taskId }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${taskId}")`,
|
|
1429
|
+
}));
|
|
1166
1430
|
}
|