@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.
- package/CLAUDE.md +11 -1
- package/README.md +13 -2
- package/bin/morph-spec.js +67 -1
- package/bin/validate.js +379 -379
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +4 -1
- package/framework/MORPH.md +4 -3
- package/framework/agents.json +6 -6
- package/framework/commands/morph-status.md +1 -1
- package/framework/docs/CLI.md +68 -45
- package/framework/evals/rubrics/code-quality.md +17 -0
- package/framework/evals/rubrics/condensed.md +3 -1
- 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/phase-utils.js +2 -0
- package/framework/hooks/shared/stdin-reader.js +26 -26
- package/framework/mcp/registry.json +39 -4
- package/framework/rubrics/backend.json +2 -2
- package/framework/rubrics/default.json +2 -2
- package/framework/rubrics/frontend.json +2 -2
- package/framework/schemas/tasks.schema.json +25 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +14 -4
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +12 -2
- package/framework/skills/core/morph-plan/SKILL.md +45 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +12 -4
- package/framework/skills/core/morph-stack-scan/SKILL.md +13 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/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 +231 -231
- package/framework/standards/ai-agents/prompts-in-db.md +214 -214
- package/framework/standards/ai-agents/providers/model-registry.md +158 -158
- package/framework/standards/ai-agents/service-tier-flex.md +135 -135
- package/framework/standards/ai-agents/structured-output.md +249 -249
- package/framework/standards/ai-agents/sweet-spot.md +109 -109
- package/framework/templates/.idea/morph-templates.xml +92 -0
- package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
- package/framework/templates/IDE-SNIPPETS.md +262 -262
- package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
- package/framework/templates/code/dotnet/database/migration.cs +83 -83
- package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
- package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
- package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
- package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
- package/framework/templates/code/typescript/contracts.ts +167 -167
- package/framework/templates/context/ARCHITECTURE.md +20 -0
- 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/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/graph/affected.js +89 -0
- package/src/commands/graph/build.js +94 -0
- package/src/commands/graph/explain.js +96 -0
- package/src/commands/graph/status.js +93 -0
- package/src/commands/index.js +13 -13
- package/src/commands/project/branch.js +170 -0
- package/src/commands/project/checkpoint.js +209 -209
- package/src/commands/project/doctor.js +148 -9
- package/src/commands/project/fleet.js +10 -3
- package/src/commands/project/worktree.js +73 -50
- package/src/commands/state/finish.js +41 -26
- package/src/commands/state/list.js +79 -6
- package/src/core/paths/output-schema.js +7 -0
- package/src/core/state/lifecycle-audit.js +127 -0
- package/src/core/state/phase-state-machine.js +197 -197
- package/src/core/templates/template-renderer.js +477 -477
- package/src/dashboard/app/components/DetailPanel.jsx +11 -11
- package/src/dashboard/app/components/FlowGraph.jsx +17 -17
- package/src/dashboard/app/components/SearchFilter.jsx +22 -22
- package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
- package/src/dashboard/app/main.jsx +13 -13
- package/src/dashboard/app/pages/AgentMap.jsx +370 -370
- package/src/dashboard/app/pages/HookInspector.jsx +418 -418
- package/src/dashboard/app/pages/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/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/vite-plugin-morph-data.js +17 -17
- package/src/dashboard/vite.config.js +24 -24
- package/src/lib/detectors/design-system-detector.js +188 -188
- package/src/lib/detectors/index.js +217 -217
- package/src/lib/generators/architecture-scanner.js +45 -4
- package/src/lib/generators/design-system-generator.js +508 -508
- package/src/lib/generators/settings-generator.js +209 -209
- package/src/lib/git/branch-classifier.js +177 -0
- package/src/lib/git/gh.js +30 -0
- package/src/lib/git/pending-prs.js +89 -0
- package/src/lib/git/pr-index.js +70 -0
- package/src/lib/graph/god-nodes.js +75 -0
- package/src/lib/graph/graph-health.js +61 -0
- package/src/lib/graph/graph-loader.js +86 -0
- package/src/lib/graph/graphify-runtime.js +100 -0
- package/src/lib/graph/graphifyignore.js +86 -0
- package/src/lib/graph/sanitize.js +280 -0
- package/src/lib/hooks/hook-executor.js +167 -167
- package/src/lib/installers/mcp-installer.js +719 -719
- package/src/lib/tasks/task-classifier.js +30 -30
- package/src/lib/tasks/task-parser.js +209 -209
- package/src/lib/tasks/tasks-json-parser.js +26 -0
- package/src/lib/validators/checkpoint-persistence.js +39 -39
- package/src/lib/validators/css/css-validator.js +340 -340
- package/src/lib/validators/reuse/index.js +10 -0
- package/src/lib/validators/reuse/reuse-validator.js +324 -0
- package/src/lib/validators/reuse/symbol-scanner.js +144 -0
- 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 +40 -1
- package/src/lib/worktree/fleet-scanner.js +51 -21
- package/src/lib/worktree/ignored-triage.js +197 -0
- package/src/lib/worktree/worktree-audit.js +200 -35
- package/src/scripts/setup-infra.js +60 -1
- package/src/utils/agent-id-mapper.js +11 -11
- package/src/utils/claude-settings-manager.js +300 -300
- package/src/utils/file-copier.js +40 -0
- 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
|
|
559
|
-
* directories that are already gone)
|
|
560
|
-
*
|
|
561
|
-
*
|
|
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.
|
|
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
|
-
|
|
592
|
-
|
|
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
|
-
|
|
598
|
-
|
|
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) {
|