@polymorphism-tech/morph-spec 7.8.1 → 7.9.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.
- package/README.md +2 -2
- package/bin/morph-spec.js +30 -1
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +3 -1
- package/framework/MORPH.md +4 -3
- package/framework/commands/commit.md +166 -166
- package/framework/commands/morph-infra.md +215 -215
- package/framework/commands/morph-preflight.md +205 -205
- package/framework/commands/morph-troubleshoot.md +124 -124
- package/framework/evals/trace-schema.md +22 -12
- package/framework/hooks/README.md +2 -0
- package/framework/hooks/claude-code/core/subagent-report-guard.js +95 -0
- package/framework/hooks/claude-code/core/trace-autogen.js +183 -0
- package/framework/hooks/claude-code/statusline.py +679 -679
- package/framework/hooks/claude-code/statusline.sh +11 -11
- package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
- package/framework/hooks/git/pre-commit/agents.sh +25 -25
- package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
- package/framework/hooks/git/pre-commit/specs.sh +50 -50
- package/framework/hooks/git/pre-push/run-tests.sh +44 -44
- package/framework/hooks/shared/activity-logger.js +160 -160
- package/framework/hooks/shared/hook-response.js +45 -45
- package/framework/hooks/shared/payload-utils.js +78 -78
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/hooks/shared/tasks-validator.js +47 -4
- package/framework/rubrics/backend.json +41 -41
- package/framework/rules/ai-agents-standards.md +45 -45
- package/framework/rules/dotnet-templates.md +86 -86
- package/framework/rules/frontend-standards.md +13 -13
- package/framework/rules/infrastructure-standards.md +27 -27
- package/framework/rules/neon-standards.md +25 -25
- package/framework/rules/nextjs-standards.md +20 -20
- package/framework/rules/nextjs-templates.md +40 -40
- package/framework/schemas/feature.schema.json +128 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +23 -10
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +3 -6
- package/framework/skills/core/morph-implement/SKILL.md +28 -15
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +2 -2
- package/framework/skills/core/morph-review/SKILL.md +2 -6
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/standards/ai-agents/agent-session.md +160 -160
- package/framework/standards/ai-agents/batching.md +193 -193
- package/framework/standards/ai-agents/context-providers.md +188 -188
- package/framework/standards/ai-agents/mcp-server.md +250 -250
- package/framework/standards/ai-agents/mcp-tools.md +197 -197
- package/framework/standards/ai-agents/middleware-patterns.md +207 -207
- package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
- package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
- package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
- package/framework/standards/ai-agents/modalities-vision.md +201 -201
- package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
- package/framework/standards/ai-agents/production.md +275 -275
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/docs/clarifications.md +253 -253
- package/framework/templates/docs/schema-analysis.md +119 -119
- package/framework/templates/docs/ui-components.md +52 -52
- package/framework/templates/docs/ui-design-system.md +76 -76
- package/framework/templates/docs/ui-flows.md +167 -167
- package/framework/templates/docs/ui-mockups.md +98 -98
- package/framework/templates/feature/decisions.md +122 -122
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
- package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
- package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
- package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
- package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
- package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
- package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +50 -50
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/github/README.md +56 -56
- package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
- package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
- package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
- package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
- package/framework/templates/integrations/asaas-client.cs +387 -387
- package/framework/templates/integrations/asaas-webhook.cs +351 -351
- package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
- package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
- package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +113 -113
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
- package/framework/templates/project/validate.js +120 -120
- package/framework/templates/state.template.json +17 -17
- package/framework/templates/ui/design-system.css +226 -226
- package/package.json +1 -1
- package/src/commands/dashboard/dashboard.js +53 -53
- package/src/commands/index.js +13 -13
- package/src/commands/mcp/mcp-setup.js +519 -519
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +23 -18
- package/src/commands/project/update.js +16 -9
- package/src/commands/state/create.js +58 -0
- package/src/commands/state/gate-decision.js +99 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/state/state-manager.js +55 -0
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/LiveState.jsx +239 -239
- package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
- package/src/dashboard/app/pages/RulesMap.jsx +273 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
- package/src/dashboard/app/pages/Standards.jsx +332 -332
- package/src/dashboard/app/styles/index.css +47 -47
- package/src/dashboard/index.html +12 -12
- package/src/dashboard/package.json +24 -24
- package/src/dashboard/scanner/hooks-scanner.js +41 -41
- package/src/dashboard/scanner/phases-scanner.js +15 -15
- package/src/dashboard/scanner/rubrics-scanner.js +12 -12
- package/src/dashboard/scanner/rules-scanner.js +37 -37
- package/src/dashboard/scanner/skills-scanner.js +43 -43
- package/src/dashboard/scanner/squads-scanner.js +13 -13
- package/src/dashboard/scanner/standards-scanner.js +26 -26
- package/src/dashboard/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/agents/metrics-aggregator.js +56 -56
- package/src/lib/agents/metrics-calculator.js +125 -125
- package/src/lib/agents/output-collector.js +73 -73
- package/src/lib/detectors/claude-config-detector.js +152 -152
- package/src/lib/detectors/design-system-detector.js +231 -231
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +639 -639
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/index.js +13 -13
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/test-runner.js +81 -81
- package/src/lib/validators/architecture/architecture-validator.js +259 -259
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/contracts/contract-compliance-validator.js +274 -274
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/packages/package-validator.js +347 -347
- package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
- package/src/lib/validators/shared/issue-counter.js +14 -14
- package/src/lib/validators/validation-runner.js +349 -349
- package/src/scripts/setup-infra.js +385 -383
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +25 -10
- package/src/utils/banner.js +37 -4
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/hooks-installer.js +32 -4
- package/src/utils/skills-installer.js +174 -172
|
@@ -1,209 +1,209 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MORPH-SPEC Checkpoint Save/Restore Command
|
|
3
|
-
*
|
|
4
|
-
* Save and restore full context at phase boundaries.
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* morph-spec checkpoint save <feature-name>
|
|
8
|
-
* morph-spec checkpoint restore <feature-name>
|
|
9
|
-
* morph-spec checkpoint list <feature-name>
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import fs from 'fs';
|
|
13
|
-
import path from 'path';
|
|
14
|
-
import chalk from 'chalk';
|
|
15
|
-
import { loadState, saveState, getFeature } from '../../core/state/state-manager.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Get checkpoints directory for a feature
|
|
19
|
-
*/
|
|
20
|
-
function getCheckpointsDir(featureName) {
|
|
21
|
-
return path.join(process.cwd(), '.morph/checkpoints', featureName);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Save checkpoint command
|
|
26
|
-
*/
|
|
27
|
-
export async function checkpointSaveCommand(featureName, options = {}) {
|
|
28
|
-
if (!featureName) {
|
|
29
|
-
console.error(chalk.red('Error: Feature name required'));
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const feature = getFeature(featureName);
|
|
34
|
-
if (!feature) {
|
|
35
|
-
console.error(chalk.red(`Error: Feature '${featureName}' not found`));
|
|
36
|
-
process.exit(1);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const outputDir = path.join(process.cwd(), '.morph/features', featureName);
|
|
40
|
-
if (!fs.existsSync(outputDir)) {
|
|
41
|
-
console.error(chalk.red(`Error: No outputs found for '${featureName}'`));
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Create checkpoint directory with timestamp
|
|
46
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
47
|
-
const label = options.label || feature.phase;
|
|
48
|
-
const checkpointName = `${label}_${timestamp}`;
|
|
49
|
-
const checkpointDir = path.join(getCheckpointsDir(featureName), checkpointName);
|
|
50
|
-
fs.mkdirSync(checkpointDir, { recursive: true });
|
|
51
|
-
|
|
52
|
-
// Copy all output files
|
|
53
|
-
const files = fs.readdirSync(outputDir);
|
|
54
|
-
let fileCount = 0;
|
|
55
|
-
for (const file of files) {
|
|
56
|
-
const src = path.join(outputDir, file);
|
|
57
|
-
const stat = fs.statSync(src);
|
|
58
|
-
if (stat.isFile()) {
|
|
59
|
-
fs.copyFileSync(src, path.join(checkpointDir, file));
|
|
60
|
-
fileCount++;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Save state snapshot
|
|
65
|
-
const stateSnapshot = {
|
|
66
|
-
feature: { ...feature },
|
|
67
|
-
timestamp: new Date().toISOString(),
|
|
68
|
-
phase: feature.phase,
|
|
69
|
-
label,
|
|
70
|
-
files: files.filter(f => fs.statSync(path.join(outputDir, f)).isFile())
|
|
71
|
-
};
|
|
72
|
-
fs.writeFileSync(
|
|
73
|
-
path.join(checkpointDir, '_checkpoint.json'),
|
|
74
|
-
JSON.stringify(stateSnapshot, null, 2)
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
console.log(chalk.green(`\n✓ Checkpoint saved: ${checkpointName}`));
|
|
78
|
-
console.log(chalk.gray(` Phase: ${feature.phase}`));
|
|
79
|
-
console.log(chalk.gray(` Files: ${fileCount}`));
|
|
80
|
-
console.log(chalk.gray(` Path: ${checkpointDir}\n`));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Restore checkpoint command
|
|
85
|
-
*/
|
|
86
|
-
export async function checkpointRestoreCommand(featureName, checkpointName, options = {}) {
|
|
87
|
-
if (!featureName) {
|
|
88
|
-
console.error(chalk.red('Error: Feature name required'));
|
|
89
|
-
process.exit(1);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const checkpointsDir = getCheckpointsDir(featureName);
|
|
93
|
-
if (!fs.existsSync(checkpointsDir)) {
|
|
94
|
-
console.error(chalk.red(`Error: No checkpoints found for '${featureName}'`));
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Find checkpoint to restore
|
|
99
|
-
let targetDir;
|
|
100
|
-
if (checkpointName) {
|
|
101
|
-
targetDir = path.join(checkpointsDir, checkpointName);
|
|
102
|
-
if (!fs.existsSync(targetDir)) {
|
|
103
|
-
console.error(chalk.red(`Error: Checkpoint '${checkpointName}' not found`));
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
// Restore latest
|
|
108
|
-
const checkpoints = fs.readdirSync(checkpointsDir)
|
|
109
|
-
.filter(d => fs.statSync(path.join(checkpointsDir, d)).isDirectory())
|
|
110
|
-
.sort()
|
|
111
|
-
.reverse();
|
|
112
|
-
|
|
113
|
-
if (checkpoints.length === 0) {
|
|
114
|
-
console.error(chalk.red(`Error: No checkpoints found for '${featureName}'`));
|
|
115
|
-
process.exit(1);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
targetDir = path.join(checkpointsDir, checkpoints[0]);
|
|
119
|
-
checkpointName = checkpoints[0];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Read checkpoint metadata
|
|
123
|
-
const metaPath = path.join(targetDir, '_checkpoint.json');
|
|
124
|
-
if (!fs.existsSync(metaPath)) {
|
|
125
|
-
console.error(chalk.red('Error: Checkpoint metadata not found'));
|
|
126
|
-
process.exit(1);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
130
|
-
|
|
131
|
-
// Restore output files
|
|
132
|
-
const outputDir = path.join(process.cwd(), '.morph/features', featureName);
|
|
133
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
134
|
-
|
|
135
|
-
const files = fs.readdirSync(targetDir).filter(f => f !== '_checkpoint.json');
|
|
136
|
-
for (const file of files) {
|
|
137
|
-
fs.copyFileSync(path.join(targetDir, file), path.join(outputDir, file));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Restore state
|
|
141
|
-
const state = loadState();
|
|
142
|
-
if (state.features[featureName] && meta.feature) {
|
|
143
|
-
// Preserve some current fields, restore phase and outputs
|
|
144
|
-
state.features[featureName].phase = meta.feature.phase;
|
|
145
|
-
state.features[featureName].outputs = meta.feature.outputs;
|
|
146
|
-
state.features[featureName].tasks = meta.feature.tasks;
|
|
147
|
-
state.features[featureName].approvalGates = meta.feature.approvalGates;
|
|
148
|
-
state.features[featureName].updatedAt = new Date().toISOString();
|
|
149
|
-
saveState(state);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
console.log(chalk.green(`\n✓ Checkpoint restored: ${checkpointName}`));
|
|
153
|
-
console.log(chalk.gray(` Phase: ${meta.phase}`));
|
|
154
|
-
console.log(chalk.gray(` Files restored: ${files.length}`));
|
|
155
|
-
console.log(chalk.gray(` Original date: ${meta.timestamp}\n`));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* List checkpoints command
|
|
160
|
-
*/
|
|
161
|
-
export async function checkpointListCommand(featureName, options = {}) {
|
|
162
|
-
if (!featureName) {
|
|
163
|
-
console.error(chalk.red('Error: Feature name required'));
|
|
164
|
-
process.exit(1);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const checkpointsDir = getCheckpointsDir(featureName);
|
|
168
|
-
if (!fs.existsSync(checkpointsDir)) {
|
|
169
|
-
console.log(chalk.gray(`No checkpoints found for '${featureName}'.\n`));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const checkpoints = fs.readdirSync(checkpointsDir)
|
|
174
|
-
.filter(d => fs.statSync(path.join(checkpointsDir, d)).isDirectory())
|
|
175
|
-
.sort()
|
|
176
|
-
.reverse();
|
|
177
|
-
|
|
178
|
-
if (checkpoints.length === 0) {
|
|
179
|
-
console.log(chalk.gray(`No checkpoints found for '${featureName}'.\n`));
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// JSON output
|
|
184
|
-
if (options.json) {
|
|
185
|
-
const list = checkpoints.map(cp => {
|
|
186
|
-
const metaPath = path.join(checkpointsDir, cp, '_checkpoint.json');
|
|
187
|
-
const meta = fs.existsSync(metaPath) ? JSON.parse(fs.readFileSync(metaPath, 'utf8')) : {};
|
|
188
|
-
return { name: cp, phase: meta.phase, timestamp: meta.timestamp, files: meta.files?.length || 0 };
|
|
189
|
-
});
|
|
190
|
-
console.log(JSON.stringify({ feature: featureName, checkpoints: list }, null, 2));
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Visual output
|
|
195
|
-
console.log(chalk.cyan(`\nCheckpoints for '${featureName}':\n`));
|
|
196
|
-
|
|
197
|
-
for (const cp of checkpoints) {
|
|
198
|
-
const metaPath = path.join(checkpointsDir, cp, '_checkpoint.json');
|
|
199
|
-
let meta = {};
|
|
200
|
-
if (fs.existsSync(metaPath)) {
|
|
201
|
-
meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const date = meta.timestamp ? new Date(meta.timestamp).toLocaleString() : 'unknown';
|
|
205
|
-
console.log(chalk.white(` ${cp}`));
|
|
206
|
-
console.log(chalk.gray(` Phase: ${meta.phase || 'unknown'} | Files: ${meta.files?.length || '?'} | ${date}`));
|
|
207
|
-
}
|
|
208
|
-
console.log('');
|
|
209
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* MORPH-SPEC Checkpoint Save/Restore Command
|
|
3
|
+
*
|
|
4
|
+
* Save and restore full context at phase boundaries.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* morph-spec checkpoint save <feature-name>
|
|
8
|
+
* morph-spec checkpoint restore <feature-name>
|
|
9
|
+
* morph-spec checkpoint list <feature-name>
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { loadState, saveState, getFeature } from '../../core/state/state-manager.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get checkpoints directory for a feature
|
|
19
|
+
*/
|
|
20
|
+
function getCheckpointsDir(featureName) {
|
|
21
|
+
return path.join(process.cwd(), '.morph/checkpoints', featureName);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Save checkpoint command
|
|
26
|
+
*/
|
|
27
|
+
export async function checkpointSaveCommand(featureName, options = {}) {
|
|
28
|
+
if (!featureName) {
|
|
29
|
+
console.error(chalk.red('Error: Feature name required'));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const feature = getFeature(featureName);
|
|
34
|
+
if (!feature) {
|
|
35
|
+
console.error(chalk.red(`Error: Feature '${featureName}' not found`));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const outputDir = path.join(process.cwd(), '.morph/features', featureName);
|
|
40
|
+
if (!fs.existsSync(outputDir)) {
|
|
41
|
+
console.error(chalk.red(`Error: No outputs found for '${featureName}'`));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Create checkpoint directory with timestamp
|
|
46
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
47
|
+
const label = options.label || feature.phase;
|
|
48
|
+
const checkpointName = `${label}_${timestamp}`;
|
|
49
|
+
const checkpointDir = path.join(getCheckpointsDir(featureName), checkpointName);
|
|
50
|
+
fs.mkdirSync(checkpointDir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
// Copy all output files
|
|
53
|
+
const files = fs.readdirSync(outputDir);
|
|
54
|
+
let fileCount = 0;
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
const src = path.join(outputDir, file);
|
|
57
|
+
const stat = fs.statSync(src);
|
|
58
|
+
if (stat.isFile()) {
|
|
59
|
+
fs.copyFileSync(src, path.join(checkpointDir, file));
|
|
60
|
+
fileCount++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Save state snapshot
|
|
65
|
+
const stateSnapshot = {
|
|
66
|
+
feature: { ...feature },
|
|
67
|
+
timestamp: new Date().toISOString(),
|
|
68
|
+
phase: feature.phase,
|
|
69
|
+
label,
|
|
70
|
+
files: files.filter(f => fs.statSync(path.join(outputDir, f)).isFile())
|
|
71
|
+
};
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
path.join(checkpointDir, '_checkpoint.json'),
|
|
74
|
+
JSON.stringify(stateSnapshot, null, 2)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
console.log(chalk.green(`\n✓ Checkpoint saved: ${checkpointName}`));
|
|
78
|
+
console.log(chalk.gray(` Phase: ${feature.phase}`));
|
|
79
|
+
console.log(chalk.gray(` Files: ${fileCount}`));
|
|
80
|
+
console.log(chalk.gray(` Path: ${checkpointDir}\n`));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Restore checkpoint command
|
|
85
|
+
*/
|
|
86
|
+
export async function checkpointRestoreCommand(featureName, checkpointName, options = {}) {
|
|
87
|
+
if (!featureName) {
|
|
88
|
+
console.error(chalk.red('Error: Feature name required'));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const checkpointsDir = getCheckpointsDir(featureName);
|
|
93
|
+
if (!fs.existsSync(checkpointsDir)) {
|
|
94
|
+
console.error(chalk.red(`Error: No checkpoints found for '${featureName}'`));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Find checkpoint to restore
|
|
99
|
+
let targetDir;
|
|
100
|
+
if (checkpointName) {
|
|
101
|
+
targetDir = path.join(checkpointsDir, checkpointName);
|
|
102
|
+
if (!fs.existsSync(targetDir)) {
|
|
103
|
+
console.error(chalk.red(`Error: Checkpoint '${checkpointName}' not found`));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
// Restore latest
|
|
108
|
+
const checkpoints = fs.readdirSync(checkpointsDir)
|
|
109
|
+
.filter(d => fs.statSync(path.join(checkpointsDir, d)).isDirectory())
|
|
110
|
+
.sort()
|
|
111
|
+
.reverse();
|
|
112
|
+
|
|
113
|
+
if (checkpoints.length === 0) {
|
|
114
|
+
console.error(chalk.red(`Error: No checkpoints found for '${featureName}'`));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
targetDir = path.join(checkpointsDir, checkpoints[0]);
|
|
119
|
+
checkpointName = checkpoints[0];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Read checkpoint metadata
|
|
123
|
+
const metaPath = path.join(targetDir, '_checkpoint.json');
|
|
124
|
+
if (!fs.existsSync(metaPath)) {
|
|
125
|
+
console.error(chalk.red('Error: Checkpoint metadata not found'));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
130
|
+
|
|
131
|
+
// Restore output files
|
|
132
|
+
const outputDir = path.join(process.cwd(), '.morph/features', featureName);
|
|
133
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
134
|
+
|
|
135
|
+
const files = fs.readdirSync(targetDir).filter(f => f !== '_checkpoint.json');
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
fs.copyFileSync(path.join(targetDir, file), path.join(outputDir, file));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Restore state
|
|
141
|
+
const state = loadState();
|
|
142
|
+
if (state.features[featureName] && meta.feature) {
|
|
143
|
+
// Preserve some current fields, restore phase and outputs
|
|
144
|
+
state.features[featureName].phase = meta.feature.phase;
|
|
145
|
+
state.features[featureName].outputs = meta.feature.outputs;
|
|
146
|
+
state.features[featureName].tasks = meta.feature.tasks;
|
|
147
|
+
state.features[featureName].approvalGates = meta.feature.approvalGates;
|
|
148
|
+
state.features[featureName].updatedAt = new Date().toISOString();
|
|
149
|
+
saveState(state);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(chalk.green(`\n✓ Checkpoint restored: ${checkpointName}`));
|
|
153
|
+
console.log(chalk.gray(` Phase: ${meta.phase}`));
|
|
154
|
+
console.log(chalk.gray(` Files restored: ${files.length}`));
|
|
155
|
+
console.log(chalk.gray(` Original date: ${meta.timestamp}\n`));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* List checkpoints command
|
|
160
|
+
*/
|
|
161
|
+
export async function checkpointListCommand(featureName, options = {}) {
|
|
162
|
+
if (!featureName) {
|
|
163
|
+
console.error(chalk.red('Error: Feature name required'));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const checkpointsDir = getCheckpointsDir(featureName);
|
|
168
|
+
if (!fs.existsSync(checkpointsDir)) {
|
|
169
|
+
console.log(chalk.gray(`No checkpoints found for '${featureName}'.\n`));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const checkpoints = fs.readdirSync(checkpointsDir)
|
|
174
|
+
.filter(d => fs.statSync(path.join(checkpointsDir, d)).isDirectory())
|
|
175
|
+
.sort()
|
|
176
|
+
.reverse();
|
|
177
|
+
|
|
178
|
+
if (checkpoints.length === 0) {
|
|
179
|
+
console.log(chalk.gray(`No checkpoints found for '${featureName}'.\n`));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// JSON output
|
|
184
|
+
if (options.json) {
|
|
185
|
+
const list = checkpoints.map(cp => {
|
|
186
|
+
const metaPath = path.join(checkpointsDir, cp, '_checkpoint.json');
|
|
187
|
+
const meta = fs.existsSync(metaPath) ? JSON.parse(fs.readFileSync(metaPath, 'utf8')) : {};
|
|
188
|
+
return { name: cp, phase: meta.phase, timestamp: meta.timestamp, files: meta.files?.length || 0 };
|
|
189
|
+
});
|
|
190
|
+
console.log(JSON.stringify({ feature: featureName, checkpoints: list }, null, 2));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Visual output
|
|
195
|
+
console.log(chalk.cyan(`\nCheckpoints for '${featureName}':\n`));
|
|
196
|
+
|
|
197
|
+
for (const cp of checkpoints) {
|
|
198
|
+
const metaPath = path.join(checkpointsDir, cp, '_checkpoint.json');
|
|
199
|
+
let meta = {};
|
|
200
|
+
if (fs.existsSync(metaPath)) {
|
|
201
|
+
meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const date = meta.timestamp ? new Date(meta.timestamp).toLocaleString() : 'unknown';
|
|
205
|
+
console.log(chalk.white(` ${cp}`));
|
|
206
|
+
console.log(chalk.gray(` Phase: ${meta.phase || 'unknown'} | Files: ${meta.files?.length || '?'} | ${date}`));
|
|
207
|
+
}
|
|
208
|
+
console.log('');
|
|
209
|
+
}
|
|
@@ -764,26 +764,31 @@ export async function doctorCommand(options = {}) {
|
|
|
764
764
|
const morphAgents = agentFiles.filter(f => f.startsWith('morph-') && f.endsWith('.md'));
|
|
765
765
|
checks.push({ name: '.claude/agents/', status: 'ok', msg: `${morphAgents.length} agents` });
|
|
766
766
|
|
|
767
|
-
// Check for missing agents based on stack tags
|
|
767
|
+
// Check for missing agents based on stack tags. Empty tags (no config
|
|
768
|
+
// yet) is NOT a skip: personas install-all by default, so with an
|
|
769
|
+
// unknown stack ALL personas are expected (agentEligible semantics) —
|
|
770
|
+
// the exact field failure where only morph-evaluator landed and every
|
|
771
|
+
// domain dispatch fell back to general-purpose.
|
|
768
772
|
try {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
|
|
773
|
+
let projectTags = [];
|
|
774
|
+
try {
|
|
775
|
+
projectTags = getProjectTags(await readJson(configPath));
|
|
776
|
+
} catch { /* no config → expected = all personas */ }
|
|
777
|
+
const tagsLabel = projectTags.length > 0 ? projectTags.join(', ') : 'all (stack unknown)';
|
|
778
|
+
const frameworkDir = join(import.meta.dirname, '..', '..', '..', 'framework');
|
|
779
|
+
const { missing } = checkMissingAgents(targetPath, frameworkDir, projectTags);
|
|
780
|
+
if (missing.length > 0) {
|
|
781
|
+
const names = missing.map(m => m.name).join(', ');
|
|
782
|
+
checks.push({
|
|
783
|
+
name: 'agents completeness',
|
|
784
|
+
status: 'warn',
|
|
785
|
+
msg: `${missing.length} agent(s) missing for [${tagsLabel}]: ${names} — run morph-spec setup-infra`
|
|
786
|
+
});
|
|
787
|
+
hasWarnings = true;
|
|
788
|
+
} else {
|
|
789
|
+
checks.push({ name: 'agents completeness', status: 'ok', msg: `all agents for [${tagsLabel}]` });
|
|
785
790
|
}
|
|
786
|
-
} catch { /*
|
|
791
|
+
} catch { /* framework dir not resolvable — skip completeness check */ }
|
|
787
792
|
} else {
|
|
788
793
|
checks.push({ name: '.claude/agents/', status: 'warn', msg: 'missing — run morph-spec update' });
|
|
789
794
|
hasWarnings = true;
|
|
@@ -322,16 +322,19 @@ export async function updateCommand(options) {
|
|
|
322
322
|
logger.warn(' ⚠ framework/commands/ source missing — commands not updated');
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
// Compute project tags for stack-aware installation of skills, agents, and rules
|
|
325
|
+
// Compute project tags for stack-aware installation of skills, agents, and rules.
|
|
326
|
+
// MUST come from the pre-clean backup: cleanFrameworkDirs() deleted .morph/config/
|
|
327
|
+
// above and restoreUserConfig() only runs at the end — re-reading the file here
|
|
328
|
+
// yields [] and silently downgrades the whole install to universal-only assets
|
|
329
|
+
// (the ".claude/agents/ only has morph-evaluator.md after update" bug).
|
|
326
330
|
let projectTags = [];
|
|
327
331
|
try {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
} catch { /* ignore — config may not exist yet */ }
|
|
332
|
+
if (configBackup) projectTags = getProjectTags(configBackup);
|
|
333
|
+
} catch { /* ignore — malformed config */ }
|
|
331
334
|
|
|
332
335
|
// Sync morph skills to .claude/skills/ for native Claude Code discovery
|
|
333
336
|
updateSpinner.text = 'Syncing morph skills to .claude/skills/...';
|
|
334
|
-
await installSkills(targetPath, { projectTags });
|
|
337
|
+
const skillsResult = await installSkills(targetPath, { projectTags });
|
|
335
338
|
|
|
336
339
|
// Sync native subagents to .claude/agents/
|
|
337
340
|
updateSpinner.text = 'Syncing agents to .claude/agents/...';
|
|
@@ -344,9 +347,10 @@ export async function updateCommand(options) {
|
|
|
344
347
|
updateSpinner.text = 'Syncing rules to .claude/rules/...';
|
|
345
348
|
const rulesSrc = join(frameworkDir, 'rules');
|
|
346
349
|
const rulesDest = join(claudeDest, 'rules');
|
|
350
|
+
let rulesResult = null;
|
|
347
351
|
if (await pathExists(rulesSrc)) {
|
|
348
352
|
await ensureDir(rulesDest);
|
|
349
|
-
installRulesForStack(rulesSrc, rulesDest, projectTags);
|
|
353
|
+
rulesResult = installRulesForStack(rulesSrc, rulesDest, projectTags);
|
|
350
354
|
}
|
|
351
355
|
|
|
352
356
|
// Sync runtime CLAUDE.md to .claude/CLAUDE.md and derive the personas
|
|
@@ -417,10 +421,13 @@ export async function updateCommand(options) {
|
|
|
417
421
|
logger.dim(' ✓ .morph/framework/schemas/ + rubrics/ + evals/ (bar 9 + LLM rubrics)');
|
|
418
422
|
if (commandsCopied) logger.dim(' ✓ .claude/commands/');
|
|
419
423
|
|
|
424
|
+
// Counts make a silent stack-filter downgrade visible at a glance —
|
|
425
|
+
// a dotnet project reporting "1 persona" is a red flag, not a success.
|
|
426
|
+
const tagsLabel = projectTags.length > 0 ? projectTags.join('+') : 'universal only';
|
|
420
427
|
logger.dim(` ✓ .claude/settings.local.json (${hooksResult.installed} hooks installed)`);
|
|
421
|
-
logger.dim(
|
|
422
|
-
logger.dim(
|
|
423
|
-
logger.dim(
|
|
428
|
+
logger.dim(` ✓ .claude/skills/ (${skillsResult?.installed ?? '?'} skills — stack: ${tagsLabel})`);
|
|
429
|
+
logger.dim(` ✓ .claude/agents/ (${agentsResult?.agents?.length ?? '?'} personas — stack: ${tagsLabel})`);
|
|
430
|
+
logger.dim(` ✓ .claude/rules/ (${rulesResult?.installed ?? '?'} stack-filtered rules)`);
|
|
424
431
|
logger.dim(' ✓ .claude/CLAUDE.md (runtime quick reference)');
|
|
425
432
|
logger.dim(' ✓ ~/.claude/statusline.sh (global statusline synced)');
|
|
426
433
|
logger.dim(' ✓ CLAUDE.md');
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { mkdirSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { createFeature } from '../../core/state/state-manager.js';
|
|
5
|
+
|
|
6
|
+
/** Feature names are kebab-case: lowercase alphanumerics and hyphens. */
|
|
7
|
+
const FEATURE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
8
|
+
|
|
9
|
+
const VALID_STATUSES = ['draft', 'in_progress', 'completed'];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `morph-spec create <feature>` — explicit feature birth: creates
|
|
13
|
+
* `.morph/features/{feature}/0-proposal/` and the canonical feature.json
|
|
14
|
+
* (shape documented in framework/schemas/feature.schema.json).
|
|
15
|
+
*
|
|
16
|
+
* Replaces the implicit birth via the state-sync hook on the first Write —
|
|
17
|
+
* the skills call this at proposal start so the authoritative state exists
|
|
18
|
+
* with the full canonical shape before any artifact lands. Idempotent: an
|
|
19
|
+
* existing feature is reported and left untouched.
|
|
20
|
+
*/
|
|
21
|
+
export async function createCommand(featureName, options = {}) {
|
|
22
|
+
try {
|
|
23
|
+
if (!FEATURE_NAME_RE.test(featureName)) {
|
|
24
|
+
console.error(chalk.red(`\n❌ Invalid feature name: "${featureName}"`));
|
|
25
|
+
console.error(chalk.gray('Use kebab-case: lowercase letters, digits and hyphens (e.g. "user-onboarding")\n'));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (options.status !== undefined && !VALID_STATUSES.includes(options.status)) {
|
|
30
|
+
console.error(chalk.red(`\n❌ Invalid --status: "${options.status}"`));
|
|
31
|
+
console.error(chalk.gray(`Valid statuses: ${VALID_STATUSES.join(', ')}\n`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { feature, created } = await createFeature(featureName, { status: options.status });
|
|
36
|
+
|
|
37
|
+
// The proposal folder is the first working directory of every feature.
|
|
38
|
+
const featureDir = join(process.cwd(), '.morph', 'features', featureName);
|
|
39
|
+
mkdirSync(join(featureDir, '0-proposal'), { recursive: true });
|
|
40
|
+
|
|
41
|
+
if (options.json) {
|
|
42
|
+
console.log(JSON.stringify({ feature: featureName, created, ...feature }, null, 2));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (created) {
|
|
47
|
+
console.log(chalk.green(`\n✅ Feature created: ${featureName}`));
|
|
48
|
+
console.log(chalk.gray(` .morph/features/${featureName}/feature.json (canonical shape) + 0-proposal/`));
|
|
49
|
+
console.log(chalk.gray(` status: ${feature.status} | createdAt: ${feature.createdAt}\n`));
|
|
50
|
+
} else {
|
|
51
|
+
console.log(chalk.yellow(`\n⚠️ Feature "${featureName}" already exists — left untouched`));
|
|
52
|
+
console.log(chalk.gray(` status: ${feature.status} | createdAt: ${feature.createdAt || 'unknown'}\n`));
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(chalk.red(`\n❌ Error creating feature: ${error.message}\n`));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|