@polymorphism-tech/morph-spec 8.29.0 → 8.32.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.
Files changed (183) hide show
  1. package/CLAUDE.md +11 -1
  2. package/README.md +13 -2
  3. package/bin/morph-spec.js +67 -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 +4 -1
  9. package/framework/MORPH.md +4 -3
  10. package/framework/agents.json +6 -6
  11. package/framework/commands/morph-status.md +1 -1
  12. package/framework/docs/CLI.md +68 -45
  13. package/framework/evals/rubrics/code-quality.md +17 -0
  14. package/framework/evals/rubrics/condensed.md +3 -1
  15. package/framework/hooks/claude-code/statusline.sh +11 -11
  16. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  17. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  18. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  19. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  20. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  21. package/framework/hooks/shared/phase-utils.js +2 -0
  22. package/framework/hooks/shared/stdin-reader.js +26 -26
  23. package/framework/mcp/registry.json +39 -4
  24. package/framework/rubrics/backend.json +2 -2
  25. package/framework/rubrics/default.json +2 -2
  26. package/framework/rubrics/frontend.json +2 -2
  27. package/framework/schemas/tasks.schema.json +25 -0
  28. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  29. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  30. package/framework/skills/core/morph-apply/SKILL.md +14 -4
  31. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  32. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  33. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  34. package/framework/skills/core/morph-implement/SKILL.md +12 -2
  35. package/framework/skills/core/morph-plan/SKILL.md +45 -3
  36. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  37. package/framework/skills/core/morph-review/SKILL.md +12 -4
  38. package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
  39. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  40. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  41. package/framework/standards/ai-agents/agent-session.md +160 -160
  42. package/framework/standards/ai-agents/batching.md +193 -193
  43. package/framework/standards/ai-agents/context-providers.md +188 -188
  44. package/framework/standards/ai-agents/mcp-server.md +250 -250
  45. package/framework/standards/ai-agents/mcp-tools.md +197 -197
  46. package/framework/standards/ai-agents/middleware-patterns.md +207 -207
  47. package/framework/standards/ai-agents/modalities-image-gen.md +173 -173
  48. package/framework/standards/ai-agents/modalities-speech-to-text.md +171 -171
  49. package/framework/standards/ai-agents/modalities-text-to-speech.md +181 -181
  50. package/framework/standards/ai-agents/modalities-vision.md +201 -201
  51. package/framework/standards/ai-agents/multi-agent-patterns.md +155 -155
  52. package/framework/standards/ai-agents/production.md +231 -231
  53. package/framework/standards/ai-agents/prompts-in-db.md +214 -214
  54. package/framework/standards/ai-agents/providers/model-registry.md +158 -158
  55. package/framework/standards/ai-agents/service-tier-flex.md +135 -135
  56. package/framework/standards/ai-agents/structured-output.md +249 -249
  57. package/framework/standards/ai-agents/sweet-spot.md +109 -109
  58. package/framework/templates/.idea/morph-templates.xml +92 -0
  59. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  60. package/framework/templates/IDE-SNIPPETS.md +262 -262
  61. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  62. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  63. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  64. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  65. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  66. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  67. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  68. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  69. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  70. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  71. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  72. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  73. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  74. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  75. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  76. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  77. package/framework/templates/code/typescript/contracts.ts +167 -167
  78. package/framework/templates/context/ARCHITECTURE.md +20 -0
  79. package/framework/templates/docs/clarifications.md +253 -253
  80. package/framework/templates/docs/schema-analysis.md +119 -119
  81. package/framework/templates/docs/ui-components.md +52 -52
  82. package/framework/templates/docs/ui-design-system.md +76 -76
  83. package/framework/templates/docs/ui-flows.md +167 -167
  84. package/framework/templates/docs/ui-mockups.md +98 -98
  85. package/framework/templates/feature/decisions.md +122 -122
  86. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  87. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  88. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  89. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  90. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  91. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  92. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  93. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  94. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  95. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  96. package/framework/templates/infrastructure/github/README.md +56 -56
  97. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  98. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  99. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  100. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  101. package/framework/templates/integrations/asaas-client.cs +387 -387
  102. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  103. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  104. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  105. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  106. package/framework/templates/project/validate.js +120 -120
  107. package/framework/templates/state.template.json +17 -17
  108. package/framework/templates/ui/design-system.css +226 -226
  109. package/package.json +1 -1
  110. package/src/commands/dashboard/dashboard.js +53 -53
  111. package/src/commands/graph/affected.js +89 -0
  112. package/src/commands/graph/build.js +94 -0
  113. package/src/commands/graph/explain.js +96 -0
  114. package/src/commands/graph/status.js +93 -0
  115. package/src/commands/index.js +13 -13
  116. package/src/commands/project/branch.js +170 -0
  117. package/src/commands/project/checkpoint.js +209 -209
  118. package/src/commands/project/doctor.js +148 -9
  119. package/src/commands/project/fleet.js +10 -3
  120. package/src/commands/project/worktree.js +73 -50
  121. package/src/commands/state/finish.js +41 -26
  122. package/src/commands/state/list.js +79 -6
  123. package/src/core/paths/output-schema.js +7 -0
  124. package/src/core/state/lifecycle-audit.js +127 -0
  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 +418 -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/phases-scanner.js +15 -15
  142. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  143. package/src/dashboard/scanner/rules-scanner.js +37 -37
  144. package/src/dashboard/scanner/skills-scanner.js +43 -43
  145. package/src/dashboard/scanner/squads-scanner.js +13 -13
  146. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  147. package/src/dashboard/vite.config.js +24 -24
  148. package/src/lib/detectors/design-system-detector.js +188 -188
  149. package/src/lib/detectors/index.js +217 -217
  150. package/src/lib/generators/architecture-scanner.js +45 -4
  151. package/src/lib/generators/design-system-generator.js +508 -508
  152. package/src/lib/generators/settings-generator.js +209 -209
  153. package/src/lib/git/branch-classifier.js +177 -0
  154. package/src/lib/git/gh.js +30 -0
  155. package/src/lib/git/pending-prs.js +89 -0
  156. package/src/lib/git/pr-index.js +70 -0
  157. package/src/lib/graph/god-nodes.js +75 -0
  158. package/src/lib/graph/graph-health.js +61 -0
  159. package/src/lib/graph/graph-loader.js +86 -0
  160. package/src/lib/graph/graphify-runtime.js +100 -0
  161. package/src/lib/graph/graphifyignore.js +86 -0
  162. package/src/lib/graph/sanitize.js +280 -0
  163. package/src/lib/hooks/hook-executor.js +167 -167
  164. package/src/lib/installers/mcp-installer.js +719 -719
  165. package/src/lib/tasks/task-classifier.js +30 -30
  166. package/src/lib/tasks/task-parser.js +209 -209
  167. package/src/lib/tasks/tasks-json-parser.js +26 -0
  168. package/src/lib/validators/checkpoint-persistence.js +39 -39
  169. package/src/lib/validators/css/css-validator.js +340 -340
  170. package/src/lib/validators/reuse/index.js +10 -0
  171. package/src/lib/validators/reuse/reuse-validator.js +324 -0
  172. package/src/lib/validators/reuse/symbol-scanner.js +144 -0
  173. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  174. package/src/lib/validators/shared/issue-counter.js +14 -14
  175. package/src/lib/validators/validation-runner.js +40 -1
  176. package/src/lib/worktree/fleet-scanner.js +51 -21
  177. package/src/lib/worktree/ignored-triage.js +197 -0
  178. package/src/lib/worktree/worktree-audit.js +200 -35
  179. package/src/scripts/setup-infra.js +60 -1
  180. package/src/utils/agent-id-mapper.js +11 -11
  181. package/src/utils/claude-settings-manager.js +300 -300
  182. package/src/utils/file-copier.js +40 -0
  183. package/src/utils/skills-installer.js +187 -187
@@ -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
+ }
@@ -1,11 +1,14 @@
1
1
  import { join, relative } from 'path';
2
2
  import { execSync } from 'child_process';
3
+ import { existsSync } from 'fs';
3
4
  import { platform, homedir } from 'os';
4
5
  import fs from 'fs-extra';
5
6
  import chalk from 'chalk';
6
7
  import { logger } from '../../utils/logger.js';
7
8
  import { pathExists, readJson } from '../../utils/file-copier.js';
8
9
  import { checkClaudeMdImport } from '../../utils/claude-md-injector.js';
10
+ import { buildPrIndex } from '../../lib/git/pr-index.js';
11
+ import { pendingPrFeatures, MORPH_BRANCH_RE } from '../../lib/git/pending-prs.js';
9
12
  import {
10
13
  checkCLIOutdated,
11
14
  checkProjectOutdated,
@@ -14,6 +17,7 @@ import {
14
17
  import { resetMorphSettings } from '../../utils/claude-settings-manager.js';
15
18
  import { checkMissingAgents, checkAgentsDrift } from '../../utils/agents-installer.js';
16
19
  import { getProjectTags } from '../../lib/stack-filter.js';
20
+ import { auditGraph } from '../../lib/graph/graph-health.js';
17
21
  import { auditFeatureLifecycle } from '../../core/state/lifecycle-audit.js';
18
22
 
19
23
  const isWindows = platform() === 'win32';
@@ -555,10 +559,12 @@ async function doctorMcpCommand(targetPath) {
555
559
  * Orphan worktrees used to accumulate invisibly: `fleet` only reports worktrees
556
560
  * on a `morph/*` branch, so anything else never showed up anywhere.
557
561
  *
558
- * `--prune` runs the one safe repair (`git worktree prune`: admin records for
559
- * directories that are already gone). Deleting a directory that still exists is
560
- * reported with the exact command, never performed — same PROVE-or-DECLINE
561
- * discipline as the finish teardown.
562
+ * `--prune` runs the safe repairs: `git worktree prune` (admin records for
563
+ * directories that are already gone) and the removal of EMPTY unregistered
564
+ * residue directories, which by definition hold nothing that can be lost.
565
+ * Deleting a directory that still HOLDS something is reported with the exact
566
+ * command, never performed — same PROVE-or-DECLINE discipline as the finish
567
+ * teardown.
562
568
  */
563
569
  export async function doctorWorktreesCommand(targetPath, options = {}) {
564
570
  const { auditWorktrees, pruneWorktrees } = await import('../../lib/worktree/worktree-audit.js');
@@ -567,19 +573,28 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
567
573
  const beforeIssues = auditWorktrees(targetPath);
568
574
  const beforeMissing = beforeIssues.filter((i) => i.type === 'registered-missing').length;
569
575
  const beforeDangling = beforeIssues.filter((i) => i.type === 'dangling-registration').length;
576
+ const beforeEmptyDirs = beforeIssues.filter((i) => i.type === 'unregistered-dir' && i.empty).length;
570
577
  const result = await pruneWorktrees(targetPath);
571
578
  if (result.pruned) logger.success(`git worktree prune: ${beforeMissing} registro(s) órfão(s) removido(s).`);
572
579
  else logger.warn(`prune falhou: ${result.error}`);
580
+ if (result.removedEmptyDirs.length) {
581
+ logger.success(`${result.removedEmptyDirs.length}/${beforeEmptyDirs} diretório(s) vazio(s) removido(s): ${result.removedEmptyDirs.join(', ')}`);
582
+ }
573
583
  if (result.clearedRegistrations.length) {
574
584
  logger.success(`registro de worktree limpo em ${result.clearedRegistrations.length}/${beforeDangling} feature(s): ${result.clearedRegistrations.map((r) => r.feature).join(', ')}`);
575
585
  }
576
586
  }
577
587
 
578
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';
579
594
 
580
595
  if (options.json) {
581
596
  console.log(JSON.stringify({ issues }, null, 2));
582
- return issues.length === 0;
597
+ return !issues.some(isProblem);
583
598
  }
584
599
 
585
600
  logger.header('MORPH-SPEC Worktrees');
@@ -588,18 +603,90 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
588
603
  return true;
589
604
  }
590
605
 
591
- logger.warn(`${issues.length} inconsistência(s):`);
592
- for (const i of issues) {
606
+ // `unintegrated-work` is NOT an inconsistency — it is a worktree doing its
607
+ // job, reported so the reader knows why no cleanup is offered for it. Counting
608
+ // it as a problem is how a diagnostic starts nagging people into destroying
609
+ // things to make the warning go away.
610
+ const informational = issues.filter((i) => !isProblem(i));
611
+ const problems = issues.filter(isProblem);
612
+
613
+ if (problems.length) logger.warn(`${problems.length} inconsistência(s):`);
614
+ for (const i of problems) {
593
615
  console.log(` ⚠ ${i.type}`);
594
616
  console.log(` ${i.detail}`);
595
617
  console.log(` → ${i.fix}`);
596
618
  }
597
- if (issues.some((i) => i.type === 'registered-missing' || i.type === 'dangling-registration') && !options.prune) {
598
- logger.dim('\n Rode `morph-spec doctor --worktrees --prune` para limpar os registros órfãos.');
619
+ for (const i of informational) {
620
+ console.log(` ℹ ${i.type}`);
621
+ console.log(` ${i.detail}`);
622
+ console.log(` → ${i.fix}`);
623
+ }
624
+ // An EMPTY unregistered directory is prunable too — it holds nothing that can
625
+ // be lost — so the hint must mention it, or the one issue `--prune` now fixes
626
+ // automatically would still read as a manual chore.
627
+ const prunable = (i) => i.type === 'registered-missing'
628
+ || i.type === 'dangling-registration'
629
+ || (i.type === 'unregistered-dir' && i.empty);
630
+ if (problems.some(prunable) && !options.prune) {
631
+ logger.dim('\n Rode `morph-spec doctor --worktrees --prune` para limpar os registros órfãos e os diretórios vazios.');
632
+ }
633
+ if (!problems.length) {
634
+ logger.success('Nenhuma inconsistência — os itens acima são informativos.');
635
+ return true;
599
636
  }
600
637
  return false;
601
638
  }
602
639
 
640
+ /** Dias a partir dos quais um PR aberto vira aviso. */
641
+ const PR_STALE_DAYS = 7;
642
+
643
+ /**
644
+ * As inconsistências de PR desta árvore.
645
+ *
646
+ * pr-awaiting-merge — PR aberto há muito, sem cópia local: trabalho em
647
+ * revisão que nenhuma superfície local reivindica.
648
+ * pr-merged-not-landed — o PR mergeou mas `archive/{f}` não está aqui. O
649
+ * archive viaja no diff do PR, então ou falta um
650
+ * `git pull`, ou ele não estava no diff.
651
+ *
652
+ * Offline-safe: índice `null` devolve `null`, NUNCA lista vazia — lista vazia
653
+ * afirmaria "perguntei e não achei nada", que é diferente de "não pude
654
+ * perguntar". Achado #3: um `[]` disfarçado de "sem problema" é o que fazia
655
+ * `doctor` imprimir `✓ pull requests` mesmo sem conseguir consultar o `gh`.
656
+ *
657
+ * @returns {Array<{feature:string,type:string,detail:string}>|null}
658
+ * null = DESCONHECIDO (índice não pôde ser construído); nunca "sem PRs".
659
+ */
660
+ function auditPullRequests(root) {
661
+ const index = buildPrIndex(root);
662
+ if (!index) return null;
663
+ const issues = [];
664
+
665
+ for (const p of pendingPrFeatures(root, { index }) || []) {
666
+ if (p.ageDays != null && p.ageDays >= PR_STALE_DAYS) {
667
+ issues.push({
668
+ feature: p.feature,
669
+ type: 'pr-awaiting-merge',
670
+ detail: `PR #${p.number} aberto há ${p.ageDays}d e sem cópia local — mergeie ou feche`,
671
+ });
672
+ }
673
+ }
674
+
675
+ for (const [branch, pr] of index) {
676
+ const m = branch.match(MORPH_BRANCH_RE);
677
+ if (!m || pr.state !== 'MERGED') continue;
678
+ const feature = m[1];
679
+ if (existsSync(join(root, '.morph', 'archive', feature))) continue;
680
+ issues.push({
681
+ feature,
682
+ type: 'pr-merged-not-landed',
683
+ detail: `PR #${pr.number} foi mergeado mas .morph/archive/${feature} não está aqui — falta \`git pull\`?`,
684
+ });
685
+ }
686
+
687
+ return issues;
688
+ }
689
+
603
690
  export async function doctorCommand(options = {}) {
604
691
  // `--json` and `--prune` only mean something under `--worktrees`. Falling
605
692
  // through to the human health check would hand a script a banner and a table
@@ -1074,6 +1161,42 @@ export async function doctorCommand(options = {}) {
1074
1161
  hasWarnings = true;
1075
1162
  }
1076
1163
 
1164
+ // Pull requests — o pendurado (aberto há dias sem cópia local) e o mergeado
1165
+ // que não pousou (archive não chegou nesta árvore). Offline-safe: sem `gh`,
1166
+ // sem origin no GitHub, ou qualquer falha, `auditPullRequests` nunca lança
1167
+ // (achado #7 — mesma guarda fail-open que `fleet-scanner.js` já usa para a
1168
+ // síntese idêntica) e devolve `null` — achado #3: um índice desconhecido
1169
+ // renderiza como "não verificado", nunca como `✓ ok`, porque `[]` e
1170
+ // "não pude perguntar" não são a mesma coisa.
1171
+ let prIssues;
1172
+ try {
1173
+ prIssues = auditPullRequests(targetPath);
1174
+ } catch {
1175
+ prIssues = null; // fail-open — um doctor não pode ficar mais ruidoso porque o gh sumiu
1176
+ }
1177
+ if (prIssues === null) {
1178
+ checks.push({ name: 'pull requests', status: 'skip', msg: 'gh indisponível — não verificado' });
1179
+ } else if (prIssues.length === 0) {
1180
+ checks.push({ name: 'pull requests', status: 'ok' });
1181
+ } else {
1182
+ checks.push({
1183
+ name: 'pull requests',
1184
+ status: 'warn',
1185
+ msg: `${prIssues.length} pendência${prIssues.length === 1 ? '' : 's'} (veja abaixo)`,
1186
+ });
1187
+ hasWarnings = true;
1188
+ }
1189
+
1190
+ // Codebase graph — opcional (ADR-002). Nunca 'missing': um projeto que não
1191
+ // usa o grafo tem de continuar com o doctor verde.
1192
+ const graphAudit = auditGraph(targetPath);
1193
+ if (graphAudit.status === 'warn') {
1194
+ checks.push({ name: 'codebase graph', status: 'warn', msg: graphAudit.msg });
1195
+ hasWarnings = true;
1196
+ } else {
1197
+ checks.push({ name: 'codebase graph', status: 'ok', msg: graphAudit.msg });
1198
+ }
1199
+
1077
1200
  // Check key templates exist (in framework package, not project copy)
1078
1201
  const frameworkTemplatesRoot = join(import.meta.dirname, '..', '..', '..', 'framework', 'templates');
1079
1202
  const keyTemplates = [
@@ -1119,6 +1242,11 @@ export async function doctorCommand(options = {}) {
1119
1242
  console.log(chalk.red(` ✗ ${check.name}`) + chalk.gray(' (missing)'));
1120
1243
  } else if (check.status === 'error') {
1121
1244
  console.log(chalk.red(` ✗ ${check.name}`) + chalk.gray(` (${check.msg})`));
1245
+ } else if (check.status === 'skip') {
1246
+ // "Não verificado" — nem ok nem falha. Nunca soma em hasWarnings/hasErrors:
1247
+ // um `gh` indisponível não pode deixar o doctor mais barulhento nem mudar
1248
+ // o exit code (achado #3).
1249
+ console.log(chalk.gray(` ○ ${check.name}`) + chalk.gray(check.msg ? ` (${check.msg})` : ''));
1122
1250
  }
1123
1251
  }
1124
1252
 
@@ -1133,6 +1261,17 @@ export async function doctorCommand(options = {}) {
1133
1261
  console.log(chalk.gray(" Fix: 'morph-spec approve' (order), '--description' on create, or 'morph-spec delete' for dead scaffolds."));
1134
1262
  }
1135
1263
 
1264
+ // Per-feature detail for the PR inconsistencies (item: pendurado ou merge
1265
+ // que não pousou).
1266
+ if (prIssues?.length) {
1267
+ logger.blank();
1268
+ console.log(chalk.yellow(' Pull requests pendentes:'));
1269
+ for (const issue of prIssues) {
1270
+ console.log(chalk.gray(` • ${issue.feature} [${issue.type}] — ${issue.detail}`));
1271
+ }
1272
+ console.log(chalk.gray(' `finish --pr` abre o PR; quem encerra a feature é o merge.'));
1273
+ }
1274
+
1136
1275
  logger.blank();
1137
1276
 
1138
1277
  if (hasErrors) {