@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,55 +1,10 @@
1
- /**
2
- * comment-stripper.js — apagar comentários SEM apagar o que está dentro de uma
3
- * string, preservando comprimento e quebras de linha.
4
- *
5
- * Existe uma vez só, e serve a consumidores que fariam a MESMA coisa de
6
- * jeitos ligeiramente diferentes se cada um escrevesse o seu:
7
- *
8
- * - `code-audit.js` — não pode acusar `AgentThread` que só aparece num
9
- * comentário ou numa mensagem de log;
10
- * - `registry-audit.js` — precisa ler **JSONC**, a forma que o exemplar
11
- * canônico `model-registry.v2.json` usa e que o leitor C# do kit aceita de
12
- * propósito (`JsonCommentHandling.Skip`, `AllowTrailingCommas`);
13
- * - `dotnet-test-introspection.js` — lê o `global.json` por `jsoncToJson`;
14
- * - `comment-scanner.js` (ao lado; lido pelo `code-budget` e pelo hook
15
- * `comment-guard`) — acha as linhas de
16
- * comentário de `.cs`/`.ts`/`.js`, e é o único que liga a opção `js`.
17
- *
18
- * **Por que não um regex.** O exemplar tem `"http://localhost:11434/v1"` e uma
19
- * `provenance` com URL. Um `replace(/\/\/.*$/gm, '')` decapitaria as duas —
20
- * apagando dado e chamando isso de comentário. Um varredor que atravessa as
21
- * strings primeiro é a única forma correta, e é o que está aqui.
22
- *
23
- * O comprimento é preservado (os removidos viram espaço, `\n` e `\r` ficam) para
24
- * que índices e números de linha do texto original continuem válidos no texto
25
- * limpo. É o que permite `code-audit.js` reportar `arquivo.cs:34` sem um segundo
26
- * mapeamento.
27
- */
1
+ // Atravessa as strings antes: um regex apagaria o `//` de `"http://localhost"`.
2
+ // O comprimento é preservado para índices e números de linha continuarem válidos.
28
3
 
29
4
  /**
30
- * @param {string} text
31
- * @param {object} [options]
32
- * @param {boolean} [options.blankStrings=false]
33
- * Apaga também o CONTEÚDO das strings. **As aspas delimitadoras ficam.**
34
- *
35
- * Apagá-las junto era um defeito com consequência: um elemento-string virava
36
- * espaço puro, e o detector de vírgula sobrando de `jsoncToJson` passava a
37
- * enxergar `[ , , ]` — separadores REAIS entre delimitadores que sumiram —
38
- * e apagava uma vírgula que era sintaxe. Medido: `{"tags":["a","b"]}` saía
39
- * `{"tags":["a","b" ]}`, JSON inválido, e o registry voltava a ser
40
- * `○ não verificado`. O B1 reintroduzido pela correção do B1.
41
- * @param {{ unterminated?: string|null }} [options.report]
42
- * Se um objeto for passado, recebe `unterminated` = `'block-comment'`,
43
- * `'string'`, `'verbatim-string'`, `'raw-string'`, `'template'` ou `null`. É o canal de
44
- * diagnóstico que `jsoncToJson` usa para ficar tão estrito quanto o leitor
45
- * de produção, sem que este lexer precise lançar (ele também varre `.cs`,
46
- * onde um comentário aberto é problema do arquivo, não da varredura).
47
- * @param {boolean} [options.js=false]
48
- * Lê também template literal e regex literal de TS/JS. Opt-in porque só
49
- * quem varre `.ts`/`.js` precisa: sem template, `https://${h}` vira
50
- * comentário; sem regex, uma crase dentro de `/[`']/` abre um template que
51
- * engole o resto do arquivo.
52
- * @returns {string} texto do MESMO comprimento
5
+ * `blankStrings` apaga o conteúdo das strings, mas as ASPAS FICAM: sem elas um
6
+ * separador real vira vírgula sobrando. `report.unterminated` recebe o literal não
7
+ * fechado; `js` lê template e regex literal, sem os quais `https://${h}` vira comentário.
53
8
  */
54
9
  export function stripComments(text, { blankStrings = false, report = null, js = false } = {}) {
55
10
  const out = text.split('');
@@ -95,7 +50,7 @@ export function stripComments(text, { blankStrings = false, report = null, js =
95
50
  if (end !== -1) { i = end; continue; }
96
51
  }
97
52
 
98
- // Raw string literal do C#: """ ... """ — delimitadores de 3 aspas.
53
+ // C# raw string literal
99
54
  if (c === '"' && next === '"' && text[i + 2] === '"') {
100
55
  const end = text.indexOf('"""', i + 3);
101
56
  if (end === -1) flag('raw-string');
@@ -105,7 +60,7 @@ export function stripComments(text, { blankStrings = false, report = null, js =
105
60
  continue;
106
61
  }
107
62
 
108
- // Verbatim string do C#: @"..." — aspas duplicadas escapam
63
+ // C# verbatim string: doubled quotes escape
109
64
  if (c === '@' && next === '"') {
110
65
  let j = i + 2;
111
66
  let closed = false;
@@ -159,7 +114,6 @@ export function stripComments(text, { blankStrings = false, report = null, js =
159
114
  return out.join('');
160
115
  }
161
116
 
162
- /** Índice logo após a crase que fecha o template aberto em `start`, ou -1. */
163
117
  function templateEnd(text, start) {
164
118
  let j = start + 1;
165
119
  while (j < text.length) {
@@ -176,11 +130,7 @@ const REGEX_AFTER_WORD = new Set([
176
130
  'throw', 'instanceof', 'yield', 'await',
177
131
  ]);
178
132
 
179
- /**
180
- * Uma `/` abre regex, e não divisão, quando o que vem antes não é um valor: o
181
- * mesmo critério de contexto dos tokenizers de JS. `out` já tem os comentários
182
- * apagados, então o caractere anterior é código. Sem `<`: em TSX, `</b>` é tag.
183
- */
133
+ // `/` abre regex quando o que vem antes não é um valor. Sem `<`: em TSX, `</b>` é tag.
184
134
  function regexMayStart(out, i) {
185
135
  let k = i - 1;
186
136
  while (k >= 0 && /\s/.test(out[k])) k--;
@@ -192,7 +142,6 @@ function regexMayStart(out, i) {
192
142
  return REGEX_AFTER_WORD.has(out.slice(w, k + 1).join(''));
193
143
  }
194
144
 
195
- /** Índice logo após a `/` (e as flags) que fecha a regex aberta em `start`, ou -1 se a linha acaba antes. */
196
145
  function regexEnd(text, start) {
197
146
  let inClass = false;
198
147
  for (let j = start + 1; j < text.length; j++) {
@@ -210,7 +159,6 @@ function regexEnd(text, start) {
210
159
  return -1;
211
160
  }
212
161
 
213
- /** Índice logo após o `}` que fecha um `${`: o código do buraco aninha chaves, strings e templates. */
214
162
  function holeEnd(text, start) {
215
163
  let depth = 1;
216
164
  let j = start;
@@ -235,31 +183,9 @@ function holeEnd(text, start) {
235
183
  return text.length;
236
184
  }
237
185
 
238
- /**
239
- * Converte JSONC em JSON: apaga comentários e vírgulas sobrando antes de `}`/`]`.
240
- *
241
- * As duas tolerâncias são **exatamente** as que `ModelRegistry.FromJson` do
242
- * ai-kit declara (`CommentHandling = Skip`, `AllowTrailingCommas = true`). A
243
- * ferramenta que audita o registry tem de enxergar a mesma forma que o leitor
244
- * de produção enxerga — senão o framework passa a recomendar uma forma que a
245
- * sua própria auditoria não lê, e as checagens ficam inertes em silêncio.
246
- *
247
- * A vírgula sobrando é localizada num texto onde o conteúdo das strings também
248
- * foi apagado — **com as aspas preservadas**, senão um elemento-string vira
249
- * espaço puro e um separador REAL passa por vírgula sobrando (o defeito B4) —
250
- * e só então removida do texto real: uma vírgula dentro de `"a,}"` é dado.
251
- *
252
- * **Um comentário de bloco não fechado LANÇA**, e a razão é a mesma que fez
253
- * esta função existir: paridade com o leitor de produção. `JsonCommentHandling.Skip`
254
- * do .NET lança nesse caso, e ser mais frouxo que o kit é o defeito INVERSO do
255
- * B1 — auditar, e aprovar, um documento que a aplicação recusa carregar.
256
- * `readJsonSafe` captura e devolve `null`, que o relatório traduz em
257
- * "não verificado" com razão.
258
- *
259
- * @param {string} raw
260
- * @returns {string} JSON estrito, do mesmo comprimento do original
261
- * @throws {SyntaxError} comentário de bloco aberto e nunca fechado
262
- */
186
+ // Paridade com `ModelRegistry.FromJson` do ai-kit, nos dois sentidos: comentário de
187
+ // bloco não fechado LANÇA, como `JsonCommentHandling.Skip`, porque ser mais frouxo
188
+ // que o kit é aprovar um documento que a aplicação recusa carregar.
263
189
  export function jsoncToJson(raw) {
264
190
  const report = {};
265
191
  const noComments = stripComments(raw, { report }).split('');
Binary file
@@ -0,0 +1,59 @@
1
+ export const ERRATA_HEADING = '## Errata';
2
+
3
+ /** The one normalization every reader of a frozen markdown applies. */
4
+ export function normalizeMarkdown(text) {
5
+ return String(text ?? '').replace(/^/, '').replace(/\r\n?/g, '\n');
6
+ }
7
+
8
+ export function markdownLines(text) {
9
+ const out = [];
10
+ let fence = null;
11
+ normalizeMarkdown(text).split('\n').forEach((raw, i) => {
12
+ const opener = /^\s*(`{3,}|~{3,})/.exec(raw);
13
+ let fenced = Boolean(fence);
14
+ if (fence) {
15
+ if (opener && opener[1][0] === fence[0] && opener[1].length >= fence.length) fence = null;
16
+ } else if (opener) {
17
+ fence = opener[1];
18
+ fenced = true;
19
+ }
20
+ out.push({ raw, n: i + 1, fenced });
21
+ });
22
+ return out;
23
+ }
24
+
25
+ const isErrataHeading = (line) => !line.fenced
26
+ && /^##\s+errata\s*$/i.test(line.raw);
27
+
28
+ /** Offset in the NORMALIZED text, or -1. */
29
+ export function errataOffset(text) {
30
+ let offset = 0;
31
+ for (const line of markdownLines(text)) {
32
+ if (isErrataHeading(line)) return offset;
33
+ offset += line.raw.length + 1;
34
+ }
35
+ return -1;
36
+ }
37
+
38
+ export function frozenBody(text) {
39
+ const norm = normalizeMarkdown(text);
40
+ const at = errataOffset(norm);
41
+ return (at === -1 ? norm : norm.slice(0, at)).trimEnd();
42
+ }
43
+
44
+ export function errataViolation(original, proposed) {
45
+ if (frozenBody(proposed) !== frozenBody(original)) {
46
+ return { ok: false, why: 'o corpo aprovado mudou — errata só ACRESCENTA ao fim, nunca altera o texto aprovado' };
47
+ }
48
+ const next = normalizeMarkdown(proposed);
49
+ const nextAt = errataOffset(next);
50
+ if (nextAt === -1) {
51
+ return { ok: false, why: `o conteúdo novo precisa estar sob um bloco \`${ERRATA_HEADING}\` no fim do arquivo` };
52
+ }
53
+ const prev = normalizeMarkdown(original);
54
+ const prevAt = errataOffset(prev);
55
+ if (prevAt !== -1 && !next.slice(nextAt).startsWith(prev.slice(prevAt).trimEnd())) {
56
+ return { ok: false, why: 'a errata existente foi reescrita — ela é append-only, entradas anteriores não se apagam' };
57
+ }
58
+ return { ok: true };
59
+ }
@@ -1,41 +1,11 @@
1
- /**
2
- * Locate which tree owns a feature, and which feature a gate question is about.
3
- *
4
- * `resolveMorphRoot` (payload-utils) answers this from a file path. Some hook
5
- * events carry no path at all — an `AskUserQuestion` gate approval is the one
6
- * that matters — and those were resolved by "the first in_progress feature of
7
- * whatever tree the session happens to sit in". That is how a Gate 2 approval
8
- * for one feature landed on `export-hub`, an unrelated feature that merely
9
- * happened to be active in the primary root.
10
- *
11
- * The honest signal for a pathless event is the question text itself: every
12
- * gate template in the core skills names its feature — "Gate 1 — Proposta de
13
- * {feature} aprovada?", "Gate 2 — Plano de {feature} aprovado?", "Gate 3 —
14
- * Review de {feature} aprovado?", "Gate 3 — Hotfix de {feature} pronto para
15
- * ship?". Rather than parse those sentences (which would break the moment a
16
- * template is reworded), match the question against the set of feature slugs
17
- * that actually exist on disk.
18
- *
19
- * When the text names no known feature, the caller must REFUSE TO GUESS.
20
- * Stamping a gate onto the wrong feature is close to unrecoverable — it inverts
21
- * gate order and invites a second stamp on the real owner; writing nothing
22
- * costs one `morph-spec approve`.
23
- *
24
- * Fail-open by contract: every function swallows errors and returns an empty
25
- * result rather than throwing.
26
- */
1
+ // A pathless gate question is matched against the slugs on disk, never parsed.
2
+ // When it names no known feature the caller must REFUSE TO GUESS: a gate on the
3
+ // wrong feature is close to unrecoverable, writing nothing costs one `approve`.
27
4
 
28
5
  import { execSync } from 'child_process';
29
6
  import { loadState } from './state-reader.js';
30
7
 
31
- /**
32
- * Feature slugs known to `root`.
33
- *
34
- * Deliberately `loadState` and not a `readdirSync` of `.morph/features/`: state
35
- * lives in BOTH shapes — the thin `.morph/state.json` index and the per-feature
36
- * `feature.json` — and `loadState` is the one place that already merges them.
37
- * Listing directories alone misses every feature carried only by the index.
38
- */
8
+ // `loadState`, not a directory listing: it also sees features only the index carries.
39
9
  export function featureNamesIn(root) {
40
10
  try {
41
11
  const state = loadState(root);
@@ -45,11 +15,6 @@ export function featureNamesIn(root) {
45
15
  }
46
16
  }
47
17
 
48
- /**
49
- * Every working tree of this repo — the primary root plus every registered
50
- * secondary worktree. Fail-open to `[sessionRoot]` when git is unavailable or
51
- * the directory is not a repo.
52
- */
53
18
  export function allWorkTrees(sessionRoot) {
54
19
  const roots = [sessionRoot];
55
20
  try {
@@ -64,26 +29,14 @@ export function allWorkTrees(sessionRoot) {
64
29
  if (path && !roots.includes(path)) roots.push(path);
65
30
  }
66
31
  } catch {
67
- /* fail-open — the session root alone */
68
32
  }
69
33
  return roots;
70
34
  }
71
35
 
72
36
  const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
73
37
 
74
- /**
75
- * The single feature slug named in `text`, or null.
76
- *
77
- * Slug boundaries are `[^a-z0-9-]` so a longer name never matches inside a
78
- * shorter one (`export` must not match the text "export-hub"). Zero matches
79
- * means the text named nothing; two or more means it named two features and is
80
- * ambiguous. Both are null — "I don't know" and "it's ambiguous" must lead to
81
- * the same refusal, not to a coin flip.
82
- *
83
- * @param {string} text
84
- * @param {string[]} names - candidate slugs
85
- * @returns {string|null}
86
- */
38
+ // `export` must not match inside "export-hub". Zero and ambiguous matches are
39
+ // both null: they must lead to the same refusal.
87
40
  export function featureNamedIn(text, names) {
88
41
  if (typeof text !== 'string' || !Array.isArray(names) || names.length === 0) return null;
89
42
  const haystack = text.toLowerCase();
@@ -93,28 +46,8 @@ export function featureNamedIn(text, names) {
93
46
  return hits.length === 1 ? hits[0] : null;
94
47
  }
95
48
 
96
- /**
97
- * A token in `text` shaped like a feature slug (kebab-case, at least one
98
- * hyphen — `morph-spec create`'s naming convention) that names none of
99
- * `names`. Signals "the text is talking about a specific feature I don't
100
- * know" as opposed to "the text names nothing in particular".
101
- *
102
- * This is what the single-feature fallback below was missing: a gate question
103
- * for a feature not yet registered (no `create`, no `proposal.md` written
104
- * yet — a fresh hotfix, say) names NO known feature either, and used to fall
105
- * through to "the tree's one feature must be it". That is exactly how a Gate 3
106
- * for `backfill-bool-filter` landed on `export-hub`, the tree's only
107
- * registered feature at the time.
108
- *
109
- * Deliberately narrow (requires a hyphen) to match real feature slugs
110
- * (`export-hub`, `backfill-bool-filter`) without flagging ordinary prose —
111
- * the fixed portions of every gate template ("Proposta", "aprovado", "Sinal",
112
- * "pronto para ship"...) are single words with no internal hyphen.
113
- *
114
- * @param {string} text
115
- * @param {string[]} names - every feature slug known across all trees
116
- * @returns {string|null}
117
- */
49
+ // A slug-shaped token naming no known feature: the question is about a feature
50
+ // not yet registered. Requires a hyphen, so template prose never matches.
118
51
  export function unrecognizedSlugLike(text, names) {
119
52
  if (typeof text !== 'string') return null;
120
53
  const known = new Set((names || []).map((n) => String(n).toLowerCase()));
@@ -122,17 +55,8 @@ export function unrecognizedSlugLike(text, names) {
122
55
  return tokens.find((t) => !known.has(t)) ?? null;
123
56
  }
124
57
 
125
- /**
126
- * Resolve a gate question to `{ name, root }`.
127
- *
128
- * The session root is consulted first so the common case costs no git call, and
129
- * so a feature present in BOTH trees resolves to the tree the session is
130
- * actually in — the same tree `morph-spec approve` would write from that cwd.
131
- * Only when the session root knows no such feature do we pay for enumerating
132
- * the worktrees.
133
- *
134
- * @returns {{name: string, root: string}|null} null ⇒ caller must not guess
135
- */
58
+ // The session root wins a feature present in both trees: it is where
59
+ // `morph-spec approve` would write from that cwd.
136
60
  export function locateFeatureForQuestion(text, sessionRoot) {
137
61
  const trees = allWorkTrees(sessionRoot);
138
62
  const localNames = featureNamesIn(sessionRoot);
@@ -146,18 +70,10 @@ export function locateFeatureForQuestion(text, sessionRoot) {
146
70
  if (found) return { name: found, root };
147
71
  }
148
72
 
149
- // The text names a specific feature none of the trees know — refuse rather
150
- // than guess it meant the tree's one registered feature (see
151
- // `unrecognizedSlugLike` above).
152
73
  const allNames = trees.flatMap(featureNamesIn);
153
74
  if (unrecognizedSlugLike(text, allNames)) return null;
154
75
 
155
- // Unambiguous fallback: the session's tree holds exactly ONE feature, so
156
- // there is no other feature the answer could have been about. This is the
157
- // single-feature project — the common case — and refusing there would break
158
- // gate questions phrased without a name ("Gate 1 — Proposta aprovada?").
159
- // With two or more features the fallback is exactly the guess that put a
160
- // Gate 2 on `export-hub`, so it stops here.
76
+ // With exactly ONE feature in the tree there is nothing else the answer could be about.
161
77
  if (localNames.length === 1) return { name: localNames[0], root: sessionRoot };
162
78
 
163
79
  return null;
@@ -0,0 +1,4 @@
1
+ // `src/lib/detectors` re-exports this list, because a hook cannot import `src/`.
2
+ export const PACKAGE_LOCATIONS = ['.', 'src/frontend', 'src/web', 'frontend', 'apps/web', 'packages/app'];
3
+
4
+ export const E2E_SPEC_LOCATIONS = [...PACKAGE_LOCATIONS, 'web'];
@@ -0,0 +1,12 @@
1
+ /** Explicit gate names win over words a feature slug may also carry ("plan", "design", "review"). */
2
+ export function gateFromQuestion(question) {
3
+ if (typeof question !== 'string') return null;
4
+ if (/gate\s+uiux/i.test(question)) return 'uiux';
5
+ if (/gate\s+3\b/i.test(question)) return 'review';
6
+ if (/gate\s+(?:[12]\b|d[oe]\s+plano|plan\b)/i.test(question)) return 'plan';
7
+ const lower = question.toLowerCase();
8
+ if (lower.includes('plano') || lower.includes('plan')) return 'plan';
9
+ if (lower.includes('design')) return 'uiux';
10
+ if (lower.includes('review') || lower.includes('ship')) return 'review';
11
+ return null;
12
+ }
@@ -1,31 +1,14 @@
1
- /**
2
- * git-exclude.js — o escritor ÚNICO dos blocos que o morph-spec mantém no
3
- * `info/exclude` do git.
4
- *
5
- * O arquivo lido é o do COMMON dir (`git rev-parse --git-common-dir`), nunca o
6
- * `.git/worktrees/<id>/info/exclude`: o git não lê o deste para uma worktree
7
- * vinculada, e o do common dir vale para toda árvore do repositório — inclusive
8
- * a raiz primária. Cada chamador passa o seu marcador, e o bloco só ganha as
9
- * linhas que ainda faltam.
10
- *
11
- * Idempotente e fail-open: um exclude que não pôde ser escrito custa um
12
- * arquivo aparecendo como `??`, nunca um erro.
13
- */
1
+ // O exclude do COMMON dir: o git não lê `.git/worktrees/<id>/info/exclude` para
2
+ // uma worktree vinculada. Fail-open: falhar custa um `??`, nunca um erro.
14
3
 
15
4
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'fs';
16
5
  import { join } from 'path';
17
6
  import { resolveGitCommonDir } from './git-paths.js';
18
7
 
19
- /**
20
- * @param {string} cwd - qualquer diretório dentro do repositório
21
- * @param {string[]} patterns - linhas prontas (já ancoradas, ex. `/graphify-out`)
22
- * @param {string} marker - comentário que identifica o bloco do chamador
23
- */
24
8
  export function ensureCommonExclude(cwd, patterns, marker) {
25
9
  if (!patterns.length) return;
26
10
  try {
27
- // Pelo disco, sem subprocesso: um `git` gerado aqui abria uma janela de
28
- // console no win32 quando o chamador era um processo destacado.
11
+ // Pelo disco: um `git` gerado por um processo destacado abre console no win32.
29
12
  const common = resolveGitCommonDir(cwd);
30
13
  if (!common) return;
31
14
  const infoDir = join(common, 'info');
@@ -1,47 +1,10 @@
1
- /**
2
- * git-paths.js — pure-filesystem git common-dir resolution (no subprocess).
3
- *
4
- * `telemetry-log.js`'s claim heartbeat used to pay `execSync('git rev-parse
5
- * --git-common-dir')` on every tool call to learn where `claims.json` lives —
6
- * a ~34ms subprocess spawn, wasted on ~99% of calls because the heartbeat
7
- * write itself is throttled (see `claim-registry.js`'s `shouldHeartbeat`).
8
- * This module answers the SAME question — mirroring `git rev-parse
9
- * --git-common-dir` — by reading the two on-disk shapes git itself uses,
10
- * without spawning `git` at all:
11
- *
12
- * 1. PRIMARY checkout: `<root>/.git` is a DIRECTORY. That directory IS the
13
- * common dir. (A directory `.git` is never a linked worktree's own —
14
- * git always represents a worktree's `.git` as a FILE — so this case is
15
- * unambiguous.)
16
- * 2. LINKED WORKTREE (`git worktree add`): `<root>/.git` is a FILE
17
- * containing `gitdir: <path>`, pointing at this worktree's PRIVATE
18
- * gitdir (normally `<primary>/.git/worktrees/<name>`). Inside that
19
- * private gitdir, a `commondir` file holds a path — relative to the
20
- * private gitdir, or absolute — to the real common dir (normally
21
- * `../..`, i.e. back up to `<primary>/.git`).
22
- *
23
- * Verified against a real `git worktree add` fixture: `commondir` held
24
- * `../..`, `.git`'s `gitdir:` line held an absolute path, and the resolved
25
- * result matched `git rev-parse --git-common-dir` exactly (see
26
- * `test/hooks/git-paths.test.js`, which asserts that parity directly rather
27
- * than against a hand-computed path).
28
- *
29
- * Fail-open: any missing file, unreadable path, or shape that doesn't match
30
- * either case above resolves to `null`. Callers must treat `null` as "not
31
- * resolvable right now" and skip whatever git-dependent step they were
32
- * guarding — never throw.
33
- */
1
+ // Answers `git rev-parse` questions from the on-disk shapes git uses, without spawning git:
2
+ // a primary checkout's `.git` is a DIRECTORY, a linked worktree's is a FILE holding `gitdir:`.
3
+ // Every unreadable or unknown shape resolves to `null`, never a throw.
34
4
 
35
5
  import { readFileSync, statSync } from 'fs';
36
6
  import { dirname, isAbsolute, join, resolve } from 'path';
37
7
 
38
- /**
39
- * Find the nearest `.git` entry (file or directory) at or above `startDir`,
40
- * climbing exactly like git itself does to find the repo root.
41
- *
42
- * @param {string} startDir
43
- * @returns {{ path: string, isDirectory: boolean }|null}
44
- */
45
8
  function findDotGit(startDir) {
46
9
  let dir = resolve(startDir);
47
10
  for (;;) {
@@ -50,21 +13,13 @@ function findDotGit(startDir) {
50
13
  const st = statSync(candidate);
51
14
  return { path: candidate, isDirectory: st.isDirectory() };
52
15
  } catch {
53
- // not here — keep climbing
54
16
  }
55
17
  const parent = dirname(dir);
56
- if (parent === dir) return null; // reached the filesystem root
18
+ if (parent === dir) return null;
57
19
  dir = parent;
58
20
  }
59
21
  }
60
22
 
61
- /**
62
- * A linked worktree's `.git` FILE holds `gitdir: <path>` (absolute or relative
63
- * to the file) pointing at its private gitdir. `null` when the line is absent.
64
- *
65
- * @param {string} dotGitFile
66
- * @returns {string|null}
67
- */
68
23
  function privateGitDirOf(dotGitFile) {
69
24
  const match = readFileSync(dotGitFile, 'utf8').match(/^gitdir:\s*(.+?)\s*$/m);
70
25
  if (!match) return null;
@@ -73,15 +28,7 @@ function privateGitDirOf(dotGitFile) {
73
28
 
74
29
  const MORPH_BRANCH = /^ref:\s*refs\/heads\/morph\/([A-Za-z0-9._-]+)\s*$/;
75
30
 
76
- /**
77
- * The feature a checkout is bound to by its branch `morph/<feature>`, read from
78
- * `<root>/.git` exactly (no climbing) — the primary's `.git/HEAD` or, for a
79
- * linked worktree, `<gitdir>/HEAD`. `null` for any other branch (`morph-task/*`
80
- * included), a detached HEAD, or no `.git` at `root`.
81
- *
82
- * @param {string} root
83
- * @returns {string|null}
84
- */
31
+ /** Reads `<root>/.git` exactly, without climbing; `null` for any branch other than `morph/<feature>`. */
85
32
  export function morphBranchFeature(root) {
86
33
  try {
87
34
  const dotGit = join(root, '.git');
@@ -94,22 +41,7 @@ export function morphBranchFeature(root) {
94
41
  }
95
42
  }
96
43
 
97
- /**
98
- * Is `startDir` inside a LINKED (secondary) worktree?
99
- *
100
- * Same on-disk fact `resolveGitCommonDir` reads: git represents a linked
101
- * worktree's `.git` as a FILE holding a `gitdir:` pointer, and a primary
102
- * checkout's `.git` as a DIRECTORY. The e2e isolation rule keys on this —
103
- * a compose that cannot be isolated is a hard failure only where a second
104
- * stack could actually collide with the primary's (see e2e-check.js
105
- * `decideIsolation`).
106
- *
107
- * Fail-open: outside any repo, or on an unreadable path, answers `false` —
108
- * the lenient direction (warn instead of fail).
109
- *
110
- * @param {string} startDir
111
- * @returns {boolean}
112
- */
44
+ /** Outside any repo or on an unreadable path, answers `false`: the lenient direction for e2e isolation. */
113
45
  export function isSecondaryWorktree(startDir) {
114
46
  try {
115
47
  const found = findDotGit(startDir);
@@ -119,14 +51,6 @@ export function isSecondaryWorktree(startDir) {
119
51
  }
120
52
  }
121
53
 
122
- /**
123
- * Top of the working tree that contains `startDir` — the directory holding the
124
- * nearest `.git` (file or directory), which is what `git rev-parse
125
- * --show-toplevel` answers, without the subprocess. `null` outside any repo.
126
- *
127
- * @param {string} startDir
128
- * @returns {string|null}
129
- */
130
54
  export function workTreeTop(startDir) {
131
55
  try {
132
56
  const found = findDotGit(startDir);
@@ -136,16 +60,7 @@ export function workTreeTop(startDir) {
136
60
  }
137
61
  }
138
62
 
139
- /**
140
- * Resolve the git COMMON dir for `startDir` — pure filesystem, no subprocess.
141
- * Always returns an absolute path (unlike `git rev-parse --git-common-dir`,
142
- * which returns a path relative to the CALLER's cwd when run from the
143
- * primary checkout — callers of this function never need their own
144
- * `resolve(cwd, ...)` step afterward).
145
- *
146
- * @param {string} startDir
147
- * @returns {string|null}
148
- */
63
+ /** Always absolute, unlike `git rev-parse --git-common-dir`, which is relative to the caller's cwd in the primary. */
149
64
  export function resolveGitCommonDir(startDir) {
150
65
  try {
151
66
  const found = findDotGit(startDir);
@@ -158,8 +73,6 @@ export function resolveGitCommonDir(startDir) {
158
73
  const privateGitDir = privateGitDirOf(found.path);
159
74
  if (!privateGitDir) return null;
160
75
 
161
- // Inside the private gitdir, `commondir` points (relative to itself, or
162
- // absolute) at the real common dir.
163
76
  const commonRaw = readFileSync(join(privateGitDir, 'commondir'), 'utf8').trim();
164
77
  if (!commonRaw) return null;
165
78
  return isAbsolute(commonRaw) ? commonRaw : resolve(privateGitDir, commonRaw);
@@ -170,15 +83,7 @@ export function resolveGitCommonDir(startDir) {
170
83
 
171
84
  const SHA = /^[0-9a-f]{40}(?:[0-9a-f]{24})?$/;
172
85
 
173
- /**
174
- * The commit HEAD points at in the PRIMARY checkout `root`, read from disk —
175
- * `git rev-parse HEAD` without the subprocess: a detached SHA, or the branch's
176
- * loose ref, then `packed-refs`. `null` for every shape it does not read (a
177
- * linked worktree, reftable, a branch with no commit yet): the caller asks git.
178
- *
179
- * @param {string} root
180
- * @returns {string|null}
181
- */
86
+ /** `null` for every shape it does not read (linked worktree, reftable, unborn branch): the caller asks git. */
182
87
  export function readPrimaryHead(root) {
183
88
  try {
184
89
  const gitDir = join(root, '.git');
@@ -191,7 +96,6 @@ export function readPrimaryHead(root) {
191
96
  const loose = readFileSync(join(gitDir, ref), 'utf8').trim();
192
97
  if (SHA.test(loose)) return loose;
193
98
  } catch {
194
- // not loose — packed
195
99
  }
196
100
  for (const line of readFileSync(join(gitDir, 'packed-refs'), 'utf8').split(/\r?\n/)) {
197
101
  const [sha, name] = line.split(' ');