@caddis/cli 0.2.6 → 0.4.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/bundles/antigravity-plugin/agents/claude-md-curator.md +1 -1
- package/bundles/antigravity-plugin/agents/knowledge-transfer.md +17 -32
- package/bundles/antigravity-plugin/caddis_statusline.py +825 -0
- package/bundles/antigravity-plugin/claude-md/folder-agents.md.tmpl +21 -0
- package/bundles/antigravity-plugin/guard_agy.py +60 -9
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +524 -0
- package/bundles/antigravity-plugin/scripts/caddis_inventory.py +439 -0
- package/bundles/antigravity-plugin/scripts/caddis_migrate_dir.py +270 -0
- package/bundles/antigravity-plugin/scripts/caddis_spawn.py +300 -0
- package/bundles/antigravity-plugin/scripts/caddis_statusline.py +825 -0
- package/bundles/antigravity-plugin/scripts/caddis_tidy.py +366 -0
- package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +924 -0
- package/bundles/antigravity-plugin/scripts/claude-deepseek.ps1 +4 -0
- package/bundles/antigravity-plugin/scripts/claude-deepseek.sh +3 -0
- package/bundles/antigravity-plugin/scripts/claude-glm.ps1 +4 -0
- package/bundles/antigravity-plugin/scripts/claude-glm.sh +3 -0
- package/bundles/antigravity-plugin/scripts/claude-oss.ps1 +64 -0
- package/bundles/antigravity-plugin/scripts/claude-oss.sh +60 -0
- package/bundles/antigravity-plugin/scripts/claudster_config.py +121 -0
- package/bundles/antigravity-plugin/scripts/claudster_doctor.py +356 -0
- package/bundles/antigravity-plugin/scripts/claudster_init.py +400 -0
- package/bundles/antigravity-plugin/scripts/claudster_migrate_rules.py +300 -0
- package/bundles/antigravity-plugin/scripts/enforcement_inventory.py +261 -0
- package/bundles/antigravity-plugin/scripts/hook_log.py +156 -0
- package/bundles/antigravity-plugin/scripts/new-machine.ps1 +170 -0
- package/bundles/antigravity-plugin/scripts/oss_ask.py +98 -0
- package/bundles/antigravity-plugin/scripts/oss_model.py +174 -0
- package/bundles/antigravity-plugin/scripts/oss_review.py +718 -0
- package/bundles/antigravity-plugin/scripts/setup_project_ai.py +1432 -0
- package/bundles/antigravity-plugin/scripts/usage_review.py +1323 -0
- package/bundles/antigravity-plugin/session_end_agy.py +52 -8
- package/bundles/antigravity-plugin/skills/_registry.md +1 -0
- package/bundles/antigravity-plugin/skills/advisory-hub/SKILL.md +44 -0
- package/bundles/antigravity-plugin/skills/code-review/SKILL.md +11 -0
- package/bundles/antigravity-plugin/skills/fastapi-dev/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +21 -0
- package/bundles/antigravity-plugin/skills/gate-review/SKILL.md +11 -0
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +36 -0
- package/bundles/antigravity-plugin/skills/security-review/SKILL.md +9 -0
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/statusline/SKILL.md +105 -0
- package/bundles/antigravity-plugin/skills/use-model/SKILL.md +15 -3
- package/bundles/antigravity-plugin/skills/validate-phase/SKILL.md +31 -0
- package/bundles/antigravity-plugin/warm_start_agy.py +117 -11
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/architecture-design/SKILL.md +1 -1
- package/bundles/antigravity-plugin-extras/skills/caching-patterns/SKILL.md +1 -1
- package/bundles/antigravity-plugin-extras/skills/webapp-development/SKILL.md +1 -1
- package/bundles/codex/.codex/config.toml.example +36 -0
- package/bundles/codex/.codex/skills/coding/api-design/SKILL.md +249 -0
- package/bundles/codex/.codex/skills/coding/backend-development/SKILL.md +305 -0
- package/bundles/codex/.codex/skills/coding/code-review/SKILL.md +140 -0
- package/bundles/codex/.codex/skills/coding/codebase-audit/SKILL.md +204 -0
- package/bundles/codex/.codex/skills/coding/dashboard-perf-sweep/SKILL.md +132 -0
- package/bundles/codex/.codex/skills/coding/dashboard-perf-sweep/scripts/perf_sweep.py +249 -0
- package/bundles/codex/.codex/skills/coding/error-handling/SKILL.md +260 -0
- package/bundles/codex/.codex/skills/coding/fastapi-dev/SKILL.md +300 -0
- package/bundles/codex/.codex/skills/coding/javascript-typescript/SKILL.md +142 -0
- package/bundles/codex/.codex/skills/coding/python/SKILL.md +388 -0
- package/bundles/codex/.codex/skills/coding/refactoring/SKILL.md +486 -0
- package/bundles/codex/.codex/skills/coding/security-review/SKILL.md +205 -0
- package/bundles/codex/.codex/skills/coding/sql/SKILL.md +321 -0
- package/bundles/codex/.codex/skills/data/database-design/SKILL.md +177 -0
- package/bundles/codex/.codex/skills/data/db-diagram/SKILL.md +148 -0
- package/bundles/codex/.codex/skills/data/db-diagram/scripts/sql_to_graph.py +1212 -0
- package/bundles/codex/.codex/skills/devops/ci-cd-pipeline/SKILL.md +315 -0
- package/bundles/codex/.codex/skills/devops/gh-cli/SKILL.md +195 -0
- package/bundles/codex/.codex/skills/devops/git-commit/SKILL.md +285 -0
- package/bundles/codex/.codex/skills/devops/using-git-worktrees/SKILL.md +217 -0
- package/bundles/codex/.codex/skills/devops/windows-deployment/SKILL.md +880 -0
- package/bundles/codex/.codex/skills/docs/code-documentation/SKILL.md +271 -0
- package/bundles/codex/.codex/skills/docs/explainer-doc/SKILL.md +97 -0
- package/bundles/codex/.codex/skills/docs/technical-writing/SKILL.md +286 -0
- package/bundles/codex/.codex/skills/docs/writing-plans/SKILL.md +387 -0
- package/bundles/codex/.codex/skills/frontend/css-architecture/SKILL.md +305 -0
- package/bundles/codex/.codex/skills/frontend/css-architecture/references/RESPONSIVE-DESIGN.md +604 -0
- package/bundles/codex/.codex/skills/frontend/frontend-design/SKILL.md +193 -0
- package/bundles/codex/.codex/skills/frontend/frontend-design/references/my-tech-stack.md +127 -0
- package/bundles/codex/.codex/skills/frontend/mockup/SKILL.md +242 -0
- package/bundles/codex/.codex/skills/frontend/react-best-practices/SKILL.md +269 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/README.md +404 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/SKILL.md +459 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/examples/generic-components.md +579 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/examples/server-components.md +579 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/references/event-handlers.md +574 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/references/hooks.md +456 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/references/react-19-patterns.md +638 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/references/react-router.md +1002 -0
- package/bundles/codex/.codex/skills/frontend/react-dev/references/tanstack-router.md +587 -0
- package/bundles/codex/.codex/skills/frontend/ui-review/SKILL.md +176 -0
- package/bundles/codex/.codex/skills/frontend/ui-review/references/framework-fixes.md +471 -0
- package/bundles/codex/.codex/skills/frontend/ui-review/references/visual-checklist.md +236 -0
- package/bundles/codex/.codex/skills/frontend/warm-editorial-ui/DESIGN_TOKENS.md +487 -0
- package/bundles/codex/.codex/skills/frontend/warm-editorial-ui/IMPLEMENTATION_GUIDE.md +177 -0
- package/bundles/codex/.codex/skills/frontend/warm-editorial-ui/SKILL.md +732 -0
- package/bundles/codex/.codex/skills/media/draw-io/SKILL.md +162 -0
- package/bundles/codex/.codex/skills/media/draw-io/references/aws-icons.md +677 -0
- package/bundles/codex/.codex/skills/media/draw-io/references/layout-guidelines.md +142 -0
- package/bundles/codex/.codex/skills/media/draw-io/references/troubleshooting.md +118 -0
- package/bundles/codex/.codex/skills/media/draw-io/references/workflows.md +103 -0
- package/bundles/codex/.codex/skills/media/draw-io/scripts/convert-drawio-to-png.sh +25 -0
- package/bundles/codex/.codex/skills/media/draw-io/scripts/find_aws_icon.py +79 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/SKILL.md +292 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/ENHANCEMENTS.md +264 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/MERMAID-SUMMARY.md +137 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/advanced-features.md +556 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/architecture-diagrams.md +192 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/c4-diagrams.md +410 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/class-diagrams.md +361 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/erd-diagrams.md +510 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/flowcharts.md +450 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/sequence-diagrams.md +394 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/troubleshooting.md +335 -0
- package/bundles/codex/.codex/skills/media/mermaid-diagrams/references/workflows.md +418 -0
- package/bundles/codex/.codex/skills/media/particle-art/SKILL.md +243 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/canvas-utils.ts +171 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/dot-field.template.tsx +203 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/flow-field.template.tsx +263 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/node-shape.template.tsx +261 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/shape-sampler.ts +281 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/stipple-morph.template.tsx +167 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/stipple.template.tsx +175 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/trail-ghost.template.tsx +266 -0
- package/bundles/codex/.codex/skills/media/particle-art/references/usage-examples.md +320 -0
- package/bundles/codex/.codex/skills/testing/playwright/API_REFERENCE.md +653 -0
- package/bundles/codex/.codex/skills/testing/playwright/SKILL.md +454 -0
- package/bundles/codex/.codex/skills/testing/playwright/lib/helpers.js +441 -0
- package/bundles/codex/.codex/skills/testing/playwright/package.json +26 -0
- package/bundles/codex/.codex/skills/testing/playwright/run.js +228 -0
- package/bundles/codex/.codex/skills/testing/tdd-workflow/SKILL.md +188 -0
- package/bundles/codex/.codex/skills/testing/test-strategy/SKILL.md +155 -0
- package/bundles/codex/.codex/skills/testing/webapp-testing/LICENSE.txt +202 -0
- package/bundles/codex/.codex/skills/testing/webapp-testing/SKILL.md +97 -0
- package/bundles/codex/.codex/skills/testing/webapp-testing/examples/console_logging.py +35 -0
- package/bundles/codex/.codex/skills/testing/webapp-testing/examples/element_discovery.py +40 -0
- package/bundles/codex/.codex/skills/testing/webapp-testing/examples/static_html_automation.py +33 -0
- package/bundles/codex/.codex/skills/testing/webapp-testing/scripts/with_server.py +106 -0
- package/bundles/codex/.codex/skills/workflow/advisory-hub/SKILL.md +227 -0
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/context-template.md +214 -0
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/hub-handbook.md +233 -0
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/report-contract.md +94 -0
- package/bundles/codex/.codex/skills/workflow/advisory-hub/reference/succession.md +118 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/SKILL.md +500 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/best-practices-referencer.md +263 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/codebase-context-builder.md +326 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/agents/task-intent-analyzer.md +245 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/references/anti-patterns.md +571 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/references/before-after-examples.md +1114 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/references/best-practices-guide.md +513 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/references/common-workflows.md +692 -0
- package/bundles/codex/.codex/skills/workflow/best-practices/references/prompt-patterns.md +547 -0
- package/bundles/codex/.codex/skills/workflow/brainstorming/SKILL.md +57 -0
- package/bundles/codex/.codex/skills/workflow/context-curator/SKILL.md +157 -0
- package/bundles/codex/.codex/skills/workflow/golden-plan/SKILL.md +586 -0
- package/bundles/codex/.codex/skills/workflow/preflight/SKILL.md +440 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/LICENSE.txt +202 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/SKILL.md +491 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/agents/analyzer.md +274 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/agents/comparator.md +202 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/agents/grader.md +223 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/assets/eval_review.html +146 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/eval-viewer/generate_review.py +471 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/eval-viewer/viewer.html +1325 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/references/schemas.md +430 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/__init__.py +0 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/generate_report.py +326 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/improve_description.py +247 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/package_skill.py +136 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/quick_validate.py +103 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/run_eval.py +310 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/run_loop.py +328 -0
- package/bundles/codex/.codex/skills/workflow/skill-creator/scripts/utils.py +47 -0
- package/bundles/codex/AGENTS.md +146 -0
- package/bundles/manifest.json +4 -3
- package/dist/claude-deepseek.js +544 -0
- package/dist/claude-glm.js +544 -0
- package/dist/claude-oss.js +544 -0
- package/dist/cli.js +542 -90
- package/package.json +5 -2
- package/bundles/antigravity-plugin/statusline-command-agy.sh +0 -91
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/DMMono-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt +0 -94
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Gloock-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Italiana-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-Light.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-Medium.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Jura-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Italic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Lora-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf +0 -0
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt +0 -93
- package/bundles/antigravity-plugin-extras/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"""claudster_migrate_rules — migrate a repo's rules hierarchy to AGENTS.md-canonical + CLAUDE.md shims.
|
|
2
|
+
|
|
3
|
+
`AGENTS.md` is the canonical rules file every agent reads (Claude Code, Codex CLI, Antigravity); each
|
|
4
|
+
`CLAUDE.md` becomes a thin `@AGENTS.md` import shim. This tool migrates an existing repo — root pair +
|
|
5
|
+
every subfolder `CLAUDE.md` — to that layout, safely and idempotently.
|
|
6
|
+
|
|
7
|
+
Behavior (per target repo):
|
|
8
|
+
• ROOT: CLAUDE.md's (fuller, possibly enriched) content becomes the canonical AGENTS.md, re-headed
|
|
9
|
+
agent-neutrally; CLAUDE.md becomes the shim. An existing AGENTS.md that is the old caddis mirror
|
|
10
|
+
(a subset of CLAUDE.md) is replaced. If the existing AGENTS.md carries content CLAUDE.md LACKS
|
|
11
|
+
(project enrichments on both sides), it is a CONFLICT — the root pair is skipped and reported
|
|
12
|
+
(exit 1); a human resolves the few by hand-merge, then re-runs to confirm.
|
|
13
|
+
• SUBFOLDERS: each <dir>/CLAUDE.md is `git mv`'d to <dir>/AGENTS.md (content verbatim) + a <dir>/CLAUDE.md shim.
|
|
14
|
+
• Idempotent (a re-run is a no-op); refuses a dirty tree (unless --allow-dirty); `--dry-run` is the
|
|
15
|
+
default (`--apply` executes); `git mv` when tracked; never touches archived `done/`-style files.
|
|
16
|
+
• --check (fork detector): flags any CLAUDE.md that is NOT a shim beside an AGENTS.md (drifted shim),
|
|
17
|
+
and any bare CLAUDE.md with no sibling AGENTS.md (a hand-created fork). Exit 1 on findings.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python scripts/claudster_migrate_rules.py [target] # dry-run (default)
|
|
21
|
+
python scripts/claudster_migrate_rules.py [target] --apply # execute
|
|
22
|
+
python scripts/claudster_migrate_rules.py [target] --check # fork detector (gate)
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import os
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
# Reuse the harness templates + folder shim so the migration output matches what the generator emits.
|
|
33
|
+
try:
|
|
34
|
+
from setup_project_ai import HARNESS_DIR, SUBFOLDER_SHIM, render as _render
|
|
35
|
+
except Exception: # pragma: no cover — standalone fallback if setup_project_ai isn't importable
|
|
36
|
+
HARNESS_DIR = None
|
|
37
|
+
SUBFOLDER_SHIM = ("# Folder conventions — canonical in AGENTS.md (imported below; Claude Code inlines it)\n"
|
|
38
|
+
"@AGENTS.md\n")
|
|
39
|
+
|
|
40
|
+
def _render(text, mapping): # type: ignore
|
|
41
|
+
for k, v in mapping.items():
|
|
42
|
+
text = text.replace("{{" + k + "}}", v)
|
|
43
|
+
return text
|
|
44
|
+
|
|
45
|
+
# Directories we never migrate into (vendored/generated + archived rules).
|
|
46
|
+
SKIP_DIRS = {".git", "node_modules", ".venv", "venv", "__pycache__", ".mypy_cache", ".ruff_cache",
|
|
47
|
+
".pytest_cache", "dist", "build", ".tanstack", "done", "archive", "archived"}
|
|
48
|
+
|
|
49
|
+
# Section headings whose bodies are pure template boilerplate — their wording differs between the old
|
|
50
|
+
# root.md.tmpl and agents.md.tmpl, so they are excluded from the divergence check (only PROJECT content
|
|
51
|
+
# is compared). "What this project is" is intentionally NOT here — its content IS compared.
|
|
52
|
+
_BOILERPLATE_HEADING_KEYS = (
|
|
53
|
+
"the laws", "the harness", "development harness", "context discipline", "conventions",
|
|
54
|
+
"where things live", "doc discipline", "resuming a session", "resources", "why subagents",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
_CANONICAL_PREAMBLE = (
|
|
58
|
+
"> **This is the canonical rules file.** Every coding agent reads it — Claude Code (via the one-line\n"
|
|
59
|
+
"> `@AGENTS.md` import in `CLAUDE.md`), OpenAI Codex CLI, and Antigravity (`agy`). `CLAUDE.md` is a\n"
|
|
60
|
+
"> thin `@AGENTS.md` import shim; keep durable project rules HERE, never in the shim."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ── git helpers ──────────────────────────────────────────────────────────────
|
|
65
|
+
def _git(target: Path, *args: str) -> subprocess.CompletedProcess:
|
|
66
|
+
return subprocess.run(["git", *args], cwd=str(target), capture_output=True, text=True)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _in_git_repo(target: Path) -> bool:
|
|
70
|
+
r = _git(target, "rev-parse", "--is-inside-work-tree")
|
|
71
|
+
return r.returncode == 0 and r.stdout.strip() == "true"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _is_dirty(target: Path) -> bool:
|
|
75
|
+
return bool(_git(target, "status", "--porcelain").stdout.strip())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _is_tracked(target: Path, rel: str) -> bool:
|
|
79
|
+
return bool(_git(target, "ls-files", "--error-unmatch", rel).returncode == 0)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── content helpers ──────────────────────────────────────────────────────────
|
|
83
|
+
def is_shim(text: str) -> bool:
|
|
84
|
+
"""A CLAUDE.md is a shim iff it carries a bare `@AGENTS.md` import line."""
|
|
85
|
+
return any(ln.strip() == "@AGENTS.md" for ln in text.splitlines())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _project_name(claude_text: str, target: Path) -> str:
|
|
89
|
+
for ln in claude_text.splitlines():
|
|
90
|
+
if ln.startswith("# "):
|
|
91
|
+
title = ln[2:].strip()
|
|
92
|
+
# "<name> — Project Memory (root)" -> "<name>"
|
|
93
|
+
for sep in (" — ", " - ", " –– "):
|
|
94
|
+
if sep in title:
|
|
95
|
+
return title.split(sep, 1)[0].strip()
|
|
96
|
+
return title
|
|
97
|
+
return target.name
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _meaningful(text: str) -> set[str]:
|
|
101
|
+
"""Normalized set of content lines (md markers/whitespace stripped), for subset comparison."""
|
|
102
|
+
out: set[str] = set()
|
|
103
|
+
for ln in text.splitlines():
|
|
104
|
+
s = ln.strip().lstrip("#>-*").strip()
|
|
105
|
+
# drop an ordered-list prefix like "1. "
|
|
106
|
+
while s[:1].isdigit():
|
|
107
|
+
s = s[1:]
|
|
108
|
+
s = s.lstrip(". ").strip()
|
|
109
|
+
if len(s) >= 8:
|
|
110
|
+
out.add(s.lower())
|
|
111
|
+
return out
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _checkable_lines(agents_text: str) -> set[str]:
|
|
115
|
+
"""AGENTS.md's PROJECT content — meaningful lines from non-boilerplate-heading sections (and the
|
|
116
|
+
preamble is dropped). Compared against CLAUDE.md to detect divergence."""
|
|
117
|
+
lines = agents_text.splitlines()
|
|
118
|
+
# find section spans by "## " headings
|
|
119
|
+
heads = [i for i, l in enumerate(lines) if l.startswith("## ")]
|
|
120
|
+
out: set[str] = set()
|
|
121
|
+
for k, start in enumerate(heads):
|
|
122
|
+
heading = lines[start][3:].strip().lower()
|
|
123
|
+
if any(key in heading for key in _BOILERPLATE_HEADING_KEYS):
|
|
124
|
+
continue
|
|
125
|
+
end = heads[k + 1] if k + 1 < len(heads) else len(lines)
|
|
126
|
+
out |= _meaningful("\n".join(lines[start:end]))
|
|
127
|
+
return out
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def root_conflict(claude_text: str, agents_text: str) -> list[str]:
|
|
131
|
+
"""Return AGENTS.md project lines that CLAUDE.md lacks (the enrichment residue). Non-empty => the
|
|
132
|
+
two files diverged and must not be auto-merged."""
|
|
133
|
+
residue = _checkable_lines(agents_text) - _meaningful(claude_text)
|
|
134
|
+
return sorted(residue)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def reheaded_agents(claude_text: str, name: str) -> str:
|
|
138
|
+
"""Canonical AGENTS.md from CLAUDE.md content: replace the shim/mirror preamble (everything before
|
|
139
|
+
the first `## ` heading) with a canonical header + preamble; keep the body (enrichments) verbatim."""
|
|
140
|
+
lines = claude_text.splitlines()
|
|
141
|
+
idx = next((i for i, l in enumerate(lines) if l.startswith("## ")), None)
|
|
142
|
+
body = "\n".join(lines[idx:]) if idx is not None else claude_text
|
|
143
|
+
header = f"# {name} — Project Memory (canonical rules)\n\n{_CANONICAL_PREAMBLE}\n\n"
|
|
144
|
+
text = header + body
|
|
145
|
+
return text if text.endswith("\n") else text + "\n"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def root_shim(name: str) -> str:
|
|
149
|
+
"""The root CLAUDE.md shim — the harness root.md.tmpl rendered (reused, so no drift)."""
|
|
150
|
+
if HARNESS_DIR is not None:
|
|
151
|
+
tmpl = HARNESS_DIR / "claude-md" / "root.md.tmpl"
|
|
152
|
+
if tmpl.is_file():
|
|
153
|
+
return _render(tmpl.read_text(encoding="utf-8"), {"PROJECT_NAME": name})
|
|
154
|
+
return (f"# {name} — Project Memory\n\n@AGENTS.md\n\n"
|
|
155
|
+
"> **Canonical rules live in `AGENTS.md`** (imported above — Claude Code inlines it). This\n"
|
|
156
|
+
"> shim adds only Claude-native conveniences; durable rules go in `AGENTS.md`.\n")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ── migration steps ──────────────────────────────────────────────────────────
|
|
160
|
+
def _write(target: Path, rel: str, content: str, apply: bool, report: list[str], verb: str):
|
|
161
|
+
report.append(f"{verb}: {rel}")
|
|
162
|
+
if apply:
|
|
163
|
+
dest = target / rel
|
|
164
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
dest.write_text(content, encoding="utf-8")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _move(target: Path, src_rel: str, dst_rel: str, apply: bool, report: list[str]):
|
|
169
|
+
report.append(f"move: {src_rel} -> {dst_rel}")
|
|
170
|
+
if not apply:
|
|
171
|
+
return
|
|
172
|
+
if _is_tracked(target, src_rel):
|
|
173
|
+
r = _git(target, "mv", src_rel, dst_rel)
|
|
174
|
+
if r.returncode == 0:
|
|
175
|
+
return
|
|
176
|
+
# untracked (or git mv failed): plain rename
|
|
177
|
+
(target / dst_rel).parent.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
os.replace(target / src_rel, target / dst_rel)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def migrate_root(target: Path, apply: bool, report: list[str]) -> int:
|
|
182
|
+
"""Migrate the root pair. Returns 1 on conflict (root skipped), else 0."""
|
|
183
|
+
claude = target / "CLAUDE.md"
|
|
184
|
+
agents = target / "AGENTS.md"
|
|
185
|
+
if not claude.exists() and not agents.exists():
|
|
186
|
+
report.append("root: no CLAUDE.md/AGENTS.md — nothing to migrate")
|
|
187
|
+
return 0
|
|
188
|
+
claude_text = claude.read_text(encoding="utf-8") if claude.exists() else ""
|
|
189
|
+
if claude.exists() and is_shim(claude_text) and agents.exists():
|
|
190
|
+
report.append("root: already migrated (CLAUDE.md is a shim) — no-op")
|
|
191
|
+
return 0
|
|
192
|
+
if not claude.exists():
|
|
193
|
+
report.append("root: AGENTS.md present, no CLAUDE.md — leaving as-is (already canonical or hand-authored)")
|
|
194
|
+
return 0
|
|
195
|
+
name = _project_name(claude_text, target)
|
|
196
|
+
if agents.exists():
|
|
197
|
+
conflict = root_conflict(claude_text, agents.read_text(encoding="utf-8"))
|
|
198
|
+
if conflict:
|
|
199
|
+
report.append("root: CONFLICT — AGENTS.md carries content CLAUDE.md lacks; skipping root pair. "
|
|
200
|
+
"Hand-merge into CLAUDE.md, then re-run. Divergent line(s): "
|
|
201
|
+
+ "; ".join(conflict[:5]) + (" …" if len(conflict) > 5 else ""))
|
|
202
|
+
return 1
|
|
203
|
+
# safe: CLAUDE.md content -> canonical AGENTS.md; CLAUDE.md -> shim
|
|
204
|
+
_write(target, "AGENTS.md", reheaded_agents(claude_text, name), apply, report,
|
|
205
|
+
"rewrite (canonical)" if agents.exists() else "create (canonical)")
|
|
206
|
+
_write(target, "CLAUDE.md", root_shim(name), apply, report, "shim")
|
|
207
|
+
return 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _iter_subfolder_claude(target: Path):
|
|
211
|
+
for dirpath, dirnames, filenames in os.walk(target):
|
|
212
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
213
|
+
d = Path(dirpath)
|
|
214
|
+
if d == target:
|
|
215
|
+
continue # root handled separately
|
|
216
|
+
if "CLAUDE.md" in filenames:
|
|
217
|
+
yield d
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def migrate_subfolders(target: Path, apply: bool, report: list[str]) -> None:
|
|
221
|
+
for d in _iter_subfolder_claude(target):
|
|
222
|
+
rel = d.relative_to(target).as_posix()
|
|
223
|
+
claude_text = (d / "CLAUDE.md").read_text(encoding="utf-8")
|
|
224
|
+
if is_shim(claude_text) and (d / "AGENTS.md").exists():
|
|
225
|
+
report.append(f"{rel}/: already migrated — no-op")
|
|
226
|
+
continue
|
|
227
|
+
if (d / "AGENTS.md").exists():
|
|
228
|
+
report.append(f"{rel}/: CONFLICT — both AGENTS.md and a non-shim CLAUDE.md present; skipped")
|
|
229
|
+
continue
|
|
230
|
+
_move(target, f"{rel}/CLAUDE.md", f"{rel}/AGENTS.md", apply, report)
|
|
231
|
+
_write(target, f"{rel}/CLAUDE.md", SUBFOLDER_SHIM, apply, report, "shim")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def check_forks(target: Path) -> list[str]:
|
|
235
|
+
"""Fork detector: non-shim CLAUDE.md beside an AGENTS.md, or a bare CLAUDE.md with no sibling AGENTS.md."""
|
|
236
|
+
findings: list[str] = []
|
|
237
|
+
for dirpath, dirnames, filenames in os.walk(target):
|
|
238
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
239
|
+
if "CLAUDE.md" not in filenames:
|
|
240
|
+
continue
|
|
241
|
+
d = Path(dirpath)
|
|
242
|
+
rel = (d / "CLAUDE.md").relative_to(target).as_posix()
|
|
243
|
+
text = (d / "CLAUDE.md").read_text(encoding="utf-8", errors="replace")
|
|
244
|
+
if "AGENTS.md" in filenames:
|
|
245
|
+
if not is_shim(text):
|
|
246
|
+
findings.append(f"{rel}: CLAUDE.md is not a shim but a sibling AGENTS.md exists (drifted shim)")
|
|
247
|
+
else:
|
|
248
|
+
findings.append(f"{rel}: CLAUDE.md has no sibling AGENTS.md (hand-created fork — run /caddis:add-rules semantics)")
|
|
249
|
+
return findings
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
# ── main ─────────────────────────────────────────────────────────────────────
|
|
253
|
+
def main(argv: list[str] | None = None) -> int:
|
|
254
|
+
for _stream in (sys.stdout, sys.stderr): # Windows cp1252 can't encode em-dashes in our messages
|
|
255
|
+
if hasattr(_stream, "reconfigure"):
|
|
256
|
+
_stream.reconfigure(encoding="utf-8")
|
|
257
|
+
ap = argparse.ArgumentParser(description="Migrate a repo to AGENTS.md-canonical rules + CLAUDE.md shims.")
|
|
258
|
+
ap.add_argument("target", nargs="?", default=".", type=Path)
|
|
259
|
+
ap.add_argument("--apply", action="store_true", help="Execute (default is --dry-run).")
|
|
260
|
+
ap.add_argument("--allow-dirty", action="store_true", help="Proceed even if the working tree is dirty.")
|
|
261
|
+
ap.add_argument("--check", action="store_true", help="Fork detector only (gate mode); exit 1 on findings.")
|
|
262
|
+
args = ap.parse_args(argv)
|
|
263
|
+
|
|
264
|
+
target = args.target.resolve()
|
|
265
|
+
if not target.is_dir():
|
|
266
|
+
print(f"ERROR: target not a directory: {target}", file=sys.stderr)
|
|
267
|
+
return 2
|
|
268
|
+
|
|
269
|
+
if args.check:
|
|
270
|
+
findings = check_forks(target)
|
|
271
|
+
for f in findings:
|
|
272
|
+
print(f" fork: {f}")
|
|
273
|
+
if findings:
|
|
274
|
+
print(f"\n{len(findings)} rules-file fork(s) found.")
|
|
275
|
+
return 1
|
|
276
|
+
print("No rules-file forks — CLAUDE.md shims are clean.")
|
|
277
|
+
return 0
|
|
278
|
+
|
|
279
|
+
apply = args.apply
|
|
280
|
+
if not _in_git_repo(target):
|
|
281
|
+
print(f"ERROR: {target} is not a git repository (rules migration uses git mv).", file=sys.stderr)
|
|
282
|
+
return 2
|
|
283
|
+
if apply and _is_dirty(target) and not args.allow_dirty:
|
|
284
|
+
print("ERROR: working tree is dirty — commit/stash first, or pass --allow-dirty.", file=sys.stderr)
|
|
285
|
+
return 2
|
|
286
|
+
|
|
287
|
+
print(f"=== caddis-migrate-rules -> {target} {'(APPLY)' if apply else '(dry-run)'}")
|
|
288
|
+
report: list[str] = []
|
|
289
|
+
rc = migrate_root(target, apply, report)
|
|
290
|
+
migrate_subfolders(target, apply, report)
|
|
291
|
+
for line in report:
|
|
292
|
+
print(f" {line}")
|
|
293
|
+
if not apply:
|
|
294
|
+
print("\n(dry-run — no files changed; pass --apply to execute)")
|
|
295
|
+
print(f"\nresult: {'CONFLICT (root skipped)' if rc else 'ok'}")
|
|
296
|
+
return rc
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Inventory caddis's load-bearing rules, and measure how many are actually enforced.
|
|
3
|
+
|
|
4
|
+
WHY THIS EXISTS
|
|
5
|
+
---------------
|
|
6
|
+
Three independent audits reached the same conclusion in different words: strip the prose from
|
|
7
|
+
this plugin and the machine content is very small. Nearly every "MUST" / "NEVER" / "STOP" is a
|
|
8
|
+
sentence in a markdown file that a model may or may not honour. Those degrade silently under
|
|
9
|
+
model change, context pressure, a cheaper lane, or a distracted session — and they read as
|
|
10
|
+
guarantees, which is worse than reading as advice.
|
|
11
|
+
|
|
12
|
+
The things that actually held under pressure were the ones with exit codes: pytest,
|
|
13
|
+
validate_pool, the hooks, oss_review's exit 2.
|
|
14
|
+
|
|
15
|
+
So this script measures the ratio of rules that a machine can fail on, to rules that only a
|
|
16
|
+
model can choose to honour. That number is the health metric for the plugin. It is expected to
|
|
17
|
+
start LOW; the point is that it is visible and moves in one direction.
|
|
18
|
+
|
|
19
|
+
THE ANTI-FLATTERY RULE
|
|
20
|
+
----------------------
|
|
21
|
+
The classification is DERIVED FROM THE LINT TESTS, never asserted by hand. A rule counts as
|
|
22
|
+
`machine` only when a test that targets its file asserts a literal string contained in that
|
|
23
|
+
rule's text — i.e. deleting the rule would turn a test red. Everything else is `advisory`.
|
|
24
|
+
|
|
25
|
+
This matters because the first version of this script DID use a hand-written registry, and it
|
|
26
|
+
was wrong in both directions: a global substring match credited an unrelated skill's "ALWAYS ask
|
|
27
|
+
via AskUserQuestion" to the headless-mode test, while genuinely-covered rules went uncounted
|
|
28
|
+
because their sentence lacked the registry keyword. A metric you can talk yourself into is not a
|
|
29
|
+
metric. Deriving it from the tests also means it moves on its own the moment someone adds or
|
|
30
|
+
removes a lint, which is the only way a health number stays honest.
|
|
31
|
+
|
|
32
|
+
Known limit, stated rather than hidden: matching is per-line, so a rule spanning two lines can be
|
|
33
|
+
missed, and a long asserted literal can match a neighbouring sentence. Treat the ratio as an
|
|
34
|
+
order-of-magnitude signal and a direction of travel, not a precise score.
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
python scripts/enforcement_inventory.py # summary + ratio
|
|
38
|
+
python scripts/enforcement_inventory.py --list # every rule, classified
|
|
39
|
+
python scripts/enforcement_inventory.py --advisory # only the unenforced ones
|
|
40
|
+
"""
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import argparse
|
|
44
|
+
import re
|
|
45
|
+
import sys
|
|
46
|
+
from dataclasses import dataclass
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
|
|
49
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
50
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
51
|
+
|
|
52
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
53
|
+
SCAN_ROOTS = [REPO / "claude-harness" / "commands", REPO / ".github" / "skills"]
|
|
54
|
+
|
|
55
|
+
# An imperative that makes a promise. Ordered longest-first so MUST NOT wins over MUST.
|
|
56
|
+
IMPERATIVE = re.compile(
|
|
57
|
+
r"\b(MUST NOT|MUST|NEVER|ALWAYS|REQUIRED|STOP|DO NOT|Do not)\b"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# ── Deriving enforcement FROM THE TESTS, not from a hand-written claim ──────
|
|
61
|
+
# An earlier version used a hand-maintained registry of rule->enforcer. It was wrong in both
|
|
62
|
+
# directions: a global substring match credited brand-design's "ALWAYS ask via AskUserQuestion"
|
|
63
|
+
# to the headless-mode test, which has nothing to do with it, while genuinely-covered rules went
|
|
64
|
+
# uncounted because their sentence lacked the registry's keyword.
|
|
65
|
+
#
|
|
66
|
+
# So the classifier now READS THE LINT TESTS. For each test module it works out which source
|
|
67
|
+
# files that module targets (from its module-level Path constants) and which literal strings it
|
|
68
|
+
# asserts on. A rule is `machine` when a test targeting ITS file asserts a literal contained in
|
|
69
|
+
# its text. Nothing is claimed that a test does not actually check, and the metric updates itself
|
|
70
|
+
# the moment someone adds or deletes a lint - which is the property a health metric must have.
|
|
71
|
+
LINT_TEST_DIRS = [REPO / "scripts" / "tests", REPO / "claude-harness" / "hooks" / "tests"]
|
|
72
|
+
|
|
73
|
+
# Literals shorter than this match too much to mean anything.
|
|
74
|
+
MIN_LITERAL = 10
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _test_modules() -> list[tuple[set[str], set[str]]]:
|
|
78
|
+
"""(source files this test targets, literals it asserts) for every lint test module."""
|
|
79
|
+
import ast
|
|
80
|
+
out = []
|
|
81
|
+
for d in LINT_TEST_DIRS:
|
|
82
|
+
if not d.exists():
|
|
83
|
+
continue
|
|
84
|
+
for f in sorted(d.glob("test_*.py")):
|
|
85
|
+
if f.name == "test_enforcement_inventory.py":
|
|
86
|
+
continue # measures the metric; must not feed it
|
|
87
|
+
try:
|
|
88
|
+
tree = ast.parse(f.read_text(encoding="utf-8", errors="ignore"))
|
|
89
|
+
except SyntaxError:
|
|
90
|
+
continue
|
|
91
|
+
# ONLY literals inside an `assert` count. Scanning every string constant swept in
|
|
92
|
+
# docstrings and fixture data, and this module's own test docstrings describe the very
|
|
93
|
+
# rules being measured - so prose about a rule was being counted as enforcement OF that
|
|
94
|
+
# rule. The metric was measuring how much we had WRITTEN about enforcement.
|
|
95
|
+
literals: set[str] = set()
|
|
96
|
+
for node in ast.walk(tree):
|
|
97
|
+
if not isinstance(node, ast.Assert):
|
|
98
|
+
continue
|
|
99
|
+
for sub in ast.walk(node):
|
|
100
|
+
if isinstance(sub, ast.Constant) and isinstance(sub.value, str):
|
|
101
|
+
v = sub.value.strip()
|
|
102
|
+
if len(v) >= MIN_LITERAL:
|
|
103
|
+
literals.add(v)
|
|
104
|
+
# TARGETS come from ALL string constants, not just asserts: a module names the file
|
|
105
|
+
# it lints in a module-level Path constant (`_IMPLEMENT = ... / "implement.md"`), which
|
|
106
|
+
# is not an assert. Deriving both from the assert set emptied every target and drove
|
|
107
|
+
# the ratio to a false 0.0% - caught by this module's own plausibility test.
|
|
108
|
+
targets = {
|
|
109
|
+
v.strip() for node in ast.walk(tree)
|
|
110
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str)
|
|
111
|
+
for v in [node.value] if v.strip().endswith(".md")
|
|
112
|
+
}
|
|
113
|
+
out.append((targets, literals))
|
|
114
|
+
return out
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
_MODULES = _test_modules()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _enforcer_for(rel_path: str, text: str) -> str | None:
|
|
121
|
+
"""The lint that would fail if this rule were deleted, or None."""
|
|
122
|
+
fname = rel_path.rsplit("/", 1)[-1]
|
|
123
|
+
for targets, literals in _MODULES:
|
|
124
|
+
if not any(t == fname or t.endswith(fname) for t in targets):
|
|
125
|
+
continue
|
|
126
|
+
for lit in literals:
|
|
127
|
+
if lit.lower() in text.lower():
|
|
128
|
+
return "lint"
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ── Gates: rules enforced by an executable, not merely by a content lint ────
|
|
133
|
+
# The first version counted only lint tests, so a rule backed by a REAL gate scored the same as
|
|
134
|
+
# one backed by nothing - the metric was blind to the strongest form of enforcement and would
|
|
135
|
+
# have shown no movement for the phase that introduced it. A rule counts as gate-enforced when
|
|
136
|
+
# its file invokes caddis_gate.py AND the rule concerns one of the gate's actual subcommands.
|
|
137
|
+
# The subcommand must exist in the script: a claim nobody implements is exactly what this file
|
|
138
|
+
# refuses to reward.
|
|
139
|
+
GATE_SCRIPT = REPO / "scripts" / "caddis_gate.py"
|
|
140
|
+
GATE_CONCERNS = {
|
|
141
|
+
"lane-check": ("lane", "spawn", "launch command", "--print", "-p"),
|
|
142
|
+
"verdict-gate": ("verdict", "advisory-hub", "blocked-pending-hub", "accept-degraded"),
|
|
143
|
+
"tracker-vs-git": ("tracker",),
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _live_gates() -> dict[str, tuple[str, ...]]:
|
|
148
|
+
"""Subcommands the gate script actually implements. Verified, never assumed."""
|
|
149
|
+
if not GATE_SCRIPT.is_file():
|
|
150
|
+
return {}
|
|
151
|
+
src = GATE_SCRIPT.read_text(encoding="utf-8", errors="ignore")
|
|
152
|
+
return {k: v for k, v in GATE_CONCERNS.items() if f'"{k}"' in src or f"'{k}'" in src}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
_GATES = _live_gates()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _gate_for(file_text: str, text: str) -> str | None:
|
|
159
|
+
if "caddis_gate.py" not in file_text:
|
|
160
|
+
return None
|
|
161
|
+
low = text.lower()
|
|
162
|
+
for sub, needles in _GATES.items():
|
|
163
|
+
if any(n.lower() in low for n in needles):
|
|
164
|
+
return sub
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass
|
|
169
|
+
class Rule:
|
|
170
|
+
path: str
|
|
171
|
+
line: int
|
|
172
|
+
marker: str
|
|
173
|
+
text: str
|
|
174
|
+
enforcer: str | None = None
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def machine(self) -> bool:
|
|
178
|
+
return self.enforcer is not None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _sentence(line: str) -> str:
|
|
182
|
+
return " ".join(line.split())[:150]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def collect(roots: list[Path] | None = None) -> list[Rule]:
|
|
186
|
+
rules: list[Rule] = []
|
|
187
|
+
for root in (roots or SCAN_ROOTS):
|
|
188
|
+
if not root.exists():
|
|
189
|
+
continue
|
|
190
|
+
for f in sorted(root.rglob("*.md")):
|
|
191
|
+
try:
|
|
192
|
+
lines = f.read_text(encoding="utf-8", errors="ignore").splitlines()
|
|
193
|
+
except Exception:
|
|
194
|
+
continue
|
|
195
|
+
# Fall back to the absolute path for roots outside the repo, so collect() can be
|
|
196
|
+
# pointed at a fixture directory. A metric script that cannot be tested on a fixture
|
|
197
|
+
# can only be verified by eyeballing its output, which is how metrics start lying.
|
|
198
|
+
whole = "\n".join(lines)
|
|
199
|
+
try:
|
|
200
|
+
rel = str(f.relative_to(REPO)).replace("\\", "/")
|
|
201
|
+
except ValueError:
|
|
202
|
+
rel = str(f).replace("\\", "/")
|
|
203
|
+
for i, line in enumerate(lines, 1):
|
|
204
|
+
m = IMPERATIVE.search(line)
|
|
205
|
+
if not m:
|
|
206
|
+
continue
|
|
207
|
+
text = _sentence(line)
|
|
208
|
+
enf = _enforcer_for(rel, text) or _gate_for(whole, text)
|
|
209
|
+
rules.append(Rule(rel, i, m.group(1), text, enf))
|
|
210
|
+
return rules
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def main(argv: list[str] | None = None) -> int:
|
|
214
|
+
ap = argparse.ArgumentParser(description="caddis enforcement inventory")
|
|
215
|
+
ap.add_argument("--list", action="store_true", help="print every rule, classified")
|
|
216
|
+
ap.add_argument("--advisory", action="store_true", help="print only the unenforced rules")
|
|
217
|
+
args = ap.parse_args(argv)
|
|
218
|
+
|
|
219
|
+
rules = collect()
|
|
220
|
+
machine = [r for r in rules if r.machine]
|
|
221
|
+
advisory = [r for r in rules if not r.machine]
|
|
222
|
+
total = len(rules)
|
|
223
|
+
ratio = (len(machine) / total) if total else 0.0
|
|
224
|
+
|
|
225
|
+
if args.list or args.advisory:
|
|
226
|
+
for r in rules:
|
|
227
|
+
if args.advisory and r.machine:
|
|
228
|
+
continue
|
|
229
|
+
tag = f"machine <- {r.enforcer}" if r.machine else "advisory"
|
|
230
|
+
print(f" [{tag}] {r.path}:{r.line} {r.marker}")
|
|
231
|
+
print(f" {r.text}")
|
|
232
|
+
print()
|
|
233
|
+
|
|
234
|
+
# Report the SPLIT, not just the headline. A single blended number is actively misleading:
|
|
235
|
+
# it is dominated by the ~235 skill rules, which are guidance ("prefer this", "consider that")
|
|
236
|
+
# and SHOULD be advisory - turning them into exit codes would be theatre. The number that
|
|
237
|
+
# matters is commands, because that is the execution path where a broken guarantee costs you.
|
|
238
|
+
cmd = [r for r in rules if "/commands/" in r.path]
|
|
239
|
+
skl = [r for r in rules if "/commands/" not in r.path]
|
|
240
|
+
def _pct(group):
|
|
241
|
+
return (sum(1 for r in group if r.machine) / len(group)) if group else 0.0
|
|
242
|
+
|
|
243
|
+
print("=" * 62)
|
|
244
|
+
print(f" load-bearing rules found : {total}")
|
|
245
|
+
print(f" machine-enforced : {len(machine)}")
|
|
246
|
+
print(f" advisory only : {len(advisory)}")
|
|
247
|
+
print("-" * 62)
|
|
248
|
+
print(f" COMMANDS (execution path): {sum(1 for r in cmd if r.machine)}/{len(cmd)} "
|
|
249
|
+
f"= {_pct(cmd):.1%} <- the figure that matters")
|
|
250
|
+
print(f" SKILLS (guidance) : {sum(1 for r in skl if r.machine)}/{len(skl)} "
|
|
251
|
+
f"= {_pct(skl):.1%} (advisory is correct here)")
|
|
252
|
+
print(f" blended : {ratio:.1%}")
|
|
253
|
+
print("=" * 62)
|
|
254
|
+
print(" A low number is not a bug report - it is the starting point. Advisory rules are")
|
|
255
|
+
print(" legitimate; the danger is an advisory rule that READS like a guarantee. Move the")
|
|
256
|
+
print(" load-bearing ones to exit codes, and label the rest honestly.")
|
|
257
|
+
return 0
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
if __name__ == "__main__":
|
|
261
|
+
raise SystemExit(main())
|