@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,11 +1,4 @@
1
- /**
2
- * Shared read-only state accessor for Claude Code hooks.
3
- *
4
- * Wraps state.json reads with fail-open semantics:
5
- * - Returns null instead of throwing on missing/corrupt state
6
- * - Never writes to state.json (read-only)
7
- * - Uses synchronous reads for <100ms performance
8
- */
1
+ // Fail-open: missing or corrupt state reads as null, never throws.
9
2
 
10
3
  import { readFileSync, existsSync, readdirSync } from 'fs';
11
4
  import { join } from 'path';
@@ -13,80 +6,15 @@ import { join } from 'path';
13
6
  const STATE_FILE = '.morph/state.json';
14
7
  const FEATURES_DIR = '.morph/features';
15
8
 
16
- /**
17
- * Module-level memoization of `loadState()`, keyed by `projectPath`.
18
- *
19
- * `loadState()` does a `readdirSync` of `.morph/features` plus a
20
- * `readFileSync` + `JSON.parse` of EVERY `feature.json` under it, on every
21
- * call. A single hook invocation calls it 1-3x (directly, or via
22
- * `getActiveFeature`/`derivePhaseForFeature`/`isGateApproved`/etc.) — re-doing
23
- * that full hydration each time is pure waste when nothing on disk changed
24
- * between those calls.
25
- *
26
- * SAFE because every hook process is short-lived and single-invocation: one
27
- * `node state-reader.js`-importing process handles exactly one tool call (or
28
- * one CLI command) and exits. There is no risk of the cache going stale
29
- * ACROSS invocations, because each invocation starts with an empty Map.
30
- * Within a SINGLE invocation, staleness would only matter if that same
31
- * process both reads and then writes a feature's `feature.json` and
32
- * expects a LATER read to see the write — verified (grep across every
33
- * consumer, 2026-08-11) that the one hook that writes feature.json
34
- * (`state-sync.js`) never re-reads through `loadState()` after writing in the
35
- * same invocation; its `AskUserQuestion` handler reads via
36
- * `readFeatureFile()` (a different, uncached module) as the PRIMARY path and
37
- * only falls back to `loadState()` for a feature that has NEVER had a
38
- * feature.json written — which, by construction, can't have just been
39
- * written to in this same invocation either.
40
- *
41
- * A directory-mtime-based invalidation was considered instead (so a
42
- * LONG-LIVED consumer could reuse this module safely too) but rejected on two
43
- * grounds: (1) verified — grep for every importer of this file — that none is
44
- * long-lived (they're all hooks, one process per tool call, or CLI commands,
45
- * one process per invocation; the dashboard is a separate Vite/React app and
46
- * the statusline is Python, neither imports this module); and (2) even if one
47
- * existed, `.morph/features`'s own mtime only changes when feature
48
- * DIRECTORIES are added/removed, not when an EXISTING feature.json's content
49
- * is overwritten in place — so mtime-of-the-dir can't actually detect the
50
- * mutation that matters here, and getting that right would mean stat'ing
51
- * every feature.json individually, which re-adds the very cost this cache
52
- * exists to remove. If a long-lived consumer is ever added, it should
53
- * re-derive state per request rather than lean on this cache.
54
- *
55
- * CAVEAT for any future consumer: the object returned by `loadState()` is the
56
- * SAME object on every cache hit, not a fresh copy — a caller that mutates it
57
- * (as `state-sync.js`'s `handleWriteEdit` does, in-place, before persisting)
58
- * is mutating the cached entry too. That's harmless today because nothing
59
- * currently reads the cache again after such a mutation within one process,
60
- * but treat the return value as read-only unless you've re-verified that.
61
- *
62
- * `clearStateCache()` is exported for tests that write/rewrite feature.json
63
- * and then call a `loadState()`-backed function again WITHIN THE SAME
64
- * process and need to see the fresh content — call it at the point of
65
- * mutation.
66
- */
9
+ // Memoized per process: each hook/CLI process handles one call. The returned
10
+ // object is shared, so treat it as read-only; writers call `clearStateCache()`.
67
11
  const stateCache = new Map();
68
12
 
69
- /** Drop all cached state — see `stateCache` above for when this is needed. */
70
13
  export function clearStateCache() {
71
14
  stateCache.clear();
72
15
  }
73
16
 
74
- /**
75
- * Load state from the project root, hydrating each feature from its
76
- * authoritative per-feature `feature.json`.
77
- *
78
- * The root `.morph/state.json` is a thin, gitignored index (status + updatedAt
79
- * per feature); the heavy feature data lives in `.morph/features/{name}/feature.json`
80
- * (committed). This reader merges them: it reads the thin index for project /
81
- * metadata and overlays each feature.json so callers see the full feature object.
82
- * A single corrupt feature.json is skipped, not fatal.
83
- *
84
- * Fail-open: returns null on any unexpected error or when there is no state at
85
- * all (no index and no feature files). Memoized per `projectPath` for the
86
- * lifetime of this process — see `stateCache` above.
87
- * @param {string} [projectPath] - Project root (defaults to cwd)
88
- * @returns {Object|null} State object or null
89
- */
17
+ /** The thin `state.json` index overlaid with each authoritative `feature.json`; null when neither exists. */
90
18
  export function loadState(projectPath) {
91
19
  const base = projectPath || process.cwd();
92
20
  if (stateCache.has(base)) return stateCache.get(base);
@@ -119,7 +47,7 @@ export function loadState(projectPath) {
119
47
  state.features = state.features || {};
120
48
  state.features[name] = fj; // feature.json is authoritative
121
49
  } catch {
122
- // skip a single corrupt feature.json — don't blind every hook
50
+ // one corrupt feature.json must not blind every hook
123
51
  }
124
52
  }
125
53
  }
@@ -134,17 +62,8 @@ export function loadState(projectPath) {
134
62
  return state;
135
63
  }
136
64
 
137
- /**
138
- * Check if there is state to read: either the thin `.morph/state.json` index
139
- * exists, or `.morph/features/` (committed, gitignore-exempt) already has at
140
- * least one feature with a `feature.json`. The latter case covers a freshly
141
- * checked-out worktree where state.json (gitignored) hasn't been materialized
142
- * yet, but `loadState()` can already self-heal from feature.json — so hooks
143
- * gating on `stateExists()` should be "on" there too, not dark.
144
- * Fail-open: any unexpected error (e.g. permission denied) returns false.
145
- * @param {string} [projectPath]
146
- * @returns {boolean}
147
- */
65
+ // A fresh worktree has `feature.json` but no gitignored `state.json` yet; hooks
66
+ // must stay on there.
148
67
  export function stateExists(projectPath) {
149
68
  try {
150
69
  const base = projectPath || process.cwd();
@@ -163,11 +82,6 @@ export function stateExists(projectPath) {
163
82
  }
164
83
  }
165
84
 
166
- /**
167
- * Get the active feature (most recently updated in_progress feature).
168
- * @param {string} [projectPath]
169
- * @returns {{ name: string, feature: Object }|null}
170
- */
171
85
  export function getActiveFeature(projectPath) {
172
86
  const state = loadState(projectPath);
173
87
  if (!state?.features) return null;
@@ -188,12 +102,6 @@ export function getActiveFeature(projectPath) {
188
102
  return active;
189
103
  }
190
104
 
191
- /**
192
- * Get the most recently updated feature (any status).
193
- * Useful as a fallback when getActiveFeature returns null (all features done).
194
- * @param {string} [projectPath]
195
- * @returns {{ name: string, feature: Object }|null}
196
- */
197
105
  export function getMostRecentFeature(projectPath) {
198
106
  const state = loadState(projectPath);
199
107
  if (!state?.features) return null;
@@ -212,39 +120,15 @@ export function getMostRecentFeature(projectPath) {
212
120
  return result;
213
121
  }
214
122
 
215
- /**
216
- * Get a specific feature by name.
217
- * @param {string} featureName
218
- * @param {string} [projectPath]
219
- * @returns {Object|null}
220
- */
221
123
  export function getFeature(featureName, projectPath) {
222
124
  const state = loadState(projectPath);
223
125
  return state?.features?.[featureName] || null;
224
126
  }
225
127
 
226
- /**
227
- * Derive phase from filesystem (phase deleted from state).
228
- *
229
- * Walks the phase map in descending order and returns the first phase whose
230
- * CANONICAL ARTIFACT exists. An empty folder is not enough — a pre-created
231
- * sibling folder never advances the phase before its content is written.
232
- *
233
- * Canonical artifacts (one per phase) match MORPH.md §3:
234
- * - 4-review → verifier.md
235
- * - 3-implement → any slice report (`reports/*.md`)
236
- * - 1-design → design-system.md
237
- * - 2-plan → plan.md
238
- *
239
- * If no canonical file is found, returns `'plan'` (the starting phase).
240
- */
241
128
  export const PHASE_INDEX = { plan: 0, uiux: 1, implement: 2, review: 3 };
242
129
 
243
- /**
244
- * The canonical artifact of each phase, descending. Exported so a caller that
245
- * holds the file list from somewhere OTHER than the local filesystem (a git
246
- * tree, say) can answer the same question without re-encoding the map.
247
- */
130
+ // Descending. An empty folder is not an artifact: a pre-created folder must
131
+ // not advance the phase.
248
132
  export const PHASE_ARTIFACTS = [
249
133
  ['4-review', 'review', 'verifier.md'],
250
134
  ['3-implement', 'implement', 'reports/'],
@@ -256,18 +140,6 @@ const DESIGN_DIR = '1-design';
256
140
 
257
141
  const gatesOf = (feature) => feature?.approvals || feature?.approvalGates;
258
142
 
259
- /**
260
- * Gate-implied phase from an ALREADY-LOADED feature object — no disk access.
261
- * An approved gate authorizes the NEXT phase:
262
- * review approved → review (terminal)
263
- * plan approved → implement, or uiux while the design branch was taken
264
- * (`1-design/` exists) and its gate is still open
265
- * otherwise → plan
266
- *
267
- * @param {Object|null} feature
268
- * @param {boolean} [designStarted] - whether `1-design/` exists for the feature
269
- * @returns {string} phase name
270
- */
271
143
  export function gateImpliedPhaseFrom(feature, designStarted = false) {
272
144
  const gates = gatesOf(feature);
273
145
  const ok = (g) => gates?.[g]?.approved === true;
@@ -276,10 +148,7 @@ export function gateImpliedPhaseFrom(feature, designStarted = false) {
276
148
  return 'plan';
277
149
  }
278
150
 
279
- /**
280
- * Artifact predicate over a feature dir on disk. A path ending in `/` asks for
281
- * any `.md` file directly under it: an empty folder is not an artifact.
282
- */
151
+ /** A path ending in `/` asks for any `.md` file directly under it. */
283
152
  export function diskArtifactProbe(featureDir) {
284
153
  return (rel) => {
285
154
  const abs = join(featureDir, ...rel.split('/').filter(Boolean));
@@ -292,7 +161,6 @@ export function diskArtifactProbe(featureDir) {
292
161
  };
293
162
  }
294
163
 
295
- /** The same predicate over a set of feature-relative POSIX paths (a git tree). */
296
164
  export function fileSetArtifactProbe(files) {
297
165
  return (rel) => {
298
166
  if (!rel.endsWith('/')) return files.has(rel);
@@ -303,30 +171,20 @@ export function fileSetArtifactProbe(files) {
303
171
  };
304
172
  }
305
173
 
306
- /**
307
- * Phase from an already-loaded feature plus an artifact predicate — no disk
308
- * access of its own, so the artifact universe can be a git tree.
309
- *
310
- * @param {Object|null} feature
311
- * @param {(relPath: string) => boolean} hasArtifact - {@link diskArtifactProbe}
312
- * or {@link fileSetArtifactProbe}: POSIX paths relative to the feature dir
313
- * @returns {string} phase name
314
- */
174
+ /** @param {(relPath: string) => boolean} hasArtifact - {@link diskArtifactProbe} or {@link fileSetArtifactProbe} */
315
175
  export function derivePhaseFrom(feature, hasArtifact) {
316
176
  let artifactPhase = 'plan';
317
177
  for (const [folder, phase, canonical] of PHASE_ARTIFACTS) {
318
178
  if (hasArtifact(`${folder}/${canonical}`)) { artifactPhase = phase; break; }
319
179
  }
320
180
 
321
- // Gate-aware: an approved gate advances the phase even before the next
322
- // phase's first canonical artifact exists, or that artifact could never be
323
- // written. Take whichever of (artifact, gate-implied) is later.
181
+ // An approved gate advances the phase before the next artifact exists, or
182
+ // that artifact could never be written.
324
183
  const designStarted = hasArtifact(DESIGN_DIR) || hasArtifact(`${DESIGN_DIR}/design-system.md`);
325
184
  const implied = gateImpliedPhaseFrom(feature, designStarted);
326
185
  return PHASE_INDEX[implied] > PHASE_INDEX[artifactPhase] ? implied : artifactPhase;
327
186
  }
328
187
 
329
- /** The phase implied purely by approved gates — see {@link gateImpliedPhaseFrom}. */
330
188
  export function gateImpliedPhase(featureName, projectPath) {
331
189
  const designDir = join(projectPath || process.cwd(), '.morph/features', featureName, DESIGN_DIR);
332
190
  return gateImpliedPhaseFrom(getFeature(featureName, projectPath), existsSync(designDir));
@@ -337,41 +195,19 @@ export function derivePhaseForFeature(featureName, projectPath) {
337
195
  return derivePhaseFrom(getFeature(featureName, projectPath), diskArtifactProbe(basePath));
338
196
  }
339
197
 
340
- /**
341
- * Get the current phase of a feature.
342
- * Falls back to filesystem derivation when phase is not in state.
343
- * @param {string} featureName
344
- * @param {string} [projectPath]
345
- * @returns {string|null}
346
- */
347
198
  export function getFeaturePhase(featureName, projectPath) {
348
199
  const feature = getFeature(featureName, projectPath);
349
200
  if (!feature) return null;
350
201
  return feature.phase || derivePhaseForFeature(featureName, projectPath);
351
202
  }
352
203
 
353
- /**
354
- * Get approval status for a gate.
355
- * @param {string} featureName
356
- * @param {string} gate - 'plan'|'uiux'|'review'
357
- * @param {string} [projectPath]
358
- * @returns {boolean}
359
- */
360
204
  export function isGateApproved(featureName, gate, projectPath) {
361
205
  const feature = getFeature(featureName, projectPath);
362
- // Support both field names: 'approvals' (v5 state) and 'approvalGates' (legacy)
363
206
  const gates = feature?.approvals || feature?.approvalGates;
364
207
  return gates?.[gate]?.approved === true;
365
208
  }
366
209
 
367
- /**
368
- * Get all pending approval gates for a feature.
369
- * @param {string} featureName
370
- * @param {string} [projectPath]
371
- * @returns {string[]} List of gate names that are not yet approved
372
- */
373
210
  export function pendingGatesFrom(feature) {
374
- // Support both field names: 'approvals' (v5 state) and 'approvalGates' (legacy)
375
211
  const gates = gatesOf(feature);
376
212
  if (!gates) return [];
377
213
 
@@ -384,12 +220,6 @@ export function getPendingGates(featureName, projectPath) {
384
220
  return pendingGatesFrom(getFeature(featureName, projectPath));
385
221
  }
386
222
 
387
- /**
388
- * Get feature outputs status.
389
- * @param {string} featureName
390
- * @param {string} [projectPath]
391
- * @returns {Object|null} outputs map or null
392
- */
393
223
  export function getOutputs(featureName, projectPath) {
394
224
  const feature = getFeature(featureName, projectPath);
395
225
  return feature?.outputs || null;
@@ -1,14 +1,5 @@
1
- /**
2
- * Shared per-feature state writer for Claude Code hooks.
3
- *
4
- * Hooks run inside the user's project and cannot import the CLI's
5
- * src/core/state-manager.js. This module is the hook-side mirror of the core
6
- * writers: it persists a feature to its authoritative `feature.json` (committed)
7
- * and refreshes the thin, gitignored root index (`.morph/state.json`).
8
- *
9
- * Fail-open by contract: every function swallows errors and returns a boolean —
10
- * a hook must never break the agent over a state write.
11
- */
1
+ // Hook-side mirror of the CLI state writers (hooks cannot import `src/`).
2
+ // Fail-open: every function returns a boolean, never throws.
12
3
 
13
4
  import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'fs';
14
5
  import { join, dirname } from 'path';
@@ -17,12 +8,10 @@ import { clearStateCache } from './state-reader.js';
17
8
  const STATE_FILE = '.morph/state.json';
18
9
  const STATE_VERSION = '5.0.0';
19
10
 
20
- /** Absolute path to a feature's authoritative feature.json. */
21
11
  export function featureJsonPath(featureName, projectPath) {
22
12
  return join(projectPath || process.cwd(), '.morph', 'features', featureName, 'feature.json');
23
13
  }
24
14
 
25
- /** Read a feature's feature.json, or null if missing/corrupt. */
26
15
  export function readFeatureFile(featureName, projectPath) {
27
16
  const p = featureJsonPath(featureName, projectPath);
28
17
  if (!existsSync(p)) return null;
@@ -33,7 +22,7 @@ export function readFeatureFile(featureName, projectPath) {
33
22
  }
34
23
  }
35
24
 
36
- /** Atomic JSON write: per-pid temp then rename, with an EPERM retry for Windows. */
25
+ // EPERM retry: Windows rename fails while another process holds the target.
37
26
  function atomicWriteJson(targetPath, data) {
38
27
  const dir = dirname(targetPath);
39
28
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
@@ -54,11 +43,6 @@ function atomicWriteJson(targetPath, data) {
54
43
  }
55
44
  }
56
45
 
57
- /**
58
- * Persist a feature to its authoritative feature.json and refresh the thin root
59
- * index entry (status + updatedAt) + aggregate metadata. Returns true on
60
- * success, false fail-open.
61
- */
62
46
  export function persistFeature(featureName, feature, projectPath) {
63
47
  try {
64
48
  atomicWriteJson(featureJsonPath(featureName, projectPath), feature);
@@ -75,19 +59,8 @@ export function persistFeature(featureName, feature, projectPath) {
75
59
  root.metadata.totalFeatures = Object.keys(root.features).length;
76
60
  atomicWriteJson(statePath, root);
77
61
 
78
- // R1's loadState() memoizes per projectPath for the lifetime of the
79
- // process. That was always safe when every hook was its own short-lived
80
- // process (README history), but core/chain.js now runs multiple hooks
81
- // — e.g. state-sync then gate-guard — IN ONE process. Without this,
82
- // a loadState() call earlier in that same process (state-sync's
83
- // handleWriteEdit reads via loadState before this write) would leave a
84
- // stale snapshot cached, and gate-guard's loadState() right after this
85
- // write would silently read the PRE-write state instead of what was
86
- // just persisted — inverting the "gate-guard runs after state-sync so it
87
- // sees the gate state-sync just recorded" ordering guarantee the
88
- // registry comment documents. Invalidating here (the one place
89
- // feature.json/state.json are actually written) covers every current
90
- // and future caller, not just this one chain.
62
+ // chain.js runs several hooks in one process: the next link must not read
63
+ // the memoized pre-write snapshot.
91
64
  clearStateCache();
92
65
  return true;
93
66
  } catch {
@@ -95,16 +68,7 @@ export function persistFeature(featureName, feature, projectPath) {
95
68
  }
96
69
  }
97
70
 
98
- /**
99
- * Canonical gate-approval writer (hook-side mirror of CLI setApprovalGate).
100
- * Stamps approvalGates[gate] = { approved, timestamp, approvedBy } — the single
101
- * canonical shape — onto the feature and persists it. Returns true on success.
102
- *
103
- * @param {string} featureName
104
- * @param {string} gate - 'plan'|'uiux'|'review'
105
- * @param {{ approvedBy?: string, timestamp?: string }} [meta]
106
- * @param {string} [projectPath]
107
- */
71
+ /** @param {{ approvedBy?: string, timestamp?: string, selfAssessed?: boolean }} [meta] */
108
72
  export function setGateApproved(featureName, gate, meta = {}, projectPath) {
109
73
  try {
110
74
  const feature = readFeatureFile(featureName, projectPath) || createDefaultFeature(featureName);
@@ -123,16 +87,8 @@ export function setGateApproved(featureName, gate, meta = {}, projectPath) {
123
87
  }
124
88
  }
125
89
 
126
- /**
127
- * Default feature shape (canonical object-form gates, incl. optional uiux).
128
- * Stamps identity — name (the slug), workType, createdAt — so the implicit
129
- * birth path (state-sync registering a feature on the first Write) never
130
- * produces an anonymous {status,updatedAt} shell. `description` stays absent
131
- * (a hook can't invent it); `list`/`doctor` flag its absence. Mirrors the CLI
132
- * createFeature's stamping.
133
- *
134
- * @param {string} [featureName] - the feature slug, stamped as `name`
135
- */
90
+ // Stamps identity so a feature born implicitly by a hook is never an anonymous
91
+ // shell; `description` stays absent because a hook cannot invent it.
136
92
  export function createDefaultFeature(featureName) {
137
93
  const now = new Date().toISOString();
138
94
  return {
@@ -1,14 +1,4 @@
1
- /**
2
- * Shared stdin reader for Claude Code hooks.
3
- *
4
- * Reads JSON payload from stdin (provided by Claude Code to hooks).
5
- * Returns parsed object or null on failure.
6
- */
7
-
8
- /**
9
- * Read and parse JSON from stdin
10
- * @returns {Promise<Object|null>} Parsed JSON or null
11
- */
1
+ /** @returns {Promise<Object|null>} the hook payload, or null when stdin is not JSON */
12
2
  export function readStdin() {
13
3
  return new Promise((resolve) => {
14
4
  let input = '';
@@ -1,20 +1,5 @@
1
- /**
2
- * Telemetry Logger — append-only harness event stream
3
- *
4
- * Writes one JSON line per event to .morph/logs/events.jsonl. This is the sink
5
- * for the `telemetry-log` hook, which fires on every tool call (PostToolUse,
6
- * all tools) and on every user prompt (UserPromptSubmit) — the only vantage
7
- * points an NLH exposes. The whole execution path (skills, sub-agent dispatch,
8
- * knowledge-base reads, prompts) is reconstructable by reading this file.
9
- *
10
- * Design constraints (same discipline as activity-logger, plus one):
11
- * - Synchronous only (hooks run in process, no async overhead)
12
- * - Fail-silent (never throws — hooks must not be blocked by the logger)
13
- * - APPEND-ONLY: one `appendFileSync` line per event, O(1) per call. The
14
- * read-modify-write of a whole JSON doc (activity-logger) is O(n²) across a
15
- * session and blows the ≤5ms budget once tool calls number in the hundreds.
16
- * - Windows-safe paths via path.join
17
- */
1
+ // Append-only on purpose: rewriting a whole JSON doc per event is O(n²) across
2
+ // a session, and this runs on every tool call. Fail-silent: never blocks a hook.
18
3
 
19
4
  import {
20
5
  appendFileSync, readFileSync, mkdirSync, existsSync, statSync, renameSync,
@@ -26,13 +11,7 @@ import { redactSecrets } from './redact-secrets.js';
26
11
  const LOGS_DIR = '.morph/logs';
27
12
  export const EVENTS_FILE = join(LOGS_DIR, 'events.jsonl');
28
13
 
29
- // SessionStart (worktree-context.js) rotates events.jsonl once it crosses
30
- // this size, alongside the pre-existing activity.jsonl rotation — same
31
- // call site, same "between sessions" checkpoint. events.jsonl has no natural
32
- // per-turn cap (prompts/commands are truncated at the write site, but the
33
- // LINE COUNT itself grows unbounded across a long-lived project), so without
34
- // this it grows forever the way activity.jsonl used to before R1.2.
35
- export const ROTATE_THRESHOLD_BYTES = 5 * 1024 * 1024; // 5 MB
14
+ export const ROTATE_THRESHOLD_BYTES = 5 * 1024 * 1024;
36
15
 
37
16
  // Bumped when a field changes meaning. Readers key legacy handling off its
38
17
  // absence (v1 events carry no schemaVersion and no eventId).
@@ -49,15 +28,7 @@ function getEventsPath(projectPath) {
49
28
  return join(projectPath || process.cwd(), EVENTS_FILE);
50
29
  }
51
30
 
52
- /**
53
- * Append a single telemetry event as one JSON line.
54
- * Stamps `ts` (unless given), `schemaVersion` and a unique `eventId` — the
55
- * dedupe key when the same line is read back from a rotated archive or a
56
- * finish backup — and redacts secrets in the free-text fields.
57
- *
58
- * @param {Object} event - Event payload (tool, skill/persona/target/prompt, feature, phase…)
59
- * @param {string} [projectPath] - Project root (defaults to cwd)
60
- */
31
+ // `eventId` dedupes the same line read back from a rotated archive or a finish backup.
61
32
  export function appendTelemetryEvent(event, projectPath) {
62
33
  try {
63
34
  const eventsPath = getEventsPath(projectPath);
@@ -75,17 +46,9 @@ export function appendTelemetryEvent(event, projectPath) {
75
46
  }
76
47
  appendFileSync(eventsPath, JSON.stringify(stamped) + '\n', 'utf-8');
77
48
  } catch {
78
- // Fail-silent — never block hooks
79
49
  }
80
50
  }
81
51
 
82
- /**
83
- * Read the full event stream back into an array. A corrupt/truncated line
84
- * (e.g. a killed process mid-write) is skipped, not fatal.
85
- *
86
- * @param {string} [projectPath]
87
- * @returns {Object[]} events in write order (may be empty)
88
- */
89
52
  export function readTelemetryEvents(projectPath) {
90
53
  try {
91
54
  const eventsPath = getEventsPath(projectPath);
@@ -98,7 +61,7 @@ export function readTelemetryEvents(projectPath) {
98
61
  try {
99
62
  events.push(JSON.parse(trimmed));
100
63
  } catch {
101
- // skip a single corrupt line — don't lose the whole stream
64
+ // a process killed mid-write leaves a truncated line
102
65
  }
103
66
  }
104
67
  return events;
@@ -107,19 +70,7 @@ export function readTelemetryEvents(projectPath) {
107
70
  }
108
71
  }
109
72
 
110
- /**
111
- * Archive the current events.jsonl to `.morph/logs/sessions/`, mirroring
112
- * activity-logger.js's `archiveSession()` — same directory, same rename-not-
113
- * rewrite mechanics (O(1) even at multi-MB sizes). Named with an `events-`
114
- * prefix (activity-logger's archives carry no prefix) so the two streams
115
- * never collide inside the shared `sessions/` directory and stay
116
- * distinguishable at a glance.
117
- *
118
- * Fail-silent — called only from `rotateEventsLogIfLarge` below, which has
119
- * already confirmed the file exists.
120
- *
121
- * @param {string} [projectPath]
122
- */
73
+ // `events-` prefix: activity-logger archives share `sessions/` without one.
123
74
  function archiveEvents(projectPath) {
124
75
  try {
125
76
  const eventsPath = getEventsPath(projectPath);
@@ -132,21 +83,10 @@ function archiveEvents(projectPath) {
132
83
  const archivePath = join(sessionsDir, `events-${stamp}.jsonl`);
133
84
  renameSync(eventsPath, archivePath);
134
85
  } catch {
135
- // Fail-silent
136
86
  }
137
87
  }
138
88
 
139
- /**
140
- * Rotate events.jsonl when it crosses `thresholdBytes`. Called from
141
- * worktree-context.js on SessionStart, right next to the existing
142
- * `rotateActivityLogIfLarge()` call — the only reliable "between sessions"
143
- * checkpoint the harness gets. `thresholdBytes` is injectable so a test can
144
- * force rotation without writing multiple megabytes.
145
- *
146
- * @param {string} [projectPath] - Project root (defaults to cwd)
147
- * @param {number} [thresholdBytes] - Rotate once the log is at least this large
148
- * @returns {boolean} true if a rotation was performed
149
- */
89
+ /** @returns {boolean} true if a rotation was performed */
150
90
  export function rotateEventsLogIfLarge(projectPath, thresholdBytes = ROTATE_THRESHOLD_BYTES) {
151
91
  try {
152
92
  const eventsPath = getEventsPath(projectPath);
@@ -1,28 +1,15 @@
1
- /**
2
- * Real token usage read from a Claude Code transcript (JSONL), shared by the
3
- * Stop hook (the main session's transcript) and SubagentStop (each
4
- * sub-agent's own transcript, which the main one does not include).
5
- */
1
+ // Shared by Stop and SubagentStop: the main transcript does not include the
2
+ // sub-agents' usage.
6
3
 
7
4
  import { readFileSync, existsSync, statSync } from 'fs';
8
5
 
9
- // A 45 MB / ~16k-line transcript parses in ~350 ms; above this, skip rather
10
- // than risk an unbounded read inside a hook.
6
+ // Skip rather than risk an unbounded read inside a hook.
11
7
  const MAX_TRANSCRIPT_BYTES = 50 * 1024 * 1024;
12
8
 
13
9
  export const USAGE_KEYS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheCreationTokens'];
14
10
 
15
- /**
16
- * Sum `message.usage` over the assistant turns of a transcript.
17
- *
18
- * One turn is written as several lines (one per content block), each repeating
19
- * the same usage under the same `message.id`: summing lines double-counts, so
20
- * the sum is deduplicated by message id.
21
- *
22
- * @param {string} [transcriptPath]
23
- * @returns {{inputTokens:number,outputTokens:number,cacheReadTokens:number,cacheCreationTokens:number}|null}
24
- * null when there is nothing to report (no/oversized/unreadable file, no usage)
25
- */
11
+ // One turn spans several lines repeating the same usage under the same
12
+ // `message.id`: summing lines double-counts, so the sum dedups by id.
26
13
  export function readTranscriptUsage(transcriptPath) {
27
14
  if (!transcriptPath || typeof transcriptPath !== 'string') return null;
28
15
  try {
@@ -61,17 +48,8 @@ export function readTranscriptUsage(transcriptPath) {
61
48
  }
62
49
  }
63
50
 
64
- /**
65
- * The usage added since the previous sample of the same transcript.
66
- *
67
- * A counter going DOWN means the transcript restarted (a new file under the
68
- * same key), so the whole current total is new usage — subtracting would
69
- * produce negative spend.
70
- *
71
- * @param {object|null|undefined} previous - last recorded totals for this key
72
- * @param {object} current - totals just read
73
- * @returns {{delta: object, changed: boolean}}
74
- */
51
+ // A counter going DOWN means the transcript restarted: the whole current
52
+ // total is new usage, and subtracting would produce negative spend.
75
53
  export function usageDelta(previous, current) {
76
54
  const reset = !previous || USAGE_KEYS.some((k) => (current[k] ?? 0) < (previous[k] ?? 0));
77
55
  const delta = {};