@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,270 @@
|
|
|
1
|
+
"""caddis_migrate_dir — rename a repo's legacy `.claudster/` artifact dir to `.caddis/`.
|
|
2
|
+
|
|
3
|
+
The `.claudster` → `.caddis` rename is OPT-IN, per repo, and never automatic: `git mv`-ing a
|
|
4
|
+
directory under a concurrent session would move files out from under its in-flight writes (a Stop
|
|
5
|
+
hook appending `usage-log.jsonl`, an agent writing a plan). So the SessionStart hook only *nudges*;
|
|
6
|
+
this command does the work when you say so.
|
|
7
|
+
|
|
8
|
+
Everything keeps working either way — every caddis reader tries `.caddis` then `.claudster`, and
|
|
9
|
+
every writer writes where the repo already lives. Migration is about consistency, not function.
|
|
10
|
+
|
|
11
|
+
Behavior (per target repo):
|
|
12
|
+
• SIMPLE CASE — only `.claudster/` exists: one `git mv .claudster .caddis` (history preserved).
|
|
13
|
+
• BOTH EXIST (the straggler case — an out-of-date session or teammate recreated `.claudster/`
|
|
14
|
+
after a migration): per-CHILD merge into `.caddis/`. A child that exists only in the legacy dir
|
|
15
|
+
is moved. A child that exists in BOTH is merged by kind:
|
|
16
|
+
- `.jsonl` append-logs (usage-log / agent-log / memory) → CONCATENATED, legacy lines FIRST
|
|
17
|
+
(they are older), so no session's history is dropped;
|
|
18
|
+
- directories → recursed, same rules;
|
|
19
|
+
- anything else → left in place and REPORTED (a human decides; we never clobber content).
|
|
20
|
+
An emptied `.claudster/` is removed at the end; one with unresolved conflicts is kept.
|
|
21
|
+
• LIVE REFS are rewritten so the migrated repo's own artifacts don't point at the old dir:
|
|
22
|
+
`.caddis/workstreams.json` (parked-workstream plan paths) and `relay.md`.
|
|
23
|
+
• Idempotent: a re-run on a migrated repo is a no-op. `--dry-run` is the DEFAULT (`--apply`
|
|
24
|
+
executes). Refuses a dirty tree unless `--allow-dirty` — the git mv must be reviewable.
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
python scripts/caddis_migrate_dir.py [target] # dry-run (default)
|
|
28
|
+
python scripts/caddis_migrate_dir.py [target] --apply # execute
|
|
29
|
+
python scripts/caddis_migrate_dir.py [target] --check # exit 1 if a legacy dir remains (gate)
|
|
30
|
+
|
|
31
|
+
Exit codes: 0 ok / nothing to do · 1 unresolved conflicts (or --check found a legacy dir) · 2 bad
|
|
32
|
+
target / not a git repo / dirty tree.
|
|
33
|
+
"""
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import json
|
|
38
|
+
import os
|
|
39
|
+
import shutil
|
|
40
|
+
import subprocess
|
|
41
|
+
import sys
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
|
|
44
|
+
NEW_DIR = ".caddis"
|
|
45
|
+
OLD_DIR = ".claudster"
|
|
46
|
+
|
|
47
|
+
#: Append-only JSONL logs: safe to concatenate when both dirs carry one (legacy lines first).
|
|
48
|
+
JSONL_MERGE_NAMES = frozenset({"usage-log.jsonl", "agent-log.jsonl", "memory.jsonl"})
|
|
49
|
+
|
|
50
|
+
#: Files whose CONTENT may name the old dir and is read live after the migration. Historical
|
|
51
|
+
#: artifacts (plans, handoffs, past reviews) are deliberately NOT rewritten — they are a record of
|
|
52
|
+
#: what was true then, and rewriting them would churn the diff for no functional gain.
|
|
53
|
+
LIVE_REF_FILES = ("workstreams.json", "relay.md")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── git helpers (mirrors claudster_migrate_rules.py) ─────────────────────────
|
|
57
|
+
def _git(target: Path, *args: str) -> subprocess.CompletedProcess:
|
|
58
|
+
return subprocess.run(["git", *args], cwd=str(target), capture_output=True, text=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _in_git_repo(target: Path) -> bool:
|
|
62
|
+
r = _git(target, "rev-parse", "--is-inside-work-tree")
|
|
63
|
+
return r.returncode == 0 and r.stdout.strip() == "true"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_dirty(target: Path) -> bool:
|
|
67
|
+
return bool(_git(target, "status", "--porcelain").stdout.strip())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_tracked(target: Path, rel: str) -> bool:
|
|
71
|
+
return _git(target, "ls-files", "--error-unmatch", "--", rel).returncode == 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _move(target: Path, src_rel: str, dst_rel: str, apply: bool, report: list[str]) -> None:
|
|
75
|
+
"""Move one path, preferring `git mv` so history follows the file."""
|
|
76
|
+
report.append(f"move: {src_rel} -> {dst_rel}")
|
|
77
|
+
if not apply:
|
|
78
|
+
return
|
|
79
|
+
dst = target / dst_rel
|
|
80
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
if _is_tracked(target, src_rel) and _git(target, "mv", src_rel, dst_rel).returncode == 0:
|
|
82
|
+
return
|
|
83
|
+
# untracked (or git mv refused): a plain move keeps the content; git sees add+delete
|
|
84
|
+
shutil.move(str(target / src_rel), str(dst))
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── merge (the both-exist case) ──────────────────────────────────────────────
|
|
88
|
+
def _concat_jsonl(old: Path, new: Path, apply: bool) -> int:
|
|
89
|
+
"""Prepend `old`'s lines to `new` (legacy is older). Returns the number of lines merged.
|
|
90
|
+
|
|
91
|
+
Blank lines are dropped and a trailing newline is guaranteed, so a half-written last line in
|
|
92
|
+
either file can't glue two records together.
|
|
93
|
+
"""
|
|
94
|
+
old_lines = [ln for ln in old.read_text(encoding="utf-8", errors="replace").splitlines() if ln.strip()]
|
|
95
|
+
if not apply:
|
|
96
|
+
return len(old_lines)
|
|
97
|
+
new_lines = [ln for ln in new.read_text(encoding="utf-8", errors="replace").splitlines() if ln.strip()]
|
|
98
|
+
new.write_text("\n".join(old_lines + new_lines) + "\n", encoding="utf-8")
|
|
99
|
+
return len(old_lines)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _merge_tree(target: Path, old_rel: str, new_rel: str, apply: bool,
|
|
103
|
+
report: list[str], conflicts: list[str]) -> None:
|
|
104
|
+
"""Merge `<old_rel>/` into `<new_rel>/`, child by child. Both are repo-relative posix paths."""
|
|
105
|
+
old_dir = target / old_rel
|
|
106
|
+
for child in sorted(old_dir.iterdir()):
|
|
107
|
+
c_old = f"{old_rel}/{child.name}"
|
|
108
|
+
c_new = f"{new_rel}/{child.name}"
|
|
109
|
+
dest = target / c_new
|
|
110
|
+
if not dest.exists():
|
|
111
|
+
_move(target, c_old, c_new, apply, report)
|
|
112
|
+
continue
|
|
113
|
+
if child.is_dir() and dest.is_dir():
|
|
114
|
+
_merge_tree(target, c_old, c_new, apply, report, conflicts)
|
|
115
|
+
continue
|
|
116
|
+
if child.is_file() and dest.is_file() and child.name in JSONL_MERGE_NAMES:
|
|
117
|
+
n = _concat_jsonl(child, dest, apply)
|
|
118
|
+
report.append(f"merge: {c_old} -> {c_new} ({n} legacy line(s), oldest first)")
|
|
119
|
+
if apply:
|
|
120
|
+
_remove(target, c_old)
|
|
121
|
+
continue
|
|
122
|
+
conflicts.append(c_old)
|
|
123
|
+
report.append(f"CONFLICT: {c_old} and {c_new} both exist and cannot be merged — left in place")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _remove(target: Path, rel: str) -> None:
|
|
127
|
+
"""Delete a merged-away file, via `git rm` when tracked so the index stays consistent."""
|
|
128
|
+
if _is_tracked(target, rel) and _git(target, "rm", "-q", "-f", "--", rel).returncode == 0:
|
|
129
|
+
return
|
|
130
|
+
try:
|
|
131
|
+
(target / rel).unlink()
|
|
132
|
+
except OSError:
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _prune_empty(target: Path, rel: str) -> bool:
|
|
137
|
+
"""Remove `<rel>` if it is now empty (depth-first). True when it is gone."""
|
|
138
|
+
d = target / rel
|
|
139
|
+
if not d.is_dir():
|
|
140
|
+
return True
|
|
141
|
+
for child in sorted(d.iterdir()):
|
|
142
|
+
if child.is_dir():
|
|
143
|
+
_prune_empty(target, f"{rel}/{child.name}")
|
|
144
|
+
try:
|
|
145
|
+
d.rmdir()
|
|
146
|
+
return True
|
|
147
|
+
except OSError:
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ── live-reference rewriting ─────────────────────────────────────────────────
|
|
152
|
+
def rewrite_refs_text(text: str) -> str:
|
|
153
|
+
"""Replace `.claudster/` path prefixes with `.caddis/` in a live artifact's text. Pure.
|
|
154
|
+
|
|
155
|
+
Only the dir followed by a separator is rewritten, so prose about "the .claudster rename"
|
|
156
|
+
survives intact while `.claudster/plans/foo.md` becomes `.caddis/plans/foo.md`. Backslash
|
|
157
|
+
separators are handled too — a Windows-written workstreams.json can carry either.
|
|
158
|
+
"""
|
|
159
|
+
return (text.replace(f"{OLD_DIR}/", f"{NEW_DIR}/")
|
|
160
|
+
.replace(f"{OLD_DIR}\\", f"{NEW_DIR}\\"))
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def rewrite_live_refs(target: Path, apply: bool, report: list[str]) -> None:
|
|
164
|
+
"""Rewrite `.claudster/...` paths inside the migrated dir's live state files."""
|
|
165
|
+
for name in LIVE_REF_FILES:
|
|
166
|
+
p = target / NEW_DIR / name
|
|
167
|
+
if not p.is_file():
|
|
168
|
+
continue
|
|
169
|
+
original = p.read_text(encoding="utf-8", errors="replace")
|
|
170
|
+
updated = rewrite_refs_text(original)
|
|
171
|
+
if updated == original:
|
|
172
|
+
continue
|
|
173
|
+
n = original.count(f"{OLD_DIR}/")
|
|
174
|
+
report.append(f"rewrite: {NEW_DIR}/{name} ({n} legacy path ref(s) -> {NEW_DIR}/)")
|
|
175
|
+
if apply:
|
|
176
|
+
p.write_text(updated, encoding="utf-8")
|
|
177
|
+
# workstreams.json must stay valid JSON — verify, and roll back the rewrite if it doesn't.
|
|
178
|
+
ws = target / NEW_DIR / "workstreams.json"
|
|
179
|
+
if apply and ws.is_file():
|
|
180
|
+
try:
|
|
181
|
+
json.loads(ws.read_text(encoding="utf-8"))
|
|
182
|
+
except Exception:
|
|
183
|
+
report.append(f"WARNING: {NEW_DIR}/workstreams.json is not valid JSON after rewriting "
|
|
184
|
+
"— check it by hand")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ── the migration ────────────────────────────────────────────────────────────
|
|
188
|
+
def migrate(target: Path, apply: bool) -> tuple[int, list[str]]:
|
|
189
|
+
"""Migrate `target`. Returns `(exit_code, report_lines)`."""
|
|
190
|
+
report: list[str] = []
|
|
191
|
+
conflicts: list[str] = []
|
|
192
|
+
old, new = target / OLD_DIR, target / NEW_DIR
|
|
193
|
+
|
|
194
|
+
if not old.is_dir():
|
|
195
|
+
report.append(f"nothing to do: no {OLD_DIR}/ in this repo"
|
|
196
|
+
+ (f" ({NEW_DIR}/ already present)" if new.is_dir() else ""))
|
|
197
|
+
return 0, report
|
|
198
|
+
|
|
199
|
+
if not new.exists():
|
|
200
|
+
_move(target, OLD_DIR, NEW_DIR, apply, report)
|
|
201
|
+
else:
|
|
202
|
+
report.append(f"both {OLD_DIR}/ and {NEW_DIR}/ exist — merging per child "
|
|
203
|
+
"(a straggler dir recreated by an out-of-date session)")
|
|
204
|
+
_merge_tree(target, OLD_DIR, NEW_DIR, apply, report, conflicts)
|
|
205
|
+
if apply:
|
|
206
|
+
if _prune_empty(target, OLD_DIR):
|
|
207
|
+
report.append(f"removed: empty {OLD_DIR}/")
|
|
208
|
+
else:
|
|
209
|
+
report.append(f"kept: {OLD_DIR}/ still holds {len(conflicts)} unmerged path(s)")
|
|
210
|
+
|
|
211
|
+
rewrite_live_refs(target, apply, report)
|
|
212
|
+
|
|
213
|
+
if conflicts:
|
|
214
|
+
report.append(f"\n{len(conflicts)} conflict(s) need a human: " + ", ".join(conflicts))
|
|
215
|
+
return 1, report
|
|
216
|
+
return 0, report
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def main(argv: list[str] | None = None) -> int:
|
|
220
|
+
for _stream in (sys.stdout, sys.stderr): # Windows cp1252 can't encode the em-dashes below
|
|
221
|
+
if hasattr(_stream, "reconfigure"):
|
|
222
|
+
_stream.reconfigure(encoding="utf-8")
|
|
223
|
+
ap = argparse.ArgumentParser(
|
|
224
|
+
description=f"Migrate a repo's legacy {OLD_DIR}/ artifact dir to {NEW_DIR}/.")
|
|
225
|
+
ap.add_argument("target", nargs="?", default=".", type=Path)
|
|
226
|
+
ap.add_argument("--apply", action="store_true", help="Execute (default is --dry-run).")
|
|
227
|
+
ap.add_argument("--allow-dirty", action="store_true", help="Proceed even if the tree is dirty.")
|
|
228
|
+
ap.add_argument("--check", action="store_true",
|
|
229
|
+
help=f"Report only; exit 1 if {OLD_DIR}/ is still present (gate mode).")
|
|
230
|
+
args = ap.parse_args(argv)
|
|
231
|
+
|
|
232
|
+
target = args.target.resolve()
|
|
233
|
+
if not target.is_dir():
|
|
234
|
+
print(f"ERROR: target not a directory: {target}", file=sys.stderr)
|
|
235
|
+
return 2
|
|
236
|
+
|
|
237
|
+
if args.check:
|
|
238
|
+
if (target / OLD_DIR).is_dir():
|
|
239
|
+
print(f" legacy {OLD_DIR}/ present — run /caddis:migrate-dir to convert it.")
|
|
240
|
+
return 1
|
|
241
|
+
print(f"No legacy {OLD_DIR}/ — this repo is on {NEW_DIR}/.")
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
apply = args.apply
|
|
245
|
+
if not _in_git_repo(target):
|
|
246
|
+
print(f"ERROR: {target} is not a git repository (the migration uses git mv to keep history).",
|
|
247
|
+
file=sys.stderr)
|
|
248
|
+
return 2
|
|
249
|
+
# The dirty-tree refusal only applies when there is actually a rename to make — a re-run on an
|
|
250
|
+
# already-migrated repo is a no-op and must not fail just because the tree has unrelated edits.
|
|
251
|
+
if apply and (target / OLD_DIR).is_dir() and _is_dirty(target) and not args.allow_dirty:
|
|
252
|
+
print("ERROR: working tree is dirty — commit/stash first, or pass --allow-dirty.\n"
|
|
253
|
+
" (A rename mixed into unrelated edits is unreviewable, and this repo's tree "
|
|
254
|
+
"may be shared with a live session.)", file=sys.stderr)
|
|
255
|
+
return 2
|
|
256
|
+
|
|
257
|
+
print(f"=== caddis-migrate-dir -> {target} {'(APPLY)' if apply else '(dry-run)'}")
|
|
258
|
+
rc, report = migrate(target, apply)
|
|
259
|
+
for line in report:
|
|
260
|
+
print(f" {line}")
|
|
261
|
+
if not apply:
|
|
262
|
+
print("\n(dry-run — no files changed; pass --apply to execute)")
|
|
263
|
+
elif rc == 0:
|
|
264
|
+
print("\nMigrated. Review with `git status` / `git diff --staged`, then commit.")
|
|
265
|
+
print(f"\nresult: {'CONFLICTS' if rc else 'ok'}")
|
|
266
|
+
return rc
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
if __name__ == "__main__":
|
|
270
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""caddis_spawn — shared machinery for handing work to a fresh session.
|
|
3
|
+
|
|
4
|
+
WHY THIS IS ONE MODULE AND NOT TWO
|
|
5
|
+
----------------------------------
|
|
6
|
+
`/caddis:spawn-hub` and `/caddis:spawn-session` transfer different things — a validation ROLE and
|
|
7
|
+
the WORK itself — but they fail the same way and defend against it with the same four mechanisms:
|
|
8
|
+
|
|
9
|
+
1. capture from the repo, never from recall
|
|
10
|
+
2. a self-check whose naive answer is WRONG, so a successor that skimmed fails visibly
|
|
11
|
+
3. no stored answer key: re-derive at validation time, which checks the DOCUMENT as well
|
|
12
|
+
4. refuse to write anything that goes stale (a commit hash, a test count) as a fact
|
|
13
|
+
|
|
14
|
+
Built as one module so a fix lands in both. The alternative was already visible in the codebase:
|
|
15
|
+
`succession.md` §J had the trap idea and `spawn-session` was about to grow a second copy of it.
|
|
16
|
+
|
|
17
|
+
THE FAILURE MODE, PRECISELY
|
|
18
|
+
---------------------------
|
|
19
|
+
A long session does not forget. It **recalls superseded facts fluently**. Measured in one session:
|
|
20
|
+
two throughput figures the agent had itself withdrawn were re-quoted days later; "E8 is blocked on
|
|
21
|
+
F1" was repeated for three days without a re-test; a commit hash written into a handover went stale
|
|
22
|
+
within the hour. None of that is amnesia, and none of it is caught by asking the outgoing agent
|
|
23
|
+
what it knows — because the failure mode IS what it knows.
|
|
24
|
+
|
|
25
|
+
Usage:
|
|
26
|
+
python scripts/caddis_spawn.py preflight # refuse to spawn from an unsafe state
|
|
27
|
+
python scripts/caddis_spawn.py fingerprint # head + counts, for stale-paste detection
|
|
28
|
+
python scripts/caddis_spawn.py check --doc <file> # round 0: the parent checks its own handover
|
|
29
|
+
python scripts/caddis_spawn.py verify-question --answer-in <file> --needle <text>
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import re
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
from dataclasses import dataclass, field
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
41
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
42
|
+
|
|
43
|
+
EXIT_OK, EXIT_REFUSED = 0, 1
|
|
44
|
+
|
|
45
|
+
ART = ".caddis"
|
|
46
|
+
SPAWN_DIR = f"{ART}/spawn-session"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _run(args: list[str], cwd: Path) -> str:
|
|
50
|
+
try:
|
|
51
|
+
r = subprocess.run(args, cwd=str(cwd), capture_output=True, text=True,
|
|
52
|
+
encoding="utf-8", errors="replace", timeout=120)
|
|
53
|
+
return r.stdout.strip() if r.returncode == 0 else ""
|
|
54
|
+
except Exception:
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── things that go stale ────────────────────────────────────────────────────────────────────
|
|
59
|
+
#
|
|
60
|
+
# A 7-to-40 character hex run is a commit hash. A handover that states one as a fact is wrong
|
|
61
|
+
# within the hour — measured twice in one session. The fix is not a fresher hash, it is to write
|
|
62
|
+
# the COMMAND that produces it, so the reader gets today's answer instead of yesterday's.
|
|
63
|
+
_HASH = re.compile(r"(?<![0-9a-zA-Z/])[0-9a-f]{7,40}(?![0-9a-zA-Z/])")
|
|
64
|
+
# "1,066 tests" / "821 passed". Same problem: true when written, false an hour later.
|
|
65
|
+
_COUNT = re.compile(r"\b\d[\d,]*\s+(?:tests?|passed|failed|skipped)\b", re.I)
|
|
66
|
+
|
|
67
|
+
# A fingerprint is a MEASUREMENT, explicitly stamped at a moment, and it exists precisely so a
|
|
68
|
+
# stale paste can be detected. That is the opposite of a fact stated as durable, so it is exempt.
|
|
69
|
+
#
|
|
70
|
+
# ANCHORED TO `SPAWN`, AND ONLY THAT. The first version also exempted any line starting with
|
|
71
|
+
# `head` — which exempts "HEAD is currently abc1234", the single most common way of writing the
|
|
72
|
+
# exact thing this check exists to catch. Its own test found it. An exemption is a hole in a check,
|
|
73
|
+
# so it gets the narrowest possible shape: the literal first token of `fingerprint_line()`.
|
|
74
|
+
_FINGERPRINT_LINE = re.compile(r"^\s*SPAWN\b", re.I)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# THE DISTINCTION THAT KEEPS THIS CHECK SWITCHED ON.
|
|
78
|
+
#
|
|
79
|
+
# A hash naming a PAST commit ("shipped in abc1234") is immutable and perfectly safe. A hash
|
|
80
|
+
# offered as CURRENT STATE ("HEAD is abc1234", "we are currently at abc1234") is wrong within the
|
|
81
|
+
# hour — that is the failure 007 recorded, twice. The first version of this flagged both and
|
|
82
|
+
# produced seven findings on one real handover, every one a historical citation. A check with that
|
|
83
|
+
# hit rate gets switched off before it ever catches the one that matters.
|
|
84
|
+
_CURRENT_STATE = re.compile(
|
|
85
|
+
r"\b(head|current|currently|latest|now at|tip of|as of now|at present|working tree)\b", re.I)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _is_exempt(line: str, in_fence: bool) -> bool:
|
|
89
|
+
"""Fenced blocks are commands and transcripts — quoting a hash there is the CURE, not the bug."""
|
|
90
|
+
return in_fence or bool(_FINGERPRINT_LINE.match(line))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class DocFindings:
|
|
95
|
+
"""`stale_prone` refuses; `historical` only advises.
|
|
96
|
+
|
|
97
|
+
Two tiers because the cost of each error differs. A current-state hash that goes stale sends
|
|
98
|
+
the successor to the wrong commit — block it. A historical citation is durable and merely worth
|
|
99
|
+
a second look — flagging it as a failure is how a check earns a reputation for crying wolf.
|
|
100
|
+
"""
|
|
101
|
+
stale_prone: list[tuple[int, str]] = field(default_factory=list)
|
|
102
|
+
historical: list[tuple[int, str]] = field(default_factory=list)
|
|
103
|
+
missing_paths: list[str] = field(default_factory=list)
|
|
104
|
+
|
|
105
|
+
def ok(self) -> bool:
|
|
106
|
+
return not self.stale_prone and not self.missing_paths
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
_DOC_PATH = re.compile(r"`([^`\s<>{}*?]+?\.(?:md|py|ts|tsx|json|ya?ml|sh|ps1|html|toml|sql))`")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def check_document(text: str, repo_root: Path | None = None) -> DocFindings:
|
|
113
|
+
"""Round 0: the parent validates its OWN handover before anyone reads it.
|
|
114
|
+
|
|
115
|
+
This is the cheapest round and the one issue 004 is really about. In the manual run a child
|
|
116
|
+
caught a stale commit hash the parent could have caught alone — a wasted relay trip, and the
|
|
117
|
+
relay trip is the expensive part of the whole exercise.
|
|
118
|
+
|
|
119
|
+
Fails closed: a claim that cannot survive this does not go in the handover.
|
|
120
|
+
"""
|
|
121
|
+
out = DocFindings()
|
|
122
|
+
in_fence = False
|
|
123
|
+
for n, line in enumerate(text.splitlines(), 1):
|
|
124
|
+
if line.lstrip().startswith("```"):
|
|
125
|
+
in_fence = not in_fence
|
|
126
|
+
continue
|
|
127
|
+
if _is_exempt(line, in_fence):
|
|
128
|
+
continue
|
|
129
|
+
for m in _HASH.finditer(line):
|
|
130
|
+
# A bare decimal number is not a hash; require at least one a-f digit so version
|
|
131
|
+
# strings and dates do not produce a check nobody can ever get to green.
|
|
132
|
+
if not any(c in "abcdef" for c in m.group(0)):
|
|
133
|
+
continue
|
|
134
|
+
if _CURRENT_STATE.search(line):
|
|
135
|
+
out.stale_prone.append((n, f"commit hash `{m.group(0)}` given as CURRENT state — "
|
|
136
|
+
"wrong within the hour, twice. Write the command "
|
|
137
|
+
"(`git rev-parse --short HEAD`) instead"))
|
|
138
|
+
else:
|
|
139
|
+
out.historical.append((n, f"commit hash `{m.group(0)}` — reads as a historical "
|
|
140
|
+
"citation, which is durable. Check that it is not "
|
|
141
|
+
"standing in for current state"))
|
|
142
|
+
for m in _COUNT.finditer(line):
|
|
143
|
+
out.stale_prone.append((n, f"`{m.group(0)}` stated as a fact — write the command "
|
|
144
|
+
"instead; this was wrong within the hour, twice"))
|
|
145
|
+
if repo_root is not None:
|
|
146
|
+
for raw in sorted(set(_DOC_PATH.findall(text))):
|
|
147
|
+
cand = next((tok for tok in raw.split() if "/" in tok or tok.endswith(".md")), raw)
|
|
148
|
+
cand = cand.strip("(),;:'\"")
|
|
149
|
+
if cand.startswith(("http://", "https://", "~", "$")) or ".." in cand:
|
|
150
|
+
continue
|
|
151
|
+
if not (repo_root / cand).exists():
|
|
152
|
+
out.missing_paths.append(cand)
|
|
153
|
+
return out
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# ── refusals ────────────────────────────────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
@dataclass
|
|
159
|
+
class Preflight:
|
|
160
|
+
refusals: list[str] = field(default_factory=list)
|
|
161
|
+
dirty: int = 0
|
|
162
|
+
parking_open: int | None = None
|
|
163
|
+
|
|
164
|
+
def ok(self) -> bool:
|
|
165
|
+
return not self.refusals
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def preflight(repo_root: Path) -> Preflight:
|
|
169
|
+
"""Conditions under which a spawn must not happen at all.
|
|
170
|
+
|
|
171
|
+
A dirty tree is the sharp one: the successor starts by pulling, so **uncommitted work is
|
|
172
|
+
invisible to it**. It will then either redo that work or build on a state that does not exist.
|
|
173
|
+
"""
|
|
174
|
+
p = Preflight()
|
|
175
|
+
status = _run(["git", "status", "--short"], repo_root)
|
|
176
|
+
p.dirty = len([l for l in status.splitlines() if l.strip()])
|
|
177
|
+
if p.dirty:
|
|
178
|
+
p.refusals.append(
|
|
179
|
+
f"{p.dirty} uncommitted file(s). The successor pulls, so uncommitted work is invisible "
|
|
180
|
+
"to it — it will redo the work or build on a state that does not exist. Commit first.")
|
|
181
|
+
|
|
182
|
+
d = repo_root / ART / "parking-lot"
|
|
183
|
+
if d.is_dir():
|
|
184
|
+
p.parking_open = len([f for f in d.glob("*.md") if f.name.lower() != "readme.md"])
|
|
185
|
+
else:
|
|
186
|
+
# The count is the INTEGRITY CHECK on the handover: a successor that revives 13 of 15 has
|
|
187
|
+
# silently dropped two, and nothing else would show it.
|
|
188
|
+
p.refusals.append(
|
|
189
|
+
f"no {ART}/parking-lot/ — the open-item count is the integrity check on a handover, "
|
|
190
|
+
"and without it a dropped item is undetectable.")
|
|
191
|
+
return p
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ── fingerprint ─────────────────────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
def fingerprint(repo_root: Path, with_tests: bool = False) -> dict:
|
|
197
|
+
"""A stamp the child echoes back, so the parent can spot a stale or mismatched paste.
|
|
198
|
+
|
|
199
|
+
NOT a fact for the handover body — a measurement, at a moment, for exactly one purpose.
|
|
200
|
+
"""
|
|
201
|
+
fp = {
|
|
202
|
+
"head": _run(["git", "rev-parse", "--short", "HEAD"], repo_root) or "unknown",
|
|
203
|
+
"branch": _run(["git", "rev-parse", "--abbrev-ref", "HEAD"], repo_root) or "unknown",
|
|
204
|
+
"parking_open": preflight(repo_root).parking_open,
|
|
205
|
+
"tests": "not measured",
|
|
206
|
+
}
|
|
207
|
+
if with_tests:
|
|
208
|
+
out = _run([sys.executable, "-m", "pytest", "-q", "--tb=no"], repo_root)
|
|
209
|
+
tail = [l for l in out.splitlines() if "passed" in l or "failed" in l]
|
|
210
|
+
fp["tests"] = tail[-1] if tail else "suite reported no summary"
|
|
211
|
+
return fp
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def fingerprint_line(fp: dict) -> str:
|
|
215
|
+
return (f"SPAWN | head {fp['head']} ({fp['branch']}) | parking-lot open {fp['parking_open']} "
|
|
216
|
+
f"| tests: {fp['tests']}")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── question answerability ──────────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
def verify_answerable(repo_root: Path, answer_in: str, needle: str) -> tuple[bool, str]:
|
|
222
|
+
"""A question must be answerable from a COMMITTED file, or it tests memory.
|
|
223
|
+
|
|
224
|
+
Testing memory is the thing being replaced, so a question that fails this is not a harder
|
|
225
|
+
question — it is the old failure wearing an exam's clothes.
|
|
226
|
+
"""
|
|
227
|
+
path = repo_root / answer_in
|
|
228
|
+
if not path.is_file():
|
|
229
|
+
return False, f"{answer_in} does not exist — the answer is not written down anywhere"
|
|
230
|
+
tracked = _run(["git", "ls-files", "--error-unmatch", answer_in], repo_root)
|
|
231
|
+
try:
|
|
232
|
+
text = path.read_text(encoding="utf-8", errors="ignore")
|
|
233
|
+
except OSError as exc:
|
|
234
|
+
return False, f"{answer_in} unreadable: {type(exc).__name__}"
|
|
235
|
+
if needle.lower() not in text.lower():
|
|
236
|
+
return False, f"{answer_in} does not contain {needle!r} — the answer is not in that file"
|
|
237
|
+
if not tracked:
|
|
238
|
+
return True, (f"{answer_in} contains the answer, but is NOT COMMITTED — the successor "
|
|
239
|
+
"pulls and will not see it")
|
|
240
|
+
return True, f"{answer_in} contains {needle!r} and is committed"
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def main(argv: list[str] | None = None) -> int:
|
|
244
|
+
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
245
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
246
|
+
for name in ("preflight", "fingerprint"):
|
|
247
|
+
s = sub.add_parser(name)
|
|
248
|
+
s.add_argument("--repo-root", default=".")
|
|
249
|
+
if name == "fingerprint":
|
|
250
|
+
s.add_argument("--with-tests", action="store_true")
|
|
251
|
+
c = sub.add_parser("check")
|
|
252
|
+
c.add_argument("--doc", required=True)
|
|
253
|
+
c.add_argument("--repo-root", default=".")
|
|
254
|
+
q = sub.add_parser("verify-question")
|
|
255
|
+
q.add_argument("--answer-in", required=True)
|
|
256
|
+
q.add_argument("--needle", required=True)
|
|
257
|
+
q.add_argument("--repo-root", default=".")
|
|
258
|
+
a = ap.parse_args(argv)
|
|
259
|
+
root = Path(a.repo_root).resolve()
|
|
260
|
+
|
|
261
|
+
if a.cmd == "preflight":
|
|
262
|
+
p = preflight(root)
|
|
263
|
+
if p.ok():
|
|
264
|
+
print(f"[spawn] ready — clean tree, {p.parking_open} open parking-lot item(s)")
|
|
265
|
+
return EXIT_OK
|
|
266
|
+
sys.stderr.write("[spawn] REFUSING to spawn:\n")
|
|
267
|
+
for r in p.refusals:
|
|
268
|
+
sys.stderr.write(f" {r}\n")
|
|
269
|
+
return EXIT_REFUSED
|
|
270
|
+
|
|
271
|
+
if a.cmd == "fingerprint":
|
|
272
|
+
print(fingerprint_line(fingerprint(root, with_tests=a.with_tests)))
|
|
273
|
+
return EXIT_OK
|
|
274
|
+
|
|
275
|
+
if a.cmd == "check":
|
|
276
|
+
doc = Path(a.doc)
|
|
277
|
+
if not doc.is_file():
|
|
278
|
+
sys.stderr.write(f"[spawn] no document at {doc}\n")
|
|
279
|
+
return EXIT_REFUSED
|
|
280
|
+
f = check_document(doc.read_text(encoding="utf-8", errors="ignore"), root)
|
|
281
|
+
for n, msg in f.historical:
|
|
282
|
+
print(f" note line {n}: {msg}")
|
|
283
|
+
if f.ok():
|
|
284
|
+
print(f"[spawn] {doc.name} passes round 0 — nothing stale-prone, every path exists")
|
|
285
|
+
return EXIT_OK
|
|
286
|
+
sys.stderr.write(f"[spawn] {doc.name} FAILS round 0:\n")
|
|
287
|
+
for n, msg in f.stale_prone:
|
|
288
|
+
sys.stderr.write(f" line {n}: {msg}\n")
|
|
289
|
+
for p_ in f.missing_paths:
|
|
290
|
+
sys.stderr.write(f" names a path that does not exist: {p_}\n")
|
|
291
|
+
sys.stderr.write(" A claim that cannot be re-derived does not go in the handover.\n")
|
|
292
|
+
return EXIT_REFUSED
|
|
293
|
+
|
|
294
|
+
ok, msg = verify_answerable(root, a.answer_in, a.needle)
|
|
295
|
+
(print if ok else sys.stderr.write)(f"[spawn] {msg}" + ("" if ok else "\n"))
|
|
296
|
+
return EXIT_OK if ok else EXIT_REFUSED
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
if __name__ == "__main__":
|
|
300
|
+
raise SystemExit(main())
|