@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 (377) hide show
  1. package/CLAUDE.md +51 -43
  2. package/README.md +39 -38
  3. package/bin/morph-spec.js +41 -113
  4. package/bin/validate.js +1 -147
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +86 -21
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-status.md +4 -4
  17. package/framework/docs/CLI.md +574 -286
  18. package/framework/evals/review-checklist.md +82 -41
  19. package/framework/hooks/README.md +10 -11
  20. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  21. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  22. package/framework/hooks/claude-code/core/chain.js +4 -65
  23. package/framework/hooks/claude-code/core/comment-guard.js +3 -17
  24. package/framework/hooks/claude-code/core/denylist.json +33 -3
  25. package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
  26. package/framework/hooks/claude-code/core/gate-guard.js +9 -78
  27. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  28. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  29. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  30. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  31. package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
  32. package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
  33. package/framework/hooks/claude-code/core/state-sync.js +15 -85
  34. package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
  35. package/framework/hooks/claude-code/core/validate-completion.js +37 -147
  36. package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
  37. package/framework/hooks/claude-code/core/worktree-context.js +15 -114
  38. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  39. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  40. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  41. package/framework/hooks/claude-code/statusline.py +55 -41
  42. package/framework/hooks/package.json +3 -0
  43. package/framework/hooks/shared/activity-logger.js +9 -89
  44. package/framework/hooks/shared/agent-reap.js +2 -17
  45. package/framework/hooks/shared/build-check.js +11 -76
  46. package/framework/hooks/shared/claim-registry.js +9 -119
  47. package/framework/hooks/shared/command-tokens.js +168 -44
  48. package/framework/hooks/shared/comment-rules.js +5 -20
  49. package/framework/hooks/shared/comment-scanner.js +11 -43
  50. package/framework/hooks/shared/comment-stripper.js +11 -85
  51. package/framework/hooks/shared/e2e-check.js +0 -0
  52. package/framework/hooks/shared/errata.js +59 -0
  53. package/framework/hooks/shared/feature-locator.js +11 -95
  54. package/framework/hooks/shared/frontend-locations.js +4 -0
  55. package/framework/hooks/shared/gate-question.js +12 -0
  56. package/framework/hooks/shared/git-exclude.js +3 -20
  57. package/framework/hooks/shared/git-paths.js +8 -104
  58. package/framework/hooks/shared/graph-freshness.js +22 -166
  59. package/framework/hooks/shared/graph-refresh.js +21 -149
  60. package/framework/hooks/shared/heavy-command.js +4 -12
  61. package/framework/hooks/shared/hook-response.js +8 -83
  62. package/framework/hooks/shared/morph-cli.js +8 -52
  63. package/framework/hooks/shared/payload-utils.js +10 -132
  64. package/framework/hooks/shared/phase-utils.js +14 -72
  65. package/framework/hooks/shared/redact-secrets.js +4 -15
  66. package/framework/hooks/shared/reparse-scan.js +6 -66
  67. package/framework/hooks/shared/slice-progress.js +99 -0
  68. package/framework/hooks/shared/source-files.js +4 -17
  69. package/framework/hooks/shared/spawn-command.js +23 -0
  70. package/framework/hooks/shared/state-reader.js +51 -247
  71. package/framework/hooks/shared/state-writer.js +9 -54
  72. package/framework/hooks/shared/stdin-reader.js +1 -11
  73. package/framework/hooks/shared/telemetry-logger.js +7 -67
  74. package/framework/hooks/shared/transcript-usage.js +7 -29
  75. package/framework/hooks/shared/worktree-root.js +7 -29
  76. package/framework/mcp/registry.json +3 -4
  77. package/framework/phases.json +23 -39
  78. package/framework/rules/csharp-standards.md +1 -1
  79. package/framework/rules/frontend-standards.md +1 -0
  80. package/framework/rules/hangfire-standards.md +1 -1
  81. package/framework/rules/neon-standards.md +1 -1
  82. package/framework/schemas/config.schema.json +14 -9
  83. package/framework/schemas/feature.schema.json +15 -50
  84. package/framework/skills/README.md +3 -4
  85. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  86. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  87. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  88. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  89. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  90. package/framework/skills/core/morph-media/SKILL.md +1 -1
  91. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  92. package/framework/skills/core/morph-review/SKILL.md +111 -136
  93. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  94. package/framework/skills/core/morph-uiux/SKILL.md +94 -152
  95. package/framework/standards/STANDARDS.json +109 -1219
  96. package/framework/standards/STANDARDS.meta.json +1319 -0
  97. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  98. package/framework/standards/ai-agents/agent-session.md +1 -1
  99. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  100. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  101. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  102. package/framework/standards/ai-agents/guardrails.md +1 -1
  103. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  104. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  105. package/framework/standards/ai-agents/production.md +1 -1
  106. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  107. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  108. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  109. package/framework/standards/ai-agents/setup.md +3 -3
  110. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  111. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  112. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  113. package/framework/standards/ai-agents/workflows.md +1 -1
  114. package/framework/standards/architecture/code-comments.md +7 -7
  115. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  116. package/framework/standards/backend/api/validation.md +1 -1
  117. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  118. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  119. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  120. package/framework/standards/data/nosql/cache/redis.md +1 -1
  121. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  122. package/framework/standards/frontend/admin-app/forms.md +1 -1
  123. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  124. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  125. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  126. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  127. package/framework/templates/CLAUDE.md.core.template +3 -3
  128. package/framework/templates/README.md +2 -3
  129. package/framework/templates/REGISTRY.json +15 -179
  130. package/framework/templates/docs/plan.md +72 -0
  131. package/framework/templates/docs/ui-mockups.md +4 -0
  132. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  134. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  135. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  136. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  137. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  138. package/package.json +1 -1
  139. package/src/commands/context/context-refresh.js +0 -16
  140. package/src/commands/dashboard/dashboard.js +2 -8
  141. package/src/commands/e2e/e2e.js +18 -105
  142. package/src/commands/graph/affected.js +0 -14
  143. package/src/commands/graph/build.js +11 -50
  144. package/src/commands/graph/explain.js +0 -16
  145. package/src/commands/graph/status.js +1 -12
  146. package/src/commands/mcp/mcp-setup.js +4 -62
  147. package/src/commands/observability/cost.js +17 -111
  148. package/src/commands/observability/telemetry.js +1 -14
  149. package/src/commands/project/branch.js +4 -45
  150. package/src/commands/project/claim.js +4 -48
  151. package/src/commands/project/doctor-procs.js +1 -1
  152. package/src/commands/project/doctor.js +53 -307
  153. package/src/commands/project/env.js +1 -27
  154. package/src/commands/project/fleet.js +6 -35
  155. package/src/commands/project/heavy.js +3 -3
  156. package/src/commands/project/init.js +8 -68
  157. package/src/commands/project/install-plugin-cmd.js +0 -1
  158. package/src/commands/project/setup-infra-cmd.js +2 -1
  159. package/src/commands/project/stack-scan.js +58 -0
  160. package/src/commands/project/standards.js +65 -72
  161. package/src/commands/project/status.js +28 -121
  162. package/src/commands/project/update.js +31 -223
  163. package/src/commands/project/worktree.js +109 -883
  164. package/src/commands/scaffold/scaffold.js +0 -25
  165. package/src/commands/state/advance.js +65 -120
  166. package/src/commands/state/approve.js +87 -229
  167. package/src/commands/state/archive.js +23 -139
  168. package/src/commands/state/auto-approval.js +65 -0
  169. package/src/commands/state/create.js +49 -109
  170. package/src/commands/state/delete.js +11 -63
  171. package/src/commands/state/finish.js +105 -494
  172. package/src/commands/state/gate-check.js +74 -183
  173. package/src/commands/state/gate-decision.js +8 -42
  174. package/src/commands/state/handoff.js +4 -31
  175. package/src/commands/state/list.js +21 -91
  176. package/src/commands/state/report-check.js +134 -338
  177. package/src/commands/state/retype.js +12 -28
  178. package/src/commands/validation/index.js +0 -3
  179. package/src/commands/validation/mutate.js +79 -19
  180. package/src/commands/validation/plan-check.js +81 -0
  181. package/src/commands/validation/validate-feature.js +52 -216
  182. package/src/commands/validation/verify.js +45 -160
  183. package/src/core/paths/output-schema.js +9 -155
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +2 -30
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +32 -395
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +21 -0
  191. package/src/core/work-types.js +10 -45
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
  194. package/src/dashboard/app/components/CommandPalette.jsx +4 -11
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +3 -9
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/pages/Activity.jsx +1 -1
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
  211. package/src/dashboard/app/pages/Features.jsx +9 -16
  212. package/src/dashboard/app/pages/Guide.jsx +2 -3
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +14 -83
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +1 -7
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +92 -113
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +4 -7
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/lib/ai/ai-checks.js +7 -52
  233. package/src/lib/ai/ai-project-scan.js +14 -108
  234. package/src/lib/ai/code-audit.js +7 -63
  235. package/src/lib/ai/pin-drift.js +8 -51
  236. package/src/lib/ai/registry-audit.js +11 -90
  237. package/src/lib/ci/ci-verify.js +14 -13
  238. package/src/lib/ci/github-ci.js +9 -37
  239. package/src/lib/ci/workflow-scaffold.js +2 -10
  240. package/src/lib/detectors/claude-config-detector.js +13 -74
  241. package/src/lib/detectors/index.js +8 -59
  242. package/src/lib/e2e/compose-env.js +5 -66
  243. package/src/lib/e2e/stack-teardown.js +6 -40
  244. package/src/lib/generators/architecture-scanner.js +18 -156
  245. package/src/lib/generators/handoff-generator.js +37 -112
  246. package/src/lib/generators/scaffold-generator.js +5 -101
  247. package/src/lib/generators/settings-generator.js +11 -122
  248. package/src/lib/git/branch-classifier.js +11 -84
  249. package/src/lib/git/default-branch.js +14 -169
  250. package/src/lib/git/evidence-staleness.js +2 -1
  251. package/src/lib/git/gh.js +2 -17
  252. package/src/lib/git/pending-prs.js +6 -37
  253. package/src/lib/git/pr-files.js +3 -28
  254. package/src/lib/git/pr-index.js +3 -34
  255. package/src/lib/git/tree-fingerprint.js +17 -17
  256. package/src/lib/git/tree-state.js +6 -33
  257. package/src/lib/graph/god-nodes.js +2 -22
  258. package/src/lib/graph/graph-health.js +3 -24
  259. package/src/lib/graph/graph-loader.js +5 -56
  260. package/src/lib/graph/graphify-runtime.js +7 -37
  261. package/src/lib/graph/graphifyignore.js +8 -74
  262. package/src/lib/graph/sanitize.js +16 -136
  263. package/src/lib/heavy/cleanup.js +2 -6
  264. package/src/lib/installers/effects.js +8 -93
  265. package/src/lib/installers/mcp-installer.js +9 -169
  266. package/src/lib/installers/memory-installer.js +8 -33
  267. package/src/lib/installers/templates-reconcile.js +5 -60
  268. package/src/lib/mutation/commands.js +3 -2
  269. package/src/lib/mutation/dotnet-introspection.js +281 -0
  270. package/src/lib/mutation/plan.js +14 -6
  271. package/src/lib/mutation/runner.js +1 -1
  272. package/src/lib/mutation/subset.js +47 -13
  273. package/src/lib/plan/artifact-vocabulary.js +40 -0
  274. package/src/lib/plan/checks.js +208 -0
  275. package/src/lib/plan/plan-parser.js +204 -0
  276. package/src/lib/plan/plan-template.js +18 -0
  277. package/src/lib/projects/registry.js +7 -44
  278. package/src/lib/review-reports.js +204 -0
  279. package/src/lib/risk-detector.js +131 -274
  280. package/src/lib/specs/adr-index.js +38 -85
  281. package/src/lib/specs/design-seed.js +18 -0
  282. package/src/lib/specs/frontmatter.js +31 -0
  283. package/src/lib/specs/gfm-table.js +11 -0
  284. package/src/lib/specs/revocations.js +31 -102
  285. package/src/lib/specs/screen-checklist.js +56 -132
  286. package/src/lib/stack/dotnet-build-ignores.js +43 -0
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +260 -0
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +37 -0
  291. package/src/lib/standards/sections.js +47 -0
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +23 -179
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -313
  296. package/src/lib/tasks/test-runner.js +107 -447
  297. package/src/lib/telemetry/metrics.js +13 -103
  298. package/src/lib/telemetry/telemetry-reader.js +13 -238
  299. package/src/lib/telemetry/verify-event.js +4 -34
  300. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  301. package/src/lib/validators/code-budget/index.js +0 -7
  302. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
  303. package/src/lib/validators/contracts/index.js +0 -6
  304. package/src/lib/validators/nextjs/index.js +0 -5
  305. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  306. package/src/lib/validators/packages/index.js +0 -6
  307. package/src/lib/validators/packages/package-validator.js +6 -56
  308. package/src/lib/validators/reuse/index.js +0 -8
  309. package/src/lib/validators/reuse/reuse-validator.js +24 -137
  310. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  311. package/src/lib/validators/traceability-check.js +54 -156
  312. package/src/lib/validators/validation-runner.js +20 -125
  313. package/src/lib/validators/verify-runner.js +152 -575
  314. package/src/lib/work-type-detector.js +5 -67
  315. package/src/lib/worktree/docker-audit.js +17 -137
  316. package/src/lib/worktree/env-allocator.js +10 -98
  317. package/src/lib/worktree/feature-truth.js +12 -115
  318. package/src/lib/worktree/finish-backup.js +32 -242
  319. package/src/lib/worktree/fleet-scanner.js +31 -126
  320. package/src/lib/worktree/ignored-triage.js +15 -91
  321. package/src/lib/worktree/vscode-settings.js +2 -3
  322. package/src/lib/worktree/worktree-audit.js +21 -213
  323. package/src/lib/worktree/worktree-include.js +3 -5
  324. package/src/scripts/global-install.js +3 -9
  325. package/src/scripts/install-plugin.js +1 -13
  326. package/src/scripts/setup-infra.js +17 -138
  327. package/src/utils/agents-installer.js +92 -272
  328. package/src/utils/banner.js +4 -25
  329. package/src/utils/claude-md-injector.js +1 -37
  330. package/src/utils/claude-settings-manager.js +1 -87
  331. package/src/utils/file-copier.js +32 -90
  332. package/src/utils/hooks-installer.js +26 -296
  333. package/src/utils/skills-installer.js +4 -102
  334. package/src/utils/version-checker.js +4 -83
  335. package/framework/commands/morph-proposal.md +0 -57
  336. package/framework/evals/trace-schema.md +0 -67
  337. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  338. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  339. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  340. package/framework/hooks/shared/tasks-validator.js +0 -111
  341. package/framework/rules/morph-task-tracking.md +0 -54
  342. package/framework/rules/morph-workflow.md +0 -244
  343. package/framework/schemas/tasks.schema.json +0 -79
  344. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  345. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  346. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  347. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  348. package/framework/templates/docs/proposal.md +0 -192
  349. package/framework/templates/docs/spec.md +0 -57
  350. package/framework/templates/docs/ui-components.md +0 -52
  351. package/framework/templates/docs/ui-design-system.md +0 -76
  352. package/framework/templates/docs/ui-flows.md +0 -167
  353. package/framework/templates/examples/spec-examples.md +0 -91
  354. package/framework/templates/feature/decisions.md +0 -124
  355. package/framework/templates/feature/recap.md +0 -145
  356. package/framework/templates/tasks.template.json +0 -17
  357. package/src/commands/state/usecase.js +0 -143
  358. package/src/commands/tasks/dag.js +0 -181
  359. package/src/commands/utils/index.js +0 -3
  360. package/src/lib/agents/output-collector.js +0 -76
  361. package/src/lib/detectors/design-system-detector.js +0 -188
  362. package/src/lib/evaluator-report.js +0 -104
  363. package/src/lib/generators/design-system-generator.js +0 -508
  364. package/src/lib/specs/acervo-format.js +0 -70
  365. package/src/lib/specs/acervo-lifecycle.js +0 -264
  366. package/src/lib/specs/done-criteria-check.js +0 -200
  367. package/src/lib/specs/usecase-check.js +0 -301
  368. package/src/lib/specs/usecase-promoter.js +0 -301
  369. package/src/lib/specs/usecase-registry.js +0 -237
  370. package/src/lib/tasks/dispatch-planner.js +0 -319
  371. package/src/lib/tasks/dotnet-test-introspection.js +0 -632
  372. package/src/lib/tasks/mermaid-renderer.js +0 -132
  373. package/src/lib/tasks/task-classifier.js +0 -30
  374. package/src/lib/tasks/task-parser.js +0 -209
  375. package/src/lib/tasks/tasks-json-parser.js +0 -296
  376. package/src/lib/tasks/test-scope.js +0 -597
  377. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,52 +1,6 @@
1
- /**
2
- * finish-backup.js — the backups `finish` and `worktree remove` leave under
3
- * `.git/morph-spec/finish-backup/`: how one is written, how they are listed,
4
- * and how long each one lives.
5
- *
6
- * A backup exists so that removing a worktree (or shedding the root-side
7
- * scaffold) never destroys the only exemplar of a file. That is a reason to keep
8
- * the last ones, not all of them, and a worktree's `.env.e2e` — the e2e stack's
9
- * credentials — has no business living inside `.git/` indefinitely. So every
10
- * backup is subject to a retention policy, on by default:
11
- *
12
- * maxCount — the newest this many backups per namespace always stay
13
- * maxAgeDays — no age cut unless configured: a quiet month must never
14
- * empty the backups of the last features closed
15
- * secretMaxAgeDays — `.env*` files inside a backup go away after this
16
- *
17
- * `config.json → finishBackup.retention` can change the numbers or turn the
18
- * whole thing off (`false`); a project without the key gets the defaults, and a
19
- * value that cannot be read skips the prune for that run.
20
- *
21
- * A backup without its manifest was written before this retention existed. The
22
- * automatic prune (end of `finish`, end of `worktree remove`) never deletes it,
23
- * never strips its `.env*` and does not count it against the budget — it only
24
- * reports how many wait (`legacyPending`). The first prune of those is the
25
- * explicit `doctor --worktrees --prune`, which ranks them with the rest and
26
- * gives each one it keeps a manifest, so from then on they are ordinary backups.
27
- *
28
- * Layout, and what counts as ONE backup:
29
- *
30
- * {feature}-{stamp}/ the feature scope the shed discards
31
- * {feature}-{stamp}-worktree-ignored/ the worktree's exclusive gitignored
32
- * {folder}.manifest.json sidecar: what was copied, the secrets
33
- * task/{name}-{stamp}-worktree-ignored/ a namespace (task isolation trees)
34
- *
35
- * One finish writes both folders with the SAME stamp, so they are one backup —
36
- * the unit the count applies to; a folder with no same-stamp sibling counts
37
- * alone. The `task/` namespace has its own budget: a fan-out of twenty
38
- * sub-agent worktrees must not evict the backup of the feature finished a
39
- * minute earlier. The backup the calling run just wrote is never pruned by it.
40
- *
41
- * Deletion is PROVE-or-DECLINE, as everywhere else near a worktree: a backup
42
- * holding a reparse point is never removed, links are never descended, and
43
- * nothing outside the backup directory is touched. Not because `rmSync` would
44
- * follow a junction — it unlinks the link itself; what once emptied a primary
45
- * root was `git worktree remove` — but because a link inside a backup is not
46
- * something this code put there. A first-level entry that is neither a backup
47
- * nor `task/` is left alone, even empty — it is not ours to judge.
48
- */
49
-
1
+ // Folders with the SAME stamp are one backup; `task/` has its own count budget.
2
+ // A backup without manifest predates retention: only the explicit `--prune` touches it.
3
+ // Deletion is prove-or-decline: a backup holding a reparse point is never removed.
50
4
  import {
51
5
  cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync,
52
6
  rmdirSync, rmSync, unlinkSync,
@@ -62,41 +16,30 @@ export const FINISH_BACKUP_RETENTION_DEFAULTS = Object.freeze({
62
16
  secretMaxAgeDays: 7,
63
17
  });
64
18
 
65
- /** The keys whose `null` means "no limit" rather than an invalid value. */
66
19
  const NULLABLE_RETENTION_KEYS = new Set(['maxAgeDays']);
67
20
 
68
21
  const DAY_MS = 86_400_000;
69
22
  const WORKTREE_IGNORED_SUFFIX = '-worktree-ignored';
70
23
  const MANIFEST_SUFFIX = '.manifest.json';
71
24
  const NAME_RE = /^(.+)-(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z(-worktree-ignored)?$/;
72
- /** How many names a payload carries; the counts are always complete. */
73
25
  const SAMPLE = 5;
74
26
 
75
- /** @param {string} root - the primary root */
76
27
  export function morphSpecStoreDir(root) {
77
28
  return join(root, '.git', 'morph-spec');
78
29
  }
79
30
 
80
- /** @param {string} root - the primary root */
81
31
  export function finishBackupDir(root) {
82
32
  return join(morphSpecStoreDir(root), 'finish-backup');
83
33
  }
84
34
 
85
- /** The stamp in a backup's name — filesystem-safe ISO-8601. */
86
35
  export function newBackupStamp(date = new Date()) {
87
36
  return date.toISOString().replace(/[:.]/g, '-');
88
37
  }
89
38
 
90
- /** `.env`, `.env.e2e`, `.env.local`… — the files that hold a stack's secrets. */
91
39
  export function isSecretFileName(name) {
92
40
  return name.startsWith('.env');
93
41
  }
94
42
 
95
- /**
96
- * @param {string} name - one directory name under finish-backup/ (or under a namespace)
97
- * @returns {{ base: string, feature: string, stamp: string,
98
- * kind: 'feature-scope'|'worktree-ignored', createdAt: number }|null}
99
- */
100
43
  export function parseBackupName(name) {
101
44
  const m = NAME_RE.exec(name);
102
45
  if (!m) return null;
@@ -122,16 +65,8 @@ const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArr
122
65
  const shown = (v) => JSON.stringify(v) ?? String(v);
123
66
  const skippedPolicy = (reason) => ({ enabled: false, skipped: reason, ...FINISH_BACKUP_RETENTION_DEFAULTS });
124
67
 
125
- /**
126
- * The policy in force, from a parsed config.json. A config without the key
127
- * gets the defaults (retention on) and an explicit `false` turns it off. What
128
- * this cannot recognise SKIPS the prune for the run instead: the operation
129
- * deletes, and a value nobody can read — `{ enabled: false }`, `"false"`, a
130
- * `maxCount` of `0` — may well have been meant as "keep everything".
131
- *
132
- * @param {object|null} config - null when there is no config.json at all
133
- * @returns {{ enabled: boolean, skipped?: string, maxCount: number, maxAgeDays: number|null, secretMaxAgeDays: number }}
134
- */
68
+ // A value this cannot recognise SKIPS the prune instead of falling back to the
69
+ // defaults: the operation deletes, and `"false"` or `maxCount: 0` may mean "keep all".
135
70
  export function resolveBackupRetention(config) {
136
71
  const d = FINISH_BACKUP_RETENTION_DEFAULTS;
137
72
  const skip = skippedPolicy;
@@ -156,13 +91,8 @@ export function resolveBackupRetention(config) {
156
91
  return policy;
157
92
  }
158
93
 
159
- /**
160
- * Unlike `readMorphConfig`, a config.json that exists and does not parse is
161
- * not "no config": the defaults would turn on a prune its author may have
162
- * switched off in the part that failed to parse.
163
- *
164
- * @param {string} root
165
- */
94
+ // Unlike `readMorphConfig`, an unparseable config.json is not "no config": the
95
+ // defaults would turn on a prune its author may have switched off.
166
96
  export function readBackupRetention(root) {
167
97
  const path = join(root, CONFIG_FILE_NAME);
168
98
  if (!existsSync(path)) return resolveBackupRetention(null);
@@ -196,13 +126,6 @@ function isStrictlyInside(parent, child) {
196
126
  return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
197
127
  }
198
128
 
199
- /**
200
- * Every `.env*` REAL file under `dir`, relative to it. Links are neither
201
- * followed nor reported: a link is not bytes this backup holds.
202
- *
203
- * @param {string} dir
204
- * @returns {string[]}
205
- */
206
129
  export function secretFilesIn(dir) {
207
130
  const out = [];
208
131
  const stack = [''];
@@ -224,13 +147,7 @@ export function secretFilesIn(dir) {
224
147
  return out.sort();
225
148
  }
226
149
 
227
- /**
228
- * Bytes and files under `dir`, by lstat — a link counts as itself, never as
229
- * what it points at. `null` when the walk could not start.
230
- *
231
- * @param {string} dir
232
- * @returns {{ bytes: number, files: number }|null}
233
- */
150
+ /** By lstat: a link counts as itself. `null` when the walk could not start. */
234
151
  export function measureDir(dir) {
235
152
  if (!isRealDir(dir)) return null;
236
153
  let bytes = 0;
@@ -247,7 +164,7 @@ export function measureDir(dir) {
247
164
  for (const e of entries) {
248
165
  const p = join(current, e.name);
249
166
  if (e.isDirectory() && !e.isSymbolicLink()) { stack.push(p); continue; }
250
- try { bytes += lstatSync(p).size; files += 1; } catch { /* vanished */ }
167
+ try { bytes += lstatSync(p).size; files += 1; } catch {}
251
168
  }
252
169
  }
253
170
  return { bytes, files };
@@ -265,30 +182,16 @@ function readManifest(backupPath) {
265
182
  }
266
183
  }
267
184
 
268
- /**
269
- * Bookkeeping, never a precondition: a backup whose bytes are on disk is a
270
- * backup, with or without its manifest. One whose manifest failed to be written
271
- * reads as legacy, which is the safe side — the automatic prune leaves it alone.
272
- */
185
+ // Fail-open: a backup whose manifest failed reads as legacy, the safe side.
273
186
  function writeManifest(backupPath, manifest) {
274
187
  try {
275
188
  atomicWriteJson(manifestPathFor(backupPath), { version: 1, ...manifest });
276
- } catch { /* fail-open */ }
189
+ } catch {}
277
190
  }
278
191
 
279
- /**
280
- * Copy the worktree's exclusive gitignored leftovers out before the removal
281
- * deletes them. Caches are excluded at every depth, not only as top-level
282
- * entries: git reports an ignored directory as ONE entry when the directory
283
- * itself matches a pattern, so an exclusive `apps/x/` would otherwise drag its
284
- * `node_modules/` and `.next/` along. They are regenerable, and the removal
285
- * deletes them either way.
286
- *
287
- * @param {{ worktreePath: string, root: string, feature: string, rels: string[], stamp?: string }} args
288
- * @returns {string|null} the backup path, or null if ANY copy failed (the caller
289
- * must then decline the removal — a partial backup authorizes nothing) or if
290
- * none of `rels` exists anymore
291
- */
192
+ // Caches are excluded at every depth: git reports an ignored dir as ONE entry,
193
+ // so `apps/x/` would drag its `node_modules/` along.
194
+ /** `null` if ANY copy failed — the caller must then decline the removal. */
292
195
  export function writeWorktreeIgnoredBackup({ worktreePath, root, feature, rels, stamp = newBackupStamp() }) {
293
196
  const dest = join(finishBackupDir(root), `${feature}-${stamp}${WORKTREE_IGNORED_SUFFIX}`);
294
197
  const copied = [];
@@ -297,7 +200,7 @@ export function writeWorktreeIgnoredBackup({ worktreePath, root, feature, rels,
297
200
  try {
298
201
  for (const rel of rels) {
299
202
  const src = join(worktreePath, rel);
300
- if (!existsSync(src)) continue; // a directory entry already covered by its children
203
+ if (!existsSync(src)) continue;
301
204
  if (isDisposableBuildArtifact(relOf(src))) {
302
205
  excludedCaches.push(relOf(src));
303
206
  continue;
@@ -332,17 +235,8 @@ export function writeWorktreeIgnoredBackup({ worktreePath, root, feature, rels,
332
235
  return dest;
333
236
  }
334
237
 
335
- /**
336
- * Copy the feature's root-side scope to a backup OUTSIDE the working tree, so
337
- * the shed can never be the reason something is lost.
338
- *
339
- * Lives under `.git/` on purpose: it is not a working-tree path, so it can never
340
- * dirty `git status` (which the very next validation reads) and is never merged,
341
- * committed, or cleaned by `git clean`.
342
- *
343
- * @param {{ root: string, feature: string, stamp?: string }} args
344
- * @returns {string|null} absolute backup path, or null if the copy failed
345
- */
238
+ // Under `.git/` so it never dirties the `git status` the next validation reads.
239
+ /** `null` if the copy failed: the caller must not shed. */
346
240
  export function writeFeatureScopeBackup({ root, feature, stamp = newBackupStamp() }) {
347
241
  const src = join(root, '.morph', 'features', feature);
348
242
  if (!existsSync(src)) return null;
@@ -351,7 +245,7 @@ export function writeFeatureScopeBackup({ root, feature, stamp = newBackupStamp(
351
245
  mkdirSync(dirname(dest), { recursive: true });
352
246
  cpSync(src, dest, { recursive: true });
353
247
  } catch {
354
- return null; // caller MUST treat this as "do not shed"
248
+ return null;
355
249
  }
356
250
  writeManifest(dest, {
357
251
  kind: 'feature-scope',
@@ -363,26 +257,10 @@ export function writeFeatureScopeBackup({ root, feature, stamp = newBackupStamp(
363
257
  return dest;
364
258
  }
365
259
 
366
- /**
367
- * The first-level directories that hold backups instead of being one: `task/`
368
- * is where the `task/{name}` key of a task isolation tree (see
369
- * `envKeyForWorktree` in worktree.js) lands. Any other first-level directory
370
- * that is not a backup was put there by someone else — never listed, never
371
- * removed, not even empty.
372
- */
260
+ // Any other first-level directory that is not a backup is not ours: never removed.
373
261
  const BACKUP_NAMESPACES = new Set(['task']);
374
262
 
375
- /**
376
- * Every backup folder on disk: the first level, plus one level down inside a
377
- * namespace of BACKUP_NAMESPACES. A link is never descended, at either level.
378
- *
379
- * @param {string} root
380
- * `managed` is whether the folder has its manifest — the one criterion that
381
- * tells a backup of this retention from one written before it.
382
- *
383
- * @returns {Array<{ name: string, path: string, namespace: string, set: string,
384
- * feature: string, stamp: string, kind: string, createdAt: number, managed: boolean }>}
385
- */
263
+ /** `managed` = has its manifest; without one the backup predates retention. */
386
264
  export function listFinishBackups(root) {
387
265
  const base = finishBackupDir(root);
388
266
  if (!isRealDir(base)) return [];
@@ -396,7 +274,7 @@ export function listFinishBackups(root) {
396
274
  }
397
275
  for (const e of entries) {
398
276
  const path = join(dir, e.name);
399
- if (!isRealDir(path)) continue; // manifests, temp files and links
277
+ if (!isRealDir(path)) continue;
400
278
  const parsed = parseBackupName(e.name);
401
279
  if (parsed) {
402
280
  const prefix = namespace ? `${namespace}/` : '';
@@ -420,30 +298,8 @@ export function listFinishBackups(root) {
420
298
  return out;
421
299
  }
422
300
 
423
- /**
424
- * Which backups the policy removes, and which it keeps but strips of `.env*`.
425
- * Pure: the caller supplies the listing and the clock.
426
- *
427
- * Two kinds of set are never removed. The ones stamped with `protectStamps` —
428
- * the backup the calling run just wrote: a retention that runs after a backup
429
- * exists to make room for it, not to take it. And the ones stamped AFTER `now`
430
- * (a clock that ran ahead and was corrected): their age is unknowable, so they
431
- * stay and do not take a place in the count — otherwise a handful of them
432
- * would push every new backup out the moment it is written.
433
- *
434
- * A set is legacy unless EVERY folder in it has its manifest. Without
435
- * `includeLegacy` (the automatic prune) legacy sets are out of the plan
436
- * altogether — no removal, no strip, no place in the count — and only counted
437
- * in `legacy`. With it (the explicit prune) they are ranked like the rest, and
438
- * the ones kept are listed in `adopt`.
439
- *
440
- * @param {ReturnType<typeof listFinishBackups>} backups
441
- * @param {ReturnType<typeof resolveBackupRetention>} retention
442
- * @param {number} now - epoch ms
443
- * @param {{ protectStamps?: string[], includeLegacy?: boolean }} [opts]
444
- * @returns {{ sets: number, legacy: number, remove: object[], stripSecrets: object[], adopt: object[] }}
445
- * each entry a `{ set, namespace, stamp, createdAt, managed, members }`
446
- */
301
+ // Sets stamped after `now` (clock ran ahead) stay and take no place in the count,
302
+ // or a handful of them would evict every new backup the moment it is written.
447
303
  export function planBackupRetention(backups, retention, now, { protectStamps = [], includeLegacy = false } = {}) {
448
304
  const sets = new Map();
449
305
  for (const b of backups) {
@@ -486,27 +342,8 @@ export function planBackupRetention(backups, retention, now, { protectStamps = [
486
342
  return plan;
487
343
  }
488
344
 
489
- /**
490
- * Apply the policy: remove what it drops, strip `.env*` from what it keeps past
491
- * the short retention, and record the stripped names in the manifest. A backup
492
- * whose removal is declined (a reparse point inside, a locked file) is stripped
493
- * instead: the policy wanted it gone, and its secrets are the part that must not
494
- * linger. A policy that could not be read (`skipped`) prunes nothing. Never
495
- * throws — a retention that fails must never be the reason a `finish` fails.
496
- *
497
- * Legacy backups (no manifest) are touched only with `includeLegacy` — the
498
- * explicit `doctor --worktrees --prune`; otherwise they are counted in
499
- * `legacyPending`. The explicit prune writes a manifest into every legacy
500
- * backup it keeps (`adopted`), which makes it an ordinary backup from then on.
501
- *
502
- * @param {string} root
503
- * @param {{ now?: number, retention?: ReturnType<typeof resolveBackupRetention>,
504
- * protectStamps?: string[], includeLegacy?: boolean }} [opts]
505
- * `protectStamps`: the stamp(s) the calling run wrote its backup under
506
- * @returns {{ enabled: boolean, skipped?: string, removed: string[],
507
- * secretsStripped: Array<{ backup: string, files: string[] }>, declined: Array<{ backup: string, reason: string }>,
508
- * legacyPending: number, adopted: string[] }}
509
- */
345
+ // A backup whose removal is declined still loses its `.env*`. Never throws:
346
+ // a failed retention must never be the reason a `finish` fails.
510
347
  export function applyBackupRetention(root, { now = Date.now(), retention, protectStamps = [], includeLegacy = false } = {}) {
511
348
  const result = { enabled: true, removed: [], secretsStripped: [], declined: [], legacyPending: 0, adopted: [] };
512
349
  try {
@@ -539,7 +376,7 @@ export function applyBackupRetention(root, { now = Date.now(), retention, protec
539
376
  toStrip.push(m);
540
377
  continue;
541
378
  }
542
- try { rmSync(manifestPathFor(m.path), { force: true }); } catch { /* the sidecar is bookkeeping */ }
379
+ try { rmSync(manifestPathFor(m.path), { force: true }); } catch {}
543
380
  result.removed.push(m.name);
544
381
  }
545
382
  }
@@ -550,7 +387,7 @@ export function applyBackupRetention(root, { now = Date.now(), retention, protec
550
387
  try {
551
388
  unlinkSync(join(m.path, rel));
552
389
  stripped.push(rel);
553
- } catch { /* locked — the next run tries again */ }
390
+ } catch {}
554
391
  }
555
392
  if (!stripped.length) continue;
556
393
  const manifest = readManifest(m.path) ?? { kind: m.kind, feature: m.feature, createdAt: new Date(m.createdAt).toISOString() };
@@ -567,27 +404,16 @@ export function applyBackupRetention(root, { now = Date.now(), retention, protec
567
404
  result.adopted.push(m.name);
568
405
  }
569
406
 
570
- // A namespace emptied by the removals goes too; rmdir refuses anything else.
571
407
  for (const name of BACKUP_NAMESPACES) {
572
408
  const p = join(base, name);
573
409
  if (isRealDir(p)) {
574
- try { rmdirSync(p); } catch { /* not empty */ }
410
+ try { rmdirSync(p); } catch {}
575
411
  }
576
412
  }
577
- } catch { /* fail-open */ }
413
+ } catch {}
578
414
  return result;
579
415
  }
580
416
 
581
- /**
582
- * What a prune WOULD do now, without doing it. `includeLegacy` picks which
583
- * prune: without it, the automatic one (what `finish --dry-run` announces);
584
- * with it, the explicit `doctor --worktrees --prune` (what the doctor reports).
585
- *
586
- * @param {string} root
587
- * @param {{ now?: number, retention?: ReturnType<typeof resolveBackupRetention>, includeLegacy?: boolean }} [opts]
588
- * @returns {{ policy: object, sets: number, folders: number, legacy: number,
589
- * outsideRetention: number, overdueSecrets: string[] }}
590
- */
591
417
  export function inspectFinishBackups(root, { now = Date.now(), retention, includeLegacy = false } = {}) {
592
418
  const policy = retention ?? readBackupRetention(root);
593
419
  const backups = listFinishBackups(root);
@@ -603,7 +429,6 @@ export function inspectFinishBackups(root, { now = Date.now(), retention, includ
603
429
  };
604
430
  }
605
431
 
606
- /** The sentence both the audit and the store line say about what `--prune` would do. */
607
432
  function pendingParts(report, { long = false } = {}) {
608
433
  const { maxCount, maxAgeDays, secretMaxAgeDays } = report.policy;
609
434
  const parts = [];
@@ -632,16 +457,7 @@ function formatSize(bytes) {
632
457
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
633
458
  }
634
459
 
635
- /**
636
- * The default `doctor`'s line about `.git/morph-spec/`: its size and the
637
- * backups in it, in the check shape the doctor renders. The size rides in the
638
- * NAME because an `ok` line prints only its name. `ok` or `warn`, never a
639
- * failure; `null` — no line — when nothing was ever stored there.
640
- *
641
- * @param {string} root - the primary root
642
- * @param {{ now?: number }} [opts]
643
- * @returns {{ name: string, status: 'ok'|'warn', msg?: string }|null}
644
- */
460
+ // The size rides in the NAME because the doctor prints only the name of an `ok` line.
645
461
  export function morphStoreCheck(root, { now = Date.now() } = {}) {
646
462
  let store;
647
463
  let report;
@@ -659,17 +475,6 @@ export function morphStoreCheck(root, { now = Date.now() } = {}) {
659
475
  return { name, status: 'warn', msg: `${pending.join(', ')} — rode morph-spec doctor --worktrees --prune` };
660
476
  }
661
477
 
662
- /**
663
- * The retention as an audit finding, in the shape `auditWorktrees` returns:
664
- * what the explicit `--prune` would do, including the legacy backups only it
665
- * prunes. `warn`, never a failure — backups waiting for a prune are
666
- * housekeeping. A policy that could not be read is a finding too: until it is
667
- * fixed, nothing is ever pruned.
668
- *
669
- * @param {string} root
670
- * @param {{ now?: number }} [opts]
671
- * @returns {Array<{ type: string, severity: string, detail: string, fix: string }>}
672
- */
673
478
  export function auditFinishBackups(root, opts = {}) {
674
479
  let report;
675
480
  try {
@@ -697,14 +502,7 @@ export function auditFinishBackups(root, opts = {}) {
697
502
  }];
698
503
  }
699
504
 
700
- /**
701
- * A payload-sized account of one retention run: complete counts, a short
702
- * sample of names; `{ skipped }` when the policy could not be read. `null` when
703
- * the run changed nothing and nothing waits for the explicit prune.
704
- *
705
- * @param {ReturnType<typeof applyBackupRetention>} result
706
- * @returns {object|null}
707
- */
505
+ /** `null` when the run changed nothing and nothing waits for the explicit prune. */
708
506
  export function summarizeRetention(result) {
709
507
  if (!result) return null;
710
508
  if (result.skipped) return { skipped: result.skipped };
@@ -721,15 +519,7 @@ export function summarizeRetention(result) {
721
519
  return Object.keys(out).length ? out : null;
722
520
  }
723
521
 
724
- /**
725
- * The retention step every writer of backups runs after its own backup:
726
- * apply + summarize, fail-open. `protectStamps` is the stamp that backup was
727
- * written under — never pruned by this run.
728
- *
729
- * @param {string} root
730
- * @param {{ protectStamps?: string[] }} [opts]
731
- * @returns {object|null}
732
- */
522
+ /** `protectStamps`: the stamp the caller just wrote its backup under, never pruned by this run. */
733
523
  export function enforceBackupRetention(root, { protectStamps = [] } = {}) {
734
524
  try {
735
525
  return summarizeRetention(applyBackupRetention(root, { protectStamps }));