@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,14 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * Stop Hook: phase-transition alert, slice report audit, session usage.
5
- *
6
- * Phase transition tracking: stores last-seen phase per feature in
7
- * .morph/memory/phase-watch.json so the alert fires exactly once.
8
- *
9
- * Fail-open: exits 0 on any error.
10
- */
11
-
12
3
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
13
4
  import { join } from 'path';
14
5
  import { createHash } from 'crypto';
@@ -21,11 +12,7 @@ import { appendTelemetryEvent } from '../../shared/telemetry-logger.js';
21
12
  import { readTranscriptUsage, usageDelta } from '../../shared/transcript-usage.js';
22
13
  import { sliceProgress, sliceReportPath, inspectReport } from '../../shared/slice-progress.js';
23
14
 
24
- /**
25
- * Slices whose report is final but lacks what the report contract asks for:
26
- * prose, the files touched, every scenario the slice covers. Advisory: the
27
- * same rule `report-check` applies when the orchestrator resumes control.
28
- */
15
+ // Advisory: the same rule `report-check` applies when the orchestrator resumes control.
29
16
  function auditSliceReports(featurePath) {
30
17
  const progress = sliceProgress(featurePath);
31
18
  if (!progress) return [];
@@ -47,30 +34,16 @@ const MISSING_HINT = {
47
34
  partial: '1ª linha PARCIAL: a fatia não terminou',
48
35
  };
49
36
 
50
- // Phases where compaction is strongly recommended before starting
51
37
  const COMPACT_TRIGGER_PHASES = new Set(['implement']);
52
38
 
53
- // Phases where compaction is suggested when arriving (heavy spec phases)
54
39
  const HEAVY_SOURCE_PHASES = new Set(['plan', 'uiux']);
55
40
 
56
- /**
57
- * @param {object} payload - parsed Stop stdin payload. Most of this hook's
58
- * logic reads its own state from disk rather than the payload, EXCEPT the
59
- * session-usage capture below, which needs `payload.transcript_path` (and
60
- * `payload.session_id` for the throttle key) — the one thing only the
61
- * payload carries.
62
- * @returns {Promise<object|null>} a hookSpecificOutput advisory (phase
63
- * transition / build failure / incomplete slice report), or null (pass/silent).
64
- */
65
41
  export async function run(payload) {
66
42
  try {
67
- // Prevent infinite loop
68
43
  if (process.env.MORPH_STOP_HOOK_ACTIVE === '1') return null;
69
44
 
70
45
  if (!stateExists()) return null;
71
46
 
72
- // getActiveFeature() only returns in_progress/draft features.
73
- // Fall back to the most recently updated feature when all features are 'done'.
74
47
  let active = getActiveFeature() || getMostRecentFeature();
75
48
  if (!active) return null;
76
49
 
@@ -79,9 +52,8 @@ export async function run(payload) {
79
52
  const warnings = [];
80
53
 
81
54
  const memDir = join(cwd, '.morph', 'memory');
82
- try { mkdirSync(memDir, { recursive: true }); } catch { /* non-blocking */ }
55
+ try { mkdirSync(memDir, { recursive: true }); } catch {}
83
56
 
84
- // ── Phase transition detection ────────────────────────────────────────────
85
57
  const currentPhase = feature.phase || derivePhaseForFeature(name, cwd);
86
58
 
87
59
  const watchPath = join(memDir, 'phase-watch.json');
@@ -92,13 +64,12 @@ export async function run(payload) {
92
64
 
93
65
  const lastPhase = phaseWatch[name];
94
66
 
95
- // Update stored phase (always, so the alert only fires once per transition)
67
+ // Always stored, so the alert fires once per transition.
96
68
  try {
97
69
  phaseWatch[name] = currentPhase;
98
70
  writeFileSync(watchPath, JSON.stringify(phaseWatch, null, 2), 'utf-8');
99
- } catch { /* non-blocking */ }
71
+ } catch {}
100
72
 
101
- // Alert when entering implement from a heavy spec phase
102
73
  const isPhaseTransition = lastPhase && lastPhase !== currentPhase;
103
74
  const enteringImplement = COMPACT_TRIGGER_PHASES.has(currentPhase);
104
75
  const comingFromHeavyPhase = HEAVY_SOURCE_PHASES.has(lastPhase);
@@ -116,14 +87,7 @@ export async function run(payload) {
116
87
  );
117
88
  }
118
89
 
119
- // ── Slice report audit ────────────────────────────────────────────────────
120
- // Advisory warnings only — Stop hook never blocks.
121
- //
122
- // The same audit re-runs every Stop; without dedup it re-injects the same
123
- // ~500-token block turn after turn even when nothing changed. Hash-gate it:
124
- // only inject when the warning SET differs from the last one actually
125
- // surfaced for this feature. Resolved (no more warnings) clears the stored
126
- // hash, so a later recurrence — even an identical one — is reported fresh.
90
+ // Hash-gated: the audit re-runs every Stop, so only a changed warning set is injected; resolving clears the hash.
127
91
  try {
128
92
  const featurePath = join(cwd, '.morph', 'features', name);
129
93
  const reportWarnings = auditSliceReports(featurePath);
@@ -140,22 +104,15 @@ export async function run(payload) {
140
104
  warnings.push('📋 Relatórios de fatia:');
141
105
  for (const w of reportWarnings) warnings.push(` • ${w}`);
142
106
  warnedHashes[name] = { ...warnedHashes[name], reports: hash };
143
- try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch { /* non-blocking */ }
107
+ try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch {}
144
108
  }
145
- // same hash as last time it fired → already surfaced, stay silent
146
109
  } else if (warnedHashes[name]?.reports) {
147
110
  delete warnedHashes[name].reports;
148
- try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch { /* non-blocking */ }
111
+ try { writeFileSync(warnedHashesPath, JSON.stringify(warnedHashes, null, 2), 'utf-8'); } catch {}
149
112
  }
150
- } catch {
151
- // Fail-open — audit must never block Stop
152
- }
153
- // ────────────────────────────────────────────────────────────────────────
113
+ } catch {}
154
114
 
155
- // ── Session usage (real tokens, from the transcript) ──────────────────────
156
- // Silent side effect. The transcript total is cumulative, so each event
157
- // also carries the `delta` since the previous sample of the same session:
158
- // summing deltas per sessionId is the only sum that does not double-count.
115
+ // The transcript total is cumulative: summing the per-session `delta` is the only sum that does not double-count.
159
116
  try {
160
117
  const usage = readTranscriptUsage(payload?.transcript_path);
161
118
  if (usage) {
@@ -177,13 +134,10 @@ export async function run(payload) {
177
134
  delta,
178
135
  }, cwd);
179
136
  stored[sessionKey] = usage;
180
- try { writeFileSync(usagePath, JSON.stringify(stored, null, 2), 'utf-8'); } catch { /* non-blocking */ }
137
+ try { writeFileSync(usagePath, JSON.stringify(stored, null, 2), 'utf-8'); } catch {}
181
138
  }
182
139
  }
183
- } catch {
184
- // Fail-open — usage capture must never block Stop
185
- }
186
- // ────────────────────────────────────────────────────────────────────────
140
+ } catch {}
187
141
 
188
142
  if (warnings.length === 0) return null;
189
143
 
@@ -1,18 +1,10 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * warn-bash-source-write — PreToolUse Bash, warn only: a project source file written
4
- * by the shell (`writeTargets`) skips every Write/Edit guard (phase folders,
5
- * protected files, comment-guard). The command still runs.
6
- */
2
+ // Warn only: a source file written by the shell skips every Write/Edit guard. The command still runs.
7
3
  import { contextResult, runStandalone } from '../../shared/hook-response.js';
8
4
  import { stateExists } from '../../shared/state-reader.js';
9
5
  import { writeTargets } from '../../shared/command-tokens.js';
10
6
  import { isProjectSource } from '../../shared/source-files.js';
11
7
 
12
- /**
13
- * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
14
- * @returns {Promise<object|null>} additionalContext naming the files, or null
15
- */
16
8
  export async function run(payload) {
17
9
  try {
18
10
  if (!stateExists()) return null;
@@ -1,40 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * worktree-context.js — SessionStart hook
4
- *
5
- * Event: SessionStart
6
- *
7
- * v8 runs each feature in its own git worktree (`worktrees/{feature}/` on
8
- * branch `morph/{feature}`), each with its own Claude Code instance. When a
9
- * session starts inside one of those worktrees, Claude has no built-in way to
10
- * know which feature it belongs to — this hook injects that identity so the
11
- * agent doesn't have to guess (or worse, wander into another feature's
12
- * `.morph/`).
13
- *
14
- * This also fulfills the SessionStart injection role long documented in
15
- * `framework/hooks/README.md` as `inject-morph-context.js` — a hook that was
16
- * described but never registered in MORPH_HOOKS. In the primary root (not a
17
- * worktree) this hook covers that same ground: a compact active-feature
18
- * summary so a fresh session doesn't have to re-read state.json by hand.
19
- *
20
- * Two branches:
21
- * 1. Secondary worktree → feature identity (name, branch, phase) +
22
- * a warning if shared infra junctions (node_modules/.claude) are
23
- * missing (they're created by `morph-spec worktree link --all`).
24
- * 2. Primary root → active-feature summary (phase, task stats, pending
25
- * gates), or silent if there's no feature in progress.
26
- *
27
- * Also the real call site for activity-log rotation (R1.2 of the performance
28
- * plan): SessionStart is the only reliable "between sessions" checkpoint the
29
- * harness gets, so this is where `.morph/logs/activity.jsonl` gets archived
30
- * to `.morph/logs/sessions/` once it crosses the size threshold. The same
31
- * checkpoint now also rotates `.morph/logs/events.jsonl` (telemetry-logger.js)
32
- * once IT crosses its own threshold — events.jsonl grew unbounded even after
33
- * R1.2, since that rotation only ever covered activity.jsonl.
34
- *
35
- * Fail-open: any error, or nothing to report, → pass() silently. This hook
36
- * never blocks a session from starting.
37
- */
38
2
 
39
3
  import { readFileSync, existsSync, readdirSync } from 'fs';
40
4
  import { join, resolve } from 'path';
@@ -80,7 +44,6 @@ function gitRevParse(arg, dir) {
80
44
  .trim();
81
45
  }
82
46
 
83
- /** Feature name from the current branch, if it matches `morph/{feature}`. */
84
47
  function featureFromBranch(dir) {
85
48
  try {
86
49
  const branch = gitRevParse('--abbrev-ref HEAD', dir);
@@ -91,12 +54,6 @@ function featureFromBranch(dir) {
91
54
  }
92
55
  }
93
56
 
94
- /**
95
- * Task-isolation worktree (`morph-task/{name}`): the tree a `WorktreeCreate`
96
- * hook provisions for a parallel sub-agent batch (morph-apply §3b-w) or for a
97
- * `claude -w` session with no feature behind it. Not a feature — no
98
- * feature.json, no gates — but it owns a port block and the same junctions.
99
- */
100
57
  function taskFromBranch(dir) {
101
58
  try {
102
59
  const branch = gitRevParse('--abbrev-ref HEAD', dir);
@@ -107,14 +64,7 @@ function taskFromBranch(dir) {
107
64
  }
108
65
  }
109
66
 
110
- /**
111
- * Is this secondary worktree inside the primary root's `worktrees/` (or the
112
- * legacy `.worktrees/`) container? A tree kept anywhere else — `.claude/
113
- * worktrees/` from a native Claude Code run before the hook existed, a
114
- * sibling `../repo-x`, superpowers' `~/.config/...` — was created without
115
- * morph: no junctions, no ports, invisible to `fleet`. Fail-open: any error
116
- * reads as "in container" so a healthy tree is never nagged by mistake.
117
- */
67
+ // Any error reads as "in container" so a healthy tree is never nagged.
118
68
  function isOutsideContainer(dir) {
119
69
  try {
120
70
  const common = resolve(dir, gitRevParse('--git-common-dir', dir)).replace(/\\/g, '/');
@@ -143,7 +93,6 @@ function buildTaskContext(dir, task) {
143
93
  return text;
144
94
  }
145
95
 
146
- /** Fallback: if exactly one feature directory exists, assume that's the one. */
147
96
  function featureFromSingleDir(dir) {
148
97
  try {
149
98
  const entries = readdirSync(join(dir, '.morph', 'features'), { withFileTypes: true })
@@ -154,12 +103,6 @@ function featureFromSingleDir(dir) {
154
103
  }
155
104
  }
156
105
 
157
- /**
158
- * Fleet env line: the worktree's allocated port block, read from the manifest
159
- * `worktree setup` writes. Fail-open — a missing or malformed manifest just
160
- * omits the line (the session then uses project defaults, which is exactly
161
- * the pre-fleet behavior).
162
- */
163
106
  function buildEnvLine(dir) {
164
107
  try {
165
108
  const envPath = join(dir, '.morph', 'worktree.env');
@@ -178,10 +121,6 @@ function buildEnvLine(dir) {
178
121
  }
179
122
  }
180
123
 
181
- /**
182
- * Fleet handoff line: point the fresh session at the compressed handoff doc
183
- * before it starts re-deriving context by hand. Fail-open.
184
- */
185
124
  function buildHandoffLine(dir, feature) {
186
125
  try {
187
126
  const handoffPath = join(dir, '.morph', 'features', feature, 'handoff.md');
@@ -209,13 +148,7 @@ function buildWorktreeContext(dir, feature) {
209
148
  return text;
210
149
  }
211
150
 
212
- /**
213
- * `.morph/framework` is checked alongside the other two because the hook
214
- * commands resolve to `$CLAUDE_PROJECT_DIR/.morph/framework/hooks/...`: if
215
- * that junction is missing, `node` finds no file and EVERY guardrail in this
216
- * worktree is silently dark — the worst failure mode of the lot, and the one
217
- * the old two-item check could not see.
218
- */
151
+ // The hook commands resolve through the .morph/framework junction: without it every guardrail here is silently off.
219
152
  function buildMissingInfraWarning(dir) {
220
153
  const missing = ['node_modules', '.claude', join('.morph', 'framework')]
221
154
  .filter((rel) => !existsSync(join(dir, rel)));
@@ -243,20 +176,7 @@ function buildActiveFeatureSummary(dir) {
243
176
  return text;
244
177
  }
245
178
 
246
- /**
247
- * Advisory feature ownership (B1): claim the feature this session is about to
248
- * work on, and — if another session already holds a LIVE claim — say so loudly
249
- * before any work starts.
250
- *
251
- * Never blocks: the claim is a warning, not a lock (see claim-registry.js).
252
- * The claim is taken regardless, so the registry reflects reality; the operator
253
- * decides what to do with the collision.
254
- *
255
- * Fail-open per FIELD: no git common dir, or any registry error, just means no
256
- * claim line — never a failed session start.
257
- *
258
- * @returns {string} a block to append to the injected context ('' when quiet)
259
- */
179
+ // Advisory: the claim is taken even on collision; it warns, never locks.
260
180
  function claimBlock(dir, feature, sessionId) {
261
181
  try {
262
182
  const gitCommonDir = resolve(dir, gitRevParse('--git-common-dir', dir));
@@ -274,37 +194,21 @@ function claimBlock(dir, feature, sessionId) {
274
194
  }
275
195
  }
276
196
 
277
- /**
278
- * @param {object} payload - parsed SessionStart stdin payload
279
- * @returns {Promise<object|null>} a hookSpecificOutput context (worktree
280
- * identity or active-feature summary), or null (pass/silent).
281
- */
282
197
  export async function run(payload) {
283
198
  try {
284
199
  const dir = sessionDir(payload);
285
- // The SessionStart payload carries session_id — the identity the claim
286
- // registry uses to tell "me" from "another session".
287
200
  const sessionId = payload?.session_id || `pid-${process.pid}`;
288
201
 
289
202
  if (!stateExists(dir)) {
290
203
  return null;
291
204
  }
292
205
 
293
- // Rotate the activity + events logs before anything else touches them
294
- // this session. Best-effort, never fatal to session start — both rotate*
295
- // helpers are themselves fail-open (return false on any error).
296
- try { rotateActivityLogIfLarge(dir); } catch { /* non-blocking */ }
297
- try { rotateEventsLogIfLarge(dir); } catch { /* non-blocking */ }
298
- // Between sessions is where commits made outside the agent (a terminal, an
299
- // IDE, a `git pull`) are caught: the detached refresh brings the ROOT's
300
- // graph to HEAD — never a worktree's (a detached process holding a
301
- // worktree directory made `finish` fail on win32). A worktree that has no
302
- // graph yet (created before seeding existed) gets the root's copy here;
303
- // from then on it is refreshed by hand (`morph-spec graph refresh`).
304
- // Returns immediately — nothing here waits on graphify.
305
- try { seedFromPrimary(dir); } catch { /* non-blocking — a no-op on the root */ }
306
- try { kickGraphRefresh(dir); } catch { /* non-blocking */ }
307
- try { kickAgentReap(dir); } catch { /* non-blocking */ }
206
+ try { rotateActivityLogIfLarge(dir); } catch {}
207
+ try { rotateEventsLogIfLarge(dir); } catch {}
208
+ // Only the ROOT's graph is refreshed in background: a detached process holding a worktree directory made `finish` fail on win32.
209
+ try { seedFromPrimary(dir); } catch {}
210
+ try { kickGraphRefresh(dir); } catch {}
211
+ try { kickAgentReap(dir); } catch {}
308
212
 
309
213
  if (isSecondaryWorktree(dir)) {
310
214
  const task = taskFromBranch(dir);
@@ -313,16 +217,12 @@ export async function run(payload) {
313
217
  }
314
218
  const feature = featureFromBranch(dir) || featureFromSingleDir(dir);
315
219
  if (!feature) {
316
- // A tree with no morph identity at all: worth a warning ONLY when it
317
- // also lives outside the container — that is the signature of a
318
- // worktree created behind morph's back (native tool, raw git, another
319
- // skill). Inside the container with no identity → stay silent.
220
+ // No identity AND outside the container is the signature of a worktree created behind morph's back.
320
221
  return isOutsideContainer(dir) ? contextResult(buildOutOfContainerWarning(dir), 'SessionStart') : null;
321
222
  }
322
223
  return contextResult(buildWorktreeContext(dir, feature) + claimBlock(dir, feature, sessionId), 'SessionStart');
323
224
  }
324
225
 
325
- // Primary root — active-feature summary (or silent if nothing's in progress)
326
226
  const summary = buildActiveFeatureSummary(dir);
327
227
  if (!summary) {
328
228
  return null;
@@ -1,40 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * worktree-create.js — WorktreeCreate hook
4
- *
5
- * Event: WorktreeCreate (fires for `claude -w`, the EnterWorktree tool, and
6
- * `Agent isolation:"worktree"` — measured, not assumed).
7
- *
8
- * Claude Code's native worktree is a bare `git worktree add` somewhere under
9
- * `.claude/worktrees/`: no infra junctions (so no hooks run in there), no port
10
- * block, no identity, invisible to the fleet. When this hook is registered it
11
- * REPLACES that logic entirely: it must print the path of the tree it created
12
- * as the last non-empty stdout line and exit 0; any non-zero exit fails the
13
- * creation. It cannot return JSON — the standalone tail below is therefore
14
- * NOT `runStandalone` (which prints a JSON verdict). Diagnostics go to stderr.
15
- *
16
- * What it does:
17
- * 1. root = the PRIMARY root of the repo `payload.cwd` is in (never a
18
- * secondary worktree — a tree under a tree is the fleet-invisible shape).
19
- * 2. name → kebab slug (`Bold_Oak` → `bold-oak`, `agent-a1b2` stays).
20
- * 3. feature or task? `<root>/.morph/features/<n>/feature.json` exists, or
21
- * branch `morph/<n>` exists → `morph-spec worktree setup <n>`; otherwise
22
- * `morph-spec worktree provision <n> [--base <ref>]`.
23
- * 4. `worktree.baseRef` from the merged settings (`~/.claude/settings.json`
24
- * < `<root>/.claude/settings.json` < `<root>/.claude/settings.local.json`):
25
- * `"head"` → the current branch (or sha) of `payload.cwd`; unset and
26
- * `payload.cwd` on `morph/<f>` → that feature branch; `"fresh"` or
27
- * anything else → the repo's default branch (the CLI's own default).
28
- * 5. CLI exit 0 or 2 (already exists) → its `path`. Exit 1/3 → error.
29
- * CLI unavailable → in-hook git fallback that still lands the tree under
30
- * `<root>/worktrees/<n>` on `morph-task/<n>` (degraded: no junctions, no
31
- * ports — but never outside the governed directory).
32
- *
33
- * `stateExists(root)` gate: outside a morph project (no `.morph/state.json`
34
- * and no feature.json) there is nothing to govern — the same in-hook git
35
- * fallback runs, so a non-morph repo with this hook installed still gets a
36
- * worktree instead of a failed creation.
37
- */
38
2
 
39
3
  import { existsSync, mkdirSync, readFileSync } from 'fs';
40
4
  import { execFileSync } from 'child_process';
@@ -51,21 +15,16 @@ function git(args, cwd) {
51
15
  return execFileSync('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
52
16
  }
53
17
 
54
- /**
55
- * The primary root of the repo containing `startDir`. `git worktree list`'s
56
- * first entry is the primary checkout, from any worktree; the pure-filesystem
57
- * common-dir resolver is the no-subprocess fallback. Null outside a repo.
58
- */
18
+ // `git worktree list`'s first entry is the primary checkout, from any worktree.
59
19
  export function resolvePrimaryRoot(startDir) {
60
20
  try {
61
21
  const first = git(['worktree', 'list', '--porcelain'], startDir).split('\n').find((l) => l.startsWith('worktree '));
62
22
  if (first) return fwd(first.slice('worktree '.length).trim());
63
- } catch { /* fall through */ }
23
+ } catch {}
64
24
  const common = resolveGitCommonDir(startDir);
65
25
  return common ? fwd(dirname(common)) : null;
66
26
  }
67
27
 
68
- /** `Bold_Oak` → `bold-oak`; empty → a timestamped slug. */
69
28
  export function slugify(name) {
70
29
  const slug = String(name || '')
71
30
  .toLowerCase()
@@ -79,11 +38,7 @@ function readJsonSafe(path) {
79
38
  try { return JSON.parse(readFileSync(path, 'utf8').replace(/^/, '')); } catch { return null; }
80
39
  }
81
40
 
82
- /**
83
- * `worktree.baseRef` across the three settings layers, last one wins.
84
- * Fail-open per file. `null` when unset — distinct from an explicit `'fresh'`,
85
- * which overrides the feature-branch default (see `resolveBase`).
86
- */
41
+ // `null` when unset, distinct from an explicit `'fresh'`, which overrides the feature-branch default.
87
42
  export function readBaseRefSetting(root, home = homedir()) {
88
43
  let value = null;
89
44
  for (const p of [
@@ -98,7 +53,6 @@ export function readBaseRefSetting(root, home = homedir()) {
98
53
  return value;
99
54
  }
100
55
 
101
- /** The ref `baseRef:"head"` means: the branch checked out in `cwd`, or its sha. */
102
56
  function headRef(cwd) {
103
57
  try {
104
58
  const name = git(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
@@ -109,14 +63,7 @@ function headRef(cwd) {
109
63
  }
110
64
  }
111
65
 
112
- /**
113
- * The base of a task tree. `"head"` → whatever `cwd` has checked out;
114
- * `"fresh"` (or any other explicit value) → the default branch; unset → the
115
- * `morph/<f>` branch `cwd` is on, when it is one. A sub-agent dispatched by
116
- * the apply needs the feature's work, and a tree born from the default branch
117
- * made every dispatch open with `git merge --ff-only morph/<f>`.
118
- * `morph-task/*` does not match: a tree of a tree tips at the default.
119
- */
66
+ // Unset → the `morph/<f>` branch `cwd` is on: a sub-agent tree born from the default branch lacks the feature's work.
120
67
  export function resolveBase(setting, cwd) {
121
68
  if (setting === 'head') return headRef(cwd);
122
69
  if (setting) return null;
@@ -138,17 +85,13 @@ function defaultBranch(root) {
138
85
  try {
139
86
  const m = git(['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], root).match(/^refs\/remotes\/origin\/(.+)$/);
140
87
  if (m) return m[1];
141
- } catch { /* no origin/HEAD */ }
88
+ } catch {}
142
89
  for (const c of ['main', 'master']) {
143
- try { git(['show-ref', '--verify', '--quiet', `refs/heads/${c}`], root); return c; } catch { /* next */ }
90
+ try { git(['show-ref', '--verify', '--quiet', `refs/heads/${c}`], root); return c; } catch {}
144
91
  }
145
92
  return null;
146
93
  }
147
94
 
148
- /**
149
- * Degraded path — the CLI is not reachable, but the tree still has to land
150
- * in the governed directory on the governed branch namespace.
151
- */
152
95
  function gitFallback(root, name, base) {
153
96
  const branch = `morph-task/${name}`;
154
97
  const container = newWorktreesDir(root);
@@ -164,11 +107,6 @@ function gitFallback(root, name, base) {
164
107
  return { path };
165
108
  }
166
109
 
167
- /**
168
- * @param {object} payload - parsed WorktreeCreate stdin payload
169
- * ({ session_id, cwd, hook_event_name, name })
170
- * @returns {Promise<{ path: string }|{ error: string }>}
171
- */
172
110
  export async function run(payload) {
173
111
  const start = payload?.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
174
112
  const root = resolvePrimaryRoot(start) || (process.env.CLAUDE_PROJECT_DIR ? fwd(resolve(process.env.CLAUDE_PROJECT_DIR)) : null);
@@ -177,8 +115,6 @@ export async function run(payload) {
177
115
  const name = slugify(payload?.name);
178
116
  const base = resolveBase(readBaseRefSetting(root), start);
179
117
 
180
- // Outside a morph project there is nothing to govern beyond WHERE the tree
181
- // lands — plain git, under <root>/worktrees/.
182
118
  if (!stateExists(root)) {
183
119
  try { return gitFallback(root, name, base); }
184
120
  catch (err) { return { error: `worktree-create: ${err?.message || err}` }; }
@@ -1,34 +1,6 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * worktree-env-inject.js — PreToolUse hook (Bash matcher), last link of the
4
- * Bash chain.
5
- *
6
- * `.morph/worktree.env` used to be a manifest nobody executed: SessionStart
7
- * printed the port block and the model was expected to pass `PORT` by hand
8
- * every time it started a dev server. Two worktrees whose sessions forgot
9
- * collided on 3000 — a runtime clash dressed as a broken app. This hook makes
10
- * the block reach the process deterministically: when the command is a
11
- * dev-server / compose / spec launcher and the cwd is a worktree that owns a
12
- * manifest, the command is rewritten to `export K='V' …; <command>` via
13
- * `updatedInput`, and the rewrite is named in `additionalContext`.
14
- *
15
- * Scope is deliberately narrow: only launcher-shaped commands, only when the
16
- * command does not already set a layout key itself (`PORT=…`, `--port`,
17
- * `compose -p`), only for the Bash tool. Everything else passes untouched.
18
- *
19
- * Second job, same mechanism — the SESSION ANCHOR. `morph-spec finish` and
20
- * `morph-spec worktree remove` tear a worktree down, and the session that runs
21
- * them may be ANCHORED in that very worktree: its CLAUDE_PROJECT_DIR is the
22
- * worktree, and every project hook is `node "$CLAUDE_PROJECT_DIR/.morph/framework/…"`.
23
- * The CLI cannot see that anchor (the Bash tool does not export
24
- * CLAUDE_PROJECT_DIR, and `cd <root> && morph-spec finish` erases the cwd), so
25
- * it unlinked the junction the session's hooks resolve through — and every hook
26
- * of every later tool call died with MODULE_NOT_FOUND (48 in one real session).
27
- * Hooks DO see the anchor, so this one exports it as MORPH_SESSION_PROJECT_DIR
28
- * on those two commands; the CLI then defers the teardown.
29
- *
30
- * Fail-open: no state, no manifest, unreadable manifest → null (pass).
31
- */
2
+ // Launchers get the worktree's port block exported; teardowns get the session anchor, because the CLI cannot see
3
+ // CLAUDE_PROJECT_DIR and tearing down the worktree a session is anchored in kills every later hook (MODULE_NOT_FOUND).
32
4
 
33
5
  import { existsSync } from 'fs';
34
6
  import { join } from 'path';
@@ -36,47 +8,30 @@ import { stateExists } from '../../shared/state-reader.js';
36
8
  import { updatedInputResult, runStandalone } from '../../shared/hook-response.js';
37
9
  import { loadEnvFile } from '../../shared/e2e-check.js';
38
10
 
39
- /** Commands that start a server or a stack and therefore need the block. */
40
11
  const LAUNCHER_RE = /\b(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start)\b|next\s+(?:dev|start)\b|vite\b|dotnet\s+(?:run|watch)\b|docker(?:\s+|-)compose\b|playwright\s+test\b)/;
41
12
 
42
- /** The command already chose its own port/project — respect it. */
43
13
  const SELF_CONFIGURED_RE = /(?:^|[\s;&|])(?:PORT|API_PORT|POSTGRES_PORT|COMPOSE_PROJECT_NAME|ASPNETCORE_URLS)=|--port\b|(?:^|\s)-p\s+\d|compose\b[^|;&]*\s-p\s/;
44
14
 
45
- /** POSIX single-quote, safe for Git Bash on Windows too. */
15
+ // POSIX single quote, safe for Git Bash on Windows too.
46
16
  function shq(v) {
47
17
  return `'${String(v).replace(/'/g, "'\\''")}'`;
48
18
  }
49
19
 
50
- /**
51
- * Pure decision: the rewritten command, or null when nothing should change.
52
- * @param {string} command
53
- * @param {Record<string,string>} env - parsed manifest
54
- * @returns {{ command: string, injected: string[] }|null}
55
- */
56
20
  export function rewriteCommand(command, env) {
57
21
  if (!command || !LAUNCHER_RE.test(command)) return null;
58
22
  if (SELF_CONFIGURED_RE.test(command)) return null;
59
23
  const pairs = Object.keys(env).filter((k) => k !== 'MORPH_FEATURE' && env[k]).map((k) => [k, env[k]]);
60
24
  if (!pairs.some(([k]) => k === 'PORT')) return null;
61
- // The web tier's URL is what the spec suite needs; derive it once here.
62
25
  if (!env.PLAYWRIGHT_BASE_URL) pairs.push(['PLAYWRIGHT_BASE_URL', `http://localhost:${env.PORT}`]);
63
26
  const prefix = `export ${pairs.map(([k, v]) => `${k}=${shq(v)}`).join(' ')}; `;
64
27
  return { command: prefix + command, injected: pairs.map(([k, v]) => `${k}=${v}`) };
65
28
  }
66
29
 
67
- /** `morph-spec finish` / `morph-spec worktree remove` — incl. `node …/morph-spec.js` and `npx morph-spec`. */
30
+ // Also matches `node …/morph-spec.js` and `npx morph-spec`.
68
31
  const TEARDOWN_RE = /\bmorph-spec(?:\.js)?\b[^\n;&|]*?\s(?:finish|worktree\s+remove)\b/;
69
32
 
70
33
  const ANCHOR_KEY = 'MORPH_SESSION_PROJECT_DIR';
71
34
 
72
- /**
73
- * Pure decision: export the session anchor in front of a teardown command, or
74
- * null when nothing should change (other command, anchor already declared,
75
- * no anchor to give).
76
- * @param {string} command
77
- * @param {string} anchorDir - the session's project dir (CLAUDE_PROJECT_DIR)
78
- * @returns {{ command: string, anchor: string }|null}
79
- */
80
35
  export function anchorSessionCommand(command, anchorDir) {
81
36
  if (!command || !anchorDir) return null;
82
37
  if (!TEARDOWN_RE.test(command)) return null;
@@ -84,10 +39,6 @@ export function anchorSessionCommand(command, anchorDir) {
84
39
  return { command: `export ${ANCHOR_KEY}=${shq(anchorDir)}; ${command}`, anchor: anchorDir };
85
40
  }
86
41
 
87
- /**
88
- * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
89
- * @returns {Promise<object|null>} an updatedInput result, or null (pass).
90
- */
91
42
  export async function run(payload) {
92
43
  try {
93
44
  if (payload?.tool_name && payload.tool_name !== 'Bash') return null;
@@ -95,9 +46,7 @@ export async function run(payload) {
95
46
  const command = payload?.tool_input?.command || '';
96
47
  if (!command) return null;
97
48
 
98
- // CLAUDE_PROJECT_DIR first: it is what the project hooks resolve
99
- // `.morph/framework` against — the thing a teardown would pull out from
100
- // under this session. The payload cwd is the fallback.
49
+ // CLAUDE_PROJECT_DIR first: it is what the project hooks resolve `.morph/framework` against.
101
50
  const anchored = anchorSessionCommand(command, process.env.CLAUDE_PROJECT_DIR || payload?.cwd || '');
102
51
  if (anchored) {
103
52
  return updatedInputResult(