@polymorphism-tech/morph-spec 4.10.4 → 4.10.6

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 (61) hide show
  1. package/README.md +7 -7
  2. package/bin/morph-spec.js +4 -2
  3. package/bin/task-manager.js +21 -5
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +5 -5
  6. package/docs/QUICKSTART.md +5 -5
  7. package/framework/CLAUDE.md +4 -4
  8. package/framework/agents/backend/hangfire-orchestrator.md +1 -1
  9. package/framework/agents/backend/ms-agent-expert.md +2 -1
  10. package/framework/agents/infrastructure/azure-deploy-specialist.md +1 -1
  11. package/framework/agents/integrations/asaas-financial.md +1 -1
  12. package/framework/agents/integrations/clerk-auth.md +1 -2
  13. package/framework/agents/integrations/hangfire-integration.md +1 -1
  14. package/framework/agents/integrations/resend-email.md +1 -1
  15. package/framework/agents/quality/code-analyzer.md +1 -1
  16. package/framework/commands/morph-apply.md +4 -4
  17. package/framework/commands/morph-infra.md +10 -10
  18. package/framework/commands/morph-preflight.md +3 -3
  19. package/framework/commands/morph-proposal.md +2 -2
  20. package/framework/commands/morph-status.md +3 -3
  21. package/framework/hooks/claude-code/post-tool-use/context-refresh.js +1 -1
  22. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +1 -1
  23. package/framework/phases.json +17 -17
  24. package/framework/rules/morph-workflow.md +3 -3
  25. package/framework/skills/README.md +1 -1
  26. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +1 -1
  27. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
  28. package/framework/skills/level-0-meta/morph-code-review/SKILL.md +1 -1
  29. package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +1 -1
  30. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +3 -3
  31. package/framework/skills/level-0-meta/morph-init/SKILL.md +8 -8
  32. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +7 -7
  33. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +4 -4
  34. package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +1 -1
  35. package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +1 -1
  36. package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +1 -1
  37. package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +1 -1
  38. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +5 -5
  39. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +2 -2
  40. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +5 -5
  41. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +6 -7
  42. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +6 -7
  43. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +6 -6
  44. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +6 -7
  45. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +5 -5
  46. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +3 -3
  47. package/framework/standards/ai-agents/team-orchestration.md +1 -1
  48. package/framework/workflows/docs/STORY-DRIVEN-DEVELOPMENT.md +1 -1
  49. package/package.json +1 -1
  50. package/src/commands/project/doctor.js +3 -3
  51. package/src/commands/project/init.js +11 -3
  52. package/src/commands/project/update.js +11 -8
  53. package/src/commands/state/advance-phase.js +24 -11
  54. package/src/commands/state/approve.js +1 -1
  55. package/src/commands/state/state.js +26 -5
  56. package/src/lib/phase-chain/phase-validator.js +6 -1
  57. package/src/lib/tasks/task-parser.js +4 -0
  58. package/src/scripts/setup-infra.js +2 -2
  59. package/src/utils/agents-installer.js +20 -11
  60. package/src/utils/banner.js +1 -1
  61. package/src/utils/hooks-installer.js +739 -739
@@ -29,6 +29,7 @@ import { installClaudeHooks, installGlobalStatusline, installVSCodeTerminalSetti
29
29
  import { installSkills } from '../../utils/skills-installer.js';
30
30
  import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
31
31
  import { injectMorphImport } from '../../utils/claude-md-injector.js';
32
+ import { getProjectTags } from '../../lib/stack-filter.js';
32
33
 
33
34
  /**
34
35
  * Backup user's config.json before cleaning
@@ -262,19 +263,21 @@ export async function updateCommand(options) {
262
263
  logger.warn(' ⚠ framework/commands/ source missing — commands not updated');
263
264
  }
264
265
 
266
+ // Compute project tags for stack-aware installation of skills, agents, and rules
267
+ let projectTags = [];
268
+ try {
269
+ const cfg = await readJson(join(morphPath, 'config', 'config.json'));
270
+ projectTags = getProjectTags(cfg);
271
+ } catch { /* ignore — config may not exist yet */ }
272
+
265
273
  // Sync morph skills to .claude/skills/ for native Claude Code discovery
266
274
  updateSpinner.text = 'Syncing morph skills to .claude/skills/...';
267
- await installSkills(targetPath);
275
+ await installSkills(targetPath, { projectTags });
268
276
 
269
277
  // Sync native subagents to .claude/agents/
270
278
  updateSpinner.text = 'Syncing agents to .claude/agents/...';
271
- let projectStack = null;
272
- try {
273
- const cfg = await readJson(join(morphPath, 'config', 'config.json'));
274
- projectStack = cfg?.project?.stack ?? null;
275
- } catch { /* ignore — config may not exist yet */ }
276
- await installAgents(targetPath, frameworkDir, { projectStack });
277
- await installDomainAgents(targetPath, frameworkDir);
279
+ await installAgents(targetPath, frameworkDir, { projectTags });
280
+ await installDomainAgents(targetPath, frameworkDir, { projectTags });
278
281
 
279
282
  // Sync path-scoped rules to .claude/rules/
280
283
  updateSpinner.text = 'Syncing rules to .claude/rules/...';
@@ -20,7 +20,7 @@ import { validateTransition } from '../../core/state/phase-state-machine.js';
20
20
  import { validateSpecContent, validateTasksContent } from '../../lib/validators/content/content-validator.js';
21
21
  import { getWorkflowConfig } from '../../core/workflows/workflow-detector.js';
22
22
  import { getStackProfile } from '../../lib/stack/stack-profile.js';
23
- import { readFileSync, existsSync } from 'fs';
23
+ import { readFileSync, existsSync, mkdirSync } from 'fs';
24
24
  import { join, dirname } from 'path';
25
25
  import { fileURLToPath } from 'url';
26
26
  import { emitValidatorDispatch } from '../../lib/validators/shared/emit-validator-dispatch.js';
@@ -339,12 +339,12 @@ export async function advancePhaseCommand(feature, options = {}) {
339
339
 
340
340
  // === GATE 6: Clarifications Required ===
341
341
  // When advancing FROM clarify, clarifications.md must exist.
342
- // This enforces that morph:phase-clarify was actually run (not just skipped).
342
+ // This enforces that morph-phase-clarify was actually run (not just skipped).
343
343
  if (currentPhase === 'clarify') {
344
344
  const clarPath = join(process.cwd(), '.morph', 'features', feature, '1-design', 'clarifications.md');
345
345
  if (!existsSync(clarPath)) {
346
346
  console.log(chalk.red('\n✗ Cannot advance from clarify — clarifications.md is missing'));
347
- console.log(chalk.yellow(' Run the phase-clarify skill first: /morph:phase-clarify'));
347
+ console.log(chalk.yellow(' Run the phase-clarify skill first: /morph-phase-clarify'));
348
348
  console.log(chalk.gray(' Output expected at: .morph/features/' + feature + '/1-design/clarifications.md\n'));
349
349
  process.exit(1);
350
350
  }
@@ -373,6 +373,19 @@ export async function advancePhaseCommand(feature, options = {}) {
373
373
  const state = loadState();
374
374
  state.features[feature].phase = nextPhase;
375
375
  state.features[feature].updatedAt = new Date().toISOString();
376
+
377
+ // Create the target phase directory so derivePhase() reflects the new phase
378
+ const PHASE_DIR_MAP = {
379
+ 'proposal': '0-proposal', 'design': '1-design', 'uiux': '2-ui',
380
+ 'plan': '3-plan', 'tasks': '4-tasks', 'implement': '5-implement',
381
+ };
382
+ const targetDir = PHASE_DIR_MAP[nextPhase];
383
+ if (targetDir) {
384
+ const dirPath = join(featureFolderPath, targetDir);
385
+ if (!existsSync(dirPath)) {
386
+ mkdirSync(dirPath, { recursive: true });
387
+ }
388
+ }
376
389
  if (skippedPhases.length > 0) {
377
390
  const existingSkipped = state.features[feature].skippedPhases || [];
378
391
  state.features[feature].skippedPhases = [...new Set([...existingSkipped, ...skippedPhases])];
@@ -468,38 +481,38 @@ export async function advancePhaseCommand(feature, options = {}) {
468
481
  function getPhaseGuidance(phase, feature) {
469
482
  const guides = {
470
483
  'setup': [
471
- `Resume spec pipeline: /morph:proposal ${feature}`,
484
+ `Resume spec pipeline: /morph-proposal ${feature}`,
472
485
  'Auto-continues from setup phase'
473
486
  ],
474
487
  'uiux': [
475
- `Resume spec pipeline: /morph:proposal ${feature}`,
488
+ `Resume spec pipeline: /morph-proposal ${feature}`,
476
489
  'Provide layout references and preferences',
477
490
  'Review wireframes before proceeding'
478
491
  ],
479
492
  'design': (() => {
480
493
  const { isDotnet } = getStackProfile();
481
494
  return [
482
- `Resume spec pipeline: /morph:proposal ${feature}`,
495
+ `Resume spec pipeline: /morph-proposal ${feature}`,
483
496
  'Review DECISION POINTS carefully',
484
497
  isDotnet ? 'Approve spec.md and contracts.cs' : 'Approve spec.md and contracts.ts (TypeScript + Zod schemas)'
485
498
  ];
486
499
  })(),
487
500
  'clarify': [
488
- `Resume spec pipeline: /morph:proposal ${feature}`,
501
+ `Resume spec pipeline: /morph-proposal ${feature}`,
489
502
  'Resolve edge cases and unknowns'
490
503
  ],
491
504
  'plan': [
492
- `Resume spec pipeline: /morph:proposal ${feature}`,
505
+ `Resume spec pipeline: /morph-proposal ${feature}`,
493
506
  'Review implementation plan for completeness',
494
507
  'Approve plan before generating task breakdown'
495
508
  ],
496
509
  'tasks': [
497
- `Resume spec pipeline: /morph:proposal ${feature}`,
510
+ `Resume spec pipeline: /morph-proposal ${feature}`,
498
511
  'Review task order and dependencies',
499
512
  'Approve task list before implementing'
500
513
  ],
501
514
  'implement': [
502
- `Start implementing: /morph:apply ${feature}`,
515
+ `Start implementing: /morph-apply ${feature}`,
503
516
  'Complete tasks one by one with: morph-spec task done',
504
517
  'Validators run automatically on task completion'
505
518
  ],
@@ -543,7 +556,7 @@ function designSystemGate(feature, projectPath = '.') {
543
556
  'Create a design system with one of these options:',
544
557
  ' 1. Project-level: .morph/context/design-system.md (shared across features)',
545
558
  ` 2. Feature-level: ${getOutputPath(feature, 'uiDesignSystem')} (feature-specific)`,
546
- ' 3. Run /morph:phase-uiux skill to auto-generate from existing codebase',
559
+ ' 3. Run /morph-phase-uiux skill to auto-generate from existing codebase',
547
560
  '',
548
561
  'Or remove UI agents if they are not needed:',
549
562
  ` morph-spec state remove-agent ${feature} blazor-builder`
@@ -54,7 +54,7 @@ const program = new Command();
54
54
  /**
55
55
  * Valid approval gate names
56
56
  */
57
- const VALID_GATES = ['proposal', 'uiux', 'design', 'tasks'];
57
+ const VALID_GATES = ['proposal', 'uiux', 'design', 'plan', 'tasks'];
58
58
 
59
59
  /**
60
60
  * Approve action function - Mark a gate as approved
@@ -11,6 +11,24 @@ import { logger } from '../../utils/logger.js';
11
11
  import * as StateManager from '../../core/state/state-manager.js';
12
12
  import { derivePhase } from '../../core/state/state-manager.js';
13
13
 
14
+ // ============================================================================
15
+ // Phase Resolution
16
+ // ============================================================================
17
+
18
+ const PHASE_ORDER_LIST = ['setup', 'proposal', 'uiux', 'design', 'clarify', 'plan', 'tasks', 'implement', 'sync', 'archived'];
19
+
20
+ /**
21
+ * Resolve the effective phase: pick whichever is further ahead between the
22
+ * stored phase (set by `phase advance`) and the derived phase (from filesystem).
23
+ */
24
+ function resolvePhase(storedPhase, derivedPhase) {
25
+ if (!storedPhase) return derivedPhase;
26
+ if (!derivedPhase) return storedPhase;
27
+ const storedIdx = PHASE_ORDER_LIST.indexOf(storedPhase);
28
+ const derivedIdx = PHASE_ORDER_LIST.indexOf(derivedPhase);
29
+ return storedIdx > derivedIdx ? storedPhase : derivedPhase;
30
+ }
31
+
14
32
  // ============================================================================
15
33
  // Phase Protection
16
34
  // ============================================================================
@@ -167,7 +185,9 @@ async function getCommand(featureName, options) {
167
185
  logger.header(`Feature: ${featureName}`);
168
186
  logger.blank();
169
187
  const featurePath = join(process.cwd(), '.morph/features', featureName);
170
- const phase = derivePhase(featurePath);
188
+ const derivedPhase = derivePhase(featurePath);
189
+ // Use stored phase when it's ahead of derived (directory may not exist yet)
190
+ const phase = resolvePhase(feature.phase, derivedPhase);
171
191
  const tasks = feature.tasks || { completed: 0, total: 0 };
172
192
  // Prefer taskList.length as total when tasks.total is stale/zero (Problem 3 complement)
173
193
  const total = tasks.total || (Array.isArray(feature.taskList) ? feature.taskList.length : 0);
@@ -293,13 +313,14 @@ async function listCommand(options) {
293
313
  }[feature.status] || '❓';
294
314
 
295
315
  const tasks = feature.tasks || {};
296
- const progress = (tasks.total || 0) > 0
297
- ? `${tasks.completed || 0}/${tasks.total}`
298
- : '0/0';
316
+ const total = tasks.total || (Array.isArray(feature.taskList) ? feature.taskList.length : 0);
317
+ const progress = total > 0
318
+ ? `${tasks.completed || 0}/${total}`
319
+ : '-';
299
320
 
300
321
  logger.info(`${statusEmoji} ${chalk.bold(name)}`);
301
322
  const featurePath = join(process.cwd(), '.morph/features', name);
302
- const featurePhase = derivePhase(featurePath);
323
+ const featurePhase = resolvePhase(feature.phase, derivePhase(featurePath));
303
324
  logger.dim(` Phase: ${featurePhase.padEnd(16)} │ Tasks: ${progress}`);
304
325
  logger.dim(` Agents: ${(feature.activeAgents || []).slice(0, 5).join(', ') || 'None'}`);
305
326
  logger.blank();
@@ -99,7 +99,12 @@ const OUTPUT_PHASE_MAP = {
99
99
  function checkOutput(featurePath, outputFile) {
100
100
  const phaseDir = OUTPUT_PHASE_MAP[outputFile] || '';
101
101
  const filePath = path.join(featurePath, phaseDir, outputFile);
102
- return fs.existsSync(filePath);
102
+ if (fs.existsSync(filePath)) return true;
103
+ // Fallback: check feature root (some generators place files there instead of phase subdirs)
104
+ if (phaseDir) {
105
+ return fs.existsSync(path.join(featurePath, outputFile));
106
+ }
107
+ return false;
103
108
  }
104
109
 
105
110
  /**
@@ -86,6 +86,10 @@ export async function ensureTaskList(feature, featureName, cwd = process.cwd())
86
86
  */
87
87
  export function syncCounters(feature) {
88
88
  if (Array.isArray(feature.tasks)) return; // v2 format — nothing to sync
89
+ // Safety: ensure feature.tasks is an object (may be undefined after migration or corruption)
90
+ if (!feature.tasks || typeof feature.tasks !== 'object') {
91
+ feature.tasks = { total: 0, completed: 0, inProgress: 0, pending: 0 };
92
+ }
89
93
  const list = feature.taskList || [];
90
94
  feature.tasks.total = list.length;
91
95
  feature.tasks.completed = list.filter(t => t.status === 'completed').length;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Headless infrastructure setup for MORPH-SPEC.
5
5
  * Extracted from init.js (steps 1-10) so it can be called by the
6
- * /morph:init Claude Code skill via `morph-spec setup-infra` without
6
+ * /morph-init Claude Code skill via `morph-spec setup-infra` without
7
7
  * interactive prompts or stack detection.
8
8
  *
9
9
  * Usage:
@@ -141,7 +141,7 @@ export async function setupInfra(targetPath, { _exec = execSync } = {}) {
141
141
  const contextReadmePath = join(contextDir, 'README.md');
142
142
  if (!(await pathExists(contextReadmePath))) {
143
143
  const dirName = targetPath.split(/[\/]/).pop();
144
- const readmeContent = `# ${dirName} — Project Context\n\nRun /morph:init to generate project context.\n`;
144
+ const readmeContent = `# ${dirName} — Project Context\n\nRun /morph-init to generate project context.\n`;
145
145
  await writeFile(contextReadmePath, readmeContent);
146
146
  }
147
147
 
@@ -39,9 +39,9 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
39
39
  mkdirSync(targetDir, { recursive: true });
40
40
 
41
41
  const eligible = agents.filter(a => {
42
- if (a.tier !== 1 && a.tier !== 2 && a.tier !== 4) return false;
43
- // Tier 4 validators must have a teammate with spawn_prompt
44
- if (a.tier === 4 && !a.teammate?.spawn_prompt) return false;
42
+ if (a.tier < 1 || a.tier > 4) return false;
43
+ // Must have a teammate with spawn_prompt (except tier-1 orchestrators which always have one)
44
+ if (a.tier >= 3 && !a.teammate?.spawn_prompt) return false;
45
45
  // Filter by stacks if the agent declares them
46
46
  const stacks = a.stacks ?? [];
47
47
  if (!shouldInstall(stacks, projectTags)) return false;
@@ -50,13 +50,14 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
50
50
 
51
51
  let tier1 = 0;
52
52
  let tier2 = 0;
53
+ let tier3 = 0;
53
54
  let tier4 = 0;
54
55
  const installed = new Set();
55
56
 
56
57
  // Build the full set of possible filenames for orphan cleanup
57
58
  const allPossible = new Set();
58
59
  for (const a of agents) {
59
- if (a.tier !== 1 && a.tier !== 2 && a.tier !== 4) continue;
60
+ if (a.tier < 1 || a.tier > 4) continue;
60
61
  const slug = a.id ?? a.name?.toLowerCase().replace(/\s+/g, '-');
61
62
  const prefix = a.tier === 4 ? 'morph-validator-' : 'morph-';
62
63
  allPossible.add(`${prefix}${slug}.md`);
@@ -78,6 +79,7 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
78
79
 
79
80
  if (agent.tier === 1) tier1++;
80
81
  else if (agent.tier === 2) tier2++;
82
+ else if (agent.tier === 3) tier3++;
81
83
  else if (agent.tier === 4) tier4++;
82
84
  }
83
85
 
@@ -88,7 +90,7 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
88
90
  }
89
91
  }
90
92
 
91
- return { tier1, tier2, tier4 };
93
+ return { tier1, tier2, tier3, tier4 };
92
94
  }
93
95
 
94
96
  /**
@@ -100,16 +102,23 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
100
102
  const TIER_DEFAULTS = {
101
103
  1: {
102
104
  model: 'inherit',
103
- tools: 'Read, Grep, Glob, Bash, Task',
105
+ tools: 'Read, Grep, Glob, Bash, Task, Write, Edit',
104
106
  maxTurns: 30,
105
- skills: ['morph:checklist'],
107
+ skills: ['morph-checklist'],
106
108
  memory: 'project',
107
109
  },
108
110
  2: {
109
111
  model: 'inherit',
110
- tools: 'Read, Grep, Glob, Bash',
112
+ tools: 'Read, Write, Edit, Bash, Glob, Grep',
111
113
  maxTurns: 20,
112
- skills: ['morph:checklist'],
114
+ skills: ['morph-checklist'],
115
+ memory: 'local',
116
+ },
117
+ 3: {
118
+ model: 'sonnet',
119
+ tools: 'Read, Write, Edit, Bash, Glob, Grep',
120
+ maxTurns: 15,
121
+ skills: [],
113
122
  memory: 'local',
114
123
  },
115
124
  4: {
@@ -309,8 +318,8 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
309
318
  .map(([id, data]) => ({ id, ...data }));
310
319
 
311
320
  for (const a of agents) {
312
- if (a.tier !== 1 && a.tier !== 2 && a.tier !== 4) continue;
313
- if (a.tier === 4 && !a.teammate?.spawn_prompt) continue;
321
+ if (a.tier < 1 || a.tier > 4) continue;
322
+ if (a.tier >= 3 && !a.teammate?.spawn_prompt) continue;
314
323
  const stacks = a.stacks ?? [];
315
324
  if (!shouldInstall(stacks, projectTags)) continue;
316
325
 
@@ -45,7 +45,7 @@ export function printInstallBanner({ version, nodeVersion, results = [] }) {
45
45
  '',
46
46
  sep('Next'),
47
47
  ` ${chalk.cyan('1.')} Open your project in Claude Code`,
48
- ` ${chalk.cyan('2.')} Run ${chalk.bold.white('/morph:init')}`,
48
+ ` ${chalk.cyan('2.')} Run ${chalk.bold.white('/morph-init')}`,
49
49
  '',
50
50
  ].join('\n'));
51
51
  }