@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,718 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Cross-vendor code review via any OpenAI-compatible chat-completions endpoint.
|
|
3
|
+
|
|
4
|
+
A different vendor's model has different blind spots, so a second model reviewing your diff
|
|
5
|
+
catches bugs a same-vendor reviewer misses. This tool is provider-agnostic: point it at
|
|
6
|
+
DeepSeek (default — cheapest + most architecturally distinct from Claude), GLM, OpenRouter,
|
|
7
|
+
or any OpenAI-compatible `/chat/completions` endpoint via three env vars.
|
|
8
|
+
|
|
9
|
+
Config precedence (highest wins): explicit CLI flag > env var > provider preset.
|
|
10
|
+
REVIEW_PROVIDER preset name: deepseek | glm | openrouter (default deepseek)
|
|
11
|
+
REVIEW_BASE_URL base URL, no trailing /chat/completions (overrides the preset's URL)
|
|
12
|
+
REVIEW_MODEL model id (overrides the preset's model)
|
|
13
|
+
REVIEW_API_KEY bearer token (REQUIRED — no key ⇒ exit 3)
|
|
14
|
+
|
|
15
|
+
Future-proofing: model ids and endpoints churn. Two layers protect against that — (1) the PROVIDERS
|
|
16
|
+
table below is the ONE place a renamed model/URL is edited; (2) REVIEW_MODEL / REVIEW_BASE_URL env
|
|
17
|
+
vars always win, so you can point at any new id without touching code. Nothing here is hard-wired.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python oss_review.py [--range <git range>] [--cwd <repo>] [--provider P] [--base-url U] [--model M]
|
|
21
|
+
--range e.g. origin/main..HEAD (default: the working tree — staged, unstaged AND
|
|
22
|
+
untracked non-ignored files; a range excludes untracked by design)
|
|
23
|
+
|
|
24
|
+
Exit codes (fail-closed):
|
|
25
|
+
0 REVIEW: CLEAN — no blocking issues
|
|
26
|
+
1 REVIEW: BLOCKING — one or more blocking issues
|
|
27
|
+
2 error — no diff verdict parsed, git failure, endpoint/parse failure, or the diff
|
|
28
|
+
exceeds REVIEW_MAX_DIFF_CHARS (see below — never silently downgraded to CLEAN)
|
|
29
|
+
3 misconfigured — REVIEW_API_KEY missing (actionable message on stderr)
|
|
30
|
+
|
|
31
|
+
Diff-size ceiling: an oversized diff sent to a chat-completions endpoint has been observed, live, to
|
|
32
|
+
come back two different unsafe ways — an empty `content` field on an HTTP 200 (at least fails closed:
|
|
33
|
+
no verdict line ⇒ exit 2), and, worse, a `REVIEW: CLEAN` verdict with no substantive engagement (a
|
|
34
|
+
silent false negative — the model was overwhelmed, not actually reviewing). Rather than risk the second
|
|
35
|
+
case, a diff over REVIEW_MAX_DIFF_CHARS (default 60,000 chars; override via the env var or
|
|
36
|
+
--max-diff-chars) is SPLIT INTO BATCHES on whole-file boundaries and each batch reviewed separately;
|
|
37
|
+
the verdict is CLEAN only if every batch is clean (aggregation is fail-closed). Exit 2 is now the
|
|
38
|
+
narrower case: a SINGLE file larger than the ceiling, which cannot be split, or more batches than
|
|
39
|
+
MAX_REVIEW_BATCHES. Refusal was the original behaviour and is kept for those two - a silent false
|
|
40
|
+
CLEAN is the worst outcome this tool has - but it no longer fires on an ordinary large phase diff.
|
|
41
|
+
|
|
42
|
+
Stdlib-only (urllib) so it runs anywhere with no pip install.
|
|
43
|
+
"""
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import argparse
|
|
47
|
+
import http.client
|
|
48
|
+
import json
|
|
49
|
+
import os
|
|
50
|
+
import subprocess
|
|
51
|
+
import sys
|
|
52
|
+
import urllib.error
|
|
53
|
+
import urllib.request
|
|
54
|
+
from pathlib import Path
|
|
55
|
+
|
|
56
|
+
EXIT_CLEAN = 0
|
|
57
|
+
EXIT_BLOCKING = 1
|
|
58
|
+
EXIT_ERROR = 2
|
|
59
|
+
EXIT_CONFIG = 3
|
|
60
|
+
|
|
61
|
+
# Provider presets — the SINGLE place a renamed model id or moved endpoint is edited. Adding a new
|
|
62
|
+
# provider (Qwen, a local vLLM, …) is one new row. Callers can always bypass this via env/flags.
|
|
63
|
+
PROVIDERS: dict[str, dict[str, str]] = {
|
|
64
|
+
"deepseek": {"base_url": "https://api.deepseek.com", "model": "deepseek-v4-flash"},
|
|
65
|
+
"glm": {"base_url": "https://api.z.ai/api/coding/paas/v4", "model": "glm-5.3"},
|
|
66
|
+
"openrouter": {"base_url": "https://openrouter.ai/api/v1", "model": "deepseek/deepseek-v4-flash"},
|
|
67
|
+
}
|
|
68
|
+
DEFAULT_PROVIDER = "deepseek"
|
|
69
|
+
|
|
70
|
+
# Tried, in order, ONLY when the primary provider fails at transport level (timeout, HTTP
|
|
71
|
+
# error, unparseable response) AND the user did not name a provider explicitly.
|
|
72
|
+
#
|
|
73
|
+
# Why a fallback rather than just picking one vendor: the whole point of this tool is that the
|
|
74
|
+
# reviewer is a DIFFERENT vendor from the author, so collapsing to a single provider means an
|
|
75
|
+
# outage leaves you with no second opinion and — worse — no signal that you have lost one.
|
|
76
|
+
# Observed on serve-sight across three consecutive phases: GLM timed out every time and the
|
|
77
|
+
# sessions recorded "cross-review inconclusive" and moved on. A review that silently does not
|
|
78
|
+
# happen is the same failure class as a diff that silently is not read.
|
|
79
|
+
#
|
|
80
|
+
# Order matters: DeepSeek leads because it has actually found real bugs (a date-boundary bug
|
|
81
|
+
# two same-vendor passes missed, an anchor bug, an orphaned-permission-rows bug). GLM is the
|
|
82
|
+
# spare tyre, not a co-equal — an alternation policy that sends half the phases to a provider
|
|
83
|
+
# that times out buys variety it never actually collects.
|
|
84
|
+
FALLBACK_PROVIDERS: tuple[str, ...] = ("glm",)
|
|
85
|
+
|
|
86
|
+
# Fail-closed diff-size ceiling (see the module docstring). Observed live: an oversized diff got back
|
|
87
|
+
# an empty `content` field (safe, but wasteful) on one occasion, and — separately, a bulk multi-document
|
|
88
|
+
# diff around 169,000 chars — a `REVIEW: CLEAN` verdict with zero substantive engagement (unsafe: a
|
|
89
|
+
# silent false negative). 60,000 chars is comfortably under both observed failure points while still
|
|
90
|
+
# covering an ordinary multi-file phase diff. REVIEW_MAX_DIFF_CHARS / --max-diff-chars override it.
|
|
91
|
+
DEFAULT_MAX_DIFF_CHARS = 60_000
|
|
92
|
+
|
|
93
|
+
# Every exception class that means "the request did not come back usable", so main() can route it
|
|
94
|
+
# to provider failover instead of letting it escape.
|
|
95
|
+
#
|
|
96
|
+
# `http.client.HTTPException` is the load-bearing entry and was MISSING. Its subclass
|
|
97
|
+
# `IncompleteRead` — a truncated response body, one of the two live failures behind
|
|
98
|
+
# `.caddis/parking-lot/006` — is NOT a ValueError despite the name suggesting a parse problem; its
|
|
99
|
+
# MRO is (IncompleteRead, HTTPException, Exception). So it escaped the handler entirely, Python
|
|
100
|
+
# exited 1, and 1 is EXIT_BLOCKING. A transport truncation was being reported to scripted callers
|
|
101
|
+
# as "the reviewer found blocking issues" — the same class of bug as the `content: null` crash
|
|
102
|
+
# documented in call_llm, in the same file, unfixed on the sibling path.
|
|
103
|
+
TRANSPORT_ERRORS = (
|
|
104
|
+
urllib.error.URLError, urllib.error.HTTPError, http.client.HTTPException,
|
|
105
|
+
RuntimeError, ValueError, TimeoutError, ConnectionError, OSError,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# How many times a failing chunk may be halved and retried before the run gives up. Two levels
|
|
109
|
+
# turns one 8-file batch into at most four 2-file requests, which is enough to clear a
|
|
110
|
+
# size-correlated truncation without turning a broken endpoint into sixteen doomed calls.
|
|
111
|
+
RETRY_SPLIT_DEPTH = 2
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# A diff over the ceiling is split into batches on whole-FILE boundaries and each batch is
|
|
115
|
+
# reviewed separately, rather than the whole thing being refused. Refusing was correct while
|
|
116
|
+
# there was no alternative — a silent false CLEAN is the worst outcome this tool has — but it
|
|
117
|
+
# put the burden on the author, and the workaround people reached for (`git stash` to split
|
|
118
|
+
# the tree) mutates the working tree mid-verification and cost a hand-resolved merge conflict
|
|
119
|
+
# on serve-sight, 2026-08-03.
|
|
120
|
+
#
|
|
121
|
+
# Batching scales in both directions: a small change is one batch, identical in cost and
|
|
122
|
+
# behaviour to before; a 175k-char phase is three or four, with no judgement call about where
|
|
123
|
+
# to cut. Verdicts aggregate fail-closed (see `aggregate_verdicts`).
|
|
124
|
+
MAX_REVIEW_BATCHES = 12
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ConfigError(Exception):
|
|
128
|
+
"""Raised when configuration can't be resolved (unknown provider, or missing API key)."""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
DEFAULT_KEYS_FILE = "~/.caddis/keys.env"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _parse_keys_file(path: str) -> dict[str, str]:
|
|
135
|
+
"""Parse an INI/``KEY=VALUE`` keys file; missing file yields ``{}``.
|
|
136
|
+
|
|
137
|
+
Mirror of claude-harness/scripts/oss_model.py::_parse_keys_file — duplicated (15 lines)
|
|
138
|
+
rather than imported so this tool stays a single stdlib-only file that runs from any repo
|
|
139
|
+
the runtime resources are exported into.
|
|
140
|
+
"""
|
|
141
|
+
p = Path(path).expanduser()
|
|
142
|
+
if not p.is_file():
|
|
143
|
+
return {}
|
|
144
|
+
out: dict[str, str] = {}
|
|
145
|
+
for raw in p.read_text(encoding="utf-8").splitlines():
|
|
146
|
+
line = raw.strip()
|
|
147
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
148
|
+
continue
|
|
149
|
+
key, val = line.split("=", 1)
|
|
150
|
+
out[key.strip()] = val.strip().strip('"').strip("'")
|
|
151
|
+
return out
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def resolve_config(
|
|
155
|
+
args: argparse.Namespace,
|
|
156
|
+
env: dict[str, str],
|
|
157
|
+
provider: str | None = None,
|
|
158
|
+
use_overrides: bool = True,
|
|
159
|
+
) -> tuple[str, str, str]:
|
|
160
|
+
"""(base_url, api_key, model). Precedence: explicit flag > env var > provider preset.
|
|
161
|
+
|
|
162
|
+
Never hard-fails on a model rename: a known --provider supplies sane defaults, and
|
|
163
|
+
REVIEW_MODEL / REVIEW_BASE_URL override them without any code change.
|
|
164
|
+
|
|
165
|
+
`provider` names one explicitly (used for fallback). `use_overrides=False` then ignores
|
|
166
|
+
--model/--base-url/REVIEW_MODEL/REVIEW_BASE_URL, because those were chosen for the PRIMARY
|
|
167
|
+
provider: pointing DeepSeek's model id at GLM's endpoint would turn a clean fallback into
|
|
168
|
+
a confusing 404.
|
|
169
|
+
"""
|
|
170
|
+
provider = (
|
|
171
|
+
provider or args.provider or env.get("REVIEW_PROVIDER") or DEFAULT_PROVIDER
|
|
172
|
+
).strip().lower()
|
|
173
|
+
preset = PROVIDERS.get(provider, {})
|
|
174
|
+
if use_overrides:
|
|
175
|
+
base_url = (args.base_url or env.get("REVIEW_BASE_URL") or preset.get("base_url") or "").rstrip("/")
|
|
176
|
+
model = args.model or env.get("REVIEW_MODEL") or preset.get("model") or ""
|
|
177
|
+
else:
|
|
178
|
+
base_url = (preset.get("base_url") or "").rstrip("/")
|
|
179
|
+
model = preset.get("model") or ""
|
|
180
|
+
if not base_url or not model:
|
|
181
|
+
known = ", ".join(sorted(PROVIDERS))
|
|
182
|
+
raise ConfigError(
|
|
183
|
+
f"could not resolve an endpoint for provider {provider!r}. Use a known --provider "
|
|
184
|
+
f"({known}) or set REVIEW_BASE_URL and REVIEW_MODEL explicitly."
|
|
185
|
+
)
|
|
186
|
+
# Key precedence: REVIEW_API_KEY > provider env (DEEPSEEK_API_KEY, …) > OSS_API_KEY >
|
|
187
|
+
# the caddis keys file — same resolution chain as the claude-oss launcher, so wiring
|
|
188
|
+
# a provider once (in ~/.caddis/keys.env) lights up both lanes.
|
|
189
|
+
key_env = f"{provider.upper()}_API_KEY"
|
|
190
|
+
api_key = ""
|
|
191
|
+
for name in ("REVIEW_API_KEY", key_env, "OSS_API_KEY"):
|
|
192
|
+
api_key = (env.get(name) or "").strip()
|
|
193
|
+
if api_key:
|
|
194
|
+
break
|
|
195
|
+
if not api_key:
|
|
196
|
+
keys_path = env.get("CADDIS_KEYS_FILE") or DEFAULT_KEYS_FILE
|
|
197
|
+
file_keys = _parse_keys_file(keys_path)
|
|
198
|
+
for name in (key_env, "OSS_API_KEY"):
|
|
199
|
+
api_key = (file_keys.get(name) or "").strip()
|
|
200
|
+
if api_key:
|
|
201
|
+
break
|
|
202
|
+
if not api_key:
|
|
203
|
+
# Name the providers that DO have a key. When DeepSeek is unset but GLM is configured, the
|
|
204
|
+
# fix is `--provider glm` — and without this line the reader goes hunting for a credential
|
|
205
|
+
# they already hold, at the least convenient possible moment (a commit pending).
|
|
206
|
+
keys_path = env.get("CADDIS_KEYS_FILE") or DEFAULT_KEYS_FILE
|
|
207
|
+
others = [p for p in configured_providers(env) if p != provider]
|
|
208
|
+
have = (f"\nProviders that DO have a key here: {', '.join(others)}. "
|
|
209
|
+
f"To use one now: --provider {others[0]}." if others else "")
|
|
210
|
+
raise ConfigError(
|
|
211
|
+
f"no API key for provider {provider!r}. Set $REVIEW_API_KEY or ${key_env}, or add\n"
|
|
212
|
+
f" {key_env}=<your-key>\n"
|
|
213
|
+
f"to your keys file ({keys_path}). Override the file path with $CADDIS_KEYS_FILE."
|
|
214
|
+
+ have
|
|
215
|
+
)
|
|
216
|
+
return base_url, api_key, model
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def configured_providers(env: dict[str, str]) -> list[str]:
|
|
220
|
+
"""Every preset provider that currently has a usable key. Never returns or logs a key value.
|
|
221
|
+
|
|
222
|
+
Mirror of claude-harness/scripts/oss_model.py::configured_providers — duplicated for the same
|
|
223
|
+
documented reason as `_parse_keys_file` above: this tool stays a single stdlib-only file that
|
|
224
|
+
runs from any repo the runtime resources are exported into.
|
|
225
|
+
|
|
226
|
+
Exists so a caller can find out BEFORE it needs a key. `/caddis:cross-review` used to discover
|
|
227
|
+
a missing one mid-task with a commit pending, and a command that fails once at an inconvenient
|
|
228
|
+
moment does not get retried — so the safety check it provides is quietly lost, and nothing
|
|
229
|
+
records that it was lost (`.caddis/parking-lot/003`).
|
|
230
|
+
"""
|
|
231
|
+
keys_path = env.get("CADDIS_KEYS_FILE") or DEFAULT_KEYS_FILE
|
|
232
|
+
file_keys = _parse_keys_file(keys_path)
|
|
233
|
+
generic = ((env.get("REVIEW_API_KEY") or env.get("OSS_API_KEY") or "").strip()
|
|
234
|
+
or (file_keys.get("OSS_API_KEY") or "").strip())
|
|
235
|
+
out = []
|
|
236
|
+
for name in sorted(PROVIDERS):
|
|
237
|
+
key_env = f"{name.upper()}_API_KEY"
|
|
238
|
+
if generic or (env.get(key_env) or "").strip() or (file_keys.get(key_env) or "").strip():
|
|
239
|
+
out.append(name)
|
|
240
|
+
return out
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _untracked_files(cwd: str) -> list[str]:
|
|
244
|
+
"""New, non-ignored files git is not tracking yet. Empty list on any git failure."""
|
|
245
|
+
out = subprocess.run(
|
|
246
|
+
["git", "ls-files", "--others", "--exclude-standard", "-z"],
|
|
247
|
+
cwd=cwd, capture_output=True, text=True,
|
|
248
|
+
encoding="utf-8", errors="replace", timeout=30,
|
|
249
|
+
)
|
|
250
|
+
if out.returncode != 0:
|
|
251
|
+
return []
|
|
252
|
+
return [p for p in out.stdout.split("\0") if p]
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _untracked_diff(path: str, cwd: str) -> str:
|
|
256
|
+
"""A synthetic add-hunk for one untracked file, WITHOUT touching the index.
|
|
257
|
+
|
|
258
|
+
`git add -N` would also work and is shorter, but it WRITES to the index — unacceptable
|
|
259
|
+
in a review tool that routinely runs mid-session against a tree the user is still
|
|
260
|
+
working in. `--no-index` gets the same diff with no side effect.
|
|
261
|
+
|
|
262
|
+
Note the exit code: `git diff --no-index` returns 1 when the files differ, which is the
|
|
263
|
+
normal outcome here. Only >1 is a real failure.
|
|
264
|
+
"""
|
|
265
|
+
null = "/dev/null" if os.name != "nt" else "NUL"
|
|
266
|
+
out = subprocess.run(
|
|
267
|
+
["git", "diff", "--no-index", "--", null, path],
|
|
268
|
+
cwd=cwd, capture_output=True, text=True,
|
|
269
|
+
encoding="utf-8", errors="replace", timeout=30,
|
|
270
|
+
)
|
|
271
|
+
return out.stdout if out.returncode <= 1 else ""
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def get_diff(rng: str | None, cwd: str, include_untracked: bool = True) -> str:
|
|
275
|
+
"""The unified diff for `rng` (e.g. 'origin/main..HEAD'), or the working tree when None.
|
|
276
|
+
|
|
277
|
+
Working tree = staged + unstaged against HEAD, PLUS untracked non-ignored files.
|
|
278
|
+
|
|
279
|
+
Untracked files are included because `git diff HEAD` reports only paths git already
|
|
280
|
+
tracks, so a change consisting entirely of NEW files produced an empty diff — and the
|
|
281
|
+
empty-diff branch in main() prints `REVIEW: CLEAN` and exits 0 without ever calling the
|
|
282
|
+
model. A brand-new module is exactly the kind of change most worth reviewing, and it was
|
|
283
|
+
the kind that silently got no review at all. Found on serve-sight 2026-08-02: four
|
|
284
|
+
consecutive cross-review runs over a phase of mostly-new files reported CLEAN, and the
|
|
285
|
+
real findings only appeared once the author happened to run `git add -A` first.
|
|
286
|
+
|
|
287
|
+
Untracked files are NOT included for an explicit `rng` — a commit range is a span of
|
|
288
|
+
history, and files that were never committed are correctly outside it.
|
|
289
|
+
"""
|
|
290
|
+
cmd = ["git", "diff", rng] if rng else ["git", "diff", "HEAD"]
|
|
291
|
+
# encoding pinned to UTF-8: git emits UTF-8, but text=True would decode with the locale
|
|
292
|
+
# codepage (cp1252 on Windows) — a non-ASCII diff then kills the reader thread and
|
|
293
|
+
# subprocess hands back stdout=None with returncode 0.
|
|
294
|
+
out = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True,
|
|
295
|
+
encoding="utf-8", errors="replace", timeout=30)
|
|
296
|
+
if out.returncode != 0:
|
|
297
|
+
raise RuntimeError(f"git diff failed: {out.stderr.strip()}")
|
|
298
|
+
if rng or not include_untracked:
|
|
299
|
+
return out.stdout
|
|
300
|
+
extra = "".join(_untracked_diff(p, cwd) for p in _untracked_files(cwd))
|
|
301
|
+
# Concatenated BEFORE returning so the caller's REVIEW_MAX_DIFF_CHARS ceiling measures
|
|
302
|
+
# the true payload rather than the tracked-only fraction of it.
|
|
303
|
+
return out.stdout + extra
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def split_diff_by_file(diff_text: str) -> list[str]:
|
|
307
|
+
"""The diff cut into per-file chunks on `diff --git` boundaries.
|
|
308
|
+
|
|
309
|
+
Splitting on file boundaries, never mid-file: half a file's hunks is worse than no
|
|
310
|
+
review of it, because the model confidently reasons about code it cannot see.
|
|
311
|
+
"""
|
|
312
|
+
if not diff_text.strip():
|
|
313
|
+
return []
|
|
314
|
+
parts: list[str] = []
|
|
315
|
+
current: list[str] = []
|
|
316
|
+
for line in diff_text.splitlines(keepends=True):
|
|
317
|
+
if line.startswith("diff --git ") and current:
|
|
318
|
+
parts.append("".join(current))
|
|
319
|
+
current = [line]
|
|
320
|
+
else:
|
|
321
|
+
current.append(line)
|
|
322
|
+
if current:
|
|
323
|
+
parts.append("".join(current))
|
|
324
|
+
return parts
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def batch_diff(diff_text: str, max_chars: int) -> list[str]:
|
|
328
|
+
"""Pack whole files into as few batches as possible, each under `max_chars`.
|
|
329
|
+
|
|
330
|
+
A single file larger than the ceiling gets its own batch and is sent anyway — it is
|
|
331
|
+
over the limit either way, and reviewing it is strictly better than skipping it. The
|
|
332
|
+
caller reports how many batches were used so an oversized single file is visible.
|
|
333
|
+
"""
|
|
334
|
+
files = split_diff_by_file(diff_text)
|
|
335
|
+
if not files:
|
|
336
|
+
return []
|
|
337
|
+
batches: list[str] = []
|
|
338
|
+
current = ""
|
|
339
|
+
for chunk in files:
|
|
340
|
+
if current and len(current) + len(chunk) > max_chars:
|
|
341
|
+
batches.append(current)
|
|
342
|
+
current = chunk
|
|
343
|
+
else:
|
|
344
|
+
current += chunk
|
|
345
|
+
if current:
|
|
346
|
+
batches.append(current)
|
|
347
|
+
return batches
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def aggregate_verdicts(verdicts: list[bool | None]) -> bool | None:
|
|
351
|
+
"""Fail-closed roll-up: any BLOCKING wins; CLEAN only if EVERY batch was clean.
|
|
352
|
+
|
|
353
|
+
A batch with no parseable verdict (`None`) poisons the result to `None`, which the
|
|
354
|
+
caller maps to EXIT_ERROR — the same fail-closed rule as the single-batch path. An
|
|
355
|
+
unreviewed batch must never be able to produce a CLEAN overall.
|
|
356
|
+
"""
|
|
357
|
+
if not verdicts:
|
|
358
|
+
return None
|
|
359
|
+
if any(v is False for v in verdicts):
|
|
360
|
+
return False
|
|
361
|
+
if any(v is None for v in verdicts):
|
|
362
|
+
return None
|
|
363
|
+
return True
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def current_branch(cwd: str) -> str:
|
|
367
|
+
try:
|
|
368
|
+
out = subprocess.run(
|
|
369
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
|
370
|
+
cwd=cwd, capture_output=True, text=True,
|
|
371
|
+
encoding="utf-8", errors="replace", timeout=10,
|
|
372
|
+
)
|
|
373
|
+
return out.stdout.strip() or "the current branch"
|
|
374
|
+
except Exception:
|
|
375
|
+
return "the current branch"
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def build_review_prompt(diff_text: str, branch: str, rng: str | None) -> str:
|
|
379
|
+
"""Adversarial review prompt — ported from docket runner._review_prompt (kept in sync).
|
|
380
|
+
|
|
381
|
+
Self-contained: the criteria are inline so a real review always happens, and it ends with
|
|
382
|
+
a single machine-parseable verdict line the caller maps to an exit code.
|
|
383
|
+
"""
|
|
384
|
+
scope = rng or "the working tree (staged, unstaged and new untracked files)"
|
|
385
|
+
return (
|
|
386
|
+
f"Perform an adversarial code review of the changes on branch '{branch}' ({scope}). "
|
|
387
|
+
"The unified diff is provided below. Judge, in priority order: (1) correctness — logic "
|
|
388
|
+
"bugs, wrong results, missed edge cases; (2) tests — would a test fail without this "
|
|
389
|
+
"change, and are the stated behaviors covered; (3) security — injection, auth, secret "
|
|
390
|
+
"exposure, unvalidated input; (4) conventions — the repo's stated rules; (5) simplicity. "
|
|
391
|
+
"Classify each issue as blocking (must fix before merge), should-fix, or nit. Be "
|
|
392
|
+
"specific: cite the file and line. Do not invent issues; if the diff is clean, say so.\n\n"
|
|
393
|
+
"End with EXACTLY one line and nothing after it: `REVIEW: CLEAN` (no blocking issues) or "
|
|
394
|
+
"`REVIEW: BLOCKING` (one or more blocking issues).\n\n"
|
|
395
|
+
"----- BEGIN DIFF -----\n"
|
|
396
|
+
f"{diff_text}\n"
|
|
397
|
+
"----- END DIFF -----"
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def classify_verdict(text: str) -> bool | None:
|
|
402
|
+
"""CLEAN→True, BLOCKING→False, neither→None. Fail-closed: any blocking signal wins."""
|
|
403
|
+
u = text.upper()
|
|
404
|
+
if "REVIEW: BLOCKING" in u:
|
|
405
|
+
return False
|
|
406
|
+
if "REVIEW: CLEAN" in u:
|
|
407
|
+
return True
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def call_llm(base_url: str, api_key: str, model: str, prompt: str, timeout: int = 180) -> str:
|
|
412
|
+
"""POST to {base_url}/chat/completions (OpenAI-compatible) → the assistant message text.
|
|
413
|
+
|
|
414
|
+
Raises on transport, HTTP, or response-shape failure so main() maps it to EXIT_ERROR.
|
|
415
|
+
"""
|
|
416
|
+
payload = json.dumps({
|
|
417
|
+
"model": model,
|
|
418
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
419
|
+
"temperature": 0,
|
|
420
|
+
"stream": False,
|
|
421
|
+
}).encode("utf-8")
|
|
422
|
+
req = urllib.request.Request(
|
|
423
|
+
f"{base_url}/chat/completions",
|
|
424
|
+
data=payload,
|
|
425
|
+
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
|
|
426
|
+
method="POST",
|
|
427
|
+
)
|
|
428
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
429
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
430
|
+
try:
|
|
431
|
+
content = data["choices"][0]["message"]["content"]
|
|
432
|
+
except (KeyError, IndexError, TypeError) as exc:
|
|
433
|
+
raise RuntimeError(f"unexpected response shape from endpoint: {exc}") from exc
|
|
434
|
+
# An HTTP 200 carrying empty or null content is a REAL observed failure (see the module
|
|
435
|
+
# docstring: an oversized diff came back with an empty `content` field). Returning it
|
|
436
|
+
# unraised had two bad consequences, both silent:
|
|
437
|
+
# - it never reached the `except` in main(), so the provider FAILOVER never fired — the
|
|
438
|
+
# run died having never tried the spare, which is the exact case failover exists for;
|
|
439
|
+
# - `content: null` (legal in OpenAI-compatible responses) returned None, and
|
|
440
|
+
# `classify_verdict(None)` then raised AttributeError. Uncaught, Python exits 1 —
|
|
441
|
+
# which is EXIT_BLOCKING. A crash was being reported to scripted callers as "the
|
|
442
|
+
# reviewer found blocking issues."
|
|
443
|
+
# Raising here routes both into the existing retry/failover path and kills the crash.
|
|
444
|
+
if not isinstance(content, str) or not content.strip():
|
|
445
|
+
raise RuntimeError(
|
|
446
|
+
"endpoint returned HTTP 200 with empty or absent message content "
|
|
447
|
+
f"(type={type(content).__name__}) — treating as a provider failure"
|
|
448
|
+
)
|
|
449
|
+
return content
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _force_utf8_stdio() -> None:
|
|
453
|
+
"""Make stdout/stderr survive a review written by a model, on any console.
|
|
454
|
+
|
|
455
|
+
A review is prose, and prose from a model contains →, —, ’, … or ≥ sooner or later. A
|
|
456
|
+
Windows console defaults to cp1252, where `print(review)` raises UnicodeEncodeError —
|
|
457
|
+
AFTER the provider round-trip has already been paid for (~8 minutes, observed) and, because
|
|
458
|
+
the print sits inside the per-batch loop, killing every remaining batch with it. Worse, the
|
|
459
|
+
caller sees a traceback and a non-zero exit, which is indistinguishable from a provider
|
|
460
|
+
timeout: the documented response is "switch vendor and retry", so the next move burns
|
|
461
|
+
another full review on the other provider and hits the identical crash, because the cause is
|
|
462
|
+
local stdout encoding, not the provider.
|
|
463
|
+
|
|
464
|
+
`errors="replace"` is the load-bearing part: a mangled arrow is a far better outcome than a
|
|
465
|
+
discarded review. Guarded because stdout may already have been swapped for something without
|
|
466
|
+
`.reconfigure` (pytest's capture, a pipe wrapper) — in which case there is nothing to fix.
|
|
467
|
+
"""
|
|
468
|
+
for stream in (sys.stdout, sys.stderr):
|
|
469
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
470
|
+
if reconfigure is None:
|
|
471
|
+
continue
|
|
472
|
+
try:
|
|
473
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
474
|
+
except Exception: # pragma: no cover — defensive; never lose a review to this
|
|
475
|
+
pass
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def main(argv: list[str] | None = None, env: dict[str, str] | None = None) -> int:
|
|
479
|
+
_force_utf8_stdio()
|
|
480
|
+
env = os.environ if env is None else env
|
|
481
|
+
parser = argparse.ArgumentParser(description="Cross-vendor code review via an OpenAI-compatible endpoint.")
|
|
482
|
+
parser.add_argument("--range", dest="range", default=None,
|
|
483
|
+
help="git range to review (e.g. origin/main..HEAD); default = working tree")
|
|
484
|
+
parser.add_argument("--cwd", default=".", help="repo directory (default: cwd)")
|
|
485
|
+
parser.add_argument("--provider", default=None,
|
|
486
|
+
help=f"preset: {', '.join(sorted(PROVIDERS))} (default {DEFAULT_PROVIDER}); env REVIEW_PROVIDER")
|
|
487
|
+
parser.add_argument("--base-url", dest="base_url", default=None, help="override REVIEW_BASE_URL / the preset")
|
|
488
|
+
parser.add_argument("--model", default=None, help="override REVIEW_MODEL / the preset")
|
|
489
|
+
parser.add_argument("--max-diff-chars", type=int, default=None,
|
|
490
|
+
help=f"override REVIEW_MAX_DIFF_CHARS (default {DEFAULT_MAX_DIFF_CHARS})")
|
|
491
|
+
parser.add_argument("--check-config", action="store_true",
|
|
492
|
+
help="report whether a usable key exists and exit — no diff, no LLM call. "
|
|
493
|
+
"Exit 0 = ready, 3 = nothing configured.")
|
|
494
|
+
args = parser.parse_args(argv)
|
|
495
|
+
|
|
496
|
+
# --check-config is deliberately NON-INTERACTIVE. This script runs headless — from CI, from a
|
|
497
|
+
# docket runner, from a `claude -p` session — where a prompt on stdin does not ask a question,
|
|
498
|
+
# it hangs forever. The ASKING belongs in `/caddis:cross-review`, which is driven by an agent
|
|
499
|
+
# that can actually talk to a human. This half just answers "is it configured?" cheaply enough
|
|
500
|
+
# to call before you need it, instead of finding out mid-commit.
|
|
501
|
+
if args.check_config:
|
|
502
|
+
available = configured_providers(env)
|
|
503
|
+
if not available:
|
|
504
|
+
sys.stderr.write(
|
|
505
|
+
"no second-vendor API key is configured anywhere.\n"
|
|
506
|
+
" Cross-review needs one — the whole point is a reviewer that does not share this\n"
|
|
507
|
+
" model's blind spots, so a same-vendor fallback would be no review at all.\n"
|
|
508
|
+
f" Add a key to {env.get('CADDIS_KEYS_FILE') or DEFAULT_KEYS_FILE}, e.g.\n"
|
|
509
|
+
" DEEPSEEK_API_KEY=<your-key>\n")
|
|
510
|
+
return EXIT_CONFIG
|
|
511
|
+
print(f"cross-review is ready. Providers with a key: {', '.join(available)}")
|
|
512
|
+
return EXIT_CLEAN
|
|
513
|
+
|
|
514
|
+
try:
|
|
515
|
+
base_url, api_key, model = resolve_config(args, env)
|
|
516
|
+
primary_provider = (
|
|
517
|
+
args.provider or env.get("REVIEW_PROVIDER") or DEFAULT_PROVIDER
|
|
518
|
+
).strip().lower()
|
|
519
|
+
except ConfigError as exc:
|
|
520
|
+
sys.stderr.write(f"{exc}\n")
|
|
521
|
+
return EXIT_CONFIG
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
diff_text = get_diff(args.range, args.cwd)
|
|
525
|
+
except Exception as exc:
|
|
526
|
+
sys.stderr.write(f"could not read diff: {exc}\n")
|
|
527
|
+
return EXIT_ERROR
|
|
528
|
+
|
|
529
|
+
if not diff_text.strip():
|
|
530
|
+
# Says what was scanned, not just that nothing was found. This branch used to fire
|
|
531
|
+
# on a tree full of NEW files (they are invisible to `git diff HEAD`) and report
|
|
532
|
+
# CLEAN without calling the model — a silent false negative that reads identically
|
|
533
|
+
# to a real pass. Untracked files are in scope now; naming the scope keeps the
|
|
534
|
+
# message honest if that ever regresses.
|
|
535
|
+
print(f"No changes to review in {args.range or 'the working tree (incl. untracked)'}.")
|
|
536
|
+
print("REVIEW: CLEAN")
|
|
537
|
+
return EXIT_CLEAN
|
|
538
|
+
|
|
539
|
+
max_diff_chars = args.max_diff_chars
|
|
540
|
+
if max_diff_chars is None:
|
|
541
|
+
env_val = (env.get("REVIEW_MAX_DIFF_CHARS") or "").strip()
|
|
542
|
+
max_diff_chars = int(env_val) if env_val.isdigit() else DEFAULT_MAX_DIFF_CHARS
|
|
543
|
+
batches = batch_diff(diff_text, max_diff_chars) or [diff_text]
|
|
544
|
+
# Batching only helps when the diff HAS file boundaries to cut on. A single file (or a
|
|
545
|
+
# blob with no `diff --git` headers) larger than the ceiling cannot be split, and sending
|
|
546
|
+
# it anyway would reintroduce exactly the silent false-CLEAN the ceiling exists to stop.
|
|
547
|
+
# Refuse, as before — the guarantee is absolute: no prompt over the ceiling is ever sent.
|
|
548
|
+
if any(len(b) > max_diff_chars for b in batches):
|
|
549
|
+
biggest = max(batches, key=len)
|
|
550
|
+
first_line = biggest.splitlines()[0][:120] if biggest.strip() else "(no file header)"
|
|
551
|
+
sys.stderr.write(
|
|
552
|
+
f"diff is {len(diff_text):,} chars and contains a single chunk of {len(biggest):,}, "
|
|
553
|
+
f"over the {max_diff_chars:,}-char review ceiling — refusing rather than risk an "
|
|
554
|
+
"unreviewable prompt silently coming back CLEAN.\n"
|
|
555
|
+
f" Largest chunk starts: {first_line}\n"
|
|
556
|
+
+ (
|
|
557
|
+
# The working tree includes untracked files by design (that fix stopped a
|
|
558
|
+
# false-CLEAN on all-new-file phases), which means an agent's own uncommitted
|
|
559
|
+
# plans/PRDs are counted as "the diff". Observed live: 77k of untracked
|
|
560
|
+
# .caddis/ markdown pushed a real phase over the ceiling. Reviewing the phase's
|
|
561
|
+
# commit range is both the workaround AND the more accurate scope, so name it
|
|
562
|
+
# as a literal command instead of leaving each caller to rediscover it.
|
|
563
|
+
" You are reviewing the WORKING TREE, which includes untracked files — "
|
|
564
|
+
"uncommitted docs/plans count toward this total.\n"
|
|
565
|
+
" If the phase is already committed, review its commits instead (usually the "
|
|
566
|
+
"scope you actually want):\n"
|
|
567
|
+
" --range HEAD~1..HEAD\n"
|
|
568
|
+
if args.range is None
|
|
569
|
+
else " Narrow --range to a smaller commit span, or review file-by-file.\n"
|
|
570
|
+
)
|
|
571
|
+
+ " Override (not recommended without knowing your endpoint's real limit): "
|
|
572
|
+
"--max-diff-chars <n> or $REVIEW_MAX_DIFF_CHARS.\n"
|
|
573
|
+
)
|
|
574
|
+
return EXIT_ERROR
|
|
575
|
+
if len(batches) > MAX_REVIEW_BATCHES:
|
|
576
|
+
# Loud, never a silent truncation: a capped review that reads like a complete one is
|
|
577
|
+
# the same class of lie as an oversized prompt coming back CLEAN.
|
|
578
|
+
sys.stderr.write(
|
|
579
|
+
f"diff is {len(diff_text):,} chars — {len(batches)} batches, over the "
|
|
580
|
+
f"{MAX_REVIEW_BATCHES}-batch cap. Refusing rather than reviewing part of it and "
|
|
581
|
+
"reporting a verdict that looks whole.\n"
|
|
582
|
+
" Narrow --range to a smaller commit span, or raise --max-diff-chars if you know "
|
|
583
|
+
"your endpoint's real limit.\n"
|
|
584
|
+
)
|
|
585
|
+
return EXIT_ERROR
|
|
586
|
+
if len(batches) > 1:
|
|
587
|
+
print(
|
|
588
|
+
f"[cross-review] diff is {len(diff_text):,} chars, over the {max_diff_chars:,} "
|
|
589
|
+
f"ceiling — reviewing in {len(batches)} batches split on file boundaries. "
|
|
590
|
+
"Verdict is CLEAN only if every batch is clean."
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
branch = current_branch(args.cwd)
|
|
594
|
+
|
|
595
|
+
# Fall back to another vendor only when the user did NOT name one. If they asked for a
|
|
596
|
+
# specific provider, silently reviewing with a different one would misreport who checked
|
|
597
|
+
# the code — the single fact this tool exists to establish.
|
|
598
|
+
# "Explicit" must include a custom ENDPOINT, not just a named preset. Someone who set
|
|
599
|
+
# REVIEW_BASE_URL/REVIEW_MODEL (the documented future-proofing path) has named exactly who
|
|
600
|
+
# should review their code; falling back to the GLM preset would misreport the reviewer —
|
|
601
|
+
# the one fact this tool exists to establish.
|
|
602
|
+
explicit = bool(
|
|
603
|
+
args.provider or env.get("REVIEW_PROVIDER")
|
|
604
|
+
or args.base_url or env.get("REVIEW_BASE_URL")
|
|
605
|
+
or args.model or env.get("REVIEW_MODEL")
|
|
606
|
+
)
|
|
607
|
+
chain: list[tuple[str, str, str, str]] = [(primary_provider, base_url, api_key, model)]
|
|
608
|
+
if not explicit:
|
|
609
|
+
for name in FALLBACK_PROVIDERS:
|
|
610
|
+
if name == primary_provider:
|
|
611
|
+
continue
|
|
612
|
+
try:
|
|
613
|
+
fb_url, fb_key, fb_model = resolve_config(args, env, provider=name, use_overrides=False)
|
|
614
|
+
except ConfigError:
|
|
615
|
+
continue # no key for the spare — not an error, just nothing to fall back to
|
|
616
|
+
chain.append((name, fb_url, fb_key, fb_model))
|
|
617
|
+
|
|
618
|
+
def _try_chain(prompt: str, label: str) -> str | None:
|
|
619
|
+
"""One payload against every provider in the chain. None when all of them failed."""
|
|
620
|
+
for idx, (name, url, key, mdl) in enumerate(chain):
|
|
621
|
+
try:
|
|
622
|
+
review = call_llm(url, key, mdl, prompt)
|
|
623
|
+
except TRANSPORT_ERRORS as exc:
|
|
624
|
+
remaining = len(chain) - idx - 1
|
|
625
|
+
sys.stderr.write(
|
|
626
|
+
f"review request failed{label} ({mdl} @ {url}): {exc}\n"
|
|
627
|
+
+ (
|
|
628
|
+
f" falling back to {chain[idx + 1][0]}...\n"
|
|
629
|
+
if remaining
|
|
630
|
+
else " If the model id was renamed, set REVIEW_MODEL to the current id "
|
|
631
|
+
"(env overrides the preset).\n"
|
|
632
|
+
)
|
|
633
|
+
)
|
|
634
|
+
continue
|
|
635
|
+
if idx:
|
|
636
|
+
# Loud on purpose. A verdict from a different vendor than the one requested is
|
|
637
|
+
# a material fact about the review, not an implementation detail — it belongs
|
|
638
|
+
# in the transcript the phase report quotes.
|
|
639
|
+
print(f"[cross-review] {chain[0][0]} unavailable; reviewed by {name} ({mdl}).")
|
|
640
|
+
return review
|
|
641
|
+
return None
|
|
642
|
+
|
|
643
|
+
def _review_chunk(chunk: str, label: str, depth: int = 0) -> list[str] | None:
|
|
644
|
+
"""Review `chunk`, halving it and retrying once per level if every provider failed.
|
|
645
|
+
|
|
646
|
+
WHY HALVE RATHER THAN JUST FAIL. Both observed failures were transport truncation on a
|
|
647
|
+
long reply — an `http.client.IncompleteRead` on one provider and an empty HTTP 200 body on
|
|
648
|
+
another — and both were SIZE-CORRELATED. Retrying the same payload against the same
|
|
649
|
+
endpoint reproduces the same overflow; retrying a smaller one usually does not. The whole
|
|
650
|
+
run used to abort here, which is safe but throws away a review the user has already
|
|
651
|
+
waited minutes for.
|
|
652
|
+
|
|
653
|
+
Bounded at RETRY_SPLIT_DEPTH levels and never splits a single file, so it cannot loop and
|
|
654
|
+
cannot silently shrink a diff to nothing. Returns None when a chunk that cannot be split
|
|
655
|
+
any further still fails — the abort path stays reachable.
|
|
656
|
+
"""
|
|
657
|
+
review = _try_chain(build_review_prompt(chunk, branch, args.range), label)
|
|
658
|
+
if review is not None:
|
|
659
|
+
return [review]
|
|
660
|
+
files = split_diff_by_file(chunk)
|
|
661
|
+
if depth >= RETRY_SPLIT_DEPTH or len(files) < 2:
|
|
662
|
+
return None
|
|
663
|
+
half = len(files) // 2
|
|
664
|
+
sys.stderr.write(
|
|
665
|
+
f"[cross-review] retrying{label} as 2 smaller chunks ({half} + {len(files) - half} "
|
|
666
|
+
"file(s)) — both observed failures were size-correlated truncation.\n")
|
|
667
|
+
out: list[str] = []
|
|
668
|
+
for part_no, part in enumerate(("".join(files[:half]), "".join(files[half:])), 1):
|
|
669
|
+
got = _review_chunk(part, f"{label} part {part_no}", depth + 1)
|
|
670
|
+
if got is None:
|
|
671
|
+
return None
|
|
672
|
+
out += got
|
|
673
|
+
return out
|
|
674
|
+
|
|
675
|
+
verdicts: list[bool | None] = []
|
|
676
|
+
for batch_no, batch in enumerate(batches, 1):
|
|
677
|
+
label = f" (batch {batch_no}/{len(batches)})" if len(batches) > 1 else ""
|
|
678
|
+
if len(batches) > 1:
|
|
679
|
+
files = len(split_diff_by_file(batch))
|
|
680
|
+
print(f"\n===== batch {batch_no}/{len(batches)} — {files} file(s), "
|
|
681
|
+
f"{len(batch):,} chars =====")
|
|
682
|
+
|
|
683
|
+
reviews = _review_chunk(batch, label)
|
|
684
|
+
if reviews is None:
|
|
685
|
+
# Every provider failed, and splitting did not rescue it. Do NOT continue to the next
|
|
686
|
+
# batch: a partial sweep that still prints a verdict is the failure mode this whole
|
|
687
|
+
# tool guards against.
|
|
688
|
+
sys.stderr.write(f"batch {batch_no}/{len(batches)} could not be reviewed — aborting.\n")
|
|
689
|
+
return EXIT_ERROR
|
|
690
|
+
|
|
691
|
+
for review in reviews:
|
|
692
|
+
print(review)
|
|
693
|
+
verdicts.append(classify_verdict(review))
|
|
694
|
+
|
|
695
|
+
verdict = aggregate_verdicts(verdicts)
|
|
696
|
+
if len(batches) > 1:
|
|
697
|
+
# A LEDGER, not a summary. The live report behind `.caddis/parking-lot/006` said the
|
|
698
|
+
# reader's only defence was "count the REVIEW: lines against the announced batch count" —
|
|
699
|
+
# so print that arithmetic instead of making a human do it. `reviewed` can exceed
|
|
700
|
+
# `announced` when a batch was split and retried, and that is worth seeing.
|
|
701
|
+
clean = sum(1 for v in verdicts if v is True)
|
|
702
|
+
blocking = sum(1 for v in verdicts if v is False)
|
|
703
|
+
unparsed = sum(1 for v in verdicts if v is None)
|
|
704
|
+
print(f"\n[cross-review] announced {len(batches)} batches · reviewed {len(verdicts)} · "
|
|
705
|
+
f"clean {clean} · blocking {blocking} · no parseable verdict {unparsed}")
|
|
706
|
+
if unparsed:
|
|
707
|
+
print("[cross-review] a chunk with no verdict is NOT a pass — the overall result "
|
|
708
|
+
"below is fail-closed.")
|
|
709
|
+
if verdict is True:
|
|
710
|
+
return EXIT_CLEAN
|
|
711
|
+
if verdict is False:
|
|
712
|
+
return EXIT_BLOCKING
|
|
713
|
+
sys.stderr.write("no REVIEW: CLEAN|BLOCKING verdict line found in the model output\n")
|
|
714
|
+
return EXIT_ERROR
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
if __name__ == "__main__":
|
|
718
|
+
raise SystemExit(main())
|