@projitive/mcp 1.0.8 → 1.1.2
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 +4 -1
- package/output/source/{helpers/catch → common}/catch.js +6 -6
- package/output/source/{helpers/catch → common}/catch.test.js +6 -6
- package/output/source/common/errors.js +120 -0
- package/output/source/{helpers/files → common}/files.js +1 -1
- package/output/source/{helpers/files → common}/files.test.js +1 -1
- package/output/source/common/index.js +9 -0
- package/output/source/{helpers/linter/codes.js → common/linter.js} +13 -0
- package/output/source/{helpers/markdown → common}/markdown.test.js +1 -1
- package/output/source/{helpers/response → common}/response.test.js +1 -1
- package/output/source/common/types.js +7 -0
- package/output/source/common/utils.js +39 -0
- package/output/source/index.js +8 -196
- package/output/source/index.test.js +110 -0
- package/output/source/prompts/index.js +9 -0
- package/output/source/prompts/quickStart.js +94 -0
- package/output/source/prompts/taskDiscovery.js +190 -0
- package/output/source/prompts/taskExecution.js +161 -0
- package/output/source/resources/designs.js +108 -0
- package/output/source/resources/designs.test.js +154 -0
- package/output/source/resources/governance.js +40 -0
- package/output/source/resources/index.js +6 -0
- package/output/source/resources/readme.test.js +167 -0
- package/output/source/{reports.js → resources/reports.js} +5 -3
- package/output/source/resources/reports.test.js +149 -0
- package/output/source/tools/index.js +8 -0
- package/output/source/{projitive.js → tools/project.js} +4 -6
- package/output/source/tools/project.test.js +322 -0
- package/output/source/{roadmap.js → tools/roadmap.js} +4 -7
- package/output/source/tools/roadmap.test.js +103 -0
- package/output/source/{tasks.js → tools/task.js} +470 -23
- package/output/source/tools/task.test.js +473 -0
- package/output/source/types.js +56 -0
- package/package.json +4 -1
- package/output/source/design-context.js +0 -515
- package/output/source/designs.js +0 -38
- package/output/source/helpers/artifacts/index.js +0 -1
- package/output/source/helpers/catch/index.js +0 -1
- package/output/source/helpers/files/index.js +0 -1
- package/output/source/helpers/index.js +0 -6
- package/output/source/helpers/linter/index.js +0 -2
- package/output/source/helpers/linter/linter.js +0 -6
- package/output/source/helpers/markdown/index.js +0 -1
- package/output/source/helpers/response/index.js +0 -1
- package/output/source/projitive.test.js +0 -111
- package/output/source/roadmap.test.js +0 -11
- package/output/source/tasks.test.js +0 -152
- /package/output/source/{helpers/artifacts → common}/artifacts.js +0 -0
- /package/output/source/{helpers/artifacts → common}/artifacts.test.js +0 -0
- /package/output/source/{helpers/linter → common}/linter.test.js +0 -0
- /package/output/source/{helpers/markdown → common}/markdown.js +0 -0
- /package/output/source/{helpers/response → common}/response.js +0 -0
- /package/output/source/{readme.js → resources/readme.js} +0 -0
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { candidateFilesFromArtifacts } from "
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { catchIt } from "./helpers/catch/index.js";
|
|
9
|
-
import { TASK_LINT_CODES, renderLintSuggestions } from "./helpers/linter/index.js";
|
|
10
|
-
import { resolveGovernanceDir, resolveScanDepth, resolveScanRoot, discoverProjects, toProjectPath } from "./projitive.js";
|
|
4
|
+
import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, findTextReferences } from "../common/index.js";
|
|
5
|
+
import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "../common/index.js";
|
|
6
|
+
import { catchIt, TASK_LINT_CODES, renderLintSuggestions } from "../common/index.js";
|
|
7
|
+
import { resolveGovernanceDir, resolveScanDepth, resolveScanRoot, discoverProjects, toProjectPath } from "./project.js";
|
|
11
8
|
import { isValidRoadmapId } from "./roadmap.js";
|
|
9
|
+
import { SUB_STATE_PHASES, BLOCKER_TYPES } from "../types.js";
|
|
12
10
|
export const TASKS_START = "<!-- PROJITIVE:TASKS:START -->";
|
|
13
11
|
export const TASKS_END = "<!-- PROJITIVE:TASKS:END -->";
|
|
14
12
|
export const ALLOWED_STATUS = ["TODO", "IN_PROGRESS", "BLOCKED", "DONE"];
|
|
@@ -52,7 +50,7 @@ const NO_TASK_DISCOVERY_HOOK_FILE = "task_no_actionable.md";
|
|
|
52
50
|
const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
|
|
53
51
|
"- Check whether current code violates project guide/spec conventions; create TODO tasks for each actionable gap.",
|
|
54
52
|
"- Check unit/integration test coverage and identify high-value missing tests; create TODO tasks for meaningful coverage improvements.",
|
|
55
|
-
"- Check development/testing workflow for bottlenecks (slow feedback, fragile scripts, unclear runbooks); create
|
|
53
|
+
"- Check development/testing workflow for bottlenecks (slow feedback, fragile scripts, unclear runbooks); create tasks to improve reliability.",
|
|
56
54
|
"- Scan for TODO/FIXME/HACK comments and convert feasible items into governed TODO tasks with evidence links.",
|
|
57
55
|
"- Check dependency freshness and security advisories; create tasks for safe upgrades when needed.",
|
|
58
56
|
"- Check repeated manual operations that can be automated (lint/test/release checks); create tasks to reduce operational toil.",
|
|
@@ -174,11 +172,119 @@ export function rankActionableTaskCandidates(candidates) {
|
|
|
174
172
|
return a.task.id.localeCompare(b.task.id);
|
|
175
173
|
});
|
|
176
174
|
}
|
|
175
|
+
// Helper function to check if a line is a top-level task field
|
|
176
|
+
function isTopLevelField(line) {
|
|
177
|
+
const topLevelFields = [
|
|
178
|
+
"- owner:",
|
|
179
|
+
"- summary:",
|
|
180
|
+
"- updatedAt:",
|
|
181
|
+
"- roadmapRefs:",
|
|
182
|
+
"- links:",
|
|
183
|
+
"- hooks:",
|
|
184
|
+
"- subState:",
|
|
185
|
+
"- blocker:",
|
|
186
|
+
];
|
|
187
|
+
return topLevelFields.some((field) => line.startsWith(field));
|
|
188
|
+
}
|
|
189
|
+
// Parse subState nested field
|
|
190
|
+
function parseSubState(lines, startIndex) {
|
|
191
|
+
const subState = {};
|
|
192
|
+
let index = startIndex + 1;
|
|
193
|
+
while (index < lines.length) {
|
|
194
|
+
const line = lines[index];
|
|
195
|
+
const trimmed = line.trim();
|
|
196
|
+
// Check if we've reached the end of subState (new top-level field or new task)
|
|
197
|
+
if (trimmed.startsWith("- ") && isTopLevelField(trimmed)) {
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
// Check if we've reached a new task
|
|
201
|
+
if (trimmed.startsWith("## TASK-")) {
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
// Parse nested fields (2-space indentation expected)
|
|
205
|
+
if (trimmed.startsWith("- phase:")) {
|
|
206
|
+
const phase = trimmed.replace("- phase:", "").trim();
|
|
207
|
+
if (SUB_STATE_PHASES.includes(phase)) {
|
|
208
|
+
subState.phase = phase;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else if (trimmed.startsWith("- confidence:")) {
|
|
212
|
+
const confidenceStr = trimmed.replace("- confidence:", "").trim();
|
|
213
|
+
const confidence = Number.parseFloat(confidenceStr);
|
|
214
|
+
if (!Number.isNaN(confidence) && confidence >= 0 && confidence <= 1) {
|
|
215
|
+
subState.confidence = confidence;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if (trimmed.startsWith("- estimatedCompletion:")) {
|
|
219
|
+
const estimatedCompletion = trimmed.replace("- estimatedCompletion:", "").trim();
|
|
220
|
+
if (estimatedCompletion && estimatedCompletion !== "(none)") {
|
|
221
|
+
subState.estimatedCompletion = estimatedCompletion;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
index++;
|
|
225
|
+
}
|
|
226
|
+
return { subState, endIndex: index - 1 };
|
|
227
|
+
}
|
|
228
|
+
// Parse blocker nested field
|
|
229
|
+
function parseBlocker(lines, startIndex) {
|
|
230
|
+
const blocker = {};
|
|
231
|
+
let index = startIndex + 1;
|
|
232
|
+
while (index < lines.length) {
|
|
233
|
+
const line = lines[index];
|
|
234
|
+
const trimmed = line.trim();
|
|
235
|
+
// Check if we've reached the end of blocker (new top-level field or new task)
|
|
236
|
+
if (trimmed.startsWith("- ") && isTopLevelField(trimmed)) {
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
// Check if we've reached a new task
|
|
240
|
+
if (trimmed.startsWith("## TASK-")) {
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
// Parse nested fields (2-space indentation expected)
|
|
244
|
+
if (trimmed.startsWith("- type:")) {
|
|
245
|
+
const type = trimmed.replace("- type:", "").trim();
|
|
246
|
+
if (BLOCKER_TYPES.includes(type)) {
|
|
247
|
+
blocker.type = type;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else if (trimmed.startsWith("- description:")) {
|
|
251
|
+
const description = trimmed.replace("- description:", "").trim();
|
|
252
|
+
if (description && description !== "(none)") {
|
|
253
|
+
blocker.description = description;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (trimmed.startsWith("- blockingEntity:")) {
|
|
257
|
+
const blockingEntity = trimmed.replace("- blockingEntity:", "").trim();
|
|
258
|
+
if (blockingEntity && blockingEntity !== "(none)") {
|
|
259
|
+
blocker.blockingEntity = blockingEntity;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else if (trimmed.startsWith("- unblockCondition:")) {
|
|
263
|
+
const unblockCondition = trimmed.replace("- unblockCondition:", "").trim();
|
|
264
|
+
if (unblockCondition && unblockCondition !== "(none)") {
|
|
265
|
+
blocker.unblockCondition = unblockCondition;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else if (trimmed.startsWith("- escalationPath:")) {
|
|
269
|
+
const escalationPath = trimmed.replace("- escalationPath:", "").trim();
|
|
270
|
+
if (escalationPath && escalationPath !== "(none)") {
|
|
271
|
+
blocker.escalationPath = escalationPath;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
index++;
|
|
275
|
+
}
|
|
276
|
+
// Validate required fields
|
|
277
|
+
if (!blocker.type || !blocker.description) {
|
|
278
|
+
// Return empty blocker if required fields are missing
|
|
279
|
+
return { blocker: { type: "external_dependency", description: "Unknown blocker" }, endIndex: index - 1 };
|
|
280
|
+
}
|
|
281
|
+
return { blocker: blocker, endIndex: index - 1 };
|
|
282
|
+
}
|
|
177
283
|
export function normalizeTask(task) {
|
|
178
284
|
const normalizedRoadmapRefs = Array.isArray(task.roadmapRefs)
|
|
179
285
|
? task.roadmapRefs.map(String).filter((value) => isValidRoadmapId(value))
|
|
180
286
|
: [];
|
|
181
|
-
|
|
287
|
+
const normalized = {
|
|
182
288
|
id: String(task.id),
|
|
183
289
|
title: String(task.title),
|
|
184
290
|
status: ALLOWED_STATUS.includes(task.status) ? task.status : "TODO",
|
|
@@ -188,6 +294,14 @@ export function normalizeTask(task) {
|
|
|
188
294
|
links: Array.isArray(task.links) ? task.links.map(String) : [],
|
|
189
295
|
roadmapRefs: Array.from(new Set(normalizedRoadmapRefs)),
|
|
190
296
|
};
|
|
297
|
+
// Include optional v1.1.0 fields if present
|
|
298
|
+
if (task.subState) {
|
|
299
|
+
normalized.subState = task.subState;
|
|
300
|
+
}
|
|
301
|
+
if (task.blocker) {
|
|
302
|
+
normalized.blocker = task.blocker;
|
|
303
|
+
}
|
|
304
|
+
return normalized;
|
|
191
305
|
}
|
|
192
306
|
export async function parseTasksBlock(markdown) {
|
|
193
307
|
const start = markdown.indexOf(TASKS_START);
|
|
@@ -224,7 +338,10 @@ export async function parseTasksBlock(markdown) {
|
|
|
224
338
|
};
|
|
225
339
|
let inLinks = false;
|
|
226
340
|
let inHooks = false;
|
|
227
|
-
for
|
|
341
|
+
// Convert to indexed for loop to allow skipping lines when parsing subState/blocker
|
|
342
|
+
const sectionLines = lines.slice(1);
|
|
343
|
+
for (let lineIndex = 0; lineIndex < sectionLines.length; lineIndex++) {
|
|
344
|
+
const line = sectionLines[lineIndex];
|
|
228
345
|
const trimmed = line.trim();
|
|
229
346
|
if (!trimmed) {
|
|
230
347
|
continue;
|
|
@@ -270,6 +387,30 @@ export async function parseTasksBlock(markdown) {
|
|
|
270
387
|
inHooks = true;
|
|
271
388
|
continue;
|
|
272
389
|
}
|
|
390
|
+
// Handle subState nested field (Spec v1.1.0)
|
|
391
|
+
if (trimmed.startsWith("- subState:")) {
|
|
392
|
+
const { subState, endIndex } = parseSubState(sectionLines, lineIndex);
|
|
393
|
+
if (Object.keys(subState).length > 0) {
|
|
394
|
+
taskDraft.subState = subState;
|
|
395
|
+
}
|
|
396
|
+
// Skip to the end of subState parsing
|
|
397
|
+
lineIndex = endIndex;
|
|
398
|
+
inLinks = false;
|
|
399
|
+
inHooks = false;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
// Handle blocker nested field (Spec v1.1.0)
|
|
403
|
+
if (trimmed.startsWith("- blocker:")) {
|
|
404
|
+
const { blocker, endIndex } = parseBlocker(sectionLines, lineIndex);
|
|
405
|
+
if (blocker.type && blocker.description) {
|
|
406
|
+
taskDraft.blocker = blocker;
|
|
407
|
+
}
|
|
408
|
+
// Skip to the end of blocker parsing
|
|
409
|
+
lineIndex = endIndex;
|
|
410
|
+
inLinks = false;
|
|
411
|
+
inHooks = false;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
273
414
|
const nestedItem = trimmed.match(/^-\s+(.+)$/);
|
|
274
415
|
if (!nestedItem) {
|
|
275
416
|
continue;
|
|
@@ -367,6 +508,60 @@ function collectTaskLintSuggestionItems(tasks, options = {}) {
|
|
|
367
508
|
});
|
|
368
509
|
}
|
|
369
510
|
}
|
|
511
|
+
// ============================================================================
|
|
512
|
+
// Spec v1.1.0 - Blocker Categorization Validation
|
|
513
|
+
// ============================================================================
|
|
514
|
+
const blockedWithoutBlocker = tasks.filter((task) => task.status === "BLOCKED" && !task.blocker);
|
|
515
|
+
if (blockedWithoutBlocker.length > 0) {
|
|
516
|
+
suggestions.push({
|
|
517
|
+
code: TASK_LINT_CODES.BLOCKED_WITHOUT_BLOCKER,
|
|
518
|
+
message: `${blockedWithoutBlocker.length} BLOCKED task(s) have no blocker metadata.`,
|
|
519
|
+
fixHint: "Add structured blocker metadata with type and description.",
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
const blockerTypeInvalid = tasks.filter((task) => task.blocker && !BLOCKER_TYPES.includes(task.blocker.type));
|
|
523
|
+
if (blockerTypeInvalid.length > 0) {
|
|
524
|
+
suggestions.push({
|
|
525
|
+
code: TASK_LINT_CODES.BLOCKER_TYPE_INVALID,
|
|
526
|
+
message: `${blockerTypeInvalid.length} task(s) have invalid blocker type.`,
|
|
527
|
+
fixHint: `Use one of: ${BLOCKER_TYPES.join(", ")}.`,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const blockerDescriptionEmpty = tasks.filter((task) => task.blocker && !task.blocker.description?.trim());
|
|
531
|
+
if (blockerDescriptionEmpty.length > 0) {
|
|
532
|
+
suggestions.push({
|
|
533
|
+
code: TASK_LINT_CODES.BLOCKER_DESCRIPTION_EMPTY,
|
|
534
|
+
message: `${blockerDescriptionEmpty.length} task(s) have empty blocker description.`,
|
|
535
|
+
fixHint: "Provide a clear description of why the task is blocked.",
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
// ============================================================================
|
|
539
|
+
// Spec v1.1.0 - Sub-state Metadata Validation (Optional but Recommended)
|
|
540
|
+
// ============================================================================
|
|
541
|
+
const inProgressWithoutSubState = tasks.filter((task) => task.status === "IN_PROGRESS" && !task.subState);
|
|
542
|
+
if (inProgressWithoutSubState.length > 0) {
|
|
543
|
+
suggestions.push({
|
|
544
|
+
code: TASK_LINT_CODES.IN_PROGRESS_WITHOUT_SUBSTATE,
|
|
545
|
+
message: `${inProgressWithoutSubState.length} IN_PROGRESS task(s) have no subState metadata.`,
|
|
546
|
+
fixHint: "Add optional subState metadata for better progress tracking.",
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
const subStatePhaseInvalid = tasks.filter((task) => task.subState?.phase && !SUB_STATE_PHASES.includes(task.subState.phase));
|
|
550
|
+
if (subStatePhaseInvalid.length > 0) {
|
|
551
|
+
suggestions.push({
|
|
552
|
+
code: TASK_LINT_CODES.SUBSTATE_PHASE_INVALID,
|
|
553
|
+
message: `${subStatePhaseInvalid.length} task(s) have invalid subState phase.`,
|
|
554
|
+
fixHint: `Use one of: ${SUB_STATE_PHASES.join(", ")}.`,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
const subStateConfidenceInvalid = tasks.filter((task) => typeof task.subState?.confidence === "number" && (task.subState.confidence < 0 || task.subState.confidence > 1));
|
|
558
|
+
if (subStateConfidenceInvalid.length > 0) {
|
|
559
|
+
suggestions.push({
|
|
560
|
+
code: TASK_LINT_CODES.SUBSTATE_CONFIDENCE_INVALID,
|
|
561
|
+
message: `${subStateConfidenceInvalid.length} task(s) have invalid confidence score.`,
|
|
562
|
+
fixHint: "Confidence must be between 0.0 and 1.0.",
|
|
563
|
+
});
|
|
564
|
+
}
|
|
370
565
|
return suggestions;
|
|
371
566
|
}
|
|
372
567
|
export function collectTaskLintSuggestions(tasks, markdown, outsideMarkerScopeIds) {
|
|
@@ -409,6 +604,54 @@ function collectSingleTaskLintSuggestions(task) {
|
|
|
409
604
|
fixHint: "Bind ROADMAP-xxxx where applicable.",
|
|
410
605
|
});
|
|
411
606
|
}
|
|
607
|
+
// ============================================================================
|
|
608
|
+
// Spec v1.1.0 - Blocker Categorization Validation (Single Task)
|
|
609
|
+
// ============================================================================
|
|
610
|
+
if (task.status === "BLOCKED" && !task.blocker) {
|
|
611
|
+
suggestions.push({
|
|
612
|
+
code: TASK_LINT_CODES.BLOCKED_WITHOUT_BLOCKER,
|
|
613
|
+
message: "Current task is BLOCKED but has no blocker metadata.",
|
|
614
|
+
fixHint: "Add structured blocker metadata with type and description.",
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (task.blocker && !BLOCKER_TYPES.includes(task.blocker.type)) {
|
|
618
|
+
suggestions.push({
|
|
619
|
+
code: TASK_LINT_CODES.BLOCKER_TYPE_INVALID,
|
|
620
|
+
message: `Current task has invalid blocker type: ${task.blocker.type}.`,
|
|
621
|
+
fixHint: `Use one of: ${BLOCKER_TYPES.join(", ")}.`,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
if (task.blocker && !task.blocker.description?.trim()) {
|
|
625
|
+
suggestions.push({
|
|
626
|
+
code: TASK_LINT_CODES.BLOCKER_DESCRIPTION_EMPTY,
|
|
627
|
+
message: "Current task has empty blocker description.",
|
|
628
|
+
fixHint: "Provide a clear description of why the task is blocked.",
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
// ============================================================================
|
|
632
|
+
// Spec v1.1.0 - Sub-state Metadata Validation (Single Task, Optional)
|
|
633
|
+
// ============================================================================
|
|
634
|
+
if (task.status === "IN_PROGRESS" && !task.subState) {
|
|
635
|
+
suggestions.push({
|
|
636
|
+
code: TASK_LINT_CODES.IN_PROGRESS_WITHOUT_SUBSTATE,
|
|
637
|
+
message: "Current task is IN_PROGRESS but has no subState metadata.",
|
|
638
|
+
fixHint: "Add optional subState metadata for better progress tracking.",
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
if (task.subState?.phase && !SUB_STATE_PHASES.includes(task.subState.phase)) {
|
|
642
|
+
suggestions.push({
|
|
643
|
+
code: TASK_LINT_CODES.SUBSTATE_PHASE_INVALID,
|
|
644
|
+
message: `Current task has invalid subState phase: ${task.subState.phase}.`,
|
|
645
|
+
fixHint: `Use one of: ${SUB_STATE_PHASES.join(", ")}.`,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if (typeof task.subState?.confidence === "number" && (task.subState.confidence < 0 || task.subState.confidence > 1)) {
|
|
649
|
+
suggestions.push({
|
|
650
|
+
code: TASK_LINT_CODES.SUBSTATE_CONFIDENCE_INVALID,
|
|
651
|
+
message: `Current task has invalid confidence score: ${task.subState.confidence}.`,
|
|
652
|
+
fixHint: "Confidence must be between 0.0 and 1.0.",
|
|
653
|
+
});
|
|
654
|
+
}
|
|
412
655
|
return renderLintSuggestions(suggestions);
|
|
413
656
|
}
|
|
414
657
|
async function collectTaskFileLintSuggestions(governanceDir, task) {
|
|
@@ -438,19 +681,48 @@ export function renderTasksMarkdown(tasks) {
|
|
|
438
681
|
const links = task.links.length > 0
|
|
439
682
|
? ["- links:", ...task.links.map((link) => ` - ${link}`)]
|
|
440
683
|
: ["- links:", " - (none)"];
|
|
441
|
-
|
|
684
|
+
const lines = [
|
|
442
685
|
`## ${task.id} | ${task.status} | ${task.title}`,
|
|
443
686
|
`- owner: ${task.owner || "(none)"}`,
|
|
444
687
|
`- summary: ${task.summary || "(none)"}`,
|
|
445
688
|
`- updatedAt: ${task.updatedAt}`,
|
|
446
689
|
`- roadmapRefs: ${roadmapRefs}`,
|
|
447
690
|
...links,
|
|
448
|
-
]
|
|
691
|
+
];
|
|
692
|
+
// Add subState for IN_PROGRESS tasks (Spec v1.1.0)
|
|
693
|
+
if (task.subState && task.status === "IN_PROGRESS") {
|
|
694
|
+
lines.push(`- subState:`);
|
|
695
|
+
if (task.subState.phase) {
|
|
696
|
+
lines.push(` - phase: ${task.subState.phase}`);
|
|
697
|
+
}
|
|
698
|
+
if (typeof task.subState.confidence === "number") {
|
|
699
|
+
lines.push(` - confidence: ${task.subState.confidence}`);
|
|
700
|
+
}
|
|
701
|
+
if (task.subState.estimatedCompletion) {
|
|
702
|
+
lines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
// Add blocker for BLOCKED tasks (Spec v1.1.0)
|
|
706
|
+
if (task.blocker && task.status === "BLOCKED") {
|
|
707
|
+
lines.push(`- blocker:`);
|
|
708
|
+
lines.push(` - type: ${task.blocker.type}`);
|
|
709
|
+
lines.push(` - description: ${task.blocker.description}`);
|
|
710
|
+
if (task.blocker.blockingEntity) {
|
|
711
|
+
lines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
712
|
+
}
|
|
713
|
+
if (task.blocker.unblockCondition) {
|
|
714
|
+
lines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
|
|
715
|
+
}
|
|
716
|
+
if (task.blocker.escalationPath) {
|
|
717
|
+
lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return lines.join("\n");
|
|
449
721
|
});
|
|
450
722
|
return [
|
|
451
723
|
"# Tasks",
|
|
452
724
|
"",
|
|
453
|
-
"
|
|
725
|
+
"This file is maintained by Projitive MCP. If editing manually, please keep the Markdown structure valid.",
|
|
454
726
|
"",
|
|
455
727
|
TASKS_START,
|
|
456
728
|
...(sections.length > 0 ? sections : ["(no tasks)"]),
|
|
@@ -719,19 +991,49 @@ export function registerTaskTools(server) {
|
|
|
719
991
|
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
|
|
720
992
|
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
721
993
|
const suggestedReadOrder = [tasksPath, ...relatedArtifacts.filter((item) => item !== tasksPath)];
|
|
994
|
+
// Build summary with subState and blocker info (v1.1.0)
|
|
995
|
+
const summaryLines = [
|
|
996
|
+
`- governanceDir: ${governanceDir}`,
|
|
997
|
+
`- taskId: ${task.id}`,
|
|
998
|
+
`- title: ${task.title}`,
|
|
999
|
+
`- status: ${task.status}`,
|
|
1000
|
+
`- owner: ${task.owner}`,
|
|
1001
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
1002
|
+
`- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
|
|
1003
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : tasksPath}`,
|
|
1004
|
+
];
|
|
1005
|
+
// Add subState info for IN_PROGRESS tasks (v1.1.0)
|
|
1006
|
+
if (task.subState && task.status === "IN_PROGRESS") {
|
|
1007
|
+
summaryLines.push(`- subState:`);
|
|
1008
|
+
if (task.subState.phase) {
|
|
1009
|
+
summaryLines.push(` - phase: ${task.subState.phase}`);
|
|
1010
|
+
}
|
|
1011
|
+
if (typeof task.subState.confidence === "number") {
|
|
1012
|
+
summaryLines.push(` - confidence: ${task.subState.confidence}`);
|
|
1013
|
+
}
|
|
1014
|
+
if (task.subState.estimatedCompletion) {
|
|
1015
|
+
summaryLines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
// Add blocker info for BLOCKED tasks (v1.1.0)
|
|
1019
|
+
if (task.blocker && task.status === "BLOCKED") {
|
|
1020
|
+
summaryLines.push(`- blocker:`);
|
|
1021
|
+
summaryLines.push(` - type: ${task.blocker.type}`);
|
|
1022
|
+
summaryLines.push(` - description: ${task.blocker.description}`);
|
|
1023
|
+
if (task.blocker.blockingEntity) {
|
|
1024
|
+
summaryLines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
1025
|
+
}
|
|
1026
|
+
if (task.blocker.unblockCondition) {
|
|
1027
|
+
summaryLines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
|
|
1028
|
+
}
|
|
1029
|
+
if (task.blocker.escalationPath) {
|
|
1030
|
+
summaryLines.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
722
1033
|
const coreMarkdown = renderToolResponseMarkdown({
|
|
723
1034
|
toolName: "taskContext",
|
|
724
1035
|
sections: [
|
|
725
|
-
summarySection(
|
|
726
|
-
`- governanceDir: ${governanceDir}`,
|
|
727
|
-
`- taskId: ${task.id}`,
|
|
728
|
-
`- title: ${task.title}`,
|
|
729
|
-
`- status: ${task.status}`,
|
|
730
|
-
`- owner: ${task.owner}`,
|
|
731
|
-
`- updatedAt: ${task.updatedAt}`,
|
|
732
|
-
`- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
|
|
733
|
-
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : tasksPath}`,
|
|
734
|
-
]),
|
|
1036
|
+
summarySection(summaryLines),
|
|
735
1037
|
evidenceSection([
|
|
736
1038
|
"### Related Artifacts",
|
|
737
1039
|
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
|
|
@@ -757,4 +1059,149 @@ export function registerTaskTools(server) {
|
|
|
757
1059
|
});
|
|
758
1060
|
return asText(coreMarkdown);
|
|
759
1061
|
});
|
|
1062
|
+
// taskUpdate tool - Update task fields including subState and blocker (Spec v1.1.0)
|
|
1063
|
+
server.registerTool("taskUpdate", {
|
|
1064
|
+
title: "Task Update",
|
|
1065
|
+
description: "Update task fields including status, owner, summary, subState, and blocker metadata",
|
|
1066
|
+
inputSchema: {
|
|
1067
|
+
projectPath: z.string(),
|
|
1068
|
+
taskId: z.string(),
|
|
1069
|
+
updates: z.object({
|
|
1070
|
+
status: z.enum(["TODO", "IN_PROGRESS", "BLOCKED", "DONE"]).optional(),
|
|
1071
|
+
owner: z.string().optional(),
|
|
1072
|
+
summary: z.string().optional(),
|
|
1073
|
+
roadmapRefs: z.array(z.string()).optional(),
|
|
1074
|
+
links: z.array(z.string()).optional(),
|
|
1075
|
+
subState: z.object({
|
|
1076
|
+
phase: z.enum(["discovery", "design", "implementation", "testing"]).optional(),
|
|
1077
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
1078
|
+
estimatedCompletion: z.string().optional(),
|
|
1079
|
+
}).optional(),
|
|
1080
|
+
blocker: z.object({
|
|
1081
|
+
type: z.enum(["internal_dependency", "external_dependency", "resource", "approval"]),
|
|
1082
|
+
description: z.string(),
|
|
1083
|
+
blockingEntity: z.string().optional(),
|
|
1084
|
+
unblockCondition: z.string().optional(),
|
|
1085
|
+
escalationPath: z.string().optional(),
|
|
1086
|
+
}).optional(),
|
|
1087
|
+
}),
|
|
1088
|
+
},
|
|
1089
|
+
}, async ({ projectPath, taskId, updates }) => {
|
|
1090
|
+
if (!isValidTaskId(taskId)) {
|
|
1091
|
+
return {
|
|
1092
|
+
...asText(renderErrorMarkdown("taskUpdate", `Invalid task ID format: ${taskId}`, ["expected format: TASK-0001", "retry with a valid task ID"], `taskUpdate(projectPath=\"${projectPath}\", taskId=\"TASK-0001\", updates={...})`)),
|
|
1093
|
+
isError: true,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1097
|
+
const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
|
|
1098
|
+
const taskIndex = tasks.findIndex((item) => item.id === taskId);
|
|
1099
|
+
if (taskIndex === -1) {
|
|
1100
|
+
return {
|
|
1101
|
+
...asText(renderErrorMarkdown("taskUpdate", `Task not found: ${taskId}`, ["run `taskList` to discover available IDs", "retry with an existing task ID"], `taskList(projectPath=\"${toProjectPath(governanceDir)}\")`)),
|
|
1102
|
+
isError: true,
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
const task = tasks[taskIndex];
|
|
1106
|
+
const originalStatus = task.status;
|
|
1107
|
+
// Validate status transition
|
|
1108
|
+
if (updates.status && !validateTransition(originalStatus, updates.status)) {
|
|
1109
|
+
return {
|
|
1110
|
+
...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}\")`)),
|
|
1111
|
+
isError: true,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
// Apply updates
|
|
1115
|
+
if (updates.status)
|
|
1116
|
+
task.status = updates.status;
|
|
1117
|
+
if (updates.owner !== undefined)
|
|
1118
|
+
task.owner = updates.owner;
|
|
1119
|
+
if (updates.summary !== undefined)
|
|
1120
|
+
task.summary = updates.summary;
|
|
1121
|
+
if (updates.roadmapRefs)
|
|
1122
|
+
task.roadmapRefs = updates.roadmapRefs;
|
|
1123
|
+
if (updates.links)
|
|
1124
|
+
task.links = updates.links;
|
|
1125
|
+
// Handle subState (Spec v1.1.0)
|
|
1126
|
+
if (updates.subState !== undefined) {
|
|
1127
|
+
if (updates.subState === null) {
|
|
1128
|
+
delete task.subState;
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
task.subState = {
|
|
1132
|
+
...(task.subState || {}),
|
|
1133
|
+
...updates.subState,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
// Handle blocker (Spec v1.1.0)
|
|
1138
|
+
if (updates.blocker !== undefined) {
|
|
1139
|
+
if (updates.blocker === null) {
|
|
1140
|
+
delete task.blocker;
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
task.blocker = updates.blocker;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
// Update updatedAt
|
|
1147
|
+
task.updatedAt = nowIso();
|
|
1148
|
+
// Save tasks
|
|
1149
|
+
await saveTasks(tasksPath, tasks);
|
|
1150
|
+
// Build response
|
|
1151
|
+
const updateSummary = [
|
|
1152
|
+
`- taskId: ${taskId}`,
|
|
1153
|
+
`- originalStatus: ${originalStatus}`,
|
|
1154
|
+
`- newStatus: ${task.status}`,
|
|
1155
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
1156
|
+
];
|
|
1157
|
+
if (task.subState) {
|
|
1158
|
+
updateSummary.push(`- subState:`);
|
|
1159
|
+
if (task.subState.phase)
|
|
1160
|
+
updateSummary.push(` - phase: ${task.subState.phase}`);
|
|
1161
|
+
if (typeof task.subState.confidence === "number")
|
|
1162
|
+
updateSummary.push(` - confidence: ${task.subState.confidence}`);
|
|
1163
|
+
if (task.subState.estimatedCompletion)
|
|
1164
|
+
updateSummary.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
1165
|
+
}
|
|
1166
|
+
if (task.blocker) {
|
|
1167
|
+
updateSummary.push(`- blocker:`);
|
|
1168
|
+
updateSummary.push(` - type: ${task.blocker.type}`);
|
|
1169
|
+
updateSummary.push(` - description: ${task.blocker.description}`);
|
|
1170
|
+
if (task.blocker.blockingEntity)
|
|
1171
|
+
updateSummary.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
1172
|
+
if (task.blocker.unblockCondition)
|
|
1173
|
+
updateSummary.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
|
|
1174
|
+
if (task.blocker.escalationPath)
|
|
1175
|
+
updateSummary.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
1176
|
+
}
|
|
1177
|
+
const markdown = renderToolResponseMarkdown({
|
|
1178
|
+
toolName: "taskUpdate",
|
|
1179
|
+
sections: [
|
|
1180
|
+
summarySection(updateSummary),
|
|
1181
|
+
evidenceSection([
|
|
1182
|
+
"### Updated Task",
|
|
1183
|
+
`- ${task.id} | ${task.status} | ${task.title}`,
|
|
1184
|
+
`- owner: ${task.owner || "(none)"}`,
|
|
1185
|
+
`- summary: ${task.summary || "(none)"}`,
|
|
1186
|
+
"",
|
|
1187
|
+
"### Update Details",
|
|
1188
|
+
...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
|
|
1189
|
+
...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
|
|
1190
|
+
...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
|
|
1191
|
+
...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(", ")}`] : []),
|
|
1192
|
+
...(updates.links ? [`- links: ${updates.links.join(", ")}`] : []),
|
|
1193
|
+
...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
|
|
1194
|
+
...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
|
|
1195
|
+
]),
|
|
1196
|
+
guidanceSection([
|
|
1197
|
+
"Task updated successfully. Run `taskContext` to verify the changes.",
|
|
1198
|
+
"If status changed to DONE, ensure evidence links are added.",
|
|
1199
|
+
"If subState or blocker were updated, verify the metadata is correct.",
|
|
1200
|
+
]),
|
|
1201
|
+
lintSection([]),
|
|
1202
|
+
nextCallSection(`taskContext(projectPath=\"${toProjectPath(governanceDir)}\", taskId=\"${taskId}\")`),
|
|
1203
|
+
],
|
|
1204
|
+
});
|
|
1205
|
+
return asText(markdown);
|
|
1206
|
+
});
|
|
760
1207
|
}
|