@projitive/mcp 1.0.8 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/output/package.json +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/confidence.js +231 -0
- package/output/source/common/confidence.test.js +205 -0
- 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 +10 -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/design-context.js +51 -500
- package/output/source/index.js +8 -193
- package/output/source/index.test.js +116 -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} +577 -22
- package/output/source/tools/task.test.js +473 -0
- package/output/source/types.js +67 -0
- package/package.json +4 -1
- 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,13 @@
|
|
|
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";
|
|
10
|
+
import { calculateConfidenceScore, calculateContextCompleteness, calculateSimilarTaskHistory, calculateSpecificationClarity, getOrCreateTaskAutoCreateValidationHook, runPreCreationValidation, generateConfidenceReport } from "../common/index.js";
|
|
12
11
|
export const TASKS_START = "<!-- PROJITIVE:TASKS:START -->";
|
|
13
12
|
export const TASKS_END = "<!-- PROJITIVE:TASKS:END -->";
|
|
14
13
|
export const ALLOWED_STATUS = ["TODO", "IN_PROGRESS", "BLOCKED", "DONE"];
|
|
@@ -174,11 +173,119 @@ export function rankActionableTaskCandidates(candidates) {
|
|
|
174
173
|
return a.task.id.localeCompare(b.task.id);
|
|
175
174
|
});
|
|
176
175
|
}
|
|
176
|
+
// Helper function to check if a line is a top-level task field
|
|
177
|
+
function isTopLevelField(line) {
|
|
178
|
+
const topLevelFields = [
|
|
179
|
+
"- owner:",
|
|
180
|
+
"- summary:",
|
|
181
|
+
"- updatedAt:",
|
|
182
|
+
"- roadmapRefs:",
|
|
183
|
+
"- links:",
|
|
184
|
+
"- hooks:",
|
|
185
|
+
"- subState:",
|
|
186
|
+
"- blocker:",
|
|
187
|
+
];
|
|
188
|
+
return topLevelFields.some((field) => line.startsWith(field));
|
|
189
|
+
}
|
|
190
|
+
// Parse subState nested field
|
|
191
|
+
function parseSubState(lines, startIndex) {
|
|
192
|
+
const subState = {};
|
|
193
|
+
let index = startIndex + 1;
|
|
194
|
+
while (index < lines.length) {
|
|
195
|
+
const line = lines[index];
|
|
196
|
+
const trimmed = line.trim();
|
|
197
|
+
// Check if we've reached the end of subState (new top-level field or new task)
|
|
198
|
+
if (trimmed.startsWith("- ") && isTopLevelField(trimmed)) {
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
// Check if we've reached a new task
|
|
202
|
+
if (trimmed.startsWith("## TASK-")) {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
// Parse nested fields (2-space indentation expected)
|
|
206
|
+
if (trimmed.startsWith("- phase:")) {
|
|
207
|
+
const phase = trimmed.replace("- phase:", "").trim();
|
|
208
|
+
if (SUB_STATE_PHASES.includes(phase)) {
|
|
209
|
+
subState.phase = phase;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else if (trimmed.startsWith("- confidence:")) {
|
|
213
|
+
const confidenceStr = trimmed.replace("- confidence:", "").trim();
|
|
214
|
+
const confidence = Number.parseFloat(confidenceStr);
|
|
215
|
+
if (!Number.isNaN(confidence) && confidence >= 0 && confidence <= 1) {
|
|
216
|
+
subState.confidence = confidence;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else if (trimmed.startsWith("- estimatedCompletion:")) {
|
|
220
|
+
const estimatedCompletion = trimmed.replace("- estimatedCompletion:", "").trim();
|
|
221
|
+
if (estimatedCompletion && estimatedCompletion !== "(none)") {
|
|
222
|
+
subState.estimatedCompletion = estimatedCompletion;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
index++;
|
|
226
|
+
}
|
|
227
|
+
return { subState, endIndex: index - 1 };
|
|
228
|
+
}
|
|
229
|
+
// Parse blocker nested field
|
|
230
|
+
function parseBlocker(lines, startIndex) {
|
|
231
|
+
const blocker = {};
|
|
232
|
+
let index = startIndex + 1;
|
|
233
|
+
while (index < lines.length) {
|
|
234
|
+
const line = lines[index];
|
|
235
|
+
const trimmed = line.trim();
|
|
236
|
+
// Check if we've reached the end of blocker (new top-level field or new task)
|
|
237
|
+
if (trimmed.startsWith("- ") && isTopLevelField(trimmed)) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
// Check if we've reached a new task
|
|
241
|
+
if (trimmed.startsWith("## TASK-")) {
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
// Parse nested fields (2-space indentation expected)
|
|
245
|
+
if (trimmed.startsWith("- type:")) {
|
|
246
|
+
const type = trimmed.replace("- type:", "").trim();
|
|
247
|
+
if (BLOCKER_TYPES.includes(type)) {
|
|
248
|
+
blocker.type = type;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
else if (trimmed.startsWith("- description:")) {
|
|
252
|
+
const description = trimmed.replace("- description:", "").trim();
|
|
253
|
+
if (description && description !== "(none)") {
|
|
254
|
+
blocker.description = description;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (trimmed.startsWith("- blockingEntity:")) {
|
|
258
|
+
const blockingEntity = trimmed.replace("- blockingEntity:", "").trim();
|
|
259
|
+
if (blockingEntity && blockingEntity !== "(none)") {
|
|
260
|
+
blocker.blockingEntity = blockingEntity;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else if (trimmed.startsWith("- unblockCondition:")) {
|
|
264
|
+
const unblockCondition = trimmed.replace("- unblockCondition:", "").trim();
|
|
265
|
+
if (unblockCondition && unblockCondition !== "(none)") {
|
|
266
|
+
blocker.unblockCondition = unblockCondition;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
else if (trimmed.startsWith("- escalationPath:")) {
|
|
270
|
+
const escalationPath = trimmed.replace("- escalationPath:", "").trim();
|
|
271
|
+
if (escalationPath && escalationPath !== "(none)") {
|
|
272
|
+
blocker.escalationPath = escalationPath;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
index++;
|
|
276
|
+
}
|
|
277
|
+
// Validate required fields
|
|
278
|
+
if (!blocker.type || !blocker.description) {
|
|
279
|
+
// Return empty blocker if required fields are missing
|
|
280
|
+
return { blocker: { type: "external_dependency", description: "Unknown blocker" }, endIndex: index - 1 };
|
|
281
|
+
}
|
|
282
|
+
return { blocker: blocker, endIndex: index - 1 };
|
|
283
|
+
}
|
|
177
284
|
export function normalizeTask(task) {
|
|
178
285
|
const normalizedRoadmapRefs = Array.isArray(task.roadmapRefs)
|
|
179
286
|
? task.roadmapRefs.map(String).filter((value) => isValidRoadmapId(value))
|
|
180
287
|
: [];
|
|
181
|
-
|
|
288
|
+
const normalized = {
|
|
182
289
|
id: String(task.id),
|
|
183
290
|
title: String(task.title),
|
|
184
291
|
status: ALLOWED_STATUS.includes(task.status) ? task.status : "TODO",
|
|
@@ -188,6 +295,14 @@ export function normalizeTask(task) {
|
|
|
188
295
|
links: Array.isArray(task.links) ? task.links.map(String) : [],
|
|
189
296
|
roadmapRefs: Array.from(new Set(normalizedRoadmapRefs)),
|
|
190
297
|
};
|
|
298
|
+
// Include optional v1.1.0 fields if present
|
|
299
|
+
if (task.subState) {
|
|
300
|
+
normalized.subState = task.subState;
|
|
301
|
+
}
|
|
302
|
+
if (task.blocker) {
|
|
303
|
+
normalized.blocker = task.blocker;
|
|
304
|
+
}
|
|
305
|
+
return normalized;
|
|
191
306
|
}
|
|
192
307
|
export async function parseTasksBlock(markdown) {
|
|
193
308
|
const start = markdown.indexOf(TASKS_START);
|
|
@@ -224,7 +339,10 @@ export async function parseTasksBlock(markdown) {
|
|
|
224
339
|
};
|
|
225
340
|
let inLinks = false;
|
|
226
341
|
let inHooks = false;
|
|
227
|
-
for
|
|
342
|
+
// Convert to indexed for loop to allow skipping lines when parsing subState/blocker
|
|
343
|
+
const sectionLines = lines.slice(1);
|
|
344
|
+
for (let lineIndex = 0; lineIndex < sectionLines.length; lineIndex++) {
|
|
345
|
+
const line = sectionLines[lineIndex];
|
|
228
346
|
const trimmed = line.trim();
|
|
229
347
|
if (!trimmed) {
|
|
230
348
|
continue;
|
|
@@ -270,6 +388,30 @@ export async function parseTasksBlock(markdown) {
|
|
|
270
388
|
inHooks = true;
|
|
271
389
|
continue;
|
|
272
390
|
}
|
|
391
|
+
// Handle subState nested field (Spec v1.1.0)
|
|
392
|
+
if (trimmed.startsWith("- subState:")) {
|
|
393
|
+
const { subState, endIndex } = parseSubState(sectionLines, lineIndex);
|
|
394
|
+
if (Object.keys(subState).length > 0) {
|
|
395
|
+
taskDraft.subState = subState;
|
|
396
|
+
}
|
|
397
|
+
// Skip to the end of subState parsing
|
|
398
|
+
lineIndex = endIndex;
|
|
399
|
+
inLinks = false;
|
|
400
|
+
inHooks = false;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
// Handle blocker nested field (Spec v1.1.0)
|
|
404
|
+
if (trimmed.startsWith("- blocker:")) {
|
|
405
|
+
const { blocker, endIndex } = parseBlocker(sectionLines, lineIndex);
|
|
406
|
+
if (blocker.type && blocker.description) {
|
|
407
|
+
taskDraft.blocker = blocker;
|
|
408
|
+
}
|
|
409
|
+
// Skip to the end of blocker parsing
|
|
410
|
+
lineIndex = endIndex;
|
|
411
|
+
inLinks = false;
|
|
412
|
+
inHooks = false;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
273
415
|
const nestedItem = trimmed.match(/^-\s+(.+)$/);
|
|
274
416
|
if (!nestedItem) {
|
|
275
417
|
continue;
|
|
@@ -367,6 +509,60 @@ function collectTaskLintSuggestionItems(tasks, options = {}) {
|
|
|
367
509
|
});
|
|
368
510
|
}
|
|
369
511
|
}
|
|
512
|
+
// ============================================================================
|
|
513
|
+
// Spec v1.1.0 - Blocker Categorization Validation
|
|
514
|
+
// ============================================================================
|
|
515
|
+
const blockedWithoutBlocker = tasks.filter((task) => task.status === "BLOCKED" && !task.blocker);
|
|
516
|
+
if (blockedWithoutBlocker.length > 0) {
|
|
517
|
+
suggestions.push({
|
|
518
|
+
code: TASK_LINT_CODES.BLOCKED_WITHOUT_BLOCKER,
|
|
519
|
+
message: `${blockedWithoutBlocker.length} BLOCKED task(s) have no blocker metadata.`,
|
|
520
|
+
fixHint: "Add structured blocker metadata with type and description.",
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
const blockerTypeInvalid = tasks.filter((task) => task.blocker && !BLOCKER_TYPES.includes(task.blocker.type));
|
|
524
|
+
if (blockerTypeInvalid.length > 0) {
|
|
525
|
+
suggestions.push({
|
|
526
|
+
code: TASK_LINT_CODES.BLOCKER_TYPE_INVALID,
|
|
527
|
+
message: `${blockerTypeInvalid.length} task(s) have invalid blocker type.`,
|
|
528
|
+
fixHint: `Use one of: ${BLOCKER_TYPES.join(", ")}.`,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
const blockerDescriptionEmpty = tasks.filter((task) => task.blocker && !task.blocker.description?.trim());
|
|
532
|
+
if (blockerDescriptionEmpty.length > 0) {
|
|
533
|
+
suggestions.push({
|
|
534
|
+
code: TASK_LINT_CODES.BLOCKER_DESCRIPTION_EMPTY,
|
|
535
|
+
message: `${blockerDescriptionEmpty.length} task(s) have empty blocker description.`,
|
|
536
|
+
fixHint: "Provide a clear description of why the task is blocked.",
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
// ============================================================================
|
|
540
|
+
// Spec v1.1.0 - Sub-state Metadata Validation (Optional but Recommended)
|
|
541
|
+
// ============================================================================
|
|
542
|
+
const inProgressWithoutSubState = tasks.filter((task) => task.status === "IN_PROGRESS" && !task.subState);
|
|
543
|
+
if (inProgressWithoutSubState.length > 0) {
|
|
544
|
+
suggestions.push({
|
|
545
|
+
code: TASK_LINT_CODES.IN_PROGRESS_WITHOUT_SUBSTATE,
|
|
546
|
+
message: `${inProgressWithoutSubState.length} IN_PROGRESS task(s) have no subState metadata.`,
|
|
547
|
+
fixHint: "Add optional subState metadata for better progress tracking.",
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
const subStatePhaseInvalid = tasks.filter((task) => task.subState?.phase && !SUB_STATE_PHASES.includes(task.subState.phase));
|
|
551
|
+
if (subStatePhaseInvalid.length > 0) {
|
|
552
|
+
suggestions.push({
|
|
553
|
+
code: TASK_LINT_CODES.SUBSTATE_PHASE_INVALID,
|
|
554
|
+
message: `${subStatePhaseInvalid.length} task(s) have invalid subState phase.`,
|
|
555
|
+
fixHint: `Use one of: ${SUB_STATE_PHASES.join(", ")}.`,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
const subStateConfidenceInvalid = tasks.filter((task) => typeof task.subState?.confidence === "number" && (task.subState.confidence < 0 || task.subState.confidence > 1));
|
|
559
|
+
if (subStateConfidenceInvalid.length > 0) {
|
|
560
|
+
suggestions.push({
|
|
561
|
+
code: TASK_LINT_CODES.SUBSTATE_CONFIDENCE_INVALID,
|
|
562
|
+
message: `${subStateConfidenceInvalid.length} task(s) have invalid confidence score.`,
|
|
563
|
+
fixHint: "Confidence must be between 0.0 and 1.0.",
|
|
564
|
+
});
|
|
565
|
+
}
|
|
370
566
|
return suggestions;
|
|
371
567
|
}
|
|
372
568
|
export function collectTaskLintSuggestions(tasks, markdown, outsideMarkerScopeIds) {
|
|
@@ -409,6 +605,54 @@ function collectSingleTaskLintSuggestions(task) {
|
|
|
409
605
|
fixHint: "Bind ROADMAP-xxxx where applicable.",
|
|
410
606
|
});
|
|
411
607
|
}
|
|
608
|
+
// ============================================================================
|
|
609
|
+
// Spec v1.1.0 - Blocker Categorization Validation (Single Task)
|
|
610
|
+
// ============================================================================
|
|
611
|
+
if (task.status === "BLOCKED" && !task.blocker) {
|
|
612
|
+
suggestions.push({
|
|
613
|
+
code: TASK_LINT_CODES.BLOCKED_WITHOUT_BLOCKER,
|
|
614
|
+
message: "Current task is BLOCKED but has no blocker metadata.",
|
|
615
|
+
fixHint: "Add structured blocker metadata with type and description.",
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
if (task.blocker && !BLOCKER_TYPES.includes(task.blocker.type)) {
|
|
619
|
+
suggestions.push({
|
|
620
|
+
code: TASK_LINT_CODES.BLOCKER_TYPE_INVALID,
|
|
621
|
+
message: `Current task has invalid blocker type: ${task.blocker.type}.`,
|
|
622
|
+
fixHint: `Use one of: ${BLOCKER_TYPES.join(", ")}.`,
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
if (task.blocker && !task.blocker.description?.trim()) {
|
|
626
|
+
suggestions.push({
|
|
627
|
+
code: TASK_LINT_CODES.BLOCKER_DESCRIPTION_EMPTY,
|
|
628
|
+
message: "Current task has empty blocker description.",
|
|
629
|
+
fixHint: "Provide a clear description of why the task is blocked.",
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
// ============================================================================
|
|
633
|
+
// Spec v1.1.0 - Sub-state Metadata Validation (Single Task, Optional)
|
|
634
|
+
// ============================================================================
|
|
635
|
+
if (task.status === "IN_PROGRESS" && !task.subState) {
|
|
636
|
+
suggestions.push({
|
|
637
|
+
code: TASK_LINT_CODES.IN_PROGRESS_WITHOUT_SUBSTATE,
|
|
638
|
+
message: "Current task is IN_PROGRESS but has no subState metadata.",
|
|
639
|
+
fixHint: "Add optional subState metadata for better progress tracking.",
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
if (task.subState?.phase && !SUB_STATE_PHASES.includes(task.subState.phase)) {
|
|
643
|
+
suggestions.push({
|
|
644
|
+
code: TASK_LINT_CODES.SUBSTATE_PHASE_INVALID,
|
|
645
|
+
message: `Current task has invalid subState phase: ${task.subState.phase}.`,
|
|
646
|
+
fixHint: `Use one of: ${SUB_STATE_PHASES.join(", ")}.`,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (typeof task.subState?.confidence === "number" && (task.subState.confidence < 0 || task.subState.confidence > 1)) {
|
|
650
|
+
suggestions.push({
|
|
651
|
+
code: TASK_LINT_CODES.SUBSTATE_CONFIDENCE_INVALID,
|
|
652
|
+
message: `Current task has invalid confidence score: ${task.subState.confidence}.`,
|
|
653
|
+
fixHint: "Confidence must be between 0.0 and 1.0.",
|
|
654
|
+
});
|
|
655
|
+
}
|
|
412
656
|
return renderLintSuggestions(suggestions);
|
|
413
657
|
}
|
|
414
658
|
async function collectTaskFileLintSuggestions(governanceDir, task) {
|
|
@@ -438,19 +682,48 @@ export function renderTasksMarkdown(tasks) {
|
|
|
438
682
|
const links = task.links.length > 0
|
|
439
683
|
? ["- links:", ...task.links.map((link) => ` - ${link}`)]
|
|
440
684
|
: ["- links:", " - (none)"];
|
|
441
|
-
|
|
685
|
+
const lines = [
|
|
442
686
|
`## ${task.id} | ${task.status} | ${task.title}`,
|
|
443
687
|
`- owner: ${task.owner || "(none)"}`,
|
|
444
688
|
`- summary: ${task.summary || "(none)"}`,
|
|
445
689
|
`- updatedAt: ${task.updatedAt}`,
|
|
446
690
|
`- roadmapRefs: ${roadmapRefs}`,
|
|
447
691
|
...links,
|
|
448
|
-
]
|
|
692
|
+
];
|
|
693
|
+
// Add subState for IN_PROGRESS tasks (Spec v1.1.0)
|
|
694
|
+
if (task.subState && task.status === "IN_PROGRESS") {
|
|
695
|
+
lines.push(`- subState:`);
|
|
696
|
+
if (task.subState.phase) {
|
|
697
|
+
lines.push(` - phase: ${task.subState.phase}`);
|
|
698
|
+
}
|
|
699
|
+
if (typeof task.subState.confidence === "number") {
|
|
700
|
+
lines.push(` - confidence: ${task.subState.confidence}`);
|
|
701
|
+
}
|
|
702
|
+
if (task.subState.estimatedCompletion) {
|
|
703
|
+
lines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// Add blocker for BLOCKED tasks (Spec v1.1.0)
|
|
707
|
+
if (task.blocker && task.status === "BLOCKED") {
|
|
708
|
+
lines.push(`- blocker:`);
|
|
709
|
+
lines.push(` - type: ${task.blocker.type}`);
|
|
710
|
+
lines.push(` - description: ${task.blocker.description}`);
|
|
711
|
+
if (task.blocker.blockingEntity) {
|
|
712
|
+
lines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
713
|
+
}
|
|
714
|
+
if (task.blocker.unblockCondition) {
|
|
715
|
+
lines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
|
|
716
|
+
}
|
|
717
|
+
if (task.blocker.escalationPath) {
|
|
718
|
+
lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return lines.join("\n");
|
|
449
722
|
});
|
|
450
723
|
return [
|
|
451
724
|
"# Tasks",
|
|
452
725
|
"",
|
|
453
|
-
"
|
|
726
|
+
"This file is maintained by Projitive MCP. If editing manually, please keep the Markdown structure valid.",
|
|
454
727
|
"",
|
|
455
728
|
TASKS_START,
|
|
456
729
|
...(sections.length > 0 ? sections : ["(no tasks)"]),
|
|
@@ -719,19 +992,49 @@ export function registerTaskTools(server) {
|
|
|
719
992
|
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
|
|
720
993
|
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
721
994
|
const suggestedReadOrder = [tasksPath, ...relatedArtifacts.filter((item) => item !== tasksPath)];
|
|
995
|
+
// Build summary with subState and blocker info (v1.1.0)
|
|
996
|
+
const summaryLines = [
|
|
997
|
+
`- governanceDir: ${governanceDir}`,
|
|
998
|
+
`- taskId: ${task.id}`,
|
|
999
|
+
`- title: ${task.title}`,
|
|
1000
|
+
`- status: ${task.status}`,
|
|
1001
|
+
`- owner: ${task.owner}`,
|
|
1002
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
1003
|
+
`- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
|
|
1004
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : tasksPath}`,
|
|
1005
|
+
];
|
|
1006
|
+
// Add subState info for IN_PROGRESS tasks (v1.1.0)
|
|
1007
|
+
if (task.subState && task.status === "IN_PROGRESS") {
|
|
1008
|
+
summaryLines.push(`- subState:`);
|
|
1009
|
+
if (task.subState.phase) {
|
|
1010
|
+
summaryLines.push(` - phase: ${task.subState.phase}`);
|
|
1011
|
+
}
|
|
1012
|
+
if (typeof task.subState.confidence === "number") {
|
|
1013
|
+
summaryLines.push(` - confidence: ${task.subState.confidence}`);
|
|
1014
|
+
}
|
|
1015
|
+
if (task.subState.estimatedCompletion) {
|
|
1016
|
+
summaryLines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
// Add blocker info for BLOCKED tasks (v1.1.0)
|
|
1020
|
+
if (task.blocker && task.status === "BLOCKED") {
|
|
1021
|
+
summaryLines.push(`- blocker:`);
|
|
1022
|
+
summaryLines.push(` - type: ${task.blocker.type}`);
|
|
1023
|
+
summaryLines.push(` - description: ${task.blocker.description}`);
|
|
1024
|
+
if (task.blocker.blockingEntity) {
|
|
1025
|
+
summaryLines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
1026
|
+
}
|
|
1027
|
+
if (task.blocker.unblockCondition) {
|
|
1028
|
+
summaryLines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
|
|
1029
|
+
}
|
|
1030
|
+
if (task.blocker.escalationPath) {
|
|
1031
|
+
summaryLines.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
722
1034
|
const coreMarkdown = renderToolResponseMarkdown({
|
|
723
1035
|
toolName: "taskContext",
|
|
724
1036
|
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
|
-
]),
|
|
1037
|
+
summarySection(summaryLines),
|
|
735
1038
|
evidenceSection([
|
|
736
1039
|
"### Related Artifacts",
|
|
737
1040
|
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
|
|
@@ -757,4 +1060,256 @@ export function registerTaskTools(server) {
|
|
|
757
1060
|
});
|
|
758
1061
|
return asText(coreMarkdown);
|
|
759
1062
|
});
|
|
1063
|
+
// taskUpdate tool - Update task fields including subState and blocker (Spec v1.1.0)
|
|
1064
|
+
server.registerTool("taskUpdate", {
|
|
1065
|
+
title: "Task Update",
|
|
1066
|
+
description: "Update task fields including status, owner, summary, subState, and blocker metadata",
|
|
1067
|
+
inputSchema: {
|
|
1068
|
+
projectPath: z.string(),
|
|
1069
|
+
taskId: z.string(),
|
|
1070
|
+
updates: z.object({
|
|
1071
|
+
status: z.enum(["TODO", "IN_PROGRESS", "BLOCKED", "DONE"]).optional(),
|
|
1072
|
+
owner: z.string().optional(),
|
|
1073
|
+
summary: z.string().optional(),
|
|
1074
|
+
roadmapRefs: z.array(z.string()).optional(),
|
|
1075
|
+
links: z.array(z.string()).optional(),
|
|
1076
|
+
subState: z.object({
|
|
1077
|
+
phase: z.enum(["discovery", "design", "implementation", "testing"]).optional(),
|
|
1078
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
1079
|
+
estimatedCompletion: z.string().optional(),
|
|
1080
|
+
}).optional(),
|
|
1081
|
+
blocker: z.object({
|
|
1082
|
+
type: z.enum(["internal_dependency", "external_dependency", "resource", "approval"]),
|
|
1083
|
+
description: z.string(),
|
|
1084
|
+
blockingEntity: z.string().optional(),
|
|
1085
|
+
unblockCondition: z.string().optional(),
|
|
1086
|
+
escalationPath: z.string().optional(),
|
|
1087
|
+
}).optional(),
|
|
1088
|
+
}),
|
|
1089
|
+
},
|
|
1090
|
+
}, async ({ projectPath, taskId, updates }) => {
|
|
1091
|
+
if (!isValidTaskId(taskId)) {
|
|
1092
|
+
return {
|
|
1093
|
+
...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={...})`)),
|
|
1094
|
+
isError: true,
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1098
|
+
const { tasksPath, tasks, markdown: tasksMarkdown } = await loadTasksDocument(governanceDir);
|
|
1099
|
+
const taskIndex = tasks.findIndex((item) => item.id === taskId);
|
|
1100
|
+
if (taskIndex === -1) {
|
|
1101
|
+
return {
|
|
1102
|
+
...asText(renderErrorMarkdown("taskUpdate", `Task not found: ${taskId}`, ["run `taskList` to discover available IDs", "retry with an existing task ID"], `taskList(projectPath=\"${toProjectPath(governanceDir)}\")`)),
|
|
1103
|
+
isError: true,
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
const task = tasks[taskIndex];
|
|
1107
|
+
const originalStatus = task.status;
|
|
1108
|
+
// Validate status transition
|
|
1109
|
+
if (updates.status && !validateTransition(originalStatus, updates.status)) {
|
|
1110
|
+
return {
|
|
1111
|
+
...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}\")`)),
|
|
1112
|
+
isError: true,
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
// Apply updates
|
|
1116
|
+
if (updates.status)
|
|
1117
|
+
task.status = updates.status;
|
|
1118
|
+
if (updates.owner !== undefined)
|
|
1119
|
+
task.owner = updates.owner;
|
|
1120
|
+
if (updates.summary !== undefined)
|
|
1121
|
+
task.summary = updates.summary;
|
|
1122
|
+
if (updates.roadmapRefs)
|
|
1123
|
+
task.roadmapRefs = updates.roadmapRefs;
|
|
1124
|
+
if (updates.links)
|
|
1125
|
+
task.links = updates.links;
|
|
1126
|
+
// Handle subState (Spec v1.1.0)
|
|
1127
|
+
if (updates.subState !== undefined) {
|
|
1128
|
+
if (updates.subState === null) {
|
|
1129
|
+
delete task.subState;
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
task.subState = {
|
|
1133
|
+
...(task.subState || {}),
|
|
1134
|
+
...updates.subState,
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// Handle blocker (Spec v1.1.0)
|
|
1139
|
+
if (updates.blocker !== undefined) {
|
|
1140
|
+
if (updates.blocker === null) {
|
|
1141
|
+
delete task.blocker;
|
|
1142
|
+
}
|
|
1143
|
+
else {
|
|
1144
|
+
task.blocker = updates.blocker;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
// Update updatedAt
|
|
1148
|
+
task.updatedAt = nowIso();
|
|
1149
|
+
// Save tasks
|
|
1150
|
+
await saveTasks(tasksPath, tasks);
|
|
1151
|
+
// Build response
|
|
1152
|
+
const updateSummary = [
|
|
1153
|
+
`- taskId: ${taskId}`,
|
|
1154
|
+
`- originalStatus: ${originalStatus}`,
|
|
1155
|
+
`- newStatus: ${task.status}`,
|
|
1156
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
1157
|
+
];
|
|
1158
|
+
if (task.subState) {
|
|
1159
|
+
updateSummary.push(`- subState:`);
|
|
1160
|
+
if (task.subState.phase)
|
|
1161
|
+
updateSummary.push(` - phase: ${task.subState.phase}`);
|
|
1162
|
+
if (typeof task.subState.confidence === "number")
|
|
1163
|
+
updateSummary.push(` - confidence: ${task.subState.confidence}`);
|
|
1164
|
+
if (task.subState.estimatedCompletion)
|
|
1165
|
+
updateSummary.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
1166
|
+
}
|
|
1167
|
+
if (task.blocker) {
|
|
1168
|
+
updateSummary.push(`- blocker:`);
|
|
1169
|
+
updateSummary.push(` - type: ${task.blocker.type}`);
|
|
1170
|
+
updateSummary.push(` - description: ${task.blocker.description}`);
|
|
1171
|
+
if (task.blocker.blockingEntity)
|
|
1172
|
+
updateSummary.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
1173
|
+
if (task.blocker.unblockCondition)
|
|
1174
|
+
updateSummary.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
|
|
1175
|
+
if (task.blocker.escalationPath)
|
|
1176
|
+
updateSummary.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
1177
|
+
}
|
|
1178
|
+
const markdown = renderToolResponseMarkdown({
|
|
1179
|
+
toolName: "taskUpdate",
|
|
1180
|
+
sections: [
|
|
1181
|
+
summarySection(updateSummary),
|
|
1182
|
+
evidenceSection([
|
|
1183
|
+
"### Updated Task",
|
|
1184
|
+
`- ${task.id} | ${task.status} | ${task.title}`,
|
|
1185
|
+
`- owner: ${task.owner || "(none)"}`,
|
|
1186
|
+
`- summary: ${task.summary || "(none)"}`,
|
|
1187
|
+
"",
|
|
1188
|
+
"### Update Details",
|
|
1189
|
+
...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
|
|
1190
|
+
...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
|
|
1191
|
+
...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
|
|
1192
|
+
...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(", ")}`] : []),
|
|
1193
|
+
...(updates.links ? [`- links: ${updates.links.join(", ")}`] : []),
|
|
1194
|
+
...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
|
|
1195
|
+
...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
|
|
1196
|
+
]),
|
|
1197
|
+
guidanceSection([
|
|
1198
|
+
"Task updated successfully. Run `taskContext` to verify the changes.",
|
|
1199
|
+
"If status changed to DONE, ensure evidence links are added.",
|
|
1200
|
+
"If subState or blocker were updated, verify the metadata is correct.",
|
|
1201
|
+
]),
|
|
1202
|
+
lintSection([]),
|
|
1203
|
+
nextCallSection(`taskContext(projectPath=\"${toProjectPath(governanceDir)}\", taskId=\"${taskId}\")`),
|
|
1204
|
+
],
|
|
1205
|
+
});
|
|
1206
|
+
return asText(markdown);
|
|
1207
|
+
});
|
|
1208
|
+
// ============================================================================
|
|
1209
|
+
// Spec v1.1.0 - Confidence Scoring Tools
|
|
1210
|
+
// ============================================================================
|
|
1211
|
+
server.registerTool("taskCalculateConfidence", {
|
|
1212
|
+
title: "Calculate Task Confidence",
|
|
1213
|
+
description: "Calculate confidence score for auto-creating a new task (Spec v1.1.0)",
|
|
1214
|
+
inputSchema: {
|
|
1215
|
+
projectPath: z.string(),
|
|
1216
|
+
candidateTaskSummary: z.string(),
|
|
1217
|
+
contextCompleteness: z.number().min(0).max(1).optional(),
|
|
1218
|
+
similarTaskHistory: z.number().min(0).max(1).optional(),
|
|
1219
|
+
specificationClarity: z.number().min(0).max(1).optional(),
|
|
1220
|
+
},
|
|
1221
|
+
}, async ({ projectPath, candidateTaskSummary, contextCompleteness, similarTaskHistory, specificationClarity }) => {
|
|
1222
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1223
|
+
const { tasks } = await loadTasks(governanceDir);
|
|
1224
|
+
// Calculate factors if not provided
|
|
1225
|
+
const calculatedContextCompleteness = contextCompleteness ?? await calculateContextCompleteness(governanceDir);
|
|
1226
|
+
const calculatedSimilarTaskHistory = similarTaskHistory ?? calculateSimilarTaskHistory(tasks, candidateTaskSummary);
|
|
1227
|
+
const calculatedSpecificationClarity = specificationClarity ?? calculateSpecificationClarity({
|
|
1228
|
+
hasRoadmap: true, // Assume roadmap exists for simplicity
|
|
1229
|
+
hasDesignDocs: true,
|
|
1230
|
+
hasClearAcceptanceCriteria: candidateTaskSummary.length > 50,
|
|
1231
|
+
});
|
|
1232
|
+
const factors = {
|
|
1233
|
+
contextCompleteness: calculatedContextCompleteness,
|
|
1234
|
+
similarTaskHistory: calculatedSimilarTaskHistory,
|
|
1235
|
+
specificationClarity: calculatedSpecificationClarity,
|
|
1236
|
+
};
|
|
1237
|
+
const confidenceScore = calculateConfidenceScore(factors);
|
|
1238
|
+
const validationResult = await runPreCreationValidation(governanceDir, confidenceScore);
|
|
1239
|
+
const hookContent = await getOrCreateTaskAutoCreateValidationHook(governanceDir);
|
|
1240
|
+
const markdown = renderToolResponseMarkdown({
|
|
1241
|
+
toolName: "taskCalculateConfidence",
|
|
1242
|
+
sections: [
|
|
1243
|
+
summarySection([
|
|
1244
|
+
`- governanceDir: ${governanceDir}`,
|
|
1245
|
+
`- confidenceScore: ${(confidenceScore.score * 100).toFixed(0)}%`,
|
|
1246
|
+
`- recommendation: ${confidenceScore.recommendation}`,
|
|
1247
|
+
`- validationPassed: ${validationResult.passed}`,
|
|
1248
|
+
]),
|
|
1249
|
+
evidenceSection([
|
|
1250
|
+
"### Confidence Report",
|
|
1251
|
+
...generateConfidenceReport(confidenceScore).split("\n"),
|
|
1252
|
+
"",
|
|
1253
|
+
"### Validation Issues",
|
|
1254
|
+
...(validationResult.issues.length > 0
|
|
1255
|
+
? validationResult.issues.map(issue => `- ${issue}`)
|
|
1256
|
+
: ["- (none)"]),
|
|
1257
|
+
"",
|
|
1258
|
+
"### Validation Hook",
|
|
1259
|
+
"- hook created/verified at: hooks/task_auto_create_validation.md",
|
|
1260
|
+
]),
|
|
1261
|
+
guidanceSection([
|
|
1262
|
+
confidenceScore.recommendation === "auto_create"
|
|
1263
|
+
? "✅ Confidence is high - you can auto-create this task."
|
|
1264
|
+
: confidenceScore.recommendation === "review_required"
|
|
1265
|
+
? "⚠️ Confidence is medium - review recommended before creating."
|
|
1266
|
+
: "❌ Confidence is low - do not auto-create this task.",
|
|
1267
|
+
"",
|
|
1268
|
+
"### Next Steps",
|
|
1269
|
+
"- If recommendation is auto_create: use taskUpdate or manually add the task",
|
|
1270
|
+
"- If review_required: review the factors and improve context before creating",
|
|
1271
|
+
"- If do_not_create: gather more requirements or context before attempting",
|
|
1272
|
+
]),
|
|
1273
|
+
lintSection([]),
|
|
1274
|
+
nextCallSection(confidenceScore.recommendation !== "do_not_create"
|
|
1275
|
+
? `projectContext(projectPath=\"${toProjectPath(governanceDir)}\")`
|
|
1276
|
+
: undefined),
|
|
1277
|
+
],
|
|
1278
|
+
});
|
|
1279
|
+
return asText(markdown);
|
|
1280
|
+
});
|
|
1281
|
+
server.registerTool("taskCreateValidationHook", {
|
|
1282
|
+
title: "Create Validation Hook",
|
|
1283
|
+
description: "Create or update the task auto-create validation hook (Spec v1.1.0)",
|
|
1284
|
+
inputSchema: {
|
|
1285
|
+
projectPath: z.string(),
|
|
1286
|
+
},
|
|
1287
|
+
}, async ({ projectPath }) => {
|
|
1288
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1289
|
+
const hookContent = await getOrCreateTaskAutoCreateValidationHook(governanceDir);
|
|
1290
|
+
const markdown = renderToolResponseMarkdown({
|
|
1291
|
+
toolName: "taskCreateValidationHook",
|
|
1292
|
+
sections: [
|
|
1293
|
+
summarySection([
|
|
1294
|
+
`- governanceDir: ${governanceDir}`,
|
|
1295
|
+
`- hookPath: ${governanceDir}/hooks/task_auto_create_validation.md`,
|
|
1296
|
+
`- status: created/verified`,
|
|
1297
|
+
]),
|
|
1298
|
+
evidenceSection([
|
|
1299
|
+
"### Hook Content",
|
|
1300
|
+
"```markdown",
|
|
1301
|
+
...hookContent.split("\n"),
|
|
1302
|
+
"```",
|
|
1303
|
+
]),
|
|
1304
|
+
guidanceSection([
|
|
1305
|
+
"Validation hook created successfully.",
|
|
1306
|
+
"Edit the hook file to customize pre-creation and post-creation actions.",
|
|
1307
|
+
"The hook will be used by taskCalculateConfidence for validation.",
|
|
1308
|
+
]),
|
|
1309
|
+
lintSection([]),
|
|
1310
|
+
nextCallSection(`taskCalculateConfidence(projectPath=\"${toProjectPath(governanceDir)}\", candidateTaskSummary=\"Your task summary here\")`),
|
|
1311
|
+
],
|
|
1312
|
+
});
|
|
1313
|
+
return asText(markdown);
|
|
1314
|
+
});
|
|
760
1315
|
}
|