@polymorphism-tech/morph-spec 8.33.0 → 8.34.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CLAUDE.md +3 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +25 -1
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +2 -2
  9. package/framework/MORPH.md +4 -5
  10. package/framework/agents.json +1 -0
  11. package/framework/docs/CLI.md +5 -2
  12. package/framework/hooks/README.md +11 -1
  13. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
  14. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
  15. package/framework/hooks/claude-code/core/chain.js +27 -7
  16. package/framework/hooks/claude-code/core/worktree-context.js +83 -14
  17. package/framework/hooks/claude-code/core/worktree-create.js +206 -0
  18. package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
  19. package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
  20. package/framework/hooks/claude-code/statusline.sh +11 -11
  21. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  22. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  23. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  24. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  25. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  26. package/framework/hooks/shared/command-tokens.js +23 -0
  27. package/framework/hooks/shared/e2e-check.js +0 -0
  28. package/framework/hooks/shared/git-paths.js +25 -0
  29. package/framework/hooks/shared/hook-response.js +17 -0
  30. package/framework/hooks/shared/morph-cli.js +134 -0
  31. package/framework/hooks/shared/stdin-reader.js +26 -26
  32. package/framework/rules/infrastructure-standards.md +3 -1
  33. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  34. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  35. package/framework/skills/core/morph-apply/SKILL.md +3 -3
  36. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  37. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  38. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  39. package/framework/skills/core/morph-implement/SKILL.md +3 -1
  40. package/framework/skills/core/morph-plan/SKILL.md +6 -3
  41. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  42. package/framework/skills/core/morph-review/SKILL.md +1 -1
  43. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  44. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  45. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  46. package/framework/standards/STANDARDS.json +30 -0
  47. package/framework/standards/ai-agents/agent-session.md +160 -160
  48. package/framework/standards/ai-agents/batching.md +193 -193
  49. package/framework/standards/ai-agents/context-providers.md +188 -188
  50. package/framework/standards/ai-agents/mcp-server.md +250 -250
  51. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  52. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  53. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  54. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  55. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  56. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  57. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  58. package/framework/standards/ai-agents/production.md +231 -231
  59. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  60. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  61. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  62. package/framework/standards/ai-agents/structured-output.md +249 -249
  63. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  64. package/framework/standards/infrastructure/docker/local-compose-isolation.md +88 -0
  65. package/framework/templates/IDE-SNIPPETS.md +262 -262
  66. package/framework/templates/REGISTRY.json +1 -1
  67. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  68. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  69. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  70. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  71. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  72. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  73. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  74. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  75. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  76. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  77. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  78. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  79. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  80. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  81. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  82. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  83. package/framework/templates/code/typescript/contracts.ts +167 -167
  84. package/framework/templates/docs/clarifications.md +253 -253
  85. package/framework/templates/docs/schema-analysis.md +119 -119
  86. package/framework/templates/docs/ui-components.md +52 -52
  87. package/framework/templates/docs/ui-design-system.md +76 -76
  88. package/framework/templates/docs/ui-flows.md +167 -167
  89. package/framework/templates/docs/ui-mockups.md +98 -98
  90. package/framework/templates/feature/decisions.md +122 -122
  91. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  92. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  93. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  94. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  95. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  96. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
  100. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  101. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  102. package/framework/templates/infrastructure/github/README.md +56 -56
  103. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  104. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  105. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  106. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  107. package/framework/templates/integrations/asaas-client.cs +387 -387
  108. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  109. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  110. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  111. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  112. package/framework/templates/project/validate.js +120 -120
  113. package/framework/templates/state.template.json +17 -17
  114. package/framework/templates/ui/design-system.css +226 -226
  115. package/package.json +1 -1
  116. package/src/commands/dashboard/dashboard.js +53 -53
  117. package/src/commands/e2e/e2e.js +77 -39
  118. package/src/commands/index.js +13 -13
  119. package/src/commands/project/checkpoint.js +209 -209
  120. package/src/commands/project/doctor.js +8 -6
  121. package/src/commands/project/env.js +95 -0
  122. package/src/commands/project/worktree.js +266 -52
  123. package/src/commands/state/delete.js +8 -0
  124. package/src/commands/state/finish.js +29 -34
  125. package/src/core/state/phase-state-machine.js +197 -197
  126. package/src/core/templates/template-renderer.js +477 -477
  127. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  128. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  129. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  130. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  131. package/src/dashboard/app/main.jsx +13 -13
  132. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  133. package/src/dashboard/app/pages/HookInspector.jsx +430 -418
  134. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  135. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  136. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  137. package/src/dashboard/app/pages/Standards.jsx +332 -332
  138. package/src/dashboard/app/styles/index.css +47 -47
  139. package/src/dashboard/index.html +12 -12
  140. package/src/dashboard/package.json +24 -24
  141. package/src/dashboard/scanner/hooks-scanner.js +5 -1
  142. package/src/dashboard/scanner/phases-scanner.js +15 -15
  143. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  144. package/src/dashboard/scanner/rules-scanner.js +37 -37
  145. package/src/dashboard/scanner/skills-scanner.js +43 -43
  146. package/src/dashboard/scanner/squads-scanner.js +13 -13
  147. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  148. package/src/dashboard/vite.config.js +24 -24
  149. package/src/lib/detectors/design-system-detector.js +188 -188
  150. package/src/lib/detectors/index.js +217 -217
  151. package/src/lib/e2e/compose-env.js +6 -11
  152. package/src/lib/e2e/stack-teardown.js +115 -0
  153. package/src/lib/generators/architecture-scanner.js +2 -1
  154. package/src/lib/generators/design-system-generator.js +508 -508
  155. package/src/lib/generators/settings-generator.js +209 -209
  156. package/src/lib/graph/graphifyignore.js +2 -1
  157. package/src/lib/hooks/hook-executor.js +167 -167
  158. package/src/lib/installers/mcp-installer.js +719 -719
  159. package/src/lib/tasks/task-classifier.js +30 -30
  160. package/src/lib/tasks/task-parser.js +209 -209
  161. package/src/lib/tasks/test-runner.js +21 -4
  162. package/src/lib/validators/checkpoint-persistence.js +39 -39
  163. package/src/lib/validators/css/css-validator.js +340 -340
  164. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  165. package/src/lib/validators/shared/issue-counter.js +14 -14
  166. package/src/lib/validators/verify-runner.js +7 -0
  167. package/src/lib/worktree/dirs.js +32 -0
  168. package/src/lib/worktree/env-allocator.js +45 -11
  169. package/src/lib/worktree/worktree-audit.js +40 -2
  170. package/src/utils/agent-id-mapper.js +11 -11
  171. package/src/utils/claude-settings-manager.js +300 -300
  172. package/src/utils/file-copier.js +18 -13
  173. package/src/utils/hooks-installer.js +46 -3
  174. package/src/utils/skills-installer.js +187 -187
  175. package/framework/templates/.idea/morph-templates.xml +0 -92
  176. package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
@@ -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
+ }
@@ -586,11 +586,13 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
586
586
  }
587
587
 
588
588
  const issues = auditWorktrees(targetPath);
589
- // `severity: 'info'` entries (a worktree carrying unintegrated work) are
590
- // REPORTED but are not failures — a `doctor --worktrees || ...` in CI must not
591
- // go red because somebody has an open PR. The human path below splits the same
592
- // way; both surfaces must agree on what counts as broken.
593
- const isProblem = (i) => i.severity !== 'info';
589
+ // `severity: 'info'` entries (a worktree carrying unintegrated work) and
590
+ // `severity: 'warn'` entries (a worktree created outside morph's container)
591
+ // are REPORTED but are not failures — a `doctor --worktrees || ...` in CI must
592
+ // not go red because somebody has an open PR or a hand-made tree. The human
593
+ // path below splits the same way; both surfaces must agree on what counts as
594
+ // broken.
595
+ const isProblem = (i) => !['info', 'warn'].includes(i.severity);
594
596
 
595
597
  if (options.json) {
596
598
  console.log(JSON.stringify({ issues }, null, 2));
@@ -617,7 +619,7 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
617
619
  console.log(` → ${i.fix}`);
618
620
  }
619
621
  for (const i of informational) {
620
- console.log(` ℹ ${i.type}`);
622
+ console.log(i.severity === 'warn' ? chalk.yellow(` ⚠ ${i.type}`) : ` ℹ ${i.type}`);
621
623
  console.log(` ${i.detail}`);
622
624
  console.log(` → ${i.fix}`);
623
625
  }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `morph-spec env [feature] [--json|--posix|--powershell]` — print the
3
+ * dedicated environment of a worktree (port block + compose project) in a
4
+ * shell-loadable form.
5
+ *
6
+ * Inside Claude Code the `worktree-env-inject` hook applies this block to
7
+ * every dev-server / compose launcher automatically. This command is the same
8
+ * contract for everything OUTSIDE the harness: a human terminal
9
+ * (`eval "$(morph-spec env)"`), a CI step, a sub-agent running without hooks.
10
+ * It derives from `buildComposeEnv` — the function the `verify` e2e node and
11
+ * `e2e up` already use — so a shell and the stack can never disagree.
12
+ *
13
+ * Feature resolution: explicit argument, else the `morph/{f}` (or
14
+ * `morph-task/{n}`) branch of the cwd. Read-only: allocation is idempotent and
15
+ * never moves an existing block.
16
+ */
17
+
18
+ import { execFileSync } from 'child_process';
19
+ import { readFileSync } from 'fs';
20
+ import { join, resolve } from 'path';
21
+ import chalk from 'chalk';
22
+ import { buildComposeEnv } from '../../lib/e2e/compose-env.js';
23
+ import { resolvePrimaryRoot } from './worktree.js';
24
+ import { currentBranch } from '../../lib/worktree/feature-truth.js';
25
+
26
+ /** `morph/<f>` → `<f>`; `morph-task/<n>` → `task/<n>`; anything else → null. */
27
+ export function envKeyFromBranch(branch) {
28
+ if (!branch) return null;
29
+ const feature = branch.match(/^morph\/(.+)$/)?.[1];
30
+ if (feature) return feature;
31
+ const task = branch.match(/^morph-task\/(.+)$/)?.[1];
32
+ return task ? `task/${task}` : null;
33
+ }
34
+
35
+ function readConfig(root) {
36
+ try {
37
+ return JSON.parse(readFileSync(join(root, '.morph', 'config', 'config.json'), 'utf8').replace(/^/, ''));
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function gitCommonDir(cwd) {
44
+ try {
45
+ const out = execFileSync('git', ['rev-parse', '--git-common-dir'], { cwd, stdio: 'pipe' }).toString().trim();
46
+ return out ? resolve(cwd, out) : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ /** POSIX single-quote. */
53
+ const shq = (v) => `'${String(v).replace(/'/g, "'\\''")}'`;
54
+
55
+ /**
56
+ * Pure: the env map for a key, or `{ error }`.
57
+ * @param {string|null} featureArg
58
+ * @param {string} cwd
59
+ */
60
+ export function resolveWorktreeEnv(featureArg, cwd = process.cwd()) {
61
+ const key = featureArg || envKeyFromBranch(currentBranch(cwd));
62
+ if (!key) {
63
+ return { error: 'no-feature', message: 'Nenhuma feature: passe o nome ou rode de uma branch morph/{f} / morph-task/{n}.' };
64
+ }
65
+ const common = gitCommonDir(cwd);
66
+ if (!common) {
67
+ return { error: 'not-a-git-repo', message: 'Fora de um repositório git não há bloco de portas.' };
68
+ }
69
+ const root = resolvePrimaryRoot(cwd);
70
+ const env = buildComposeEnv({ cwd: root, config: readConfig(root), featureName: key, gitCommonDir: common });
71
+ return { key, env };
72
+ }
73
+
74
+ export function render(env, format) {
75
+ const entries = Object.entries(env);
76
+ if (format === 'json') return JSON.stringify(env, null, 2);
77
+ if (format === 'powershell') return entries.map(([k, v]) => `$env:${k}="${String(v).replace(/"/g, '`"')}"`).join('\n');
78
+ return entries.map(([k, v]) => `export ${k}=${shq(v)}`).join('\n');
79
+ }
80
+
81
+ /**
82
+ * @param {string|undefined} featureArg
83
+ * @param {{ json?: boolean, posix?: boolean, powershell?: boolean }} options
84
+ */
85
+ export function envCommand(featureArg, options = {}) {
86
+ const result = resolveWorktreeEnv(featureArg || null, process.cwd());
87
+ if (result.error) {
88
+ if (options.json) console.log(JSON.stringify({ error: result.error, message: result.message }));
89
+ else console.error(chalk.red(`✗ ${result.message}`));
90
+ process.exit(1);
91
+ }
92
+ const format = options.json ? 'json' : options.powershell ? 'powershell' : 'posix';
93
+ console.log(render(result.env, format));
94
+ process.exit(0);
95
+ }