@polymorphism-tech/morph-spec 8.41.0 → 8.43.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 +14 -4
- package/README.md +3 -3
- 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 +6 -5
- package/framework/MORPH.md +10 -9
- package/framework/agents.json +7 -7
- package/framework/docs/CLI.md +10 -3
- package/framework/evals/proof-rubrics/mutation-proof.md +43 -13
- package/framework/hooks/README.md +1 -0
- package/framework/hooks/claude-code/core/chain.js +2 -0
- package/framework/hooks/claude-code/core/comment-guard.js +107 -0
- 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/shared/agent-reap.js +89 -0
- package/framework/hooks/shared/comment-rules.js +171 -0
- package/{src/lib/validators/code-budget → framework/hooks/shared}/comment-scanner.js +2 -2
- package/{src/lib/ai → framework/hooks/shared}/comment-stripper.js +2 -1
- 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/source-files.js +34 -0
- package/framework/hooks/shared/worktree-root.js +93 -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 +11 -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 +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +12 -5
- 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 +1 -1
- package/framework/standards/architecture/code-comments.md +9 -2
- package/framework/standards/frontend/design-system/ai-video-generation.md +2 -0
- package/framework/standards/frontend/scroll-driven/frame-scrub.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 +78 -27
- package/src/commands/validation/mutate.js +200 -0
- package/src/commands/validation/verify.js +26 -1
- package/src/dashboard/scanner/features-scanner.js +4 -4
- package/src/dashboard/scanner/project-api.js +4 -4
- package/src/lib/ai/code-audit.js +1 -1
- package/src/lib/ai/registry-audit.js +1 -1
- package/src/lib/detectors/claude-config-detector.js +30 -1
- 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/tasks/dotnet-test-introspection.js +1 -1
- package/src/lib/tasks/linux-runner.js +30 -3
- package/src/lib/tasks/test-runner.js +1 -1
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -6
- package/src/lib/validators/code-budget/index.js +1 -1
- package/src/lib/validators/reuse/symbol-scanner.js +1 -28
- 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/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 +14 -6
|
@@ -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
|
+
}
|