@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.
Files changed (102) hide show
  1. package/CLAUDE.md +14 -4
  2. package/README.md +3 -3
  3. package/bin/morph-spec.js +48 -2
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +6 -5
  8. package/framework/MORPH.md +10 -9
  9. package/framework/agents.json +7 -7
  10. package/framework/docs/CLI.md +10 -3
  11. package/framework/evals/proof-rubrics/mutation-proof.md +43 -13
  12. package/framework/hooks/README.md +1 -0
  13. package/framework/hooks/claude-code/core/chain.js +2 -0
  14. package/framework/hooks/claude-code/core/comment-guard.js +107 -0
  15. package/framework/hooks/claude-code/core/heavy-gate.js +72 -0
  16. package/framework/hooks/claude-code/core/heavy-release.js +34 -0
  17. package/framework/hooks/claude-code/core/worktree-context.js +21 -20
  18. package/framework/hooks/claude-code/core/worktree-create.js +4 -4
  19. package/framework/hooks/shared/agent-reap.js +89 -0
  20. package/framework/hooks/shared/comment-rules.js +171 -0
  21. package/{src/lib/validators/code-budget → framework/hooks/shared}/comment-scanner.js +2 -2
  22. package/{src/lib/ai → framework/hooks/shared}/comment-stripper.js +2 -1
  23. package/framework/hooks/shared/e2e-check.js +3 -1
  24. package/framework/hooks/shared/graph-refresh.js +1 -9
  25. package/framework/hooks/shared/heavy-command.js +128 -0
  26. package/framework/hooks/shared/heavy-config.js +65 -0
  27. package/framework/hooks/shared/heavy-holders.js +85 -0
  28. package/framework/hooks/shared/heavy-slots.js +207 -0
  29. package/framework/hooks/shared/process-alive.js +8 -0
  30. package/framework/hooks/shared/source-files.js +34 -0
  31. package/framework/hooks/shared/worktree-root.js +93 -0
  32. package/framework/schemas/config.schema.json +80 -0
  33. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  34. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  35. package/framework/skills/core/morph-apply/SKILL.md +11 -4
  36. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  37. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  38. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  39. package/framework/skills/core/morph-implement/SKILL.md +12 -5
  40. package/framework/skills/core/morph-machine/SKILL.md +46 -0
  41. package/framework/skills/core/morph-media/SKILL.md +1 -1
  42. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  43. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  44. package/framework/skills/core/morph-review/SKILL.md +45 -16
  45. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  46. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  47. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  48. package/framework/standards/PROVENANCE.json +10 -10
  49. package/framework/standards/STANDARDS.json +1 -1
  50. package/framework/standards/architecture/code-comments.md +9 -2
  51. package/framework/standards/frontend/design-system/ai-video-generation.md +2 -0
  52. package/framework/standards/frontend/scroll-driven/frame-scrub.md +11 -1
  53. package/package.json +1 -1
  54. package/src/commands/e2e/e2e.js +27 -8
  55. package/src/commands/mcp/mcp-setup.js +10 -9
  56. package/src/commands/project/doctor-procs.js +162 -0
  57. package/src/commands/project/doctor.js +42 -4
  58. package/src/commands/project/heavy.js +61 -0
  59. package/src/commands/project/init.js +1 -1
  60. package/src/commands/project/update.js +4 -0
  61. package/src/commands/project/worktree-reap.js +34 -0
  62. package/src/commands/project/worktree.js +78 -27
  63. package/src/commands/validation/mutate.js +200 -0
  64. package/src/commands/validation/verify.js +26 -1
  65. package/src/dashboard/scanner/features-scanner.js +4 -4
  66. package/src/dashboard/scanner/project-api.js +4 -4
  67. package/src/lib/ai/code-audit.js +1 -1
  68. package/src/lib/ai/registry-audit.js +1 -1
  69. package/src/lib/detectors/claude-config-detector.js +30 -1
  70. package/src/lib/heavy/cleanup.js +70 -0
  71. package/src/lib/heavy/gate.js +96 -0
  72. package/src/lib/installers/mcp-installer.js +15 -2
  73. package/src/lib/mutation/adapters.js +90 -0
  74. package/src/lib/mutation/commands.js +60 -0
  75. package/src/lib/mutation/manifest.js +138 -0
  76. package/src/lib/mutation/plan.js +36 -0
  77. package/src/lib/mutation/report.js +66 -0
  78. package/src/lib/mutation/runner.js +351 -0
  79. package/src/lib/mutation/schemata.js +60 -0
  80. package/src/lib/mutation/snapshot.js +69 -0
  81. package/src/lib/mutation/subset.js +75 -0
  82. package/src/lib/mutation/trx.js +51 -0
  83. package/src/lib/procs/classify.js +91 -0
  84. package/src/lib/procs/list.js +124 -0
  85. package/src/lib/procs/mcp-config.js +103 -0
  86. package/src/lib/procs/prune.js +25 -0
  87. package/src/lib/procs/wslconfig.js +70 -0
  88. package/src/lib/projects/registry.js +6 -2
  89. package/src/lib/tasks/dotnet-test-introspection.js +1 -1
  90. package/src/lib/tasks/linux-runner.js +30 -3
  91. package/src/lib/tasks/test-runner.js +1 -1
  92. package/src/lib/validators/code-budget/code-budget-validator.js +4 -6
  93. package/src/lib/validators/code-budget/index.js +1 -1
  94. package/src/lib/validators/reuse/symbol-scanner.js +1 -28
  95. package/src/lib/validators/verify-runner.js +26 -8
  96. package/src/lib/worktree/agent-reaper.js +247 -0
  97. package/src/lib/worktree/dirs.js +3 -32
  98. package/src/lib/worktree/vscode-settings.js +113 -0
  99. package/src/lib/worktree/worktree-audit.js +49 -5
  100. package/src/lib/worktree/worktree-include.js +46 -0
  101. package/src/scripts/setup-infra.js +4 -0
  102. 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
+ }