@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.
Files changed (101) hide show
  1. package/CLAUDE.md +15 -5
  2. package/README.md +2 -2
  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 +5 -4
  8. package/framework/MORPH.md +10 -9
  9. package/framework/agents.json +1 -1
  10. package/framework/docs/CLI.md +22 -10
  11. package/framework/evals/proof-rubrics/mutation-proof.md +43 -13
  12. package/framework/hooks/claude-code/core/heavy-gate.js +72 -0
  13. package/framework/hooks/claude-code/core/heavy-release.js +34 -0
  14. package/framework/hooks/claude-code/core/worktree-context.js +21 -20
  15. package/framework/hooks/claude-code/core/worktree-create.js +4 -4
  16. package/framework/hooks/claude-code/core/worktree-env-inject.js +1 -4
  17. package/framework/hooks/shared/agent-reap.js +89 -0
  18. package/framework/hooks/shared/e2e-check.js +3 -1
  19. package/framework/hooks/shared/graph-refresh.js +1 -9
  20. package/framework/hooks/shared/heavy-command.js +128 -0
  21. package/framework/hooks/shared/heavy-config.js +65 -0
  22. package/framework/hooks/shared/heavy-holders.js +85 -0
  23. package/framework/hooks/shared/heavy-slots.js +207 -0
  24. package/framework/hooks/shared/process-alive.js +8 -0
  25. package/framework/hooks/shared/worktree-root.js +93 -0
  26. package/framework/rules/ai-agents-standards.md +1 -0
  27. package/framework/schemas/config.schema.json +80 -0
  28. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  29. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  30. package/framework/skills/core/morph-apply/SKILL.md +8 -3
  31. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  32. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  33. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  34. package/framework/skills/core/morph-implement/SKILL.md +9 -4
  35. package/framework/skills/core/morph-machine/SKILL.md +46 -0
  36. package/framework/skills/core/morph-media/SKILL.md +1 -1
  37. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  38. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  39. package/framework/skills/core/morph-review/SKILL.md +45 -16
  40. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  41. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  42. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  43. package/framework/standards/PROVENANCE.json +10 -10
  44. package/framework/standards/STANDARDS.json +45 -1
  45. package/framework/standards/ai-agents/tool-adapters.md +226 -0
  46. package/framework/standards/frontend/design-system/ai-video-generation.md +2 -0
  47. package/framework/standards/frontend/scroll-driven/frame-scrub.md +11 -1
  48. package/framework/standards/infrastructure/docker/local-compose-isolation.md +11 -1
  49. package/package.json +1 -1
  50. package/src/commands/e2e/e2e.js +27 -8
  51. package/src/commands/mcp/mcp-setup.js +10 -9
  52. package/src/commands/project/doctor-procs.js +162 -0
  53. package/src/commands/project/doctor.js +42 -4
  54. package/src/commands/project/heavy.js +61 -0
  55. package/src/commands/project/init.js +1 -1
  56. package/src/commands/project/update.js +4 -0
  57. package/src/commands/project/worktree-reap.js +34 -0
  58. package/src/commands/project/worktree.js +112 -43
  59. package/src/commands/state/approve.js +2 -1
  60. package/src/commands/state/finish.js +4 -0
  61. package/src/commands/state/gate-check.js +2 -1
  62. package/src/commands/validation/mutate.js +200 -0
  63. package/src/commands/validation/verify.js +39 -22
  64. package/src/dashboard/scanner/features-scanner.js +4 -4
  65. package/src/dashboard/scanner/project-api.js +4 -4
  66. package/src/lib/detectors/claude-config-detector.js +30 -1
  67. package/src/lib/e2e/compose-env.js +5 -2
  68. package/src/lib/git/evidence-staleness.js +39 -0
  69. package/src/lib/heavy/cleanup.js +70 -0
  70. package/src/lib/heavy/gate.js +96 -0
  71. package/src/lib/installers/mcp-installer.js +15 -2
  72. package/src/lib/mutation/adapters.js +90 -0
  73. package/src/lib/mutation/commands.js +60 -0
  74. package/src/lib/mutation/manifest.js +138 -0
  75. package/src/lib/mutation/plan.js +36 -0
  76. package/src/lib/mutation/report.js +66 -0
  77. package/src/lib/mutation/runner.js +351 -0
  78. package/src/lib/mutation/schemata.js +60 -0
  79. package/src/lib/mutation/snapshot.js +69 -0
  80. package/src/lib/mutation/subset.js +75 -0
  81. package/src/lib/mutation/trx.js +51 -0
  82. package/src/lib/procs/classify.js +91 -0
  83. package/src/lib/procs/list.js +124 -0
  84. package/src/lib/procs/mcp-config.js +103 -0
  85. package/src/lib/procs/prune.js +25 -0
  86. package/src/lib/procs/wslconfig.js +70 -0
  87. package/src/lib/projects/registry.js +6 -2
  88. package/src/lib/risk-detector.js +3 -3
  89. package/src/lib/tasks/eval-node.js +4 -34
  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/contracts/contract-compliance-validator.js +4 -42
  93. package/src/lib/validators/verify-runner.js +26 -8
  94. package/src/lib/worktree/agent-reaper.js +247 -0
  95. package/src/lib/worktree/dirs.js +3 -32
  96. package/src/lib/worktree/env-allocator.js +35 -5
  97. package/src/lib/worktree/vscode-settings.js +113 -0
  98. package/src/lib/worktree/worktree-audit.js +49 -5
  99. package/src/lib/worktree/worktree-include.js +46 -0
  100. package/src/scripts/setup-infra.js +4 -0
  101. package/src/utils/hooks-installer.js +11 -4
@@ -0,0 +1,207 @@
1
+ import {
2
+ closeSync, mkdirSync, openSync, readdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync, writeSync,
3
+ } from 'node:fs';
4
+ import { freemem } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { pidAlive } from './process-alive.js';
7
+ import {
8
+ GB, SLOT_FILE, describeHolder, mtimeOf, readExclusive, scanHolders,
9
+ } from './heavy-holders.js';
10
+
11
+ export { heavyDir, heavyStatus, describeHolder } from './heavy-holders.js';
12
+
13
+ const MUTEX_STALE_MS = 10_000;
14
+ const POLL_MS = 500;
15
+ const REPORT_EVERY_MS = 30_000;
16
+
17
+ export function sleepSync(ms) {
18
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(1, ms));
19
+ }
20
+
21
+ function withClaimMutex(dir, fn, { now }) {
22
+ const path = join(dir, 'claim.lock');
23
+ for (let attempt = 0; attempt < 50; attempt++) {
24
+ try {
25
+ mkdirSync(path);
26
+ try {
27
+ return { value: fn() };
28
+ } finally {
29
+ try { rmdirSync(path); } catch { /* já removido */ }
30
+ }
31
+ } catch (err) {
32
+ if (err?.code !== 'EEXIST') return null;
33
+ const mtime = mtimeOf(path);
34
+ if (mtime !== null && now() - mtime > MUTEX_STALE_MS) {
35
+ try { rmdirSync(path); } catch { /* outro já recolheu */ }
36
+ } else {
37
+ sleepSync(20);
38
+ }
39
+ }
40
+ }
41
+ return null;
42
+ }
43
+
44
+ function liveHoldersPruningStale(dir, ctx) {
45
+ const { live, stale } = scanHolders(dir, ctx);
46
+ for (const holder of stale) {
47
+ try { unlinkSync(holder.path); } catch { /* já solto */ }
48
+ }
49
+ return live;
50
+ }
51
+
52
+ function blockedText(blocked, slots, now) {
53
+ const holders = blocked.holders.map((h) => describeHolder(h, now)).join('; ');
54
+ if (blocked.reason === 'exclusive') return `aguardando limpeza do servidor de build (${describeHolder(blocked.exclusive, now)})`;
55
+ if (blocked.reason === 'memory') {
56
+ const cause = blocked.holders.length
57
+ ? `slots em uso: ${holders}`
58
+ : 'nenhum slot em uso: a pressão vem de fora do semáforo (vmmem do WSL, IDE, outros processos)';
59
+ return `aguardando RAM livre: ${blocked.freeGb.toFixed(1)} GB < ${blocked.minFreeMemGb} GB mínimos — ${cause}`;
60
+ }
61
+ return `aguardando slot pesado (${blocked.holders.length}/${slots}): ${holders}`;
62
+ }
63
+
64
+ function tryClaim(dir, opts, ctx) {
65
+ const {
66
+ slots, label, feature, node, cwd, minFreeMemGb, freeMem, lease: leaseSpec,
67
+ } = opts;
68
+ const guarded = withClaimMutex(dir, () => {
69
+ const holders = liveHoldersPruningStale(dir, ctx);
70
+ const exclusive = readExclusive(dir, ctx);
71
+ if (exclusive) return { blocked: { reason: 'exclusive', holders, exclusive } };
72
+ if (holders.length >= slots) return { blocked: { reason: 'slots', holders } };
73
+ const freeGb = freeMem() / GB;
74
+ if (minFreeMemGb > 0 && freeGb < minFreeMemGb) return { blocked: { reason: 'memory', holders, freeGb, minFreeMemGb } };
75
+
76
+ for (let i = 0; i < slots + holders.length + 1; i++) {
77
+ const path = join(dir, `slot-${i}.lock`);
78
+ try {
79
+ const fd = openSync(path, 'wx');
80
+ const meta = {
81
+ pid: leaseSpec ? null : process.pid,
82
+ since: new Date(ctx.now()).toISOString(),
83
+ label,
84
+ feature,
85
+ node,
86
+ cwd,
87
+ ...(leaseSpec ? { leaseId: leaseSpec.id, ttlMs: leaseSpec.ttlMs } : {}),
88
+ };
89
+ writeSync(fd, JSON.stringify(meta));
90
+ closeSync(fd);
91
+ return { lease: { path, pid: meta.pid, slot: i, meta } };
92
+ } catch (err) {
93
+ if (err?.code !== 'EEXIST') return { blocked: { reason: 'slots', holders } };
94
+ }
95
+ }
96
+ return { blocked: { reason: 'slots', holders } };
97
+ }, ctx);
98
+ return guarded ? guarded.value : { blocked: { reason: 'contended', holders: [] } };
99
+ }
100
+
101
+ export function acquireHeavy(opts) {
102
+ const {
103
+ dir, slots, waitTimeoutMs, maxHoldMs,
104
+ now = Date.now, sleep = sleepSync, isAlive = pidAlive, freeMem = freemem,
105
+ log = (m) => process.stderr.write(`${m}\n`), env = process.env,
106
+ } = opts;
107
+ const ctx = { now, isAlive, maxHoldMs };
108
+ const claimOpts = { ...opts, freeMem, minFreeMemGb: opts.minFreeMemGb ?? 0 };
109
+
110
+ try { mkdirSync(dir, { recursive: true }); } catch { return { status: 'acquired', lease: null, waitedMs: 0, unavailable: true }; }
111
+
112
+ const leaseOwner = Number(env.MORPH_HEAVY_LEASE);
113
+ if (Number.isInteger(leaseOwner) && leaseOwner > 0 && scanHolders(dir, ctx).live.some((h) => h.pid === leaseOwner)) {
114
+ return { status: 'acquired', lease: null, waitedMs: 0, reentrant: true };
115
+ }
116
+
117
+ const start = now();
118
+ let lastReport = -Infinity;
119
+ for (;;) {
120
+ const attempt = tryClaim(dir, claimOpts, ctx);
121
+ if (attempt.lease) return { status: 'acquired', lease: attempt.lease, waitedMs: now() - start };
122
+
123
+ const waited = now() - start;
124
+ const { blocked } = attempt;
125
+ if (waited >= waitTimeoutMs) {
126
+ return {
127
+ status: 'busy', reason: blocked.reason, holders: blocked.holders, freeGb: blocked.freeGb ?? null, waitedMs: waited,
128
+ };
129
+ }
130
+ if (blocked.reason !== 'contended' && now() - lastReport >= REPORT_EVERY_MS) {
131
+ log(blockedText(blocked, slots, now()));
132
+ lastReport = now();
133
+ }
134
+ sleep(Math.min(POLL_MS, Math.max(1, waitTimeoutMs - waited)));
135
+ }
136
+ }
137
+
138
+ export function updateLease(lease, patch) {
139
+ if (!lease) return;
140
+ try {
141
+ const current = JSON.parse(readFileSync(lease.path, 'utf8'));
142
+ if (current.pid !== lease.pid) return;
143
+ writeFileSync(lease.path, JSON.stringify({ ...current, ...patch }));
144
+ } catch { /* o lease foi recolhido; nada a atualizar */ }
145
+ }
146
+
147
+ export function releaseHeavy(lease) {
148
+ if (!lease) return;
149
+ try {
150
+ if (JSON.parse(readFileSync(lease.path, 'utf8')).pid === lease.pid) unlinkSync(lease.path);
151
+ } catch { /* já solto */ }
152
+ }
153
+
154
+ export function releaseLeaseById(dir, id) {
155
+ let names = [];
156
+ try { names = readdirSync(dir).filter((n) => SLOT_FILE.test(n)); } catch { return false; }
157
+ for (const name of names) {
158
+ const path = join(dir, name);
159
+ try {
160
+ if (JSON.parse(readFileSync(path, 'utf8')).leaseId !== id) continue;
161
+ unlinkSync(path);
162
+ return true;
163
+ } catch { /* já solto, ou ainda sendo gravado */ }
164
+ }
165
+ return false;
166
+ }
167
+
168
+ export function describeBusy(result, config) {
169
+ const waited = Math.round(result.waitedMs / 1000);
170
+ if (result.reason === 'memory') {
171
+ return `sem RAM livre há ${waited}s (${result.freeGb?.toFixed(1)} GB < ${config.minFreeMemGb} GB mínimos, heavy.minFreeMemGb)`;
172
+ }
173
+ if (result.reason === 'exclusive') return `limpeza do servidor de build ainda em curso após ${waited}s`;
174
+ return `${result.holders.length}/${config.slots} slots pesados ocupados por ${waited}s (heavy.waitTimeoutSec)`;
175
+ }
176
+
177
+ export function runExclusive(opts, fn) {
178
+ const {
179
+ dir, label, maxHoldMs, now = Date.now, isAlive = pidAlive,
180
+ } = opts;
181
+ const ctx = { now, isAlive, maxHoldMs };
182
+ try { mkdirSync(dir, { recursive: true }); } catch { return { ran: false, reason: 'unavailable', holders: [] }; }
183
+
184
+ const own = Number((opts.env ?? process.env).MORPH_HEAVY_LEASE);
185
+ const claimed = withClaimMutex(dir, () => {
186
+ const holders = liveHoldersPruningStale(dir, ctx).filter((h) => h.pid !== own);
187
+ if (holders.length) return { ran: false, reason: 'holders', holders };
188
+ if (readExclusive(dir, ctx)) return { ran: false, reason: 'exclusive', holders: [] };
189
+ try {
190
+ const fd = openSync(join(dir, 'exclusive.lock'), 'wx');
191
+ writeSync(fd, JSON.stringify({ pid: process.pid, since: new Date(now()).toISOString(), label }));
192
+ closeSync(fd);
193
+ } catch {
194
+ return { ran: false, reason: 'exclusive', holders: [] };
195
+ }
196
+ return { ran: true };
197
+ }, ctx);
198
+
199
+ if (!claimed) return { ran: false, reason: 'contended', holders: [] };
200
+ if (!claimed.value.ran) return claimed.value;
201
+
202
+ try {
203
+ return { ran: true, result: fn() };
204
+ } finally {
205
+ try { unlinkSync(join(dir, 'exclusive.lock')); } catch { /* já solto */ }
206
+ }
207
+ }
@@ -0,0 +1,8 @@
1
+ export function pidAlive(pid) {
2
+ try {
3
+ process.kill(pid, 0);
4
+ return true;
5
+ } catch (err) {
6
+ return err?.code === 'EPERM';
7
+ }
8
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * worktree-root.js — onde as worktrees do morph-spec moram, num lugar só.
3
+ *
4
+ * Padrão: `<raiz>/worktrees/<nome>` (mais o legado `.worktrees/`). Com `worktree.root` na
5
+ * config (ou `MORPH_WORKTREE_ROOT`), as novas nascem em `<root>/<chave-do-repo>/<nome>`, FORA da
6
+ * árvore aberta no editor. A chave é `basename-<hash da raiz>`: só o basename colidiria entre dois
7
+ * repositórios de mesmo nome que dividem o mesmo root.
8
+ *
9
+ * Um root DENTRO da raiz é recusado: toda lista que se orienta pelo nome do container
10
+ * (.gitignore, graphifyignore, descoberta de testes, tar do --linux, globs do editor) deixaria de
11
+ * enxergá-lo, em silêncio. Recusado, vale o padrão e o motivo volta em `warning`.
12
+ *
13
+ * O predicado "está no container" é a UNIÃO do layout configurado com o padrão e o legado: uma
14
+ * worktree que já existe continua reconhecida depois que a chave muda. Mora em `hooks/shared`
15
+ * porque hook não importa `src/`; o `src/` importa daqui.
16
+ *
17
+ * @module worktree-root
18
+ */
19
+
20
+ import { createHash } from 'crypto';
21
+ import { readFileSync } from 'fs';
22
+ import { homedir } from 'os';
23
+ import { basename, isAbsolute, join, relative, resolve } from 'path';
24
+
25
+ // No leading dot (C1): Jest escapes it inside an absolute `rootDir` glob, so `.worktrees/` made a whole
26
+ // frontend suite report 0 tests without an error. `.worktrees/` is the pre-C1 name, still read everywhere.
27
+ export const WORKTREES_DIR = 'worktrees';
28
+ export const LEGACY_WORKTREES_DIR = '.worktrees';
29
+ export const WORKTREE_DIR_NAMES = [WORKTREES_DIR, LEGACY_WORKTREES_DIR];
30
+
31
+ const fwd = (p) => String(p).replace(/\\/g, '/');
32
+ const foldCase = (p) => (process.platform === 'win32' ? p.toLowerCase() : p);
33
+
34
+ /** `basename-<8 hex do hash da raiz>` — estável entre `R:` e `r:` no Windows. */
35
+ export function repoKey(primaryRoot) {
36
+ const root = fwd(resolve(primaryRoot));
37
+ const hash = createHash('sha1').update(foldCase(root)).digest('hex').slice(0, 8);
38
+ return `${basename(root)}-${hash}`;
39
+ }
40
+
41
+ function configuredRoot(primaryRoot, env) {
42
+ if (typeof env.MORPH_WORKTREE_ROOT === 'string' && env.MORPH_WORKTREE_ROOT.trim()) return env.MORPH_WORKTREE_ROOT.trim();
43
+ try {
44
+ const raw = readFileSync(join(primaryRoot, '.morph', 'config', 'config.json'), 'utf8').replace(/^/, '');
45
+ const value = JSON.parse(raw)?.worktree?.root;
46
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function isInside(root, path) {
53
+ const rel = relative(root, path);
54
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
55
+ }
56
+
57
+ /**
58
+ * @param {string} primaryRoot
59
+ * @param {{ env?: NodeJS.ProcessEnv }} [opts]
60
+ * @returns {{ dir: string|null, base?: string, warning?: string }} `dir` = `<root>/<repoKey>`
61
+ */
62
+ export function externalContainer(primaryRoot, { env = process.env } = {}) {
63
+ const configured = configuredRoot(primaryRoot, env);
64
+ if (!configured) return { dir: null };
65
+ const expanded = configured === '~' || /^~[\\/]/.test(configured) ? join(homedir(), configured.slice(1)) : configured;
66
+ const base = resolve(primaryRoot, expanded);
67
+ if (isInside(resolve(primaryRoot), base)) {
68
+ return { dir: null, warning: `worktree.root (${configured}) fica dentro da raiz do projeto; ignorado, valem worktrees/ — as listas de exclusão se orientam por esse nome` };
69
+ }
70
+ return { dir: fwd(join(base, repoKey(primaryRoot))), base: fwd(base) };
71
+ }
72
+
73
+ /** Where a NEW worktree lands. */
74
+ export function newWorktreesDir(primaryRoot, opts) {
75
+ return externalContainer(primaryRoot, opts).dir ?? `${fwd(resolve(primaryRoot))}/${WORKTREES_DIR}`;
76
+ }
77
+
78
+ /** Every directory that counts as "the container", external first. Forward slashes. */
79
+ export function containerDirs(primaryRoot, opts) {
80
+ const root = fwd(resolve(primaryRoot));
81
+ const external = externalContainer(primaryRoot, opts).dir;
82
+ return [...(external ? [external] : []), ...WORKTREE_DIR_NAMES.map((name) => `${root}/${name}`)];
83
+ }
84
+
85
+ /** Is `path` under any of `dirs` (a `containerDirs` result)? Ask `containerDirs` once per audit, not once per path. */
86
+ export function isUnder(dirs, path) {
87
+ const target = foldCase(fwd(resolve(path)));
88
+ return dirs.some((dir) => target.startsWith(`${foldCase(dir)}/`));
89
+ }
90
+
91
+ export function isInContainer(primaryRoot, path, opts) {
92
+ return isUnder(containerDirs(primaryRoot, opts), path);
93
+ }
@@ -50,6 +50,7 @@ stacks:
50
50
  | `ai-agents/conversational-agent-with-phases.md` | conversa com fases no turno — allow-list de tools, `ChatToolMode`, teto de tool calls |
51
51
  | `ai-agents/guardrails.md` | regra proibida vira código — guard de tool, pertinência, argumento não confiável, fail-open |
52
52
  | `ai-agents/testing-ai.md` | testar agente sem rede — `IChatClient` fake, harness de turno, golden de prompt |
53
+ | `ai-agents/tool-adapters.md` | tool de agente que carrega regra de negócio — a regra em `*Ops` com desfecho tipado, o texto do modelo no adaptador, teste em duas camadas e migração neutra |
53
54
  | `ai-agents/evals-with-cache.md` | eval de modelo/prompt no CI **sem credencial e sem rede**: cache de respostas commitada, dataset versionado, o que entra na chave de cache, troca de modelo e regressão de prompt |
54
55
  | `ai-agents/cost-and-budget.md` | custo em dinheiro — `UsageCostMiddleware`, tarifa por token, teto de orçamento |
55
56
  | `ai-agents/llm-runtime-defaults.md` | knobs de runtime da chamada — timeout, retry, `strict` JSON schema, temperatura/reasoning |
@@ -149,6 +149,86 @@
149
149
  ]
150
150
  }
151
151
  }
152
+ },
153
+ "heavy": {
154
+ "type": "object",
155
+ "additionalProperties": false,
156
+ "description": "Slots per USER (shared by every session on the machine, kept in %LOCALAPPDATA%/morph-spec/heavy on Windows and ~/.local/state/morph-spec/heavy elsewhere) for the heavy nodes: build, tests and e2e of `verify`, `e2e up`, and `morph-spec heavy run`. A value it cannot read falls back to the default with a warning - this block never blocks work by being wrong. `MORPH_HEAVY=off` switches the whole semaphore off (the test suite does), `MORPH_HEAVY_SLOTS` overrides `slots`, `MORPH_HEAVY_DIR` moves the directory.",
157
+ "properties": {
158
+ "enabled": {
159
+ "type": "boolean",
160
+ "default": true,
161
+ "description": "false turns the semaphore off: no slot is taken, no RAM is checked, and `dotnet build-server shutdown` is never run (without the semaphore nothing proves another session is not compiling)."
162
+ },
163
+ "slots": {
164
+ "type": "integer",
165
+ "minimum": 1,
166
+ "default": 2,
167
+ "description": "How many heavy nodes run at once on this machine."
168
+ },
169
+ "minFreeMemGb": {
170
+ "type": "number",
171
+ "minimum": 0,
172
+ "default": 4,
173
+ "description": "Free RAM (os.freemem) below which a heavy node WAITS instead of starting and dying 'killed' for memory. It waits with or without a slot holder - the pressure often comes from outside (WSL vmmem, the IDE). 0 disables the check."
174
+ },
175
+ "waitTimeoutSec": {
176
+ "type": "number",
177
+ "minimum": 0,
178
+ "default": 600,
179
+ "description": "How long to wait for a slot or for RAM. On timeout `verify` exits 3 (busy) and persists NOTHING - a queue is neither a green nor a red gate. 0 means do not wait."
180
+ },
181
+ "maxHoldMinutes": {
182
+ "type": "number",
183
+ "exclusiveMinimum": 0,
184
+ "default": 90,
185
+ "description": "A slot held longer than this is treated as stale even if its pid is alive (pid reuse)."
186
+ },
187
+ "shutdownBuildServer": {
188
+ "type": "boolean",
189
+ "default": true,
190
+ "description": "Run `dotnet build-server shutdown` at the end of a feature-scope `verify` and of `e2e down`, only when no slot is held and no dotnet build/test is alive. Never at the end of a task-scope verify: every task would pay a cold compiler."
191
+ },
192
+ "disableNodeReuse": {
193
+ "type": "boolean",
194
+ "default": false,
195
+ "description": "Set MSBUILDDISABLENODEREUSE=1 while a heavy node runs, so MSBuild worker nodes do not stay resident."
196
+ },
197
+ "gateBash": {
198
+ "type": "boolean",
199
+ "default": false,
200
+ "description": "Opt-in. A PreToolUse hook takes a slot before a raw heavy Bash command (dotnet build|test|publish|pack, npm|pnpm|yarn test|build, vitest, jest, docker compose up|build, ...) runs and the matching Post hook gives it back. Waits up to heavy.waitTimeoutSec (capped below the hook timeout), then refuses the call with a busy message instead of running it. Foreground Bash only: a run_in_background call is not gated, and a call denied or interrupted after the hook keeps its slot until its timeout lapses, and that timeout starts before the permission prompt."
201
+ }
202
+ }
203
+ },
204
+ "worktree": {
205
+ "type": "object",
206
+ "additionalProperties": true,
207
+ "description": "Worktree behaviour of this project. Other keys are left open; the readers are src/lib/worktree/vscode-settings.js and src/lib/worktree/agent-reaper.js.",
208
+ "properties": {
209
+ "root": {
210
+ "type": "string",
211
+ "minLength": 1,
212
+ "description": "Where NEW worktrees are created, outside the project tree: they land in <root>/<repo>-<hash>/<name> (the hash keeps two repos with the same folder name apart). Absolute, `~/...`, or relative to the primary root. A path that resolves INSIDE the project root is ignored with a warning, because every exclusion list (.gitignore, graph ignore, test discovery, the --linux tar, editor globs) is keyed on worktrees/. Existing worktrees under worktrees/ keep being recognised. `MORPH_WORKTREE_ROOT` overrides it for one run."
213
+ },
214
+ "editorSettings": {
215
+ "type": "boolean",
216
+ "default": true,
217
+ "description": "false stops `init`, `update`, `worktree setup|provision` and `doctor --worktrees --prune` from adding the worktree/node_modules (and, on .NET, bin/obj) excludes to .vscode/settings.json. `MORPH_EDITOR_SETTINGS=0` does the same for one run."
218
+ },
219
+ "reap": {
220
+ "type": "object",
221
+ "additionalProperties": false,
222
+ "properties": {
223
+ "graceMinutes": {
224
+ "type": "number",
225
+ "minimum": 0,
226
+ "default": 30,
227
+ "description": "How long a sub-agent's events.jsonl must stay quiet after its AgentUsage before `worktree reap` removes the tree: a stopped agent can still be resumed by SendMessage. An invalid value falls back to 30."
228
+ }
229
+ }
230
+ }
231
+ }
152
232
  }
153
233
  }
154
234
  }
@@ -4,7 +4,7 @@ description: Guia a criação de uma landing page nova ponta-a-ponta a partir do
4
4
  version: core
5
5
  when-to-invoke: ao iniciar um novo projeto de LP com o template Umbraco genérico
6
6
  user-invokable: true
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  stacks:
9
9
  - umbraco
10
10
  ---
@@ -4,7 +4,7 @@ description: Migra uma landing page existente (React/Vite, Next.js estático ou
4
4
  version: core
5
5
  when-to-invoke: ao receber instrução "use /migrate-lp-to-umbraco {projeto}" do usuário, ou quando o usuário descrever necessidade de migrar uma LP para Umbraco
6
6
  user-invokable: true
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  stacks:
9
9
  - umbraco
10
10
  ---
@@ -4,7 +4,7 @@ description: Orquestra a execução autônoma de tasks de uma feature morph-spec
4
4
  version: core
5
5
  when-to-invoke: após Gate 2 aprovado, ao receber instrução "use /morph-apply" ou "implementar feature"
6
6
  user-invokable: true
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-apply
@@ -107,6 +107,8 @@ while (true):
107
107
  // de uma task não paga a suíte inteira). `--filter {ids}` só para FORÇAR a
108
108
  // escolha de nós; `--full` desliga o recorte.
109
109
  run: morph-spec verify {feature} {task.id} --json
110
+ // exit 3 / status == 'busy': fila de slot pesado cheia, NÃO vermelho — nada foi verificado nem gravado.
111
+ // Repita a mesma chamada; não conta como tentativa e não dispara o loop de correção.
110
112
  if verify.status == 'fail':
111
113
  // vermelho: feedback determinístico direto ao loop, SEM gastar o juiz LLM.
112
114
  feedback = verify.feedback // usado como input do próximo attempt
@@ -238,7 +240,10 @@ dispatch Agent (subagent_type: "morph-{id}") with prompt:
238
240
  por decisão sua; ao concluir, o relatório final substitui o parcial, sem o
239
241
  `PARCIAL`. Sem limite declarado, bastam os commits por marco.
240
242
  6. PROCESSOS: NUNCA `taskkill /IM node.exe` (derruba os MCPs de todas as
241
- sessões). Encerre dev server pelo PID que você mesmo subiu.
243
+ sessões). Encerre dev server pelo PID que você mesmo subiu. Build e
244
+ suíte pesados (`dotnet build`, `dotnet test`, `docker compose up`) rodam
245
+ por `morph-spec heavy run -- <comando>`: exit 3 é fila cheia, não erro do
246
+ código — espere e repita.
242
247
  7. RETORNO: o relatório no arquivo canônico é o retorno. `SendMessage` não é
243
248
  substituto do relatório e não deve ser chamado.
244
249
  8. MUTAÇÃO: nunca altere, reverta, mova ou apague código de produção para ver
@@ -296,7 +301,7 @@ Quando o `morph-spec dag {feature}` reporta `isolation.recommended: "worktree"`
296
301
  1. **Dispatch:** mesmo `subagent_type: "morph-{id}"` e standards de §3b, mais `isolation: "worktree"`. O worktree vem do hook `WorktreeCreate` do morph: nasce em `worktrees/<n>` na branch `morph-task/<n>`, com junctions (`node_modules`, `.claude`, `.morph/framework`) e `.morph/worktree.env` já prontos — se faltar infra mesmo assim, o agente roda `morph-spec worktree link --all` (fail-open); se ainda impossível, reporta "verificação: não executada" (o verify pós-merge cobre). Dev server/compose dentro do worktree já sai nas portas do bloco (o hook `worktree-env-inject` prefixa o comando; fora do Claude, `eval "$(morph-spec env)"`). O worktree nasce da branch `morph/<feature>` quando a sessão está nela (o trabalho já integrado da feature vem junto, sem `merge` manual) e com os `.env*` da raiz copiados — o prompt de dispatch não manda o agente preparar nada disso. Sessão fora de `morph/<feature>` (orquestrador na raiz) faz o worktree nascer da default: aí, e só aí, o prompt abre com `git merge --ff-only morph/<feature>`. O agente commita na branch do worktree como último ato e reporta a branch. Ele não roda `morph-spec verify` (cláusula 3): a prova é o verify do orquestrador depois do merge.
297
302
  2. **Integração (orquestrador, a cada relatório que chega):** faça o merge de cada branch reportada, uma por vez, na ordem de chegada — `git merge --no-ff <branch>`. Conflito → `git merge --abort`, marque a task de volta a `pending` com nota "merge conflict — reexecutar sequencial", siga para a próxima branch. Os paths em conflito são exatamente os de `isolation.collisions` do dag.
298
303
  3. **Pós-merge:** `verify {feature} {taskId}` no merge de cada task (dispatch rolling, §3b), depois `morph-eval` + `morph-spec score`; `morph-spec verify {feature} --json` de escopo feature UMA vez, quando nada mais está em voo (o merge é o novo ponto de integração — verificação compartilhada obrigatória).
299
- 4. **Cleanup:** o hook `WorktreeRemove` remove o worktree **limpo** ao fim do subagent (branch sobrevive) — mas não conte com ele: rode `morph-spec worktree remove --target <path>` por worktree que sobrou (NUNCA `git worktree remove` cru — as junctions do passo 1 seriam atravessadas e o `node_modules`/`.claude` da raiz apagados) + `git branch -d morph-task/<n>` das branches integradas + `git worktree prune` (fail-open). Worktree sujo (subagent não commitou) fica, vira `unintegrated-work` no `doctor --worktrees`, e a task volta a `pending`. Branches não integradas ficam e são registradas no recap com o motivo.
304
+ 4. **Cleanup:** o hook `WorktreeRemove` remove o worktree **limpo** ao fim do subagent (branch sobrevive) — mas não conte com ele: rode `morph-spec worktree remove --target <path>` por worktree que sobrou (NUNCA `git worktree remove` cru — as junctions do passo 1 seriam atravessadas e o `node_modules`/`.claude` da raiz apagados) + `git branch -d morph-task/<n>` das branches integradas + `git worktree prune` (fail-open). Depois de cada retorno de agent com `isolation`, e ao fim do batch, rode `morph-spec worktree reap --json`: remove só o worktree de `agent-<hex>` que parou limpo e sem commit exclusivo, e lista o resto com a razão (`not-a-registered-worktree` no `remove --target` seguinte = o reaper já o levou). Worktree sujo (subagent não commitou) fica, vira `agent-worktree-leftover` no `doctor --worktrees`, e a task volta a `pending`. Branches não integradas ficam e são registradas no recap com o motivo.
300
305
  5. **Agente morto no meio:** se commitou, a branch sobrevive (retomável); senão, a task volta a `pending` na lane sequencial. Registre a falha de protocolo no recap.
301
306
 
302
307
  > Anti-padrões: não misture unidades isoladas e não-isoladas no mesmo batch; o subagent nunca faz merge (integração é exclusiva do orquestrador).
@@ -4,7 +4,7 @@ description: Entende o negócio, os usuários e a arquitetura antes de qualquer
4
4
  version: core
5
5
  when-to-invoke: ao receber um novo request de feature, antes de morph-plan
6
6
  user-invokable: true
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-brainstorming
@@ -4,7 +4,7 @@ description: Avalia a qualidade de uma tarefa ou fase com rubricas graduadas 0-1
4
4
  version: core
5
5
  when-to-invoke: ao completar uma tarefa ou ao finalizar uma fase de implementação
6
6
  user-invokable: false
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-eval
@@ -4,7 +4,7 @@ description: ADW cirúrgico de hotfix — resolve uma queda/bug crítico de prod
4
4
  version: core
5
5
  when-to-invoke: ao receber "/morph-hotfix {feature}", quando a morph-proposal classifica o pedido como workType hotfix (Passo 0), ou o usuário diz "produção caiu / hotfix / urgente"
6
6
  user-invokable: true
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-hotfix
@@ -4,7 +4,7 @@ description: Guia a execução de cada tarefa de implementação com standard e
4
4
  version: core
5
5
  when-to-invoke: ao iniciar cada task de implementação listada em tasks.json
6
6
  user-invokable: false
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-implement
@@ -132,7 +132,10 @@ Ao iniciar cada task de `tasks.json` — antes de escrever qualquer código. Nor
132
132
  por decisão sua; ao concluir, o relatório final substitui o parcial, sem o
133
133
  `PARCIAL`. Sem limite declarado, bastam os commits por marco.
134
134
  6. PROCESSOS: NUNCA `taskkill /IM node.exe` (derruba os MCPs de todas as
135
- sessões). Encerre dev server pelo PID que você mesmo subiu.
135
+ sessões). Encerre dev server pelo PID que você mesmo subiu. Build e
136
+ suíte pesados (`dotnet build`, `dotnet test`, `docker compose up`) rodam
137
+ por `morph-spec heavy run -- <comando>`: exit 3 é fila cheia, não erro do
138
+ código — espere e repita.
136
139
  7. RETORNO: o relatório no arquivo canônico é o retorno. `SendMessage` não é
137
140
  substituto do relatório e não deve ser chamado.
138
141
  8. MUTAÇÃO: nunca altere, reverta, mova ou apague código de produção para ver
@@ -153,7 +156,9 @@ Ao iniciar cada task de `tasks.json` — antes de escrever qualquer código. Nor
153
156
  deixa o disco a meio caminho — e um relatório lido sobre uma árvore que ninguém conferiu avalia
154
157
  um estado que ninguém quis. Árvore inesperadamente suja ou `markers` não vazio é falha de
155
158
  protocolo: registre no recap antes de pontuar. Essa saída serve ao passo 7 enquanto a árvore
156
- não mudar; depois de uma correção sua, rode de novo.
159
+ não mudar; depois de uma correção sua, rode de novo. Dispatch com `isolation: "worktree"`:
160
+ depois de ler o relatório, `morph-spec worktree reap --json` remove o worktree do agent que parou
161
+ limpo e sem commit exclusivo; o que fica vem listado com a razão.
157
162
 
158
163
  Após o retorno do dispatch: **leia primeiro** `.morph/features/{feature}/3-implement/reports/[ID].md`
159
164
  — é o valor de retorno autoritativo. Trate a mensagem de texto do sub-agent, se chegou inteira,
@@ -267,7 +272,7 @@ Ao iniciar cada task de `tasks.json` — antes de escrever qualquer código. Nor
267
272
  > tasks.json + recap.md + `taskScores` do feature.json. **NÃO escreva o trace
268
273
  > manualmente** — a escrita manual foi removida da cerimônia por task.
269
274
 
270
- 9. Quando invocada por `morph-apply`, retornar o controle à skill orquestradora — ela decide qual task vem em seguida via `getReadyTasks(data)`. Não rode a suíte de testes inteira do projeto por conta própria ao fim da task quando invocada pela `morph-apply` — a verificação determinística (`morph-spec verify {feature} {task}`) é do orquestrador, logo após seu retorno (ver morph-apply §3a). **Única exceção:** a execução de evidência do passo 5 — só o nó `evals`, filtrado, com `--skip-e2e`, nunca persistida (`--no-persist`); ela não é a verificação da task e não substitui a do orquestrador. **A divisão:** no loop por task o recorte é **automático** — o `group` da task escolhe as suítes e os arquivos de teste dos `outputs` (passo 8) estreitam o comando dentro de cada uma (o relógio de uma task não paga a suíte inteira); `--filter {ids}` só força a escolha de suítes (um nó não casado aparece como `skip` nomeando o filtro, e um filtro que não casa nada é erro de uso, exit 2) e `--full` desliga o recorte. O sub-agent despachado roda só o filtro dos próprios testes, uma vez (cláusula 3), e nunca o `verify`. A suíte **inteira** roda no Gate 3 e no CI, sem filtro — `verify {feature}` nunca recorta. A suíte completa de um projeto grande não cabe numa invocação de Bash do harness — rode em background ou por partes, nunca "suba o limite" (não há constante de timeout global no CLI). Detalhe e forma de `project.tests[]`: `.morph/framework/CLI.md`, seção "Suítes de teste". Quando invocada manualmente, verificar se há próxima task ready e iniciá-la imediatamente aplicando esta skill desde o passo 1 (auto-advance). Pause apenas se encontrar ambiguidade bloqueante não coberta pelo mandate.
275
+ 9. Quando invocada por `morph-apply`, retornar o controle à skill orquestradora — ela decide qual task vem em seguida via `getReadyTasks(data)`. Não rode a suíte de testes inteira do projeto por conta própria ao fim da task quando invocada pela `morph-apply` — a verificação determinística (`morph-spec verify {feature} {task}`) é do orquestrador, logo após seu retorno (ver morph-apply §3a). **`verify` com exit 3** (`status: "busy"`) é fila de slot pesado cheia, não vermelho: nada foi verificado e nada foi gravado — repita a mesma chamada em vez de abrir uma tentativa de correção. **Única exceção:** a execução de evidência do passo 5 — só o nó `evals`, filtrado, com `--skip-e2e`, nunca persistida (`--no-persist`); ela não é a verificação da task e não substitui a do orquestrador. **A divisão:** no loop por task o recorte é **automático** — o `group` da task escolhe as suítes e os arquivos de teste dos `outputs` (passo 8) estreitam o comando dentro de cada uma (o relógio de uma task não paga a suíte inteira); `--filter {ids}` só força a escolha de suítes (um nó não casado aparece como `skip` nomeando o filtro, e um filtro que não casa nada é erro de uso, exit 2) e `--full` desliga o recorte. O sub-agent despachado roda só o filtro dos próprios testes, uma vez (cláusula 3), e nunca o `verify`. A suíte **inteira** roda no Gate 3 e no CI, sem filtro — `verify {feature}` nunca recorta. A suíte completa de um projeto grande não cabe numa invocação de Bash do harness — rode em background ou por partes, nunca "suba o limite" (não há constante de timeout global no CLI). Detalhe e forma de `project.tests[]`: `.morph/framework/CLI.md`, seção "Suítes de teste". Quando invocada manualmente, verificar se há próxima task ready e iniciá-la imediatamente aplicando esta skill desde o passo 1 (auto-advance). Pause apenas se encontrar ambiguidade bloqueante não coberta pelo mandate.
271
276
 
272
277
  ## Output esperado
273
278
  - Código implementado conforme o standard consultado (direto ou via sub-agent)
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: morph-machine
3
+ description: Cuida da RAM e da CPU da máquina enquanto o morph-spec trabalha — fila de builds e testes pesados (heavy), processos órfãos, worktrees de sub-agent que sobram. Invoque quando a máquina ou o editor estiver lento ou sem RAM, quando um comando voltar "⏳ não rodou" ou sair com exit 3 (fila cheia), quando sobrar agent-* em worktrees/, ou antes de despachar vários sub-agents que compilam ou testam em paralelo.
4
+ version: core
5
+ when-to-invoke: quando a máquina está lenta ou sem RAM, um comando pesado esperou ou foi recusado pela fila, sobrou worktree de sub-agent, ou vários sub-agents pesados vão rodar em paralelo
6
+ user-invokable: false
7
+ cliVersion: "8.44.0"
8
+ ---
9
+
10
+ # morph-machine
11
+
12
+ ## Objetivo
13
+
14
+ Manter a máquina do dev utilizável enquanto o trabalho paralelo do morph-spec roda. Você aplica isto sozinho quando o sintoma aparece; o usuário não precisa pedir.
15
+
16
+ ## Regras que não se negociam
17
+
18
+ - **Nunca** `taskkill /IM`, `pkill`, `killall` nem `Stop-Process -Name`: derrubam os MCPs de todas as sessões abertas. O único que encerra processo é `doctor --procs --prune`, e por PID.
19
+ - **Fila cheia não é erro de código.** `⏳ … não rodou`, `slots pesados ocupados` e exit 3 querem dizer "espere e repita o MESMO comando". Não entra no loop de correção, não conta como tentativa e não é vermelho no `verify`.
20
+ - **Nunca remova worktree** por `git worktree remove`, `rm -rf` ou `Remove-Item -Recurse`: atravessa a junction e apaga o conteúdo real da raiz. Só `finish`, `worktree remove` ou `worktree reap`.
21
+ - **Destrutivo pergunta antes.** `doctor --procs --prune` e `worktree reap` (sem `--dry-run`): mostre a lista e use `AskUserQuestion`.
22
+
23
+ ## Sintoma → comando
24
+
25
+ | Sintoma | Comando | Como ler e o que fazer |
26
+ |---|---|---|
27
+ | Máquina ou editor lento, pouca RAM | `morph-spec doctor --procs` (só lê) | Cada linha diz órfão ou protegido, com a razão. Sem órfão, a pressão vem de fora (vmmem do WSL, IDE): diga isso e passe a sugestão de `.wslconfig` que o relatório traz. Com órfão, mostre a lista e ofereça `--prune`. |
28
+ | Comando voltou `⏳ … não rodou` ou exit 3 | `morph-spec heavy status` | Mostra quem segura cada vaga, há quanto tempo, e a RAM livre. A vaga solta sozinha (processo morto ou timeout). Espere e repita. Se a causa for RAM e nenhuma vaga estiver presa, rode `doctor --procs`. |
29
+ | Vão rodar vários sub-agents pesados em paralelo | `morph-spec heavy status` antes | São 2 vagas por padrão. O que não couber espera na fila sem falhar, mas cada espera custa turnos: despache só o que cabe. |
30
+ | Build ou teste pesado avulso, fora do `verify` | `morph-spec heavy run -- <comando>` | Ou ligue o `heavy.gateBash` (abaixo) e o hook cuida disso. |
31
+ | Sobrou `agent-*` em `worktrees/` ou branch `morph-task/agent-*` | `morph-spec worktree reap --dry-run`, depois `worktree reap` | Só remove o que parou, está limpo e não tem commit exclusivo; o resto volta com a razão (`dirty`, `locked`…). `doctor --worktrees` lista tudo, inclusive árvore fora do contêiner. |
32
+ | A worktree precisa de arquivo fora do git (`.env`, segredos) | `.worktreeinclude` na raiz do repo | Um padrão por linha, sintaxe do gitignore. Copia só o que casa **e** é ignorado; nunca `node_modules`, build ou `.git`. O `setup` e o `provision` aplicam. |
33
+ | Editor pesado por causa das worktrees | `worktree.root` em `.morph/config/config.json` | Põe as worktrees fora da árvore aberta (caminho absoluto fora do projeto). `init` e `update` já gravam as exclusões no `.vscode/settings.json`. |
34
+
35
+ ## Ligar o `heavy.gateBash`
36
+
37
+ Por padrão só o `verify`, o `e2e up` e o `heavy run` passam pela fila. O `dotnet build`, `dotnet test` ou `npm test` cru de um sub-agent não passa. Com `"heavy": { "gateBash": true }` em `.morph/config/config.json` (depois `morph-spec update`), um hook toma a vaga antes desses comandos e a devolve depois; sem vaga no prazo, a chamada é recusada com a causa.
38
+
39
+ Recomende (`AskUserQuestion`, com a opção recomendada primeiro) quando sub-agents rodam build ou teste cru em paralelo, quando há mais de uma sessão na máquina, ou quando já houve queda por RAM. **Nunca ligue sem perguntar:** o efeito é uma chamada recusada quando a fila enche.
40
+
41
+ Limites: não cobre `run_in_background`, PowerShell nem comando fora da lista; uma chamada negada por regra ou interrompida segura a vaga até o timeout dela.
42
+
43
+ ## Referências
44
+
45
+ - `.morph/framework/CLI.md` — seções `heavy`, `worktree reap`, `doctor --procs` e `worktree.root`.
46
+ - `MORPH.md` §2.2 (worktrees) e a cláusula 6 dos templates de dispatch (PROCESSOS).
@@ -4,7 +4,7 @@ description: Conduz a AUTORIA de mídia gerada por IA — escolher o modelo pelo
4
4
  version: core
5
5
  when-to-invoke: quando a tarefa é PRODUZIR imagem ou vídeo por IA (catálogo, marketing, asset de LP) e alguém precisa decidir modelo, prompt e aprovação
6
6
  user-invokable: true
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  stacks:
9
9
  - dotnet
10
10
  - nextjs
@@ -4,7 +4,7 @@ description: Guia a produção dos artefatos de planejamento de uma feature (use
4
4
  version: core
5
5
  when-to-invoke: ao iniciar a fase plan de uma feature, após o Gate 1 (proposal aprovado)
6
6
  user-invokable: true
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-plan
@@ -4,7 +4,7 @@ description: Orquestra a fase de proposta+plano de uma feature morph-spec. Encad
4
4
  version: core
5
5
  when-to-invoke: ao receber instrução "use /morph-proposal" do usuário, ou ao iniciar uma nova feature
6
6
  user-invokable: true
7
- cliVersion: "8.42.0"
7
+ cliVersion: "8.44.0"
8
8
  ---
9
9
 
10
10
  # morph-proposal