@polymorphism-tech/morph-spec 9.8.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 (185) hide show
  1. package/CLAUDE.md +10 -10
  2. package/README.md +4 -4
  3. package/bin/morph-spec.js +40 -43
  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 +83 -94
  18. package/framework/evals/review-checklist.md +45 -53
  19. package/framework/hooks/README.md +4 -4
  20. package/framework/hooks/claude-code/core/README.md +1 -1
  21. package/framework/hooks/claude-code/core/denylist.json +4 -4
  22. package/framework/hooks/claude-code/core/gate-guard.js +9 -0
  23. package/framework/hooks/claude-code/core/masked-exit.js +49 -0
  24. package/framework/hooks/claude-code/core/state-sync.js +4 -1
  25. package/framework/hooks/claude-code/core/telemetry-log.js +1 -2
  26. package/framework/hooks/claude-code/core/test-budget.js +10 -70
  27. package/framework/hooks/claude-code/core/validate-completion.js +11 -24
  28. package/framework/hooks/claude-code/core/worktree-context.js +5 -22
  29. package/framework/hooks/claude-code/core/worktree-create.js +6 -45
  30. package/framework/hooks/claude-code/core/worktree-env-inject.js +2 -10
  31. package/framework/hooks/shared/e2e-check.js +0 -0
  32. package/framework/hooks/shared/gate-event.js +2 -2
  33. package/framework/hooks/shared/graph-refresh.js +20 -3
  34. package/framework/hooks/shared/heavy-command.js +32 -1
  35. package/framework/hooks/shared/heavy-config.js +1 -1
  36. package/framework/hooks/shared/next-step.js +63 -24
  37. package/framework/hooks/shared/phase-utils.js +6 -9
  38. package/framework/hooks/shared/report-progress.js +86 -0
  39. package/framework/hooks/shared/state-reader.js +3 -2
  40. package/framework/hooks/statusline.js +332 -0
  41. package/framework/phases.json +32 -10
  42. package/framework/rules/ai-agents-standards.md +9 -1
  43. package/framework/rules/csharp-standards.md +4 -0
  44. package/framework/rules/ef-standards.md +20 -0
  45. package/framework/rules/frontend-standards.md +1 -1
  46. package/framework/rules/implement-report.md +25 -0
  47. package/framework/rules/infrastructure-standards.md +2 -0
  48. package/framework/rules/nextjs-standards.md +7 -0
  49. package/framework/rules/plan-grammar.md +53 -0
  50. package/framework/rules/ui-design.md +27 -0
  51. package/framework/schemas/config.schema.json +11 -18
  52. package/framework/schemas/feature.schema.json +7 -1
  53. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  54. package/framework/skills/core/create-umbraco-lp/references/umbraco-lp-patterns.md +1 -1
  55. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +2 -2
  56. package/framework/skills/core/morph-apply/SKILL.md +38 -166
  57. package/framework/skills/core/morph-apply/references/debugging.md +8 -0
  58. package/framework/skills/core/morph-apply/references/hotfix.md +42 -0
  59. package/framework/skills/core/morph-apply/references/machine.md +29 -0
  60. package/framework/skills/core/{morph-media/SKILL.md → morph-apply/references/media.md} +11 -74
  61. package/framework/skills/core/morph-apply/references/review.md +65 -0
  62. package/framework/skills/core/morph-plan/SKILL.md +42 -108
  63. package/framework/skills/core/morph-plan/references/critique.md +40 -0
  64. package/framework/skills/core/morph-plan/references/interview.md +25 -0
  65. package/framework/skills/core/morph-uiux/SKILL.md +47 -64
  66. package/framework/skills/core/morph-uiux/surfaces.json +32 -0
  67. package/framework/standards/STANDARDS.json +32 -5
  68. package/framework/standards/STANDARDS.meta.json +5 -1
  69. package/framework/standards/ai-agents/agent-session.md +2 -3
  70. package/framework/standards/ai-agents/context-providers.md +2 -2
  71. package/framework/standards/ai-agents/durable-workflows-hitl.md +2 -2
  72. package/framework/standards/ai-agents/providers/model-registry.md +2 -2
  73. package/framework/standards/ai-agents/rag-custom-pgvector.md +6 -6
  74. package/framework/standards/ai-agents/setup.md +1 -1
  75. package/framework/standards/ai-agents/tool-adapters.md +2 -2
  76. package/framework/standards/architecture/stack-baseline.md +47 -0
  77. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -4
  78. package/framework/standards/frontend/design-system/ai-image-generation.md +51 -12
  79. package/framework/standards/frontend/design-system/ai-video-generation.md +50 -4
  80. package/framework/standards/frontend/design-system/art-direction.md +2 -2
  81. package/framework/standards/frontend/scroll-driven/frame-scrub.md +12 -2
  82. package/framework/standards/frontend/scroll-driven/scroll-components.md +76 -4
  83. package/framework/templates/REGISTRY.json +1 -2
  84. package/framework/templates/context/ARCHITECTURE.md +1 -1
  85. package/framework/templates/docs/plan.md +12 -12
  86. package/framework/templates/docs/ui-mockups.md +17 -105
  87. package/package.json +1 -1
  88. package/src/commands/e2e/e2e.js +88 -23
  89. package/src/commands/graph/diff.js +74 -0
  90. package/src/commands/graph/status.js +1 -1
  91. package/src/commands/observability/cost.js +2 -4
  92. package/src/commands/observability/telemetry.js +1 -1
  93. package/src/commands/project/doctor.js +23 -39
  94. package/src/commands/project/fleet.js +5 -6
  95. package/src/commands/project/init.js +63 -36
  96. package/src/commands/project/stack-scan.js +3 -42
  97. package/src/commands/project/stack-set.js +26 -0
  98. package/src/commands/project/status.js +12 -13
  99. package/src/commands/project/update.js +39 -3
  100. package/src/commands/project/worktree.js +3 -14
  101. package/src/commands/state/advance.js +17 -17
  102. package/src/commands/state/approve.js +0 -6
  103. package/src/commands/state/archive.js +27 -1
  104. package/src/commands/state/auto-approval.js +1 -1
  105. package/src/commands/state/create.js +0 -5
  106. package/src/commands/state/gate-check.js +37 -30
  107. package/src/commands/state/gate-decision.js +3 -2
  108. package/src/commands/state/next.js +1 -0
  109. package/src/commands/state/recap.js +67 -0
  110. package/src/commands/state/report-check.js +36 -178
  111. package/src/commands/validation/plan-check.js +16 -11
  112. package/src/commands/validation/validate-feature.js +3 -3
  113. package/src/commands/validation/verify.js +3 -6
  114. package/src/core/paths/output-schema.js +3 -3
  115. package/src/core/state/state-manager.js +2 -4
  116. package/src/dashboard/app/components/CommandPalette.jsx +2 -5
  117. package/src/dashboard/app/components/GatePauses.jsx +116 -0
  118. package/src/dashboard/app/components/SessionMap.jsx +77 -0
  119. package/src/dashboard/app/pages/Activity.jsx +4 -0
  120. package/src/dashboard/app/pages/AgentMap.jsx +1 -2
  121. package/src/dashboard/app/pages/FeatureDetail.jsx +129 -186
  122. package/src/dashboard/app/pages/Features.jsx +7 -7
  123. package/src/dashboard/app/pages/PhasePipeline.jsx +1 -1
  124. package/src/dashboard/app/pages/Portfolio.jsx +40 -5
  125. package/src/dashboard/scanner/activity-scanner.js +29 -0
  126. package/src/dashboard/scanner/agents-scanner.js +11 -14
  127. package/src/dashboard/scanner/features-scanner.js +45 -24
  128. package/src/dashboard/scanner/project-api.js +11 -2
  129. package/src/dashboard/scanner/skills-scanner.js +11 -1
  130. package/src/lib/e2e/applicability.js +3 -1
  131. package/src/lib/git/tree-state.js +1 -16
  132. package/src/lib/graph/diff-tree.js +127 -0
  133. package/src/lib/graph/graphify-runtime.js +12 -5
  134. package/src/lib/installers/memory-installer.js +2 -2
  135. package/src/lib/installers/rules-installer.js +57 -0
  136. package/src/lib/installers/stack-assets.js +17 -0
  137. package/src/lib/installers/stack-sync.js +219 -0
  138. package/src/lib/plan/checks.js +55 -110
  139. package/src/lib/plan/critique.js +28 -0
  140. package/src/lib/plan/media.js +42 -0
  141. package/src/lib/plan/plan-html.js +125 -0
  142. package/src/lib/plan/plan-parser.js +34 -45
  143. package/src/lib/recap/piper.js +36 -0
  144. package/src/lib/recap/recap.js +232 -0
  145. package/src/lib/review-reports.js +28 -4
  146. package/src/lib/risk-detector.js +90 -11
  147. package/src/lib/specs/screen-checklist.js +33 -48
  148. package/src/lib/stack/dotnet-build-ignores.js +8 -9
  149. package/src/lib/telemetry/metrics.js +27 -29
  150. package/src/lib/validators/reuse/reuse-validator.js +1 -9
  151. package/src/lib/validators/verify-runner.js +0 -7
  152. package/src/lib/worktree/fleet-scanner.js +7 -20
  153. package/src/lib/worktree/worktree-audit.js +0 -37
  154. package/src/scripts/setup-infra.js +15 -50
  155. package/src/utils/agents-installer.js +27 -201
  156. package/src/utils/hooks-installer.js +11 -95
  157. package/framework/hooks/claude-code/core/warn-bash-source-write.js +0 -23
  158. package/framework/hooks/claude-code/statusline.py +0 -699
  159. package/framework/hooks/claude-code/statusline.sh +0 -11
  160. package/framework/hooks/shared/agent-reap.js +0 -74
  161. package/framework/hooks/shared/slice-progress.js +0 -110
  162. package/framework/skills/core/morph-hotfix/SKILL.md +0 -133
  163. package/framework/skills/core/morph-machine/SKILL.md +0 -46
  164. package/framework/skills/core/morph-review/SKILL.md +0 -129
  165. package/framework/skills/core/morph-standards/SKILL.md +0 -52
  166. package/framework/standards/PROVENANCE.json +0 -119
  167. package/src/commands/project/worktree-reap.js +0 -34
  168. package/src/commands/state/handoff.js +0 -65
  169. package/src/commands/validation/mutate.js +0 -263
  170. package/src/commands/validation/scout-check.js +0 -28
  171. package/src/lib/generators/handoff-generator.js +0 -172
  172. package/src/lib/mutation/adapters.js +0 -79
  173. package/src/lib/mutation/commands.js +0 -61
  174. package/src/lib/mutation/dotnet-introspection.js +0 -281
  175. package/src/lib/mutation/manifest.js +0 -144
  176. package/src/lib/mutation/plan.js +0 -44
  177. package/src/lib/mutation/report.js +0 -68
  178. package/src/lib/mutation/runner.js +0 -301
  179. package/src/lib/mutation/schemata.js +0 -60
  180. package/src/lib/mutation/snapshot.js +0 -65
  181. package/src/lib/mutation/subset.js +0 -109
  182. package/src/lib/mutation/trx.js +0 -50
  183. package/src/lib/scout/scout-check.js +0 -293
  184. package/src/lib/validators/traceability-check.js +0 -103
  185. package/src/lib/worktree/agent-reaper.js +0 -247
@@ -1,5 +1,5 @@
1
- import { MAX_SLICES } from './plan/checks.js';
2
1
  import { BLOCKER_TYPES, reviewFileName } from './review-reports.js';
2
+ import { FIX_ATTEMPTS } from '../../framework/hooks/shared/next-step.js';
3
3
  import { isTestPath } from './graph/sanitize.js';
4
4
  import { isTestArtifact } from './worktree/ignored-triage.js';
5
5
 
@@ -55,7 +55,7 @@ function firstOf(items) {
55
55
 
56
56
  // Exportada porque approve/advance `auto` recusam pela mesma regra. Sem design de tela não conta,
57
57
  // e o legado chega sem problemas: a auditoria o isenta. Checklist
58
- // completo ainda deve o protótipo e os prints 1280/390 (`prototypes`).
58
+ // completo ainda deve o protótipo, os prints 1280/390 e os estados (`prototypes`).
59
59
  export function uiuxChecklistPause(screenChecklist) {
60
60
  if (screenChecklist?.mockups !== true) return null;
61
61
  const problems = screenChecklist.problems ?? [];
@@ -95,6 +95,11 @@ export function reviewPause(signal) {
95
95
  return `${untyped} bloqueador(es) sem tipo de evidência (${BLOCKER_TYPES.join(' | ')})`
96
96
  + ' — sem evidência tipada não é bloqueador: reclassifique como nota ou complete a evidência';
97
97
  }
98
+ for (const [role, r] of present) {
99
+ if ((r.humanBlockerIds?.length ?? 0) > 0) {
100
+ return `${role} achou mudança irreversível que o plano não declara: ${firstOf(r.humanBlockerIds)} — o usuário decide`;
101
+ }
102
+ }
98
103
  for (const [role, r] of present) {
99
104
  if ((r.blockers ?? 0) > 0) return `${role} abriu ${r.blockers} bloqueador(es): ${firstOf(r.blockerIds ?? [])}`;
100
105
  }
@@ -162,7 +167,78 @@ export function planChecksPause(status) {
162
167
  return `${what} — rode \`morph-spec plan-check {feature}\` até zero erro antes do gate do plano`;
163
168
  }
164
169
 
170
+ export const PAUSING_RUNGS = ['migrar', 'irreversível'];
171
+ const MIGRATION_PATH_RE = /(^|\/)migrations?\//i;
172
+
173
+ export function pausingRungs(checks) {
174
+ const list = Array.isArray(checks?.checks) ? checks.checks : [];
175
+ return list.filter((c) => PAUSING_RUNGS.includes(c.rung)).map((c) => ({ id: c.id, rung: c.rung }));
176
+ }
177
+
178
+ // Só o arquivo que É o contrato. Webhook e endpoint ficam com o revisor (`irreversivel`): o caminho não diz se quebra quem consome.
179
+ const CONTRACT_PATH_RE = /(^|\/)(openapi|swagger)\.(ya?ml|json)$|\.proto$/i;
180
+
181
+ const touchedBy = (re) => (changedFiles = []) =>
182
+ changedFiles.map((f) => f.replaceAll('\\', '/')).filter((f) => re.test(f) && !isTestOnly(f));
183
+ export const migrationsTouched = touchedBy(MIGRATION_PATH_RE);
184
+ export const contractsTouched = touchedBy(CONTRACT_PATH_RE);
185
+
186
+ export const humanApprover = (approvedBy) => !!approvedBy && approvedBy !== 'auto' && !String(approvedBy).startsWith('policy:');
187
+
188
+ // Regra única do gate-check (pausa) e do approve/advance automáticos do plano (recusa).
189
+ export function rungPause(signals) {
190
+ const rungs = signals.rungs ?? [];
191
+ const named = rungs.map((r) => `${r.id} (${r.rung})`).join(', ');
192
+ if (signals.gate === 'plan') {
193
+ return rungs.length ? { signal: 'rung', reason: `${named} não se desfaz com revert — o usuário confirma antes de implementar` } : null;
194
+ }
195
+ if (signals.gate !== 'review') return null;
196
+ const migrations = signals.migrationsTouched ?? [];
197
+ if (migrations.length > 0 && rungs.length === 0) {
198
+ return { signal: 'rung-undeclared', reason: `a feature toca migration (${firstOf(migrations)}) e nenhum cenário declara \`degrau: migrar\` ou \`irreversível\`` };
199
+ }
200
+ const contracts = signals.contractsTouched ?? [];
201
+ if (contracts.length > 0 && !rungs.some((r) => r.rung === 'irreversível')) {
202
+ return { signal: 'rung-undeclared', reason: `a feature toca contrato externo (${firstOf(contracts)}) e nenhum cenário declara \`degrau: irreversível\`` };
203
+ }
204
+ if (rungs.length > 0 && !signals.planApprovedByHuman) return { signal: 'rung', reason: `${named} não passou por aprovação humana no plano` };
205
+ return null;
206
+ }
207
+
208
+ const PLAN_CRITIQUE_REASONS = {
209
+ missing: 'sem `2-plan/critica.md`',
210
+ 'no-hash': '`2-plan/critica.md` sem `**Plan-Hash:**`',
211
+ stale: '`2-plan/critica.md` é de outra versão do plano e nenhum `**Aplicado-em:**` cobre a atual',
212
+ };
213
+
214
+ // Regra única do gate-check (pausa) e do approve/advance automáticos do plano (recusa).
215
+ export function planCritiquePause(status) {
216
+ const what = PLAN_CRITIQUE_REASONS[status];
217
+ return what ? `${what} — despache a crítica em contexto fresco (morph-plan, references/critique.md) antes do gate do plano` : null;
218
+ }
219
+
220
+ // Pauses the session clears by doing the work the reason names; the rest wait for the human.
221
+ const REPORT_SIGNALS = ['review-reports', 'screen-checklist', 'media-claims'];
222
+ export const SESSION_SIGNALS = ['verify-red', 'verify-missing', 'verify-stale', 'e2e-skip', 'plan-checks', 'plan-critique', 'uiux-checklist', ...REPORT_SIGNALS];
223
+
224
+ const reviewRounds = (review) => Math.max(0, ...Object.values(review?.reports ?? {}).map((r) => r.rounds ?? 0));
225
+ const humanBlockers = (review) => Object.values(review?.reports ?? {}).some((r) => (r.humanBlockerIds?.length ?? 0) > 0);
226
+
227
+ export function pauseResolver(signal, signals = {}) {
228
+ if (!SESSION_SIGNALS.includes(signal)) return 'human';
229
+ if (signal === 'verify-red' && (signals.failStreak ?? 0) >= FIX_ATTEMPTS) return 'human';
230
+ if (signal === 'e2e-skip' && classifyE2eSkip(signals.e2eReason) !== 'requested') return 'human';
231
+ if (REPORT_SIGNALS.includes(signal) && reviewRounds(signals.reviewReports) >= FIX_ATTEMPTS) return 'human';
232
+ if (signal === 'review-reports' && humanBlockers(signals.reviewReports)) return 'human';
233
+ return 'session';
234
+ }
235
+
165
236
  export function shouldPauseGate(config, signals) {
237
+ const decision = pauseDecision(config, signals);
238
+ return decision.pause ? { ...decision, resolver: pauseResolver(decision.signal, signals) } : decision;
239
+ }
240
+
241
+ function pauseDecision(config, signals) {
166
242
  const trust = config?.trust ?? 'auto';
167
243
 
168
244
  if (trust === 'manual') {
@@ -301,15 +377,13 @@ export function shouldPauseGate(config, signals) {
301
377
  if (signals.gate === 'plan') {
302
378
  const reason = planChecksPause(signals.planChecks);
303
379
  if (reason) return { pause: true, signal: 'plan-checks', reason };
380
+ const critique = planCritiquePause(signals.planCritique);
381
+ if (critique) return { pause: true, signal: 'plan-critique', reason: critique };
304
382
  }
305
383
 
306
- if (typeof signals.sliceCount === 'number' && signals.sliceCount > MAX_SLICES) {
307
- return {
308
- pause: true,
309
- signal: 'slice-count',
310
- reason: `${signals.sliceCount} fatias, acima de ${MAX_SLICES} — o plano é grande demais para uma feature`,
311
- };
312
- }
384
+ // Por último: o usuário só é chamado pelo degrau quando a sessão não tem mais nada a corrigir.
385
+ const rung = rungPause(signals);
386
+ if (rung) return { pause: true, ...rung };
313
387
 
314
388
  return { pause: false, signal: null, reason: '' };
315
389
  }
@@ -346,8 +420,13 @@ export function getRiskSignals(signals) {
346
420
  if (signals.gate === 'plan' && signals.planChecks && signals.planChecks !== 'fresh') {
347
421
  out.push(`checks.json ${signals.planChecks}`);
348
422
  }
349
- if (typeof signals.sliceCount === 'number' && signals.sliceCount > MAX_SLICES) {
350
- out.push(`${signals.sliceCount} fatias (acima de ${MAX_SLICES})`);
423
+ if (signals.gate === 'plan' && signals.planCritique) out.push(`crítica do plano: ${signals.planCritique}`);
424
+ if ((signals.rungs?.length ?? 0) > 0) out.push(`degrau: ${signals.rungs.map((r) => `${r.id} ${r.rung}`).join(', ')}`);
425
+ if (signals.gate === 'review' && (signals.migrationsTouched?.length ?? 0) > 0) {
426
+ out.push(`migration tocada: ${firstOf(signals.migrationsTouched)}`);
427
+ }
428
+ if (signals.gate === 'review' && (signals.contractsTouched?.length ?? 0) > 0) {
429
+ out.push(`contrato externo tocado: ${firstOf(signals.contractsTouched)}`);
351
430
  }
352
431
  if (signals.gate === 'review' && signals.mediaClaims?.featureGeneratesMedia === true) {
353
432
  out.push(signals.mediaClaims.section === true
@@ -5,30 +5,19 @@
5
5
  import { createHash } from 'crypto';
6
6
  import { existsSync, readFileSync } from 'fs';
7
7
  import { dirname, join } from 'path';
8
- import { fileURLToPath } from 'url';
9
8
  import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
10
9
  import { verifierScreenChecklist } from '../review-reports.js';
11
10
  import { LEGACY_MOCKUP_EXEMPTION, uiuxChecklistPause } from '../risk-detector.js';
12
11
 
13
- const PACKAGE_AGENTS = fileURLToPath(new URL('../../../framework/agents.json', import.meta.url));
14
-
15
12
  /** `match` roda sobre o rótulo normalizado; um rótulo que casa com vários itens responde a todos. */
16
13
  export const SCREEN_CHECKLIST_ITEMS = [
17
- { id: 'superficie', label: 'Superfície', match: /\bsuperficie\b/ },
18
- { id: 'volume', label: 'Volume esperado', match: /\bvolume\b/ },
19
- { id: 'paginacao', label: 'Paginação', match: /\bpaginacao\b/ },
20
- { id: 'ordenacao', label: 'Ordenação', match: /\bordenacao\b/ },
21
- { id: 'filtro', label: 'Filtro', match: /\bfiltros?\b/ },
22
- { id: 'vazio', label: 'Estado vazio', match: /\bvazio\b/ },
23
- { id: 'carregando', label: 'Estado carregando', match: /\bcarregando\b|\bcarregamento\b/ },
24
- { id: 'erro', label: 'Estado de erro', match: /\berro\b/ },
25
- { id: 'mobile', label: 'Mobile', match: /\bmobile\b/ },
26
- { id: 'abas', label: 'Abas', match: /\babas\b/ },
14
+ { id: 'servidor', label: 'Lista no servidor', match: /\bpaginacao\b|\bordenacao\b|\bservidor\b/ },
15
+ { id: 'estados', label: 'Estados vazio e carregando', match: /\bvazio\b|\bcarregando\b|\bcarregamento\b/ },
16
+ { id: 'erro', label: 'Erro com próxima ação', match: /\berro\b/ },
17
+ { id: 'acao', label: 'Uma ação primária', match: /\bprimaria\b/ },
18
+ { id: 'mobile', label: 'Celular', match: /\bmobile\b|\bcelular\b/ },
27
19
  ];
28
20
 
29
- // A superfície decide quais standards a tela lê, não um comportamento que a tela entregue exiba.
30
- const NOT_VERIFIED_AT_REVIEW = new Set(['superficie']);
31
-
32
21
  // As exatas, e as que começam adiando a decisão ("a definir depois", "TBD.").
33
22
  const PLACEHOLDERS = new Set(['', '-', '--', '—', '–', '...', 'x', 'todo', 'definir', 'pendente']);
34
23
  const PLACEHOLDER_PREFIX_RE = /^(a definir|tbd|\?|…)/;
@@ -141,30 +130,9 @@ const isNotApplicable = (answer) => NOT_APPLICABLE_RE.test(normalizeLabel(answer
141
130
 
142
131
  const labelOf = (id) => SCREEN_CHECKLIST_ITEMS.find((item) => item.id === id)?.label ?? id;
143
132
 
144
- // O vocabulário válido de `Superfície`: as `surfaces` da `ui-designer`, da cópia instalada ou do pacote.
145
- // null quando a persona não declara o campo, e então o valor não é validado.
146
- export function knownSurfaces(cwd) {
147
- const surfaces = readAgentsJson(cwd)?.agents?.['ui-designer']?.surfaces;
148
- return surfaces && typeof surfaces === 'object' && !Array.isArray(surfaces) ? Object.keys(surfaces) : null;
149
- }
150
-
151
- /** The project's installed `agents.json`, else the package's; null when unreadable. */
152
- export function readAgentsJson(cwd) {
153
- for (const path of [join(cwd, '.morph', 'framework', 'agents.json'), PACKAGE_AGENTS]) {
154
- if (!existsSync(path)) continue;
155
- try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return null; }
156
- }
157
- return null;
158
- }
159
-
160
- function namesSurface(answer, surfaces) {
161
- const norm = normalizeLabel(answer);
162
- return surfaces.some((key) => new RegExp(`(^|[^a-z0-9-])${key}([^a-z0-9-]|$)`).test(norm));
163
- }
164
-
165
133
  // Sem nenhuma `## Tela:`, só o formato antigo é legado; qualquer outro heading é "nenhuma tela
166
134
  // declarada", senão trocar o heading pularia o checklist.
167
- export function auditScreenChecklist(markdown, { surfaces = null } = {}) {
135
+ export function auditScreenChecklist(markdown) {
168
136
  const parsed = parseScreens(markdown);
169
137
  const problems = [];
170
138
  if (parsed.length === 0) {
@@ -185,18 +153,25 @@ export function auditScreenChecklist(markdown, { surfaces = null } = {}) {
185
153
  } else {
186
154
  if (missing.length) problems.push(`tela "${screen.name}": falta ${missing.map(labelOf).join(', ')}`);
187
155
  if (unanswered.length) problems.push(`tela "${screen.name}": sem resposta em ${unanswered.map(labelOf).join(', ')}`);
188
- const surface = screen.answers.get('superficie');
189
- if (surfaces?.length && surface !== undefined && !isPlaceholder(surface) && !isNotApplicable(surface)
190
- && !namesSurface(surface, surfaces)) {
191
- problems.push(`tela "${screen.name}": Superfície "${surface}" não é uma das superfícies da ui-designer (${surfaces.join(', ')})`);
192
- }
193
156
  }
194
- return { name: screen.name, hasChecklist: screen.hasChecklist, missing, unanswered, prototype: screen.prototype };
157
+ return { name: screen.name, hasChecklist: screen.hasChecklist, missing, unanswered, prototype: screen.prototype, states: statesOf(screen) };
195
158
  });
196
159
  return { legacy: false, screens, problems };
197
160
  }
198
161
 
199
- // Linha **Protótipo:** da tela + o HTML + os prints do laço de autocrítica ao lado dele.
162
+ /** The states the prototype must switch to, by the checklist items that apply to the screen. */
163
+ export const PROTOTYPE_STATES = { estados: ['vazio', 'carregando'], erro: ['erro'] };
164
+ export const STATE_PRINT_WIDTH = 390;
165
+
166
+ function statesOf(screen) {
167
+ return Object.entries(PROTOTYPE_STATES).flatMap(([id, states]) => {
168
+ const answer = screen.answers.get(id);
169
+ return answer && !isPlaceholder(answer) && !isNotApplicable(answer) ? states : [];
170
+ });
171
+ }
172
+
173
+ // Linha **Protótipo:** da tela + o HTML + os prints do laço de autocrítica ao lado dele; cada estado
174
+ // que o checklist diz que se aplica tem `data-estado` no HTML e o print de celular.
200
175
  export function auditPrototypes(screens, designDir) {
201
176
  const problems = [];
202
177
  for (const screen of screens) {
@@ -205,9 +180,19 @@ export function auditPrototypes(screens, designDir) {
205
180
  continue;
206
181
  }
207
182
  const base = screen.prototype.replace(/\.html$/i, '');
208
- const expected = [screen.prototype, ...PROTOTYPE_WIDTHS.map((width) => `${base}-${width}.png`)];
183
+ const states = screen.states ?? [];
184
+ const expected = [
185
+ screen.prototype,
186
+ ...PROTOTYPE_WIDTHS.map((width) => `${base}-${width}.png`),
187
+ ...states.map((state) => `${base}-${state}-${STATE_PRINT_WIDTH}.png`),
188
+ ];
209
189
  const missing = expected.filter((rel) => !existsSync(join(designDir, rel)));
210
190
  if (missing.length) problems.push(`tela "${screen.name}": falta ${missing.map((rel) => `1-design/${rel}`).join(', ')}`);
191
+ const html = existsSync(join(designDir, screen.prototype)) ? readFileSync(join(designDir, screen.prototype), 'utf8') : '';
192
+ const unswitchable = html ? states.filter((state) => !new RegExp(`data-estado\\s*=\\s*["']${state}["']`).test(html)) : [];
193
+ if (unswitchable.length) {
194
+ problems.push(`tela "${screen.name}": o protótipo não alterna para ${unswitchable.join(', ')} (falta \`data-estado="…"\` no seletor de estado)`);
195
+ }
211
196
  }
212
197
  return problems;
213
198
  }
@@ -216,7 +201,7 @@ export function auditPrototypes(screens, designDir) {
216
201
  export function collectScreenChecklist(cwd, featureName, gateName) {
217
202
  const mockups = readMockups(cwd, featureName);
218
203
  if (mockups === null) return { mockups: false, legacy: false, screens: [], problems: [] };
219
- const audit = auditScreenChecklist(mockups, { surfaces: knownSurfaces(cwd) });
204
+ const audit = auditScreenChecklist(mockups);
220
205
  const signal = { mockups: true, legacy: audit.legacy, screens: audit.screens.map((s) => s.name), problems: audit.problems };
221
206
  if (audit.legacy) signal.exempt = LEGACY_MOCKUP_EXEMPTION;
222
207
  if (gateName === 'uiux' && !audit.legacy) {
@@ -255,7 +240,7 @@ export function checklistCoverage(designMarkdown, reportSection) {
255
240
  for (const screen of parseScreens(designMarkdown)) {
256
241
  const done = covered.get(screenKey(screen.name)) ?? new Set();
257
242
  const owed = [...screen.answers]
258
- .filter(([id, answer]) => !NOT_VERIFIED_AT_REVIEW.has(id) && !isPlaceholder(answer) && !isNotApplicable(answer))
243
+ .filter(([, answer]) => !isPlaceholder(answer) && !isNotApplicable(answer))
259
244
  .map(([id]) => id)
260
245
  .filter((id) => !done.has(id));
261
246
  if (owed.length) gaps.push(`${screen.name}: ${owed.map(labelOf).join(', ')}`);
@@ -1,15 +1,14 @@
1
- import { readdirSync } from 'fs';
1
+ import { existsSync, readdirSync, readFileSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { ensureDotnetBuildIgnores } from '../../utils/file-copier.js';
4
4
  import { getProjectTags } from '../stack-filter.js';
5
5
  import { readMorphConfig } from '../tasks/test-runner.js';
6
- import { readPlanChecks } from '../plan/checks.js';
6
+ import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
7
7
  import { WORKTREE_DIR_NAMES } from '../worktree/dirs.js';
8
8
 
9
- export const DOTNET_IMPLEMENTERS = ['dotnet-senior', 'ef-modeler', 'maf-expert'];
10
-
11
9
  const SKIPPED_DIRS = new Set([...WORKTREE_DIR_NAMES, 'node_modules', 'bin', 'obj']);
12
10
  const DOTNET_PROJECT_FILE = /\.(?:csproj|slnx?)$/i;
11
+ const DOTNET_IN_PLAN = /\.(?:cs|csproj|slnx?)\b|\bdotnet\s+(?:test|build|new)\b/i;
13
12
  const MAX_DEPTH = 6;
14
13
 
15
14
  export function hasDotnetProjectFile(root, depth = 0) {
@@ -21,16 +20,16 @@ export function hasDotnetProjectFile(root, depth = 0) {
21
20
  && hasDotnetProjectFile(join(root, e.name), depth + 1));
22
21
  }
23
22
 
24
- function planHasDotnetSlice(tree, featureName) {
23
+ function planMentionsDotnet(tree, featureName) {
25
24
  if (!featureName) return false;
26
- const { checks } = readPlanChecks(tree, featureName);
27
- return (checks?.slices || []).some((slice) => DOTNET_IMPLEMENTERS.includes(String(slice.persona || '').replace(/^morph-/, '')));
25
+ const path = getAbsoluteOutputPath(tree, featureName, 'plan');
26
+ try { return existsSync(path) && DOTNET_IN_PLAN.test(readFileSync(path, 'utf8')); } catch { return false; }
28
27
  }
29
28
 
30
- // The plan's slices are checked because a greenfield tree has no stack or .csproj yet.
29
+ // The plan is read because a greenfield tree has no stack or .csproj yet.
31
30
  export function isDotnetProject(tree, { featureName, configRoot = tree } = {}) {
32
31
  if (getProjectTags(readMorphConfig(configRoot)).includes('dotnet')) return true;
33
- return planHasDotnetSlice(tree, featureName) || hasDotnetProjectFile(tree);
32
+ return planMentionsDotnet(tree, featureName) || hasDotnetProjectFile(tree);
34
33
  }
35
34
 
36
35
  /** Returns the `.gitignore` entries added; `[]` when not .NET or on any error. */
@@ -95,36 +95,16 @@ function addRun(slot, durationMs) {
95
95
  const REVIEW_NAME = /^(?:ver|rev|sec|g3|gate3|eval|review|reviewer|security)\d*-/;
96
96
  const isReviewAgent = (type) => REVIEW_AGENT_TYPES.has(type) || type === 'morph-evaluator' || REVIEW_NAME.test(String(type ?? ''));
97
97
 
98
- // Same split as the test-budget hook: review first, then an unfiltered verify, then the round count.
99
98
  // A run refused by the full heavy queue ran no test.
100
99
  function testOutsideVerify(bashEvents) {
101
- const out = {
102
- total: newTimeSlot(),
103
- subagentVerify: newTimeSlot(),
104
- implementerRound3Plus: newTimeSlot(),
105
- reviewTests: { ...newTimeSlot(), byPersona: newTimeSlot() },
106
- };
107
- const byAgent = new Map();
100
+ const out = { total: newTimeSlot(), reviewTests: { ...newTimeSlot(), byPersona: newTimeSlot() } };
108
101
  for (const e of bashEvents) {
109
102
  if (e.event === 'PostToolUseFailure' && e.exitCode === QUEUE_FULL_EXIT) continue;
110
- const verify = morphVerifyCall(e.command);
111
- if (!verify && !testRunKind(e.command)) continue;
103
+ if (!morphVerifyCall(e.command) && !testRunKind(e.command)) continue;
112
104
  addRun(out.total, e.durationMs);
113
- if (!e.agentId) continue;
114
- if (isReviewAgent(e.agentType)) {
115
- addRun(out.reviewTests, e.durationMs);
116
- if (REVIEW_AGENT_TYPES.has(e.agentType)) addRun(out.reviewTests.byPersona, e.durationMs);
117
- continue;
118
- }
119
- if (!byAgent.has(e.agentId)) byAgent.set(e.agentId, []);
120
- byAgent.get(e.agentId).push({ e, unfilteredVerify: Boolean(verify && !verify.filtered) });
121
- }
122
- for (const runs of byAgent.values()) {
123
- runs.sort((a, b) => String(a.e.ts ?? '').localeCompare(String(b.e.ts ?? '')));
124
- runs.forEach(({ e, unfilteredVerify }, round) => {
125
- if (unfilteredVerify) addRun(out.subagentVerify, e.durationMs);
126
- else if (round >= 2) addRun(out.implementerRound3Plus, e.durationMs);
127
- });
105
+ if (!e.agentId || !isReviewAgent(e.agentType)) continue;
106
+ addRun(out.reviewTests, e.durationMs);
107
+ if (REVIEW_AGENT_TYPES.has(e.agentType)) addRun(out.reviewTests.byPersona, e.durationMs);
128
108
  }
129
109
  return out;
130
110
  }
@@ -164,7 +144,7 @@ function reuseStateOf(e) {
164
144
  return { state: 'absent', reason: null };
165
145
  }
166
146
 
167
- const newGateSlot = () => ({ decisions: 0, paused: 0, autoPassed: 0, approvedAfterPause: 0, reworkedAfterPause: 0, unapproved: 0, bySignal: {} });
147
+ const newGateSlot = () => ({ decisions: 0, paused: 0, pausedForHuman: 0, pausedUnattributed: 0, autoPassed: 0, approvedAfterPause: 0, reworkedAfterPause: 0, unapproved: 0, bySignal: {} });
168
148
 
169
149
  // A pause stays open until the next approve or a decision that no longer pauses on the same signal:
170
150
  // re-running gate-check over the same unchanged pause is not rework.
@@ -173,7 +153,7 @@ function trackGate(gates, open, e) {
173
153
  const slot = gates[e.gate] ||= newGateSlot();
174
154
  const key = `${e.feature || ''}\u0000${e.gate}`;
175
155
  const pending = open.get(key);
176
- const bySignal = (signal) => slot.bySignal[signal || 'desconhecido'] ||= { paused: 0, approvedAfterPause: 0, reworkedAfterPause: 0 };
156
+ const bySignal = (signal) => slot.bySignal[signal || 'desconhecido'] ||= { paused: 0, pausedForHuman: 0, pausedUnattributed: 0, approvedAfterPause: 0, reworkedAfterPause: 0 };
177
157
  const credit = (field) => {
178
158
  slot[field] += 1;
179
159
  bySignal(pending.signal)[field] += 1;
@@ -189,12 +169,30 @@ function trackGate(gates, open, e) {
189
169
  return;
190
170
  }
191
171
  slot.decisions += 1;
192
- if (pending && e.pause && e.signal === pending.signal) return;
172
+ const resolver = e.resolver ?? 'human';
173
+ if (pending && e.pause && e.signal === pending.signal) {
174
+ // Same signal, new owner: the session ran out of attempts and the pause escalated to the human.
175
+ if (pending.resolver === 'session' && resolver === 'human') {
176
+ slot.pausedForHuman += 1;
177
+ bySignal(pending.signal).pausedForHuman += 1;
178
+ }
179
+ pending.resolver = resolver;
180
+ return;
181
+ }
193
182
  if (pending) credit(e.source === 'manual' && !e.pause ? 'approvedAfterPause' : 'reworkedAfterPause');
194
183
  if (e.pause) {
195
184
  slot.paused += 1;
196
185
  bySignal(e.signal).paused += 1;
197
- open.set(key, { signal: e.signal });
186
+ if (resolver === 'human') {
187
+ slot.pausedForHuman += 1;
188
+ bySignal(e.signal).pausedForHuman += 1;
189
+ }
190
+ // Written before the resolver existed: counted as human above, but nobody recorded who cleared it.
191
+ if (e.resolver === undefined) {
192
+ slot.pausedUnattributed += 1;
193
+ bySignal(e.signal).pausedUnattributed += 1;
194
+ }
195
+ open.set(key, { signal: e.signal, resolver });
198
196
  } else {
199
197
  slot.autoPassed += 1;
200
198
  open.delete(key);
@@ -123,16 +123,8 @@ export function readReuseVerdicts(projectPath, featureName) {
123
123
  return verdicts;
124
124
  }
125
125
 
126
- // União de duas fontes cegas em momentos diferentes: `arquivos:` das fatias sobrevivem
127
- // ao commit; `git status` pega o que o plano não previu.
128
- export function collectChangedFiles(projectPath, featureName) {
126
+ export function collectChangedFiles(projectPath) {
129
127
  const files = new Set();
130
- for (const slice of readPlanChecks(projectPath, featureName).checks?.slices ?? []) {
131
- for (const file of slice.files ?? []) {
132
- if (typeof file === 'string') files.add(file.replace(/\\/g, '/'));
133
- }
134
- }
135
-
136
128
  try {
137
129
  const out = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], {
138
130
  cwd: projectPath,
@@ -21,7 +21,6 @@ import {
21
21
  evalEmptyRunProblem, evalColdCacheWarning,
22
22
  } from '../tasks/eval-node.js';
23
23
  import { runValidation } from './validation-runner.js';
24
- import { checkOutputsTraceability } from './traceability-check.js';
25
24
  import { createHeavyGate } from '../heavy/gate.js';
26
25
 
27
26
  const DEFAULT_BUILD_TIMEOUT = 1_800_000;
@@ -70,7 +69,6 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
70
69
  _runValidation = runValidation,
71
70
  _runE2eCheck = runE2eCheck,
72
71
  _e2eApplicability = e2eApplicability,
73
- _checkOutputsTraceability = checkOutputsTraceability,
74
72
  } = options;
75
73
 
76
74
  const stack = readStack(projectPath);
@@ -170,10 +168,6 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
170
168
  appendTestsFeedback(feedback, nodes.tests);
171
169
  }
172
170
 
173
- // Outside `nodes` on purpose: status comes only from the nodes, so a traceability
174
- // warning never turns red.
175
- const traceability = _checkOutputsTraceability(projectPath, featureName);
176
-
177
171
  const baseline = recordBaseline ? recordBaselineFrom(projectPath, nodes.tests) : null;
178
172
  if (baseline && !baseline.recorded) {
179
173
  feedback.push(`[baseline] não gravada: ${baseline.reason}`);
@@ -193,7 +187,6 @@ async function runVerifyGated(projectPath, featureName, options, gate) {
193
187
  status,
194
188
  nodes,
195
189
  feedback,
196
- traceability,
197
190
  ...(baseline ? { baseline } : {}),
198
191
  };
199
192
  }
@@ -18,12 +18,12 @@ import {
18
18
  import { listClaims } from '../../../framework/hooks/shared/claim-registry.js';
19
19
  import { buildPrIndex } from '../git/pr-index.js';
20
20
  import { pendingPrFeatures } from '../git/pending-prs.js';
21
- import { sliceProgress, sliceProgressFrom } from '../../../framework/hooks/shared/slice-progress.js';
21
+ import { implementProgress, implementProgressFrom } from '../../../framework/hooks/shared/report-progress.js';
22
22
 
23
23
  const counts = (progress) => (progress ? { done: progress.done, total: progress.total } : null);
24
24
 
25
- // A root-resident feature's reports live on `morph/{f}`; the root tree only has the pre-work copy.
26
- function sliceCounts(featureDir, { root, feature, featureBranch } = {}) {
25
+ // A root-resident feature's report lives on `morph/{f}`; the root tree only has the pre-work copy.
26
+ function scenarioCounts(featureDir, { root, feature, featureBranch } = {}) {
27
27
  if (featureBranch && root && feature) {
28
28
  const show = (rel) => {
29
29
  try {
@@ -32,21 +32,10 @@ function sliceCounts(featureDir, { root, feature, featureBranch } = {}) {
32
32
  return null;
33
33
  }
34
34
  };
35
- const onBranch = sliceProgressFrom(show('2-plan/checks.json'), (id) => show(`3-implement/reports/${id}.md`));
35
+ const onBranch = implementProgressFrom(show('2-plan/checks.json'), show('3-implement/report.md'), (id) => show(`3-implement/reports/${id}.md`));
36
36
  if (onBranch) return counts(onBranch);
37
37
  }
38
- return counts(sliceProgress(featureDir));
39
- }
40
-
41
- function handoffInfo(featureDir) {
42
- try {
43
- const path = join(featureDir, 'handoff.md');
44
- if (!existsSync(path)) return { exists: false, generatedAt: null };
45
- const generatedAt = readFileSync(path, 'utf8').match(/gerado em (\d{4}-\d{2}-\d{2}T[\d:.]+Z)/)?.[1] || null;
46
- return { exists: true, generatedAt };
47
- } catch {
48
- return { exists: false, generatedAt: null };
49
- }
38
+ return counts(implementProgress(featureDir));
50
39
  }
51
40
 
52
41
  function lastCommitRelative(root, branch) {
@@ -132,11 +121,10 @@ function buildEntry(feature, { location, path, branch, root, allocations, blockS
132
121
  location,
133
122
  path,
134
123
  branch,
135
- slices: sliceCounts(featureDir, { root, feature, featureBranch }),
124
+ scenarios: scenarioCounts(featureDir, { root, feature, featureBranch }),
136
125
  env: alloc && Number.isInteger(alloc.base)
137
126
  ? { base: alloc.base, range: [alloc.base, alloc.base + blockSize - 1] }
138
127
  : null,
139
- handoff: handoffInfo(featureDir),
140
128
  lastCommit: location === 'worktree' && branch ? lastCommitRelative(root, branch) : null,
141
129
  dirty: location === 'worktree' ? dirtyCount(path) : null,
142
130
  remote: buildRemote(feature, branch, heads, prs),
@@ -221,9 +209,8 @@ export function scanFleet(cwd = process.cwd()) {
221
209
  location: 'pr-pending',
222
210
  path: null,
223
211
  branch: p.branch,
224
- slices: null,
212
+ scenarios: null,
225
213
  env: null,
226
- handoff: { exists: false, generatedAt: null },
227
214
  lastCommit: null,
228
215
  dirty: null,
229
216
  remote: { branch: p.branch, pushed: true, sha: null, pr: { number: p.number, state: p.state } },
@@ -13,40 +13,8 @@ import { buildPrIndex } from '../git/pr-index.js';
13
13
  import { classifyBranch, commitsAhead, resolveBaseRef } from '../git/branch-classifier.js';
14
14
  import { auditDockerStacks } from './docker-audit.js';
15
15
  import { auditFinishBackups } from './finish-backup.js';
16
- import { planAgentReap } from './agent-reaper.js';
17
16
 
18
17
  const norm = (p) => resolve(p).replace(/\\/g, '/');
19
- const AGENT_TASK = /^agent-[0-9a-f]+$/;
20
-
21
- const AGENT_FIX = {
22
- 'stopped-clean': 'morph-spec worktree reap (ou `morph-spec doctor --worktrees --prune`)',
23
- 'orphan-branch': 'morph-spec worktree reap (ou `morph-spec doctor --worktrees --prune`)',
24
- 'directory-missing': 'morph-spec doctor --worktrees --prune',
25
- locked: 'a árvore está com `git worktree lock`; `git worktree unlock <path>` libera, se o lock já não vale',
26
- unknown: 'não foi possível provar que é seguro remover — deixe como está',
27
- };
28
-
29
- function agentLeftoverIssue(item) {
30
- const where = item.path ?? item.branch;
31
- const reapable = item.state === 'reapable';
32
- const fix = AGENT_FIX[item.reason]
33
- ?? (item.reason === 'exclusive-commits'
34
- ? `integre a branch (\`git merge --no-ff ${item.branch}\` no orquestrador) antes de remover — ela é o único exemplar`
35
- : item.reason === 'dirty'
36
- ? `confira o trabalho na árvore (commite ou descarte) e rode \`morph-spec worktree remove --target "${item.path}"\``
37
- : `o harness não registrou o fim do agente; se ele já terminou, \`morph-spec worktree remove --target "${item.path}"\``);
38
- return {
39
- type: 'agent-worktree-leftover',
40
- severity: 'info',
41
- ...(item.path ? { path: item.path } : {}),
42
- branch: item.branch,
43
- reapable,
44
- detail: reapable
45
- ? `${where} é sobra de sub-agent que já parou, sem trabalho pendente`
46
- : `${where} é sobra de sub-agent com trabalho a preservar (${item.reason}${item.detail ? `: ${item.detail}` : ''})`,
47
- fix,
48
- };
49
- }
50
18
 
51
19
  function knownFeatures(root) {
52
20
  const names = new Set();
@@ -197,7 +165,6 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
197
165
  }
198
166
 
199
167
  if (task) {
200
- if (AGENT_TASK.test(task)) continue; // reported once, by the agent plan below
201
168
  const work = unintegratedWork(root, entry.branch, prIndex);
202
169
  if (work.unintegrated) {
203
170
  issues.push({
@@ -262,10 +229,6 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
262
229
  }
263
230
  }
264
231
 
265
- for (const item of planAgentReap(root, { cwd }).items) {
266
- if (item.state === 'reapable' || item.state === 'preserved') issues.push(agentLeftoverIssue(item));
267
- }
268
-
269
232
  for (const d of dirsOnDisk(root)) {
270
233
  if (registeredPaths.has(d.path)) continue;
271
234
  issues.push({