@polymorphism-tech/morph-spec 9.0.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CLAUDE.md +8 -9
  2. package/README.md +38 -36
  3. package/bin/morph-spec.js +4 -62
  4. package/bin/validate.js +1 -39
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +1 -1
  9. package/framework/MORPH.md +1 -1
  10. package/framework/agents.json +12 -4
  11. package/framework/commands/morph-apply.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +6 -17
  14. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  15. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  16. package/framework/hooks/claude-code/core/chain.js +4 -65
  17. package/framework/hooks/claude-code/core/comment-guard.js +2 -16
  18. package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
  19. package/framework/hooks/claude-code/core/gate-guard.js +7 -67
  20. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  21. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  22. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  23. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  24. package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
  25. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
  26. package/framework/hooks/claude-code/core/state-sync.js +11 -67
  27. package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
  28. package/framework/hooks/claude-code/core/validate-completion.js +11 -57
  29. package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
  30. package/framework/hooks/claude-code/core/worktree-context.js +10 -110
  31. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  32. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  33. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  34. package/framework/hooks/shared/activity-logger.js +9 -89
  35. package/framework/hooks/shared/agent-reap.js +2 -17
  36. package/framework/hooks/shared/build-check.js +6 -75
  37. package/framework/hooks/shared/claim-registry.js +9 -119
  38. package/framework/hooks/shared/command-tokens.js +6 -51
  39. package/framework/hooks/shared/comment-rules.js +5 -20
  40. package/framework/hooks/shared/comment-scanner.js +6 -25
  41. package/framework/hooks/shared/comment-stripper.js +11 -85
  42. package/framework/hooks/shared/e2e-check.js +0 -0
  43. package/framework/hooks/shared/errata.js +2 -15
  44. package/framework/hooks/shared/feature-locator.js +11 -95
  45. package/framework/hooks/shared/frontend-locations.js +1 -3
  46. package/framework/hooks/shared/gate-question.js +1 -7
  47. package/framework/hooks/shared/git-exclude.js +3 -20
  48. package/framework/hooks/shared/git-paths.js +8 -104
  49. package/framework/hooks/shared/graph-freshness.js +22 -166
  50. package/framework/hooks/shared/graph-refresh.js +21 -149
  51. package/framework/hooks/shared/heavy-command.js +4 -12
  52. package/framework/hooks/shared/hook-response.js +8 -83
  53. package/framework/hooks/shared/morph-cli.js +8 -52
  54. package/framework/hooks/shared/payload-utils.js +10 -132
  55. package/framework/hooks/shared/phase-utils.js +1 -42
  56. package/framework/hooks/shared/redact-secrets.js +4 -15
  57. package/framework/hooks/shared/reparse-scan.js +6 -66
  58. package/framework/hooks/shared/slice-progress.js +4 -23
  59. package/framework/hooks/shared/source-files.js +4 -17
  60. package/framework/hooks/shared/state-reader.js +13 -183
  61. package/framework/hooks/shared/state-writer.js +8 -52
  62. package/framework/hooks/shared/stdin-reader.js +1 -11
  63. package/framework/hooks/shared/telemetry-logger.js +7 -67
  64. package/framework/hooks/shared/transcript-usage.js +7 -29
  65. package/framework/hooks/shared/worktree-root.js +7 -29
  66. package/framework/rules/csharp-standards.md +1 -1
  67. package/framework/rules/frontend-standards.md +1 -0
  68. package/framework/rules/hangfire-standards.md +1 -1
  69. package/framework/rules/neon-standards.md +1 -1
  70. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  71. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  72. package/framework/skills/core/morph-apply/SKILL.md +2 -2
  73. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  74. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  75. package/framework/skills/core/morph-media/SKILL.md +1 -1
  76. package/framework/skills/core/morph-plan/SKILL.md +2 -2
  77. package/framework/skills/core/morph-review/SKILL.md +1 -1
  78. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  79. package/framework/skills/core/morph-uiux/SKILL.md +29 -12
  80. package/framework/standards/STANDARDS.json +34 -12
  81. package/framework/standards/STANDARDS.meta.json +16 -2
  82. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  83. package/framework/standards/ai-agents/agent-session.md +1 -1
  84. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  85. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  86. package/framework/standards/ai-agents/guardrails.md +1 -1
  87. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  88. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  89. package/framework/standards/ai-agents/production.md +1 -1
  90. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  91. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  92. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  93. package/framework/standards/ai-agents/setup.md +2 -2
  94. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  95. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  96. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  97. package/framework/standards/ai-agents/workflows.md +1 -1
  98. package/framework/standards/architecture/code-comments.md +1 -1
  99. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  100. package/framework/standards/backend/api/validation.md +1 -1
  101. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  102. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  103. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  104. package/framework/standards/data/nosql/cache/redis.md +1 -1
  105. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  106. package/framework/standards/frontend/admin-app/forms.md +1 -1
  107. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  108. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  109. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  110. package/framework/templates/CLAUDE.md.core.template +3 -3
  111. package/framework/templates/REGISTRY.json +9 -9
  112. package/framework/templates/docs/ui-mockups.md +4 -0
  113. package/package.json +1 -1
  114. package/src/commands/context/context-refresh.js +0 -16
  115. package/src/commands/dashboard/dashboard.js +2 -8
  116. package/src/commands/e2e/e2e.js +17 -104
  117. package/src/commands/graph/affected.js +0 -14
  118. package/src/commands/graph/build.js +11 -50
  119. package/src/commands/graph/explain.js +0 -16
  120. package/src/commands/graph/status.js +1 -12
  121. package/src/commands/mcp/mcp-setup.js +4 -62
  122. package/src/commands/observability/cost.js +5 -57
  123. package/src/commands/observability/telemetry.js +1 -14
  124. package/src/commands/project/branch.js +4 -45
  125. package/src/commands/project/claim.js +4 -48
  126. package/src/commands/project/doctor.js +48 -302
  127. package/src/commands/project/env.js +1 -27
  128. package/src/commands/project/fleet.js +3 -32
  129. package/src/commands/project/init.js +4 -68
  130. package/src/commands/project/install-plugin-cmd.js +0 -1
  131. package/src/commands/project/standards.js +1 -62
  132. package/src/commands/project/status.js +0 -53
  133. package/src/commands/project/update.js +31 -239
  134. package/src/commands/project/worktree.js +108 -882
  135. package/src/commands/scaffold/scaffold.js +0 -25
  136. package/src/commands/state/advance.js +14 -85
  137. package/src/commands/state/approve.js +14 -96
  138. package/src/commands/state/archive.js +12 -64
  139. package/src/commands/state/auto-approval.js +2 -5
  140. package/src/commands/state/create.js +22 -109
  141. package/src/commands/state/delete.js +11 -63
  142. package/src/commands/state/finish.js +98 -475
  143. package/src/commands/state/gate-check.js +22 -132
  144. package/src/commands/state/gate-decision.js +3 -39
  145. package/src/commands/state/handoff.js +3 -30
  146. package/src/commands/state/list.js +14 -84
  147. package/src/commands/state/report-check.js +8 -58
  148. package/src/commands/state/retype.js +4 -26
  149. package/src/commands/validation/index.js +0 -3
  150. package/src/commands/validation/mutate.js +2 -5
  151. package/src/commands/validation/validate-feature.js +5 -94
  152. package/src/commands/validation/verify.js +15 -98
  153. package/src/core/paths/output-schema.js +2 -101
  154. package/src/core/state/feature-identity.js +1 -25
  155. package/src/core/state/gate-order.js +0 -27
  156. package/src/core/state/lifecycle-audit.js +8 -100
  157. package/src/core/state/state-manager.js +22 -304
  158. package/src/core/templates/template-registry.js +2 -97
  159. package/src/core/templates/template-renderer.js +13 -93
  160. package/src/core/templates/template-seed.js +0 -1
  161. package/src/core/work-types.js +1 -36
  162. package/src/dashboard/app/App.jsx +1 -8
  163. package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
  164. package/src/dashboard/app/components/CommandPalette.jsx +0 -7
  165. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  166. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  167. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  168. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  169. package/src/dashboard/app/components/Markdown.jsx +0 -9
  170. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  171. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  172. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  173. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  174. package/src/dashboard/app/hooks/usePref.js +1 -5
  175. package/src/dashboard/app/lib/format.js +0 -2
  176. package/src/dashboard/app/lib/phases.js +1 -5
  177. package/src/dashboard/app/lib/project.js +0 -12
  178. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  179. package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
  180. package/src/dashboard/app/pages/Features.jsx +1 -8
  181. package/src/dashboard/app/pages/Guide.jsx +0 -1
  182. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  183. package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
  184. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  185. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  186. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  187. package/src/dashboard/app/pages/Standards.jsx +2 -17
  188. package/src/dashboard/scanner/activity-scanner.js +10 -69
  189. package/src/dashboard/scanner/agents-scanner.js +1 -23
  190. package/src/dashboard/scanner/docs-scanner.js +0 -6
  191. package/src/dashboard/scanner/feature-status.js +2 -5
  192. package/src/dashboard/scanner/features-scanner.js +10 -66
  193. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  194. package/src/dashboard/scanner/index.js +0 -4
  195. package/src/dashboard/scanner/portfolio.js +5 -29
  196. package/src/dashboard/scanner/project-api.js +8 -54
  197. package/src/dashboard/scanner/rules-scanner.js +1 -2
  198. package/src/dashboard/scanner/skills-scanner.js +2 -12
  199. package/src/dashboard/scanner/standards-scanner.js +2 -6
  200. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  201. package/src/lib/ai/ai-checks.js +7 -52
  202. package/src/lib/ai/ai-project-scan.js +14 -108
  203. package/src/lib/ai/code-audit.js +7 -63
  204. package/src/lib/ai/pin-drift.js +8 -51
  205. package/src/lib/ai/registry-audit.js +11 -90
  206. package/src/lib/ci/ci-verify.js +4 -19
  207. package/src/lib/ci/github-ci.js +9 -37
  208. package/src/lib/ci/workflow-scaffold.js +2 -10
  209. package/src/lib/detectors/claude-config-detector.js +13 -74
  210. package/src/lib/detectors/index.js +4 -50
  211. package/src/lib/e2e/compose-env.js +5 -66
  212. package/src/lib/e2e/stack-teardown.js +6 -40
  213. package/src/lib/generators/architecture-scanner.js +18 -156
  214. package/src/lib/generators/handoff-generator.js +9 -53
  215. package/src/lib/generators/scaffold-generator.js +5 -101
  216. package/src/lib/generators/settings-generator.js +4 -37
  217. package/src/lib/git/branch-classifier.js +11 -84
  218. package/src/lib/git/default-branch.js +14 -169
  219. package/src/lib/git/evidence-staleness.js +1 -0
  220. package/src/lib/git/gh.js +2 -17
  221. package/src/lib/git/pending-prs.js +6 -37
  222. package/src/lib/git/pr-files.js +3 -28
  223. package/src/lib/git/pr-index.js +3 -34
  224. package/src/lib/git/tree-fingerprint.js +3 -15
  225. package/src/lib/git/tree-state.js +6 -33
  226. package/src/lib/graph/god-nodes.js +2 -22
  227. package/src/lib/graph/graph-health.js +3 -24
  228. package/src/lib/graph/graph-loader.js +5 -56
  229. package/src/lib/graph/graphify-runtime.js +7 -37
  230. package/src/lib/graph/graphifyignore.js +8 -74
  231. package/src/lib/graph/sanitize.js +16 -136
  232. package/src/lib/heavy/cleanup.js +1 -1
  233. package/src/lib/installers/effects.js +8 -93
  234. package/src/lib/installers/mcp-installer.js +9 -169
  235. package/src/lib/installers/memory-installer.js +4 -27
  236. package/src/lib/installers/templates-reconcile.js +5 -60
  237. package/src/lib/mutation/dotnet-introspection.js +20 -106
  238. package/src/lib/mutation/subset.js +1 -4
  239. package/src/lib/plan/artifact-vocabulary.js +5 -25
  240. package/src/lib/plan/checks.js +5 -31
  241. package/src/lib/plan/plan-parser.js +1 -6
  242. package/src/lib/projects/registry.js +7 -44
  243. package/src/lib/review-reports.js +5 -23
  244. package/src/lib/risk-detector.js +39 -173
  245. package/src/lib/specs/adr-index.js +6 -34
  246. package/src/lib/specs/frontmatter.js +2 -15
  247. package/src/lib/specs/gfm-table.js +1 -7
  248. package/src/lib/specs/revocations.js +1 -14
  249. package/src/lib/specs/screen-checklist.js +47 -124
  250. package/src/lib/stack/dotnet-build-ignores.js +2 -6
  251. package/src/lib/stack/stack-profile.js +1 -28
  252. package/src/lib/stack/stack-scan.js +1 -8
  253. package/src/lib/stack-filter.js +4 -27
  254. package/src/lib/standards/registry.js +1 -7
  255. package/src/lib/standards/sections.js +2 -13
  256. package/src/lib/standards/verified-against.js +19 -191
  257. package/src/lib/tasks/eval-node.js +21 -169
  258. package/src/lib/tasks/linux-runner.js +18 -142
  259. package/src/lib/tasks/test-nodes.js +43 -240
  260. package/src/lib/tasks/test-runner.js +43 -341
  261. package/src/lib/telemetry/metrics.js +7 -36
  262. package/src/lib/telemetry/telemetry-reader.js +13 -125
  263. package/src/lib/telemetry/verify-event.js +2 -30
  264. package/src/lib/validators/code-budget/code-budget-validator.js +3 -43
  265. package/src/lib/validators/code-budget/index.js +0 -7
  266. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
  267. package/src/lib/validators/contracts/index.js +0 -6
  268. package/src/lib/validators/nextjs/index.js +0 -5
  269. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  270. package/src/lib/validators/packages/index.js +0 -6
  271. package/src/lib/validators/packages/package-validator.js +6 -56
  272. package/src/lib/validators/reuse/index.js +0 -8
  273. package/src/lib/validators/reuse/reuse-validator.js +11 -101
  274. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  275. package/src/lib/validators/traceability-check.js +5 -63
  276. package/src/lib/validators/validation-runner.js +20 -125
  277. package/src/lib/validators/verify-runner.js +52 -358
  278. package/src/lib/work-type-detector.js +5 -67
  279. package/src/lib/worktree/docker-audit.js +17 -136
  280. package/src/lib/worktree/env-allocator.js +10 -98
  281. package/src/lib/worktree/feature-truth.js +12 -115
  282. package/src/lib/worktree/finish-backup.js +32 -242
  283. package/src/lib/worktree/fleet-scanner.js +13 -95
  284. package/src/lib/worktree/ignored-triage.js +15 -91
  285. package/src/lib/worktree/vscode-settings.js +2 -3
  286. package/src/lib/worktree/worktree-audit.js +21 -213
  287. package/src/lib/worktree/worktree-include.js +3 -5
  288. package/src/scripts/global-install.js +3 -9
  289. package/src/scripts/install-plugin.js +1 -13
  290. package/src/scripts/setup-infra.js +17 -147
  291. package/src/utils/agents-installer.js +28 -275
  292. package/src/utils/banner.js +4 -25
  293. package/src/utils/claude-md-injector.js +1 -37
  294. package/src/utils/claude-settings-manager.js +1 -52
  295. package/src/utils/file-copier.js +11 -95
  296. package/src/utils/hooks-installer.js +21 -298
  297. package/src/utils/skills-installer.js +4 -101
  298. package/src/utils/version-checker.js +4 -83
  299. package/framework/commands/morph-proposal.md +0 -9
  300. package/src/commands/utils/index.js +0 -3
  301. package/src/lib/detectors/design-system-detector.js +0 -188
  302. package/src/lib/generators/design-system-generator.js +0 -508
  303. package/src/lib/specs/old-format.js +0 -68
  304. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,25 +1,6 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * worktree-remove.js — WorktreeRemove hook
4
- *
5
- * Event: WorktreeRemove (Claude Code's own cleanup of a tree it created via
6
- * `claude -w` / EnterWorktree / Agent isolation). Input carries
7
- * `worktree_path` (absolute). No decision control, output ignored — this hook
8
- * ALWAYS exits 0.
9
- *
10
- * Why it exists: Claude Code's native removal is a raw `git worktree remove`,
11
- * which FOLLOWS the infra junctions the WorktreeCreate hook put in that tree
12
- * and empties the primary root's `.claude/` and `.morph/framework/`. This
13
- * hook routes the removal through `morph-spec worktree remove --target
14
- * --if-clean` instead: junctions dropped first, PROVE-or-DECLINE, and a tree
15
- * holding uncommitted work is left alone (Claude Code's notion of "clean" is
16
- * not the user's).
17
- *
18
- * Secondary net: in headless runs the event was observed NOT to fire; the
19
- * PreToolUse guard (block-destructive-worktree-removal) stays the primary
20
- * defense. Fail-open everywhere — diagnostics to stderr, never a throw, never
21
- * a non-zero exit.
22
- */
2
+ // Routes Claude Code's native removal (a raw `git worktree remove`, which follows the infra junctions out) through
3
+ // `morph-spec worktree remove --if-clean`. The event ignores output, so this hook always exits 0.
23
4
 
24
5
  import { existsSync } from 'fs';
25
6
  import { execFileSync } from 'child_process';
@@ -35,15 +16,11 @@ function primaryRoot(startDir) {
35
16
  const out = execFileSync('git', ['worktree', 'list', '--porcelain'], { cwd: startDir, stdio: ['ignore', 'pipe', 'pipe'] }).toString();
36
17
  const first = out.split('\n').find((l) => l.startsWith('worktree '));
37
18
  if (first) return fwd(first.slice('worktree '.length).trim());
38
- } catch { /* fall through */ }
19
+ } catch {}
39
20
  const common = resolveGitCommonDir(startDir);
40
21
  return common ? fwd(dirname(common)) : null;
41
22
  }
42
23
 
43
- /**
44
- * @param {object} payload - parsed WorktreeRemove stdin payload ({ cwd, worktree_path })
45
- * @returns {Promise<null>} always null — the event ignores output
46
- */
47
24
  export async function run(payload) {
48
25
  try {
49
26
  const target = payload?.worktree_path;
@@ -52,8 +29,7 @@ export async function run(payload) {
52
29
  const start = payload?.cwd || process.env.CLAUDE_PROJECT_DIR || dirname(resolve(target));
53
30
  const root = primaryRoot(start) || primaryRoot(dirname(resolve(target)));
54
31
  if (!root) return null;
55
- // Nothing to govern outside a morph project — and never a raw removal
56
- // from here either: the tree is simply left for git's own cleanup.
32
+ // Never a raw removal from here: outside a morph project the tree is left for git's own cleanup.
57
33
  if (!stateExists(root)) return null;
58
34
 
59
35
  const r = runMorphSpec(['worktree', 'remove', '--target', fwd(resolve(target)), '--if-clean', '--json'], { cwd: root });
@@ -72,10 +48,8 @@ export async function run(payload) {
72
48
  }
73
49
  }
74
50
 
75
- // Standalone tail: output is ignored by this event, so nothing is printed;
76
- // the exit code is always 0.
77
51
  if (!process.env.MORPH_HOOK_CHAINED) {
78
52
  const { readStdin } = await import('../../shared/stdin-reader.js');
79
- try { await run(await readStdin()); } catch { /* fail-open */ }
53
+ try { await run(await readStdin()); } catch {}
80
54
  process.exit(0);
81
55
  }
@@ -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 '—';