@polymorphism-tech/morph-spec 7.9.4 → 8.0.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/CLAUDE.md +2 -2
- package/README.md +31 -2
- package/bin/morph-spec.js +50 -2
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +38 -1
- package/docs/QUICKSTART.md +60 -1
- package/framework/CLAUDE.md +13 -2
- package/framework/MORPH.md +38 -2
- package/framework/agents.json +22 -3
- package/framework/commands/commit.md +166 -166
- package/framework/commands/morph-hotfix.md +39 -0
- 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/hooks/README.md +25 -10
- package/framework/hooks/claude-code/core/gate-guard.js +4 -2
- package/framework/hooks/claude-code/core/loop-detect.js +2 -1
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +14 -5
- package/framework/hooks/claude-code/core/tasks-json-guard.js +4 -2
- package/framework/hooks/claude-code/core/validate-completion.js +1 -1
- package/framework/hooks/claude-code/core/worktree-context.js +160 -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/build-check.js +2 -2
- package/framework/hooks/shared/hook-response.js +17 -5
- package/framework/hooks/shared/payload-utils.js +42 -0
- package/framework/hooks/shared/state-reader.js +22 -2
- package/framework/hooks/shared/stdin-reader.js +26 -26
- 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 +95 -1
- 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 +39 -8
- package/framework/skills/core/morph-brainstorming/SKILL.md +9 -1
- package/framework/skills/core/morph-eval/SKILL.md +6 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +136 -0
- package/framework/skills/core/morph-implement/SKILL.md +8 -4
- package/framework/skills/core/morph-plan/SKILL.md +5 -1
- package/framework/skills/core/morph-proposal/SKILL.md +24 -1
- package/framework/skills/core/morph-review/SKILL.md +3 -2
- 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/REGISTRY.json +0 -62
- 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/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 +4 -2
- package/src/commands/project/init.js +46 -2
- package/src/commands/project/update.js +5 -3
- package/src/commands/project/worktree.js +1005 -46
- package/src/commands/state/advance.js +17 -2
- package/src/commands/state/approve.js +18 -1
- package/src/commands/state/create.js +158 -14
- package/src/commands/state/retype.js +126 -0
- package/src/commands/state/score.js +4 -0
- package/src/commands/tasks/dag.js +32 -0
- package/src/commands/validation/index.js +1 -0
- package/src/commands/validation/validate-feature.js +33 -5
- package/src/commands/validation/verify.js +190 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/state/state-manager.js +35 -10
- package/src/core/templates/template-renderer.js +477 -477
- package/src/core/work-types.js +79 -0
- 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 +11 -1
- 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/scanner/state-scanner.js +42 -0
- 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/installers/memory-installer.js +1 -1
- package/src/lib/risk-detector.js +16 -0
- package/src/lib/tasks/dispatch-planner.js +122 -4
- package/src/lib/tasks/mermaid-renderer.js +132 -0
- 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 +27 -6
- 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/lib/validators/verify-runner.js +322 -0
- package/src/lib/work-type-detector.js +143 -0
- package/src/scripts/setup-infra.js +32 -5
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/agents-installer.js +10 -6
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +43 -0
- package/src/utils/hooks-installer.js +41 -11
- package/src/utils/skills-installer.js +174 -174
- package/framework/templates/meta-prompts/parallel-workers/parallel-coordinator.md +0 -113
- package/framework/templates/meta-prompts/parallel-workers/parallel-worker.md +0 -80
- package/src/lib/tasks/wave-resolver.js +0 -353
|
@@ -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
|
+
}
|
|
@@ -98,9 +98,11 @@ const FRAMEWORK_STANDARDS = [
|
|
|
98
98
|
'framework/standards/workflows/parallel-execution.md'
|
|
99
99
|
];
|
|
100
100
|
|
|
101
|
-
// required personas expected in agents.json (v5 —
|
|
101
|
+
// required personas expected in agents.json (v5 — the universal, stack-agnostic
|
|
102
|
+
// personas: evaluator (domain axis) and scout (stage axis))
|
|
102
103
|
const REQUIRED_AGENTS = [
|
|
103
|
-
'evaluator'
|
|
104
|
+
'evaluator',
|
|
105
|
+
'scout'
|
|
104
106
|
];
|
|
105
107
|
|
|
106
108
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, dirname } from 'path';
|
|
1
|
+
import { join, dirname, resolve } from 'path';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
import fs from 'fs-extra';
|
|
@@ -32,6 +32,36 @@ import { installSkills } from '../../utils/skills-installer.js';
|
|
|
32
32
|
import { installAgents, installDomainAgents } from '../../utils/agents-installer.js';
|
|
33
33
|
import { getProjectTags } from '../../lib/stack-filter.js';
|
|
34
34
|
import { updateCommand } from './update.js';
|
|
35
|
+
import { resolvePrimaryRoot } from './worktree.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Guard against running a full `init` install from inside a secondary
|
|
39
|
+
* (linked) git worktree.
|
|
40
|
+
*
|
|
41
|
+
* A worktree's `.claude/` and `.morph/framework/` are junctions back to the
|
|
42
|
+
* primary root (see `worktree.js`'s `setupInfraLinks`), but `.morph/config/`,
|
|
43
|
+
* `.morph/context/`, `.morph/checkpoints/`, `.morph/memory/`, `.morph/archive/`
|
|
44
|
+
* and `.morph/state.json` are NOT linked — a full `init` there would scaffold
|
|
45
|
+
* a second, disconnected install (config, state) inside the worktree, orphaned
|
|
46
|
+
* from the primary root's real install.
|
|
47
|
+
*
|
|
48
|
+
* Fail-SAFE (not fail-open): blocks whenever `resolvePrimaryRoot(targetPath)`
|
|
49
|
+
* resolves to a path OTHER than `targetPath` itself. `resolvePrimaryRoot`
|
|
50
|
+
* itself fails open to `targetPath` outside a git repo (or on any git error),
|
|
51
|
+
* so non-git projects and the primary root itself are never blocked.
|
|
52
|
+
* `--force` is the explicit override, mirroring init's own `--force` flag.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} targetPath - Effective install target (init's targetPath)
|
|
55
|
+
* @param {{ force?: boolean }} [options]
|
|
56
|
+
* @returns {{ blocked: boolean, root?: string, target?: string }}
|
|
57
|
+
*/
|
|
58
|
+
export function checkSecondaryWorktreeGuard(targetPath, options = {}) {
|
|
59
|
+
if (options.force) return { blocked: false };
|
|
60
|
+
const target = resolve(targetPath).replace(/\\/g, '/');
|
|
61
|
+
const root = resolvePrimaryRoot(targetPath);
|
|
62
|
+
if (root === target) return { blocked: false };
|
|
63
|
+
return { blocked: true, root, target };
|
|
64
|
+
}
|
|
35
65
|
|
|
36
66
|
export async function initCommand(options) {
|
|
37
67
|
const targetPath = options.path || process.cwd();
|
|
@@ -40,6 +70,17 @@ export async function initCommand(options) {
|
|
|
40
70
|
logger.dim(`Target: ${targetPath}`);
|
|
41
71
|
logger.blank();
|
|
42
72
|
|
|
73
|
+
// Guard: refuse a full install from inside a secondary worktree — it would
|
|
74
|
+
// corrupt the shared infra junctioned back to the primary root and scaffold
|
|
75
|
+
// a second, disconnected .morph/config + state.json. --force overrides.
|
|
76
|
+
const worktreeGuard = checkSecondaryWorktreeGuard(targetPath, options);
|
|
77
|
+
if (worktreeGuard.blocked) {
|
|
78
|
+
logger.warn(`Você está num worktree secundário (${worktreeGuard.target}).`);
|
|
79
|
+
logger.warn(`O morph-spec já está instalado na raiz primária (${worktreeGuard.root}).`);
|
|
80
|
+
logger.warn('Para reparar infra do worktree use `morph-spec worktree link --all`, não `init`.');
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
43
84
|
// Check if already initialized
|
|
44
85
|
const morphPath = join(targetPath, '.morph');
|
|
45
86
|
if (await pathExists(morphPath)) {
|
|
@@ -64,7 +105,10 @@ export async function initCommand(options) {
|
|
|
64
105
|
|
|
65
106
|
// Steps 1-10: Infrastructure (headless, no prompts)
|
|
66
107
|
spinner.stop();
|
|
67
|
-
|
|
108
|
+
// force: true — the secondary-worktree guard already ran above (either we
|
|
109
|
+
// ARE the primary root, or the caller explicitly passed --force), so it
|
|
110
|
+
// must not be redundantly re-applied inside setupInfra().
|
|
111
|
+
const infraResult = await setupInfra(targetPath, { force: true });
|
|
68
112
|
spinner.start('Continuing...');
|
|
69
113
|
|
|
70
114
|
// Derive display variables from what setupInfra installed
|
|
@@ -16,7 +16,8 @@ import {
|
|
|
16
16
|
ensureDir,
|
|
17
17
|
readJson,
|
|
18
18
|
writeJson,
|
|
19
|
-
updateGitignore
|
|
19
|
+
updateGitignore,
|
|
20
|
+
ensureGitattributes
|
|
20
21
|
} from '../../utils/file-copier.js';
|
|
21
22
|
import {
|
|
22
23
|
checkCLIOutdated,
|
|
@@ -387,9 +388,10 @@ export async function updateCommand(options) {
|
|
|
387
388
|
await restoreUserConfig(morphPath, targetPath, configBackup, cliCheck.current);
|
|
388
389
|
logger.dim(' ✓ User config restored with updated framework version');
|
|
389
390
|
|
|
390
|
-
// Update .gitignore with current morph rules
|
|
391
|
-
updateSpinner.text = 'Updating .gitignore...';
|
|
391
|
+
// Update .gitignore + .gitattributes with current morph rules
|
|
392
|
+
updateSpinner.text = 'Updating .gitignore + .gitattributes...';
|
|
392
393
|
await updateGitignore(targetPath);
|
|
394
|
+
await ensureGitattributes(targetPath);
|
|
393
395
|
|
|
394
396
|
// Migration: untrack a pre-v5 committed state.json so it stops being swept
|
|
395
397
|
// into feature commits. Gitignore alone can't do this for an already-tracked file.
|