@polymorphism-tech/morph-spec 4.8.11 → 4.8.14
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/{task-manager.cjs → task-manager.js} +47 -158
- package/claude-plugin.json +14 -14
- package/docs/CHEATSHEET.md +203 -203
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents.json +111 -24
- package/framework/hooks/README.md +202 -202
- package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +6 -0
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +7 -0
- package/framework/hooks/claude-code/statusline.py +6 -0
- package/framework/hooks/claude-code/stop/validate-completion.js +21 -2
- package/framework/hooks/dev/guard-version-numbers.js +1 -1
- package/framework/hooks/shared/phase-utils.js +3 -0
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +55 -0
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +57 -1
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +23 -1
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +25 -2
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +1 -1
- package/package.json +87 -87
- package/src/commands/project/update.js +12 -2
- package/src/commands/state/advance-phase.js +32 -13
- package/src/commands/tasks/task.js +2 -2
- package/src/core/paths/output-schema.js +1 -0
- package/src/lib/detectors/design-system-detector.js +5 -4
- package/src/lib/tasks/task-parser.js +94 -0
- package/src/lib/validators/content/content-validator.js +34 -106
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Parser — Shared utilities for reading and syncing task state.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from bin/task-manager.cjs so both the task-manager and any
|
|
5
|
+
* future consumers can share the same parsing logic without duplication.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile } from 'fs/promises';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// parseTasksMd
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parse tasks.md to extract task stubs for the v3 state format.
|
|
17
|
+
* Accepts headings in either format:
|
|
18
|
+
* ### T001 — Task title (em-dash / en-dash / hyphen)
|
|
19
|
+
* ### T001: Task title (colon)
|
|
20
|
+
*
|
|
21
|
+
* @param {string} featureName
|
|
22
|
+
* @param {string} [cwd] - Project root (defaults to process.cwd())
|
|
23
|
+
* @returns {Promise<Array<{id, title, status, dependencies, files, checkpoint}>>}
|
|
24
|
+
*/
|
|
25
|
+
export async function parseTasksMd(featureName, cwd = process.cwd()) {
|
|
26
|
+
const tasksPath = join(cwd, `.morph/features/${featureName}/3-tasks/tasks.md`);
|
|
27
|
+
let content = '';
|
|
28
|
+
try {
|
|
29
|
+
content = await readFile(tasksPath, 'utf-8');
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const tasks = [];
|
|
35
|
+
const headingRe = /^###\s+(T\d+)\s*[—–:\-]\s*(.+)$/gm;
|
|
36
|
+
let match;
|
|
37
|
+
while ((match = headingRe.exec(content)) !== null) {
|
|
38
|
+
tasks.push({
|
|
39
|
+
id: match[1],
|
|
40
|
+
title: match[2].trim(),
|
|
41
|
+
status: 'pending',
|
|
42
|
+
dependencies: [],
|
|
43
|
+
files: [],
|
|
44
|
+
checkpoint: null,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return tasks;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// ensureTaskList
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Ensure feature.taskList exists (array of individual task objects).
|
|
56
|
+
*
|
|
57
|
+
* In v3 state, feature.tasks is a counter object {total, completed, …}.
|
|
58
|
+
* Individual task objects live in feature.taskList.
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} feature - Feature state object (mutated in place)
|
|
61
|
+
* @param {string} featureName
|
|
62
|
+
* @param {string} [cwd]
|
|
63
|
+
* @returns {Promise<Array>} The task list
|
|
64
|
+
*/
|
|
65
|
+
export async function ensureTaskList(feature, featureName, cwd = process.cwd()) {
|
|
66
|
+
if (Array.isArray(feature.tasks)) {
|
|
67
|
+
// v2 format: tasks IS the array
|
|
68
|
+
return feature.tasks;
|
|
69
|
+
}
|
|
70
|
+
// v3 format: use taskList or build from tasks.md
|
|
71
|
+
if (!feature.taskList || feature.taskList.length === 0) {
|
|
72
|
+
feature.taskList = await parseTasksMd(featureName, cwd);
|
|
73
|
+
}
|
|
74
|
+
return feature.taskList;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// syncCounters
|
|
79
|
+
// ============================================================================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* After modifying taskList, sync the counter fields back to feature.tasks.
|
|
83
|
+
* tasks.total is derived from the actual taskList length (not trusted from state).
|
|
84
|
+
*
|
|
85
|
+
* @param {Object} feature - Feature state object (mutated in place)
|
|
86
|
+
*/
|
|
87
|
+
export function syncCounters(feature) {
|
|
88
|
+
if (Array.isArray(feature.tasks)) return; // v2 format — nothing to sync
|
|
89
|
+
const list = feature.taskList || [];
|
|
90
|
+
feature.tasks.total = list.length;
|
|
91
|
+
feature.tasks.completed = list.filter(t => t.status === 'completed').length;
|
|
92
|
+
feature.tasks.inProgress = list.filter(t => t.status === 'in_progress').length;
|
|
93
|
+
feature.tasks.pending = list.filter(t => t.status === 'pending').length;
|
|
94
|
+
}
|
|
@@ -23,16 +23,19 @@ export function validateSpecContent(specPath) {
|
|
|
23
23
|
|
|
24
24
|
const content = readFileSync(specPath, 'utf8');
|
|
25
25
|
|
|
26
|
-
// Required sections for a complete spec
|
|
26
|
+
// Required sections for a complete spec (prefix-match to handle variants like
|
|
27
|
+
// "## Functional Requirements" and "## Technical Architecture")
|
|
27
28
|
const requiredSections = [
|
|
28
29
|
'## Overview',
|
|
29
|
-
'## Requirements',
|
|
30
|
-
'## Technical
|
|
30
|
+
'## Functional Requirements',
|
|
31
|
+
'## Technical',
|
|
31
32
|
'## Data Model',
|
|
32
|
-
'## API Contracts'
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
-
const
|
|
35
|
+
const lines = content.split('\n');
|
|
36
|
+
const missing = requiredSections.filter(section =>
|
|
37
|
+
!lines.some(line => line.startsWith(section))
|
|
38
|
+
);
|
|
36
39
|
|
|
37
40
|
// Additional quality checks
|
|
38
41
|
const errors = [];
|
|
@@ -79,8 +82,8 @@ export function validateSpecContent(specPath) {
|
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
/**
|
|
82
|
-
* Validate tasks.
|
|
83
|
-
* @param {string} tasksPath - Path to tasks.
|
|
85
|
+
* Validate tasks.md structure
|
|
86
|
+
* @param {string} tasksPath - Path to tasks.md file
|
|
84
87
|
* @returns {Object} Validation result
|
|
85
88
|
*/
|
|
86
89
|
export function validateTasksContent(tasksPath) {
|
|
@@ -91,111 +94,36 @@ export function validateTasksContent(tasksPath) {
|
|
|
91
94
|
};
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const content = readFileSync(tasksPath, 'utf8');
|
|
97
|
-
tasks = JSON.parse(content);
|
|
98
|
-
} catch (error) {
|
|
99
|
-
return {
|
|
100
|
-
valid: false,
|
|
101
|
-
errors: ['Invalid JSON in tasks file: ' + error.message]
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
97
|
+
const content = readFileSync(tasksPath, 'utf8');
|
|
105
98
|
const errors = [];
|
|
106
99
|
const warnings = [];
|
|
107
100
|
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
101
|
+
// Extract task IDs from headings: ### T001 — title OR ### T001: title
|
|
102
|
+
const taskRe = /^###\s+(T\d{3})\s*[—–:\-]\s*(.+)$/gm;
|
|
103
|
+
const checkpointRe = /^###\s+(CHECKPOINT[_-]\d+)\s*[—–:\-]\s*(.+)$/gm;
|
|
112
104
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return { valid: false, errors, warnings };
|
|
116
|
-
}
|
|
105
|
+
const tasks = [...content.matchAll(taskRe)];
|
|
106
|
+
const checkpoints = [...content.matchAll(checkpointRe)];
|
|
117
107
|
|
|
118
|
-
if (tasks.
|
|
119
|
-
errors.push('
|
|
108
|
+
if (tasks.length === 0) {
|
|
109
|
+
errors.push('No tasks found in tasks.md — expected headings like "### T001 — Title" or "### T001: Title"');
|
|
120
110
|
return { valid: false, errors, warnings };
|
|
121
111
|
}
|
|
122
112
|
|
|
123
|
-
// Validate
|
|
124
|
-
tasks.
|
|
125
|
-
|
|
113
|
+
// Validate no duplicate IDs
|
|
114
|
+
const ids = tasks.map(m => m[1]);
|
|
115
|
+
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
|
|
116
|
+
dupes.forEach(id => errors.push(`Duplicate task ID: ${id}`));
|
|
126
117
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
warnings.push(`${taskId}: ID should follow format T### or CHECKPOINT_###`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (!task.title) {
|
|
135
|
-
errors.push(`${taskId}: Missing "title" field`);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (!task.description) {
|
|
139
|
-
errors.push(`${taskId}: Missing "description" field`);
|
|
140
|
-
}
|
|
118
|
+
// Check for dependency references (look for "Dependencies: T###")
|
|
119
|
+
const depRe = /\*\*Dependenc(?:y|ies):\*\*\s*([T\d, ]+)/gi;
|
|
120
|
+
const allDepRefs = [...content.matchAll(depRe)]
|
|
121
|
+
.flatMap(m => m[1].split(/[,\s]+/).filter(s => /^T\d{3}$/.test(s)));
|
|
141
122
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// For regular tasks (not checkpoints)
|
|
147
|
-
if (task.id && task.id.startsWith('T')) {
|
|
148
|
-
if (!task.category) {
|
|
149
|
-
warnings.push(`${taskId}: Missing "category" field`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (!task.estimatedMinutes) {
|
|
153
|
-
warnings.push(`${taskId}: Missing "estimatedMinutes" field`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (!task.files || task.files.length === 0) {
|
|
157
|
-
warnings.push(`${taskId}: No files specified - consider adding affected files`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// For checkpoints
|
|
162
|
-
if (task.id && task.id.startsWith('CHECKPOINT')) {
|
|
163
|
-
if (!task.afterTasks || task.afterTasks.length === 0) {
|
|
164
|
-
warnings.push(`${taskId}: Checkpoint should specify "afterTasks"`);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (!task.validations || task.validations.length === 0) {
|
|
168
|
-
warnings.push(`${taskId}: Checkpoint should specify "validations"`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// Check for orphaned tasks (missing dependencies)
|
|
174
|
-
const taskIds = new Set(tasks.tasks.map(t => t.id));
|
|
175
|
-
tasks.tasks.forEach(task => {
|
|
176
|
-
if (task.dependencies && Array.isArray(task.dependencies)) {
|
|
177
|
-
task.dependencies.forEach(depId => {
|
|
178
|
-
if (depId && !taskIds.has(depId)) {
|
|
179
|
-
errors.push(`${task.id}: References non-existent dependency "${depId}"`);
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Check for circular dependencies (simple check)
|
|
186
|
-
const hasCycle = (taskId, visited = new Set()) => {
|
|
187
|
-
if (visited.has(taskId)) return true;
|
|
188
|
-
visited.add(taskId);
|
|
189
|
-
|
|
190
|
-
const task = tasks.tasks.find(t => t.id === taskId);
|
|
191
|
-
if (!task || !task.dependencies) return false;
|
|
192
|
-
|
|
193
|
-
return task.dependencies.some(depId => hasCycle(depId, new Set(visited)));
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
tasks.tasks.forEach(task => {
|
|
197
|
-
if (task.id && hasCycle(task.id)) {
|
|
198
|
-
errors.push(`Circular dependency detected involving task ${task.id}`);
|
|
123
|
+
const idSet = new Set(ids);
|
|
124
|
+
allDepRefs.forEach(depId => {
|
|
125
|
+
if (!idSet.has(depId)) {
|
|
126
|
+
warnings.push(`Dependency reference "${depId}" not found in task list`);
|
|
199
127
|
}
|
|
200
128
|
});
|
|
201
129
|
|
|
@@ -204,10 +132,10 @@ export function validateTasksContent(tasksPath) {
|
|
|
204
132
|
errors,
|
|
205
133
|
warnings,
|
|
206
134
|
stats: {
|
|
207
|
-
totalTasks: tasks.
|
|
208
|
-
regularTasks: tasks.
|
|
209
|
-
checkpoints:
|
|
210
|
-
}
|
|
135
|
+
totalTasks: tasks.length + checkpoints.length,
|
|
136
|
+
regularTasks: tasks.length,
|
|
137
|
+
checkpoints: checkpoints.length,
|
|
138
|
+
},
|
|
211
139
|
};
|
|
212
140
|
}
|
|
213
141
|
|