@polymorphism-tech/morph-spec 8.1.1 → 8.2.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/README.md +2 -2
- package/bin/morph-spec.js +15 -0
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +6 -4
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +6 -3
- package/framework/MORPH.md +8 -2
- package/framework/hooks/claude-code/core/validate-completion.js +2 -1
- package/framework/hooks/claude-code/core/worktree-context.js +43 -0
- package/framework/hooks/shared/build-check.js +97 -24
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +2 -2
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/project/fleet.js +54 -0
- package/src/commands/project/worktree.js +56 -7
- package/src/commands/state/advance.js +8 -0
- package/src/commands/state/create.js +6 -0
- package/src/commands/state/handoff.js +49 -0
- package/src/lib/generators/handoff-generator.js +267 -0
- package/src/lib/generators/index.js +1 -3
- package/src/lib/validators/verify-runner.js +24 -7
- package/src/lib/worktree/env-allocator.js +149 -0
- package/src/lib/worktree/fleet-scanner.js +156 -0
- package/src/lib/context/context-builder.js +0 -289
- package/src/lib/generators/context-generator.js +0 -519
- package/src/lib/generators/mandate-generator.js +0 -468
- package/src/lib/generators/recap-generator.js +0 -285
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Recap Generator
|
|
3
|
-
*
|
|
4
|
-
* Auto-generates recap.md from real project data: state.json, contract compliance,
|
|
5
|
-
* validation results, and decisions. Replaces manual template filling.
|
|
6
|
-
*
|
|
7
|
-
* MORPH-SPEC 5.0 - Recap Generator
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
11
|
-
import { join, dirname } from 'path';
|
|
12
|
-
import chalk from 'chalk';
|
|
13
|
-
import { loadState, markOutput } from '../../core/state/state-manager.js';
|
|
14
|
-
import { runValidation } from '../validators/validation-runner.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Generate recap.md for a feature
|
|
18
|
-
*
|
|
19
|
-
* @param {string} projectPath - Project root path
|
|
20
|
-
* @param {string} featureName - Feature name
|
|
21
|
-
* @param {Object} options - Options
|
|
22
|
-
* @returns {string} Generated recap content
|
|
23
|
-
*/
|
|
24
|
-
export async function generateRecap(projectPath, featureName, options = {}) {
|
|
25
|
-
const state = loadState(false);
|
|
26
|
-
if (!state || !state.features[featureName]) {
|
|
27
|
-
throw new Error(`Feature '${featureName}' not found in state.json`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const feature = state.features[featureName];
|
|
31
|
-
const outputsPath = join(projectPath, '.morph/features', featureName);
|
|
32
|
-
|
|
33
|
-
// Gather data
|
|
34
|
-
const tasksSummary = getTasksSummary(feature);
|
|
35
|
-
const agentsSummary = getAgentsSummary(feature);
|
|
36
|
-
const checkpointsSummary = getCheckpointsSummary(feature);
|
|
37
|
-
// Decision constraints support (stub - decision-constraint-loader removed as orphaned code)
|
|
38
|
-
const decisions = { decisions: [] };
|
|
39
|
-
const validationResult = await runValidation(projectPath, featureName, { verbose: false });
|
|
40
|
-
|
|
41
|
-
// Contract coverage — derived from validation issues. v5 keeps contracts
|
|
42
|
-
// inside spec.md, so there is no separate contracts file to probe.
|
|
43
|
-
let contractCoverage = null;
|
|
44
|
-
if (validationResult.issues) {
|
|
45
|
-
const contractIssues = validationResult.issues.filter(i => i.message?.includes('Interface') || i.message?.includes('contracts'));
|
|
46
|
-
if (contractIssues.length > 0) {
|
|
47
|
-
contractCoverage = extractContractCoverage(validationResult);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Generate markdown
|
|
52
|
-
const recap = buildRecapMarkdown({
|
|
53
|
-
featureName,
|
|
54
|
-
feature,
|
|
55
|
-
tasksSummary,
|
|
56
|
-
agentsSummary,
|
|
57
|
-
checkpointsSummary,
|
|
58
|
-
decisions: decisions.decisions,
|
|
59
|
-
validationResult,
|
|
60
|
-
contractCoverage
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Write to file
|
|
64
|
-
const recapPath = join(outputsPath, 'recap.md');
|
|
65
|
-
if (!existsSync(dirname(recapPath))) {
|
|
66
|
-
mkdirSync(dirname(recapPath), { recursive: true });
|
|
67
|
-
}
|
|
68
|
-
writeFileSync(recapPath, recap, 'utf-8');
|
|
69
|
-
|
|
70
|
-
// Auto-mark recap output as created in state
|
|
71
|
-
try {
|
|
72
|
-
await markOutput(featureName, 'recap');
|
|
73
|
-
} catch {
|
|
74
|
-
// Non-fatal: state may not exist in all contexts
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!options.quiet) {
|
|
78
|
-
console.log(chalk.green(`\n✅ Recap generated: ${recapPath}`));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return recap;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function getTasksSummary(feature) {
|
|
85
|
-
// v2: feature.tasks is an array of task objects
|
|
86
|
-
if (Array.isArray(feature.tasks)) {
|
|
87
|
-
const tasks = feature.tasks;
|
|
88
|
-
const completed = tasks.filter(t => t.status === 'completed');
|
|
89
|
-
const pending = tasks.filter(t => t.status === 'pending');
|
|
90
|
-
const inProgress = tasks.filter(t => t.status === 'in_progress');
|
|
91
|
-
return {
|
|
92
|
-
total: tasks.length,
|
|
93
|
-
completed: completed.length,
|
|
94
|
-
pending: pending.length,
|
|
95
|
-
inProgress: inProgress.length,
|
|
96
|
-
percentage: tasks.length > 0 ? Math.round((completed.length / tasks.length) * 100) : 0,
|
|
97
|
-
items: tasks
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// v3-v4 legacy: feature.tasks is a counter object {total, completed, inProgress, pending}
|
|
102
|
-
const counters = feature.tasks || {};
|
|
103
|
-
const items = [];
|
|
104
|
-
const total = counters.total || items.length || 0;
|
|
105
|
-
const completedCount = counters.completed ?? items.filter(t => t.status === 'completed').length;
|
|
106
|
-
const pendingCount = counters.pending ?? items.filter(t => t.status === 'pending').length;
|
|
107
|
-
const inProgressCount = counters.inProgress ?? items.filter(t => t.status === 'in_progress').length;
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
total,
|
|
111
|
-
completed: completedCount,
|
|
112
|
-
pending: pendingCount,
|
|
113
|
-
inProgress: inProgressCount,
|
|
114
|
-
percentage: total > 0 ? Math.round((completedCount / total) * 100) : 0,
|
|
115
|
-
items
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function getAgentsSummary(feature) {
|
|
120
|
-
return feature.activeAgents || [];
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function getCheckpointsSummary(feature) {
|
|
124
|
-
return (feature.checkpoints || []).map(cp => ({
|
|
125
|
-
timestamp: cp.timestamp,
|
|
126
|
-
note: cp.note,
|
|
127
|
-
tasks: cp.tasksCompleted || []
|
|
128
|
-
}));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function extractContractCoverage(validationResult) {
|
|
132
|
-
// Look for contract compliance data in validation issues
|
|
133
|
-
const interfaceErrors = validationResult.issues.filter(i =>
|
|
134
|
-
i.message?.includes('Interface') && i.level === 'error'
|
|
135
|
-
);
|
|
136
|
-
const methodErrors = validationResult.issues.filter(i =>
|
|
137
|
-
i.message?.includes('Method') && i.message?.includes('missing') && i.level === 'error'
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
missingInterfaces: interfaceErrors.length,
|
|
142
|
-
missingMethods: methodErrors.length,
|
|
143
|
-
hasIssues: interfaceErrors.length > 0 || methodErrors.length > 0
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function buildRecapMarkdown(data) {
|
|
148
|
-
const { featureName, feature, tasksSummary, agentsSummary, checkpointsSummary, decisions, validationResult, contractCoverage } = data;
|
|
149
|
-
const now = new Date().toISOString().split('T')[0];
|
|
150
|
-
|
|
151
|
-
let md = `# Recap: ${featureName}\n\n`;
|
|
152
|
-
md += `> Generated: ${now} | Phase: ${feature.phase} | Status: ${feature.status}\n\n`;
|
|
153
|
-
md += `---\n\n`;
|
|
154
|
-
|
|
155
|
-
// Progress
|
|
156
|
-
md += `## Progress\n\n`;
|
|
157
|
-
md += `| Metric | Value |\n|--------|-------|\n`;
|
|
158
|
-
md += `| Tasks completed | ${tasksSummary.completed}/${tasksSummary.total} (${tasksSummary.percentage}%) |\n`;
|
|
159
|
-
md += `| Tasks pending | ${tasksSummary.pending} |\n`;
|
|
160
|
-
md += `| Tasks in progress | ${tasksSummary.inProgress} |\n`;
|
|
161
|
-
md += `| Active agents | ${agentsSummary.length} |\n`;
|
|
162
|
-
md += `| Checkpoints | ${checkpointsSummary.length} |\n\n`;
|
|
163
|
-
|
|
164
|
-
// Validation Results
|
|
165
|
-
md += `## Validation Results\n\n`;
|
|
166
|
-
if (validationResult.passed) {
|
|
167
|
-
md += `**Status: PASSED** (${validationResult.warnings} warnings)\n\n`;
|
|
168
|
-
} else {
|
|
169
|
-
md += `**Status: FAILED** (${validationResult.errors} errors, ${validationResult.warnings} warnings)\n\n`;
|
|
170
|
-
if (validationResult.issues.length > 0) {
|
|
171
|
-
md += `| Level | Issue |\n|-------|-------|\n`;
|
|
172
|
-
for (const issue of validationResult.issues.slice(0, 15)) {
|
|
173
|
-
md += `| ${issue.level} | ${issue.message} |\n`;
|
|
174
|
-
}
|
|
175
|
-
md += `\n`;
|
|
176
|
-
}
|
|
177
|
-
md += `> **Note:** Errors above are from morph-spec static validators (may include false positives).\n`;
|
|
178
|
-
md += `> Verify actual build status with the commands in the section below.\n\n`;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Score Breakdown (when rubric scoring is available)
|
|
182
|
-
if (validationResult.scores) {
|
|
183
|
-
const scores = validationResult.scores;
|
|
184
|
-
md += `### Quality Score: ${scores.score}/10\n\n`;
|
|
185
|
-
md += `| Category | Score | Weight | Status |\n|----------|-------|--------|--------|\n`;
|
|
186
|
-
const approveThreshold = scores.approveThreshold ?? 9.0;
|
|
187
|
-
for (const [category, data] of Object.entries(scores.breakdown)) {
|
|
188
|
-
if (category === '_unmapped') continue;
|
|
189
|
-
const pct = Math.round(data.weight * 100);
|
|
190
|
-
const status = data.score >= approveThreshold ? 'Pass' : data.score >= 6 ? 'Needs work' : 'Critical';
|
|
191
|
-
md += `| ${category} | ${Math.round(data.score * 10) / 10}/10 | ${pct}% | ${status} |\n`;
|
|
192
|
-
}
|
|
193
|
-
md += `\n`;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Suggested Quality Verification
|
|
197
|
-
const agents = agentsSummary;
|
|
198
|
-
const hasNextjs = agents.some(a => ['nextjs-expert', 'css-specialist', 'ui-designer'].includes(a));
|
|
199
|
-
const hasDotnet = agents.some(a => ['dotnet-senior', 'ef-modeler'].includes(a));
|
|
200
|
-
if (hasNextjs || hasDotnet) {
|
|
201
|
-
md += `## Suggested Quality Verification\n\n`;
|
|
202
|
-
if (hasNextjs) {
|
|
203
|
-
md += `**Next.js / TypeScript:**\n\`\`\`bash\nnpx tsc --noEmit\nnpx eslint . --max-warnings 0\n\`\`\`\n\n`;
|
|
204
|
-
}
|
|
205
|
-
if (hasDotnet) {
|
|
206
|
-
md += `**.NET:**\n\`\`\`bash\ndotnet build\ndotnet test\n\`\`\`\n\n`;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Contract Coverage
|
|
211
|
-
if (contractCoverage) {
|
|
212
|
-
md += `## Contract Compliance\n\n`;
|
|
213
|
-
if (contractCoverage.hasIssues) {
|
|
214
|
-
md += `- Missing interfaces: ${contractCoverage.missingInterfaces}\n`;
|
|
215
|
-
md += `- Missing methods: ${contractCoverage.missingMethods}\n\n`;
|
|
216
|
-
} else {
|
|
217
|
-
md += `All contracts implemented.\n\n`;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Decisions
|
|
222
|
-
if (decisions.length > 0) {
|
|
223
|
-
md += `## Decisions Applied\n\n`;
|
|
224
|
-
for (const d of decisions) {
|
|
225
|
-
if (d.title) {
|
|
226
|
-
md += `- **${d.title}**: ${d.decision.slice(0, 100)}${d.decision.length > 100 ? '...' : ''}\n`;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
md += `\n`;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Agents
|
|
233
|
-
if (agentsSummary.length > 0) {
|
|
234
|
-
md += `## Active Agents\n\n`;
|
|
235
|
-
md += agentsSummary.map(a => `- ${a}`).join('\n');
|
|
236
|
-
md += `\n\n`;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Checkpoints
|
|
240
|
-
if (checkpointsSummary.length > 0) {
|
|
241
|
-
md += `## Checkpoints\n\n`;
|
|
242
|
-
for (const cp of checkpointsSummary) {
|
|
243
|
-
md += `- ${cp.timestamp}: ${cp.note}\n`;
|
|
244
|
-
}
|
|
245
|
-
md += `\n`;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Tasks Detail
|
|
249
|
-
if (tasksSummary.items.length > 0) {
|
|
250
|
-
const anyNotes = tasksSummary.items.some(t => t.notes?.length > 0);
|
|
251
|
-
md += `## Tasks\n\n`;
|
|
252
|
-
if (anyNotes) {
|
|
253
|
-
md += `| ID | Title | Status | Notes |\n|----|-------|--------|-------|\n`;
|
|
254
|
-
for (const task of tasksSummary.items) {
|
|
255
|
-
const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
|
|
256
|
-
const noteCount = task.notes?.length || 0;
|
|
257
|
-
const notesCell = noteCount > 0 ? `${noteCount} note${noteCount > 1 ? 's' : ''}` : '-';
|
|
258
|
-
md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} | ${notesCell} |\n`;
|
|
259
|
-
}
|
|
260
|
-
} else {
|
|
261
|
-
md += `| ID | Title | Status |\n|----|-------|--------|\n`;
|
|
262
|
-
for (const task of tasksSummary.items) {
|
|
263
|
-
const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
|
|
264
|
-
md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} |\n`;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
md += `\n`;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Deviations from Plan
|
|
271
|
-
const deviations = tasksSummary.items
|
|
272
|
-
.filter(t => t.notes?.some(n => n.type === 'deviation'))
|
|
273
|
-
.flatMap(t => t.notes.filter(n => n.type === 'deviation').map(n => ({ taskId: t.id, text: n.text })));
|
|
274
|
-
if (deviations.length > 0) {
|
|
275
|
-
md += `## Deviations from Plan\n\n`;
|
|
276
|
-
for (const d of deviations) {
|
|
277
|
-
md += `- **${d.taskId}**: ${d.text}\n`;
|
|
278
|
-
}
|
|
279
|
-
md += `\n`;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
md += `---\n\n*Generated by MORPH-SPEC Recap Generator*\n`;
|
|
283
|
-
|
|
284
|
-
return md;
|
|
285
|
-
}
|