@polymorphism-tech/morph-spec 4.10.10 → 4.11.1

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 (62) hide show
  1. package/CLAUDE.md +106 -2
  2. package/README.md +5 -11
  3. package/bin/morph-spec.js +30 -2
  4. package/bin/task-manager.js +394 -38
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +2 -8
  8. package/framework/agents/frontend/css-specialist.md +1 -1
  9. package/framework/agents/infrastructure/azure-deploy-specialist.md +4 -4
  10. package/framework/commands/morph-apply.md +39 -19
  11. package/framework/commands/morph-preflight.md +3 -3
  12. package/framework/commands/morph-proposal.md +59 -16
  13. package/framework/commands/morph-status.md +3 -3
  14. package/framework/commands/morph-troubleshoot.md +1 -1
  15. package/framework/hooks/claude-code/post-tool-use/dispatch.js +47 -2
  16. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +2 -2
  17. package/framework/hooks/claude-code/session-start/inject-morph-context.js +5 -4
  18. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +3 -3
  19. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +2 -2
  20. package/framework/hooks/git/pre-commit/agents.sh +1 -1
  21. package/framework/hooks/shared/dispatch-helpers.js +14 -0
  22. package/framework/hooks/shared/state-reader.js +7 -3
  23. package/framework/rules/morph-task-tracking.md +49 -0
  24. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +14 -18
  25. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +3 -3
  26. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +1 -1
  27. package/framework/skills/level-0-meta/morph-init/SKILL.md +11 -11
  28. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +3 -3
  29. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +1 -1
  30. package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +4 -4
  31. package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +25 -25
  32. package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +8 -8
  33. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +16 -7
  34. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +4 -4
  35. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +29 -35
  36. package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +1 -1
  37. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +35 -29
  38. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +4 -4
  39. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +17 -20
  40. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +23 -26
  41. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +21 -29
  42. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +24 -16
  43. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +5 -5
  44. package/framework/workflows/README.md +18 -18
  45. package/framework/workflows/docs/enforcement-pipeline.md +1 -1
  46. package/framework/workflows/docs/full-morph.md +2 -2
  47. package/package.json +1 -1
  48. package/src/commands/agents/dispatch-agents.js +25 -1
  49. package/src/commands/project/doctor.js +3 -4
  50. package/src/commands/project/status.js +8 -1
  51. package/src/commands/scope/escalate.js +5 -3
  52. package/src/commands/tasks/task.js +54 -0
  53. package/src/commands/validation/validate-feature.js +32 -3
  54. package/src/lib/build/build-runner.js +99 -0
  55. package/src/lib/checkpoints/checkpoint-hooks.js +110 -0
  56. package/src/lib/generators/recap-generator.js +27 -4
  57. package/src/lib/stack/stack-profile.js +3 -0
  58. package/src/lib/tasks/task-parser.js +23 -9
  59. package/src/scripts/global-install.js +57 -3
  60. package/src/utils/file-copier.js +1 -0
  61. package/src/utils/version-checker.js +1 -7
  62. package/framework/hooks/claude-code/pre-tool-use/protect-readonly-files.js +0 -7
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Build Runner — Resolves and executes the project's build command.
3
+ *
4
+ * Resolution order for build command:
5
+ * 1. .morph/config/config.json → project.buildCommand
6
+ * 2. Stack profile → profile.buildCommand
7
+ * 3. package.json → scripts.build → "npm run build"
8
+ * 4. tsconfig.json at root → "npx tsc --noEmit"
9
+ * 5. null (no build command detected)
10
+ */
11
+
12
+ import { execSync } from 'child_process';
13
+ import { existsSync, readFileSync } from 'fs';
14
+ import { join } from 'path';
15
+ import { getStackProfile } from '../stack/stack-profile.js';
16
+
17
+ /**
18
+ * Resolve the build command for a project.
19
+ *
20
+ * @param {string} [projectPath] - Project root directory (defaults to '.')
21
+ * @returns {string | null} The build command to run, or null if none found
22
+ */
23
+ export function resolveBuildCommand(projectPath = '.') {
24
+ // 1. Explicit override from .morph/config/config.json
25
+ const configPath = join(projectPath, '.morph', 'config', 'config.json');
26
+ if (existsSync(configPath)) {
27
+ try {
28
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
29
+ const cmd = config?.project?.buildCommand;
30
+ if (cmd && typeof cmd === 'string') {
31
+ return cmd;
32
+ }
33
+ } catch {
34
+ // Malformed config — fall through to detection
35
+ }
36
+ }
37
+
38
+ // 2. Stack profile default (only when an actual stack is configured)
39
+ const profile = getStackProfile(projectPath);
40
+ if (profile.stack !== 'unknown' && profile.buildCommand) {
41
+ return profile.buildCommand;
42
+ }
43
+
44
+ // 3. package.json with a build script
45
+ const packageJsonPath = join(projectPath, 'package.json');
46
+ if (existsSync(packageJsonPath)) {
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
49
+ if (pkg?.scripts?.build) {
50
+ return 'npm run build';
51
+ }
52
+ } catch {
53
+ // Malformed package.json — fall through
54
+ }
55
+ }
56
+
57
+ // 4. tsconfig.json at root → generic type-check
58
+ if (existsSync(join(projectPath, 'tsconfig.json'))) {
59
+ return 'npx tsc --noEmit';
60
+ }
61
+
62
+ // 5. Nothing found
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Run the project's build check.
68
+ *
69
+ * @param {string} [projectPath] - Project root directory (defaults to '.')
70
+ * @param {object} [opts] - Optional overrides for testing
71
+ * @param {Function} [opts._execSync] - Override execSync for unit testing
72
+ * @returns {{ passed?: boolean, output?: string, exitCode?: number, command?: string, skipped?: boolean, reason?: string }}
73
+ */
74
+ export function runBuildCheck(projectPath = '.', opts = {}) {
75
+ const cmd = resolveBuildCommand(projectPath);
76
+
77
+ if (!cmd) {
78
+ return { skipped: true, reason: 'No buildCommand configured and no known project file detected' };
79
+ }
80
+
81
+ const exec = opts._execSync ?? execSync;
82
+
83
+ try {
84
+ const output = exec(cmd, {
85
+ cwd: projectPath,
86
+ timeout: 120_000,
87
+ encoding: 'utf8',
88
+ stdio: 'pipe'
89
+ });
90
+ return { passed: true, output: output ?? '', exitCode: 0, command: cmd };
91
+ } catch (err) {
92
+ return {
93
+ passed: false,
94
+ output: (err.stdout || '') + (err.stderr || ''),
95
+ exitCode: err.status ?? 1,
96
+ command: cmd
97
+ };
98
+ }
99
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Checkpoint Hooks — Orchestrates validation, build, and test phases at checkpoints.
3
+ *
4
+ * This is the module that task-manager.js imports at:
5
+ * import('../src/lib/checkpoints/checkpoint-hooks.js')
6
+ *
7
+ * All three phases run independently (failures don't short-circuit) so the user
8
+ * gets a complete picture of all issues in a single checkpoint report.
9
+ */
10
+
11
+ import chalk from 'chalk';
12
+ import { runValidation } from '../validators/validation-runner.js';
13
+ import { runBuildCheck } from '../build/build-runner.js';
14
+ import { runTestSuite } from '../tasks/test-runner.js';
15
+ import { persistCheckpointValidation } from '../validators/checkpoint-persistence.js';
16
+
17
+ /**
18
+ * Determine whether a checkpoint should run based on task count and frequency.
19
+ *
20
+ * @param {number} tasksCompleted - Number of tasks completed so far
21
+ * @param {number} frequency - Run every N tasks (default: 3)
22
+ * @returns {boolean}
23
+ */
24
+ export function shouldRunCheckpoint(tasksCompleted, frequency = 3) {
25
+ return tasksCompleted > 0 && tasksCompleted % frequency === 0;
26
+ }
27
+
28
+ /**
29
+ * Run all checkpoint hooks: static validation, build check, and test suite.
30
+ *
31
+ * @param {string} featureName - The feature being worked on
32
+ * @param {number} checkpointNum - Checkpoint sequence number
33
+ * @param {object} [opts] - Options
34
+ * @param {string} [opts.projectPath] - Project root (defaults to cwd)
35
+ * @returns {Promise<{ passed: boolean, summary: { errors: number, warnings: number }, results: object }>}
36
+ */
37
+ export async function runCheckpointHooks(featureName, checkpointNum, opts = {}) {
38
+ const projectPath = opts.projectPath ?? process.cwd();
39
+ const results = {};
40
+ let totalErrors = 0;
41
+ let totalWarnings = 0;
42
+
43
+ // Phase 1: Static validators
44
+ console.log(chalk.gray('\n Phase 1/3: Running static validators...'));
45
+ try {
46
+ const valResult = await runValidation(projectPath, featureName, { verbose: false, mode: 'full' });
47
+ results.validators = {
48
+ passed: valResult.passed,
49
+ errors: valResult.errors,
50
+ warnings: valResult.warnings,
51
+ details: valResult.results
52
+ };
53
+ totalErrors += valResult.errors;
54
+ totalWarnings += valResult.warnings;
55
+ } catch (err) {
56
+ results.validators = { passed: false, errors: 1, warnings: 0, details: {}, error: err.message };
57
+ totalErrors += 1;
58
+ }
59
+
60
+ // Phase 2: Build check
61
+ console.log(chalk.gray(' Phase 2/3: Running build check...'));
62
+ const buildResult = runBuildCheck(projectPath);
63
+ results.build = buildResult;
64
+ if (buildResult.skipped) {
65
+ console.log(chalk.gray(` Skipped: ${buildResult.reason}`));
66
+ } else if (buildResult.passed) {
67
+ console.log(chalk.green(` ✓ Build passed (${buildResult.command})`));
68
+ } else {
69
+ console.log(chalk.red(` ✗ Build failed (${buildResult.command})`));
70
+ totalErrors += 1;
71
+ }
72
+
73
+ // Phase 3: Test suite
74
+ console.log(chalk.gray(' Phase 3/3: Running test suite...'));
75
+ const testResult = runTestSuite(projectPath);
76
+ results.tests = testResult;
77
+ if (testResult.skipped) {
78
+ console.log(chalk.gray(` Skipped: ${testResult.reason}`));
79
+ } else if (testResult.passed) {
80
+ console.log(chalk.green(' ✓ Tests passed'));
81
+ } else {
82
+ console.log(chalk.red(' ✗ Tests failed'));
83
+ totalErrors += 1;
84
+ }
85
+
86
+ // Persist results
87
+ const summaryText = [
88
+ `Validators: ${results.validators?.errors || 0} errors, ${results.validators?.warnings || 0} warnings`,
89
+ buildResult.skipped ? 'Build: skipped' : `Build: ${buildResult.passed ? 'passed' : 'FAILED'}`,
90
+ testResult.skipped ? 'Tests: skipped' : `Tests: ${testResult.passed ? 'passed' : 'FAILED'}`
91
+ ].join(' | ');
92
+
93
+ try {
94
+ persistCheckpointValidation(projectPath, featureName, {
95
+ checkpointNum,
96
+ timestamp: new Date().toISOString(),
97
+ passed: totalErrors === 0,
98
+ validators: results,
99
+ summary: summaryText
100
+ });
101
+ } catch {
102
+ // Non-blocking — persistence failure shouldn't break the checkpoint
103
+ }
104
+
105
+ return {
106
+ passed: totalErrors === 0,
107
+ summary: { errors: totalErrors, warnings: totalWarnings },
108
+ results
109
+ };
110
+ }
@@ -233,11 +233,34 @@ function buildRecapMarkdown(data) {
233
233
 
234
234
  // Tasks Detail
235
235
  if (tasksSummary.items.length > 0) {
236
+ const anyNotes = tasksSummary.items.some(t => t.notes?.length > 0);
236
237
  md += `## Tasks\n\n`;
237
- md += `| ID | Title | Status |\n|----|-------|--------|\n`;
238
- for (const task of tasksSummary.items) {
239
- const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
240
- md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} |\n`;
238
+ if (anyNotes) {
239
+ md += `| ID | Title | Status | Notes |\n|----|-------|--------|-------|\n`;
240
+ for (const task of tasksSummary.items) {
241
+ const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
242
+ const noteCount = task.notes?.length || 0;
243
+ const notesCell = noteCount > 0 ? `${noteCount} note${noteCount > 1 ? 's' : ''}` : '-';
244
+ md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} | ${notesCell} |\n`;
245
+ }
246
+ } else {
247
+ md += `| ID | Title | Status |\n|----|-------|--------|\n`;
248
+ for (const task of tasksSummary.items) {
249
+ const statusIcon = task.status === 'completed' ? '✅' : task.status === 'in_progress' ? '🔄' : '⏳';
250
+ md += `| ${task.id} | ${task.title || '-'} | ${statusIcon} ${task.status} |\n`;
251
+ }
252
+ }
253
+ md += `\n`;
254
+ }
255
+
256
+ // Deviations from Plan
257
+ const deviations = tasksSummary.items
258
+ .filter(t => t.notes?.some(n => n.type === 'deviation'))
259
+ .flatMap(t => t.notes.filter(n => n.type === 'deviation').map(n => ({ taskId: t.id, text: n.text })));
260
+ if (deviations.length > 0) {
261
+ md += `## Deviations from Plan\n\n`;
262
+ for (const d of deviations) {
263
+ md += `- **${d.taskId}**: ${d.text}\n`;
241
264
  }
242
265
  md += `\n`;
243
266
  }
@@ -24,6 +24,7 @@ const PROFILES = {
24
24
  isDotnet: true, isNextjs: false,
25
25
  defaultValidators: ['architecture', 'packages'],
26
26
  checkpointValidators: ['architecture', 'packages', 'design-system', 'security'],
27
+ buildCommand: 'dotnet build --no-restore',
27
28
  contractsOutputType: 'contracts',
28
29
  contractsFilename: 'contracts.cs',
29
30
  contractsLanguage: 'csharp',
@@ -32,6 +33,7 @@ const PROFILES = {
32
33
  isDotnet: false, isNextjs: true,
33
34
  defaultValidators: ['nextjs-component'],
34
35
  checkpointValidators: ['nextjs-component', 'css'],
36
+ buildCommand: 'npx tsc --noEmit',
35
37
  contractsOutputType: 'contractsTs',
36
38
  contractsFilename: 'contracts.ts',
37
39
  contractsLanguage: 'typescript',
@@ -40,6 +42,7 @@ const PROFILES = {
40
42
  isDotnet: false, isNextjs: false,
41
43
  defaultValidators: [],
42
44
  checkpointValidators: [],
45
+ buildCommand: null,
43
46
  contractsOutputType: 'contracts',
44
47
  contractsFilename: 'contracts.cs',
45
48
  contractsLanguage: 'csharp',
@@ -122,18 +122,32 @@ export async function parseTasksMd(featureName, cwd = process.cwd()) {
122
122
  return [];
123
123
  }
124
124
 
125
- const tasks = [];
125
+ // Phase 1: collect heading positions
126
126
  const headingRe = /^###\s+(T\d+)\s*[—–:\-]\s*(.+)$/gm;
127
+ const depRe = /\*\*Dependenc(?:y|ies):\*\*\s*(.+)/i;
128
+ const positions = [];
127
129
  let match;
128
130
  while ((match = headingRe.exec(content)) !== null) {
129
- tasks.push({
130
- id: match[1],
131
- title: match[2].trim(),
132
- status: 'pending',
133
- dependencies: [],
134
- files: [],
135
- checkpoint: null,
136
- });
131
+ positions.push({ id: match[1], title: match[2].trim(), pos: match.index });
132
+ }
133
+
134
+ // Phase 2: extract body and parse dependencies
135
+ const tasks = [];
136
+ for (let i = 0; i < positions.length; i++) {
137
+ const { id, title, pos } = positions[i];
138
+ const end = i + 1 < positions.length ? positions[i + 1].pos : content.length;
139
+ const block = content.slice(pos, end);
140
+
141
+ let dependencies = [];
142
+ const depMatch = depRe.exec(block);
143
+ if (depMatch) {
144
+ const val = depMatch[1].trim();
145
+ if (val.toLowerCase() !== 'none') {
146
+ dependencies = val.split(/[,\s]+/).filter(s => /^T\d+$/.test(s));
147
+ }
148
+ }
149
+
150
+ tasks.push({ id, title, status: 'pending', dependencies, files: [], checkpoint: null });
137
151
  }
138
152
  return tasks;
139
153
  }
@@ -1,9 +1,10 @@
1
1
  // src/scripts/global-install.js
2
2
  import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
- import { homedir } from 'os';
5
- import { mkdir, copyFile } from 'fs/promises';
4
+ import { homedir, platform } from 'os';
5
+ import { mkdir, copyFile, readFile, writeFile } from 'fs/promises';
6
6
  import { existsSync } from 'fs';
7
+ import { execSync } from 'child_process';
7
8
  import { installGlobalStatusline } from '../utils/hooks-installer.js';
8
9
 
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -27,15 +28,62 @@ export async function installGlobalSkill(claudeDir = join(homedir(), '.claude'))
27
28
  await copyFile(src, dest);
28
29
  }
29
30
 
31
+ /**
32
+ * Ensure the npm global bin directory is in the user's PATH on Windows.
33
+ * Adds it to the PowerShell $PROFILE so new terminals find `morph-spec`.
34
+ */
35
+ export async function ensureNpmGlobalInPath() {
36
+ if (platform() !== 'win32') return;
37
+
38
+ let npmGlobalDir;
39
+ try {
40
+ npmGlobalDir = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
41
+ } catch {
42
+ return; // can't determine npm global dir
43
+ }
44
+
45
+ if (!npmGlobalDir || !existsSync(npmGlobalDir)) return;
46
+
47
+ // --- PowerShell $PROFILE ---
48
+ const psProfile = join(homedir(), 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
49
+ const psProfileLegacy = join(homedir(), 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1');
50
+
51
+ const marker = '# morph-spec: ensure npm global in PATH';
52
+ const snippet = [
53
+ marker,
54
+ `if ($env:PATH -notlike "*${npmGlobalDir.replace(/\\/g, '\\\\')}*") {`,
55
+ ` $env:PATH += ";${npmGlobalDir}"`,
56
+ '}',
57
+ '# end morph-spec PATH',
58
+ ].join('\n');
59
+
60
+ for (const profilePath of [psProfile, psProfileLegacy]) {
61
+ try {
62
+ let content = '';
63
+ if (existsSync(profilePath)) {
64
+ content = await readFile(profilePath, 'utf8');
65
+ if (content.includes(marker)) continue; // already patched
66
+ } else {
67
+ await mkdir(dirname(profilePath), { recursive: true });
68
+ }
69
+ const newContent = content ? `${content}\n\n${snippet}\n` : `${snippet}\n`;
70
+ await writeFile(profilePath, newContent, 'utf8');
71
+ } catch {
72
+ // non-critical — skip silently
73
+ }
74
+ }
75
+ }
76
+
30
77
  // Run as postinstall script
31
78
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
32
79
  const HOOKS_SRC = join(FRAMEWORK_DIR, 'hooks', 'claude-code');
33
80
  const { readFileSync } = await import('fs');
34
81
  const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
35
82
 
36
- const [skillResult, statuslineResult] = await Promise.allSettled([
83
+ const [skillResult, statuslineResult, pathResult] = await Promise.allSettled([
37
84
  installGlobalSkill(),
38
85
  installGlobalStatusline(HOOKS_SRC),
86
+ ensureNpmGlobalInPath(),
39
87
  ]);
40
88
 
41
89
  const results = [
@@ -51,6 +99,12 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) {
51
99
  path: '~/.claude/',
52
100
  error: statuslineResult.reason?.message,
53
101
  },
102
+ {
103
+ ok: pathResult.status === 'fulfilled',
104
+ label: 'PATH (npm global)',
105
+ path: '$PROFILE',
106
+ error: pathResult.reason?.message,
107
+ },
54
108
  ];
55
109
 
56
110
  const { printInstallBanner } = await import('../utils/banner.js');
@@ -149,6 +149,7 @@ export async function updateGitignore(projectPath) {
149
149
  '.claude/CLAUDE.md',
150
150
  '.claude/settings.local.json',
151
151
  '.worktrees/',
152
+ 'docs/superpowers/',
152
153
  ''
153
154
  ];
154
155
 
@@ -125,16 +125,10 @@ export function getUpdateInstructions(method) {
125
125
  'morph-spec update'
126
126
  ];
127
127
  case 'npx':
128
- return [
129
- 'npx @polymorphism-tech/morph-spec@latest update'
130
- ];
131
128
  default:
132
129
  return [
133
130
  'npm install -g @polymorphism-tech/morph-spec@latest',
134
- 'morph-spec update',
135
- '',
136
- 'Or use npx:',
137
- 'npx @polymorphism-tech/morph-spec@latest update'
131
+ 'morph-spec update'
138
132
  ];
139
133
  }
140
134
  }
@@ -1,7 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * DEPRECATED — This hook has been replaced by native permissions.deny in settings.local.json.
4
- * The file is kept as an empty stub. It is NOT registered in hooks-installer.js.
5
- * Safe to delete.
6
- */
7
- process.exit(0);