@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,21 +1,10 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * heavy-release.js — PostToolUse / PostToolUseFailure link: gives back the slot `heavy-gate` took
4
- * for this Bash call, matched by `tool_use_id`.
5
- *
6
- * Silent and fail-open. A call that fires neither event (refused by a rule, interrupted) leaves its
7
- * slot to the TTL. Releasing is not deciding, so this link is safe in the async failure binding.
8
- */
2
+ // A call that fires neither Post event leaves its slot to the TTL. Releasing is not deciding, so this link is safe async.
9
3
 
10
4
  import { existsSync } from 'node:fs';
11
5
  import { stateExists } from '../../shared/state-reader.js';
12
6
  import { runStandalone } from '../../shared/hook-response.js';
13
7
 
14
- /**
15
- * @param {object} payload - PostToolUse or PostToolUseFailure stdin payload
16
- * @param {{ env?: object }} [deps] - test seam
17
- * @returns {Promise<null>}
18
- */
19
8
  export async function run(payload, deps = {}) {
20
9
  try {
21
10
  if (payload?.tool_name !== 'Bash' || !payload.tool_use_id) return null;
@@ -27,7 +16,7 @@ export async function run(payload, deps = {}) {
27
16
  if (!existsSync(dir)) return null;
28
17
  const { releaseLeaseById } = await import('../../shared/heavy-slots.js');
29
18
  releaseLeaseById(dir, payload.tool_use_id);
30
- } catch { /* fail-open */ }
19
+ } catch {}
31
20
  return null;
32
21
  }
33
22
 
@@ -1,19 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * PostToolUse Hook: Loop Detection
5
- *
6
- * Event: PostToolUse | Matcher: none (all tools)
7
- *
8
- * Detects when an agent is stuck in a repetitive loop by tracking
9
- * the last 15 actions. If the last 8 actions have <= 2 unique
10
- * signatures, injects a warning with escape instructions.
11
- *
12
- * Cooldown: 5 minutes between warnings to prevent meta-loop.
13
- *
14
- * Fail-open: exits 0 on any error.
15
- */
16
-
17
3
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
18
4
  import { join } from 'path';
19
5
  import { stateExists } from '../../shared/state-reader.js';
@@ -24,13 +10,8 @@ import { logEvent } from '../../shared/activity-logger.js';
24
10
  const MAX_BUFFER = 15;
25
11
  const LOOP_WINDOW = 8;
26
12
  const MAX_UNIQUE_SIGS = 2;
27
- const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
13
+ const COOLDOWN_MS = 5 * 60 * 1000;
28
14
 
29
- /**
30
- * @param {object} payload - parsed PostToolUse stdin payload (any tool)
31
- * @returns {Promise<object|null>} a hookSpecificOutput loop warning, or null
32
- * (pass/silent).
33
- */
34
15
  export async function run(payload) {
35
16
  try {
36
17
  if (!stateExists()) return null;
@@ -43,7 +24,6 @@ export async function run(payload) {
43
24
 
44
25
  mkdirSync(memoryDir, { recursive: true });
45
26
 
46
- // Read existing buffer
47
27
  let buffer = [];
48
28
  try {
49
29
  if (existsSync(bufferPath)) {
@@ -54,19 +34,15 @@ export async function run(payload) {
54
34
  buffer = [];
55
35
  }
56
36
 
57
- // Append current action
58
37
  const sig = getCommandSignature(payload);
59
38
  buffer.push({ sig, ts: new Date().toISOString() });
60
39
 
61
- // Trim to max size (FIFO)
62
40
  if (buffer.length > MAX_BUFFER) {
63
41
  buffer = buffer.slice(-MAX_BUFFER);
64
42
  }
65
43
 
66
- // Save updated buffer
67
44
  writeFileSync(bufferPath, JSON.stringify(buffer), 'utf-8');
68
45
 
69
- // Check for loop (need at least LOOP_WINDOW actions)
70
46
  if (buffer.length < LOOP_WINDOW) return null;
71
47
 
72
48
  const recent = buffer.slice(-LOOP_WINDOW);
@@ -74,18 +50,14 @@ export async function run(payload) {
74
50
 
75
51
  if (uniqueSigs.size > MAX_UNIQUE_SIGS) return null;
76
52
 
77
- // Check cooldown
78
53
  try {
79
54
  if (existsSync(cooldownPath)) {
80
55
  const cooldown = JSON.parse(readFileSync(cooldownPath, 'utf-8'));
81
56
  const elapsed = Date.now() - new Date(cooldown.lastDetected).getTime();
82
57
  if (elapsed < COOLDOWN_MS) return null;
83
58
  }
84
- } catch {
85
- // Ignore corrupt cooldown — proceed with detection
86
- }
59
+ } catch {}
87
60
 
88
- // Loop detected — set cooldown and warn
89
61
  writeFileSync(cooldownPath, JSON.stringify({ lastDetected: new Date().toISOString() }), 'utf-8');
90
62
 
91
63
  logEvent('loop_detected', { signatures: uniqueSigs.size }, process.cwd());
@@ -102,7 +74,6 @@ export async function run(payload) {
102
74
  'PostToolUse'
103
75
  );
104
76
  } catch {
105
- // Fail-open
106
77
  return null;
107
78
  }
108
79
  }
@@ -38,10 +38,6 @@ export function matchesExecutableRule(command, rule) {
38
38
  return false;
39
39
  }
40
40
 
41
- /**
42
- * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
43
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
44
- */
45
41
  export async function run(payload) {
46
42
  try {
47
43
  if (!stateExists()) return null;
@@ -1,22 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * PreToolUse Hook: Protect Approved Spec Files
5
- *
6
- * Event: PreToolUse | Matcher: Write|Edit
7
- *
8
- * Blocks Write/Edit to spec artifacts after their approval gate has passed:
9
- * - 2-plan/plan.md → after 'plan', only an append under `## Errata`
10
- * - 2-plan/checks.json → never by hand: `morph-spec plan-check` is its only writer
11
- * - 1-design/design-system.md, etc. → after 'uiux', only an errata append
12
- *
13
- * A gate freezes the APPROVED ARTIFACT, so a protected file that does not yet
14
- * exist on disk is a CREATION and passes — the criterion is the file's
15
- * existence, never who signed the gate (see the carve-out in run()).
16
- *
17
- * Fail-open: exits 0 on any error.
18
- */
19
-
20
3
  import { existsSync, readFileSync } from 'fs';
21
4
  import { stateExists, isGateApproved } from '../../shared/state-reader.js';
22
5
  import {
@@ -32,10 +15,6 @@ import { ERRATA_HEADING, errataViolation } from '../../shared/errata.js';
32
15
 
33
16
  const CHECKS_FILE = 'checks.json';
34
17
 
35
- /**
36
- * Texto proposto para um arquivo totalmente travado, a partir de Write ou Edit.
37
- * @returns {string|null} null quando o payload não permite reconstruir (fail-open).
38
- */
39
18
  function proposedText(payload, toolName, original) {
40
19
  if (toolName === 'Edit') {
41
20
  const oldStr = payload?.tool_input?.old_string;
@@ -49,14 +28,8 @@ function proposedText(payload, toolName, original) {
49
28
  return typeof content === 'string' ? content : null;
50
29
  }
51
30
 
52
- /**
53
- * @param {object} payload - parsed PreToolUse stdin payload
54
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
55
- * Never prints, never exits — see runStandalone()/chain.js at the bottom.
56
- */
57
31
  export async function run(payload) {
58
32
  try {
59
- // Amend mode: bypass spec protection for legitimate in-implementation corrections
60
33
  if (process.env.MORPH_AMEND_PHASE) {
61
34
  console.error(`[morph-spec] AMEND MODE: bypassing spec protection for phase '${process.env.MORPH_AMEND_PHASE}'`);
62
35
  return null;
@@ -67,14 +40,9 @@ export async function run(payload) {
67
40
  const filePath = getFilePath(payload);
68
41
  if (!filePath) return null;
69
42
 
70
- // Only check files inside .morph/features/
71
43
  if (!isFeaturePath(filePath)) return null;
72
44
 
73
- // The gate that locks this file is recorded in the feature.json of the tree
74
- // that OWNS it. Asking the session's tree instead made this guard go DARK in
75
- // a secondary worktree: the root had no approved gate, so a post-Gate-2
76
- // plan.md rewrite sailed through. Same defect as enforce-phase-writes,
77
- // opposite direction — there it over-blocked, here it under-protected.
45
+ // The gate lives in the feature.json of the tree that OWNS the file: asking the session's tree left this guard dark in a worktree.
78
46
  const root = resolveMorphRoot(filePath, process.cwd());
79
47
  if (!stateExists(root)) return null;
80
48
 
@@ -84,12 +52,10 @@ export async function run(payload) {
84
52
  const filename = getBasename(filePath);
85
53
  const requiredGate = PROTECTED_SPEC_FILES[filename];
86
54
 
87
- // Not a protected file
88
55
  if (!requiredGate) return null;
89
56
 
90
- // Only plan-check writes checks.json; a hand-written one would carry
91
- // checks the parser never judged. Decided before the creation carve-out:
92
- // chore/hotfix are born with the plan gate stamped.
57
+ // Generated only by the CLI: a hand-written one would carry checks the parser never judged.
58
+ // Decided before the creation carve-out, since chore/hotfix are born with the plan gate stamped.
93
59
  if (filename === CHECKS_FILE) {
94
60
  logHookActivity('protect-spec-files', 'PreToolUse', 'blocked');
95
61
  return blockResult(
@@ -100,53 +66,28 @@ export async function run(payload) {
100
66
  );
101
67
  }
102
68
 
103
- // Gate not yet approved — allow editing
104
69
  if (!isGateApproved(featureName, requiredGate, root)) {
105
70
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
106
71
  return null;
107
72
  }
108
73
 
109
- // ── Gate approved — file is locked ──────────────────────────────────────
110
74
  const absPath = resolveTargetPath(filePath, root);
111
75
 
112
- // Um gate aprovado congela o ARTEFATO APROVADO. Se o arquivo ainda não
113
- // existe no disco, não há artefato aprovado a proteger: isto é a CRIAÇÃO
114
- // inicial, não a edição de uma spec fechada. O caso é real e não é raro —
115
- // `chore`/`hotfix` nascem com `plan` carimbado `policy:{tipo}`
116
- // (create.js → work-types.js skippedGates), então o primeiro `Write` de
117
- // `2-plan/plan.md` já chega aqui com o gate "aprovado" sem que humano
118
- // nenhum tenha aprovado coisa alguma — e era barrado com uma mensagem
119
- // mandando revogar o gate de um arquivo inexistente.
120
- //
121
- // O critério é a EXISTÊNCIA, nunca o `approvedBy`. Liberar por assinatura
122
- // (`policy:` passa, `lucas` não) transformaria este carve-out em licença
123
- // para reescrever qualquer spec aprovada: bastaria apagá-la antes. Por ser
124
- // sobre o disco, a janela fecha sozinha no instante em que o arquivo
125
- // existe — os contra-canários de test/hooks/protect-spec-files.test.js
126
- // pinam exatamente isso.
127
- // O `result` distinto no log é o que torna a criação observável.
76
+ // A file not yet on disk is its CREATION (chore/hotfix are born with `plan` stamped), not an edit of a frozen spec.
77
+ // The criterion is existence, never `approvedBy`: by signature, deleting a spec first would license rewriting it.
128
78
  if (!existsSync(absPath)) {
129
79
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok:absent');
130
80
  return null;
131
81
  }
132
82
 
133
- // ── Arquivo totalmente travado (plan.md, design) ────────────
134
- // O corpo continua congelado. A ÚNICA passagem é a errata append-only:
135
- // corpo byte-idêntico + texto novo sob `## Errata` no fim. Qualquer falha
136
- // de leitura/reconstrução cai no bloqueio: a regra base é "não se edita".
137
- //
138
- // O arquivo EXISTE neste ponto — a inexistência foi decidida lá em cima, e
139
- // é a única coisa que este catch NÃO significa mais. ENOENT tratado como
140
- // "ilegível" era o defeito F14; "ilegível de verdade" continua bloqueando.
83
+ // The only way through is the append-only errata; any read or rebuild failure falls into the block.
141
84
  const toolName = getToolName(payload);
142
85
  let errataCheck = null;
143
86
  try {
144
87
  const original = readFileSync(absPath, 'utf-8');
145
88
  const proposed = proposedText(payload, toolName, original);
146
89
  if (proposed !== null) errataCheck = errataViolation(original, proposed);
147
- } catch {
148
- // arquivo ilegível → sem errata possível, segue para o bloqueio
149
- }
90
+ } catch {}
150
91
 
151
92
  if (errataCheck?.ok) {
152
93
  logHookActivity('protect-spec-files', 'PreToolUse', 'ok');
@@ -165,7 +106,6 @@ export async function run(payload) {
165
106
  `edite, rode plan-check quando for o plan.md, e re-apresente o gate para aprovação.`
166
107
  );
167
108
  } catch {
168
- // Fail-open
169
109
  return null;
170
110
  }
171
111
  }
@@ -1,15 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * state-sync.js — PostToolUse hook
4
- *
5
- * Keeps .morph/state.json in sync with the agent's actions, without requiring
6
- * the agent to think about state. Fail-open: any error returns {} silently.
7
- *
8
- * Triggers:
9
- * - Write/Edit of .morph/features/{feature}/2-plan/plan.md → register feature
10
- * - Write/Edit of .morph/features/{feature}/{phase}/{output} → mark output created
11
- * - AskUserQuestion answered with option starting "aprovado" → advance gate
12
- */
13
2
 
14
3
  import { existsSync } from 'fs';
15
4
  import { join } from 'path';
@@ -27,7 +16,7 @@ import {
27
16
 
28
17
  const PHASE_FOLDERS = {
29
18
  '1-design': 'design',
30
- '1-ui': 'uiux', // legacy alias — still recognized during folder transition
19
+ '1-ui': 'uiux', // legacy alias
31
20
  '2-plan': 'plan',
32
21
  '3-implement': 'implement',
33
22
  '4-review': 'review',
@@ -62,8 +51,6 @@ function registerFeatureIfMissing(state, featureName) {
62
51
  if (!state.features) state.features = {};
63
52
  if (state.features[featureName]) return false;
64
53
 
65
- // Canonical object-form gates (incl. optional uiux) + stamped identity
66
- // (name=slug, workType, createdAt) — no anonymous shell, no boolean drift.
67
54
  state.features[featureName] = createDefaultFeature(featureName);
68
55
  return true;
69
56
  }
@@ -94,9 +81,7 @@ function isApprovalAnswer(answer) {
94
81
  );
95
82
  }
96
83
 
97
- // Gate monotonicity — mirror of src/core/state/gate-order.js (hooks are
98
- // installed standalone and cannot import from src/). Keep in sync:
99
- // plan first; uiux and review require plan.
84
+ // Mirror of src/core/state/gate-order.js: hooks are installed standalone and cannot import src/.
100
85
  const GATE_PREREQ = { plan: [], uiux: ['plan'], review: ['plan'] };
101
86
  function unmetGatePrereqs(gate, gates) {
102
87
  const isApproved = (g) => gates?.[g]?.approved === true || gates?.[g] === true;
@@ -108,9 +93,7 @@ function handleWriteEdit(input, sessionRoot) {
108
93
  const featureName = extractFeatureName(filePath);
109
94
  if (!featureName) return false;
110
95
 
111
- // Sync the tree that OWNS the written file. Syncing the session's tree
112
- // instead registered the feature (and its outputs) in the wrong `.morph/`
113
- // whenever a root-rooted session wrote into a secondary worktree.
96
+ // Sync the tree that OWNS the written file: a root session also writes into worktrees.
114
97
  const cwd = resolveMorphRoot(filePath, sessionRoot);
115
98
  if (!stateExists(cwd)) return false;
116
99
 
@@ -125,8 +108,6 @@ function handleWriteEdit(input, sessionRoot) {
125
108
  }
126
109
 
127
110
  if (changed) {
128
- // Persist the touched feature to its authoritative feature.json and refresh
129
- // the thin root index. The feature's state thus travels in its own folder.
130
111
  persistFeature(featureName, state.features[featureName], cwd);
131
112
  }
132
113
  return true;
@@ -142,22 +123,11 @@ function handleAskUserQuestion(input, sessionRoot) {
142
123
  const gate = gateFromQuestion(question);
143
124
  if (!gate) continue;
144
125
 
145
- // Attribute the approval to the feature the QUESTION names, in the tree
146
- // that owns it — never to "the first in_progress feature of whatever tree
147
- // the session sits in", which is how a Gate 2 for one feature was stamped
148
- // onto `export-hub`, leaving it with G2 approved and G1 never asked.
126
+ // The feature the QUESTION names, in the tree that owns it — never the session tree's first in-progress feature.
149
127
  const located = locateFeatureForQuestion(question, sessionRoot);
150
128
  if (!located) {
151
- // Refuse to guess. A gate on the wrong feature inverts gate order and
152
- // invites a second stamp on the real owner; writing nothing costs one
153
- // explicit remediation step.
154
- //
155
- // The remediation differs by WHY it refused. When the question names a
156
- // specific slug that no known tree has registered (a hotfix gate asked
157
- // before `create` ran — the case that used to fall through to "the
158
- // tree's one feature"), `morph-spec approve <that-name> <gate>` is a
159
- // dead end: approve.js does `getFeature()` and hard-refuses "Feature
160
- // not found". Name the real fix instead of the wrong command.
129
+ // Refuse to guess: a gate on the wrong feature inverts gate order. For an unregistered slug
130
+ // `approve` is a dead end ("Feature not found"), so the remediation names `create`.
161
131
  const allNames = allWorkTrees(sessionRoot).flatMap((r) => featureNamesIn(r));
162
132
  const candidate = unrecognizedSlugLike(question, allNames);
163
133
  const remediation = candidate
@@ -176,20 +146,14 @@ function handleAskUserQuestion(input, sessionRoot) {
176
146
  const { name: featureName, root } = located;
177
147
  if (!stateExists(root)) continue;
178
148
 
179
- // Prefer the authoritative feature.json; fall back to loadState's merged
180
- // view so a feature carried only by the thin index still reports its real
181
- // gates (otherwise every predecessor reads as unapproved and the
182
- // out-of-order warning below fires on a perfectly ordered approval).
149
+ // Fallback to the merged view: a feature only in the thin index would read every predecessor as unapproved.
183
150
  const feature = readFeatureFile(featureName, root) || loadState(root)?.features?.[featureName];
184
151
  const gates = feature?.approvalGates;
185
152
  const existing = gates?.[gate];
186
153
  const alreadyApproved = existing === true || existing?.approved === true;
187
154
  if (alreadyApproved) continue;
188
155
 
189
- // Gate monotonicity: fail-open. The human said "aprovado", so we DO approve
190
- // (never block a human in the loop), but if a predecessor gate is still
191
- // pending we surface a non-blocking warning — the CLI `morph-spec approve`
192
- // hard-refuses the same shape; here we alert so the operator can reconcile.
156
+ // Out of order still approves (never block a human in the loop); `morph-spec approve` refuses the same shape.
193
157
  const unmet = unmetGatePrereqs(gate, gates);
194
158
  if (unmet.length) {
195
159
  warnings.push(
@@ -199,13 +163,7 @@ function handleAskUserQuestion(input, sessionRoot) {
199
163
  );
200
164
  }
201
165
 
202
- // An "aprovado" answer to AskUserQuestion is a human approving in the loop.
203
- // Route through the shared canonical gate writer so the hook and the CLI
204
- // (`morph-spec approve`) produce one consistent shape
205
- // ({ approved, timestamp, approvedBy }) in the authoritative feature.json —
206
- // and a manual approval stays distinguishable from a policy auto-approval.
207
- // Same contract as `approve`'s --self-assessed: a human may ship Gate 3 without the
208
- // independent review reports, but the degradation is stamped, never silent.
166
+ // Same contract as `approve --self-assessed`: approving review without its reports is stamped, never silent.
209
167
  const selfAssessed = gate === 'review' && feature?.workType !== 'hotfix'
210
168
  && ['verifier.md', 'reviewer.md'].some((f) => !existsSync(join(root, '.morph', 'features', featureName, '4-review', f)));
211
169
  if (selfAssessed) {
@@ -219,19 +177,9 @@ function handleAskUserQuestion(input, sessionRoot) {
219
177
  return warnings.length ? warnings.join('\n') : null;
220
178
  }
221
179
 
222
- /**
223
- * @param {object} payload - parsed PostToolUse stdin payload (Write|Edit|AskUserQuestion)
224
- * @returns {Promise<object|null>} a hookSpecificOutput warning on a gate
225
- * order/attribution violation, or null (pass/silent).
226
- */
227
180
  export async function run(payload) {
228
181
  try {
229
- // The SESSION's root. It is only a starting point: which `.morph/` actually
230
- // gets read and written is decided per event — by the target path for
231
- // Write/Edit, by the feature the question names for AskUserQuestion. The
232
- // `stateExists` gate therefore moved into each handler, against the tree that
233
- // handler resolved; gating on the session's tree up here would skip work that
234
- // belongs to a worktree (and, worse, silently pass it to the wrong one).
182
+ // Only a starting point: each handler resolves the tree it reads and writes, so the stateExists gate lives there.
235
183
  const sessionRoot = projectDir();
236
184
 
237
185
  const input = payload || {};
@@ -244,12 +192,8 @@ export async function run(payload) {
244
192
  } else if (toolName === 'AskUserQuestion') {
245
193
  warning = handleAskUserQuestion(input, sessionRoot);
246
194
  }
247
- } catch {
248
- // fail-open
249
- }
195
+ } catch {}
250
196
 
251
- // A non-blocking order-violation warning is surfaced as injected context
252
- // (hookSpecificOutput.additionalContext); everything else stays silent.
253
197
  return warning ? contextResult(warning, 'PostToolUse') : null;
254
198
  } catch {
255
199
  return null;
@@ -1,41 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * Telemetry Hook: harness execution-path capture
5
- *
6
- * Events: PostToolUse + PostToolUseFailure (all tools), PostToolBatch,
7
- * SubagentStop, UserPromptSubmit
8
- *
9
- * morph-spec is a Natural Language Harness: there is no central runtime to
10
- * instrument — Claude Code executes the skills and sub-agents. The only
11
- * vantage points are hooks. This one is the generic capture point: every tool
12
- * call and every user prompt is appended (one JSON line) to
13
- * .morph/logs/events.jsonl, from which `morph-spec telemetry` and
14
- * `morph-spec cost` reconstruct the path and measure it.
15
- *
16
- * Correlation is by an EXPLICIT key (feature + phase) written into each event,
17
- * not by ambient span context — hooks fire as separate node processes with no
18
- * shared trace context. `sessionId` and `agentId` (present only inside a
19
- * sub-agent) separate the orchestrator from the sub-agents it dispatched.
20
- *
21
- * PostToolUse fires only when a tool SUCCEEDS; a Bash command that exits
22
- * non-zero fires PostToolUseFailure instead. Only the exit code parsed from
23
- * its `error` first line is kept — the rest of `error` is the command's
24
- * output, where a secret value can appear with no name beside it.
25
- *
26
- * SINK RESOLUTION: the event is appended to the `.morph/` tree that OWNS the
27
- * tool's target file — the tree of its `.morph/features/` segment
28
- * (`resolveMorphRoot`), else the nearest morph tree above the file; tools with
29
- * no file path sink on the nearest morph tree above the payload's `cwd`, so a
30
- * sub-agent working in a worktree logs its Bash calls next to its Reads and
31
- * Writes. The feature is the tree's BINDING (`morph/<f>` branch, then
32
- * `MORPH_FEATURE` in `.morph/worktree.env`) before its most recent active
33
- * feature; a bound feature with no `feature.json` in that tree keeps phase null.
34
- *
35
- * SILENT: never blocks, never injects context — only appends. Fail-open on any
36
- * error (exit 0). Only active inside a morph project (stateExists gate).
37
- */
38
-
39
3
  import { statSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
40
4
  import { dirname, join } from 'path';
41
5
  import { stateExists, getActiveFeature, derivePhaseForFeature } from '../../shared/state-reader.js';
@@ -51,10 +15,7 @@ function normalize(p) {
51
15
  return (p || '').replace(/\\/g, '/');
52
16
  }
53
17
 
54
- // A multi-KB prompt or command written verbatim on every call dominates the
55
- // per-line cost. The ORIGINAL length travels in a sibling `<field>Chars` key,
56
- // which `morph-spec cost` prefers over the truncated string's own length —
57
- // a contract with the telemetry reader: do not rename either half.
18
+ // The original length travels in a sibling `<field>Chars` key that `morph-spec cost` prefers: do not rename either half.
58
19
  const MAX_FIELD_CHARS = 2000;
59
20
  const TRUNCATE_SUFFIX = '…[truncado]';
60
21
 
@@ -64,13 +25,7 @@ const DEFAULT_READ_LINES = 2000;
64
25
  const MAX_SLICE_BYTES = 10 * 1024 * 1024;
65
26
  const BINARY_READ = /\.(png|jpe?g|gif|webp|bmp|ico|pdf)$/i;
66
27
 
67
- /**
68
- * Redact, then cap a free-text field, reporting its original length. Redacting
69
- * first keeps a token cut at the cap from surviving as a recognizable prefix.
70
- *
71
- * @param {string} value
72
- * @returns {{ value: string, chars: number }}
73
- */
28
+ // Redact before capping: a token cut at the cap would survive as a recognizable prefix.
74
29
  function capField(value) {
75
30
  const str = typeof value === 'string' ? value : '';
76
31
  const clean = redactSecrets(str);
@@ -78,15 +33,11 @@ function capField(value) {
78
33
  return { value: clean.slice(0, MAX_FIELD_CHARS) + TRUNCATE_SUFFIX, chars: str.length };
79
34
  }
80
35
 
81
- /**
82
- * Classify a Read target into the telemetry `kind` taxonomy — a contract with
83
- * src/lib/telemetry/telemetry-reader.js, so these values are pinned. Specific
84
- * categories first, 'code' last.
85
- */
36
+ // The `kind` values are a contract with src/lib/telemetry/telemetry-reader.js.
86
37
  function classifyReadKind(target) {
87
38
  const t = (target || '').toLowerCase();
88
39
 
89
- if (t.includes('evals/review-checklist')) return 'rubric';
40
+ if (t.includes('evals/review-checklist')) return 'checklist';
90
41
  if (t.includes('standards/')) return 'standard';
91
42
  if (t.endsWith('skill.md') || t.includes('/skills/')) return 'skill';
92
43
  if (t.split('/').pop() === 'agents.json') return 'agents';
@@ -107,7 +58,6 @@ function classifyReadKind(target) {
107
58
  return 'code';
108
59
  }
109
60
 
110
- /** Nearest directory at or above `dir` that is a morph project; `fallback` when none. */
111
61
  function morphRootAbove(dir, fallback) {
112
62
  if (!dir) return fallback;
113
63
  let current = normalize(dir);
@@ -122,12 +72,7 @@ function morphRootAbove(dir, fallback) {
122
72
 
123
73
  const FEATURE_NAME = /^[A-Za-z0-9._-]+$/;
124
74
 
125
- /**
126
- * The feature the TREE is bound to, before any guess from its state: the
127
- * branch `morph/<f>` checked out there, then `MORPH_FEATURE` in its
128
- * `.morph/worktree.env` (a task worktree sits on `morph-task/<n>`). Without
129
- * it, a sub-agent's worktree reported the root's most recent active feature.
130
- */
75
+ // Branch binding before state: otherwise a sub-agent's worktree reports the root's most recent active feature.
131
76
  async function boundFeature(root) {
132
77
  const fromBranch = morphBranchFeature(root);
133
78
  if (fromBranch) return fromBranch;
@@ -143,10 +88,6 @@ function featureFromPath(target) {
143
88
  return m ? m[1] : null;
144
89
  }
145
90
 
146
- /**
147
- * What a Read actually put in context: the requested line window, not the
148
- * whole file. Binary reads (images, PDFs) go in whole.
149
- */
150
91
  function readSliceBytes(absPath, input, size) {
151
92
  if (BINARY_READ.test(absPath)) return size;
152
93
  if (size > MAX_SLICE_BYTES) return undefined;
@@ -157,7 +98,7 @@ function readSliceBytes(absPath, input, size) {
157
98
  return Buffer.byteLength(lines.slice(offset - 1, offset - 1 + limit).join('\n'), 'utf-8');
158
99
  }
159
100
 
160
- /** Exit code and outcome of a finished tool call, without keeping its output. */
101
+ // Only the exit code is kept: the rest of `error` is command output, where a secret can appear with no name beside it.
161
102
  function outcomeOf(payload, input) {
162
103
  if (payload.hook_event_name === 'PostToolUseFailure') {
163
104
  const firstLine = String(payload.error ?? '').split('\n', 1)[0];
@@ -181,11 +122,7 @@ function countByName(calls) {
181
122
  return out;
182
123
  }
183
124
 
184
- /**
185
- * Sub-agent tokens live in their own transcript; each stop emits the delta.
186
- * One file per agent: two sub-agents stopping together never share a
187
- * read-modify-write, and one agent never stops twice at once.
188
- */
125
+ // One store file per agent, so two sub-agents stopping together never share a read-modify-write.
189
126
  function agentUsageEvent(payload, cwd) {
190
127
  const usage = readTranscriptUsage(payload.agent_transcript_path);
191
128
  if (!usage || !payload.agent_id) return null;
@@ -204,11 +141,6 @@ function agentUsageEvent(payload, cwd) {
204
141
  return { event: 'AgentUsage', ...usage, delta };
205
142
  }
206
143
 
207
- /**
208
- * @param {object} payload - parsed hook payload (PostToolUse, PostToolUseFailure,
209
- * PostToolBatch, SubagentStop or UserPromptSubmit)
210
- * @returns {Promise<null>} always null — pure side effect (append + claim heartbeat)
211
- */
212
144
  export async function run(payload) {
213
145
  try {
214
146
  if (!payload) return null;
@@ -222,8 +154,6 @@ export async function run(payload) {
222
154
 
223
155
  if (!stateExists(cwd)) return null;
224
156
 
225
- // getActiveFeature → loadState hydrates every feature.json: O(active
226
- // features) per call, fine while archive keeps that set small.
227
157
  let feature = null;
228
158
  let phase = null;
229
159
  try {
@@ -237,14 +167,9 @@ export async function run(payload) {
237
167
  feature = active?.name ?? featureFromPath(targetFilePath);
238
168
  if (feature) phase = active?.feature?.phase || derivePhaseForFeature(feature, cwd);
239
169
  }
240
- } catch {
241
- // enrichment is best-effort — a missing feature never blocks capture
242
- }
170
+ } catch {}
243
171
 
244
- // Claim heartbeat: this hook fires on every tool call, so it is the proof
245
- // of life for the advisory feature claim. shouldHeartbeat() is a local,
246
- // git-free pre-check that says no on ~99% of calls; only then is the git
247
- // common dir resolved (pure filesystem, no subprocess).
172
+ // Claim heartbeat: shouldHeartbeat is a git-free pre-check that says no on ~99% of calls.
248
173
  if (feature && payload.session_id) {
249
174
  try {
250
175
  const { shouldHeartbeat, heartbeatClaim } = await import('../../shared/claim-registry.js');
@@ -253,7 +178,7 @@ export async function run(payload) {
253
178
  const common = resolveGitCommonDir(cwd);
254
179
  if (common) heartbeatClaim(feature, { gitCommonDir: common, sessionId: payload.session_id });
255
180
  }
256
- } catch { /* fail-open — a missed heartbeat only shortens a claim's life */ }
181
+ } catch {}
257
182
  }
258
183
 
259
184
  const hookEvent = payload.hook_event_name;
@@ -290,17 +215,14 @@ export async function run(payload) {
290
215
  event.kind = classifyReadKind(event.target);
291
216
  if (Number.isInteger(input.offset)) event.offset = input.offset;
292
217
  if (Number.isInteger(input.limit)) event.limit = input.limit;
293
- // A failed Read ingested nothing. A stat/read race with a file deleted
294
- // between the Read and this hook omits the numbers rather than fail.
218
+ // A failed Read ingested nothing; a file deleted before this hook just omits the numbers.
295
219
  if (hookEvent !== 'PostToolUseFailure') {
296
220
  try {
297
221
  const abs = targetAbs || resolveTargetPath(event.target, projectDir);
298
222
  event.bytes = statSync(abs).size;
299
223
  const read = readSliceBytes(abs, input, event.bytes);
300
224
  if (read !== undefined) event.bytesRead = read;
301
- } catch {
302
- // omit bytes
303
- }
225
+ } catch {}
304
226
  }
305
227
  } else if (tool === 'Bash' || tool === 'PowerShell') {
306
228
  const command = capField(input.command || '');
@@ -311,8 +233,6 @@ export async function run(payload) {
311
233
  } else if (tool === 'Write' || tool === 'Edit') {
312
234
  event.target = normalize(input.file_path || input.path);
313
235
  } else {
314
- // Every other tool still counts toward "the whole path" — a compact
315
- // target keeps Grep/Glob/WebFetch bursts distinguishable.
316
236
  const target = input.file_path || input.path || input.pattern || input.query || input.url || '';
317
237
  if (target) event.target = normalize(String(target));
318
238
  }
@@ -333,7 +253,6 @@ export async function run(payload) {
333
253
  appendTelemetryEvent(event, cwd);
334
254
  return null;
335
255
  } catch {
336
- // Fail-open
337
256
  return null;
338
257
  }
339
258
  }