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