@polymorphism-tech/morph-spec 4.11.0 → 4.12.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 (47) hide show
  1. package/CLAUDE.md +106 -2
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +31 -3
  4. package/bin/task-manager.js +391 -35
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/agents.json +16 -6
  9. package/framework/commands/morph-apply.md +21 -1
  10. package/framework/commands/morph-proposal.md +44 -1
  11. package/framework/hooks/claude-code/post-tool-use/agent-complete.js +122 -0
  12. package/framework/hooks/claude-code/post-tool-use/dispatch.js +47 -2
  13. package/framework/hooks/claude-code/session-start/inject-morph-context.js +4 -3
  14. package/framework/hooks/shared/dispatch-helpers.js +31 -0
  15. package/framework/hooks/shared/state-reader.js +7 -3
  16. package/framework/rules/morph-task-tracking.md +49 -0
  17. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +6 -10
  18. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +10 -1
  19. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
  20. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +7 -13
  21. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +7 -1
  22. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +9 -12
  23. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +7 -10
  24. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +10 -18
  25. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +256 -14
  26. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
  27. package/framework/standards/STANDARDS.json +31 -0
  28. package/framework/standards/frontend/design-system/aesthetic-direction.md +60 -7
  29. package/framework/standards/frontend/design-system/design-trends.md +171 -0
  30. package/framework/templates/context/CONTEXT.md +2 -2
  31. package/framework/templates/context/design-system.md +346 -211
  32. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  33. package/package.json +1 -1
  34. package/src/commands/agents/dispatch-agents.js +25 -1
  35. package/src/commands/mcp/mcp-setup.js +28 -6
  36. package/src/commands/project/init.js +20 -1
  37. package/src/commands/tasks/task.js +54 -0
  38. package/src/lib/build/build-runner.js +99 -0
  39. package/src/lib/checkpoints/checkpoint-hooks.js +110 -0
  40. package/src/lib/generators/design-system-generator.js +340 -51
  41. package/src/lib/generators/recap-generator.js +27 -4
  42. package/src/lib/stack/stack-profile.js +3 -0
  43. package/src/lib/tasks/task-parser.js +23 -9
  44. package/src/lib/validators/design-system/design-system-validator.js +11 -5
  45. package/src/utils/file-copier.js +1 -0
  46. package/src/utils/hooks-installer.js +11 -1
  47. package/framework/hooks/claude-code/pre-tool-use/protect-readonly-files.js +0 -7
@@ -211,12 +211,17 @@ async function setupSpecificMcp(targetPath, name, stack, existingMcps) {
211
211
  }]);
212
212
 
213
213
  if (installNow) {
214
- const spinner = ora(`Configuring ${name}...`).start();
215
- const result = await installAutoMcps(targetPath, { [name]: mcpEntry });
216
- if (result.added.length > 0) {
217
- spinner.succeed(`${name} MCP configured (authenticate via /mcp in Claude Code)`);
218
- } else {
219
- spinner.info(`${name} MCP already configured`);
214
+ console.log(chalk.cyan(` Launching ${name} OAuth authentication...`));
215
+ try {
216
+ const { execSync } = await import('child_process');
217
+ execSync(`claude mcp add --transport http ${name} ${mcpEntry.install.config.url}`, {
218
+ stdio: 'inherit',
219
+ timeout: 120000
220
+ });
221
+ console.log(chalk.green(` ✓ ${name} MCP configured via OAuth`));
222
+ } catch {
223
+ const instructions = generateSetupInstructions(name, mcpEntry);
224
+ console.log(chalk.yellow(` Setup cancelled. Run later: ${instructions.cliCommand}`));
220
225
  }
221
226
  }
222
227
 
@@ -323,6 +328,23 @@ async function interactiveWizard(targetPath, stack, existingMcps) {
323
328
  }]);
324
329
 
325
330
  if (setupNow) {
331
+ // Remote MCPs — launch OAuth via claude mcp add
332
+ if (isRemoteMcp(entry.install.config)) {
333
+ console.log(chalk.cyan(` Launching ${capitalizedName} OAuth...`));
334
+ try {
335
+ const { execSync } = await import('child_process');
336
+ execSync(`claude mcp add --transport http ${name} ${entry.install.config.url}`, {
337
+ stdio: 'inherit',
338
+ timeout: 120000
339
+ });
340
+ console.log(chalk.green(` ✓ ${capitalizedName} configured via OAuth`));
341
+ } catch {
342
+ const instructions = generateSetupInstructions(name, entry);
343
+ console.log(chalk.yellow(` Setup cancelled. Run later: ${instructions.cliCommand}`));
344
+ }
345
+ continue;
346
+ }
347
+
326
348
  const credentialValues = {};
327
349
  for (const cred of entry.install.credentials) {
328
350
  const { value } = await inquirer.prompt([{
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'url';
3
3
  import { homedir } from 'os';
4
4
  import fs from 'fs-extra';
5
5
  import ora from 'ora';
6
+ import { execSync } from 'child_process';
6
7
  import { logger } from '../../utils/logger.js';
7
8
  import {
8
9
  pathExists,
@@ -21,7 +22,8 @@ import {
21
22
  installAutoMcps,
22
23
  installMcpWithCredentials,
23
24
  generateSetupInstructions,
24
- formatMcpStatusTable
25
+ formatMcpStatusTable,
26
+ isRemoteMcp
25
27
  } from '../../lib/installers/mcp-installer.js';
26
28
  import { setupInfra, installRulesForStack } from '../../scripts/setup-infra.js';
27
29
  import { installSkills } from '../../utils/skills-installer.js';
@@ -277,6 +279,23 @@ export async function initCommand(options) {
277
279
  }]);
278
280
 
279
281
  if (setupNow) {
282
+ // Remote MCPs — launch OAuth via claude mcp add
283
+ if (isRemoteMcp(entry.install.config)) {
284
+ const url = entry.install.config.url;
285
+ logger.info(` Launching ${capitalizedName} OAuth authentication...`);
286
+ try {
287
+ execSync(`claude mcp add --transport http ${name} ${url}`, {
288
+ stdio: 'inherit',
289
+ timeout: 120000
290
+ });
291
+ logger.success(` ${capitalizedName} MCP configured`);
292
+ manualInstalled[name] = entry;
293
+ } catch {
294
+ logger.warn(` ${capitalizedName} setup cancelled. Run later: claude mcp add --transport http ${name} ${url}`);
295
+ }
296
+ continue;
297
+ }
298
+
280
299
  // Collect credentials
281
300
  const credentialValues = {};
282
301
  for (const cred of entry.install.credentials) {
@@ -42,6 +42,8 @@ export async function taskDoneCommand(featureName, taskIds, options) {
42
42
  try {
43
43
  const args = ['done', featureName, ...taskIds];
44
44
  if (options.skipValidation) args.push('--skip-validation');
45
+ if (options.reason) args.push('--reason', options.reason);
46
+ if (options.note) args.push('--note', options.note);
45
47
  if (options.dryRun) args.push('--dry-run');
46
48
  await executeTaskManager(args);
47
49
  } catch (error) {
@@ -64,6 +66,58 @@ export async function taskStartCommand(featureName, taskId, options) {
64
66
  }
65
67
  }
66
68
 
69
+ /**
70
+ * Bulk-start tasks command
71
+ */
72
+ export async function taskBulkStartCommand(featureName, range, options) {
73
+ try {
74
+ const args = ['bulk-start', featureName];
75
+ if (range) args.push(range);
76
+ if (options.all) args.push('--all');
77
+ if (options.from) args.push('--from', options.from);
78
+ if (options.to) args.push('--to', options.to);
79
+ await executeTaskManager(args);
80
+ } catch (error) {
81
+ console.error(chalk.red(`Error: ${error.message}`));
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Bulk-done tasks command
88
+ */
89
+ export async function taskBulkDoneCommand(featureName, range, options) {
90
+ try {
91
+ const args = ['bulk-done', featureName];
92
+ if (range) args.push(range);
93
+ if (options.all) args.push('--all');
94
+ if (options.from) args.push('--from', options.from);
95
+ if (options.to) args.push('--to', options.to);
96
+ if (options.skipValidation) args.push('--skip-validation');
97
+ if (options.reason) args.push('--reason', options.reason);
98
+ if (options.note) args.push('--note', options.note);
99
+ if (options.dryRun) args.push('--dry-run');
100
+ await executeTaskManager(args);
101
+ } catch (error) {
102
+ console.error(chalk.red(`Error: ${error.message}`));
103
+ process.exit(1);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Annotate a task with a note
109
+ */
110
+ export async function taskAnnotateCommand(featureName, taskId, text, options) {
111
+ try {
112
+ const args = ['annotate', featureName, taskId, text];
113
+ if (options.type) args.push('--type', options.type);
114
+ await executeTaskManager(args);
115
+ } catch (error) {
116
+ console.error(chalk.red(`Error: ${error.message}`));
117
+ process.exit(1);
118
+ }
119
+ }
120
+
67
121
  /**
68
122
  * Get next task command
69
123
  */
@@ -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
+ }