@polymorphism-tech/morph-spec 9.0.0 → 9.2.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 (345) hide show
  1. package/CLAUDE.md +8 -9
  2. package/README.md +38 -36
  3. package/bin/morph-spec.js +11 -63
  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 +2 -1
  9. package/framework/MORPH.md +3 -3
  10. package/framework/agents.json +19 -11
  11. package/framework/commands/morph-apply.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +22 -31
  14. package/framework/evals/review-checklist.md +12 -2
  15. package/framework/hooks/README.md +8 -44
  16. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  17. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  18. package/framework/hooks/claude-code/core/chain.js +4 -65
  19. package/framework/hooks/claude-code/core/comment-guard.js +2 -16
  20. package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
  21. package/framework/hooks/claude-code/core/gate-guard.js +7 -67
  22. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  23. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  24. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  25. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  26. package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
  27. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
  28. package/framework/hooks/claude-code/core/state-sync.js +12 -68
  29. package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
  30. package/framework/hooks/claude-code/core/validate-completion.js +11 -57
  31. package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
  32. package/framework/hooks/claude-code/core/worktree-context.js +10 -110
  33. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  34. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  35. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  36. package/framework/hooks/claude-code/statusline.sh +11 -11
  37. package/framework/hooks/shared/activity-logger.js +9 -89
  38. package/framework/hooks/shared/agent-reap.js +2 -17
  39. package/framework/hooks/shared/build-check.js +6 -75
  40. package/framework/hooks/shared/claim-registry.js +9 -119
  41. package/framework/hooks/shared/command-tokens.js +6 -51
  42. package/framework/hooks/shared/comment-rules.js +5 -20
  43. package/framework/hooks/shared/comment-scanner.js +6 -25
  44. package/framework/hooks/shared/comment-stripper.js +11 -85
  45. package/framework/hooks/shared/e2e-check.js +0 -0
  46. package/framework/hooks/shared/errata.js +2 -15
  47. package/framework/hooks/shared/feature-locator.js +11 -95
  48. package/framework/hooks/shared/frontend-locations.js +1 -3
  49. package/framework/hooks/shared/gate-question.js +1 -7
  50. package/framework/hooks/shared/git-exclude.js +3 -20
  51. package/framework/hooks/shared/git-paths.js +8 -104
  52. package/framework/hooks/shared/graph-freshness.js +22 -166
  53. package/framework/hooks/shared/graph-refresh.js +21 -149
  54. package/framework/hooks/shared/heavy-command.js +4 -12
  55. package/framework/hooks/shared/hook-response.js +8 -83
  56. package/framework/hooks/shared/morph-cli.js +8 -52
  57. package/framework/hooks/shared/payload-utils.js +10 -132
  58. package/framework/hooks/shared/phase-utils.js +1 -42
  59. package/framework/hooks/shared/redact-secrets.js +4 -15
  60. package/framework/hooks/shared/reparse-scan.js +6 -66
  61. package/framework/hooks/shared/slice-progress.js +4 -23
  62. package/framework/hooks/shared/source-files.js +4 -17
  63. package/framework/hooks/shared/state-reader.js +13 -183
  64. package/framework/hooks/shared/state-writer.js +8 -52
  65. package/framework/hooks/shared/stdin-reader.js +1 -11
  66. package/framework/hooks/shared/telemetry-logger.js +7 -67
  67. package/framework/hooks/shared/transcript-usage.js +7 -29
  68. package/framework/hooks/shared/worktree-root.js +7 -29
  69. package/framework/rules/csharp-standards.md +1 -1
  70. package/framework/rules/frontend-standards.md +1 -0
  71. package/framework/rules/hangfire-standards.md +1 -1
  72. package/framework/rules/neon-standards.md +1 -1
  73. package/framework/rules/nextjs-standards.md +1 -1
  74. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  75. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  76. package/framework/skills/core/morph-apply/SKILL.md +4 -4
  77. package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
  78. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  79. package/framework/skills/core/morph-media/SKILL.md +1 -1
  80. package/framework/skills/core/morph-plan/SKILL.md +9 -7
  81. package/framework/skills/core/morph-review/SKILL.md +9 -6
  82. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  83. package/framework/skills/core/morph-uiux/SKILL.md +29 -12
  84. package/framework/standards/STANDARDS.json +36 -14
  85. package/framework/standards/STANDARDS.meta.json +16 -2
  86. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  87. package/framework/standards/ai-agents/agent-session.md +1 -1
  88. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  89. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  90. package/framework/standards/ai-agents/guardrails.md +1 -1
  91. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  92. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  93. package/framework/standards/ai-agents/production.md +1 -1
  94. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  95. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  96. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  97. package/framework/standards/ai-agents/setup.md +2 -2
  98. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  99. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  100. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  101. package/framework/standards/ai-agents/workflows.md +1 -1
  102. package/framework/standards/architecture/code-comments.md +1 -1
  103. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  104. package/framework/standards/backend/api/validation.md +1 -1
  105. package/framework/standards/backend/dotnet/testing.md +10 -4
  106. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  107. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  108. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  109. package/framework/standards/data/nosql/cache/redis.md +1 -1
  110. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  111. package/framework/standards/frontend/admin-app/forms.md +1 -1
  112. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  113. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  114. package/framework/standards/frontend/nextjs/testing.md +10 -6
  115. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  116. package/framework/templates/.idea/morph-templates.xml +92 -0
  117. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  118. package/framework/templates/CLAUDE.md.core.template +3 -3
  119. package/framework/templates/IDE-SNIPPETS.md +262 -262
  120. package/framework/templates/REGISTRY.json +9 -9
  121. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  122. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  123. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  124. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  125. package/framework/templates/docs/plan.md +3 -1
  126. package/framework/templates/docs/ui-mockups.md +4 -0
  127. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  128. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  129. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  130. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  131. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  132. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  133. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  134. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  135. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  136. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  137. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  138. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  139. package/framework/templates/state.template.json +17 -17
  140. package/framework/templates/ui/design-system.css +226 -226
  141. package/package.json +1 -1
  142. package/src/commands/context/context-refresh.js +0 -16
  143. package/src/commands/dashboard/dashboard.js +2 -8
  144. package/src/commands/e2e/e2e.js +17 -104
  145. package/src/commands/graph/affected.js +0 -14
  146. package/src/commands/graph/build.js +11 -50
  147. package/src/commands/graph/explain.js +0 -16
  148. package/src/commands/graph/status.js +1 -12
  149. package/src/commands/mcp/mcp-setup.js +4 -62
  150. package/src/commands/observability/cost.js +5 -57
  151. package/src/commands/observability/telemetry.js +1 -14
  152. package/src/commands/project/branch.js +4 -45
  153. package/src/commands/project/claim.js +4 -48
  154. package/src/commands/project/doctor.js +48 -302
  155. package/src/commands/project/env.js +1 -27
  156. package/src/commands/project/fleet.js +3 -32
  157. package/src/commands/project/init.js +4 -68
  158. package/src/commands/project/install-plugin-cmd.js +0 -1
  159. package/src/commands/project/standards.js +1 -62
  160. package/src/commands/project/status.js +0 -53
  161. package/src/commands/project/update.js +31 -239
  162. package/src/commands/project/worktree.js +108 -882
  163. package/src/commands/scaffold/scaffold.js +0 -25
  164. package/src/commands/state/advance.js +14 -85
  165. package/src/commands/state/approve.js +15 -97
  166. package/src/commands/state/archive.js +12 -64
  167. package/src/commands/state/auto-approval.js +2 -5
  168. package/src/commands/state/create.js +22 -109
  169. package/src/commands/state/delete.js +11 -63
  170. package/src/commands/state/finish.js +98 -475
  171. package/src/commands/state/gate-check.js +22 -132
  172. package/src/commands/state/gate-decision.js +3 -39
  173. package/src/commands/state/handoff.js +3 -30
  174. package/src/commands/state/list.js +14 -84
  175. package/src/commands/state/report-check.js +8 -58
  176. package/src/commands/state/retype.js +4 -26
  177. package/src/commands/validation/index.js +0 -3
  178. package/src/commands/validation/mutate.js +11 -11
  179. package/src/commands/validation/plan-check.js +4 -2
  180. package/src/commands/validation/tests-audit.js +7 -0
  181. package/src/commands/validation/validate-feature.js +10 -97
  182. package/src/commands/validation/verify.js +30 -113
  183. package/src/core/paths/output-schema.js +2 -101
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +0 -27
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +22 -304
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +0 -1
  191. package/src/core/work-types.js +1 -36
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
  194. package/src/dashboard/app/components/CommandPalette.jsx +0 -7
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +1 -5
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/main.jsx +13 -13
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
  211. package/src/dashboard/app/pages/Features.jsx +1 -8
  212. package/src/dashboard/app/pages/Guide.jsx +0 -1
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +10 -69
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +0 -6
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +10 -66
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +2 -6
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/dashboard/vite.config.js +24 -24
  233. package/src/lib/ai/ai-checks.js +7 -52
  234. package/src/lib/ai/ai-project-scan.js +14 -108
  235. package/src/lib/ai/code-audit.js +7 -63
  236. package/src/lib/ai/pin-drift.js +8 -51
  237. package/src/lib/ai/registry-audit.js +11 -90
  238. package/src/lib/ci/ci-verify.js +4 -19
  239. package/src/lib/ci/github-ci.js +9 -37
  240. package/src/lib/ci/workflow-scaffold.js +14 -10
  241. package/src/lib/detectors/claude-config-detector.js +13 -74
  242. package/src/lib/detectors/index.js +4 -50
  243. package/src/lib/e2e/applicability.js +37 -0
  244. package/src/lib/e2e/compose-env.js +5 -66
  245. package/src/lib/e2e/stack-teardown.js +6 -40
  246. package/src/lib/generators/architecture-scanner.js +18 -156
  247. package/src/lib/generators/handoff-generator.js +9 -53
  248. package/src/lib/generators/scaffold-generator.js +5 -101
  249. package/src/lib/generators/settings-generator.js +4 -37
  250. package/src/lib/git/branch-classifier.js +11 -84
  251. package/src/lib/git/default-branch.js +14 -169
  252. package/src/lib/git/evidence-staleness.js +1 -0
  253. package/src/lib/git/gh.js +2 -17
  254. package/src/lib/git/pending-prs.js +6 -37
  255. package/src/lib/git/pr-files.js +3 -28
  256. package/src/lib/git/pr-index.js +3 -34
  257. package/src/lib/git/tree-fingerprint.js +3 -15
  258. package/src/lib/git/tree-state.js +6 -33
  259. package/src/lib/graph/god-nodes.js +2 -22
  260. package/src/lib/graph/graph-health.js +3 -24
  261. package/src/lib/graph/graph-loader.js +5 -56
  262. package/src/lib/graph/graphify-runtime.js +7 -37
  263. package/src/lib/graph/graphifyignore.js +8 -74
  264. package/src/lib/graph/sanitize.js +16 -136
  265. package/src/lib/heavy/cleanup.js +1 -1
  266. package/src/lib/installers/effects.js +8 -93
  267. package/src/lib/installers/mcp-installer.js +9 -169
  268. package/src/lib/installers/memory-installer.js +4 -27
  269. package/src/lib/installers/templates-reconcile.js +5 -60
  270. package/src/lib/mutation/adapters.js +6 -17
  271. package/src/lib/mutation/dotnet-introspection.js +20 -106
  272. package/src/lib/mutation/runner.js +23 -75
  273. package/src/lib/mutation/subset.js +1 -4
  274. package/src/lib/mutation/trx.js +0 -1
  275. package/src/lib/plan/artifact-vocabulary.js +5 -25
  276. package/src/lib/plan/checks.js +34 -33
  277. package/src/lib/plan/plan-parser.js +7 -8
  278. package/src/lib/projects/registry.js +7 -44
  279. package/src/lib/review-reports.js +11 -26
  280. package/src/lib/risk-detector.js +41 -173
  281. package/src/lib/specs/adr-index.js +6 -34
  282. package/src/lib/specs/frontmatter.js +2 -15
  283. package/src/lib/specs/gfm-table.js +1 -7
  284. package/src/lib/specs/revocations.js +1 -14
  285. package/src/lib/specs/screen-checklist.js +47 -124
  286. package/src/lib/stack/dotnet-build-ignores.js +2 -6
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +1 -8
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +1 -7
  291. package/src/lib/standards/sections.js +2 -13
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +21 -169
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -240
  296. package/src/lib/tasks/test-runner.js +43 -341
  297. package/src/lib/telemetry/metrics.js +7 -36
  298. package/src/lib/telemetry/telemetry-reader.js +13 -125
  299. package/src/lib/telemetry/verify-event.js +2 -30
  300. package/src/lib/tests-audit/audit.js +96 -0
  301. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  302. package/src/lib/validators/code-budget/index.js +0 -7
  303. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
  304. package/src/lib/validators/contracts/index.js +0 -6
  305. package/src/lib/validators/nextjs/index.js +0 -5
  306. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  307. package/src/lib/validators/packages/index.js +0 -6
  308. package/src/lib/validators/packages/package-validator.js +6 -56
  309. package/src/lib/validators/reuse/index.js +0 -8
  310. package/src/lib/validators/reuse/reuse-validator.js +11 -101
  311. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  312. package/src/lib/validators/traceability-check.js +5 -63
  313. package/src/lib/validators/validation-runner.js +21 -158
  314. package/src/lib/validators/verify-runner.js +85 -376
  315. package/src/lib/work-type-detector.js +5 -67
  316. package/src/lib/worktree/docker-audit.js +17 -136
  317. package/src/lib/worktree/env-allocator.js +10 -98
  318. package/src/lib/worktree/feature-truth.js +12 -115
  319. package/src/lib/worktree/finish-backup.js +32 -242
  320. package/src/lib/worktree/fleet-scanner.js +13 -95
  321. package/src/lib/worktree/ignored-triage.js +15 -91
  322. package/src/lib/worktree/vscode-settings.js +2 -3
  323. package/src/lib/worktree/worktree-audit.js +21 -213
  324. package/src/lib/worktree/worktree-include.js +3 -5
  325. package/src/scripts/global-install.js +3 -9
  326. package/src/scripts/install-plugin.js +1 -13
  327. package/src/scripts/setup-infra.js +17 -147
  328. package/src/utils/agents-installer.js +29 -276
  329. package/src/utils/banner.js +4 -25
  330. package/src/utils/claude-md-injector.js +1 -37
  331. package/src/utils/claude-settings-manager.js +1 -52
  332. package/src/utils/file-copier.js +11 -95
  333. package/src/utils/hooks-installer.js +21 -298
  334. package/src/utils/skills-installer.js +4 -101
  335. package/src/utils/version-checker.js +4 -83
  336. package/framework/commands/morph-proposal.md +0 -9
  337. package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
  338. package/framework/hooks/git/pre-commit/agents.sh +0 -25
  339. package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
  340. package/framework/hooks/git/pre-push/run-tests.sh +0 -44
  341. package/src/commands/utils/index.js +0 -3
  342. package/src/lib/detectors/design-system-detector.js +0 -188
  343. package/src/lib/generators/design-system-generator.js +0 -508
  344. package/src/lib/specs/old-format.js +0 -68
  345. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,28 +1,5 @@
1
- /**
2
- * Activity Logger — Session-scoped hook event log
3
- *
4
- * Writes hook activity to .morph/logs/activity.jsonl for live consumption
5
- * (statusline, `morph-spec fleet`, etc).
6
- *
7
- * Design constraints:
8
- * - Synchronous only (hooks run in process, no async overhead)
9
- * - Fail-silent (never throws — hooks must not be blocked by the logger)
10
- * - ≤5ms per call (no retries, no network)
11
- * - APPEND-ONLY: one `appendFileSync` line per event, O(1) per call. The
12
- * previous format did a read-modify-write of the WHOLE JSON doc per call
13
- * (logHookActivity/logEvent both read, mutated, and rewrote the entire
14
- * file) — O(n²) across a session once tool calls number in the hundreds,
15
- * with 6 hooks each calling this 3-5x per Write. Same discipline as
16
- * telemetry-logger.js's events.jsonl.
17
- * - Windows-safe paths via path.join
18
- *
19
- * Exported names/signatures are unchanged from the previous doc-based
20
- * implementation on purpose — hooks outside this file's ownership
21
- * (gate-guard.js, enforce-phase-writes.js,
22
- * loop-detect.js, protect-spec-files.js, validate-completion.js) call
23
- * logHookActivity/logEvent and must keep working untouched. Only the
24
- * on-disk format changed.
25
- */
1
+ // Append-only on purpose: rewriting a whole JSON doc per event is O(n²) across
2
+ // a session. Fail-silent: never blocks a hook.
26
3
 
27
4
  import {
28
5
  appendFileSync, readFileSync, mkdirSync, existsSync, statSync, renameSync, writeFileSync,
@@ -32,10 +9,7 @@ import { join } from 'path';
32
9
  const LOGS_DIR = '.morph/logs';
33
10
  export const ACTIVITY_FILE = join(LOGS_DIR, 'activity.jsonl');
34
11
 
35
- // SessionStart (worktree-context.js) rotates the log once it crosses this
36
- // size — the archive/rotation call site resetActivity()/archiveSession()
37
- // were written for but never got until now.
38
- export const ROTATE_THRESHOLD_BYTES = 2 * 1024 * 1024; // 2 MB
12
+ export const ROTATE_THRESHOLD_BYTES = 2 * 1024 * 1024;
39
13
 
40
14
  function getActivityPath(projectPath) {
41
15
  return join(projectPath || process.cwd(), ACTIVITY_FILE);
@@ -49,20 +23,17 @@ function ensureLogsDir(projectPath) {
49
23
  }
50
24
 
51
25
  function nowTs() {
52
- return new Date().toTimeString().slice(0, 8); // HH:MM:SS — matches the previous format
26
+ return new Date().toTimeString().slice(0, 8);
53
27
  }
54
28
 
55
- /** Append one JSON line. Fail-silent, O(1) regardless of file size. */
56
29
  function appendLine(projectPath, line) {
57
30
  try {
58
31
  ensureLogsDir(projectPath);
59
32
  appendFileSync(getActivityPath(projectPath), JSON.stringify(line) + '\n', 'utf-8');
60
33
  } catch {
61
- // Fail-silent — never block hooks
62
34
  }
63
35
  }
64
36
 
65
- /** Read every parseable line back. A corrupt/truncated line is skipped, not fatal. */
66
37
  function readLines(projectPath) {
67
38
  try {
68
39
  const activityPath = getActivityPath(projectPath);
@@ -75,7 +46,7 @@ function readLines(projectPath) {
75
46
  try {
76
47
  lines.push(JSON.parse(trimmed));
77
48
  } catch {
78
- // skip a single corrupt line — don't lose the whole stream
49
+ // a process killed mid-write leaves a truncated line
79
50
  }
80
51
  }
81
52
  return lines;
@@ -84,38 +55,15 @@ function readLines(projectPath) {
84
55
  }
85
56
  }
86
57
 
87
- /**
88
- * Log a hook firing event to the session activity log.
89
- *
90
- * @param {string} name - Hook name (e.g. 'dispatch.js', 'enrich-prompt')
91
- * @param {string} event - Claude Code event (e.g. 'PostToolUse', 'UserPromptSubmit')
92
- * @param {string} result - Outcome summary (e.g. 'ok', 'blocked', 'checkpoint_saved')
93
- * @param {string} [projectPath] - Project root (defaults to cwd)
94
- */
95
58
  export function logHookActivity(name, event, result, projectPath) {
96
59
  appendLine(projectPath, { kind: 'hook', name, event, result, ts: nowTs() });
97
60
  }
98
61
 
99
- /**
100
- * Log a structured event to the session activity log.
101
- *
102
- * @param {string} type - Event type (e.g., 'task_start', 'loop_detected')
103
- * @param {Object} data - Event data payload
104
- * @param {string} [projectPath] - Project root (defaults to cwd)
105
- */
106
62
  export function logEvent(type, data, projectPath) {
107
63
  appendLine(projectPath, { kind: 'event', type, data, ts: nowTs() });
108
64
  }
109
65
 
110
- /**
111
- * Read the current activity log, reconstructed into the aggregate shape
112
- * older consumers expect: `{ sessionId, feature, phase, hooks, events }`.
113
- * `sessionId`/`feature`/`phase` come from the synthetic 'session' header
114
- * line `resetActivity()` writes, when present.
115
- *
116
- * @param {string} [projectPath]
117
- * @returns {{ sessionId: string, feature: string, phase: string, hooks: Array, events: Array }|null}
118
- */
66
+ /** @returns {{ sessionId: string, feature: string, phase: string, hooks: Array, events: Array }|null} */
119
67
  export function readActivity(projectPath) {
120
68
  const lines = readLines(projectPath);
121
69
  if (lines.length === 0) return null;
@@ -132,17 +80,8 @@ export function readActivity(projectPath) {
132
80
  };
133
81
  }
134
82
 
135
- /**
136
- * Reset the activity log for a new session — the one operation in this file
137
- * that is NOT append-only (a session boundary, not a log entry). Truncates
138
- * the file and writes a 'session' header line so `readActivity()` can
139
- * recover sessionId/feature/phase.
140
- *
141
- * @param {string} sessionId - ISO timestamp of new session
142
- * @param {string} feature - Active feature name (empty string if none)
143
- * @param {string} phase - Current phase (empty string if none)
144
- * @param {string} [projectPath]
145
- */
83
+ // The one write that truncates: the 'session' header is what `readActivity()`
84
+ // recovers sessionId/feature/phase from.
146
85
  export function resetActivity(sessionId, feature, phase, projectPath) {
147
86
  try {
148
87
  ensureLogsDir(projectPath);
@@ -154,18 +93,9 @@ export function resetActivity(sessionId, feature, phase, projectPath) {
154
93
  };
155
94
  writeFileSync(getActivityPath(projectPath), JSON.stringify(session) + '\n', 'utf-8');
156
95
  } catch {
157
- // Fail-silent
158
96
  }
159
97
  }
160
98
 
161
- /**
162
- * Archive the current activity log to sessions/ for historical logging.
163
- * Only archives when there is at least one hook or event line — a bare (or
164
- * absent) log is not worth preserving. `renameSync` keeps this O(1) even at
165
- * the multi-MB sizes rotation is meant to catch (no read+rewrite of content).
166
- *
167
- * @param {string} [projectPath] - Project root (defaults to cwd)
168
- */
169
99
  export function archiveSession(projectPath) {
170
100
  try {
171
101
  const activityPath = getActivityPath(projectPath);
@@ -183,20 +113,10 @@ export function archiveSession(projectPath) {
183
113
  const archivePath = join(sessionsDir, `${stamp}.jsonl`);
184
114
  renameSync(activityPath, archivePath);
185
115
  } catch {
186
- // Fail-silent
187
116
  }
188
117
  }
189
118
 
190
- /**
191
- * Rotate the activity log when it crosses `thresholdBytes` — the real call
192
- * site for archiveSession() (previously written but never invoked anywhere).
193
- * Called from worktree-context.js on SessionStart, the only reliable
194
- * "between sessions" checkpoint the harness gets.
195
- *
196
- * @param {string} [projectPath] - Project root (defaults to cwd)
197
- * @param {number} [thresholdBytes] - Rotate once the log is at least this large
198
- * @returns {boolean} true if a rotation was performed
199
- */
119
+ /** @returns {boolean} true if a rotation was performed */
200
120
  export function rotateActivityLogIfLarge(projectPath, thresholdBytes = ROTATE_THRESHOLD_BYTES) {
201
121
  try {
202
122
  const activityPath = getActivityPath(projectPath);
@@ -1,15 +1,5 @@
1
- /**
2
- * agent-reap.js — dispara `morph-spec worktree reap` em background no SessionStart.
3
- *
4
- * O hook de SessionStart tem 5 s de timeout e o reaper mede git por árvore: por isso o
5
- * disparo só olha o disco (nenhum subprocesso) e solta este arquivo como worker DESTACADO
6
- * (`--run <raiz>`), que chama o CLI. Só a raiz: o worker nasce com `cwd` no git common dir
7
- * e nunca segura o diretório de uma worktree.
8
- *
9
- * Fail-open em tudo. `MORPH_WORKTREE_REAP=0` desliga.
10
- *
11
- * @module agent-reap
12
- */
1
+ // O SessionStart tem 5 s: o disparo só olha o disco e solta um worker DESTACADO
2
+ // com `cwd` no git common dir, que nunca segura o diretório de uma worktree.
13
3
 
14
4
  import { spawn } from 'child_process';
15
5
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
@@ -29,7 +19,6 @@ function anyEntry(dir, pattern) {
29
19
  try { return readdirSync(dir).some((name) => pattern.test(name)); } catch { return false; }
30
20
  }
31
21
 
32
- /** Uma worktree de agent registrada, ou uma branch `morph-task/agent-*` (solta ou empacotada). */
33
22
  export function hasAgentLeftovers(common) {
34
23
  if (anyEntry(join(common, 'worktrees'), ADMIN_DIR)) return true;
35
24
  if (anyEntry(join(common, 'refs', 'heads', 'morph-task'), AGENT_BRANCH_FILE)) return true;
@@ -49,10 +38,6 @@ function recentlyKicked(marker, now) {
49
38
  }
50
39
  }
51
40
 
52
- /**
53
- * @param {string} dir - diretório do projeto da sessão
54
- * @returns {{ kicked: boolean, reason?: string, pid?: number }}
55
- */
56
41
  export function kickAgentReap(dir, { spawnFn = spawn, env = process.env, now = Date.now } = {}) {
57
42
  if (env.MORPH_WORKTREE_REAP === '0') return { kicked: false, reason: 'disabled' };
58
43
  const common = resolveGitCommonDir(dir);
@@ -1,12 +1,3 @@
1
- /**
2
- * Build/Typecheck verification utility.
3
- *
4
- * Stack-aware: determines the correct build command(s) based on project stack.
5
- * Used by the Stop hook and `morph-spec verify` to verify code health.
6
- *
7
- * Pure functions — no I/O except the actual build execution.
8
- */
9
-
10
1
  import { execSync } from 'child_process';
11
2
  import { existsSync } from 'fs';
12
3
  import { join } from 'path';
@@ -22,25 +13,8 @@ function isFrontendStack(s) {
22
13
  return s.includes('nextjs') || s.includes('node') || s.includes('react') || s.includes('vue');
23
14
  }
24
15
 
25
- /**
26
- * Determine the build command for a SINGLE (non-monorepo) stack string.
27
- *
28
- * Kept for backward compatibility with single-stack projects and existing
29
- * callers. A monorepo stack that mixes a frontend token with "dotnet" (e.g.
30
- * the detector's own `${frontendStack}+dotnet` format, or a hyphenated
31
- * `dotnet-nextjs`) should resolve through `getBuildSteps()` instead: this
32
- * function alone can only ever return ONE command, and — because it checks
33
- * `dotnet` before the frontend tokens — a compound stack always resolves to
34
- * `dotnet build` even when the frontend is what actually needs checking (or
35
- * when there's no root-level `.sln`/`.csproj`, e.g. in a monorepo where the
36
- * backend lives in a subfolder). `getBuildSteps()` runs BOTH sides, each
37
- * from its own working directory, instead of one command winning by
38
- * substring match order.
39
- *
40
- * @param {string} stack - Project stack (e.g., 'dotnet-nextjs', 'nextjs')
41
- * @param {boolean} [hasTsConfig] - Whether tsconfig.json exists (for TS projects)
42
- * @returns {string|null} Build command or null if no check available
43
- */
16
+ // Single stack only: a compound stack resolves to `dotnet build` alone. Use
17
+ // `getBuildSteps()` for those.
44
18
  export function getBuildCommand(stack, hasTsConfig = false) {
45
19
  if (!stack) return null;
46
20
  const s = stack.toLowerCase();
@@ -56,26 +30,8 @@ export function getBuildCommand(stack, hasTsConfig = false) {
56
30
  return null;
57
31
  }
58
32
 
59
- /**
60
- * Resolve the build step(s) to run for a project, monorepo-aware.
61
- *
62
- * A monorepo stack mixes a frontend token and "dotnet" (see
63
- * `detectProject()`'s own `${frontendStack}+dotnet` format) — in that case
64
- * BOTH sides are checked, each from its own working directory
65
- * (`frontendPath` / `backendPath`, as stored in
66
- * `.morph/config/config.json → project`), instead of one command silently
67
- * winning — or running from the wrong cwd — by substring match order.
68
- * Single-stack projects (no `frontendPath`/`backendPath`, or a stack that
69
- * only matches one side) resolve to exactly one step, from `cwd`, matching
70
- * `getBuildCommand()`'s existing single-stack behavior.
71
- *
72
- * @param {string} stack
73
- * `backendTarget` names the solution/project to build (relative to the backend
74
- * directory) when that directory holds none itself.
75
- *
76
- * @param {{ cwd: string, frontendPath?: string, backendPath?: string, backendTarget?: string }} opts
77
- * @returns {Array<{ command: string, cwd: string }>}
78
- */
33
+ // A compound stack checks BOTH sides, each from its own directory.
34
+ // `backendTarget` names the solution when the backend directory holds none.
79
35
  export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTarget } = {}) {
80
36
  if (!stack || !cwd) return [];
81
37
  const s = stack.toLowerCase();
@@ -88,10 +44,7 @@ export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTa
88
44
 
89
45
  if (isFrontendStack(s)) {
90
46
  const frontendDir = frontendPath ? join(cwd, frontendPath) : cwd;
91
- // node_modules guard : a fresh worktree before
92
- // `morph-spec worktree link` has tsconfig.json (versioned) but no toolchain — running
93
- // `npx tsc` there fetches an unrelated "tsc" package and fails noisily. No toolchain →
94
- // no tsc step; the real typecheck runs wherever node_modules exists (root/CI).
47
+ // Without node_modules, `npx tsc` fetches an unrelated "tsc" package.
95
48
  if (
96
49
  existsSync(join(frontendDir, 'tsconfig.json')) &&
97
50
  existsSync(join(frontendDir, 'node_modules'))
@@ -103,39 +56,17 @@ export function getBuildSteps(stack, { cwd, frontendPath, backendPath, backendTa
103
56
  return steps;
104
57
  }
105
58
 
106
- /**
107
- * Parse build command result into a structured object.
108
- *
109
- * @param {number} exitCode - Process exit code (0 = success)
110
- * @param {string} output - stdout+stderr from build
111
- * @returns {{ status: 'pass'|'fail', output: string }}
112
- */
113
59
  export function parseBuildResult(exitCode, output) {
114
60
  const trimmed = (output || '').trim();
115
61
  if (exitCode === 0) {
116
62
  return { status: 'pass', output: '' };
117
63
  }
118
- // Truncate to last 20 lines
119
64
  const lines = trimmed.split('\n');
120
65
  const truncated = lines.slice(-20).join('\n');
121
66
  return { status: 'fail', output: truncated };
122
67
  }
123
68
 
124
- /**
125
- * Run build check for a project — monorepo-aware (see `getBuildSteps()`).
126
- *
127
- * For a compound stack (e.g. "nextjs+dotnet"), runs each resolved step in
128
- * its own working directory and fails if any step fails; output from all
129
- * failing steps is concatenated (prefixed by the command that produced it)
130
- * so a frontend typecheck failure and a backend build failure are never
131
- * silently masked by one another.
132
- *
133
- * @param {string} cwd - Project root directory
134
- * @param {string} stack - Project stack string
135
- * @param {number} [timeout=30000] - Timeout in ms
136
- * @param {{ frontendPath?: string, backendPath?: string, backendTarget?: string }} [paths] - Monorepo sub-paths, from config.json → project
137
- * @returns {{ status: 'pass'|'fail'|'skip', output: string }}
138
- */
69
+ /** @returns {{ status: 'pass'|'fail'|'skip', output: string }} */
139
70
  export function runBuildCheck(cwd, stack, timeout = 30000, paths = {}) {
140
71
  const steps = getBuildSteps(stack, { cwd, frontendPath: paths.frontendPath, backendPath: paths.backendPath, backendTarget: paths.backendTarget });
141
72
  if (steps.length === 0) return { status: 'skip', output: '' };
@@ -1,44 +1,13 @@
1
- /**
2
- * claim-registry.js — advisory feature ownership across parallel sessions.
3
- *
4
- * Two Claude Code sessions worked the SAME feature at the same time without
5
- * seeing each other: one finished it and removed the worktree out from under
6
- * the other's running sub-agent. A complete recap.md was discarded, a sub-agent
7
- * wasted, and a decision taken on state that was already obsolete. There was no
8
- * lock, no claim, no heartbeat — not even a warning.
9
- *
10
- * DELIBERATELY ADVISORY. This is not a lock: nothing here ever blocks a
11
- * command. The framework is fail-open by principle, and a strong lock left
12
- * behind by a crashed session would strand a feature for no gain. Being
13
- * VISIBLE is the whole fix.
14
- *
15
- * Location: `<git-common-dir>/morph-spec/claims.json`, beside the fleet's
16
- * env-registry, for the same three reasons — it can never dirty the `git
17
- * status` the finish gates read, it survives `git clean -fdx`, and the COMMON
18
- * dir is the one place every linked worktree of the repo shares. That last one
19
- * is decisive here: in the observed incident one session sat inside the
20
- * worktree and the other in the primary root, so a claim written to the
21
- * branch-local `feature.json` would have been invisible to exactly the session
22
- * that needed to read it.
23
- *
24
- * Writes are tmp+rename (torn file impossible); a corrupt registry reads as
25
- * empty — the cost is a lost warning, never a crash.
26
- */
1
+ // ADVISORY, never a lock: a lock left by a crashed session would strand a feature.
2
+ // Lives in the git COMMON dir, the one place every worktree of the repo shares.
27
3
 
28
4
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
29
5
  import { dirname, join } from 'path';
30
6
 
31
- /**
32
- * A claim with no heartbeat for this long is considered dead. The heartbeat
33
- * fires on tool activity, so this is really "how long a session may sit idle
34
- * before its claim stops warning others".
35
- */
36
7
  export const CLAIM_TTL_MS = 30 * 60 * 1000;
37
8
 
38
- /** Minimum gap between heartbeat WRITES. The heartbeat runs on every tool call. */
39
9
  export const HEARTBEAT_THROTTLE_MS = 60 * 1000;
40
10
 
41
- /** Claim file path for a repo's common git dir. */
42
11
  export function claimsPath(gitCommonDir) {
43
12
  return join(gitCommonDir, 'morph-spec', 'claims.json');
44
13
  }
@@ -60,23 +29,13 @@ function writeRegistry(gitCommonDir, registry) {
60
29
  renameSync(tmp, path);
61
30
  }
62
31
 
63
- /** Is this claim still within its TTL? A missing/unparseable heartbeat is dead. */
64
32
  export function isLive(claim, now = Date.now()) {
65
33
  const beat = Date.parse(claim?.heartbeatAt ?? '');
66
34
  return Number.isFinite(beat) && now - beat < CLAIM_TTL_MS;
67
35
  }
68
36
 
69
- /**
70
- * Claim a feature for this session, or refresh an existing claim of ours.
71
- *
72
- * A claim held by ANOTHER session is TAKEN OVER, not refused — advisory by
73
- * design, and the caller has already been warned via `foreignClaim`. The
74
- * takeover is recorded in `previousSessionId` so it is never silent.
75
- *
76
- * @param {string} feature
77
- * @param {{ gitCommonDir: string, sessionId: string, pid?: number, path?: string }} opts
78
- * @returns {object} the stored claim
79
- */
37
+ // Another session's claim is TAKEN OVER, not refused; `previousSessionId` keeps
38
+ // the takeover visible.
80
39
  export function acquireClaim(feature, { gitCommonDir, sessionId, pid = process.pid, path = null }) {
81
40
  const registry = readRegistry(gitCommonDir);
82
41
  const previous = registry.claims[feature];
@@ -95,17 +54,7 @@ export function acquireClaim(feature, { gitCommonDir, sessionId, pid = process.p
95
54
  return claim;
96
55
  }
97
56
 
98
- /**
99
- * Refresh our own claim's heartbeat. Throttled — this runs on every tool call,
100
- * and a write per call would be pure I/O waste.
101
- *
102
- * Never touches a claim owned by another session: a heartbeat is proof of life,
103
- * and forging someone else's would defeat the TTL.
104
- *
105
- * @param {string} feature
106
- * @param {{ gitCommonDir: string, sessionId: string }} opts
107
- * @returns {boolean} true when a write actually happened
108
- */
57
+ // Never another session's claim: forging its heartbeat would defeat the TTL.
109
58
  export function heartbeatClaim(feature, { gitCommonDir, sessionId }) {
110
59
  const registry = readRegistry(gitCommonDir);
111
60
  const claim = registry.claims[feature];
@@ -119,45 +68,13 @@ export function heartbeatClaim(feature, { gitCommonDir, sessionId }) {
119
68
  return true;
120
69
  }
121
70
 
122
- /** Local, git-independent heartbeat pre-check marker — see `shouldHeartbeat`. */
123
71
  function heartbeatMarkPath(cwd) {
124
72
  return join(cwd, '.morph', 'logs', '.heartbeat-check.json');
125
73
  }
126
74
 
127
- /**
128
- * Cheap, git-independent pre-check for whether a heartbeat is even worth
129
- * ATTEMPTING. `heartbeatClaim` already throttles the WRITE to `claims.json`,
130
- * but that registry lives under the git COMMON dir — resolving it (even via
131
- * the pure-FS `git-paths.js`, never mind the old `execSync('git rev-parse
132
- * --git-common-dir')`) is work `telemetry-log.js` pays on every tool call.
133
- * This answers "is it due" from a tiny marker colocated with the CALLER's own
134
- * `.morph/logs/` (already known, no git lookup of any kind) so that on ~99%
135
- * of calls NOTHING git-related happens at all — not the subprocess, not even
136
- * the FS walk.
137
- *
138
- * Side effect: when it returns `true` (heartbeat IS due), it immediately
139
- * stamps the marker — mirroring `heartbeatClaim`'s own check-and-write
140
- * pattern — so the NEXT call within the throttle window short-circuits here
141
- * even if the caller's subsequent git resolution then fails (not actually a
142
- * git repo, say): a failed attempt should not be retried on every single tool
143
- * call either.
144
- *
145
- * This is a SEPARATE, LOCAL throttle from `claims.json`'s own authoritative
146
- * `heartbeatAt` (keyed by git-common-dir, enforced inside `heartbeatClaim`) —
147
- * the two can drift by up to one throttle window with no consequence: a
148
- * missed local pre-check only costs a claim heartbeat that gets written on
149
- * the very next call, and claims are advisory to begin with.
150
- *
151
- * Fail-open: any read/write error is treated as "due" — a broken marker must
152
- * never permanently suppress the actual (authoritative) heartbeat. Not
153
- * tmp+rename like `claims.json` — this file is low-stakes (worst case from a
154
- * torn write is corrupt JSON, which just reads back as "due" next time) and
155
- * shared by nobody outside this one project tree.
156
- *
157
- * @param {string} feature
158
- * @param {{ cwd: string, sessionId: string, now?: number }} opts
159
- * @returns {boolean}
160
- */
75
+ // Local pre-check so most tool calls skip resolving the git common dir. Stamps
76
+ // the marker when due, so a failed attempt is not retried on every call; an
77
+ // unreadable marker counts as due.
161
78
  export function shouldHeartbeat(feature, { cwd, sessionId, now = Date.now() }) {
162
79
  const markPath = heartbeatMarkPath(cwd);
163
80
  try {
@@ -167,27 +84,16 @@ export function shouldHeartbeat(feature, { cwd, sessionId, now = Date.now() }) {
167
84
  if (Number.isFinite(at) && now - at < HEARTBEAT_THROTTLE_MS) return false;
168
85
  }
169
86
  } catch {
170
- // missing/corrupt marker — treat as due
171
87
  }
172
88
 
173
89
  try {
174
90
  mkdirSync(dirname(markPath), { recursive: true });
175
91
  writeFileSync(markPath, JSON.stringify({ feature, sessionId, at: new Date(now).toISOString() }));
176
92
  } catch {
177
- // best-effort — a failed stamp only costs a repeated attempt next call
178
93
  }
179
94
  return true;
180
95
  }
181
96
 
182
- /**
183
- * The LIVE claim on this feature held by a session that is not us — the thing
184
- * worth warning about. Returns null when the feature is unclaimed, claimed by
185
- * us, or claimed by a session whose heartbeat went stale.
186
- *
187
- * @param {string} feature
188
- * @param {{ gitCommonDir: string, sessionId?: string }} opts
189
- * @returns {object|null}
190
- */
191
97
  export function foreignClaim(feature, { gitCommonDir, sessionId }) {
192
98
  const claim = readRegistry(gitCommonDir).claims[feature];
193
99
  if (!claim) return null;
@@ -195,14 +101,6 @@ export function foreignClaim(feature, { gitCommonDir, sessionId }) {
195
101
  return isLive(claim) ? claim : null;
196
102
  }
197
103
 
198
- /**
199
- * Drop a feature's claim. Called by the manual `morph-spec claim <f> --release`
200
- * (recovering from a session that died without releasing) and by `finish`.
201
- *
202
- * @param {string} feature
203
- * @param {{ gitCommonDir: string }} opts
204
- * @returns {boolean} false when there was nothing to release
205
- */
206
104
  export function releaseClaim(feature, { gitCommonDir }) {
207
105
  const registry = readRegistry(gitCommonDir);
208
106
  if (!registry.claims[feature]) return false;
@@ -211,14 +109,7 @@ export function releaseClaim(feature, { gitCommonDir }) {
211
109
  return true;
212
110
  }
213
111
 
214
- /**
215
- * Every claim on record, live and stale, newest heartbeat first. `live` is
216
- * computed, never stored — a stored flag would go wrong the moment nobody
217
- * rewrites the file.
218
- *
219
- * @param {string} gitCommonDir
220
- * @returns {object[]}
221
- */
112
+ // `live` is computed, never stored: a stored flag goes wrong once nobody rewrites the file.
222
113
  export function listClaims(gitCommonDir) {
223
114
  const now = Date.now();
224
115
  return Object.values(readRegistry(gitCommonDir).claims)
@@ -226,7 +117,6 @@ export function listClaims(gitCommonDir) {
226
117
  .sort((a, b) => String(b.heartbeatAt).localeCompare(String(a.heartbeatAt)));
227
118
  }
228
119
 
229
- /** Human-readable age of a claim's last heartbeat ("3 min", "2 h", "—"). */
230
120
  export function claimAge(claim, now = Date.now()) {
231
121
  const beat = Date.parse(claim?.heartbeatAt ?? '');
232
122
  if (!Number.isFinite(beat)) return '—';
@@ -1,19 +1,6 @@
1
- /**
2
- * command-tokens.js — shell-ish tokenizer shared by the PreToolUse Bash guards.
3
- *
4
- * Lives in shared/ (not inside a hook) on purpose: every core hook ends in a
5
- * standalone tail that reads stdin and exits, so a hook importing a sibling
6
- * hook for a helper would fire THAT tail in its own process — consuming the
7
- * payload and exiting before the importer ever ran. block-raw-worktree-add
8
- * and block-destructive-worktree-removal both read paths off these tokens,
9
- * so they agree on what a quoted path is.
10
- */
1
+ // Lives in shared/: a hook importing a sibling hook would fire that hook's
2
+ // stdin-reading tail in its own process.
11
3
 
12
- /**
13
- * Split a shell-ish command into tokens, honoring single and double quotes.
14
- * @param {string} command
15
- * @returns {string[]}
16
- */
17
4
  export function tokenize(command) {
18
5
  const tokens = [];
19
6
  const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
@@ -22,7 +9,6 @@ export function tokenize(command) {
22
9
  return tokens;
23
10
  }
24
11
 
25
- /** Index of the first pipe (`|`, not `||` nor `>|`) outside quotes, or -1. */
26
12
  function firstUnquotedPipe(command) {
27
13
  const s = String(command || '');
28
14
  let quote = null;
@@ -43,24 +29,11 @@ function firstUnquotedPipe(command) {
43
29
  return -1;
44
30
  }
45
31
 
46
- /**
47
- * Does the command pipe one process into another outside quotes? Without
48
- * `pipefail` the shell reports the LAST command's status, so the exit code of
49
- * `dotnet test … | grep …` is grep's, not the test run's.
50
- * @param {string} command
51
- * @returns {boolean}
52
- */
53
32
  export function hasUnquotedPipe(command) {
54
33
  return firstUnquotedPipe(command) >= 0;
55
34
  }
56
35
 
57
- /**
58
- * The work a command does, without the filter that shapes its output:
59
- * `dotnet test 2>&1 | tail -25` and `dotnet test | tail -20` run the same
60
- * tests. Whitespace collapsed, stderr redirections dropped.
61
- * @param {string} command
62
- * @returns {string}
63
- */
36
+ /** The command without the pipe filter and stderr redirections that shape its output. */
64
37
  export function producerOf(command) {
65
38
  const s = String(command || '');
66
39
  const pipe = firstUnquotedPipe(s);
@@ -70,11 +43,6 @@ export function producerOf(command) {
70
43
  .trim();
71
44
  }
72
45
 
73
- /**
74
- * The command split at `;`, newline and background `&` outside quotes, plus
75
- * whether a background `&` was seen. `&&`, `||` and `>&`/`&>` redirections
76
- * stay inside their segment.
77
- */
78
46
  function topLevelSegments(command) {
79
47
  const s = String(command || '');
80
48
  const segments = [];
@@ -102,14 +70,7 @@ function topLevelSegments(command) {
102
70
 
103
71
  const REPORTER = /^(?:echo|printf|true|:|exit\s+0)\b/;
104
72
 
105
- /**
106
- * Is the exit code of this command the exit code of the work it ran? The
107
- * shell reports the last segment of a `;` sequence: `dotnet test > log;
108
- * echo "exit=$?"` always exits 0, and so does `dotnet test || true`. A pipe
109
- * without `pipefail` reports the filter, and a background `&` reports 0.
110
- * @param {string} command
111
- * @returns {boolean}
112
- */
73
+ /** True when the shell reports something other than the work's exit code (`; echo`, `|| true`, pipe, `&`). */
113
74
  export function exitCodeMasked(command) {
114
75
  const { segments, background } = topLevelSegments(command);
115
76
  if (background) return true;
@@ -253,14 +214,8 @@ function commandWriteTargets(head, body) {
253
214
  return targets;
254
215
  }
255
216
 
256
- /**
257
- * Files a command writes: stdout redirection (`>`, `>>`, `1>`, `&>`), `tee`, `sed -i`,
258
- * `perl -i`, the `cp`/`mv` destination, and a python/node `-c`/`-e`/heredoc script's writes.
259
- * The redirect scan stops at the line that opens a heredoc: the body is data, and an
260
- * apostrophe in it would derail the quote-aware split. Quoted operators are text.
261
- * @param {string} command
262
- * @returns {string[]}
263
- */
217
+ // The redirect scan stops at the heredoc line: the body is data, and an
218
+ // apostrophe in it would derail the quote-aware split.
264
219
  export function writeTargets(command) {
265
220
  const lines = String(command || '').split('\n');
266
221
  const heredocAt = lines.findIndex((line) => line.includes('<<'));