@polymorphism-tech/morph-spec 9.0.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 (304) hide show
  1. package/CLAUDE.md +8 -9
  2. package/README.md +38 -36
  3. package/bin/morph-spec.js +4 -62
  4. package/bin/validate.js +1 -39
  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 +1 -1
  9. package/framework/MORPH.md +1 -1
  10. package/framework/agents.json +12 -4
  11. package/framework/commands/morph-apply.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +6 -17
  14. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  15. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  16. package/framework/hooks/claude-code/core/chain.js +4 -65
  17. package/framework/hooks/claude-code/core/comment-guard.js +2 -16
  18. package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
  19. package/framework/hooks/claude-code/core/gate-guard.js +7 -67
  20. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  21. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  22. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  23. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  24. package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
  25. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
  26. package/framework/hooks/claude-code/core/state-sync.js +11 -67
  27. package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -57
  29. package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
  30. package/framework/hooks/claude-code/core/worktree-context.js +10 -110
  31. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  32. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  33. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  34. package/framework/hooks/shared/activity-logger.js +9 -89
  35. package/framework/hooks/shared/agent-reap.js +2 -17
  36. package/framework/hooks/shared/build-check.js +6 -75
  37. package/framework/hooks/shared/claim-registry.js +9 -119
  38. package/framework/hooks/shared/command-tokens.js +6 -51
  39. package/framework/hooks/shared/comment-rules.js +5 -20
  40. package/framework/hooks/shared/comment-scanner.js +6 -25
  41. package/framework/hooks/shared/comment-stripper.js +11 -85
  42. package/framework/hooks/shared/e2e-check.js +0 -0
  43. package/framework/hooks/shared/errata.js +2 -15
  44. package/framework/hooks/shared/feature-locator.js +11 -95
  45. package/framework/hooks/shared/frontend-locations.js +1 -3
  46. package/framework/hooks/shared/gate-question.js +1 -7
  47. package/framework/hooks/shared/git-exclude.js +3 -20
  48. package/framework/hooks/shared/git-paths.js +8 -104
  49. package/framework/hooks/shared/graph-freshness.js +22 -166
  50. package/framework/hooks/shared/graph-refresh.js +21 -149
  51. package/framework/hooks/shared/heavy-command.js +4 -12
  52. package/framework/hooks/shared/hook-response.js +8 -83
  53. package/framework/hooks/shared/morph-cli.js +8 -52
  54. package/framework/hooks/shared/payload-utils.js +10 -132
  55. package/framework/hooks/shared/phase-utils.js +1 -42
  56. package/framework/hooks/shared/redact-secrets.js +4 -15
  57. package/framework/hooks/shared/reparse-scan.js +6 -66
  58. package/framework/hooks/shared/slice-progress.js +4 -23
  59. package/framework/hooks/shared/source-files.js +4 -17
  60. package/framework/hooks/shared/state-reader.js +13 -183
  61. package/framework/hooks/shared/state-writer.js +8 -52
  62. package/framework/hooks/shared/stdin-reader.js +1 -11
  63. package/framework/hooks/shared/telemetry-logger.js +7 -67
  64. package/framework/hooks/shared/transcript-usage.js +7 -29
  65. package/framework/hooks/shared/worktree-root.js +7 -29
  66. package/framework/rules/csharp-standards.md +1 -1
  67. package/framework/rules/frontend-standards.md +1 -0
  68. package/framework/rules/hangfire-standards.md +1 -1
  69. package/framework/rules/neon-standards.md +1 -1
  70. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  71. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  72. package/framework/skills/core/morph-apply/SKILL.md +2 -2
  73. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  74. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  75. package/framework/skills/core/morph-media/SKILL.md +1 -1
  76. package/framework/skills/core/morph-plan/SKILL.md +2 -2
  77. package/framework/skills/core/morph-review/SKILL.md +1 -1
  78. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  79. package/framework/skills/core/morph-uiux/SKILL.md +29 -12
  80. package/framework/standards/STANDARDS.json +34 -12
  81. package/framework/standards/STANDARDS.meta.json +16 -2
  82. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  83. package/framework/standards/ai-agents/agent-session.md +1 -1
  84. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  85. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  86. package/framework/standards/ai-agents/guardrails.md +1 -1
  87. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  88. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  89. package/framework/standards/ai-agents/production.md +1 -1
  90. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  91. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  92. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  93. package/framework/standards/ai-agents/setup.md +2 -2
  94. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  95. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  96. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  97. package/framework/standards/ai-agents/workflows.md +1 -1
  98. package/framework/standards/architecture/code-comments.md +1 -1
  99. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  100. package/framework/standards/backend/api/validation.md +1 -1
  101. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  102. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  103. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  104. package/framework/standards/data/nosql/cache/redis.md +1 -1
  105. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  106. package/framework/standards/frontend/admin-app/forms.md +1 -1
  107. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  108. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  109. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  110. package/framework/templates/CLAUDE.md.core.template +3 -3
  111. package/framework/templates/REGISTRY.json +9 -9
  112. package/framework/templates/docs/ui-mockups.md +4 -0
  113. package/package.json +1 -1
  114. package/src/commands/context/context-refresh.js +0 -16
  115. package/src/commands/dashboard/dashboard.js +2 -8
  116. package/src/commands/e2e/e2e.js +17 -104
  117. package/src/commands/graph/affected.js +0 -14
  118. package/src/commands/graph/build.js +11 -50
  119. package/src/commands/graph/explain.js +0 -16
  120. package/src/commands/graph/status.js +1 -12
  121. package/src/commands/mcp/mcp-setup.js +4 -62
  122. package/src/commands/observability/cost.js +5 -57
  123. package/src/commands/observability/telemetry.js +1 -14
  124. package/src/commands/project/branch.js +4 -45
  125. package/src/commands/project/claim.js +4 -48
  126. package/src/commands/project/doctor.js +48 -302
  127. package/src/commands/project/env.js +1 -27
  128. package/src/commands/project/fleet.js +3 -32
  129. package/src/commands/project/init.js +4 -68
  130. package/src/commands/project/install-plugin-cmd.js +0 -1
  131. package/src/commands/project/standards.js +1 -62
  132. package/src/commands/project/status.js +0 -53
  133. package/src/commands/project/update.js +31 -239
  134. package/src/commands/project/worktree.js +108 -882
  135. package/src/commands/scaffold/scaffold.js +0 -25
  136. package/src/commands/state/advance.js +14 -85
  137. package/src/commands/state/approve.js +14 -96
  138. package/src/commands/state/archive.js +12 -64
  139. package/src/commands/state/auto-approval.js +2 -5
  140. package/src/commands/state/create.js +22 -109
  141. package/src/commands/state/delete.js +11 -63
  142. package/src/commands/state/finish.js +98 -475
  143. package/src/commands/state/gate-check.js +22 -132
  144. package/src/commands/state/gate-decision.js +3 -39
  145. package/src/commands/state/handoff.js +3 -30
  146. package/src/commands/state/list.js +14 -84
  147. package/src/commands/state/report-check.js +8 -58
  148. package/src/commands/state/retype.js +4 -26
  149. package/src/commands/validation/index.js +0 -3
  150. package/src/commands/validation/mutate.js +2 -5
  151. package/src/commands/validation/validate-feature.js +5 -94
  152. package/src/commands/validation/verify.js +15 -98
  153. package/src/core/paths/output-schema.js +2 -101
  154. package/src/core/state/feature-identity.js +1 -25
  155. package/src/core/state/gate-order.js +0 -27
  156. package/src/core/state/lifecycle-audit.js +8 -100
  157. package/src/core/state/state-manager.js +22 -304
  158. package/src/core/templates/template-registry.js +2 -97
  159. package/src/core/templates/template-renderer.js +13 -93
  160. package/src/core/templates/template-seed.js +0 -1
  161. package/src/core/work-types.js +1 -36
  162. package/src/dashboard/app/App.jsx +1 -8
  163. package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
  164. package/src/dashboard/app/components/CommandPalette.jsx +0 -7
  165. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  166. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  167. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  168. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  169. package/src/dashboard/app/components/Markdown.jsx +0 -9
  170. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  171. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  172. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  173. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  174. package/src/dashboard/app/hooks/usePref.js +1 -5
  175. package/src/dashboard/app/lib/format.js +0 -2
  176. package/src/dashboard/app/lib/phases.js +1 -5
  177. package/src/dashboard/app/lib/project.js +0 -12
  178. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  179. package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
  180. package/src/dashboard/app/pages/Features.jsx +1 -8
  181. package/src/dashboard/app/pages/Guide.jsx +0 -1
  182. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  183. package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
  184. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  185. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  186. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  187. package/src/dashboard/app/pages/Standards.jsx +2 -17
  188. package/src/dashboard/scanner/activity-scanner.js +10 -69
  189. package/src/dashboard/scanner/agents-scanner.js +1 -23
  190. package/src/dashboard/scanner/docs-scanner.js +0 -6
  191. package/src/dashboard/scanner/feature-status.js +2 -5
  192. package/src/dashboard/scanner/features-scanner.js +10 -66
  193. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  194. package/src/dashboard/scanner/index.js +0 -4
  195. package/src/dashboard/scanner/portfolio.js +5 -29
  196. package/src/dashboard/scanner/project-api.js +8 -54
  197. package/src/dashboard/scanner/rules-scanner.js +1 -2
  198. package/src/dashboard/scanner/skills-scanner.js +2 -12
  199. package/src/dashboard/scanner/standards-scanner.js +2 -6
  200. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  201. package/src/lib/ai/ai-checks.js +7 -52
  202. package/src/lib/ai/ai-project-scan.js +14 -108
  203. package/src/lib/ai/code-audit.js +7 -63
  204. package/src/lib/ai/pin-drift.js +8 -51
  205. package/src/lib/ai/registry-audit.js +11 -90
  206. package/src/lib/ci/ci-verify.js +4 -19
  207. package/src/lib/ci/github-ci.js +9 -37
  208. package/src/lib/ci/workflow-scaffold.js +2 -10
  209. package/src/lib/detectors/claude-config-detector.js +13 -74
  210. package/src/lib/detectors/index.js +4 -50
  211. package/src/lib/e2e/compose-env.js +5 -66
  212. package/src/lib/e2e/stack-teardown.js +6 -40
  213. package/src/lib/generators/architecture-scanner.js +18 -156
  214. package/src/lib/generators/handoff-generator.js +9 -53
  215. package/src/lib/generators/scaffold-generator.js +5 -101
  216. package/src/lib/generators/settings-generator.js +4 -37
  217. package/src/lib/git/branch-classifier.js +11 -84
  218. package/src/lib/git/default-branch.js +14 -169
  219. package/src/lib/git/evidence-staleness.js +1 -0
  220. package/src/lib/git/gh.js +2 -17
  221. package/src/lib/git/pending-prs.js +6 -37
  222. package/src/lib/git/pr-files.js +3 -28
  223. package/src/lib/git/pr-index.js +3 -34
  224. package/src/lib/git/tree-fingerprint.js +3 -15
  225. package/src/lib/git/tree-state.js +6 -33
  226. package/src/lib/graph/god-nodes.js +2 -22
  227. package/src/lib/graph/graph-health.js +3 -24
  228. package/src/lib/graph/graph-loader.js +5 -56
  229. package/src/lib/graph/graphify-runtime.js +7 -37
  230. package/src/lib/graph/graphifyignore.js +8 -74
  231. package/src/lib/graph/sanitize.js +16 -136
  232. package/src/lib/heavy/cleanup.js +1 -1
  233. package/src/lib/installers/effects.js +8 -93
  234. package/src/lib/installers/mcp-installer.js +9 -169
  235. package/src/lib/installers/memory-installer.js +4 -27
  236. package/src/lib/installers/templates-reconcile.js +5 -60
  237. package/src/lib/mutation/dotnet-introspection.js +20 -106
  238. package/src/lib/mutation/subset.js +1 -4
  239. package/src/lib/plan/artifact-vocabulary.js +5 -25
  240. package/src/lib/plan/checks.js +5 -31
  241. package/src/lib/plan/plan-parser.js +1 -6
  242. package/src/lib/projects/registry.js +7 -44
  243. package/src/lib/review-reports.js +5 -23
  244. package/src/lib/risk-detector.js +39 -173
  245. package/src/lib/specs/adr-index.js +6 -34
  246. package/src/lib/specs/frontmatter.js +2 -15
  247. package/src/lib/specs/gfm-table.js +1 -7
  248. package/src/lib/specs/revocations.js +1 -14
  249. package/src/lib/specs/screen-checklist.js +47 -124
  250. package/src/lib/stack/dotnet-build-ignores.js +2 -6
  251. package/src/lib/stack/stack-profile.js +1 -28
  252. package/src/lib/stack/stack-scan.js +1 -8
  253. package/src/lib/stack-filter.js +4 -27
  254. package/src/lib/standards/registry.js +1 -7
  255. package/src/lib/standards/sections.js +2 -13
  256. package/src/lib/standards/verified-against.js +19 -191
  257. package/src/lib/tasks/eval-node.js +21 -169
  258. package/src/lib/tasks/linux-runner.js +18 -142
  259. package/src/lib/tasks/test-nodes.js +43 -240
  260. package/src/lib/tasks/test-runner.js +43 -341
  261. package/src/lib/telemetry/metrics.js +7 -36
  262. package/src/lib/telemetry/telemetry-reader.js +13 -125
  263. package/src/lib/telemetry/verify-event.js +2 -30
  264. package/src/lib/validators/code-budget/code-budget-validator.js +3 -43
  265. package/src/lib/validators/code-budget/index.js +0 -7
  266. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
  267. package/src/lib/validators/contracts/index.js +0 -6
  268. package/src/lib/validators/nextjs/index.js +0 -5
  269. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  270. package/src/lib/validators/packages/index.js +0 -6
  271. package/src/lib/validators/packages/package-validator.js +6 -56
  272. package/src/lib/validators/reuse/index.js +0 -8
  273. package/src/lib/validators/reuse/reuse-validator.js +11 -101
  274. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  275. package/src/lib/validators/traceability-check.js +5 -63
  276. package/src/lib/validators/validation-runner.js +20 -125
  277. package/src/lib/validators/verify-runner.js +52 -358
  278. package/src/lib/work-type-detector.js +5 -67
  279. package/src/lib/worktree/docker-audit.js +17 -136
  280. package/src/lib/worktree/env-allocator.js +10 -98
  281. package/src/lib/worktree/feature-truth.js +12 -115
  282. package/src/lib/worktree/finish-backup.js +32 -242
  283. package/src/lib/worktree/fleet-scanner.js +13 -95
  284. package/src/lib/worktree/ignored-triage.js +15 -91
  285. package/src/lib/worktree/vscode-settings.js +2 -3
  286. package/src/lib/worktree/worktree-audit.js +21 -213
  287. package/src/lib/worktree/worktree-include.js +3 -5
  288. package/src/scripts/global-install.js +3 -9
  289. package/src/scripts/install-plugin.js +1 -13
  290. package/src/scripts/setup-infra.js +17 -147
  291. package/src/utils/agents-installer.js +28 -275
  292. package/src/utils/banner.js +4 -25
  293. package/src/utils/claude-md-injector.js +1 -37
  294. package/src/utils/claude-settings-manager.js +1 -52
  295. package/src/utils/file-copier.js +11 -95
  296. package/src/utils/hooks-installer.js +21 -298
  297. package/src/utils/skills-installer.js +4 -101
  298. package/src/utils/version-checker.js +4 -83
  299. package/framework/commands/morph-proposal.md +0 -9
  300. package/src/commands/utils/index.js +0 -3
  301. package/src/lib/detectors/design-system-detector.js +0 -188
  302. package/src/lib/generators/design-system-generator.js +0 -508
  303. package/src/lib/specs/old-format.js +0 -68
  304. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,44 +1,6 @@
1
- /**
2
- * graph-refresh.js — refresh em background do grafo da árvore PRINCIPAL.
3
- *
4
- * Quem dispara: o elo `graph-refresh-kick` (PostToolUse, depois de um comando
5
- * Bash ou PowerShell com git/gh/morph-spec) e o SessionStart
6
- * (`worktree-context`). Os dois chamam `kickGraphRefresh`, que só olha o disco
7
- * e solta este mesmo arquivo como processo DESTACADO (`--run <raiz>`): quem
8
- * dispara nunca espera o graphify, que custa ~80 s no MORPH_GHLBrain mesmo sem
9
- * mudança nenhuma.
10
- *
11
- * Só a raiz. O processo destacado segura o diretório em que roda, e no win32
12
- * um diretório em uso não se apaga: com o refresh de uma worktree correndo, o
13
- * `finish` e o `worktree remove` falhavam com "Permission denied" e deixavam a
14
- * worktree meio removida. Por isso o worker nasce com `cwd` no git common dir,
15
- * nunca recebe o caminho de uma worktree, e o `graphify update` roda só na
16
- * raiz — que nenhum `finish` remove. A worktree nasce com o grafo semeado da
17
- * raiz (`seedGraphFromRoot`) e, dali em diante, é atualizada por
18
- * `morph-spec graph refresh`, em primeiro plano.
19
- *
20
- * Por que não o hook git do próprio graphify (`graphify hook install`), medido
21
- * no 0.9.50:
22
- * - ele sai sem fazer nada numa worktree vinculada (compara `--git-dir` com
23
- * `--git-common-dir`);
24
- * - não instala `post-merge`: merge, fast-forward e `git pull` — o caminho
25
- * pelo qual toda feature volta para a raiz — nunca rebuildam;
26
- * - o rebuild dele só re-extrai `git diff HEAD~1 HEAD`; um commit depois de
27
- * um fast-forward carimba `built_at_commit` = HEAD num grafo SEM os
28
- * arquivos que o merge trouxe — "fresh" mentiroso.
29
- * Aqui o critério é o da regra única (graph-freshness.js): o carimbo e o
30
- * registro da última rodada contra o HEAD, e as regras do ignore por conteúdo.
31
- *
32
- * Um lock por REPOSITÓRIO (no git common dir) serializa o refresh em
33
- * background com o `graph build|refresh` manual — cada `graphify update` usa
34
- * todos os núcleos e os dois escreveriam o mesmo graph.json. O dono renova o
35
- * carimbo do lock antes de cada rodada, e a checagem é refeita depois de
36
- * soltá-lo, para não perder o commit que chegou enquanto ele era segurado.
37
- *
38
- * Fail-open em tudo: sem git, sem graphify ou sem grafo, nada acontece.
39
- *
40
- * @module graph-refresh
41
- */
1
+ // Só a raiz: o worker destacado nasce com `cwd` no git common dir, porque no win32
2
+ // um diretório em uso não se apaga e o `finish` de uma worktree falhava no meio.
3
+ // A worktree anda por `morph-spec graph refresh`, em primeiro plano.
42
4
 
43
5
  import { spawn, spawnSync } from 'child_process';
44
6
  import {
@@ -58,14 +20,8 @@ import {
58
20
 
59
21
  export { primaryTree };
60
22
 
61
- /**
62
- * O que uma worktree herda do grafo da raiz. Fora da lista, de propósito:
63
- * `.graphify_root` guarda o caminho ABSOLUTO da raiz, e `cache/` é indexado
64
- * pelo mtime da árvore de origem — medido: copiado, não poupou re-extração.
65
- * O `.graphifyignore` também não vem: a worktree lê o do próprio checkout, e
66
- * pôr `/.graphifyignore` no exclude do common dir escondia também o da RAIZ
67
- * ainda não versionado (o `git add` dele passava a ser recusado).
68
- */
23
+ // Ficam fora de propósito: `.graphify_root` guarda o caminho absoluto da raiz, `cache/`
24
+ // é indexado pelo mtime da origem, e excluir `.graphifyignore` esconderia o da raiz.
69
25
  const SEED_FILES = ['manifest.json', '.graphify_analysis.json', '.graphify_labels.json', 'graph.json'];
70
26
 
71
27
  const EXCLUDE_MARKER = '# morph-spec: codebase graph inside worktrees (managed — see graph-refresh)';
@@ -77,10 +33,6 @@ const GRAPHIFY_TIMEOUT_MS = 20 * 60 * 1000;
77
33
  const MAX_ROUNDS = 3;
78
34
  const LOG_MAX_BYTES = 256 * 1024;
79
35
 
80
- /**
81
- * @param {string} dir
82
- * @returns {boolean} `dir` está na árvore principal (o refresh automático a cobre)
83
- */
84
36
  export function isPrimaryTree(dir) {
85
37
  const top = dir ? workTreeTop(dir) : null;
86
38
  const primary = primaryTree(dir);
@@ -92,7 +44,6 @@ function stateDir(dir) {
92
44
  return common ? join(common, 'morph-spec') : null;
93
45
  }
94
46
 
95
- /** @param {string} dir */
96
47
  export function refreshLockPath(dir) {
97
48
  const d = stateDir(dir);
98
49
  return d ? join(d, 'graph-refresh.lock') : null;
@@ -111,16 +62,8 @@ function mtimeOf(path) {
111
62
  }
112
63
  }
113
64
 
114
- /**
115
- * Estado do lock do repositório — o que `graph status` mostra como "refresh em
116
- * andamento". A idade conta da última renovação: um lock de processo morto,
117
- * ou sem renovação há mais que o teto, é `live: false`, e o próximo refresh o
118
- * recupera.
119
- *
120
- * @param {string} dir
121
- * @param {{now?: number, isAlive?: (pid: number) => boolean}} [deps]
122
- * @returns {{pid: number, since: string, renewed?: string, live: boolean}|null}
123
- */
65
+ // A idade conta da última renovação: processo morto ou sem renovação além do
66
+ // teto é `live: false`, e o próximo refresh recupera o lock.
124
67
  export function readRefreshLock(dir, { now = Date.now(), isAlive = pidAlive } = {}) {
125
68
  const path = refreshLockPath(dir);
126
69
  if (!path) return null;
@@ -133,11 +76,6 @@ export function readRefreshLock(dir, { now = Date.now(), isAlive = pidAlive } =
133
76
  }
134
77
  }
135
78
 
136
- /**
137
- * @param {string} dir
138
- * @param {{isAlive?: (pid: number) => boolean}} [deps]
139
- * @returns {string|null} o caminho do lock adquirido, ou null (ocupado ou sem git)
140
- */
141
79
  export function acquireRefreshLock(dir, deps = {}) {
142
80
  const path = refreshLockPath(dir);
143
81
  if (!path) return null;
@@ -162,13 +100,8 @@ export function acquireRefreshLock(dir, deps = {}) {
162
100
  return null;
163
101
  }
164
102
 
165
- /**
166
- * Renova o carimbo do lock, só se ainda for deste processo. Chamado antes de
167
- * cada rodada do graphify: uma posse que atravessa várias rodadas passaria do
168
- * teto de idade e seria roubada no meio de uma escrita.
169
- *
170
- * @param {string|null} path
171
- */
103
+ // Uma posse que atravessa várias rodadas passaria do teto de idade e seria
104
+ // roubada no meio de uma escrita.
172
105
  export function renewRefreshLock(path) {
173
106
  if (!path) return;
174
107
  try {
@@ -178,7 +111,6 @@ export function renewRefreshLock(path) {
178
111
  } catch { /* solto por outro — o release confere de novo */ }
179
112
  }
180
113
 
181
- /** Solta o lock só se ainda for deste processo. */
182
114
  export function releaseRefreshLock(path) {
183
115
  if (!path) return;
184
116
  try {
@@ -199,13 +131,7 @@ function appendRefreshLog(dir, entry) {
199
131
  } catch { /* log é diagnóstico, nunca bloqueia */ }
200
132
  }
201
133
 
202
- /**
203
- * `graphify` no PATH. No win32 o libuv só acha `.exe` sem shell; `.cmd`/`.bat`
204
- * (o que um shim ou um dublê de teste instala) passa pelo shell.
205
- *
206
- * @param {NodeJS.ProcessEnv} [env]
207
- * @returns {{file: string, shell: boolean}|null}
208
- */
134
+ // No win32 o libuv só acha `.exe` sem shell; `.cmd`/`.bat` passa pelo shell.
209
135
  export function resolveGraphify(env = process.env) {
210
136
  const names = process.platform === 'win32'
211
137
  ? ['graphify.exe', 'graphify.cmd', 'graphify.bat']
@@ -214,23 +140,13 @@ export function resolveGraphify(env = process.env) {
214
140
  return file ? { file, shell: /\.(cmd|bat)$/i.test(file) } : null;
215
141
  }
216
142
 
217
- /**
218
- * `graphify <args>` síncrono, com `cwd` em `tree`. É o runner do refresh em
219
- * background E do `graph build|refresh` manual: o mesmo binário resolvido e o
220
- * mesmo `PYTHONHASHSEED=0` — é o que o hook do próprio graphify fixa para o
221
- * clustering não trocar as comunidades de lugar a cada rodada.
222
- *
223
- * @param {string} tree
224
- * @param {string[]} args
225
- * @param {{env?: NodeJS.ProcessEnv, timeout?: number|null}} [opts] `timeout: null` = sem teto
226
- * @returns {{status: number|null, output: string, unavailable?: boolean, error?: string}}
227
- */
143
+ // `PYTHONHASHSEED=0` impede o clustering de trocar as comunidades de lugar a cada
144
+ // rodada. `timeout: null` = sem teto.
228
145
  export function runGraphify(tree, args, { env = process.env, timeout = GRAPHIFY_TIMEOUT_MS } = {}) {
229
146
  const bin = resolveGraphify(env);
230
147
  if (!bin) return { status: null, unavailable: true, output: '' };
231
- // Nunca com cwd na árvore: o graphify lê o alvo pelo argumento (o ignore e o
232
- // git dele, medido no 0.9.50), e um graphify órfão com cwd numa worktree a
233
- // prende no win32 até terminar — o finish e o worktree remove falhavam.
148
+ // Nunca com cwd na árvore: o graphify lê o alvo pelo argumento, e um órfão com
149
+ // cwd numa worktree a prende no win32 até terminar.
234
150
  const opts = {
235
151
  cwd: resolveGitCommonDir(tree) ?? tree,
236
152
  env: { ...env, PYTHONHASHSEED: '0' },
@@ -247,20 +163,8 @@ export function runGraphify(tree, args, { env = process.env, timeout = GRAPHIFY_
247
163
  return { status: r.status, output, ...(r.error ? { error: r.error.code } : {}) };
248
164
  }
249
165
 
250
- /**
251
- * Semeia uma worktree com o grafo da raiz. A worktree nasce de um commit da
252
- * raiz, então o grafo copiado ou já é o do HEAD dela ou fica a um
253
- * `morph-spec graph refresh` de distância — nunca a uma extração do zero.
254
- *
255
- * Só `graphify-out/` (no exclude do common dir, senão a worktree fica suja
256
- * para o `finish`). O registro da worktree copia o que se SABE do grafo da
257
- * raiz: o commit em que ele está em dia e as regras com que foi extraído —
258
- * nada inventado. Raiz sem registro das regras deixa a semente sem elas.
259
- *
260
- * @param {string} tree
261
- * @param {string} root
262
- * @returns {{seeded: boolean, reason?: string}}
263
- */
166
+ // O registro da semente copia só o que se SABE do grafo da raiz; `graphify-out/`
167
+ // vai para o exclude do common dir, senão a worktree fica suja para o `finish`.
264
168
  export function seedGraphFromRoot(tree, root) {
265
169
  if (!tree || !root || treeKey(tree) === treeKey(root)) return { seeded: false, reason: 'is-root' };
266
170
  if (existsSync(graphFile(tree))) return { seeded: false, reason: 'present' };
@@ -272,8 +176,7 @@ export function seedGraphFromRoot(tree, root) {
272
176
  for (const name of SEED_FILES) {
273
177
  const src = join(root, GRAPH_DIR, name);
274
178
  if (!existsSync(src)) continue;
275
- // Por rename, e o graph.json por último: a presença dele é o sinal de
276
- // semeadura completa, e quem lê no meio nunca vê meio arquivo.
179
+ // graph.json por último: a presença dele é o sinal de semeadura completa.
277
180
  const tmp = join(dst, `${name}.seed.${process.pid}`);
278
181
  copyFileSync(src, tmp);
279
182
  renameSync(tmp, join(dst, name));
@@ -294,13 +197,6 @@ export function seedGraphFromRoot(tree, root) {
294
197
  }
295
198
  }
296
199
 
297
- /**
298
- * Semeia a árvore de `dir` a partir da raiz, se ela for uma worktree sem
299
- * grafo — o que o SessionStart faz por uma worktree criada antes da semeadura
300
- * existir.
301
- *
302
- * @param {string} dir
303
- */
304
200
  export function seedFromPrimary(dir) {
305
201
  const tree = dir ? workTreeTop(dir) : null;
306
202
  const root = primaryTree(dir);
@@ -308,17 +204,8 @@ export function seedFromPrimary(dir) {
308
204
  return seedGraphFromRoot(tree, root);
309
205
  }
310
206
 
311
- /**
312
- * O trabalho do processo destacado, em primeiro plano (é o que os testes
313
- * chamam). Roda `graphify update` na raiz do repositório de `dir` enquanto o
314
- * grafo dela precisar (`refreshPending`), até `MAX_ROUNDS` rodadas — um
315
- * commit que chega no meio pede outra. Cada rodada vai para o registro
316
- * (graph-freshness.js) e para o log.
317
- *
318
- * @param {string} dir - qualquer diretório do repositório; o alvo é sempre a raiz
319
- * @param {{env?: NodeJS.ProcessEnv, runUpdate?: (tree: string) => {status: number|null, unavailable?: boolean, output?: string}}} [deps]
320
- * @returns {{ran: boolean, locked?: boolean, reason?: string, results: object[]}}
321
- */
207
+ // A checagem é refeita depois de soltar o lock: um commit que chegou enquanto
208
+ // ele era segurado pede outra rodada.
322
209
  export function runRefresh(dir, { env = process.env, runUpdate = (tree) => runGraphify(tree, ['update', tree], { env }) } = {}) {
323
210
  const root = primaryTree(dir);
324
211
  if (!root) return { ran: false, reason: 'no-primary-tree', results: [] };
@@ -357,23 +244,8 @@ export function runRefresh(dir, { env = process.env, runUpdate = (tree) => runGr
357
244
  return { ran: results.length > 0, results };
358
245
  }
359
246
 
360
- /**
361
- * Dispara o refresh da RAIZ em background e volta na hora — de qualquer
362
- * árvore do repositório. Só olha o disco, sem subprocesso: sem grafo na raiz
363
- * não há o que refrescar; grafo que não pede refresh no HEAD lido do disco não
364
- * paga um processo por comando git; com o graphify registrado como ausente e
365
- * ainda fora do PATH, não há quem rode. HEAD que o disco não responde (reftable,
366
- * branch sem commit) fica para o worker, que confere de novo sob o lock.
367
- *
368
- * O worker nasce com `cwd` no git common dir e só recebe a raiz: nenhum
369
- * processo destacado segura o diretório de uma worktree.
370
- *
371
- * `MORPH_GRAPH_AUTO_REFRESH=0` desliga (CI, máquina sem folga de CPU).
372
- *
373
- * @param {string} dir
374
- * @param {{spawnFn?: typeof spawn, env?: NodeJS.ProcessEnv}} [deps]
375
- * @returns {{kicked: boolean, reason?: string, pid?: number}}
376
- */
247
+ // Só olha o disco: roda a cada comando git do agente. HEAD que o disco não
248
+ // responde (reftable, branch sem commit) fica para o worker, sob o lock.
377
249
  export function kickGraphRefresh(dir, { spawnFn = spawn, env = process.env } = {}) {
378
250
  if (env.MORPH_GRAPH_AUTO_REFRESH === '0') return { kicked: false, reason: 'disabled' };
379
251
  const root = primaryTree(dir);
@@ -95,13 +95,8 @@ function kindOfTokens(tokens) {
95
95
  return null;
96
96
  }
97
97
 
98
- /**
99
- * The first heavy command of a Bash command line, as a label, or null. Looks at the executable of
100
- * each `;`/`&&`/`||`/`|`/newline segment, so `morph-spec heavy run -- dotnet test` and `echo "dotnet build"`
101
- * are not heavy: the first runs under its own slot, the second runs nothing.
102
- * @param {string} command
103
- * @returns {string|null}
104
- */
98
+ // Judges only each segment's executable: `morph-spec heavy run -- dotnet test` already holds its
99
+ // own slot, and `echo "dotnet build"` runs nothing.
105
100
  export function heavyKind(command) {
106
101
  for (const segment of splitSegments(command)) {
107
102
  const tokens = skipWrappers(tokenize(segment.replace(/^[\s({]+/, '').replace(/[\s)}]+$/, '')));
@@ -111,11 +106,8 @@ export function heavyKind(command) {
111
106
  return null;
112
107
  }
113
108
 
114
- /**
115
- * How long a foreground Bash call can run: its own `timeout` (capped at the platform maximum) plus slack.
116
- * The lease has no live process to watch, so this is the only thing that frees it after a denied or
117
- * interrupted call.
118
- */
109
+ // The lease has no live process to watch: this TTL is the only thing that frees it after a
110
+ // denied or interrupted call.
119
111
  const BASH_DEFAULT_TIMEOUT_MS = 2 * 60_000;
120
112
  const BASH_MAX_TIMEOUT_MS = 10 * 60_000;
121
113
  const LEASE_SLACK_MS = 30_000;
@@ -1,47 +1,18 @@
1
- /**
2
- * Shared hook response builders for Claude Code hooks.
3
- *
4
- * Claude Code hooks communicate via JSON to stdout.
5
- * - PreToolUse: { "decision": "block"|"approve", "reason": "..." }
6
- * - SessionStart/Stop/PostToolUse/etc (non-blocking context injection):
7
- * { "hookSpecificOutput": { "hookEventName": "...", "additionalContext": "..." } }
8
- * A top-level `additionalContext` is NOT honored by Claude Code for any
9
- * event — it must be nested under `hookSpecificOutput` alongside the
10
- * event name (see https://code.claude.com/docs/en/hooks.md).
11
- */
1
+ // A top-level `additionalContext` is NOT honored for any event: it must be nested
2
+ // under `hookSpecificOutput` (https://code.claude.com/docs/en/hooks.md).
12
3
 
13
- /**
14
- * Block a tool use with a reason.
15
- * @param {string} reason - Human-readable reason for blocking
16
- */
17
4
  export function block(reason) {
18
5
  console.log(JSON.stringify({ decision: 'block', reason }));
19
6
  process.exit(0);
20
7
  }
21
8
 
22
- /**
23
- * Approve a tool use, optionally injecting context.
24
- * @param {string} [context] - Additional context to inject
25
- */
26
9
  export function approve(context) {
27
10
  if (context) {
28
11
  console.log(JSON.stringify({ decision: 'approve', additionalContext: context }));
29
12
  }
30
- // No output = implicit approve
31
13
  process.exit(0);
32
14
  }
33
15
 
34
- /**
35
- * Inject additional (non-blocking) context for the current hook event.
36
- *
37
- * Must be nested under `hookSpecificOutput` — Claude Code does not honor a
38
- * top-level `additionalContext` for any event.
39
- *
40
- * @param {string} text - Context text to inject
41
- * @param {string} eventName - The hook event this is responding to (e.g.
42
- * 'SessionStart', 'Stop', 'PostToolUse') — required, echoed back as
43
- * `hookSpecificOutput.hookEventName`.
44
- */
45
16
  export function injectContext(text, eventName) {
46
17
  console.log(JSON.stringify({
47
18
  hookSpecificOutput: { hookEventName: eventName, additionalContext: text },
@@ -49,63 +20,26 @@ export function injectContext(text, eventName) {
49
20
  process.exit(0);
50
21
  }
51
22
 
52
- /**
53
- * Silent exit — no output, no blocking.
54
- */
55
23
  export function pass() {
56
24
  process.exit(0);
57
25
  }
58
26
 
59
- // ── Pure (non-exiting) result builders ──────────────────────────────────────
60
- //
61
- // Every core hook used to run inline at module scope and call block()/pass()/
62
- // injectContext() above to print+exit(0) the moment it knew its verdict. That
63
- // stops being safe once a hook's logic moves into an exported `run(payload)`
64
- // that can ALSO be called by framework/hooks/claude-code/core/chain.js — which
65
- // imports several hooks' run() into ONE process and cannot have any of them
66
- // call process.exit() mid-chain (it would kill the other links). run() must
67
- // instead RETURN the verdict object; chain.js (or this file's own
68
- // runStandalone(), below) is what actually prints it and exits.
69
- //
70
- // These builders produce the exact same JSON shapes as block()/approve()/
71
- // injectContext() above — keep them in sync if that shape ever changes.
72
-
73
- /** Pure equivalent of block(reason) — returns the verdict instead of printing+exiting. */
27
+ // `run()` returns these instead of exiting: chain.js runs several hooks in one
28
+ // process. Same JSON shapes as block()/approve()/injectContext() above.
74
29
  export function blockResult(reason) {
75
30
  return { decision: 'block', reason };
76
31
  }
77
32
 
78
- /** Pure equivalent of approve(context) — null means "pass" (nothing to say). */
79
33
  export function approveResult(context) {
80
34
  return context ? { decision: 'approve', additionalContext: context } : null;
81
35
  }
82
36
 
83
- /** Pure equivalent of injectContext(text, eventName). */
84
37
  export function contextResult(text, eventName) {
85
38
  return { hookSpecificOutput: { hookEventName: eventName, additionalContext: text } };
86
39
  }
87
40
 
88
- /**
89
- * Standalone tail every core hook calls as its last line:
90
- * `await runStandalone(run);`
91
- *
92
- * Reads stdin once, calls the hook's `run(payload)`, prints the JSON verdict
93
- * (if any) and exits 0 — reproducing exactly what block()/injectContext()/
94
- * pass() used to do when a hook's logic lived inline at module scope.
95
- *
96
- * No-ops when MORPH_HOOK_CHAINED is set: that means core/chain.js already
97
- * imported this module and will call run() itself with a payload it read
98
- * once for the whole chain — this file must not ALSO read stdin or exit here
99
- * (doing so would race chain.js's own stdin read and kill the chain process
100
- * after the first link). chain.js sets the flag BEFORE its first dynamic
101
- * import, so every hook it loads sees it already set at call time.
102
- *
103
- * Fail-open: any error (including a rejected run()) prints nothing and exits
104
- * 0 — the same silent-pass behavior every hook's own outer try/catch used to
105
- * produce on an unexpected error.
106
- *
107
- * @param {(payload: object) => Promise<object|null>} run
108
- */
41
+ // No-op under MORPH_HOOK_CHAINED: chain.js reads stdin once for the whole chain,
42
+ // and reading or exiting here would kill it after the first link.
109
43
  export async function runStandalone(run) {
110
44
  if (process.env.MORPH_HOOK_CHAINED) return;
111
45
 
@@ -123,17 +57,8 @@ export async function runStandalone(run) {
123
57
  process.exit(0);
124
58
  }
125
59
 
126
- /**
127
- * Pure builder for a PreToolUse rewrite: Claude Code runs the tool with
128
- * `updatedInput` in place of the original `tool_input`, and the decision is
129
- * an explicit `allow` (a rewrite without a decision is ignored). Use it ONLY
130
- * when the hook actually changed something — an unconditional `allow` would
131
- * skip the permission prompt for every call it touches.
132
- *
133
- * @param {object} updatedInput - the full replacement tool_input
134
- * @param {string} eventName - 'PreToolUse'
135
- * @param {string} [context] - optional additionalContext (what was changed)
136
- */
60
+ // A rewrite without a decision is ignored, so it carries `allow`: use it ONLY when
61
+ // the input changed, or every call it touches skips the permission prompt.
137
62
  export function updatedInputResult(updatedInput, eventName, context) {
138
63
  const out = { hookEventName: eventName, permissionDecision: 'allow', updatedInput };
139
64
  if (context) out.additionalContext = context;
@@ -1,36 +1,10 @@
1
- /**
2
- * morph-cli.js — run the morph-spec CLI from inside a hook.
3
- *
4
- * Hooks are installed STANDALONE into `.morph/framework/hooks/` and may never
5
- * import from `src/`; the only way for a hook to reuse a CLI command (the
6
- * WorktreeCreate hook needs `worktree setup`/`provision`, WorktreeRemove needs
7
- * `worktree remove --if-clean`) is to spawn the CLI as a subprocess. This
8
- * module is the ONE place that knows how to find it.
9
- *
10
- * Resolution order:
11
- * 1. `MORPH_SPEC_BIN` — an explicit command, possibly with leading args and
12
- * quoted paths (`node "R:/my repo/bin/morph-spec.js"`). What tests and a
13
- * framework checkout use.
14
- * 2. `morph-spec` on PATH — the npm-installed bin. On win32 the shim is
15
- * `morph-spec.cmd`, which Node refuses to spawn without a shell, so it is
16
- * located by scanning PATH ourselves and then run through the shell with
17
- * each arg quoted; on POSIX a plain `shell:false` spawn.
18
- * 3. `npx --no-install morph-spec` — a project-local dependency.
19
- *
20
- * ENOENT (or the shell's "not recognized"/"could not determine executable")
21
- * everywhere → `{ unavailable: true }`, never a throw: the caller decides what
22
- * degraded behavior looks like.
23
- *
24
- * `json` is the LAST line of stdout that parses as a JSON object (the CLI's
25
- * commands print exactly one JSON line; anything a stray console.log adds
26
- * before it is skipped), or null.
27
- */
28
-
1
+ // A hook cannot import `src/`, so it reuses a CLI command by spawning the CLI; this is the one
2
+ // place that finds it. On win32 `morph-spec.cmd` is located on PATH by hand and run through the
3
+ // shell, since Node refuses to spawn a `.cmd` without one.
29
4
  import { spawnSync } from 'child_process';
30
5
  import { existsSync } from 'fs';
31
6
  import { delimiter, join } from 'path';
32
7
 
33
- /** Split a command string into argv, honoring single and double quotes. */
34
8
  function splitCommand(command) {
35
9
  const tokens = [];
36
10
  const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
@@ -39,22 +13,13 @@ function splitCommand(command) {
39
13
  return tokens;
40
14
  }
41
15
 
42
- /** Quote one argument for cmd.exe (the only shell path this module uses). */
43
16
  export function quoteForCmd(arg) {
44
17
  if (arg === '') return '""';
45
18
  if (!/[\s"&|<>^()]/.test(arg)) return arg;
46
19
  return `"${arg.replace(/"/g, '""')}"`;
47
20
  }
48
21
 
49
- /**
50
- * First PATH entry holding any of `files`, or null. The entries are walked in
51
- * PATH order and every name is tried inside each one, the way cmd.exe applies
52
- * PATHEXT: a `graphify.cmd` early in PATH wins over a `graphify.exe` later.
53
- *
54
- * @param {string[]} files
55
- * @param {NodeJS.ProcessEnv} env
56
- * @returns {string|null}
57
- */
22
+ /** Walks PATH in order and tries every name in each entry, the way cmd.exe applies PATHEXT. */
58
23
  export function findFirstOnPath(files, env) {
59
24
  const raw = env.PATH || env.Path || env.path || '';
60
25
  for (const dir of raw.split(delimiter)) {
@@ -69,7 +34,6 @@ export function findFirstOnPath(files, env) {
69
34
 
70
35
  const findOnPath = (file, env) => findFirstOnPath([file], env);
71
36
 
72
- /** Last stdout line that parses as a JSON object, or null. */
73
37
  export function lastJsonLine(stdout) {
74
38
  const lines = String(stdout || '').split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
75
39
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -82,10 +46,6 @@ export function lastJsonLine(stdout) {
82
46
  return null;
83
47
  }
84
48
 
85
- /**
86
- * The candidates to try, in order. Each is `{ file, args, shell }`.
87
- * @param {NodeJS.ProcessEnv} env
88
- */
89
49
  function candidates(env) {
90
50
  const list = [];
91
51
  const explicit = env.MORPH_SPEC_BIN && splitCommand(env.MORPH_SPEC_BIN);
@@ -107,10 +67,8 @@ function candidates(env) {
107
67
  const NOT_FOUND_RE = /not recognized as an internal or external command|could not determine executable to run|command not found|ENOENT/i;
108
68
 
109
69
  /**
110
- * Run `morph-spec <args>` and return its result.
111
- *
112
- * @param {string[]} args - CLI argv after the program name
113
- * @param {{ cwd?: string, timeoutMs?: number, env?: NodeJS.ProcessEnv }} [opts]
70
+ * A missing CLI is `{ unavailable: true }`, never a throw. `json` is the last stdout line that
71
+ * parses as an object.
114
72
  * @returns {{ ok: boolean, code: number|null, stdout: string, stderr: string, json: object|null, unavailable: boolean }}
115
73
  */
116
74
  export function runMorphSpec(args, { cwd, timeoutMs = 120000, env } = {}) {
@@ -134,10 +92,8 @@ export function runMorphSpec(args, { cwd, timeoutMs = 120000, env } = {}) {
134
92
  }
135
93
  const stdout = r.stdout || '';
136
94
  const stderr = r.stderr || '';
137
- // The npx/shell paths cannot surface ENOENT: the shell (or npx itself)
138
- // reports "not found" as a non-zero exit with a message. Only those two
139
- // kinds are read that way — a real morph-spec failure is never mistaken
140
- // for absence.
95
+ // Shell and npx report "not found" as a non-zero exit, never ENOENT; the explicit
96
+ // MORPH_SPEC_BIN is excluded so a real morph-spec failure is never read as absence.
141
97
  if (cand.kind !== 'env' && r.status !== 0 && !stdout.trim() && NOT_FOUND_RE.test(stderr)) continue;
142
98
  return { ok: r.status === 0, code: r.status, stdout, stderr, json: lastJsonLine(stdout), unavailable: false };
143
99
  }