@polymorphism-tech/morph-spec 9.7.0 → 10.0.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 (211) hide show
  1. package/CLAUDE.md +11 -11
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +41 -44
  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 +24 -30
  8. package/framework/MORPH.md +47 -123
  9. package/framework/agents.json +6 -374
  10. package/framework/commands/morph-apply.md +1 -1
  11. package/framework/commands/morph-archive.md +4 -3
  12. package/framework/commands/morph-hotfix.md +8 -30
  13. package/framework/commands/morph-infra.md +2 -2
  14. package/framework/commands/morph-media.md +10 -9
  15. package/framework/commands/morph-review.md +15 -0
  16. package/framework/commands/morph-status.md +1 -1
  17. package/framework/docs/CLI.md +100 -102
  18. package/framework/evals/review-checklist.md +46 -54
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/__pycache__/statusline.cpython-313.pyc +0 -0
  21. package/framework/hooks/claude-code/core/README.md +1 -1
  22. package/framework/hooks/claude-code/core/denylist.json +4 -4
  23. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  24. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  25. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  26. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  27. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  29. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  30. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  31. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  32. package/framework/hooks/shared/e2e-check.js +0 -0
  33. package/framework/hooks/shared/gate-event.js +2 -2
  34. package/framework/hooks/shared/graph-refresh.js +20 -3
  35. package/framework/hooks/shared/heavy-command.js +32 -1
  36. package/framework/hooks/shared/heavy-config.js +1 -1
  37. package/framework/hooks/shared/heavy-slots.js +41 -4
  38. package/framework/hooks/shared/next-step.js +63 -24
  39. package/framework/hooks/shared/phase-utils.js +6 -9
  40. package/framework/hooks/shared/report-progress.js +86 -0
  41. package/framework/hooks/shared/state-reader.js +3 -2
  42. package/framework/hooks/statusline.js +332 -0
  43. package/framework/phases.json +32 -10
  44. package/framework/rules/ai-agents-standards.md +9 -1
  45. package/framework/rules/csharp-standards.md +4 -0
  46. package/framework/rules/ef-standards.md +20 -0
  47. package/framework/rules/frontend-standards.md +1 -1
  48. package/framework/rules/implement-report.md +25 -0
  49. package/framework/rules/infrastructure-standards.md +2 -0
  50. package/framework/rules/nextjs-standards.md +7 -0
  51. package/framework/rules/plan-grammar.md +53 -0
  52. package/framework/rules/ui-design.md +27 -0
  53. package/framework/schemas/config.schema.json +12 -19
  54. package/framework/schemas/feature.schema.json +7 -1
  55. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  56. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  57. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  58. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  59. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  60. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  61. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  62. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  63. package/framework/skills/core/morph-apply/references/review.md +65 -0
  64. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  65. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  66. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  67. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  68. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  69. package/framework/standards/STANDARDS.json +32 -5
  70. package/framework/standards/STANDARDS.meta.json +5 -1
  71. package/framework/standards/ai-agents/agent-session.md +2 -3
  72. package/framework/standards/ai-agents/context-providers.md +2 -2
  73. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  74. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  75. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  76. package/framework/standards/ai-agents/setup.md +1 -1
  77. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  78. package/framework/standards/architecture/stack-baseline.md +47 -0
  79. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  80. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  81. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  82. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  83. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  84. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  85. package/framework/templates/.idea/morph-templates.xml +92 -0
  86. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  87. package/framework/templates/IDE-SNIPPETS.md +262 -262
  88. package/framework/templates/REGISTRY.json +1 -2
  89. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  90. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  91. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  92. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  93. package/framework/templates/context/ARCHITECTURE.md +1 -1
  94. package/framework/templates/docs/plan.md +12 -12
  95. package/framework/templates/docs/ui-mockups.md +17 -105
  96. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  97. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  98. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  99. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  100. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  101. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  102. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  103. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  104. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  105. package/framework/templates/state.template.json +17 -17
  106. package/framework/templates/ui/design-system.css +226 -226
  107. package/package.json +1 -1
  108. package/src/commands/e2e/e2e.js +88 -23
  109. package/src/commands/graph/diff.js +74 -0
  110. package/src/commands/graph/status.js +1 -1
  111. package/src/commands/observability/cost.js +2 -4
  112. package/src/commands/observability/telemetry.js +1 -1
  113. package/src/commands/project/doctor.js +23 -39
  114. package/src/commands/project/fleet.js +5 -6
  115. package/src/commands/project/init.js +63 -36
  116. package/src/commands/project/stack-scan.js +3 -42
  117. package/src/commands/project/stack-set.js +26 -0
  118. package/src/commands/project/status.js +12 -13
  119. package/src/commands/project/update.js +39 -3
  120. package/src/commands/project/worktree.js +27 -16
  121. package/src/commands/state/advance.js +17 -17
  122. package/src/commands/state/approve.js +0 -6
  123. package/src/commands/state/archive.js +27 -1
  124. package/src/commands/state/auto-approval.js +1 -1
  125. package/src/commands/state/create.js +0 -5
  126. package/src/commands/state/finish.js +43 -1
  127. package/src/commands/state/gate-check.js +37 -30
  128. package/src/commands/state/gate-decision.js +3 -2
  129. package/src/commands/state/next.js +1 -0
  130. package/src/commands/state/recap.js +67 -0
  131. package/src/commands/state/report-check.js +39 -146
  132. package/src/commands/validation/plan-check.js +16 -11
  133. package/src/commands/validation/validate-feature.js +3 -3
  134. package/src/commands/validation/verify.js +6 -6
  135. package/src/core/paths/output-schema.js +3 -3
  136. package/src/core/state/state-manager.js +2 -4
  137. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  138. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  139. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  140. package/src/dashboard/app/main.jsx +13 -13
  141. package/src/dashboard/app/pages/Activity.jsx +4 -0
  142. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  143. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  144. package/src/dashboard/app/pages/Features.jsx +7 -7
  145. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  146. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  147. package/src/dashboard/scanner/activity-scanner.js +29 -0
  148. package/src/dashboard/scanner/agents-scanner.js +11 -14
  149. package/src/dashboard/scanner/features-scanner.js +45 -24
  150. package/src/dashboard/scanner/project-api.js +11 -2
  151. package/src/dashboard/scanner/skills-scanner.js +11 -1
  152. package/src/dashboard/vite.config.js +24 -24
  153. package/src/lib/e2e/applicability.js +3 -1
  154. package/src/lib/git/tree-state.js +1 -16
  155. package/src/lib/graph/diff-tree.js +127 -0
  156. package/src/lib/graph/graphify-runtime.js +12 -5
  157. package/src/lib/heavy/gate.js +22 -1
  158. package/src/lib/installers/memory-installer.js +2 -2
  159. package/src/lib/installers/rules-installer.js +57 -0
  160. package/src/lib/installers/stack-assets.js +17 -0
  161. package/src/lib/installers/stack-sync.js +219 -0
  162. package/src/lib/plan/checks.js +55 -110
  163. package/src/lib/plan/critique.js +28 -0
  164. package/src/lib/plan/media.js +42 -0
  165. package/src/lib/plan/plan-html.js +125 -0
  166. package/src/lib/plan/plan-parser.js +34 -45
  167. package/src/lib/recap/piper.js +36 -0
  168. package/src/lib/recap/recap.js +232 -0
  169. package/src/lib/review-reports.js +33 -7
  170. package/src/lib/risk-detector.js +90 -11
  171. package/src/lib/specs/screen-checklist.js +33 -48
  172. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  173. package/src/lib/tasks/test-nodes.js +24 -6
  174. package/src/lib/tasks/test-runner.js +40 -1
  175. package/src/lib/telemetry/metrics.js +27 -29
  176. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  177. package/src/lib/validators/verify-runner.js +50 -19
  178. package/src/lib/worktree/fleet-scanner.js +7 -20
  179. package/src/lib/worktree/worktree-audit.js +0 -37
  180. package/src/scripts/setup-infra.js +15 -50
  181. package/src/utils/agents-installer.js +27 -201
  182. package/src/utils/hooks-installer.js +11 -95
  183. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  184. package/framework/hooks/claude-code/statusline.py +0 -699
  185. package/framework/hooks/claude-code/statusline.sh +0 -11
  186. package/framework/hooks/shared/agent-reap.js +0 -74
  187. package/framework/hooks/shared/slice-progress.js +0 -110
  188. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  189. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  190. package/framework/skills/core/morph-review/SKILL.md +0 -129
  191. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  192. package/framework/standards/PROVENANCE.json +0 -119
  193. package/src/commands/project/worktree-reap.js +0 -34
  194. package/src/commands/state/handoff.js +0 -65
  195. package/src/commands/validation/mutate.js +0 -263
  196. package/src/commands/validation/scout-check.js +0 -28
  197. package/src/lib/generators/handoff-generator.js +0 -172
  198. package/src/lib/mutation/adapters.js +0 -79
  199. package/src/lib/mutation/commands.js +0 -61
  200. package/src/lib/mutation/dotnet-introspection.js +0 -281
  201. package/src/lib/mutation/manifest.js +0 -144
  202. package/src/lib/mutation/plan.js +0 -44
  203. package/src/lib/mutation/report.js +0 -68
  204. package/src/lib/mutation/runner.js +0 -301
  205. package/src/lib/mutation/schemata.js +0 -60
  206. package/src/lib/mutation/snapshot.js +0 -65
  207. package/src/lib/mutation/subset.js +0 -109
  208. package/src/lib/mutation/trx.js +0 -50
  209. package/src/lib/scout/scout-check.js +0 -293
  210. package/src/lib/validators/traceability-check.js +0 -103
  211. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -1,247 +0,0 @@
1
- import { execFileSync } from 'node:child_process';
2
- import {
3
- closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, statSync, unlinkSync, writeFileSync,
4
- } from 'node:fs';
5
- import { basename, join, resolve } from 'node:path';
6
- import {
7
- cwdIsInsideWorktree, isGitRepo, parseWorktreeList, resolvePrimaryRoot, runWorktreeRemove,
8
- sessionAnchoredInWorktree,
9
- } from '../../commands/project/worktree.js';
10
- import { containerDirs, isUnder } from '../../../framework/hooks/shared/worktree-root.js';
11
- import { withoutLeftoverIgnore } from '../../../framework/hooks/shared/graph-freshness.js';
12
- import { resolveGitCommonDir } from '../../../framework/hooks/shared/git-paths.js';
13
- import { pidAlive } from '../../../framework/hooks/shared/process-alive.js';
14
- import { readProjectConfig } from '../../../framework/hooks/shared/heavy-config.js';
15
-
16
- export const DEFAULT_GRACE_MINUTES = 30;
17
-
18
- const AGENT_DIR = /^agent-([0-9a-f]+)$/;
19
- const AGENT_BRANCH = /^morph-task\/agent-([0-9a-f]+)$/;
20
- const AGENT_REF = /^refs\/heads\/morph-task\/agent-[0-9a-f]+$/;
21
- const TAIL_BYTES = 64 * 1024;
22
- const LOCK_MAX_AGE_MS = 15 * 60_000;
23
- const SAMPLE = 5;
24
-
25
- const norm = (p) => resolve(p).replace(/\\/g, '/');
26
- const gitOut = (cwd, args, input) => execFileSync('git', args, {
27
- cwd, stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8', input,
28
- });
29
- const sample = (list) => `${list.slice(0, SAMPLE).join(', ')}${list.length > SAMPLE ? `, +${list.length - SAMPLE}` : ''}`;
30
-
31
- export function resolveGraceMinutes(root, override) {
32
- const valid = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
33
- if (valid(override)) return override;
34
- const configured = readProjectConfig(root)?.worktree?.reap?.graceMinutes;
35
- return valid(configured) ? configured : DEFAULT_GRACE_MINUTES;
36
- }
37
-
38
- /**
39
- * Commits reachable from `tip` and from no other branch, remote or tag; agent branches are left out
40
- * of the comparison, and `--all` is not used (the agent's own HEAD would protect it). `null` = unmeasured.
41
- */
42
- export function exclusiveCommitCount(root, tip) {
43
- if (!tip) return null;
44
- try {
45
- const refs = gitOut(root, ['for-each-ref', '--format=%(refname)', 'refs/heads', 'refs/remotes', 'refs/tags'])
46
- .split(/\r?\n/).filter((r) => r && !AGENT_REF.test(r));
47
- const input = `${[tip, ...refs.map((r) => `^${r}`)].join('\n')}\n`;
48
- const count = Number.parseInt(gitOut(root, ['rev-list', '--count', '--stdin'], input).trim(), 10);
49
- return Number.isInteger(count) ? count : null;
50
- } catch {
51
- return null;
52
- }
53
- }
54
-
55
- function tailOfEvents(file) {
56
- const fd = openSync(file, 'r');
57
- try {
58
- const { size, mtimeMs } = fstatSync(fd);
59
- const len = Math.min(size, TAIL_BYTES);
60
- const buf = Buffer.alloc(len);
61
- readSync(fd, buf, 0, len, size - len);
62
- const lines = buf.toString('utf8').split(/\r?\n/).filter((l) => l.trim());
63
- if (size > len) lines.shift();
64
- return { last: lines.at(-1) ?? null, mtimeMs };
65
- } finally {
66
- closeSync(fd);
67
- }
68
- }
69
-
70
- /** `stopped` only when the tree's last event is this agent's own `AgentUsage`. */
71
- export function readAgentStop(worktreePath, hex) {
72
- const file = join(worktreePath, '.morph', 'logs', 'events.jsonl');
73
- if (!existsSync(file)) return { status: 'unknown', detail: 'sem .morph/logs/events.jsonl na árvore' };
74
- let tail;
75
- try { tail = tailOfEvents(file); } catch { return { status: 'unknown', detail: 'events.jsonl ilegível' }; }
76
- let event = null;
77
- try { event = tail.last ? JSON.parse(tail.last) : null; } catch { event = null; }
78
- if (!event || typeof event !== 'object') return { status: 'unknown', detail: 'último evento ilegível' };
79
- if (event.event !== 'AgentUsage') return { status: 'active', detail: `último evento: ${event.event ?? '?'}` };
80
- if (String(event.agentId ?? '').toLowerCase() !== hex) return { status: 'unknown', detail: 'AgentUsage de outro agente' };
81
- return { status: 'stopped', mtimeMs: tail.mtimeMs };
82
- }
83
-
84
- function dirtOf(worktreePath) {
85
- try {
86
- const lines = gitOut(worktreePath, ['status', '--porcelain', '--untracked-files=all']).split(/\r?\n/).filter((l) => l.trim());
87
- return withoutLeftoverIgnore(worktreePath, lines).map((l) => (l.startsWith('??') ? `untracked ${l.slice(3)}` : l.slice(3)));
88
- } catch {
89
- return null;
90
- }
91
- }
92
-
93
- function agentBranches(root) {
94
- try {
95
- return gitOut(root, ['for-each-ref', '--format=%(refname:short) %(objectname)', 'refs/heads/morph-task/'])
96
- .split(/\r?\n/).filter(Boolean)
97
- .map((line) => { const [name, sha] = line.split(' '); return { name, sha, hex: AGENT_BRANCH.exec(name)?.[1] }; })
98
- .filter((b) => b.hex);
99
- } catch {
100
- return [];
101
- }
102
- }
103
-
104
- function branchTouchedMs(root, branch) {
105
- const common = resolveGitCommonDir(root);
106
- if (!common) return null;
107
- try { return statSync(join(common, 'logs', 'refs', 'heads', ...branch.split('/'))).mtimeMs; } catch { return null; }
108
- }
109
-
110
- function agentWorktrees(root) {
111
- const rootNorm = norm(root);
112
- const containers = containerDirs(root);
113
- return parseWorktreeList(root).flatMap((e) => {
114
- const path = norm(e.path);
115
- const hex = AGENT_DIR.exec(basename(path))?.[1];
116
- if (!hex || path === rootNorm) return [];
117
- if (!isUnder(containers, path)) return [];
118
- const branch = e.branch ? e.branch.replace(/^refs\/heads\//, '') : null;
119
- if (branch && branch !== `morph-task/agent-${hex}`) return [];
120
- return [{ path, hex, branch, tip: e.head, locked: e.locked }];
121
- });
122
- }
123
-
124
- function judgeWorktree(item, ctx) {
125
- const keep = (state, reason, detail) => ({ ...item, state, reason, detail });
126
- if (!existsSync(item.path)) return keep('preserved', 'directory-missing', 'o diretório sumiu; `morph-spec doctor --worktrees --prune` limpa o registro');
127
- if (cwdIsInsideWorktree(ctx.cwd, item.path) || sessionAnchoredInWorktree(item.path, ctx.env)) return keep('active', 'anchored');
128
- if (item.locked) return keep('preserved', 'locked', '`git worktree unlock` libera, se o lock já não vale');
129
-
130
- const stop = readAgentStop(item.path, item.hex);
131
- if (stop.status === 'active') return keep('active', 'agent-active', stop.detail);
132
- if (stop.status !== 'stopped') return keep('preserved', 'no-usage-event', stop.detail);
133
- const idleMin = (ctx.now - stop.mtimeMs) / 60_000;
134
- if (idleMin < ctx.graceMinutes) return keep('pending', 'within-grace', `parou há ${Math.floor(idleMin)} min (graça de ${ctx.graceMinutes} min)`);
135
-
136
- const dirt = dirtOf(item.path);
137
- if (dirt === null) return keep('preserved', 'unknown', 'git status falhou');
138
- if (dirt.length) return keep('preserved', 'dirty', sample(dirt));
139
-
140
- const exclusive = exclusiveCommitCount(ctx.root, item.tip);
141
- if (exclusive === null) return keep('preserved', 'unknown', 'não foi possível contar os commits exclusivos');
142
- if (exclusive > 0) return keep('preserved', 'exclusive-commits', `${exclusive} commit(s) só nesta branch`);
143
- return keep('reapable', 'stopped-clean');
144
- }
145
-
146
- function judgeOrphanBranch(branch, ctx) {
147
- const item = { path: null, hex: branch.hex, branch: branch.name, tip: branch.sha };
148
- const keep = (state, reason, detail) => ({ ...item, state, reason, detail });
149
- const exclusive = exclusiveCommitCount(ctx.root, branch.sha);
150
- if (exclusive === null) return keep('preserved', 'unknown', 'não foi possível contar os commits exclusivos');
151
- if (exclusive > 0) return keep('preserved', 'exclusive-commits', `${exclusive} commit(s) só nesta branch`);
152
- const touched = branchTouchedMs(ctx.root, branch.name);
153
- if (touched === null) return keep('preserved', 'unknown', 'sem reflog para medir a idade da branch');
154
- const idleMin = (ctx.now - touched) / 60_000;
155
- if (idleMin < ctx.graceMinutes) return keep('pending', 'within-grace', `branch tocada há ${Math.floor(idleMin)} min (graça de ${ctx.graceMinutes} min)`);
156
- return keep('reapable', 'orphan-branch');
157
- }
158
-
159
- /** Read-only: every agent tree and orphan agent branch with its verdict. */
160
- export function planAgentReap(root, { cwd = root, now = Date.now(), graceMinutes, env = process.env } = {}) {
161
- const ctx = { root, cwd, now, env, graceMinutes: resolveGraceMinutes(root, graceMinutes) };
162
- const trees = agentWorktrees(root);
163
- const withTree = new Set(trees.map((t) => t.branch).filter(Boolean));
164
- const orphans = agentBranches(root).filter((b) => !withTree.has(b.name));
165
- const items = [...trees.map((t) => judgeWorktree(t, ctx)), ...orphans.map((b) => judgeOrphanBranch(b, ctx))];
166
- return { graceMinutes: ctx.graceMinutes, items };
167
- }
168
-
169
- function acquireReapLock(root, now) {
170
- const common = resolveGitCommonDir(root);
171
- if (!common) return null;
172
- const dir = join(common, 'morph-spec');
173
- const file = join(dir, 'agent-reap.lock');
174
- try { mkdirSync(dir, { recursive: true }); } catch { return null; }
175
- for (let attempt = 0; attempt < 2; attempt += 1) {
176
- try {
177
- writeFileSync(file, JSON.stringify({ pid: process.pid, since: now }), { flag: 'wx' });
178
- return () => { try { unlinkSync(file); } catch { /* already gone */ } };
179
- } catch (err) {
180
- if (err.code !== 'EEXIST') return null;
181
- }
182
- let holder = null;
183
- try { holder = JSON.parse(readFileSync(file, 'utf8')); } catch { holder = null; }
184
- const stale = !holder || !pidAlive(holder.pid) || now - Number(holder.since) > LOCK_MAX_AGE_MS;
185
- if (!stale) return null;
186
- try { unlinkSync(file); } catch { return null; }
187
- }
188
- return null;
189
- }
190
-
191
- function deleteAgentBranch(root, branch, sha) {
192
- if (exclusiveCommitCount(root, sha) !== 0) return false;
193
- try { gitOut(root, ['branch', '-D', branch]); return true; } catch { return false; }
194
- }
195
-
196
- const brief = ({ path, branch, hex }) => ({ path, branch, hex });
197
- const why = ({ path, branch, hex, reason, detail }) => ({ path, branch, hex, reason, ...(detail ? { detail } : {}) });
198
-
199
- /**
200
- * Removes the worktrees and branches of stopped, clean agents that hold no commit only they have;
201
- * everything else is listed with the reason and left alone.
202
- */
203
- export async function reapAgentWorktrees(cwd = process.cwd(), {
204
- dryRun = false, graceMinutes, now = Date.now, env = process.env, remove = runWorktreeRemove,
205
- } = {}) {
206
- if (!isGitRepo(cwd)) return { ok: false, error: 'not-a-git-repo', dryRun };
207
- const root = resolvePrimaryRoot(cwd);
208
- const summarize = (plan, extra = {}) => {
209
- const of = (state) => plan.items.filter((i) => i.state === state);
210
- return {
211
- ok: true, dryRun, graceMinutes: plan.graceMinutes,
212
- reaped: [], branchesDeleted: [],
213
- ...(dryRun ? { wouldReap: of('reapable').map(brief) } : {}),
214
- preserved: [...of('preserved'), ...of('pending')].map(why),
215
- active: of('active').length,
216
- ...extra,
217
- };
218
- };
219
-
220
- if (dryRun) return summarize(planAgentReap(root, { cwd, now: now(), graceMinutes, env }));
221
-
222
- const release = acquireReapLock(root, now());
223
- if (!release) return { ...summarize({ graceMinutes: resolveGraceMinutes(root, graceMinutes), items: [] }), busy: true };
224
- try {
225
- const plan = planAgentReap(root, { cwd, now: now(), graceMinutes, env });
226
- const out = summarize(plan);
227
- for (const item of plan.items.filter((i) => i.state === 'reapable')) {
228
- if (item.path) {
229
- const { out: removal } = await remove(null, { target: item.path, ifClean: true, relinkOnFailure: true, cwd: root });
230
- if (!removal.removed) {
231
- out.preserved.push(why({ ...item, reason: 'remove-failed', detail: removal.hint || removal.error }));
232
- continue;
233
- }
234
- out.reaped.push({ ...brief(item), ...(removal.ignoredBackup ? { backup: removal.ignoredBackup } : {}) });
235
- if (item.branch && deleteAgentBranch(root, item.branch, item.tip)) out.branchesDeleted.push(item.branch);
236
- } else if (deleteAgentBranch(root, item.branch, item.tip)) {
237
- out.branchesDeleted.push(item.branch);
238
- out.reaped.push(brief(item));
239
- } else {
240
- out.preserved.push(why({ ...item, reason: 'unknown', detail: 'a branch mudou antes de ser apagada' }));
241
- }
242
- }
243
- return out;
244
- } finally {
245
- release();
246
- }
247
- }