@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
@@ -6,70 +6,31 @@ import { stateExists } from '../../shared/state-reader.js';
6
6
  import { scanForBlockers } from '../../shared/reparse-scan.js';
7
7
  import { tokenize } from '../../shared/command-tokens.js';
8
8
 
9
- /**
10
- * block-destructive-worktree-removal — the only defense that does not depend on
11
- * someone reading documentation.
12
- *
13
- * A morph worktree carries JUNCTIONS to the primary root's shared infra
14
- * (`node_modules`, `.claude`, `.morph/framework`). Every generic recursive
15
- * removal — `git worktree remove` (plain AND `--force`), `rm -rf`,
16
- * `Remove-Item -Recurse`, `rmdir /s` — FOLLOWS those links out of the worktree
17
- * and deletes what they point at, leaving the primary root's `.claude/` and
18
- * `.morph/framework/` as empty directories. That is the incident this hook
19
- * exists for, and it is not hypothetical: it happened, and the command was
20
- * copied from a message the CLI itself printed.
21
- *
22
- * `morph-spec worktree remove` is safe because it drops the junctions BEFORE
23
- * letting git near the directory. This hook is what stops the unsafe path from
24
- * being reachable by accident.
25
- *
26
- * DELIBERATELY NARROW — an inert or over-eager guardrail is worse than none:
27
- * - only recursive-removal shapes, only when a target path can be extracted;
28
- * - only when that path exists AND the scan finds reparse points under it
29
- * (an already-unlinked worktree, or one that never had junctions, passes);
30
- * - anything unparseable passes.
31
- * The scan is the authority, not the path shape: a worktree kept outside
32
- * `worktrees/` is protected just the same, and a `worktrees/` directory with
33
- * nothing to traverse is not blocked for looking dangerous.
34
- */
9
+ // Generic recursive removals FOLLOW a worktree's junctions and empty the root's `.claude/` and `.morph/framework/`.
10
+ // The reparse scan is the authority, not the path shape; anything unparseable passes.
35
11
 
36
- // The tokenizer lives in shared/command-tokens.js — block-raw-worktree-add.js
37
- // reads `git worktree add` off the same tokens, and a hook must never import a
38
- // sibling hook (its standalone tail would fire in the importer's process).
12
+ // A hook never imports a sibling hook (its standalone tail would fire), so the tokenizer lives in shared/.
39
13
  export { tokenize };
40
14
 
41
- /**
42
- * The path a recursive-removal command would destroy, or null when this is not
43
- * such a command (or the target cannot be read off it).
44
- *
45
- * @param {string} command
46
- * @returns {string|null}
47
- */
48
15
  export function extractRemovalTarget(command) {
49
16
  const tokens = tokenize(command);
50
17
  if (!tokens.length) return null;
51
18
  const lower = tokens.map((t) => t.toLowerCase());
52
19
 
53
- // `git worktree remove [--force] <path>` — the path is always the last
54
- // non-flag token; git has no form of this command without one.
20
+ // The path is always the last non-flag token.
55
21
  const gi = lower.indexOf('git');
56
22
  if (gi !== -1 && lower[gi + 1] === 'worktree' && lower[gi + 2] === 'remove') {
57
23
  const rest = tokens.slice(gi + 3).filter((t) => !t.startsWith('-'));
58
24
  return rest.length ? rest[rest.length - 1] : null;
59
25
  }
60
26
 
61
- // Generic recursive deletes. Each needs BOTH its command word and a recursive
62
- // flag — a plain `rm file` or `Remove-Item file` cannot traverse a junction's
63
- // contents and is none of this hook's business.
27
+ // Needs both the command word and a recursive flag: a plain `rm file` cannot traverse a junction.
64
28
  const recursive = tokens.some((t) => /^(-[a-z]*r[a-z]*|--recursive|\/s)$/i.test(t));
65
29
  const removalCmd = lower.find((t) => /^(rm|remove-item|ri|rd|rmdir|del)$/.test(t));
66
30
  if (!removalCmd || !recursive) return null;
67
31
 
68
32
  const idx = lower.indexOf(removalCmd);
69
- // A cmd.exe switch is a slash plus a letter or two (`rmdir /s /q`). Anything
70
- // longer that starts with `/` is a POSIX ABSOLUTE PATH — filtering on the
71
- // bare `/` made every Linux target look like a flag, so `rm -rf /tmp/wt`
72
- // extracted nothing and the hook passed exactly where it had to block.
33
+ // A cmd.exe switch is a slash plus one or two letters; anything longer is a POSIX absolute path.
73
34
  const isCmdSwitch = (t) => /^\/[a-z]{1,2}$/i.test(t);
74
35
  const operands = tokens.slice(idx + 1).filter((t) => !t.startsWith('-') && !isCmdSwitch(t));
75
36
  // Named-parameter form: `Remove-Item -Path <p> -Recurse`.
@@ -78,10 +39,6 @@ export function extractRemovalTarget(command) {
78
39
  return operands.length ? operands[0] : null;
79
40
  }
80
41
 
81
- /**
82
- * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
83
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
84
- */
85
42
  export async function run(payload) {
86
43
  try {
87
44
  if (!stateExists()) return null;
@@ -5,56 +5,23 @@ import { stateExists } from '../../shared/state-reader.js';
5
5
  // standalone tail (stdin read + exit) inside THIS process.
6
6
  import { tokenize } from '../../shared/command-tokens.js';
7
7
 
8
- /**
9
- * block-raw-worktree-add — PreToolUse (Bash) guard against `git worktree add`.
10
- *
11
- * A worktree created by raw git is a worktree morph cannot see: no infra
12
- * junctions (`.claude`, `node_modules`, `.morph/framework` — so NO hooks run
13
- * in there), no port block (its dev server collides with the root's), no
14
- * feature/task identity (invisible to `fleet`, `doctor --worktrees`,
15
- * `worktree list`), and a location git chose, not `<root>/worktrees/`. The
16
- * sibling guard (block-destructive-worktree-removal) protects the way OUT;
17
- * this one protects the way IN, by naming the two commands that build a
18
- * governed tree:
19
- *
20
- * morph-spec worktree setup <feature> — a feature's tree
21
- * morph-spec worktree provision <name> [--base <ref>] — a task-isolation tree
22
- *
23
- * DELIBERATELY NARROW: only the `add` subcommand. `list`, `prune`, `remove`
24
- * (the sibling's business), `lock`, `unlock`, `move`, `repair` all pass. The
25
- * `git` token may be followed by global options (`-C <dir>`, `--git-dir=<d>`,
26
- * `-c k=v`) before `worktree`, and may sit anywhere in a chained command
27
- * (`cd a && git worktree add b`) — every `git` occurrence is checked.
28
- */
29
-
30
- /** Global `git` options that take a separate value: `git -C <dir> worktree add`. */
31
8
  const GIT_GLOBAL_WITH_VALUE = new Set(['-c', '-C', '--git-dir', '--work-tree', '--namespace', '--exec-path', '--config-env']);
32
9
 
33
- /**
34
- * Does this command line invoke `git worktree add` anywhere?
35
- * @param {string} command
36
- * @returns {boolean}
37
- */
38
10
  export function isRawWorktreeAdd(command) {
39
11
  const tokens = tokenize(command);
40
12
  const lower = tokens.map((t) => t.toLowerCase());
41
13
  for (let i = 0; i < lower.length; i++) {
42
14
  if (lower[i] !== 'git' && !/(^|[\\/])git(\.exe)?$/.test(lower[i])) continue;
43
15
  let j = i + 1;
44
- // Skip global options between `git` and the subcommand.
45
16
  while (j < lower.length && lower[j].startsWith('-')) {
46
- if (GIT_GLOBAL_WITH_VALUE.has(tokens[j])) j += 2; // `-C dir` consumes its value
47
- else j += 1; // `--git-dir=x`, `--no-pager`, …
17
+ if (GIT_GLOBAL_WITH_VALUE.has(tokens[j])) j += 2;
18
+ else j += 1;
48
19
  }
49
20
  if (lower[j] === 'worktree' && lower[j + 1] === 'add') return true;
50
21
  }
51
22
  return false;
52
23
  }
53
24
 
54
- /**
55
- * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
56
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
57
- */
58
25
  export async function run(payload) {
59
26
  try {
60
27
  if (!stateExists()) return null;
@@ -1,54 +1,6 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * chain.js — roda vários hooks core num único processo node.
4
- *
5
- * Motivação (onda de performance R2): cada hook registrado no Claude Code é
6
- * hoje um processo node próprio (~60ms de cold start). Um único Write/Edit
7
- * disparava até 9 desses processos — 2 PreToolUse + 3+2+2 PostToolUse. Este
8
- * script agrupa os elos de um mesmo event+matcher (a mesma entrada do
9
- * registry em src/utils/hooks-installer.js) num só spawn: lê stdin UMA vez,
10
- * importa e chama `run(payload)` de cada hook em sequência, e combina as
11
- * respostas — reduzindo esses 9 spawns para 4 por Write/Edit (1 PreToolUse +
12
- * 3 PostToolUse: Write|Edit / Write|Edit|AskUserQuestion / matcher-`*`).
13
- *
14
- * Uso: `node chain.js <hook1> <hook2> ...` — nomes de arquivo SEM `.js`,
15
- * resolvidos no mesmo diretório deste script (core/), na ORDEM em que devem
16
- * rodar. A ordem é a mesma que o registry antigo (duas entries separadas)
17
- * já implicava — ver o comentário de cada grupo em hooks-installer.js.
18
- *
19
- * Cada hook core exporta `run(payload)` (ver framework/hooks/shared/hook-response.js
20
- * `runStandalone`) — o arquivo continua executável standalone (spawnSync
21
- * direto, como os testes já fazem) porque a própria cauda do arquivo só lê
22
- * stdin/imprime/sai quando NÃO está sendo importado por esta cadeia.
23
- *
24
- * Regras de combinação:
25
- * - PreToolUse (decision:'block'): o PRIMEIRO elo que retornar um bloqueio
26
- * VENCE e encerra a cadeia — os elos seguintes nem chegam a rodar. Isto é
27
- * uma otimização NOVA desta consolidação (antes, Claude Code podia
28
- * disparar os dois processos de qualquer forma); o resultado observável
29
- * pro Claude Code é idêntico (o tool call é bloqueado do mesmo jeito).
30
- * - PostToolUse / outros eventos de contexto: os textos de TODOS os elos que
31
- * retornarem `hookSpecificOutput.additionalContext` (ou `decision:'approve'`
32
- * com additionalContext) são concatenados, na ORDEM dos argv, separados por
33
- * uma linha em branco — nenhum elo é pulado por um vizinho ter respondido.
34
- * - Nome do evento (hookSpecificOutput.hookEventName) no resultado final:
35
- * o valor que o PRIMEIRO elo a declarar um venceu (mesma regra de
36
- * "primeiro vence" do bloqueio acima — na prática todos os elos de um
37
- * mesmo grupo observam o MESMO evento real do Claude Code, então isto só
38
- * importa como desempate determinístico); na ausência de qualquer elo
39
- * declarando, cai para `payload.hook_event_name`; por último, `'PostToolUse'`.
40
- * - updatedInput (PreToolUse): um elo pode devolver
41
- * `hookSpecificOutput.updatedInput` (ver `updatedInputResult` em
42
- * hook-response.js) para reescrever o tool_input. O ÚLTIMO elo a devolver um
43
- * vence (só um elo do grupo reescreve, por construção). `permissionDecision:
44
- * 'allow'` + `updatedInput` só entram no resultado final quando algum elo
45
- * reescreveu — um `allow` incondicional pularia o prompt de permissão de
46
- * TODO Bash, que é exatamente o contrário de um guardrail.
47
- * - Fail-open POR ELO: uma exceção não tratada por um hook individual (import
48
- * falhou, run() não existe, etc.) não derruba a cadeia — os elos seguintes
49
- * ainda rodam. Cada hook já fail-opens sozinho dentro do seu próprio
50
- * try/catch; isto é a segunda rede de segurança, para o import em si.
51
- */
2
+ // Usage: node chain.js <hook> ... (names without .js, run in argv order). The first block wins and ends the chain,
3
+ // the contexts of every link concatenate, and an exception in one link never stops the next.
52
4
 
53
5
  import { dirname, join } from 'path';
54
6
  import { fileURLToPath, pathToFileURL } from 'url';
@@ -56,20 +8,12 @@ import { readStdin } from '../../shared/stdin-reader.js';
56
8
 
57
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
58
10
 
59
- // Marca "já estou dentro de uma cadeia" para os hooks importados abaixo: a
60
- // cauda de execução standalone de cada hook (runStandalone() em
61
- // hook-response.js) só dispara quando esta flag NÃO está setada. Setada
62
- // ANTES do primeiro import dinâmico — cada hook importado herda o mesmo
63
- // process.env deste processo.
11
+ // Set before the first import: each hook's standalone tail runs only when this is unset.
64
12
  process.env.MORPH_HOOK_CHAINED = '1';
65
13
 
66
- // Override só para testes da mecânica genérica desta cadeia (fixtures fora
67
- // de core/, sem precisar ser hooks reais nem satisfazer as convenções que
68
- // hook-wiring.test.js exige de todo arquivo em core/). Produção nunca seta
69
- // isto — resolve sempre no próprio diretório deste script.
14
+ // Test-only override; production always resolves next to this script.
70
15
  const coreDir = process.env.MORPH_HOOK_CORE_DIR || __dirname;
71
16
 
72
- /** Extrai o texto de contexto de um resultado de hook, se houver. */
73
17
  function contextOf(result) {
74
18
  if (!result) return null;
75
19
  if (result.decision === 'approve' && typeof result.additionalContext === 'string') {
@@ -93,13 +37,11 @@ async function main() {
93
37
  const mod = await import(pathToFileURL(join(coreDir, `${name}.js`)).href);
94
38
  result = await mod.run(payload);
95
39
  } catch {
96
- // Fail-open POR ELO — ver doc do módulo acima.
97
40
  result = null;
98
41
  }
99
42
 
100
43
  if (!result) continue;
101
44
 
102
- // PreToolUse: o primeiro bloqueio vence e encerra a cadeia.
103
45
  if (result.decision === 'block') {
104
46
  // A later link blocking must not swallow what earlier links reported (a tsc error on the same edit).
105
47
  if (contexts.length) result = { ...result, reason: [...contexts, result.reason].join('\n\n') };
@@ -116,9 +58,6 @@ async function main() {
116
58
  const ctx = contextOf(result);
117
59
  if (ctx) {
118
60
  contexts.push(ctx);
119
- // First link to declare a name wins (mirrors block-first-wins above) —
120
- // in practice every link in a group observes the SAME real Claude Code
121
- // event, so this only matters as a deterministic tie-break.
122
61
  eventName = eventName || result?.hookSpecificOutput?.hookEventName;
123
62
  }
124
63
  }
@@ -1,17 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * PostToolUse Hook: comments the edit added in a shape the
5
- * `architecture-code-comments` standard never allows.
6
- *
7
- * Event: PostToolUse | Matcher: Write|Edit
8
- *
9
- * `decision: block` makes the agent fix it in the same turn; the edit itself
10
- * already landed. Only comments absent from `tool_response.originalFile`
11
- * count, so editing a legacy file never reports its old comments.
12
- *
13
- * Fail-open: any error passes silently.
14
- */
3
+ // `decision: block` makes the agent fix it in the same turn (the edit already landed). Only comments absent from
4
+ // `tool_response.originalFile` count, so editing a legacy file never reports its old comments.
15
5
 
16
6
  import { readFileSync } from 'fs';
17
7
  import { isAbsolute, relative, resolve } from 'path';
@@ -78,10 +68,6 @@ function describe(file, findings) {
78
68
  ].join('\n');
79
69
  }
80
70
 
81
- /**
82
- * @param {object} payload - PostToolUse stdin payload
83
- * @returns {Promise<object|null>}
84
- */
85
71
  export async function run(payload) {
86
72
  try {
87
73
  if (!['Write', 'Edit'].includes(payload?.tool_name)) return null;
@@ -1,22 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * PreToolUse Hook: Enforce Phase-Appropriate File Writes
5
- *
6
- * Event: PreToolUse | Matcher: Write|Edit
7
- *
8
- * Ensures writes to .morph/features/{feature}/ go to the current phase's subfolder:
9
- * plan → only 2-plan/ (and 2-plan/ is writable in every phase: the errata
10
- * and plan-check live there)
11
- * uiux → only 1-design/
12
- * implement → 3-implement/ + any source code (unrestricted)
13
- * review → only 4-review/
14
- *
15
- * Files outside .morph/features/ are always allowed.
16
- *
17
- * Fail-open: exits 0 on any error.
18
- */
19
-
20
3
  import { existsSync } from 'fs';
21
4
  import { stateExists, getFeaturePhase, isGateApproved } from '../../shared/state-reader.js';
22
5
  import {
@@ -26,13 +9,7 @@ import {
26
9
  PHASE_DIRS
27
10
  } from '../../shared/phase-utils.js';
28
11
 
29
- /**
30
- * The gate that authorizes writing a phase folder's FIRST artifact. Once that
31
- * gate is approved, the next phase's canonical file may be created even though
32
- * the phase only advances (via derivation) once that file exists — this is the
33
- * fix for the forward-transition chicken-and-egg. The plan gate unlocks both
34
- * the optional `1-design/` and `3-implement/` (the uiux fork).
35
- */
12
+ // Approving this gate lets the next phase's first file be created: the phase only advances once that file exists.
36
13
  const PHASE_DIR_ENTRY_GATE = {
37
14
  '1-design': 'plan',
38
15
  '3-implement': 'plan',
@@ -44,14 +21,8 @@ import { blockResult, runStandalone } from '../../shared/hook-response.js';
44
21
  import { logHookActivity } from '../../shared/activity-logger.js';
45
22
  import { getFilePath, resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
46
23
 
47
- /**
48
- * @param {object} payload - parsed PreToolUse stdin payload
49
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
50
- * Never prints, never exits — see runStandalone()/chain.js at the bottom.
51
- */
52
24
  export async function run(payload) {
53
25
  try {
54
- // Amend mode: bypass phase write enforcement for legitimate corrections
55
26
  if (process.env.MORPH_AMEND_PHASE) {
56
27
  console.error(`[morph-spec] AMEND MODE: bypassing phase protection for phase '${process.env.MORPH_AMEND_PHASE}'`);
57
28
  return null;
@@ -62,14 +33,9 @@ export async function run(payload) {
62
33
  const filePath = getFilePath(payload);
63
34
  if (!filePath) return null;
64
35
 
65
- // Only check files inside .morph/features/
66
36
  if (!isFeaturePath(filePath)) return null;
67
37
 
68
- // Resolve the tree that OWNS this file, NOT the tree the session sits in.
69
- // A session rooted in the primary repo may legitimately write to a feature
70
- // living in a secondary worktree; reading the session's `.morph/` there
71
- // reported the ROOT's phase for the WORKTREE's file and blocked a legal write.
72
- // `stateExists` must be asked of that same tree — a worktree carries its own state.
38
+ // The tree that OWNS the file, not the session's: a root session may write into a worktree's feature.
73
39
  const root = resolveMorphRoot(filePath, process.cwd());
74
40
  if (!stateExists(root)) return null;
75
41
 
@@ -79,28 +45,22 @@ export async function run(payload) {
79
45
  const phase = getFeaturePhase(featureName, root);
80
46
  if (!phase) return null;
81
47
 
82
- // During implement phase, all writes are allowed (source code is unrestricted)
83
- if (phase === 'implement' || phase === 'sync') return null;
48
+ if (phase === 'implement') return null;
84
49
 
85
50
  const targetDir = extractPhaseDir(filePath);
86
- if (!targetDir) return null; // File directly in feature root — allow
51
+ if (!targetDir) return null;
87
52
  if (targetDir === ALWAYS_WRITABLE_DIR) return null;
88
53
 
89
54
  const allowedDir = PHASE_DIRS[phase];
90
- if (!allowedDir) return null; // Unknown phase — allow
55
+ if (!allowedDir) return null;
91
56
 
92
57
  if (targetDir !== allowedDir) {
93
- // Allow editing already-existing files from previous phases
94
58
  const absolutePath = resolveTargetPath(filePath, root);
95
59
  if (existsSync(absolutePath)) {
96
60
  logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
97
- return null; // editing existing file — allow
61
+ return null;
98
62
  }
99
63
 
100
- // Gate-aware forward transition: allow creating the NEXT phase's first
101
- // artifact as soon as the gate authorizing that phase is approved. Without
102
- // this, the first file of a new phase can never be written (the phase only
103
- // advances once that file exists) — the deadlock this hook used to create.
104
64
  const entryGate = PHASE_DIR_ENTRY_GATE[targetDir];
105
65
  if (entryGate && isGateApproved(featureName, entryGate, root)) {
106
66
  // uiux is optional, but once 1-design/ exists its gate is not
@@ -124,9 +84,6 @@ export async function run(payload) {
124
84
  }
125
85
 
126
86
  const phaseLabel = phase.toUpperCase();
127
- // Name the tree the verdict came from whenever it is NOT the session's own.
128
- // The phase read is per-tree, so "which .morph/ said plan?" is the first
129
- // question anyone debugging this asks.
130
87
  const treeNote = root !== process.cwd().replace(/\\/g, '/')
131
88
  ? `\n(phase read from the tree that owns the file: ${root})`
132
89
  : '';
@@ -143,7 +100,6 @@ export async function run(payload) {
143
100
  logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
144
101
  return null;
145
102
  } catch {
146
- // Fail-open
147
103
  return null;
148
104
  }
149
105
  }
@@ -1,28 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * gate-guard.js — PostToolUse hook (detect-and-warn, never block)
4
- *
5
- * Closes the gap from issue #63: the trust-mode contract lived only in skill
6
- * pseudocode, with nothing verifying the LLM actually honored it. This hook
7
- * compares the OBSERVED gate action against the decision the skill persisted
8
- * to state.json (`features[feature].gateDecisions[gate]`) and injects a
9
- * non-blocking warning when they disagree.
10
- *
11
- * It is a guardrail, NOT an orchestrator (per MORPH.md): it never blocks,
12
- * never writes state, and fails open on any error or missing data.
13
- *
14
- * Two mismatch directions:
15
- * - LLM PAUSED (AskUserQuestion on a gate) but the persisted decision said
16
- * pause:false → over-pausing under trust:auto. Warn.
17
- * - LLM AUTO-APPROVED (gate became approvedBy:"auto") but the persisted
18
- * decision said pause:true → skipped a required human pause. Warn.
19
- *
20
- * Observability: every time a gate action is actually checked against a
21
- * persisted gateDecisions entry, one line lands in the activity log
22
- * (.morph/logs/activity.json) with result 'coherent' or 'divergent' — so a
23
- * human can confirm the guard verified coherence instead of absolute silence.
24
- * No persisted decision, non-gate action, or any error → silence.
25
- */
2
+ // Detect-and-warn, never block: compares the observed gate action with the persisted gateDecisions.
26
3
 
27
4
  import { loadState, stateExists, getActiveFeature } from '../../shared/state-reader.js';
28
5
  import { contextResult, runStandalone } from '../../shared/hook-response.js';
@@ -35,17 +12,12 @@ function projectDir() {
35
12
  return process.env.CLAUDE_PROJECT_DIR || process.cwd();
36
13
  }
37
14
 
38
- /** Feature slug from a `.morph/features/{name}/feature.json` path. */
39
15
  function featureFromStatePath(filePath) {
40
16
  const fp = String(filePath ?? '').replace(/\\/g, '/');
41
17
  return (fp.match(/\.morph\/features\/([^/]+)\/feature\.json$/) ?? [])[1] ?? null;
42
18
  }
43
19
 
44
- /**
45
- * The tree owning an edited gate-state file. `resolveMorphRoot` covers the v5
46
- * per-feature `feature.json`; the legacy `.morph/state.json` has no feature
47
- * segment to anchor on, so strip its own suffix instead.
48
- */
20
+ // The legacy `.morph/state.json` has no feature segment to anchor on, so strip its own suffix.
49
21
  function rootOfGateStateFile(filePath, fallback) {
50
22
  const abs = resolveTargetPath(filePath, fallback);
51
23
  const suffix = '/.morph/state.json';
@@ -53,7 +25,6 @@ function rootOfGateStateFile(filePath, fallback) {
53
25
  return resolveMorphRoot(abs, fallback);
54
26
  }
55
27
 
56
- /** Every question/header text this AskUserQuestion call surfaced. */
57
28
  function askedTexts(input) {
58
29
  const texts = [];
59
30
  const questions = input.tool_input?.questions;
@@ -75,29 +46,20 @@ function getDecision(state, feature, gate) {
75
46
  return d;
76
47
  }
77
48
 
78
- /**
79
- * The LLM just asked a gate question. If the persisted decision said this
80
- * gate should auto-pass (pause:false), the human was interrupted needlessly.
81
- */
82
49
  function checkAskUserQuestion(input, sessionRoot) {
83
50
  for (const text of askedTexts(input)) {
84
51
  const gate = gateFromQuestion(text);
85
52
  if (!gate) continue;
86
53
 
87
- // Check the decision of the feature the question NAMES, in its own tree.
88
- // Reading the session tree's first in_progress feature made this guard
89
- // compare a gate against an unrelated feature's gateDecisions — a warning
90
- // about the wrong thing is worse than silence, so an unattributable
91
- // question is skipped rather than guessed at (state-sync already tells the
92
- // operator when a gate answer could not be attributed).
54
+ // Only the feature the question NAMES: a warning about the wrong feature is worse than silence.
93
55
  const located = locateFeatureForQuestion(text, sessionRoot);
94
56
  if (!located) continue;
95
57
  const { name: feature, root: cwd } = located;
96
- if (!stateExists(cwd)) continue; // fail-open
58
+ if (!stateExists(cwd)) continue;
97
59
 
98
60
  const state = loadState(cwd);
99
61
  const decision = getDecision(state, feature, gate);
100
- if (!decision) continue; // no persisted decision → nothing to verify
62
+ if (!decision) continue;
101
63
  if (decision.pause === false) {
102
64
  logHookActivity('gate-guard', 'PostToolUse', 'divergent', cwd);
103
65
  return contextResult(
@@ -110,34 +72,24 @@ function checkAskUserQuestion(input, sessionRoot) {
110
72
  'PostToolUse'
111
73
  );
112
74
  }
113
- // The decision said pause:true and the LLM did pause — coherent.
114
75
  logHookActivity('gate-guard', 'PostToolUse', 'coherent', cwd);
115
76
  return null;
116
77
  }
117
78
  return null;
118
79
  }
119
80
 
120
- /** True for edits to .morph/state.json (legacy) or a per-feature feature.json (v5). */
121
81
  function isGateStateFile(filePath) {
122
82
  const fp = String(filePath ?? '').replace(/\\/g, '/');
123
83
  if (fp.endsWith('.morph/state.json')) return true;
124
84
  return fp.includes('.morph/features/') && fp.endsWith('/feature.json');
125
85
  }
126
86
 
127
- /**
128
- * The LLM just edited gate state (state.json legacy, or the authoritative
129
- * feature.json in v5). If a gate flipped to approvedBy:"auto" but the
130
- * persisted decision said pause:true, a required human pause was skipped.
131
- */
132
87
  function checkStateEdit(input, sessionRoot) {
133
88
  const filePath = input.tool_input?.file_path;
134
89
  if (!isGateStateFile(filePath)) return null;
135
90
 
136
- // The edited file names its own tree — and, for the v5 per-feature
137
- // feature.json, its own feature. Only the legacy `.morph/state.json` shape
138
- // still needs a most-recently-updated fallback within that tree.
139
91
  const cwd = rootOfGateStateFile(filePath, sessionRoot);
140
- if (!stateExists(cwd)) return null; // fail-open
92
+ if (!stateExists(cwd)) return null;
141
93
  const state = loadState(cwd);
142
94
  if (!state) return null;
143
95
  const feature = featureFromStatePath(filePath) || getActiveFeature(cwd)?.name;
@@ -168,22 +120,12 @@ function checkStateEdit(input, sessionRoot) {
168
120
  return null;
169
121
  }
170
122
 
171
- /**
172
- * @param {object} payload - parsed PostToolUse stdin payload (Write|Edit|AskUserQuestion)
173
- * @returns {Promise<object|null>} a hookSpecificOutput warning on a
174
- * trust-mode divergence, or null (pass/silent).
175
- */
176
123
  export async function run(payload) {
177
- // Starting point only — each branch resolves the tree it actually inspects
178
- // (from the edited path, or from the feature the question names).
179
124
  const sessionRoot = projectDir();
180
125
 
181
126
  const input = payload || {};
182
127
  const toolName = input.tool_name;
183
128
 
184
- // Short-circuit before loading state: a Write/Edit only matters if it
185
- // touched gate state (state.json legacy, or the v5 per-feature
186
- // feature.json). Skip the disk read for unrelated edits.
187
129
  if (toolName === 'Edit' || toolName === 'Write') {
188
130
  if (!isGateStateFile(input.tool_input?.file_path)) return null;
189
131
  } else if (toolName !== 'AskUserQuestion') {
@@ -193,9 +135,7 @@ export async function run(payload) {
193
135
  try {
194
136
  if (toolName === 'AskUserQuestion') return checkAskUserQuestion(input, sessionRoot);
195
137
  return checkStateEdit(input, sessionRoot);
196
- } catch {
197
- // fail-open
198
- }
138
+ } catch {}
199
139
  return null;
200
140
  }
201
141
 
@@ -1,20 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * PostToolUse Hook: dispara o refresh do grafo de codebase
5
- *
6
- * Event: PostToolUse | Matcher: none (elo da cadeia de todos os tools)
7
- *
8
- * Depois de um comando de shell — Bash ou PowerShell, que no Windows é o shell
9
- * principal do Claude Code — que chama git, gh ou morph-spec, solta o refresh
10
- * do grafo da RAIZ em background (framework/hooks/shared/graph-refresh.js) e
11
- * volta na hora. O filtro é largo de propósito: o refresh só roda quando o
12
- * grafo da raiz está atrás do HEAD, então um `git status` custa um processo
13
- * que sai sem fazer nada — e um comando que move o HEAD sem ser reconhecido é
14
- * exatamente como o grafo ficou 85 commits atrás.
15
- *
16
- * Silencioso e fail-open: nunca injeta contexto, nunca bloqueia.
17
- */
3
+ // Wide filter on purpose: the refresh only runs when the root's graph is behind HEAD, and a HEAD-moving command
4
+ // left unrecognized is how the graph falls behind.
18
5
 
19
6
  import { stateExists } from '../../shared/state-reader.js';
20
7
  import { runStandalone } from '../../shared/hook-response.js';
@@ -24,20 +11,12 @@ import { kickGraphRefresh } from '../../shared/graph-refresh.js';
24
11
  const SHELL_TOOLS = new Set(['Bash', 'PowerShell']);
25
12
  const GIT_TOOL = /(^|[\\/])(git|gh|morph-spec)(\.exe|\.cmd)?$/i;
26
13
 
27
- /**
28
- * @param {string} command
29
- * @returns {boolean} o comando chama git, gh ou morph-spec em algum ponto
30
- */
31
14
  export function touchesGit(command) {
32
15
  return tokenize(command)
33
16
  .flatMap((t) => t.split(/[;&|()]+/))
34
17
  .some((t) => GIT_TOOL.test(t));
35
18
  }
36
19
 
37
- /**
38
- * @param {object} payload - PostToolUse stdin payload
39
- * @returns {Promise<null>}
40
- */
41
20
  export async function run(payload) {
42
21
  try {
43
22
  if (!SHELL_TOOLS.has(payload?.tool_name)) return null;
@@ -45,7 +24,7 @@ export async function run(payload) {
45
24
  const dir = payload?.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
46
25
  if (!stateExists(dir)) return null;
47
26
  kickGraphRefresh(dir);
48
- } catch { /* fail-open */ }
27
+ } catch {}
49
28
  return null;
50
29
  }
51
30
 
@@ -1,20 +1,7 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * heavy-gate.js — PreToolUse hook (Bash matcher), link of the Bash chain before worktree-env-inject.
4
- *
5
- * A sub-agent that runs `dotnet build` or `npm test` straight through Bash bypasses the heavy-node
6
- * semaphore that `morph-spec verify` respects. With `heavy.gateBash: true` this hook takes a slot
7
- * before such a command runs and `heavy-release` gives it back on Post/PostFailure, matched by
8
- * `tool_use_id`. It never rewrites the command: a rewrite would be judged against the user's
9
- * permission rules in its rewritten form, and would auto-approve what the prompt would have asked.
10
- *
11
- * A slot taken here has no live process to watch — the hook exits at once — so it lapses by TTL
12
- * (the call's own timeout plus slack). A call refused by a permission rule or interrupted by the
13
- * user fires no Post hook and keeps its slot until then.
14
- *
15
- * Waits below the platform's 600 s hook timeout, because a timed-out PreToolUse hook lets the call
16
- * run: the refusal has to come from here. Fail-open otherwise.
17
- */
2
+ // Never rewrites the command: a rewrite would be judged against the user's permission rules in its rewritten form.
3
+ // The lease has no live pid (the hook exits at once), so it lapses by TTL; a call that fires no Post hook holds it until then.
4
+ // The wait stays under the 600 s hook timeout because a timed-out PreToolUse hook lets the call run.
18
5
 
19
6
  import { stateExists } from '../../shared/state-reader.js';
20
7
  import { blockResult, runStandalone } from '../../shared/hook-response.js';
@@ -22,11 +9,6 @@ import { heavyKind, leaseTtlMs } from '../../shared/heavy-command.js';
22
9
 
23
10
  const HOOK_BUDGET_SEC = 540;
24
11
 
25
- /**
26
- * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
27
- * @param {{ env?: object, acquire?: Function }} [deps] - test seams; production passes none
28
- * @returns {Promise<object|null>} a block result when no slot came free, else null (pass)
29
- */
30
12
  export async function run(payload, deps = {}) {
31
13
  try {
32
14
  if (payload?.tool_name && payload.tool_name !== 'Bash') return null;