@polymorphism-tech/morph-spec 8.42.0 → 8.44.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 +15 -5
- package/README.md +2 -2
- package/bin/morph-spec.js +48 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +5 -4
- package/framework/MORPH.md +10 -9
- package/framework/agents.json +1 -1
- package/framework/docs/CLI.md +22 -10
- package/framework/evals/proof-rubrics/mutation-proof.md +43 -13
- package/framework/hooks/claude-code/core/heavy-gate.js +72 -0
- package/framework/hooks/claude-code/core/heavy-release.js +34 -0
- package/framework/hooks/claude-code/core/worktree-context.js +21 -20
- package/framework/hooks/claude-code/core/worktree-create.js +4 -4
- package/framework/hooks/claude-code/core/worktree-env-inject.js +1 -4
- package/framework/hooks/shared/agent-reap.js +89 -0
- package/framework/hooks/shared/e2e-check.js +3 -1
- package/framework/hooks/shared/graph-refresh.js +1 -9
- package/framework/hooks/shared/heavy-command.js +128 -0
- package/framework/hooks/shared/heavy-config.js +65 -0
- package/framework/hooks/shared/heavy-holders.js +85 -0
- package/framework/hooks/shared/heavy-slots.js +207 -0
- package/framework/hooks/shared/process-alive.js +8 -0
- package/framework/hooks/shared/worktree-root.js +93 -0
- package/framework/rules/ai-agents-standards.md +1 -0
- package/framework/schemas/config.schema.json +80 -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 +8 -3
- 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 +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +9 -4
- package/framework/skills/core/morph-machine/SKILL.md +46 -0
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +45 -16
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/PROVENANCE.json +10 -10
- package/framework/standards/STANDARDS.json +45 -1
- package/framework/standards/ai-agents/tool-adapters.md +226 -0
- package/framework/standards/frontend/design-system/ai-video-generation.md +2 -0
- package/framework/standards/frontend/scroll-driven/frame-scrub.md +11 -1
- package/framework/standards/infrastructure/docker/local-compose-isolation.md +11 -1
- package/package.json +1 -1
- package/src/commands/e2e/e2e.js +27 -8
- package/src/commands/mcp/mcp-setup.js +10 -9
- package/src/commands/project/doctor-procs.js +162 -0
- package/src/commands/project/doctor.js +42 -4
- package/src/commands/project/heavy.js +61 -0
- package/src/commands/project/init.js +1 -1
- package/src/commands/project/update.js +4 -0
- package/src/commands/project/worktree-reap.js +34 -0
- package/src/commands/project/worktree.js +112 -43
- package/src/commands/state/approve.js +2 -1
- package/src/commands/state/finish.js +4 -0
- package/src/commands/state/gate-check.js +2 -1
- package/src/commands/validation/mutate.js +200 -0
- package/src/commands/validation/verify.js +39 -22
- package/src/dashboard/scanner/features-scanner.js +4 -4
- package/src/dashboard/scanner/project-api.js +4 -4
- package/src/lib/detectors/claude-config-detector.js +30 -1
- package/src/lib/e2e/compose-env.js +5 -2
- package/src/lib/git/evidence-staleness.js +39 -0
- package/src/lib/heavy/cleanup.js +70 -0
- package/src/lib/heavy/gate.js +96 -0
- package/src/lib/installers/mcp-installer.js +15 -2
- package/src/lib/mutation/adapters.js +90 -0
- package/src/lib/mutation/commands.js +60 -0
- package/src/lib/mutation/manifest.js +138 -0
- package/src/lib/mutation/plan.js +36 -0
- package/src/lib/mutation/report.js +66 -0
- package/src/lib/mutation/runner.js +351 -0
- package/src/lib/mutation/schemata.js +60 -0
- package/src/lib/mutation/snapshot.js +69 -0
- package/src/lib/mutation/subset.js +75 -0
- package/src/lib/mutation/trx.js +51 -0
- package/src/lib/procs/classify.js +91 -0
- package/src/lib/procs/list.js +124 -0
- package/src/lib/procs/mcp-config.js +103 -0
- package/src/lib/procs/prune.js +25 -0
- package/src/lib/procs/wslconfig.js +70 -0
- package/src/lib/projects/registry.js +6 -2
- package/src/lib/risk-detector.js +3 -3
- package/src/lib/tasks/eval-node.js +4 -34
- package/src/lib/tasks/linux-runner.js +30 -3
- package/src/lib/tasks/test-runner.js +1 -1
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -42
- package/src/lib/validators/verify-runner.js +26 -8
- package/src/lib/worktree/agent-reaper.js +247 -0
- package/src/lib/worktree/dirs.js +3 -32
- package/src/lib/worktree/env-allocator.js +35 -5
- package/src/lib/worktree/vscode-settings.js +113 -0
- package/src/lib/worktree/worktree-audit.js +49 -5
- package/src/lib/worktree/worktree-include.js +46 -0
- package/src/scripts/setup-infra.js +4 -0
- package/src/utils/hooks-installer.js +11 -4
package/src/commands/e2e/e2e.js
CHANGED
|
@@ -42,9 +42,13 @@ import {
|
|
|
42
42
|
import { isSecondaryWorktree } from '../../../framework/hooks/shared/git-paths.js';
|
|
43
43
|
import { buildComposeEnv } from '../../lib/e2e/compose-env.js';
|
|
44
44
|
import { findWorktreeForFeature, resolvePrimaryRoot } from '../project/worktree.js';
|
|
45
|
+
import { createHeavyGate, HeavyBusyError } from '../../lib/heavy/gate.js';
|
|
46
|
+
import { afterHeavyRun } from '../../lib/heavy/cleanup.js';
|
|
47
|
+
import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
|
|
45
48
|
|
|
46
49
|
/** Exit code for a usage error, matching `verify`'s convention. */
|
|
47
50
|
const EXIT_USAGE = 2;
|
|
51
|
+
const EXIT_BUSY = 3;
|
|
48
52
|
|
|
49
53
|
// ============================================================================
|
|
50
54
|
// Shared context
|
|
@@ -158,13 +162,27 @@ export async function e2eUpCommand(featureName, options = {}) {
|
|
|
158
162
|
process.exit(EXIT_USAGE);
|
|
159
163
|
}
|
|
160
164
|
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
const gate = createHeavyGate({ projectPath: ctx.cwd, label: 'e2e up', feature: featureName });
|
|
166
|
+
let result;
|
|
167
|
+
try {
|
|
168
|
+
gate.enter('e2e');
|
|
169
|
+
result = bringUpStack(ctx.cwd, {
|
|
170
|
+
composeFile: ctx.composeFile,
|
|
171
|
+
projectName: ctx.projectName,
|
|
172
|
+
gitCommonDir: ctx.gitCommonDir,
|
|
173
|
+
env: ctx.env,
|
|
174
|
+
forceFresh: Boolean(options.fresh),
|
|
175
|
+
});
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (!(err instanceof HeavyBusyError)) throw err;
|
|
178
|
+
if (options.json) {
|
|
179
|
+
console.log(JSON.stringify({ status: 'busy', feature: featureName, busy: { reason: err.reason, waitedMs: err.waitedMs } }, null, 2));
|
|
180
|
+
}
|
|
181
|
+
console.error(chalk.yellow(`\n⏳ e2e up não rodou: ${err.message}. Tente de novo quando liberar (\`morph-spec heavy status\`).\n`));
|
|
182
|
+
process.exit(EXIT_BUSY);
|
|
183
|
+
} finally {
|
|
184
|
+
gate.release();
|
|
185
|
+
}
|
|
168
186
|
|
|
169
187
|
const auth =
|
|
170
188
|
result.status === 'pass'
|
|
@@ -313,10 +331,11 @@ export async function e2eDownCommand(featureName, options = {}) {
|
|
|
313
331
|
volumes: Boolean(options.volumes),
|
|
314
332
|
});
|
|
315
333
|
const stampDeleted = deleteStamp(ctx.gitCommonDir, ctx.projectName);
|
|
334
|
+
const cleanup = afterHeavyRun({ projectPath: ctx.cwd, config: readHeavyConfig(ctx.cwd), scope: 'e2e-down' });
|
|
316
335
|
|
|
317
336
|
if (options.json) {
|
|
318
337
|
console.log(JSON.stringify(
|
|
319
|
-
{ feature: featureName, project: ctx.projectName, tree: ctx.tree, status: result.status, stampDeleted },
|
|
338
|
+
{ feature: featureName, project: ctx.projectName, tree: ctx.tree, status: result.status, stampDeleted, cleanup },
|
|
320
339
|
null, 2,
|
|
321
340
|
));
|
|
322
341
|
} else if (result.status === 'pass') {
|
|
@@ -100,13 +100,14 @@ export async function mcpSetupCommand(name, options) {
|
|
|
100
100
|
const integrations = await detectIntegrations(targetPath);
|
|
101
101
|
const claudeConfig = await detectClaudeConfig(targetPath);
|
|
102
102
|
const existingMcps = claudeConfig.mcpServers || [];
|
|
103
|
+
const enabledPlugins = claudeConfig.enabledPlugins || [];
|
|
103
104
|
|
|
104
105
|
if (options.list) {
|
|
105
|
-
return showStatusList(targetPath, stack, existingMcps, integrations);
|
|
106
|
+
return showStatusList(targetPath, stack, existingMcps, integrations, enabledPlugins);
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
if (options.auto) {
|
|
109
|
-
return autoInstall(targetPath, stack, existingMcps, integrations);
|
|
110
|
+
return autoInstall(targetPath, stack, existingMcps, integrations, enabledPlugins);
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
if (name) {
|
|
@@ -114,7 +115,7 @@ export async function mcpSetupCommand(name, options) {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
// Interactive wizard
|
|
117
|
-
return interactiveWizard(targetPath, stack, existingMcps, integrations);
|
|
118
|
+
return interactiveWizard(targetPath, stack, existingMcps, integrations, enabledPlugins);
|
|
118
119
|
} catch (err) {
|
|
119
120
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
120
121
|
process.exit(1);
|
|
@@ -124,8 +125,8 @@ export async function mcpSetupCommand(name, options) {
|
|
|
124
125
|
/**
|
|
125
126
|
* Show MCP status list (--list)
|
|
126
127
|
*/
|
|
127
|
-
async function showStatusList(targetPath, stack, existingMcps, integrations) {
|
|
128
|
-
const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { integrations });
|
|
128
|
+
async function showStatusList(targetPath, stack, existingMcps, integrations, enabledPlugins) {
|
|
129
|
+
const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { integrations, enabledPlugins });
|
|
129
130
|
const statusRows = formatMcpStatusTable(orchestration, {});
|
|
130
131
|
|
|
131
132
|
console.log(chalk.cyan('\n MCP Server Status\n'));
|
|
@@ -158,8 +159,8 @@ async function showStatusList(targetPath, stack, existingMcps, integrations) {
|
|
|
158
159
|
/**
|
|
159
160
|
* Auto-install credential-free MCPs (--auto)
|
|
160
161
|
*/
|
|
161
|
-
async function autoInstall(targetPath, stack, existingMcps, integrations) {
|
|
162
|
-
const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { autoOnly: true, integrations });
|
|
162
|
+
async function autoInstall(targetPath, stack, existingMcps, integrations, enabledPlugins) {
|
|
163
|
+
const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { autoOnly: true, integrations, enabledPlugins });
|
|
163
164
|
const autoNames = Object.keys(orchestration.autoInstallable);
|
|
164
165
|
|
|
165
166
|
if (autoNames.length === 0) {
|
|
@@ -419,8 +420,8 @@ async function setupSpecificMcp(targetPath, name, stack, existingMcps) {
|
|
|
419
420
|
/**
|
|
420
421
|
* Interactive wizard showing all MCPs
|
|
421
422
|
*/
|
|
422
|
-
async function interactiveWizard(targetPath, stack, existingMcps, integrations) {
|
|
423
|
-
const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { integrations });
|
|
423
|
+
async function interactiveWizard(targetPath, stack, existingMcps, integrations, enabledPlugins) {
|
|
424
|
+
const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { integrations, enabledPlugins });
|
|
424
425
|
|
|
425
426
|
console.log(chalk.cyan('\n MCP Setup Wizard'));
|
|
426
427
|
console.log(chalk.gray(` Stack: ${stack}`));
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
import { listProcesses } from '../../lib/procs/list.js';
|
|
4
|
+
import { classifyProcs } from '../../lib/procs/classify.js';
|
|
5
|
+
import { readMcpConfig, findDuplicateMcps, mcpCommandTokens } from '../../lib/procs/mcp-config.js';
|
|
6
|
+
import { inspectWslConfig } from '../../lib/procs/wslconfig.js';
|
|
7
|
+
import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
|
|
8
|
+
import { heavyDir, heavyStatus } from '../../../framework/hooks/shared/heavy-holders.js';
|
|
9
|
+
|
|
10
|
+
function readHolderPids(targetPath) {
|
|
11
|
+
try {
|
|
12
|
+
const config = readHeavyConfig(targetPath);
|
|
13
|
+
const { holders } = heavyStatus({ dir: heavyDir(), maxHoldMs: config.maxHoldMinutes * 60_000 });
|
|
14
|
+
return holders.map((h) => h.pid).filter(Number.isInteger);
|
|
15
|
+
} catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatAge(ms) {
|
|
21
|
+
if (ms === null) return '?';
|
|
22
|
+
const min = Math.floor(ms / 60000);
|
|
23
|
+
if (min < 60) return `${min}min`;
|
|
24
|
+
const h = Math.floor(min / 60);
|
|
25
|
+
if (h < 48) return `${h}h${String(min % 60).padStart(2, '0')}`;
|
|
26
|
+
return `${Math.floor(h / 24)}d`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const VERDICT_TEXT = {
|
|
30
|
+
orphan: 'órfão — `--prune` encerra por PID',
|
|
31
|
+
protected: 'protegido',
|
|
32
|
+
'build-server': 'nó de build',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function collectProcsReport(targetPath, deps = {}) {
|
|
36
|
+
const d = { listProcesses, readMcpConfig, inspectWslConfig, now: Date.now(), ...deps };
|
|
37
|
+
d.holderPids ??= readHolderPids(targetPath);
|
|
38
|
+
const listing = d.listProcesses();
|
|
39
|
+
const mcp = d.readMcpConfig({ projectDir: targetPath });
|
|
40
|
+
|
|
41
|
+
let procs;
|
|
42
|
+
let vmmemMb = 0;
|
|
43
|
+
if (listing.procs === null) {
|
|
44
|
+
procs = { status: 'skip', reason: listing.reason };
|
|
45
|
+
} else {
|
|
46
|
+
const { rows, sessions } = classifyProcs(listing.procs, {
|
|
47
|
+
now: d.now,
|
|
48
|
+
mcpTokens: mcpCommandTokens(mcp),
|
|
49
|
+
holderPids: d.holderPids,
|
|
50
|
+
});
|
|
51
|
+
procs = { status: 'ok', sessions, total: rows.length, rows };
|
|
52
|
+
const vmmem = listing.procs.filter((p) => p.name.startsWith('vmmem')).reduce((sum, p) => sum + p.rss, 0);
|
|
53
|
+
vmmemMb = Math.round(vmmem / (1024 * 1024));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
procs,
|
|
58
|
+
mcp: {
|
|
59
|
+
duplicates: findDuplicateMcps(mcp),
|
|
60
|
+
unreadable: mcp.unreadable,
|
|
61
|
+
servers: mcp.servers.length,
|
|
62
|
+
pluginServers: mcp.pluginServers.length,
|
|
63
|
+
},
|
|
64
|
+
wsl: { ...d.inspectWslConfig(), vmmemMb },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function renderProcs(procs) {
|
|
69
|
+
if (procs.status === 'skip') {
|
|
70
|
+
console.log(chalk.gray(` ○ Processos — não verificado: ${procs.reason}`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const orphans = procs.rows.filter((r) => r.orphan);
|
|
74
|
+
console.log(` ${chalk.bold('Processos')} — ${procs.total} candidato(s) (node, dotnet, testhost, esbuild, chrome headless), `
|
|
75
|
+
+ `${procs.total - orphans.length} com pai vivo, ${orphans.length} sem pai; ${procs.sessions} sessão(ões) do Claude viva(s)`);
|
|
76
|
+
if (!orphans.length) {
|
|
77
|
+
console.log(chalk.green(' ✓ nenhum processo órfão'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const r of orphans) {
|
|
81
|
+
const label = VERDICT_TEXT[r.verdict] ?? r.verdict;
|
|
82
|
+
const mark = r.verdict === 'orphan' ? chalk.yellow(' ⚠') : chalk.gray(' ○');
|
|
83
|
+
console.log(`${mark} pid ${r.pid} ${r.name} ${r.rssMb} MB, ${formatAge(r.ageMs)} — ${label}${r.reason ? `: ${r.reason}` : ''}`);
|
|
84
|
+
if (r.cmd) console.log(chalk.gray(` ${r.cmd.length > 110 ? `${r.cmd.slice(0, 107)}...` : r.cmd}`));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function renderMcp(mcp) {
|
|
89
|
+
console.log(` ${chalk.bold('MCP duplicado')} — ${mcp.servers} servidor(es) de configuração, ${mcp.pluginServers} de plugin`);
|
|
90
|
+
if (!mcp.duplicates.length) console.log(chalk.green(' ✓ nenhum MCP declarado no projeto e também entregue por plugin'));
|
|
91
|
+
for (const dup of mcp.duplicates) {
|
|
92
|
+
console.log(chalk.yellow(` ⚠ ${dup.server}: declarado em ${dup.declaredIn.join(', ')} e entregue pelo plugin ${dup.plugin} — sobem 2 processos`));
|
|
93
|
+
console.log(chalk.gray(` tools: ${dup.toolNames.join(' e ')}`));
|
|
94
|
+
console.log(chalk.gray(` conserto (só sugestão): remova "${dup.server}" do .mcp.json, ou desligue o plugin ${dup.plugin}`));
|
|
95
|
+
}
|
|
96
|
+
for (const file of mcp.unreadable) console.log(chalk.gray(` ○ não verificado: ${file} (ilegível)`));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderWsl(wsl) {
|
|
100
|
+
if (wsl.status === 'skip') {
|
|
101
|
+
console.log(chalk.gray(` ○ WSL — não verificado: ${wsl.reason}`));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const held = wsl.vmmemMb > 0 ? ` (vmmem segura ${wsl.vmmemMb} MB agora)` : '';
|
|
105
|
+
if (wsl.status === 'missing') console.log(chalk.yellow(` ⚠ WSL — ${wsl.path} ausente${held}`));
|
|
106
|
+
else if (wsl.suggestion) console.log(chalk.yellow(` ⚠ WSL — ${wsl.path} sem limite de memória ou sem devolução gradual${held}`));
|
|
107
|
+
else console.log(chalk.green(` ✓ WSL — ${wsl.path} com memory=${wsl.memory} e autoMemoryReclaim=${wsl.autoMemoryReclaim}`));
|
|
108
|
+
if (wsl.suggestion) {
|
|
109
|
+
console.log(chalk.gray(' conserto (só sugestão; o morph-spec não escreve fora do repo), acrescente a esse arquivo:'));
|
|
110
|
+
for (const line of wsl.suggestion.split('\n')) console.log(chalk.gray(` ${line}`));
|
|
111
|
+
console.log(chalk.gray(' depois rode `wsl --shutdown` para o WSL reler o arquivo'));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderPrune(results) {
|
|
116
|
+
console.log(` ${chalk.bold('Poda')}`);
|
|
117
|
+
if (!results.length) {
|
|
118
|
+
console.log(chalk.green(' ✓ nada a podar'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
for (const r of results) {
|
|
122
|
+
const line = `pid ${r.pid} ${r.name}${r.reason ? ` — ${r.reason}` : ''}`;
|
|
123
|
+
if (r.outcome === 'killed') console.log(chalk.green(` ✓ encerrado: ${line}`));
|
|
124
|
+
else if (r.outcome === 'skipped') console.log(chalk.gray(` ○ pulado: ${line}`));
|
|
125
|
+
else console.log(chalk.red(` ✗ falhou: ${line}`));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function doctorProcsCommand(targetPath, options = {}) {
|
|
130
|
+
const report = collectProcsReport(targetPath, options.deps);
|
|
131
|
+
|
|
132
|
+
if (options.prune) {
|
|
133
|
+
const { pruneOrphans } = await import('../../lib/procs/prune.js');
|
|
134
|
+
const { afterHeavyRun } = await import('../../lib/heavy/cleanup.js');
|
|
135
|
+
report.prune = report.procs.status === 'ok' ? pruneOrphans(report.procs.rows, options.pruneDeps) : [];
|
|
136
|
+
report.buildServer = afterHeavyRun({
|
|
137
|
+
projectPath: targetPath, config: readHeavyConfig(targetPath), scope: 'prune', deps: options.cleanupDeps,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (options.json) {
|
|
142
|
+
console.log(JSON.stringify(report, null, 2));
|
|
143
|
+
} else {
|
|
144
|
+
logger.header(options.prune ? 'Processos e memória (com --prune)' : 'Processos e memória (somente leitura)');
|
|
145
|
+
renderProcs(report.procs);
|
|
146
|
+
logger.blank();
|
|
147
|
+
renderMcp(report.mcp);
|
|
148
|
+
logger.blank();
|
|
149
|
+
renderWsl(report.wsl);
|
|
150
|
+
if (report.prune) {
|
|
151
|
+
logger.blank();
|
|
152
|
+
renderPrune(report.prune);
|
|
153
|
+
const bs = report.buildServer;
|
|
154
|
+
const mark = bs.status === 'ran' ? chalk.green('✓') : bs.status === 'failed' ? chalk.red('✗') : chalk.gray('○');
|
|
155
|
+
console.log(` ${mark} nós de build (dotnet build-server shutdown): ${bs.status}${bs.reason ? ` — ${bs.reason}` : ''}`);
|
|
156
|
+
}
|
|
157
|
+
logger.blank();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
process.exitCode = report.prune?.some((r) => r.outcome === 'failed') ? 1 : 0;
|
|
161
|
+
return report;
|
|
162
|
+
}
|
|
@@ -725,7 +725,24 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
725
725
|
}
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
+
const { reapAgentWorktrees } = await import('../../lib/worktree/agent-reaper.js');
|
|
729
|
+
const reap = await reapAgentWorktrees(targetPath);
|
|
730
|
+
if (reap.ok) {
|
|
731
|
+
const gone = reap.reaped.map((r) => r.path ?? r.branch);
|
|
732
|
+
prune.agentReap = { reaped: gone, branchesDeleted: reap.branchesDeleted, preserved: reap.preserved, ...(reap.busy ? { busy: true } : {}) };
|
|
733
|
+
if (!options.json && gone.length) logger.success(`sobras de sub-agent removidas: ${gone.join(', ')}`);
|
|
734
|
+
if (!options.json && reap.busy) logger.warn('reaper de sub-agent já em execução em outro processo; nada foi feito nesta rodada');
|
|
735
|
+
}
|
|
736
|
+
|
|
728
737
|
const { resolvePrimaryRoot } = await import('./worktree.js');
|
|
738
|
+
const { applyEditorSettings } = await import('../../lib/worktree/vscode-settings.js');
|
|
739
|
+
const editor = applyEditorSettings(resolvePrimaryRoot(targetPath));
|
|
740
|
+
if (editor.status !== 'off') {
|
|
741
|
+
prune.editorSettings = editor;
|
|
742
|
+
if (!options.json && editor.status === 'write') logger.success(`.vscode/settings.json: ${editor.added.length} exclusão(ões) de worktree adicionada(s)`);
|
|
743
|
+
if (!options.json && editor.status === 'suggest') logger.warn(`.vscode/settings.json não foi alterado (${editor.reason}); cole isto nele:\n${editor.snippet}`);
|
|
744
|
+
}
|
|
745
|
+
|
|
729
746
|
const { applyBackupRetention, summarizeRetention } = await import('../../lib/worktree/finish-backup.js');
|
|
730
747
|
// The explicit prune is the one that reaches the backups written before the
|
|
731
748
|
// retention existed (no manifest): the automatic one never touches them.
|
|
@@ -788,6 +805,9 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
|
|
|
788
805
|
if (problems.some(prunable) && !options.prune) {
|
|
789
806
|
logger.dim('\n Rode `morph-spec doctor --worktrees --prune` para limpar os registros órfãos e os diretórios vazios.');
|
|
790
807
|
}
|
|
808
|
+
if (issues.some((i) => i.type === 'agent-worktree-leftover' && i.reapable) && !options.prune) {
|
|
809
|
+
logger.dim('\n Rode `morph-spec worktree reap` (ou `doctor --worktrees --prune`) para remover as sobras de sub-agent que já pararam.');
|
|
810
|
+
}
|
|
791
811
|
if (!problems.length) {
|
|
792
812
|
logger.success('Nenhuma inconsistência — os itens acima são informativos.');
|
|
793
813
|
return true;
|
|
@@ -932,25 +952,38 @@ export async function doctorAiCommand(targetPath, options = {}) {
|
|
|
932
952
|
}
|
|
933
953
|
|
|
934
954
|
export async function doctorCommand(options = {}) {
|
|
935
|
-
// `--json` and `--prune` only mean something under `--worktrees`. Falling
|
|
955
|
+
// `--json` and `--prune` only mean something under `--worktrees` or `--procs`. Falling
|
|
936
956
|
// through to the human health check would hand a script a banner and a table
|
|
937
957
|
// where it asked for JSON — a caller has no way to notice a flag that was
|
|
938
958
|
// dropped in silence, so refuse instead of pretending.
|
|
939
959
|
const scoped = ['json', 'prune'].filter((f) => options[f]);
|
|
940
|
-
if (scoped.length && !options.worktrees) {
|
|
960
|
+
if (scoped.length && !options.worktrees && !options.procs) {
|
|
941
961
|
const flags = scoped.map((f) => `--${f}`).join(' e ');
|
|
942
|
-
logger.error(`${flags} só se aplica${scoped.length > 1 ? 'm' : ''} a \`morph-spec doctor --worktrees\`.`);
|
|
962
|
+
logger.error(`${flags} só se aplica${scoped.length > 1 ? 'm' : ''} a \`morph-spec doctor --worktrees\` ou \`morph-spec doctor --procs\`.`);
|
|
943
963
|
logger.dim(` Rode: morph-spec doctor --worktrees ${scoped.map((f) => `--${f}`).join(' ')}`);
|
|
944
964
|
process.exitCode = 1;
|
|
945
965
|
return false;
|
|
946
966
|
}
|
|
947
967
|
|
|
968
|
+
// `--prune` é uma flag só e poda coisas diferentes em cada modo; juntar os dois
|
|
969
|
+
// modos faria as duas podas destrutivas rodarem sob um único pedido.
|
|
970
|
+
if (options.procs) {
|
|
971
|
+
const conflicts = ['full', 'mcp', 'worktrees', 'reset'].filter((f) => options[f]);
|
|
972
|
+
if (conflicts.length) {
|
|
973
|
+
const flags = conflicts.map((f) => `--${f}`).join(' e ');
|
|
974
|
+
logger.error(`--procs não se combina com ${flags} — cada superfície tem o seu relatório, o seu código de saída e a sua poda.`);
|
|
975
|
+
logger.dim(' Rode: morph-spec doctor --procs');
|
|
976
|
+
process.exitCode = 1;
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
948
981
|
// `--ai` é superfície própria (D1). Combiná-la com outra superfície pediria
|
|
949
982
|
// dois relatórios com dois códigos de saída no mesmo processo — o segundo
|
|
950
983
|
// apagaria o primeiro em silêncio. Mesmo molde da guarda acima: recusa em
|
|
951
984
|
// vez de fingir.
|
|
952
985
|
if (options.ai) {
|
|
953
|
-
const conflicts = ['full', 'mcp', 'worktrees'].filter((f) => options[f]);
|
|
986
|
+
const conflicts = ['full', 'mcp', 'worktrees', 'procs'].filter((f) => options[f]);
|
|
954
987
|
if (conflicts.length) {
|
|
955
988
|
const flags = conflicts.map((f) => `--${f}`).join(' e ');
|
|
956
989
|
logger.error(`--ai não se combina com ${flags} — cada superfície tem o seu relatório e o seu código de saída.`);
|
|
@@ -1011,6 +1044,11 @@ export async function doctorCommand(options = {}) {
|
|
|
1011
1044
|
return clean;
|
|
1012
1045
|
}
|
|
1013
1046
|
|
|
1047
|
+
if (options.procs) {
|
|
1048
|
+
const { doctorProcsCommand } = await import('./doctor-procs.js');
|
|
1049
|
+
return doctorProcsCommand(process.cwd(), options);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1014
1052
|
const targetPath = process.cwd();
|
|
1015
1053
|
|
|
1016
1054
|
logger.header('MORPH-SPEC Health Check');
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { logger } from '../../utils/logger.js';
|
|
4
|
+
import { readHeavyConfig } from '../../../framework/hooks/shared/heavy-config.js';
|
|
5
|
+
import { createHeavyGate, HeavyBusyError } from '../../lib/heavy/gate.js';
|
|
6
|
+
import { describeHolder, heavyDir, heavyStatus } from '../../../framework/hooks/shared/heavy-slots.js';
|
|
7
|
+
|
|
8
|
+
const EXIT_BUSY = 3;
|
|
9
|
+
const NEEDS_CMD_SHIM = /^(npm|npx|pnpm|yarn)$/;
|
|
10
|
+
|
|
11
|
+
export function heavyStatusCommand(options = {}) {
|
|
12
|
+
const config = readHeavyConfig(process.cwd());
|
|
13
|
+
const status = heavyStatus({ dir: heavyDir(), maxHoldMs: config.maxHoldMinutes * 60_000 });
|
|
14
|
+
const report = {
|
|
15
|
+
enabled: config.enabled,
|
|
16
|
+
slots: config.slots,
|
|
17
|
+
minFreeMemGb: config.minFreeMemGb,
|
|
18
|
+
dir: status.dir,
|
|
19
|
+
freeGb: Number(status.freeGb.toFixed(2)),
|
|
20
|
+
holders: status.holders.map(({ pid, label, feature, node, cwd, since, leaseId }) => ({ pid, label, feature, node, cwd, since, ...(leaseId ? { leaseId } : {}) })),
|
|
21
|
+
exclusive: status.exclusive ? { pid: status.exclusive.pid, label: status.exclusive.label, since: status.exclusive.since } : null,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (options.json) {
|
|
25
|
+
console.log(JSON.stringify(report, null, 2));
|
|
26
|
+
return report;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
logger.header('Slots de nós pesados (build, testes, e2e)');
|
|
30
|
+
if (!config.enabled) console.log(chalk.yellow(' ⚠ semáforo desligado (heavy.enabled: false ou MORPH_HEAVY=off)'));
|
|
31
|
+
console.log(chalk.gray(` diretório: ${report.dir}`));
|
|
32
|
+
const memMark = report.freeGb < config.minFreeMemGb ? chalk.yellow('⚠') : chalk.green('✓');
|
|
33
|
+
console.log(` ${memMark} RAM livre: ${report.freeGb.toFixed(1)} GB (mínimo ${config.minFreeMemGb} GB)`);
|
|
34
|
+
console.log(` slots em uso: ${status.holders.length}/${config.slots}`);
|
|
35
|
+
for (const h of status.holders) console.log(chalk.gray(` • ${describeHolder(h)}${h.cwd ? ` — ${h.cwd}` : ''}`));
|
|
36
|
+
if (status.exclusive) console.log(chalk.yellow(` ⚠ limpeza do servidor de build em curso (${describeHolder(status.exclusive)})`));
|
|
37
|
+
logger.blank();
|
|
38
|
+
return report;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function heavyRunCommand(command, options = {}) {
|
|
42
|
+
const [file, ...args] = command;
|
|
43
|
+
const gate = createHeavyGate({ projectPath: process.cwd(), label: options.label ?? 'run' });
|
|
44
|
+
try {
|
|
45
|
+
gate.enter(file);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (!(err instanceof HeavyBusyError)) throw err;
|
|
48
|
+
console.error(chalk.yellow(`\n⏳ ${file} não rodou: ${err.message}. Tente de novo quando liberar (\`morph-spec heavy status\`).\n`));
|
|
49
|
+
process.exit(EXIT_BUSY);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const exe = process.platform === 'win32' && NEEDS_CMD_SHIM.test(file) ? `${file}.cmd` : file;
|
|
53
|
+
const res = spawnSync(exe, args, { stdio: 'inherit', shell: exe.endsWith('.cmd') });
|
|
54
|
+
gate.release();
|
|
55
|
+
|
|
56
|
+
if (res.error) {
|
|
57
|
+
console.error(chalk.red(`\n❌ não consegui executar ${file}: ${res.error.message}\n`));
|
|
58
|
+
process.exit(127);
|
|
59
|
+
}
|
|
60
|
+
process.exit(res.status ?? 1);
|
|
61
|
+
}
|
|
@@ -288,7 +288,7 @@ export async function initCommand(options) {
|
|
|
288
288
|
spinner.stop();
|
|
289
289
|
const existingMcps = claudeConfigDetected?.mcpServers || [];
|
|
290
290
|
const orchestration = orchestrateMcpSetup(targetPath, detectedStack, existingMcps,
|
|
291
|
-
{ integrations: config.project?.integrations ?? [] });
|
|
291
|
+
{ integrations: config.project?.integrations ?? [], enabledPlugins: claudeConfigDetected?.enabledPlugins ?? [] });
|
|
292
292
|
|
|
293
293
|
const autoNames = Object.keys(orchestration.autoInstallable);
|
|
294
294
|
const manualNames = Object.keys(orchestration.needsManualSetup);
|
|
@@ -33,6 +33,7 @@ import { getProjectTags } from '../../lib/stack-filter.js';
|
|
|
33
33
|
import { updateClaudeMdAgentsBlock, installRulesForStack } from '../../scripts/setup-infra.js';
|
|
34
34
|
import { reconcileLegacyTemplatesCopy } from '../../lib/installers/templates-reconcile.js';
|
|
35
35
|
import { createEffects } from '../../lib/installers/effects.js';
|
|
36
|
+
import { applyEditorSettings } from '../../lib/worktree/vscode-settings.js';
|
|
36
37
|
import { registerProject } from '../../lib/projects/registry.js';
|
|
37
38
|
import {
|
|
38
39
|
writeGraphifyignore, describeOutdatedIgnore, graphifyignoreTracked,
|
|
@@ -729,6 +730,9 @@ export async function updateCommand(options = {}) {
|
|
|
729
730
|
await updateGitignore(targetPath, fx);
|
|
730
731
|
await ensureGitattributes(targetPath, fx);
|
|
731
732
|
|
|
733
|
+
updateSpinner.text = 'Editor excludes for worktrees (.vscode/settings.json)...';
|
|
734
|
+
try { applyEditorSettings(targetPath, { fx }); } catch { /* fail-open */ }
|
|
735
|
+
|
|
732
736
|
// Migration: a .graphifyignore the previous version generated and nobody
|
|
733
737
|
// edited is swapped for the current one when its RULES change (test doubles
|
|
734
738
|
// and fixtures back in the graph — a header-only difference is left alone);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { reapAgentWorktrees } from '../../lib/worktree/agent-reaper.js';
|
|
3
|
+
|
|
4
|
+
const label = (item) => item.path ?? item.branch ?? item.hex;
|
|
5
|
+
|
|
6
|
+
function printReport(out) {
|
|
7
|
+
const mode = out.dryRun ? ' (simulação)' : '';
|
|
8
|
+
console.log(chalk.bold(`\nWorktrees de sub-agent${mode} — graça de ${out.graceMinutes} min\n`));
|
|
9
|
+
if (out.busy) console.log(chalk.yellow(' ⏳ outro reaper está em execução; nada foi feito\n'));
|
|
10
|
+
|
|
11
|
+
for (const item of out.wouldReap ?? []) console.log(chalk.cyan(` → removeria: ${label(item)}`));
|
|
12
|
+
for (const item of out.reaped) console.log(chalk.green(` ✓ removido: ${label(item)}${item.backup ? chalk.gray(` (backup: ${item.backup})`) : ''}`));
|
|
13
|
+
for (const item of out.preserved) {
|
|
14
|
+
console.log(chalk.yellow(` ⚠ preservado: ${label(item)} — ${item.reason}${item.detail ? `: ${item.detail}` : ''}`));
|
|
15
|
+
}
|
|
16
|
+
if (out.active) console.log(chalk.gray(` ${out.active} agente(s) ainda ativo(s), intocado(s)`));
|
|
17
|
+
const nothing = !out.reaped.length && !out.preserved.length && !out.active && !(out.wouldReap ?? []).length;
|
|
18
|
+
if (nothing && !out.busy) console.log(chalk.green(' ✓ nenhuma worktree de sub-agent sobrando'));
|
|
19
|
+
console.log();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function worktreeReapCommand(options = {}) {
|
|
23
|
+
const grace = options.grace === undefined ? undefined : Number(options.grace);
|
|
24
|
+
if (grace !== undefined && !(Number.isFinite(grace) && grace >= 0)) {
|
|
25
|
+
console.error(chalk.red(`--grace precisa ser um número de minutos >= 0 (recebi "${options.grace}")`));
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const out = await reapAgentWorktrees(options.cwd || process.cwd(), { dryRun: options.dryRun === true, graceMinutes: grace });
|
|
30
|
+
if (options.json) console.log(JSON.stringify(out));
|
|
31
|
+
else if (out.ok) printReport(out);
|
|
32
|
+
else console.log(chalk.gray(` fora de um repositório git — nada a fazer (${out.error})`));
|
|
33
|
+
return out;
|
|
34
|
+
}
|