@polymorphism-tech/morph-spec 8.42.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 (87) hide show
  1. package/CLAUDE.md +9 -1
  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 +10 -3
  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/shared/agent-reap.js +89 -0
  17. package/framework/hooks/shared/e2e-check.js +3 -1
  18. package/framework/hooks/shared/graph-refresh.js +1 -9
  19. package/framework/hooks/shared/heavy-command.js +128 -0
  20. package/framework/hooks/shared/heavy-config.js +65 -0
  21. package/framework/hooks/shared/heavy-holders.js +85 -0
  22. package/framework/hooks/shared/heavy-slots.js +207 -0
  23. package/framework/hooks/shared/process-alive.js +8 -0
  24. package/framework/hooks/shared/worktree-root.js +93 -0
  25. package/framework/schemas/config.schema.json +80 -0
  26. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  27. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  28. package/framework/skills/core/morph-apply/SKILL.md +8 -3
  29. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  30. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  31. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  32. package/framework/skills/core/morph-implement/SKILL.md +9 -4
  33. package/framework/skills/core/morph-machine/SKILL.md +46 -0
  34. package/framework/skills/core/morph-media/SKILL.md +1 -1
  35. package/framework/skills/core/morph-plan/SKILL.md +1 -1
  36. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  37. package/framework/skills/core/morph-review/SKILL.md +45 -16
  38. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  39. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  40. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  41. package/framework/standards/PROVENANCE.json +10 -10
  42. package/framework/standards/frontend/design-system/ai-video-generation.md +2 -0
  43. package/framework/standards/frontend/scroll-driven/frame-scrub.md +11 -1
  44. package/package.json +1 -1
  45. package/src/commands/e2e/e2e.js +27 -8
  46. package/src/commands/mcp/mcp-setup.js +10 -9
  47. package/src/commands/project/doctor-procs.js +162 -0
  48. package/src/commands/project/doctor.js +42 -4
  49. package/src/commands/project/heavy.js +61 -0
  50. package/src/commands/project/init.js +1 -1
  51. package/src/commands/project/update.js +4 -0
  52. package/src/commands/project/worktree-reap.js +34 -0
  53. package/src/commands/project/worktree.js +78 -27
  54. package/src/commands/validation/mutate.js +200 -0
  55. package/src/commands/validation/verify.js +26 -1
  56. package/src/dashboard/scanner/features-scanner.js +4 -4
  57. package/src/dashboard/scanner/project-api.js +4 -4
  58. package/src/lib/detectors/claude-config-detector.js +30 -1
  59. package/src/lib/heavy/cleanup.js +70 -0
  60. package/src/lib/heavy/gate.js +96 -0
  61. package/src/lib/installers/mcp-installer.js +15 -2
  62. package/src/lib/mutation/adapters.js +90 -0
  63. package/src/lib/mutation/commands.js +60 -0
  64. package/src/lib/mutation/manifest.js +138 -0
  65. package/src/lib/mutation/plan.js +36 -0
  66. package/src/lib/mutation/report.js +66 -0
  67. package/src/lib/mutation/runner.js +351 -0
  68. package/src/lib/mutation/schemata.js +60 -0
  69. package/src/lib/mutation/snapshot.js +69 -0
  70. package/src/lib/mutation/subset.js +75 -0
  71. package/src/lib/mutation/trx.js +51 -0
  72. package/src/lib/procs/classify.js +91 -0
  73. package/src/lib/procs/list.js +124 -0
  74. package/src/lib/procs/mcp-config.js +103 -0
  75. package/src/lib/procs/prune.js +25 -0
  76. package/src/lib/procs/wslconfig.js +70 -0
  77. package/src/lib/projects/registry.js +6 -2
  78. package/src/lib/tasks/linux-runner.js +30 -3
  79. package/src/lib/tasks/test-runner.js +1 -1
  80. package/src/lib/validators/verify-runner.js +26 -8
  81. package/src/lib/worktree/agent-reaper.js +247 -0
  82. package/src/lib/worktree/dirs.js +3 -32
  83. package/src/lib/worktree/vscode-settings.js +113 -0
  84. package/src/lib/worktree/worktree-audit.js +49 -5
  85. package/src/lib/worktree/worktree-include.js +46 -0
  86. package/src/scripts/setup-infra.js +4 -0
  87. package/src/utils/hooks-installer.js +11 -4
@@ -30,16 +30,48 @@ import {
30
30
  isGitRepo,
31
31
  resolvePrimaryRoot,
32
32
  parseWorktreeList,
33
- WORKTREE_DIR_NAMES,
34
33
  clearDanglingWorktreeRegistration,
35
34
  } from '../../commands/project/worktree.js';
35
+ import { containerDirs, isUnder } from '../../../framework/hooks/shared/worktree-root.js';
36
36
  import { detectDefaultBranch } from '../git/default-branch.js';
37
37
  import { buildPrIndex } from '../git/pr-index.js';
38
38
  import { classifyBranch, commitsAhead, resolveBaseRef } from '../git/branch-classifier.js';
39
39
  import { auditDockerStacks } from './docker-audit.js';
40
40
  import { auditFinishBackups } from './finish-backup.js';
41
+ import { planAgentReap } from './agent-reaper.js';
41
42
 
42
43
  const norm = (p) => resolve(p).replace(/\\/g, '/');
44
+ const AGENT_TASK = /^agent-[0-9a-f]+$/;
45
+
46
+ const AGENT_FIX = {
47
+ 'stopped-clean': 'morph-spec worktree reap (ou `morph-spec doctor --worktrees --prune`)',
48
+ 'orphan-branch': 'morph-spec worktree reap (ou `morph-spec doctor --worktrees --prune`)',
49
+ 'directory-missing': 'morph-spec doctor --worktrees --prune',
50
+ locked: 'a árvore está com `git worktree lock`; `git worktree unlock <path>` libera, se o lock já não vale',
51
+ unknown: 'não foi possível provar que é seguro remover — deixe como está',
52
+ };
53
+
54
+ function agentLeftoverIssue(item) {
55
+ const where = item.path ?? item.branch;
56
+ const reapable = item.state === 'reapable';
57
+ const fix = AGENT_FIX[item.reason]
58
+ ?? (item.reason === 'exclusive-commits'
59
+ ? `integre a branch (\`git merge --no-ff ${item.branch}\` no orquestrador) antes de remover — ela é o único exemplar`
60
+ : item.reason === 'dirty'
61
+ ? `confira o trabalho na árvore (commite ou descarte) e rode \`morph-spec worktree remove --target "${item.path}"\``
62
+ : `o harness não registrou o fim do agente; se ele já terminou, \`morph-spec worktree remove --target "${item.path}"\``);
63
+ return {
64
+ type: 'agent-worktree-leftover',
65
+ severity: 'info',
66
+ ...(item.path ? { path: item.path } : {}),
67
+ branch: item.branch,
68
+ reapable,
69
+ detail: reapable
70
+ ? `${where} é sobra de sub-agent que já parou, sem trabalho pendente`
71
+ : `${where} é sobra de sub-agent com trabalho a preservar (${item.reason}${item.detail ? `: ${item.detail}` : ''})`,
72
+ fix,
73
+ };
74
+ }
43
75
 
44
76
  /** Feature names morph knows about in the primary root. */
45
77
  function knownFeatures(root) {
@@ -170,15 +202,16 @@ function isEmptyDir(path) {
170
202
  /** Directories physically present under either worktree root. */
171
203
  function dirsOnDisk(root) {
172
204
  const found = [];
173
- for (const dirName of WORKTREE_DIR_NAMES) {
174
- const base = join(root, dirName);
205
+ const rootNorm = norm(root);
206
+ for (const base of containerDirs(root)) {
175
207
  if (!existsSync(base)) continue;
208
+ const label = base.startsWith(`${rootNorm}/`) ? base.slice(rootNorm.length + 1) : base;
176
209
  try {
177
210
  for (const name of readdirSync(base)) {
178
211
  const full = join(base, name);
179
212
  try {
180
213
  if (statSync(full).isDirectory()) {
181
- found.push({ name, path: norm(full), container: dirName, empty: isEmptyDir(full) });
214
+ found.push({ name, path: norm(full), container: label, empty: isEmptyDir(full) });
182
215
  }
183
216
  } catch { /* unreadable entry — skip */ }
184
217
  }
@@ -214,6 +247,11 @@ function dirsOnDisk(root) {
214
247
  * directory that exists but git's own
215
248
  * registry does not know as a worktree
216
249
  *
250
+ * • agent-worktree-leftover (info) — a sub-agent's `agent-<hex>` tree or
251
+ * `morph-task/agent-<hex>` branch left behind:
252
+ * `reapable` when the agent stopped and nothing
253
+ * is lost, otherwise the reason it is kept
254
+ *
217
255
  * • docker-stack-orphan (warn) — a `morph-*` docker stack of this repository
218
256
  * still running for a feature this tree does
219
257
  * not know, and
@@ -242,6 +280,7 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
242
280
  // two differently on purpose.
243
281
  const prIndex = buildPrIndex(root);
244
282
 
283
+ const containers = containerDirs(root);
245
284
  const registered = parseWorktreeList(root);
246
285
  const registeredPaths = new Set();
247
286
  const features = knownFeatures(root);
@@ -263,7 +302,7 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
263
302
 
264
303
  const feature = entry.branch?.match(/^refs\/heads\/morph\/(.+)$/)?.[1] || null;
265
304
  if (!feature) {
266
- const inContainer = WORKTREE_DIR_NAMES.some((d) => path.startsWith(`${rootNorm}/${d}/`));
305
+ const inContainer = isUnder(containers, path);
267
306
  const task = entry.branch?.match(/^refs\/heads\/morph-task\/(.+)$/)?.[1] || null;
268
307
 
269
308
  if (!inContainer) {
@@ -287,6 +326,7 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
287
326
  }
288
327
 
289
328
  if (task) {
329
+ if (AGENT_TASK.test(task)) continue; // reported once, by the agent plan below
290
330
  // `morph-task/*`: a task-isolation tree provisioned by the WorktreeCreate
291
331
  // hook. Clean → nothing to say (the orchestrator removes it after the
292
332
  // merge). With commits the default does not have → report, never
@@ -367,6 +407,10 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
367
407
  }
368
408
  }
369
409
 
410
+ for (const item of planAgentReap(root, { cwd }).items) {
411
+ if (item.state === 'reapable' || item.state === 'preserved') issues.push(agentLeftoverIssue(item));
412
+ }
413
+
370
414
  for (const d of dirsOnDisk(root)) {
371
415
  if (registeredPaths.has(d.path)) continue;
372
416
  issues.push({
@@ -0,0 +1,46 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { copyFileSync, existsSync, lstatSync, mkdirSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { isDisposableBuildArtifact } from './ignored-triage.js';
5
+
6
+ const INCLUDE_FILE = '.worktreeinclude';
7
+ const NESTED_GIT = /(^|\/)\.git(\/|$)/;
8
+
9
+ const gitOut = (cwd, args, input) => execFileSync('git', args, { cwd, stdio: ['pipe', 'pipe', 'ignore'], encoding: 'utf8', input });
10
+
11
+ /**
12
+ * Claude Code's `.worktreeinclude` for a worktree morph-spec creates (a WorktreeCreate hook replaces
13
+ * the native creation, so the native copy never runs): files matching a pattern AND gitignored in the
14
+ * tree are copied when the tree lacks them. Real files only; build artifacts and `node_modules` never.
15
+ *
16
+ * @returns {string[]} relative paths copied
17
+ */
18
+ export function copyWorktreeIncludes(worktreePath, root) {
19
+ const file = join(root, INCLUDE_FILE);
20
+ if (!existsSync(file)) return [];
21
+ try {
22
+ // No --exclude-standard: with --ignored it would list every gitignored file, not just the pattern matches.
23
+ const matched = gitOut(root, ['ls-files', '--others', '--ignored', '--exclude-from', file, '-z'])
24
+ .split('\0').filter(Boolean).map((p) => p.replace(/\\/g, '/'))
25
+ .filter((p) => !NESTED_GIT.test(p) && !isDisposableBuildArtifact(p));
26
+ if (!matched.length) return [];
27
+
28
+ const ignoredThere = new Set(gitOut(worktreePath, ['check-ignore', '--stdin', '-z'], `${matched.join('\0')}\0`).split('\0').filter(Boolean));
29
+ const copied = [];
30
+ for (const rel of matched) {
31
+ if (!ignoredThere.has(rel)) continue;
32
+ const src = join(root, rel);
33
+ const dst = join(worktreePath, rel);
34
+ try {
35
+ if (!lstatSync(src).isFile()) continue;
36
+ try { lstatSync(dst); continue; } catch { /* absent, and not a dangling link either */ }
37
+ mkdirSync(dirname(dst), { recursive: true });
38
+ copyFileSync(src, dst);
39
+ copied.push(rel);
40
+ } catch { /* unreadable or unwritable — leave it out */ }
41
+ }
42
+ return copied;
43
+ } catch {
44
+ return [];
45
+ }
46
+ }
@@ -25,6 +25,7 @@ import {
25
25
  ensureGitattributes
26
26
  } from '../utils/file-copier.js';
27
27
  import { parseStacks, getProjectTags, shouldInstall } from '../lib/stack-filter.js';
28
+ import { applyEditorSettings } from '../lib/worktree/vscode-settings.js';
28
29
  import { injectMorphImport } from '../utils/claude-md-injector.js';
29
30
  import { saveProjectMorphVersion, getInstalledCLIVersion, getProjectMorphVersion } from '../utils/version-checker.js';
30
31
  import { installClaudeHooks, installGlobalStatusline } from '../utils/claude-settings-manager.js';
@@ -440,6 +441,9 @@ export async function setupInfra(targetPath, {
440
441
  await updateGitignore(targetPath);
441
442
  await ensureGitattributes(targetPath);
442
443
 
444
+ log('Step 18a: Editor excludes for worktrees...');
445
+ try { applyEditorSettings(targetPath); } catch { /* fail-open */ }
446
+
443
447
  // --- 18b. Codebase graph (graphify) ---
444
448
  // Fail-open por inteiro (ADR-002): sem Python, sem uv e sem pipx, o step
445
449
  // registra e segue. O wrapper de CLI mata o processo em qualquer throw
@@ -52,8 +52,11 @@ import { realEffects } from '../lib/installers/effects.js';
52
52
  * 3.11.0 writes every `timeout` in seconds, the unit Claude Code reads: the
53
53
  * values were milliseconds, so a hung hook held the session for hours (120000
54
54
  * on WorktreeCreate is 33 h) instead of the seconds they meant.
55
+ *
56
+ * 3.12.0 adds heavy-gate to the Bash PreToolUse chain and heavy-release to the
57
+ * PostToolUse and PostToolUseFailure chains: the opt-in `heavy.gateBash`.
55
58
  */
56
- const HOOKS_VERSION = '3.11.0';
59
+ const HOOKS_VERSION = '3.12.0';
57
60
 
58
61
  /** Marker for old dispatch.js (v1) */
59
62
  const OLD_DISPATCH_COMMAND = 'node framework/hooks/agent-teams/dispatch.js';
@@ -165,6 +168,8 @@ const MORPH_HOOKS = [
165
168
  // on someone having read MORPH.md. block-raw-worktree-add is its way-IN
166
169
  // counterpart (a raw `git worktree add` builds a tree with no junctions, no
167
170
  // ports, no identity) — static, so it costs nothing after the first two.
171
+ // heavy-gate comes after every guard — it takes a slot, and a command a guard
172
+ // blocks must not hold one — and before the rewriter.
168
173
  // worktree-env-inject is LAST: it rewrites launcher commands (updatedInput)
169
174
  // with the worktree's port block, and a command a previous link blocked must
170
175
  // never be rewritten. Block-first-wins guarantees it never sees one.
@@ -174,7 +179,7 @@ const MORPH_HOOKS = [
174
179
  hooks: [{
175
180
  type: 'command',
176
181
  command: 'node framework/hooks/claude-code/core/chain.js',
177
- args: ['pre-bash-denylist', 'block-destructive-worktree-removal', 'block-raw-worktree-add', 'worktree-env-inject']
182
+ args: ['pre-bash-denylist', 'block-destructive-worktree-removal', 'block-raw-worktree-add', 'heavy-gate', 'worktree-env-inject']
178
183
  }]
179
184
  },
180
185
 
@@ -264,19 +269,21 @@ const MORPH_HOOKS = [
264
269
  hooks: [{
265
270
  type: 'command',
266
271
  command: 'node framework/hooks/claude-code/core/chain.js',
267
- args: ['telemetry-log', 'loop-detect', 'graph-refresh-kick'],
272
+ args: ['telemetry-log', 'loop-detect', 'graph-refresh-kick', 'heavy-release'],
268
273
  timeout: 10
269
274
  }]
270
275
  },
271
276
 
272
277
  // === PostToolUseFailure: All — telemetry of the failed call (exit code,
273
278
  // duration). loop-detect stays out: its warning is shaped for PostToolUse.
279
+ // heavy-release gives back the slot of a heavy Bash call that failed.
274
280
  {
275
281
  event: 'PostToolUseFailure',
276
282
  matcher: null,
277
283
  hooks: [{
278
284
  type: 'command',
279
- command: 'node framework/hooks/claude-code/core/telemetry-log.js',
285
+ command: 'node framework/hooks/claude-code/core/chain.js',
286
+ args: ['telemetry-log', 'heavy-release'],
280
287
  timeout: 5,
281
288
  async: true
282
289
  }]