@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.
- package/CLAUDE.md +155 -0
- package/bin/morph-spec.js +2 -2
- package/bin/task-manager.cjs +102 -14
- package/package.json +1 -1
- package/src/commands/agents/agents-fuse.js +2 -1
- package/src/commands/project/detect-agents.js +31 -1
- package/src/commands/project/detect.js +11 -1
- package/src/commands/project/doctor.js +52 -52
- package/src/commands/project/init.js +8 -1
- package/src/commands/project/update.js +12 -2
- package/src/commands/state/advance-phase.js +19 -4
- package/src/commands/state/state.js +14 -12
- package/src/commands/tasks/task.js +1 -1
- package/src/commands/threads/thread-template.js +1 -1
- package/src/core/state/state-manager.js +19 -15
- package/src/core/workflows/workflow-detector.js +14 -1
- package/src/lib/checkpoints/checkpoint-hooks.js +8 -3
- package/src/lib/detectors/index.js +1 -1
- package/src/lib/detectors/standards-generator.js +77 -17
- package/src/lib/detectors/structure-detector.js +67 -39
- package/src/lib/generators/recap-generator.js +30 -10
- package/src/lib/validators/validation-runner.js +8 -24
- package/src/utils/hooks-installer.js +69 -0
|
@@ -53,7 +53,7 @@ function getNpmGlobalPrefix() {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
// lib files
|
|
56
|
-
const
|
|
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
|
-
//
|
|
75
|
-
const
|
|
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
|
-
//
|
|
89
|
-
const
|
|
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/
|
|
101
|
+
'framework/templates/REGISTRY.json'
|
|
102
102
|
];
|
|
103
103
|
|
|
104
|
-
//
|
|
105
|
-
const
|
|
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
|
-
//
|
|
132
|
-
const
|
|
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
|
|
140
|
+
* Run full health checks
|
|
141
141
|
*/
|
|
142
|
-
async function
|
|
143
|
-
console.log(chalk.bold('\nš¬ MORPH-SPEC
|
|
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.
|
|
161
|
-
console.log(chalk.cyan(
|
|
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
|
|
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(`
|
|
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.
|
|
171
|
-
console.log(chalk.cyan(
|
|
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
|
|
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(`
|
|
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
|
|
181
|
-
console.log(chalk.cyan(
|
|
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
|
|
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
|
|
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.
|
|
191
|
-
console.log(chalk.cyan(
|
|
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
|
|
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(`
|
|
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.
|
|
201
|
-
console.log(chalk.cyan(
|
|
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 =
|
|
208
|
-
check(`
|
|
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
|
|
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
|
|
230
|
-
check(`state.json schema (
|
|
231
|
-
|
|
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 ā
|
|
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
|
|
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
|
-
//
|
|
268
|
-
if (options.
|
|
267
|
+
// full health check mode
|
|
268
|
+
if (options.full) {
|
|
269
269
|
const frameworkRoot = process.cwd();
|
|
270
|
-
return
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
212
|
-
|
|
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
|
|
236
|
-
|
|
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.
|
|
248
|
-
logger.info(`Completed: ${chalk.cyan(summary.metadata
|
|
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
|
|
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.
|
|
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
|
-
|
|
300
|
-
|
|
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
|
|
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
|
-
|
|
401
|
-
if (
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
72
|
+
...(structure?.uiLibrary ? [`- **UI Library**: ${structure.uiLibrary}`] : []),
|
|
73
73
|
'',
|
|
74
74
|
'### Technologies',
|
|
75
75
|
`- **Language**: ${config?.language || 'unknown'}`,
|