@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.
- package/CLAUDE.md +9 -1
- package/README.md +2 -2
- package/bin/morph-spec.js +48 -2
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +5 -4
- package/framework/MORPH.md +10 -9
- package/framework/agents.json +1 -1
- package/framework/docs/CLI.md +10 -3
- package/framework/evals/proof-rubrics/mutation-proof.md +43 -13
- package/framework/hooks/claude-code/core/heavy-gate.js +72 -0
- package/framework/hooks/claude-code/core/heavy-release.js +34 -0
- package/framework/hooks/claude-code/core/worktree-context.js +21 -20
- package/framework/hooks/claude-code/core/worktree-create.js +4 -4
- package/framework/hooks/shared/agent-reap.js +89 -0
- package/framework/hooks/shared/e2e-check.js +3 -1
- package/framework/hooks/shared/graph-refresh.js +1 -9
- package/framework/hooks/shared/heavy-command.js +128 -0
- package/framework/hooks/shared/heavy-config.js +65 -0
- package/framework/hooks/shared/heavy-holders.js +85 -0
- package/framework/hooks/shared/heavy-slots.js +207 -0
- package/framework/hooks/shared/process-alive.js +8 -0
- package/framework/hooks/shared/worktree-root.js +93 -0
- package/framework/schemas/config.schema.json +80 -0
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +8 -3
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
- package/framework/skills/core/morph-implement/SKILL.md +9 -4
- package/framework/skills/core/morph-machine/SKILL.md +46 -0
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +1 -1
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +45 -16
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +1 -1
- package/framework/standards/PROVENANCE.json +10 -10
- package/framework/standards/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/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/linux-runner.js +30 -3
- package/src/lib/tasks/test-runner.js +1 -1
- 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 +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
|
-
|
|
174
|
-
|
|
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:
|
|
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 =
|
|
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.
|
|
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/
|
|
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
|
}]
|