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.
- package/.claude-plugin/archetypes.json +166 -0
- package/.claude-plugin/components.json +75 -0
- package/.claude-plugin/marketplace.json +15 -0
- package/.claude-plugin/plugin.json +174 -0
- package/.claude-plugin/specialist.json +48 -0
- package/.claude-plugin/stack-registry.json +96 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +712 -0
- package/CONTRIBUTING.md +99 -0
- package/ETHOS.md +83 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/WICKED_GARDEN_BUS_EVENTS.md +149 -0
- package/agents/agentic/architect.md +449 -0
- package/agents/agentic/performance-analyst.md +702 -0
- package/agents/agentic/safety-reviewer.md +662 -0
- package/agents/crew/implementer.md +163 -0
- package/agents/crew/researcher.md +94 -0
- package/agents/crew/reviewer.md +184 -0
- package/agents/data/data-engineer.md +227 -0
- package/agents/engineering/api-documentarian.md +440 -0
- package/agents/engineering/migration-engineer.md +301 -0
- package/agents/engineering/solution-architect.md +208 -0
- package/agents/jam/brainstorm-facilitator.md +297 -0
- package/agents/jam/council.md +322 -0
- package/agents/jam/quick-facilitator.md +85 -0
- package/agents/persona/persona-agent.md +121 -0
- package/agents/platform/compliance-officer.md +266 -0
- package/agents/platform/privacy-expert.md +496 -0
- package/agents/platform/security-engineer.md +287 -0
- package/agents/product/a11y-expert.md +323 -0
- package/agents/product/requirements-analyst.md +238 -0
- package/agents/product/ui-reviewer.md +180 -0
- package/agents/product/ux-designer.md +199 -0
- package/agents/product/value-strategist.md +269 -0
- package/agents/qe/semantic-reviewer.md +78 -0
- package/commands/agentic/audit.md +24 -0
- package/commands/agentic/design.md +24 -0
- package/commands/agentic/frameworks.md +25 -0
- package/commands/agentic/review.md +27 -0
- package/commands/archetype/build.md +12 -0
- package/commands/archetype/decide.md +12 -0
- package/commands/archetype/explore.md +12 -0
- package/commands/archetype/incident.md +12 -0
- package/commands/archetype/migrate.md +12 -0
- package/commands/archetype/modernize.md +12 -0
- package/commands/archetype/review.md +14 -0
- package/commands/archetype/ship.md +12 -0
- package/commands/archetype/specify.md +12 -0
- package/commands/archetype/triage.md +12 -0
- package/commands/compile.md +37 -0
- package/commands/data/analyze.md +18 -0
- package/commands/data/data.md +21 -0
- package/commands/data/ml.md +18 -0
- package/commands/data/ontology.md +22 -0
- package/commands/data/pipeline.md +17 -0
- package/commands/deliberate.md +181 -0
- package/commands/engineering/add-field.md +77 -0
- package/commands/engineering/apply.md +58 -0
- package/commands/engineering/arch.md +26 -0
- package/commands/engineering/debug.md +27 -0
- package/commands/engineering/docs.md +27 -0
- package/commands/engineering/new-generator.md +257 -0
- package/commands/engineering/patch-plan.md +59 -0
- package/commands/engineering/plan.md +29 -0
- package/commands/engineering/remove.md +48 -0
- package/commands/engineering/rename.md +44 -0
- package/commands/engineering/review.md +32 -0
- package/commands/help.md +128 -0
- package/commands/install.md +118 -0
- package/commands/intent.md +54 -0
- package/commands/jam/brainstorm.md +37 -0
- package/commands/jam/council.md +49 -0
- package/commands/jam/quick.md +21 -0
- package/commands/jam/revisit.md +79 -0
- package/commands/persona/as.md +136 -0
- package/commands/persona/define.md +24 -0
- package/commands/persona/list.md +118 -0
- package/commands/platform/actions.md +20 -0
- package/commands/platform/assert.md +18 -0
- package/commands/platform/audit.md +23 -0
- package/commands/platform/compliance.md +20 -0
- package/commands/platform/gh.md +22 -0
- package/commands/platform/health.md +22 -0
- package/commands/platform/incident.md +23 -0
- package/commands/platform/infra.md +24 -0
- package/commands/platform/peer-health.md +103 -0
- package/commands/platform/plugin-health.md +58 -0
- package/commands/platform/security.md +38 -0
- package/commands/platform/toolchain.md +96 -0
- package/commands/platform/traces.md +20 -0
- package/commands/product/a11y.md +19 -0
- package/commands/product/acceptance.md +19 -0
- package/commands/product/align.md +20 -0
- package/commands/product/analyze.md +23 -0
- package/commands/product/elicit.md +19 -0
- package/commands/product/listen.md +24 -0
- package/commands/product/mockup.md +20 -0
- package/commands/product/screenshot.md +19 -0
- package/commands/product/strategy.md +40 -0
- package/commands/product/synthesize.md +23 -0
- package/commands/product/ux-review.md +42 -0
- package/commands/product/ux.md +20 -0
- package/commands/product/visual-direction.md +34 -0
- package/commands/prove.md +47 -0
- package/commands/report-issue.md +243 -0
- package/commands/reset.md +144 -0
- package/commands/search/blast-radius.md +58 -0
- package/commands/search/hotspots.md +55 -0
- package/commands/search/index.md +38 -0
- package/commands/search/lineage.md +47 -0
- package/commands/search/service-map.md +79 -0
- package/commands/setup.md +363 -0
- package/commands/smaht/briefing.md +200 -0
- package/commands/smaht/events-import.md +95 -0
- package/commands/smaht/state.md +19 -0
- package/commands/where-am-i.md +58 -0
- package/docs/README.md +32 -0
- package/docs/adr/0001-code-relationship-graph-engine.md +116 -0
- package/docs/adr/0002-major-cleanup-keep-cut.md +91 -0
- package/docs/adr/0003-peers-gate-required-layers-optin.md +41 -0
- package/docs/adr/0004-code-graph-moves-to-wicked-brain.md +47 -0
- package/docs/brain-chunk-format.md +135 -0
- package/docs/compiler.md +95 -0
- package/docs/daemon-api.md +70 -0
- package/docs/domains.md +197 -0
- package/docs/getting-started.md +208 -0
- package/docs/plans/2026-06-08-wicked-loom-conduct.md +1337 -0
- package/docs/plans/2026-06-08-wicked-loom-contract.md +638 -0
- package/docs/plans/2026-06-08-wicked-loom-cutover.md +1300 -0
- package/docs/plans/2026-06-08-wicked-loom-expand-compose.md +843 -0
- package/docs/plans/2026-06-08-wicked-loom-flow-cutover.md +403 -0
- package/docs/plans/2026-06-10-codegraph-to-brain-phase1a-graph-core.md +838 -0
- package/docs/plans/2026-06-10-codegraph-to-brain-phase1b-injected-edges.md +44 -0
- package/docs/required-peers.md +60 -0
- package/docs/specs/2026-06-08-wicked-loom-extraction.md +225 -0
- package/docs/specs/2026-06-10-codegraph-to-brain-migration.md +168 -0
- package/docs/v11/archetypes.md +231 -0
- package/hooks/hooks.json +275 -0
- package/hooks/scripts/bootstrap.py +1797 -0
- package/hooks/scripts/invoke.py +62 -0
- package/hooks/scripts/notification.py +166 -0
- package/hooks/scripts/permission_request.py +223 -0
- package/hooks/scripts/post_tool.py +1374 -0
- package/hooks/scripts/pre_compact.py +272 -0
- package/hooks/scripts/pre_tool.py +1278 -0
- package/hooks/scripts/prompt_submit.py +1109 -0
- package/hooks/scripts/session_end.py +95 -0
- package/hooks/scripts/stop.py +624 -0
- package/hooks/scripts/subagent_lifecycle.py +464 -0
- package/hooks/scripts/subscribers/on_gate_decided.py +84 -0
- package/hooks/scripts/task_completed.py +322 -0
- package/hooks/scripts/worktree_guard.py +148 -0
- package/hooks/subscriptions/on_gate_decided.json +9 -0
- package/install.mjs +128 -0
- package/package.json +41 -0
- package/pyproject.toml +59 -0
- package/schemas/README.md +32 -0
- package/schemas/assert_contracts.json +37 -0
- package/schemas/health_probe.json +46 -0
- package/schemas/plugin_status.json +37 -0
- package/scripts/_adapters/__init__.py +166 -0
- package/scripts/_adapters/jira_adapter.py +18 -0
- package/scripts/_adapters/linear_adapter.py +18 -0
- package/scripts/_adapters/miro_adapter.py +18 -0
- package/scripts/_adapters/notion_adapter.py +18 -0
- package/scripts/_agents.py +357 -0
- package/scripts/_brain_ingest/__init__.py +2 -0
- package/scripts/_brain_ingest/session_fact_extractor.py +341 -0
- package/scripts/_brain_port.py +163 -0
- package/scripts/_bus.py +1081 -0
- package/scripts/_bus_catalog_gen.py +93 -0
- package/scripts/_bus_consumers.json +46 -0
- package/scripts/_bus_emit.py +43 -0
- package/scripts/_capability_registry.py +334 -0
- package/scripts/_capability_resolver.py +322 -0
- package/scripts/_domain_store.py +559 -0
- package/scripts/_event_log_reader.py +154 -0
- package/scripts/_event_schema.py +305 -0
- package/scripts/_event_store.py +397 -0
- package/scripts/_heavy_cadence.py +324 -0
- package/scripts/_integration_resolver.py +321 -0
- package/scripts/_logger.py +151 -0
- package/scripts/_loom.py +482 -0
- package/scripts/_paths.py +133 -0
- package/scripts/_python.sh +22 -0
- package/scripts/_run.py +137 -0
- package/scripts/_session.py +502 -0
- package/scripts/_validate_registry.py +1042 -0
- package/scripts/_wicked_testing_probe.py +277 -0
- package/scripts/agentic/analyze_agents.py +391 -0
- package/scripts/agentic/detect_framework.py +420 -0
- package/scripts/agentic/issue_taxonomy.py +357 -0
- package/scripts/agentic/pattern_scorer.py +434 -0
- package/scripts/classify/persist.py +169 -0
- package/scripts/compiler/compile.py +552 -0
- package/scripts/compiler/phase0/VERDICT.md +60 -0
- package/scripts/compiler/phase0/detect.py +292 -0
- package/scripts/compiler/phase0/emit.py +228 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.bindings.json +30 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/.wg/evidence/honest-tests.txt +2 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/Enterprise-AISDLC.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/command_iq.bindings.json +45 -0
- package/scripts/compiler/phase0/evidence/command_iq.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/.projects/evidence-domain/phase-1-clarify/evidence/honest-tests.txt +3 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/command_iq.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/memos.bindings.json +28 -0
- package/scripts/compiler/phase0/evidence/memos.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/memos.stage/.wg/evidence/honest-tests.txt +2 -0
- package/scripts/compiler/phase0/evidence/memos.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/memos.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/verdict.json +104 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.bindings.json +29 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.lint-output.txt +17 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/failing-tests.txt +4 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/coverage/honest-tests.txt +3 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-fail.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/fab-missing.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/honest.build.md +8 -0
- package/scripts/compiler/phase0/evidence/wicked-bus.stage/wg_check_claims.py +174 -0
- package/scripts/compiler/phase0/evidence/wire-vault-transcript.txt +60 -0
- package/scripts/compiler/phase0/run_probe.py +234 -0
- package/scripts/compiler/phase0/wire_vault.py +310 -0
- package/scripts/crew/__init__.py +0 -0
- package/scripts/crew/_task_audit_writer.py +243 -0
- package/scripts/crew/_task_reader.py +389 -0
- package/scripts/crew/archetypes_v11.py +348 -0
- package/scripts/crew/detectors/__init__.py +12 -0
- package/scripts/crew/flow_compiler.py +121 -0
- package/scripts/crew/phase_manager.py +703 -0
- package/scripts/crew/scope_delta.py +264 -0
- package/scripts/crew/specialist_resolver.py +218 -0
- package/scripts/crew/stack_registry.py +189 -0
- package/scripts/data/data_profiler.py +271 -0
- package/scripts/data/ontology_recommender.py +231 -0
- package/scripts/data/schema_validator.py +267 -0
- package/scripts/delivery/drift.py +739 -0
- package/scripts/delivery/telemetry.py +614 -0
- package/scripts/engineering/patch/codegraph_db.py +125 -0
- package/scripts/engineering/patch/generators/__init__.py +56 -0
- package/scripts/engineering/patch/generators/base.py +373 -0
- package/scripts/engineering/patch/generators/csharp_generator.py +494 -0
- package/scripts/engineering/patch/generators/go_generator.py +430 -0
- package/scripts/engineering/patch/generators/java_generator.py +928 -0
- package/scripts/engineering/patch/generators/jsp_generator.py +454 -0
- package/scripts/engineering/patch/generators/kotlin_generator.py +561 -0
- package/scripts/engineering/patch/generators/perl_generator.py +510 -0
- package/scripts/engineering/patch/generators/php_generator.py +644 -0
- package/scripts/engineering/patch/generators/propagation_engine.py +929 -0
- package/scripts/engineering/patch/generators/python_generator.py +487 -0
- package/scripts/engineering/patch/generators/ruby_generator.py +463 -0
- package/scripts/engineering/patch/generators/rust_generator.py +400 -0
- package/scripts/engineering/patch/generators/sql_generator.py +594 -0
- package/scripts/engineering/patch/generators/typescript_generator.py +439 -0
- package/scripts/engineering/patch/patch.py +800 -0
- package/scripts/engineering/patch/safety.py +429 -0
- package/scripts/jam/__init__.py +0 -0
- package/scripts/jam/_bus_consumers.py +271 -0
- package/scripts/jam/agentic_cli_registry.py +703 -0
- package/scripts/jam/consensus.py +737 -0
- package/scripts/jam/detect_clis.py +409 -0
- package/scripts/jam/jam.py +283 -0
- package/scripts/jam/save_transcript.py +171 -0
- package/scripts/loom/__init__.py +22 -0
- package/scripts/loom/cli.py +170 -0
- package/scripts/loom/compose.py +180 -0
- package/scripts/loom/gate.py +120 -0
- package/scripts/loom/manifest.py +140 -0
- package/scripts/loom/resolve.py +59 -0
- package/scripts/persona/__init__.py +0 -0
- package/scripts/persona/registry.py +688 -0
- package/scripts/platform/guard_pipeline.py +1214 -0
- package/scripts/platform/guard_profiles.py +216 -0
- package/scripts/platform/observability/assert_contracts.py +434 -0
- package/scripts/platform/observability/health_probe.py +915 -0
- package/scripts/platform/observability/ops_log_viewer.py +209 -0
- package/scripts/platform/prereq_doctor.py +567 -0
- package/scripts/qe/conditions_manifest.py +260 -0
- package/scripts/qe/content_sanitizer.py +160 -0
- package/scripts/qe/coverage_tracker.py +535 -0
- package/scripts/qe/discover_scenarios.py +243 -0
- package/scripts/qe/evidence_tracker.py +232 -0
- package/scripts/qe/prove.py +196 -0
- package/scripts/qe/registry_lookup.py +80 -0
- package/scripts/qe/registry_store.py +176 -0
- package/scripts/qe/semantic_review.py +1005 -0
- package/scripts/qe/vault_gate.py +382 -0
- package/scripts/qe/verdict_audit.py +175 -0
- package/scripts/qe/verdict_schema.py +335 -0
- package/scripts/reset.py +220 -0
- package/scripts/resolve_path.py +22 -0
- package/scripts/sentinel/invariants.py +402 -0
- package/scripts/sentinel/pre_push.py +73 -0
- package/scripts/setup/__init__.py +1 -0
- package/scripts/setup/detect_state.py +114 -0
- package/scripts/setup/migrate_legacy.py +67 -0
- package/scripts/setup/migrate_v6_projects.py +232 -0
- package/scripts/setup/onboarding.py +114 -0
- package/scripts/smaht/_bus_consumers.py +98 -0
- package/scripts/smaht/adapters/__init__.py +83 -0
- package/scripts/smaht/adapters/brain_adapter.py +254 -0
- package/scripts/smaht/adapters/context7_adapter.py +438 -0
- package/scripts/smaht/adapters/delegation_adapter.py +104 -0
- package/scripts/smaht/adapters/domain_adapter.py +206 -0
- package/scripts/smaht/adapters/events_adapter.py +231 -0
- package/scripts/smaht/adapters/tools_adapter.py +49 -0
- package/scripts/smaht/cheatsheet_store.py +236 -0
- package/scripts/smaht/context_package.py +386 -0
- package/scripts/smaht/knowledge_graph.py +395 -0
- package/scripts/smaht/propose_skills.py +856 -0
- package/scripts/smaht/test_cheatsheet_store.py +428 -0
- package/scripts/statusline.py +151 -0
- package/scripts/where_am_i.py +236 -0
- package/skills/agentic/agentic-patterns/SKILL.md +91 -0
- package/skills/agentic/agentic-patterns/refs/anti-patterns-design.md +283 -0
- package/skills/agentic/agentic-patterns/refs/anti-patterns-operational.md +216 -0
- package/skills/agentic/agentic-patterns/refs/pattern-catalog.md +329 -0
- package/skills/agentic/context-engineering/SKILL.md +142 -0
- package/skills/agentic/context-engineering/refs/caching-and-optimization.md +251 -0
- package/skills/agentic/context-engineering/refs/compression-techniques.md +171 -0
- package/skills/agentic/context-engineering/refs/cost-calculation-budget.md +250 -0
- package/skills/agentic/context-engineering/refs/cost-optimization-reporting.md +300 -0
- package/skills/agentic/context-engineering/refs/selective-loading.md +179 -0
- package/skills/agentic/frameworks/SKILL.md +191 -0
- package/skills/agentic/frameworks/refs/framework-profiles-1.md +209 -0
- package/skills/agentic/frameworks/refs/framework-profiles-2.md +214 -0
- package/skills/agentic/frameworks/refs/migration-patterns-paths.md +267 -0
- package/skills/agentic/frameworks/refs/migration-patterns-testing.md +208 -0
- package/skills/agentic/refs/audit.md +134 -0
- package/skills/agentic/refs/design.md +137 -0
- package/skills/agentic/refs/frameworks.md +102 -0
- package/skills/agentic/refs/review.md +160 -0
- package/skills/agentic/review-methodology/SKILL.md +186 -0
- package/skills/agentic/review-methodology/refs/deliverable-templates.md +246 -0
- package/skills/agentic/review-methodology/refs/issue-taxonomy-quality-testing.md +277 -0
- package/skills/agentic/review-methodology/refs/issue-taxonomy-reliability-safety.md +253 -0
- package/skills/agentic/trust-and-safety/SKILL.md +177 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-actions.md +246 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-input-output.md +238 -0
- package/skills/agentic/trust-and-safety/refs/guardrails-resources.md +209 -0
- package/skills/agentic/trust-and-safety/refs/safety-checklist-advanced.md +209 -0
- package/skills/agentic/trust-and-safety/refs/safety-checklist-core.md +215 -0
- package/skills/archetype/SKILL.md +104 -0
- package/skills/archetype/refs/build.md +142 -0
- package/skills/archetype/refs/decide.md +117 -0
- package/skills/archetype/refs/explore.md +66 -0
- package/skills/archetype/refs/incident.md +149 -0
- package/skills/archetype/refs/migrate.md +170 -0
- package/skills/archetype/refs/modernize.md +187 -0
- package/skills/archetype/refs/review.md +145 -0
- package/skills/archetype/refs/ship.md +117 -0
- package/skills/archetype/refs/specify.md +108 -0
- package/skills/archetype/refs/triage.md +62 -0
- package/skills/classify/SKILL.md +157 -0
- package/skills/data/analysis/SKILL.md +125 -0
- package/skills/data/analysis/refs/templates.md +170 -0
- package/skills/data/data/SKILL.md +118 -0
- package/skills/data/data/refs/examples.md +235 -0
- package/skills/data/ml/SKILL.md +125 -0
- package/skills/data/ml/refs/templates.md +172 -0
- package/skills/data/pipeline/SKILL.md +136 -0
- package/skills/data/pipeline/refs/templates.md +175 -0
- package/skills/data/refs/analyze.md +123 -0
- package/skills/data/refs/data.md +95 -0
- package/skills/data/refs/ml.md +118 -0
- package/skills/data/refs/pipeline.md +92 -0
- package/skills/deliberate/SKILL.md +125 -0
- package/skills/deliberate/refs/architecture-lens.md +37 -0
- package/skills/deliberate/refs/assumptions-lens.md +43 -0
- package/skills/deliberate/refs/code-lens.md +38 -0
- package/skills/deliberate/refs/content-lens.md +37 -0
- package/skills/deliberate/refs/data-lens.md +43 -0
- package/skills/deliberate/refs/design-lens.md +37 -0
- package/skills/deliberate/refs/opportunity-patterns.md +107 -0
- package/skills/deliberate/refs/ops-lens.md +43 -0
- package/skills/deliberate/refs/product-lens.md +46 -0
- package/skills/deliberate/refs/rethink-framework.md +77 -0
- package/skills/deliberate/refs/security-lens.md +47 -0
- package/skills/deliberate/refs/testing-lens.md +45 -0
- package/skills/deliberate/refs/timing-lens.md +42 -0
- package/skills/engineering/architecture/SKILL.md +177 -0
- package/skills/engineering/architecture/refs/adr-template.md +268 -0
- package/skills/engineering/architecture/refs/architecture-template-deployment.md +176 -0
- package/skills/engineering/architecture/refs/architecture-template-design.md +300 -0
- package/skills/engineering/architecture/refs/examples-ecommerce-analytics.md +299 -0
- package/skills/engineering/architecture/refs/examples-saas-trading.md +291 -0
- package/skills/engineering/architecture/refs/patterns-advanced.md +238 -0
- package/skills/engineering/architecture/refs/patterns-structural.md +303 -0
- package/skills/engineering/backend/SKILL.md +102 -0
- package/skills/engineering/backend/refs/checklists.md +146 -0
- package/skills/engineering/backend/refs/patterns.md +181 -0
- package/skills/engineering/debugging/SKILL.md +99 -0
- package/skills/engineering/debugging/refs/patterns.md +248 -0
- package/skills/engineering/debugging/refs/process.md +203 -0
- package/skills/engineering/docs/SKILL.md +97 -0
- package/skills/engineering/engineering/SKILL.md +88 -0
- package/skills/engineering/engineering/refs/checklists.md +91 -0
- package/skills/engineering/engineering/refs/templates.md +71 -0
- package/skills/engineering/frontend/SKILL.md +92 -0
- package/skills/engineering/frontend/refs/checklists.md +96 -0
- package/skills/engineering/frontend/refs/patterns.md +167 -0
- package/skills/engineering/integration/SKILL.md +183 -0
- package/skills/engineering/integration/refs/error-handling-best-practices.md +139 -0
- package/skills/engineering/integration/refs/error-handling-error-classes-typescript.md +212 -0
- package/skills/engineering/integration/refs/error-handling-error-response-format.md +301 -0
- package/skills/engineering/integration/refs/error-handling-retry-strategies.md +190 -0
- package/skills/engineering/integration/refs/event-schemas-best-practices.md +225 -0
- package/skills/engineering/integration/refs/event-schemas-event-naming-conventions.md +303 -0
- package/skills/engineering/integration/refs/event-schemas-event-structure.md +288 -0
- package/skills/engineering/integration/refs/graphql-best-practices.md +138 -0
- package/skills/engineering/integration/refs/graphql-error-handling.md +198 -0
- package/skills/engineering/integration/refs/graphql-graphql-basics.md +197 -0
- package/skills/engineering/integration/refs/graphql-query-examples.md +226 -0
- package/skills/engineering/integration/refs/graphql-schema-design-patterns.md +280 -0
- package/skills/engineering/integration/refs/grpc-grpc-overview.md +246 -0
- package/skills/engineering/integration/refs/grpc-implementation-examples.md +269 -0
- package/skills/engineering/integration/refs/grpc-metadata-headers.md +304 -0
- package/skills/engineering/integration/refs/grpc-testing.md +49 -0
- package/skills/engineering/integration/refs/openapi-template.yaml +682 -0
- package/skills/engineering/integration/refs/resilience-best-practices.md +109 -0
- package/skills/engineering/integration/refs/resilience-circuit-breaker-pattern.md +302 -0
- package/skills/engineering/integration/refs/resilience-rate-limiting.md +245 -0
- package/skills/engineering/integration/refs/resilience-timeout-pattern.md +212 -0
- package/skills/engineering/integration/refs/rest-api-fundamentals.md +299 -0
- package/skills/engineering/integration/refs/rest-api-responses-security.md +302 -0
- package/skills/engineering/integration/refs/versioning-breaking-changes.md +237 -0
- package/skills/engineering/integration/refs/versioning-migration-strategies.md +257 -0
- package/skills/engineering/integration/refs/versioning-versioning-strategies.md +296 -0
- package/skills/engineering/large-scale-migration/SKILL.md +65 -0
- package/skills/engineering/patch/SKILL.md +184 -0
- package/skills/engineering/refs/arch.md +126 -0
- package/skills/engineering/refs/debug.md +142 -0
- package/skills/engineering/refs/docs.md +190 -0
- package/skills/engineering/refs/plan.md +109 -0
- package/skills/engineering/refs/review.md +138 -0
- package/skills/engineering/system-design/SKILL.md +154 -0
- package/skills/engineering/system-design/refs/anti-patterns-big-ball-of-mud.md +254 -0
- package/skills/engineering/system-design/refs/anti-patterns-circular-dependencies.md +241 -0
- package/skills/engineering/system-design/refs/anti-patterns-feature-envy.md +168 -0
- package/skills/engineering/system-design/refs/anti-patterns-god-object.md +247 -0
- package/skills/engineering/system-design/refs/component-template-lifecycle.md +210 -0
- package/skills/engineering/system-design/refs/component-template-maintenance.md +219 -0
- package/skills/engineering/system-design/refs/component-template-structure.md +276 -0
- package/skills/engineering/system-design/refs/dependencies-best-practices.md +260 -0
- package/skills/engineering/system-design/refs/dependencies-dependency-principles.md +295 -0
- package/skills/engineering/system-design/refs/dependencies-dependency-tracking.md +261 -0
- package/skills/engineering/system-design/refs/interface-template-lifecycle.md +211 -0
- package/skills/engineering/system-design/refs/interface-template-maintenance.md +212 -0
- package/skills/engineering/system-design/refs/interface-template-structure.md +269 -0
- package/skills/engineering/system-design/refs/patterns-dependency-injection.md +151 -0
- package/skills/engineering/system-design/refs/patterns-facade-pattern.md +257 -0
- package/skills/engineering/system-design/refs/patterns-layered-pattern.md +204 -0
- package/skills/engineering/system-design/refs/patterns-plugin-architecture.md +229 -0
- package/skills/engineering/unit-test-quality/SKILL.md +194 -0
- package/skills/ground/SKILL.md +130 -0
- package/skills/integration-discovery/SKILL.md +168 -0
- package/skills/integration-discovery/refs/capabilities.md +282 -0
- package/skills/integration-discovery/refs/cli-detection.md +295 -0
- package/skills/integration-discovery/refs/discovery-sources.md +233 -0
- package/skills/integration-discovery/refs/task-patterns.md +206 -0
- package/skills/jam/SKILL.md +45 -0
- package/skills/jam/refs/facilitation-patterns.md +125 -0
- package/skills/jam/refs/quick.md +64 -0
- package/skills/jam/refs/synthesis-patterns.md +114 -0
- package/skills/multi-model/SKILL.md +168 -0
- package/skills/multi-model/refs/aichat.md +119 -0
- package/skills/multi-model/refs/aider.md +100 -0
- package/skills/multi-model/refs/auditability.md +238 -0
- package/skills/multi-model/refs/codex.md +169 -0
- package/skills/multi-model/refs/context.md +306 -0
- package/skills/multi-model/refs/copilot.md +161 -0
- package/skills/multi-model/refs/examples.md +224 -0
- package/skills/multi-model/refs/gemini.md +185 -0
- package/skills/multi-model/refs/goose.md +112 -0
- package/skills/multi-model/refs/llm.md +124 -0
- package/skills/multi-model/refs/opencode.md +196 -0
- package/skills/multi-model/refs/orchestration.md +188 -0
- package/skills/multi-model/refs/pi.md +208 -0
- package/skills/persona/SKILL.md +144 -0
- package/skills/persona/refs/define.md +133 -0
- package/skills/platform/actions/refs/actions.md +114 -0
- package/skills/platform/audit/SKILL.md +174 -0
- package/skills/platform/audit/refs/audit.md +145 -0
- package/skills/platform/audit/refs/checklists-evidence-operations.md +144 -0
- package/skills/platform/audit/refs/checklists-gdpr-pci-evidence.md +213 -0
- package/skills/platform/audit/refs/checklists-soc2-hipaa.md +120 -0
- package/skills/platform/audit/refs/frameworks-gdpr-pci.md +225 -0
- package/skills/platform/audit/refs/frameworks-soc2-hipaa.md +228 -0
- package/skills/platform/compliance/SKILL.md +169 -0
- package/skills/platform/compliance/refs/checklists.md +303 -0
- package/skills/platform/compliance/refs/compliance.md +117 -0
- package/skills/platform/compliance/refs/frameworks.md +282 -0
- package/skills/platform/errors/SKILL.md +183 -0
- package/skills/platform/errors/refs/patterns.md +271 -0
- package/skills/platform/errors/refs/severity.md +264 -0
- package/skills/platform/gate-benchmark-rebaseline/SKILL.md +108 -0
- package/skills/platform/gh/refs/gh.md +105 -0
- package/skills/platform/gh-cli/SKILL.md +120 -0
- package/skills/platform/gh-cli/refs/automation.md +63 -0
- package/skills/platform/gh-cli/refs/gh-ops.md +103 -0
- package/skills/platform/gh-cli/refs/patterns.md +132 -0
- package/skills/platform/gh-cli/scripts/gh_ops.py +454 -0
- package/skills/platform/github-actions/SKILL.md +159 -0
- package/skills/platform/github-actions/refs/security.md +150 -0
- package/skills/platform/github-actions/refs/templates.md +230 -0
- package/skills/platform/github-actions/refs/troubleshooting.md +174 -0
- package/skills/platform/github-actions/scripts/workflow_gen.py +215 -0
- package/skills/platform/gitlab-ci/SKILL.md +140 -0
- package/skills/platform/gitlab-ci/refs/templates.md +223 -0
- package/skills/platform/gitlab-ci/refs/troubleshooting.md +213 -0
- package/skills/platform/glab-cli/SKILL.md +133 -0
- package/skills/platform/glab-cli/refs/ci-debugging.md +95 -0
- package/skills/platform/glab-cli/refs/glab-ops.md +78 -0
- package/skills/platform/glab-cli/refs/patterns.md +131 -0
- package/skills/platform/glab-cli/scripts/glab_ops.py +313 -0
- package/skills/platform/health/SKILL.md +177 -0
- package/skills/platform/health/refs/health.md +72 -0
- package/skills/platform/health/refs/sources.md +302 -0
- package/skills/platform/incident/refs/incident.md +136 -0
- package/skills/platform/infra/refs/infra.md +104 -0
- package/skills/platform/observability/SKILL.md +128 -0
- package/skills/platform/observability/refs/toolchain-discovery.md +243 -0
- package/skills/platform/prereq-doctor/SKILL.md +57 -0
- package/skills/platform/prereq-doctor/refs/tool-registry.md +58 -0
- package/skills/platform/traces/refs/traces.md +101 -0
- package/skills/product/acceptance-criteria/SKILL.md +193 -0
- package/skills/product/acceptance-criteria/refs/ac-anti-patterns.md +291 -0
- package/skills/product/acceptance-criteria/refs/ac-examples.md +277 -0
- package/skills/product/accessibility/SKILL.md +197 -0
- package/skills/product/accessibility/refs/aria-patterns-basics.md +250 -0
- package/skills/product/accessibility/refs/aria-patterns-dynamic.md +187 -0
- package/skills/product/accessibility/refs/aria-patterns-interactive.md +246 -0
- package/skills/product/accessibility/refs/audit-report-findings.md +248 -0
- package/skills/product/accessibility/refs/audit-report-results.md +276 -0
- package/skills/product/accessibility/refs/common-violations-html-css.md +270 -0
- package/skills/product/accessibility/refs/common-violations-interactive.md +303 -0
- package/skills/product/accessibility/refs/keyboard-testing-basics.md +222 -0
- package/skills/product/accessibility/refs/keyboard-testing-patterns.md +173 -0
- package/skills/product/accessibility/refs/screen-reader-testing-advanced.md +294 -0
- package/skills/product/accessibility/refs/screen-reader-testing-commands.md +255 -0
- package/skills/product/accessibility/refs/wcag-checklist.md +262 -0
- package/skills/product/analyze/SKILL.md +164 -0
- package/skills/product/analyze/refs/algorithms.md +238 -0
- package/skills/product/analyze/refs/sentiment-patterns.md +180 -0
- package/skills/product/imagery/SKILL.md +97 -0
- package/skills/product/imagery/alter/SKILL.md +105 -0
- package/skills/product/imagery/alter/refs/editing-reference.md +146 -0
- package/skills/product/imagery/create/SKILL.md +109 -0
- package/skills/product/imagery/create/refs/prompt-engineering.md +112 -0
- package/skills/product/imagery/create/refs/provider-reference.md +219 -0
- package/skills/product/imagery/review/SKILL.md +101 -0
- package/skills/product/imagery/review/refs/analysis-lenses.md +136 -0
- package/skills/product/imagery/review/refs/quality-gates.md +153 -0
- package/skills/product/imagery/scripts/analyze.sh +23 -0
- package/skills/product/imagery/scripts/edit.sh +14 -0
- package/skills/product/imagery/scripts/generate.sh +13 -0
- package/skills/product/imagery/scripts/provider.py +787 -0
- package/skills/product/listen/SKILL.md +175 -0
- package/skills/product/listen/refs/channels.md +220 -0
- package/skills/product/mockup/SKILL.md +106 -0
- package/skills/product/mockup/refs/templates.md +95 -0
- package/skills/product/refs/a11y.md +109 -0
- package/skills/product/refs/acceptance.md +79 -0
- package/skills/product/refs/align.md +67 -0
- package/skills/product/refs/analyze.md +77 -0
- package/skills/product/refs/elicit.md +67 -0
- package/skills/product/refs/listen.md +58 -0
- package/skills/product/refs/mockup.md +77 -0
- package/skills/product/refs/screenshot.md +59 -0
- package/skills/product/refs/strategy.md +77 -0
- package/skills/product/refs/synthesize.md +54 -0
- package/skills/product/refs/ux-review.md +68 -0
- package/skills/product/refs/ux.md +101 -0
- package/skills/product/requirements-analysis/SKILL.md +136 -0
- package/skills/product/requirements-analysis/refs/requirements-example-auth.md +264 -0
- package/skills/product/requirements-analysis/refs/requirements-example-export-integration.md +88 -0
- package/skills/product/requirements-analysis/refs/requirements-output-format-template.md +247 -0
- package/skills/product/requirements-analysis/refs/user-story-guide.md +296 -0
- package/skills/product/requirements-graph/SKILL.md +155 -0
- package/skills/product/requirements-graph/refs/examples.md +223 -0
- package/skills/product/requirements-graph/refs/schema.md +162 -0
- package/skills/product/requirements-migrate/SKILL.md +90 -0
- package/skills/product/requirements-navigate/SKILL.md +141 -0
- package/skills/product/requirements-navigate/refs/meta-templates.md +141 -0
- package/skills/product/screenshot/SKILL.md +147 -0
- package/skills/product/strategy/SKILL.md +198 -0
- package/skills/product/strategy/refs/frameworks.md +295 -0
- package/skills/product/strategy/refs/templates-competitive-market.md +299 -0
- package/skills/product/strategy/refs/templates-roi-value.md +207 -0
- package/skills/product/synthesize/SKILL.md +116 -0
- package/skills/product/synthesize/refs/journey-mapping.md +212 -0
- package/skills/product/synthesize/refs/prioritization.md +193 -0
- package/skills/product/ux-review/SKILL.md +164 -0
- package/skills/product/visual-direction/SKILL.md +137 -0
- package/skills/product/visual-review/SKILL.md +131 -0
- package/skills/runtime-exec/SKILL.md +138 -0
- package/skills/search/codebase-narrator/SKILL.md +131 -0
- package/skills/search/codebase-narrator/refs/output-template.md +100 -0
- package/skills/smaht/SKILL.md +72 -0
- package/skills/smaht/discovery/SKILL.md +65 -0
- package/skills/smaht/intent/SKILL.md +89 -0
- package/skills/smaht/propose-skills/SKILL.md +142 -0
- package/skills/smaht/refs/state.md +76 -0
- package/skills/swarm/SKILL.md +132 -0
- package/skills/swarm/refs/fan-out.md +104 -0
- package/skills/swarm/refs/independent-verification.md +86 -0
- package/skills/swarm/refs/receipts-and-evidence.md +69 -0
- package/skills/swarm/refs/recon-synthesis.md +87 -0
- package/skills/swarm/refs/ship-discipline.md +90 -0
- package/skills/wickedizer/SKILL.md +147 -0
- package/skills/wickedizer/refs/artifact-types.md +238 -0
- package/skills/wickedizer/refs/complexity.md +278 -0
- package/skills/wickedizer/refs/de-ai-pass.md +140 -0
- package/skills/wickedizer/refs/failure-modes.md +249 -0
- package/skills/wickedizer/refs/patterns.md +282 -0
- package/skills/wickedizer/refs/review.md +221 -0
- package/skills/wickedizer/refs/structure.md +282 -0
- package/skills/wickedizer/refs/voice-profile.md +125 -0
- package/skills/workflow/SKILL.md +132 -0
- package/skills/workflow/refs/archetype-adjustments.md +205 -0
- package/skills/workflow/refs/evidence.md +58 -0
- package/skills/workflow/refs/integration.md +130 -0
- package/skills/workflow/refs/risk-dimension-signals.md +260 -0
- package/skills/workflow/refs/scoring-rubric.md +49 -0
- package/skills/workflow/refs/signal-keywords.md +176 -0
- package/skills/workflow/refs/specialist-routing-rules.md +159 -0
- package/skills/worktrees/SKILL.md +87 -0
- 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())
|