@polymorphism-tech/morph-spec 8.1.2 → 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.
Files changed (34) hide show
  1. package/README.md +2 -2
  2. package/bin/morph-spec.js +15 -0
  3. package/claude-plugin.json +1 -1
  4. package/docs/CHEATSHEET.md +6 -4
  5. package/docs/QUICKSTART.md +1 -1
  6. package/framework/CLAUDE.md +6 -3
  7. package/framework/MORPH.md +8 -2
  8. package/framework/hooks/claude-code/core/worktree-context.js +43 -0
  9. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  10. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  11. package/framework/skills/core/morph-apply/SKILL.md +2 -2
  12. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  13. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  14. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  15. package/framework/skills/core/morph-implement/SKILL.md +1 -1
  16. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  17. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  18. package/framework/skills/core/morph-review/SKILL.md +1 -1
  19. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  20. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  21. package/package.json +1 -1
  22. package/src/commands/project/fleet.js +54 -0
  23. package/src/commands/project/worktree.js +56 -7
  24. package/src/commands/state/advance.js +8 -0
  25. package/src/commands/state/create.js +6 -0
  26. package/src/commands/state/handoff.js +49 -0
  27. package/src/lib/generators/handoff-generator.js +267 -0
  28. package/src/lib/generators/index.js +1 -3
  29. package/src/lib/worktree/env-allocator.js +149 -0
  30. package/src/lib/worktree/fleet-scanner.js +156 -0
  31. package/src/lib/context/context-builder.js +0 -289
  32. package/src/lib/generators/context-generator.js +0 -519
  33. package/src/lib/generators/mandate-generator.js +0 -468
  34. package/src/lib/generators/recap-generator.js +0 -285
@@ -1,468 +0,0 @@
1
- /**
2
- * Implementation Mandate Generator
3
- *
4
- * Synthesizes the plan-phase outputs (spec.md, tasks.json, decisions.md) into
5
- * a single task-structured implementation mandate. Contracts are read from
6
- * spec.md (v5 keeps contracts inside the spec, not in separate files).
7
- *
8
- * The mandate is the primary context artifact for the implement phase —
9
- * each task section is self-contained with acceptance criteria, contracts,
10
- * file paths, and architectural constraints.
11
- */
12
-
13
- import { readFileSync, existsSync } from 'fs';
14
- import { join } from 'path';
15
- import { validateTasksJson } from '../tasks/tasks-json-parser.js';
16
-
17
- /**
18
- * Resolve a single task object from parsed tasks.json data.
19
- * FR references are extracted from the task `description` (v5 convention:
20
- * tasks reference their FR(s) inline in the description).
21
- *
22
- * @param {object} tasksData - Parsed tasks.json structure
23
- * @param {string} taskId - Task id (e.g. 'T1')
24
- * @returns {{ id: string, title: string, description: string, frs: string[] }|null}
25
- */
26
- function resolveTask(tasksData, taskId) {
27
- const task = tasksData.tasks.find(t => t.id === taskId);
28
- if (!task) return null;
29
- const frRefs = [...(task.description || '').matchAll(/\bFR-?\d+\b/g)].map(m => m[0]);
30
- return {
31
- id: task.id,
32
- title: task.title || '',
33
- description: task.description || '',
34
- frs: [...new Set(frRefs)],
35
- files: Array.isArray(task.outputs) ? task.outputs : [],
36
- };
37
- }
38
-
39
- /**
40
- * Extract all FR sections from spec.md.
41
- *
42
- * @param {string} specContent - Full spec.md content
43
- * @returns {Map<string, { title: string, content: string, criteria: string[] }>}
44
- */
45
- function parseFRSections(specContent) {
46
- const sections = new Map();
47
- const frPattern = /^##\s+(FR\d+)[:\s—–-]+(.+?)$/gm;
48
- let match;
49
- const matches = [];
50
-
51
- while ((match = frPattern.exec(specContent)) !== null) {
52
- matches.push({
53
- id: match[1],
54
- title: match[2].trim(),
55
- start: match.index,
56
- headerEnd: match.index + match[0].length,
57
- });
58
- }
59
-
60
- for (let i = 0; i < matches.length; i++) {
61
- const current = matches[i];
62
- const nextStart = i + 1 < matches.length ? matches[i + 1].start : specContent.length;
63
- const body = specContent.slice(current.headerEnd, nextStart).trim();
64
-
65
- // Extract bullet points as acceptance criteria
66
- const criteria = [];
67
- const bulletPattern = /^[\s]*[-*]\s+(.+)/gm;
68
- let bulletMatch;
69
- while ((bulletMatch = bulletPattern.exec(body)) !== null) {
70
- criteria.push(bulletMatch[1].trim());
71
- }
72
-
73
- sections.set(current.id, {
74
- title: current.title,
75
- content: body,
76
- criteria,
77
- });
78
- }
79
-
80
- return sections;
81
- }
82
-
83
- /**
84
- * Extract contract types/interfaces from content. Matches C# records, classes,
85
- * interfaces and TypeScript types/interfaces — v5 contracts are defined inside
86
- * spec.md, so this scans the spec content.
87
- *
88
- * @param {string} contractsContent - Content to scan for contract definitions
89
- * @returns {Map<string, string>} Map of type name → definition
90
- */
91
- function parseContracts(contractsContent) {
92
- const types = new Map();
93
-
94
- // C# sealed records: sealed record Foo(...)
95
- const csRecordPattern = /^\s*((?:public\s+)?sealed\s+record\s+\w+\([^)]*\)\s*;)/gm;
96
- let match;
97
- while ((match = csRecordPattern.exec(contractsContent)) !== null) {
98
- const nameMatch = match[1].match(/record\s+(\w+)/);
99
- if (nameMatch) {
100
- types.set(nameMatch[1], match[1].trim());
101
- }
102
- }
103
-
104
- // C# interfaces: public interface IFoo { ... }
105
- const csInterfacePattern = /^\s*((?:public\s+)?interface\s+\w+\s*\{[^}]*\})/gms;
106
- while ((match = csInterfacePattern.exec(contractsContent)) !== null) {
107
- const nameMatch = match[1].match(/interface\s+(\w+)/);
108
- if (nameMatch) {
109
- types.set(nameMatch[1], match[1].trim());
110
- }
111
- }
112
-
113
- // TypeScript types/interfaces
114
- const tsTypePattern = /^\s*(export\s+(?:type|interface)\s+\w+[\s\S]*?(?:\};?|=[^;]+;))/gm;
115
- while ((match = tsTypePattern.exec(contractsContent)) !== null) {
116
- const nameMatch = match[1].match(/(?:type|interface)\s+(\w+)/);
117
- if (nameMatch) {
118
- types.set(nameMatch[1], match[1].trim());
119
- }
120
- }
121
-
122
- return types;
123
- }
124
-
125
- /**
126
- * Extract architectural constraints from decisions.md.
127
- *
128
- * @param {string} decisionsContent - Full decisions.md content
129
- * @returns {string[]} One-line constraint summaries
130
- */
131
- function parseDecisions(decisionsContent) {
132
- const constraints = [];
133
- const adrPattern = /^###?\s+(ADR-\d+)[:\s—–-]+(.+?)$/gm;
134
- let match;
135
-
136
- while ((match = adrPattern.exec(decisionsContent)) !== null) {
137
- constraints.push(`${match[1]}: ${match[2].trim()}`);
138
- }
139
-
140
- return constraints;
141
- }
142
-
143
- /**
144
- * Extract all task IDs from parsed tasks.json data, in declared order.
145
- *
146
- * @param {object} tasksData - Parsed tasks.json structure
147
- * @returns {string[]} Task IDs in order (e.g., ['T1', 'T2', ...])
148
- */
149
- function extractTaskIds(tasksData) {
150
- return tasksData.tasks.map(t => t.id);
151
- }
152
-
153
- /**
154
- * Find contract types relevant to an FR or task.
155
- * Matches by: type name in content, or FR title keywords in type name.
156
- *
157
- * @param {string} searchText - FR content + task title/description
158
- * @param {Map<string, string>} contracts - Parsed contracts
159
- * @returns {string[]} Matching contract definitions
160
- */
161
- function findRelevantContracts(searchText, contracts) {
162
- const relevant = [];
163
- const searchLower = searchText.toLowerCase();
164
-
165
- for (const [typeName, definition] of contracts) {
166
- // Direct match: type name appears in the search text
167
- if (searchText.includes(typeName)) {
168
- relevant.push(definition);
169
- continue;
170
- }
171
- // Keyword match: split type name into words (e.g., UserRegisterRequest → user, register, request)
172
- // and check if the significant words appear in the search text
173
- const typeWords = typeName
174
- .replace(/([a-z])([A-Z])/g, '$1 $2')
175
- .toLowerCase()
176
- .split(/\s+/)
177
- .filter(w => w.length > 3 && !['request', 'response', 'result'].includes(w));
178
- if (typeWords.length > 0 && typeWords.every(w => searchLower.includes(w))) {
179
- relevant.push(definition);
180
- }
181
- }
182
- return relevant;
183
- }
184
-
185
- /**
186
- * Generate an implementation mandate from spec phase outputs.
187
- *
188
- * @param {string} projectPath - Project root
189
- * @param {string} featureName - Feature name
190
- * @returns {{ mandate: string, stats: { frCount: number, taskCount: number, totalChars: number } }|null}
191
- */
192
- export function generateMandate(projectPath, featureName) {
193
- const featureDir = join(projectPath, '.morph/features', featureName);
194
-
195
- // Read required files
196
- const tasksPath = join(featureDir, '2-plan/tasks.json');
197
- const specPath = join(featureDir, '2-plan/spec.md');
198
-
199
- if (!existsSync(tasksPath) || !existsSync(specPath)) return null;
200
-
201
- let tasksData;
202
- try {
203
- tasksData = JSON.parse(readFileSync(tasksPath, 'utf-8'));
204
- } catch {
205
- return null;
206
- }
207
- if (!validateTasksJson(tasksData).valid) return null;
208
- const specContent = readFileSync(specPath, 'utf-8');
209
-
210
- // Read optional files
211
- const decisionsPath = join(featureDir, '2-plan/decisions.md');
212
- const decisionsContent = existsSync(decisionsPath) ? readFileSync(decisionsPath, 'utf-8') : '';
213
-
214
- const proposalPath = join(featureDir, '0-proposal/proposal.md');
215
- const proposalContent = existsSync(proposalPath) ? readFileSync(proposalPath, 'utf-8') : '';
216
-
217
- // Read project config
218
- let stack = '';
219
- let architecture = '';
220
- try {
221
- const configPath = join(projectPath, '.morph/config/config.json');
222
- if (existsSync(configPath)) {
223
- const config = JSON.parse(readFileSync(configPath, 'utf-8'));
224
- stack = config?.project?.stack || '';
225
- architecture = config?.architecture?.style || '';
226
- }
227
- } catch { /* ignore */ }
228
-
229
- // Parse all inputs. v5: contracts live inside spec.md (no separate
230
- // contracts.cs/.ts file) — parseContracts scans the spec content.
231
- const frSections = parseFRSections(specContent);
232
- const contracts = parseContracts(specContent);
233
- const decisions = parseDecisions(decisionsContent);
234
- const taskIds = extractTaskIds(tasksData);
235
-
236
- // Build mandate
237
- const lines = [];
238
-
239
- // Header
240
- lines.push(`# Implementation Mandate: ${featureName}`);
241
- lines.push('');
242
- lines.push(`**Generated:** ${new Date().toISOString().split('T')[0]} | **FRs:** ${frSections.size} | **Tasks:** ${taskIds.length}`);
243
- if (stack || architecture) {
244
- lines.push(`**Stack:** ${stack || 'unspecified'} | **Architecture:** ${architecture || 'unspecified'}`);
245
- }
246
-
247
- // Overview from proposal
248
- if (proposalContent) {
249
- lines.push('');
250
- lines.push('## Overview');
251
- // Extract first paragraph after the title
252
- const paragraphs = proposalContent
253
- .split(/\n\n+/)
254
- .filter(p => p.trim() && !p.trim().startsWith('#'))
255
- .slice(0, 1);
256
- if (paragraphs.length > 0) {
257
- lines.push(paragraphs[0].trim());
258
- }
259
- }
260
-
261
- // Architectural constraints
262
- if (decisions.length > 0) {
263
- lines.push('');
264
- lines.push('## Architectural Constraints');
265
- for (const d of decisions) {
266
- lines.push(`- ${d}`);
267
- }
268
- }
269
-
270
- // Task sections
271
- for (const taskId of taskIds) {
272
- const task = resolveTask(tasksData, taskId);
273
- if (!task) continue;
274
-
275
- lines.push('');
276
- lines.push('---');
277
- lines.push('');
278
- lines.push(`## ${taskId}: ${task.title}`);
279
-
280
- // Metadata line
281
- const metaParts = [];
282
- if (task.frs.length > 0) metaParts.push(`**FRs:** ${task.frs.join(', ')}`);
283
- if (task.files.length > 0) metaParts.push(`**Files:** ${task.files.join(', ')}`);
284
- if (metaParts.length > 0) lines.push(metaParts.join(' | '));
285
-
286
- // Acceptance criteria from FR sections
287
- const allCriteria = [];
288
- const allFRContracts = [];
289
- for (const frId of task.frs) {
290
- const fr = frSections.get(frId);
291
- if (fr) {
292
- allCriteria.push(...fr.criteria);
293
- // Search for contracts in FR content + task title for broader matching
294
- const searchText = `${fr.title} ${fr.content} ${task.title} ${task.description}`;
295
- allFRContracts.push(...findRelevantContracts(searchText, contracts));
296
- }
297
- }
298
-
299
- if (allCriteria.length > 0) {
300
- lines.push('');
301
- lines.push('### Acceptance Criteria');
302
- for (const c of allCriteria) {
303
- lines.push(`- [ ] ${c}`);
304
- }
305
- }
306
-
307
- // Contracts
308
- const uniqueContracts = [...new Set(allFRContracts)];
309
- if (uniqueContracts.length > 0) {
310
- lines.push('');
311
- lines.push('### Contracts');
312
- lines.push('```');
313
- lines.push(uniqueContracts.join('\n'));
314
- lines.push('```');
315
- }
316
-
317
- // Task description as notes
318
- if (task.description) {
319
- lines.push('');
320
- lines.push('### Notes');
321
- lines.push(task.description);
322
- }
323
- }
324
-
325
- // Verification summary
326
- lines.push('');
327
- lines.push('---');
328
- lines.push('');
329
- lines.push('## Verification');
330
- lines.push('```bash');
331
- if (stack.includes('dotnet')) {
332
- lines.push('dotnet build');
333
- lines.push('dotnet test');
334
- } else {
335
- lines.push('npm run build');
336
- lines.push('npm test');
337
- }
338
- lines.push('```');
339
-
340
- const mandate = lines.join('\n');
341
-
342
- return {
343
- mandate,
344
- stats: {
345
- frCount: frSections.size,
346
- taskCount: taskIds.length,
347
- totalChars: mandate.length,
348
- },
349
- };
350
- }
351
-
352
- /**
353
- * Extract a single task's section from a generated mandate.
354
- *
355
- * @param {string} mandateContent - Full mandate.md content
356
- * @param {string} taskId - Task ID (e.g., 'T003')
357
- * @returns {string|null} Task section text or null if not found
358
- */
359
- export function extractTaskSection(mandateContent, taskId) {
360
- // Find the start of this task's section
361
- const headerPattern = new RegExp(`^## ${taskId}:`, 'm');
362
- const headerMatch = headerPattern.exec(mandateContent);
363
- if (!headerMatch) return null;
364
-
365
- const start = headerMatch.index;
366
-
367
- // Find the end: next task header, Verification section, or end of content
368
- const rest = mandateContent.slice(start + 1);
369
- const endPatterns = [
370
- /\n## T\d+:/,
371
- /\n## Verification/,
372
- ];
373
-
374
- let end = mandateContent.length;
375
- for (const pattern of endPatterns) {
376
- const endMatch = pattern.exec(rest);
377
- if (endMatch) {
378
- const candidate = start + 1 + endMatch.index;
379
- if (candidate < end) end = candidate;
380
- }
381
- }
382
-
383
- // Extract and trim, removing trailing --- separators
384
- let section = mandateContent.slice(start, end).trim();
385
- section = section.replace(/\n---\s*$/, '').trim();
386
-
387
- return section;
388
- }
389
-
390
- /**
391
- * Extract multiple task sections (for agent dispatch).
392
- *
393
- * @param {string} mandateContent - Full mandate.md content
394
- * @param {string[]} taskIds - Task IDs (e.g., ['T001', 'T002', 'T003'])
395
- * @returns {string} Combined task sections
396
- */
397
- export function extractTaskSections(mandateContent, taskIds) {
398
- const sections = [];
399
- for (const taskId of taskIds) {
400
- const section = extractTaskSection(mandateContent, taskId);
401
- if (section) sections.push(section);
402
- }
403
- return sections.join('\n\n---\n\n');
404
- }
405
-
406
- /**
407
- * Generate a mandate overview (for session start injection).
408
- * Shows FR list with task mapping and completion status.
409
- *
410
- * @param {string} mandateContent - Full mandate.md content
411
- * @param {Object} taskStatus - Map of taskId → status ('completed'|'in_progress'|'pending')
412
- * @returns {string} Overview text
413
- */
414
- export function generateMandateOverview(mandateContent, taskStatus = {}) {
415
- const lines = [];
416
-
417
- // Extract task-to-FR mapping from mandate
418
- const taskFRMap = new Map();
419
- const frTaskMap = new Map();
420
-
421
- const taskPattern = /^## (T\d+): .+$/gm;
422
- const frLinePattern = /^\*\*FRs:\*\*\s+(.+?)(?:\s*\||$)/gm;
423
-
424
- let match;
425
- let lastTaskId = null;
426
-
427
- while ((match = taskPattern.exec(mandateContent)) !== null) {
428
- lastTaskId = match[1];
429
- // Look for FRs line right after task header
430
- const afterHeader = mandateContent.slice(match.index + match[0].length, match.index + match[0].length + 200);
431
- const frMatch = afterHeader.match(/\*\*FRs:\*\*\s+([^\n|]+)/);
432
- if (frMatch) {
433
- const frs = frMatch[1].split(',').map(f => f.trim()).filter(Boolean);
434
- taskFRMap.set(lastTaskId, frs);
435
- for (const fr of frs) {
436
- if (!frTaskMap.has(fr)) frTaskMap.set(fr, []);
437
- frTaskMap.get(fr).push(lastTaskId);
438
- }
439
- }
440
- }
441
-
442
- // Build overview
443
- lines.push('Implementation Mandate Overview:');
444
-
445
- for (const [frId, taskIds] of frTaskMap) {
446
- const taskStatuses = taskIds.map(tid => {
447
- const status = taskStatus[tid] || 'pending';
448
- if (status === 'completed') return `${tid} ✓`;
449
- if (status === 'in_progress') return `${tid} →`;
450
- return tid;
451
- });
452
- lines.push(` ${frId} → ${taskStatuses.join(', ')}`);
453
- }
454
-
455
- // Next task
456
- const allTaskIds = [...taskFRMap.keys()];
457
- const nextTask = allTaskIds.find(tid => {
458
- const status = taskStatus[tid];
459
- return status === 'in_progress' || !status || status === 'pending';
460
- });
461
- if (nextTask) {
462
- const status = taskStatus[nextTask];
463
- const verb = status === 'in_progress' ? 'In progress' : 'Next';
464
- lines.push(` ${verb}: ${nextTask}`);
465
- }
466
-
467
- return lines.join('\n');
468
- }