@projitive/mcp 2.0.3 → 2.0.4
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/files.js +11 -11
- package/output/source/common/files.test.js +14 -14
- package/output/source/common/index.js +10 -10
- package/output/source/common/linter.js +27 -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 +74 -74
- 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/types.js +3 -3
- package/output/source/common/utils.js +8 -8
- package/output/source/index.js +16 -16
- package/output/source/index.test.js +64 -64
- package/output/source/prompts/index.js +3 -3
- package/output/source/prompts/quickStart.js +96 -96
- package/output/source/prompts/taskDiscovery.js +184 -184
- package/output/source/prompts/taskExecution.js +148 -148
- package/output/source/resources/designs.js +26 -26
- package/output/source/resources/designs.test.js +88 -88
- package/output/source/resources/governance.js +19 -19
- package/output/source/resources/index.js +2 -2
- 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/project.js +191 -191
- package/output/source/tools/project.test.js +174 -173
- package/output/source/tools/roadmap.js +110 -95
- package/output/source/tools/roadmap.test.js +54 -46
- package/output/source/tools/task.js +305 -277
- package/output/source/tools/task.test.js +117 -110
- package/output/source/types.js +22 -22
- package/package.json +8 -2
|
@@ -1,61 +1,61 @@
|
|
|
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 { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from
|
|
6
|
-
import { TASK_LINT_CODES, renderLintSuggestions } from
|
|
7
|
-
import { resolveGovernanceDir, resolveScanDepth, resolveScanRoots, discoverProjectsAcrossRoots, toProjectPath } from
|
|
8
|
-
import { isValidRoadmapId } from
|
|
9
|
-
import { SUB_STATE_PHASES, BLOCKER_TYPES } from
|
|
10
|
-
export const ALLOWED_STATUS = [
|
|
11
|
-
export const TASK_ID_REGEX = /^TASK
|
|
12
|
-
export const TASKS_MARKDOWN_FILE =
|
|
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, } 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';
|
|
7
|
+
import { resolveGovernanceDir, resolveScanDepth, resolveScanRoots, discoverProjectsAcrossRoots, toProjectPath } from './project.js';
|
|
8
|
+
import { isValidRoadmapId } from './roadmap.js';
|
|
9
|
+
import { SUB_STATE_PHASES, BLOCKER_TYPES } from '../types.js';
|
|
10
|
+
export const ALLOWED_STATUS = ['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE'];
|
|
11
|
+
export const TASK_ID_REGEX = /^TASK-(\d+)$/;
|
|
12
|
+
export const TASKS_MARKDOWN_FILE = 'tasks.md';
|
|
13
13
|
function appendLintSuggestions(target, suggestions) {
|
|
14
14
|
target.push(...renderLintSuggestions(suggestions));
|
|
15
15
|
}
|
|
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
|
+
'- Keep each discovery round small (1-3 tasks), then rerun taskNext immediately for re-ranking and execution.',
|
|
50
50
|
];
|
|
51
51
|
const DEFAULT_TASK_CONTEXT_READING_GUIDANCE = [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
'- Read governance workspace overview first (README.md / projitive://governance/workspace).',
|
|
53
|
+
'- Read roadmap and active milestones (roadmap.md / projitive://governance/roadmap).',
|
|
54
|
+
'- Read task view and related task cards (tasks.md / projitive://governance/tasks).',
|
|
55
|
+
'- Read design specs and technical decisions under designs/ (architecture, API contracts, constraints).',
|
|
56
|
+
'- Read reports/ for latest execution evidence, regressions, and unresolved risks.',
|
|
57
|
+
'- Read process guides under templates/docs/project guidelines to align with local governance rules.',
|
|
58
|
+
'- If available, read docs/ architecture or migration guides before major structural changes.',
|
|
59
59
|
];
|
|
60
60
|
export async function resolveNoTaskDiscoveryGuidance(governanceDir) {
|
|
61
61
|
void governanceDir;
|
|
@@ -66,7 +66,7 @@ export async function resolveTaskContextReadingGuidance(governanceDir) {
|
|
|
66
66
|
return DEFAULT_TASK_CONTEXT_READING_GUIDANCE;
|
|
67
67
|
}
|
|
68
68
|
async function readRoadmapIds(governanceDir) {
|
|
69
|
-
const dbPath = path.join(governanceDir,
|
|
69
|
+
const dbPath = path.join(governanceDir, '.projitive');
|
|
70
70
|
try {
|
|
71
71
|
await ensureStore(dbPath);
|
|
72
72
|
const milestones = await loadRoadmapsFromStore(dbPath);
|
|
@@ -79,16 +79,16 @@ async function readRoadmapIds(governanceDir) {
|
|
|
79
79
|
}
|
|
80
80
|
export function renderTaskSeedTemplate(roadmapRef) {
|
|
81
81
|
return [
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
'```markdown',
|
|
83
|
+
'## TASK-0001 | TODO | Define initial executable objective',
|
|
84
|
+
'- owner: ai-copilot',
|
|
85
|
+
'- summary: Convert one roadmap milestone or report gap into an actionable task.',
|
|
86
|
+
'- updatedAt: 2026-01-01T00:00:00.000Z',
|
|
87
87
|
`- roadmapRefs: ${roadmapRef}`,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
'- links:',
|
|
89
|
+
' - README.md',
|
|
90
|
+
' - .projitive/roadmap.md',
|
|
91
|
+
'```',
|
|
92
92
|
];
|
|
93
93
|
}
|
|
94
94
|
function isHttpUrl(value) {
|
|
@@ -96,9 +96,9 @@ function isHttpUrl(value) {
|
|
|
96
96
|
}
|
|
97
97
|
function isProjectRootRelativePath(value) {
|
|
98
98
|
return value.length > 0
|
|
99
|
-
&& !value.startsWith(
|
|
100
|
-
&& !value.startsWith(
|
|
101
|
-
&& !value.startsWith(
|
|
99
|
+
&& !value.startsWith('/')
|
|
100
|
+
&& !value.startsWith('./')
|
|
101
|
+
&& !value.startsWith('../')
|
|
102
102
|
&& !/^[A-Za-z]:\//.test(value);
|
|
103
103
|
}
|
|
104
104
|
function normalizeTaskLink(link) {
|
|
@@ -106,16 +106,16 @@ function normalizeTaskLink(link) {
|
|
|
106
106
|
if (trimmed.length === 0 || isHttpUrl(trimmed)) {
|
|
107
107
|
return trimmed;
|
|
108
108
|
}
|
|
109
|
-
const slashNormalized = trimmed.replace(/\\/g,
|
|
110
|
-
const withoutDotPrefix = slashNormalized.replace(/^\.\//,
|
|
111
|
-
return withoutDotPrefix.replace(/^\/+/,
|
|
109
|
+
const slashNormalized = trimmed.replace(/\\/g, '/');
|
|
110
|
+
const withoutDotPrefix = slashNormalized.replace(/^\.\//, '');
|
|
111
|
+
return withoutDotPrefix.replace(/^\/+/, '');
|
|
112
112
|
}
|
|
113
113
|
function resolveTaskLinkPath(projectPath, link) {
|
|
114
114
|
return path.join(projectPath, link);
|
|
115
115
|
}
|
|
116
116
|
async function readActionableTaskCandidates(governanceDirs) {
|
|
117
117
|
const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
|
|
118
|
-
const tasksPath = path.join(governanceDir,
|
|
118
|
+
const tasksPath = path.join(governanceDir, '.projitive');
|
|
119
119
|
await ensureStore(tasksPath);
|
|
120
120
|
const [stats, actionableTasks] = await Promise.all([
|
|
121
121
|
loadTaskStatusStatsFromStore(tasksPath),
|
|
@@ -125,11 +125,11 @@ async function readActionableTaskCandidates(governanceDirs) {
|
|
|
125
125
|
governanceDir,
|
|
126
126
|
tasks: actionableTasks,
|
|
127
127
|
projectScore: stats.inProgress * 2 + stats.todo,
|
|
128
|
-
projectLatestUpdatedAt: stats.latestUpdatedAt ||
|
|
128
|
+
projectLatestUpdatedAt: stats.latestUpdatedAt || '(unknown)',
|
|
129
129
|
};
|
|
130
130
|
}));
|
|
131
131
|
return snapshots.flatMap((item) => item.tasks
|
|
132
|
-
.filter((task) => task.status ===
|
|
132
|
+
.filter((task) => task.status === 'IN_PROGRESS' || task.status === 'TODO')
|
|
133
133
|
.map((task) => ({
|
|
134
134
|
governanceDir: item.governanceDir,
|
|
135
135
|
task,
|
|
@@ -143,13 +143,13 @@ export function nowIso() {
|
|
|
143
143
|
return new Date().toISOString();
|
|
144
144
|
}
|
|
145
145
|
export function isValidTaskId(id) {
|
|
146
|
-
return
|
|
146
|
+
return toTaskIdNumericSuffix(id) > 0;
|
|
147
147
|
}
|
|
148
148
|
export function taskPriority(status) {
|
|
149
|
-
if (status ===
|
|
149
|
+
if (status === 'IN_PROGRESS') {
|
|
150
150
|
return 2;
|
|
151
151
|
}
|
|
152
|
-
if (status ===
|
|
152
|
+
if (status === 'TODO') {
|
|
153
153
|
return 1;
|
|
154
154
|
}
|
|
155
155
|
return 0;
|
|
@@ -159,11 +159,21 @@ export function toTaskUpdatedAtMs(updatedAt) {
|
|
|
159
159
|
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
160
160
|
}
|
|
161
161
|
function toTaskIdNumericSuffix(taskId) {
|
|
162
|
-
const match = taskId.match(
|
|
162
|
+
const match = taskId.match(TASK_ID_REGEX);
|
|
163
163
|
if (!match) {
|
|
164
164
|
return -1;
|
|
165
165
|
}
|
|
166
|
-
|
|
166
|
+
const suffix = Number.parseInt(match[1], 10);
|
|
167
|
+
return Number.isFinite(suffix) ? suffix : -1;
|
|
168
|
+
}
|
|
169
|
+
function nextTaskId(tasks) {
|
|
170
|
+
const maxSuffix = tasks
|
|
171
|
+
.map((item) => toTaskIdNumericSuffix(item.id))
|
|
172
|
+
.filter((value) => value > 0)
|
|
173
|
+
.reduce((max, value) => Math.max(max, value), 0);
|
|
174
|
+
const next = maxSuffix + 1;
|
|
175
|
+
const minWidth = Math.max(4, String(next).length);
|
|
176
|
+
return `TASK-${String(next).padStart(minWidth, '0')}`;
|
|
167
177
|
}
|
|
168
178
|
export function sortTasksNewestFirst(tasks) {
|
|
169
179
|
return [...tasks].sort((a, b) => {
|
|
@@ -183,13 +193,13 @@ function normalizeAndSortTasks(tasks) {
|
|
|
183
193
|
}
|
|
184
194
|
function resolveTaskArtifactPaths(governanceDir) {
|
|
185
195
|
return {
|
|
186
|
-
tasksPath: path.join(governanceDir,
|
|
196
|
+
tasksPath: path.join(governanceDir, '.projitive'),
|
|
187
197
|
markdownPath: path.join(governanceDir, TASKS_MARKDOWN_FILE),
|
|
188
198
|
};
|
|
189
199
|
}
|
|
190
200
|
async function syncTasksMarkdownView(tasksPath, markdownPath, markdown, force = false) {
|
|
191
|
-
const sourceVersion = await getStoreVersion(tasksPath,
|
|
192
|
-
const viewState = await getMarkdownViewState(tasksPath,
|
|
201
|
+
const sourceVersion = await getStoreVersion(tasksPath, 'tasks');
|
|
202
|
+
const viewState = await getMarkdownViewState(tasksPath, 'tasks_markdown');
|
|
193
203
|
const markdownExists = await fs.access(markdownPath).then(() => true).catch(() => false);
|
|
194
204
|
const shouldWrite = force
|
|
195
205
|
|| !markdownExists
|
|
@@ -198,8 +208,8 @@ async function syncTasksMarkdownView(tasksPath, markdownPath, markdown, force =
|
|
|
198
208
|
if (!shouldWrite) {
|
|
199
209
|
return;
|
|
200
210
|
}
|
|
201
|
-
await fs.writeFile(markdownPath, markdown,
|
|
202
|
-
await markMarkdownViewBuilt(tasksPath,
|
|
211
|
+
await fs.writeFile(markdownPath, markdown, 'utf-8');
|
|
212
|
+
await markMarkdownViewBuilt(tasksPath, 'tasks_markdown', sourceVersion);
|
|
203
213
|
}
|
|
204
214
|
export function rankActionableTaskCandidates(candidates) {
|
|
205
215
|
return [...candidates].sort((a, b) => {
|
|
@@ -225,9 +235,9 @@ export function normalizeTask(task) {
|
|
|
225
235
|
const normalized = {
|
|
226
236
|
id: String(task.id),
|
|
227
237
|
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) :
|
|
238
|
+
status: ALLOWED_STATUS.includes(task.status) ? task.status : 'TODO',
|
|
239
|
+
owner: task.owner ? String(task.owner) : '',
|
|
240
|
+
summary: task.summary ? String(task.summary) : '',
|
|
231
241
|
updatedAt: task.updatedAt ? String(task.updatedAt) : nowIso(),
|
|
232
242
|
links: Array.isArray(task.links)
|
|
233
243
|
? Array.from(new Set(task.links
|
|
@@ -258,32 +268,32 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
258
268
|
if (duplicateIds.length > 0) {
|
|
259
269
|
suggestions.push({
|
|
260
270
|
code: TASK_LINT_CODES.DUPLICATE_ID,
|
|
261
|
-
message: `Duplicate task IDs detected: ${duplicateIds.join(
|
|
262
|
-
fixHint:
|
|
271
|
+
message: `Duplicate task IDs detected: ${duplicateIds.join(', ')}.`,
|
|
272
|
+
fixHint: 'Keep task IDs unique in marker block.',
|
|
263
273
|
});
|
|
264
274
|
}
|
|
265
|
-
const inProgressWithoutOwner = tasks.filter((task) => task.status ===
|
|
275
|
+
const inProgressWithoutOwner = tasks.filter((task) => task.status === 'IN_PROGRESS' && task.owner.trim().length === 0);
|
|
266
276
|
if (inProgressWithoutOwner.length > 0) {
|
|
267
277
|
suggestions.push({
|
|
268
278
|
code: TASK_LINT_CODES.IN_PROGRESS_OWNER_EMPTY,
|
|
269
279
|
message: `${inProgressWithoutOwner.length} IN_PROGRESS task(s) have empty owner.`,
|
|
270
|
-
fixHint:
|
|
280
|
+
fixHint: 'Set owner before continuing execution.',
|
|
271
281
|
});
|
|
272
282
|
}
|
|
273
|
-
const doneWithoutLinks = tasks.filter((task) => task.status ===
|
|
283
|
+
const doneWithoutLinks = tasks.filter((task) => task.status === 'DONE' && task.links.length === 0);
|
|
274
284
|
if (doneWithoutLinks.length > 0) {
|
|
275
285
|
suggestions.push({
|
|
276
286
|
code: TASK_LINT_CODES.DONE_LINKS_MISSING,
|
|
277
287
|
message: `${doneWithoutLinks.length} DONE task(s) have no links evidence.`,
|
|
278
|
-
fixHint:
|
|
288
|
+
fixHint: 'Add at least one evidence link before keeping DONE.',
|
|
279
289
|
});
|
|
280
290
|
}
|
|
281
|
-
const blockedWithoutReason = tasks.filter((task) => task.status ===
|
|
291
|
+
const blockedWithoutReason = tasks.filter((task) => task.status === 'BLOCKED' && task.summary.trim().length === 0);
|
|
282
292
|
if (blockedWithoutReason.length > 0) {
|
|
283
293
|
suggestions.push({
|
|
284
294
|
code: TASK_LINT_CODES.BLOCKED_SUMMARY_EMPTY,
|
|
285
295
|
message: `${blockedWithoutReason.length} BLOCKED task(s) have empty summary.`,
|
|
286
|
-
fixHint:
|
|
296
|
+
fixHint: 'Add blocker reason and unblock condition.',
|
|
287
297
|
});
|
|
288
298
|
}
|
|
289
299
|
const invalidUpdatedAt = tasks.filter((task) => !Number.isFinite(new Date(task.updatedAt).getTime()));
|
|
@@ -291,7 +301,7 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
291
301
|
suggestions.push({
|
|
292
302
|
code: TASK_LINT_CODES.UPDATED_AT_INVALID,
|
|
293
303
|
message: `${invalidUpdatedAt.length} task(s) have invalid updatedAt format.`,
|
|
294
|
-
fixHint:
|
|
304
|
+
fixHint: 'Use ISO8601 UTC timestamp.',
|
|
295
305
|
});
|
|
296
306
|
}
|
|
297
307
|
const missingRoadmapRefs = tasks.filter((task) => task.roadmapRefs.length === 0);
|
|
@@ -299,7 +309,7 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
299
309
|
suggestions.push({
|
|
300
310
|
code: TASK_LINT_CODES.ROADMAP_REFS_EMPTY,
|
|
301
311
|
message: `${missingRoadmapRefs.length} task(s) have empty roadmapRefs.`,
|
|
302
|
-
fixHint:
|
|
312
|
+
fixHint: 'Bind at least one ROADMAP-xxxx when applicable.',
|
|
303
313
|
});
|
|
304
314
|
}
|
|
305
315
|
const invalidLinkPathFormat = tasks.filter((task) => task.links.some((link) => {
|
|
@@ -310,18 +320,18 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
310
320
|
suggestions.push({
|
|
311
321
|
code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
|
|
312
322
|
message: `${invalidLinkPathFormat.length} task(s) contain invalid links path format.`,
|
|
313
|
-
fixHint:
|
|
323
|
+
fixHint: 'Use project-root-relative paths without leading slash (for example reports/task-0001.md) or http(s) URL.',
|
|
314
324
|
});
|
|
315
325
|
}
|
|
316
326
|
// ============================================================================
|
|
317
327
|
// Spec v1.1.0 - Blocker Categorization Validation
|
|
318
328
|
// ============================================================================
|
|
319
|
-
const blockedWithoutBlocker = tasks.filter((task) => task.status ===
|
|
329
|
+
const blockedWithoutBlocker = tasks.filter((task) => task.status === 'BLOCKED' && !task.blocker);
|
|
320
330
|
if (blockedWithoutBlocker.length > 0) {
|
|
321
331
|
suggestions.push({
|
|
322
332
|
code: TASK_LINT_CODES.BLOCKED_WITHOUT_BLOCKER,
|
|
323
333
|
message: `${blockedWithoutBlocker.length} BLOCKED task(s) have no blocker metadata.`,
|
|
324
|
-
fixHint:
|
|
334
|
+
fixHint: 'Add structured blocker metadata with type and description.',
|
|
325
335
|
});
|
|
326
336
|
}
|
|
327
337
|
const blockerTypeInvalid = tasks.filter((task) => task.blocker && !BLOCKER_TYPES.includes(task.blocker.type));
|
|
@@ -329,7 +339,7 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
329
339
|
suggestions.push({
|
|
330
340
|
code: TASK_LINT_CODES.BLOCKER_TYPE_INVALID,
|
|
331
341
|
message: `${blockerTypeInvalid.length} task(s) have invalid blocker type.`,
|
|
332
|
-
fixHint: `Use one of: ${BLOCKER_TYPES.join(
|
|
342
|
+
fixHint: `Use one of: ${BLOCKER_TYPES.join(', ')}.`,
|
|
333
343
|
});
|
|
334
344
|
}
|
|
335
345
|
const blockerDescriptionEmpty = tasks.filter((task) => task.blocker && !task.blocker.description?.trim());
|
|
@@ -337,18 +347,18 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
337
347
|
suggestions.push({
|
|
338
348
|
code: TASK_LINT_CODES.BLOCKER_DESCRIPTION_EMPTY,
|
|
339
349
|
message: `${blockerDescriptionEmpty.length} task(s) have empty blocker description.`,
|
|
340
|
-
fixHint:
|
|
350
|
+
fixHint: 'Provide a clear description of why the task is blocked.',
|
|
341
351
|
});
|
|
342
352
|
}
|
|
343
353
|
// ============================================================================
|
|
344
354
|
// Spec v1.1.0 - Sub-state Metadata Validation (Optional but Recommended)
|
|
345
355
|
// ============================================================================
|
|
346
|
-
const inProgressWithoutSubState = tasks.filter((task) => task.status ===
|
|
356
|
+
const inProgressWithoutSubState = tasks.filter((task) => task.status === 'IN_PROGRESS' && !task.subState);
|
|
347
357
|
if (inProgressWithoutSubState.length > 0) {
|
|
348
358
|
suggestions.push({
|
|
349
359
|
code: TASK_LINT_CODES.IN_PROGRESS_WITHOUT_SUBSTATE,
|
|
350
360
|
message: `${inProgressWithoutSubState.length} IN_PROGRESS task(s) have no subState metadata.`,
|
|
351
|
-
fixHint:
|
|
361
|
+
fixHint: 'Add optional subState metadata for better progress tracking.',
|
|
352
362
|
});
|
|
353
363
|
}
|
|
354
364
|
const subStatePhaseInvalid = tasks.filter((task) => task.subState?.phase && !SUB_STATE_PHASES.includes(task.subState.phase));
|
|
@@ -356,15 +366,15 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
356
366
|
suggestions.push({
|
|
357
367
|
code: TASK_LINT_CODES.SUBSTATE_PHASE_INVALID,
|
|
358
368
|
message: `${subStatePhaseInvalid.length} task(s) have invalid subState phase.`,
|
|
359
|
-
fixHint: `Use one of: ${SUB_STATE_PHASES.join(
|
|
369
|
+
fixHint: `Use one of: ${SUB_STATE_PHASES.join(', ')}.`,
|
|
360
370
|
});
|
|
361
371
|
}
|
|
362
|
-
const subStateConfidenceInvalid = tasks.filter((task) => typeof task.subState?.confidence ===
|
|
372
|
+
const subStateConfidenceInvalid = tasks.filter((task) => typeof task.subState?.confidence === 'number' && (task.subState.confidence < 0 || task.subState.confidence > 1));
|
|
363
373
|
if (subStateConfidenceInvalid.length > 0) {
|
|
364
374
|
suggestions.push({
|
|
365
375
|
code: TASK_LINT_CODES.SUBSTATE_CONFIDENCE_INVALID,
|
|
366
376
|
message: `${subStateConfidenceInvalid.length} task(s) have invalid confidence score.`,
|
|
367
|
-
fixHint:
|
|
377
|
+
fixHint: 'Confidence must be between 0.0 and 1.0.',
|
|
368
378
|
});
|
|
369
379
|
}
|
|
370
380
|
return suggestions;
|
|
@@ -374,18 +384,18 @@ export function collectTaskLintSuggestions(tasks) {
|
|
|
374
384
|
}
|
|
375
385
|
function collectSingleTaskLintSuggestions(task) {
|
|
376
386
|
const suggestions = [];
|
|
377
|
-
if (task.status ===
|
|
387
|
+
if (task.status === 'IN_PROGRESS' && task.owner.trim().length === 0) {
|
|
378
388
|
suggestions.push({
|
|
379
389
|
code: TASK_LINT_CODES.IN_PROGRESS_OWNER_EMPTY,
|
|
380
|
-
message:
|
|
381
|
-
fixHint:
|
|
390
|
+
message: 'Current task is IN_PROGRESS but owner is empty.',
|
|
391
|
+
fixHint: 'Set owner before continuing execution.',
|
|
382
392
|
});
|
|
383
393
|
}
|
|
384
|
-
if (task.status ===
|
|
394
|
+
if (task.status === 'DONE' && task.links.length === 0) {
|
|
385
395
|
suggestions.push({
|
|
386
396
|
code: TASK_LINT_CODES.DONE_LINKS_MISSING,
|
|
387
|
-
message:
|
|
388
|
-
fixHint:
|
|
397
|
+
message: 'Current task is DONE but has no links evidence.',
|
|
398
|
+
fixHint: 'Add at least one evidence link.',
|
|
389
399
|
});
|
|
390
400
|
}
|
|
391
401
|
const invalidLinkPathFormat = task.links.some((link) => {
|
|
@@ -395,77 +405,77 @@ function collectSingleTaskLintSuggestions(task) {
|
|
|
395
405
|
if (invalidLinkPathFormat) {
|
|
396
406
|
suggestions.push({
|
|
397
407
|
code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
|
|
398
|
-
message:
|
|
399
|
-
fixHint:
|
|
408
|
+
message: 'Current task has invalid links path format.',
|
|
409
|
+
fixHint: 'Use project-root-relative paths without leading slash (for example reports/task-0001.md) or http(s) URL.',
|
|
400
410
|
});
|
|
401
411
|
}
|
|
402
|
-
if (task.status ===
|
|
412
|
+
if (task.status === 'BLOCKED' && task.summary.trim().length === 0) {
|
|
403
413
|
suggestions.push({
|
|
404
414
|
code: TASK_LINT_CODES.BLOCKED_SUMMARY_EMPTY,
|
|
405
|
-
message:
|
|
406
|
-
fixHint:
|
|
415
|
+
message: 'Current task is BLOCKED but summary is empty.',
|
|
416
|
+
fixHint: 'Add blocker reason and unblock condition.',
|
|
407
417
|
});
|
|
408
418
|
}
|
|
409
419
|
if (!Number.isFinite(new Date(task.updatedAt).getTime())) {
|
|
410
420
|
suggestions.push({
|
|
411
421
|
code: TASK_LINT_CODES.UPDATED_AT_INVALID,
|
|
412
|
-
message:
|
|
413
|
-
fixHint:
|
|
422
|
+
message: 'Current task updatedAt is invalid.',
|
|
423
|
+
fixHint: 'Use ISO8601 UTC timestamp.',
|
|
414
424
|
});
|
|
415
425
|
}
|
|
416
426
|
if (task.roadmapRefs.length === 0) {
|
|
417
427
|
suggestions.push({
|
|
418
428
|
code: TASK_LINT_CODES.ROADMAP_REFS_EMPTY,
|
|
419
|
-
message:
|
|
420
|
-
fixHint:
|
|
429
|
+
message: 'Current task has empty roadmapRefs.',
|
|
430
|
+
fixHint: 'Bind ROADMAP-xxxx where applicable.',
|
|
421
431
|
});
|
|
422
432
|
}
|
|
423
433
|
// ============================================================================
|
|
424
434
|
// Spec v1.1.0 - Blocker Categorization Validation (Single Task)
|
|
425
435
|
// ============================================================================
|
|
426
|
-
if (task.status ===
|
|
436
|
+
if (task.status === 'BLOCKED' && !task.blocker) {
|
|
427
437
|
suggestions.push({
|
|
428
438
|
code: TASK_LINT_CODES.BLOCKED_WITHOUT_BLOCKER,
|
|
429
|
-
message:
|
|
430
|
-
fixHint:
|
|
439
|
+
message: 'Current task is BLOCKED but has no blocker metadata.',
|
|
440
|
+
fixHint: 'Add structured blocker metadata with type and description.',
|
|
431
441
|
});
|
|
432
442
|
}
|
|
433
443
|
if (task.blocker && !BLOCKER_TYPES.includes(task.blocker.type)) {
|
|
434
444
|
suggestions.push({
|
|
435
445
|
code: TASK_LINT_CODES.BLOCKER_TYPE_INVALID,
|
|
436
446
|
message: `Current task has invalid blocker type: ${task.blocker.type}.`,
|
|
437
|
-
fixHint: `Use one of: ${BLOCKER_TYPES.join(
|
|
447
|
+
fixHint: `Use one of: ${BLOCKER_TYPES.join(', ')}.`,
|
|
438
448
|
});
|
|
439
449
|
}
|
|
440
450
|
if (task.blocker && !task.blocker.description?.trim()) {
|
|
441
451
|
suggestions.push({
|
|
442
452
|
code: TASK_LINT_CODES.BLOCKER_DESCRIPTION_EMPTY,
|
|
443
|
-
message:
|
|
444
|
-
fixHint:
|
|
453
|
+
message: 'Current task has empty blocker description.',
|
|
454
|
+
fixHint: 'Provide a clear description of why the task is blocked.',
|
|
445
455
|
});
|
|
446
456
|
}
|
|
447
457
|
// ============================================================================
|
|
448
458
|
// Spec v1.1.0 - Sub-state Metadata Validation (Single Task, Optional)
|
|
449
459
|
// ============================================================================
|
|
450
|
-
if (task.status ===
|
|
460
|
+
if (task.status === 'IN_PROGRESS' && !task.subState) {
|
|
451
461
|
suggestions.push({
|
|
452
462
|
code: TASK_LINT_CODES.IN_PROGRESS_WITHOUT_SUBSTATE,
|
|
453
|
-
message:
|
|
454
|
-
fixHint:
|
|
463
|
+
message: 'Current task is IN_PROGRESS but has no subState metadata.',
|
|
464
|
+
fixHint: 'Add optional subState metadata for better progress tracking.',
|
|
455
465
|
});
|
|
456
466
|
}
|
|
457
467
|
if (task.subState?.phase && !SUB_STATE_PHASES.includes(task.subState.phase)) {
|
|
458
468
|
suggestions.push({
|
|
459
469
|
code: TASK_LINT_CODES.SUBSTATE_PHASE_INVALID,
|
|
460
470
|
message: `Current task has invalid subState phase: ${task.subState.phase}.`,
|
|
461
|
-
fixHint: `Use one of: ${SUB_STATE_PHASES.join(
|
|
471
|
+
fixHint: `Use one of: ${SUB_STATE_PHASES.join(', ')}.`,
|
|
462
472
|
});
|
|
463
473
|
}
|
|
464
|
-
if (typeof task.subState?.confidence ===
|
|
474
|
+
if (typeof task.subState?.confidence === 'number' && (task.subState.confidence < 0 || task.subState.confidence > 1)) {
|
|
465
475
|
suggestions.push({
|
|
466
476
|
code: TASK_LINT_CODES.SUBSTATE_CONFIDENCE_INVALID,
|
|
467
477
|
message: `Current task has invalid confidence score: ${task.subState.confidence}.`,
|
|
468
|
-
fixHint:
|
|
478
|
+
fixHint: 'Confidence must be between 0.0 and 1.0.',
|
|
469
479
|
});
|
|
470
480
|
}
|
|
471
481
|
return renderLintSuggestions(suggestions);
|
|
@@ -485,7 +495,7 @@ async function collectTaskFileLintSuggestions(governanceDir, task) {
|
|
|
485
495
|
suggestions.push({
|
|
486
496
|
code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
|
|
487
497
|
message: `Link path should be project-root-relative without leading slash: ${normalized}.`,
|
|
488
|
-
fixHint:
|
|
498
|
+
fixHint: 'Use path/from/project/root format.',
|
|
489
499
|
});
|
|
490
500
|
continue;
|
|
491
501
|
}
|
|
@@ -502,25 +512,25 @@ async function collectTaskFileLintSuggestions(governanceDir, task) {
|
|
|
502
512
|
}
|
|
503
513
|
export function renderTasksMarkdown(tasks) {
|
|
504
514
|
const sections = sortTasksNewestFirst(tasks).map((task) => {
|
|
505
|
-
const roadmapRefs = task.roadmapRefs.length > 0 ? task.roadmapRefs.join(
|
|
515
|
+
const roadmapRefs = task.roadmapRefs.length > 0 ? task.roadmapRefs.join(', ') : '(none)';
|
|
506
516
|
const links = task.links.length > 0
|
|
507
|
-
? [
|
|
508
|
-
: [
|
|
517
|
+
? ['- links:', ...task.links.map((link) => ` - ${link}`)]
|
|
518
|
+
: ['- links:', ' - (none)'];
|
|
509
519
|
const lines = [
|
|
510
520
|
`## ${task.id} | ${task.status} | ${task.title}`,
|
|
511
|
-
`- owner: ${task.owner ||
|
|
512
|
-
`- summary: ${task.summary ||
|
|
521
|
+
`- owner: ${task.owner || '(none)'}`,
|
|
522
|
+
`- summary: ${task.summary || '(none)'}`,
|
|
513
523
|
`- updatedAt: ${task.updatedAt}`,
|
|
514
524
|
`- roadmapRefs: ${roadmapRefs}`,
|
|
515
525
|
...links,
|
|
516
526
|
];
|
|
517
527
|
// Add subState for IN_PROGRESS tasks (Spec v1.1.0)
|
|
518
|
-
if (task.subState && task.status ===
|
|
519
|
-
lines.push(
|
|
528
|
+
if (task.subState && task.status === 'IN_PROGRESS') {
|
|
529
|
+
lines.push('- subState:');
|
|
520
530
|
if (task.subState.phase) {
|
|
521
531
|
lines.push(` - phase: ${task.subState.phase}`);
|
|
522
532
|
}
|
|
523
|
-
if (typeof task.subState.confidence ===
|
|
533
|
+
if (typeof task.subState.confidence === 'number') {
|
|
524
534
|
lines.push(` - confidence: ${task.subState.confidence}`);
|
|
525
535
|
}
|
|
526
536
|
if (task.subState.estimatedCompletion) {
|
|
@@ -528,8 +538,8 @@ export function renderTasksMarkdown(tasks) {
|
|
|
528
538
|
}
|
|
529
539
|
}
|
|
530
540
|
// Add blocker for BLOCKED tasks (Spec v1.1.0)
|
|
531
|
-
if (task.blocker && task.status ===
|
|
532
|
-
lines.push(
|
|
541
|
+
if (task.blocker && task.status === 'BLOCKED') {
|
|
542
|
+
lines.push('- blocker:');
|
|
533
543
|
lines.push(` - type: ${task.blocker.type}`);
|
|
534
544
|
lines.push(` - description: ${task.blocker.description}`);
|
|
535
545
|
if (task.blocker.blockingEntity) {
|
|
@@ -542,16 +552,20 @@ export function renderTasksMarkdown(tasks) {
|
|
|
542
552
|
lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
543
553
|
}
|
|
544
554
|
}
|
|
545
|
-
return lines.join(
|
|
555
|
+
return lines.join('\n');
|
|
546
556
|
});
|
|
547
557
|
return [
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
558
|
+
'# Tasks',
|
|
559
|
+
'',
|
|
560
|
+
'Projitive is an AI-first project governance framework for tasks, roadmaps, reports, and designs.',
|
|
561
|
+
'Author: yinxulai',
|
|
562
|
+
'Repository: https://github.com/yinxulai/projitive',
|
|
563
|
+
'Do not edit this file manually. This file is automatically generated by Projitive.',
|
|
564
|
+
'This file is generated from .projitive governance store by Projitive MCP. Manual edits will be overwritten.',
|
|
565
|
+
'',
|
|
566
|
+
...(sections.length > 0 ? sections : ['(no tasks)']),
|
|
567
|
+
'',
|
|
568
|
+
].join('\n');
|
|
555
569
|
}
|
|
556
570
|
export async function ensureTasksFile(inputPath) {
|
|
557
571
|
const governanceDir = await resolveGovernanceDir(inputPath);
|
|
@@ -588,26 +602,27 @@ export function validateTransition(from, to) {
|
|
|
588
602
|
return true;
|
|
589
603
|
}
|
|
590
604
|
const allowed = {
|
|
591
|
-
TODO: new Set([
|
|
592
|
-
IN_PROGRESS: new Set([
|
|
593
|
-
BLOCKED: new Set([
|
|
605
|
+
TODO: new Set(['IN_PROGRESS', 'BLOCKED']),
|
|
606
|
+
IN_PROGRESS: new Set(['BLOCKED', 'DONE']),
|
|
607
|
+
BLOCKED: new Set(['IN_PROGRESS', 'TODO']),
|
|
594
608
|
DONE: new Set(),
|
|
595
609
|
};
|
|
596
610
|
return allowed[from].has(to);
|
|
597
611
|
}
|
|
598
612
|
export function registerTaskTools(server) {
|
|
599
|
-
server.registerTool(
|
|
600
|
-
title:
|
|
601
|
-
description:
|
|
613
|
+
server.registerTool('taskList', {
|
|
614
|
+
title: 'Task List',
|
|
615
|
+
description: 'List tasks for a known project and optionally filter by status',
|
|
602
616
|
inputSchema: {
|
|
603
617
|
projectPath: z.string(),
|
|
604
|
-
status: z.enum([
|
|
618
|
+
status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
|
|
605
619
|
limit: z.number().int().min(1).max(200).optional(),
|
|
606
620
|
},
|
|
607
621
|
}, async ({ projectPath, status, limit }) => {
|
|
608
622
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
609
623
|
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
610
|
-
const { tasks } = await loadTasksDocument(governanceDir);
|
|
624
|
+
const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
|
|
625
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
611
626
|
const filtered = tasks
|
|
612
627
|
.filter((task) => (status ? task.status === status : true))
|
|
613
628
|
.slice(0, limit ?? 100);
|
|
@@ -617,52 +632,54 @@ export function registerTaskTools(server) {
|
|
|
617
632
|
{
|
|
618
633
|
code: TASK_LINT_CODES.FILTER_EMPTY,
|
|
619
634
|
message: `No tasks matched status=${status}.`,
|
|
620
|
-
fixHint:
|
|
635
|
+
fixHint: 'Confirm status values or update task states.',
|
|
621
636
|
},
|
|
622
637
|
]);
|
|
623
638
|
}
|
|
624
639
|
const nextTaskId = filtered[0]?.id;
|
|
625
640
|
const markdown = renderToolResponseMarkdown({
|
|
626
|
-
toolName:
|
|
641
|
+
toolName: 'taskList',
|
|
627
642
|
sections: [
|
|
628
643
|
summarySection([
|
|
629
644
|
`- projectPath: ${normalizedProjectPath}`,
|
|
630
645
|
`- governanceDir: ${governanceDir}`,
|
|
631
|
-
`-
|
|
646
|
+
`- tasksView: ${tasksViewPath}`,
|
|
647
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
648
|
+
`- filter.status: ${status ?? '(none)'}`,
|
|
632
649
|
`- returned: ${filtered.length}`,
|
|
633
650
|
]),
|
|
634
651
|
evidenceSection([
|
|
635
|
-
|
|
636
|
-
...filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner ||
|
|
652
|
+
'- tasks:',
|
|
653
|
+
...filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner || ''} | updatedAt=${task.updatedAt}`),
|
|
637
654
|
]),
|
|
638
|
-
guidanceSection([
|
|
655
|
+
guidanceSection(['- Pick one task ID and call `taskContext`.']),
|
|
639
656
|
lintSection(lintSuggestions),
|
|
640
657
|
nextCallSection(nextTaskId
|
|
641
|
-
? `taskContext(projectPath
|
|
658
|
+
? `taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${nextTaskId}")`
|
|
642
659
|
: undefined),
|
|
643
660
|
],
|
|
644
661
|
});
|
|
645
662
|
return asText(markdown);
|
|
646
663
|
});
|
|
647
|
-
server.registerTool(
|
|
648
|
-
title:
|
|
649
|
-
description:
|
|
664
|
+
server.registerTool('taskCreate', {
|
|
665
|
+
title: 'Task Create',
|
|
666
|
+
description: 'Create a new task in governance store with stable TASK-<number> ID',
|
|
650
667
|
inputSchema: {
|
|
651
668
|
projectPath: z.string(),
|
|
652
|
-
taskId: z.string(),
|
|
669
|
+
taskId: z.string().optional(),
|
|
653
670
|
title: z.string(),
|
|
654
|
-
status: z.enum([
|
|
671
|
+
status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
|
|
655
672
|
owner: z.string().optional(),
|
|
656
673
|
summary: z.string().optional(),
|
|
657
674
|
roadmapRefs: z.array(z.string()).optional(),
|
|
658
675
|
links: z.array(z.string()).optional(),
|
|
659
676
|
subState: z.object({
|
|
660
|
-
phase: z.enum([
|
|
677
|
+
phase: z.enum(['discovery', 'design', 'implementation', 'testing']).optional(),
|
|
661
678
|
confidence: z.number().min(0).max(1).optional(),
|
|
662
679
|
estimatedCompletion: z.string().optional(),
|
|
663
680
|
}).optional(),
|
|
664
681
|
blocker: z.object({
|
|
665
|
-
type: z.enum([
|
|
682
|
+
type: z.enum(['internal_dependency', 'external_dependency', 'resource', 'approval']),
|
|
666
683
|
description: z.string(),
|
|
667
684
|
blockingEntity: z.string().optional(),
|
|
668
685
|
unblockCondition: z.string().optional(),
|
|
@@ -670,26 +687,28 @@ export function registerTaskTools(server) {
|
|
|
670
687
|
}).optional(),
|
|
671
688
|
},
|
|
672
689
|
}, async ({ projectPath, taskId, title, status, owner, summary, roadmapRefs, links, subState, blocker }) => {
|
|
673
|
-
if (!isValidTaskId(taskId)) {
|
|
690
|
+
if (taskId && !isValidTaskId(taskId)) {
|
|
674
691
|
return {
|
|
675
|
-
...asText(renderErrorMarkdown(
|
|
692
|
+
...asText(renderErrorMarkdown('taskCreate', `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")`)),
|
|
676
693
|
isError: true,
|
|
677
694
|
};
|
|
678
695
|
}
|
|
679
696
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
680
697
|
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
681
|
-
const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
|
|
682
|
-
const
|
|
698
|
+
const { tasksPath, tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
|
|
699
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
700
|
+
const finalTaskId = taskId ?? nextTaskId(tasks);
|
|
701
|
+
const duplicated = tasks.some((item) => item.id === finalTaskId);
|
|
683
702
|
if (duplicated) {
|
|
684
703
|
return {
|
|
685
|
-
...asText(renderErrorMarkdown(
|
|
704
|
+
...asText(renderErrorMarkdown('taskCreate', `Task already exists: ${finalTaskId}`, ['task IDs must be unique', 'use taskUpdate for existing tasks'], `taskUpdate(projectPath="${normalizedProjectPath}", taskId="${finalTaskId}", updates={...})`)),
|
|
686
705
|
isError: true,
|
|
687
706
|
};
|
|
688
707
|
}
|
|
689
708
|
const createdTask = normalizeTask({
|
|
690
|
-
id:
|
|
709
|
+
id: finalTaskId,
|
|
691
710
|
title,
|
|
692
|
-
status: status ??
|
|
711
|
+
status: status ?? 'TODO',
|
|
693
712
|
owner,
|
|
694
713
|
summary,
|
|
695
714
|
roadmapRefs,
|
|
@@ -705,36 +724,38 @@ export function registerTaskTools(server) {
|
|
|
705
724
|
...(await collectTaskFileLintSuggestions(governanceDir, createdTask)),
|
|
706
725
|
];
|
|
707
726
|
const markdown = renderToolResponseMarkdown({
|
|
708
|
-
toolName:
|
|
727
|
+
toolName: 'taskCreate',
|
|
709
728
|
sections: [
|
|
710
729
|
summarySection([
|
|
711
730
|
`- projectPath: ${normalizedProjectPath}`,
|
|
712
731
|
`- governanceDir: ${governanceDir}`,
|
|
732
|
+
`- tasksView: ${tasksViewPath}`,
|
|
733
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
713
734
|
`- taskId: ${createdTask.id}`,
|
|
714
735
|
`- status: ${createdTask.status}`,
|
|
715
|
-
`- owner: ${createdTask.owner ||
|
|
736
|
+
`- owner: ${createdTask.owner || '(none)'}`,
|
|
716
737
|
`- updatedAt: ${createdTask.updatedAt}`,
|
|
717
738
|
]),
|
|
718
739
|
evidenceSection([
|
|
719
|
-
|
|
740
|
+
'### Created Task',
|
|
720
741
|
`- ${createdTask.id} | ${createdTask.status} | ${createdTask.title}`,
|
|
721
|
-
`- summary: ${createdTask.summary ||
|
|
722
|
-
`- roadmapRefs: ${createdTask.roadmapRefs.join(
|
|
723
|
-
`- links: ${createdTask.links.join(
|
|
742
|
+
`- summary: ${createdTask.summary || '(none)'}`,
|
|
743
|
+
`- roadmapRefs: ${createdTask.roadmapRefs.join(', ') || '(none)'}`,
|
|
744
|
+
`- links: ${createdTask.links.join(', ') || '(none)'}`,
|
|
724
745
|
]),
|
|
725
746
|
guidanceSection([
|
|
726
|
-
|
|
727
|
-
|
|
747
|
+
'Task created in governance store successfully and tasks.md has been synced.',
|
|
748
|
+
'Run taskContext to verify references and lint guidance.',
|
|
728
749
|
]),
|
|
729
750
|
lintSection(lintSuggestions),
|
|
730
|
-
nextCallSection(`taskContext(projectPath
|
|
751
|
+
nextCallSection(`taskContext(projectPath="${normalizedProjectPath}", taskId="${createdTask.id}")`),
|
|
731
752
|
],
|
|
732
753
|
});
|
|
733
754
|
return asText(markdown);
|
|
734
755
|
});
|
|
735
|
-
server.registerTool(
|
|
736
|
-
title:
|
|
737
|
-
description:
|
|
756
|
+
server.registerTool('taskNext', {
|
|
757
|
+
title: 'Task Next',
|
|
758
|
+
description: 'Start here to auto-select the highest-priority actionable task',
|
|
738
759
|
inputSchema: {
|
|
739
760
|
limit: z.number().int().min(1).max(20).optional(),
|
|
740
761
|
},
|
|
@@ -745,7 +766,7 @@ export function registerTaskTools(server) {
|
|
|
745
766
|
const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
|
|
746
767
|
if (rankedCandidates.length === 0) {
|
|
747
768
|
const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
|
|
748
|
-
const tasksPath = path.join(governanceDir,
|
|
769
|
+
const tasksPath = path.join(governanceDir, '.projitive');
|
|
749
770
|
await ensureStore(tasksPath);
|
|
750
771
|
const stats = await loadTaskStatusStatsFromStore(tasksPath);
|
|
751
772
|
const roadmapIds = await readRoadmapIds(governanceDir);
|
|
@@ -760,47 +781,47 @@ export function registerTaskTools(server) {
|
|
|
760
781
|
};
|
|
761
782
|
}));
|
|
762
783
|
const preferredProject = projectSnapshots[0];
|
|
763
|
-
const preferredRoadmapRef = preferredProject?.roadmapIds[0] ??
|
|
784
|
+
const preferredRoadmapRef = preferredProject?.roadmapIds[0] ?? 'ROADMAP-0001';
|
|
764
785
|
const noTaskDiscoveryGuidance = await resolveNoTaskDiscoveryGuidance(preferredProject?.governanceDir);
|
|
765
786
|
const markdown = renderToolResponseMarkdown({
|
|
766
|
-
toolName:
|
|
787
|
+
toolName: 'taskNext',
|
|
767
788
|
sections: [
|
|
768
789
|
summarySection([
|
|
769
|
-
`- rootPaths: ${roots.join(
|
|
790
|
+
`- rootPaths: ${roots.join(', ')}`,
|
|
770
791
|
`- rootCount: ${roots.length}`,
|
|
771
792
|
`- maxDepth: ${depth}`,
|
|
772
793
|
`- matchedProjects: ${projects.length}`,
|
|
773
|
-
|
|
794
|
+
'- actionableTasks: 0',
|
|
774
795
|
]),
|
|
775
796
|
evidenceSection([
|
|
776
|
-
|
|
797
|
+
'### Project Snapshots',
|
|
777
798
|
...(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(
|
|
779
|
-
: [
|
|
780
|
-
|
|
781
|
-
|
|
799
|
+
? 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)'}`)
|
|
800
|
+
: ['- (none)']),
|
|
801
|
+
'',
|
|
802
|
+
'### Seed Task Template',
|
|
782
803
|
...renderTaskSeedTemplate(preferredRoadmapRef),
|
|
783
804
|
]),
|
|
784
805
|
guidanceSection([
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
806
|
+
'- No TODO/IN_PROGRESS task is available.',
|
|
807
|
+
'- Create 1-3 new TODO tasks using `taskCreate(...)` from active roadmap slices.',
|
|
808
|
+
'- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.',
|
|
809
|
+
'- If roadmap has active milestones, analyze milestone intent and split into 1-3 executable TODO tasks.',
|
|
810
|
+
'',
|
|
811
|
+
'### No-Task Discovery Checklist',
|
|
791
812
|
...noTaskDiscoveryGuidance,
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
813
|
+
'',
|
|
814
|
+
'- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.',
|
|
815
|
+
'- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.',
|
|
816
|
+
'- After creating tasks, rerun `taskNext` to re-rank actionable work.',
|
|
796
817
|
]),
|
|
797
818
|
lintSection([
|
|
798
|
-
|
|
799
|
-
|
|
819
|
+
'- No actionable tasks found. Verify task statuses and required fields in .projitive task table.',
|
|
820
|
+
'- Ensure each new task has stable TASK-<number> ID and at least one roadmapRefs item.',
|
|
800
821
|
]),
|
|
801
822
|
nextCallSection(preferredProject
|
|
802
|
-
? `taskCreate(projectPath
|
|
803
|
-
:
|
|
823
|
+
? `taskCreate(projectPath="${toProjectPath(preferredProject.governanceDir)}", title="Create first executable slice", roadmapRefs=["${preferredRoadmapRef}"], summary="Derived from active roadmap milestone")`
|
|
824
|
+
: 'projectScan()'),
|
|
804
825
|
],
|
|
805
826
|
});
|
|
806
827
|
return asText(markdown);
|
|
@@ -816,10 +837,10 @@ export function registerTaskTools(server) {
|
|
|
816
837
|
const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
|
|
817
838
|
const candidateLimit = limit ?? 5;
|
|
818
839
|
const markdown = renderToolResponseMarkdown({
|
|
819
|
-
toolName:
|
|
840
|
+
toolName: 'taskNext',
|
|
820
841
|
sections: [
|
|
821
842
|
summarySection([
|
|
822
|
-
`- rootPaths: ${roots.join(
|
|
843
|
+
`- rootPaths: ${roots.join(', ')}`,
|
|
823
844
|
`- rootCount: ${roots.length}`,
|
|
824
845
|
`- maxDepth: ${depth}`,
|
|
825
846
|
`- matchedProjects: ${projects.length}`,
|
|
@@ -829,48 +850,48 @@ export function registerTaskTools(server) {
|
|
|
829
850
|
`- selectedTaskStatus: ${selected.task.status}`,
|
|
830
851
|
]),
|
|
831
852
|
evidenceSection([
|
|
832
|
-
|
|
853
|
+
'### Selected Task',
|
|
833
854
|
`- id: ${selected.task.id}`,
|
|
834
855
|
`- title: ${selected.task.title}`,
|
|
835
|
-
`- owner: ${selected.task.owner ||
|
|
856
|
+
`- owner: ${selected.task.owner || '(none)'}`,
|
|
836
857
|
`- updatedAt: ${selected.task.updatedAt}`,
|
|
837
|
-
`- roadmapRefs: ${selected.task.roadmapRefs.join(
|
|
858
|
+
`- roadmapRefs: ${selected.task.roadmapRefs.join(', ') || '(none)'}`,
|
|
838
859
|
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selectedTaskDocument.markdownPath}`,
|
|
839
|
-
|
|
840
|
-
|
|
860
|
+
'',
|
|
861
|
+
'### Top Candidates',
|
|
841
862
|
...rankedCandidates
|
|
842
863
|
.slice(0, candidateLimit)
|
|
843
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}`),
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
865
|
+
'',
|
|
866
|
+
'### Selection Reason',
|
|
867
|
+
'- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.',
|
|
847
868
|
`- Selected candidate scores: projectScore=${selected.projectScore}, taskPriority=${selected.taskPriority}, taskUpdatedAtMs=${selected.taskUpdatedAtMs}.`,
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : [
|
|
851
|
-
|
|
852
|
-
|
|
869
|
+
'',
|
|
870
|
+
'### Related Artifacts',
|
|
871
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
|
|
872
|
+
'',
|
|
873
|
+
'### Reference Locations',
|
|
853
874
|
...(referenceLocations.length > 0
|
|
854
875
|
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
855
|
-
: [
|
|
856
|
-
|
|
857
|
-
|
|
876
|
+
: ['- (none)']),
|
|
877
|
+
'',
|
|
878
|
+
'### Suggested Read Order',
|
|
858
879
|
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
859
880
|
]),
|
|
860
881
|
guidanceSection([
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
882
|
+
'- Start immediately with Suggested Read Order and execute the selected task.',
|
|
883
|
+
'- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.',
|
|
884
|
+
'- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.',
|
|
864
885
|
]),
|
|
865
886
|
lintSection(lintSuggestions),
|
|
866
|
-
nextCallSection(`taskContext(projectPath
|
|
887
|
+
nextCallSection(`taskContext(projectPath="${toProjectPath(selected.governanceDir)}", taskId="${selected.task.id}")`),
|
|
867
888
|
],
|
|
868
889
|
});
|
|
869
890
|
return asText(markdown);
|
|
870
891
|
});
|
|
871
|
-
server.registerTool(
|
|
872
|
-
title:
|
|
873
|
-
description:
|
|
892
|
+
server.registerTool('taskContext', {
|
|
893
|
+
title: 'Task Context',
|
|
894
|
+
description: 'Get deep context, evidence links, and read order for one task',
|
|
874
895
|
inputSchema: {
|
|
875
896
|
projectPath: z.string(),
|
|
876
897
|
taskId: z.string(),
|
|
@@ -878,17 +899,18 @@ export function registerTaskTools(server) {
|
|
|
878
899
|
}, async ({ projectPath, taskId }) => {
|
|
879
900
|
if (!isValidTaskId(taskId)) {
|
|
880
901
|
return {
|
|
881
|
-
...asText(renderErrorMarkdown(
|
|
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")`)),
|
|
882
903
|
isError: true,
|
|
883
904
|
};
|
|
884
905
|
}
|
|
885
906
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
886
907
|
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
887
|
-
const { markdownPath, tasks
|
|
908
|
+
const { markdownPath, tasks } = await loadTasksDocument(governanceDir);
|
|
909
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
888
910
|
const task = tasks.find((item) => item.id === taskId);
|
|
889
911
|
if (!task) {
|
|
890
912
|
return {
|
|
891
|
-
...asText(renderErrorMarkdown(
|
|
913
|
+
...asText(renderErrorMarkdown('taskContext', `Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`)),
|
|
892
914
|
isError: true,
|
|
893
915
|
};
|
|
894
916
|
}
|
|
@@ -907,21 +929,23 @@ export function registerTaskTools(server) {
|
|
|
907
929
|
const summaryLines = [
|
|
908
930
|
`- projectPath: ${normalizedProjectPath}`,
|
|
909
931
|
`- governanceDir: ${governanceDir}`,
|
|
932
|
+
`- tasksView: ${markdownPath}`,
|
|
933
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
910
934
|
`- taskId: ${task.id}`,
|
|
911
935
|
`- title: ${task.title}`,
|
|
912
936
|
`- status: ${task.status}`,
|
|
913
937
|
`- owner: ${task.owner}`,
|
|
914
938
|
`- updatedAt: ${task.updatedAt}`,
|
|
915
|
-
`- roadmapRefs: ${task.roadmapRefs.join(
|
|
939
|
+
`- roadmapRefs: ${task.roadmapRefs.join(', ') || '(none)'}`,
|
|
916
940
|
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
|
|
917
941
|
];
|
|
918
942
|
// Add subState info for IN_PROGRESS tasks (v1.1.0)
|
|
919
|
-
if (task.subState && task.status ===
|
|
920
|
-
summaryLines.push(
|
|
943
|
+
if (task.subState && task.status === 'IN_PROGRESS') {
|
|
944
|
+
summaryLines.push('- subState:');
|
|
921
945
|
if (task.subState.phase) {
|
|
922
946
|
summaryLines.push(` - phase: ${task.subState.phase}`);
|
|
923
947
|
}
|
|
924
|
-
if (typeof task.subState.confidence ===
|
|
948
|
+
if (typeof task.subState.confidence === 'number') {
|
|
925
949
|
summaryLines.push(` - confidence: ${task.subState.confidence}`);
|
|
926
950
|
}
|
|
927
951
|
if (task.subState.estimatedCompletion) {
|
|
@@ -929,8 +953,8 @@ export function registerTaskTools(server) {
|
|
|
929
953
|
}
|
|
930
954
|
}
|
|
931
955
|
// Add blocker info for BLOCKED tasks (v1.1.0)
|
|
932
|
-
if (task.blocker && task.status ===
|
|
933
|
-
summaryLines.push(
|
|
956
|
+
if (task.blocker && task.status === 'BLOCKED') {
|
|
957
|
+
summaryLines.push('- blocker:');
|
|
934
958
|
summaryLines.push(` - type: ${task.blocker.type}`);
|
|
935
959
|
summaryLines.push(` - description: ${task.blocker.description}`);
|
|
936
960
|
if (task.blocker.blockingEntity) {
|
|
@@ -944,58 +968,58 @@ export function registerTaskTools(server) {
|
|
|
944
968
|
}
|
|
945
969
|
}
|
|
946
970
|
const coreMarkdown = renderToolResponseMarkdown({
|
|
947
|
-
toolName:
|
|
971
|
+
toolName: 'taskContext',
|
|
948
972
|
sections: [
|
|
949
973
|
summarySection(summaryLines),
|
|
950
974
|
evidenceSection([
|
|
951
|
-
|
|
952
|
-
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : [
|
|
953
|
-
|
|
954
|
-
|
|
975
|
+
'### Related Artifacts',
|
|
976
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
|
|
977
|
+
'',
|
|
978
|
+
'### Reference Locations',
|
|
955
979
|
...(referenceLocations.length > 0
|
|
956
980
|
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
957
|
-
: [
|
|
958
|
-
|
|
959
|
-
|
|
981
|
+
: ['- (none)']),
|
|
982
|
+
'',
|
|
983
|
+
'### Suggested Read Order',
|
|
960
984
|
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
961
985
|
]),
|
|
962
986
|
guidanceSection([
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
987
|
+
'- Read the files in Suggested Read Order.',
|
|
988
|
+
'',
|
|
989
|
+
'### Recommended Context Reading',
|
|
966
990
|
...contextReadingGuidance,
|
|
967
|
-
|
|
968
|
-
|
|
991
|
+
'',
|
|
992
|
+
'- Verify whether current status and evidence are consistent.',
|
|
969
993
|
...taskStatusGuidance(task),
|
|
970
|
-
|
|
971
|
-
|
|
994
|
+
'- If updates are needed, use tool writes for governance store (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.',
|
|
995
|
+
'- After editing, re-run `taskContext` to verify references and context consistency.',
|
|
972
996
|
]),
|
|
973
997
|
lintSection(lintSuggestions),
|
|
974
|
-
nextCallSection(`taskContext(projectPath
|
|
998
|
+
nextCallSection(`taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${task.id}")`),
|
|
975
999
|
],
|
|
976
1000
|
});
|
|
977
1001
|
return asText(coreMarkdown);
|
|
978
1002
|
});
|
|
979
1003
|
// taskUpdate tool - Update task fields including subState and blocker (Spec v1.1.0)
|
|
980
|
-
server.registerTool(
|
|
981
|
-
title:
|
|
982
|
-
description:
|
|
1004
|
+
server.registerTool('taskUpdate', {
|
|
1005
|
+
title: 'Task Update',
|
|
1006
|
+
description: 'Update task fields including status, owner, summary, subState, and blocker metadata',
|
|
983
1007
|
inputSchema: {
|
|
984
1008
|
projectPath: z.string(),
|
|
985
1009
|
taskId: z.string(),
|
|
986
1010
|
updates: z.object({
|
|
987
|
-
status: z.enum([
|
|
1011
|
+
status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
|
|
988
1012
|
owner: z.string().optional(),
|
|
989
1013
|
summary: z.string().optional(),
|
|
990
1014
|
roadmapRefs: z.array(z.string()).optional(),
|
|
991
1015
|
links: z.array(z.string()).optional(),
|
|
992
1016
|
subState: z.object({
|
|
993
|
-
phase: z.enum([
|
|
1017
|
+
phase: z.enum(['discovery', 'design', 'implementation', 'testing']).optional(),
|
|
994
1018
|
confidence: z.number().min(0).max(1).optional(),
|
|
995
1019
|
estimatedCompletion: z.string().optional(),
|
|
996
1020
|
}).optional(),
|
|
997
1021
|
blocker: z.object({
|
|
998
|
-
type: z.enum([
|
|
1022
|
+
type: z.enum(['internal_dependency', 'external_dependency', 'resource', 'approval']),
|
|
999
1023
|
description: z.string(),
|
|
1000
1024
|
blockingEntity: z.string().optional(),
|
|
1001
1025
|
unblockCondition: z.string().optional(),
|
|
@@ -1006,17 +1030,19 @@ export function registerTaskTools(server) {
|
|
|
1006
1030
|
}, async ({ projectPath, taskId, updates }) => {
|
|
1007
1031
|
if (!isValidTaskId(taskId)) {
|
|
1008
1032
|
return {
|
|
1009
|
-
...asText(renderErrorMarkdown(
|
|
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={...})`)),
|
|
1010
1034
|
isError: true,
|
|
1011
1035
|
};
|
|
1012
1036
|
}
|
|
1013
1037
|
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1014
1038
|
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
1015
1039
|
const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
|
|
1040
|
+
const tasksViewPath = path.join(governanceDir, TASKS_MARKDOWN_FILE);
|
|
1041
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
1016
1042
|
const taskIndex = tasks.findIndex((item) => item.id === taskId);
|
|
1017
1043
|
if (taskIndex === -1) {
|
|
1018
1044
|
return {
|
|
1019
|
-
...asText(renderErrorMarkdown(
|
|
1045
|
+
...asText(renderErrorMarkdown('taskUpdate', `Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`)),
|
|
1020
1046
|
isError: true,
|
|
1021
1047
|
};
|
|
1022
1048
|
}
|
|
@@ -1025,7 +1051,7 @@ export function registerTaskTools(server) {
|
|
|
1025
1051
|
// Validate status transition
|
|
1026
1052
|
if (updates.status && !validateTransition(originalStatus, updates.status)) {
|
|
1027
1053
|
return {
|
|
1028
|
-
...asText(renderErrorMarkdown(
|
|
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}")`)),
|
|
1029
1055
|
isError: true,
|
|
1030
1056
|
};
|
|
1031
1057
|
}
|
|
@@ -1079,22 +1105,24 @@ export function registerTaskTools(server) {
|
|
|
1079
1105
|
const updateSummary = [
|
|
1080
1106
|
`- projectPath: ${normalizedProjectPath}`,
|
|
1081
1107
|
`- governanceDir: ${governanceDir}`,
|
|
1108
|
+
`- tasksView: ${tasksViewPath}`,
|
|
1109
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
1082
1110
|
`- taskId: ${taskId}`,
|
|
1083
1111
|
`- originalStatus: ${originalStatus}`,
|
|
1084
1112
|
`- newStatus: ${task.status}`,
|
|
1085
1113
|
`- updatedAt: ${task.updatedAt}`,
|
|
1086
1114
|
];
|
|
1087
1115
|
if (task.subState) {
|
|
1088
|
-
updateSummary.push(
|
|
1116
|
+
updateSummary.push('- subState:');
|
|
1089
1117
|
if (task.subState.phase)
|
|
1090
1118
|
updateSummary.push(` - phase: ${task.subState.phase}`);
|
|
1091
|
-
if (typeof task.subState.confidence ===
|
|
1119
|
+
if (typeof task.subState.confidence === 'number')
|
|
1092
1120
|
updateSummary.push(` - confidence: ${task.subState.confidence}`);
|
|
1093
1121
|
if (task.subState.estimatedCompletion)
|
|
1094
1122
|
updateSummary.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
1095
1123
|
}
|
|
1096
1124
|
if (task.blocker) {
|
|
1097
|
-
updateSummary.push(
|
|
1125
|
+
updateSummary.push('- blocker:');
|
|
1098
1126
|
updateSummary.push(` - type: ${task.blocker.type}`);
|
|
1099
1127
|
updateSummary.push(` - description: ${task.blocker.description}`);
|
|
1100
1128
|
if (task.blocker.blockingEntity)
|
|
@@ -1105,32 +1133,32 @@ export function registerTaskTools(server) {
|
|
|
1105
1133
|
updateSummary.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
1106
1134
|
}
|
|
1107
1135
|
const markdown = renderToolResponseMarkdown({
|
|
1108
|
-
toolName:
|
|
1136
|
+
toolName: 'taskUpdate',
|
|
1109
1137
|
sections: [
|
|
1110
1138
|
summarySection(updateSummary),
|
|
1111
1139
|
evidenceSection([
|
|
1112
|
-
|
|
1140
|
+
'### Updated Task',
|
|
1113
1141
|
`- ${task.id} | ${task.status} | ${task.title}`,
|
|
1114
|
-
`- owner: ${task.owner ||
|
|
1115
|
-
`- summary: ${task.summary ||
|
|
1116
|
-
|
|
1117
|
-
|
|
1142
|
+
`- owner: ${task.owner || '(none)'}`,
|
|
1143
|
+
`- summary: ${task.summary || '(none)'}`,
|
|
1144
|
+
'',
|
|
1145
|
+
'### Update Details',
|
|
1118
1146
|
...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
|
|
1119
1147
|
...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
|
|
1120
1148
|
...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
|
|
1121
|
-
...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(
|
|
1122
|
-
...(updates.links ? [`- links: ${updates.links.join(
|
|
1149
|
+
...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(', ')}`] : []),
|
|
1150
|
+
...(updates.links ? [`- links: ${updates.links.join(', ')}`] : []),
|
|
1123
1151
|
...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
|
|
1124
1152
|
...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
|
|
1125
1153
|
]),
|
|
1126
1154
|
guidanceSection([
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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.',
|
|
1131
1159
|
]),
|
|
1132
1160
|
lintSection([]),
|
|
1133
|
-
nextCallSection(`taskContext(projectPath
|
|
1161
|
+
nextCallSection(`taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${taskId}")`),
|
|
1134
1162
|
],
|
|
1135
1163
|
});
|
|
1136
1164
|
return asText(markdown);
|