@projitive/mcp 1.0.7 → 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.
Files changed (56) hide show
  1. package/README.md +3 -3
  2. package/output/package.json +4 -1
  3. package/output/source/{helpers/catch → common}/catch.js +6 -6
  4. package/output/source/{helpers/catch → common}/catch.test.js +6 -6
  5. package/output/source/common/confidence.js +231 -0
  6. package/output/source/common/confidence.test.js +205 -0
  7. package/output/source/common/errors.js +120 -0
  8. package/output/source/{helpers/files → common}/files.js +1 -1
  9. package/output/source/{helpers/files → common}/files.test.js +1 -1
  10. package/output/source/common/index.js +10 -0
  11. package/output/source/{helpers/linter/codes.js → common/linter.js} +13 -0
  12. package/output/source/{helpers/markdown → common}/markdown.test.js +1 -1
  13. package/output/source/{helpers/response → common}/response.test.js +1 -1
  14. package/output/source/common/types.js +7 -0
  15. package/output/source/common/utils.js +39 -0
  16. package/output/source/design-context.js +51 -500
  17. package/output/source/index.js +8 -193
  18. package/output/source/index.test.js +116 -0
  19. package/output/source/prompts/index.js +9 -0
  20. package/output/source/prompts/quickStart.js +94 -0
  21. package/output/source/prompts/taskDiscovery.js +190 -0
  22. package/output/source/prompts/taskExecution.js +161 -0
  23. package/output/source/resources/designs.js +108 -0
  24. package/output/source/resources/designs.test.js +154 -0
  25. package/output/source/resources/governance.js +40 -0
  26. package/output/source/resources/index.js +6 -0
  27. package/output/source/resources/readme.test.js +167 -0
  28. package/output/source/{reports.js → resources/reports.js} +5 -3
  29. package/output/source/resources/reports.test.js +149 -0
  30. package/output/source/tools/index.js +8 -0
  31. package/output/source/{projitive.js → tools/project.js} +6 -9
  32. package/output/source/tools/project.test.js +322 -0
  33. package/output/source/{roadmap.js → tools/roadmap.js} +4 -7
  34. package/output/source/tools/roadmap.test.js +103 -0
  35. package/output/source/{tasks.js → tools/task.js} +581 -27
  36. package/output/source/tools/task.test.js +473 -0
  37. package/output/source/types.js +67 -0
  38. package/package.json +4 -1
  39. package/output/source/designs.js +0 -38
  40. package/output/source/helpers/artifacts/index.js +0 -1
  41. package/output/source/helpers/catch/index.js +0 -1
  42. package/output/source/helpers/files/index.js +0 -1
  43. package/output/source/helpers/index.js +0 -6
  44. package/output/source/helpers/linter/index.js +0 -2
  45. package/output/source/helpers/linter/linter.js +0 -6
  46. package/output/source/helpers/markdown/index.js +0 -1
  47. package/output/source/helpers/response/index.js +0 -1
  48. package/output/source/projitive.test.js +0 -111
  49. package/output/source/roadmap.test.js +0 -11
  50. package/output/source/tasks.test.js +0 -152
  51. /package/output/source/{helpers/artifacts → common}/artifacts.js +0 -0
  52. /package/output/source/{helpers/artifacts → common}/artifacts.test.js +0 -0
  53. /package/output/source/{helpers/linter → common}/linter.test.js +0 -0
  54. /package/output/source/{helpers/markdown → common}/markdown.js +0 -0
  55. /package/output/source/{helpers/response → common}/response.js +0 -0
  56. /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 "./helpers/artifacts/index.js";
5
- import { discoverGovernanceArtifacts } from "./helpers/files/index.js";
6
- import { findTextReferences } from "./helpers/markdown/index.js";
7
- import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from "./helpers/response/index.js";
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
- return {
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 (const line of lines.slice(1)) {
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
- return [
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
- ].join("\n");
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
- "本文件由 Projitive MCP 维护,手动编辑请保持 Markdown 结构合法。",
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)"]),
@@ -545,12 +818,11 @@ export function registerTaskTools(server) {
545
818
  title: "Task Next",
546
819
  description: "Start here to auto-select the highest-priority actionable task",
547
820
  inputSchema: {
548
- maxDepth: z.number().int().min(0).max(8).optional(),
549
- topCandidates: z.number().int().min(1).max(20).optional(),
821
+ limit: z.number().int().min(1).max(20).optional(),
550
822
  },
551
- }, async ({ maxDepth, topCandidates }) => {
823
+ }, async ({ limit }) => {
552
824
  const root = resolveScanRoot();
553
- const depth = resolveScanDepth(maxDepth);
825
+ const depth = resolveScanDepth();
554
826
  const projects = await discoverProjects(root, depth);
555
827
  const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
556
828
  if (rankedCandidates.length === 0) {
@@ -624,7 +896,7 @@ export function registerTaskTools(server) {
624
896
  const taskLocation = (await findTextReferences(selected.tasksPath, selected.task.id))[0];
625
897
  const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
626
898
  const suggestedReadOrder = [selected.tasksPath, ...relatedArtifacts.filter((item) => item !== selected.tasksPath)];
627
- const candidateLimit = topCandidates ?? 5;
899
+ const candidateLimit = limit ?? 5;
628
900
  const markdown = renderToolResponseMarkdown({
629
901
  toolName: "taskNext",
630
902
  sections: [
@@ -720,19 +992,49 @@ export function registerTaskTools(server) {
720
992
  const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
721
993
  const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
722
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
+ }
723
1034
  const coreMarkdown = renderToolResponseMarkdown({
724
1035
  toolName: "taskContext",
725
1036
  sections: [
726
- summarySection([
727
- `- governanceDir: ${governanceDir}`,
728
- `- taskId: ${task.id}`,
729
- `- title: ${task.title}`,
730
- `- status: ${task.status}`,
731
- `- owner: ${task.owner}`,
732
- `- updatedAt: ${task.updatedAt}`,
733
- `- roadmapRefs: ${task.roadmapRefs.join(", ") || "(none)"}`,
734
- `- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : tasksPath}`,
735
- ]),
1037
+ summarySection(summaryLines),
736
1038
  evidenceSection([
737
1039
  "### Related Artifacts",
738
1040
  ...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ["- (none)"]),
@@ -758,4 +1060,256 @@ export function registerTaskTools(server) {
758
1060
  });
759
1061
  return asText(coreMarkdown);
760
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
+ });
761
1315
  }