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,856 @@
1
+ #!/usr/bin/env python3
2
+ """scripts/smaht/propose_skills.py — session-mined skill builder (MVP for #677).
3
+
4
+ Read-only analyzer over Claude Code session transcripts.
5
+
6
+ Reads ``${CLAUDE_CONFIG_DIR:-~/.claude}/projects/{project-slug}/*.jsonl`` for the
7
+ current project (default: cwd basename → slug), detects repetitive patterns across
8
+ sessions, clusters overlapping detections, and writes a markdown report at
9
+ ``tempfile.gettempdir()/wg-propose-skills-{timestamp}.md`` (on macOS this resolves
10
+ to a per-user dir under ``/var/folders/...``).
11
+
12
+ Hard constraints (per scope and CLAUDE.md):
13
+
14
+ * **stdlib-only** — no external deps.
15
+ * **read-only** — never writes outside ``tempfile.gettempdir()``.
16
+ * **local-only** — no network, no telemetry, no LLM calls.
17
+ * **cross-platform** — uses ``pathlib.Path`` and ``tempfile.gettempdir()``; honors
18
+ ``CLAUDE_CONFIG_DIR`` for users running Claude Code with a non-default config.
19
+ * **privacy** — scrubs absolute paths to ``~/...`` form (matched at path-segment
20
+ boundaries to avoid substring scrubbing), skips any session whose user prompt
21
+ contains the literal token ``private`` or ``secret``. Privacy check runs on the
22
+ full untruncated prompt before any 200-char trimming.
23
+
24
+ The MVP intentionally outputs a markdown report only. There is no interactive
25
+ UI and no scaffolding handoff in v1 — both are explicit v2 follow-ups
26
+ documented in the report's footer.
27
+
28
+ Usage::
29
+
30
+ python3 scripts/smaht/propose_skills.py [--project SLUG] [--limit N]
31
+ [--sessions-root PATH]
32
+ [--output PATH] [--json]
33
+
34
+ Exit codes:
35
+
36
+ * ``0`` — graceful run (even when no candidates are found).
37
+ * ``1`` — failed to write the report (I/O error after analysis succeeded).
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import argparse
43
+ import json
44
+ import os
45
+ import re
46
+ import sys
47
+ import tempfile
48
+ import time
49
+ from collections import Counter, defaultdict
50
+ from pathlib import Path
51
+ from typing import Any, Iterable
52
+
53
+ # ---------------------------------------------------------------------------
54
+ # Constants — surfaced as module-level so tests can patch / inspect.
55
+ # ---------------------------------------------------------------------------
56
+
57
+ # Patterns whose first 5 normalized words are skipped as "too generic" — they
58
+ # inflate every report otherwise (e.g. continuations and approvals).
59
+ _GENERIC_PROMPT_PREFIXES: frozenset[str] = frozenset(
60
+ {
61
+ "yes",
62
+ "ok",
63
+ "okay",
64
+ "continue",
65
+ "go",
66
+ "do it",
67
+ "looks good",
68
+ "thanks",
69
+ "thank you",
70
+ "sounds good",
71
+ "lgtm",
72
+ }
73
+ )
74
+
75
+ # Bash command first-tokens we ignore (file inspection, not workflow).
76
+ _GENERIC_BASH_TOKENS: frozenset[str] = frozenset(
77
+ {"ls", "cat", "echo", "cd", "pwd", "true", "false", ":"}
78
+ )
79
+
80
+ # Min total occurrences (across all sessions, with intra-session repeats counted)
81
+ # before a candidate qualifies.
82
+ MIN_FREQUENCY = 3
83
+ # Min DISTINCT sessions a candidate must appear in. Frequency alone is not enough —
84
+ # a single chatty session shouldn't surface a workflow as a cross-cutting pattern.
85
+ MIN_SESSION_COUNT = 2
86
+
87
+ # Sequence detection bounds — short sequences are noisy, long sequences are rare.
88
+ SEQUENCE_MIN_LEN = 2
89
+ SEQUENCE_MAX_LEN = 5
90
+
91
+ # Privacy: skip whole sessions whose user prompts mention these tokens.
92
+ PRIVACY_SKIP_TOKENS: tuple[str, ...] = ("private", "secret")
93
+
94
+ # Cap report candidates so the markdown stays scannable.
95
+ TOP_CANDIDATES = 10
96
+
97
+ # Frontmatter description hard limit — matches Claude Code's surface convention.
98
+ DESCRIPTION_MAX_CHARS = 140
99
+
100
+ # Domain inference lookup — picks the scaffolding domain by string heuristic on
101
+ # the tool names + bash tokens that appear inside the pattern.
102
+ _DOMAIN_KEYWORDS: dict[str, tuple[str, ...]] = {
103
+ "platform": ("gh", "docker", "kubectl", "helm", "terraform", "aws", "gcloud"),
104
+ "engineering": ("git", "pytest", "npm", "node", "uv", "ruff", "mypy"),
105
+ "search": ("grep", "rg", "glob", "find"),
106
+ "qe": ("test", "wg-test", "scenarios"),
107
+ "data": ("psql", "sqlite", "duckdb", "jq"),
108
+ "delivery": ("release", "changelog", "version"),
109
+ }
110
+
111
+
112
+ # ---------------------------------------------------------------------------
113
+ # Project + session discovery.
114
+ # ---------------------------------------------------------------------------
115
+
116
+
117
+ def project_slug(cwd: Path | None = None) -> str:
118
+ """Return the Claude Code project slug for ``cwd`` (default: real cwd).
119
+
120
+ Claude Code encodes project paths by replacing path separators *and dots*
121
+ with ``-`` and prefixing with ``-``. Cross-platform: uses ``Path.as_posix()``
122
+ so Windows ``\\`` separators and drive letters (``C:``) are normalized before
123
+ the substitution.
124
+
125
+ Example (POSIX): ``/home/alice/Projects/wicked-garden`` →
126
+ ``-home-alice-Projects-wicked-garden``.
127
+ Example (dotted user): ``/Users/first.last/Projects/wg`` →
128
+ ``-Users-first-last-Projects-wg`` (the ``.`` becomes ``-``).
129
+ Example (Windows): ``C:\\Users\\x\\Projects\\wg`` → ``-C-Users-x-Projects-wg``.
130
+ """
131
+ base = (cwd or Path.cwd()).expanduser().resolve()
132
+ posix = base.as_posix()
133
+ # Strip the Windows drive colon (e.g. ``C:`` → ``C``) so the slug stays
134
+ # filesystem-safe under Claude Code's project-dir naming convention.
135
+ posix = posix.replace(":", "")
136
+ # Claude Code replaces dots with ``-`` too, so a dotted username
137
+ # (``first.last``) maps to the real ~/.claude/projects/ directory.
138
+ return "-" + posix.strip("/").replace("/", "-").replace(".", "-")
139
+
140
+
141
+ def resolve_claude_config_dir() -> Path:
142
+ """Return the active Claude config dir, honoring ``CLAUDE_CONFIG_DIR``.
143
+
144
+ Falls back to ``~/.claude`` when the env var is unset or empty. Mirrors
145
+ the convention documented in CLAUDE.md (``${CLAUDE_CONFIG_DIR:-~/.claude}``).
146
+ """
147
+ env = os.environ.get("CLAUDE_CONFIG_DIR")
148
+ if env:
149
+ return Path(env).expanduser()
150
+ return Path.home() / ".claude"
151
+
152
+
153
+ def session_root() -> Path:
154
+ """Return the default Claude Code session root for the active config dir."""
155
+ return resolve_claude_config_dir() / "projects"
156
+
157
+
158
+ def default_sessions_root() -> Path:
159
+ """Backward-compat alias for ``session_root()``. Honors ``CLAUDE_CONFIG_DIR``."""
160
+ return session_root()
161
+
162
+
163
+ def find_session_files(
164
+ *,
165
+ sessions_root: Path,
166
+ project: str,
167
+ limit: int,
168
+ ) -> list[Path]:
169
+ """Return the ``limit`` most recently modified ``*.jsonl`` files for ``project``."""
170
+ project_dir = sessions_root / project
171
+ if not project_dir.is_dir():
172
+ return []
173
+ files = [p for p in project_dir.glob("*.jsonl") if p.is_file()]
174
+ files.sort(key=lambda p: p.stat().st_mtime, reverse=True)
175
+ return files[: max(0, int(limit))]
176
+
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # Privacy scrub.
180
+ # ---------------------------------------------------------------------------
181
+
182
+
183
+ _HOME_STR = str(Path.home())
184
+
185
+
186
+ def scrub_path(text: str, *, home: str = _HOME_STR) -> str:
187
+ """Replace ``$HOME`` in ``text`` with ``~``, matched at path boundaries only.
188
+
189
+ The naive ``str.replace`` approach corrupts neighbouring paths — e.g. with
190
+ ``home='/Users/alice'`` an input ``/Users/alice2/`` would become ``~2/``.
191
+ To avoid that, ``home`` only matches when followed by end-of-string, ``/``,
192
+ or ``\\`` (the path-separator boundary). Idempotent — running twice on an
193
+ already-scrubbed string is a no-op.
194
+ """
195
+ if not text or not home:
196
+ return text
197
+ pattern = re.escape(home) + r"(?=$|[/\\])"
198
+ return re.sub(pattern, "~", text)
199
+
200
+
201
+ def _prompt_is_private(prompt: str) -> bool:
202
+ """Return True when ``prompt`` (untruncated) contains a privacy-skip token."""
203
+ low = (prompt or "").lower()
204
+ for token in PRIVACY_SKIP_TOKENS:
205
+ if token in low:
206
+ return True
207
+ return False
208
+
209
+
210
+ def session_is_private(user_prompts: Iterable[str]) -> bool:
211
+ """Return ``True`` when any prompt contains a privacy-skip token (case-insensitive).
212
+
213
+ Note: ``parse_session`` now sets a ``privacy_skip`` flag on the digest using
214
+ the full untruncated prompt text. ``analyze`` prefers that flag, so this
215
+ function is only consulted for tests and external callers that hand-build
216
+ session digests.
217
+ """
218
+ return any(_prompt_is_private(p) for p in user_prompts)
219
+
220
+
221
+ # ---------------------------------------------------------------------------
222
+ # Session parsing.
223
+ # ---------------------------------------------------------------------------
224
+
225
+
226
+ def _extract_user_text(content: Any) -> str:
227
+ """Return the leading text of a user message ``content`` field (str or list)."""
228
+ if isinstance(content, str):
229
+ return content
230
+ if isinstance(content, list):
231
+ for item in content:
232
+ if isinstance(item, dict) and item.get("type") == "text":
233
+ txt = item.get("text", "")
234
+ if isinstance(txt, str) and txt.strip():
235
+ return txt
236
+ return ""
237
+
238
+
239
+ def _extract_tool_uses(content: Any) -> list[dict[str, Any]]:
240
+ """Return assistant ``tool_use`` items as ``{"name": str, "input": dict}`` records."""
241
+ out: list[dict[str, Any]] = []
242
+ if not isinstance(content, list):
243
+ return out
244
+ for item in content:
245
+ if not isinstance(item, dict):
246
+ continue
247
+ if item.get("type") != "tool_use":
248
+ continue
249
+ name = item.get("name") or "?"
250
+ inp = item.get("input") if isinstance(item.get("input"), dict) else {}
251
+ out.append({"name": str(name), "input": inp})
252
+ return out
253
+
254
+
255
+ def parse_session(path: Path) -> dict[str, Any]:
256
+ """Parse one ``*.jsonl`` session file and return a structured digest.
257
+
258
+ Returns ``{"path": str, "user_prompts": [...], "tool_calls": [...],
259
+ "privacy_skip": bool}``. The privacy check inspects the FULL untruncated
260
+ user-prompt text (so a ``private``/``secret`` token past the 200-char
261
+ detector limit is still caught) — only after that do we trim each prompt
262
+ to 200 chars for downstream detector economy. Robust against malformed
263
+ lines: each bad line is skipped, never raised.
264
+ """
265
+ user_prompts: list[str] = []
266
+ tool_calls: list[dict[str, Any]] = []
267
+ privacy_skip = False
268
+ try:
269
+ text = path.read_text(encoding="utf-8", errors="replace")
270
+ except OSError:
271
+ return {
272
+ "path": str(path),
273
+ "user_prompts": user_prompts,
274
+ "tool_calls": tool_calls,
275
+ "privacy_skip": privacy_skip,
276
+ }
277
+ for line in text.splitlines():
278
+ line = line.strip()
279
+ if not line:
280
+ continue
281
+ try:
282
+ obj = json.loads(line)
283
+ except json.JSONDecodeError:
284
+ continue
285
+ if not isinstance(obj, dict):
286
+ continue
287
+ record_type = obj.get("type")
288
+ message = obj.get("message") if isinstance(obj.get("message"), dict) else {}
289
+ content = message.get("content") if message else None
290
+ if record_type == "user":
291
+ txt = _extract_user_text(content if content is not None else obj.get("message"))
292
+ if txt:
293
+ # Privacy gate runs on the FULL prompt — never on the truncated
294
+ # detector view — so a trigger token past char 200 still flags.
295
+ if not privacy_skip and _prompt_is_private(txt):
296
+ privacy_skip = True
297
+ user_prompts.append(txt[:200])
298
+ elif record_type == "assistant":
299
+ tool_calls.extend(_extract_tool_uses(content))
300
+ return {
301
+ "path": str(path),
302
+ "user_prompts": user_prompts,
303
+ "tool_calls": tool_calls,
304
+ "privacy_skip": privacy_skip,
305
+ }
306
+
307
+
308
+ # ---------------------------------------------------------------------------
309
+ # Pattern detectors.
310
+ # ---------------------------------------------------------------------------
311
+
312
+
313
+ _PROMPT_PUNCT_RE = re.compile(r"[^\w\s]+", re.UNICODE)
314
+
315
+
316
+ def _normalize_prompt_prefix(prompt: str) -> str | None:
317
+ """Return the first 5 normalized words of ``prompt`` (lowercase, no punct).
318
+
319
+ Returns ``None`` for prompts shorter than 5 words, whose prefix is in the
320
+ generic-prompt blocklist, or that look like a Claude Code system envelope
321
+ rather than a user-authored prompt (e.g. ``<local-command-...>`` or
322
+ ``<command-name>...</command-name>`` wrappers).
323
+
324
+ The punctuation strip uses a Unicode-aware ``\\w``/``\\s`` character class
325
+ so CJK ideographs, accented Latin characters, and other non-ASCII letters
326
+ are preserved as part of the prefix instead of being collapsed to spaces.
327
+ """
328
+ if not prompt:
329
+ return None
330
+ stripped = prompt.lstrip()
331
+ if stripped.startswith("<local-command") or stripped.startswith("<command-"):
332
+ return None
333
+ cleaned = _PROMPT_PUNCT_RE.sub(" ", prompt.lower()).strip()
334
+ if not cleaned:
335
+ return None
336
+ words = cleaned.split()
337
+ if len(words) < 5:
338
+ return None
339
+ prefix = " ".join(words[:5])
340
+ if prefix in _GENERIC_PROMPT_PREFIXES:
341
+ return None
342
+ # Reject if first word alone is a generic single-token continuation.
343
+ if words[0] in _GENERIC_PROMPT_PREFIXES:
344
+ return None
345
+ # Reject if any multi-word generic phrase ("do it", "looks good", "thank you",
346
+ # "sounds good") appears as a substring of the normalized prefix. These never
347
+ # match the equality check above because they're shorter than 5 words.
348
+ for generic in _GENERIC_PROMPT_PREFIXES:
349
+ if " " in generic and generic in prefix:
350
+ return None
351
+ return prefix
352
+
353
+
354
+ def _bash_first_tokens(input_dict: dict[str, Any]) -> tuple[str, ...] | None:
355
+ """Return the first 2 tokens of a Bash command, or ``None`` for generic shapes."""
356
+ cmd = input_dict.get("command")
357
+ if not isinstance(cmd, str):
358
+ return None
359
+ tokens = cmd.strip().split()
360
+ if len(tokens) < 2:
361
+ return None
362
+ head = tokens[0].lstrip("()`$\"'")
363
+ if not head or head in _GENERIC_BASH_TOKENS:
364
+ return None
365
+ return (head, tokens[1])
366
+
367
+
368
+ def _is_homogeneous(tup: tuple[str, ...]) -> bool:
369
+ """Return True when every element of ``tup`` is the same tool name.
370
+
371
+ These are noise — e.g. ``Bash → Bash → Bash`` just means the agent ran
372
+ several bash commands in a row, not a workflow worth automating.
373
+ """
374
+ return len(set(tup)) <= 1
375
+
376
+
377
+ def detect_repeated_sequences(
378
+ sessions: list[dict[str, Any]],
379
+ *,
380
+ min_freq: int = MIN_FREQUENCY,
381
+ min_len: int = SEQUENCE_MIN_LEN,
382
+ max_len: int = SEQUENCE_MAX_LEN,
383
+ min_sessions: int = MIN_SESSION_COUNT,
384
+ ) -> list[dict[str, Any]]:
385
+ """Detect ordered N-tuples of tool names appearing ``>= min_freq`` times.
386
+
387
+ Tracks two independent counters per N-tuple:
388
+
389
+ * ``frequency`` — TOTAL occurrences across all sessions (with intra-session
390
+ repeats included). Reflects how often the agent runs the pattern overall.
391
+ * ``sessions`` — number of DISTINCT sessions the N-tuple appears in.
392
+ Reflects how broadly the pattern shows up across the project's history.
393
+
394
+ A candidate must satisfy ``frequency >= min_freq`` AND
395
+ ``sessions >= min_sessions``. Homogeneous sequences (all the same tool name)
396
+ are filtered because they reflect run-of-the-mill repetition, not a procedure.
397
+ """
398
+ total_counter: Counter[tuple[str, ...]] = Counter()
399
+ sessions_by_seq: dict[tuple[str, ...], set[str]] = defaultdict(set)
400
+ examples: dict[tuple[str, ...], str] = {}
401
+ for sess in sessions:
402
+ names = [tc["name"] for tc in sess["tool_calls"]]
403
+ sid = sess["path"]
404
+ for n in range(min_len, max_len + 1):
405
+ for i in range(0, max(0, len(names) - n + 1)):
406
+ tup = tuple(names[i : i + n])
407
+ if _is_homogeneous(tup):
408
+ continue
409
+ # Count EVERY occurrence — repeats within a session count, so
410
+ # frequency reflects true workload, not just session breadth.
411
+ total_counter[tup] += 1
412
+ sessions_by_seq[tup].add(sid)
413
+ examples.setdefault(tup, " → ".join(tup))
414
+ out: list[dict[str, Any]] = []
415
+ for tup, freq in total_counter.items():
416
+ sess_count = len(sessions_by_seq[tup])
417
+ if freq < min_freq or sess_count < min_sessions:
418
+ continue
419
+ out.append(
420
+ {
421
+ "kind": "tool-sequence",
422
+ "key": tup,
423
+ "frequency": freq,
424
+ "sessions": sess_count,
425
+ "example": examples[tup],
426
+ "names": list(tup),
427
+ }
428
+ )
429
+ return out
430
+
431
+
432
+ def detect_repeated_prompt_templates(
433
+ sessions: list[dict[str, Any]],
434
+ *,
435
+ min_freq: int = MIN_FREQUENCY,
436
+ min_sessions: int = MIN_SESSION_COUNT,
437
+ ) -> list[dict[str, Any]]:
438
+ """Detect user prompts whose first 5 normalized words match across sessions.
439
+
440
+ Tracks both total occurrences and distinct-session count (see
441
+ ``detect_repeated_sequences`` for rationale). A candidate must satisfy
442
+ both ``min_freq`` and ``min_sessions``.
443
+ """
444
+ total_counter: Counter[str] = Counter()
445
+ sessions_by_prefix: dict[str, set[str]] = defaultdict(set)
446
+ examples: dict[str, str] = {}
447
+ for sess in sessions:
448
+ sid = sess["path"]
449
+ for prompt in sess["user_prompts"]:
450
+ prefix = _normalize_prompt_prefix(prompt)
451
+ if prefix is None:
452
+ continue
453
+ total_counter[prefix] += 1
454
+ sessions_by_prefix[prefix].add(sid)
455
+ examples.setdefault(prefix, prompt[:160])
456
+ out: list[dict[str, Any]] = []
457
+ for prefix, freq in total_counter.items():
458
+ sess_count = len(sessions_by_prefix[prefix])
459
+ if freq < min_freq or sess_count < min_sessions:
460
+ continue
461
+ out.append(
462
+ {
463
+ "kind": "prompt-template",
464
+ "key": prefix,
465
+ "frequency": freq,
466
+ "sessions": sess_count,
467
+ "example": examples[prefix],
468
+ }
469
+ )
470
+ return out
471
+
472
+
473
+ def detect_repeated_bash_shapes(
474
+ sessions: list[dict[str, Any]],
475
+ *,
476
+ min_freq: int = MIN_FREQUENCY,
477
+ min_sessions: int = MIN_SESSION_COUNT,
478
+ ) -> list[dict[str, Any]]:
479
+ """Detect bash commands sharing the same first 2 tokens across sessions.
480
+
481
+ Tracks both total occurrences and distinct-session count (see
482
+ ``detect_repeated_sequences`` for rationale). A candidate must satisfy
483
+ both ``min_freq`` and ``min_sessions``.
484
+ """
485
+ total_counter: Counter[tuple[str, str]] = Counter()
486
+ sessions_by_shape: dict[tuple[str, str], set[str]] = defaultdict(set)
487
+ examples: dict[tuple[str, str], str] = {}
488
+ for sess in sessions:
489
+ sid = sess["path"]
490
+ for tc in sess["tool_calls"]:
491
+ if tc["name"] != "Bash":
492
+ continue
493
+ shape = _bash_first_tokens(tc.get("input") or {})
494
+ if shape is None:
495
+ continue
496
+ total_counter[shape] += 1
497
+ sessions_by_shape[shape].add(sid)
498
+ cmd = (tc.get("input") or {}).get("command", "")
499
+ if isinstance(cmd, str):
500
+ examples.setdefault(shape, cmd[:160])
501
+ out: list[dict[str, Any]] = []
502
+ for shape, freq in total_counter.items():
503
+ sess_count = len(sessions_by_shape[shape])
504
+ if freq < min_freq or sess_count < min_sessions:
505
+ continue
506
+ out.append(
507
+ {
508
+ "kind": "bash-shape",
509
+ "key": shape,
510
+ "frequency": freq,
511
+ "sessions": sess_count,
512
+ "example": examples.get(shape, " ".join(shape)),
513
+ "names": list(shape),
514
+ }
515
+ )
516
+ return out
517
+
518
+
519
+ # ---------------------------------------------------------------------------
520
+ # Deduplication — drop sub-sequences subsumed by a more frequent super-sequence.
521
+ # ---------------------------------------------------------------------------
522
+
523
+
524
+ def _is_subsequence(short: tuple[str, ...], long: tuple[str, ...]) -> bool:
525
+ """Return True if ``short`` appears as a contiguous subsequence of ``long``."""
526
+ if len(short) >= len(long):
527
+ return False
528
+ for i in range(0, len(long) - len(short) + 1):
529
+ if long[i : i + len(short)] == short:
530
+ return True
531
+ return False
532
+
533
+
534
+ def dedupe_candidates(candidates: list[dict[str, Any]]) -> list[dict[str, Any]]:
535
+ """Drop tool-sequence candidates that are sub-sequences of a more frequent super-sequence.
536
+
537
+ Non tool-sequence kinds pass through untouched. Among tool-sequence
538
+ candidates, ``short`` is dropped only when a strictly longer ``long`` exists
539
+ with frequency ``>=`` ``short.frequency`` AND ``short`` is contiguous in
540
+ ``long`` — this preserves cases where the shorter pattern is genuinely more
541
+ common in different contexts.
542
+ """
543
+ sequences = [c for c in candidates if c["kind"] == "tool-sequence"]
544
+ others = [c for c in candidates if c["kind"] != "tool-sequence"]
545
+ keep: list[dict[str, Any]] = []
546
+ for short in sequences:
547
+ short_key = tuple(short["key"])
548
+ is_subsumed = False
549
+ for long in sequences:
550
+ if long is short:
551
+ continue
552
+ long_key = tuple(long["key"])
553
+ if not _is_subsequence(short_key, long_key):
554
+ continue
555
+ if long["frequency"] >= short["frequency"]:
556
+ is_subsumed = True
557
+ break
558
+ if not is_subsumed:
559
+ keep.append(short)
560
+ return keep + others
561
+
562
+
563
+ # ---------------------------------------------------------------------------
564
+ # Skill proposal builder — name + description + scaffolding command.
565
+ # ---------------------------------------------------------------------------
566
+
567
+
568
+ def _slugify(text: str, *, max_len: int = 48) -> str:
569
+ """Return a kebab-case slug from ``text`` (alnum + hyphens only, no leading/trailing hyphens)."""
570
+ cleaned = re.sub(r"[^A-Za-z0-9]+", "-", text.lower()).strip("-")
571
+ if not cleaned:
572
+ cleaned = "candidate"
573
+ return cleaned[:max_len].strip("-") or "candidate"
574
+
575
+
576
+ def infer_domain(candidate: dict[str, Any]) -> str:
577
+ """Pick a scaffolding domain by keyword presence in the candidate's pattern."""
578
+ haystack_parts: list[str] = []
579
+ if candidate["kind"] == "tool-sequence":
580
+ haystack_parts.extend(candidate.get("names", []))
581
+ elif candidate["kind"] == "bash-shape":
582
+ haystack_parts.extend(candidate.get("names", []))
583
+ elif candidate["kind"] == "prompt-template":
584
+ haystack_parts.append(str(candidate.get("key", "")))
585
+ haystack = " ".join(haystack_parts).lower()
586
+ for domain, keywords in _DOMAIN_KEYWORDS.items():
587
+ if any(k in haystack for k in keywords):
588
+ return domain
589
+ return "engineering"
590
+
591
+
592
+ def propose_skill(candidate: dict[str, Any]) -> dict[str, str]:
593
+ """Return ``{"slug", "description", "scaffold_command"}`` for ``candidate``."""
594
+ if candidate["kind"] == "tool-sequence":
595
+ names = candidate["names"]
596
+ slug = _slugify("auto " + "-".join(n.lower() for n in names))
597
+ description = (
598
+ f"Automate the recurring {' → '.join(names)} sequence "
599
+ f"observed {candidate['frequency']}x across {candidate['sessions']} sessions."
600
+ )
601
+ elif candidate["kind"] == "prompt-template":
602
+ slug = _slugify("answer " + candidate["key"])
603
+ description = (
604
+ f"Handle the recurring prompt template "
605
+ f"\"{candidate['key']}…\" — observed {candidate['frequency']}x."
606
+ )
607
+ else: # bash-shape
608
+ head, second = candidate["names"]
609
+ slug = _slugify(f"run {head} {second}")
610
+ description = (
611
+ f"Wrap the recurring `{head} {second} …` shell pattern — "
612
+ f"observed {candidate['frequency']}x across {candidate['sessions']} sessions."
613
+ )
614
+ description = description[:DESCRIPTION_MAX_CHARS]
615
+ domain = infer_domain(candidate)
616
+ return {
617
+ "slug": slug,
618
+ "description": description,
619
+ "domain": domain,
620
+ "scaffold_command": f"/wg-scaffold skill {slug} --domain {domain}",
621
+ }
622
+
623
+
624
+ # ---------------------------------------------------------------------------
625
+ # Report rendering.
626
+ # ---------------------------------------------------------------------------
627
+
628
+
629
+ def _candidate_score(c: dict[str, Any]) -> tuple[int, int, int]:
630
+ """Sort key — frequency desc, sessions desc, length desc."""
631
+ length = len(c.get("names", [])) if c["kind"] != "prompt-template" else 1
632
+ return (-int(c["frequency"]), -int(c["sessions"]), -length)
633
+
634
+
635
+ def render_report(
636
+ *,
637
+ project: str,
638
+ sessions_scanned: int,
639
+ sessions_skipped: int,
640
+ candidates: list[dict[str, Any]],
641
+ timestamp: str,
642
+ ) -> str:
643
+ """Render the markdown report. Pure function — no I/O."""
644
+ lines: list[str] = []
645
+ lines.append("# Session-mined skill proposals")
646
+ lines.append("")
647
+ lines.append(f"_Generated: {timestamp}_")
648
+ lines.append("")
649
+ lines.append(
650
+ f"**Project**: `{project}` · **Sessions scanned**: {sessions_scanned} · "
651
+ f"**Skipped (privacy)**: {sessions_skipped} · "
652
+ f"**Candidates**: {len(candidates)}"
653
+ )
654
+ lines.append("")
655
+ if not candidates:
656
+ lines.append("No repetitive patterns met the minimum-frequency threshold.")
657
+ lines.append("")
658
+ lines.append("This is normal for short or one-off projects — try `--limit 50`")
659
+ lines.append("once you have more session history.")
660
+ return "\n".join(lines) + "\n"
661
+ candidates_sorted = sorted(candidates, key=_candidate_score)[:TOP_CANDIDATES]
662
+ lines.append("## Candidates")
663
+ lines.append("")
664
+ for idx, c in enumerate(candidates_sorted, 1):
665
+ proposal = propose_skill(c)
666
+ lines.append(f"### {idx}. {proposal['slug']}")
667
+ lines.append("")
668
+ lines.append(f"- **Pattern kind**: `{c['kind']}`")
669
+ # Show TOTAL occurrences and DISTINCT sessions independently — when a
670
+ # pattern repeats inside a session, the two numbers genuinely differ.
671
+ lines.append(
672
+ f"- **Frequency**: {c['frequency']} total occurrences across "
673
+ f"{c['sessions']} distinct sessions"
674
+ )
675
+ lines.append(f"- **Proposed description**: {proposal['description']}")
676
+ lines.append(f"- **Suggested scaffold**: `{proposal['scaffold_command']}`")
677
+ example = scrub_path(str(c.get("example", "")))
678
+ lines.append("- **Example**:")
679
+ lines.append("")
680
+ lines.append(" ```")
681
+ lines.append(f" {example}")
682
+ lines.append(" ```")
683
+ lines.append("")
684
+ lines.append("---")
685
+ lines.append("")
686
+ lines.append("## Notes")
687
+ lines.append("")
688
+ lines.append("This is the **MVP** of the session-mined skill builder (#677).")
689
+ lines.append("")
690
+ lines.append("- Detection is heuristic — false positives are expected. Treat each candidate as a prompt for")
691
+ lines.append(" judgment, not a directive.")
692
+ lines.append("- v1 is read-only and does not scaffold anything. Run the suggested `/wg-scaffold` command")
693
+ lines.append(" yourself if a candidate looks worth building.")
694
+ lines.append("- Interactive accept/reject UI and direct scaffolding handoff are explicit v2 / v3 follow-ups.")
695
+ return "\n".join(lines) + "\n"
696
+
697
+
698
+ # ---------------------------------------------------------------------------
699
+ # Orchestration.
700
+ # ---------------------------------------------------------------------------
701
+
702
+
703
+ def analyze(
704
+ *,
705
+ sessions_root: Path,
706
+ project: str,
707
+ limit: int,
708
+ ) -> dict[str, Any]:
709
+ """Run end-to-end analysis and return a structured result.
710
+
711
+ Pure orchestration — never writes to disk. ``run()`` writes the report.
712
+ """
713
+ files = find_session_files(sessions_root=sessions_root, project=project, limit=limit)
714
+ parsed: list[dict[str, Any]] = []
715
+ skipped = 0
716
+ for f in files:
717
+ sess = parse_session(f)
718
+ # Prefer the per-session ``privacy_skip`` flag set by parse_session
719
+ # (which inspects the FULL untruncated prompt). Fall back to the
720
+ # truncated-prompt scan for digests built by external callers (tests).
721
+ if sess.get("privacy_skip") or session_is_private(sess["user_prompts"]):
722
+ skipped += 1
723
+ continue
724
+ parsed.append(sess)
725
+ candidates = (
726
+ detect_repeated_sequences(parsed)
727
+ + detect_repeated_prompt_templates(parsed)
728
+ + detect_repeated_bash_shapes(parsed)
729
+ )
730
+ candidates = dedupe_candidates(candidates)
731
+ return {
732
+ "project": project,
733
+ "sessions_scanned": len(parsed),
734
+ "sessions_skipped": skipped,
735
+ "candidates": candidates,
736
+ }
737
+
738
+
739
+ def _timestamp_slug() -> str:
740
+ return time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
741
+
742
+
743
+ def _json_safe_candidate(c: dict[str, Any]) -> dict[str, Any]:
744
+ """Return a JSON-serializable copy of ``c`` (tuples → lists)."""
745
+ safe: dict[str, Any] = {}
746
+ for k, v in c.items():
747
+ if isinstance(v, tuple):
748
+ safe[k] = list(v)
749
+ else:
750
+ safe[k] = v
751
+ return safe
752
+
753
+
754
+ def run(argv: list[str] | None = None) -> int:
755
+ """CLI entry point. Returns process exit code.
756
+
757
+ * ``0`` on graceful runs (including when no candidates are found).
758
+ * ``1`` when the report file could not be written after a successful analysis.
759
+ """
760
+ parser = argparse.ArgumentParser(
761
+ prog="propose_skills",
762
+ description="Mine recent Claude Code sessions for skill candidates (MVP, read-only).",
763
+ )
764
+ parser.add_argument(
765
+ "--project",
766
+ default=None,
767
+ help="Claude Code project slug (default: derived from current working directory).",
768
+ )
769
+ parser.add_argument(
770
+ "--limit",
771
+ type=int,
772
+ default=10,
773
+ help="Number of most-recent sessions to scan (default: 10).",
774
+ )
775
+ parser.add_argument(
776
+ "--sessions-root",
777
+ default=None,
778
+ help=(
779
+ "Override the Claude session root "
780
+ "(default: ${CLAUDE_CONFIG_DIR:-~/.claude}/projects)."
781
+ ),
782
+ )
783
+ parser.add_argument(
784
+ "--output",
785
+ default=None,
786
+ help="Override the report output path (default: tempfile.gettempdir()/wg-propose-skills-{ts}.md).",
787
+ )
788
+ parser.add_argument(
789
+ "--json",
790
+ action="store_true",
791
+ help=(
792
+ "Print the structured analysis result as JSON to stdout (in addition to "
793
+ "the report). Includes the full candidates payload."
794
+ ),
795
+ )
796
+ args = parser.parse_args(argv)
797
+
798
+ sessions_root = (
799
+ Path(args.sessions_root).expanduser() if args.sessions_root else default_sessions_root()
800
+ )
801
+ project = args.project or project_slug()
802
+ timestamp = _timestamp_slug()
803
+
804
+ try:
805
+ result = analyze(sessions_root=sessions_root, project=project, limit=args.limit)
806
+ except Exception as exc: # pragma: no cover — last-resort safety net
807
+ sys.stderr.write(f"propose_skills: analyze failed: {exc}\n")
808
+ result = {
809
+ "project": project,
810
+ "sessions_scanned": 0,
811
+ "sessions_skipped": 0,
812
+ "candidates": [],
813
+ }
814
+
815
+ report = render_report(
816
+ project=result["project"],
817
+ sessions_scanned=result["sessions_scanned"],
818
+ sessions_skipped=result["sessions_skipped"],
819
+ candidates=result["candidates"],
820
+ timestamp=timestamp,
821
+ )
822
+
823
+ if args.output:
824
+ out_path = Path(args.output).expanduser()
825
+ else:
826
+ out_path = Path(tempfile.gettempdir()) / f"wg-propose-skills-{timestamp}.md"
827
+ try:
828
+ out_path.parent.mkdir(parents=True, exist_ok=True)
829
+ out_path.write_text(report, encoding="utf-8")
830
+ except OSError as exc:
831
+ sys.stderr.write(f"propose_skills: failed to write report: {exc}\n")
832
+ return 1
833
+
834
+ if args.json:
835
+ sys.stdout.write(
836
+ json.dumps(
837
+ {
838
+ "report_path": str(out_path),
839
+ "project": result["project"],
840
+ "sessions_root": str(sessions_root),
841
+ "sessions_scanned": result["sessions_scanned"],
842
+ "sessions_skipped": result["sessions_skipped"],
843
+ "candidate_count": len(result["candidates"]),
844
+ "candidates": [_json_safe_candidate(c) for c in result["candidates"]],
845
+ },
846
+ indent=2,
847
+ )
848
+ + "\n"
849
+ )
850
+ else:
851
+ sys.stdout.write(str(out_path) + "\n")
852
+ return 0
853
+
854
+
855
+ if __name__ == "__main__":
856
+ raise SystemExit(run())