@projitive/mcp 2.0.3 → 2.1.0
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 +8 -2
- package/output/source/common/artifacts.js +1 -1
- package/output/source/common/artifacts.test.js +11 -11
- package/output/source/common/errors.js +19 -19
- package/output/source/common/errors.test.js +59 -0
- package/output/source/common/files.js +30 -19
- package/output/source/common/files.test.js +14 -14
- package/output/source/common/index.js +11 -10
- package/output/source/common/linter.js +29 -27
- package/output/source/common/linter.test.js +9 -9
- package/output/source/common/markdown.js +3 -3
- package/output/source/common/markdown.test.js +15 -15
- package/output/source/common/response.js +91 -107
- package/output/source/common/response.test.js +30 -30
- package/output/source/common/store.js +40 -40
- package/output/source/common/store.test.js +72 -72
- package/output/source/common/tool.js +43 -0
- package/output/source/common/types.js +3 -3
- package/output/source/common/utils.js +8 -8
- package/output/source/common/utils.test.js +48 -0
- package/output/source/index.js +16 -16
- package/output/source/index.runtime.test.js +57 -0
- package/output/source/index.test.js +64 -64
- package/output/source/prompts/index.js +3 -3
- package/output/source/prompts/index.test.js +23 -0
- package/output/source/prompts/quickStart.js +96 -96
- package/output/source/prompts/quickStart.test.js +24 -0
- package/output/source/prompts/taskDiscovery.js +184 -184
- package/output/source/prompts/taskDiscovery.test.js +24 -0
- package/output/source/prompts/taskExecution.js +164 -148
- package/output/source/prompts/taskExecution.test.js +27 -0
- package/output/source/resources/designs.js +26 -26
- package/output/source/resources/designs.resources.test.js +52 -0
- package/output/source/resources/designs.test.js +88 -88
- package/output/source/resources/governance.js +19 -19
- package/output/source/resources/governance.test.js +35 -0
- package/output/source/resources/index.js +2 -2
- package/output/source/resources/index.test.js +18 -0
- package/output/source/resources/readme.js +7 -7
- package/output/source/resources/readme.test.js +113 -113
- package/output/source/resources/reports.js +10 -10
- package/output/source/resources/reports.test.js +83 -83
- package/output/source/tools/index.js +3 -3
- package/output/source/tools/index.test.js +23 -0
- package/output/source/tools/project.js +330 -377
- package/output/source/tools/project.test.js +308 -175
- package/output/source/tools/roadmap.js +236 -255
- package/output/source/tools/roadmap.test.js +241 -46
- package/output/source/tools/task.js +770 -652
- package/output/source/tools/task.test.js +433 -105
- package/output/source/types.js +28 -22
- package/package.json +8 -2
|
@@ -1,61 +1,62 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import { z } from
|
|
4
|
-
import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, findTextReferences, ensureStore, loadActionableTasksFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, replaceTasksInStore, upsertTaskInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export const
|
|
11
|
-
export const
|
|
12
|
-
export const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
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';
|
|
5
|
+
import { resolveGovernanceDir, resolveScanDepth, resolveScanRoots, discoverProjectsAcrossRoots, toProjectPath } from './project.js';
|
|
6
|
+
import { isValidRoadmapId } from './roadmap.js';
|
|
7
|
+
import { SUB_STATE_PHASES, BLOCKER_TYPES } from '../types.js';
|
|
8
|
+
export const ALLOWED_STATUS = ['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE'];
|
|
9
|
+
export const TASK_ID_REGEX = /^TASK-(\d+)$/;
|
|
10
|
+
export const TASKS_MARKDOWN_FILE = 'tasks.md';
|
|
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
|
-
if (task.status ===
|
|
17
|
+
if (task.status === 'TODO') {
|
|
18
18
|
return [
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
'- This task is TODO: confirm scope and set execution plan before edits.',
|
|
20
|
+
'- Move to IN_PROGRESS only after owner and initial evidence are ready.',
|
|
21
21
|
];
|
|
22
22
|
}
|
|
23
|
-
if (task.status ===
|
|
23
|
+
if (task.status === 'IN_PROGRESS') {
|
|
24
24
|
return [
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
'- This task is IN_PROGRESS: prioritize finishing with report/design evidence updates.',
|
|
26
|
+
'- Verify references stay consistent before marking DONE.',
|
|
27
27
|
];
|
|
28
28
|
}
|
|
29
|
-
if (task.status ===
|
|
29
|
+
if (task.status === 'BLOCKED') {
|
|
30
30
|
return [
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
'- This task is BLOCKED: identify blocker and required unblock condition first.',
|
|
32
|
+
'- Reopen only after blocker evidence is documented.',
|
|
33
33
|
];
|
|
34
34
|
}
|
|
35
35
|
return [
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
'- This task is DONE: only reopen when new requirement changes scope.',
|
|
37
|
+
'- Keep report evidence immutable unless correction is required.',
|
|
38
38
|
];
|
|
39
39
|
}
|
|
40
40
|
const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
'- Recheck project state first: run projectContext and confirm there is truly no TODO/IN_PROGRESS task to execute.',
|
|
42
|
+
'- Create new tasks via `taskCreate(...)` (do not edit tasks.md directly).',
|
|
43
|
+
'- If all remaining tasks are BLOCKED, create one unblock task with explicit unblock condition and dependency owner.',
|
|
44
|
+
'- Start from active roadmap milestones and split into the smallest executable slices with a single done condition each.',
|
|
45
|
+
'- Prefer slices that unlock multiple downstream tasks before isolated refactors or low-impact cleanups.',
|
|
46
|
+
'- Create TODO tasks only when evidence is clear: each new task must produce at least one report/designs/readme artifact update.',
|
|
47
|
+
'- Skip duplicate scope: do not create tasks that overlap existing TODO/IN_PROGRESS/BLOCKED task intent.',
|
|
48
|
+
'- Use quality gates for discovery candidates: user value, delivery risk reduction, or measurable throughput improvement.',
|
|
49
|
+
'- Review and update project architecture docs under designs/core/ (architecture.md, style-guide.md) if they are missing or outdated.',
|
|
50
|
+
'- Keep each discovery round small (1-3 tasks), then rerun taskNext immediately for re-ranking and execution.',
|
|
50
51
|
];
|
|
51
52
|
const DEFAULT_TASK_CONTEXT_READING_GUIDANCE = [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
'- Read governance workspace overview first (README.md / projitive://governance/workspace).',
|
|
54
|
+
'- Read roadmap and active milestones (roadmap.md / projitive://governance/roadmap).',
|
|
55
|
+
'- Read task view and related task cards (tasks.md / projitive://governance/tasks).',
|
|
56
|
+
'- Read design specs and technical decisions under designs/ (architecture, API contracts, constraints).',
|
|
57
|
+
'- Read reports/ for latest execution evidence, regressions, and unresolved risks.',
|
|
58
|
+
'- Read process guides under templates/docs/project guidelines to align with local governance rules.',
|
|
59
|
+
'- If available, read docs/ architecture or migration guides before major structural changes.',
|
|
59
60
|
];
|
|
60
61
|
export async function resolveNoTaskDiscoveryGuidance(governanceDir) {
|
|
61
62
|
void governanceDir;
|
|
@@ -66,7 +67,7 @@ export async function resolveTaskContextReadingGuidance(governanceDir) {
|
|
|
66
67
|
return DEFAULT_TASK_CONTEXT_READING_GUIDANCE;
|
|
67
68
|
}
|
|
68
69
|
async function readRoadmapIds(governanceDir) {
|
|
69
|
-
const dbPath = path.join(governanceDir,
|
|
70
|
+
const dbPath = path.join(governanceDir, '.projitive');
|
|
70
71
|
try {
|
|
71
72
|
await ensureStore(dbPath);
|
|
72
73
|
const milestones = await loadRoadmapsFromStore(dbPath);
|
|
@@ -79,16 +80,16 @@ async function readRoadmapIds(governanceDir) {
|
|
|
79
80
|
}
|
|
80
81
|
export function renderTaskSeedTemplate(roadmapRef) {
|
|
81
82
|
return [
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
'```markdown',
|
|
84
|
+
'## TASK-0001 | TODO | Define initial executable objective',
|
|
85
|
+
'- owner: ai-copilot',
|
|
86
|
+
'- summary: Convert one roadmap milestone or report gap into an actionable task.',
|
|
87
|
+
'- updatedAt: 2026-01-01T00:00:00.000Z',
|
|
87
88
|
`- roadmapRefs: ${roadmapRef}`,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
'- links:',
|
|
90
|
+
' - README.md',
|
|
91
|
+
' - .projitive/roadmap.md',
|
|
92
|
+
'```',
|
|
92
93
|
];
|
|
93
94
|
}
|
|
94
95
|
function isHttpUrl(value) {
|
|
@@ -96,9 +97,9 @@ function isHttpUrl(value) {
|
|
|
96
97
|
}
|
|
97
98
|
function isProjectRootRelativePath(value) {
|
|
98
99
|
return value.length > 0
|
|
99
|
-
&& !value.startsWith(
|
|
100
|
-
&& !value.startsWith(
|
|
101
|
-
&& !value.startsWith(
|
|
100
|
+
&& !value.startsWith('/')
|
|
101
|
+
&& !value.startsWith('./')
|
|
102
|
+
&& !value.startsWith('../')
|
|
102
103
|
&& !/^[A-Za-z]:\//.test(value);
|
|
103
104
|
}
|
|
104
105
|
function normalizeTaskLink(link) {
|
|
@@ -106,16 +107,97 @@ function normalizeTaskLink(link) {
|
|
|
106
107
|
if (trimmed.length === 0 || isHttpUrl(trimmed)) {
|
|
107
108
|
return trimmed;
|
|
108
109
|
}
|
|
109
|
-
const slashNormalized = trimmed.replace(/\\/g,
|
|
110
|
-
const withoutDotPrefix = slashNormalized.replace(/^\.\//,
|
|
111
|
-
return withoutDotPrefix.replace(/^\/+/,
|
|
110
|
+
const slashNormalized = trimmed.replace(/\\/g, '/');
|
|
111
|
+
const withoutDotPrefix = slashNormalized.replace(/^\.\//, '');
|
|
112
|
+
return withoutDotPrefix.replace(/^\/+/, '');
|
|
112
113
|
}
|
|
113
114
|
function resolveTaskLinkPath(projectPath, link) {
|
|
114
115
|
return path.join(projectPath, link);
|
|
115
116
|
}
|
|
117
|
+
function taskResearchBriefRelativePath(taskId) {
|
|
118
|
+
return `${TASK_RESEARCH_DIR}/${taskId}${TASK_RESEARCH_FILE_SUFFIX}`;
|
|
119
|
+
}
|
|
120
|
+
function renderTaskResearchBriefTemplate(task) {
|
|
121
|
+
return [
|
|
122
|
+
`# ${task.id} Implementation Research Brief`,
|
|
123
|
+
'',
|
|
124
|
+
`Task: ${task.title}`,
|
|
125
|
+
`Summary: ${task.summary || '(fill this with a short objective summary)'}`,
|
|
126
|
+
'',
|
|
127
|
+
'## Design Guidelines and Specs',
|
|
128
|
+
'- [ ] List relevant design/governance/spec files with line location',
|
|
129
|
+
'- Example: designs/ARCHITECTURE.md#L42-L76 - API boundary and constraints',
|
|
130
|
+
'- Example: roadmap.md#L18 - milestone acceptance criteria',
|
|
131
|
+
'',
|
|
132
|
+
'## Code Architecture and Implementation Findings',
|
|
133
|
+
'- [ ] Document current architecture and extension points with line location',
|
|
134
|
+
'- Example: packages/mcp/source/tools/task.ts#L1020-L1130 - taskContext response assembly',
|
|
135
|
+
'- Example: packages/mcp/source/prompts/taskExecution.ts#L25-L130 - execution workflow prompt',
|
|
136
|
+
'',
|
|
137
|
+
'## Implementation Plan',
|
|
138
|
+
'- [ ] Proposed change list with impacted modules',
|
|
139
|
+
'- [ ] Validation and regression test plan',
|
|
140
|
+
'',
|
|
141
|
+
'## Risks and Open Questions',
|
|
142
|
+
'- [ ] Known risks, assumptions, and unresolved questions',
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
async function inspectTaskResearchBrief(governanceDir, task) {
|
|
146
|
+
const projectPath = toProjectPath(governanceDir);
|
|
147
|
+
const relativePath = taskResearchBriefRelativePath(task.id);
|
|
148
|
+
const absolutePath = resolveTaskLinkPath(projectPath, relativePath);
|
|
149
|
+
const exists = await fs.access(absolutePath).then(() => true).catch(() => false);
|
|
150
|
+
return { relativePath, absolutePath, exists, ready: exists };
|
|
151
|
+
}
|
|
152
|
+
function collectTaskResearchBriefLintSuggestions(state) {
|
|
153
|
+
if (!state.exists) {
|
|
154
|
+
return [{
|
|
155
|
+
code: TASK_LINT_CODES.RESEARCH_BRIEF_MISSING,
|
|
156
|
+
message: `Pre-execution research brief missing: ${state.relativePath}.`,
|
|
157
|
+
fixHint: 'Create the file and fill required sections before implementation.',
|
|
158
|
+
}];
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
function inspectProjectContextDocsFromArtifacts(files) {
|
|
163
|
+
const markdownFiles = files
|
|
164
|
+
.map((item) => item.replace(/\\/g, '/'))
|
|
165
|
+
.filter((item) => item.toLowerCase().endsWith('.md'));
|
|
166
|
+
const architectureDocSuffix = `/${CORE_ARCHITECTURE_DOC_FILE}`.toLowerCase();
|
|
167
|
+
const styleDocSuffix = `/${CORE_STYLE_DOC_FILE}`.toLowerCase();
|
|
168
|
+
const architectureDocs = markdownFiles.filter((item) => item.toLowerCase().endsWith(architectureDocSuffix));
|
|
169
|
+
const styleDocs = markdownFiles.filter((item) => item.toLowerCase().endsWith(styleDocSuffix));
|
|
170
|
+
const missingArchitectureDocs = architectureDocs.length === 0;
|
|
171
|
+
const missingStyleDocs = styleDocs.length === 0;
|
|
172
|
+
return {
|
|
173
|
+
architectureDocs,
|
|
174
|
+
styleDocs,
|
|
175
|
+
missingArchitectureDocs,
|
|
176
|
+
missingStyleDocs,
|
|
177
|
+
ready: !missingArchitectureDocs && !missingStyleDocs,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function collectProjectContextDocsLintSuggestions(state) {
|
|
181
|
+
const suggestions = [];
|
|
182
|
+
if (state.missingArchitectureDocs) {
|
|
183
|
+
suggestions.push({
|
|
184
|
+
code: PROJECT_LINT_CODES.ARCHITECTURE_DOC_MISSING,
|
|
185
|
+
message: 'Project context is missing architecture design documentation.',
|
|
186
|
+
fixHint: `Add required file: ${CORE_ARCHITECTURE_DOC_FILE}.`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (state.missingStyleDocs) {
|
|
190
|
+
suggestions.push({
|
|
191
|
+
code: PROJECT_LINT_CODES.STYLE_DOC_MISSING,
|
|
192
|
+
message: 'Project context is missing design style documentation.',
|
|
193
|
+
fixHint: `Add required file: ${CORE_STYLE_DOC_FILE}.`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return suggestions;
|
|
197
|
+
}
|
|
116
198
|
async function readActionableTaskCandidates(governanceDirs) {
|
|
117
199
|
const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
|
|
118
|
-
const tasksPath = path.join(governanceDir,
|
|
200
|
+
const tasksPath = path.join(governanceDir, '.projitive');
|
|
119
201
|
await ensureStore(tasksPath);
|
|
120
202
|
const [stats, actionableTasks] = await Promise.all([
|
|
121
203
|
loadTaskStatusStatsFromStore(tasksPath),
|
|
@@ -125,11 +207,11 @@ async function readActionableTaskCandidates(governanceDirs) {
|
|
|
125
207
|
governanceDir,
|
|
126
208
|
tasks: actionableTasks,
|
|
127
209
|
projectScore: stats.inProgress * 2 + stats.todo,
|
|
128
|
-
projectLatestUpdatedAt: stats.latestUpdatedAt ||
|
|
210
|
+
projectLatestUpdatedAt: stats.latestUpdatedAt || '(unknown)',
|
|
129
211
|
};
|
|
130
212
|
}));
|
|
131
213
|
return snapshots.flatMap((item) => item.tasks
|
|
132
|
-
.filter((task) => task.status ===
|
|
214
|
+
.filter((task) => task.status === 'IN_PROGRESS' || task.status === 'TODO')
|
|
133
215
|
.map((task) => ({
|
|
134
216
|
governanceDir: item.governanceDir,
|
|
135
217
|
task,
|
|
@@ -143,13 +225,13 @@ export function nowIso() {
|
|
|
143
225
|
return new Date().toISOString();
|
|
144
226
|
}
|
|
145
227
|
export function isValidTaskId(id) {
|
|
146
|
-
return
|
|
228
|
+
return toTaskIdNumericSuffix(id) > 0;
|
|
147
229
|
}
|
|
148
230
|
export function taskPriority(status) {
|
|
149
|
-
if (status ===
|
|
231
|
+
if (status === 'IN_PROGRESS') {
|
|
150
232
|
return 2;
|
|
151
233
|
}
|
|
152
|
-
if (status ===
|
|
234
|
+
if (status === 'TODO') {
|
|
153
235
|
return 1;
|
|
154
236
|
}
|
|
155
237
|
return 0;
|
|
@@ -159,11 +241,21 @@ export function toTaskUpdatedAtMs(updatedAt) {
|
|
|
159
241
|
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
160
242
|
}
|
|
161
243
|
function toTaskIdNumericSuffix(taskId) {
|
|
162
|
-
const match = taskId.match(
|
|
244
|
+
const match = taskId.match(TASK_ID_REGEX);
|
|
163
245
|
if (!match) {
|
|
164
246
|
return -1;
|
|
165
247
|
}
|
|
166
|
-
|
|
248
|
+
const suffix = Number.parseInt(match[1], 10);
|
|
249
|
+
return Number.isFinite(suffix) ? suffix : -1;
|
|
250
|
+
}
|
|
251
|
+
function nextTaskId(tasks) {
|
|
252
|
+
const maxSuffix = tasks
|
|
253
|
+
.map((item) => toTaskIdNumericSuffix(item.id))
|
|
254
|
+
.filter((value) => value > 0)
|
|
255
|
+
.reduce((max, value) => Math.max(max, value), 0);
|
|
256
|
+
const next = maxSuffix + 1;
|
|
257
|
+
const minWidth = Math.max(4, String(next).length);
|
|
258
|
+
return `TASK-${String(next).padStart(minWidth, '0')}`;
|
|
167
259
|
}
|
|
168
260
|
export function sortTasksNewestFirst(tasks) {
|
|
169
261
|
return [...tasks].sort((a, b) => {
|
|
@@ -183,13 +275,13 @@ function normalizeAndSortTasks(tasks) {
|
|
|
183
275
|
}
|
|
184
276
|
function resolveTaskArtifactPaths(governanceDir) {
|
|
185
277
|
return {
|
|
186
|
-
tasksPath: path.join(governanceDir,
|
|
278
|
+
tasksPath: path.join(governanceDir, '.projitive'),
|
|
187
279
|
markdownPath: path.join(governanceDir, TASKS_MARKDOWN_FILE),
|
|
188
280
|
};
|
|
189
281
|
}
|
|
190
282
|
async function syncTasksMarkdownView(tasksPath, markdownPath, markdown, force = false) {
|
|
191
|
-
const sourceVersion = await getStoreVersion(tasksPath,
|
|
192
|
-
const viewState = await getMarkdownViewState(tasksPath,
|
|
283
|
+
const sourceVersion = await getStoreVersion(tasksPath, 'tasks');
|
|
284
|
+
const viewState = await getMarkdownViewState(tasksPath, 'tasks_markdown');
|
|
193
285
|
const markdownExists = await fs.access(markdownPath).then(() => true).catch(() => false);
|
|
194
286
|
const shouldWrite = force
|
|
195
287
|
|| !markdownExists
|
|
@@ -198,8 +290,8 @@ async function syncTasksMarkdownView(tasksPath, markdownPath, markdown, force =
|
|
|
198
290
|
if (!shouldWrite) {
|
|
199
291
|
return;
|
|
200
292
|
}
|
|
201
|
-
await fs.writeFile(markdownPath, markdown,
|
|
202
|
-
await markMarkdownViewBuilt(tasksPath,
|
|
293
|
+
await fs.writeFile(markdownPath, markdown, 'utf-8');
|
|
294
|
+
await markMarkdownViewBuilt(tasksPath, 'tasks_markdown', sourceVersion);
|
|
203
295
|
}
|
|
204
296
|
export function rankActionableTaskCandidates(candidates) {
|
|
205
297
|
return [...candidates].sort((a, b) => {
|
|
@@ -225,9 +317,9 @@ export function normalizeTask(task) {
|
|
|
225
317
|
const normalized = {
|
|
226
318
|
id: String(task.id),
|
|
227
319
|
title: String(task.title),
|
|
228
|
-
status: ALLOWED_STATUS.includes(task.status) ? task.status :
|
|
229
|
-
owner: task.owner ? String(task.owner) :
|
|
230
|
-
summary: task.summary ? String(task.summary) :
|
|
320
|
+
status: ALLOWED_STATUS.includes(task.status) ? task.status : 'TODO',
|
|
321
|
+
owner: task.owner ? String(task.owner) : '',
|
|
322
|
+
summary: task.summary ? String(task.summary) : '',
|
|
231
323
|
updatedAt: task.updatedAt ? String(task.updatedAt) : nowIso(),
|
|
232
324
|
links: Array.isArray(task.links)
|
|
233
325
|
? Array.from(new Set(task.links
|
|
@@ -258,32 +350,32 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
258
350
|
if (duplicateIds.length > 0) {
|
|
259
351
|
suggestions.push({
|
|
260
352
|
code: TASK_LINT_CODES.DUPLICATE_ID,
|
|
261
|
-
message: `Duplicate task IDs detected: ${duplicateIds.join(
|
|
262
|
-
fixHint:
|
|
353
|
+
message: `Duplicate task IDs detected: ${duplicateIds.join(', ')}.`,
|
|
354
|
+
fixHint: 'Keep task IDs unique in marker block.',
|
|
263
355
|
});
|
|
264
356
|
}
|
|
265
|
-
const inProgressWithoutOwner = tasks.filter((task) => task.status ===
|
|
357
|
+
const inProgressWithoutOwner = tasks.filter((task) => task.status === 'IN_PROGRESS' && task.owner.trim().length === 0);
|
|
266
358
|
if (inProgressWithoutOwner.length > 0) {
|
|
267
359
|
suggestions.push({
|
|
268
360
|
code: TASK_LINT_CODES.IN_PROGRESS_OWNER_EMPTY,
|
|
269
361
|
message: `${inProgressWithoutOwner.length} IN_PROGRESS task(s) have empty owner.`,
|
|
270
|
-
fixHint:
|
|
362
|
+
fixHint: 'Set owner before continuing execution.',
|
|
271
363
|
});
|
|
272
364
|
}
|
|
273
|
-
const doneWithoutLinks = tasks.filter((task) => task.status ===
|
|
365
|
+
const doneWithoutLinks = tasks.filter((task) => task.status === 'DONE' && task.links.length === 0);
|
|
274
366
|
if (doneWithoutLinks.length > 0) {
|
|
275
367
|
suggestions.push({
|
|
276
368
|
code: TASK_LINT_CODES.DONE_LINKS_MISSING,
|
|
277
369
|
message: `${doneWithoutLinks.length} DONE task(s) have no links evidence.`,
|
|
278
|
-
fixHint:
|
|
370
|
+
fixHint: 'Add at least one evidence link before keeping DONE.',
|
|
279
371
|
});
|
|
280
372
|
}
|
|
281
|
-
const blockedWithoutReason = tasks.filter((task) => task.status ===
|
|
373
|
+
const blockedWithoutReason = tasks.filter((task) => task.status === 'BLOCKED' && task.summary.trim().length === 0);
|
|
282
374
|
if (blockedWithoutReason.length > 0) {
|
|
283
375
|
suggestions.push({
|
|
284
376
|
code: TASK_LINT_CODES.BLOCKED_SUMMARY_EMPTY,
|
|
285
377
|
message: `${blockedWithoutReason.length} BLOCKED task(s) have empty summary.`,
|
|
286
|
-
fixHint:
|
|
378
|
+
fixHint: 'Add blocker reason and unblock condition.',
|
|
287
379
|
});
|
|
288
380
|
}
|
|
289
381
|
const invalidUpdatedAt = tasks.filter((task) => !Number.isFinite(new Date(task.updatedAt).getTime()));
|
|
@@ -291,7 +383,7 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
291
383
|
suggestions.push({
|
|
292
384
|
code: TASK_LINT_CODES.UPDATED_AT_INVALID,
|
|
293
385
|
message: `${invalidUpdatedAt.length} task(s) have invalid updatedAt format.`,
|
|
294
|
-
fixHint:
|
|
386
|
+
fixHint: 'Use ISO8601 UTC timestamp.',
|
|
295
387
|
});
|
|
296
388
|
}
|
|
297
389
|
const missingRoadmapRefs = tasks.filter((task) => task.roadmapRefs.length === 0);
|
|
@@ -299,7 +391,7 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
299
391
|
suggestions.push({
|
|
300
392
|
code: TASK_LINT_CODES.ROADMAP_REFS_EMPTY,
|
|
301
393
|
message: `${missingRoadmapRefs.length} task(s) have empty roadmapRefs.`,
|
|
302
|
-
fixHint:
|
|
394
|
+
fixHint: 'Bind at least one ROADMAP-xxxx when applicable.',
|
|
303
395
|
});
|
|
304
396
|
}
|
|
305
397
|
const invalidLinkPathFormat = tasks.filter((task) => task.links.some((link) => {
|
|
@@ -310,18 +402,18 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
310
402
|
suggestions.push({
|
|
311
403
|
code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
|
|
312
404
|
message: `${invalidLinkPathFormat.length} task(s) contain invalid links path format.`,
|
|
313
|
-
fixHint:
|
|
405
|
+
fixHint: 'Use project-root-relative paths without leading slash (for example reports/task-0001.md) or http(s) URL.',
|
|
314
406
|
});
|
|
315
407
|
}
|
|
316
408
|
// ============================================================================
|
|
317
409
|
// Spec v1.1.0 - Blocker Categorization Validation
|
|
318
410
|
// ============================================================================
|
|
319
|
-
const blockedWithoutBlocker = tasks.filter((task) => task.status ===
|
|
411
|
+
const blockedWithoutBlocker = tasks.filter((task) => task.status === 'BLOCKED' && !task.blocker);
|
|
320
412
|
if (blockedWithoutBlocker.length > 0) {
|
|
321
413
|
suggestions.push({
|
|
322
414
|
code: TASK_LINT_CODES.BLOCKED_WITHOUT_BLOCKER,
|
|
323
415
|
message: `${blockedWithoutBlocker.length} BLOCKED task(s) have no blocker metadata.`,
|
|
324
|
-
fixHint:
|
|
416
|
+
fixHint: 'Add structured blocker metadata with type and description.',
|
|
325
417
|
});
|
|
326
418
|
}
|
|
327
419
|
const blockerTypeInvalid = tasks.filter((task) => task.blocker && !BLOCKER_TYPES.includes(task.blocker.type));
|
|
@@ -329,7 +421,7 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
329
421
|
suggestions.push({
|
|
330
422
|
code: TASK_LINT_CODES.BLOCKER_TYPE_INVALID,
|
|
331
423
|
message: `${blockerTypeInvalid.length} task(s) have invalid blocker type.`,
|
|
332
|
-
fixHint: `Use one of: ${BLOCKER_TYPES.join(
|
|
424
|
+
fixHint: `Use one of: ${BLOCKER_TYPES.join(', ')}.`,
|
|
333
425
|
});
|
|
334
426
|
}
|
|
335
427
|
const blockerDescriptionEmpty = tasks.filter((task) => task.blocker && !task.blocker.description?.trim());
|
|
@@ -337,18 +429,18 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
337
429
|
suggestions.push({
|
|
338
430
|
code: TASK_LINT_CODES.BLOCKER_DESCRIPTION_EMPTY,
|
|
339
431
|
message: `${blockerDescriptionEmpty.length} task(s) have empty blocker description.`,
|
|
340
|
-
fixHint:
|
|
432
|
+
fixHint: 'Provide a clear description of why the task is blocked.',
|
|
341
433
|
});
|
|
342
434
|
}
|
|
343
435
|
// ============================================================================
|
|
344
436
|
// Spec v1.1.0 - Sub-state Metadata Validation (Optional but Recommended)
|
|
345
437
|
// ============================================================================
|
|
346
|
-
const inProgressWithoutSubState = tasks.filter((task) => task.status ===
|
|
438
|
+
const inProgressWithoutSubState = tasks.filter((task) => task.status === 'IN_PROGRESS' && !task.subState);
|
|
347
439
|
if (inProgressWithoutSubState.length > 0) {
|
|
348
440
|
suggestions.push({
|
|
349
441
|
code: TASK_LINT_CODES.IN_PROGRESS_WITHOUT_SUBSTATE,
|
|
350
442
|
message: `${inProgressWithoutSubState.length} IN_PROGRESS task(s) have no subState metadata.`,
|
|
351
|
-
fixHint:
|
|
443
|
+
fixHint: 'Add optional subState metadata for better progress tracking.',
|
|
352
444
|
});
|
|
353
445
|
}
|
|
354
446
|
const subStatePhaseInvalid = tasks.filter((task) => task.subState?.phase && !SUB_STATE_PHASES.includes(task.subState.phase));
|
|
@@ -356,15 +448,15 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
356
448
|
suggestions.push({
|
|
357
449
|
code: TASK_LINT_CODES.SUBSTATE_PHASE_INVALID,
|
|
358
450
|
message: `${subStatePhaseInvalid.length} task(s) have invalid subState phase.`,
|
|
359
|
-
fixHint: `Use one of: ${SUB_STATE_PHASES.join(
|
|
451
|
+
fixHint: `Use one of: ${SUB_STATE_PHASES.join(', ')}.`,
|
|
360
452
|
});
|
|
361
453
|
}
|
|
362
|
-
const subStateConfidenceInvalid = tasks.filter((task) => typeof task.subState?.confidence ===
|
|
454
|
+
const subStateConfidenceInvalid = tasks.filter((task) => typeof task.subState?.confidence === 'number' && (task.subState.confidence < 0 || task.subState.confidence > 1));
|
|
363
455
|
if (subStateConfidenceInvalid.length > 0) {
|
|
364
456
|
suggestions.push({
|
|
365
457
|
code: TASK_LINT_CODES.SUBSTATE_CONFIDENCE_INVALID,
|
|
366
458
|
message: `${subStateConfidenceInvalid.length} task(s) have invalid confidence score.`,
|
|
367
|
-
fixHint:
|
|
459
|
+
fixHint: 'Confidence must be between 0.0 and 1.0.',
|
|
368
460
|
});
|
|
369
461
|
}
|
|
370
462
|
return suggestions;
|
|
@@ -372,20 +464,20 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
372
464
|
export function collectTaskLintSuggestions(tasks) {
|
|
373
465
|
return renderLintSuggestions(collectTaskLintSuggestionItems(tasks));
|
|
374
466
|
}
|
|
375
|
-
function
|
|
467
|
+
function collectSingleTaskLintSuggestionItems(task) {
|
|
376
468
|
const suggestions = [];
|
|
377
|
-
if (task.status ===
|
|
469
|
+
if (task.status === 'IN_PROGRESS' && task.owner.trim().length === 0) {
|
|
378
470
|
suggestions.push({
|
|
379
471
|
code: TASK_LINT_CODES.IN_PROGRESS_OWNER_EMPTY,
|
|
380
|
-
message:
|
|
381
|
-
fixHint:
|
|
472
|
+
message: 'Current task is IN_PROGRESS but owner is empty.',
|
|
473
|
+
fixHint: 'Set owner before continuing execution.',
|
|
382
474
|
});
|
|
383
475
|
}
|
|
384
|
-
if (task.status ===
|
|
476
|
+
if (task.status === 'DONE' && task.links.length === 0) {
|
|
385
477
|
suggestions.push({
|
|
386
478
|
code: TASK_LINT_CODES.DONE_LINKS_MISSING,
|
|
387
|
-
message:
|
|
388
|
-
fixHint:
|
|
479
|
+
message: 'Current task is DONE but has no links evidence.',
|
|
480
|
+
fixHint: 'Add at least one evidence link.',
|
|
389
481
|
});
|
|
390
482
|
}
|
|
391
483
|
const invalidLinkPathFormat = task.links.some((link) => {
|
|
@@ -395,132 +487,116 @@ function collectSingleTaskLintSuggestions(task) {
|
|
|
395
487
|
if (invalidLinkPathFormat) {
|
|
396
488
|
suggestions.push({
|
|
397
489
|
code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
|
|
398
|
-
message:
|
|
399
|
-
fixHint:
|
|
490
|
+
message: 'Current task has invalid links path format.',
|
|
491
|
+
fixHint: 'Use project-root-relative paths without leading slash (for example reports/task-0001.md) or http(s) URL.',
|
|
400
492
|
});
|
|
401
493
|
}
|
|
402
|
-
if (task.status ===
|
|
494
|
+
if (task.status === 'BLOCKED' && task.summary.trim().length === 0) {
|
|
403
495
|
suggestions.push({
|
|
404
496
|
code: TASK_LINT_CODES.BLOCKED_SUMMARY_EMPTY,
|
|
405
|
-
message:
|
|
406
|
-
fixHint:
|
|
497
|
+
message: 'Current task is BLOCKED but summary is empty.',
|
|
498
|
+
fixHint: 'Add blocker reason and unblock condition.',
|
|
407
499
|
});
|
|
408
500
|
}
|
|
409
501
|
if (!Number.isFinite(new Date(task.updatedAt).getTime())) {
|
|
410
502
|
suggestions.push({
|
|
411
503
|
code: TASK_LINT_CODES.UPDATED_AT_INVALID,
|
|
412
|
-
message:
|
|
413
|
-
fixHint:
|
|
504
|
+
message: 'Current task updatedAt is invalid.',
|
|
505
|
+
fixHint: 'Use ISO8601 UTC timestamp.',
|
|
414
506
|
});
|
|
415
507
|
}
|
|
416
508
|
if (task.roadmapRefs.length === 0) {
|
|
417
509
|
suggestions.push({
|
|
418
510
|
code: TASK_LINT_CODES.ROADMAP_REFS_EMPTY,
|
|
419
|
-
message:
|
|
420
|
-
fixHint:
|
|
511
|
+
message: 'Current task has empty roadmapRefs.',
|
|
512
|
+
fixHint: 'Bind ROADMAP-xxxx where applicable.',
|
|
421
513
|
});
|
|
422
514
|
}
|
|
423
515
|
// ============================================================================
|
|
424
516
|
// Spec v1.1.0 - Blocker Categorization Validation (Single Task)
|
|
425
517
|
// ============================================================================
|
|
426
|
-
if (task.status ===
|
|
518
|
+
if (task.status === 'BLOCKED' && !task.blocker) {
|
|
427
519
|
suggestions.push({
|
|
428
520
|
code: TASK_LINT_CODES.BLOCKED_WITHOUT_BLOCKER,
|
|
429
|
-
message:
|
|
430
|
-
fixHint:
|
|
521
|
+
message: 'Current task is BLOCKED but has no blocker metadata.',
|
|
522
|
+
fixHint: 'Add structured blocker metadata with type and description.',
|
|
431
523
|
});
|
|
432
524
|
}
|
|
433
525
|
if (task.blocker && !BLOCKER_TYPES.includes(task.blocker.type)) {
|
|
434
526
|
suggestions.push({
|
|
435
527
|
code: TASK_LINT_CODES.BLOCKER_TYPE_INVALID,
|
|
436
528
|
message: `Current task has invalid blocker type: ${task.blocker.type}.`,
|
|
437
|
-
fixHint: `Use one of: ${BLOCKER_TYPES.join(
|
|
529
|
+
fixHint: `Use one of: ${BLOCKER_TYPES.join(', ')}.`,
|
|
438
530
|
});
|
|
439
531
|
}
|
|
440
532
|
if (task.blocker && !task.blocker.description?.trim()) {
|
|
441
533
|
suggestions.push({
|
|
442
534
|
code: TASK_LINT_CODES.BLOCKER_DESCRIPTION_EMPTY,
|
|
443
|
-
message:
|
|
444
|
-
fixHint:
|
|
535
|
+
message: 'Current task has empty blocker description.',
|
|
536
|
+
fixHint: 'Provide a clear description of why the task is blocked.',
|
|
445
537
|
});
|
|
446
538
|
}
|
|
447
539
|
// ============================================================================
|
|
448
540
|
// Spec v1.1.0 - Sub-state Metadata Validation (Single Task, Optional)
|
|
449
541
|
// ============================================================================
|
|
450
|
-
if (task.status ===
|
|
542
|
+
if (task.status === 'IN_PROGRESS' && !task.subState) {
|
|
451
543
|
suggestions.push({
|
|
452
544
|
code: TASK_LINT_CODES.IN_PROGRESS_WITHOUT_SUBSTATE,
|
|
453
|
-
message:
|
|
454
|
-
fixHint:
|
|
545
|
+
message: 'Current task is IN_PROGRESS but has no subState metadata.',
|
|
546
|
+
fixHint: 'Add optional subState metadata for better progress tracking.',
|
|
455
547
|
});
|
|
456
548
|
}
|
|
457
549
|
if (task.subState?.phase && !SUB_STATE_PHASES.includes(task.subState.phase)) {
|
|
458
550
|
suggestions.push({
|
|
459
551
|
code: TASK_LINT_CODES.SUBSTATE_PHASE_INVALID,
|
|
460
552
|
message: `Current task has invalid subState phase: ${task.subState.phase}.`,
|
|
461
|
-
fixHint: `Use one of: ${SUB_STATE_PHASES.join(
|
|
553
|
+
fixHint: `Use one of: ${SUB_STATE_PHASES.join(', ')}.`,
|
|
462
554
|
});
|
|
463
555
|
}
|
|
464
|
-
if (typeof task.subState?.confidence ===
|
|
556
|
+
if (typeof task.subState?.confidence === 'number' && (task.subState.confidence < 0 || task.subState.confidence > 1)) {
|
|
465
557
|
suggestions.push({
|
|
466
558
|
code: TASK_LINT_CODES.SUBSTATE_CONFIDENCE_INVALID,
|
|
467
559
|
message: `Current task has invalid confidence score: ${task.subState.confidence}.`,
|
|
468
|
-
fixHint:
|
|
560
|
+
fixHint: 'Confidence must be between 0.0 and 1.0.',
|
|
469
561
|
});
|
|
470
562
|
}
|
|
471
|
-
return
|
|
563
|
+
return suggestions;
|
|
472
564
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
|
|
487
|
-
message: `Link path should be project-root-relative without leading slash: ${normalized}.`,
|
|
488
|
-
fixHint: "Use path/from/project/root format.",
|
|
489
|
-
});
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
const resolvedPath = resolveTaskLinkPath(projectPath, normalized);
|
|
493
|
-
const exists = await fs.access(resolvedPath).then(() => true).catch(() => false);
|
|
494
|
-
if (!exists) {
|
|
495
|
-
suggestions.push({
|
|
496
|
-
code: TASK_LINT_CODES.LINK_TARGET_MISSING,
|
|
497
|
-
message: `Link target not found: ${normalized} (resolved: ${resolvedPath}).`,
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return renderLintSuggestions(suggestions);
|
|
565
|
+
function collectSingleTaskLintSuggestions(task) {
|
|
566
|
+
return renderLintSuggestions(collectSingleTaskLintSuggestionItems(task));
|
|
567
|
+
}
|
|
568
|
+
async function collectDoneConformanceSuggestions(governanceDir, task) {
|
|
569
|
+
const researchBriefState = await inspectTaskResearchBrief(governanceDir, task);
|
|
570
|
+
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
571
|
+
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
572
|
+
const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
|
|
573
|
+
return [
|
|
574
|
+
...collectSingleTaskLintSuggestionItems(task),
|
|
575
|
+
...collectTaskResearchBriefLintSuggestions(researchBriefState),
|
|
576
|
+
...collectProjectContextDocsLintSuggestions(projectContextDocsState),
|
|
577
|
+
];
|
|
502
578
|
}
|
|
503
579
|
export function renderTasksMarkdown(tasks) {
|
|
504
580
|
const sections = sortTasksNewestFirst(tasks).map((task) => {
|
|
505
|
-
const roadmapRefs = task.roadmapRefs.length > 0 ? task.roadmapRefs.join(
|
|
581
|
+
const roadmapRefs = task.roadmapRefs.length > 0 ? task.roadmapRefs.join(', ') : '(none)';
|
|
506
582
|
const links = task.links.length > 0
|
|
507
|
-
? [
|
|
508
|
-
: [
|
|
583
|
+
? ['- links:', ...task.links.map((link) => ` - ${link}`)]
|
|
584
|
+
: ['- links:', ' - (none)'];
|
|
509
585
|
const lines = [
|
|
510
586
|
`## ${task.id} | ${task.status} | ${task.title}`,
|
|
511
|
-
`- owner: ${task.owner ||
|
|
512
|
-
`- summary: ${task.summary ||
|
|
587
|
+
`- owner: ${task.owner || '(none)'}`,
|
|
588
|
+
`- summary: ${task.summary || '(none)'}`,
|
|
513
589
|
`- updatedAt: ${task.updatedAt}`,
|
|
514
590
|
`- roadmapRefs: ${roadmapRefs}`,
|
|
515
591
|
...links,
|
|
516
592
|
];
|
|
517
593
|
// Add subState for IN_PROGRESS tasks (Spec v1.1.0)
|
|
518
|
-
if (task.subState && task.status ===
|
|
519
|
-
lines.push(
|
|
594
|
+
if (task.subState && task.status === 'IN_PROGRESS') {
|
|
595
|
+
lines.push('- subState:');
|
|
520
596
|
if (task.subState.phase) {
|
|
521
597
|
lines.push(` - phase: ${task.subState.phase}`);
|
|
522
598
|
}
|
|
523
|
-
if (typeof task.subState.confidence ===
|
|
599
|
+
if (typeof task.subState.confidence === 'number') {
|
|
524
600
|
lines.push(` - confidence: ${task.subState.confidence}`);
|
|
525
601
|
}
|
|
526
602
|
if (task.subState.estimatedCompletion) {
|
|
@@ -528,8 +604,8 @@ export function renderTasksMarkdown(tasks) {
|
|
|
528
604
|
}
|
|
529
605
|
}
|
|
530
606
|
// Add blocker for BLOCKED tasks (Spec v1.1.0)
|
|
531
|
-
if (task.blocker && task.status ===
|
|
532
|
-
lines.push(
|
|
607
|
+
if (task.blocker && task.status === 'BLOCKED') {
|
|
608
|
+
lines.push('- blocker:');
|
|
533
609
|
lines.push(` - type: ${task.blocker.type}`);
|
|
534
610
|
lines.push(` - description: ${task.blocker.description}`);
|
|
535
611
|
if (task.blocker.blockingEntity) {
|
|
@@ -542,16 +618,20 @@ export function renderTasksMarkdown(tasks) {
|
|
|
542
618
|
lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
543
619
|
}
|
|
544
620
|
}
|
|
545
|
-
return lines.join(
|
|
621
|
+
return lines.join('\n');
|
|
546
622
|
});
|
|
547
623
|
return [
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
624
|
+
'# Tasks',
|
|
625
|
+
'',
|
|
626
|
+
'Projitive is an AI-first project governance framework for tasks, roadmaps, reports, and designs.',
|
|
627
|
+
'Author: yinxulai',
|
|
628
|
+
'Repository: https://github.com/yinxulai/projitive',
|
|
629
|
+
'Do not edit this file manually. This file is automatically generated by Projitive.',
|
|
630
|
+
'This file is generated from .projitive governance store by Projitive MCP. Manual edits will be overwritten.',
|
|
631
|
+
'',
|
|
632
|
+
...(sections.length > 0 ? sections : ['(no tasks)']),
|
|
633
|
+
'',
|
|
634
|
+
].join('\n');
|
|
555
635
|
}
|
|
556
636
|
export async function ensureTasksFile(inputPath) {
|
|
557
637
|
const governanceDir = await resolveGovernanceDir(inputPath);
|
|
@@ -588,414 +668,480 @@ export function validateTransition(from, to) {
|
|
|
588
668
|
return true;
|
|
589
669
|
}
|
|
590
670
|
const allowed = {
|
|
591
|
-
TODO: new Set([
|
|
592
|
-
IN_PROGRESS: new Set([
|
|
593
|
-
BLOCKED: new Set([
|
|
671
|
+
TODO: new Set(['IN_PROGRESS', 'BLOCKED']),
|
|
672
|
+
IN_PROGRESS: new Set(['BLOCKED', 'DONE']),
|
|
673
|
+
BLOCKED: new Set(['IN_PROGRESS', 'TODO']),
|
|
594
674
|
DONE: new Set(),
|
|
595
675
|
};
|
|
596
676
|
return allowed[from].has(to);
|
|
597
677
|
}
|
|
598
678
|
export function registerTaskTools(server) {
|
|
599
|
-
server.registerTool(
|
|
600
|
-
|
|
601
|
-
|
|
679
|
+
server.registerTool(...createGovernedTool({
|
|
680
|
+
name: 'taskList',
|
|
681
|
+
title: 'Task List',
|
|
682
|
+
description: 'List tasks for a known project and optionally filter by status',
|
|
602
683
|
inputSchema: {
|
|
603
684
|
projectPath: z.string(),
|
|
604
|
-
status: z.enum([
|
|
685
|
+
status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
|
|
605
686
|
limit: z.number().int().min(1).max(200).optional(),
|
|
606
687
|
},
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
server.registerTool(
|
|
648
|
-
|
|
649
|
-
|
|
688
|
+
async execute({ projectPath, status, limit }) {
|
|
689
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
690
|
+
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
691
|
+
const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
|
|
692
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
693
|
+
const filtered = tasks
|
|
694
|
+
.filter((task) => (status ? task.status === status : true))
|
|
695
|
+
.slice(0, limit ?? 100);
|
|
696
|
+
return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, filtered, status };
|
|
697
|
+
},
|
|
698
|
+
summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, filtered, status }) => [
|
|
699
|
+
`- projectPath: ${normalizedProjectPath}`,
|
|
700
|
+
`- governanceDir: ${governanceDir}`,
|
|
701
|
+
`- tasksView: ${tasksViewPath}`,
|
|
702
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
703
|
+
`- filter.status: ${status ?? '(none)'}`,
|
|
704
|
+
`- returned: ${filtered.length}`,
|
|
705
|
+
],
|
|
706
|
+
evidence: ({ filtered }) => [
|
|
707
|
+
'- tasks:',
|
|
708
|
+
...filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner || ''} | updatedAt=${task.updatedAt}`),
|
|
709
|
+
],
|
|
710
|
+
guidance: () => ['- Pick one task ID and call `taskContext`.'],
|
|
711
|
+
suggestions: ({ filtered, status }) => {
|
|
712
|
+
const suggestions = collectTaskLintSuggestions(filtered);
|
|
713
|
+
if (status && filtered.length === 0) {
|
|
714
|
+
suggestions.push(...renderLintSuggestions([
|
|
715
|
+
{
|
|
716
|
+
code: TASK_LINT_CODES.FILTER_EMPTY,
|
|
717
|
+
message: `No tasks matched status=${status}.`,
|
|
718
|
+
fixHint: 'Confirm status values or update task states.',
|
|
719
|
+
},
|
|
720
|
+
]));
|
|
721
|
+
}
|
|
722
|
+
return suggestions;
|
|
723
|
+
},
|
|
724
|
+
nextCall: ({ filtered, normalizedProjectPath }) => filtered[0]
|
|
725
|
+
? `taskContext(projectPath="${normalizedProjectPath}", taskId="${filtered[0].id}")`
|
|
726
|
+
: undefined,
|
|
727
|
+
}));
|
|
728
|
+
server.registerTool(...createGovernedTool({
|
|
729
|
+
name: 'taskCreate',
|
|
730
|
+
title: 'Task Create',
|
|
731
|
+
description: 'Create a new task in governance store with stable TASK-<number> ID',
|
|
650
732
|
inputSchema: {
|
|
651
733
|
projectPath: z.string(),
|
|
652
|
-
taskId: z.string(),
|
|
734
|
+
taskId: z.string().optional(),
|
|
653
735
|
title: z.string(),
|
|
654
|
-
status: z.enum([
|
|
736
|
+
status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
|
|
655
737
|
owner: z.string().optional(),
|
|
656
738
|
summary: z.string().optional(),
|
|
657
739
|
roadmapRefs: z.array(z.string()).optional(),
|
|
658
740
|
links: z.array(z.string()).optional(),
|
|
659
741
|
subState: z.object({
|
|
660
|
-
phase: z.enum([
|
|
742
|
+
phase: z.enum(['discovery', 'design', 'implementation', 'testing']).optional(),
|
|
661
743
|
confidence: z.number().min(0).max(1).optional(),
|
|
662
744
|
estimatedCompletion: z.string().optional(),
|
|
663
745
|
}).optional(),
|
|
664
746
|
blocker: z.object({
|
|
665
|
-
type: z.enum([
|
|
747
|
+
type: z.enum(['internal_dependency', 'external_dependency', 'resource', 'approval']),
|
|
666
748
|
description: z.string(),
|
|
667
749
|
blockingEntity: z.string().optional(),
|
|
668
750
|
unblockCondition: z.string().optional(),
|
|
669
751
|
escalationPath: z.string().optional(),
|
|
670
752
|
}).optional(),
|
|
671
753
|
},
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
links,
|
|
697
|
-
subState,
|
|
698
|
-
blocker,
|
|
699
|
-
updatedAt: nowIso(),
|
|
700
|
-
});
|
|
701
|
-
await upsertTaskInStore(tasksPath, createdTask);
|
|
702
|
-
await loadTasksDocumentWithOptions(governanceDir, true);
|
|
703
|
-
const lintSuggestions = [
|
|
704
|
-
...collectSingleTaskLintSuggestions(createdTask),
|
|
705
|
-
...(await collectTaskFileLintSuggestions(governanceDir, createdTask)),
|
|
706
|
-
];
|
|
707
|
-
const markdown = renderToolResponseMarkdown({
|
|
708
|
-
toolName: "taskCreate",
|
|
709
|
-
sections: [
|
|
710
|
-
summarySection([
|
|
711
|
-
`- projectPath: ${normalizedProjectPath}`,
|
|
712
|
-
`- governanceDir: ${governanceDir}`,
|
|
713
|
-
`- taskId: ${createdTask.id}`,
|
|
714
|
-
`- status: ${createdTask.status}`,
|
|
715
|
-
`- owner: ${createdTask.owner || "(none)"}`,
|
|
716
|
-
`- updatedAt: ${createdTask.updatedAt}`,
|
|
717
|
-
]),
|
|
718
|
-
evidenceSection([
|
|
719
|
-
"### Created Task",
|
|
720
|
-
`- ${createdTask.id} | ${createdTask.status} | ${createdTask.title}`,
|
|
721
|
-
`- summary: ${createdTask.summary || "(none)"}`,
|
|
722
|
-
`- roadmapRefs: ${createdTask.roadmapRefs.join(", ") || "(none)"}`,
|
|
723
|
-
`- links: ${createdTask.links.join(", ") || "(none)"}`,
|
|
724
|
-
]),
|
|
725
|
-
guidanceSection([
|
|
726
|
-
"Task created in governance store successfully and tasks.md has been synced.",
|
|
727
|
-
"Run taskContext to verify references and lint guidance.",
|
|
728
|
-
]),
|
|
729
|
-
lintSection(lintSuggestions),
|
|
730
|
-
nextCallSection(`taskContext(projectPath=\"${normalizedProjectPath}\", taskId=\"${createdTask.id}\")`),
|
|
731
|
-
],
|
|
732
|
-
});
|
|
733
|
-
return asText(markdown);
|
|
734
|
-
});
|
|
735
|
-
server.registerTool("taskNext", {
|
|
736
|
-
title: "Task Next",
|
|
737
|
-
description: "Start here to auto-select the highest-priority actionable task",
|
|
738
|
-
inputSchema: {
|
|
739
|
-
limit: z.number().int().min(1).max(20).optional(),
|
|
740
|
-
},
|
|
741
|
-
}, async ({ limit }) => {
|
|
742
|
-
const roots = resolveScanRoots();
|
|
743
|
-
const depth = resolveScanDepth();
|
|
744
|
-
const projects = await discoverProjectsAcrossRoots(roots, depth);
|
|
745
|
-
const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
|
|
746
|
-
if (rankedCandidates.length === 0) {
|
|
747
|
-
const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
|
|
748
|
-
const tasksPath = path.join(governanceDir, ".projitive");
|
|
749
|
-
await ensureStore(tasksPath);
|
|
750
|
-
const stats = await loadTaskStatusStatsFromStore(tasksPath);
|
|
751
|
-
const roadmapIds = await readRoadmapIds(governanceDir);
|
|
752
|
-
return {
|
|
753
|
-
governanceDir,
|
|
754
|
-
roadmapIds,
|
|
755
|
-
total: stats.total,
|
|
756
|
-
todo: stats.todo,
|
|
757
|
-
inProgress: stats.inProgress,
|
|
758
|
-
blocked: stats.blocked,
|
|
759
|
-
done: stats.done,
|
|
760
|
-
};
|
|
761
|
-
}));
|
|
762
|
-
const preferredProject = projectSnapshots[0];
|
|
763
|
-
const preferredRoadmapRef = preferredProject?.roadmapIds[0] ?? "ROADMAP-0001";
|
|
764
|
-
const noTaskDiscoveryGuidance = await resolveNoTaskDiscoveryGuidance(preferredProject?.governanceDir);
|
|
765
|
-
const markdown = renderToolResponseMarkdown({
|
|
766
|
-
toolName: "taskNext",
|
|
767
|
-
sections: [
|
|
768
|
-
summarySection([
|
|
769
|
-
`- rootPaths: ${roots.join(", ")}`,
|
|
770
|
-
`- rootCount: ${roots.length}`,
|
|
771
|
-
`- maxDepth: ${depth}`,
|
|
772
|
-
`- matchedProjects: ${projects.length}`,
|
|
773
|
-
"- actionableTasks: 0",
|
|
774
|
-
]),
|
|
775
|
-
evidenceSection([
|
|
776
|
-
"### Project Snapshots",
|
|
777
|
-
...(projectSnapshots.length > 0
|
|
778
|
-
? projectSnapshots.map((item, index) => `${index + 1}. ${toProjectPath(item.governanceDir)} | total=${item.total} | todo=${item.todo} | in_progress=${item.inProgress} | blocked=${item.blocked} | done=${item.done} | roadmapIds=${item.roadmapIds.join(", ") || "(none)"}`)
|
|
779
|
-
: ["- (none)"]),
|
|
780
|
-
"",
|
|
781
|
-
"### Seed Task Template",
|
|
782
|
-
...renderTaskSeedTemplate(preferredRoadmapRef),
|
|
783
|
-
]),
|
|
784
|
-
guidanceSection([
|
|
785
|
-
"- No TODO/IN_PROGRESS task is available.",
|
|
786
|
-
"- Create 1-3 new TODO tasks using `taskCreate(...)` from active roadmap slices.",
|
|
787
|
-
"- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.",
|
|
788
|
-
"- If roadmap has active milestones, analyze milestone intent and split into 1-3 executable TODO tasks.",
|
|
789
|
-
"",
|
|
790
|
-
"### No-Task Discovery Checklist",
|
|
791
|
-
...noTaskDiscoveryGuidance,
|
|
792
|
-
"",
|
|
793
|
-
"- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.",
|
|
794
|
-
"- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.",
|
|
795
|
-
"- After creating tasks, rerun `taskNext` to re-rank actionable work.",
|
|
796
|
-
]),
|
|
797
|
-
lintSection([
|
|
798
|
-
"- No actionable tasks found. Verify task statuses and required fields in .projitive task table.",
|
|
799
|
-
"- Ensure each new task has stable TASK-xxxx ID and at least one roadmapRefs item.",
|
|
800
|
-
]),
|
|
801
|
-
nextCallSection(preferredProject
|
|
802
|
-
? `taskCreate(projectPath=\"${toProjectPath(preferredProject.governanceDir)}\", taskId=\"TASK-0001\", title=\"Create first executable slice\", roadmapRefs=[\"${preferredRoadmapRef}\"], summary=\"Derived from active roadmap milestone\")`
|
|
803
|
-
: "projectScan()"),
|
|
804
|
-
],
|
|
754
|
+
async execute({ projectPath, taskId, title, status, owner, summary, roadmapRefs, links, subState, blocker }) {
|
|
755
|
+
if (taskId && !isValidTaskId(taskId)) {
|
|
756
|
+
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")`);
|
|
757
|
+
}
|
|
758
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
759
|
+
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
760
|
+
const { tasksPath, tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
|
|
761
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
762
|
+
const finalTaskId = taskId ?? nextTaskId(tasks);
|
|
763
|
+
const duplicated = tasks.some((item) => item.id === finalTaskId);
|
|
764
|
+
if (duplicated) {
|
|
765
|
+
throw new ToolExecutionError(`Task already exists: ${finalTaskId}`, ['task IDs must be unique', 'use taskUpdate for existing tasks'], `taskUpdate(projectPath="${normalizedProjectPath}", taskId="${finalTaskId}", updates={...})`);
|
|
766
|
+
}
|
|
767
|
+
const createdTask = normalizeTask({
|
|
768
|
+
id: finalTaskId,
|
|
769
|
+
title,
|
|
770
|
+
status: status ?? 'TODO',
|
|
771
|
+
owner,
|
|
772
|
+
summary,
|
|
773
|
+
roadmapRefs,
|
|
774
|
+
links,
|
|
775
|
+
subState,
|
|
776
|
+
blocker,
|
|
777
|
+
updatedAt: nowIso(),
|
|
805
778
|
});
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selectedTaskDocument.markdownPath}`,
|
|
839
|
-
"",
|
|
840
|
-
"### Top Candidates",
|
|
841
|
-
...rankedCandidates
|
|
842
|
-
.slice(0, candidateLimit)
|
|
843
|
-
.map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | projectPath=${toProjectPath(item.governanceDir)} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
|
|
844
|
-
"",
|
|
845
|
-
"### Selection Reason",
|
|
846
|
-
"- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.",
|
|
847
|
-
`- Selected candidate scores: projectScore=${selected.projectScore}, taskPriority=${selected.taskPriority}, taskUpdatedAtMs=${selected.taskUpdatedAtMs}.`,
|
|
848
|
-
"",
|
|
849
|
-
"### Related Artifacts",
|
|
850
|
-
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
|
|
851
|
-
"",
|
|
852
|
-
"### Reference Locations",
|
|
853
|
-
...(referenceLocations.length > 0
|
|
854
|
-
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
855
|
-
: ["- (none)"]),
|
|
856
|
-
"",
|
|
857
|
-
"### Suggested Read Order",
|
|
858
|
-
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
859
|
-
]),
|
|
860
|
-
guidanceSection([
|
|
861
|
-
"- Start immediately with Suggested Read Order and execute the selected task.",
|
|
862
|
-
"- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.",
|
|
863
|
-
"- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.",
|
|
864
|
-
]),
|
|
865
|
-
lintSection(lintSuggestions),
|
|
866
|
-
nextCallSection(`taskContext(projectPath=\"${toProjectPath(selected.governanceDir)}\", taskId=\"${selected.task.id}\")`),
|
|
867
|
-
],
|
|
868
|
-
});
|
|
869
|
-
return asText(markdown);
|
|
870
|
-
});
|
|
871
|
-
server.registerTool("taskContext", {
|
|
872
|
-
title: "Task Context",
|
|
873
|
-
description: "Get deep context, evidence links, and read order for one task",
|
|
779
|
+
await upsertTaskInStore(tasksPath, createdTask);
|
|
780
|
+
await loadTasksDocumentWithOptions(governanceDir, true);
|
|
781
|
+
return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, createdTask };
|
|
782
|
+
},
|
|
783
|
+
summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, createdTask }) => [
|
|
784
|
+
`- projectPath: ${normalizedProjectPath}`,
|
|
785
|
+
`- governanceDir: ${governanceDir}`,
|
|
786
|
+
`- tasksView: ${tasksViewPath}`,
|
|
787
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
788
|
+
`- taskId: ${createdTask.id}`,
|
|
789
|
+
`- status: ${createdTask.status}`,
|
|
790
|
+
`- owner: ${createdTask.owner || '(none)'}`,
|
|
791
|
+
`- updatedAt: ${createdTask.updatedAt}`,
|
|
792
|
+
],
|
|
793
|
+
evidence: ({ createdTask }) => [
|
|
794
|
+
'### Created Task',
|
|
795
|
+
`- ${createdTask.id} | ${createdTask.status} | ${createdTask.title}`,
|
|
796
|
+
`- summary: ${createdTask.summary || '(none)'}`,
|
|
797
|
+
`- roadmapRefs: ${createdTask.roadmapRefs.join(', ') || '(none)'}`,
|
|
798
|
+
`- links: ${createdTask.links.join(', ') || '(none)'}`,
|
|
799
|
+
],
|
|
800
|
+
guidance: () => [
|
|
801
|
+
'Task created in governance store successfully and tasks.md has been synced.',
|
|
802
|
+
'Run taskContext to verify references and lint guidance.',
|
|
803
|
+
],
|
|
804
|
+
suggestions: ({ createdTask }) => collectSingleTaskLintSuggestions(createdTask),
|
|
805
|
+
nextCall: ({ normalizedProjectPath, createdTask }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${createdTask.id}")`,
|
|
806
|
+
}));
|
|
807
|
+
server.registerTool(...createGovernedTool({
|
|
808
|
+
name: 'taskNext',
|
|
809
|
+
title: 'Task Next',
|
|
810
|
+
description: 'Start here to auto-select the highest-priority actionable task',
|
|
874
811
|
inputSchema: {
|
|
875
|
-
|
|
876
|
-
taskId: z.string(),
|
|
812
|
+
limit: z.number().int().min(1).max(20).optional(),
|
|
877
813
|
},
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
814
|
+
async execute({ limit }) {
|
|
815
|
+
const roots = resolveScanRoots();
|
|
816
|
+
const depth = resolveScanDepth();
|
|
817
|
+
const projects = await discoverProjectsAcrossRoots(roots, depth);
|
|
818
|
+
const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
|
|
819
|
+
if (rankedCandidates.length === 0) {
|
|
820
|
+
const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
|
|
821
|
+
const tasksPath = path.join(governanceDir, '.projitive');
|
|
822
|
+
await ensureStore(tasksPath);
|
|
823
|
+
const stats = await loadTaskStatusStatsFromStore(tasksPath);
|
|
824
|
+
const roadmapIds = await readRoadmapIds(governanceDir);
|
|
825
|
+
return { governanceDir, roadmapIds, total: stats.total, todo: stats.todo, inProgress: stats.inProgress, blocked: stats.blocked, done: stats.done };
|
|
826
|
+
}));
|
|
827
|
+
const preferredProject = projectSnapshots[0];
|
|
828
|
+
const preferredRoadmapRef = preferredProject?.roadmapIds[0] ?? 'ROADMAP-0001';
|
|
829
|
+
const noTaskDiscoveryGuidance = await resolveNoTaskDiscoveryGuidance(preferredProject?.governanceDir);
|
|
830
|
+
return { isEmpty: true, roots, depth, projects, projectSnapshots, preferredProject, preferredRoadmapRef, noTaskDiscoveryGuidance };
|
|
831
|
+
}
|
|
832
|
+
const selected = rankedCandidates[0];
|
|
833
|
+
const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
|
|
834
|
+
const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
|
|
835
|
+
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
836
|
+
const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
|
|
837
|
+
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
|
|
838
|
+
const taskLocation = (await findTextReferences(selectedTaskDocument.markdownPath, selected.task.id))[0];
|
|
839
|
+
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
840
|
+
const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
|
|
841
|
+
const candidateLimit = limit ?? 5;
|
|
890
842
|
return {
|
|
891
|
-
|
|
892
|
-
|
|
843
|
+
isEmpty: false,
|
|
844
|
+
roots, depth, projects,
|
|
845
|
+
rankedCandidates, selected, selectedTaskDocument,
|
|
846
|
+
relatedArtifacts, referenceLocations,
|
|
847
|
+
suggestedReadOrder, projectContextDocsState, taskLocation, candidateLimit,
|
|
893
848
|
};
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
905
|
-
const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
|
|
906
|
-
// Build summary with subState and blocker info (v1.1.0)
|
|
907
|
-
const summaryLines = [
|
|
908
|
-
`- projectPath: ${normalizedProjectPath}`,
|
|
909
|
-
`- governanceDir: ${governanceDir}`,
|
|
910
|
-
`- taskId: ${task.id}`,
|
|
911
|
-
`- title: ${task.title}`,
|
|
912
|
-
`- status: ${task.status}`,
|
|
913
|
-
`- owner: ${task.owner}`,
|
|
914
|
-
`- updatedAt: ${task.updatedAt}`,
|
|
915
|
-
`- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
|
|
916
|
-
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
|
|
917
|
-
];
|
|
918
|
-
// Add subState info for IN_PROGRESS tasks (v1.1.0)
|
|
919
|
-
if (task.subState && task.status === "IN_PROGRESS") {
|
|
920
|
-
summaryLines.push(`- subState:`);
|
|
921
|
-
if (task.subState.phase) {
|
|
922
|
-
summaryLines.push(` - phase: ${task.subState.phase}`);
|
|
849
|
+
},
|
|
850
|
+
summary: (data) => {
|
|
851
|
+
if (data.isEmpty) {
|
|
852
|
+
return [
|
|
853
|
+
`- rootPaths: ${data.roots.join(', ')}`,
|
|
854
|
+
`- rootCount: ${data.roots.length}`,
|
|
855
|
+
`- maxDepth: ${data.depth}`,
|
|
856
|
+
`- matchedProjects: ${data.projects.length}`,
|
|
857
|
+
'- actionableTasks: 0',
|
|
858
|
+
];
|
|
923
859
|
}
|
|
924
|
-
|
|
925
|
-
|
|
860
|
+
return [
|
|
861
|
+
`- rootPaths: ${data.roots.join(', ')}`,
|
|
862
|
+
`- rootCount: ${data.roots.length}`,
|
|
863
|
+
`- maxDepth: ${data.depth}`,
|
|
864
|
+
`- matchedProjects: ${data.projects.length}`,
|
|
865
|
+
`- actionableTasks: ${data.rankedCandidates.length}`,
|
|
866
|
+
`- selectedProject: ${toProjectPath(data.selected.governanceDir)}`,
|
|
867
|
+
`- selectedTaskId: ${data.selected.task.id}`,
|
|
868
|
+
`- selectedTaskStatus: ${data.selected.task.status}`,
|
|
869
|
+
];
|
|
870
|
+
},
|
|
871
|
+
evidence: (data) => {
|
|
872
|
+
if (data.isEmpty) {
|
|
873
|
+
return [
|
|
874
|
+
'### Project Snapshots',
|
|
875
|
+
...(data.projectSnapshots.length > 0
|
|
876
|
+
? 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)'}`)
|
|
877
|
+
: ['- (none)']),
|
|
878
|
+
'',
|
|
879
|
+
'### Seed Task Template',
|
|
880
|
+
...renderTaskSeedTemplate(data.preferredRoadmapRef),
|
|
881
|
+
];
|
|
926
882
|
}
|
|
927
|
-
|
|
928
|
-
|
|
883
|
+
const { taskLocation, selectedTaskDocument, rankedCandidates, candidateLimit, relatedArtifacts, referenceLocations, suggestedReadOrder } = data;
|
|
884
|
+
const taskLocationStr = taskLocation
|
|
885
|
+
? `${taskLocation.filePath}#L${taskLocation.line}`
|
|
886
|
+
: selectedTaskDocument.markdownPath;
|
|
887
|
+
return [
|
|
888
|
+
'### Selected Task',
|
|
889
|
+
`- id: ${data.selected.task.id}`,
|
|
890
|
+
`- title: ${data.selected.task.title}`,
|
|
891
|
+
`- owner: ${data.selected.task.owner || '(none)'}`,
|
|
892
|
+
`- updatedAt: ${data.selected.task.updatedAt}`,
|
|
893
|
+
`- roadmapRefs: ${data.selected.task.roadmapRefs.join(', ') || '(none)'}`,
|
|
894
|
+
`- taskLocation: ${taskLocationStr}`,
|
|
895
|
+
'',
|
|
896
|
+
'### Top Candidates',
|
|
897
|
+
...rankedCandidates
|
|
898
|
+
.slice(0, candidateLimit)
|
|
899
|
+
.map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | projectPath=${toProjectPath(item.governanceDir)} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
|
|
900
|
+
'',
|
|
901
|
+
'### Selection Reason',
|
|
902
|
+
'- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.',
|
|
903
|
+
`- Selected candidate scores: projectScore=${data.selected.projectScore}, taskPriority=${data.selected.taskPriority}, taskUpdatedAtMs=${data.selected.taskUpdatedAtMs}.`,
|
|
904
|
+
'',
|
|
905
|
+
'### Related Artifacts',
|
|
906
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
|
|
907
|
+
'',
|
|
908
|
+
'### Reference Locations',
|
|
909
|
+
...(referenceLocations.length > 0
|
|
910
|
+
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
911
|
+
: ['- (none)']),
|
|
912
|
+
'',
|
|
913
|
+
'### Suggested Read Order',
|
|
914
|
+
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
915
|
+
];
|
|
916
|
+
},
|
|
917
|
+
guidance: (data) => {
|
|
918
|
+
if (data.isEmpty) {
|
|
919
|
+
return [
|
|
920
|
+
'- No TODO/IN_PROGRESS task is available.',
|
|
921
|
+
'- Create 1-3 new TODO tasks using `taskCreate(...)` from active roadmap slices.',
|
|
922
|
+
'- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.',
|
|
923
|
+
'- If roadmap has active milestones, analyze milestone intent and split into 1-3 executable TODO tasks.',
|
|
924
|
+
'',
|
|
925
|
+
'### No-Task Discovery Checklist',
|
|
926
|
+
...data.noTaskDiscoveryGuidance,
|
|
927
|
+
'',
|
|
928
|
+
'- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.',
|
|
929
|
+
'- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.',
|
|
930
|
+
'- After creating tasks, rerun `taskNext` to re-rank actionable work.',
|
|
931
|
+
];
|
|
929
932
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
933
|
+
return [
|
|
934
|
+
...(!data.projectContextDocsState.ready
|
|
935
|
+
? [
|
|
936
|
+
'- Project context docs are incomplete. Complete missing project architecture/style docs before deep implementation.',
|
|
937
|
+
...(data.projectContextDocsState.missingArchitectureDocs
|
|
938
|
+
? [`- Missing architecture design doc: create required file ${CORE_ARCHITECTURE_DOC_FILE}.`]
|
|
939
|
+
: []),
|
|
940
|
+
...(data.projectContextDocsState.missingStyleDocs
|
|
941
|
+
? [`- Missing design style doc: create required file ${CORE_STYLE_DOC_FILE}.`]
|
|
942
|
+
: []),
|
|
943
|
+
]
|
|
944
|
+
: []),
|
|
945
|
+
'- Start immediately with Suggested Read Order and execute the selected task.',
|
|
946
|
+
'- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.',
|
|
947
|
+
'- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.',
|
|
948
|
+
];
|
|
949
|
+
},
|
|
950
|
+
suggestions: (data) => {
|
|
951
|
+
if (data.isEmpty) {
|
|
952
|
+
return [
|
|
953
|
+
'- No actionable tasks found. Verify task statuses and required fields in .projitive task table.',
|
|
954
|
+
'- Ensure each new task has stable TASK-<number> ID and at least one roadmapRefs item.',
|
|
955
|
+
];
|
|
938
956
|
}
|
|
939
|
-
|
|
940
|
-
|
|
957
|
+
return [
|
|
958
|
+
...collectTaskLintSuggestions(data.selectedTaskDocument.tasks),
|
|
959
|
+
...renderLintSuggestions(collectProjectContextDocsLintSuggestions(data.projectContextDocsState)),
|
|
960
|
+
];
|
|
961
|
+
},
|
|
962
|
+
nextCall: (data) => {
|
|
963
|
+
if (data.isEmpty) {
|
|
964
|
+
return data.preferredProject
|
|
965
|
+
? `taskCreate(projectPath="${toProjectPath(data.preferredProject.governanceDir)}", title="Create first executable slice", roadmapRefs=["${data.preferredRoadmapRef}"], summary="Derived from active roadmap milestone")`
|
|
966
|
+
: 'projectScan()';
|
|
941
967
|
}
|
|
942
|
-
|
|
943
|
-
|
|
968
|
+
return `taskContext(projectPath="${toProjectPath(data.selected.governanceDir)}", taskId="${data.selected.task.id}")`;
|
|
969
|
+
},
|
|
970
|
+
}));
|
|
971
|
+
server.registerTool(...createGovernedTool({
|
|
972
|
+
name: 'taskContext',
|
|
973
|
+
title: 'Task Context',
|
|
974
|
+
description: 'Get deep context, evidence links, and read order for one task',
|
|
975
|
+
inputSchema: {
|
|
976
|
+
projectPath: z.string(),
|
|
977
|
+
taskId: z.string(),
|
|
978
|
+
},
|
|
979
|
+
async execute({ projectPath, taskId }) {
|
|
980
|
+
if (!isValidTaskId(taskId)) {
|
|
981
|
+
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")`);
|
|
944
982
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
983
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
984
|
+
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
985
|
+
const { markdownPath, tasks } = await loadTasksDocument(governanceDir);
|
|
986
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
987
|
+
const task = tasks.find((item) => item.id === taskId);
|
|
988
|
+
if (!task) {
|
|
989
|
+
throw new ToolExecutionError(`Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`);
|
|
990
|
+
}
|
|
991
|
+
const researchBriefState = await inspectTaskResearchBrief(governanceDir, task);
|
|
992
|
+
const contextReadingGuidance = await resolveTaskContextReadingGuidance(governanceDir);
|
|
993
|
+
const taskLocation = (await findTextReferences(markdownPath, taskId))[0];
|
|
994
|
+
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
995
|
+
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
996
|
+
const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
|
|
997
|
+
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
|
|
998
|
+
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
999
|
+
const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
|
|
1000
|
+
return {
|
|
1001
|
+
normalizedProjectPath, governanceDir, markdownPath, roadmapViewPath,
|
|
1002
|
+
task, researchBriefState, contextReadingGuidance,
|
|
1003
|
+
taskLocation, referenceLocations, relatedArtifacts, suggestedReadOrder,
|
|
1004
|
+
projectContextDocsState,
|
|
1005
|
+
};
|
|
1006
|
+
},
|
|
1007
|
+
summary: ({ normalizedProjectPath, governanceDir, markdownPath, roadmapViewPath, task, researchBriefState, projectContextDocsState, taskLocation }) => {
|
|
1008
|
+
const lines = [
|
|
1009
|
+
`- projectPath: ${normalizedProjectPath}`,
|
|
1010
|
+
`- governanceDir: ${governanceDir}`,
|
|
1011
|
+
`- tasksView: ${markdownPath}`,
|
|
1012
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
1013
|
+
`- taskId: ${task.id}`,
|
|
1014
|
+
`- title: ${task.title}`,
|
|
1015
|
+
`- status: ${task.status}`,
|
|
1016
|
+
`- owner: ${task.owner}`,
|
|
1017
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
1018
|
+
`- roadmapRefs: ${task.roadmapRefs.join(', ') || '(none)'}`,
|
|
1019
|
+
`- researchBriefPath: ${researchBriefState.relativePath}`,
|
|
1020
|
+
`- researchBriefStatus: ${researchBriefState.ready ? 'READY' : 'MISSING'}`,
|
|
1021
|
+
`- architectureDocsStatus: ${projectContextDocsState.missingArchitectureDocs ? 'MISSING' : 'READY'}`,
|
|
1022
|
+
`- styleDocsStatus: ${projectContextDocsState.missingStyleDocs ? 'MISSING' : 'READY'}`,
|
|
1023
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
|
|
1024
|
+
];
|
|
1025
|
+
if (task.subState && task.status === 'IN_PROGRESS') {
|
|
1026
|
+
lines.push('- subState:');
|
|
1027
|
+
if (task.subState.phase)
|
|
1028
|
+
lines.push(` - phase: ${task.subState.phase}`);
|
|
1029
|
+
if (typeof task.subState.confidence === 'number')
|
|
1030
|
+
lines.push(` - confidence: ${task.subState.confidence}`);
|
|
1031
|
+
if (task.subState.estimatedCompletion)
|
|
1032
|
+
lines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
1033
|
+
}
|
|
1034
|
+
if (task.blocker && task.status === 'BLOCKED') {
|
|
1035
|
+
lines.push('- blocker:');
|
|
1036
|
+
lines.push(` - type: ${task.blocker.type}`);
|
|
1037
|
+
lines.push(` - description: ${task.blocker.description}`);
|
|
1038
|
+
if (task.blocker.blockingEntity)
|
|
1039
|
+
lines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
1040
|
+
if (task.blocker.unblockCondition)
|
|
1041
|
+
lines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
|
|
1042
|
+
if (task.blocker.escalationPath)
|
|
1043
|
+
lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
1044
|
+
}
|
|
1045
|
+
return lines;
|
|
1046
|
+
},
|
|
1047
|
+
evidence: ({ task, researchBriefState, projectContextDocsState, relatedArtifacts, referenceLocations, suggestedReadOrder }) => [
|
|
1048
|
+
'### Pre-Execution Research Brief',
|
|
1049
|
+
`- path: ${researchBriefState.relativePath}`,
|
|
1050
|
+
`- absolutePath: ${researchBriefState.absolutePath}`,
|
|
1051
|
+
`- status: ${researchBriefState.ready ? 'READY' : 'MISSING'}`,
|
|
1052
|
+
...(!researchBriefState.ready
|
|
1053
|
+
? [
|
|
1054
|
+
'',
|
|
1055
|
+
'### Required Research Brief Template',
|
|
1056
|
+
...renderTaskResearchBriefTemplate(task).map((line) => `- ${line}`),
|
|
1057
|
+
]
|
|
1058
|
+
: []),
|
|
1059
|
+
'',
|
|
1060
|
+
'### Project Context Docs Check',
|
|
1061
|
+
`- architecture docs: ${projectContextDocsState.architectureDocs.length > 0 ? 'found' : 'missing'}`,
|
|
1062
|
+
...(projectContextDocsState.architectureDocs.length > 0
|
|
1063
|
+
? projectContextDocsState.architectureDocs.map((item) => `- architecture: ${item}`)
|
|
1064
|
+
: [`- architecture: add required file ${CORE_ARCHITECTURE_DOC_FILE}.`]),
|
|
1065
|
+
`- design style docs: ${projectContextDocsState.styleDocs.length > 0 ? 'found' : 'missing'}`,
|
|
1066
|
+
...(projectContextDocsState.styleDocs.length > 0
|
|
1067
|
+
? projectContextDocsState.styleDocs.map((item) => `- style: ${item}`)
|
|
1068
|
+
: [`- style: add required file ${CORE_STYLE_DOC_FILE}.`]),
|
|
1069
|
+
'',
|
|
1070
|
+
'### Related Artifacts',
|
|
1071
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
|
|
1072
|
+
'',
|
|
1073
|
+
'### Reference Locations',
|
|
1074
|
+
...(referenceLocations.length > 0
|
|
1075
|
+
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
1076
|
+
: ['- (none)']),
|
|
1077
|
+
'',
|
|
1078
|
+
'### Suggested Read Order',
|
|
1079
|
+
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
1080
|
+
],
|
|
1081
|
+
guidance: ({ researchBriefState, projectContextDocsState, contextReadingGuidance, task }) => [
|
|
1082
|
+
...(!researchBriefState.ready
|
|
1083
|
+
? [
|
|
1084
|
+
'- Pre-execution gate is NOT satisfied. Complete research brief first, then proceed with implementation.',
|
|
1085
|
+
`- Create or update ${researchBriefState.relativePath} with design guidelines + code architecture findings before code changes.`,
|
|
1086
|
+
'- Include exact file/line locations in the brief (for example path/to/file.ts#L120).',
|
|
1087
|
+
'- Re-run taskContext after writing the brief and confirm researchBriefStatus becomes READY.',
|
|
1088
|
+
]
|
|
1089
|
+
: [
|
|
1090
|
+
'- Pre-execution gate satisfied. Read the research brief first, then continue implementation.',
|
|
1091
|
+
`- Must read ${researchBriefState.relativePath} before any task execution changes.`,
|
|
961
1092
|
]),
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1093
|
+
...(!projectContextDocsState.ready
|
|
1094
|
+
? [
|
|
1095
|
+
'- Project context docs gate is NOT satisfied. Complete missing project architecture/style docs first.',
|
|
1096
|
+
...(projectContextDocsState.missingArchitectureDocs
|
|
1097
|
+
? [`- Missing architecture design doc. Add required file ${CORE_ARCHITECTURE_DOC_FILE} and include architecture boundaries and module responsibilities.`]
|
|
1098
|
+
: []),
|
|
1099
|
+
...(projectContextDocsState.missingStyleDocs
|
|
1100
|
+
? [`- Missing design style doc. Add required file ${CORE_STYLE_DOC_FILE} and include style language, tokens/themes, and UI consistency rules.`]
|
|
1101
|
+
: []),
|
|
1102
|
+
'- Re-run taskContext and confirm both architectureDocsStatus/styleDocsStatus are READY.',
|
|
1103
|
+
]
|
|
1104
|
+
: [
|
|
1105
|
+
'- Project context docs gate satisfied. Architecture/style docs are available for execution alignment.',
|
|
972
1106
|
]),
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1107
|
+
'- Read the files in Suggested Read Order.',
|
|
1108
|
+
'',
|
|
1109
|
+
'### Context Reading',
|
|
1110
|
+
...contextReadingGuidance,
|
|
1111
|
+
'',
|
|
1112
|
+
'- Verify whether current status and evidence are consistent.',
|
|
1113
|
+
...taskStatusGuidance(task),
|
|
1114
|
+
'- If updates are needed, use tool writes for governance store (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.',
|
|
1115
|
+
'- After editing, re-run `taskContext` to verify references and context consistency.',
|
|
1116
|
+
],
|
|
1117
|
+
suggestions: ({ task, researchBriefState, projectContextDocsState }) => [
|
|
1118
|
+
...collectSingleTaskLintSuggestions(task),
|
|
1119
|
+
...renderLintSuggestions(collectTaskResearchBriefLintSuggestions(researchBriefState)),
|
|
1120
|
+
...renderLintSuggestions(collectProjectContextDocsLintSuggestions(projectContextDocsState)),
|
|
1121
|
+
],
|
|
1122
|
+
nextCall: ({ normalizedProjectPath, task }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${task.id}")`,
|
|
1123
|
+
}));
|
|
979
1124
|
// taskUpdate tool - Update task fields including subState and blocker (Spec v1.1.0)
|
|
980
|
-
server.registerTool(
|
|
981
|
-
|
|
982
|
-
|
|
1125
|
+
server.registerTool(...createGovernedTool({
|
|
1126
|
+
name: 'taskUpdate',
|
|
1127
|
+
title: 'Task Update',
|
|
1128
|
+
description: 'Update task fields including status, owner, summary, subState, and blocker metadata',
|
|
983
1129
|
inputSchema: {
|
|
984
1130
|
projectPath: z.string(),
|
|
985
1131
|
taskId: z.string(),
|
|
986
1132
|
updates: z.object({
|
|
987
|
-
status: z.enum([
|
|
1133
|
+
status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
|
|
988
1134
|
owner: z.string().optional(),
|
|
989
1135
|
summary: z.string().optional(),
|
|
990
1136
|
roadmapRefs: z.array(z.string()).optional(),
|
|
991
1137
|
links: z.array(z.string()).optional(),
|
|
992
1138
|
subState: z.object({
|
|
993
|
-
phase: z.enum([
|
|
1139
|
+
phase: z.enum(['discovery', 'design', 'implementation', 'testing']).optional(),
|
|
994
1140
|
confidence: z.number().min(0).max(1).optional(),
|
|
995
1141
|
estimatedCompletion: z.string().optional(),
|
|
996
1142
|
}).optional(),
|
|
997
1143
|
blocker: z.object({
|
|
998
|
-
type: z.enum([
|
|
1144
|
+
type: z.enum(['internal_dependency', 'external_dependency', 'resource', 'approval']),
|
|
999
1145
|
description: z.string(),
|
|
1000
1146
|
blockingEntity: z.string().optional(),
|
|
1001
1147
|
unblockCondition: z.string().optional(),
|
|
@@ -1003,136 +1149,108 @@ export function registerTaskTools(server) {
|
|
|
1003
1149
|
}).optional(),
|
|
1004
1150
|
}),
|
|
1005
1151
|
},
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
...asText(renderErrorMarkdown("taskUpdate", `Invalid task ID format: ${taskId}`, ["expected format: TASK-0001", "retry with a valid task ID"], `taskUpdate(projectPath=\"${projectPath}\", taskId=\"TASK-0001\", updates={...})`)),
|
|
1010
|
-
isError: true,
|
|
1011
|
-
};
|
|
1012
|
-
}
|
|
1013
|
-
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1014
|
-
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
1015
|
-
const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
|
|
1016
|
-
const taskIndex = tasks.findIndex((item) => item.id === taskId);
|
|
1017
|
-
if (taskIndex === -1) {
|
|
1018
|
-
return {
|
|
1019
|
-
...asText(renderErrorMarkdown("taskUpdate", `Task not found: ${taskId}`, ["run `taskList` to discover available IDs", "retry with an existing task ID"], `taskList(projectPath=\"${toProjectPath(governanceDir)}\")`)),
|
|
1020
|
-
isError: true,
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
const task = tasks[taskIndex];
|
|
1024
|
-
const originalStatus = task.status;
|
|
1025
|
-
// Validate status transition
|
|
1026
|
-
if (updates.status && !validateTransition(originalStatus, updates.status)) {
|
|
1027
|
-
return {
|
|
1028
|
-
...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}\")`)),
|
|
1029
|
-
isError: true,
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
// Apply updates
|
|
1033
|
-
if (updates.status)
|
|
1034
|
-
task.status = updates.status;
|
|
1035
|
-
if (updates.owner !== undefined)
|
|
1036
|
-
task.owner = updates.owner;
|
|
1037
|
-
if (updates.summary !== undefined)
|
|
1038
|
-
task.summary = updates.summary;
|
|
1039
|
-
if (updates.roadmapRefs)
|
|
1040
|
-
task.roadmapRefs = updates.roadmapRefs;
|
|
1041
|
-
if (updates.links)
|
|
1042
|
-
task.links = updates.links;
|
|
1043
|
-
// Handle subState (Spec v1.1.0)
|
|
1044
|
-
if (updates.subState !== undefined) {
|
|
1045
|
-
if (updates.subState === null) {
|
|
1046
|
-
delete task.subState;
|
|
1152
|
+
async execute({ projectPath, taskId, updates }) {
|
|
1153
|
+
if (!isValidTaskId(taskId)) {
|
|
1154
|
+
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={...})`);
|
|
1047
1155
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1156
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1157
|
+
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
1158
|
+
const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
|
|
1159
|
+
const tasksViewPath = path.join(governanceDir, TASKS_MARKDOWN_FILE);
|
|
1160
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
1161
|
+
const taskIndex = tasks.findIndex((item) => item.id === taskId);
|
|
1162
|
+
if (taskIndex === -1) {
|
|
1163
|
+
throw new ToolExecutionError(`Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`);
|
|
1053
1164
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
if (updates.
|
|
1058
|
-
|
|
1165
|
+
const task = tasks[taskIndex];
|
|
1166
|
+
const originalStatus = task.status;
|
|
1167
|
+
const previewTask = normalizeTask({ ...task, ...updates, updatedAt: nowIso() });
|
|
1168
|
+
if (updates.status && !validateTransition(originalStatus, updates.status)) {
|
|
1169
|
+
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}")`);
|
|
1059
1170
|
}
|
|
1060
|
-
|
|
1061
|
-
task.
|
|
1171
|
+
const updatedSubState = updates.subState === null ? undefined
|
|
1172
|
+
: updates.subState !== undefined ? { ...(task.subState ?? {}), ...updates.subState }
|
|
1173
|
+
: task.subState;
|
|
1174
|
+
const updatedBlocker = updates.blocker === null ? undefined
|
|
1175
|
+
: updates.blocker !== undefined ? updates.blocker
|
|
1176
|
+
: task.blocker;
|
|
1177
|
+
const normalizedTask = normalizeTask({
|
|
1178
|
+
...task,
|
|
1179
|
+
...(updates.status ? { status: updates.status } : {}),
|
|
1180
|
+
...(updates.owner !== undefined ? { owner: updates.owner } : {}),
|
|
1181
|
+
...(updates.summary !== undefined ? { summary: updates.summary } : {}),
|
|
1182
|
+
...(updates.roadmapRefs ? { roadmapRefs: updates.roadmapRefs } : {}),
|
|
1183
|
+
...(updates.links ? { links: updates.links } : {}),
|
|
1184
|
+
subState: updatedSubState,
|
|
1185
|
+
blocker: updatedBlocker,
|
|
1186
|
+
updatedAt: nowIso(),
|
|
1187
|
+
});
|
|
1188
|
+
await upsertTaskInStore(tasksPath, normalizedTask);
|
|
1189
|
+
await loadTasksDocumentWithOptions(governanceDir, true);
|
|
1190
|
+
return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, taskId, originalStatus, task: normalizedTask, previewTask, updates };
|
|
1191
|
+
},
|
|
1192
|
+
summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, taskId, originalStatus, task }) => {
|
|
1193
|
+
const lines = [
|
|
1194
|
+
`- projectPath: ${normalizedProjectPath}`,
|
|
1195
|
+
`- governanceDir: ${governanceDir}`,
|
|
1196
|
+
`- tasksView: ${tasksViewPath}`,
|
|
1197
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
1198
|
+
`- taskId: ${taskId}`,
|
|
1199
|
+
`- originalStatus: ${originalStatus}`,
|
|
1200
|
+
`- newStatus: ${task.status}`,
|
|
1201
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
1202
|
+
];
|
|
1203
|
+
if (task.subState) {
|
|
1204
|
+
lines.push('- subState:');
|
|
1205
|
+
if (task.subState.phase)
|
|
1206
|
+
lines.push(` - phase: ${task.subState.phase}`);
|
|
1207
|
+
if (typeof task.subState.confidence === 'number')
|
|
1208
|
+
lines.push(` - confidence: ${task.subState.confidence}`);
|
|
1209
|
+
if (task.subState.estimatedCompletion)
|
|
1210
|
+
lines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
1062
1211
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
task
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
`-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
`-
|
|
1084
|
-
`-
|
|
1085
|
-
`-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
const markdown = renderToolResponseMarkdown({
|
|
1108
|
-
toolName: "taskUpdate",
|
|
1109
|
-
sections: [
|
|
1110
|
-
summarySection(updateSummary),
|
|
1111
|
-
evidenceSection([
|
|
1112
|
-
"### Updated Task",
|
|
1113
|
-
`- ${task.id} | ${task.status} | ${task.title}`,
|
|
1114
|
-
`- owner: ${task.owner || "(none)"}`,
|
|
1115
|
-
`- summary: ${task.summary || "(none)"}`,
|
|
1116
|
-
"",
|
|
1117
|
-
"### Update Details",
|
|
1118
|
-
...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
|
|
1119
|
-
...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
|
|
1120
|
-
...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
|
|
1121
|
-
...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(", ")}`] : []),
|
|
1122
|
-
...(updates.links ? [`- links: ${updates.links.join(", ")}`] : []),
|
|
1123
|
-
...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
|
|
1124
|
-
...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
|
|
1125
|
-
]),
|
|
1126
|
-
guidanceSection([
|
|
1127
|
-
"Task updated successfully and tasks.md has been synced. Run `taskContext` to verify the changes.",
|
|
1128
|
-
"If status changed to DONE, ensure evidence links are added.",
|
|
1129
|
-
"If subState or blocker were updated, verify the metadata is correct.",
|
|
1130
|
-
".projitive governance store is source of truth; tasks.md is a generated view and may be overwritten.",
|
|
1131
|
-
]),
|
|
1132
|
-
lintSection([]),
|
|
1133
|
-
nextCallSection(`taskContext(projectPath=\"${toProjectPath(governanceDir)}\", taskId=\"${taskId}\")`),
|
|
1134
|
-
],
|
|
1135
|
-
});
|
|
1136
|
-
return asText(markdown);
|
|
1137
|
-
});
|
|
1212
|
+
if (task.blocker) {
|
|
1213
|
+
lines.push('- blocker:');
|
|
1214
|
+
lines.push(` - type: ${task.blocker.type}`);
|
|
1215
|
+
lines.push(` - description: ${task.blocker.description}`);
|
|
1216
|
+
if (task.blocker.blockingEntity)
|
|
1217
|
+
lines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
1218
|
+
if (task.blocker.unblockCondition)
|
|
1219
|
+
lines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
|
|
1220
|
+
if (task.blocker.escalationPath)
|
|
1221
|
+
lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
1222
|
+
}
|
|
1223
|
+
return lines;
|
|
1224
|
+
},
|
|
1225
|
+
evidence: ({ task, originalStatus, updates }) => [
|
|
1226
|
+
'### Updated Task',
|
|
1227
|
+
`- ${task.id} | ${task.status} | ${task.title}`,
|
|
1228
|
+
`- owner: ${task.owner || '(none)'}`,
|
|
1229
|
+
`- summary: ${task.summary || '(none)'}`,
|
|
1230
|
+
'',
|
|
1231
|
+
'### Update Details',
|
|
1232
|
+
...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
|
|
1233
|
+
...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
|
|
1234
|
+
...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
|
|
1235
|
+
...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(', ')}`] : []),
|
|
1236
|
+
...(updates.links ? [`- links: ${updates.links.join(', ')}`] : []),
|
|
1237
|
+
...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
|
|
1238
|
+
...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
|
|
1239
|
+
],
|
|
1240
|
+
guidance: ({ updates, originalStatus }) => [
|
|
1241
|
+
'Task updated successfully and tasks.md has been synced. Run `taskContext` to verify the changes.',
|
|
1242
|
+
...(updates.status === 'IN_PROGRESS' && originalStatus === 'TODO'
|
|
1243
|
+
? ['- Ensure pre-execution research brief exists before deep implementation.']
|
|
1244
|
+
: []),
|
|
1245
|
+
...(updates.status === 'DONE'
|
|
1246
|
+
? ['- Verify evidence links are attached and reflect completed work.']
|
|
1247
|
+
: []),
|
|
1248
|
+
'.projitive governance store is source of truth; tasks.md is a generated view and may be overwritten.',
|
|
1249
|
+
],
|
|
1250
|
+
suggestions: async ({ previewTask, governanceDir }) => [
|
|
1251
|
+
...collectSingleTaskLintSuggestions(previewTask),
|
|
1252
|
+
...renderLintSuggestions(await collectDoneConformanceSuggestions(governanceDir, previewTask)),
|
|
1253
|
+
],
|
|
1254
|
+
nextCall: ({ normalizedProjectPath, taskId }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${taskId}")`,
|
|
1255
|
+
}));
|
|
1138
1256
|
}
|