@polymorphism-tech/morph-spec 8.47.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 (377) hide show
  1. package/CLAUDE.md +51 -43
  2. package/README.md +39 -38
  3. package/bin/morph-spec.js +41 -113
  4. package/bin/validate.js +1 -147
  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 +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +86 -21
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-status.md +4 -4
  17. package/framework/docs/CLI.md +574 -286
  18. package/framework/evals/review-checklist.md +82 -41
  19. package/framework/hooks/README.md +10 -11
  20. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  21. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  22. package/framework/hooks/claude-code/core/chain.js +4 -65
  23. package/framework/hooks/claude-code/core/comment-guard.js +3 -17
  24. package/framework/hooks/claude-code/core/denylist.json +33 -3
  25. package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
  26. package/framework/hooks/claude-code/core/gate-guard.js +9 -78
  27. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  28. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  29. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  30. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  31. package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
  32. package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
  33. package/framework/hooks/claude-code/core/state-sync.js +15 -85
  34. package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
  35. package/framework/hooks/claude-code/core/validate-completion.js +37 -147
  36. package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
  37. package/framework/hooks/claude-code/core/worktree-context.js +15 -114
  38. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  39. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  40. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  41. package/framework/hooks/claude-code/statusline.py +55 -41
  42. package/framework/hooks/package.json +3 -0
  43. package/framework/hooks/shared/activity-logger.js +9 -89
  44. package/framework/hooks/shared/agent-reap.js +2 -17
  45. package/framework/hooks/shared/build-check.js +11 -76
  46. package/framework/hooks/shared/claim-registry.js +9 -119
  47. package/framework/hooks/shared/command-tokens.js +168 -44
  48. package/framework/hooks/shared/comment-rules.js +5 -20
  49. package/framework/hooks/shared/comment-scanner.js +11 -43
  50. package/framework/hooks/shared/comment-stripper.js +11 -85
  51. package/framework/hooks/shared/e2e-check.js +0 -0
  52. package/framework/hooks/shared/errata.js +59 -0
  53. package/framework/hooks/shared/feature-locator.js +11 -95
  54. package/framework/hooks/shared/frontend-locations.js +4 -0
  55. package/framework/hooks/shared/gate-question.js +12 -0
  56. package/framework/hooks/shared/git-exclude.js +3 -20
  57. package/framework/hooks/shared/git-paths.js +8 -104
  58. package/framework/hooks/shared/graph-freshness.js +22 -166
  59. package/framework/hooks/shared/graph-refresh.js +21 -149
  60. package/framework/hooks/shared/heavy-command.js +4 -12
  61. package/framework/hooks/shared/hook-response.js +8 -83
  62. package/framework/hooks/shared/morph-cli.js +8 -52
  63. package/framework/hooks/shared/payload-utils.js +10 -132
  64. package/framework/hooks/shared/phase-utils.js +14 -72
  65. package/framework/hooks/shared/redact-secrets.js +4 -15
  66. package/framework/hooks/shared/reparse-scan.js +6 -66
  67. package/framework/hooks/shared/slice-progress.js +99 -0
  68. package/framework/hooks/shared/source-files.js +4 -17
  69. package/framework/hooks/shared/spawn-command.js +23 -0
  70. package/framework/hooks/shared/state-reader.js +51 -247
  71. package/framework/hooks/shared/state-writer.js +9 -54
  72. package/framework/hooks/shared/stdin-reader.js +1 -11
  73. package/framework/hooks/shared/telemetry-logger.js +7 -67
  74. package/framework/hooks/shared/transcript-usage.js +7 -29
  75. package/framework/hooks/shared/worktree-root.js +7 -29
  76. package/framework/mcp/registry.json +3 -4
  77. package/framework/phases.json +23 -39
  78. package/framework/rules/csharp-standards.md +1 -1
  79. package/framework/rules/frontend-standards.md +1 -0
  80. package/framework/rules/hangfire-standards.md +1 -1
  81. package/framework/rules/neon-standards.md +1 -1
  82. package/framework/schemas/config.schema.json +14 -9
  83. package/framework/schemas/feature.schema.json +15 -50
  84. package/framework/skills/README.md +3 -4
  85. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  86. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  87. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  88. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  89. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  90. package/framework/skills/core/morph-media/SKILL.md +1 -1
  91. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  92. package/framework/skills/core/morph-review/SKILL.md +111 -136
  93. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  94. package/framework/skills/core/morph-uiux/SKILL.md +94 -152
  95. package/framework/standards/STANDARDS.json +109 -1219
  96. package/framework/standards/STANDARDS.meta.json +1319 -0
  97. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  98. package/framework/standards/ai-agents/agent-session.md +1 -1
  99. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  100. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  101. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  102. package/framework/standards/ai-agents/guardrails.md +1 -1
  103. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  104. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  105. package/framework/standards/ai-agents/production.md +1 -1
  106. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  107. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  108. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  109. package/framework/standards/ai-agents/setup.md +3 -3
  110. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  111. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  112. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  113. package/framework/standards/ai-agents/workflows.md +1 -1
  114. package/framework/standards/architecture/code-comments.md +7 -7
  115. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  116. package/framework/standards/backend/api/validation.md +1 -1
  117. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  118. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  119. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  120. package/framework/standards/data/nosql/cache/redis.md +1 -1
  121. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  122. package/framework/standards/frontend/admin-app/forms.md +1 -1
  123. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  124. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  125. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  126. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  127. package/framework/templates/CLAUDE.md.core.template +3 -3
  128. package/framework/templates/README.md +2 -3
  129. package/framework/templates/REGISTRY.json +15 -179
  130. package/framework/templates/docs/plan.md +72 -0
  131. package/framework/templates/docs/ui-mockups.md +4 -0
  132. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  134. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  135. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  136. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  137. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  138. package/package.json +1 -1
  139. package/src/commands/context/context-refresh.js +0 -16
  140. package/src/commands/dashboard/dashboard.js +2 -8
  141. package/src/commands/e2e/e2e.js +18 -105
  142. package/src/commands/graph/affected.js +0 -14
  143. package/src/commands/graph/build.js +11 -50
  144. package/src/commands/graph/explain.js +0 -16
  145. package/src/commands/graph/status.js +1 -12
  146. package/src/commands/mcp/mcp-setup.js +4 -62
  147. package/src/commands/observability/cost.js +17 -111
  148. package/src/commands/observability/telemetry.js +1 -14
  149. package/src/commands/project/branch.js +4 -45
  150. package/src/commands/project/claim.js +4 -48
  151. package/src/commands/project/doctor-procs.js +1 -1
  152. package/src/commands/project/doctor.js +53 -307
  153. package/src/commands/project/env.js +1 -27
  154. package/src/commands/project/fleet.js +6 -35
  155. package/src/commands/project/heavy.js +3 -3
  156. package/src/commands/project/init.js +8 -68
  157. package/src/commands/project/install-plugin-cmd.js +0 -1
  158. package/src/commands/project/setup-infra-cmd.js +2 -1
  159. package/src/commands/project/stack-scan.js +58 -0
  160. package/src/commands/project/standards.js +65 -72
  161. package/src/commands/project/status.js +28 -121
  162. package/src/commands/project/update.js +31 -223
  163. package/src/commands/project/worktree.js +109 -883
  164. package/src/commands/scaffold/scaffold.js +0 -25
  165. package/src/commands/state/advance.js +65 -120
  166. package/src/commands/state/approve.js +87 -229
  167. package/src/commands/state/archive.js +23 -139
  168. package/src/commands/state/auto-approval.js +65 -0
  169. package/src/commands/state/create.js +49 -109
  170. package/src/commands/state/delete.js +11 -63
  171. package/src/commands/state/finish.js +105 -494
  172. package/src/commands/state/gate-check.js +74 -183
  173. package/src/commands/state/gate-decision.js +8 -42
  174. package/src/commands/state/handoff.js +4 -31
  175. package/src/commands/state/list.js +21 -91
  176. package/src/commands/state/report-check.js +134 -338
  177. package/src/commands/state/retype.js +12 -28
  178. package/src/commands/validation/index.js +0 -3
  179. package/src/commands/validation/mutate.js +79 -19
  180. package/src/commands/validation/plan-check.js +81 -0
  181. package/src/commands/validation/validate-feature.js +52 -216
  182. package/src/commands/validation/verify.js +45 -160
  183. package/src/core/paths/output-schema.js +9 -155
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +2 -30
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +32 -395
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +21 -0
  191. package/src/core/work-types.js +10 -45
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
  194. package/src/dashboard/app/components/CommandPalette.jsx +4 -11
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +3 -9
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/pages/Activity.jsx +1 -1
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
  211. package/src/dashboard/app/pages/Features.jsx +9 -16
  212. package/src/dashboard/app/pages/Guide.jsx +2 -3
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +14 -83
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +1 -7
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +92 -113
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +4 -7
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/lib/ai/ai-checks.js +7 -52
  233. package/src/lib/ai/ai-project-scan.js +14 -108
  234. package/src/lib/ai/code-audit.js +7 -63
  235. package/src/lib/ai/pin-drift.js +8 -51
  236. package/src/lib/ai/registry-audit.js +11 -90
  237. package/src/lib/ci/ci-verify.js +14 -13
  238. package/src/lib/ci/github-ci.js +9 -37
  239. package/src/lib/ci/workflow-scaffold.js +2 -10
  240. package/src/lib/detectors/claude-config-detector.js +13 -74
  241. package/src/lib/detectors/index.js +8 -59
  242. package/src/lib/e2e/compose-env.js +5 -66
  243. package/src/lib/e2e/stack-teardown.js +6 -40
  244. package/src/lib/generators/architecture-scanner.js +18 -156
  245. package/src/lib/generators/handoff-generator.js +37 -112
  246. package/src/lib/generators/scaffold-generator.js +5 -101
  247. package/src/lib/generators/settings-generator.js +11 -122
  248. package/src/lib/git/branch-classifier.js +11 -84
  249. package/src/lib/git/default-branch.js +14 -169
  250. package/src/lib/git/evidence-staleness.js +2 -1
  251. package/src/lib/git/gh.js +2 -17
  252. package/src/lib/git/pending-prs.js +6 -37
  253. package/src/lib/git/pr-files.js +3 -28
  254. package/src/lib/git/pr-index.js +3 -34
  255. package/src/lib/git/tree-fingerprint.js +17 -17
  256. package/src/lib/git/tree-state.js +6 -33
  257. package/src/lib/graph/god-nodes.js +2 -22
  258. package/src/lib/graph/graph-health.js +3 -24
  259. package/src/lib/graph/graph-loader.js +5 -56
  260. package/src/lib/graph/graphify-runtime.js +7 -37
  261. package/src/lib/graph/graphifyignore.js +8 -74
  262. package/src/lib/graph/sanitize.js +16 -136
  263. package/src/lib/heavy/cleanup.js +2 -6
  264. package/src/lib/installers/effects.js +8 -93
  265. package/src/lib/installers/mcp-installer.js +9 -169
  266. package/src/lib/installers/memory-installer.js +8 -33
  267. package/src/lib/installers/templates-reconcile.js +5 -60
  268. package/src/lib/mutation/commands.js +3 -2
  269. package/src/lib/mutation/dotnet-introspection.js +281 -0
  270. package/src/lib/mutation/plan.js +14 -6
  271. package/src/lib/mutation/runner.js +1 -1
  272. package/src/lib/mutation/subset.js +47 -13
  273. package/src/lib/plan/artifact-vocabulary.js +40 -0
  274. package/src/lib/plan/checks.js +208 -0
  275. package/src/lib/plan/plan-parser.js +204 -0
  276. package/src/lib/plan/plan-template.js +18 -0
  277. package/src/lib/projects/registry.js +7 -44
  278. package/src/lib/review-reports.js +204 -0
  279. package/src/lib/risk-detector.js +131 -274
  280. package/src/lib/specs/adr-index.js +38 -85
  281. package/src/lib/specs/design-seed.js +18 -0
  282. package/src/lib/specs/frontmatter.js +31 -0
  283. package/src/lib/specs/gfm-table.js +11 -0
  284. package/src/lib/specs/revocations.js +31 -102
  285. package/src/lib/specs/screen-checklist.js +56 -132
  286. package/src/lib/stack/dotnet-build-ignores.js +43 -0
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +260 -0
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +37 -0
  291. package/src/lib/standards/sections.js +47 -0
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +23 -179
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -313
  296. package/src/lib/tasks/test-runner.js +107 -447
  297. package/src/lib/telemetry/metrics.js +13 -103
  298. package/src/lib/telemetry/telemetry-reader.js +13 -238
  299. package/src/lib/telemetry/verify-event.js +4 -34
  300. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  301. package/src/lib/validators/code-budget/index.js +0 -7
  302. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
  303. package/src/lib/validators/contracts/index.js +0 -6
  304. package/src/lib/validators/nextjs/index.js +0 -5
  305. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  306. package/src/lib/validators/packages/index.js +0 -6
  307. package/src/lib/validators/packages/package-validator.js +6 -56
  308. package/src/lib/validators/reuse/index.js +0 -8
  309. package/src/lib/validators/reuse/reuse-validator.js +24 -137
  310. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  311. package/src/lib/validators/traceability-check.js +54 -156
  312. package/src/lib/validators/validation-runner.js +20 -125
  313. package/src/lib/validators/verify-runner.js +152 -575
  314. package/src/lib/work-type-detector.js +5 -67
  315. package/src/lib/worktree/docker-audit.js +17 -137
  316. package/src/lib/worktree/env-allocator.js +10 -98
  317. package/src/lib/worktree/feature-truth.js +12 -115
  318. package/src/lib/worktree/finish-backup.js +32 -242
  319. package/src/lib/worktree/fleet-scanner.js +31 -126
  320. package/src/lib/worktree/ignored-triage.js +15 -91
  321. package/src/lib/worktree/vscode-settings.js +2 -3
  322. package/src/lib/worktree/worktree-audit.js +21 -213
  323. package/src/lib/worktree/worktree-include.js +3 -5
  324. package/src/scripts/global-install.js +3 -9
  325. package/src/scripts/install-plugin.js +1 -13
  326. package/src/scripts/setup-infra.js +17 -138
  327. package/src/utils/agents-installer.js +92 -272
  328. package/src/utils/banner.js +4 -25
  329. package/src/utils/claude-md-injector.js +1 -37
  330. package/src/utils/claude-settings-manager.js +1 -87
  331. package/src/utils/file-copier.js +32 -90
  332. package/src/utils/hooks-installer.js +26 -296
  333. package/src/utils/skills-installer.js +4 -102
  334. package/src/utils/version-checker.js +4 -83
  335. package/framework/commands/morph-proposal.md +0 -57
  336. package/framework/evals/trace-schema.md +0 -67
  337. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  338. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  339. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  340. package/framework/hooks/shared/tasks-validator.js +0 -111
  341. package/framework/rules/morph-task-tracking.md +0 -54
  342. package/framework/rules/morph-workflow.md +0 -244
  343. package/framework/schemas/tasks.schema.json +0 -79
  344. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  345. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  346. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  347. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  348. package/framework/templates/docs/proposal.md +0 -192
  349. package/framework/templates/docs/spec.md +0 -57
  350. package/framework/templates/docs/ui-components.md +0 -52
  351. package/framework/templates/docs/ui-design-system.md +0 -76
  352. package/framework/templates/docs/ui-flows.md +0 -167
  353. package/framework/templates/examples/spec-examples.md +0 -91
  354. package/framework/templates/feature/decisions.md +0 -124
  355. package/framework/templates/feature/recap.md +0 -145
  356. package/framework/templates/tasks.template.json +0 -17
  357. package/src/commands/state/usecase.js +0 -143
  358. package/src/commands/tasks/dag.js +0 -181
  359. package/src/commands/utils/index.js +0 -3
  360. package/src/lib/agents/output-collector.js +0 -76
  361. package/src/lib/detectors/design-system-detector.js +0 -188
  362. package/src/lib/evaluator-report.js +0 -104
  363. package/src/lib/generators/design-system-generator.js +0 -508
  364. package/src/lib/specs/acervo-format.js +0 -70
  365. package/src/lib/specs/acervo-lifecycle.js +0 -264
  366. package/src/lib/specs/done-criteria-check.js +0 -200
  367. package/src/lib/specs/usecase-check.js +0 -301
  368. package/src/lib/specs/usecase-promoter.js +0 -301
  369. package/src/lib/specs/usecase-registry.js +0 -237
  370. package/src/lib/tasks/dispatch-planner.js +0 -319
  371. package/src/lib/tasks/dotnet-test-introspection.js +0 -632
  372. package/src/lib/tasks/mermaid-renderer.js +0 -132
  373. package/src/lib/tasks/task-classifier.js +0 -30
  374. package/src/lib/tasks/task-parser.js +0 -209
  375. package/src/lib/tasks/tasks-json-parser.js +0 -296
  376. package/src/lib/tasks/test-scope.js +0 -597
  377. package/src/utils/agent-id-mapper.js +0 -11
@@ -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
- * proposal → only 0-proposal/
10
- * uiux → only 1-design/
11
- * plan → only 2-plan/
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,31 +9,20 @@ 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. proposal-approval unlocks both
34
- * the optional `1-design/` and `2-plan/` (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
- '1-design': 'proposal',
38
- '2-plan': 'proposal',
14
+ '1-design': 'plan',
39
15
  '3-implement': 'plan',
40
16
  '4-review': 'plan',
41
17
  };
18
+
19
+ const ALWAYS_WRITABLE_DIR = '2-plan';
42
20
  import { blockResult, runStandalone } from '../../shared/hook-response.js';
43
21
  import { logHookActivity } from '../../shared/activity-logger.js';
44
22
  import { getFilePath, resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
45
23
 
46
- /**
47
- * @param {object} payload - parsed PreToolUse stdin payload
48
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
49
- * Never prints, never exits — see runStandalone()/chain.js at the bottom.
50
- */
51
24
  export async function run(payload) {
52
25
  try {
53
- // Amend mode: bypass phase write enforcement for legitimate corrections
54
26
  if (process.env.MORPH_AMEND_PHASE) {
55
27
  console.error(`[morph-spec] AMEND MODE: bypassing phase protection for phase '${process.env.MORPH_AMEND_PHASE}'`);
56
28
  return null;
@@ -61,15 +33,9 @@ export async function run(payload) {
61
33
  const filePath = getFilePath(payload);
62
34
  if (!filePath) return null;
63
35
 
64
- // Only check files inside .morph/features/
65
36
  if (!isFeaturePath(filePath)) return null;
66
37
 
67
- // Resolve the tree that OWNS this file, NOT the tree the session sits in.
68
- // A session rooted in the primary repo may legitimately write to a feature
69
- // living in a secondary worktree; reading the session's `.morph/` there
70
- // reported the ROOT's phase for the WORKTREE's file, and blocked a legal
71
- // `2-plan/` write with "not allowed during PROPOSAL phase". `stateExists`
72
- // 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,37 +45,27 @@ 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;
52
+ if (targetDir === ALWAYS_WRITABLE_DIR) return null;
87
53
 
88
54
  const allowedDir = PHASE_DIRS[phase];
89
- if (!allowedDir) return null; // Unknown phase — allow
55
+ if (!allowedDir) return null;
90
56
 
91
57
  if (targetDir !== allowedDir) {
92
- // Allow editing already-existing files from previous phases
93
58
  const absolutePath = resolveTargetPath(filePath, root);
94
59
  if (existsSync(absolutePath)) {
95
60
  logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
96
- return null; // editing existing file — allow
61
+ return null;
97
62
  }
98
63
 
99
- // Gate-aware forward transition: allow creating the NEXT phase's first
100
- // artifact as soon as the gate authorizing that phase is approved. Without
101
- // this, the first file of a new phase can never be written (the phase only
102
- // advances once that file exists) — the deadlock this hook used to create.
103
64
  const entryGate = PHASE_DIR_ENTRY_GATE[targetDir];
104
65
  if (entryGate && isGateApproved(featureName, entryGate, root)) {
105
- // uiux is OPTIONAL — proposal approval alone is enough to enter
106
- // 2-plan/ for a feature that never took the design branch (no
107
- // 1-design/ dir), exactly as before this check. But once a feature
108
- // HAS opted into uiux (1-design/ exists), that gate is not
109
- // decorative: 2-plan/ must wait for it too, or a feature can skip
110
- // straight from an approved proposal into planning with its design
111
- // gate silently left pending.
112
- const tookDesignBranch = targetDir === '2-plan'
66
+ // uiux is optional, but once 1-design/ exists its gate is not
67
+ // decorative: implementation waits for it too.
68
+ const tookDesignBranch = targetDir !== '1-design'
113
69
  && existsSync(resolveTargetPath(`.morph/features/${featureName}/1-design`, root));
114
70
 
115
71
  if (!tookDesignBranch || isGateApproved(featureName, 'uiux', root)) {
@@ -119,18 +75,15 @@ export async function run(payload) {
119
75
 
120
76
  logHookActivity('enforce-phase-writes', 'PreToolUse', 'blocked');
121
77
  return blockResult(
122
- `MORPH-SPEC: Writing to '2-plan/' is not allowed yet.\n` +
78
+ `MORPH-SPEC: Writing to '${targetDir}/' is not allowed yet.\n` +
123
79
  `This feature entered the uiux branch (1-design/ exists) and the 'uiux' gate is still pending.\n\n` +
124
- `Approve the design gate first, then re-enter plan:\n` +
80
+ `Approve the design gate first, then move on to implementation:\n` +
125
81
  ` morph-spec approve ${featureName} uiux\n` +
126
82
  ` morph-spec advance ${featureName}`
127
83
  );
128
84
  }
129
85
 
130
86
  const phaseLabel = phase.toUpperCase();
131
- // Name the tree the verdict came from whenever it is NOT the session's own.
132
- // The phase read is per-tree, so "which .morph/ said proposal?" is the first
133
- // question anyone debugging this asks.
134
87
  const treeNote = root !== process.cwd().replace(/\\/g, '/')
135
88
  ? `\n(phase read from the tree that owns the file: ${root})`
136
89
  : '';
@@ -147,7 +100,6 @@ export async function run(payload) {
147
100
  logHookActivity('enforce-phase-writes', 'PreToolUse', 'ok');
148
101
  return null;
149
102
  } catch {
150
- // Fail-open
151
103
  return null;
152
104
  }
153
105
  }
@@ -1,31 +1,9 @@
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';
6
+ import { gateFromQuestion } from '../../shared/gate-question.js';
29
7
  import { logHookActivity } from '../../shared/activity-logger.js';
30
8
  import { resolveMorphRoot, resolveTargetPath } from '../../shared/payload-utils.js';
31
9
  import { locateFeatureForQuestion } from '../../shared/feature-locator.js';
@@ -34,17 +12,12 @@ function projectDir() {
34
12
  return process.env.CLAUDE_PROJECT_DIR || process.cwd();
35
13
  }
36
14
 
37
- /** Feature slug from a `.morph/features/{name}/feature.json` path. */
38
15
  function featureFromStatePath(filePath) {
39
16
  const fp = String(filePath ?? '').replace(/\\/g, '/');
40
17
  return (fp.match(/\.morph\/features\/([^/]+)\/feature\.json$/) ?? [])[1] ?? null;
41
18
  }
42
19
 
43
- /**
44
- * The tree owning an edited gate-state file. `resolveMorphRoot` covers the v5
45
- * per-feature `feature.json`; the legacy `.morph/state.json` has no feature
46
- * segment to anchor on, so strip its own suffix instead.
47
- */
20
+ // The legacy `.morph/state.json` has no feature segment to anchor on, so strip its own suffix.
48
21
  function rootOfGateStateFile(filePath, fallback) {
49
22
  const abs = resolveTargetPath(filePath, fallback);
50
23
  const suffix = '/.morph/state.json';
@@ -52,7 +25,6 @@ function rootOfGateStateFile(filePath, fallback) {
52
25
  return resolveMorphRoot(abs, fallback);
53
26
  }
54
27
 
55
- /** Every question/header text this AskUserQuestion call surfaced. */
56
28
  function askedTexts(input) {
57
29
  const texts = [];
58
30
  const questions = input.tool_input?.questions;
@@ -68,45 +40,26 @@ function askedTexts(input) {
68
40
  return texts;
69
41
  }
70
42
 
71
- /** Map a gate question's text to a gate id. Mirrors state-sync.js. */
72
- function gateFromQuestion(question) {
73
- if (typeof question !== 'string') return null;
74
- const lower = question.toLowerCase();
75
- if (lower.includes('gate 1') || lower.includes('proposta')) return 'proposal';
76
- if (lower.includes('gate 2') || lower.includes('plano') || lower.includes('plan')) return 'plan';
77
- if (lower.includes('gate 3') || lower.includes('review')) return 'review';
78
- return null;
79
- }
80
-
81
43
  function getDecision(state, feature, gate) {
82
44
  const d = state.features?.[feature]?.gateDecisions?.[gate];
83
45
  if (!d || typeof d.pause !== 'boolean') return null;
84
46
  return d;
85
47
  }
86
48
 
87
- /**
88
- * The LLM just asked a gate question. If the persisted decision said this
89
- * gate should auto-pass (pause:false), the human was interrupted needlessly.
90
- */
91
49
  function checkAskUserQuestion(input, sessionRoot) {
92
50
  for (const text of askedTexts(input)) {
93
51
  const gate = gateFromQuestion(text);
94
52
  if (!gate) continue;
95
53
 
96
- // Check the decision of the feature the question NAMES, in its own tree.
97
- // Reading the session tree's first in_progress feature made this guard
98
- // compare a gate against an unrelated feature's gateDecisions — a warning
99
- // about the wrong thing is worse than silence, so an unattributable
100
- // question is skipped rather than guessed at (state-sync already tells the
101
- // 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.
102
55
  const located = locateFeatureForQuestion(text, sessionRoot);
103
56
  if (!located) continue;
104
57
  const { name: feature, root: cwd } = located;
105
- if (!stateExists(cwd)) continue; // fail-open
58
+ if (!stateExists(cwd)) continue;
106
59
 
107
60
  const state = loadState(cwd);
108
61
  const decision = getDecision(state, feature, gate);
109
- if (!decision) continue; // no persisted decision → nothing to verify
62
+ if (!decision) continue;
110
63
  if (decision.pause === false) {
111
64
  logHookActivity('gate-guard', 'PostToolUse', 'divergent', cwd);
112
65
  return contextResult(
@@ -119,34 +72,24 @@ function checkAskUserQuestion(input, sessionRoot) {
119
72
  'PostToolUse'
120
73
  );
121
74
  }
122
- // The decision said pause:true and the LLM did pause — coherent.
123
75
  logHookActivity('gate-guard', 'PostToolUse', 'coherent', cwd);
124
76
  return null;
125
77
  }
126
78
  return null;
127
79
  }
128
80
 
129
- /** True for edits to .morph/state.json (legacy) or a per-feature feature.json (v5). */
130
81
  function isGateStateFile(filePath) {
131
82
  const fp = String(filePath ?? '').replace(/\\/g, '/');
132
83
  if (fp.endsWith('.morph/state.json')) return true;
133
84
  return fp.includes('.morph/features/') && fp.endsWith('/feature.json');
134
85
  }
135
86
 
136
- /**
137
- * The LLM just edited gate state (state.json legacy, or the authoritative
138
- * feature.json in v5). If a gate flipped to approvedBy:"auto" but the
139
- * persisted decision said pause:true, a required human pause was skipped.
140
- */
141
87
  function checkStateEdit(input, sessionRoot) {
142
88
  const filePath = input.tool_input?.file_path;
143
89
  if (!isGateStateFile(filePath)) return null;
144
90
 
145
- // The edited file names its own tree — and, for the v5 per-feature
146
- // feature.json, its own feature. Only the legacy `.morph/state.json` shape
147
- // still needs a most-recently-updated fallback within that tree.
148
91
  const cwd = rootOfGateStateFile(filePath, sessionRoot);
149
- if (!stateExists(cwd)) return null; // fail-open
92
+ if (!stateExists(cwd)) return null;
150
93
  const state = loadState(cwd);
151
94
  if (!state) return null;
152
95
  const feature = featureFromStatePath(filePath) || getActiveFeature(cwd)?.name;
@@ -154,7 +97,7 @@ function checkStateEdit(input, sessionRoot) {
154
97
 
155
98
  let checked = false;
156
99
  const gates = state.features?.[feature]?.approvalGates ?? {};
157
- for (const gate of ['proposal', 'plan', 'review']) {
100
+ for (const gate of ['plan', 'uiux', 'review']) {
158
101
  const g = gates[gate];
159
102
  const autoApproved = g?.approved === true && g?.approvedBy === 'auto';
160
103
  if (!autoApproved) continue;
@@ -177,22 +120,12 @@ function checkStateEdit(input, sessionRoot) {
177
120
  return null;
178
121
  }
179
122
 
180
- /**
181
- * @param {object} payload - parsed PostToolUse stdin payload (Write|Edit|AskUserQuestion)
182
- * @returns {Promise<object|null>} a hookSpecificOutput warning on a
183
- * trust-mode divergence, or null (pass/silent).
184
- */
185
123
  export async function run(payload) {
186
- // Starting point only — each branch resolves the tree it actually inspects
187
- // (from the edited path, or from the feature the question names).
188
124
  const sessionRoot = projectDir();
189
125
 
190
126
  const input = payload || {};
191
127
  const toolName = input.tool_name;
192
128
 
193
- // Short-circuit before loading state: a Write/Edit only matters if it
194
- // touched gate state (state.json legacy, or the v5 per-feature
195
- // feature.json). Skip the disk read for unrelated edits.
196
129
  if (toolName === 'Edit' || toolName === 'Write') {
197
130
  if (!isGateStateFile(input.tool_input?.file_path)) return null;
198
131
  } else if (toolName !== 'AskUserQuestion') {
@@ -202,9 +135,7 @@ export async function run(payload) {
202
135
  try {
203
136
  if (toolName === 'AskUserQuestion') return checkAskUserQuestion(input, sessionRoot);
204
137
  return checkStateEdit(input, sessionRoot);
205
- } catch {
206
- // fail-open
207
- }
138
+ } catch {}
208
139
  return null;
209
140
  }
210
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;
@@ -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
  }
@@ -4,13 +4,40 @@ import { join, dirname } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { blockResult, runStandalone } from '../../shared/hook-response.js';
6
6
  import { stateExists } from '../../shared/state-reader.js';
7
+ import { tokenize } from '../../shared/command-tokens.js';
7
8
 
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const SEPARATOR = /^[;&|]+$/;
11
+ const executableName = (token) => token.replace(/\\/g, '/').split('/').pop().toLowerCase().replace(/\.exe$/, '');
12
+
13
+ const hasFlag = (token, flag) => (flag.startsWith('--') || token.startsWith('--')
14
+ ? token === flag
15
+ : /^-[A-Za-z]+$/.test(token) && token.includes(flag.slice(1)));
9
16
 
10
17
  /**
11
- * @param {object} payload - parsed PreToolUse stdin payload (Bash matcher)
12
- * @returns {Promise<object|null>} `{decision:'block',reason}` or null (pass).
18
+ * A quoted argument is one token, never the executable: mentioning the command in a message is not calling it.
19
+ * `args` match a whole token; each group in `flags` (case-sensitive, clusters like `-rf` count) must appear in the same command.
13
20
  */
21
+ export function matchesExecutableRule(command, rule) {
22
+ const exe = rule.executable.toLowerCase();
23
+ const args = new Set(rule.args.map((a) => a.toLowerCase()));
24
+ const flagGroups = rule.flags || [];
25
+ const tokens = tokenize(command);
26
+ for (let i = 0; i < tokens.length; i++) {
27
+ if (executableName(tokens[i].replace(/^[;&|(]+/, '')) !== exe) continue;
28
+ const seen = [];
29
+ let argHit = false;
30
+ for (let j = i + 1; j < tokens.length && !SEPARATOR.test(tokens[j]); j++) {
31
+ const raw = tokens[j].replace(/[;&|)]+$/, '');
32
+ if (args.has(raw.toLowerCase())) argHit = true;
33
+ seen.push(raw);
34
+ if (tokens[j] !== raw) break;
35
+ }
36
+ if (argHit && flagGroups.every((group) => seen.some((t) => group.some((flag) => hasFlag(t, flag))))) return true;
37
+ }
38
+ return false;
39
+ }
40
+
14
41
  export async function run(payload) {
15
42
  try {
16
43
  if (!stateExists()) return null;
@@ -26,6 +53,13 @@ export async function run(payload) {
26
53
  }
27
54
  }
28
55
 
56
+ for (const rule of denylist.executableArgs || []) {
57
+ if (matchesExecutableRule(command, rule)) {
58
+ const shown = [rule.executable, ...(rule.flags || []).map((group) => group[0]), rule.args[0]].join(' ');
59
+ return blockResult(`Comando bloqueado pela denylist: \`${shown}\` ${rule.reason}.`);
60
+ }
61
+ }
62
+
29
63
  return null;
30
64
  } catch {
31
65
  return null;