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,1797 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ SessionStart hook — wicked-garden unified bootstrap.
4
+
5
+ Consolidates: crew session_start, mem session_start,
6
+ search session_start, smaht session_start.
7
+
8
+ Flow:
9
+ 1. Read config from ~/.something-wicked/wicked-garden/config.json
10
+ 2. If missing or setup_complete=false: emit setup instructions, return
11
+ 3. Load dynamic agents
12
+ 4. Load active crew project
13
+ 5. Run memory decay
14
+ 6. Check onboarding status → imperative directive if needed
15
+ 7. Assemble session briefing
16
+ 8. Return {"continue": true, "systemMessage": "<briefing>"}
17
+
18
+ Always fails open — any unhandled exception returns {"continue": true}.
19
+ """
20
+
21
+ import json
22
+ import os
23
+ import subprocess
24
+ import sys
25
+ import time
26
+ from datetime import datetime, timezone
27
+ from pathlib import Path
28
+
29
+ # Add shared scripts directory to path so hook can import shared modules
30
+ _PLUGIN_ROOT = Path(os.environ.get("CLAUDE_PLUGIN_ROOT", Path(__file__).resolve().parents[2]))
31
+ sys.path.insert(0, str(_PLUGIN_ROOT / "scripts"))
32
+
33
+ from _brain_port import resolve_port
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Ops logger wrapper — fail-silent, never crashes the hook
37
+ # ---------------------------------------------------------------------------
38
+
39
+ def _log(domain, level, event, ok=True, ms=None, detail=None):
40
+ """Ops logger — fail-silent, never crashes the hook."""
41
+ try:
42
+ from _logger import log
43
+ log(domain, level, event, ok=ok, ms=ms, detail=detail)
44
+ except Exception:
45
+ pass
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Shared module imports — each wrapped so missing modules degrade gracefully
50
+ # ---------------------------------------------------------------------------
51
+
52
+ def _load_session_state():
53
+ try:
54
+ from _session import SessionState
55
+ return SessionState.load()
56
+ except Exception:
57
+ return None
58
+
59
+
60
+ def _save_session_state(state):
61
+ try:
62
+ state.save()
63
+ except Exception:
64
+ pass
65
+
66
+
67
+ def _check_prior_guard_findings():
68
+ """Load the most recent guard-pipeline findings and render a briefing note.
69
+
70
+ Reads ${TMPDIR}/wicked-guard/*/findings.json files written by stop.py's
71
+ guard pipeline (Issue #448). Returns a briefing string or None.
72
+
73
+ Fail-open — never blocks bootstrap.
74
+ """
75
+ try:
76
+ import tempfile
77
+ guard_root = Path(tempfile.gettempdir()) / "wicked-guard"
78
+ if not guard_root.is_dir():
79
+ return None
80
+
81
+ newest = None # type: ignore
82
+ newest_mtime = 0.0
83
+ for session_dir in guard_root.iterdir():
84
+ if not session_dir.is_dir():
85
+ continue
86
+ f = session_dir / "findings.json"
87
+ if not f.is_file():
88
+ continue
89
+ try:
90
+ mt = f.stat().st_mtime
91
+ except OSError:
92
+ continue
93
+ if mt > newest_mtime:
94
+ newest_mtime = mt
95
+ newest = f
96
+
97
+ if newest is None:
98
+ return None
99
+
100
+ # Only surface if report is recent (<24h) — stale reports are noise.
101
+ if (time.time() - newest_mtime) > 86400:
102
+ return None
103
+
104
+ with open(newest, "r", encoding="utf-8") as fh:
105
+ report = json.load(fh)
106
+
107
+ total = int(report.get("total_findings", 0) or 0)
108
+ if total == 0:
109
+ return None
110
+
111
+ by_sev = report.get("findings_by_severity", {}) or {}
112
+ profile = report.get("profile", "?")
113
+ lines = [
114
+ f"[Guard] Last session's {profile} guard pipeline surfaced {total} finding(s) "
115
+ f"(block={by_sev.get('block', 0)} warn={by_sev.get('warn', 0)} info={by_sev.get('info', 0)}). "
116
+ f"Report: {newest}"
117
+ ]
118
+ # Show up to 5 non-info findings for visibility
119
+ shown = 0
120
+ for check in report.get("checks", []) or []:
121
+ for f in check.get("findings", []) or []:
122
+ if shown >= 5:
123
+ break
124
+ sev = f.get("severity", "info")
125
+ if sev == "info":
126
+ continue
127
+ msg = f.get("message", "")[:140]
128
+ loc = ""
129
+ if f.get("file"):
130
+ loc = f" @ {f['file']}"
131
+ if f.get("line"):
132
+ loc += f":{f['line']}"
133
+ lines.append(f" - [{sev}] {f.get('rule_id', '?')}: {msg}{loc}")
134
+ shown += 1
135
+ if shown >= 5:
136
+ break
137
+ return "\n".join(lines)
138
+ except Exception:
139
+ return None
140
+
141
+
142
+ def _probe_plugin_readiness():
143
+ """Dynamically discover installed plugins and probe their hook readiness.
144
+
145
+ Scans the plugin cache for installed plugins (excluding ourselves), finds
146
+ their hooks.json, and runs a lightweight test of each hook command.
147
+ Returns a list of dicts for plugins whose hooks fail, e.g.:
148
+ [{"name": "semgrep", "hook": "PostToolUse", "error": "No SEMGREP_APP_TOKEN found"}]
149
+
150
+ Runs with a tight timeout (2s per probe) and fails open — never blocks bootstrap.
151
+ """
152
+ unready = []
153
+ try:
154
+ # Discover plugin cache directories
155
+ cache_dirs = [
156
+ Path.home() / ".claude" / "plugins" / "cache",
157
+ Path(os.environ.get("CLAUDE_CONFIG_DIR", "")) / "plugins" / "cache" if os.environ.get("CLAUDE_CONFIG_DIR") else None,
158
+ ]
159
+ # Also check alt-configs location
160
+ alt_config = Path.home() / "alt-configs" / ".claude" / "plugins" / "cache"
161
+ if alt_config.exists():
162
+ cache_dirs.append(alt_config)
163
+
164
+ our_name = "wicked-garden"
165
+ seen_plugins = set()
166
+
167
+ for cache_dir in cache_dirs:
168
+ if cache_dir is None or not cache_dir.exists():
169
+ continue
170
+ # Walk plugin org/name directories
171
+ for org_dir in cache_dir.iterdir():
172
+ if not org_dir.is_dir():
173
+ continue
174
+ for plugin_dir in org_dir.iterdir():
175
+ if not plugin_dir.is_dir():
176
+ continue
177
+ # Find the actual plugin root (may be versioned)
178
+ hooks_candidates = list(plugin_dir.rglob("hooks/hooks.json"))
179
+ for hooks_file in hooks_candidates:
180
+ plugin_root = hooks_file.parent.parent
181
+ # Read plugin.json to get name
182
+ pj = plugin_root / ".claude-plugin" / "plugin.json"
183
+ if not pj.exists():
184
+ pj = plugin_root / "plugin.json"
185
+ plugin_name = plugin_dir.name
186
+ if pj.exists():
187
+ try:
188
+ plugin_name = json.loads(pj.read_text()).get("name", plugin_dir.name)
189
+ except Exception:
190
+ pass
191
+
192
+ # Skip ourselves and already-checked plugins
193
+ if plugin_name == our_name or plugin_name in seen_plugins:
194
+ continue
195
+ seen_plugins.add(plugin_name)
196
+
197
+ # Read their hooks.json
198
+ try:
199
+ hooks_data = json.loads(hooks_file.read_text())
200
+ except Exception:
201
+ continue
202
+
203
+ # Probe each hook command with a dry-run (empty stdin, 2s timeout)
204
+ for event, entries in hooks_data.get("hooks", {}).items():
205
+ for entry in entries:
206
+ for hook in entry.get("hooks", []):
207
+ cmd = hook.get("command", "")
208
+ if not cmd or hook.get("type") != "command":
209
+ continue
210
+ # Extract the base binary name for the probe
211
+ parts = cmd.split()
212
+ if not parts:
213
+ continue
214
+ binary = parts[0]
215
+ # Skip if binary references plugin root (needs env var)
216
+ if "${CLAUDE_PLUGIN_ROOT}" in cmd:
217
+ continue
218
+ # Quick probe: just check if the binary is available and
219
+ # runs without error when given empty input
220
+ try:
221
+ result = subprocess.run(
222
+ parts + ["--help"] if len(parts) == 1 else parts[:2] + ["--version"],
223
+ capture_output=True, text=True, timeout=2,
224
+ input="",
225
+ )
226
+ # If the command itself isn't found, skip — that's a
227
+ # different problem (missing install, not unready)
228
+ except FileNotFoundError:
229
+ continue # Binary not installed — not our problem
230
+ except subprocess.TimeoutExpired:
231
+ continue # Timed out — assume it works but is slow
232
+ except Exception:
233
+ continue
234
+
235
+ # For env-var-gated tools, check common auth patterns
236
+ # by looking at stderr for auth/token messages
237
+ if result.returncode != 0:
238
+ stderr = result.stderr.lower()
239
+ auth_signals = ["token", "login", "auth", "credential", "api_key", "api key"]
240
+ if any(sig in stderr for sig in auth_signals):
241
+ # Extract a short error message
242
+ err_lines = [l.strip() for l in result.stderr.strip().split("\n") if l.strip()]
243
+ short_err = err_lines[-1][:120] if err_lines else "auth required"
244
+ unready.append({
245
+ "name": plugin_name,
246
+ "hook": event,
247
+ "command": cmd[:80],
248
+ "error": short_err,
249
+ })
250
+ break # One failure per plugin is enough
251
+ break # Only test first hook per event
252
+ if any(u["name"] == plugin_name for u in unready):
253
+ break # Already flagged this plugin
254
+ except Exception:
255
+ pass # Fail open — probe errors never block bootstrap
256
+ return unready
257
+
258
+
259
+ def _suggest_auth_fix(probe_result):
260
+ """Dynamically extract or infer an auth fix command from a probe failure.
261
+
262
+ Looks for common patterns in the error message (e.g., "please login", "run X login",
263
+ "set SOME_TOKEN") and returns the fix command. No hardcoded tool list — works by
264
+ parsing the error text that the tool itself produced.
265
+
266
+ Returns a fix command string, or None if no fix can be inferred.
267
+ """
268
+ error = probe_result.get("error", "")
269
+ name = probe_result.get("name", "")
270
+ cmd_base = probe_result.get("command", "").split()[0] if probe_result.get("command") else name
271
+
272
+ # Pattern 1: error says "run '<tool> login'" or "please login"
273
+ # e.g., "please login to Semgrep" → "semgrep login"
274
+ import re
275
+ login_match = re.search(r"(?:run|please)\s+['\"]?(\w[\w-]*\s+(?:login|auth\s+login))['\"]?", error, re.IGNORECASE)
276
+ if login_match:
277
+ return login_match.group(1)
278
+
279
+ # Pattern 2: error mentions a specific env var
280
+ # e.g., "No SEMGREP_APP_TOKEN found" → "export SEMGREP_APP_TOKEN=<your-token>"
281
+ env_match = re.search(r"(?:no|missing|set|need)\s+(\w+_(?:TOKEN|KEY|SECRET|API_KEY))\b", error, re.IGNORECASE)
282
+ if env_match:
283
+ var = env_match.group(1)
284
+ return f"export {var}=<your-token> # or: {cmd_base} login"
285
+
286
+ # Pattern 3: generic "login" or "auth" mention
287
+ if "login" in error.lower() or "auth" in error.lower():
288
+ return f"{cmd_base} login"
289
+
290
+ return None
291
+
292
+
293
+ def _load_agents():
294
+ """Load dynamic agents from disk. Returns (agent_count, agents_dict)."""
295
+ try:
296
+ from _agents import AgentLoader
297
+ loader = AgentLoader()
298
+ agents = loader.load_disk_agents(_PLUGIN_ROOT / "agents")
299
+ return len(agents), agents
300
+ except Exception as e:
301
+ print(f"[wicked-garden] agent load error: {e}", file=sys.stderr)
302
+ return 0, {}
303
+
304
+
305
+ def _resolve_capabilities(agents, config=None):
306
+ """Run capability resolution for pre-loaded agents.
307
+
308
+ Returns dict[agent_name, tool_list] or empty dict on any error.
309
+ Fails open — never blocks bootstrap.
310
+ """
311
+ try:
312
+ from _capability_resolver import resolve_all_agents, discover_mcp_servers
313
+
314
+ mcp_servers = discover_mcp_servers()
315
+ return resolve_all_agents(agents, config, mcp_servers)
316
+ except Exception as e:
317
+ print(f"[wicked-garden] capability resolution error: {e}", file=sys.stderr)
318
+ return {}
319
+
320
+
321
+ def _find_active_crew_project(workspace: str = ""):
322
+ """Return (project_data, project_name) for most recently updated active crew project
323
+ scoped to the given workspace.
324
+
325
+ Only returns projects whose ``workspace`` field matches the current folder.
326
+ Projects without a workspace field (legacy) are never auto-selected.
327
+
328
+ Uses DomainStore for consistent data access. Falls back to filesystem
329
+ scan if DomainStore fails (legacy compat for one release).
330
+ """
331
+ try:
332
+ from _domain_store import DomainStore
333
+ ds = DomainStore("wicked-crew", hook_mode=True)
334
+ projects = ds.list("projects") or []
335
+ # Sort by updated_at descending, return first non-archived, non-complete
336
+ # that matches the current workspace
337
+ for p in sorted(projects, key=lambda x: x.get("updated_at", ""), reverse=True):
338
+ if p.get("archived"):
339
+ continue
340
+ # Workspace scoping — skip projects from other workspaces
341
+ if workspace and p.get("workspace", "") != workspace:
342
+ continue
343
+ phase = p.get("current_phase", "")
344
+ if phase and phase != "complete":
345
+ return p, p.get("name")
346
+ return None, None
347
+ except Exception:
348
+ pass
349
+
350
+ # Legacy fallback: direct filesystem scan (remove after one release)
351
+ return _find_active_crew_project_legacy(workspace)
352
+
353
+
354
+ def _find_active_crew_project_legacy(workspace: str = ""):
355
+ """Filesystem-based crew project scan. Legacy fallback."""
356
+ projects_dir = Path.home() / ".something-wicked" / "wicked-crew" / "projects"
357
+ if not projects_dir.exists():
358
+ return None, None
359
+
360
+ for pf in sorted(
361
+ projects_dir.glob("*/project.json"),
362
+ key=lambda p: p.stat().st_mtime,
363
+ reverse=True,
364
+ ):
365
+ try:
366
+ data = json.loads(pf.read_text())
367
+ if data.get("archived"):
368
+ continue
369
+ # Workspace scoping — skip projects from other workspaces
370
+ if workspace and data.get("workspace", "") != workspace:
371
+ continue
372
+ phase = data.get("current_phase", "")
373
+ if phase and phase != "complete":
374
+ return data, data.get("name", pf.parent.name)
375
+ except (json.JSONDecodeError, OSError):
376
+ continue
377
+
378
+ return None, None
379
+
380
+
381
+ def _brain_api(action, params=None, timeout=3):
382
+ """Call brain API. Returns parsed JSON or None."""
383
+ try:
384
+ import urllib.request
385
+ port = resolve_port()
386
+ payload = json.dumps({"action": action, "params": params or {}}).encode("utf-8")
387
+ req = urllib.request.Request(
388
+ f"http://localhost:{port}/api",
389
+ data=payload,
390
+ headers={"Content-Type": "application/json"},
391
+ method="POST",
392
+ )
393
+ with urllib.request.urlopen(req, timeout=timeout) as resp: # nosem: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected -- URL is f"http://localhost:{port}/api" with int-validated port from resolve_port()
394
+ return json.loads(resp.read().decode("utf-8"))
395
+ except Exception:
396
+ return None
397
+
398
+
399
+ def _run_memory_decay():
400
+ """Run memory decay maintenance via brain lint. Returns a summary string or None."""
401
+ try:
402
+ result = _brain_api("lint", {}, timeout=5)
403
+ if result and (result.get("archived", 0) > 0 or result.get("deleted", 0) > 0):
404
+ return f"Memory: {result.get('archived', 0)} archived, {result.get('deleted', 0)} cleaned"
405
+ return None
406
+ except Exception:
407
+ return None
408
+
409
+
410
+ def _check_search_staleness():
411
+ """Check brain index health. Returns a briefing note or None.
412
+
413
+ Brain is the unified knowledge layer — no legacy SQLite fallback.
414
+ Fails open — any exception returns None so the session always continues.
415
+ """
416
+ stats = _brain_api("stats", {}, timeout=2)
417
+ if stats is None:
418
+ # Deterministic auto-start — server lifecycle is a hook job, not a
419
+ # model directive. ensure_server is lock-safe and fails open.
420
+ try:
421
+ from _brain_port import ensure_server
422
+ if ensure_server():
423
+ stats = _brain_api("stats", {}, timeout=2)
424
+ except Exception:
425
+ pass
426
+ if stats is None:
427
+ return (
428
+ "Brain server unreachable and auto-start failed — run "
429
+ "`wicked-brain-call --start` to see the cause. Brain skills "
430
+ "auto-start the server on every call; do NOT skip brain usage."
431
+ )
432
+ if stats.get("total", 0) == 0:
433
+ return "Brain index is empty — run `wicked-brain:ingest` to index your codebase"
434
+ return None # Brain is healthy
435
+
436
+
437
+ def _check_onboarding_status():
438
+ """Check if the current project has been onboarded (search index + memories).
439
+
440
+ Returns (has_index, has_memories, directive_or_none).
441
+ Fails open when the brain is unreachable: transient API errors must not be
442
+ interpreted as "this project was never onboarded". Per-install setup state
443
+ is enforced separately by _check_setup_gate in prompt_submit.py — that
444
+ flag is user-level (~/.something-wicked/wicked-garden/config.json), not
445
+ per-project, so it cannot answer the per-project onboarding question here.
446
+ """
447
+ project = os.environ.get("CLAUDE_PROJECT_NAME") or Path.cwd().name
448
+ cwd = str(Path.cwd())
449
+
450
+ has_index = False
451
+ has_memories = False # default: assume not onboarded (fail-closed for new projects)
452
+
453
+ # Check search index — brain is the knowledge layer
454
+ stats = _brain_api("stats", {}, timeout=2)
455
+ brain_reachable = stats is not None
456
+ if stats and stats.get("total", 0) > 0:
457
+ has_index = True
458
+
459
+ # If brain is unreachable, we cannot determine onboarding state — fail open.
460
+ # Brain unavailability is surfaced separately by _check_brain_dependency.
461
+ if not brain_reachable:
462
+ return False, True, None
463
+
464
+ # Check for any stored memories (broad: any brain result, not just /mem- paths).
465
+ # If brain has an index with content (has_index=True), that alone is sufficient
466
+ # evidence that the project has been set up — skip the memory search.
467
+ if has_index:
468
+ has_memories = True # indexed brain content ≡ project was onboarded
469
+ else:
470
+ result = _brain_api("search", {"query": "project memory onboarding", "limit": 5}, timeout=3)
471
+ if result is None:
472
+ # Brain went unreachable between calls — fail open.
473
+ return False, True, None
474
+ if isinstance(result, list) and result:
475
+ has_memories = True
476
+ elif isinstance(result, dict) and result.get("results"):
477
+ has_memories = True
478
+
479
+ directive = None
480
+ if not has_index and not has_memories:
481
+ # Neither index nor memories → first-time setup required
482
+ directive = (
483
+ f"[Action Required] Project '{project}' has not been onboarded.\n"
484
+ "You MUST immediately invoke the Skill tool with skill='wicked-garden:setup' "
485
+ "to launch the interactive onboarding wizard.\n"
486
+ "Do NOT ask the user for confirmation — invoke the skill now.\n"
487
+ "Do NOT respond with text first — invoke the skill as your first action."
488
+ )
489
+ elif not has_index and has_memories:
490
+ # Memories exist but brain index is gone → rebuild only
491
+ directive = (
492
+ f"[Onboarding] IMPORTANT: Project '{project}' has memories but no search index. "
493
+ "You MUST run /wicked-garden:search:index now to build the search index."
494
+ )
495
+
496
+ return has_index, has_memories, directive
497
+
498
+
499
+ def _read_config():
500
+ """Read config from ~/.something-wicked/wicked-garden/config.json."""
501
+ config_path = Path.home() / ".something-wicked" / "wicked-garden" / "config.json"
502
+ if not config_path.exists():
503
+ return None
504
+ try:
505
+ return json.loads(config_path.read_text())
506
+ except (json.JSONDecodeError, OSError):
507
+ return None
508
+
509
+
510
+ # v9.2.11 deleted _MEMORY_INSTRUCTIONS. v9.2.10 stopped APPENDING it to the
511
+ # briefing (CLAUDE.md "Memory Management" section already overrides the
512
+ # system-level auto-memory instructions); v9.2.11 confirmed zero imports
513
+ # across hooks/, scripts/, tests/, commands/ and removed the dead constant.
514
+ # Same cleanup pattern as the v9.2.10 yolo test deletion — a constant kept
515
+ # alive only by a test that asserts it exists is a tautology.
516
+
517
+ _DANGEROUS_MODE_WARNING = (
518
+ "[Question Mode] Dangerous mode is active (skipDangerousModePermissionPrompt). "
519
+ "AskUserQuestion is BROKEN in this mode — it auto-completes with empty answers. "
520
+ "You MUST use plain text questions instead: present numbered options, "
521
+ "then STOP and wait for the user to reply before proceeding."
522
+ )
523
+
524
+
525
+ # ---------------------------------------------------------------------------
526
+ # Dangerous mode detection
527
+ # ---------------------------------------------------------------------------
528
+
529
+ def _detect_dangerous_mode():
530
+ """Check if Claude Code is running in dangerous mode.
531
+
532
+ Dangerous mode (skipDangerousModePermissionPrompt=true) breaks
533
+ AskUserQuestion by auto-completing it with empty answers.
534
+ """
535
+ try:
536
+ settings_path = Path.home() / ".claude" / "settings.json"
537
+ if settings_path.exists():
538
+ settings = json.loads(settings_path.read_text())
539
+ return bool(settings.get("skipDangerousModePermissionPrompt"))
540
+ except Exception:
541
+ pass
542
+ return False
543
+
544
+
545
+
546
+ # ---------------------------------------------------------------------------
547
+ # wicked-brain dependency check
548
+ # ---------------------------------------------------------------------------
549
+
550
+ def _check_brain_dependency():
551
+ """Check whether the wicked-brain plugin is installed.
552
+
553
+ Scans ~/.claude/settings.json and .claude/settings.json (project-level)
554
+ for 'wicked-brain' in their enabledPlugins keys.
555
+
556
+ If found:
557
+ - Returns (True, None) — no briefing note needed.
558
+ - Also attempts a lightweight server health probe on the default port
559
+ and returns (True, note) where note describes the offline state if
560
+ the server is not reachable.
561
+
562
+ If NOT found:
563
+ - Returns (False, note) — note contains the install instruction.
564
+
565
+ Always fails open — any exception returns (None, None) so the session
566
+ is never blocked.
567
+ """
568
+ try:
569
+ # Settings file locations to check (global first, then project-local)
570
+ settings_candidates = [
571
+ Path.home() / ".claude" / "settings.json",
572
+ Path(".claude") / "settings.json",
573
+ ]
574
+
575
+ # Also honour CLAUDE_CONFIG_DIR if set
576
+ config_dir_env = os.environ.get("CLAUDE_CONFIG_DIR")
577
+ if config_dir_env:
578
+ settings_candidates.append(Path(config_dir_env) / "settings.json")
579
+
580
+ brain_installed = False
581
+
582
+ # Check 1: enabledPlugins in settings.json (plugin-style install)
583
+ for settings_path in settings_candidates:
584
+ try:
585
+ if not settings_path.exists():
586
+ continue
587
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
588
+ enabled = settings.get("enabledPlugins", {})
589
+ if isinstance(enabled, dict) and "wicked-brain" in enabled:
590
+ brain_installed = True
591
+ break
592
+ if isinstance(enabled, list) and "wicked-brain" in enabled:
593
+ brain_installed = True
594
+ break
595
+ except (json.JSONDecodeError, OSError):
596
+ continue
597
+
598
+ # Check 2: loose skill installs at ~/.claude/skills/wicked-brain-*
599
+ # Users can install wicked-brain skills directly without the plugin wrapper.
600
+ if not brain_installed:
601
+ try:
602
+ skills_dir = Path.home() / ".claude" / "skills"
603
+ if skills_dir.exists():
604
+ for entry in skills_dir.iterdir():
605
+ if entry.is_dir() and entry.name.startswith("wicked-brain"):
606
+ brain_installed = True
607
+ break
608
+ except OSError:
609
+ pass # fail open
610
+
611
+ # Check 3: CLAUDE_CONFIG_DIR skills directory
612
+ if not brain_installed and config_dir_env:
613
+ try:
614
+ alt_skills_dir = Path(config_dir_env) / "skills"
615
+ if alt_skills_dir.exists():
616
+ for entry in alt_skills_dir.iterdir():
617
+ if entry.is_dir() and entry.name.startswith("wicked-brain"):
618
+ brain_installed = True
619
+ break
620
+ except OSError:
621
+ pass # fail open
622
+
623
+ if not brain_installed:
624
+ note = (
625
+ "[wicked-brain] optional layer not installed.\n"
626
+ "Add it for cross-session memory + cited search: claude plugin install wicked-brain --scope project\n"
627
+ "The toolkit works without it — you lose the memory/context layer "
628
+ "(cross-session recall, brain-backed search, smaht:briefing)."
629
+ )
630
+ return False, note
631
+
632
+ # wicked-brain is installed — probe server health and auto-start on
633
+ # miss. _brain_api returns None on any error (server not running,
634
+ # connection refused, timeout, parse error); ensure_server is the
635
+ # deterministic fix for the "not running" case and fails open.
636
+ server_note = None
637
+ if _brain_api("health", {}, timeout=1) is None:
638
+ started = False
639
+ try:
640
+ from _brain_port import ensure_server
641
+ started = ensure_server()
642
+ except Exception:
643
+ pass
644
+ if started:
645
+ server_note = "[wicked-brain] Server was down — auto-started for this session."
646
+ else:
647
+ server_note = (
648
+ "[wicked-brain] Plugin installed but the server could not be "
649
+ "auto-started. Diagnose with `wicked-brain-call --start` "
650
+ "(log: {brain}/_meta/server.log). Brain skills auto-start the "
651
+ "server on every call — do NOT skip brain usage."
652
+ )
653
+
654
+ return True, server_note
655
+
656
+ except Exception:
657
+ return None, None # Fail open — never block session start
658
+
659
+
660
+ # ---------------------------------------------------------------------------
661
+ # wicked-vault dependency check (required evidence backend)
662
+ # ---------------------------------------------------------------------------
663
+
664
+ def _check_vault_dependency():
665
+ """Return a briefing note if wicked-vault is not resolvable, else None.
666
+
667
+ wicked-vault is a required peer (sibling to wicked-bus / wicked-brain /
668
+ wicked-testing): every archetype produces-gate re-derives against it via
669
+ ``scripts/qe/vault_gate.py``. When it is absent those gates fail closed,
670
+ so we surface a one-line install pointer at SessionStart.
671
+
672
+ Fast + stdlib-only (no subprocess to npx): checks PATH and loose skill
673
+ installs only. Always fails open — never blocks the session.
674
+ """
675
+ try:
676
+ import shutil
677
+
678
+ # WICKED_VAULT_BIN explicitly set (even empty kill-switch) → operator
679
+ # is driving resolution deliberately; don't nag.
680
+ if "WICKED_VAULT_BIN" in os.environ:
681
+ return None
682
+
683
+ if shutil.which("wicked-vault"):
684
+ return None
685
+
686
+ # Loose skill installs land as skills/wicked-vault-*/ under each CLI
687
+ # config root (npx wicked-vault-install). PATH miss + skills present
688
+ # still means the CLI runs via npx, so treat skills as "installed".
689
+ skill_roots = [Path.home() / ".claude" / "skills"]
690
+ cfg = os.environ.get("CLAUDE_CONFIG_DIR")
691
+ if cfg:
692
+ skill_roots.append(Path(cfg) / "skills")
693
+ for root in skill_roots:
694
+ try:
695
+ if root.exists():
696
+ for entry in root.iterdir():
697
+ if entry.is_dir() and entry.name.startswith("wicked-vault"):
698
+ return None
699
+ except OSError:
700
+ continue # fail open
701
+
702
+ return (
703
+ "[wicked-vault] REQUIRED but not installed.\n"
704
+ "Install now: npx wicked-vault-install (or: npm i -g wicked-vault)\n"
705
+ "wicked-vault is the evidence backend wicked-loom re-runs every "
706
+ "gate's verifier against — without it, produces-gates fail closed."
707
+ )
708
+ except Exception:
709
+ return None # Fail open — never block session start
710
+
711
+
712
+ def _check_loom_dependency():
713
+ """Return a briefing note if loom peer-resolution is unavailable, else None.
714
+
715
+ After the Phase B absorption (ECOSYSTEM-RATIONALIZATION.md §5a), the
716
+ wicked-loom peer-resolution surface (resolve/doctor/compose/gate) is
717
+ internal to wicked-garden — absorbed into scripts/loom/. External
718
+ installation of the standalone wicked-loom npm package is no longer
719
+ required.
720
+
721
+ This function returns None (no warning) when the absorbed internal module
722
+ is present, which is always the case in a properly installed wicked-garden.
723
+ The legacy external-binary checks are retained as a fallback for
724
+ environments that may be running a partial or development install.
725
+
726
+ Always fails open — never blocks the session start.
727
+ """
728
+ try:
729
+ # Primary check: internal absorbed module (scripts/loom/).
730
+ # In a properly installed wicked-garden this always resolves.
731
+ _scripts_dir = Path(__file__).resolve().parents[2] / "scripts"
732
+ if (_scripts_dir / "loom" / "__init__.py").exists():
733
+ return None # Absorbed internally — no external install needed.
734
+
735
+ # Kill-switch / operator override: don't nag when deliberately managed.
736
+ if "WICKED_LOOM_BIN" in os.environ:
737
+ return None
738
+ if os.environ.get("WICKED_LOOM_CUTOVER", "").strip().lower() == "off":
739
+ return None
740
+
741
+ # Legacy external-binary check (development / partial installs only).
742
+ if shutil.which("wicked-loom"):
743
+ return None
744
+
745
+ skill_roots = [Path.home() / ".claude" / "skills"]
746
+ cfg = os.environ.get("CLAUDE_CONFIG_DIR")
747
+ if cfg:
748
+ skill_roots.append(Path(cfg) / "skills")
749
+ for root in skill_roots:
750
+ try:
751
+ if root.exists():
752
+ for entry in root.iterdir():
753
+ if entry.is_dir() and entry.name.startswith("wicked-loom"):
754
+ return None
755
+ except OSError:
756
+ continue # fail open
757
+
758
+ # No loom surface found — surface a note (never a block).
759
+ return (
760
+ "[loom] Peer-resolution runtime not found.\n"
761
+ "wicked-loom is now absorbed into wicked-garden — reinstall "
762
+ "wicked-garden to restore the peer-resolution surface.\n"
763
+ "Without it the produces-gate fails closed."
764
+ )
765
+ except Exception:
766
+ return None # Fail open — never block session start
767
+
768
+
769
+ # ---------------------------------------------------------------------------
770
+ # wicked-bus dependency check (required event backbone)
771
+ # ---------------------------------------------------------------------------
772
+
773
+ def _check_bus_dependency():
774
+ """Return a briefing note if wicked-bus is not installed, else None.
775
+
776
+ wicked-bus is a required peer (sibling to wicked-brain / wicked-vault /
777
+ wicked-testing): the garden's archetype events flow through it. When it
778
+ is absent, cross-plugin event wiring is silently dropped, so we surface a
779
+ one-line install pointer at SessionStart.
780
+
781
+ Detection mirrors ``_check_brain_dependency`` — wicked-bus installs as a
782
+ Claude Code *plugin*, not an npx CLI, so we verify by presence:
783
+ 1. ``enabledPlugins`` in ~/.claude/settings.json, project-local
784
+ .claude/settings.json, and CLAUDE_CONFIG_DIR/settings.json.
785
+ 2. Loose skill installs at ``skills/wicked-bus-*`` under the home and
786
+ CLAUDE_CONFIG_DIR skills directories.
787
+
788
+ Fast + stdlib-only (no subprocess). Always fails open — never blocks the
789
+ session.
790
+ """
791
+ try:
792
+ # Settings file locations to check (global first, then project-local)
793
+ settings_candidates = [
794
+ Path.home() / ".claude" / "settings.json",
795
+ Path(".claude") / "settings.json",
796
+ ]
797
+
798
+ # Also honour CLAUDE_CONFIG_DIR if set
799
+ config_dir_env = os.environ.get("CLAUDE_CONFIG_DIR")
800
+ if config_dir_env:
801
+ settings_candidates.append(Path(config_dir_env) / "settings.json")
802
+
803
+ bus_installed = False
804
+
805
+ # Check 1: enabledPlugins in settings.json (plugin-style install)
806
+ for settings_path in settings_candidates:
807
+ try:
808
+ if not settings_path.exists():
809
+ continue
810
+ settings = json.loads(settings_path.read_text(encoding="utf-8"))
811
+ enabled = settings.get("enabledPlugins", {})
812
+ if isinstance(enabled, dict) and "wicked-bus" in enabled:
813
+ bus_installed = True
814
+ break
815
+ if isinstance(enabled, list) and "wicked-bus" in enabled:
816
+ bus_installed = True
817
+ break
818
+ except (json.JSONDecodeError, OSError):
819
+ continue
820
+
821
+ # Check 2: loose skill installs at skills/wicked-bus-* under the home
822
+ # and CLAUDE_CONFIG_DIR skills directories.
823
+ if not bus_installed:
824
+ skill_roots = [Path.home() / ".claude" / "skills"]
825
+ if config_dir_env:
826
+ skill_roots.append(Path(config_dir_env) / "skills")
827
+ for root in skill_roots:
828
+ try:
829
+ if root.exists():
830
+ for entry in root.iterdir():
831
+ if entry.is_dir() and entry.name.startswith("wicked-bus"):
832
+ bus_installed = True
833
+ break
834
+ except OSError:
835
+ continue # fail open
836
+ if bus_installed:
837
+ break
838
+
839
+ if not bus_installed:
840
+ return (
841
+ "[wicked-bus] optional layer not installed.\n"
842
+ "Add it for the cross-session audit trail: /plugin install wicked-bus\n"
843
+ "The toolkit works without it — events are simply not recorded "
844
+ "(emission is already fire-and-forget / fail-open)."
845
+ )
846
+
847
+ return None
848
+ except Exception:
849
+ return None # Fail open — never block session start
850
+
851
+
852
+ # ---------------------------------------------------------------------------
853
+ # Discovery: contextual command suggestions based on project type (Issue #322)
854
+ # ---------------------------------------------------------------------------
855
+
856
+ def _check_facilitator_context(project_name: str | None) -> str | None:
857
+ """Return a briefing line for unresolved action items aged >= 2 sessions.
858
+
859
+ Lazily imports ``scripts.delivery.process_memory`` so environments that
860
+ do not ship that module (pre-#447 plugin installs) continue to boot.
861
+ Returns a short ``[ProcessMemory]`` line when the project has any
862
+ aging AIs, or ``None`` when:
863
+
864
+ - No active project was detected (nothing to surface for).
865
+ - ``delivery.process_memory`` is not importable (fail-open).
866
+ - The project has no unresolved action items at the aging threshold.
867
+ - Any exception is raised while reading the memory file.
868
+
869
+ Matches the fail-open pattern used by ``_run_quality_telemetry`` and
870
+ the guard pipeline in ``stop.py``.
871
+ """
872
+ if not project_name:
873
+ return None
874
+ try:
875
+ # Lazy import — absence must NOT break bootstrap on legacy installs.
876
+ sys.path.insert(0, str(_PLUGIN_ROOT / "scripts" / "delivery"))
877
+ try:
878
+ import process_memory as _pm # type: ignore
879
+ except ImportError as exc:
880
+ print(
881
+ f"[wicked-garden] process_memory not importable ({exc!r}); "
882
+ "skipping facilitator context",
883
+ file=sys.stderr,
884
+ )
885
+ return None
886
+
887
+ ctx = _pm.facilitator_context(project_name)
888
+ aging = ctx.get("aging_action_items", []) if isinstance(ctx, dict) else []
889
+ if not aging:
890
+ return None
891
+
892
+ # Show up to 3 AIs inline so the line stays readable; rest are
893
+ # countable via the summary prefix and fully visible via
894
+ # process-memory.md on disk.
895
+ shown = aging[:3]
896
+ preview = ", ".join(
897
+ f"{ai.get('id', '?')} \"{(ai.get('title') or '')[:60]}\""
898
+ for ai in shown
899
+ )
900
+ more = f" (+{len(aging) - len(shown)} more)" if len(aging) > len(shown) else ""
901
+ return (
902
+ f"[ProcessMemory] {len(aging)} unresolved action item(s) "
903
+ f">=2 sessions old: {preview}{more}"
904
+ )
905
+ except Exception as exc:
906
+ # Fail-open: never block session start on process-memory errors.
907
+ print(
908
+ f"[wicked-garden] facilitator_context error: {exc!r}",
909
+ file=sys.stderr,
910
+ )
911
+ return None
912
+
913
+
914
+ def _probe_onedrive_path() -> str | None:
915
+ """Detect if cwd is under a OneDrive or CloudStorage path with spaces.
916
+
917
+ On macOS, OneDrive-synced directories live under paths like:
918
+ ~/Library/CloudStorage/OneDrive - CompanyName/...
919
+ The spaces cause failures when paths are not properly quoted.
920
+
921
+ Returns the resolved canonical base path (via Path.resolve()), or None
922
+ if cwd is not under a OneDrive/CloudStorage directory.
923
+ Fails open — any error returns None.
924
+ """
925
+ try:
926
+ cwd = Path.cwd()
927
+ cwd_str = str(cwd)
928
+ if "OneDrive" in cwd_str or "CloudStorage" in cwd_str:
929
+ resolved = cwd.resolve()
930
+ return str(resolved)
931
+ except Exception:
932
+ pass
933
+ return None
934
+
935
+
936
+ # Critical v6 skills — if these files exist on disk under the plugin root
937
+ # but a Skill() call fails with "Unknown skill", the plugin cache is stale
938
+ # and needs `/reload-plugins`. Issue #434.
939
+ _CRITICAL_V11_SKILLS = (
940
+ "skills/archetype/SKILL.md",
941
+ )
942
+
943
+
944
+ # v9.2.10 — first-session-per-project gating for informational briefing notices.
945
+ # Both `[Setup]` and `[Quick Start]` are useful exactly once per project; on
946
+ # every subsequent session they are pure noise. The sentinel file lives under
947
+ # the wicked-garden local store (per-project automatically because
948
+ # `get_local_path` resolves under the active project's slug), so a fresh
949
+ # project sees the notices on its first session and never again.
950
+
951
+ _BOOTSTRAP_NOTICE_FILE = "shown.json"
952
+ _NOTICE_SETUP = "setup_reconfigure"
953
+ _NOTICE_QUICK_START = "quick_start"
954
+
955
+
956
+ def _notice_path() -> "Path | None":
957
+ """Resolve the per-project sentinel file path. None on resolution failure."""
958
+ try:
959
+ # Lazy import — keeps bootstrap fail-open if scripts/ is unreachable.
960
+ sys.path.insert(0, str(_PLUGIN_ROOT / "scripts"))
961
+ from _domain_store import get_local_path # type: ignore
962
+ return get_local_path("wicked-garden", "bootstrap-notices") / _BOOTSTRAP_NOTICE_FILE
963
+ except Exception:
964
+ return None
965
+
966
+
967
+ def _notice_already_shown(notice_id: str) -> bool:
968
+ """Return True if `notice_id` has been emitted for this project before."""
969
+ p = _notice_path()
970
+ if p is None or not p.exists():
971
+ return False
972
+ try:
973
+ data = json.loads(p.read_text(encoding="utf-8"))
974
+ return notice_id in data
975
+ except (json.JSONDecodeError, OSError):
976
+ return False
977
+
978
+
979
+ def _record_notice_shown(notice_id: str) -> None:
980
+ """Record `notice_id` as shown. Best-effort; failures don't block bootstrap."""
981
+ p = _notice_path()
982
+ if p is None:
983
+ return
984
+ try:
985
+ p.parent.mkdir(parents=True, exist_ok=True)
986
+ data = {}
987
+ if p.exists():
988
+ try:
989
+ data = json.loads(p.read_text(encoding="utf-8"))
990
+ except (json.JSONDecodeError, OSError):
991
+ data = {}
992
+ data[notice_id] = {"shown_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")}
993
+ p.write_text(json.dumps(data), encoding="utf-8")
994
+ except OSError:
995
+ pass # fail open
996
+
997
+
998
+ def _check_critical_skills() -> str | None:
999
+ """Verify critical v6 skill files are present in the plugin root.
1000
+
1001
+ Claude Code's skill registry is cached at plugin-install time. When the
1002
+ plugin ships a new skill mid-version, the cache lags until the user runs
1003
+ `/reload-plugins`. We can't query the live registry from a hook — but we
1004
+ can check the files exist on disk and emit a diagnostic directive so an
1005
+ "Unknown skill" error surfaces a fix path instead of being cryptic.
1006
+
1007
+ Returns a directive string ONLY when one or more critical skills are
1008
+ missing from disk (a real install problem). The previous "happy path"
1009
+ message — "skills are present; if Skill() fails, run /reload-plugins" —
1010
+ fired every session even with a clean install and was chronic noise. A
1011
+ user who has hit a cache-stale failure once knows the fix; emitting it
1012
+ on every healthy session adds no signal. v9.2.10 dropped that branch.
1013
+
1014
+ Fails open on errors.
1015
+ """
1016
+ try:
1017
+ plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT")
1018
+ if not plugin_root:
1019
+ return None
1020
+ root = Path(plugin_root)
1021
+ missing = [s for s in _CRITICAL_V11_SKILLS if not (root / s).exists()]
1022
+ if not missing:
1023
+ return None # healthy install — nothing to say
1024
+ return (
1025
+ "[Skills] Critical v6 skill files are missing from the plugin root:\n"
1026
+ + "\n".join(f" - {s}" for s in missing)
1027
+ + "\nThe plugin install is incomplete. Reinstall with "
1028
+ "`claude plugin install wicked-garden --scope project`."
1029
+ )
1030
+ except Exception:
1031
+ return None # fail open — never block bootstrap
1032
+
1033
+
1034
+ def _suggest_commands_for_project() -> str | None:
1035
+ """Detect project type from files in cwd and suggest 2-3 relevant commands.
1036
+
1037
+ Lightweight: checks only for a few marker files (no directory walks).
1038
+ Returns a formatted suggestion string, or None if no suggestions apply.
1039
+ """
1040
+ try:
1041
+ cwd = Path.cwd()
1042
+ suggestions = []
1043
+
1044
+ # Detect project signals from marker files
1045
+ has_package_json = (cwd / "package.json").exists()
1046
+ has_pyproject = (cwd / "pyproject.toml").exists() or (cwd / "setup.py").exists()
1047
+ has_dockerfile = (cwd / "Dockerfile").exists() or (cwd / "docker-compose.yml").exists()
1048
+ has_terraform = (cwd / "main.tf").exists() or (cwd / "terraform").is_dir()
1049
+ has_tests = (
1050
+ (cwd / "tests").is_dir() or (cwd / "test").is_dir()
1051
+ or (cwd / "__tests__").is_dir() or (cwd / "spec").is_dir()
1052
+ )
1053
+ has_csv_data = any(cwd.glob("*.csv")) or (cwd / "data").is_dir()
1054
+ has_ci = (cwd / ".github" / "workflows").is_dir()
1055
+
1056
+ # Always useful
1057
+ suggestions.append("`wicked-brain:search` — semantic code search")
1058
+ suggestions.append("`/wicked-garden:engineering:review` — structured code review")
1059
+
1060
+ # Project-type-specific
1061
+ if has_tests:
1062
+ suggestions.append("`/wicked-testing:plan` — generate test plan and scenarios")
1063
+ if has_dockerfile or has_terraform:
1064
+ suggestions.append("`/wicked-garden:platform:security` — security review")
1065
+ if has_csv_data:
1066
+ suggestions.append("`/wicked-garden:data:analyze` — interactive data analysis with DuckDB")
1067
+ if has_ci:
1068
+ suggestions.append("`/wicked-garden:platform:actions` — GitHub Actions optimization")
1069
+
1070
+ # Cap at 3 suggestions
1071
+ suggestions = suggestions[:3]
1072
+
1073
+ if not suggestions:
1074
+ return None
1075
+
1076
+ return (
1077
+ "[Quick Start] Available commands for this project:\n"
1078
+ + "\n".join(f" - {s}" for s in suggestions)
1079
+ )
1080
+ except Exception:
1081
+ return None
1082
+
1083
+
1084
+ # ---------------------------------------------------------------------------
1085
+ # #506 — WG_GATE_RESULT_STRICT_AFTER pre-flip monitoring
1086
+ #
1087
+ # Surfaces a visible warning as the strict-mode cutover approaches so
1088
+ # operators aren't surprised when orphan gate-results start rejecting.
1089
+ # Thresholds (T = days until flip, computed in UTC):
1090
+ # T > 7 silent
1091
+ # 7 >= T >= 1 stderr WARN, every SessionStart
1092
+ # T == 0 stderr INFO, one-time per session (latched via
1093
+ # SessionState.strict_mode_active_announced)
1094
+ # T < 0 stderr INFO, same one-time latch (post-flip banner)
1095
+ #
1096
+ # Fail-open: malformed dates fall back to the module default and the
1097
+ # signal is skipped silently rather than blocking bootstrap.
1098
+ # ---------------------------------------------------------------------------
1099
+
1100
+
1101
+ def _check_pre_flip_notice(state, today=None) -> None:
1102
+ """Emit pre-flip / post-flip monitoring signals on stderr.
1103
+
1104
+ Arguments:
1105
+ state: The loaded SessionState (or None). When None, the
1106
+ post-flip banner still fires but won't latch — next
1107
+ SessionStart in the same session may re-fire, which is
1108
+ the expected degrade.
1109
+ today: ``datetime.date`` override for deterministic tests.
1110
+ Production callers pass ``None`` and the helper uses
1111
+ today's UTC date.
1112
+ """
1113
+ try:
1114
+ # Lazy import — keeps hook cheap; dispatch_log is in the crew
1115
+ # scripts dir which is already on sys.path via the hook bootstrap.
1116
+ sys.path.insert(0, str(_PLUGIN_ROOT / "scripts" / "crew"))
1117
+ from dispatch_log import ( # type: ignore
1118
+ DEFAULT_STRICT_AFTER,
1119
+ _get_strict_after_date,
1120
+ )
1121
+ except Exception:
1122
+ return # fail-open: module unavailable, skip the signal
1123
+
1124
+ try:
1125
+ from datetime import datetime, timezone
1126
+
1127
+ flip_date = _get_strict_after_date()
1128
+ if today is None:
1129
+ today = datetime.now(timezone.utc).date()
1130
+ delta_days = (flip_date - today).days
1131
+ raw_env_val = os.environ.get("WG_GATE_RESULT_STRICT_AFTER", "").strip()
1132
+ # Display the user-supplied value when it parsed; otherwise show
1133
+ # the effective default. A malformed env-var produces the fallback
1134
+ # WARN in ``_get_strict_after_date`` already — we don't duplicate.
1135
+ if raw_env_val and flip_date.isoformat() == raw_env_val:
1136
+ raw_env = raw_env_val
1137
+ else:
1138
+ raw_env = flip_date.isoformat()
1139
+
1140
+ if delta_days > 7:
1141
+ return # silent
1142
+
1143
+ if 1 <= delta_days <= 7:
1144
+ sys.stderr.write(
1145
+ f"[PreFlipNotice] Strict-mode activation in {delta_days} "
1146
+ f"days (WG_GATE_RESULT_STRICT_AFTER={raw_env}). "
1147
+ "Orphan gate-result files will start rejecting.\n"
1148
+ )
1149
+ return
1150
+
1151
+ # delta_days <= 0 → post-flip; emit one-time-per-session INFO.
1152
+ already = bool(getattr(state, "strict_mode_active_announced", False)) \
1153
+ if state is not None else False
1154
+ if already:
1155
+ return
1156
+ sys.stderr.write(
1157
+ f"[StrictMode] Strict-mode active (flipped on "
1158
+ f"{flip_date.isoformat()}). Any orphan gate-result now rejects.\n"
1159
+ )
1160
+ if state is not None:
1161
+ try:
1162
+ state.update(strict_mode_active_announced=True)
1163
+ except Exception: # pragma: no cover — defensive
1164
+ pass # session-state write failure: banner may re-fire, acceptable
1165
+ except Exception:
1166
+ # Signal computation errors must never block bootstrap.
1167
+ return
1168
+
1169
+
1170
+ # ---------------------------------------------------------------------------
1171
+ # #544 — wicked-testing peer-plugin probe (AC-1 through AC-6)
1172
+ # ---------------------------------------------------------------------------
1173
+
1174
+ # Module-level list of blocking notices to inject into session briefing.
1175
+ # Populated by _probe_wicked_testing(); consumed by main() before building
1176
+ # the briefing. At most one entry per session (the probe runs once).
1177
+ _wt_blocking_notices: list = []
1178
+
1179
+
1180
+ def _probe_wicked_testing(state) -> None:
1181
+ """Run the wicked-testing availability probe once per session.
1182
+
1183
+ Caches the result in SessionState.extras["wicked_testing_probe"] so
1184
+ crew_command_gate() can read it without re-running the subprocess.
1185
+
1186
+ Session cache semantics:
1187
+ - Probe runs exactly once at SessionStart (first call after fresh state).
1188
+ - SessionState is cleared on session reset (session_ended path above),
1189
+ so a new session always re-probes. No cross-session persistence.
1190
+ - Re-entrant guard: if the probe key is already present, return early.
1191
+
1192
+ Fail-open boundary: exceptions that escape this function are caught by
1193
+ the calling try/except in main() which returns {"continue": true}.
1194
+ This matches every other probe in bootstrap.py.
1195
+
1196
+ CH-02 hardening: the exception handler logs actionable detail, NOT just
1197
+ the bare exception string. crew_command_gate() closes the fail-open gap
1198
+ at the crew layer by treating an absent probe key as missing.
1199
+ """
1200
+ try:
1201
+ # Re-entrant guard: probe runs at most once per session.
1202
+ extras = getattr(state, "extras", None) or {}
1203
+ if "wicked_testing_probe" in extras:
1204
+ return
1205
+
1206
+ # Import the probe from the scripts/ directory (already on sys.path
1207
+ # from the top of bootstrap.py).
1208
+ from _wicked_testing_probe import probe
1209
+
1210
+ result = probe(state)
1211
+
1212
+ missing = result["status"] != "ok"
1213
+ new_extras = {**extras, "wicked_testing_probe": result}
1214
+
1215
+ if state is not None:
1216
+ state.update(
1217
+ wicked_testing_missing=missing,
1218
+ extras=new_extras,
1219
+ )
1220
+
1221
+ # Emit a blocking notice into the session briefing (once per session).
1222
+ # crew_command_gate() only emits a pointer back, suppressing the full
1223
+ # notice on repeat invocations within the same session (AC-2).
1224
+ if result["status"] == "missing":
1225
+ _wt_blocking_notices.append(
1226
+ "[wicked-testing] wicked-testing is not installed.\n"
1227
+ "wicked-testing required — run: npx wicked-testing install\n"
1228
+ "Crew commands (start, execute, just-finish) are blocked until "
1229
+ "wicked-testing is installed."
1230
+ )
1231
+ elif result["status"] == "out-of-range":
1232
+ ver = result.get("version", "unknown")
1233
+ pin = result.get("pin", "unknown")
1234
+ _wt_blocking_notices.append(
1235
+ f"[wicked-testing] wicked-testing {ver} does not satisfy required range {pin}.\n"
1236
+ "wicked-testing required — run: npx wicked-testing install\n"
1237
+ "Crew commands (start, execute, just-finish) are blocked until "
1238
+ "the correct version is installed."
1239
+ )
1240
+ elif result["status"] == "error":
1241
+ err = result.get("error", "unknown error")
1242
+ _wt_blocking_notices.append(
1243
+ f"[wicked-testing] probe error: {err}\n"
1244
+ "Crew commands are blocked. If this is a transient error, "
1245
+ "restart the session. Use WG_SKIP_WICKED_TESTING_CHECK=1 for "
1246
+ "offline dev mode (CONTRIBUTING.md)."
1247
+ )
1248
+ # "ok" and "skipped" → silent (AC-6, AC-5)
1249
+
1250
+ except Exception as exc:
1251
+ # CH-02 hardening: log actionable context so operators can diagnose.
1252
+ # Include what failed and where (not just the bare exception string).
1253
+ print(
1254
+ f"[wicked-garden] wicked-testing probe failed: "
1255
+ f"exception={exc!r} "
1256
+ f"probe_module='_wicked_testing_probe' "
1257
+ f"action='npx wicked-testing --version' "
1258
+ f"note='crew_command_gate will treat absent probe key as missing'",
1259
+ file=sys.stderr,
1260
+ )
1261
+ # Fail-open at this boundary: do NOT block bootstrap.
1262
+ # crew_command_gate() is fail-closed for the absent-key case (CH-02).
1263
+ if state is not None:
1264
+ try:
1265
+ state.update(wicked_testing_missing=False)
1266
+ except Exception:
1267
+ pass # session-state write failure — acceptable
1268
+
1269
+
1270
+ # ---------------------------------------------------------------------------
1271
+ # Registry allowlist validation
1272
+ #
1273
+ # Walks in-code allowlists (phase fallback agents, gate-policy reviewers,
1274
+ # bus event handlers, agent skill references) and confirms each referenced
1275
+ # asset exists on disk / in catalog. Catches the "invisible gap" failure
1276
+ # mode where probe traffic passes but the first request that hits the
1277
+ # missing asset blows up with a cryptic error.
1278
+ #
1279
+ # Fail-OPEN per the hook contract: any exception, any timeout, any malformed
1280
+ # allowlist surfaces as a session-briefing warning — never blocks bootstrap.
1281
+ # Findings categorised as `missing` / `malformed` / `invalid_id` are blocking
1282
+ # (CI-fail) but the bootstrap path only WARNs. External (drop-in plugin)
1283
+ # and skipped checks are advisory.
1284
+ # ---------------------------------------------------------------------------
1285
+
1286
+
1287
+ def _run_registry_validation() -> str | None:
1288
+ """Run the aggregated registry validator and return a briefing line.
1289
+
1290
+ Returns a multi-line string when findings exist, or None when the
1291
+ registry is fully clean (or the validator is unavailable). Caller
1292
+ is expected to append the result to `mode_notes` so it surfaces as
1293
+ a SessionStart [Registry] warning.
1294
+
1295
+ Fail-open boundary: any exception returns None. Validator timing
1296
+ is bounded by file IO (no subprocess, no network), so we trust it
1297
+ to complete inside the bootstrap budget.
1298
+ """
1299
+ try:
1300
+ # _validate_registry lives in scripts/ which is already on sys.path
1301
+ # via the top-of-file insertion.
1302
+ from _validate_registry import run_all_checks, format_briefing
1303
+
1304
+ report = run_all_checks()
1305
+ return format_briefing(report)
1306
+ except Exception as exc:
1307
+ # Fail-open: validator import or run failure must NOT block bootstrap.
1308
+ # Log to stderr so operators can see why the [Registry] line is absent
1309
+ # but otherwise carry on.
1310
+ print(
1311
+ f"[wicked-garden] registry validation skipped: "
1312
+ f"{type(exc).__name__}: {exc}",
1313
+ file=sys.stderr,
1314
+ )
1315
+ return None
1316
+
1317
+
1318
+ # ---------------------------------------------------------------------------
1319
+ # CH-02: SessionStart legacy reeval-log scan
1320
+ #
1321
+ # Scans known crew project directories for reeval-log.jsonl and amendments.jsonl
1322
+ # files containing legacy 'qe-evaluator' reviewer names or trigger prefixes.
1323
+ # Advisory only — does NOT block session startup.
1324
+ # If legacy entries are found, adds a structured migration notice to mode_notes.
1325
+ # ---------------------------------------------------------------------------
1326
+
1327
+ def _scan_for_legacy_reeval_entries() -> str | None:
1328
+ """Scan crew project dirs for reeval-log.jsonl / amendments.jsonl with legacy qe-evaluator entries.
1329
+
1330
+ Light scan — greps for literal strings; does not parse full JSON.
1331
+ Fails open — any exception returns None so session is never blocked.
1332
+
1333
+ Returns a formatted migration notice string when legacy entries are found,
1334
+ or None when the tree is clean or unreadable.
1335
+ """
1336
+ try:
1337
+ projects_root = Path.home() / ".something-wicked" / "wicked-garden" / "projects"
1338
+ if not projects_root.exists():
1339
+ return None
1340
+
1341
+ _LEGACY_STRINGS = ('"reviewer": "qe-evaluator"', '"trigger": "qe-evaluator:')
1342
+ dirty_files: list[str] = []
1343
+
1344
+ for log_file in projects_root.rglob("phases/*/reeval-log.jsonl"):
1345
+ try:
1346
+ text = log_file.read_text(encoding="utf-8", errors="replace")
1347
+ if any(s in text for s in _LEGACY_STRINGS):
1348
+ dirty_files.append(str(log_file))
1349
+ except OSError:
1350
+ continue
1351
+
1352
+ for log_file in projects_root.rglob("phases/*/amendments.jsonl"):
1353
+ try:
1354
+ text = log_file.read_text(encoding="utf-8", errors="replace")
1355
+ if any(s in text for s in _LEGACY_STRINGS):
1356
+ dirty_files.append(str(log_file))
1357
+ except OSError:
1358
+ continue
1359
+
1360
+ if not dirty_files:
1361
+ return None
1362
+
1363
+ file_list = "\n".join(f" - {f}" for f in dirty_files[:10])
1364
+ more = f"\n ... and {len(dirty_files) - 10} more" if len(dirty_files) > 10 else ""
1365
+ return (
1366
+ "[Migration Required] Legacy 'qe-evaluator' entries detected in reeval-log.jsonl "
1367
+ "or amendments.jsonl. These will cause errors when crew gates run.\n"
1368
+ f"Affected files:\n{file_list}{more}\n"
1369
+ "Run the migration script before starting a crew workflow:\n"
1370
+ " python scripts/crew/migrate_qe_evaluator_name.py --project-dir PATH/TO/PROJECT\n"
1371
+ "See docs/MIGRATION-v7.md for full guidance."
1372
+ )
1373
+ except Exception:
1374
+ return None # fail open — never block session start
1375
+
1376
+
1377
+ # ---------------------------------------------------------------------------
1378
+ # Main
1379
+ # ---------------------------------------------------------------------------
1380
+
1381
+ def main():
1382
+ _t0 = time.monotonic()
1383
+ _log("bootstrap", "debug", "hook.start")
1384
+
1385
+ try:
1386
+ sys.stdin.read() # consume hook payload (not needed for SessionStart)
1387
+ except Exception:
1388
+ pass
1389
+
1390
+ # Export CLAUDE_PLUGIN_ROOT to CLAUDE_ENV_FILE so Bash tool calls can access it.
1391
+ # Hooks receive CLAUDE_PLUGIN_ROOT from the plugin runtime, but Bash tool calls
1392
+ # do not inherit it. Writing to CLAUDE_ENV_FILE bridges the gap.
1393
+ env_file = os.environ.get("CLAUDE_ENV_FILE")
1394
+ plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT")
1395
+ if env_file and plugin_root:
1396
+ try:
1397
+ with open(env_file, "a") as f:
1398
+ f.write(f'export CLAUDE_PLUGIN_ROOT="{plugin_root}"\n')
1399
+ except OSError:
1400
+ pass
1401
+
1402
+ try:
1403
+ # 1. Read config
1404
+ config = _read_config()
1405
+
1406
+ if config is None or not config.get("setup_complete", False):
1407
+ # Clear stale session state before early return. Without this,
1408
+ # setup_in_progress=True from a crashed/killed previous session
1409
+ # survives and lets prompt_submit bypass the setup gate.
1410
+ _stale = _load_session_state()
1411
+ if _stale is not None:
1412
+ if _stale.session_ended:
1413
+ _stale.delete()
1414
+ _stale = _load_session_state()
1415
+ if _stale is not None:
1416
+ _stale.update(
1417
+ setup_in_progress=False,
1418
+ setup_complete=False,
1419
+ setup_confirmed=False,
1420
+ )
1421
+
1422
+ print(json.dumps({
1423
+ "hookSpecificOutput": {
1424
+ "hookEventName": "SessionStart",
1425
+ "additionalContext": (
1426
+ "[Action Required] wicked-garden is not configured for this project.\n"
1427
+ "You MUST immediately invoke the Skill tool with skill='wicked-garden:setup' "
1428
+ "to launch the interactive setup wizard.\n"
1429
+ "Do NOT ask the user for confirmation — invoke the skill now.\n"
1430
+ "Do NOT respond with text first — invoke the skill as your first action."
1431
+ ),
1432
+ },
1433
+ }))
1434
+ return
1435
+
1436
+ # Clear one-time task suggestion flag from crew
1437
+ flag = Path.home() / ".something-wicked" / "wicked-crew" / ".task_suggest_shown"
1438
+ flag.unlink(missing_ok=True)
1439
+
1440
+ # 2. Load session state — if stale from a previous session, start fresh.
1441
+ state = _load_session_state()
1442
+ if state is not None and state.session_ended:
1443
+ state.delete()
1444
+ state = _load_session_state() # fresh defaults
1445
+ if state is not None:
1446
+ state.update(
1447
+ setup_complete=True,
1448
+ turn_count=0,
1449
+ session_ended=False,
1450
+ )
1451
+
1452
+ # 2a. Probe OneDrive / spaces-in-paths (Issue #321)
1453
+ onedrive_path = _probe_onedrive_path()
1454
+ if onedrive_path and state is not None:
1455
+ state.update(onedrive_base_path=onedrive_path)
1456
+
1457
+ _log("bootstrap", "normal", "storage.local", ok=True)
1458
+
1459
+ # 2b. Initialize event store schema (fire-and-forget)
1460
+ try:
1461
+ from _event_store import EventStore
1462
+ EventStore.ensure_schema()
1463
+ _log("bootstrap", "debug", "event_store.schema", ok=True)
1464
+ except Exception:
1465
+ pass # event store is optional — never block session start
1466
+
1467
+ # 2c. #506 — pre-flip monitoring signal for WG_GATE_RESULT_STRICT_AFTER.
1468
+ # Fail-open: never blocks bootstrap. Emits stderr WARN at T-7..T-1,
1469
+ # stderr INFO once per session post-flip.
1470
+ _check_pre_flip_notice(state)
1471
+
1472
+ # 2d. Probe wicked-testing availability (AC-1 through AC-6, #544).
1473
+ # Runs once per session; result cached in SessionState.extras.
1474
+ # Fail-open: any unhandled exception in _probe_wicked_testing() returns
1475
+ # gracefully so bootstrap continues. The crew layer (crew_command_gate)
1476
+ # is fail-closed — it treats an absent probe key as missing (CH-02).
1477
+ _probe_wicked_testing(state)
1478
+
1479
+ # 2e. CH-02 safeguard: scan for legacy qe-evaluator entries in reeval-log.jsonl
1480
+ # and amendments.jsonl. Advisory only — does NOT block session startup.
1481
+ # Emits a migration notice when legacy entries are found so the user is
1482
+ # informed at a clean session boundary (not mid-gate).
1483
+ _legacy_reeval_notice = _scan_for_legacy_reeval_entries()
1484
+ if _legacy_reeval_notice and state is not None:
1485
+ state.update(legacy_reeval_entries_detected=True)
1486
+
1487
+ # 3. Load dynamic agents
1488
+ agents_loaded, agents_dict = _load_agents()
1489
+ if state is not None:
1490
+ state.update(agents_loaded=agents_loaded)
1491
+
1492
+ # 3b. Resolve tool-capabilities for agents that declare them
1493
+ resolutions = _resolve_capabilities(agents_dict, config)
1494
+ if resolutions and state is not None:
1495
+ state.update(resolved_capabilities=resolutions)
1496
+
1497
+ # 4. Load last crew project for this workspace
1498
+ workspace = os.environ.get("CLAUDE_PROJECT_NAME") or Path.cwd().name
1499
+ project_data, project_name = _find_active_crew_project(workspace)
1500
+
1501
+ # 6. Run memory decay
1502
+ decay_summary = _run_memory_decay()
1503
+
1504
+ # 6b. Check search index staleness and auto-reindex if needed
1505
+ search_staleness_note = _check_search_staleness()
1506
+ mode_notes = []
1507
+ # Inject wicked-testing blocking notices first (high-priority, AC-2).
1508
+ # These were populated by _probe_wicked_testing() in step 2d above.
1509
+ for _wt_notice in _wt_blocking_notices:
1510
+ mode_notes.append(_wt_notice)
1511
+ # CH-02: inject legacy reeval-log migration notice when legacy entries are found.
1512
+ if _legacy_reeval_notice:
1513
+ mode_notes.append(_legacy_reeval_notice)
1514
+ # Required-peer check: wicked-vault (the evidence backend for gates).
1515
+ _vault_note = _check_vault_dependency()
1516
+ if _vault_note:
1517
+ mode_notes.append(_vault_note)
1518
+ # Required-peer check: wicked-bus (the event backbone for archetype events).
1519
+ _bus_note = _check_bus_dependency()
1520
+ if _bus_note:
1521
+ mode_notes.append(_bus_note)
1522
+ # Required-peer check: wicked-loom (the orchestration runtime garden drives).
1523
+ _loom_note = _check_loom_dependency()
1524
+ if _loom_note:
1525
+ mode_notes.append(_loom_note)
1526
+ if onedrive_path:
1527
+ mode_notes.append(
1528
+ f"[Path] OneDrive directory detected. Resolved base: {onedrive_path}. "
1529
+ "Always quote paths with spaces."
1530
+ )
1531
+ if search_staleness_note:
1532
+ mode_notes.append(f"[Search] {search_staleness_note}")
1533
+
1534
+ # 6c. Surface prior session's guard-pipeline findings (Issue #448)
1535
+ guard_note = _check_prior_guard_findings()
1536
+ if guard_note:
1537
+ mode_notes.append(guard_note)
1538
+
1539
+ # 7. Check onboarding status (search index + memories)
1540
+ has_index, has_memories, onboarding_directive = _check_onboarding_status()
1541
+ _log("bootstrap", "normal", "onboarding.status",
1542
+ ok=(has_memories and has_index),
1543
+ detail={"has_memories": has_memories, "has_index": has_index})
1544
+
1545
+ # Persist onboarding gate flags so prompt_submit can enforce them
1546
+ if state is not None:
1547
+ if not has_memories:
1548
+ state.update(
1549
+ needs_onboarding=True,
1550
+ onboarding_complete=False,
1551
+ )
1552
+ else:
1553
+ state.update(
1554
+ needs_onboarding=False,
1555
+ onboarding_complete=True,
1556
+ )
1557
+
1558
+ # 7b. Probe installed plugin hooks for readiness
1559
+ unready_plugins = _probe_plugin_readiness()
1560
+ if unready_plugins and state is not None:
1561
+ state.update(unready_plugins=[p["name"] for p in unready_plugins])
1562
+ if unready_plugins:
1563
+ plugin_lines = []
1564
+ for p in unready_plugins:
1565
+ fix_cmd = _suggest_auth_fix(p)
1566
+ plugin_lines.append(f" - **{p['name']}**: {p['error']}")
1567
+ if fix_cmd:
1568
+ plugin_lines.append(f" Fix: `{fix_cmd}`")
1569
+ mode_notes.append(
1570
+ f"[Plugins] {len(unready_plugins)} plugin(s) installed but not ready:\n"
1571
+ + "\n".join(plugin_lines)
1572
+ + "\n After authenticating, run `/wicked-garden:platform:health` to re-probe."
1573
+ )
1574
+
1575
+ # 7c. Detect dangerous mode (AskUserQuestion broken)
1576
+ dangerous_mode = _detect_dangerous_mode()
1577
+ if state is not None:
1578
+ state.update(dangerous_mode=dangerous_mode)
1579
+
1580
+ # 7c.1. Critical v6 skill smoke-test (issue #434)
1581
+ skills_note = _check_critical_skills()
1582
+ if skills_note:
1583
+ mode_notes.append(skills_note)
1584
+
1585
+ # 7c.2. Registry allowlist validation — surface drift between
1586
+ # in-code allowlists (phases, reviewers, bus handlers, skill refs)
1587
+ # and the assets they reference. Fail-OPEN: validator errors are
1588
+ # swallowed inside _run_registry_validation, returning None so the
1589
+ # briefing simply omits the [Registry] line on failure. Findings
1590
+ # are advisory at the session-start surface — they do NOT block.
1591
+ registry_note = _run_registry_validation()
1592
+ if registry_note:
1593
+ mode_notes.append(registry_note)
1594
+
1595
+ # 7d. Check wicked-brain dependency (opt-in memory/context layer)
1596
+ brain_available, brain_note = _check_brain_dependency()
1597
+ if state is not None and brain_available is not None:
1598
+ state.update(brain_available=brain_available)
1599
+ if brain_note:
1600
+ # brain is an opt-in toolkit layer, not a hard requirement — surface
1601
+ # the install pointer informationally, never block the session.
1602
+ mode_notes.append(brain_note)
1603
+
1604
+ # 7e. Brain auto-init and auto-ingest
1605
+ # If brain is installed but has no content for this project, emit
1606
+ # a directive so the agent auto-runs init + ingest on first use.
1607
+ # If the server isn't reachable, emit a start directive instead.
1608
+ if brain_available:
1609
+ stats = _brain_api("stats", {}, timeout=2)
1610
+ if stats is None:
1611
+ # The dependency check above already attempted a deterministic
1612
+ # auto-start; re-probe once in case readiness landed late.
1613
+ try:
1614
+ from _brain_port import ensure_server
1615
+ if ensure_server():
1616
+ stats = _brain_api("stats", {}, timeout=2)
1617
+ except Exception:
1618
+ pass
1619
+ if stats is None:
1620
+ mode_notes.append(
1621
+ "[wicked-brain] Plugin installed but the server did not respond "
1622
+ "after auto-start. Diagnose with `wicked-brain-call --start`, "
1623
+ "then run the brain pipeline: ingest → retag → compile."
1624
+ )
1625
+ elif stats.get("total", 0) == 0:
1626
+ mode_notes.append(
1627
+ "[wicked-brain] Brain is empty for this project. "
1628
+ "You MUST run the full brain pipeline now as background agents:\n"
1629
+ " 1. `wicked-brain:init` — initialize the brain if needed\n"
1630
+ " 2. `wicked-brain:ingest` with the current working directory\n"
1631
+ " 3. `wicked-brain:retag` — expand keywords into semantic tags\n"
1632
+ " 4. `wicked-brain:compile` — synthesize wiki articles from chunks\n"
1633
+ "Run these sequentially in a background agent. "
1634
+ "Do NOT wait for completion before responding to the user."
1635
+ )
1636
+
1637
+ # 8. Assemble session briefing
1638
+ # --- Status block (user-facing) ---
1639
+ project = os.environ.get("CLAUDE_PROJECT_NAME") or Path.cwd().name
1640
+
1641
+ onboarding_parts = []
1642
+ if has_index:
1643
+ onboarding_parts.append("search index")
1644
+ if has_memories:
1645
+ onboarding_parts.append("memories")
1646
+ if onboarding_parts:
1647
+ onboarding_status = "Complete (" + " + ".join(onboarding_parts) + ")"
1648
+ else:
1649
+ onboarding_status = "Not started"
1650
+
1651
+ status_lines = [
1652
+ f"wicked-garden | {project}",
1653
+ " Storage: local",
1654
+ f" Onboarding: {onboarding_status}",
1655
+ ]
1656
+
1657
+ # Show last crew project for this workspace as informational only.
1658
+ # Do NOT auto-activate — the user must explicitly start/resume a project.
1659
+ if project_data:
1660
+ name = project_data.get("name", project_name or "unknown")
1661
+ phase = project_data.get("current_phase", "unknown")
1662
+ status_lines.append(f" Last crew: {name} ({phase})")
1663
+
1664
+ # Store as reference info only — NOT as active_project
1665
+ if state is not None:
1666
+ # active_project_id: only set when project is in a non-complete, non-skipped phase
1667
+ active_phase = project_data.get("current_phase", "")
1668
+ is_active = active_phase not in ("", "complete", "done", "archived")
1669
+ state.update(active_project_id=project_name if is_active else None)
1670
+
1671
+ # v9.2.4: also write active_chain_id so smaht's chain-aware
1672
+ # event scoring (CLAUDE.md "Chain-aware smaht scoring") actually
1673
+ # works. Field declared in v9.2.3, producer was missing — events
1674
+ # got the 0.1 baseline instead of the documented 0.8+ chain
1675
+ # boost. Format mirrors the chain_id convention in CLAUDE.md
1676
+ # "Native Tasks as Dual-Purpose Event Queue":
1677
+ # {slug}.root for the root chain
1678
+ # {slug}.{phase} per active phase
1679
+ # Cleared (set None) when the project is not active so events
1680
+ # in unrelated sessions don't accidentally inherit the chain.
1681
+ if is_active:
1682
+ chain_id = f"{project_name}.{active_phase}" if active_phase != "clarify" else f"{project_name}.root"
1683
+ state.update(active_chain_id=chain_id)
1684
+ else:
1685
+ state.update(active_chain_id=None)
1686
+
1687
+ # v9.2.5: also write active_project (the dict snapshot, distinct
1688
+ # from active_project_id which is the slug). Read by:
1689
+ # - post_tool.py:1428-1431 (tracks current_phase + current_specialist
1690
+ # for QE change-tracking nudge)
1691
+ # - pre_compact.py (WIP recovery dump shows "Active Project" section)
1692
+ # Producer was missing — both consumers got None and silently fell
1693
+ # through their default branches. Same bug class as active_chain_id
1694
+ # (declared, read, never written). Same fix: write in bootstrap when
1695
+ # the project is active.
1696
+ if is_active:
1697
+ state.update(active_project=dict(project_data))
1698
+ else:
1699
+ state.update(active_project=None)
1700
+
1701
+ # #459 telemetry producer: anchor complexity_at_session_open
1702
+ # from the active project's complexity_score so session-close
1703
+ # drift capture has a delta baseline. Only set when not
1704
+ # already populated this session (first SessionStart wins).
1705
+ try:
1706
+ raw_complexity = project_data.get("complexity_score", 0)
1707
+ complexity_val = int(raw_complexity) if raw_complexity is not None else 0
1708
+ updates = {"complexity_score": complexity_val}
1709
+ if getattr(state, "complexity_at_session_open", None) is None:
1710
+ updates["complexity_at_session_open"] = complexity_val
1711
+ state.update(**updates)
1712
+ except (TypeError, ValueError):
1713
+ # Malformed complexity_score — fail-open, don't block bootstrap.
1714
+ pass
1715
+
1716
+ # Enable memory compliance directives for crew sessions.
1717
+ # task_completed.py reads this flag to decide whether to emit
1718
+ # per-task memory prompts; stop.py reads it for session summary.
1719
+ if state is not None:
1720
+ state.update(memory_compliance_required=True)
1721
+
1722
+ briefing_parts = ["\n".join(status_lines)]
1723
+
1724
+ # --- Solo-mode HITL banner (#651) ---
1725
+ # Emit once per session when the active crew project has solo_mode: true.
1726
+ if project_data and project_data.get("solo_mode"):
1727
+ briefing_parts.append(
1728
+ "[SOLO MODE ACTIVE] Council gates replaced with inline human review "
1729
+ "for this project. Gate artifacts are preserved. "
1730
+ "Merge-gate council pattern is unchanged."
1731
+ )
1732
+
1733
+ # --- Mode-specific warnings/notes ---
1734
+ for note in mode_notes:
1735
+ briefing_parts.append(note)
1736
+
1737
+ if decay_summary:
1738
+ briefing_parts.append(f"[Memory] {decay_summary}")
1739
+
1740
+ # NOTE (v9.2.10): the per-session _MEMORY_INSTRUCTIONS append was
1741
+ # removed. CLAUDE.md's "Memory Management" section already covers
1742
+ # what the briefing was telling Claude. v9.2.11 deleted the constant
1743
+ # itself after confirming zero imports.
1744
+
1745
+ if dangerous_mode:
1746
+ briefing_parts.append(_DANGEROUS_MODE_WARNING)
1747
+
1748
+ # [Setup] and [Quick Start] are first-session-per-project notices.
1749
+ # Once shown for a project, they are recorded in the per-project
1750
+ # sentinel file and suppressed on every subsequent bootstrap. v9.2.10.
1751
+ if not onboarding_directive:
1752
+ if not _notice_already_shown(_NOTICE_SETUP):
1753
+ briefing_parts.append(
1754
+ "[Setup] Run `/wicked-garden:setup --reconfigure` to change connection or re-onboard."
1755
+ )
1756
+ _record_notice_shown(_NOTICE_SETUP)
1757
+
1758
+ # Discovery: show 2-3 contextual command suggestions based on project files.
1759
+ # Also gated on first-session-per-project since the suggestions don't change
1760
+ # session-to-session.
1761
+ if not onboarding_directive and has_memories:
1762
+ if not _notice_already_shown(_NOTICE_QUICK_START):
1763
+ discovery_lines = _suggest_commands_for_project()
1764
+ if discovery_lines:
1765
+ briefing_parts.append(discovery_lines)
1766
+ _record_notice_shown(_NOTICE_QUICK_START)
1767
+
1768
+ # Facilitator context: surface aging action items from process_memory.
1769
+ # Additive briefing line (issue #461) — runs last of the informational
1770
+ # notes and is independent of onboarding or setup state.
1771
+ fc_note = _check_facilitator_context(project_name)
1772
+ if fc_note:
1773
+ briefing_parts.append(fc_note)
1774
+
1775
+ # Onboarding directive goes LAST for highest priority
1776
+ if onboarding_directive:
1777
+ briefing_parts.append(onboarding_directive)
1778
+
1779
+ # 9. Persist final session state
1780
+ if state is not None:
1781
+ _save_session_state(state)
1782
+
1783
+ _log("bootstrap", "debug", "hook.end", ms=int((time.monotonic() - _t0) * 1000))
1784
+ print(json.dumps({
1785
+ "hookSpecificOutput": {
1786
+ "hookEventName": "SessionStart",
1787
+ "additionalContext": "\n".join(briefing_parts),
1788
+ },
1789
+ }))
1790
+
1791
+ except Exception as e:
1792
+ print(f"[wicked-garden] bootstrap error: {e}", file=sys.stderr)
1793
+ print(json.dumps({}))
1794
+
1795
+
1796
+ if __name__ == "__main__":
1797
+ main()