@polymorphism-tech/morph-spec 4.8.12 → 4.8.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +379 -379
- package/bin/morph-spec.js +23 -2
- package/bin/{task-manager.cjs → task-manager.js} +249 -172
- package/claude-plugin.json +14 -14
- package/docs/CHEATSHEET.md +203 -203
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +224 -140
- package/framework/hooks/README.md +202 -202
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +48 -2
- package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +151 -0
- package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +12 -0
- package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +6 -0
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +34 -0
- package/framework/hooks/claude-code/statusline.py +6 -0
- package/framework/hooks/claude-code/stop/validate-completion.js +38 -4
- package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +87 -0
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +58 -0
- package/framework/hooks/shared/phase-utils.js +4 -1
- package/framework/hooks/shared/state-reader.js +1 -0
- package/framework/skills/README.md +1 -0
- package/framework/skills/level-0-meta/brainstorming/SKILL.md +2 -0
- package/framework/skills/level-0-meta/code-review/SKILL.md +16 -0
- package/framework/skills/level-0-meta/code-review/references/review-guidelines.md +100 -0
- package/framework/skills/level-0-meta/code-review/scripts/scan-csharp.mjs +36 -6
- package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +16 -0
- package/framework/skills/level-0-meta/code-review-nextjs/scripts/scan-nextjs.mjs +189 -0
- package/framework/skills/level-0-meta/frontend-review/SKILL.md +359 -0
- package/framework/skills/level-0-meta/frontend-review/scripts/scan-accessibility.mjs +376 -0
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +1 -1
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +10 -8
- package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +70 -0
- package/framework/skills/level-0-meta/post-implementation/SKILL.md +315 -0
- package/framework/skills/level-0-meta/post-implementation/scripts/detect-dev-server.mjs +153 -0
- package/framework/skills/level-0-meta/post-implementation/scripts/detect-stack.mjs +234 -0
- package/framework/skills/level-0-meta/terminal-title/SKILL.md +61 -0
- package/framework/skills/level-0-meta/terminal-title/scripts/set_title.sh +65 -0
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +50 -188
- package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +213 -0
- package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +2 -0
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +4 -7
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +71 -109
- package/framework/skills/level-1-workflows/phase-design/references/architecture-analysis-guide.md +89 -0
- package/framework/skills/level-1-workflows/phase-design/references/spec-authoring-guide.md +55 -0
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +171 -114
- package/framework/skills/level-1-workflows/phase-implement/references/vsa-implementation-guide.md +92 -0
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -2
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +35 -159
- package/framework/skills/level-1-workflows/phase-tasks/references/task-planning-patterns.md +172 -0
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +42 -3
- package/framework/squad-templates/backend-only.json +14 -1
- package/framework/squad-templates/frontend-only.json +14 -1
- package/framework/squad-templates/full-stack.json +25 -8
- package/framework/standards/STANDARDS.json +631 -86
- package/framework/standards/frontend/design-system/aesthetic-direction.md +213 -0
- package/framework/templates/project/validate.js +122 -0
- package/framework/workflows/configs/zero-touch.json +7 -0
- package/package.json +87 -87
- package/src/commands/agents/dispatch-agents.js +53 -10
- package/src/commands/state/advance-phase.js +88 -13
- package/src/commands/state/index.js +2 -1
- package/src/commands/state/phase-runner.js +215 -0
- package/src/commands/tasks/task.js +25 -4
- package/src/core/paths/output-schema.js +2 -1
- package/src/lib/detectors/design-system-detector.js +5 -4
- package/src/lib/generators/recap-generator.js +16 -0
- package/src/lib/orchestration/team-orchestrator.js +171 -89
- package/src/lib/phase-chain/eligibility-checker.js +243 -0
- package/src/lib/standards/digest-builder.js +231 -0
- package/src/lib/tasks/task-parser.js +94 -0
- package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +39 -0
- package/src/lib/validators/content/content-validator.js +34 -106
- package/src/lib/validators/nextjs/next-component-validator.js +2 -0
- package/src/lib/validators/validation-runner.js +2 -2
- package/src/utils/file-copier.js +1 -0
- package/src/utils/hooks-installer.js +31 -7
|
@@ -7,84 +7,21 @@
|
|
|
7
7
|
* Part of MORPH-SPEC 3.0 - Event-driven state management.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const chalk = {
|
|
16
|
-
red: (s) => `\x1b[31m${s}\x1b[0m`,
|
|
17
|
-
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
18
|
-
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
19
|
-
blue: (s) => `\x1b[34m${s}\x1b[0m`,
|
|
20
|
-
magenta: (s) => `\x1b[35m${s}\x1b[0m`,
|
|
21
|
-
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
22
|
-
gray: (s) => `\x1b[90m${s}\x1b[0m`,
|
|
23
|
-
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
24
|
-
};
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { join, dirname } from 'path';
|
|
12
|
+
import { access } from 'fs/promises';
|
|
13
|
+
import { execSync } from 'child_process';
|
|
14
|
+
import chalk from 'chalk';
|
|
25
15
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// ============================================================================
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Parse tasks.md to extract task stubs for v3 state format.
|
|
32
|
-
* Looks for headings like: ### T001 — Task title
|
|
33
|
-
*/
|
|
34
|
-
async function parseTasksMd(featureName) {
|
|
35
|
-
const tasksPath = path.join(process.cwd(), `.morph/features/${featureName}/3-tasks/tasks.md`);
|
|
36
|
-
let content = '';
|
|
37
|
-
try {
|
|
38
|
-
content = await fs.readFile(tasksPath, 'utf-8');
|
|
39
|
-
} catch {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const tasks = [];
|
|
44
|
-
const headingRe = /^###\s+(T\d+)\s+[—–-]\s+(.+)$/gm;
|
|
45
|
-
let match;
|
|
46
|
-
while ((match = headingRe.exec(content)) !== null) {
|
|
47
|
-
tasks.push({
|
|
48
|
-
id: match[1],
|
|
49
|
-
title: match[2].trim(),
|
|
50
|
-
status: 'pending',
|
|
51
|
-
dependencies: [],
|
|
52
|
-
files: [],
|
|
53
|
-
checkpoint: null
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return tasks;
|
|
57
|
-
}
|
|
16
|
+
import { loadState, saveState } from '../src/core/state/state-manager.js';
|
|
17
|
+
import { parseTasksMd, ensureTaskList, syncCounters } from '../src/lib/tasks/task-parser.js';
|
|
58
18
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* In v3 state, feature.tasks is a counter object {total, completed, ...}.
|
|
62
|
-
* Individual task objects live in feature.taskList.
|
|
63
|
-
*/
|
|
64
|
-
async function ensureTaskList(feature, featureName) {
|
|
65
|
-
if (Array.isArray(feature.tasks)) {
|
|
66
|
-
// v2 format: tasks IS the array
|
|
67
|
-
return feature.tasks;
|
|
68
|
-
}
|
|
69
|
-
// v3 format: use taskList or build from tasks.md
|
|
70
|
-
if (!feature.taskList || feature.taskList.length === 0) {
|
|
71
|
-
feature.taskList = await parseTasksMd(featureName);
|
|
72
|
-
}
|
|
73
|
-
return feature.taskList;
|
|
74
|
-
}
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = dirname(__filename);
|
|
75
21
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
*/
|
|
80
|
-
function syncCounters(feature) {
|
|
81
|
-
if (Array.isArray(feature.tasks)) return; // v2, nothing to sync
|
|
82
|
-
const list = feature.taskList || [];
|
|
83
|
-
feature.tasks.total = list.length; // Auto-sync total from parsed taskList
|
|
84
|
-
feature.tasks.completed = list.filter(t => t.status === 'completed').length;
|
|
85
|
-
feature.tasks.inProgress = list.filter(t => t.status === 'in_progress').length;
|
|
86
|
-
feature.tasks.pending = list.filter(t => t.status === 'pending').length;
|
|
87
|
-
}
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Breaking-Change Detection
|
|
24
|
+
// ============================================================================
|
|
88
25
|
|
|
89
26
|
/**
|
|
90
27
|
* Detect potentially broken consumers of recently removed exports.
|
|
@@ -102,7 +39,6 @@ function syncCounters(feature) {
|
|
|
102
39
|
*/
|
|
103
40
|
async function detectBreakingChanges() {
|
|
104
41
|
try {
|
|
105
|
-
const { execSync } = require('child_process');
|
|
106
42
|
const execOpts = { cwd: process.cwd(), stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: 5 * 1024 * 1024 };
|
|
107
43
|
|
|
108
44
|
// Collect diff from staged + unstaged changes in source files
|
|
@@ -133,7 +69,6 @@ async function detectBreakingChanges() {
|
|
|
133
69
|
const warnings = [];
|
|
134
70
|
for (const sym of removedExports) {
|
|
135
71
|
try {
|
|
136
|
-
// git grep returns exit code 1 when nothing found — that's handled by catch
|
|
137
72
|
const result = execSync(`git grep -l "${sym}"`, execOpts).toString().trim();
|
|
138
73
|
if (result) {
|
|
139
74
|
const consumers = result.split('\n').filter(Boolean);
|
|
@@ -150,39 +85,11 @@ async function detectBreakingChanges() {
|
|
|
150
85
|
}
|
|
151
86
|
}
|
|
152
87
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Load state.json
|
|
160
|
-
*/
|
|
161
|
-
async loadState() {
|
|
162
|
-
try {
|
|
163
|
-
const content = await fs.readFile(this.statePath, 'utf-8');
|
|
164
|
-
return JSON.parse(content);
|
|
165
|
-
} catch (error) {
|
|
166
|
-
if (error.code === 'ENOENT') {
|
|
167
|
-
throw new Error(`State file not found: ${this.statePath}. Run 'npx morph-spec state init' first.`);
|
|
168
|
-
}
|
|
169
|
-
throw error;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Save state.json
|
|
175
|
-
*/
|
|
176
|
-
async saveState(state) {
|
|
177
|
-
// v3 state uses metadata.lastUpdated, v2 used project.updatedAt
|
|
178
|
-
if (state.metadata) {
|
|
179
|
-
state.metadata.lastUpdated = new Date().toISOString();
|
|
180
|
-
} else if (state.project) {
|
|
181
|
-
state.project.updatedAt = new Date().toISOString();
|
|
182
|
-
}
|
|
183
|
-
await fs.writeFile(this.statePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
184
|
-
}
|
|
88
|
+
// ============================================================================
|
|
89
|
+
// TaskManager
|
|
90
|
+
// ============================================================================
|
|
185
91
|
|
|
92
|
+
class TaskManager {
|
|
186
93
|
/**
|
|
187
94
|
* Complete one or more tasks (runs validation first)
|
|
188
95
|
* @param {string} featureName - Feature name
|
|
@@ -191,7 +98,7 @@ class TaskManager {
|
|
|
191
98
|
* @param {boolean} options.skipValidation - Skip code validation
|
|
192
99
|
*/
|
|
193
100
|
async completeTasks(featureName, taskIds, options = {}) {
|
|
194
|
-
const state =
|
|
101
|
+
const state = loadState();
|
|
195
102
|
const feature = state.features[featureName];
|
|
196
103
|
|
|
197
104
|
if (!feature) {
|
|
@@ -201,12 +108,12 @@ class TaskManager {
|
|
|
201
108
|
const taskList = await ensureTaskList(feature, featureName);
|
|
202
109
|
|
|
203
110
|
if (taskList.length === 0) {
|
|
204
|
-
const tasksPath =
|
|
205
|
-
const tasksExist = await
|
|
111
|
+
const tasksPath = join(process.cwd(), `.morph/features/${featureName}/3-tasks/tasks.md`);
|
|
112
|
+
const tasksExist = await access(tasksPath).then(() => true).catch(() => false);
|
|
206
113
|
if (!tasksExist) {
|
|
207
114
|
throw new Error(`No tasks found for '${featureName}' — tasks.md not generated yet.\n Complete the tasks phase first: run /phase-tasks`);
|
|
208
115
|
}
|
|
209
|
-
throw new Error(`tasks.md found but no tasks could be parsed for '${featureName}'.\n Ensure tasks use the format: ### T001 — Title`);
|
|
116
|
+
throw new Error(`tasks.md found but no tasks could be parsed for '${featureName}'.\n Ensure tasks use the format: ### T001 — Title or ### T001: Title`);
|
|
210
117
|
}
|
|
211
118
|
|
|
212
119
|
const results = [];
|
|
@@ -237,13 +144,60 @@ class TaskManager {
|
|
|
237
144
|
|
|
238
145
|
// Run validation BEFORE marking tasks as complete
|
|
239
146
|
if (tasksToComplete.length > 0 && !options.skipValidation) {
|
|
240
|
-
const
|
|
241
|
-
if (
|
|
242
|
-
console.
|
|
243
|
-
|
|
147
|
+
const validationResult = await this.runValidation(featureName);
|
|
148
|
+
if (options.dryRun) {
|
|
149
|
+
console.log(chalk.cyan('\n ℹ️ Dry-run — tasks NOT marked as complete'));
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
if (!validationResult.passed) {
|
|
153
|
+
// Reload state to ensure we have the latest (may have been modified by validation)
|
|
154
|
+
const currentState = loadState();
|
|
155
|
+
const currentFeature = currentState.features[featureName];
|
|
156
|
+
if (currentFeature) {
|
|
157
|
+
for (const task of tasksToComplete) {
|
|
158
|
+
this.persistValidationHistory(currentFeature, task.id, validationResult);
|
|
159
|
+
}
|
|
160
|
+
saveState(currentState);
|
|
161
|
+
|
|
162
|
+
// Check escalation status
|
|
163
|
+
const blockedTasks = tasksToComplete.filter(t => {
|
|
164
|
+
const hist = currentFeature.validationHistory?.[t.id];
|
|
165
|
+
return hist?.status === 'blocked';
|
|
166
|
+
});
|
|
167
|
+
if (blockedTasks.length > 0) {
|
|
168
|
+
console.error(chalk.red(`\n⛔ ESCALATION — Task(s) ${blockedTasks.map(t => t.id).join(', ')} have failed 3 times`));
|
|
169
|
+
console.error(chalk.red(' Human review required. Mark resolved with: morph-spec state set <feature> validationHistory.<taskId>.status passed'));
|
|
170
|
+
} else {
|
|
171
|
+
const attempt = currentFeature.validationHistory?.[tasksToComplete[0]?.id]?.attempt || 1;
|
|
172
|
+
console.error(chalk.red(`\n❌ Validation failed (attempt ${attempt}/3) — tasks NOT marked as complete`));
|
|
173
|
+
console.log(chalk.gray(' Fix the issues above, then run task done again'));
|
|
174
|
+
if (attempt >= 2) {
|
|
175
|
+
console.log(chalk.yellow(` ⚠️ Next failure will escalate to human review`));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
console.error(chalk.red('\n❌ Validation failed — tasks NOT marked as complete'));
|
|
180
|
+
console.log(chalk.gray(' Fix the issues above, then run task done again'));
|
|
181
|
+
}
|
|
244
182
|
console.log(chalk.gray(' Or use --skip-validation to bypass (not recommended)\n'));
|
|
245
183
|
process.exit(1);
|
|
246
184
|
}
|
|
185
|
+
|
|
186
|
+
// Validation passed — mark any pending history as passed
|
|
187
|
+
const successState = loadState();
|
|
188
|
+
const successFeature = successState.features[featureName];
|
|
189
|
+
if (successFeature) {
|
|
190
|
+
for (const task of tasksToComplete) {
|
|
191
|
+
if (successFeature.validationHistory?.[task.id]) {
|
|
192
|
+
successFeature.validationHistory[task.id].status = 'passed';
|
|
193
|
+
successFeature.validationHistory[task.id].updatedAt = new Date().toISOString();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
saveState(successState);
|
|
197
|
+
}
|
|
198
|
+
} else if (options.dryRun) {
|
|
199
|
+
console.log(chalk.cyan('\n ℹ️ Dry-run — tasks NOT marked as complete (validation skipped)'));
|
|
200
|
+
return [];
|
|
247
201
|
}
|
|
248
202
|
|
|
249
203
|
// Breaking change detection (non-blocking warning)
|
|
@@ -296,8 +250,8 @@ class TaskManager {
|
|
|
296
250
|
}
|
|
297
251
|
}
|
|
298
252
|
|
|
299
|
-
//
|
|
300
|
-
|
|
253
|
+
// Persist state (atomic write via state-manager)
|
|
254
|
+
saveState(state);
|
|
301
255
|
|
|
302
256
|
// Run TaskCompleted agent-teams hook for each completed task (non-blocking)
|
|
303
257
|
for (const task of results) {
|
|
@@ -335,9 +289,11 @@ class TaskManager {
|
|
|
335
289
|
}
|
|
336
290
|
|
|
337
291
|
/**
|
|
338
|
-
* Run validation for a feature using the ValidationRunner (ESM dynamic import)
|
|
292
|
+
* Run validation for a feature using the ValidationRunner (ESM dynamic import).
|
|
293
|
+
* Returns a structured result with per-validator breakdown for validationHistory.
|
|
294
|
+
*
|
|
339
295
|
* @param {string} featureName - Feature name
|
|
340
|
-
* @returns {boolean
|
|
296
|
+
* @returns {{ passed: boolean, validators: Object, passRate: number }}
|
|
341
297
|
*/
|
|
342
298
|
async runValidation(featureName) {
|
|
343
299
|
try {
|
|
@@ -347,15 +303,74 @@ class TaskManager {
|
|
|
347
303
|
const result = await runValidation('.', featureName, { verbose: true });
|
|
348
304
|
|
|
349
305
|
formatValidationResults(result);
|
|
350
|
-
|
|
306
|
+
|
|
307
|
+
// Build structured validators map from result
|
|
308
|
+
const validators = {};
|
|
309
|
+
if (result.results && typeof result.results === 'object') {
|
|
310
|
+
for (const [name, vResult] of Object.entries(result.results)) {
|
|
311
|
+
const issues = (vResult.errors || []).map(e => ({
|
|
312
|
+
message: typeof e === 'string' ? e : (e.message || String(e)),
|
|
313
|
+
file: e.file,
|
|
314
|
+
line: e.line,
|
|
315
|
+
rule: e.rule,
|
|
316
|
+
}));
|
|
317
|
+
validators[name] = {
|
|
318
|
+
passed: vResult.passed ?? (issues.length === 0),
|
|
319
|
+
issues,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
} else if (!result.passed && result.errors?.length > 0) {
|
|
323
|
+
// Flat format fallback — wrap as single 'validation' validator
|
|
324
|
+
validators['validation'] = {
|
|
325
|
+
passed: false,
|
|
326
|
+
issues: result.errors.map(e => ({
|
|
327
|
+
message: typeof e === 'string' ? e : (e.message || String(e)),
|
|
328
|
+
file: e.file,
|
|
329
|
+
line: e.line,
|
|
330
|
+
rule: e.rule || 'validation',
|
|
331
|
+
})),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const validatorEntries = Object.values(validators);
|
|
336
|
+
const passRate = validatorEntries.length > 0
|
|
337
|
+
? validatorEntries.filter(v => v.passed).length / validatorEntries.length
|
|
338
|
+
: (result.passed ? 1.0 : 0.0);
|
|
339
|
+
|
|
340
|
+
return { passed: result.passed, validators, passRate };
|
|
351
341
|
} catch (error) {
|
|
352
342
|
// If validation runner fails to load, warn but don't block task completion.
|
|
353
343
|
// This is fail-open by design: a broken validator shouldn't block commits.
|
|
354
|
-
// Common cause:
|
|
344
|
+
// Common cause: missing optional deps.
|
|
355
345
|
console.log(chalk.yellow(`\n⚠️ Validation skipped (${error.message})`));
|
|
356
346
|
console.log(chalk.gray(' Run manually: npx morph-spec validate --verbose'));
|
|
357
|
-
return true;
|
|
347
|
+
return { passed: true, validators: {}, passRate: 1.0 };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Persist a validation result to feature.validationHistory[taskId].
|
|
353
|
+
* Increments attempt counter and sets status to 'failed' or 'blocked' (attempt >= 3).
|
|
354
|
+
*
|
|
355
|
+
* @param {Object} feature - Mutable feature state object
|
|
356
|
+
* @param {string} taskId - Task ID
|
|
357
|
+
* @param {{ passed: boolean, validators: Object, passRate: number }} validationResult
|
|
358
|
+
*/
|
|
359
|
+
persistValidationHistory(feature, taskId, validationResult) {
|
|
360
|
+
if (!feature.validationHistory) {
|
|
361
|
+
feature.validationHistory = {};
|
|
358
362
|
}
|
|
363
|
+
const existing = feature.validationHistory[taskId] || { attempt: 0 };
|
|
364
|
+
const attempt = (existing.attempt || 0) + 1;
|
|
365
|
+
const status = attempt >= 3 ? 'blocked' : 'failed';
|
|
366
|
+
|
|
367
|
+
feature.validationHistory[taskId] = {
|
|
368
|
+
attempt,
|
|
369
|
+
validators: validationResult.validators || {},
|
|
370
|
+
passRate: validationResult.passRate || 0,
|
|
371
|
+
status,
|
|
372
|
+
updatedAt: new Date().toISOString(),
|
|
373
|
+
};
|
|
359
374
|
}
|
|
360
375
|
|
|
361
376
|
/**
|
|
@@ -382,13 +397,7 @@ class TaskManager {
|
|
|
382
397
|
const pending = tasks.filter(t => t.status === 'pending').length;
|
|
383
398
|
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
384
399
|
|
|
385
|
-
return {
|
|
386
|
-
total,
|
|
387
|
-
completed,
|
|
388
|
-
inProgress,
|
|
389
|
-
pending,
|
|
390
|
-
percentage
|
|
391
|
-
};
|
|
400
|
+
return { total, completed, inProgress, pending, percentage };
|
|
392
401
|
}
|
|
393
402
|
|
|
394
403
|
/**
|
|
@@ -420,7 +429,6 @@ class TaskManager {
|
|
|
420
429
|
let checkpointResult = null;
|
|
421
430
|
let validationNote = '';
|
|
422
431
|
|
|
423
|
-
// Run checkpoint hooks (new enhanced validation system)
|
|
424
432
|
try {
|
|
425
433
|
const { runCheckpointHooks, shouldRunCheckpoint } = await import('../src/lib/checkpoints/checkpoint-hooks.js');
|
|
426
434
|
|
|
@@ -432,7 +440,6 @@ class TaskManager {
|
|
|
432
440
|
} else {
|
|
433
441
|
validationNote = ` | ✗ ${checkpointResult.summary.errors} errors, ${checkpointResult.summary.warnings} warnings`;
|
|
434
442
|
|
|
435
|
-
// Check if we should block progress
|
|
436
443
|
const config = await this.loadCheckpointConfig();
|
|
437
444
|
if (config.checkpoints?.onFailure?.blockProgress && checkpointResult.summary.errors > 0) {
|
|
438
445
|
console.log(chalk.red('\n❌ Checkpoint FAILED - Fix violations before proceeding'));
|
|
@@ -442,7 +449,7 @@ class TaskManager {
|
|
|
442
449
|
}
|
|
443
450
|
} catch (error) {
|
|
444
451
|
if (error.message === 'Checkpoint validation failed') {
|
|
445
|
-
throw error;
|
|
452
|
+
throw error;
|
|
446
453
|
}
|
|
447
454
|
// Fallback to old validation if checkpoint-hooks not available
|
|
448
455
|
console.log(chalk.yellow('⚠️ Checkpoint hooks not available, using legacy validation'));
|
|
@@ -457,7 +464,6 @@ class TaskManager {
|
|
|
457
464
|
}
|
|
458
465
|
}
|
|
459
466
|
|
|
460
|
-
// Create checkpoint record
|
|
461
467
|
const checkpoint = {
|
|
462
468
|
id: `CHECKPOINT_AUTO_${Date.now()}`,
|
|
463
469
|
timestamp: new Date().toISOString(),
|
|
@@ -483,18 +489,16 @@ class TaskManager {
|
|
|
483
489
|
*/
|
|
484
490
|
async loadCheckpointConfig() {
|
|
485
491
|
try {
|
|
486
|
-
const
|
|
487
|
-
const
|
|
492
|
+
const { readFile } = await import('fs/promises');
|
|
493
|
+
const configPath = join(process.cwd(), '.morph/config/llm-interaction.json');
|
|
494
|
+
const content = await readFile(configPath, 'utf-8');
|
|
488
495
|
return JSON.parse(content);
|
|
489
496
|
} catch {
|
|
490
|
-
// Return defaults if config doesn't exist
|
|
491
497
|
return {
|
|
492
498
|
checkpoints: {
|
|
493
499
|
frequency: 3,
|
|
494
500
|
autoValidate: true,
|
|
495
|
-
onFailure: {
|
|
496
|
-
blockProgress: false
|
|
497
|
-
}
|
|
501
|
+
onFailure: { blockProgress: false }
|
|
498
502
|
}
|
|
499
503
|
};
|
|
500
504
|
}
|
|
@@ -507,7 +511,6 @@ class TaskManager {
|
|
|
507
511
|
try {
|
|
508
512
|
const config = await this.loadCheckpointConfig();
|
|
509
513
|
|
|
510
|
-
// Check if auto-generation is enabled
|
|
511
514
|
if (!config.metadata?.autoGenerate) {
|
|
512
515
|
return;
|
|
513
516
|
}
|
|
@@ -515,21 +518,14 @@ class TaskManager {
|
|
|
515
518
|
const { extractFeatureMetadata } = await import('../src/lib/generators/metadata-extractor.js');
|
|
516
519
|
const metadata = extractFeatureMetadata(feature);
|
|
517
520
|
|
|
518
|
-
const outputPath =
|
|
519
|
-
process.cwd(),
|
|
520
|
-
`.morph/features/${featureName}/metadata.json`
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
// Ensure directory exists
|
|
524
|
-
const outputDir = path.dirname(outputPath);
|
|
525
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
521
|
+
const outputPath = join(process.cwd(), `.morph/features/${featureName}/metadata.json`);
|
|
526
522
|
|
|
527
|
-
|
|
528
|
-
await
|
|
523
|
+
const { mkdir, writeFile } = await import('fs/promises');
|
|
524
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
525
|
+
await writeFile(outputPath, JSON.stringify(metadata, null, 2), 'utf-8');
|
|
529
526
|
|
|
530
|
-
console.log(chalk.gray(` 📊 Metadata updated:
|
|
527
|
+
console.log(chalk.gray(` 📊 Metadata updated: .morph/features/${featureName}/metadata.json`));
|
|
531
528
|
} catch (error) {
|
|
532
|
-
// Don't block task completion if metadata generation fails
|
|
533
529
|
console.log(chalk.yellow(` ⚠️ Metadata generation failed: ${error.message}`));
|
|
534
530
|
}
|
|
535
531
|
}
|
|
@@ -551,7 +547,6 @@ class TaskManager {
|
|
|
551
547
|
getNextTask(tasks) {
|
|
552
548
|
const pending = tasks.filter(t => t.status === 'pending');
|
|
553
549
|
|
|
554
|
-
// Find first task with all dependencies completed
|
|
555
550
|
for (const task of pending) {
|
|
556
551
|
const missingDeps = this.checkDependencies(task, tasks);
|
|
557
552
|
if (missingDeps.length === 0) {
|
|
@@ -571,7 +566,6 @@ class TaskManager {
|
|
|
571
566
|
console.log(chalk.bold(`\n📊 Progress: ${percentage}% (${completed}/${total})`));
|
|
572
567
|
console.log(chalk.gray(` Completed: ${completed} | In Progress: ${inProgress} | Pending: ${pending}`));
|
|
573
568
|
|
|
574
|
-
// Progress bar
|
|
575
569
|
const barLength = 30;
|
|
576
570
|
const filledLength = Math.round((percentage / 100) * barLength);
|
|
577
571
|
const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength);
|
|
@@ -582,7 +576,7 @@ class TaskManager {
|
|
|
582
576
|
* Start a task (mark as in_progress)
|
|
583
577
|
*/
|
|
584
578
|
async startTask(featureName, taskId) {
|
|
585
|
-
const state =
|
|
579
|
+
const state = loadState();
|
|
586
580
|
const feature = state.features[featureName];
|
|
587
581
|
|
|
588
582
|
if (!feature) {
|
|
@@ -592,12 +586,12 @@ class TaskManager {
|
|
|
592
586
|
const taskList = await ensureTaskList(feature, featureName);
|
|
593
587
|
|
|
594
588
|
if (taskList.length === 0) {
|
|
595
|
-
const tasksPath =
|
|
596
|
-
const tasksExist = await
|
|
589
|
+
const tasksPath = join(process.cwd(), `.morph/features/${featureName}/3-tasks/tasks.md`);
|
|
590
|
+
const tasksExist = await access(tasksPath).then(() => true).catch(() => false);
|
|
597
591
|
if (!tasksExist) {
|
|
598
592
|
throw new Error(`No tasks found for '${featureName}' — tasks.md not generated yet.\n Complete the tasks phase first: run /phase-tasks`);
|
|
599
593
|
}
|
|
600
|
-
throw new Error(`tasks.md found but no tasks could be parsed for '${featureName}'.\n Ensure tasks use the format: ### T001 — Title`);
|
|
594
|
+
throw new Error(`tasks.md found but no tasks could be parsed for '${featureName}'.\n Ensure tasks use the format: ### T001 — Title or ### T001: Title`);
|
|
601
595
|
}
|
|
602
596
|
|
|
603
597
|
const task = taskList.find(t => t.id === taskId);
|
|
@@ -611,7 +605,6 @@ class TaskManager {
|
|
|
611
605
|
return;
|
|
612
606
|
}
|
|
613
607
|
|
|
614
|
-
// Validate dependencies
|
|
615
608
|
const missingDeps = this.checkDependencies(task, taskList);
|
|
616
609
|
if (missingDeps.length > 0) {
|
|
617
610
|
throw new Error(`Cannot start ${taskId}: missing dependencies: ${missingDeps.join(', ')}`);
|
|
@@ -621,16 +614,68 @@ class TaskManager {
|
|
|
621
614
|
task.startedAt = new Date().toISOString();
|
|
622
615
|
syncCounters(feature);
|
|
623
616
|
|
|
624
|
-
|
|
617
|
+
saveState(state);
|
|
625
618
|
|
|
626
619
|
console.log(chalk.blue(`▶️ Task ${taskId} started: ${task.title}`));
|
|
627
620
|
}
|
|
628
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Bulk-complete tasks with a single validation pass.
|
|
624
|
+
* @param {string} featureName
|
|
625
|
+
* @param {Object} opts
|
|
626
|
+
* @param {boolean} [opts.all] - Complete all pending tasks
|
|
627
|
+
* @param {string} [opts.from] - Start of range (e.g. T001)
|
|
628
|
+
* @param {string} [opts.to] - End of range (e.g. T053)
|
|
629
|
+
* @param {string} [opts.range] - Compact range string (e.g. T001..T082)
|
|
630
|
+
* @param {boolean} [opts.skipValidation]
|
|
631
|
+
* @param {boolean} [opts.dryRun]
|
|
632
|
+
*/
|
|
633
|
+
async bulkCompleteTasks(featureName, opts = {}) {
|
|
634
|
+
const state = loadState();
|
|
635
|
+
const feature = state.features[featureName];
|
|
636
|
+
if (!feature) throw new Error(`Feature '${featureName}' not found in state.json`);
|
|
637
|
+
|
|
638
|
+
const taskList = await ensureTaskList(feature, featureName);
|
|
639
|
+
if (taskList.length === 0) throw new Error(`No tasks found for '${featureName}'`);
|
|
640
|
+
|
|
641
|
+
// Resolve task IDs from range options
|
|
642
|
+
let targetIds;
|
|
643
|
+
if (opts.all) {
|
|
644
|
+
targetIds = taskList.filter(t => t.status !== 'completed').map(t => t.id);
|
|
645
|
+
} else if (opts.from && opts.to) {
|
|
646
|
+
targetIds = this.expandRange(taskList, opts.from, opts.to);
|
|
647
|
+
} else if (opts.range && opts.range.includes('..')) {
|
|
648
|
+
const [from, to] = opts.range.split('..');
|
|
649
|
+
targetIds = this.expandRange(taskList, from.trim(), to.trim());
|
|
650
|
+
} else {
|
|
651
|
+
throw new Error('bulk-done requires --all, --from/--to, or a T001..T082 range argument');
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
console.log(chalk.cyan(`\n📦 Bulk-done: ${targetIds.length} task(s) targeted`));
|
|
655
|
+
await this.completeTasks(featureName, targetIds, { skipValidation: opts.skipValidation, dryRun: opts.dryRun });
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Expand a task range like T001..T053 using zero-padded numeric IDs.
|
|
660
|
+
*/
|
|
661
|
+
expandRange(taskList, from, to) {
|
|
662
|
+
const numOf = id => parseInt(id.replace(/\D/g, ''), 10);
|
|
663
|
+
const prefix = from.replace(/\d+$/, '');
|
|
664
|
+
const padLen = from.replace(prefix, '').length;
|
|
665
|
+
const fromNum = numOf(from);
|
|
666
|
+
const toNum = numOf(to);
|
|
667
|
+
const ids = [];
|
|
668
|
+
for (let n = fromNum; n <= toNum; n++) {
|
|
669
|
+
ids.push(prefix + String(n).padStart(padLen, '0'));
|
|
670
|
+
}
|
|
671
|
+
return ids.filter(id => taskList.some(t => t.id === id));
|
|
672
|
+
}
|
|
673
|
+
|
|
629
674
|
/**
|
|
630
675
|
* Get next task suggestion
|
|
631
676
|
*/
|
|
632
677
|
async getNext(featureName) {
|
|
633
|
-
const state =
|
|
678
|
+
const state = loadState();
|
|
634
679
|
const feature = state.features[featureName];
|
|
635
680
|
|
|
636
681
|
if (!feature) {
|
|
@@ -655,7 +700,10 @@ class TaskManager {
|
|
|
655
700
|
}
|
|
656
701
|
}
|
|
657
702
|
|
|
658
|
-
//
|
|
703
|
+
// ============================================================================
|
|
704
|
+
// CLI entry point
|
|
705
|
+
// ============================================================================
|
|
706
|
+
|
|
659
707
|
async function main() {
|
|
660
708
|
const args = process.argv.slice(2);
|
|
661
709
|
const command = args[0];
|
|
@@ -667,16 +715,40 @@ async function main() {
|
|
|
667
715
|
case 'done':
|
|
668
716
|
case 'complete': {
|
|
669
717
|
const skipValidation = args.includes('--skip-validation');
|
|
670
|
-
const
|
|
718
|
+
const dryRun = args.includes('--dry-run');
|
|
719
|
+
const filteredArgs = args.filter(a => !['--skip-validation', '--dry-run'].includes(a));
|
|
671
720
|
const featureName = filteredArgs[1];
|
|
672
721
|
const taskIds = filteredArgs.slice(2);
|
|
673
722
|
|
|
674
723
|
if (!featureName || taskIds.length === 0) {
|
|
675
|
-
console.error(chalk.red('Usage: npx morph-spec task done <feature> <task-id> [task-id...] [--skip-validation]'));
|
|
724
|
+
console.error(chalk.red('Usage: npx morph-spec task done <feature> <task-id> [task-id...] [--skip-validation] [--dry-run]'));
|
|
676
725
|
process.exit(1);
|
|
677
726
|
}
|
|
678
727
|
|
|
679
|
-
await manager.completeTasks(featureName, taskIds, { skipValidation });
|
|
728
|
+
await manager.completeTasks(featureName, taskIds, { skipValidation, dryRun });
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
case 'bulk-done': {
|
|
733
|
+
const skipValidation = args.includes('--skip-validation');
|
|
734
|
+
const dryRun = args.includes('--dry-run');
|
|
735
|
+
const allFlag = args.includes('--all');
|
|
736
|
+
const fromIdx = args.indexOf('--from');
|
|
737
|
+
const toIdx = args.indexOf('--to');
|
|
738
|
+
const filteredArgs = args.filter(a => !['--skip-validation', '--dry-run', '--all'].includes(a)
|
|
739
|
+
&& !a.startsWith('--from') && !a.startsWith('--to'));
|
|
740
|
+
const featureName = filteredArgs[1];
|
|
741
|
+
const rangeArg = filteredArgs[2]; // e.g. T001..T082
|
|
742
|
+
|
|
743
|
+
if (!featureName) {
|
|
744
|
+
console.error(chalk.red('Usage: npx morph-spec task bulk-done <feature> [--all | --from T001 --to T053 | T001..T082] [--skip-validation] [--dry-run]'));
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const fromId = fromIdx !== -1 ? args[fromIdx + 1] : null;
|
|
749
|
+
const toId = toIdx !== -1 ? args[toIdx + 1] : null;
|
|
750
|
+
|
|
751
|
+
await manager.bulkCompleteTasks(featureName, { all: allFlag, from: fromId, to: toId, range: rangeArg, skipValidation, dryRun });
|
|
680
752
|
break;
|
|
681
753
|
}
|
|
682
754
|
|
|
@@ -708,9 +780,13 @@ async function main() {
|
|
|
708
780
|
default:
|
|
709
781
|
console.error(chalk.red(`Unknown command: ${command}`));
|
|
710
782
|
console.log(chalk.gray('\nAvailable commands:'));
|
|
711
|
-
console.log(chalk.gray(' done
|
|
712
|
-
console.log(chalk.gray('
|
|
713
|
-
console.log(chalk.gray('
|
|
783
|
+
console.log(chalk.gray(' done <feature> <task-id...> - Mark tasks as completed'));
|
|
784
|
+
console.log(chalk.gray(' bulk-done <feature> --all - Mark all pending tasks as completed'));
|
|
785
|
+
console.log(chalk.gray(' bulk-done <feature> --from T001 --to T053 - Mark range as completed'));
|
|
786
|
+
console.log(chalk.gray(' bulk-done <feature> T001..T082 - Mark range as completed'));
|
|
787
|
+
console.log(chalk.gray(' start <feature> <task-id> - Start a task (mark as in_progress)'));
|
|
788
|
+
console.log(chalk.gray(' next <feature> - Show next suggested task'));
|
|
789
|
+
console.log(chalk.gray('\nFlags: --skip-validation, --dry-run'));
|
|
714
790
|
process.exit(1);
|
|
715
791
|
}
|
|
716
792
|
} catch (error) {
|
|
@@ -719,8 +795,9 @@ async function main() {
|
|
|
719
795
|
}
|
|
720
796
|
}
|
|
721
797
|
|
|
722
|
-
if (require.main === module)
|
|
798
|
+
// ESM equivalent of `if (require.main === module)`
|
|
799
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
723
800
|
main();
|
|
724
801
|
}
|
|
725
802
|
|
|
726
|
-
|
|
803
|
+
export default TaskManager;
|
package/claude-plugin.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "morph-spec",
|
|
3
|
-
"version": "4.8.
|
|
4
|
-
"displayName": "MORPH-SPEC Framework",
|
|
5
|
-
"description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
|
|
6
|
-
"publisher": "polymorphism-tech",
|
|
7
|
-
"skills": {
|
|
8
|
-
"directory": "framework/skills",
|
|
9
|
-
"namespace": "morph-spec"
|
|
10
|
-
},
|
|
11
|
-
"hooks": {
|
|
12
|
-
"directory": "framework/hooks/claude-code"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "morph-spec",
|
|
3
|
+
"version": "4.8.15",
|
|
4
|
+
"displayName": "MORPH-SPEC Framework",
|
|
5
|
+
"description": "Spec-driven development with 38 agents and 8-phase workflow for .NET/Blazor/Next.js/Azure",
|
|
6
|
+
"publisher": "polymorphism-tech",
|
|
7
|
+
"skills": {
|
|
8
|
+
"directory": "framework/skills",
|
|
9
|
+
"namespace": "morph-spec"
|
|
10
|
+
},
|
|
11
|
+
"hooks": {
|
|
12
|
+
"directory": "framework/hooks/claude-code"
|
|
13
|
+
}
|
|
14
|
+
}
|