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,1374 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PostToolUse / PostToolUseFailure hook — wicked-garden unified post-tool dispatcher.
4
+
5
+ Consolidates: crew posttool_task, delivery metrics_refresh, mem task_checkpoint,
6
+ search mark_stale, agentic detect_framework,
7
+ observability trace_writer.
8
+
9
+ Dispatches by tool_name from hook payload:
10
+ TaskCreate / TaskUpdate / TodoWrite → mismatch detection on TaskUpdate
11
+ Write / Edit → stale file marking + QE change tracking + MEMORY.md guard
12
+ Task → subagent activity tracking
13
+ Read → large-file-read warning + agentic framework detection
14
+ Bash → activity tracking + claim-sentinel ref-watch
15
+ Grep / Glob → no-op (ambient tips retired; sentinel replaced them)
16
+ PostToolUseFailure (any tool_name) → failure counting + auto issue detection
17
+
18
+ Traces are written to a session-scoped JSONL file in $TMPDIR (local only).
19
+
20
+ Always fails open — any unhandled exception returns {"continue": true}.
21
+ """
22
+
23
+ import json
24
+ import os
25
+ import re
26
+ import sys
27
+ import time
28
+ import uuid
29
+ from datetime import datetime, timezone
30
+ from pathlib import Path
31
+
32
+ # Add shared scripts directory to path
33
+ _PLUGIN_ROOT = Path(os.environ.get("CLAUDE_PLUGIN_ROOT", Path(__file__).resolve().parents[2]))
34
+ sys.path.insert(0, str(_PLUGIN_ROOT / "scripts"))
35
+
36
+ def _resolve_brain_port():
37
+ try:
38
+ from _brain_port import resolve_port
39
+ return resolve_port()
40
+ except Exception:
41
+ return int(os.environ.get("WICKED_BRAIN_PORT", "4242"))
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Ops logger wrapper — fail-silent, never crashes the hook
46
+ # ---------------------------------------------------------------------------
47
+
48
+ def _log(domain, level, event, ok=True, ms=None, detail=None):
49
+ """Ops logger — fail-silent, never crashes the hook."""
50
+ try:
51
+ from _logger import log
52
+ log(domain, level, event, ok=ok, ms=ms, detail=detail)
53
+ except Exception:
54
+ pass
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Helpers
59
+ # ---------------------------------------------------------------------------
60
+
61
+ def _now_iso() -> str:
62
+ return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
63
+
64
+
65
+ def _sanitize_session_id(raw: str) -> str:
66
+ """Strip path separators and traversal sequences from session ID."""
67
+ sanitized = raw.replace("/", "_").replace("\\", "_").replace("..", "_")
68
+ return sanitized if sanitized else "default"
69
+
70
+
71
+ def _get_session_id() -> str:
72
+ return _sanitize_session_id(os.environ.get("CLAUDE_SESSION_ID", "default"))
73
+
74
+
75
+ def _bus_as_truth_flag_on() -> bool:
76
+ """Return True iff Site 3 bus-as-truth cutover flag is enabled.
77
+
78
+ Site 3 of the bus cutover (#746) gates on ``WG_BUS_AS_TRUTH_REVIEWER_REPORT``.
79
+ This token is in the default-ON set (shipped, PR #776), so the flag is ON
80
+ unless explicitly set to ``"off"`` (case/whitespace normalised).
81
+
82
+ Mirrors the ``_bus_as_truth_enabled(site)`` helper in ``scripts/_bus.py``
83
+ (same normalization + default-map fall-through) but lives in this hook
84
+ module so the hook does not need to import ``_bus`` merely to read the flag.
85
+
86
+ Resolution order (flag-fold PR #777):
87
+ 1. Explicit ``"on"`` (case/whitespace normalised) → True.
88
+ 2. Explicit ``"off"`` (case/whitespace normalised) → False.
89
+ 3. Empty / any other value → default-ON (REVIEWER_REPORT is a shipped site).
90
+
91
+ Reading the env var directly anywhere else in this module is forbidden —
92
+ all reads go through this helper so there is one place to flip and audit.
93
+ """
94
+ raw = os.environ.get("WG_BUS_AS_TRUTH_REVIEWER_REPORT", "").strip().lower()
95
+ if raw == "on":
96
+ return True
97
+ if raw == "off":
98
+ return False
99
+ # REVIEWER_REPORT is a shipped site — default ON.
100
+ return True
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Handler: Grep / Glob (discovery hint for search commands)
105
+ # ---------------------------------------------------------------------------
106
+
107
+ def _handle_grep_glob(tool_name: str, tool_input: dict) -> dict:
108
+ """No-op handler. The ambient discovery tips that lived here were retired —
109
+ repetition trained the agent to ignore the channel. Claim-time signals
110
+ (scripts/sentinel/) replaced them."""
111
+ return {"continue": True}
112
+
113
+
114
+ # ---------------------------------------------------------------------------
115
+ # Handler: Write / Edit
116
+ # (stale file marking, QE change tracking)
117
+ # ---------------------------------------------------------------------------
118
+
119
+ def _handle_write_edit(tool_input: dict) -> dict:
120
+ """Mark file as stale for search index + CLAUDE.md/AGENTS.md sync directive."""
121
+ file_path = tool_input.get("file_path", "")
122
+ if not file_path:
123
+ return {"continue": True}
124
+
125
+ messages = []
126
+
127
+ # 0. CLAUDE.md ↔ AGENTS.md sync directive
128
+ sync_msg = _check_instruction_file_sync(file_path)
129
+ if sync_msg:
130
+ messages.append(sync_msg)
131
+
132
+ # 1. Mark file stale + re-index in brain
133
+ _mark_file_stale(file_path)
134
+ _brain_reindex_file(file_path)
135
+
136
+ # 2. The QE threshold nudge was retired: it recommended test menus off a
137
+ # file-count heuristic (it once suggested unit tests for a README edit).
138
+ # The testing signal now fires at claim time via the sentinel's
139
+ # evidence-freshness invariant (scripts/sentinel/invariants.py).
140
+
141
+ # v9.2.2: dropped the [Scenarios] staleness nudge entirely. The heuristic
142
+ # (any edit under commands/{domain}/ or skills/{domain}/ → "scenarios in
143
+ # scenarios/{domain}/ may need updating") fired multiple times per
144
+ # session with low signal. Users rarely acted on it because most edits
145
+ # don't actually invalidate scenarios. /wg-test stays available on demand.
146
+
147
+ result = {"continue": True}
148
+ if messages:
149
+ result["systemMessage"] = "\n".join(messages)
150
+ return result
151
+
152
+
153
+ def _check_instruction_file_sync(file_path: str) -> str | None:
154
+ """When CLAUDE.md or AGENTS.md is edited, prompt to sync the counterpart.
155
+
156
+ Both files serve as instruction files for different AI tools and must stay
157
+ consistent. CLAUDE.md is the primary (loaded by Claude Code), AGENTS.md is
158
+ the cross-tool counterpart (Codex, Cursor, Amp, etc.).
159
+
160
+ Only fires once per session to avoid repeated nudges.
161
+ """
162
+ try:
163
+ p = Path(file_path)
164
+ name = p.name
165
+
166
+ # Determine which file was edited and which needs syncing
167
+ if name == "CLAUDE.md":
168
+ counterpart_name = "AGENTS.md"
169
+ counterpart = p.parent.parent / "AGENTS.md" if p.parent.name == ".claude" else p.parent / "AGENTS.md"
170
+ elif name == "AGENTS.md":
171
+ counterpart_name = "CLAUDE.md"
172
+ # CLAUDE.md lives in .claude/ subdirectory
173
+ counterpart = p.parent / ".claude" / "CLAUDE.md"
174
+ if not counterpart.exists():
175
+ counterpart = p.parent / "CLAUDE.md"
176
+ else:
177
+ return None
178
+
179
+ # Only fire once per session
180
+ from _session import SessionState
181
+ state = SessionState.load()
182
+ synced_flags = getattr(state, "instruction_sync_fired", None) or []
183
+ if name in synced_flags:
184
+ return None
185
+ synced_flags.append(name)
186
+ state.update(instruction_sync_fired=synced_flags)
187
+
188
+ if not counterpart.exists():
189
+ return (
190
+ f"[Sync] {name} was updated but {counterpart_name} does not exist yet. "
191
+ f"Create {counterpart_name} with the same user-facing instructions so "
192
+ f"other AI tools (Codex, Cursor, Amp) follow the same rules."
193
+ )
194
+
195
+ return (
196
+ f"[Sync] {name} was updated. Read {counterpart.name} and update it to "
197
+ f"reflect the same user instructions and preferences. Both files must stay "
198
+ f"consistent — {counterpart_name} is used by other AI coding tools."
199
+ )
200
+ except Exception:
201
+ return None
202
+
203
+
204
+ def _mark_file_stale(file_path: str) -> None:
205
+ """Accumulate stale file paths in SessionState for incremental re-index."""
206
+ try:
207
+ from _session import SessionState
208
+ state = SessionState.load()
209
+ stale = state.stale_files or []
210
+ if file_path not in stale:
211
+ stale.append(file_path)
212
+ state.update(stale_files=stale)
213
+ except Exception:
214
+ pass
215
+
216
+
217
+ def _brain_reindex_file(file_path: str) -> None:
218
+ """Re-index a changed file in the brain FTS5 index. Fails silently.
219
+
220
+ Reads the file content and POSTs it to the brain index API so the brain
221
+ stays current as files are edited during the session. Async-safe: uses
222
+ stdlib urllib with a short timeout.
223
+ """
224
+ try:
225
+ import urllib.request
226
+ p = Path(file_path)
227
+ if not p.exists() or not p.is_file():
228
+ return
229
+ # Only index text files the brain cares about
230
+ text_ext = {".md", ".txt", ".py", ".js", ".ts", ".jsx", ".tsx", ".sh",
231
+ ".json", ".yaml", ".yml", ".toml", ".html", ".css", ".mjs"}
232
+ if p.suffix.lower() not in text_ext:
233
+ return
234
+ # Read content (cap at 50KB to avoid slow indexing on huge files)
235
+ content = p.read_text(encoding="utf-8", errors="replace")[:50000]
236
+ if not content.strip():
237
+ return
238
+ # Build a safe chunk ID from the file path
239
+ safe = file_path.lower().replace("/", "-").replace("\\", "-")
240
+ safe = re.sub(r"[^a-z0-9.-]", "-", safe)
241
+ safe = re.sub(r"-+", "-", safe).strip("-")
242
+ chunk_id = f"chunks/extracted/{safe}/chunk-001.md"
243
+
244
+ port = _resolve_brain_port()
245
+ payload = json.dumps({
246
+ "action": "index",
247
+ "params": {
248
+ "id": chunk_id,
249
+ "path": chunk_id,
250
+ "content": content,
251
+ "brain_id": "wicked-brain",
252
+ },
253
+ }).encode("utf-8")
254
+ req = urllib.request.Request(
255
+ f"http://localhost:{port}/api",
256
+ data=payload,
257
+ headers={"Content-Type": "application/json"},
258
+ method="POST",
259
+ )
260
+ urllib.request.urlopen(req, timeout=2)
261
+ except Exception:
262
+ pass # brain re-index is best-effort, never blocks the hook
263
+
264
+
265
+ def _check_permission_failure(tool_response) -> str | None:
266
+ """Inspect subagent result text for permission failure signals.
267
+
268
+ Returns a short snippet of the matching text if a permission failure
269
+ pattern is detected, or None if the result looks normal.
270
+ """
271
+ if not tool_response:
272
+ return None
273
+
274
+ # tool_response can be a string or a dict with a "result" or "text" field
275
+ if isinstance(tool_response, dict):
276
+ text = (
277
+ tool_response.get("result", "")
278
+ or tool_response.get("text", "")
279
+ or tool_response.get("content", "")
280
+ or tool_response.get("output", "")
281
+ )
282
+ # Also check stringified dict as fallback
283
+ if not text:
284
+ text = json.dumps(tool_response)
285
+ elif isinstance(tool_response, str):
286
+ text = tool_response
287
+ else:
288
+ text = str(tool_response)
289
+
290
+ text_lower = text.lower()
291
+ for pattern in _PERMISSION_FAILURE_PATTERNS:
292
+ idx = text_lower.find(pattern)
293
+ if idx != -1:
294
+ # Extract a short snippet around the match for the system message
295
+ start = max(0, idx - 20)
296
+ end = min(len(text), idx + len(pattern) + 60)
297
+ snippet = text[start:end].strip()
298
+ # Truncate if still too long
299
+ if len(snippet) > 120:
300
+ snippet = snippet[:120] + "..."
301
+ return snippet
302
+
303
+ return None
304
+
305
+
306
+ def _handle_task_dispatch(tool_input: dict, tool_response=None) -> dict:
307
+ """Record subagent activity in session state and detect permission failures."""
308
+ messages = []
309
+
310
+ try:
311
+ subagent_type = tool_input.get("subagent_type", "")
312
+ if not subagent_type:
313
+ return {"continue": True}
314
+
315
+ from _session import SessionState
316
+ state = SessionState.load()
317
+ dispatches = getattr(state, "subagent_dispatches", None) or []
318
+ dispatches.append({
319
+ "agent": subagent_type,
320
+ "ts": _now_iso(),
321
+ })
322
+ # Keep last 20 dispatches only
323
+ state.update(subagent_dispatches=dispatches[-20:])
324
+
325
+ # --- Permission failure detection (Issue #318) ---
326
+ snippet = _check_permission_failure(tool_response)
327
+ if snippet:
328
+ failures = (getattr(state, "subagent_permission_failures", None) or 0) + 1
329
+ state.update(subagent_permission_failures=failures)
330
+
331
+ _log(
332
+ "crew", "warn", "subagent.permission_failure",
333
+ ok=False,
334
+ detail={"agent": subagent_type, "snippet": snippet, "count": failures},
335
+ )
336
+
337
+ messages.append(
338
+ "[Crew] Subagent completed without work \u2014 permission issue detected. "
339
+ "The subagent '{}' reported: '{}'. "
340
+ "Consider running with broader permissions or executing the task inline.".format(
341
+ subagent_type, snippet
342
+ )
343
+ )
344
+
345
+ state.save()
346
+ except Exception:
347
+ pass
348
+
349
+ result = {"continue": True}
350
+ if messages:
351
+ result["systemMessage"] = "\n".join(messages)
352
+ return result
353
+
354
+
355
+ # ---------------------------------------------------------------------------
356
+ # Handler: Read (agentic framework detection)
357
+ # ---------------------------------------------------------------------------
358
+
359
+ _FRAMEWORK_PATTERNS = {
360
+ "langchain": ["langchain", "langsmith"],
361
+ "langgraph": ["langgraph"],
362
+ "autogen": ["autogen", "pyautogen"],
363
+ "crewai": ["crewai", "crew_ai"],
364
+ "dspy": ["dspy"],
365
+ "llamaindex": ["llama_index", "llama-index"],
366
+ "pydantic-ai": ["pydantic_ai", "pydantic-ai"],
367
+ "anthropic-sdk": ["anthropic"],
368
+ "openai-sdk": ["openai"],
369
+ }
370
+
371
+
372
+ _READ_LINE_THRESHOLD = 500
373
+ _READ_CHAR_THRESHOLD = 5000
374
+ _CUMULATIVE_WARN_THRESHOLD = 50000
375
+
376
+
377
+ def _handle_read(tool_input: dict, tool_response=None) -> dict:
378
+ """Large-file-read warning + path-based agentic framework detection.
379
+
380
+ Checks the Read tool output size and emits a systemMessage warning when
381
+ a single read exceeds _READ_LINE_THRESHOLD lines or _READ_CHAR_THRESHOLD
382
+ chars. Tracks cumulative bytes read for escalating warnings.
383
+
384
+ Optimized (Issue #312): uses a single SessionState load/save for both
385
+ large-file tracking and framework detection, avoiding redundant disk I/O
386
+ on the highest-frequency tool.
387
+
388
+ Always returns {"continue": True} — never blocks reads.
389
+ """
390
+ file_path = (tool_input.get("file_path") or "")
391
+ messages = []
392
+
393
+ # --- Quick path-based framework check (no I/O needed) ---
394
+ file_path_lower = file_path.lower()
395
+ detected_frameworks = []
396
+ if file_path_lower:
397
+ for framework, patterns in _FRAMEWORK_PATTERNS.items():
398
+ if any(p in file_path_lower for p in patterns):
399
+ detected_frameworks.append(framework)
400
+
401
+ # --- Parse response text for size check ---
402
+ response_text = ""
403
+ try:
404
+ if isinstance(tool_response, str):
405
+ response_text = tool_response
406
+ elif isinstance(tool_response, dict):
407
+ response_text = tool_response.get("content", "") or tool_response.get("output", "") or ""
408
+ elif isinstance(tool_response, list):
409
+ # Some hook payloads wrap content in a list of blocks
410
+ parts = []
411
+ for block in tool_response:
412
+ if isinstance(block, dict):
413
+ parts.append(block.get("text", "") or block.get("content", "") or "")
414
+ elif isinstance(block, str):
415
+ parts.append(block)
416
+ response_text = "\n".join(parts)
417
+ except Exception:
418
+ pass
419
+
420
+ # --- Single SessionState load/save for both concerns ---
421
+ needs_state = bool(response_text) or bool(detected_frameworks)
422
+ if needs_state:
423
+ try:
424
+ from _session import SessionState
425
+ state = SessionState.load()
426
+ state_dirty = False
427
+
428
+ # Large-file-read detection
429
+ if response_text:
430
+ char_count = len(response_text)
431
+ line_count = response_text.count("\n") + 1
432
+
433
+ cumulative = (state.read_bytes_total or 0) + char_count
434
+ warn_count = state.read_large_warn_count or 0
435
+
436
+ if line_count >= _READ_LINE_THRESHOLD or char_count >= _READ_CHAR_THRESHOLD:
437
+ warn_count += 1
438
+ short_path = Path(file_path).name if file_path else "unknown"
439
+
440
+ if cumulative >= _CUMULATIVE_WARN_THRESHOLD and warn_count > 2:
441
+ messages.append(
442
+ f"[Context] Large file read: {short_path} ({line_count} lines, "
443
+ f"{char_count:,} chars). Session total: {cumulative:,} chars read inline. "
444
+ f"Context window pressure is high — delegate remaining reads to "
445
+ f"subagents via Task tool to preserve context."
446
+ )
447
+ else:
448
+ messages.append(
449
+ f"[Context] Large file read detected ({line_count} lines, "
450
+ f"{char_count:,} chars). Consider delegating to a subagent to "
451
+ f"preserve context. Use Task tool with the file path instead of "
452
+ f"reading inline."
453
+ )
454
+
455
+ state.read_bytes_total = cumulative
456
+ state.read_large_warn_count = warn_count
457
+ state_dirty = True
458
+
459
+ # Framework detection (merged into same state load)
460
+ if detected_frameworks:
461
+ existing = state.detected_frameworks or []
462
+ combined = list(set(existing + detected_frameworks))
463
+ if combined != existing:
464
+ state.detected_frameworks = combined
465
+ state_dirty = True
466
+
467
+ if state_dirty:
468
+ state.save()
469
+ except Exception:
470
+ pass
471
+
472
+ result = {"continue": True}
473
+ if messages:
474
+ result["systemMessage"] = "\n".join(messages)
475
+ return result
476
+
477
+
478
+ # ---------------------------------------------------------------------------
479
+ # Handler: Skill (memory compliance counter reset + pull-model tracking)
480
+ # ---------------------------------------------------------------------------
481
+
482
+ def _handle_skill(tool_input: dict) -> dict:
483
+ """Handle Skill PostToolUse: memory compliance reset + pull-model tracking.
484
+
485
+ 1. Reset memory_compliance_escalations when a wicked-brain:memory skill
486
+ call succeeds. (mem:store was removed in #603/#604; wicked-brain:memory
487
+ is the canonical memory API as of v9.)
488
+ 2. Track wicked-brain:query/search calls for pull-model calibration (Issue #416).
489
+ """
490
+ skill = (tool_input.get("skill") or "").lower()
491
+
492
+ # Memory compliance reset: zero the escalation counter when the model
493
+ # calls wicked-brain:memory, confirming it acted on the directive.
494
+ # Issue #608: exact match only — substring would false-positive on future
495
+ # skills like wicked-brain:memory-export, wicked-brain:memory-audit, etc.
496
+ if skill != "wicked-brain:memory":
497
+ return {"continue": True}
498
+ try:
499
+ from _session import SessionState
500
+ state = SessionState.load()
501
+ if (state.memory_compliance_escalations or 0) > 0:
502
+ state.update(memory_compliance_escalations=0)
503
+ except Exception:
504
+ pass
505
+ return {"continue": True}
506
+
507
+
508
+ # ---------------------------------------------------------------------------
509
+ # Handler: Bash async — consensus gate (Issue #368, Tier 2)
510
+ # ---------------------------------------------------------------------------
511
+ # This handler is invoked from the ASYNC PostToolUse/Bash hook entry.
512
+ # It fires on every Bash call but fast-exits unless the command is a
513
+ # phase_manager.py approve invocation on a high-complexity project.
514
+ #
515
+ # When matched, it runs the full consensus evaluation synchronously within
516
+ # this process (which itself runs async/non-blocking to the agent).
517
+ # The result is written to phases/{phase}/reviewer-report.md for the
518
+ # PreToolUse Tier 1 hook to check on the NEXT phase's approve call.
519
+ # ---------------------------------------------------------------------------
520
+
521
+ _REVIEWER_REPORT_HEADER = """\
522
+ ---
523
+ verdict: {verdict}
524
+ evidence_items_checked: {evidence_items_checked}
525
+ reviewer: consensus-gate
526
+ reviewed_at: {reviewed_at}
527
+ agreement_ratio: {agreement_ratio}
528
+ findings: {findings}
529
+ conditions: {conditions}
530
+ ---
531
+ """
532
+
533
+ _REVIEWER_REPORT_PENDING = """\
534
+ ---
535
+ verdict: pending
536
+ evidence_items_checked: 0
537
+ reviewer: consensus-gate
538
+ reviewed_at: {reviewed_at}
539
+ agreement_ratio: 0.0
540
+ findings: []
541
+ conditions: []
542
+ note: "consensus evaluation failed or was skipped — will be re-evaluated on next approve"
543
+ ---
544
+ """
545
+
546
+
547
+ def _parse_project_phase_from_command(command: str):
548
+ """Extract project name and optional --phase from a phase_manager.py approve command.
549
+
550
+ Returns (project_name, phase) tuple. phase may be None if --phase not given.
551
+ Example command:
552
+ sh ...phase_manager.py my-project approve --phase design
553
+ """
554
+ import shlex
555
+ try:
556
+ tokens = shlex.split(command)
557
+ except ValueError:
558
+ tokens = command.split()
559
+
560
+ project_name = None
561
+ phase = None
562
+
563
+ # Find index of phase_manager.py in tokens
564
+ pm_idx = next(
565
+ (i for i, t in enumerate(tokens) if "phase_manager.py" in t),
566
+ None,
567
+ )
568
+ if pm_idx is None:
569
+ return None, None
570
+
571
+ # Tokens after phase_manager.py: project action [--phase name] [--other flags]
572
+ after = tokens[pm_idx + 1:]
573
+
574
+ # after[0] = project name (positional arg comes before action in phase_manager CLI)
575
+ # Format is: phase_manager.py <project> <action> [flags]
576
+ if len(after) >= 2:
577
+ project_name = after[0]
578
+ # after[1] should be "approve"
579
+ # Look for --phase flag
580
+ for i, tok in enumerate(after[2:], start=2):
581
+ if tok == "--phase" and i + 1 < len(after):
582
+ phase = after[i + 1]
583
+ break
584
+
585
+ # Safety: validate project name
586
+ if project_name:
587
+ import re as _re
588
+ if not _re.match(r'^[a-zA-Z0-9_-]{1,64}$', project_name):
589
+ project_name = None
590
+
591
+ return project_name, phase
592
+
593
+
594
+ def _load_project_for_consensus(project_name: str):
595
+ """Load project state dict for consensus evaluation.
596
+
597
+ Returns (project_state_dict, project_dir_path) or (None, None) on failure.
598
+ project_state_dict contains at minimum 'complexity_score'.
599
+ """
600
+ try:
601
+ scripts_crew = _PLUGIN_ROOT / "scripts" / "crew"
602
+ if str(scripts_crew) not in sys.path:
603
+ sys.path.insert(0, str(scripts_crew))
604
+
605
+ from phase_manager import load_project_state, get_project_dir
606
+ state = load_project_state(project_name)
607
+ if state is None:
608
+ return None, None
609
+
610
+ project_dir = get_project_dir(project_name)
611
+
612
+ # Convert dataclass to dict (handles both dataclass and plain dict)
613
+ try:
614
+ import dataclasses
615
+ if dataclasses.is_dataclass(state):
616
+ state_dict = dataclasses.asdict(state)
617
+ else:
618
+ state_dict = dict(state)
619
+ except Exception:
620
+ state_dict = {"complexity_score": getattr(state, "complexity_score", 0)}
621
+
622
+ return state_dict, project_dir
623
+ except Exception:
624
+ return None, None
625
+
626
+
627
+ def _resolve_phase_for_consensus(project_state_dict: dict, explicit_phase) -> str:
628
+ """Resolve which phase was just approved.
629
+
630
+ Uses explicit_phase if provided, else current_phase from project state.
631
+ """
632
+ if explicit_phase:
633
+ return str(explicit_phase)
634
+
635
+ # project_state_dict may have current_phase directly or nested
636
+ phase = project_state_dict.get("current_phase")
637
+ if phase:
638
+ return str(phase)
639
+
640
+ return "unknown"
641
+
642
+
643
+ def _load_phases_config() -> dict:
644
+ """Load phases.json from .claude-plugin/. Returns {} on failure."""
645
+ try:
646
+ phases_file = _PLUGIN_ROOT / ".claude-plugin" / "phases.json"
647
+ if phases_file.exists():
648
+ return json.loads(phases_file.read_text(encoding="utf-8"))
649
+ except Exception:
650
+ pass
651
+ return {}
652
+
653
+
654
+ def _build_reviewer_report_yaml(verdict: str, consensus_result: dict) -> str:
655
+ """Build the YAML frontmatter block for reviewer-report.md from consensus result."""
656
+ agreement_ratio = consensus_result.get("agreement_ratio", 0.0)
657
+ try:
658
+ agreement_ratio = round(float(agreement_ratio), 4)
659
+ except (TypeError, ValueError):
660
+ agreement_ratio = 0.0
661
+
662
+ # Collect findings from consensus_points
663
+ findings_raw = consensus_result.get("consensus_points", [])
664
+ findings_list = []
665
+ for fp in findings_raw[:10]: # cap at 10
666
+ if isinstance(fp, dict):
667
+ text = fp.get("point", fp.get("description", str(fp)))
668
+ else:
669
+ text = str(fp)
670
+ # YAML-safe single-line entry
671
+ findings_list.append(
672
+ ' - "' + text[:200].replace('"', "'") + '"'
673
+ )
674
+
675
+ # Collect conditions from consensus result
676
+ conditions_raw = consensus_result.get("conditions", []) or []
677
+ conditions_list = []
678
+ for cond in conditions_raw[:10]:
679
+ if isinstance(cond, dict):
680
+ text = cond.get("description", str(cond))
681
+ else:
682
+ text = str(cond)
683
+ conditions_list.append(
684
+ ' - "' + text[:200].replace('"', "'") + '"'
685
+ )
686
+
687
+ evidence_count = len(findings_raw) + len(
688
+ consensus_result.get("dissenting_views", []) or []
689
+ )
690
+
691
+ findings_yaml = "[\n" + "\n".join(findings_list) + "\n]" if findings_list else "[]"
692
+ conditions_yaml = "[\n" + "\n".join(conditions_list) + "\n]" if conditions_list else "[]"
693
+
694
+ return _REVIEWER_REPORT_HEADER.format(
695
+ verdict=verdict.lower(),
696
+ evidence_items_checked=evidence_count,
697
+ reviewed_at=_now_iso(),
698
+ agreement_ratio=agreement_ratio,
699
+ findings=findings_yaml,
700
+ conditions=conditions_yaml,
701
+ )
702
+
703
+
704
+ def _write_reviewer_report(
705
+ phase_dir: "Path",
706
+ verdict: str,
707
+ consensus_result: dict,
708
+ eval_id: str,
709
+ ) -> None:
710
+ """Emit wicked.consensus.gate_completed — projector materialises the file.
711
+
712
+ Site 3 of bus-cutover (#746, PR #799). Pre-PR-#799 this helper did
713
+ write-then-emit: it read ``report_path.exists()`` to choose
714
+ create-vs-append, wrote the file, then emitted the bus event. PR #799
715
+ deleted the source-side disk writes — the projector handler
716
+ ``_consensus_gate_completed`` is now the canonical writer AND owns the
717
+ branch decision (it reads disk state at projection time, eliminating
718
+ the source/projector race).
719
+
720
+ ``eval_id`` is threaded from ``_handle_bash_consensus`` so the
721
+ chain_id stays unique across emits in one hook invocation (bus-chain-id
722
+ dedupe gotcha). The ``branch`` payload field is set to ``"append"``
723
+ for mixed-version safety: pre-PR-#799 daemons treat ``"append"`` as
724
+ "append-if-exists, create-if-absent" — exactly the right semantics for
725
+ BOTH the first event (file absent → create) and subsequent events
726
+ (file exists → append). ``"auto"`` or any other value would fall into
727
+ the pre-#799 daemon's create/overwrite path on the second event and
728
+ clobber existing content during a mixed-version rollout. Post-#799
729
+ daemons ignore ``branch`` and decide from disk state at projection time.
730
+
731
+ ``_bus.emit_event`` is non-raising — no try/except wrapper needed here.
732
+ """
733
+ yaml_block = _build_reviewer_report_yaml(verdict, consensus_result)
734
+
735
+ # Extract project_id and phase from phase_dir for payload + chain_id.
736
+ # phase_dir layout: {project_dir}/phases/{phase}
737
+ phase = phase_dir.name
738
+ project_id = phase_dir.parents[1].name
739
+
740
+ if _bus_as_truth_flag_on():
741
+ from _bus import emit_event # noqa: PLC0415 — lazy import per hook pattern
742
+ # raw_payload = yaml_block (just this section). The projector
743
+ # decides create-vs-append by inspecting disk state at projection
744
+ # time (PR #799 — projector self-deciding, eliminates source-side
745
+ # race against the projector).
746
+ emit_event(
747
+ "wicked.consensus.gate_completed",
748
+ {
749
+ "project_id": project_id,
750
+ "phase": phase,
751
+ "verdict": verdict,
752
+ "eval_id": eval_id,
753
+ # ``branch="append"`` is mixed-version safe — pre-#799
754
+ # daemons treat append as "append-if-exists, create-if-
755
+ # absent"; post-#799 daemons ignore the hint entirely
756
+ # and decide from disk state.
757
+ "branch": "append",
758
+ "raw_payload": yaml_block,
759
+ },
760
+ chain_id=f"{project_id}.{phase}.consensus.{eval_id}",
761
+ )
762
+
763
+
764
+ def _write_pending_reviewer_report(phase_dir: "Path", eval_id: str) -> None:
765
+ """Emit wicked.consensus.gate_pending — projector materialises the file.
766
+
767
+ Fires when consensus evaluation fails. The projector handler
768
+ ``_consensus_gate_pending`` materialises the pending placeholder ONLY
769
+ when reviewer-report.md does not already exist — it never overwrites a
770
+ real result (the no-clobber invariant moved into the projector in
771
+ PR #799 along with the legacy direct-write deletion).
772
+
773
+ ``eval_id`` MUST be threaded from the caller — no consensus_result is
774
+ available on this failure path, so the caller mints eval_id at the
775
+ entry point and passes it in explicitly (Option A from the pre-impl
776
+ council).
777
+
778
+ ``_bus.emit_event`` is non-raising — no try/except wrapper needed here.
779
+ """
780
+ phase = phase_dir.name
781
+ project_id = phase_dir.parents[1].name
782
+ pending_content = _REVIEWER_REPORT_PENDING.format(reviewed_at=_now_iso())
783
+
784
+ if _bus_as_truth_flag_on():
785
+ from _bus import emit_event # noqa: PLC0415 — lazy import per hook pattern
786
+ # raw_payload = full pending template; projector writes it only if
787
+ # no reviewer-report.md exists yet (no-clobber invariant lives in
788
+ # the projector handler).
789
+ emit_event(
790
+ "wicked.consensus.gate_pending",
791
+ {
792
+ "project_id": project_id,
793
+ "phase": phase,
794
+ "eval_id": eval_id,
795
+ "raw_payload": pending_content,
796
+ },
797
+ chain_id=f"{project_id}.{phase}.consensus.{eval_id}",
798
+ )
799
+
800
+
801
+ def _handle_bash_consensus(tool_input: dict, tool_response) -> dict:
802
+ """Async consensus gate — fires after phase_manager.py approve on high-complexity projects.
803
+
804
+ FAST EARLY-EXIT: Returns immediately if the Bash command is not a
805
+ phase_manager.py approve call (the vast majority of Bash invocations).
806
+
807
+ On match:
808
+ 1. Parse project name and phase from the command.
809
+ 2. Load project state. If complexity < 5, skip.
810
+ 3. Invoke consensus_gate.evaluate_consensus_gate() directly (this process is async).
811
+ 4. Write result to phases/{phase}/reviewer-report.md.
812
+ 5. If evaluation fails, write a pending report. Never block.
813
+
814
+ """
815
+ # --- Fast early-exit: only act on phase_manager.py approve ---
816
+ command = (tool_input.get("command") or "")
817
+ if "phase_manager.py" not in command or "approve" not in command:
818
+ return {"continue": True}
819
+
820
+ # Verify the command contains both keywords in the right order
821
+ # (avoid false matches on e.g. "grep approve phase_manager.py")
822
+ pm_pos = command.find("phase_manager.py")
823
+ approve_pos = command.find("approve", pm_pos)
824
+ if pm_pos < 0 or approve_pos < 0:
825
+ return {"continue": True}
826
+
827
+ # --- Parse project name and phase from command ---
828
+ project_name, explicit_phase = _parse_project_phase_from_command(command)
829
+ if not project_name:
830
+ return {"continue": True}
831
+
832
+ # --- Load project state ---
833
+ project_state_dict, project_dir = _load_project_for_consensus(project_name)
834
+ if project_state_dict is None or project_dir is None:
835
+ return {"continue": True}
836
+
837
+ # --- Complexity gate: skip if complexity < 5 ---
838
+ try:
839
+ complexity = int(project_state_dict.get("complexity_score") or 0)
840
+ except (TypeError, ValueError):
841
+ complexity = 0
842
+
843
+ if complexity < 5:
844
+ return {"continue": True}
845
+
846
+ # --- Determine which phase was just approved ---
847
+ phase = _resolve_phase_for_consensus(project_state_dict, explicit_phase)
848
+ phase_dir = project_dir / "phases" / phase
849
+
850
+ # --- Load phases config and check if consensus is configured for this phase ---
851
+ phases_config = _load_phases_config()
852
+ phase_config = phases_config.get(phase, {})
853
+ proposers = phase_config.get("consensus_proposers", [])
854
+ consensus_threshold = phase_config.get("consensus_threshold")
855
+
856
+ # If phase has no consensus config, check for a generic threshold of 5
857
+ if not proposers:
858
+ # Default proposers for unconfigured phases at high complexity
859
+ proposers = ["engineering", "quality-engineering", "product"]
860
+ if consensus_threshold is None:
861
+ consensus_threshold = 5
862
+
863
+ # Final consensus threshold check
864
+ try:
865
+ if complexity < int(consensus_threshold):
866
+ return {"continue": True}
867
+ except (TypeError, ValueError):
868
+ pass
869
+
870
+ # --- Mint eval_id ONCE at the entry point (Finding #2 fix) ---
871
+ # Per the bus-chain-id uniqueness gotcha, every emit in this invocation
872
+ # must carry a per-invocation token. Mint here — before any try block —
873
+ # so the success path, pending path, and exception path all reuse the
874
+ # same value. eval_id is the per-INVOCATION token, not a per-RESULT
875
+ # token; consensus_result.get("eval_id") is NOT preferred here.
876
+ # Width: uuid4().hex[:16] == 64 bits (matches Site 2's PR #762 widening).
877
+ eval_id: str = uuid.uuid4().hex[:16]
878
+
879
+ # --- Run consensus evaluation ---
880
+ try:
881
+ scripts_crew = _PLUGIN_ROOT / "scripts" / "crew"
882
+ if str(scripts_crew) not in sys.path:
883
+ sys.path.insert(0, str(scripts_crew))
884
+
885
+ import consensus_gate as _cg
886
+
887
+ # Inject proposers into phases_config if not present (default fallback)
888
+ if phase not in phases_config:
889
+ phases_config[phase] = {
890
+ "consensus_threshold": consensus_threshold,
891
+ "consensus_proposers": proposers,
892
+ "strong_dissent_blocks": True,
893
+ "confidence_threshold": 0.7,
894
+ }
895
+
896
+ consensus_result = _cg.evaluate_consensus_gate(
897
+ project_dir=str(project_dir),
898
+ phase=phase,
899
+ project_state=project_state_dict,
900
+ phases_config=phases_config,
901
+ )
902
+
903
+ if consensus_result is None:
904
+ # No gate-result.json to evaluate — not an error, just nothing to do
905
+ return {"continue": True}
906
+
907
+ verdict = consensus_result.get("result", "pending").lower()
908
+ if verdict not in ("approved", "conditional", "rejected"):
909
+ # Map gate result codes to reviewer-report verdicts
910
+ verdict_map = {"approve": "approved", "reject": "rejected",
911
+ "conditional": "conditional"}
912
+ verdict = verdict_map.get(verdict, "pending")
913
+
914
+ _write_reviewer_report(phase_dir, verdict, consensus_result, eval_id)
915
+ _log("crew", "info", "consensus_gate.complete",
916
+ detail={"project": project_name, "phase": phase, "verdict": verdict,
917
+ "complexity": complexity})
918
+
919
+ except Exception as exc:
920
+ # Fail-open: write pending report, log error, never crash.
921
+ # Reuse the eval_id minted at the entry point — never mint a second
922
+ # UUID here. This ensures the exception-path chain_id is consistent
923
+ # with what the success path would have emitted.
924
+ try:
925
+ _write_pending_reviewer_report(phase_dir, eval_id)
926
+ _log("crew", "warn", "consensus_gate.error",
927
+ ok=False, detail={"project": project_name, "phase": phase,
928
+ "error": str(exc)[:200]})
929
+ except Exception:
930
+ pass
931
+
932
+ return {"continue": True}
933
+
934
+
935
+ # ---------------------------------------------------------------------------
936
+ # Handler: Bash (general activity tracking)
937
+ # ---------------------------------------------------------------------------
938
+
939
+ # Theme 9: word-boundary check for search-tool invocations. The discovery
940
+ # hint must NOT fire on benign references like ``echo "use grep instead"``
941
+ # or comments. A real invocation appears at start-of-line/command-start or
942
+ # after a shell separator (``;``, ``&&``, ``||``, ``|``, ``&``, newline).
943
+ #
944
+ # Precision rules:
945
+ # * Allow optional leading whitespace after ``^`` and after each separator,
946
+ # so ``" grep pattern"`` and ``"ls; grep foo"`` both match.
947
+ # * Use ``\b`` before each tool name — without it, ``"ls; tag"`` would
948
+ # false-match ``ag`` inside ``tag``.
949
+ # * Accept multi-char ``&&`` and ``||`` as well as single ``;`` ``|`` ``&``;
950
+ # ``&&`` must precede ``&`` (and ``||`` precede ``|``) in the alternation
951
+ # so the longer separator wins the leftmost-match.
952
+ _SEARCH_INVOCATION_RE = re.compile(
953
+ r"(?:^\s*|[\n;]\s*|&&\s*|\|\|\s*|[|&]\s*)\b(?:grep|rg|ripgrep|ag)\b",
954
+ )
955
+
956
+
957
+ def _handle_bash(tool_input: dict, tool_response) -> dict:
958
+ """Track bash activity.
959
+
960
+ The claim sentinel used to ref-watch here on every Bash call; it moved to
961
+ the Stop hook where it is claim-gated (fires only when the turn's final
962
+ message asserts done/passing/shipped). See scripts/sentinel/invariants.py
963
+ ::claim_tick and hooks/scripts/stop.py. Publish-time protection (an actual
964
+ push to main or a tag) remains the block tier in scripts/sentinel/pre_push.py.
965
+ """
966
+ try:
967
+ from _session import SessionState
968
+ state = SessionState.load()
969
+ bash_count = (getattr(state, "bash_count", None) or 0) + 1
970
+ state.update(bash_count=bash_count)
971
+ state.save()
972
+ except Exception:
973
+ pass
974
+ return {"continue": True}
975
+
976
+
977
+ # ---------------------------------------------------------------------------
978
+ # Handler: PostToolUseFailure
979
+ # (failure counting + threshold alerts)
980
+ # ---------------------------------------------------------------------------
981
+
982
+ _FAILURE_THRESHOLD_DEFAULT = 3
983
+ _MISMATCH_SIGNALS = [
984
+ "failed", "not working", "broken", "error", "couldn't", "unable", "blocked"
985
+ ]
986
+
987
+ # Patterns that indicate a missing CLI or Python module
988
+ _MISSING_TOOL_RE = [
989
+ re.compile(r"command not found:\s*(\S+)"),
990
+ re.compile(r"not found:\s*(\S+)"),
991
+ ]
992
+ _MISSING_MODULE_RE = [
993
+ re.compile(r"ModuleNotFoundError: No module named ['\"](\S+?)['\"]"),
994
+ re.compile(r"ImportError: No module named ['\"](\S+?)['\"]"),
995
+ ]
996
+
997
+
998
+ def _detect_missing_tool(error_text: str) -> str | None:
999
+ """Check if error indicates a missing tool/module. Returns a prereq-doctor hint or None."""
1000
+ if not error_text:
1001
+ return None
1002
+ try:
1003
+ plugin_root = Path(os.environ.get("CLAUDE_PLUGIN_ROOT", ".")).resolve()
1004
+ doctor = plugin_root / "scripts" / "platform" / "prereq_doctor.py"
1005
+ if not doctor.exists():
1006
+ return None
1007
+
1008
+ import subprocess as _sp
1009
+ proc = _sp.run(
1010
+ [sys.executable, str(doctor), "diagnose", error_text],
1011
+ capture_output=True, text=True, timeout=5,
1012
+ )
1013
+ if proc.returncode != 0:
1014
+ return None
1015
+ diag = json.loads(proc.stdout)
1016
+ if diag.get("error_type") == "unknown":
1017
+ return None
1018
+
1019
+ # Build a concise hint for the model
1020
+ if diag.get("fix") == "uv_sync":
1021
+ return (
1022
+ f"[Prereq Doctor] Missing Python module '{diag.get('module')}'. "
1023
+ f"Fix: run `uv sync` from the plugin root. "
1024
+ f"Or use: Skill(skill=\"wicked-garden:platform:prereq-doctor\", args=\"check uv\")"
1025
+ )
1026
+ if diag.get("status") == "missing":
1027
+ install_cmd = diag.get("install_cmd", "")
1028
+ return (
1029
+ f"[Prereq Doctor] CLI '{diag.get('cli', diag.get('tool'))}' not installed. "
1030
+ f"Install with: `{install_cmd}` — "
1031
+ f"Ask the user before installing. "
1032
+ f"Or use: Skill(skill=\"wicked-garden:platform:prereq-doctor\", args=\"check {diag.get('tool')}\")"
1033
+ )
1034
+ return None
1035
+ except Exception:
1036
+ return None
1037
+
1038
+
1039
+ def _handle_failure(payload: dict) -> dict:
1040
+ """PostToolUseFailure: detect missing tools, count failures, queue issue at threshold."""
1041
+ tool_name = payload.get("tool_name", "unknown")
1042
+ tool_error = payload.get("tool_error", "") or payload.get("tool_use_error", "")
1043
+
1044
+ # --- Missing tool detection (fires immediately, no threshold) ---
1045
+ prereq_hint = _detect_missing_tool(str(tool_error))
1046
+ if prereq_hint:
1047
+ return {"continue": True, "systemMessage": prereq_hint}
1048
+
1049
+ # --- General failure counting (existing behavior) ---
1050
+ try:
1051
+ threshold = int(os.environ.get("WICKED_ISSUE_THRESHOLD", str(_FAILURE_THRESHOLD_DEFAULT)))
1052
+ except (ValueError, TypeError):
1053
+ threshold = _FAILURE_THRESHOLD_DEFAULT
1054
+
1055
+ try:
1056
+ from _session import SessionState
1057
+ state = SessionState.load()
1058
+ counts = state.failure_counts or {}
1059
+ counts[tool_name] = counts.get(tool_name, 0) + 1
1060
+ current_count = counts[tool_name]
1061
+ state.update(failure_counts=counts)
1062
+
1063
+ if current_count >= threshold:
1064
+ record = {
1065
+ "type": "tool_failure",
1066
+ "tool": tool_name,
1067
+ "count": current_count,
1068
+ "last_error": str(tool_error)[:500],
1069
+ "session_id": _get_session_id(),
1070
+ "ts": _now_iso(),
1071
+ }
1072
+ pending = state.pending_issues or []
1073
+ pending.append(record)
1074
+ counts[tool_name] = 0
1075
+ state.update(failure_counts=counts, pending_issues=pending)
1076
+
1077
+ return {
1078
+ "continue": True,
1079
+ "systemMessage": (
1080
+ f"[Issue Reporter] {current_count} {tool_name} failures — issue queued."
1081
+ ),
1082
+ }
1083
+ except Exception:
1084
+ pass
1085
+
1086
+ return {"continue": True}
1087
+
1088
+
1089
+ def _handle_task_update_mismatch(tool_input: dict) -> dict:
1090
+ """Detect task completion mismatch signals (task marked done but looks blocked)."""
1091
+ if tool_input.get("status") != "completed":
1092
+ return {"continue": True}
1093
+
1094
+ subject = tool_input.get("subject", "") or ""
1095
+ description = tool_input.get("description", "") or ""
1096
+ task_id = tool_input.get("taskId", "") or ""
1097
+
1098
+ combined = (subject + " " + description).lower()
1099
+ found_signal = next((s for s in _MISMATCH_SIGNALS if s in combined), None)
1100
+
1101
+ if found_signal:
1102
+ record = {
1103
+ "type": "task_mismatch",
1104
+ "task_id": task_id,
1105
+ "subject": subject[:200],
1106
+ "signal": found_signal,
1107
+ "detail": description[:300],
1108
+ "ts": _now_iso(),
1109
+ }
1110
+ try:
1111
+ from _session import SessionState
1112
+ state = SessionState.load()
1113
+ pending = state.pending_issues or []
1114
+ pending.append(record)
1115
+ state.update(pending_issues=pending)
1116
+ except Exception:
1117
+ pass
1118
+
1119
+ return {"continue": True}
1120
+
1121
+
1122
+ # ---------------------------------------------------------------------------
1123
+ # Observability trace writer
1124
+ # ---------------------------------------------------------------------------
1125
+
1126
+ def _write_trace(payload: dict) -> None:
1127
+ """Write a trace entry to a session-scoped JSONL file in $TMPDIR."""
1128
+ if os.environ.get("WICKED_TRACE_ACTIVE"):
1129
+ return
1130
+ try:
1131
+ os.environ["WICKED_TRACE_ACTIVE"] = "1"
1132
+ session_id = _get_session_id()
1133
+ tool_name = payload.get("tool_name", "")
1134
+ event = payload.get("hook_event_name", "PostToolUse")
1135
+ ts = _now_iso()
1136
+
1137
+ entry = {
1138
+ "ts": ts,
1139
+ "session_id": session_id,
1140
+ "tool": tool_name,
1141
+ "event": event,
1142
+ }
1143
+
1144
+ tmpdir = os.environ.get("TMPDIR") or __import__("tempfile").gettempdir()
1145
+ trace_file = Path(tmpdir) / f"wicked-trace-{session_id}.jsonl"
1146
+ with open(trace_file, "a", encoding="utf-8") as f:
1147
+ f.write(json.dumps(entry) + "\n")
1148
+ except Exception:
1149
+ pass
1150
+ finally:
1151
+ os.environ.pop("WICKED_TRACE_ACTIVE", None)
1152
+
1153
+
1154
+ # ---------------------------------------------------------------------------
1155
+ # Turn progress visibility (Issue #323)
1156
+ # ---------------------------------------------------------------------------
1157
+
1158
+ _TURN_STATUS_THRESHOLD_SECS = 120 # 2 minutes
1159
+
1160
+
1161
+ def _check_turn_progress(tool_name: str) -> str | None:
1162
+ """Increment turn tool count and return a status note if the turn is long-running.
1163
+
1164
+ Returns a status string when elapsed time exceeds _TURN_STATUS_THRESHOLD_SECS,
1165
+ or None if the turn is still short, state is unavailable, or the [Status]
1166
+ banner has already fired this turn (v9.2.12 per-turn latch).
1167
+
1168
+ v9.2.12: Pre-fix, the [Status] banner re-fired on every PostToolUse once
1169
+ the 2-minute threshold tripped — meaning every long turn surfaced N
1170
+ redundant "running for X min, Y tool calls so far" banners (one per
1171
+ subsequent tool call). The `last_status_turn` latch (declared on
1172
+ SessionState, written here) suppresses re-emission within the same turn.
1173
+ A new turn (turn_count incremented by prompt_submit) re-arms the banner.
1174
+ """
1175
+ try:
1176
+ from _session import SessionState
1177
+ state = SessionState.load()
1178
+
1179
+ # Increment tool count
1180
+ new_count = (state.turn_tool_count or 0) + 1
1181
+ state.update(turn_tool_count=new_count)
1182
+
1183
+ # Check elapsed time
1184
+ turn_start = state.turn_start_ts
1185
+ if not turn_start:
1186
+ return None
1187
+
1188
+ start_dt = datetime.fromisoformat(turn_start.replace("Z", "+00:00"))
1189
+ now_dt = datetime.now(timezone.utc)
1190
+ elapsed_secs = (now_dt - start_dt).total_seconds()
1191
+
1192
+ if elapsed_secs < _TURN_STATUS_THRESHOLD_SECS:
1193
+ return None
1194
+
1195
+ # Per-turn latch (v9.2.12): suppress re-emission once shown this turn.
1196
+ current_turn = state.turn_count or 0
1197
+ if current_turn and current_turn == (state.last_status_turn or 0):
1198
+ return None
1199
+ state.update(last_status_turn=current_turn)
1200
+
1201
+ elapsed_mins = int(elapsed_secs / 60)
1202
+
1203
+ # Build status note
1204
+ parts = [
1205
+ f"[Status] This turn has been running for {elapsed_mins} "
1206
+ f"minute{'s' if elapsed_mins != 1 else ''}. "
1207
+ f"{new_count} tool call{'s' if new_count != 1 else ''} so far."
1208
+ ]
1209
+
1210
+ # Include active crew phase/specialist if available
1211
+ active_project = state.active_project
1212
+ if active_project and isinstance(active_project, dict):
1213
+ phase = active_project.get("current_phase", "")
1214
+ specialist = active_project.get("current_specialist", "")
1215
+ if phase:
1216
+ detail = f"Active crew phase: {phase}"
1217
+ if specialist:
1218
+ detail += f" ({specialist})"
1219
+ parts.append(detail)
1220
+
1221
+ return " ".join(parts)
1222
+ except Exception:
1223
+ return None
1224
+
1225
+
1226
+ # ---------------------------------------------------------------------------
1227
+ # Latency profiling (Issue #312) — cumulative PostToolUse timing in SessionState
1228
+ # ---------------------------------------------------------------------------
1229
+
1230
+ def _record_latency(handler_label: str, handler_ms: int, total_ms: int) -> None:
1231
+ """Accumulate PostToolUse latency in SessionState for profiling.
1232
+
1233
+ Updates three fields:
1234
+ - post_tool_total_ms: cumulative wall-clock time across all invocations
1235
+ - post_tool_call_count: total invocation count
1236
+ - post_tool_handler_ms: per-handler cumulative ms (keyed by handler_label)
1237
+
1238
+ Fails silently — never crashes the hook.
1239
+ """
1240
+ try:
1241
+ from _session import SessionState
1242
+ state = SessionState.load()
1243
+ state.post_tool_total_ms = (state.post_tool_total_ms or 0) + total_ms
1244
+ state.post_tool_call_count = (state.post_tool_call_count or 0) + 1
1245
+ handler_breakdown = state.post_tool_handler_ms or {}
1246
+ handler_breakdown[handler_label] = handler_breakdown.get(handler_label, 0) + handler_ms
1247
+ state.post_tool_handler_ms = handler_breakdown
1248
+ state.save()
1249
+ except Exception:
1250
+ pass
1251
+
1252
+
1253
+ # ---------------------------------------------------------------------------
1254
+ # Main dispatcher
1255
+ # ---------------------------------------------------------------------------
1256
+
1257
+ def main():
1258
+ _t0 = time.monotonic()
1259
+
1260
+ try:
1261
+ raw = sys.stdin.read()
1262
+ payload = json.loads(raw) if raw.strip() else {}
1263
+ except Exception:
1264
+ print(json.dumps({"continue": True}))
1265
+ return
1266
+
1267
+ try:
1268
+ tool_name = payload.get("tool_name", "")
1269
+ tool_input = payload.get("tool_input", {}) or {}
1270
+ has_error = "tool_error" in payload or "tool_use_error" in payload
1271
+
1272
+ _log("posttool", "debug", "hook.start", detail={"tool": tool_name})
1273
+
1274
+ # Write observability trace (low-priority, always runs)
1275
+ _t_trace = time.monotonic()
1276
+ _write_trace(payload)
1277
+ _trace_ms = int((time.monotonic() - _t_trace) * 1000)
1278
+
1279
+ # --- Route by event type and tool name ---
1280
+ handler_label = tool_name or "unknown"
1281
+ _t_handler = time.monotonic()
1282
+
1283
+ # PostToolUseFailure path
1284
+ if has_error:
1285
+ handler_label = "failure"
1286
+ result = _handle_failure(payload)
1287
+ # Task-management tools — native task store is the source of truth.
1288
+ # PreToolUse validates metadata; PostToolUse only runs the mismatch
1289
+ # detector for TaskUpdate (status/subject/metadata consistency).
1290
+ # v10 Phase 3 (#813 successor): also append a cross-session audit
1291
+ # entry so verify_chain_emission can find tasks created in earlier
1292
+ # sessions. The writer fails-open — any I/O error swallowed.
1293
+ elif tool_name in ("TaskCreate", "TaskUpdate", "TodoWrite"):
1294
+ handler_label = "TaskCreate|TaskUpdate|TodoWrite"
1295
+ if tool_name == "TaskUpdate":
1296
+ _handle_task_update_mismatch(tool_input)
1297
+ if tool_name in ("TaskCreate", "TaskUpdate"):
1298
+ try:
1299
+ from crew._task_audit_writer import append_task_audit
1300
+ append_task_audit(
1301
+ tool_name=tool_name,
1302
+ tool_input=tool_input,
1303
+ tool_response=payload.get("tool_response"),
1304
+ session_id=_get_session_id(),
1305
+ )
1306
+ except Exception:
1307
+ pass # fail-open per Phase 3 contract
1308
+ result = {"continue": True}
1309
+ # Write / Edit tools (async — quick operations only)
1310
+ elif tool_name in ("Write", "Edit"):
1311
+ handler_label = "Write|Edit"
1312
+ result = _handle_write_edit(tool_input)
1313
+ # Task (subagent dispatch + permission failure detection)
1314
+ elif tool_name == "Task":
1315
+ tool_response = payload.get("tool_response", {})
1316
+ result = _handle_task_dispatch(tool_input, tool_response)
1317
+ # Read (large-file warning + framework detection)
1318
+ elif tool_name == "Read":
1319
+ tool_response = payload.get("tool_response", "")
1320
+ result = _handle_read(tool_input, tool_response)
1321
+ # Bash (activity tracking + discovery hints + async consensus gate)
1322
+ elif tool_name == "Bash":
1323
+ tool_response = payload.get("tool_response", {})
1324
+ result = _handle_bash(tool_input, tool_response)
1325
+ # Tier 2 consensus gate (Issue #368) — fast-exits unless phase_manager approve
1326
+ _handle_bash_consensus(tool_input, tool_response)
1327
+ # Skill (memory compliance counter reset + pull-model tracking)
1328
+ elif tool_name == "Skill":
1329
+ result = _handle_skill(tool_input)
1330
+ # Grep / Glob (discovery hints for search commands)
1331
+ elif tool_name in ("Grep", "Glob"):
1332
+ handler_label = "Grep|Glob"
1333
+ result = _handle_grep_glob(tool_name, tool_input)
1334
+ # All other tools — pass through
1335
+ else:
1336
+ handler_label = "passthrough"
1337
+ result = {"continue": True}
1338
+
1339
+ _handler_ms = int((time.monotonic() - _t_handler) * 1000)
1340
+
1341
+ # Turn progress visibility (Issue #323): append status note on long turns
1342
+ _t_turn = time.monotonic()
1343
+ turn_status = _check_turn_progress(tool_name)
1344
+ _turn_ms = int((time.monotonic() - _t_turn) * 1000)
1345
+ if turn_status:
1346
+ existing_msg = result.get("systemMessage", "")
1347
+ if existing_msg:
1348
+ result["systemMessage"] = existing_msg + "\n" + turn_status
1349
+ else:
1350
+ result["systemMessage"] = turn_status
1351
+
1352
+ # --- Latency profiling (Issue #312) ---
1353
+ _total_ms = int((time.monotonic() - _t0) * 1000)
1354
+ _record_latency(handler_label, _handler_ms, _total_ms)
1355
+
1356
+ _log("posttool", "verbose", "hook.latency", ms=_total_ms, detail={
1357
+ "tool": tool_name,
1358
+ "handler": handler_label,
1359
+ "handler_ms": _handler_ms,
1360
+ "trace_ms": _trace_ms,
1361
+ "turn_progress_ms": _turn_ms,
1362
+ "total_ms": _total_ms,
1363
+ })
1364
+
1365
+ _log("posttool", "debug", "hook.end", ms=_total_ms)
1366
+ print(json.dumps(result))
1367
+
1368
+ except Exception as e:
1369
+ print(f"[wicked-garden] post_tool error: {e}", file=sys.stderr)
1370
+ print(json.dumps({"continue": True}))
1371
+
1372
+
1373
+ if __name__ == "__main__":
1374
+ main()