wicked-garden 12.27.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 (640) hide show
  1. package/.claude-plugin/archetypes.json +166 -0
  2. package/.claude-plugin/components.json +75 -0
  3. package/.claude-plugin/marketplace.json +15 -0
  4. package/.claude-plugin/plugin.json +174 -0
  5. package/.claude-plugin/specialist.json +48 -0
  6. package/.claude-plugin/stack-registry.json +96 -0
  7. package/AGENTS.md +13 -0
  8. package/CHANGELOG.md +712 -0
  9. package/CONTRIBUTING.md +99 -0
  10. package/ETHOS.md +83 -0
  11. package/LICENSE +21 -0
  12. package/README.md +115 -0
  13. package/WICKED_GARDEN_BUS_EVENTS.md +149 -0
  14. package/agents/agentic/architect.md +449 -0
  15. package/agents/agentic/performance-analyst.md +702 -0
  16. package/agents/agentic/safety-reviewer.md +662 -0
  17. package/agents/crew/implementer.md +163 -0
  18. package/agents/crew/researcher.md +94 -0
  19. package/agents/crew/reviewer.md +184 -0
  20. package/agents/data/data-engineer.md +227 -0
  21. package/agents/engineering/api-documentarian.md +440 -0
  22. package/agents/engineering/migration-engineer.md +301 -0
  23. package/agents/engineering/solution-architect.md +208 -0
  24. package/agents/jam/brainstorm-facilitator.md +297 -0
  25. package/agents/jam/council.md +322 -0
  26. package/agents/jam/quick-facilitator.md +85 -0
  27. package/agents/persona/persona-agent.md +121 -0
  28. package/agents/platform/compliance-officer.md +266 -0
  29. package/agents/platform/privacy-expert.md +496 -0
  30. package/agents/platform/security-engineer.md +287 -0
  31. package/agents/product/a11y-expert.md +323 -0
  32. package/agents/product/requirements-analyst.md +238 -0
  33. package/agents/product/ui-reviewer.md +180 -0
  34. package/agents/product/ux-designer.md +199 -0
  35. package/agents/product/value-strategist.md +269 -0
  36. package/agents/qe/semantic-reviewer.md +78 -0
  37. package/commands/agentic/audit.md +24 -0
  38. package/commands/agentic/design.md +24 -0
  39. package/commands/agentic/frameworks.md +25 -0
  40. package/commands/agentic/review.md +27 -0
  41. package/commands/archetype/build.md +12 -0
  42. package/commands/archetype/decide.md +12 -0
  43. package/commands/archetype/explore.md +12 -0
  44. package/commands/archetype/incident.md +12 -0
  45. package/commands/archetype/migrate.md +12 -0
  46. package/commands/archetype/modernize.md +12 -0
  47. package/commands/archetype/review.md +14 -0
  48. package/commands/archetype/ship.md +12 -0
  49. package/commands/archetype/specify.md +12 -0
  50. package/commands/archetype/triage.md +12 -0
  51. package/commands/compile.md +37 -0
  52. package/commands/data/analyze.md +18 -0
  53. package/commands/data/data.md +21 -0
  54. package/commands/data/ml.md +18 -0
  55. package/commands/data/ontology.md +22 -0
  56. package/commands/data/pipeline.md +17 -0
  57. package/commands/deliberate.md +181 -0
  58. package/commands/engineering/add-field.md +77 -0
  59. package/commands/engineering/apply.md +58 -0
  60. package/commands/engineering/arch.md +26 -0
  61. package/commands/engineering/debug.md +27 -0
  62. package/commands/engineering/docs.md +27 -0
  63. package/commands/engineering/new-generator.md +257 -0
  64. package/commands/engineering/patch-plan.md +59 -0
  65. package/commands/engineering/plan.md +29 -0
  66. package/commands/engineering/remove.md +48 -0
  67. package/commands/engineering/rename.md +44 -0
  68. package/commands/engineering/review.md +32 -0
  69. package/commands/help.md +128 -0
  70. package/commands/install.md +118 -0
  71. package/commands/intent.md +54 -0
  72. package/commands/jam/brainstorm.md +37 -0
  73. package/commands/jam/council.md +49 -0
  74. package/commands/jam/quick.md +21 -0
  75. package/commands/jam/revisit.md +79 -0
  76. package/commands/persona/as.md +136 -0
  77. package/commands/persona/define.md +24 -0
  78. package/commands/persona/list.md +118 -0
  79. package/commands/platform/actions.md +20 -0
  80. package/commands/platform/assert.md +18 -0
  81. package/commands/platform/audit.md +23 -0
  82. package/commands/platform/compliance.md +20 -0
  83. package/commands/platform/gh.md +22 -0
  84. package/commands/platform/health.md +22 -0
  85. package/commands/platform/incident.md +23 -0
  86. package/commands/platform/infra.md +24 -0
  87. package/commands/platform/peer-health.md +103 -0
  88. package/commands/platform/plugin-health.md +58 -0
  89. package/commands/platform/security.md +38 -0
  90. package/commands/platform/toolchain.md +96 -0
  91. package/commands/platform/traces.md +20 -0
  92. package/commands/product/a11y.md +19 -0
  93. package/commands/product/acceptance.md +19 -0
  94. package/commands/product/align.md +20 -0
  95. package/commands/product/analyze.md +23 -0
  96. package/commands/product/elicit.md +19 -0
  97. package/commands/product/listen.md +24 -0
  98. package/commands/product/mockup.md +20 -0
  99. package/commands/product/screenshot.md +19 -0
  100. package/commands/product/strategy.md +40 -0
  101. package/commands/product/synthesize.md +23 -0
  102. package/commands/product/ux-review.md +42 -0
  103. package/commands/product/ux.md +20 -0
  104. package/commands/product/visual-direction.md +34 -0
  105. package/commands/prove.md +47 -0
  106. package/commands/report-issue.md +243 -0
  107. package/commands/reset.md +144 -0
  108. package/commands/search/blast-radius.md +58 -0
  109. package/commands/search/hotspots.md +55 -0
  110. package/commands/search/index.md +38 -0
  111. package/commands/search/lineage.md +47 -0
  112. package/commands/search/service-map.md +79 -0
  113. package/commands/setup.md +363 -0
  114. package/commands/smaht/briefing.md +200 -0
  115. package/commands/smaht/events-import.md +95 -0
  116. package/commands/smaht/state.md +19 -0
  117. package/commands/where-am-i.md +58 -0
  118. package/docs/README.md +32 -0
  119. package/docs/adr/0001-code-relationship-graph-engine.md +116 -0
  120. package/docs/adr/0002-major-cleanup-keep-cut.md +91 -0
  121. package/docs/adr/0003-peers-gate-required-layers-optin.md +41 -0
  122. package/docs/adr/0004-code-graph-moves-to-wicked-brain.md +47 -0
  123. package/docs/brain-chunk-format.md +135 -0
  124. package/docs/compiler.md +95 -0
  125. package/docs/daemon-api.md +70 -0
  126. package/docs/domains.md +197 -0
  127. package/docs/getting-started.md +208 -0
  128. package/docs/plans/2026-06-08-wicked-loom-conduct.md +1337 -0
  129. package/docs/plans/2026-06-08-wicked-loom-contract.md +638 -0
  130. package/docs/plans/2026-06-08-wicked-loom-cutover.md +1300 -0
  131. package/docs/plans/2026-06-08-wicked-loom-expand-compose.md +843 -0
  132. package/docs/plans/2026-06-08-wicked-loom-flow-cutover.md +403 -0
  133. package/docs/plans/2026-06-10-codegraph-to-brain-phase1a-graph-core.md +838 -0
  134. package/docs/plans/2026-06-10-codegraph-to-brain-phase1b-injected-edges.md +44 -0
  135. package/docs/required-peers.md +60 -0
  136. package/docs/specs/2026-06-08-wicked-loom-extraction.md +225 -0
  137. package/docs/specs/2026-06-10-codegraph-to-brain-migration.md +168 -0
  138. package/docs/v11/archetypes.md +231 -0
  139. package/hooks/hooks.json +275 -0
  140. package/hooks/scripts/bootstrap.py +1797 -0
  141. package/hooks/scripts/invoke.py +62 -0
  142. package/hooks/scripts/notification.py +166 -0
  143. package/hooks/scripts/permission_request.py +223 -0
  144. package/hooks/scripts/post_tool.py +1374 -0
  145. package/hooks/scripts/pre_compact.py +272 -0
  146. package/hooks/scripts/pre_tool.py +1278 -0
  147. package/hooks/scripts/prompt_submit.py +1109 -0
  148. package/hooks/scripts/session_end.py +95 -0
  149. package/hooks/scripts/stop.py +624 -0
  150. package/hooks/scripts/subagent_lifecycle.py +464 -0
  151. package/hooks/scripts/subscribers/on_gate_decided.py +84 -0
  152. package/hooks/scripts/task_completed.py +322 -0
  153. package/hooks/scripts/worktree_guard.py +148 -0
  154. package/hooks/subscriptions/on_gate_decided.json +9 -0
  155. package/install.mjs +128 -0
  156. package/package.json +41 -0
  157. package/pyproject.toml +59 -0
  158. package/schemas/README.md +32 -0
  159. package/schemas/assert_contracts.json +37 -0
  160. package/schemas/health_probe.json +46 -0
  161. package/schemas/plugin_status.json +37 -0
  162. package/scripts/_adapters/__init__.py +166 -0
  163. package/scripts/_adapters/jira_adapter.py +18 -0
  164. package/scripts/_adapters/linear_adapter.py +18 -0
  165. package/scripts/_adapters/miro_adapter.py +18 -0
  166. package/scripts/_adapters/notion_adapter.py +18 -0
  167. package/scripts/_agents.py +357 -0
  168. package/scripts/_brain_ingest/__init__.py +2 -0
  169. package/scripts/_brain_ingest/session_fact_extractor.py +341 -0
  170. package/scripts/_brain_port.py +163 -0
  171. package/scripts/_bus.py +1081 -0
  172. package/scripts/_bus_catalog_gen.py +93 -0
  173. package/scripts/_bus_consumers.json +46 -0
  174. package/scripts/_bus_emit.py +43 -0
  175. package/scripts/_capability_registry.py +334 -0
  176. package/scripts/_capability_resolver.py +322 -0
  177. package/scripts/_domain_store.py +559 -0
  178. package/scripts/_event_log_reader.py +154 -0
  179. package/scripts/_event_schema.py +305 -0
  180. package/scripts/_event_store.py +397 -0
  181. package/scripts/_heavy_cadence.py +324 -0
  182. package/scripts/_integration_resolver.py +321 -0
  183. package/scripts/_logger.py +151 -0
  184. package/scripts/_loom.py +482 -0
  185. package/scripts/_paths.py +133 -0
  186. package/scripts/_python.sh +22 -0
  187. package/scripts/_run.py +137 -0
  188. package/scripts/_session.py +502 -0
  189. package/scripts/_validate_registry.py +1042 -0
  190. package/scripts/_wicked_testing_probe.py +277 -0
  191. package/scripts/agentic/analyze_agents.py +391 -0
  192. package/scripts/agentic/detect_framework.py +420 -0
  193. package/scripts/agentic/issue_taxonomy.py +357 -0
  194. package/scripts/agentic/pattern_scorer.py +434 -0
  195. package/scripts/classify/persist.py +169 -0
  196. package/scripts/compiler/compile.py +552 -0
  197. package/scripts/compiler/phase0/VERDICT.md +60 -0
  198. package/scripts/compiler/phase0/detect.py +292 -0
  199. package/scripts/compiler/phase0/emit.py +228 -0
  200. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.bindings.json +30 -0
  201. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.lint-output.txt +17 -0
  202. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/failing-tests.txt +4 -0
  203. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/honest-tests.txt +2 -0
  204. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-fail.build.md +8 -0
  205. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-missing.build.md +8 -0
  206. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/honest.build.md +8 -0
  207. package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/wg_check_claims.py +174 -0
  208. package/scripts/compiler/phase0/evidence/command_iq.bindings.json +45 -0
  209. package/scripts/compiler/phase0/evidence/command_iq.lint-output.txt +17 -0
  210. package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/failing-tests.txt +4 -0
  211. package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/honest-tests.txt +3 -0
  212. package/scripts/compiler/phase0/evidence/command_iq.stage/fab-fail.build.md +8 -0
  213. package/scripts/compiler/phase0/evidence/command_iq.stage/fab-missing.build.md +8 -0
  214. package/scripts/compiler/phase0/evidence/command_iq.stage/honest.build.md +8 -0
  215. package/scripts/compiler/phase0/evidence/command_iq.stage/wg_check_claims.py +174 -0
  216. package/scripts/compiler/phase0/evidence/memos.bindings.json +28 -0
  217. package/scripts/compiler/phase0/evidence/memos.lint-output.txt +17 -0
  218. package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/failing-tests.txt +4 -0
  219. package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/honest-tests.txt +2 -0
  220. package/scripts/compiler/phase0/evidence/memos.stage/fab-fail.build.md +8 -0
  221. package/scripts/compiler/phase0/evidence/memos.stage/fab-missing.build.md +8 -0
  222. package/scripts/compiler/phase0/evidence/memos.stage/honest.build.md +8 -0
  223. package/scripts/compiler/phase0/evidence/memos.stage/wg_check_claims.py +174 -0
  224. package/scripts/compiler/phase0/evidence/verdict.json +104 -0
  225. package/scripts/compiler/phase0/evidence/wicked-bus.bindings.json +29 -0
  226. package/scripts/compiler/phase0/evidence/wicked-bus.lint-output.txt +17 -0
  227. package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/failing-tests.txt +4 -0
  228. package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/honest-tests.txt +3 -0
  229. package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-fail.build.md +8 -0
  230. package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-missing.build.md +8 -0
  231. package/scripts/compiler/phase0/evidence/wicked-bus.stage/honest.build.md +8 -0
  232. package/scripts/compiler/phase0/evidence/wicked-bus.stage/wg_check_claims.py +174 -0
  233. package/scripts/compiler/phase0/evidence/wire-vault-transcript.txt +60 -0
  234. package/scripts/compiler/phase0/run_probe.py +234 -0
  235. package/scripts/compiler/phase0/wire_vault.py +310 -0
  236. package/scripts/crew/__init__.py +0 -0
  237. package/scripts/crew/_task_audit_writer.py +243 -0
  238. package/scripts/crew/_task_reader.py +389 -0
  239. package/scripts/crew/archetypes_v11.py +348 -0
  240. package/scripts/crew/detectors/__init__.py +12 -0
  241. package/scripts/crew/flow_compiler.py +121 -0
  242. package/scripts/crew/phase_manager.py +703 -0
  243. package/scripts/crew/scope_delta.py +264 -0
  244. package/scripts/crew/specialist_resolver.py +218 -0
  245. package/scripts/crew/stack_registry.py +189 -0
  246. package/scripts/data/data_profiler.py +271 -0
  247. package/scripts/data/ontology_recommender.py +231 -0
  248. package/scripts/data/schema_validator.py +267 -0
  249. package/scripts/delivery/drift.py +739 -0
  250. package/scripts/delivery/telemetry.py +614 -0
  251. package/scripts/engineering/patch/codegraph_db.py +125 -0
  252. package/scripts/engineering/patch/generators/__init__.py +56 -0
  253. package/scripts/engineering/patch/generators/base.py +373 -0
  254. package/scripts/engineering/patch/generators/csharp_generator.py +494 -0
  255. package/scripts/engineering/patch/generators/go_generator.py +430 -0
  256. package/scripts/engineering/patch/generators/java_generator.py +928 -0
  257. package/scripts/engineering/patch/generators/jsp_generator.py +454 -0
  258. package/scripts/engineering/patch/generators/kotlin_generator.py +561 -0
  259. package/scripts/engineering/patch/generators/perl_generator.py +510 -0
  260. package/scripts/engineering/patch/generators/php_generator.py +644 -0
  261. package/scripts/engineering/patch/generators/propagation_engine.py +929 -0
  262. package/scripts/engineering/patch/generators/python_generator.py +487 -0
  263. package/scripts/engineering/patch/generators/ruby_generator.py +463 -0
  264. package/scripts/engineering/patch/generators/rust_generator.py +400 -0
  265. package/scripts/engineering/patch/generators/sql_generator.py +594 -0
  266. package/scripts/engineering/patch/generators/typescript_generator.py +439 -0
  267. package/scripts/engineering/patch/patch.py +800 -0
  268. package/scripts/engineering/patch/safety.py +429 -0
  269. package/scripts/jam/__init__.py +0 -0
  270. package/scripts/jam/_bus_consumers.py +271 -0
  271. package/scripts/jam/agentic_cli_registry.py +703 -0
  272. package/scripts/jam/consensus.py +737 -0
  273. package/scripts/jam/detect_clis.py +409 -0
  274. package/scripts/jam/jam.py +283 -0
  275. package/scripts/jam/save_transcript.py +171 -0
  276. package/scripts/loom/__init__.py +22 -0
  277. package/scripts/loom/cli.py +170 -0
  278. package/scripts/loom/compose.py +180 -0
  279. package/scripts/loom/gate.py +120 -0
  280. package/scripts/loom/manifest.py +140 -0
  281. package/scripts/loom/resolve.py +59 -0
  282. package/scripts/persona/__init__.py +0 -0
  283. package/scripts/persona/registry.py +688 -0
  284. package/scripts/platform/guard_pipeline.py +1214 -0
  285. package/scripts/platform/guard_profiles.py +216 -0
  286. package/scripts/platform/observability/assert_contracts.py +434 -0
  287. package/scripts/platform/observability/health_probe.py +915 -0
  288. package/scripts/platform/observability/ops_log_viewer.py +209 -0
  289. package/scripts/platform/prereq_doctor.py +567 -0
  290. package/scripts/qe/conditions_manifest.py +260 -0
  291. package/scripts/qe/content_sanitizer.py +160 -0
  292. package/scripts/qe/coverage_tracker.py +535 -0
  293. package/scripts/qe/discover_scenarios.py +243 -0
  294. package/scripts/qe/evidence_tracker.py +232 -0
  295. package/scripts/qe/prove.py +196 -0
  296. package/scripts/qe/registry_lookup.py +80 -0
  297. package/scripts/qe/registry_store.py +176 -0
  298. package/scripts/qe/semantic_review.py +1005 -0
  299. package/scripts/qe/vault_gate.py +382 -0
  300. package/scripts/qe/verdict_audit.py +175 -0
  301. package/scripts/qe/verdict_schema.py +335 -0
  302. package/scripts/reset.py +220 -0
  303. package/scripts/resolve_path.py +22 -0
  304. package/scripts/sentinel/invariants.py +402 -0
  305. package/scripts/sentinel/pre_push.py +73 -0
  306. package/scripts/setup/__init__.py +1 -0
  307. package/scripts/setup/detect_state.py +114 -0
  308. package/scripts/setup/migrate_legacy.py +67 -0
  309. package/scripts/setup/migrate_v6_projects.py +232 -0
  310. package/scripts/setup/onboarding.py +114 -0
  311. package/scripts/smaht/_bus_consumers.py +98 -0
  312. package/scripts/smaht/adapters/__init__.py +83 -0
  313. package/scripts/smaht/adapters/brain_adapter.py +254 -0
  314. package/scripts/smaht/adapters/context7_adapter.py +438 -0
  315. package/scripts/smaht/adapters/delegation_adapter.py +104 -0
  316. package/scripts/smaht/adapters/domain_adapter.py +206 -0
  317. package/scripts/smaht/adapters/events_adapter.py +231 -0
  318. package/scripts/smaht/adapters/tools_adapter.py +49 -0
  319. package/scripts/smaht/cheatsheet_store.py +236 -0
  320. package/scripts/smaht/context_package.py +386 -0
  321. package/scripts/smaht/knowledge_graph.py +395 -0
  322. package/scripts/smaht/propose_skills.py +856 -0
  323. package/scripts/smaht/test_cheatsheet_store.py +428 -0
  324. package/scripts/statusline.py +151 -0
  325. package/scripts/where_am_i.py +236 -0
  326. package/skills/agentic/agentic-patterns/SKILL.md +91 -0
  327. package/skills/agentic/agentic-patterns/refs/anti-patterns-design.md +283 -0
  328. package/skills/agentic/agentic-patterns/refs/anti-patterns-operational.md +216 -0
  329. package/skills/agentic/agentic-patterns/refs/pattern-catalog.md +329 -0
  330. package/skills/agentic/context-engineering/SKILL.md +142 -0
  331. package/skills/agentic/context-engineering/refs/caching-and-optimization.md +251 -0
  332. package/skills/agentic/context-engineering/refs/compression-techniques.md +171 -0
  333. package/skills/agentic/context-engineering/refs/cost-calculation-budget.md +250 -0
  334. package/skills/agentic/context-engineering/refs/cost-optimization-reporting.md +300 -0
  335. package/skills/agentic/context-engineering/refs/selective-loading.md +179 -0
  336. package/skills/agentic/frameworks/SKILL.md +191 -0
  337. package/skills/agentic/frameworks/refs/framework-profiles-1.md +209 -0
  338. package/skills/agentic/frameworks/refs/framework-profiles-2.md +214 -0
  339. package/skills/agentic/frameworks/refs/migration-patterns-paths.md +267 -0
  340. package/skills/agentic/frameworks/refs/migration-patterns-testing.md +208 -0
  341. package/skills/agentic/refs/audit.md +134 -0
  342. package/skills/agentic/refs/design.md +137 -0
  343. package/skills/agentic/refs/frameworks.md +102 -0
  344. package/skills/agentic/refs/review.md +160 -0
  345. package/skills/agentic/review-methodology/SKILL.md +186 -0
  346. package/skills/agentic/review-methodology/refs/deliverable-templates.md +246 -0
  347. package/skills/agentic/review-methodology/refs/issue-taxonomy-quality-testing.md +277 -0
  348. package/skills/agentic/review-methodology/refs/issue-taxonomy-reliability-safety.md +253 -0
  349. package/skills/agentic/trust-and-safety/SKILL.md +177 -0
  350. package/skills/agentic/trust-and-safety/refs/guardrails-actions.md +246 -0
  351. package/skills/agentic/trust-and-safety/refs/guardrails-input-output.md +238 -0
  352. package/skills/agentic/trust-and-safety/refs/guardrails-resources.md +209 -0
  353. package/skills/agentic/trust-and-safety/refs/safety-checklist-advanced.md +209 -0
  354. package/skills/agentic/trust-and-safety/refs/safety-checklist-core.md +215 -0
  355. package/skills/archetype/SKILL.md +104 -0
  356. package/skills/archetype/refs/build.md +142 -0
  357. package/skills/archetype/refs/decide.md +117 -0
  358. package/skills/archetype/refs/explore.md +66 -0
  359. package/skills/archetype/refs/incident.md +149 -0
  360. package/skills/archetype/refs/migrate.md +170 -0
  361. package/skills/archetype/refs/modernize.md +187 -0
  362. package/skills/archetype/refs/review.md +145 -0
  363. package/skills/archetype/refs/ship.md +117 -0
  364. package/skills/archetype/refs/specify.md +108 -0
  365. package/skills/archetype/refs/triage.md +62 -0
  366. package/skills/classify/SKILL.md +157 -0
  367. package/skills/data/analysis/SKILL.md +125 -0
  368. package/skills/data/analysis/refs/templates.md +170 -0
  369. package/skills/data/data/SKILL.md +118 -0
  370. package/skills/data/data/refs/examples.md +235 -0
  371. package/skills/data/ml/SKILL.md +125 -0
  372. package/skills/data/ml/refs/templates.md +172 -0
  373. package/skills/data/pipeline/SKILL.md +136 -0
  374. package/skills/data/pipeline/refs/templates.md +175 -0
  375. package/skills/data/refs/analyze.md +123 -0
  376. package/skills/data/refs/data.md +95 -0
  377. package/skills/data/refs/ml.md +118 -0
  378. package/skills/data/refs/pipeline.md +92 -0
  379. package/skills/deliberate/SKILL.md +125 -0
  380. package/skills/deliberate/refs/architecture-lens.md +37 -0
  381. package/skills/deliberate/refs/assumptions-lens.md +43 -0
  382. package/skills/deliberate/refs/code-lens.md +38 -0
  383. package/skills/deliberate/refs/content-lens.md +37 -0
  384. package/skills/deliberate/refs/data-lens.md +43 -0
  385. package/skills/deliberate/refs/design-lens.md +37 -0
  386. package/skills/deliberate/refs/opportunity-patterns.md +107 -0
  387. package/skills/deliberate/refs/ops-lens.md +43 -0
  388. package/skills/deliberate/refs/product-lens.md +46 -0
  389. package/skills/deliberate/refs/rethink-framework.md +77 -0
  390. package/skills/deliberate/refs/security-lens.md +47 -0
  391. package/skills/deliberate/refs/testing-lens.md +45 -0
  392. package/skills/deliberate/refs/timing-lens.md +42 -0
  393. package/skills/engineering/architecture/SKILL.md +177 -0
  394. package/skills/engineering/architecture/refs/adr-template.md +268 -0
  395. package/skills/engineering/architecture/refs/architecture-template-deployment.md +176 -0
  396. package/skills/engineering/architecture/refs/architecture-template-design.md +300 -0
  397. package/skills/engineering/architecture/refs/examples-ecommerce-analytics.md +299 -0
  398. package/skills/engineering/architecture/refs/examples-saas-trading.md +291 -0
  399. package/skills/engineering/architecture/refs/patterns-advanced.md +238 -0
  400. package/skills/engineering/architecture/refs/patterns-structural.md +303 -0
  401. package/skills/engineering/backend/SKILL.md +102 -0
  402. package/skills/engineering/backend/refs/checklists.md +146 -0
  403. package/skills/engineering/backend/refs/patterns.md +181 -0
  404. package/skills/engineering/debugging/SKILL.md +99 -0
  405. package/skills/engineering/debugging/refs/patterns.md +248 -0
  406. package/skills/engineering/debugging/refs/process.md +203 -0
  407. package/skills/engineering/docs/SKILL.md +97 -0
  408. package/skills/engineering/engineering/SKILL.md +88 -0
  409. package/skills/engineering/engineering/refs/checklists.md +91 -0
  410. package/skills/engineering/engineering/refs/templates.md +71 -0
  411. package/skills/engineering/frontend/SKILL.md +92 -0
  412. package/skills/engineering/frontend/refs/checklists.md +96 -0
  413. package/skills/engineering/frontend/refs/patterns.md +167 -0
  414. package/skills/engineering/integration/SKILL.md +183 -0
  415. package/skills/engineering/integration/refs/error-handling-best-practices.md +139 -0
  416. package/skills/engineering/integration/refs/error-handling-error-classes-typescript.md +212 -0
  417. package/skills/engineering/integration/refs/error-handling-error-response-format.md +301 -0
  418. package/skills/engineering/integration/refs/error-handling-retry-strategies.md +190 -0
  419. package/skills/engineering/integration/refs/event-schemas-best-practices.md +225 -0
  420. package/skills/engineering/integration/refs/event-schemas-event-naming-conventions.md +303 -0
  421. package/skills/engineering/integration/refs/event-schemas-event-structure.md +288 -0
  422. package/skills/engineering/integration/refs/graphql-best-practices.md +138 -0
  423. package/skills/engineering/integration/refs/graphql-error-handling.md +198 -0
  424. package/skills/engineering/integration/refs/graphql-graphql-basics.md +197 -0
  425. package/skills/engineering/integration/refs/graphql-query-examples.md +226 -0
  426. package/skills/engineering/integration/refs/graphql-schema-design-patterns.md +280 -0
  427. package/skills/engineering/integration/refs/grpc-grpc-overview.md +246 -0
  428. package/skills/engineering/integration/refs/grpc-implementation-examples.md +269 -0
  429. package/skills/engineering/integration/refs/grpc-metadata-headers.md +304 -0
  430. package/skills/engineering/integration/refs/grpc-testing.md +49 -0
  431. package/skills/engineering/integration/refs/openapi-template.yaml +682 -0
  432. package/skills/engineering/integration/refs/resilience-best-practices.md +109 -0
  433. package/skills/engineering/integration/refs/resilience-circuit-breaker-pattern.md +302 -0
  434. package/skills/engineering/integration/refs/resilience-rate-limiting.md +245 -0
  435. package/skills/engineering/integration/refs/resilience-timeout-pattern.md +212 -0
  436. package/skills/engineering/integration/refs/rest-api-fundamentals.md +299 -0
  437. package/skills/engineering/integration/refs/rest-api-responses-security.md +302 -0
  438. package/skills/engineering/integration/refs/versioning-breaking-changes.md +237 -0
  439. package/skills/engineering/integration/refs/versioning-migration-strategies.md +257 -0
  440. package/skills/engineering/integration/refs/versioning-versioning-strategies.md +296 -0
  441. package/skills/engineering/large-scale-migration/SKILL.md +65 -0
  442. package/skills/engineering/patch/SKILL.md +184 -0
  443. package/skills/engineering/refs/arch.md +126 -0
  444. package/skills/engineering/refs/debug.md +142 -0
  445. package/skills/engineering/refs/docs.md +190 -0
  446. package/skills/engineering/refs/plan.md +109 -0
  447. package/skills/engineering/refs/review.md +138 -0
  448. package/skills/engineering/system-design/SKILL.md +154 -0
  449. package/skills/engineering/system-design/refs/anti-patterns-big-ball-of-mud.md +254 -0
  450. package/skills/engineering/system-design/refs/anti-patterns-circular-dependencies.md +241 -0
  451. package/skills/engineering/system-design/refs/anti-patterns-feature-envy.md +168 -0
  452. package/skills/engineering/system-design/refs/anti-patterns-god-object.md +247 -0
  453. package/skills/engineering/system-design/refs/component-template-lifecycle.md +210 -0
  454. package/skills/engineering/system-design/refs/component-template-maintenance.md +219 -0
  455. package/skills/engineering/system-design/refs/component-template-structure.md +276 -0
  456. package/skills/engineering/system-design/refs/dependencies-best-practices.md +260 -0
  457. package/skills/engineering/system-design/refs/dependencies-dependency-principles.md +295 -0
  458. package/skills/engineering/system-design/refs/dependencies-dependency-tracking.md +261 -0
  459. package/skills/engineering/system-design/refs/interface-template-lifecycle.md +211 -0
  460. package/skills/engineering/system-design/refs/interface-template-maintenance.md +212 -0
  461. package/skills/engineering/system-design/refs/interface-template-structure.md +269 -0
  462. package/skills/engineering/system-design/refs/patterns-dependency-injection.md +151 -0
  463. package/skills/engineering/system-design/refs/patterns-facade-pattern.md +257 -0
  464. package/skills/engineering/system-design/refs/patterns-layered-pattern.md +204 -0
  465. package/skills/engineering/system-design/refs/patterns-plugin-architecture.md +229 -0
  466. package/skills/engineering/unit-test-quality/SKILL.md +194 -0
  467. package/skills/ground/SKILL.md +130 -0
  468. package/skills/integration-discovery/SKILL.md +168 -0
  469. package/skills/integration-discovery/refs/capabilities.md +282 -0
  470. package/skills/integration-discovery/refs/cli-detection.md +295 -0
  471. package/skills/integration-discovery/refs/discovery-sources.md +233 -0
  472. package/skills/integration-discovery/refs/task-patterns.md +206 -0
  473. package/skills/jam/SKILL.md +45 -0
  474. package/skills/jam/refs/facilitation-patterns.md +125 -0
  475. package/skills/jam/refs/quick.md +64 -0
  476. package/skills/jam/refs/synthesis-patterns.md +114 -0
  477. package/skills/multi-model/SKILL.md +168 -0
  478. package/skills/multi-model/refs/aichat.md +119 -0
  479. package/skills/multi-model/refs/aider.md +100 -0
  480. package/skills/multi-model/refs/auditability.md +238 -0
  481. package/skills/multi-model/refs/codex.md +169 -0
  482. package/skills/multi-model/refs/context.md +306 -0
  483. package/skills/multi-model/refs/copilot.md +161 -0
  484. package/skills/multi-model/refs/examples.md +224 -0
  485. package/skills/multi-model/refs/gemini.md +185 -0
  486. package/skills/multi-model/refs/goose.md +112 -0
  487. package/skills/multi-model/refs/llm.md +124 -0
  488. package/skills/multi-model/refs/opencode.md +196 -0
  489. package/skills/multi-model/refs/orchestration.md +188 -0
  490. package/skills/multi-model/refs/pi.md +208 -0
  491. package/skills/persona/SKILL.md +144 -0
  492. package/skills/persona/refs/define.md +133 -0
  493. package/skills/platform/actions/refs/actions.md +114 -0
  494. package/skills/platform/audit/SKILL.md +174 -0
  495. package/skills/platform/audit/refs/audit.md +145 -0
  496. package/skills/platform/audit/refs/checklists-evidence-operations.md +144 -0
  497. package/skills/platform/audit/refs/checklists-gdpr-pci-evidence.md +213 -0
  498. package/skills/platform/audit/refs/checklists-soc2-hipaa.md +120 -0
  499. package/skills/platform/audit/refs/frameworks-gdpr-pci.md +225 -0
  500. package/skills/platform/audit/refs/frameworks-soc2-hipaa.md +228 -0
  501. package/skills/platform/compliance/SKILL.md +169 -0
  502. package/skills/platform/compliance/refs/checklists.md +303 -0
  503. package/skills/platform/compliance/refs/compliance.md +117 -0
  504. package/skills/platform/compliance/refs/frameworks.md +282 -0
  505. package/skills/platform/errors/SKILL.md +183 -0
  506. package/skills/platform/errors/refs/patterns.md +271 -0
  507. package/skills/platform/errors/refs/severity.md +264 -0
  508. package/skills/platform/gate-benchmark-rebaseline/SKILL.md +108 -0
  509. package/skills/platform/gh/refs/gh.md +105 -0
  510. package/skills/platform/gh-cli/SKILL.md +120 -0
  511. package/skills/platform/gh-cli/refs/automation.md +63 -0
  512. package/skills/platform/gh-cli/refs/gh-ops.md +103 -0
  513. package/skills/platform/gh-cli/refs/patterns.md +132 -0
  514. package/skills/platform/gh-cli/scripts/gh_ops.py +454 -0
  515. package/skills/platform/github-actions/SKILL.md +159 -0
  516. package/skills/platform/github-actions/refs/security.md +150 -0
  517. package/skills/platform/github-actions/refs/templates.md +230 -0
  518. package/skills/platform/github-actions/refs/troubleshooting.md +174 -0
  519. package/skills/platform/github-actions/scripts/workflow_gen.py +215 -0
  520. package/skills/platform/gitlab-ci/SKILL.md +140 -0
  521. package/skills/platform/gitlab-ci/refs/templates.md +223 -0
  522. package/skills/platform/gitlab-ci/refs/troubleshooting.md +213 -0
  523. package/skills/platform/glab-cli/SKILL.md +133 -0
  524. package/skills/platform/glab-cli/refs/ci-debugging.md +95 -0
  525. package/skills/platform/glab-cli/refs/glab-ops.md +78 -0
  526. package/skills/platform/glab-cli/refs/patterns.md +131 -0
  527. package/skills/platform/glab-cli/scripts/glab_ops.py +313 -0
  528. package/skills/platform/health/SKILL.md +177 -0
  529. package/skills/platform/health/refs/health.md +72 -0
  530. package/skills/platform/health/refs/sources.md +302 -0
  531. package/skills/platform/incident/refs/incident.md +136 -0
  532. package/skills/platform/infra/refs/infra.md +104 -0
  533. package/skills/platform/observability/SKILL.md +128 -0
  534. package/skills/platform/observability/refs/toolchain-discovery.md +243 -0
  535. package/skills/platform/prereq-doctor/SKILL.md +57 -0
  536. package/skills/platform/prereq-doctor/refs/tool-registry.md +58 -0
  537. package/skills/platform/traces/refs/traces.md +101 -0
  538. package/skills/product/acceptance-criteria/SKILL.md +193 -0
  539. package/skills/product/acceptance-criteria/refs/ac-anti-patterns.md +291 -0
  540. package/skills/product/acceptance-criteria/refs/ac-examples.md +277 -0
  541. package/skills/product/accessibility/SKILL.md +197 -0
  542. package/skills/product/accessibility/refs/aria-patterns-basics.md +250 -0
  543. package/skills/product/accessibility/refs/aria-patterns-dynamic.md +187 -0
  544. package/skills/product/accessibility/refs/aria-patterns-interactive.md +246 -0
  545. package/skills/product/accessibility/refs/audit-report-findings.md +248 -0
  546. package/skills/product/accessibility/refs/audit-report-results.md +276 -0
  547. package/skills/product/accessibility/refs/common-violations-html-css.md +270 -0
  548. package/skills/product/accessibility/refs/common-violations-interactive.md +303 -0
  549. package/skills/product/accessibility/refs/keyboard-testing-basics.md +222 -0
  550. package/skills/product/accessibility/refs/keyboard-testing-patterns.md +173 -0
  551. package/skills/product/accessibility/refs/screen-reader-testing-advanced.md +294 -0
  552. package/skills/product/accessibility/refs/screen-reader-testing-commands.md +255 -0
  553. package/skills/product/accessibility/refs/wcag-checklist.md +262 -0
  554. package/skills/product/analyze/SKILL.md +164 -0
  555. package/skills/product/analyze/refs/algorithms.md +238 -0
  556. package/skills/product/analyze/refs/sentiment-patterns.md +180 -0
  557. package/skills/product/imagery/SKILL.md +97 -0
  558. package/skills/product/imagery/alter/SKILL.md +105 -0
  559. package/skills/product/imagery/alter/refs/editing-reference.md +146 -0
  560. package/skills/product/imagery/create/SKILL.md +109 -0
  561. package/skills/product/imagery/create/refs/prompt-engineering.md +112 -0
  562. package/skills/product/imagery/create/refs/provider-reference.md +219 -0
  563. package/skills/product/imagery/review/SKILL.md +101 -0
  564. package/skills/product/imagery/review/refs/analysis-lenses.md +136 -0
  565. package/skills/product/imagery/review/refs/quality-gates.md +153 -0
  566. package/skills/product/imagery/scripts/analyze.sh +23 -0
  567. package/skills/product/imagery/scripts/edit.sh +14 -0
  568. package/skills/product/imagery/scripts/generate.sh +13 -0
  569. package/skills/product/imagery/scripts/provider.py +787 -0
  570. package/skills/product/listen/SKILL.md +175 -0
  571. package/skills/product/listen/refs/channels.md +220 -0
  572. package/skills/product/mockup/SKILL.md +106 -0
  573. package/skills/product/mockup/refs/templates.md +95 -0
  574. package/skills/product/refs/a11y.md +109 -0
  575. package/skills/product/refs/acceptance.md +79 -0
  576. package/skills/product/refs/align.md +67 -0
  577. package/skills/product/refs/analyze.md +77 -0
  578. package/skills/product/refs/elicit.md +67 -0
  579. package/skills/product/refs/listen.md +58 -0
  580. package/skills/product/refs/mockup.md +77 -0
  581. package/skills/product/refs/screenshot.md +59 -0
  582. package/skills/product/refs/strategy.md +77 -0
  583. package/skills/product/refs/synthesize.md +54 -0
  584. package/skills/product/refs/ux-review.md +68 -0
  585. package/skills/product/refs/ux.md +101 -0
  586. package/skills/product/requirements-analysis/SKILL.md +136 -0
  587. package/skills/product/requirements-analysis/refs/requirements-example-auth.md +264 -0
  588. package/skills/product/requirements-analysis/refs/requirements-example-export-integration.md +88 -0
  589. package/skills/product/requirements-analysis/refs/requirements-output-format-template.md +247 -0
  590. package/skills/product/requirements-analysis/refs/user-story-guide.md +296 -0
  591. package/skills/product/requirements-graph/SKILL.md +155 -0
  592. package/skills/product/requirements-graph/refs/examples.md +223 -0
  593. package/skills/product/requirements-graph/refs/schema.md +162 -0
  594. package/skills/product/requirements-migrate/SKILL.md +90 -0
  595. package/skills/product/requirements-navigate/SKILL.md +141 -0
  596. package/skills/product/requirements-navigate/refs/meta-templates.md +141 -0
  597. package/skills/product/screenshot/SKILL.md +147 -0
  598. package/skills/product/strategy/SKILL.md +198 -0
  599. package/skills/product/strategy/refs/frameworks.md +295 -0
  600. package/skills/product/strategy/refs/templates-competitive-market.md +299 -0
  601. package/skills/product/strategy/refs/templates-roi-value.md +207 -0
  602. package/skills/product/synthesize/SKILL.md +116 -0
  603. package/skills/product/synthesize/refs/journey-mapping.md +212 -0
  604. package/skills/product/synthesize/refs/prioritization.md +193 -0
  605. package/skills/product/ux-review/SKILL.md +164 -0
  606. package/skills/product/visual-direction/SKILL.md +137 -0
  607. package/skills/product/visual-review/SKILL.md +131 -0
  608. package/skills/runtime-exec/SKILL.md +138 -0
  609. package/skills/search/codebase-narrator/SKILL.md +131 -0
  610. package/skills/search/codebase-narrator/refs/output-template.md +100 -0
  611. package/skills/smaht/SKILL.md +72 -0
  612. package/skills/smaht/discovery/SKILL.md +65 -0
  613. package/skills/smaht/intent/SKILL.md +89 -0
  614. package/skills/smaht/propose-skills/SKILL.md +142 -0
  615. package/skills/smaht/refs/state.md +76 -0
  616. package/skills/swarm/SKILL.md +132 -0
  617. package/skills/swarm/refs/fan-out.md +104 -0
  618. package/skills/swarm/refs/independent-verification.md +86 -0
  619. package/skills/swarm/refs/receipts-and-evidence.md +69 -0
  620. package/skills/swarm/refs/recon-synthesis.md +87 -0
  621. package/skills/swarm/refs/ship-discipline.md +90 -0
  622. package/skills/wickedizer/SKILL.md +147 -0
  623. package/skills/wickedizer/refs/artifact-types.md +238 -0
  624. package/skills/wickedizer/refs/complexity.md +278 -0
  625. package/skills/wickedizer/refs/de-ai-pass.md +140 -0
  626. package/skills/wickedizer/refs/failure-modes.md +249 -0
  627. package/skills/wickedizer/refs/patterns.md +282 -0
  628. package/skills/wickedizer/refs/review.md +221 -0
  629. package/skills/wickedizer/refs/structure.md +282 -0
  630. package/skills/wickedizer/refs/voice-profile.md +125 -0
  631. package/skills/workflow/SKILL.md +132 -0
  632. package/skills/workflow/refs/archetype-adjustments.md +205 -0
  633. package/skills/workflow/refs/evidence.md +58 -0
  634. package/skills/workflow/refs/integration.md +130 -0
  635. package/skills/workflow/refs/risk-dimension-signals.md +260 -0
  636. package/skills/workflow/refs/scoring-rubric.md +49 -0
  637. package/skills/workflow/refs/signal-keywords.md +176 -0
  638. package/skills/workflow/refs/specialist-routing-rules.md +159 -0
  639. package/skills/worktrees/SKILL.md +87 -0
  640. package/skills/worktrees/refs/recipes.md +96 -0
@@ -0,0 +1,1278 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PreToolUse hook — wicked-garden unified pre-tool dispatcher.
4
+
5
+ Consolidates: crew pretool_taskcreate, crew pretool_planmode, mem block_memory_md,
6
+ crew gate preflight (Bash approve calls).
7
+
8
+ Dispatches by tool_name from hook payload:
9
+ TaskCreate → validate event metadata envelope + crew initiative injection
10
+ TaskUpdate → validate event metadata envelope on updates
11
+ EnterPlanMode → deny and redirect to crew workflow
12
+ Write / Edit → MEMORY.md write guard (AGENTS.md writes allowed, synced via PostToolUse)
13
+ Bash → crew gate preflight (phase_manager.py approve calls) +
14
+ worktree cwd-leak guard (#878, hooks/scripts/worktree_guard.py)
15
+
16
+ Always fails open — any unhandled exception returns permissionDecision: "allow".
17
+ """
18
+
19
+ import json
20
+ import os
21
+ import re
22
+ import sys
23
+ import time
24
+ from pathlib import Path
25
+
26
+ # Add shared scripts directory to path
27
+ _PLUGIN_ROOT = Path(os.environ.get("CLAUDE_PLUGIN_ROOT", Path(__file__).resolve().parents[2]))
28
+ sys.path.insert(0, str(_PLUGIN_ROOT / "scripts"))
29
+ # Allow importing sibling hook modules (e.g. worktree_guard) in any context.
30
+ sys.path.append(str(Path(__file__).resolve().parent))
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Ops logger wrapper — fail-silent, never crashes the hook
35
+ # ---------------------------------------------------------------------------
36
+
37
+ def _log(domain, level, event, ok=True, ms=None, detail=None):
38
+ """Ops logger — fail-silent, never crashes the hook."""
39
+ try:
40
+ from _logger import log
41
+ log(domain, level, event, ok=ok, ms=ms, detail=detail)
42
+ except Exception:
43
+ pass
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Response helpers
48
+ # ---------------------------------------------------------------------------
49
+
50
+ def _allow(updated_input=None, system_message=None) -> str:
51
+ output = {
52
+ "hookSpecificOutput": {
53
+ "hookEventName": "PreToolUse",
54
+ "permissionDecision": "allow",
55
+ }
56
+ }
57
+ if updated_input is not None:
58
+ output["hookSpecificOutput"]["updatedInput"] = updated_input
59
+ if system_message:
60
+ output["systemMessage"] = system_message
61
+ return json.dumps(output)
62
+
63
+
64
+ def _deny(reason: str) -> str:
65
+ return json.dumps({
66
+ "hookSpecificOutput": {
67
+ "hookEventName": "PreToolUse",
68
+ "permissionDecision": "deny",
69
+ "permissionDecisionReason": reason,
70
+ }
71
+ })
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Handler: TaskCreate
76
+ # (inject crew initiative metadata, one-time crew suggestion)
77
+ # ---------------------------------------------------------------------------
78
+
79
+ def _find_active_crew_project():
80
+ """Return (project_data_dict, project_name, initiative_name).
81
+
82
+ Scoped to the current workspace (CLAUDE_PROJECT_NAME or cwd basename).
83
+ Only returns projects whose ``workspace`` field matches.
84
+ Uses DomainStore exclusively — operates local-only in hook mode.
85
+
86
+ ``initiative_name`` is the project name (used to tag task metadata
87
+ with a coarse grouping). Custom display names can be set via the
88
+ optional ``initiative`` field on project.json.
89
+ """
90
+ workspace = os.environ.get("CLAUDE_PROJECT_NAME") or Path.cwd().name
91
+ try:
92
+ from _domain_store import DomainStore
93
+ ds = DomainStore("wicked-crew", hook_mode=True)
94
+ projects = ds.list("projects") or []
95
+ for p in sorted(projects, key=lambda x: x.get("updated_at", x.get("created_at", "")), reverse=True):
96
+ if p.get("archived"):
97
+ continue
98
+ # Workspace scoping — skip projects from other workspaces
99
+ if workspace and p.get("workspace", "") != workspace:
100
+ continue
101
+ phase = p.get("current_phase", "")
102
+ if phase and phase not in ("complete", "done", ""):
103
+ name = p.get("name", "")
104
+ initiative = p.get("initiative") or name
105
+ return p, name, initiative
106
+ except Exception:
107
+ pass
108
+
109
+ return None, None, None
110
+
111
+
112
+ def _validate_event_metadata(tool_input: dict) -> "str | None":
113
+ """Validate the metadata envelope on TaskCreate / TaskUpdate.
114
+
115
+ Returns an error string on failure, or None on pass. Loads phases.json
116
+ for phase enum validation. Fail-silent on import errors — an
117
+ un-importable schema module must never break task creation.
118
+ """
119
+ try:
120
+ from _event_schema import validate_metadata
121
+ except Exception:
122
+ return None # schema module unavailable; fail open
123
+
124
+ metadata = tool_input.get("metadata") or {}
125
+ phases = set(_load_phases_config_hook().keys())
126
+ status = tool_input.get("status")
127
+ return validate_metadata(
128
+ metadata, valid_phases=phases or None, status=status,
129
+ )
130
+
131
+
132
+ def _task_metadata_mode() -> str:
133
+ """``off`` | ``warn`` | ``strict`` — mirrors gate enforcement convention."""
134
+ mode = (os.environ.get("WG_TASK_METADATA") or "warn").strip().lower()
135
+ return mode if mode in ("off", "warn", "strict") else "warn"
136
+
137
+
138
+ def _handle_task_create(tool_input: dict) -> str:
139
+ """Inject crew initiative metadata and validate the event envelope."""
140
+ mode = _task_metadata_mode()
141
+ err = None if mode == "off" else _validate_event_metadata(tool_input)
142
+
143
+ if err and mode == "strict":
144
+ _log("pretool", "info", "task.metadata.blocked", detail={"reason": err})
145
+ return _deny(f"[wicked-garden] task metadata invalid: {err}")
146
+
147
+ _data, project_name, initiative_name = _find_active_crew_project()
148
+
149
+ if project_name:
150
+ metadata = tool_input.get("metadata") or {}
151
+ if not metadata.get("initiative"):
152
+ metadata["initiative"] = initiative_name
153
+ tool_input["metadata"] = metadata
154
+ system_message = None
155
+ if err and mode == "warn":
156
+ _log("pretool", "warn", "task.metadata.deprecated", detail={"reason": err})
157
+ system_message = f"[wicked-garden] DEPRECATION: task metadata: {err}"
158
+ return _allow(updated_input=tool_input, system_message=system_message)
159
+
160
+ # No active project — show one-time suggestion via SessionState
161
+ system_message = None
162
+ if err and mode == "warn":
163
+ _log("pretool", "warn", "task.metadata.deprecated", detail={"reason": err})
164
+ system_message = f"[wicked-garden] DEPRECATION: task metadata: {err}"
165
+ try:
166
+ from _session import SessionState
167
+ state = SessionState.load()
168
+ if not state.task_suggest_shown:
169
+ state.update(task_suggest_shown=True)
170
+ suggest = "Creating tasks? Consider `/wicked-garden:crew:start` for quality gates."
171
+ return _allow(
172
+ system_message=(system_message + "\n" + suggest) if system_message else suggest
173
+ )
174
+ except Exception:
175
+ pass
176
+
177
+ return _allow(system_message=system_message)
178
+
179
+
180
+ def _handle_task_update(tool_input: dict) -> str:
181
+ """Validate the event envelope on TaskUpdate (metadata merges).
182
+
183
+ TaskUpdate metadata is a merge patch — an empty/absent metadata is valid
184
+ (the caller is updating status or subject only). Validation only fires
185
+ when metadata is present and contains an event_type claim.
186
+ """
187
+ mode = _task_metadata_mode()
188
+ if mode == "off":
189
+ return _allow()
190
+
191
+ metadata = tool_input.get("metadata") or {}
192
+ if not metadata.get("event_type"):
193
+ return _allow() # pure status/subject update
194
+
195
+ err = _validate_event_metadata(tool_input)
196
+ if err and mode == "strict":
197
+ _log("pretool", "info", "task.metadata.blocked", detail={"reason": err})
198
+ return _deny(f"[wicked-garden] task metadata invalid: {err}")
199
+ if err and mode == "warn":
200
+ _log("pretool", "warn", "task.metadata.deprecated", detail={"reason": err})
201
+ return _allow(
202
+ system_message=f"[wicked-garden] DEPRECATION: task metadata: {err}"
203
+ )
204
+ return _allow()
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Handler: EnterPlanMode
209
+ # (deny native plan mode, redirect to crew workflow)
210
+ # ---------------------------------------------------------------------------
211
+
212
+ def _handle_enter_plan_mode(tool_input: dict) -> str:
213
+ """Block native plan mode — redirect to crew workflow instead.
214
+
215
+ wicked-garden uses crew projects for planning, not Claude's built-in
216
+ plan mode. Always deny and point to /wicked-garden:crew:start.
217
+ """
218
+ try:
219
+ data, project_name, _ = _find_active_crew_project()
220
+ if project_name and data:
221
+ current_phase = data.get("current_phase", "")
222
+ return _deny(
223
+ f"Do not use native plan mode. A crew project '{project_name}' is already active "
224
+ f"(phase: {current_phase}). Continue working within the crew workflow. "
225
+ f"Use `/wicked-garden:crew:execute` to proceed or `/wicked-garden:crew:status` to check progress."
226
+ )
227
+ except Exception:
228
+ pass
229
+
230
+ return _deny(
231
+ "Do not use native plan mode. This project uses wicked-garden crew workflows for planning. "
232
+ "Use `/wicked-garden:crew:start` to create a new crew project with outcome clarification, "
233
+ "phased execution, and quality gates."
234
+ )
235
+
236
+
237
+ # ---------------------------------------------------------------------------
238
+ # Handler: Write / Edit
239
+ # (MEMORY.md and AGENTS.md write guard)
240
+ # ---------------------------------------------------------------------------
241
+
242
+ # Claude Code auto-memory directory pattern
243
+ _AUTO_MEMORY_MARKER = ".claude/projects/"
244
+
245
+
246
+ def _handle_write_guard(tool_input: dict) -> str:
247
+ """Block direct writes to MEMORY.md, auto-memory directory, and AGENTS.md.
248
+
249
+ Also warns when an active crew project is in build or review phase and the
250
+ file path is not on the orchestrator allowlist. This enforces the
251
+ orchestrator-only principle (Issue #251): orchestrators should write only
252
+ to project state files, not directly produce implementation artifacts.
253
+
254
+ Issue #442: Also blocks Write/Edit during build phase when the
255
+ challenge-artifacts gate has not cleared (complexity >= 4). Respects
256
+ ``WG_CHALLENGE_GATE=off`` as a scoped bypass. (The legacy global
257
+ ``CREW_GATE_ENFORCEMENT=legacy`` switch was removed in #466 — v6 gate
258
+ enforcement is unconditional; use ``git revert`` to roll back.)
259
+
260
+ TODO (Issue #329): When Claude Code supports updatedInput for PreToolUse hooks
261
+ to redirect tool calls, change the MEMORY.md deny into an updatedInput redirect
262
+ that rewrites the Write/Edit call into a wicked-brain:memory invocation instead.
263
+ This would be less disruptive than a hard deny — the intent to persist data would
264
+ still succeed, just through the correct channel.
265
+ """
266
+ file_path = tool_input.get("file_path", "")
267
+
268
+ # AGENTS.md is a cross-tool instruction file shared with Codex, Cursor, Amp, etc.
269
+ # Writes are allowed — a PostToolUse hook keeps CLAUDE.md and AGENTS.md in sync.
270
+
271
+ # Theme 9: bare endswith("MEMORY.md") would match e.g. "notMEMORY.md".
272
+ # Use Path().name for a true basename check, which is platform-agnostic
273
+ # (works for both POSIX and Windows separators).
274
+ try:
275
+ _basename = Path(file_path).name
276
+ except (TypeError, ValueError):
277
+ _basename = ""
278
+ is_memory_md = _basename == "MEMORY.md"
279
+ is_auto_memory = _AUTO_MEMORY_MARKER in file_path and "/memory/" in file_path
280
+
281
+ if is_memory_md or is_auto_memory:
282
+ return _deny(
283
+ "Do not write to MEMORY.md or the auto memory directory. "
284
+ "This project uses wicked-garden memory for persistence. "
285
+ "Use wicked-brain:memory to save decisions, patterns, and gotchas instead."
286
+ )
287
+
288
+ # Issue #442: challenge-artifacts gate for build-phase writes
289
+ challenge_block = _check_challenge_gate(file_path)
290
+ if challenge_block:
291
+ return _deny(challenge_block)
292
+
293
+ # Issue #734 Part B: bus-emit lint for orphan writes to load-bearing
294
+ # state artifacts. WG_BUS_EMIT_LINT=warn (default) emits a systemMessage;
295
+ # =strict denies the write; =off disables. Fail-open on any error.
296
+ lint_action, lint_msg = _check_bus_emit_lint(file_path)
297
+ if lint_action == "deny":
298
+ return _deny(lint_msg)
299
+
300
+ # Orchestrator-only warning (Issue #251): warn when writing outside allowlist
301
+ # during build or review phases. Fail open — always allow, but emit systemMessage.
302
+ warning = _check_orchestrator_write(file_path)
303
+
304
+ # Combine lint warn + orchestrator warn into a single systemMessage
305
+ # rather than letting the second overwrite the first.
306
+ messages: list[str] = []
307
+ if lint_action == "warn" and lint_msg:
308
+ messages.append(lint_msg)
309
+ if warning:
310
+ messages.append(warning)
311
+ if messages:
312
+ return _allow(system_message="\n\n".join(messages))
313
+
314
+ return _allow()
315
+
316
+
317
+ # ---------------------------------------------------------------------------
318
+ # Challenge gate (Issue #442)
319
+ # ---------------------------------------------------------------------------
320
+
321
+ def _challenge_gate_bypassed() -> bool:
322
+ """Respect ``WG_CHALLENGE_GATE=off`` as a scoped bypass.
323
+
324
+ Only the scoped flag disables the challenge gate. The global
325
+ ``CREW_GATE_ENFORCEMENT=legacy`` switch was removed in #466 — v6 gate
326
+ enforcement is unconditional. Rollback is a ``git revert``, not a
327
+ runtime toggle.
328
+ """
329
+ if (os.environ.get("WG_CHALLENGE_GATE") or "").strip().lower() == "off":
330
+ return True
331
+ return False
332
+
333
+
334
+ def _check_challenge_gate(file_path: str) -> str:
335
+ """Return a deny-reason if the challenge gate blocks this Write/Edit.
336
+
337
+ Only fires when ALL of the following are true:
338
+ * an active crew project exists in the current workspace
339
+ * the project is in the ``build`` phase
340
+ * the project complexity_score is >= the configured threshold
341
+ (``WG_CHALLENGE_MIN_COMPLEXITY`` env var, default 4)
342
+ * ``phases/design/challenge-artifacts.md`` is missing or invalid
343
+ * the file being written is NOT on the orchestrator allowlist
344
+ (we still allow orchestrator state writes so the project can
345
+ self-recover — the contrarian itself needs to be able to write
346
+ the artifact)
347
+
348
+ Any unexpected error → returns "" (fail-open). This mirrors the
349
+ always-fail-open contract the rest of this file upholds.
350
+ """
351
+ if _challenge_gate_bypassed():
352
+ return ""
353
+
354
+ # Contrarian must be able to write the challenge artifact itself — never
355
+ # block on the artifact's own path. Also never block state writes.
356
+ if "challenge-artifacts.md" in file_path:
357
+ return ""
358
+ if _is_allowlisted(file_path):
359
+ return ""
360
+
361
+ try:
362
+ data, project_name, _ = _find_active_crew_project()
363
+ if not project_name or not data:
364
+ return ""
365
+
366
+ current_phase = (data.get("current_phase") or "").lower()
367
+ if current_phase != "build":
368
+ return ""
369
+
370
+ complexity = int(data.get("complexity_score", 0) or 0)
371
+
372
+ # Import here so pre_tool.py stays robust if the crew module is
373
+ # missing / malformed. Fail-open on any import or attribute error.
374
+ try:
375
+ scripts_crew = _PLUGIN_ROOT / "scripts" / "crew"
376
+ if str(scripts_crew) not in sys.path:
377
+ sys.path.insert(0, str(scripts_crew))
378
+ from challenge_manifest import ( # type: ignore
379
+ is_required,
380
+ artifact_satisfies_gate,
381
+ )
382
+ except Exception:
383
+ return ""
384
+
385
+ if not is_required(complexity, phase="build"):
386
+ return ""
387
+
388
+ # Resolve the project directory to look up the artifact
389
+ try:
390
+ from _paths import get_local_path
391
+ base = get_local_path("wicked-crew", "projects")
392
+ project_id = data.get("id") or project_name
393
+ project_dir = base / project_id
394
+ except Exception:
395
+ return ""
396
+
397
+ if not (project_dir / "project.json").exists():
398
+ return ""
399
+
400
+ ok, reason = artifact_satisfies_gate(project_dir, phase="design")
401
+ if ok:
402
+ return ""
403
+
404
+ _log(
405
+ "pretool",
406
+ "info",
407
+ "challenge.gate.blocked",
408
+ detail={"project": project_name, "reason": reason},
409
+ )
410
+
411
+ return (
412
+ f"[wicked-garden] Challenge gate is unresolved for crew project "
413
+ f"'{project_name}' (complexity={complexity}). {reason} "
414
+ f"Invoke the contrarian specialist to produce and clear "
415
+ f"phases/design/challenge-artifacts.md before continuing build. "
416
+ f"Run `wicked-garden:crew:contrarian` or dispatch Task("
417
+ f"subagent_type='wicked-garden:crew:contrarian'). "
418
+ f"To bypass temporarily, set WG_CHALLENGE_GATE=off."
419
+ )
420
+ except Exception:
421
+ return ""
422
+
423
+
424
+ # ---------------------------------------------------------------------------
425
+ # Orchestrator allowlist (Issue #251)
426
+ # ---------------------------------------------------------------------------
427
+
428
+ # Paths that the orchestrator is permitted to write directly.
429
+ # .something-wicked/ contains project state, status.md files are orchestrator output.
430
+ _ORCHESTRATOR_ALLOWLIST = [
431
+ ".something-wicked/",
432
+ ]
433
+
434
+ _ORCHESTRATOR_ALLOWLIST_SUFFIXES = [
435
+ "status.md",
436
+ ]
437
+
438
+ _ORCHESTRATOR_PHASES = {"build", "review"}
439
+
440
+
441
+ def _is_allowlisted(file_path: str) -> bool:
442
+ """Return True if file_path is on the orchestrator write allowlist.
443
+
444
+ Theme 9: ``endswith("status.md")`` would false-match ``bogusstatus.md``.
445
+ Suffix entries match against the basename (Path.name) so the check
446
+ requires the exact filename, not a tail-substring.
447
+ """
448
+ for fragment in _ORCHESTRATOR_ALLOWLIST:
449
+ if fragment in file_path:
450
+ return True
451
+ try:
452
+ basename = Path(file_path).name
453
+ except (TypeError, ValueError):
454
+ return False
455
+ for suffix in _ORCHESTRATOR_ALLOWLIST_SUFFIXES:
456
+ if basename == suffix:
457
+ return True
458
+ return False
459
+
460
+
461
+ def _check_orchestrator_write(file_path: str) -> str:
462
+ """Return a warning message if the write appears to be orchestrator inline work.
463
+
464
+ Only warns — never denies. Returns empty string if no warning needed.
465
+ """
466
+ if _is_allowlisted(file_path):
467
+ return ""
468
+
469
+ # Check if there is an active crew project in build or review phase
470
+ try:
471
+ data, project_name, _ = _find_active_crew_project()
472
+ if not project_name or not data:
473
+ return ""
474
+
475
+ current_phase = data.get("current_phase", "")
476
+ if current_phase.lower() not in _ORCHESTRATOR_PHASES:
477
+ return ""
478
+
479
+ return (
480
+ f"[wicked-garden] Orchestrator principle: The active crew project "
481
+ f"'{project_name}' is in '{current_phase}' phase. "
482
+ f"Orchestrators should not write implementation files directly. "
483
+ f"Delegate to implementer, researcher, or specialist subagents via Task(). "
484
+ f"Allowed direct writes: .something-wicked/ paths and status.md files."
485
+ )
486
+ except Exception:
487
+ return ""
488
+
489
+
490
+ # ---------------------------------------------------------------------------
491
+ # Handler: Bash
492
+ # (crew gate preflight — intercepts phase_manager.py approve calls)
493
+ # ---------------------------------------------------------------------------
494
+
495
+ # Regex to extract project and phase from phase_manager.py approve invocations.
496
+ # Matches forms like:
497
+ # phase_manager.py myproject approve --phase build
498
+ # phase_manager.py myproject approve --phase build --override-gate
499
+ _APPROVE_RE = re.compile(
500
+ r"phase_manager\.py\s+(\S+)\s+approve(?:\s+.*?)?(?:--phase\s+(\S+))?",
501
+ re.DOTALL,
502
+ )
503
+
504
+
505
+ # ---------------------------------------------------------------------------
506
+ # Bus-emit lint (Issue #734 Part B)
507
+ # ---------------------------------------------------------------------------
508
+ #
509
+ # Detects orphan writes to load-bearing crew state artifacts that are not
510
+ # accompanied by a recent ``wicked-bus`` event on the project's chain. The
511
+ # resume projector (#734 Part A) and any future bus-as-truth subscriber
512
+ # rely on emits to stay current — silent writes leave projections stale
513
+ # and force callers back to file polling.
514
+ #
515
+ # Configuration:
516
+ # WG_BUS_EMIT_LINT=warn|strict|off (default: warn)
517
+ # WG_BUS_EMIT_LINT_WINDOW_SEC=<int> (default: 60)
518
+ #
519
+ # Detection mechanism: query the daemon's projections.db ``event_log`` table
520
+ # for any event ingested in the last N seconds whose chain_id starts with
521
+ # ``{active_project_id}.``. Read-only sqlite URI mode keeps the projector
522
+ # untouched. ANY error fail-opens — the lint is advisory unless strict.
523
+
524
+ #: Filenames (basename match, platform-agnostic) that mark a path as
525
+ #: load-bearing crew state. Each is a post-audit-#735 high-priority gap.
526
+ #: Basename matching avoids the "Windows backslash vs POSIX forward-slash"
527
+ #: bug — Path(file_path).name is the same on every platform.
528
+ _BUS_EMIT_LINT_TARGET_BASENAMES = frozenset({
529
+ "gate-result.json",
530
+ "dispatch-log.jsonl",
531
+ "conditions-manifest.json",
532
+ "reviewer-report.md",
533
+ })
534
+
535
+ #: A file with a target basename only counts as crew state if its parent
536
+ #: chain contains a ``phases`` directory — the wicked-crew artifact
537
+ #: convention. This prevents a scratch file like
538
+ #: ``/tmp/notes/gate-result.json`` from triggering the lint.
539
+ _BUS_EMIT_LINT_PARENT_MARKERS = ("phases",)
540
+
541
+ #: Event types that semantically pair with the target artifacts. A "recent
542
+ #: emit" only counts if its event_type is in this set — otherwise an
543
+ #: unrelated ``wicked.project.created`` at session start would buy 60s of
544
+ #: silent orphan-write passes, which is exactly the false-negative the
545
+ #: lint exists to catch.
546
+ #:
547
+ #: Pairing rationale per target:
548
+ #: gate-result.json ← wicked.gate.decided
549
+ #: conditions-manifest.json ← wicked.gate.decided (CONDITIONAL branch)
550
+ #: dispatch-log.jsonl ← wicked.dispatch.log_entry_appended (Part C)
551
+ #: reviewer-report.md ← wicked.consensus.report_created /
552
+ #: wicked.consensus.evidence_recorded (Part C)
553
+ #:
554
+ #: Phase + project lifecycle events stay in the set because callers may
555
+ #: write multiple state files in the wake of a single phase/project event.
556
+ _BUS_EMIT_LINT_PAIR_EVENTS = (
557
+ "wicked.gate.decided",
558
+ "wicked.gate.blocked",
559
+ "wicked.rework.triggered",
560
+ "wicked.phase.transitioned",
561
+ "wicked.project.completed",
562
+ # Part C of #734 — new emits paired with their target artifact writes
563
+ "wicked.dispatch.log_entry_appended",
564
+ "wicked.consensus.report_created",
565
+ "wicked.consensus.evidence_recorded",
566
+ )
567
+
568
+
569
+ def _bus_emit_lint_mode() -> str:
570
+ """Return ``'off' | 'warn' | 'strict'`` (default ``'warn'``)."""
571
+ raw = (os.environ.get("WG_BUS_EMIT_LINT") or "warn").strip().lower()
572
+ return raw if raw in ("off", "warn", "strict") else "warn"
573
+
574
+
575
+ def _bus_emit_lint_window_sec() -> int:
576
+ """Return the recent-emit window in seconds (default 60)."""
577
+ try:
578
+ return max(1, int(os.environ.get("WG_BUS_EMIT_LINT_WINDOW_SEC") or "60"))
579
+ except (TypeError, ValueError):
580
+ return 60
581
+
582
+
583
+ def _is_bus_emit_lint_target(file_path: str) -> bool:
584
+ """True iff file_path's basename matches a target AND it lives under a
585
+ crew-style ``phases/`` parent chain.
586
+
587
+ Basename-based to be Windows-safe (``C:\\proj\\phases\\build\\gate-result.json``
588
+ matches the same as the POSIX form). The parent-marker check prevents a
589
+ scratch file like ``/tmp/notes/gate-result.json`` from triggering the
590
+ lint just because the basename collides.
591
+ """
592
+ try:
593
+ p = Path(file_path)
594
+ except (TypeError, ValueError):
595
+ return False
596
+ if p.name not in _BUS_EMIT_LINT_TARGET_BASENAMES:
597
+ return False
598
+ # Walk parent components looking for a wicked-crew style marker. Use
599
+ # ``parts`` (Path.parts) so the comparison is OS-native.
600
+ parts = p.parts
601
+ return any(marker in parts for marker in _BUS_EMIT_LINT_PARENT_MARKERS)
602
+
603
+
604
+ def _resolve_daemon_db_path() -> "str | None":
605
+ """Find the daemon DB. Honors ``WG_DAEMON_DB`` first (matches daemon/db.py)."""
606
+ candidates = [
607
+ os.environ.get("WG_DAEMON_DB"),
608
+ os.environ.get("WG_PROJECTOR_DB_PATH"), # legacy alias
609
+ str(Path.home() / ".something-wicked"
610
+ / "wicked-garden-daemon" / "projections.db"),
611
+ ]
612
+ for c in candidates:
613
+ if c and Path(c).is_file():
614
+ return c
615
+ return None
616
+
617
+
618
+ def _has_recent_bus_emit(project_id: str, window_sec: int) -> bool:
619
+ """Query event_log for a recent SEMANTICALLY-PAIRED emit on this project's chain.
620
+
621
+ Returns ``True`` on any error or missing DB — fail-open per this file's
622
+ convention (mirrors ``_check_challenge_gate``'s "any unexpected error
623
+ → return '' (fail-open)" docstring). The lint stays silent when we
624
+ cannot verify; users running without the daemon don't get false
625
+ positives. The lint only fires when we CAN verify and find no
626
+ matching emit.
627
+
628
+ "Semantically paired" means the event_type is in
629
+ :data:`_BUS_EMIT_LINT_PAIR_EVENTS` — gate / phase / project lifecycle
630
+ events. An unrelated ``wicked.project.created`` at session start MUST
631
+ NOT buy a 60-second window of silent orphan-write passes; that would
632
+ be the exact false-negative the lint exists to catch.
633
+
634
+ Caveat — daemon projection race: ``emit_event()`` is fire-and-forget
635
+ and the daemon's consumer ingests into ``event_log`` on its polling
636
+ loop. A caller that emits and then writes within milliseconds may be
637
+ flagged because the projection has not yet caught up. The 60s default
638
+ window is sized to absorb realistic poll intervals; production users
639
+ can widen via ``WG_BUS_EMIT_LINT_WINDOW_SEC`` if their daemon polls
640
+ more slowly.
641
+
642
+ Read-only URI mode keeps us honest about not mutating the projector.
643
+ """
644
+ db_path = _resolve_daemon_db_path()
645
+ if not db_path:
646
+ return True # fail-open — daemon not running / no projector
647
+ try:
648
+ import sqlite3
649
+ import time as _time
650
+ uri = Path(db_path).absolute().as_uri() + "?mode=ro"
651
+ conn = sqlite3.connect(uri, uri=True)
652
+ try:
653
+ since = int(_time.time()) - window_sec
654
+ # LIKE escape: '_' and '%' in project_id would otherwise match
655
+ # unrelated chains. Same idiom as scripts/crew/resume_projector.py
656
+ # — see brain memory "sqlite-like-wildcard-escape-gotcha".
657
+ escaped = (
658
+ project_id
659
+ .replace("!", "!!")
660
+ .replace("_", "!_")
661
+ .replace("%", "!%")
662
+ )
663
+ placeholders = ",".join("?" for _ in _BUS_EMIT_LINT_PAIR_EVENTS)
664
+ params = (
665
+ f"{escaped}.%",
666
+ since,
667
+ *_BUS_EMIT_LINT_PAIR_EVENTS,
668
+ )
669
+ row = conn.execute(
670
+ f"""SELECT 1 FROM event_log
671
+ WHERE chain_id LIKE ? ESCAPE '!'
672
+ AND ingested_at >= ?
673
+ AND event_type IN ({placeholders})
674
+ LIMIT 1""",
675
+ params,
676
+ ).fetchone()
677
+ return row is not None
678
+ finally:
679
+ conn.close()
680
+ except Exception:
681
+ # sqlite3.Error, OSError on connect, anything unexpected — same
682
+ # fail-open convention as the missing-DB case above.
683
+ return True
684
+
685
+
686
+ def _check_bus_emit_lint(file_path: str) -> "tuple[str, str]":
687
+ """Return ``(action, message)``.
688
+
689
+ ``action``:
690
+ * ``'allow'`` — no message; lint disabled, not a target, no active
691
+ project, or recent emit found
692
+ * ``'warn'`` — caller should attach ``message`` as systemMessage
693
+ * ``'deny'`` — strict mode, no recent emit; caller should deny
694
+
695
+ Fail-open on any unexpected error.
696
+ """
697
+ try:
698
+ mode = _bus_emit_lint_mode()
699
+ if mode == "off":
700
+ return "allow", ""
701
+ if not _is_bus_emit_lint_target(file_path):
702
+ return "allow", ""
703
+
704
+ data, project_name, _ = _find_active_crew_project()
705
+ if not data or not project_name:
706
+ return "allow", ""
707
+
708
+ project_id = data.get("id") or project_name
709
+ window = _bus_emit_lint_window_sec()
710
+ if _has_recent_bus_emit(project_id, window):
711
+ return "allow", ""
712
+
713
+ msg = (
714
+ f"[bus-emit lint] writing to {file_path} but no wicked-bus "
715
+ f"event was emitted on chain '{project_id}.*' in the last "
716
+ f"{window}s. The resume projector (#734) and future bus-as-truth "
717
+ f"subscribers (#732) rely on emits to track state — silent "
718
+ f"writes leave projections stale. "
719
+ f"Bypass: WG_BUS_EMIT_LINT=off | "
720
+ f"adjust window: WG_BUS_EMIT_LINT_WINDOW_SEC=<sec>"
721
+ )
722
+ if mode == "strict":
723
+ return "deny", msg
724
+ return "warn", msg
725
+ except Exception:
726
+ return "allow", ""
727
+
728
+
729
+ def _parse_frontmatter(text: str) -> dict:
730
+ """Extract YAML frontmatter fields using simple regex — no YAML library.
731
+
732
+ Parses only the first ``---`` block. Returns a dict of key → raw string
733
+ values. Multi-line values and lists are NOT parsed; only flat scalar
734
+ ``key: value`` pairs are extracted.
735
+ """
736
+ fm: dict = {}
737
+ match = re.match(r"^---\s*\n(.*?)\n---", text, re.DOTALL)
738
+ if not match:
739
+ return fm
740
+ block = match.group(1)
741
+ for line in block.splitlines():
742
+ kv = re.match(r"^(\w[\w_-]*):\s*(.*)", line)
743
+ if kv:
744
+ fm[kv.group(1)] = kv.group(2).strip()
745
+ return fm
746
+
747
+
748
+ def _safe_read_text(path: Path) -> str:
749
+ """Read file text; return '' on any error."""
750
+ try:
751
+ return path.read_text(encoding="utf-8", errors="replace")
752
+ except Exception:
753
+ return ""
754
+
755
+
756
+ def _safe_read_json(path: Path):
757
+ """Read and parse JSON; return None on any error."""
758
+ try:
759
+ return json.loads(path.read_text(encoding="utf-8", errors="replace"))
760
+ except Exception:
761
+ return None
762
+
763
+
764
+ def _load_phases_config_hook() -> dict:
765
+ """Load phases.json from .claude-plugin/ relative to plugin root.
766
+
767
+ Returns the ``phases`` sub-dict, or an empty dict on any error.
768
+ This is a local copy of the logic in phase_manager.py so the hook
769
+ stays stdlib-only and avoids importing phase_manager (which has
770
+ side-effects at import time like reading env vars at module level).
771
+ """
772
+ try:
773
+ config_path = _PLUGIN_ROOT / ".claude-plugin" / "phases.json"
774
+ if config_path.exists():
775
+ data = _safe_read_json(config_path)
776
+ if isinstance(data, dict):
777
+ return data.get("phases", {})
778
+ except Exception:
779
+ pass
780
+ return {}
781
+
782
+
783
+ def _get_project_id_for(project_arg: str) -> str:
784
+ """Resolve project ID from a project name / slug argument.
785
+
786
+ The phase_manager CLI accepts a project name or ID. We look inside the
787
+ wicked-crew projects directory for a project.json whose ``name`` or ``id``
788
+ matches ``project_arg``. Falls back to using ``project_arg`` verbatim as
789
+ the directory name (which works when the CLI was given the raw ID).
790
+ """
791
+ try:
792
+ from _paths import get_local_path
793
+ base = get_local_path("wicked-crew", "projects")
794
+ # Validate before using as a path component
795
+ if re.match(r'^[a-zA-Z0-9_-]{1,64}$', project_arg):
796
+ direct = base / project_arg
797
+ if direct.is_dir():
798
+ return project_arg
799
+
800
+ # Scan all project directories for a name match
801
+ if base.exists():
802
+ for p in base.iterdir():
803
+ if not p.is_dir():
804
+ continue
805
+ pjson = _safe_read_json(p / "project.json")
806
+ if pjson and (pjson.get("name") == project_arg or pjson.get("id") == project_arg):
807
+ return p.name
808
+ except Exception:
809
+ pass
810
+ return project_arg
811
+
812
+
813
+ def _get_phase_plan(project_dir: Path) -> list:
814
+ """Return the phase_plan list from project.json, or [] on failure."""
815
+ pjson = _safe_read_json(project_dir / "project.json")
816
+ if isinstance(pjson, dict):
817
+ return [str(p) for p in pjson.get("phase_plan", [])]
818
+ return []
819
+
820
+
821
+ def _get_project_complexity(project_dir: Path) -> int:
822
+ """Return complexity_score from project.json, or 0 on failure."""
823
+ pjson = _safe_read_json(project_dir / "project.json")
824
+ if isinstance(pjson, dict):
825
+ return int(pjson.get("complexity_score", 0))
826
+ return 0
827
+
828
+
829
+ def _check_skipped_phases(phase_dir_base: Path, phase_plan: list, current_phase: str) -> str:
830
+ """Return error message if any planned phase before current_phase has no directory.
831
+
832
+ A missing phase directory means the phase was silently skipped without
833
+ going through the proper skip flow (which records a skip reason).
834
+ """
835
+ try:
836
+ idx = phase_plan.index(current_phase)
837
+ except ValueError:
838
+ return "" # current phase not in plan — can't assess
839
+
840
+ for prev in phase_plan[:idx]:
841
+ if not (phase_dir_base / prev).is_dir():
842
+ return (
843
+ f"phase '{prev}' has no execution directory — it appears to have been "
844
+ f"silently skipped without a recorded skip reason. "
845
+ f"Use /wicked-garden:crew:approve on '{prev}' first, or record a valid "
846
+ f"skip reason via phase_manager.py {prev} skip."
847
+ )
848
+ return ""
849
+
850
+
851
+ def _check_deliverables(phase_dir: Path, phase_name: str, phases_config: dict) -> str:
852
+ """Return error message if any required deliverable is missing or too small."""
853
+ phase = phases_config.get(phase_name, {})
854
+ raw_deliverables = phase.get("required_deliverables", [])
855
+
856
+ for entry in raw_deliverables:
857
+ if isinstance(entry, str):
858
+ entry = {"file": entry, "min_bytes": 100, "frontmatter": []}
859
+ elif not isinstance(entry, dict):
860
+ continue
861
+
862
+ fname = entry.get("file", "")
863
+ min_bytes = int(entry.get("min_bytes", 100))
864
+ required_fm = entry.get("frontmatter", [])
865
+
866
+ if not fname:
867
+ continue
868
+
869
+ fpath = phase_dir / fname
870
+ if not fpath.exists():
871
+ return (
872
+ f"required deliverable '{fname}' is missing from phases/{phase_name}/. "
873
+ f"Complete the phase work before approving."
874
+ )
875
+
876
+ size = fpath.stat().st_size
877
+ if size < min_bytes:
878
+ return (
879
+ f"required deliverable '{fname}' is too small ({size} bytes, "
880
+ f"minimum {min_bytes} bytes). The file may be a stub or incomplete."
881
+ )
882
+
883
+ # Check frontmatter fields
884
+ if required_fm:
885
+ text = _safe_read_text(fpath)
886
+ fm = _parse_frontmatter(text)
887
+ for field in required_fm:
888
+ if field not in fm or not fm[field]:
889
+ return (
890
+ f"required deliverable '{fname}' is missing frontmatter field "
891
+ f"'{field}'. Add the field to the YAML frontmatter block."
892
+ )
893
+
894
+ return ""
895
+
896
+
897
+ def _check_test_coverage(phase_dir_base: Path, phase_name: str, phases_config: dict) -> str:
898
+ """Return error message if test coverage ratio is below threshold."""
899
+ phase = phases_config.get(phase_name, {})
900
+ min_coverage = phase.get("min_test_coverage")
901
+
902
+ # Only enforce for phases that declare a coverage requirement
903
+ if min_coverage is None:
904
+ return ""
905
+
906
+ # --- Find the planned count from test-strategy ---
907
+ planned = 0
908
+ for strategy_fname in ("test-plan.md", "test-strategy.md"):
909
+ strategy_path = phase_dir_base / "test-strategy" / strategy_fname
910
+ if strategy_path.exists():
911
+ fm = _parse_frontmatter(_safe_read_text(strategy_path))
912
+ raw = fm.get("case_count", "")
913
+ if raw:
914
+ try:
915
+ planned = int(re.sub(r"[^\d]", "", raw))
916
+ except ValueError:
917
+ pass
918
+ if planned:
919
+ break
920
+
921
+ if not planned:
922
+ return "" # Can't assess without a planned count
923
+
924
+ # --- Find the executed count from test-results ---
925
+ executed = 0
926
+ results_path = phase_dir_base / phase_name / "test-results.md"
927
+ if results_path.exists():
928
+ text = _safe_read_text(results_path)
929
+ fm = _parse_frontmatter(text)
930
+
931
+ # Try frontmatter fields first
932
+ for key in ("executed", "pass_count", "total_count", "test_count"):
933
+ if key in fm:
934
+ try:
935
+ executed = int(re.sub(r"[^\d]", "", fm[key]))
936
+ break
937
+ except ValueError:
938
+ pass
939
+
940
+ # Fallback: count test entries in markdown body (lines starting with - [ ] or - [x])
941
+ if not executed:
942
+ executed = len(re.findall(r"- \[[ xX]\]", text))
943
+
944
+ if not executed:
945
+ return "" # Can't assess without an executed count
946
+
947
+ ratio = executed / planned
948
+ if ratio < min_coverage:
949
+ pct_actual = int(ratio * 100)
950
+ pct_required = int(min_coverage * 100)
951
+ return (
952
+ f"test coverage is {pct_actual}% ({executed}/{planned} tests executed), "
953
+ f"but {pct_required}% is required for phase '{phase_name}'. "
954
+ f"Run the missing tests and update test-results.md before approving."
955
+ )
956
+
957
+ return ""
958
+
959
+
960
+ def _check_conditions_resolved(phase_dir_base: Path, phase_plan: list, current_phase: str,
961
+ phases_config: dict) -> str:
962
+ """Return error message if any prior phase has unresolved conditions."""
963
+ try:
964
+ idx = phase_plan.index(current_phase)
965
+ except ValueError:
966
+ return ""
967
+
968
+ for prev in phase_plan[:idx]:
969
+ prev_cfg = phases_config.get(prev, {})
970
+ if not prev_cfg.get("conditions_manifest_required", False):
971
+ continue
972
+
973
+ manifest_path = phase_dir_base / prev / "conditions-manifest.json"
974
+ if not manifest_path.exists():
975
+ continue # No manifest = no conditional gate was issued
976
+
977
+ manifest = _safe_read_json(manifest_path)
978
+ if not isinstance(manifest, (list, dict)):
979
+ continue
980
+
981
+ conditions = manifest if isinstance(manifest, list) else manifest.get("conditions", [])
982
+ unresolved = [c for c in conditions if isinstance(c, dict) and not c.get("resolved_at")]
983
+ if unresolved:
984
+ labels = [c.get("id") or c.get("description", "unknown") for c in unresolved[:3]]
985
+ extra = f" (and {len(unresolved) - 3} more)" if len(unresolved) > 3 else ""
986
+ return (
987
+ f"phase '{prev}' has {len(unresolved)} unresolved condition(s): "
988
+ f"{', '.join(labels)}{extra}. "
989
+ f"Resolve all conditions in phases/{prev}/conditions-manifest.json "
990
+ f"before approving '{current_phase}'."
991
+ )
992
+
993
+ return ""
994
+
995
+
996
+ def _check_specialist_engagement(phase_dir: Path, phase_name: str, phases_config: dict,
997
+ complexity: int) -> str:
998
+ """Return error message if required specialists are missing and complexity >= 5."""
999
+ phase = phases_config.get(phase_name, {})
1000
+ required = phase.get("required_specialists", [])
1001
+
1002
+ if not required or complexity < 5:
1003
+ return ""
1004
+
1005
+ # Site W9 of bus-cutover wave-2 (#746): the engagement file
1006
+ # was refactored from JSON-array (read-modify-write) to JSONL
1007
+ # (append-only) in W9a. Read the JSONL form preferentially; fall
1008
+ # back to the legacy .json form for production projects that
1009
+ # haven't yet had their first post-W9a engagement recorded. The
1010
+ # next engagement append starts the .jsonl file fresh; both readers
1011
+ # eventually converge on JSONL as projects exercise the hook.
1012
+ engaged_domains: set = set()
1013
+ engagement_jsonl = phase_dir / "specialist-engagement.jsonl"
1014
+ engagement_json = phase_dir / "specialist-engagement.json"
1015
+ if engagement_jsonl.exists():
1016
+ try:
1017
+ for line in engagement_jsonl.read_text(encoding="utf-8").splitlines():
1018
+ line = line.strip()
1019
+ if not line:
1020
+ continue
1021
+ try:
1022
+ entry = json.loads(line)
1023
+ except json.JSONDecodeError:
1024
+ continue
1025
+ if isinstance(entry, dict):
1026
+ engaged_domains.add(entry.get("domain", ""))
1027
+ except OSError:
1028
+ pass # fail-open: file unreadable → treat as no engagement
1029
+ elif engagement_json.exists():
1030
+ # Legacy backward-compat read for pre-W9a projects.
1031
+ entries = _safe_read_json(engagement_json)
1032
+ if isinstance(entries, list):
1033
+ engaged_domains = {
1034
+ e.get("domain", "") for e in entries if isinstance(e, dict)
1035
+ }
1036
+
1037
+ missing = [s for s in required if s not in engaged_domains]
1038
+ if missing:
1039
+ return (
1040
+ f"required specialist(s) did not engage for phase '{phase_name}': "
1041
+ f"{', '.join(missing)}. "
1042
+ f"This project has complexity {complexity} (>= 5), so specialist engagement "
1043
+ f"is mandatory. Invoke the missing specialists via Task() before approving."
1044
+ )
1045
+
1046
+ return ""
1047
+
1048
+
1049
+ def _warn_reviewer_report(phase_dir: Path, phase_name: str, complexity: int) -> str:
1050
+ """Return a warning (non-blocking) if reviewer-report.md is missing for high-complexity phases."""
1051
+ if complexity < 5:
1052
+ return ""
1053
+ report_path = phase_dir / "reviewer-report.md"
1054
+ if not report_path.exists():
1055
+ return (
1056
+ f"[wicked-garden] Warning: phases/{phase_name}/reviewer-report.md is missing "
1057
+ f"for a complexity-{complexity} project. The async reviewer (Tier 2) should "
1058
+ f"have written this. Proceeding with approve, but consider running "
1059
+ f"/wicked-garden:crew:gate to generate a reviewer report."
1060
+ )
1061
+ return ""
1062
+
1063
+
1064
+ def _crew_gate_preflight(command: str) -> dict:
1065
+ """Run deterministic file-based gate checks before phase_manager.py approve executes.
1066
+
1067
+ Returns {"ok": True} to allow, or {"ok": False, "reason": "..."} to block.
1068
+ All checks complete in <100ms (file stat + small reads only).
1069
+ Fails open on any unexpected error.
1070
+ """
1071
+ # ------------------------------------------------------------------ #
1072
+ # FAST EARLY-EXIT: skip if not an approve call #
1073
+ # ------------------------------------------------------------------ #
1074
+ if "phase_manager.py" not in command or "approve" not in command:
1075
+ return {"ok": True}
1076
+
1077
+ # ------------------------------------------------------------------ #
1078
+ # v6.0: strict enforcement always active (D3 — no legacy bypass) #
1079
+ # ------------------------------------------------------------------ #
1080
+
1081
+ # ------------------------------------------------------------------ #
1082
+ # Parse project and phase from command #
1083
+ # ------------------------------------------------------------------ #
1084
+ m = _APPROVE_RE.search(command)
1085
+ if not m:
1086
+ return {"ok": True} # Unrecognised form — fail open
1087
+
1088
+ project_arg = m.group(1)
1089
+ phase_name = m.group(2)
1090
+
1091
+ if not project_arg:
1092
+ return {"ok": True}
1093
+
1094
+ # If --phase was not explicit, try to derive from session state
1095
+ if not phase_name:
1096
+ try:
1097
+ from _session import SessionState
1098
+ state = SessionState.load()
1099
+ if state.active_project and isinstance(state.active_project, dict):
1100
+ phase_name = state.active_project.get("current_phase")
1101
+ except Exception:
1102
+ pass
1103
+
1104
+ if not phase_name:
1105
+ return {"ok": True} # Can't validate without knowing the phase
1106
+
1107
+ # ------------------------------------------------------------------ #
1108
+ # Resolve paths #
1109
+ # ------------------------------------------------------------------ #
1110
+ try:
1111
+ project_id = _get_project_id_for(project_arg)
1112
+ from _paths import get_local_path
1113
+ base = get_local_path("wicked-crew", "projects")
1114
+ project_dir = base / project_id
1115
+ phase_dir = project_dir / "phases" / phase_name
1116
+ phase_dir_base = project_dir / "phases"
1117
+ except Exception:
1118
+ return {"ok": True} # Path resolution failure — fail open
1119
+
1120
+ # If project.json doesn't exist, we have no ground truth — fail open.
1121
+ # This handles the case where phase_manager.py is invoked on a project
1122
+ # that doesn't yet have local state (e.g. first run, or different workspace).
1123
+ if not (project_dir / "project.json").exists():
1124
+ return {"ok": True}
1125
+
1126
+ phases_config = _load_phases_config_hook()
1127
+ phase_plan = _get_phase_plan(project_dir)
1128
+ complexity = _get_project_complexity(project_dir)
1129
+
1130
+ # ------------------------------------------------------------------ #
1131
+ # Check a: Skipped phase detection #
1132
+ # ------------------------------------------------------------------ #
1133
+ if phase_plan:
1134
+ msg = _check_skipped_phases(phase_dir_base, phase_plan, phase_name)
1135
+ if msg:
1136
+ return {"ok": False, "reason": f"Cannot approve {phase_name}: {msg}"}
1137
+
1138
+ # ------------------------------------------------------------------ #
1139
+ # Check b: Phase directory exists #
1140
+ # ------------------------------------------------------------------ #
1141
+ if not phase_dir.is_dir():
1142
+ return {
1143
+ "ok": False,
1144
+ "reason": (
1145
+ f"Cannot approve {phase_name}: phases/{phase_name}/ directory does not exist. "
1146
+ f"The phase has not produced any output. Execute the phase before approving."
1147
+ ),
1148
+ }
1149
+
1150
+ # ------------------------------------------------------------------ #
1151
+ # Check c: Required deliverables present + frontmatter valid #
1152
+ # ------------------------------------------------------------------ #
1153
+ msg = _check_deliverables(phase_dir, phase_name, phases_config)
1154
+ if msg:
1155
+ return {"ok": False, "reason": f"Cannot approve {phase_name}: {msg}"}
1156
+
1157
+ # ------------------------------------------------------------------ #
1158
+ # Check d: Test coverage ratio #
1159
+ # ------------------------------------------------------------------ #
1160
+ msg = _check_test_coverage(phase_dir_base, phase_name, phases_config)
1161
+ if msg:
1162
+ return {"ok": False, "reason": f"Cannot approve {phase_name}: {msg}"}
1163
+
1164
+ # ------------------------------------------------------------------ #
1165
+ # Check e: Conditions resolved in prior phases #
1166
+ # ------------------------------------------------------------------ #
1167
+ if phase_plan:
1168
+ msg = _check_conditions_resolved(phase_dir_base, phase_plan, phase_name, phases_config)
1169
+ if msg:
1170
+ return {"ok": False, "reason": f"Cannot approve {phase_name}: {msg}"}
1171
+
1172
+ # ------------------------------------------------------------------ #
1173
+ # Check f: Specialist engagement (blocks if complexity >= 5) #
1174
+ # ------------------------------------------------------------------ #
1175
+ msg = _check_specialist_engagement(phase_dir, phase_name, phases_config, complexity)
1176
+ if msg:
1177
+ return {"ok": False, "reason": f"Cannot approve {phase_name}: {msg}"}
1178
+
1179
+ # ------------------------------------------------------------------ #
1180
+ # Check g: Reviewer report (warn only) #
1181
+ # ------------------------------------------------------------------ #
1182
+ warning = _warn_reviewer_report(phase_dir, phase_name, complexity)
1183
+
1184
+ return {"ok": True, "_warning": warning}
1185
+
1186
+
1187
+ def _handle_bash(tool_input: dict, cwd: str = "") -> str:
1188
+ """Bash preflight: crew gate check + worktree cwd-leak guard (#878)."""
1189
+ command = tool_input.get("command", "") or ""
1190
+
1191
+ # Crew gate preflight first — a hard deny here blocks regardless of cwd.
1192
+ # Fast path — non-approve Bash commands take <1ms.
1193
+ result = _crew_gate_preflight(command)
1194
+ if not result.get("ok"):
1195
+ return _deny(result.get("reason", "Gate preflight check failed."))
1196
+ warning = result.get("_warning", "") or None
1197
+
1198
+ # Worktree cwd-leak guard (#878): when an isolated-worktree agent runs a
1199
+ # Bash command, anchor it in the worktree (cd <root> && ...) so a leaked
1200
+ # persistent cwd cannot land commits in the main repo. Fail-open by design.
1201
+ try:
1202
+ from worktree_guard import evaluate as _wt_evaluate
1203
+ decision = _wt_evaluate(command, cwd)
1204
+ except Exception:
1205
+ decision = None
1206
+ if decision:
1207
+ if decision.get("action") == "deny":
1208
+ return _deny(decision["reason"])
1209
+ if decision.get("action") == "rewrite":
1210
+ updated = dict(tool_input)
1211
+ updated["command"] = decision["command"]
1212
+ return _allow(updated_input=updated, system_message=warning)
1213
+
1214
+ return _allow(system_message=warning)
1215
+
1216
+
1217
+ # ---------------------------------------------------------------------------
1218
+ # Main dispatcher
1219
+ # ---------------------------------------------------------------------------
1220
+
1221
+ def main():
1222
+ _t0 = time.monotonic()
1223
+
1224
+ try:
1225
+ raw = sys.stdin.read()
1226
+ input_data = json.loads(raw) if raw.strip() else {}
1227
+ except Exception:
1228
+ print(_allow())
1229
+ return
1230
+
1231
+ try:
1232
+ tool_name = input_data.get("tool_name", "")
1233
+ tool_input = input_data.get("tool_input", {}) or {}
1234
+
1235
+ _log("pretool", "debug", "hook.start", detail={"tool": tool_name})
1236
+
1237
+ if tool_name == "TaskCreate":
1238
+ result = _handle_task_create(tool_input)
1239
+ _log("pretool", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
1240
+ print(result)
1241
+ return
1242
+
1243
+ if tool_name == "TaskUpdate":
1244
+ result = _handle_task_update(tool_input)
1245
+ _log("pretool", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
1246
+ print(result)
1247
+ return
1248
+
1249
+ if tool_name == "EnterPlanMode":
1250
+ result = _handle_enter_plan_mode(tool_input)
1251
+ _log("pretool", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
1252
+ print(result)
1253
+ return
1254
+
1255
+ if tool_name in ("Write", "Edit"):
1256
+ result = _handle_write_guard(tool_input)
1257
+ _log("pretool", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
1258
+ print(result)
1259
+ return
1260
+
1261
+ if tool_name == "Bash":
1262
+ result = _handle_bash(tool_input, input_data.get("cwd", "") or "")
1263
+ _log("pretool", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
1264
+ print(result)
1265
+ return
1266
+
1267
+ # All other tools — allow
1268
+ _log("pretool", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
1269
+ print(_allow())
1270
+
1271
+ except Exception as e:
1272
+ print(f"[wicked-garden] pre_tool error: {e}", file=sys.stderr)
1273
+ # Always fail open on error
1274
+ print(_allow())
1275
+
1276
+
1277
+ if __name__ == "__main__":
1278
+ main()