@polymorphism-tech/morph-spec 4.3.0 → 4.3.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.
@@ -53,7 +53,7 @@ function getNpmGlobalPrefix() {
53
53
  }
54
54
 
55
55
  // lib files
56
- const V3_LIB_FILES = [
56
+ const REQUIRED_LIB_FILES = [
57
57
  'src/lib/analytics/analytics-engine.js',
58
58
  'src/lib/tracking/artifact-trail.js',
59
59
  'src/lib/context/context-bundler.js',
@@ -71,22 +71,22 @@ const V3_LIB_FILES = [
71
71
  'src/lib/trust/trust-manager.js'
72
72
  ];
73
73
 
74
- // v3.0 command files (top-level new commands)
75
- const V3_COMMAND_FILES = [
76
- 'src/commands/agents-fuse.js',
77
- 'src/commands/analytics.js',
78
- 'src/commands/context-prime.js',
79
- 'src/commands/core-four.js',
80
- 'src/commands/mcp.js',
81
- 'src/commands/micro-agent.js',
82
- 'src/commands/squad-template.js',
83
- 'src/commands/thread-template.js',
84
- 'src/commands/threads.js',
85
- 'src/commands/trust.js'
74
+ // command files
75
+ const REQUIRED_COMMAND_FILES = [
76
+ 'src/commands/agents/agents-fuse.js',
77
+ 'src/commands/analytics/analytics.js',
78
+ 'src/commands/context/context-prime.js',
79
+ 'src/commands/context/core-four.js',
80
+ 'src/commands/mcp/mcp.js',
81
+ 'src/commands/agents/micro-agent.js',
82
+ 'src/commands/agents/squad-template.js',
83
+ 'src/commands/threads/thread-template.js',
84
+ 'src/commands/threads/threads.js',
85
+ 'src/commands/trust/trust.js'
86
86
  ];
87
87
 
88
- // v3.0 HOP templates (meta-prompts)
89
- const V3_HOP_TEMPLATES = [
88
+ // HOP templates (meta-prompts)
89
+ const HOP_TEMPLATES = [
90
90
  'framework/templates/meta-prompts/squad-leaders/backend-squad.md',
91
91
  'framework/templates/meta-prompts/squad-leaders/frontend-squad.md',
92
92
  'framework/templates/meta-prompts/parallel-workers/parallel-worker.md',
@@ -98,11 +98,11 @@ const V3_HOP_TEMPLATES = [
98
98
  'framework/templates/meta-prompts/validators/pre-commit-validator.md',
99
99
  'framework/templates/meta-prompts/fusion/fusion-agent.md',
100
100
  'framework/templates/meta-prompts/fusion/fusion-aggregator.md',
101
- 'framework/templates/meta-prompts/REGISTRY.json'
101
+ 'framework/templates/REGISTRY.json'
102
102
  ];
103
103
 
104
- // v3.0 new standards (framework-level)
105
- const V3_STANDARDS = [
104
+ // framework standards
105
+ const FRAMEWORK_STANDARDS = [
106
106
  'framework/standards/observability/monitoring.md',
107
107
  'framework/standards/observability/logging.md',
108
108
  'framework/standards/observability/tracing.md',
@@ -128,8 +128,8 @@ const V3_STANDARDS = [
128
128
  'framework/standards/workflows/parallel-execution.md'
129
129
  ];
130
130
 
131
- // v3.0 new agents expected in agents.json
132
- const V3_NEW_AGENTS = [
131
+ // required agents expected in agents.json
132
+ const REQUIRED_AGENTS = [
133
133
  'vector-search-expert',
134
134
  'thread-orchestrator',
135
135
  'context-optimizer',
@@ -137,10 +137,10 @@ const V3_NEW_AGENTS = [
137
137
  ];
138
138
 
139
139
  /**
140
- * Run v3.0-specific health checks
140
+ * Run full health checks
141
141
  */
142
- async function doctorV3Command(frameworkRoot) {
143
- console.log(chalk.bold('\nšŸ”¬ MORPH-SPEC v3.0 Health Check\n'));
142
+ async function doctorFullCommand(frameworkRoot) {
143
+ console.log(chalk.bold('\nšŸ”¬ MORPH-SPEC Full Health Check\n'));
144
144
  console.log('─'.repeat(60));
145
145
 
146
146
  const checks = [];
@@ -157,55 +157,55 @@ async function doctorV3Command(frameworkRoot) {
157
157
  }
158
158
  };
159
159
 
160
- // ── 1. v3.0 Lib Files (15) ──────────────────────────────────────────────
161
- console.log(chalk.cyan('\n src/lib/ — v3.0 Core Libraries (15 files)'));
160
+ // ── 1. Core Libraries ───────────────────────────────────────────────────
161
+ console.log(chalk.cyan(`\n src/lib/ — Core Libraries (${REQUIRED_LIB_FILES.length} files)`));
162
162
  const missingLibs = [];
163
- for (const f of V3_LIB_FILES) {
163
+ for (const f of REQUIRED_LIB_FILES) {
164
164
  if (!(await pathExists(join(frameworkRoot, f)))) missingLibs.push(f.split('/').pop());
165
165
  }
166
- check(`v3.0 lib files (${V3_LIB_FILES.length - missingLibs.length}/${V3_LIB_FILES.length})`,
166
+ check(`Core Libraries (${REQUIRED_LIB_FILES.length - missingLibs.length}/${REQUIRED_LIB_FILES.length})`,
167
167
  missingLibs.length === 0, false,
168
168
  missingLibs.length > 0 ? `missing: ${missingLibs.join(', ')}` : '');
169
169
 
170
- // ── 2. v3.0 Command Files (11) ──────────────────────────────────────────
171
- console.log(chalk.cyan('\n src/commands/ — v3.0 Command Files (11 files)'));
170
+ // ── 2. Command Files ─────────────────────────────────────────────────────
171
+ console.log(chalk.cyan(`\n src/commands/ — Command Files (${REQUIRED_COMMAND_FILES.length} files)`));
172
172
  const missingCmds = [];
173
- for (const f of V3_COMMAND_FILES) {
173
+ for (const f of REQUIRED_COMMAND_FILES) {
174
174
  if (!(await pathExists(join(frameworkRoot, f)))) missingCmds.push(f.split('/').pop());
175
175
  }
176
- check(`v3.0 command files (${V3_COMMAND_FILES.length - missingCmds.length}/${V3_COMMAND_FILES.length})`,
176
+ check(`Command Files (${REQUIRED_COMMAND_FILES.length - missingCmds.length}/${REQUIRED_COMMAND_FILES.length})`,
177
177
  missingCmds.length === 0, false,
178
178
  missingCmds.length > 0 ? `missing: ${missingCmds.join(', ')}` : '');
179
179
 
180
- // ── 3. HOP Templates (12) ───────────────────────────────────────────────
181
- console.log(chalk.cyan('\n framework/templates/meta-prompts/ — HOP Templates (12)'));
180
+ // ── 3. HOP Templates ────────────────────────────────────────────────────
181
+ console.log(chalk.cyan(`\n framework/templates/meta-prompts/ — HOP Templates (${HOP_TEMPLATES.length})`));
182
182
  const missingHOPs = [];
183
- for (const f of V3_HOP_TEMPLATES) {
183
+ for (const f of HOP_TEMPLATES) {
184
184
  if (!(await pathExists(join(frameworkRoot, f)))) missingHOPs.push(f.split('/').pop());
185
185
  }
186
- check(`HOP templates (${V3_HOP_TEMPLATES.length - missingHOPs.length}/${V3_HOP_TEMPLATES.length})`,
186
+ check(`HOP Templates (${HOP_TEMPLATES.length - missingHOPs.length}/${HOP_TEMPLATES.length})`,
187
187
  missingHOPs.length === 0, false,
188
188
  missingHOPs.length > 0 ? `missing: ${missingHOPs.join(', ')}` : '');
189
189
 
190
- // ── 4. New Standards (23) ───────────────────────────────────────────────
191
- console.log(chalk.cyan('\n framework/standards/ — v3.0 Standards (23 files)'));
190
+ // ── 4. Framework Standards ───────────────────────────────────────────────
191
+ console.log(chalk.cyan(`\n framework/standards/ — Framework Standards (${FRAMEWORK_STANDARDS.length} files)`));
192
192
  const missingStds = [];
193
- for (const f of V3_STANDARDS) {
193
+ for (const f of FRAMEWORK_STANDARDS) {
194
194
  if (!(await pathExists(join(frameworkRoot, f)))) missingStds.push(f.replace('framework/standards/', ''));
195
195
  }
196
- check(`v3.0 standards (${V3_STANDARDS.length - missingStds.length}/${V3_STANDARDS.length})`,
196
+ check(`Framework Standards (${FRAMEWORK_STANDARDS.length - missingStds.length}/${FRAMEWORK_STANDARDS.length})`,
197
197
  missingStds.length === 0, false,
198
198
  missingStds.length > 0 ? `missing: ${missingStds.join(', ')}` : '');
199
199
 
200
- // ── 5. New Agents in agents.json ────────────────────────────────────────
201
- console.log(chalk.cyan('\n .morph/config/agents.json — v3.0 Agents (4 new)'));
200
+ // ── 5. Required Agents in agents.json ───────────────────────────────────
201
+ console.log(chalk.cyan(`\n .morph/config/agents.json — Required Agents (${REQUIRED_AGENTS.length})`));
202
202
  const agentsPath = join(frameworkRoot, '.morph/config/agents.json');
203
203
  if (await pathExists(agentsPath)) {
204
204
  try {
205
205
  const agentsConfig = JSON.parse(await fs.readFile(agentsPath, 'utf8'));
206
206
  const agents = agentsConfig.agents || {};
207
- const missingAgents = V3_NEW_AGENTS.filter(id => !agents[id]);
208
- check(`v3.0 agents (${V3_NEW_AGENTS.length - missingAgents.length}/${V3_NEW_AGENTS.length})`,
207
+ const missingAgents = REQUIRED_AGENTS.filter(id => !agents[id]);
208
+ check(`Required Agents (${REQUIRED_AGENTS.length - missingAgents.length}/${REQUIRED_AGENTS.length})`,
209
209
  missingAgents.length === 0, false,
210
210
  missingAgents.length > 0 ? `missing: ${missingAgents.join(', ')}` : '');
211
211
  } catch {
@@ -220,15 +220,15 @@ async function doctorV3Command(frameworkRoot) {
220
220
  const ztPath = join(frameworkRoot, 'framework/workflows/configs/zero-touch.json');
221
221
  check('zero-touch.json workflow config', await pathExists(ztPath));
222
222
 
223
- // ── 8. state.json v3.0 version ──────────────────────────────────────────
223
+ // ── 8. state.json schema version ────────────────────────────────────────
224
224
  console.log(chalk.cyan('\n .morph/state.json — Schema Version'));
225
225
  const statePath = join(frameworkRoot, '.morph/state.json');
226
226
  if (await pathExists(statePath)) {
227
227
  try {
228
228
  const state = JSON.parse(await fs.readFile(statePath, 'utf8'));
229
- const isV3 = state.version === '3.0.0';
230
- check(`state.json schema (v${state.version})`, isV3, true,
231
- isV3 ? '' : 'run: morph-spec migrate v2-to-v3');
229
+ const isCurrent = state.version === '3.0.0';
230
+ check(`state.json schema (${state.version})`, isCurrent, true,
231
+ isCurrent ? '' : 'run: morph-spec state init --force');
232
232
  } catch {
233
233
  check('state.json parse', false, false, 'invalid JSON');
234
234
  }
@@ -254,20 +254,20 @@ async function doctorV3Command(frameworkRoot) {
254
254
  const total = checks.length;
255
255
 
256
256
  if (hasErrors) {
257
- console.log(chalk.red(`\nāŒ ${passed}/${total} checks passed — run "morph-spec migrate v2-to-v3" to fix\n`));
257
+ console.log(chalk.red(`\nāŒ ${passed}/${total} checks passed — see errors above\n`));
258
258
  process.exit(1);
259
259
  } else if (hasWarnings) {
260
260
  console.log(chalk.yellow(`\nāš ļø ${passed}/${total} checks passed (warnings only)\n`));
261
261
  } else {
262
- console.log(chalk.green(`\nāœ… ${passed}/${total} — All v3.0 checks passed!\n`));
262
+ console.log(chalk.green(`\nāœ… ${passed}/${total} — All checks passed!\n`));
263
263
  }
264
264
  }
265
265
 
266
266
  export async function doctorCommand(options = {}) {
267
- // v3-specific check mode
268
- if (options.v3) {
267
+ // full health check mode
268
+ if (options.full) {
269
269
  const frameworkRoot = process.cwd();
270
- return doctorV3Command(frameworkRoot);
270
+ return doctorFullCommand(frameworkRoot);
271
271
  }
272
272
 
273
273
  const targetPath = process.cwd();
@@ -18,6 +18,7 @@ import {
18
18
  createDirectoryLink
19
19
  } from '../../utils/file-copier.js';
20
20
  import { saveProjectMorphVersion, getInstalledCLIVersion } from '../../utils/version-checker.js';
21
+ import { installClaudeHooks } from '../../utils/hooks-installer.js';
21
22
  import { AutoContextOrchestrator } from '../../core/orchestrator.js';
22
23
  import { detectClaudeCode } from '../../llm/environment-detector.js';
23
24
 
@@ -155,8 +156,9 @@ Run \`morph-spec detect\` to analyze your project.
155
156
  }
156
157
 
157
158
  // 9. Copy .claude commands and create symlinks for skills
159
+ // Source: framework root .claude/ (canonical for all stacks)
158
160
  spinner.text = 'Setting up Claude Code integration...';
159
- const claudeSrc = join(contentDir, '.claude');
161
+ const claudeSrc = join(import.meta.dirname, '..', '..', '..', '.claude');
160
162
  const claudeDest = join(targetPath, '.claude');
161
163
 
162
164
  let symlinkCount = 0;
@@ -217,6 +219,10 @@ Run \`morph-spec detect\` to analyze your project.
217
219
  }
218
220
  }
219
221
 
222
+ // 9b. Install/update agent-teams hooks in .claude/settings.local.json
223
+ spinner.text = 'Installing Claude Code hooks...';
224
+ const hooksInstalled = await installClaudeHooks(targetPath);
225
+
220
226
  // 10. Save version info
221
227
  spinner.text = 'Saving version info...';
222
228
  const cliVersion = getInstalledCLIVersion();
@@ -251,6 +257,7 @@ Run \`morph-spec detect\` to analyze your project.
251
257
  logger.dim(` āœ“ .morph/templates/ (Bicep, integrations, saas, ...)`);
252
258
  logger.dim(` āœ“ .morph/project/ (context, standards, outputs)`);
253
259
  logger.dim(` āœ“ .claude/commands/ (slash commands)`);
260
+ logger.dim(` āœ“ .claude/settings.local.json (agent-teams hooks)`);
254
261
 
255
262
  if (symlinkCount > 0) {
256
263
  const linkType = process.platform === 'win32' ? 'junction-linked' : 'symlinked';
@@ -1,4 +1,7 @@
1
- import { join } from 'path';
1
+ import { join, dirname } from 'path';
2
+ import { fileURLToPath } from 'url';
3
+
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
2
5
  import fs from 'fs-extra';
3
6
  import ora from 'ora';
4
7
  import chalk from 'chalk';
@@ -19,6 +22,7 @@ import {
19
22
  getUpdateInstructions,
20
23
  detectInstallMethod
21
24
  } from '../../utils/version-checker.js';
25
+ import { installClaudeHooks } from '../../utils/hooks-installer.js';
22
26
  import { AutoContextOrchestrator } from '../../core/orchestrator.js';
23
27
  import { detectClaudeCode } from '../../llm/environment-detector.js';
24
28
 
@@ -117,8 +121,9 @@ export async function updateCommand(options) {
117
121
  }
118
122
 
119
123
  // Update .claude commands and skills
124
+ // Source: framework root .claude/ (canonical for all stacks)
120
125
  updateSpinner.text = 'Updating Claude commands and skills...';
121
- const claudeSrc = join(contentDir, '.claude');
126
+ const claudeSrc = join(__dirname, '..', '..', '..', '.claude');
122
127
  const claudeDest = join(targetPath, '.claude');
123
128
  if (await pathExists(claudeSrc)) {
124
129
  // Copy commands (always copy, these are small)
@@ -154,6 +159,10 @@ export async function updateCommand(options) {
154
159
  }
155
160
  }
156
161
 
162
+ // Update agent-teams hooks in .claude/settings.local.json
163
+ updateSpinner.text = 'Updating Claude Code hooks...';
164
+ await installClaudeHooks(targetPath);
165
+
157
166
  // Update CLAUDE.md
158
167
  updateSpinner.text = 'Updating CLAUDE.md...';
159
168
  const claudeMdSrc = join(contentDir, 'CLAUDE.md');
@@ -192,6 +201,7 @@ export async function updateCommand(options) {
192
201
  if (updateStandards) logger.dim(' āœ“ .morph/standards/');
193
202
  logger.dim(' āœ“ .morph/config/agents.json');
194
203
  logger.dim(' āœ“ .claude/commands/ and .claude/skills/');
204
+ logger.dim(' āœ“ .claude/settings.local.json (agent-teams hooks)');
195
205
  logger.dim(' āœ“ CLAUDE.md');
196
206
  logger.blank();
197
207
  logger.info('Your config.json was preserved.');
@@ -30,7 +30,7 @@ const PHASE_ORDER = ['proposal', 'setup', 'uiux', 'design', 'clarify', 'tasks',
30
30
  /**
31
31
  * Get the next phase after the current one
32
32
  */
33
- function getNextPhase(currentPhase, feature, skipOptional) {
33
+ function getNextPhase(currentPhase, feature, skipOptional, featureName) {
34
34
  // Load workflow config if workflow is set
35
35
  let workflowConfig = null;
36
36
  if (feature.workflow && feature.workflow !== 'auto') {
@@ -80,7 +80,7 @@ function getNextPhase(currentPhase, feature, skipOptional) {
80
80
  const condition = workflowConfig.phases.skipIfCondition[candidate];
81
81
  if (condition) {
82
82
  // Evaluate condition (simple conditions for now)
83
- if (condition === '!hasUIAgents' && !hasUIAgentsActive(feature)) {
83
+ if (condition === '!hasUIAgents' && !hasUIAgentsActive(process.cwd(), featureName)) {
84
84
  console.log(chalk.gray(` ā© Skipping ${candidate}: no UI agents active`));
85
85
  continue;
86
86
  }
@@ -145,7 +145,7 @@ export async function advancePhaseCommand(feature, options = {}) {
145
145
  console.log(chalk.gray('Workflow:'), featureData.workflow || 'auto');
146
146
 
147
147
  // Determine next phase
148
- const nextPhase = getNextPhase(currentPhase, featureData, options.skipOptional);
148
+ const nextPhase = getNextPhase(currentPhase, featureData, options.skipOptional, feature);
149
149
 
150
150
  if (!nextPhase) {
151
151
  console.log(chalk.green('\nāœ“ Feature is at the final phase!'));
@@ -304,7 +304,7 @@ export async function advancePhaseCommand(feature, options = {}) {
304
304
 
305
305
  // Gate: Check design system when advancing to implement with UI agents
306
306
  if (nextPhase === 'implement') {
307
- const gateResult = designSystemGate(feature);
307
+ const gateResult = designSystemGate(feature, process.cwd());
308
308
 
309
309
  if (gateResult.blocked) {
310
310
  console.log(chalk.red(`\nāœ— ${gateResult.message}`));
@@ -327,6 +327,21 @@ export async function advancePhaseCommand(feature, options = {}) {
327
327
  state.features[feature].updatedAt = new Date().toISOString();
328
328
  saveState(state);
329
329
 
330
+ // Run PhaseAdvanced agent-teams hook (non-blocking)
331
+ try {
332
+ const { executeHook, formatHookResults } = await import('../../lib/hooks/hook-executor.js');
333
+ const hookResult = await executeHook(process.cwd(), feature, 'PhaseAdvanced', {
334
+ fromPhase: currentPhase,
335
+ toPhase: nextPhase
336
+ });
337
+ if (!hookResult.passed && hookResult.errors.length > 0) {
338
+ const output = formatHookResults(hookResult, 'PhaseAdvanced');
339
+ console.log(output);
340
+ }
341
+ } catch {
342
+ // Hook executor unavailable — non-blocking
343
+ }
344
+
330
345
  console.log(chalk.green(`\nāœ“ Advanced to ${nextPhaseDef.name}`));
331
346
 
332
347
  // Show what's needed in the new phase
@@ -181,13 +181,13 @@ async function checkpointCommand(featureName, note, options) {
181
181
  try {
182
182
  const spinner = ora('Creating checkpoint...').start();
183
183
 
184
- const checkpoint = StateManager.addCheckpoint(featureName, note);
184
+ const checkpoint = await StateManager.addCheckpoint(featureName, note);
185
185
 
186
186
  spinner.succeed(`Checkpoint registered for ${chalk.cyan(featureName)}`);
187
187
  logger.blank();
188
- logger.dim(` ${checkpoint.note}`);
188
+ logger.dim(` ${checkpoint.summary?.note || note}`);
189
189
  logger.dim(` Phase: ${checkpoint.phase}`);
190
- logger.dim(` Tasks completed: ${checkpoint.completedTasks}`);
190
+ logger.dim(` Tasks completed: ${checkpoint.summary?.completedTasks ?? 0}`);
191
191
  logger.blank();
192
192
 
193
193
  } catch (error) {
@@ -208,8 +208,10 @@ async function listCommand(options) {
208
208
  const features = StateManager.listFeatures();
209
209
 
210
210
  // Project info
211
- logger.info(`Project: ${chalk.cyan(summary.project.name)}`);
212
- logger.info(`Type: ${chalk.cyan(summary.project.type)}`);
211
+ const projectName = summary.project?.name || '(unnamed)';
212
+ const projectType = summary.project?.type || '(unknown)';
213
+ logger.info(`Project: ${chalk.cyan(projectName)}`);
214
+ logger.info(`Type: ${chalk.cyan(projectType)}`);
213
215
  logger.blank();
214
216
 
215
217
  if (features.length === 0) {
@@ -232,21 +234,21 @@ async function listCommand(options) {
232
234
  archived: 'šŸ“¦'
233
235
  }[feature.status] || 'ā“';
234
236
 
235
- const progress = feature.tasks.total > 0
236
- ? `${feature.tasks.completed}/${feature.tasks.total}`
237
+ const tasks = feature.tasks || {};
238
+ const progress = (tasks.total || 0) > 0
239
+ ? `${tasks.completed || 0}/${tasks.total}`
237
240
  : '0/0';
238
241
 
239
242
  logger.info(`${statusEmoji} ${chalk.bold(name)}`);
240
- logger.dim(` Phase: ${feature.phase.padEnd(16)} │ Tasks: ${progress}`);
241
- logger.dim(` Agents: ${feature.activeAgents.slice(0, 5).join(', ') || 'None'}`);
243
+ logger.dim(` Phase: ${(feature.phase || 'unknown').padEnd(16)} │ Tasks: ${progress}`);
244
+ logger.dim(` Agents: ${(feature.activeAgents || []).slice(0, 5).join(', ') || 'None'}`);
242
245
  logger.blank();
243
246
  });
244
247
 
245
248
  // Summary
246
249
  logger.header('Summary:');
247
- logger.info(`Total Features: ${chalk.cyan(summary.metadata.totalFeatures)}`);
248
- logger.info(`Completed: ${chalk.cyan(summary.metadata.completedFeatures)}`);
249
- logger.info(`Estimated Cost: ${chalk.cyan(`$${summary.metadata.totalCostEstimated.toFixed(2)}/month`)}`);
250
+ logger.info(`Total Features: ${chalk.cyan(summary.metadata?.totalFeatures ?? features.length)}`);
251
+ logger.info(`Completed: ${chalk.cyan(summary.metadata?.completedFeatures ?? 0)}`);
250
252
  logger.blank();
251
253
 
252
254
  } catch (error) {
@@ -9,7 +9,7 @@ import { dirname, join } from 'path';
9
9
  import chalk from 'chalk';
10
10
 
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
- const taskManagerPath = join(__dirname, '..', '..', 'bin', 'task-manager.cjs');
12
+ const taskManagerPath = join(__dirname, '..', '..', '..', 'bin', 'task-manager.cjs');
13
13
 
14
14
  /**
15
15
  * Execute task-manager.js with given arguments
@@ -53,7 +53,7 @@ const THREAD_TYPE_INFO = {
53
53
  };
54
54
 
55
55
  export async function threadTemplateListCommand(options) {
56
- console.log(chalk.cyan('\n Thread Types (v3.0)\n'));
56
+ console.log(chalk.cyan('\n Thread Types\n'));
57
57
  console.log(' ' + '─'.repeat(70));
58
58
 
59
59
  for (const [type, info] of Object.entries(THREAD_TYPE_INFO)) {
@@ -188,7 +188,7 @@ async function ensureFeature(featureName, options = {}) {
188
188
  proposal: { created: false, path: `.morph/project/outputs/${featureName}/proposal.md` },
189
189
  spec: { created: false, path: `.morph/project/outputs/${featureName}/spec.md` },
190
190
  contracts: { created: false, path: `.morph/project/outputs/${featureName}/contracts.cs` },
191
- tasks: { created: false, path: `.morph/project/outputs/${featureName}/tasks.json` },
191
+ tasks: { created: false, path: `.morph/project/outputs/${featureName}/tasks.md` },
192
192
  uiDesignSystem: { created: false, path: `.morph/project/outputs/${featureName}/ui-design-system.md` },
193
193
  uiMockups: { created: false, path: `.morph/project/outputs/${featureName}/ui-mockups.md` },
194
194
  uiComponents: { created: false, path: `.morph/project/outputs/${featureName}/ui-components.md` },
@@ -294,10 +294,15 @@ export async function addCheckpoint(featureName, note) {
294
294
  const feature = state.features[featureName];
295
295
 
296
296
  const checkpoint = {
297
+ passed: true,
298
+ checkpointNum: feature.checkpoints.length + 1,
297
299
  timestamp: new Date().toISOString(),
298
300
  phase: feature.phase,
299
- completedTasks: feature.tasks.completed,
300
- note: note
301
+ results: [],
302
+ summary: {
303
+ note: note,
304
+ completedTasks: feature.tasks.completed
305
+ }
301
306
  };
302
307
 
303
308
  feature.checkpoints.push(checkpoint);
@@ -393,21 +398,20 @@ export async function markOutput(featureName, outputType) {
393
398
  }
394
399
 
395
400
  /**
396
- * Sync tasks count from tasks array (if exists)
401
+ * Sync progress counters from taskList array into feature.progress
397
402
  * @param {Object} feature - Feature object
398
403
  */
399
404
  function syncTasksCount(feature) {
400
- // If feature has tasks array (schema 3.0), count them
401
- if (feature.tasks && Array.isArray(feature.tasks)) {
402
- const tasks = feature.tasks;
403
- feature.progress = {
404
- total: tasks.length,
405
- completed: tasks.filter(t => t.status === 'completed').length,
406
- inProgress: tasks.filter(t => t.status === 'in_progress').length,
407
- pending: tasks.filter(t => t.status === 'pending').length,
408
- percentage: tasks.length > 0 ? Math.round((tasks.filter(t => t.status === 'completed').length / tasks.length) * 100) : 0
409
- };
410
- }
405
+ const list = feature.taskList;
406
+ if (!Array.isArray(list) || list.length === 0) return;
407
+ const completed = list.filter(t => t.status === 'completed').length;
408
+ feature.progress = {
409
+ total: list.length,
410
+ completed,
411
+ inProgress: list.filter(t => t.status === 'in_progress').length,
412
+ pending: list.filter(t => t.status === 'pending').length,
413
+ percentage: Math.round((completed / list.length) * 100)
414
+ };
411
415
  }
412
416
 
413
417
  /**
@@ -12,6 +12,7 @@
12
12
  import { readFileSync, existsSync, readdirSync } from 'fs';
13
13
  import { join, dirname } from 'path';
14
14
  import { fileURLToPath } from 'url';
15
+ import { getPhaseSequence } from '../state/phase-state-machine.js';
15
16
 
16
17
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
18
 
@@ -336,7 +337,19 @@ export async function detectWorkflow(options) {
336
337
  */
337
338
  export function getWorkflowConfig(workflowId, projectPath = '.') {
338
339
  const workflows = loadWorkflowConfigs(projectPath);
339
- return workflows.find(w => w.id === workflowId) || null;
340
+ const config = workflows.find(w => w.id === workflowId) || null;
341
+
342
+ if (config) {
343
+ const validPhases = new Set(getPhaseSequence());
344
+ const declaredPhases = config.phases?.run || [];
345
+ for (const phase of declaredPhases) {
346
+ if (!validPhases.has(phase)) {
347
+ console.warn(`[workflow] Warning: workflow "${workflowId}" declares unknown phase "${phase}" — ignored`);
348
+ }
349
+ }
350
+ }
351
+
352
+ return config;
340
353
  }
341
354
 
342
355
  /**
@@ -1,8 +1,10 @@
1
1
  import { readFileSync, existsSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { execSync } from 'child_process';
4
+ import chalk from 'chalk';
4
5
  import { executeStopHooks, isMaxRetriesExceeded, incrementRetryCount } from '../hooks/stop-hook-executor.js';
5
6
  import { recordEvent } from '../analytics/analytics-engine.js';
7
+ import { getFeature } from '../../core/state/state-manager.js';
6
8
 
7
9
  /**
8
10
  * Checkpoint Hooks - Automated validation orchestration
@@ -236,10 +238,11 @@ export async function runCheckpointHooks(featureName, checkpointNum) {
236
238
 
237
239
  if (!stopHookResult.passed) {
238
240
  // Check retry count
241
+ let retryCount = null;
239
242
  if (isMaxRetriesExceeded(pseudoThreadId)) {
240
243
  console.log(chalk.red('\nāŒ Stop hooks failed 5+ times. Escalating to user.'));
241
244
  } else {
242
- const retryCount = incrementRetryCount(pseudoThreadId);
245
+ retryCount = incrementRetryCount(pseudoThreadId);
243
246
  console.log(chalk.yellow(`\n⚠ Stop hooks failed (attempt ${retryCount}/5). Feedback saved.`));
244
247
  if (stopHookResult.feedback?.suggestions?.length > 0) {
245
248
  stopHookResult.feedback.suggestions.forEach(s => console.log(` → ${s}`));
@@ -250,7 +253,7 @@ export async function runCheckpointHooks(featureName, checkpointNum) {
250
253
  recordEvent({
251
254
  type: 'stop_hook_failed_at_checkpoint',
252
255
  feature: featureName,
253
- data: { checkpointNum, retryCount: getRetryCount ? undefined : undefined }
256
+ data: { checkpointNum, retryCount }
254
257
  });
255
258
  }
256
259
  } catch {
@@ -258,12 +261,14 @@ export async function runCheckpointHooks(featureName, checkpointNum) {
258
261
  }
259
262
  }
260
263
 
264
+ const phase = featureName ? (getFeature(featureName)?.phase || null) : null;
265
+
261
266
  return {
262
267
  passed,
263
268
  checkpointNum,
264
269
  timestamp: new Date().toISOString(),
270
+ phase,
265
271
  results,
266
- stopHookResult,
267
272
  summary: {
268
273
  errors: errorCount,
269
274
  warnings: warningCount,
@@ -69,7 +69,7 @@ export function getDetectionSummary(results) {
69
69
  '### Stack',
70
70
  `- **Type**: ${structure?.stack || 'unknown'}`,
71
71
  `- **Architecture**: ${structure?.architecture || 'unknown'}`,
72
- `- **UI Library**: ${structure?.uiLibrary || 'none'}`,
72
+ ...(structure?.uiLibrary ? [`- **UI Library**: ${structure.uiLibrary}`] : []),
73
73
  '',
74
74
  '### Technologies',
75
75
  `- **Language**: ${config?.language || 'unknown'}`,