@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,36 +1,6 @@
1
- /**
2
- * branch-classifier.js — answer "is this branch's work already in the default
3
- * branch?" for a repository that squash-merges.
4
- *
5
- * WHY THIS EXISTS: a squash-merge puts the WORK in `origin/master` while
6
- * leaving none of the branch's own commits reachable from it. So
7
- * `git branch --merged origin/master` does not list the branch, and
8
- * `git branch -d` refuses it as unmerged. The only remaining command is
9
- * `git branch -D`, which is byte-identical to the command that discards real,
10
- * never-integrated work — the tool gives the user no way to tell the two apart,
11
- * and after enough of these the habit becomes "just -D it", which is how work
12
- * gets lost.
13
- *
14
- * Every branch lands in exactly one class, and only the two MERGED ones are
15
- * ever deletable:
16
- *
17
- * merged-ancestral — git itself proves it: the branch tip is an ancestor of
18
- * the default. A plain merge, or a fast-forward.
19
- * merged-squash — git cannot see it, but the evidence is positive: either
20
- * GitHub reports the PR as MERGED, or the branch's tree is
21
- * IDENTICAL to the default's (every change it carries is
22
- * already there under a different commit).
23
- * open — a PR is OPEN for it. Never deleted: the PR is the whole
24
- * reason the branch must survive.
25
- * in-worktree — checked out by a live worktree. Never deleted.
26
- * unmerged — none of the above could be PROVEN. This is the fail-safe
27
- * default and covers "no `gh`", "no origin", and "genuinely
28
- * unintegrated work" alike, because from here they are the
29
- * same thing: not proven integrated.
30
- *
31
- * Nothing here mutates. `pruneBranches` in commands/project/branch.js does the
32
- * deleting, and it deletes ONLY what this file classified as merged.
33
- */
1
+ // A squash-merge leaves the work in the default branch but none of the branch's commits,
2
+ // so `branch -d` refuses it and `-D` looks like discarding real work. Only the two
3
+ // `merged-*` classes are deletable; anything not PROVEN integrated is `unmerged`.
34
4
 
35
5
  import { execFileSync } from 'child_process';
36
6
 
@@ -38,11 +8,7 @@ function git(args, cwd) {
38
8
  return execFileSync('git', args, { cwd, stdio: 'pipe' }).toString();
39
9
  }
40
10
 
41
- /**
42
- * Local branches with their tips. Excludes nothing — the caller decides.
43
- *
44
- * @returns {Array<{ name: string, sha: string, current: boolean, worktreePath: string|null }>}
45
- */
11
+ /** @returns {Array<{ name: string, sha: string, current: boolean, worktreePath: string|null }>} */
46
12
  export function listLocalBranches(cwd) {
47
13
  try {
48
14
  const out = git(['for-each-ref', '--format=%(refname:short)%09%(objectname)%09%(HEAD)%09%(worktreepath)', 'refs/heads/'], cwd);
@@ -55,7 +21,6 @@ export function listLocalBranches(cwd) {
55
21
  }
56
22
  }
57
23
 
58
- /** Is `ancestor` reachable from `descendant`? False on any git error. */
59
24
  function isAncestor(ancestor, descendant, cwd) {
60
25
  try {
61
26
  git(['merge-base', '--is-ancestor', ancestor, descendant], cwd);
@@ -65,19 +30,8 @@ function isAncestor(ancestor, descendant, cwd) {
65
30
  }
66
31
  }
67
32
 
68
- /**
69
- * Does the branch introduce NOTHING the base does not already have?
70
- *
71
- * `git diff --quiet <base> <branch>` compares the two TREES: an empty diff
72
- * means every file the branch touched already reads exactly as the branch left
73
- * it in the base. That is precisely the state a squash-merge produces, and it
74
- * is a positive proof rather than an inference from history shape.
75
- *
76
- * Deliberately NOT `base...branch` (the three-dot form diffs against the merge
77
- * base, which for a squash-merged branch still shows the branch's own changes).
78
- *
79
- * Fail-CLOSED: any git error reads as "introduces something", never as merged.
80
- */
33
+ // Two-dot tree diff, never `base...branch`: the three-dot form still shows a squash-merged
34
+ // branch's own changes. Fail-closed: a git error never reads as merged.
81
35
  function treeIsIdentical(base, branch, cwd) {
82
36
  try {
83
37
  execFileSync('git', ['diff', '--quiet', base, branch], { cwd, stdio: 'pipe' });
@@ -87,13 +41,7 @@ function treeIsIdentical(base, branch, cwd) {
87
41
  }
88
42
  }
89
43
 
90
- /**
91
- * How many commits the branch carries that the base does not. Used only for
92
- * REPORTING — the classification never turns on it, because a squash-merged
93
- * branch legitimately has commits ahead while carrying no unintegrated work.
94
- *
95
- * @returns {number|null} null when it cannot be computed
96
- */
44
+ /** Reporting only: a squash-merged branch legitimately has commits ahead. @returns {number|null} */
97
45
  export function commitsAhead(base, branch, cwd) {
98
46
  try {
99
47
  const out = git(['rev-list', '--count', `${base}..${branch}`], cwd).trim();
@@ -105,12 +53,8 @@ export function commitsAhead(base, branch, cwd) {
105
53
  }
106
54
 
107
55
  /**
108
- * Resolve the ref to compare against: `origin/<default>` when the remote-tracking
109
- * ref exists (the authority on what has actually landed), else the local branch.
110
- *
111
- * @returns {string|null} null when neither resolves — the caller must then
112
- * classify everything as `unmerged`, because there is no baseline to prove
113
- * integration against.
56
+ * `origin/<default>` when it exists, else the local branch.
57
+ * @returns {string|null} null = no baseline, so everything is `unmerged`.
114
58
  */
115
59
  export function resolveBaseRef(defaultBranch, cwd) {
116
60
  if (!defaultBranch) return null;
@@ -123,19 +67,8 @@ export function resolveBaseRef(defaultBranch, cwd) {
123
67
  return null;
124
68
  }
125
69
 
126
- /**
127
- * Classify one branch. Pure decision logic over already-gathered facts, so the
128
- * table above is testable without a GitHub account.
129
- *
130
- * ORDER IS THE POLICY, and the two protective classes come FIRST: a branch that
131
- * is checked out, or that carries an OPEN PR, is never deleted even if its tree
132
- * happens to match the base — a PR being open means a human still has business
133
- * with it.
134
- *
135
- * @param {object} b - from listLocalBranches
136
- * @param {{ baseRef: string|null, cwd: string, pr: {number:number,state:string}|null|undefined }} ctx
137
- * @returns {{ name: string, kind: string, reason: string, pr: number|null, ahead: number|null, deletable: boolean }}
138
- */
70
+ // Order is the policy: checked-out and open-PR branches are protected before any
71
+ // merged check, even when the tree matches the base.
139
72
  export function classifyBranch(b, { baseRef, cwd, pr }) {
140
73
  const prNum = pr?.number ?? null;
141
74
  const mk = (kind, reason, deletable) => ({
@@ -163,12 +96,6 @@ export function classifyBranch(b, { baseRef, cwd, pr }) {
163
96
  return mk('unmerged', `carrega mudanças que não estão em ${baseRef} e nenhum PR MERGED comprova o contrário`, false);
164
97
  }
165
98
 
166
- /**
167
- * Classify every local branch except the default itself.
168
- *
169
- * @param {{ cwd: string, defaultBranch: string|null, prIndex: Map|null }} ctx
170
- * @returns {Array} one entry per branch, in `for-each-ref` order
171
- */
172
99
  export function classifyBranches({ cwd, defaultBranch, prIndex }) {
173
100
  const baseRef = resolveBaseRef(defaultBranch, cwd);
174
101
  return listLocalBranches(cwd)
@@ -1,21 +1,5 @@
1
1
  import { execFileSync } from 'child_process';
2
2
 
3
- /**
4
- * Git helpers shared by `create` (which branches `morph/{f}` off the default)
5
- * and `finish` (which merges/PRs back into it). Kept tiny and pure so both
6
- * call sites read the same source of truth for "what is the default branch"
7
- * and "is there a remote to push to".
8
- */
9
-
10
- /**
11
- * Does a local branch exist? execFile (argv, never a cmd.exe string) so the
12
- * name reaches git as one token — the same anti-injection discipline the rest
13
- * of the worktree/finish path uses.
14
- *
15
- * @param {string} branch
16
- * @param {string} cwd
17
- * @returns {boolean}
18
- */
19
3
  function localBranchExists(branch, cwd) {
20
4
  try {
21
5
  execFileSync('git', ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], { cwd, stdio: 'pipe' });
@@ -26,26 +10,14 @@ function localBranchExists(branch, cwd) {
26
10
  }
27
11
 
28
12
  /**
29
- * Resolve the repository's default branch NAME (e.g. `main`), never assuming
30
- * `master`. Strategy, in order:
31
- * 1. `refs/remotes/origin/HEAD` symbolic ref — the remote's declared default,
32
- * the authoritative answer when a remote is configured.
33
- * 2. A local `main`, then `master` — the fallback for a repo with no remote
34
- * (or one whose `origin/HEAD` was never set).
35
- *
36
- * Returns `null` when none of these resolve (a repo whose default is some other
37
- * name, with no `origin/HEAD` set) — the caller decides: `create` fails open
38
- * (no branch), `finish` refuses with a clear message.
39
- *
40
- * @param {string} [cwd]
41
- * @returns {string|null} the default branch name, or null if undeterminable
13
+ * `origin/HEAD` first, then a local `main`, then `master`.
14
+ * @returns {string|null} null when none resolves; the caller decides.
42
15
  */
43
16
  export function detectDefaultBranch(cwd = process.cwd()) {
44
17
  try {
45
18
  const ref = execFileSync('git', ['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], { cwd, stdio: 'pipe' })
46
19
  .toString()
47
20
  .trim();
48
- // e.g. "refs/remotes/origin/main" → "main"
49
21
  const m = ref.match(/^refs\/remotes\/origin\/(.+)$/);
50
22
  if (m && m[1]) return m[1];
51
23
  } catch { /* no origin/HEAD — fall back to a local default */ }
@@ -56,13 +28,6 @@ export function detectDefaultBranch(cwd = process.cwd()) {
56
28
  return null;
57
29
  }
58
30
 
59
- /**
60
- * Is an `origin` remote configured? `--pr` needs one to push the feature branch;
61
- * `--merge` is purely local and never calls this.
62
- *
63
- * @param {string} [cwd]
64
- * @returns {boolean}
65
- */
66
31
  export function hasOriginRemote(cwd = process.cwd()) {
67
32
  try {
68
33
  const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd, stdio: 'pipe' }).toString().trim();
@@ -72,18 +37,7 @@ export function hasOriginRemote(cwd = process.cwd()) {
72
37
  }
73
38
  }
74
39
 
75
- /**
76
- * Resolve HEAD's full commit SHA. Fail-open: null when cwd isn't a git repo,
77
- * or the repo has no commits yet (unborn HEAD) — never throws.
78
- *
79
- * Shared by `morph-spec verify` (the `verification` stamp) and
80
- * `morph-spec gate-check` (signals + staleness-vs-code checks) — both need
81
- * the exact commit a result covers, mirroring the same fail-open pattern
82
- * `reviewStaleness()` in approve.js uses for the same `git rev-parse HEAD` call.
83
- *
84
- * @param {string} [cwd]
85
- * @returns {string|null}
86
- */
40
+ /** @returns {string|null} null outside a repo or on an unborn HEAD; never throws. */
87
41
  export function getHeadSha(cwd = process.cwd()) {
88
42
  try {
89
43
  return execFileSync('git', ['rev-parse', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
@@ -92,15 +46,6 @@ export function getHeadSha(cwd = process.cwd()) {
92
46
  }
93
47
  }
94
48
 
95
- /**
96
- * Parse `git diff --shortstat` output ("3 files changed, 45 insertions(+), 2
97
- * deletions(-)") into counts. Any line missing (e.g. only insertions, no
98
- * deletions) contributes 0 for that field. Empty input (no diff) is all-zero,
99
- * not an error.
100
- *
101
- * @param {string} text
102
- * @returns {{ filesChanged: number, insertions: number, deletions: number }}
103
- */
104
49
  function parseShortstat(text) {
105
50
  const files = /(\d+) files? changed/.exec(text || '');
106
51
  const ins = /(\d+) insertions?\(\+\)/.exec(text || '');
@@ -112,33 +57,10 @@ function parseShortstat(text) {
112
57
  };
113
58
  }
114
59
 
115
- /**
116
- * Count brand-new UNTRACKED files (never `git add`-ed) — the gap plain
117
- * `git diff --shortstat` never sees, tracked-edits-only by design. Fail-open:
118
- * any error (not a repo, git missing) reads as 0 untracked, never throws —
119
- * this must not take down the whole `getDiffStat` result over a probe that
120
- * is secondary to the diff itself.
121
- *
122
- * `--exclude-standard` matches gitignore the same way `git status` does, so
123
- * scratch/build output never inflates the count.
124
- *
125
- * @param {string} cwd
126
- * @returns {number}
127
- */
128
60
  function countUntrackedFiles(cwd) {
129
61
  return listUntrackedFiles(cwd).length;
130
62
  }
131
63
 
132
- /**
133
- * Split git's stdout into clean, forward-slashed, non-empty path lines.
134
- *
135
- * One helper for both probes because the two disagreeing about what a "line" is
136
- * — a stray `\r` on Windows, a trailing empty line — would make the count and
137
- * the listing describe different sets from the same output.
138
- *
139
- * @param {string} text
140
- * @returns {string[]}
141
- */
142
64
  function splitLines(text) {
143
65
  return String(text)
144
66
  .split('\n')
@@ -146,25 +68,8 @@ function splitLines(text) {
146
68
  .filter(Boolean);
147
69
  }
148
70
 
149
- /**
150
- * The brand-new UNTRACKED files, by NAME — the listing behind
151
- * {@link countUntrackedFiles}, and the second half of {@link changedPathsAgainst}.
152
- *
153
- * The two callers need the same fact at different resolutions, and this is the
154
- * ONLY place the probe is spelled: `git diff` never shows a file that was never
155
- * `git add`-ed, regardless of ref or working-tree mode, so anything that answers
156
- * "what changed" has to ask twice. A second copy of this command is exactly how
157
- * the invisible-new-file bug would come back on one caller and not the other.
158
- *
159
- * `--exclude-standard` honours gitignore the way `git status` does, so build
160
- * output never inflates either answer.
161
- *
162
- * Fail-open to `[]`: this probe is secondary to the diff and must never take the
163
- * whole result down.
164
- *
165
- * @param {string} cwd
166
- * @returns {string[]}
167
- */
71
+ // `git diff` never shows a file that was never `git add`-ed, so every "what changed"
72
+ // answer asks here too. The only spelling of this probe: a copy is how one caller goes blind.
168
73
  function listUntrackedFiles(cwd) {
169
74
  try {
170
75
  return splitLines(
@@ -176,29 +81,8 @@ function listUntrackedFiles(cwd) {
176
81
  }
177
82
 
178
83
  /**
179
- * `git diff --shortstat <baseRef>` against the CURRENT WORKING TREE (tracked
180
- * edits — staged, unstaged, and everything committed since baseRef —
181
- * deliberately not `baseRef...HEAD`, so a gate-check mid-implement still sees
182
- * uncommitted work), PLUS a count of brand-new UNTRACKED files (never
183
- * `git add`-ed) that plain `git diff` never shows regardless of commit vs
184
- * working tree. `filesChanged` is the combined total (tracked-changed +
185
- * untracked) so a caller reading just that one number gets the true scope of
186
- * what changed; `untrackedFiles` is broken out separately so a caller that
187
- * cares CAN tell diff-vs-new apart instead of the two being silently fused.
188
- *
189
- * Both git calls run with the same `cwd` — every current caller passes a
190
- * feature/worktree root there, so `git diff`'s repo-wide default and
191
- * `git ls-files`'s cwd-relative listing agree; a caller passing a
192
- * sub-directory as `cwd` would see `untrackedFiles` scoped to that
193
- * sub-directory while `insertions`/`deletions` stay repo-wide — not a
194
- * concern today, but worth knowing if that assumption ever changes.
195
- *
196
- * Fail-open: null when `baseRef` is falsy, cwd isn't a git repo, or the diff
197
- * itself fails (e.g. baseRef doesn't resolve). The untracked probe fails open
198
- * on its own (see countUntrackedFiles) and never turns a good diff into null.
199
- *
200
- * @param {string|null} baseRef - typically `detectDefaultBranch(cwd)`
201
- * @param {string} [cwd]
84
+ * Diff against the WORKING TREE (not `baseRef...HEAD`), so uncommitted work counts.
85
+ * `filesChanged` includes untracked files; `untrackedFiles` breaks them out.
202
86
  * @returns {{ filesChanged: number, insertions: number, deletions: number, untrackedFiles: number }|null}
203
87
  */
204
88
  export function getDiffStat(baseRef, cwd = process.cwd()) {
@@ -214,35 +98,9 @@ export function getDiffStat(baseRef, cwd = process.cwd()) {
214
98
  }
215
99
 
216
100
  /**
217
- * Every path this working tree CHANGED against `baseRef` — tracked edits plus
218
- * brand-new untracked files.
219
- *
220
- * WHY IT LIVES BESIDE getDiffStat, AND REUSES ITS UNTRACKED PROBE
221
- *
222
- * The two answer the same question at different resolutions ("how much changed"
223
- * versus "what changed"), and both depend on the SAME fragile fact: `git diff`
224
- * is tracked-edits-only, so a brand-new file is invisible to it no matter which
225
- * ref is named. `getDiffStat`'s docblock has documented that for a while; a
226
- * second copy of the probe here is precisely how the "new file is invisible"
227
- * bug comes back on one of the two call sites and not the other. So
228
- * `countUntrackedFiles`'s command is not retyped — the listing is.
229
- *
230
- * `git diff --name-only <baseRef>` against the WORKING TREE, deliberately not
231
- * `baseRef...HEAD`: a gate-check in the middle of implement must see work that
232
- * is not committed yet. Someone who has only staged the prompt change has still
233
- * changed the prompt.
234
- *
235
- * FAIL-OPEN, and the caller must know what that means. `[]` comes back both for
236
- * "nothing changed" and for "the ref did not resolve", exactly like
237
- * `getDiffStat` returning null. The distinction that MATTERS to the caller is a
238
- * different one — "the project declares no watched path" versus "it declares
239
- * some and none were touched" — and that one is answered by the caller's own
240
- * `declared` flag, never by the emptiness of this array.
241
- *
242
- * @param {string|null} baseRef - typically `detectDefaultBranch(cwd)`
243
- * @param {string} [cwd]
244
- * @returns {string[]} Repo-relative paths with forward slashes, deduplicated,
245
- * sorted. Empty when `baseRef` is falsy, cwd is not a repo, or the diff fails.
101
+ * Paths changed in the working tree against `baseRef`, untracked included, sorted.
102
+ * `[]` means both "nothing changed" and "ref did not resolve".
103
+ * @returns {string[]}
246
104
  */
247
105
  export function changedPathsAgainst(baseRef, cwd = process.cwd()) {
248
106
  if (!baseRef) return [];
@@ -253,8 +111,6 @@ export function changedPathsAgainst(baseRef, cwd = process.cwd()) {
253
111
  execFileSync('git', ['diff', '--name-only', baseRef], { cwd, stdio: 'pipe' }).toString()
254
112
  );
255
113
  } catch {
256
- // A baseRef that does not resolve is not a reason to claim "nothing
257
- // changed" loudly — the caller's policy is written to be inert on `[]`.
258
114
  return [];
259
115
  }
260
116
 
@@ -262,21 +118,10 @@ export function changedPathsAgainst(baseRef, cwd = process.cwd()) {
262
118
  }
263
119
 
264
120
  /**
265
- * The lines this working tree ADDED against `baseRef`, per file: the new-side
266
- * line numbers of `git diff -U0`, plus every brand-new untracked file whole
267
- * (the same probe as {@link changedPathsAgainst}).
268
- *
269
- * Measured from the merge-base, not the tip: a default branch that moved on
270
- * after the feature branched must not lend its edits to the feature. `-M`
271
- * keeps a moved file from reading as added. `--relative` gives paths relative
272
- * to `cwd` and drops what lies outside it, the same base as the untracked
273
- * listing: a project in a subfolder of the repo reads its own files.
274
- *
275
- * @param {string|null} baseRef - typically `detectDefaultBranch(cwd)`
276
- * @param {string} [cwd]
277
- * @returns {Map<string, number[]|null>|null} path → added line numbers, or
278
- * `null` for a whole untracked file. `null` (not an empty map) when it cannot
279
- * tell: falsy or unresolvable ref, not a repo, diff failed.
121
+ * Added lines per file, measured from the merge-base so a moved-on default branch
122
+ * lends nothing; paths relative to `cwd` (`--relative`), like the untracked listing.
123
+ * @returns {Map<string, number[]|null>|null} `null` value = whole untracked file;
124
+ * `null` result = cannot tell.
280
125
  */
281
126
  export function addedLinesAgainst(baseRef, cwd = process.cwd()) {
282
127
  if (!baseRef) return null;
@@ -7,7 +7,7 @@ const CONFIG_PATH = '.morph/config/config.json';
7
7
 
8
8
  const gitOut = (cwd, args) => execFileSync('git', args, { cwd, stdio: 'pipe', maxBuffer: 64 * 1024 * 1024 }).toString();
9
9
 
10
- function withoutBaseline(text) {
10
+ export function withoutBaseline(text) {
11
11
  const config = JSON.parse(text);
12
12
  if (Array.isArray(config?.project?.tests)) {
13
13
  for (const node of config.project.tests) delete node?.expectedCount;
@@ -31,6 +31,7 @@ export function treeMovedSince(cwd, sha, head = getHeadSha(cwd)) {
31
31
  if (!sha || !head || sha === head) return false;
32
32
  let changed;
33
33
  try {
34
+ // Without --no-renames a file moved into .morph/ shows only its new path and the deletion outside is lost.
34
35
  changed = gitOut(cwd, ['diff', '--name-only', '--no-renames', '-z', sha, head]).split('\0').filter(Boolean);
35
36
  } catch {
36
37
  return true;
package/src/lib/git/gh.js CHANGED
@@ -1,21 +1,6 @@
1
1
  /**
2
- * gh.js — como o `gh` é invocado, num lugar só.
3
- *
4
- * Extraído de `finish.js`, onde nasceu, porque o `pr-index.js` chamava `'gh'`
5
- * cru: o caminho de SUCESSO do índice era intestável, já que não havia por
6
- * onde injetar um stub. Duas formas de chamar o mesmo binário também são duas
7
- * formas de discordar sobre qual binário é.
8
- */
9
-
10
- /**
11
- * Resolve a invocação do `gh`. Por padrão, o `gh` do PATH. Sobrescrevível via
12
- * MORPH_GH_BIN: uma string simples nomeia outro binário (um wrapper corporativo,
13
- * `hub`, …); um array JSON é um prefixo de argv COMPLETO — por exemplo
14
- * `["/path/to/node","/path/to/gh-shim.js"]` — que é como os testes injetam um
15
- * stub cross-platform que o execFileSync consegue spawnar (Node ≥20 recusa
16
- * rodar shims `.cmd`/`.bat` sem `shell:true`, e `shell:true` reabriria o buraco
17
- * de injeção de argumento que estes caminhos evitam).
18
- *
2
+ * MORPH_GH_BIN: string = outro binário; array JSON = prefixo de argv completo
3
+ * (Node ≥20 recusa shim `.cmd` sem `shell:true`, e `shell:true` reabre injeção de argumento).
19
4
  * @returns {{ bin: string, prefix: string[] }}
20
5
  */
21
6
  export function resolveGh() {
@@ -1,60 +1,29 @@
1
- /**
2
- * pending-prs.js — as features que existem SÓ como PR aberto.
3
- *
4
- * `finish --pr` deixa o root primário sem vestígio da feature: o
5
- * `shedRootFeatureScope` apaga `.morph/features/{f}` e o archive foi commitado
6
- * na branch `morph/{f}`, chegando na default apenas no merge. Entre abrir e
7
- * mergear, portanto, nenhuma superfície local sabe que aquela branch tem dono —
8
- * foi assim que uma branch com PR #51 aberto quase foi varrida.
9
- *
10
- * Este módulo devolve o dono. Não há estado persistido a consultar, e é de
11
- * propósito: o PR aberto no GitHub É o registro, a branch é sempre
12
- * `morph/{feature}`, e o `pr-index.js` já é o leitor único desse fato. Lembrar
13
- * o número numa segunda cópia só criaria como divergir dele.
14
- *
15
- * `null` (do índice ou daqui) significa DESCONHECIDO e jamais "não há
16
- * pendência" — a mesma regra que torna seguros os caminhos destrutivos que
17
- * leem o índice.
18
- */
1
+ // Depois do `finish --pr` a raiz não guarda nada da feature até o merge: o PR aberto
2
+ // é o único registro de que a branch tem dono. `null` = DESCONHECIDO, nunca "sem pendência".
19
3
 
20
4
  import { existsSync } from 'fs';
21
5
  import { join } from 'path';
22
6
  import { buildPrIndex } from './pr-index.js';
23
7
 
24
- /** As branches que o framework cria por feature. */
25
8
  export const MORPH_BRANCH_RE = /^morph\/(.+)$/;
26
9
 
27
10
  const DAY_MS = 86400000;
28
11
 
29
- /**
30
- * O PR da branch canônica de uma feature.
31
- *
32
- * @param {Map<string,object>|null} index - de buildPrIndex
33
- * @param {string} feature
34
- * @returns {{number:number,state:string,mergedAt:string|null,url:string|null,createdAt:string|null}|null}
35
- * null = desconhecido OU sem PR; quem precisa distinguir os dois olha o índice.
36
- */
12
+ /** @returns {object|null} null = desconhecido OU sem PR; quem precisa distinguir olha o índice. */
37
13
  export function prForFeature(index, feature) {
38
14
  if (!index) return null;
39
15
  return index.get(`morph/${feature}`) || null;
40
16
  }
41
17
 
42
- /** A feature tem pasta (ativa ou arquivada) nesta árvore? */
43
18
  function hasLocalFolder(root, feature) {
44
19
  return existsSync(join(root, '.morph', 'features', feature))
45
20
  || existsSync(join(root, '.morph', 'archive', feature));
46
21
  }
47
22
 
48
23
  /**
49
- * As features aguardando merge: PR OPEN numa branch `morph/*` cujo slug não
50
- * tem pasta nesta árvore.
51
- *
52
- * @param {string} root - a raiz a inspecionar (e de onde o `gh` roda)
53
- * @param {{ index?: Map<string,object>|null, now?: Date }} [opts]
54
- * `index` injeta um índice já construído (o `list`/`fleet` constroem um por
55
- * processo e reaproveitam; os testes passam um sintético). Ausente = constrói.
56
- * @returns {Array<{feature:string,branch:string,number:number,state:string,url:string|null,createdAt:string|null,ageDays:number|null}>|null}
57
- * null = DESCONHECIDO. Nunca lista vazia por falta de informação.
24
+ * PR OPEN numa branch `morph/*` cujo slug não tem pasta nesta árvore.
25
+ * @param {{ index?: Map<string,object>|null, now?: Date }} [opts] `index` ausente = constrói.
26
+ * @returns {Array<object>|null} null = DESCONHECIDO, nunca lista vazia por falta de informação.
58
27
  */
59
28
  export function pendingPrFeatures(root, opts = {}) {
60
29
  const index = opts.index !== undefined ? opts.index : buildPrIndex(root);
@@ -1,37 +1,12 @@
1
- /**
2
- * pr-files.js — o diff de um PR carregou uma feature morph-spec?
3
- *
4
- * `morph/{f}` é a convenção de nome das branches que o framework cria, não
5
- * prova de que a branch carregou uma feature: nada impede um PR só de docs
6
- * numa `morph/x`. O `doctor` lia o nome como prova e acusava
7
- * `pr-merged-not-landed` para sempre sobre PRs que nunca tiveram archive para
8
- * pousar (medido no MORPH_GHLBrain: #5, um único `docs/*.md`, e #23, a onda 2
9
- * de outra feature — nenhum caminho `.morph/` no diff de nenhum dos dois).
10
- *
11
- * A pergunta é feita ao diff, e só para o candidato: o `doctor` chama isto
12
- * depois de já saber que o PR mergeou e que `archive/{f}` falta aqui, então o
13
- * custo (~650 ms por `gh pr view`) não recai sobre quem não tem pendência. O
14
- * índice (`pr-index.js`) segue sem o campo de propósito: pedir `mergeCommit` no
15
- * `pr list` custou ~400 ms a mais em CADA chamada, paga pelos quatro leitores.
16
- *
17
- * Três respostas, nunca duas: `true` (o diff tocou `.morph/{features,archive}/{f}/`),
18
- * `false` (o diff foi lido inteiro e não tocou) e `null` (DESCONHECIDO — sem
19
- * `gh`, falha, JSON ilegível, ou o diff bateu no teto de arquivos do `gh` sem
20
- * achar). Só `false` autoriza calar um aviso.
21
- */
22
-
23
1
  import { execFileSync } from 'child_process';
24
2
  import { resolveGh } from './gh.js';
25
3
 
26
- /** Quantos arquivos o `gh pr view --json files` devolve, no máximo. */
27
4
  export const GH_FILES_CAP = 100;
28
5
 
29
6
  /**
30
- * @param {string} root - de onde o `gh` roda (resolve o repositório pelo origin)
31
- * @param {number} number - número do PR
32
- * @param {string} feature - o `{f}` de `morph/{f}`
33
- * @param {{ timeout?: number }} [opts]
34
- * @returns {boolean|null} null = DESCONHECIDO, nunca "não carregou"
7
+ * Uma branch `morph/{f}` não prova que o PR carregou a feature: só o diff tocando
8
+ * `.morph/{features,archive}/{f}/` prova.
9
+ * @returns {boolean|null} null = DESCONHECIDO; só `false` autoriza calar um aviso.
35
10
  */
36
11
  export function prCarriesFeature(root, number, feature, opts = {}) {
37
12
  const timeout = opts.timeout ?? 8000;
@@ -1,42 +1,11 @@
1
- /**
2
- * pr-index.js — one `gh pr list` per process, shared by every caller that needs
3
- * to answer "does a branch have a PR, and in what state".
4
- *
5
- * Extracted from `fleet-scanner.js` because three independent leaders now ask
6
- * the SAME question and must get the SAME answer:
7
- *
8
- * • `morph-spec fleet` — the PR column
9
- * • `morph-spec branch prune` — a branch with an OPEN PR is never deleted,
10
- * and a MERGED one is deletable even when git
11
- * cannot see the merge (squash-merge)
12
- * • `morph-spec doctor --worktrees` — a worktree whose branch carries an open
13
- * PR must NOT be offered up for removal
14
- *
15
- * Fail-open by construction: no `gh`, not authenticated, not GitHub, or a
16
- * timeout all yield `null`. `null` means "unknown", NEVER "no PR" — every
17
- * caller must treat the two differently, because the destructive paths
18
- * (prune, remove) are only safe when the answer is a positive fact.
19
- */
20
-
21
1
  import { execFileSync } from 'child_process';
22
2
  import { resolveGh } from './gh.js';
23
3
 
24
4
  /**
25
- * Build the branch → PR index for a repository.
26
- *
27
- * Gated on the origin URL actually pointing at github.com: `gh` against any
28
- * other host is a guaranteed error, and paying its start-up cost to learn that
29
- * on every call is pure latency.
30
- *
31
- * `--state all` on purpose: a MERGED PR is exactly the evidence a squash-merge
32
- * erases from git history, and it is the whole reason `branch prune` exists.
33
- * First wins — `gh` lists newest-first, so an OPEN PR shadows an older closed
34
- * one on the same branch.
35
- *
36
- * @param {string} root - repository root to run `gh` from
37
- * @param {{ limit?: number, timeout?: number }} [opts]
5
+ * Branch → PR. `--state all`: a MERGED PR is the evidence a squash-merge erases from git.
6
+ * `gh` lists newest-first, so the first entry per branch wins.
38
7
  * @returns {Map<string,{number:number,state:string,mergedAt:string|null,url:string|null,createdAt:string|null}>|null}
39
- * null = DESCONHECIDO (sem gh / não é GitHub / falhou), não "sem PRs"
8
+ * null = unknown, NEVER "no PR": destructive callers act only on a positive fact.
40
9
  */
41
10
  export function buildPrIndex(root, opts = {}) {
42
11
  const limit = String(opts.limit ?? 200);