@projitive/mcp 2.0.4 → 2.1.0
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 +1 -1
- package/output/source/common/errors.test.js +59 -0
- package/output/source/common/files.js +20 -9
- package/output/source/common/index.js +1 -0
- package/output/source/common/linter.js +3 -1
- package/output/source/common/response.js +51 -67
- package/output/source/common/tool.js +43 -0
- package/output/source/common/utils.test.js +48 -0
- package/output/source/index.runtime.test.js +57 -0
- package/output/source/prompts/index.test.js +23 -0
- package/output/source/prompts/quickStart.test.js +24 -0
- package/output/source/prompts/taskDiscovery.test.js +24 -0
- package/output/source/prompts/taskExecution.js +17 -1
- package/output/source/prompts/taskExecution.test.js +27 -0
- package/output/source/resources/designs.resources.test.js +52 -0
- package/output/source/resources/governance.test.js +35 -0
- package/output/source/resources/index.test.js +18 -0
- package/output/source/tools/index.test.js +23 -0
- package/output/source/tools/project.js +210 -257
- package/output/source/tools/project.test.js +136 -4
- package/output/source/tools/roadmap.js +182 -216
- package/output/source/tools/roadmap.test.js +187 -0
- package/output/source/tools/task.js +598 -508
- package/output/source/tools/task.test.js +323 -2
- package/output/source/types.js +6 -0
- package/package.json +1 -1
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, findTextReferences, ensureStore, loadActionableTasksFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, replaceTasksInStore, upsertTaskInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, } from '../common/index.js';
|
|
5
|
-
import { asText, evidenceSection, guidanceSection, lintSection, nextCallSection, renderErrorMarkdown, renderToolResponseMarkdown, summarySection, } from '../common/index.js';
|
|
6
|
-
import { TASK_LINT_CODES, renderLintSuggestions } from '../common/index.js';
|
|
4
|
+
import { candidateFilesFromArtifacts, discoverGovernanceArtifacts, findTextReferences, ensureStore, loadActionableTasksFromStore, loadRoadmapsFromStore, loadTaskStatusStatsFromStore, loadTasksFromStore, replaceTasksInStore, upsertTaskInStore, getStoreVersion, getMarkdownViewState, markMarkdownViewBuilt, createGovernedTool, ToolExecutionError, PROJECT_LINT_CODES, TASK_LINT_CODES, renderLintSuggestions, } from '../common/index.js';
|
|
7
5
|
import { resolveGovernanceDir, resolveScanDepth, resolveScanRoots, discoverProjectsAcrossRoots, toProjectPath } from './project.js';
|
|
8
6
|
import { isValidRoadmapId } from './roadmap.js';
|
|
9
7
|
import { SUB_STATE_PHASES, BLOCKER_TYPES } from '../types.js';
|
|
10
8
|
export const ALLOWED_STATUS = ['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE'];
|
|
11
9
|
export const TASK_ID_REGEX = /^TASK-(\d+)$/;
|
|
12
10
|
export const TASKS_MARKDOWN_FILE = 'tasks.md';
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
export const TASK_RESEARCH_DIR = 'designs/research';
|
|
12
|
+
export const TASK_RESEARCH_FILE_SUFFIX = '.implementation-research.md';
|
|
13
|
+
export const CORE_DESIGN_DOCS_DIR = 'designs/core';
|
|
14
|
+
export const CORE_ARCHITECTURE_DOC_FILE = `${CORE_DESIGN_DOCS_DIR}/architecture.md`;
|
|
15
|
+
export const CORE_STYLE_DOC_FILE = `${CORE_DESIGN_DOCS_DIR}/style-guide.md`;
|
|
16
16
|
function taskStatusGuidance(task) {
|
|
17
17
|
if (task.status === 'TODO') {
|
|
18
18
|
return [
|
|
@@ -46,6 +46,7 @@ const DEFAULT_NO_TASK_DISCOVERY_GUIDANCE = [
|
|
|
46
46
|
'- Create TODO tasks only when evidence is clear: each new task must produce at least one report/designs/readme artifact update.',
|
|
47
47
|
'- Skip duplicate scope: do not create tasks that overlap existing TODO/IN_PROGRESS/BLOCKED task intent.',
|
|
48
48
|
'- Use quality gates for discovery candidates: user value, delivery risk reduction, or measurable throughput improvement.',
|
|
49
|
+
'- Review and update project architecture docs under designs/core/ (architecture.md, style-guide.md) if they are missing or outdated.',
|
|
49
50
|
'- Keep each discovery round small (1-3 tasks), then rerun taskNext immediately for re-ranking and execution.',
|
|
50
51
|
];
|
|
51
52
|
const DEFAULT_TASK_CONTEXT_READING_GUIDANCE = [
|
|
@@ -113,6 +114,87 @@ function normalizeTaskLink(link) {
|
|
|
113
114
|
function resolveTaskLinkPath(projectPath, link) {
|
|
114
115
|
return path.join(projectPath, link);
|
|
115
116
|
}
|
|
117
|
+
function taskResearchBriefRelativePath(taskId) {
|
|
118
|
+
return `${TASK_RESEARCH_DIR}/${taskId}${TASK_RESEARCH_FILE_SUFFIX}`;
|
|
119
|
+
}
|
|
120
|
+
function renderTaskResearchBriefTemplate(task) {
|
|
121
|
+
return [
|
|
122
|
+
`# ${task.id} Implementation Research Brief`,
|
|
123
|
+
'',
|
|
124
|
+
`Task: ${task.title}`,
|
|
125
|
+
`Summary: ${task.summary || '(fill this with a short objective summary)'}`,
|
|
126
|
+
'',
|
|
127
|
+
'## Design Guidelines and Specs',
|
|
128
|
+
'- [ ] List relevant design/governance/spec files with line location',
|
|
129
|
+
'- Example: designs/ARCHITECTURE.md#L42-L76 - API boundary and constraints',
|
|
130
|
+
'- Example: roadmap.md#L18 - milestone acceptance criteria',
|
|
131
|
+
'',
|
|
132
|
+
'## Code Architecture and Implementation Findings',
|
|
133
|
+
'- [ ] Document current architecture and extension points with line location',
|
|
134
|
+
'- Example: packages/mcp/source/tools/task.ts#L1020-L1130 - taskContext response assembly',
|
|
135
|
+
'- Example: packages/mcp/source/prompts/taskExecution.ts#L25-L130 - execution workflow prompt',
|
|
136
|
+
'',
|
|
137
|
+
'## Implementation Plan',
|
|
138
|
+
'- [ ] Proposed change list with impacted modules',
|
|
139
|
+
'- [ ] Validation and regression test plan',
|
|
140
|
+
'',
|
|
141
|
+
'## Risks and Open Questions',
|
|
142
|
+
'- [ ] Known risks, assumptions, and unresolved questions',
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
async function inspectTaskResearchBrief(governanceDir, task) {
|
|
146
|
+
const projectPath = toProjectPath(governanceDir);
|
|
147
|
+
const relativePath = taskResearchBriefRelativePath(task.id);
|
|
148
|
+
const absolutePath = resolveTaskLinkPath(projectPath, relativePath);
|
|
149
|
+
const exists = await fs.access(absolutePath).then(() => true).catch(() => false);
|
|
150
|
+
return { relativePath, absolutePath, exists, ready: exists };
|
|
151
|
+
}
|
|
152
|
+
function collectTaskResearchBriefLintSuggestions(state) {
|
|
153
|
+
if (!state.exists) {
|
|
154
|
+
return [{
|
|
155
|
+
code: TASK_LINT_CODES.RESEARCH_BRIEF_MISSING,
|
|
156
|
+
message: `Pre-execution research brief missing: ${state.relativePath}.`,
|
|
157
|
+
fixHint: 'Create the file and fill required sections before implementation.',
|
|
158
|
+
}];
|
|
159
|
+
}
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
function inspectProjectContextDocsFromArtifacts(files) {
|
|
163
|
+
const markdownFiles = files
|
|
164
|
+
.map((item) => item.replace(/\\/g, '/'))
|
|
165
|
+
.filter((item) => item.toLowerCase().endsWith('.md'));
|
|
166
|
+
const architectureDocSuffix = `/${CORE_ARCHITECTURE_DOC_FILE}`.toLowerCase();
|
|
167
|
+
const styleDocSuffix = `/${CORE_STYLE_DOC_FILE}`.toLowerCase();
|
|
168
|
+
const architectureDocs = markdownFiles.filter((item) => item.toLowerCase().endsWith(architectureDocSuffix));
|
|
169
|
+
const styleDocs = markdownFiles.filter((item) => item.toLowerCase().endsWith(styleDocSuffix));
|
|
170
|
+
const missingArchitectureDocs = architectureDocs.length === 0;
|
|
171
|
+
const missingStyleDocs = styleDocs.length === 0;
|
|
172
|
+
return {
|
|
173
|
+
architectureDocs,
|
|
174
|
+
styleDocs,
|
|
175
|
+
missingArchitectureDocs,
|
|
176
|
+
missingStyleDocs,
|
|
177
|
+
ready: !missingArchitectureDocs && !missingStyleDocs,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function collectProjectContextDocsLintSuggestions(state) {
|
|
181
|
+
const suggestions = [];
|
|
182
|
+
if (state.missingArchitectureDocs) {
|
|
183
|
+
suggestions.push({
|
|
184
|
+
code: PROJECT_LINT_CODES.ARCHITECTURE_DOC_MISSING,
|
|
185
|
+
message: 'Project context is missing architecture design documentation.',
|
|
186
|
+
fixHint: `Add required file: ${CORE_ARCHITECTURE_DOC_FILE}.`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (state.missingStyleDocs) {
|
|
190
|
+
suggestions.push({
|
|
191
|
+
code: PROJECT_LINT_CODES.STYLE_DOC_MISSING,
|
|
192
|
+
message: 'Project context is missing design style documentation.',
|
|
193
|
+
fixHint: `Add required file: ${CORE_STYLE_DOC_FILE}.`,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return suggestions;
|
|
197
|
+
}
|
|
116
198
|
async function readActionableTaskCandidates(governanceDirs) {
|
|
117
199
|
const snapshots = await Promise.all(governanceDirs.map(async (governanceDir) => {
|
|
118
200
|
const tasksPath = path.join(governanceDir, '.projitive');
|
|
@@ -382,7 +464,7 @@ function collectTaskLintSuggestionItems(tasks) {
|
|
|
382
464
|
export function collectTaskLintSuggestions(tasks) {
|
|
383
465
|
return renderLintSuggestions(collectTaskLintSuggestionItems(tasks));
|
|
384
466
|
}
|
|
385
|
-
function
|
|
467
|
+
function collectSingleTaskLintSuggestionItems(task) {
|
|
386
468
|
const suggestions = [];
|
|
387
469
|
if (task.status === 'IN_PROGRESS' && task.owner.trim().length === 0) {
|
|
388
470
|
suggestions.push({
|
|
@@ -478,37 +560,21 @@ function collectSingleTaskLintSuggestions(task) {
|
|
|
478
560
|
fixHint: 'Confidence must be between 0.0 and 1.0.',
|
|
479
561
|
});
|
|
480
562
|
}
|
|
481
|
-
return
|
|
563
|
+
return suggestions;
|
|
482
564
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
code: TASK_LINT_CODES.LINK_PATH_FORMAT_INVALID,
|
|
497
|
-
message: `Link path should be project-root-relative without leading slash: ${normalized}.`,
|
|
498
|
-
fixHint: 'Use path/from/project/root format.',
|
|
499
|
-
});
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
const resolvedPath = resolveTaskLinkPath(projectPath, normalized);
|
|
503
|
-
const exists = await fs.access(resolvedPath).then(() => true).catch(() => false);
|
|
504
|
-
if (!exists) {
|
|
505
|
-
suggestions.push({
|
|
506
|
-
code: TASK_LINT_CODES.LINK_TARGET_MISSING,
|
|
507
|
-
message: `Link target not found: ${normalized} (resolved: ${resolvedPath}).`,
|
|
508
|
-
});
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
return renderLintSuggestions(suggestions);
|
|
565
|
+
function collectSingleTaskLintSuggestions(task) {
|
|
566
|
+
return renderLintSuggestions(collectSingleTaskLintSuggestionItems(task));
|
|
567
|
+
}
|
|
568
|
+
async function collectDoneConformanceSuggestions(governanceDir, task) {
|
|
569
|
+
const researchBriefState = await inspectTaskResearchBrief(governanceDir, task);
|
|
570
|
+
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
571
|
+
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
572
|
+
const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
|
|
573
|
+
return [
|
|
574
|
+
...collectSingleTaskLintSuggestionItems(task),
|
|
575
|
+
...collectTaskResearchBriefLintSuggestions(researchBriefState),
|
|
576
|
+
...collectProjectContextDocsLintSuggestions(projectContextDocsState),
|
|
577
|
+
];
|
|
512
578
|
}
|
|
513
579
|
export function renderTasksMarkdown(tasks) {
|
|
514
580
|
const sections = sortTasksNewestFirst(tasks).map((task) => {
|
|
@@ -610,7 +676,8 @@ export function validateTransition(from, to) {
|
|
|
610
676
|
return allowed[from].has(to);
|
|
611
677
|
}
|
|
612
678
|
export function registerTaskTools(server) {
|
|
613
|
-
server.registerTool(
|
|
679
|
+
server.registerTool(...createGovernedTool({
|
|
680
|
+
name: 'taskList',
|
|
614
681
|
title: 'Task List',
|
|
615
682
|
description: 'List tasks for a known project and optionally filter by status',
|
|
616
683
|
inputSchema: {
|
|
@@ -618,50 +685,48 @@ export function registerTaskTools(server) {
|
|
|
618
685
|
status: z.enum(['TODO', 'IN_PROGRESS', 'BLOCKED', 'DONE']).optional(),
|
|
619
686
|
limit: z.number().int().min(1).max(200).optional(),
|
|
620
687
|
},
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
});
|
|
664
|
-
server.registerTool('taskCreate', {
|
|
688
|
+
async execute({ projectPath, status, limit }) {
|
|
689
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
690
|
+
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
691
|
+
const { tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
|
|
692
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
693
|
+
const filtered = tasks
|
|
694
|
+
.filter((task) => (status ? task.status === status : true))
|
|
695
|
+
.slice(0, limit ?? 100);
|
|
696
|
+
return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, filtered, status };
|
|
697
|
+
},
|
|
698
|
+
summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, filtered, status }) => [
|
|
699
|
+
`- projectPath: ${normalizedProjectPath}`,
|
|
700
|
+
`- governanceDir: ${governanceDir}`,
|
|
701
|
+
`- tasksView: ${tasksViewPath}`,
|
|
702
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
703
|
+
`- filter.status: ${status ?? '(none)'}`,
|
|
704
|
+
`- returned: ${filtered.length}`,
|
|
705
|
+
],
|
|
706
|
+
evidence: ({ filtered }) => [
|
|
707
|
+
'- tasks:',
|
|
708
|
+
...filtered.map((task) => `- ${task.id} | ${task.status} | ${task.title} | owner=${task.owner || ''} | updatedAt=${task.updatedAt}`),
|
|
709
|
+
],
|
|
710
|
+
guidance: () => ['- Pick one task ID and call `taskContext`.'],
|
|
711
|
+
suggestions: ({ filtered, status }) => {
|
|
712
|
+
const suggestions = collectTaskLintSuggestions(filtered);
|
|
713
|
+
if (status && filtered.length === 0) {
|
|
714
|
+
suggestions.push(...renderLintSuggestions([
|
|
715
|
+
{
|
|
716
|
+
code: TASK_LINT_CODES.FILTER_EMPTY,
|
|
717
|
+
message: `No tasks matched status=${status}.`,
|
|
718
|
+
fixHint: 'Confirm status values or update task states.',
|
|
719
|
+
},
|
|
720
|
+
]));
|
|
721
|
+
}
|
|
722
|
+
return suggestions;
|
|
723
|
+
},
|
|
724
|
+
nextCall: ({ filtered, normalizedProjectPath }) => filtered[0]
|
|
725
|
+
? `taskContext(projectPath="${normalizedProjectPath}", taskId="${filtered[0].id}")`
|
|
726
|
+
: undefined,
|
|
727
|
+
}));
|
|
728
|
+
server.registerTool(...createGovernedTool({
|
|
729
|
+
name: 'taskCreate',
|
|
665
730
|
title: 'Task Create',
|
|
666
731
|
description: 'Create a new task in governance store with stable TASK-<number> ID',
|
|
667
732
|
inputSchema: {
|
|
@@ -686,322 +751,379 @@ export function registerTaskTools(server) {
|
|
|
686
751
|
escalationPath: z.string().optional(),
|
|
687
752
|
}).optional(),
|
|
688
753
|
},
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
`- links: ${createdTask.links.join(', ') || '(none)'}`,
|
|
745
|
-
]),
|
|
746
|
-
guidanceSection([
|
|
747
|
-
'Task created in governance store successfully and tasks.md has been synced.',
|
|
748
|
-
'Run taskContext to verify references and lint guidance.',
|
|
749
|
-
]),
|
|
750
|
-
lintSection(lintSuggestions),
|
|
751
|
-
nextCallSection(`taskContext(projectPath="${normalizedProjectPath}", taskId="${createdTask.id}")`),
|
|
752
|
-
],
|
|
753
|
-
});
|
|
754
|
-
return asText(markdown);
|
|
755
|
-
});
|
|
756
|
-
server.registerTool('taskNext', {
|
|
754
|
+
async execute({ projectPath, taskId, title, status, owner, summary, roadmapRefs, links, subState, blocker }) {
|
|
755
|
+
if (taskId && !isValidTaskId(taskId)) {
|
|
756
|
+
throw new ToolExecutionError(`Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'omit taskId to auto-generate next ID'], `taskCreate(projectPath="${projectPath}", title="Define executable objective")`);
|
|
757
|
+
}
|
|
758
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
759
|
+
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
760
|
+
const { tasksPath, tasks, markdownPath: tasksViewPath } = await loadTasksDocument(governanceDir);
|
|
761
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
762
|
+
const finalTaskId = taskId ?? nextTaskId(tasks);
|
|
763
|
+
const duplicated = tasks.some((item) => item.id === finalTaskId);
|
|
764
|
+
if (duplicated) {
|
|
765
|
+
throw new ToolExecutionError(`Task already exists: ${finalTaskId}`, ['task IDs must be unique', 'use taskUpdate for existing tasks'], `taskUpdate(projectPath="${normalizedProjectPath}", taskId="${finalTaskId}", updates={...})`);
|
|
766
|
+
}
|
|
767
|
+
const createdTask = normalizeTask({
|
|
768
|
+
id: finalTaskId,
|
|
769
|
+
title,
|
|
770
|
+
status: status ?? 'TODO',
|
|
771
|
+
owner,
|
|
772
|
+
summary,
|
|
773
|
+
roadmapRefs,
|
|
774
|
+
links,
|
|
775
|
+
subState,
|
|
776
|
+
blocker,
|
|
777
|
+
updatedAt: nowIso(),
|
|
778
|
+
});
|
|
779
|
+
await upsertTaskInStore(tasksPath, createdTask);
|
|
780
|
+
await loadTasksDocumentWithOptions(governanceDir, true);
|
|
781
|
+
return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, createdTask };
|
|
782
|
+
},
|
|
783
|
+
summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, createdTask }) => [
|
|
784
|
+
`- projectPath: ${normalizedProjectPath}`,
|
|
785
|
+
`- governanceDir: ${governanceDir}`,
|
|
786
|
+
`- tasksView: ${tasksViewPath}`,
|
|
787
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
788
|
+
`- taskId: ${createdTask.id}`,
|
|
789
|
+
`- status: ${createdTask.status}`,
|
|
790
|
+
`- owner: ${createdTask.owner || '(none)'}`,
|
|
791
|
+
`- updatedAt: ${createdTask.updatedAt}`,
|
|
792
|
+
],
|
|
793
|
+
evidence: ({ createdTask }) => [
|
|
794
|
+
'### Created Task',
|
|
795
|
+
`- ${createdTask.id} | ${createdTask.status} | ${createdTask.title}`,
|
|
796
|
+
`- summary: ${createdTask.summary || '(none)'}`,
|
|
797
|
+
`- roadmapRefs: ${createdTask.roadmapRefs.join(', ') || '(none)'}`,
|
|
798
|
+
`- links: ${createdTask.links.join(', ') || '(none)'}`,
|
|
799
|
+
],
|
|
800
|
+
guidance: () => [
|
|
801
|
+
'Task created in governance store successfully and tasks.md has been synced.',
|
|
802
|
+
'Run taskContext to verify references and lint guidance.',
|
|
803
|
+
],
|
|
804
|
+
suggestions: ({ createdTask }) => collectSingleTaskLintSuggestions(createdTask),
|
|
805
|
+
nextCall: ({ normalizedProjectPath, createdTask }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${createdTask.id}")`,
|
|
806
|
+
}));
|
|
807
|
+
server.registerTool(...createGovernedTool({
|
|
808
|
+
name: 'taskNext',
|
|
757
809
|
title: 'Task Next',
|
|
758
810
|
description: 'Start here to auto-select the highest-priority actionable task',
|
|
759
811
|
inputSchema: {
|
|
760
812
|
limit: z.number().int().min(1).max(20).optional(),
|
|
761
813
|
},
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const
|
|
784
|
-
const
|
|
785
|
-
const
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
});
|
|
827
|
-
return asText(markdown);
|
|
828
|
-
}
|
|
829
|
-
const selected = rankedCandidates[0];
|
|
830
|
-
const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
|
|
831
|
-
const lintSuggestions = collectTaskLintSuggestions(selectedTaskDocument.tasks);
|
|
832
|
-
const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
|
|
833
|
-
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
834
|
-
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
|
|
835
|
-
const taskLocation = (await findTextReferences(selectedTaskDocument.markdownPath, selected.task.id))[0];
|
|
836
|
-
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
837
|
-
const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
|
|
838
|
-
const candidateLimit = limit ?? 5;
|
|
839
|
-
const markdown = renderToolResponseMarkdown({
|
|
840
|
-
toolName: 'taskNext',
|
|
841
|
-
sections: [
|
|
842
|
-
summarySection([
|
|
843
|
-
`- rootPaths: ${roots.join(', ')}`,
|
|
844
|
-
`- rootCount: ${roots.length}`,
|
|
845
|
-
`- maxDepth: ${depth}`,
|
|
846
|
-
`- matchedProjects: ${projects.length}`,
|
|
847
|
-
`- actionableTasks: ${rankedCandidates.length}`,
|
|
848
|
-
`- selectedProject: ${toProjectPath(selected.governanceDir)}`,
|
|
849
|
-
`- selectedTaskId: ${selected.task.id}`,
|
|
850
|
-
`- selectedTaskStatus: ${selected.task.status}`,
|
|
851
|
-
]),
|
|
852
|
-
evidenceSection([
|
|
853
|
-
'### Selected Task',
|
|
854
|
-
`- id: ${selected.task.id}`,
|
|
855
|
-
`- title: ${selected.task.title}`,
|
|
856
|
-
`- owner: ${selected.task.owner || '(none)'}`,
|
|
857
|
-
`- updatedAt: ${selected.task.updatedAt}`,
|
|
858
|
-
`- roadmapRefs: ${selected.task.roadmapRefs.join(', ') || '(none)'}`,
|
|
859
|
-
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : selectedTaskDocument.markdownPath}`,
|
|
860
|
-
'',
|
|
861
|
-
'### Top Candidates',
|
|
862
|
-
...rankedCandidates
|
|
863
|
-
.slice(0, candidateLimit)
|
|
864
|
-
.map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | projectPath=${toProjectPath(item.governanceDir)} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
|
|
865
|
-
'',
|
|
866
|
-
'### Selection Reason',
|
|
867
|
-
'- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.',
|
|
868
|
-
`- Selected candidate scores: projectScore=${selected.projectScore}, taskPriority=${selected.taskPriority}, taskUpdatedAtMs=${selected.taskUpdatedAtMs}.`,
|
|
814
|
+
async execute({ limit }) {
|
|
815
|
+
const roots = resolveScanRoots();
|
|
816
|
+
const depth = resolveScanDepth();
|
|
817
|
+
const projects = await discoverProjectsAcrossRoots(roots, depth);
|
|
818
|
+
const rankedCandidates = rankActionableTaskCandidates(await readActionableTaskCandidates(projects));
|
|
819
|
+
if (rankedCandidates.length === 0) {
|
|
820
|
+
const projectSnapshots = await Promise.all(projects.map(async (governanceDir) => {
|
|
821
|
+
const tasksPath = path.join(governanceDir, '.projitive');
|
|
822
|
+
await ensureStore(tasksPath);
|
|
823
|
+
const stats = await loadTaskStatusStatsFromStore(tasksPath);
|
|
824
|
+
const roadmapIds = await readRoadmapIds(governanceDir);
|
|
825
|
+
return { governanceDir, roadmapIds, total: stats.total, todo: stats.todo, inProgress: stats.inProgress, blocked: stats.blocked, done: stats.done };
|
|
826
|
+
}));
|
|
827
|
+
const preferredProject = projectSnapshots[0];
|
|
828
|
+
const preferredRoadmapRef = preferredProject?.roadmapIds[0] ?? 'ROADMAP-0001';
|
|
829
|
+
const noTaskDiscoveryGuidance = await resolveNoTaskDiscoveryGuidance(preferredProject?.governanceDir);
|
|
830
|
+
return { isEmpty: true, roots, depth, projects, projectSnapshots, preferredProject, preferredRoadmapRef, noTaskDiscoveryGuidance };
|
|
831
|
+
}
|
|
832
|
+
const selected = rankedCandidates[0];
|
|
833
|
+
const selectedTaskDocument = await loadTasksDocument(selected.governanceDir);
|
|
834
|
+
const artifacts = await discoverGovernanceArtifacts(selected.governanceDir);
|
|
835
|
+
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
836
|
+
const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
|
|
837
|
+
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, selected.task.id)))).flat();
|
|
838
|
+
const taskLocation = (await findTextReferences(selectedTaskDocument.markdownPath, selected.task.id))[0];
|
|
839
|
+
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
840
|
+
const suggestedReadOrder = [selectedTaskDocument.markdownPath, ...relatedArtifacts.filter((item) => item !== selectedTaskDocument.markdownPath)];
|
|
841
|
+
const candidateLimit = limit ?? 5;
|
|
842
|
+
return {
|
|
843
|
+
isEmpty: false,
|
|
844
|
+
roots, depth, projects,
|
|
845
|
+
rankedCandidates, selected, selectedTaskDocument,
|
|
846
|
+
relatedArtifacts, referenceLocations,
|
|
847
|
+
suggestedReadOrder, projectContextDocsState, taskLocation, candidateLimit,
|
|
848
|
+
};
|
|
849
|
+
},
|
|
850
|
+
summary: (data) => {
|
|
851
|
+
if (data.isEmpty) {
|
|
852
|
+
return [
|
|
853
|
+
`- rootPaths: ${data.roots.join(', ')}`,
|
|
854
|
+
`- rootCount: ${data.roots.length}`,
|
|
855
|
+
`- maxDepth: ${data.depth}`,
|
|
856
|
+
`- matchedProjects: ${data.projects.length}`,
|
|
857
|
+
'- actionableTasks: 0',
|
|
858
|
+
];
|
|
859
|
+
}
|
|
860
|
+
return [
|
|
861
|
+
`- rootPaths: ${data.roots.join(', ')}`,
|
|
862
|
+
`- rootCount: ${data.roots.length}`,
|
|
863
|
+
`- maxDepth: ${data.depth}`,
|
|
864
|
+
`- matchedProjects: ${data.projects.length}`,
|
|
865
|
+
`- actionableTasks: ${data.rankedCandidates.length}`,
|
|
866
|
+
`- selectedProject: ${toProjectPath(data.selected.governanceDir)}`,
|
|
867
|
+
`- selectedTaskId: ${data.selected.task.id}`,
|
|
868
|
+
`- selectedTaskStatus: ${data.selected.task.status}`,
|
|
869
|
+
];
|
|
870
|
+
},
|
|
871
|
+
evidence: (data) => {
|
|
872
|
+
if (data.isEmpty) {
|
|
873
|
+
return [
|
|
874
|
+
'### Project Snapshots',
|
|
875
|
+
...(data.projectSnapshots.length > 0
|
|
876
|
+
? data.projectSnapshots.map((item, index) => `${index + 1}. ${toProjectPath(item.governanceDir)} | total=${item.total} | todo=${item.todo} | in_progress=${item.inProgress} | blocked=${item.blocked} | done=${item.done} | roadmapIds=${item.roadmapIds.join(', ') || '(none)'}`)
|
|
877
|
+
: ['- (none)']),
|
|
869
878
|
'',
|
|
870
|
-
'###
|
|
871
|
-
...(
|
|
879
|
+
'### Seed Task Template',
|
|
880
|
+
...renderTaskSeedTemplate(data.preferredRoadmapRef),
|
|
881
|
+
];
|
|
882
|
+
}
|
|
883
|
+
const { taskLocation, selectedTaskDocument, rankedCandidates, candidateLimit, relatedArtifacts, referenceLocations, suggestedReadOrder } = data;
|
|
884
|
+
const taskLocationStr = taskLocation
|
|
885
|
+
? `${taskLocation.filePath}#L${taskLocation.line}`
|
|
886
|
+
: selectedTaskDocument.markdownPath;
|
|
887
|
+
return [
|
|
888
|
+
'### Selected Task',
|
|
889
|
+
`- id: ${data.selected.task.id}`,
|
|
890
|
+
`- title: ${data.selected.task.title}`,
|
|
891
|
+
`- owner: ${data.selected.task.owner || '(none)'}`,
|
|
892
|
+
`- updatedAt: ${data.selected.task.updatedAt}`,
|
|
893
|
+
`- roadmapRefs: ${data.selected.task.roadmapRefs.join(', ') || '(none)'}`,
|
|
894
|
+
`- taskLocation: ${taskLocationStr}`,
|
|
895
|
+
'',
|
|
896
|
+
'### Top Candidates',
|
|
897
|
+
...rankedCandidates
|
|
898
|
+
.slice(0, candidateLimit)
|
|
899
|
+
.map((item, index) => `${index + 1}. ${item.task.id} | ${item.task.status} | ${item.task.title} | projectPath=${toProjectPath(item.governanceDir)} | projectScore=${item.projectScore} | latest=${item.projectLatestUpdatedAt}`),
|
|
900
|
+
'',
|
|
901
|
+
'### Selection Reason',
|
|
902
|
+
'- Rank rule: projectScore DESC -> taskPriority DESC -> taskUpdatedAt DESC.',
|
|
903
|
+
`- Selected candidate scores: projectScore=${data.selected.projectScore}, taskPriority=${data.selected.taskPriority}, taskUpdatedAtMs=${data.selected.taskUpdatedAtMs}.`,
|
|
904
|
+
'',
|
|
905
|
+
'### Related Artifacts',
|
|
906
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
|
|
907
|
+
'',
|
|
908
|
+
'### Reference Locations',
|
|
909
|
+
...(referenceLocations.length > 0
|
|
910
|
+
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
911
|
+
: ['- (none)']),
|
|
912
|
+
'',
|
|
913
|
+
'### Suggested Read Order',
|
|
914
|
+
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
915
|
+
];
|
|
916
|
+
},
|
|
917
|
+
guidance: (data) => {
|
|
918
|
+
if (data.isEmpty) {
|
|
919
|
+
return [
|
|
920
|
+
'- No TODO/IN_PROGRESS task is available.',
|
|
921
|
+
'- Create 1-3 new TODO tasks using `taskCreate(...)` from active roadmap slices.',
|
|
922
|
+
'- Use no-task discovery checklist below to proactively find and create meaningful TODO tasks.',
|
|
923
|
+
'- If roadmap has active milestones, analyze milestone intent and split into 1-3 executable TODO tasks.',
|
|
872
924
|
'',
|
|
873
|
-
'###
|
|
874
|
-
...
|
|
875
|
-
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
876
|
-
: ['- (none)']),
|
|
925
|
+
'### No-Task Discovery Checklist',
|
|
926
|
+
...data.noTaskDiscoveryGuidance,
|
|
877
927
|
'',
|
|
878
|
-
'
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
928
|
+
'- If no tasks exist, derive 1-3 TODO tasks from roadmap milestones, README scope, or unresolved report gaps.',
|
|
929
|
+
'- If only BLOCKED/DONE tasks exist, reopen one blocked item or create a follow-up TODO task.',
|
|
930
|
+
'- After creating tasks, rerun `taskNext` to re-rank actionable work.',
|
|
931
|
+
];
|
|
932
|
+
}
|
|
933
|
+
return [
|
|
934
|
+
...(!data.projectContextDocsState.ready
|
|
935
|
+
? [
|
|
936
|
+
'- Project context docs are incomplete. Complete missing project architecture/style docs before deep implementation.',
|
|
937
|
+
...(data.projectContextDocsState.missingArchitectureDocs
|
|
938
|
+
? [`- Missing architecture design doc: create required file ${CORE_ARCHITECTURE_DOC_FILE}.`]
|
|
939
|
+
: []),
|
|
940
|
+
...(data.projectContextDocsState.missingStyleDocs
|
|
941
|
+
? [`- Missing design style doc: create required file ${CORE_STYLE_DOC_FILE}.`]
|
|
942
|
+
: []),
|
|
943
|
+
]
|
|
944
|
+
: []),
|
|
945
|
+
'- Start immediately with Suggested Read Order and execute the selected task.',
|
|
946
|
+
'- Update markdown artifacts directly while keeping TASK/ROADMAP IDs unchanged.',
|
|
947
|
+
'- Re-run `taskContext` for the selectedTaskId after edits to verify evidence consistency.',
|
|
948
|
+
];
|
|
949
|
+
},
|
|
950
|
+
suggestions: (data) => {
|
|
951
|
+
if (data.isEmpty) {
|
|
952
|
+
return [
|
|
953
|
+
'- No actionable tasks found. Verify task statuses and required fields in .projitive task table.',
|
|
954
|
+
'- Ensure each new task has stable TASK-<number> ID and at least one roadmapRefs item.',
|
|
955
|
+
];
|
|
956
|
+
}
|
|
957
|
+
return [
|
|
958
|
+
...collectTaskLintSuggestions(data.selectedTaskDocument.tasks),
|
|
959
|
+
...renderLintSuggestions(collectProjectContextDocsLintSuggestions(data.projectContextDocsState)),
|
|
960
|
+
];
|
|
961
|
+
},
|
|
962
|
+
nextCall: (data) => {
|
|
963
|
+
if (data.isEmpty) {
|
|
964
|
+
return data.preferredProject
|
|
965
|
+
? `taskCreate(projectPath="${toProjectPath(data.preferredProject.governanceDir)}", title="Create first executable slice", roadmapRefs=["${data.preferredRoadmapRef}"], summary="Derived from active roadmap milestone")`
|
|
966
|
+
: 'projectScan()';
|
|
967
|
+
}
|
|
968
|
+
return `taskContext(projectPath="${toProjectPath(data.selected.governanceDir)}", taskId="${data.selected.task.id}")`;
|
|
969
|
+
},
|
|
970
|
+
}));
|
|
971
|
+
server.registerTool(...createGovernedTool({
|
|
972
|
+
name: 'taskContext',
|
|
893
973
|
title: 'Task Context',
|
|
894
974
|
description: 'Get deep context, evidence links, and read order for one task',
|
|
895
975
|
inputSchema: {
|
|
896
976
|
projectPath: z.string(),
|
|
897
977
|
taskId: z.string(),
|
|
898
978
|
},
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
...asText(renderErrorMarkdown('taskContext', `Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'retry with a valid task ID'], `taskContext(projectPath="${projectPath}", taskId="TASK-0001")`)),
|
|
903
|
-
isError: true,
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
907
|
-
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
908
|
-
const { markdownPath, tasks } = await loadTasksDocument(governanceDir);
|
|
909
|
-
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
910
|
-
const task = tasks.find((item) => item.id === taskId);
|
|
911
|
-
if (!task) {
|
|
912
|
-
return {
|
|
913
|
-
...asText(renderErrorMarkdown('taskContext', `Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`)),
|
|
914
|
-
isError: true,
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
const lintSuggestions = [
|
|
918
|
-
...collectSingleTaskLintSuggestions(task),
|
|
919
|
-
...(await collectTaskFileLintSuggestions(governanceDir, task)),
|
|
920
|
-
];
|
|
921
|
-
const contextReadingGuidance = await resolveTaskContextReadingGuidance(governanceDir);
|
|
922
|
-
const taskLocation = (await findTextReferences(markdownPath, taskId))[0];
|
|
923
|
-
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
924
|
-
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
925
|
-
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
|
|
926
|
-
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
927
|
-
const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
|
|
928
|
-
// Build summary with subState and blocker info (v1.1.0)
|
|
929
|
-
const summaryLines = [
|
|
930
|
-
`- projectPath: ${normalizedProjectPath}`,
|
|
931
|
-
`- governanceDir: ${governanceDir}`,
|
|
932
|
-
`- tasksView: ${markdownPath}`,
|
|
933
|
-
`- roadmapView: ${roadmapViewPath}`,
|
|
934
|
-
`- taskId: ${task.id}`,
|
|
935
|
-
`- title: ${task.title}`,
|
|
936
|
-
`- status: ${task.status}`,
|
|
937
|
-
`- owner: ${task.owner}`,
|
|
938
|
-
`- updatedAt: ${task.updatedAt}`,
|
|
939
|
-
`- roadmapRefs: ${task.roadmapRefs.join(', ') || '(none)'}`,
|
|
940
|
-
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
|
|
941
|
-
];
|
|
942
|
-
// Add subState info for IN_PROGRESS tasks (v1.1.0)
|
|
943
|
-
if (task.subState && task.status === 'IN_PROGRESS') {
|
|
944
|
-
summaryLines.push('- subState:');
|
|
945
|
-
if (task.subState.phase) {
|
|
946
|
-
summaryLines.push(` - phase: ${task.subState.phase}`);
|
|
979
|
+
async execute({ projectPath, taskId }) {
|
|
980
|
+
if (!isValidTaskId(taskId)) {
|
|
981
|
+
throw new ToolExecutionError(`Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'retry with a valid task ID'], `taskContext(projectPath="${projectPath}", taskId="TASK-0001")`);
|
|
947
982
|
}
|
|
948
|
-
|
|
949
|
-
|
|
983
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
984
|
+
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
985
|
+
const { markdownPath, tasks } = await loadTasksDocument(governanceDir);
|
|
986
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
987
|
+
const task = tasks.find((item) => item.id === taskId);
|
|
988
|
+
if (!task) {
|
|
989
|
+
throw new ToolExecutionError(`Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`);
|
|
950
990
|
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
991
|
+
const researchBriefState = await inspectTaskResearchBrief(governanceDir, task);
|
|
992
|
+
const contextReadingGuidance = await resolveTaskContextReadingGuidance(governanceDir);
|
|
993
|
+
const taskLocation = (await findTextReferences(markdownPath, taskId))[0];
|
|
994
|
+
const artifacts = await discoverGovernanceArtifacts(governanceDir);
|
|
995
|
+
const fileCandidates = candidateFilesFromArtifacts(artifacts);
|
|
996
|
+
const projectContextDocsState = inspectProjectContextDocsFromArtifacts(fileCandidates);
|
|
997
|
+
const referenceLocations = (await Promise.all(fileCandidates.map((file) => findTextReferences(file, taskId)))).flat();
|
|
998
|
+
const relatedArtifacts = Array.from(new Set(referenceLocations.map((item) => item.filePath)));
|
|
999
|
+
const suggestedReadOrder = [markdownPath, ...relatedArtifacts.filter((item) => item !== markdownPath)];
|
|
1000
|
+
return {
|
|
1001
|
+
normalizedProjectPath, governanceDir, markdownPath, roadmapViewPath,
|
|
1002
|
+
task, researchBriefState, contextReadingGuidance,
|
|
1003
|
+
taskLocation, referenceLocations, relatedArtifacts, suggestedReadOrder,
|
|
1004
|
+
projectContextDocsState,
|
|
1005
|
+
};
|
|
1006
|
+
},
|
|
1007
|
+
summary: ({ normalizedProjectPath, governanceDir, markdownPath, roadmapViewPath, task, researchBriefState, projectContextDocsState, taskLocation }) => {
|
|
1008
|
+
const lines = [
|
|
1009
|
+
`- projectPath: ${normalizedProjectPath}`,
|
|
1010
|
+
`- governanceDir: ${governanceDir}`,
|
|
1011
|
+
`- tasksView: ${markdownPath}`,
|
|
1012
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
1013
|
+
`- taskId: ${task.id}`,
|
|
1014
|
+
`- title: ${task.title}`,
|
|
1015
|
+
`- status: ${task.status}`,
|
|
1016
|
+
`- owner: ${task.owner}`,
|
|
1017
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
1018
|
+
`- roadmapRefs: ${task.roadmapRefs.join(', ') || '(none)'}`,
|
|
1019
|
+
`- researchBriefPath: ${researchBriefState.relativePath}`,
|
|
1020
|
+
`- researchBriefStatus: ${researchBriefState.ready ? 'READY' : 'MISSING'}`,
|
|
1021
|
+
`- architectureDocsStatus: ${projectContextDocsState.missingArchitectureDocs ? 'MISSING' : 'READY'}`,
|
|
1022
|
+
`- styleDocsStatus: ${projectContextDocsState.missingStyleDocs ? 'MISSING' : 'READY'}`,
|
|
1023
|
+
`- taskLocation: ${taskLocation ? `${taskLocation.filePath}#L${taskLocation.line}` : markdownPath}`,
|
|
1024
|
+
];
|
|
1025
|
+
if (task.subState && task.status === 'IN_PROGRESS') {
|
|
1026
|
+
lines.push('- subState:');
|
|
1027
|
+
if (task.subState.phase)
|
|
1028
|
+
lines.push(` - phase: ${task.subState.phase}`);
|
|
1029
|
+
if (typeof task.subState.confidence === 'number')
|
|
1030
|
+
lines.push(` - confidence: ${task.subState.confidence}`);
|
|
1031
|
+
if (task.subState.estimatedCompletion)
|
|
1032
|
+
lines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
962
1033
|
}
|
|
963
|
-
if (task.blocker.
|
|
964
|
-
|
|
1034
|
+
if (task.blocker && task.status === 'BLOCKED') {
|
|
1035
|
+
lines.push('- blocker:');
|
|
1036
|
+
lines.push(` - type: ${task.blocker.type}`);
|
|
1037
|
+
lines.push(` - description: ${task.blocker.description}`);
|
|
1038
|
+
if (task.blocker.blockingEntity)
|
|
1039
|
+
lines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
1040
|
+
if (task.blocker.unblockCondition)
|
|
1041
|
+
lines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
|
|
1042
|
+
if (task.blocker.escalationPath)
|
|
1043
|
+
lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
965
1044
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
'### Related Artifacts',
|
|
976
|
-
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
|
|
977
|
-
'',
|
|
978
|
-
'### Reference Locations',
|
|
979
|
-
...(referenceLocations.length > 0
|
|
980
|
-
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
981
|
-
: ['- (none)']),
|
|
1045
|
+
return lines;
|
|
1046
|
+
},
|
|
1047
|
+
evidence: ({ task, researchBriefState, projectContextDocsState, relatedArtifacts, referenceLocations, suggestedReadOrder }) => [
|
|
1048
|
+
'### Pre-Execution Research Brief',
|
|
1049
|
+
`- path: ${researchBriefState.relativePath}`,
|
|
1050
|
+
`- absolutePath: ${researchBriefState.absolutePath}`,
|
|
1051
|
+
`- status: ${researchBriefState.ready ? 'READY' : 'MISSING'}`,
|
|
1052
|
+
...(!researchBriefState.ready
|
|
1053
|
+
? [
|
|
982
1054
|
'',
|
|
983
|
-
'###
|
|
984
|
-
...
|
|
1055
|
+
'### Required Research Brief Template',
|
|
1056
|
+
...renderTaskResearchBriefTemplate(task).map((line) => `- ${line}`),
|
|
1057
|
+
]
|
|
1058
|
+
: []),
|
|
1059
|
+
'',
|
|
1060
|
+
'### Project Context Docs Check',
|
|
1061
|
+
`- architecture docs: ${projectContextDocsState.architectureDocs.length > 0 ? 'found' : 'missing'}`,
|
|
1062
|
+
...(projectContextDocsState.architectureDocs.length > 0
|
|
1063
|
+
? projectContextDocsState.architectureDocs.map((item) => `- architecture: ${item}`)
|
|
1064
|
+
: [`- architecture: add required file ${CORE_ARCHITECTURE_DOC_FILE}.`]),
|
|
1065
|
+
`- design style docs: ${projectContextDocsState.styleDocs.length > 0 ? 'found' : 'missing'}`,
|
|
1066
|
+
...(projectContextDocsState.styleDocs.length > 0
|
|
1067
|
+
? projectContextDocsState.styleDocs.map((item) => `- style: ${item}`)
|
|
1068
|
+
: [`- style: add required file ${CORE_STYLE_DOC_FILE}.`]),
|
|
1069
|
+
'',
|
|
1070
|
+
'### Related Artifacts',
|
|
1071
|
+
...(relatedArtifacts.length > 0 ? relatedArtifacts.map((file) => `- ${file}`) : ['- (none)']),
|
|
1072
|
+
'',
|
|
1073
|
+
'### Reference Locations',
|
|
1074
|
+
...(referenceLocations.length > 0
|
|
1075
|
+
? referenceLocations.map((item) => `- ${item.filePath}#L${item.line}: ${item.text}`)
|
|
1076
|
+
: ['- (none)']),
|
|
1077
|
+
'',
|
|
1078
|
+
'### Suggested Read Order',
|
|
1079
|
+
...suggestedReadOrder.map((item, index) => `${index + 1}. ${item}`),
|
|
1080
|
+
],
|
|
1081
|
+
guidance: ({ researchBriefState, projectContextDocsState, contextReadingGuidance, task }) => [
|
|
1082
|
+
...(!researchBriefState.ready
|
|
1083
|
+
? [
|
|
1084
|
+
'- Pre-execution gate is NOT satisfied. Complete research brief first, then proceed with implementation.',
|
|
1085
|
+
`- Create or update ${researchBriefState.relativePath} with design guidelines + code architecture findings before code changes.`,
|
|
1086
|
+
'- Include exact file/line locations in the brief (for example path/to/file.ts#L120).',
|
|
1087
|
+
'- Re-run taskContext after writing the brief and confirm researchBriefStatus becomes READY.',
|
|
1088
|
+
]
|
|
1089
|
+
: [
|
|
1090
|
+
'- Pre-execution gate satisfied. Read the research brief first, then continue implementation.',
|
|
1091
|
+
`- Must read ${researchBriefState.relativePath} before any task execution changes.`,
|
|
985
1092
|
]),
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
'',
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
'-
|
|
1093
|
+
...(!projectContextDocsState.ready
|
|
1094
|
+
? [
|
|
1095
|
+
'- Project context docs gate is NOT satisfied. Complete missing project architecture/style docs first.',
|
|
1096
|
+
...(projectContextDocsState.missingArchitectureDocs
|
|
1097
|
+
? [`- Missing architecture design doc. Add required file ${CORE_ARCHITECTURE_DOC_FILE} and include architecture boundaries and module responsibilities.`]
|
|
1098
|
+
: []),
|
|
1099
|
+
...(projectContextDocsState.missingStyleDocs
|
|
1100
|
+
? [`- Missing design style doc. Add required file ${CORE_STYLE_DOC_FILE} and include style language, tokens/themes, and UI consistency rules.`]
|
|
1101
|
+
: []),
|
|
1102
|
+
'- Re-run taskContext and confirm both architectureDocsStatus/styleDocsStatus are READY.',
|
|
1103
|
+
]
|
|
1104
|
+
: [
|
|
1105
|
+
'- Project context docs gate satisfied. Architecture/style docs are available for execution alignment.',
|
|
996
1106
|
]),
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1107
|
+
'- Read the files in Suggested Read Order.',
|
|
1108
|
+
'',
|
|
1109
|
+
'### Context Reading',
|
|
1110
|
+
...contextReadingGuidance,
|
|
1111
|
+
'',
|
|
1112
|
+
'- Verify whether current status and evidence are consistent.',
|
|
1113
|
+
...taskStatusGuidance(task),
|
|
1114
|
+
'- If updates are needed, use tool writes for governance store (`taskUpdate` / `roadmapUpdate`) and keep TASK IDs unchanged.',
|
|
1115
|
+
'- After editing, re-run `taskContext` to verify references and context consistency.',
|
|
1116
|
+
],
|
|
1117
|
+
suggestions: ({ task, researchBriefState, projectContextDocsState }) => [
|
|
1118
|
+
...collectSingleTaskLintSuggestions(task),
|
|
1119
|
+
...renderLintSuggestions(collectTaskResearchBriefLintSuggestions(researchBriefState)),
|
|
1120
|
+
...renderLintSuggestions(collectProjectContextDocsLintSuggestions(projectContextDocsState)),
|
|
1121
|
+
],
|
|
1122
|
+
nextCall: ({ normalizedProjectPath, task }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${task.id}")`,
|
|
1123
|
+
}));
|
|
1003
1124
|
// taskUpdate tool - Update task fields including subState and blocker (Spec v1.1.0)
|
|
1004
|
-
server.registerTool(
|
|
1125
|
+
server.registerTool(...createGovernedTool({
|
|
1126
|
+
name: 'taskUpdate',
|
|
1005
1127
|
title: 'Task Update',
|
|
1006
1128
|
description: 'Update task fields including status, owner, summary, subState, and blocker metadata',
|
|
1007
1129
|
inputSchema: {
|
|
@@ -1027,140 +1149,108 @@ export function registerTaskTools(server) {
|
|
|
1027
1149
|
}).optional(),
|
|
1028
1150
|
}),
|
|
1029
1151
|
},
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
...asText(renderErrorMarkdown('taskUpdate', `Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'retry with a valid task ID'], `taskUpdate(projectPath="${projectPath}", taskId="TASK-0001", updates={...})`)),
|
|
1034
|
-
isError: true,
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1038
|
-
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
1039
|
-
const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
|
|
1040
|
-
const tasksViewPath = path.join(governanceDir, TASKS_MARKDOWN_FILE);
|
|
1041
|
-
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
1042
|
-
const taskIndex = tasks.findIndex((item) => item.id === taskId);
|
|
1043
|
-
if (taskIndex === -1) {
|
|
1044
|
-
return {
|
|
1045
|
-
...asText(renderErrorMarkdown('taskUpdate', `Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`)),
|
|
1046
|
-
isError: true,
|
|
1047
|
-
};
|
|
1048
|
-
}
|
|
1049
|
-
const task = tasks[taskIndex];
|
|
1050
|
-
const originalStatus = task.status;
|
|
1051
|
-
// Validate status transition
|
|
1052
|
-
if (updates.status && !validateTransition(originalStatus, updates.status)) {
|
|
1053
|
-
return {
|
|
1054
|
-
...asText(renderErrorMarkdown('taskUpdate', `Invalid status transition: ${originalStatus} -> ${updates.status}`, ['use `validateTransition` to check allowed transitions', 'provide evidence when transitioning to DONE'], `taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${taskId}")`)),
|
|
1055
|
-
isError: true,
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1058
|
-
// Apply updates
|
|
1059
|
-
if (updates.status)
|
|
1060
|
-
task.status = updates.status;
|
|
1061
|
-
if (updates.owner !== undefined)
|
|
1062
|
-
task.owner = updates.owner;
|
|
1063
|
-
if (updates.summary !== undefined)
|
|
1064
|
-
task.summary = updates.summary;
|
|
1065
|
-
if (updates.roadmapRefs)
|
|
1066
|
-
task.roadmapRefs = updates.roadmapRefs;
|
|
1067
|
-
if (updates.links)
|
|
1068
|
-
task.links = updates.links;
|
|
1069
|
-
// Handle subState (Spec v1.1.0)
|
|
1070
|
-
if (updates.subState !== undefined) {
|
|
1071
|
-
if (updates.subState === null) {
|
|
1072
|
-
delete task.subState;
|
|
1152
|
+
async execute({ projectPath, taskId, updates }) {
|
|
1153
|
+
if (!isValidTaskId(taskId)) {
|
|
1154
|
+
throw new ToolExecutionError(`Invalid task ID format: ${taskId}`, ['expected format: TASK-1 or TASK-0001', 'retry with a valid task ID'], `taskUpdate(projectPath="${projectPath}", taskId="TASK-0001", updates={...})`);
|
|
1073
1155
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1156
|
+
const governanceDir = await resolveGovernanceDir(projectPath);
|
|
1157
|
+
const normalizedProjectPath = toProjectPath(governanceDir);
|
|
1158
|
+
const { tasksPath, tasks } = await loadTasksDocument(governanceDir);
|
|
1159
|
+
const tasksViewPath = path.join(governanceDir, TASKS_MARKDOWN_FILE);
|
|
1160
|
+
const roadmapViewPath = path.join(governanceDir, 'roadmap.md');
|
|
1161
|
+
const taskIndex = tasks.findIndex((item) => item.id === taskId);
|
|
1162
|
+
if (taskIndex === -1) {
|
|
1163
|
+
throw new ToolExecutionError(`Task not found: ${taskId}`, ['run `taskList` to discover available IDs', 'retry with an existing task ID'], `taskList(projectPath="${toProjectPath(governanceDir)}")`);
|
|
1079
1164
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
if (updates.
|
|
1084
|
-
|
|
1165
|
+
const task = tasks[taskIndex];
|
|
1166
|
+
const originalStatus = task.status;
|
|
1167
|
+
const previewTask = normalizeTask({ ...task, ...updates, updatedAt: nowIso() });
|
|
1168
|
+
if (updates.status && !validateTransition(originalStatus, updates.status)) {
|
|
1169
|
+
throw new ToolExecutionError(`Invalid status transition: ${originalStatus} -> ${updates.status}`, ['use `validateTransition` to check allowed transitions', 'provide evidence when transitioning to DONE'], `taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${taskId}")`);
|
|
1085
1170
|
}
|
|
1086
|
-
|
|
1087
|
-
task.
|
|
1171
|
+
const updatedSubState = updates.subState === null ? undefined
|
|
1172
|
+
: updates.subState !== undefined ? { ...(task.subState ?? {}), ...updates.subState }
|
|
1173
|
+
: task.subState;
|
|
1174
|
+
const updatedBlocker = updates.blocker === null ? undefined
|
|
1175
|
+
: updates.blocker !== undefined ? updates.blocker
|
|
1176
|
+
: task.blocker;
|
|
1177
|
+
const normalizedTask = normalizeTask({
|
|
1178
|
+
...task,
|
|
1179
|
+
...(updates.status ? { status: updates.status } : {}),
|
|
1180
|
+
...(updates.owner !== undefined ? { owner: updates.owner } : {}),
|
|
1181
|
+
...(updates.summary !== undefined ? { summary: updates.summary } : {}),
|
|
1182
|
+
...(updates.roadmapRefs ? { roadmapRefs: updates.roadmapRefs } : {}),
|
|
1183
|
+
...(updates.links ? { links: updates.links } : {}),
|
|
1184
|
+
subState: updatedSubState,
|
|
1185
|
+
blocker: updatedBlocker,
|
|
1186
|
+
updatedAt: nowIso(),
|
|
1187
|
+
});
|
|
1188
|
+
await upsertTaskInStore(tasksPath, normalizedTask);
|
|
1189
|
+
await loadTasksDocumentWithOptions(governanceDir, true);
|
|
1190
|
+
return { normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, taskId, originalStatus, task: normalizedTask, previewTask, updates };
|
|
1191
|
+
},
|
|
1192
|
+
summary: ({ normalizedProjectPath, governanceDir, tasksViewPath, roadmapViewPath, taskId, originalStatus, task }) => {
|
|
1193
|
+
const lines = [
|
|
1194
|
+
`- projectPath: ${normalizedProjectPath}`,
|
|
1195
|
+
`- governanceDir: ${governanceDir}`,
|
|
1196
|
+
`- tasksView: ${tasksViewPath}`,
|
|
1197
|
+
`- roadmapView: ${roadmapViewPath}`,
|
|
1198
|
+
`- taskId: ${taskId}`,
|
|
1199
|
+
`- originalStatus: ${originalStatus}`,
|
|
1200
|
+
`- newStatus: ${task.status}`,
|
|
1201
|
+
`- updatedAt: ${task.updatedAt}`,
|
|
1202
|
+
];
|
|
1203
|
+
if (task.subState) {
|
|
1204
|
+
lines.push('- subState:');
|
|
1205
|
+
if (task.subState.phase)
|
|
1206
|
+
lines.push(` - phase: ${task.subState.phase}`);
|
|
1207
|
+
if (typeof task.subState.confidence === 'number')
|
|
1208
|
+
lines.push(` - confidence: ${task.subState.confidence}`);
|
|
1209
|
+
if (task.subState.estimatedCompletion)
|
|
1210
|
+
lines.push(` - estimatedCompletion: ${task.subState.estimatedCompletion}`);
|
|
1088
1211
|
}
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
task
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
`-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
`-
|
|
1110
|
-
`-
|
|
1111
|
-
`-
|
|
1112
|
-
`-
|
|
1113
|
-
`-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
updateSummary.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
1134
|
-
}
|
|
1135
|
-
const markdown = renderToolResponseMarkdown({
|
|
1136
|
-
toolName: 'taskUpdate',
|
|
1137
|
-
sections: [
|
|
1138
|
-
summarySection(updateSummary),
|
|
1139
|
-
evidenceSection([
|
|
1140
|
-
'### Updated Task',
|
|
1141
|
-
`- ${task.id} | ${task.status} | ${task.title}`,
|
|
1142
|
-
`- owner: ${task.owner || '(none)'}`,
|
|
1143
|
-
`- summary: ${task.summary || '(none)'}`,
|
|
1144
|
-
'',
|
|
1145
|
-
'### Update Details',
|
|
1146
|
-
...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
|
|
1147
|
-
...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
|
|
1148
|
-
...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
|
|
1149
|
-
...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(', ')}`] : []),
|
|
1150
|
-
...(updates.links ? [`- links: ${updates.links.join(', ')}`] : []),
|
|
1151
|
-
...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
|
|
1152
|
-
...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
|
|
1153
|
-
]),
|
|
1154
|
-
guidanceSection([
|
|
1155
|
-
'Task updated successfully and tasks.md has been synced. Run `taskContext` to verify the changes.',
|
|
1156
|
-
'If status changed to DONE, ensure evidence links are added.',
|
|
1157
|
-
'If subState or blocker were updated, verify the metadata is correct.',
|
|
1158
|
-
'.projitive governance store is source of truth; tasks.md is a generated view and may be overwritten.',
|
|
1159
|
-
]),
|
|
1160
|
-
lintSection([]),
|
|
1161
|
-
nextCallSection(`taskContext(projectPath="${toProjectPath(governanceDir)}", taskId="${taskId}")`),
|
|
1162
|
-
],
|
|
1163
|
-
});
|
|
1164
|
-
return asText(markdown);
|
|
1165
|
-
});
|
|
1212
|
+
if (task.blocker) {
|
|
1213
|
+
lines.push('- blocker:');
|
|
1214
|
+
lines.push(` - type: ${task.blocker.type}`);
|
|
1215
|
+
lines.push(` - description: ${task.blocker.description}`);
|
|
1216
|
+
if (task.blocker.blockingEntity)
|
|
1217
|
+
lines.push(` - blockingEntity: ${task.blocker.blockingEntity}`);
|
|
1218
|
+
if (task.blocker.unblockCondition)
|
|
1219
|
+
lines.push(` - unblockCondition: ${task.blocker.unblockCondition}`);
|
|
1220
|
+
if (task.blocker.escalationPath)
|
|
1221
|
+
lines.push(` - escalationPath: ${task.blocker.escalationPath}`);
|
|
1222
|
+
}
|
|
1223
|
+
return lines;
|
|
1224
|
+
},
|
|
1225
|
+
evidence: ({ task, originalStatus, updates }) => [
|
|
1226
|
+
'### Updated Task',
|
|
1227
|
+
`- ${task.id} | ${task.status} | ${task.title}`,
|
|
1228
|
+
`- owner: ${task.owner || '(none)'}`,
|
|
1229
|
+
`- summary: ${task.summary || '(none)'}`,
|
|
1230
|
+
'',
|
|
1231
|
+
'### Update Details',
|
|
1232
|
+
...(updates.status ? [`- status: ${originalStatus} → ${updates.status}`] : []),
|
|
1233
|
+
...(updates.owner !== undefined ? [`- owner: ${updates.owner}`] : []),
|
|
1234
|
+
...(updates.summary !== undefined ? [`- summary: ${updates.summary}`] : []),
|
|
1235
|
+
...(updates.roadmapRefs ? [`- roadmapRefs: ${updates.roadmapRefs.join(', ')}`] : []),
|
|
1236
|
+
...(updates.links ? [`- links: ${updates.links.join(', ')}`] : []),
|
|
1237
|
+
...(updates.subState ? [`- subState: ${JSON.stringify(updates.subState)}`] : []),
|
|
1238
|
+
...(updates.blocker ? [`- blocker: ${JSON.stringify(updates.blocker)}`] : []),
|
|
1239
|
+
],
|
|
1240
|
+
guidance: ({ updates, originalStatus }) => [
|
|
1241
|
+
'Task updated successfully and tasks.md has been synced. Run `taskContext` to verify the changes.',
|
|
1242
|
+
...(updates.status === 'IN_PROGRESS' && originalStatus === 'TODO'
|
|
1243
|
+
? ['- Ensure pre-execution research brief exists before deep implementation.']
|
|
1244
|
+
: []),
|
|
1245
|
+
...(updates.status === 'DONE'
|
|
1246
|
+
? ['- Verify evidence links are attached and reflect completed work.']
|
|
1247
|
+
: []),
|
|
1248
|
+
'.projitive governance store is source of truth; tasks.md is a generated view and may be overwritten.',
|
|
1249
|
+
],
|
|
1250
|
+
suggestions: async ({ previewTask, governanceDir }) => [
|
|
1251
|
+
...collectSingleTaskLintSuggestions(previewTask),
|
|
1252
|
+
...renderLintSuggestions(await collectDoneConformanceSuggestions(governanceDir, previewTask)),
|
|
1253
|
+
],
|
|
1254
|
+
nextCall: ({ normalizedProjectPath, taskId }) => `taskContext(projectPath="${normalizedProjectPath}", taskId="${taskId}")`,
|
|
1255
|
+
}));
|
|
1166
1256
|
}
|