@caddis/cli 0.0.0 → 0.1.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/LICENSE +21 -0
- package/README.md +150 -1
- package/bundles/antigravity-plugin/agents/code-reviewer.md +47 -0
- package/bundles/antigravity-plugin/agents/preflight.md +53 -0
- package/bundles/antigravity-plugin/guard_agy.py +338 -0
- package/bundles/antigravity-plugin/hooks.json +36 -0
- package/bundles/antigravity-plugin/mcp_config.json +8 -0
- package/bundles/antigravity-plugin/mcp_ping_server.py +55 -0
- package/bundles/antigravity-plugin/plugin.json +5 -0
- package/bundles/antigravity-plugin/session_end_agy.py +57 -0
- package/bundles/antigravity-plugin/skills/_registry.md +115 -0
- package/bundles/antigravity-plugin/skills/add-rules/SKILL.md +45 -0
- package/bundles/antigravity-plugin/skills/api-design/SKILL.md +249 -0
- package/bundles/antigravity-plugin/skills/backend-development/SKILL.md +305 -0
- package/bundles/antigravity-plugin/skills/best-practices/SKILL.md +500 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/best-practices-referencer.md +263 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/codebase-context-builder.md +326 -0
- package/bundles/antigravity-plugin/skills/best-practices/agents/task-intent-analyzer.md +245 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/anti-patterns.md +571 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/before-after-examples.md +1114 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/best-practices-guide.md +513 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/common-workflows.md +692 -0
- package/bundles/antigravity-plugin/skills/best-practices/references/prompt-patterns.md +547 -0
- package/bundles/antigravity-plugin/skills/brainstorming/SKILL.md +57 -0
- package/bundles/antigravity-plugin/skills/ci-cd-pipeline/SKILL.md +315 -0
- package/bundles/antigravity-plugin/skills/code-documentation/SKILL.md +271 -0
- package/bundles/antigravity-plugin/skills/code-review/SKILL.md +122 -0
- package/bundles/antigravity-plugin/skills/codebase-audit/SKILL.md +204 -0
- package/bundles/antigravity-plugin/skills/context-curator/SKILL.md +157 -0
- package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +40 -0
- package/bundles/antigravity-plugin/skills/css-architecture/SKILL.md +305 -0
- package/bundles/antigravity-plugin/skills/css-architecture/references/RESPONSIVE-DESIGN.md +604 -0
- package/bundles/antigravity-plugin/skills/database-design/SKILL.md +177 -0
- package/bundles/antigravity-plugin/skills/db-diagram/SKILL.md +148 -0
- package/bundles/antigravity-plugin/skills/db-diagram/scripts/sql_to_graph.py +1212 -0
- package/bundles/antigravity-plugin/skills/digress/SKILL.md +61 -0
- package/bundles/antigravity-plugin/skills/draw-io/SKILL.md +162 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/aws-icons.md +677 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/layout-guidelines.md +142 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/troubleshooting.md +118 -0
- package/bundles/antigravity-plugin/skills/draw-io/references/workflows.md +103 -0
- package/bundles/antigravity-plugin/skills/draw-io/scripts/convert-drawio-to-png.sh +25 -0
- package/bundles/antigravity-plugin/skills/draw-io/scripts/find_aws_icon.py +79 -0
- package/bundles/antigravity-plugin/skills/error-handling/SKILL.md +260 -0
- package/bundles/antigravity-plugin/skills/excalidraw-db/SKILL.md +38 -0
- package/bundles/antigravity-plugin/skills/fastapi-dev/SKILL.md +300 -0
- package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +198 -0
- package/bundles/antigravity-plugin/skills/frontend-design/SKILL.md +193 -0
- package/bundles/antigravity-plugin/skills/frontend-design/references/my-tech-stack.md +127 -0
- package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +195 -0
- package/bundles/antigravity-plugin/skills/git-commit/SKILL.md +285 -0
- package/bundles/antigravity-plugin/skills/golden-plan/SKILL.md +577 -0
- package/bundles/antigravity-plugin/skills/handoff/SKILL.md +100 -0
- package/bundles/antigravity-plugin/skills/implement/SKILL.md +114 -0
- package/bundles/antigravity-plugin/skills/javascript-typescript/SKILL.md +142 -0
- package/bundles/antigravity-plugin/skills/kb/SKILL.md +60 -0
- package/bundles/antigravity-plugin/skills/mermaid-db/SKILL.md +33 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/SKILL.md +236 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/ENHANCEMENTS.md +264 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/MERMAID-SUMMARY.md +137 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/advanced-features.md +556 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/architecture-diagrams.md +192 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/c4-diagrams.md +410 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/class-diagrams.md +361 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/erd-diagrams.md +510 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/flowcharts.md +450 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/sequence-diagrams.md +394 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/troubleshooting.md +335 -0
- package/bundles/antigravity-plugin/skills/mermaid-diagrams/references/workflows.md +418 -0
- package/bundles/antigravity-plugin/skills/migrate-dir/SKILL.md +68 -0
- package/bundles/antigravity-plugin/skills/mockup/SKILL.md +242 -0
- package/bundles/antigravity-plugin/skills/particle-art/SKILL.md +243 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/canvas-utils.ts +171 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/dot-field.template.tsx +203 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/flow-field.template.tsx +263 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/node-shape.template.tsx +261 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/shape-sampler.ts +281 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/stipple-morph.template.tsx +167 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/stipple.template.tsx +175 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/trail-ghost.template.tsx +266 -0
- package/bundles/antigravity-plugin/skills/particle-art/references/usage-examples.md +320 -0
- package/bundles/antigravity-plugin/skills/playwright/API_REFERENCE.md +653 -0
- package/bundles/antigravity-plugin/skills/playwright/SKILL.md +454 -0
- package/bundles/antigravity-plugin/skills/playwright/lib/helpers.js +441 -0
- package/bundles/antigravity-plugin/skills/playwright/package.json +26 -0
- package/bundles/antigravity-plugin/skills/playwright/run.js +228 -0
- package/bundles/antigravity-plugin/skills/prd/SKILL.md +107 -0
- package/bundles/antigravity-plugin/skills/preflight/SKILL.md +435 -0
- package/bundles/antigravity-plugin/skills/python/SKILL.md +388 -0
- package/bundles/antigravity-plugin/skills/react-best-practices/SKILL.md +269 -0
- package/bundles/antigravity-plugin/skills/react-dev/README.md +404 -0
- package/bundles/antigravity-plugin/skills/react-dev/SKILL.md +459 -0
- package/bundles/antigravity-plugin/skills/react-dev/examples/generic-components.md +579 -0
- package/bundles/antigravity-plugin/skills/react-dev/examples/server-components.md +579 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/event-handlers.md +574 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/hooks.md +456 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/react-19-patterns.md +638 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/react-router.md +1002 -0
- package/bundles/antigravity-plugin/skills/react-dev/references/tanstack-router.md +587 -0
- package/bundles/antigravity-plugin/skills/refactoring/SKILL.md +486 -0
- package/bundles/antigravity-plugin/skills/resume/SKILL.md +36 -0
- package/bundles/antigravity-plugin/skills/security-review/SKILL.md +196 -0
- package/bundles/antigravity-plugin/skills/setup-project-ai/SKILL.md +61 -0
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +107 -0
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +103 -0
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +102 -0
- package/bundles/antigravity-plugin/skills/skill-creator/LICENSE.txt +202 -0
- package/bundles/antigravity-plugin/skills/skill-creator/SKILL.md +491 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/analyzer.md +274 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/comparator.md +202 -0
- package/bundles/antigravity-plugin/skills/skill-creator/agents/grader.md +223 -0
- package/bundles/antigravity-plugin/skills/skill-creator/assets/eval_review.html +146 -0
- package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/bundles/antigravity-plugin/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/bundles/antigravity-plugin/skills/skill-creator/references/schemas.md +430 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/__init__.py +0 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/generate_report.py +326 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/improve_description.py +247 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/package_skill.py +136 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_eval.py +310 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/run_loop.py +328 -0
- package/bundles/antigravity-plugin/skills/skill-creator/scripts/utils.py +47 -0
- package/bundles/antigravity-plugin/skills/sql/SKILL.md +321 -0
- package/bundles/antigravity-plugin/skills/tdd/SKILL.md +37 -0
- package/bundles/antigravity-plugin/skills/tdd-workflow/SKILL.md +188 -0
- package/bundles/antigravity-plugin/skills/technical-writing/SKILL.md +286 -0
- package/bundles/antigravity-plugin/skills/test-strategy/SKILL.md +155 -0
- package/bundles/antigravity-plugin/skills/ui-brief/SKILL.md +84 -0
- package/bundles/antigravity-plugin/skills/ui-review/SKILL.md +176 -0
- package/bundles/antigravity-plugin/skills/ui-review/references/framework-fixes.md +471 -0
- package/bundles/antigravity-plugin/skills/ui-review/references/visual-checklist.md +236 -0
- package/bundles/antigravity-plugin/skills/usage-review/SKILL.md +77 -0
- package/bundles/antigravity-plugin/skills/use-model/SKILL.md +64 -0
- package/bundles/antigravity-plugin/skills/using-git-worktrees/SKILL.md +217 -0
- package/bundles/antigravity-plugin/skills/version/SKILL.md +18 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/DESIGN_TOKENS.md +487 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/IMPLEMENTATION_GUIDE.md +177 -0
- package/bundles/antigravity-plugin/skills/warm-editorial-ui/SKILL.md +732 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/LICENSE.txt +202 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/SKILL.md +97 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/console_logging.py +35 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/element_discovery.py +40 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/examples/static_html_automation.py +33 -0
- package/bundles/antigravity-plugin/skills/webapp-testing/scripts/with_server.py +106 -0
- package/bundles/antigravity-plugin/skills/windows-deployment/SKILL.md +880 -0
- package/bundles/antigravity-plugin/skills/writing-plans/SKILL.md +384 -0
- package/bundles/antigravity-plugin/statusline-command-agy.sh +91 -0
- package/bundles/antigravity-plugin/warm_start_agy.py +149 -0
- package/bundles/manifest.json +6 -0
- package/dist/cli.js +5363 -0
- package/package.json +61 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 junaid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1 +1,150 @@
|
|
|
1
|
-
|
|
1
|
+
# @caddis/cli
|
|
2
|
+
|
|
3
|
+
**One command to install, update and diagnose the [caddis](https://github.com/saajunaid/caddis-plugin) dev harness across every coding agent you use.**
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @caddis/cli init # detect your agents, confirm, install caddis into each
|
|
7
|
+
npx @caddis/cli doctor # what is installed where, and what to do about it
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
If you use more than one coding agent, you have more than one place caddis can go stale.
|
|
11
|
+
This CLI is the single front door.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## What it does
|
|
16
|
+
|
|
17
|
+
caddis is a **meta-installer**. It detects the agents on your machine and drives **each one's own
|
|
18
|
+
native install mechanism** — it never reimplements them, and it never installs an agent for you.
|
|
19
|
+
|
|
20
|
+
| Agent | Detected via | How caddis is installed | v0.1 |
|
|
21
|
+
| --- | --- | --- | --- |
|
|
22
|
+
| **Claude Code** | `claude` on PATH | its plugin marketplace | ✅ |
|
|
23
|
+
| **agy** (Antigravity) | `agy` on PATH | the caddis bundle shipped inside this package | ✅ |
|
|
24
|
+
| **Codex** | `codex` on PATH / `~/.codex` | config merge | v0.2 |
|
|
25
|
+
| **GitHub Copilot** | `copilot` on PATH / `.github/copilot-instructions.md` | config merge | v0.2 |
|
|
26
|
+
|
|
27
|
+
Codex and Copilot are **detected and reported** in v0.1, never written to. They are file-configured,
|
|
28
|
+
which means supporting them properly requires merging into config you already own — that ships when
|
|
29
|
+
it is safe, not before.
|
|
30
|
+
|
|
31
|
+
**Absence is graceful.** Missing agents are skipped with a reason, never an error. One agent failing
|
|
32
|
+
never stops the others.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx @caddis/cli <command> # no install
|
|
40
|
+
npm i -g @caddis/cli # then just: caddis <command>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires **Node >= 20.19**.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
### `caddis doctor`
|
|
50
|
+
|
|
51
|
+
The one worth knowing. Every agent, the caddis version each actually has, whether that matches what
|
|
52
|
+
this CLI ships, where the version was read from, and the exact command that fixes each gap.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Environment
|
|
56
|
+
✓ node 22.14.0
|
|
57
|
+
• platform win32 x64
|
|
58
|
+
• @caddis/cli 0.1.0
|
|
59
|
+
✓ shipped pool 1.3.39
|
|
60
|
+
bundle antigravity-plugin @ 1.3.39
|
|
61
|
+
|
|
62
|
+
Agents
|
|
63
|
+
▲ Claude Code — caddis 1.3.38 → 1.3.39 available
|
|
64
|
+
C:\Users\you\AppData\Roaming\npm\claude.CMD
|
|
65
|
+
agent version 2.1.220
|
|
66
|
+
read from `claude plugin list`
|
|
67
|
+
→ caddis update --agent claude
|
|
68
|
+
▲ agy (Antigravity) — caddis 1.3.38 → 1.3.39 available
|
|
69
|
+
~\.gemini\config\plugins\caddis\plugin.json
|
|
70
|
+
→ caddis update --agent agy
|
|
71
|
+
• Codex — detected — not yet supported (v0.2)
|
|
72
|
+
|
|
73
|
+
Summary
|
|
74
|
+
3 of 4 agents detected · 0 current · 2 stale · 0 not installed
|
|
75
|
+
|
|
76
|
+
2 things to fix:
|
|
77
|
+
• Claude Code has caddis 1.3.38, this CLI ships 1.3.39
|
|
78
|
+
caddis update --agent claude
|
|
79
|
+
• agy (Antigravity) has caddis 1.3.38, this CLI ships 1.3.39
|
|
80
|
+
caddis update --agent agy
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`--strict` exits non-zero when something needs fixing (for CI). Detected-but-unsupported agents are
|
|
84
|
+
information, not failures, and never trip `--strict`. `--json` gives machine-readable output.
|
|
85
|
+
|
|
86
|
+
### `caddis status`
|
|
87
|
+
|
|
88
|
+
The same facts as a glanceable table. Running bare `caddis` does this.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
caddis cli 0.1.0 · pool 1.3.39
|
|
92
|
+
|
|
93
|
+
AGENT DETECTED CADDIS STATE
|
|
94
|
+
───────────────── ──────── ────── ────────────────
|
|
95
|
+
Claude Code yes 1.3.38 update available
|
|
96
|
+
agy (Antigravity) yes 1.3.38 update available
|
|
97
|
+
Codex yes — v0.2
|
|
98
|
+
GitHub Copilot no — —
|
|
99
|
+
|
|
100
|
+
2 agent(s) behind — run caddis update (or caddis doctor for detail)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `caddis update`
|
|
104
|
+
|
|
105
|
+
Brings every detected, supported agent up to the caddis version shipped in this CLI, through that
|
|
106
|
+
agent's native mechanism. Agents already current are skipped (`--force` re-drives them).
|
|
107
|
+
|
|
108
|
+
### `caddis init`
|
|
109
|
+
|
|
110
|
+
The first run. Detects, shows exactly what it is about to run, asks, then installs. `--yes` skips
|
|
111
|
+
the prompt for CI.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Flags
|
|
116
|
+
|
|
117
|
+
| Flag | Effect |
|
|
118
|
+
| --- | --- |
|
|
119
|
+
| `--dry-run` | print the exact vendor commands; change nothing |
|
|
120
|
+
| `-y, --yes` | never prompt |
|
|
121
|
+
| `-a, --agent <name...>` | limit to specific agents (`claude`, `agy`, `codex`, `copilot`) |
|
|
122
|
+
| `--json` | machine-readable output (`doctor`, `status`) |
|
|
123
|
+
| `--strict` | exit 1 when something needs fixing (`doctor`) |
|
|
124
|
+
| `--no-update-notifier` | skip the "newer @caddis/cli available" check |
|
|
125
|
+
|
|
126
|
+
Flags work before or after the subcommand: `caddis update --dry-run` and `caddis --dry-run update`
|
|
127
|
+
are the same.
|
|
128
|
+
|
|
129
|
+
**Always safe to preview:**
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
$ caddis update --dry-run
|
|
133
|
+
· Claude Code — dry run — nothing executed
|
|
134
|
+
would run: claude plugin marketplace update caddis
|
|
135
|
+
would run: claude plugin update caddis@caddis
|
|
136
|
+
· agy (Antigravity) — dry run — nothing executed
|
|
137
|
+
would run: agy plugin install <shipped bundle>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Where the caddis content comes from
|
|
143
|
+
|
|
144
|
+
The caddis bundle ships **inside this npm package**, versioned in lockstep with the CLI — the caddis
|
|
145
|
+
you install is the caddis you get, with no extra fetch. `doctor` compares each agent's installed
|
|
146
|
+
version against that shipped version; that difference is what "drift" means throughout.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-reviewer
|
|
3
|
+
description: Use this agent to review a diff or set of changes for correctness, security, and convention adherence. Use proactively after a phase is green and before commit/merge. Reads the diff and relevant files in its own context and returns a verdict + prioritized issue list — it does NOT edit code.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are a principal engineer doing a focused code review. You read the change and judge it. You do
|
|
7
|
+
**not** modify code — you report. Keep the main thread's context clean: return conclusions, not a tour.
|
|
8
|
+
|
|
9
|
+
## How to review
|
|
10
|
+
1. Get the diff: `git diff` (unstaged) and `git diff --staged`, plus `git status`. If given specific
|
|
11
|
+
files/commits, scope to those (`git show <sha>`).
|
|
12
|
+
2. Read the changed files and their immediate collaborators. Check against the relevant `AGENTS.md`
|
|
13
|
+
(root + the folder being changed; `CLAUDE.md` is just an `@AGENTS.md` shim) — convention violations are review findings.
|
|
14
|
+
3. Judge on these axes, in priority order:
|
|
15
|
+
- **Correctness** — does it do what was intended? Logic bugs, races, off-by-one, wrong async/await.
|
|
16
|
+
- **Tests** — is there a test that would fail without this change? (TDD law.) Edge cases covered?
|
|
17
|
+
- **Security** — injection (parameterized queries?), authn/z, secret exposure, unvalidated input,
|
|
18
|
+
over-broad exception swallowing that hides failures.
|
|
19
|
+
- **Contracts** — backend response models ↔ frontend types alignment; API shape changes; **the path
|
|
20
|
+
a client calls actually reaches the server** (prefixes, proxies, base URLs).
|
|
21
|
+
- **Conventions** — per the project's `AGENTS.md` (typed boundaries, no absolute paths, no silent
|
|
22
|
+
failures/logging, layering, framework idioms).
|
|
23
|
+
- **Simplicity** — dead code, duplication, needless abstraction (YAGNI), clearer alternative.
|
|
24
|
+
|
|
25
|
+
## Severity
|
|
26
|
+
- **blocking** — must fix before merge (bug, security hole, missing test for behavior change, broken
|
|
27
|
+
client↔server contract, violation of a stated project law).
|
|
28
|
+
- **should-fix** — real issue, fix now unless there's a reason.
|
|
29
|
+
- **nit** — style/polish; optional.
|
|
30
|
+
|
|
31
|
+
## Return format (always end with this)
|
|
32
|
+
```
|
|
33
|
+
review:
|
|
34
|
+
verdict: approved | changes-requested
|
|
35
|
+
blocking:
|
|
36
|
+
- file: <path:line> issue: <what> fix: <concrete suggestion>
|
|
37
|
+
should_fix:
|
|
38
|
+
- file: <path:line> issue: <what> fix: <suggestion>
|
|
39
|
+
nits:
|
|
40
|
+
- file: <path:line> note: <what>
|
|
41
|
+
good: <one line on what's done well, if anything>
|
|
42
|
+
```
|
|
43
|
+
`verdict: approved` requires an empty `blocking` list. Be specific with `file:line`. Do not fix; report.
|
|
44
|
+
|
|
45
|
+
**Then, as the very last line of your output (nothing after it), emit the machine verdict:**
|
|
46
|
+
`REVIEW: CLEAN` (empty `blocking` list) or `REVIEW: BLOCKING` (one or more blocking items). Automated
|
|
47
|
+
runners read only this line, so it is mandatory and must exactly match one of those two forms.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: preflight
|
|
3
|
+
description: Use this agent to validate an implementation plan (or a phase of it) against the ACTUAL codebase before writing code. Use proactively right after a plan is drafted and before starting a phase. Checks that every technical claim in the plan — file paths, symbol names, API shapes, data fields, dependencies — matches reality. Read-only; returns a PASS/FAIL report. Does not fix anything.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
You are a plan-validation specialist. Your job: catch the plan's wrong assumptions **before** they
|
|
7
|
+
become wasted implementation. You diagnose; you do not fix and you do not write code.
|
|
8
|
+
|
|
9
|
+
## What to validate (7 categories)
|
|
10
|
+
For the plan (or the named phase) check each claim against the codebase:
|
|
11
|
+
1. **File paths** — every file the plan says to create/edit: does the parent dir exist? Does an
|
|
12
|
+
existing file already cover it (collision/duplication)?
|
|
13
|
+
2. **Symbols** — referenced functions/classes/components/hooks: do they exist with that exact name and
|
|
14
|
+
signature? (`grep` them.) Flag renamed/missing ones.
|
|
15
|
+
3. **API shapes** — endpoints, request/response models: do the cited routes and types exist and match
|
|
16
|
+
the fields the plan assumes? Does the path a client will call actually reach the server (prefix/proxy)?
|
|
17
|
+
4. **Data fields** — JSON paths / DB columns the plan binds to: do they exist in the models/fixtures?
|
|
18
|
+
5. **Dependencies** — libraries/primitives the plan needs: declared in the manifest? installed? (e.g. a
|
|
19
|
+
UI primitive the plan assumes exists — confirm the component/package is actually present.)
|
|
20
|
+
6. **Conventions** — does the plan's approach fit the patterns in the relevant `AGENTS.md`? Flag drift.
|
|
21
|
+
7. **Test strategy** — does the plan put a failing test first for each behavior change (TDD law)?
|
|
22
|
+
8. **Local-coder readiness** — could a low-capability model implement each phase with no reasoning of
|
|
23
|
+
its own? Flag any phase that leaves a judgment call open, names a file/symbol vaguely, omits exact
|
|
24
|
+
data bindings, or has a non-literal exit gate. (Supports the planner→coder handoff.)
|
|
25
|
+
|
|
26
|
+
## Method
|
|
27
|
+
- Read the plan. For each claim, run the cheapest check that confirms or refutes it (Glob/Grep/Read).
|
|
28
|
+
- Don't trust the plan's prose — verify against files. A claim you can't verify is a finding, not a pass.
|
|
29
|
+
|
|
30
|
+
## Return format (always end with this)
|
|
31
|
+
```
|
|
32
|
+
preflight:
|
|
33
|
+
verdict: PASS | FAIL
|
|
34
|
+
phase: <which phase/scope validated, or "whole plan">
|
|
35
|
+
findings:
|
|
36
|
+
- category: <paths|symbols|api|data|deps|conventions|tests|local-coder>
|
|
37
|
+
severity: blocker | warning
|
|
38
|
+
claim: <what the plan asserts>
|
|
39
|
+
reality: <what the codebase actually shows>
|
|
40
|
+
fix: <what the plan should say instead>
|
|
41
|
+
local_coder_ready: yes | no
|
|
42
|
+
local_coder_gaps:
|
|
43
|
+
- phase: <phase> — <what a weak coder would have to reason out / what's underspecified>
|
|
44
|
+
verified_ok:
|
|
45
|
+
- <short list of key claims that checked out>
|
|
46
|
+
```
|
|
47
|
+
`PASS` requires zero `blocker` findings. `local_coder_ready: no` lists the phases that lean on implied
|
|
48
|
+
reasoning (a warning, not a hard blocker unless the plan targets a local coder). Be concrete: cite the
|
|
49
|
+
file/symbol you checked.
|
|
50
|
+
|
|
51
|
+
**Then, as the very last line of your output (nothing after it), emit the machine verdict:**
|
|
52
|
+
`PREFLIGHT: PASS` (zero blocker findings) or `PREFLIGHT: FAIL` (one or more blockers). Automated runners
|
|
53
|
+
read only this line, so it is mandatory and must exactly match one of those two forms.
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""caddis `guard` hook for agy (Antigravity) — PreToolUse event.
|
|
3
|
+
|
|
4
|
+
Ports claude-harness/hooks/guard.py to agy's contract: the risk CLASSIFIERS are copied in verbatim
|
|
5
|
+
(same tiers, same regexes, same escape hatch); only the I/O envelope is rewritten. Self-contained
|
|
6
|
+
(the agy bundle ships neither claude-harness/hooks/ nor scripts/), stdlib-only.
|
|
7
|
+
|
|
8
|
+
agy PreToolUse stdin (camelCase protojson):
|
|
9
|
+
{"toolCall": {"name": "run_command", "args": {"CommandLine": "rm -rf /"}}, "stepIdx": N,
|
|
10
|
+
"conversationId": "…", "workspacePaths": ["…"], "transcriptPath": "…", "modelName": "…"}
|
|
11
|
+
NOTE: cwd is the PLUGIN dir, not the workspace — the repo root comes from workspacePaths[0].
|
|
12
|
+
|
|
13
|
+
agy PreToolUse stdout (a decision object):
|
|
14
|
+
deny -> {"decision": "deny", "reason": "…"} (hard block)
|
|
15
|
+
ask/confirm -> {"decision": "force_ask", "reason": "…"} (always prompt; defeats the allow-cache)
|
|
16
|
+
allow -> nothing at all. Printing {"decision":"allow"} would *override* agy's own permission
|
|
17
|
+
prompt for that call; staying silent falls through to normal handling, which is what
|
|
18
|
+
the Claude Code guard does too.
|
|
19
|
+
|
|
20
|
+
*** FAIL OPEN — the load-bearing property of this file ***
|
|
21
|
+
agy may fail-closed on a hook error, so a bug here must degrade to "no guard" (agy's state today),
|
|
22
|
+
never to a bricked agent. Every stage is wrapped: on ANY exception, malformed payload, unknown shape
|
|
23
|
+
or encoding problem this prints NOTHING and exits 0. It never raises, never exits non-zero, and never
|
|
24
|
+
emits a partial/malformed decision (the JSON is built and serialized before a single byte is printed).
|
|
25
|
+
|
|
26
|
+
Write-tier gating (classify_write) is wired defensively: agy's file-edit tool name is still
|
|
27
|
+
LIVE-CHECK-PENDING (see .caddis/plans/agy-hooks-port.md), so it applies only to a tool whose name
|
|
28
|
+
looks like an edit/write and whose args carry a path. Unknown tools -> allow.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
# Functional identity, mirrored from scripts/claudster_config.py. Read order: the current dir first,
|
|
38
|
+
# the pre-rename dir as a one-version fallback.
|
|
39
|
+
_ARTIFACT_DIRS = (".caddis", ".claudster")
|
|
40
|
+
_ENV_PREFIX = "CADDIS"
|
|
41
|
+
_LEGACY_ENV_PREFIX = "CLAUDSTER"
|
|
42
|
+
|
|
43
|
+
# ── protected-path rules (write → deny) ──────────────────────────────────────
|
|
44
|
+
_SECRET_BASENAMES = {"id_rsa", "id_dsa", "id_ecdsa", "id_ed25519", "credentials",
|
|
45
|
+
".npmrc", ".pypirc", ".netrc", ".pgpass", ".htpasswd"}
|
|
46
|
+
_SECRET_SUFFIXES = (".pem", ".key", ".p12", ".pfx", ".keystore", ".jks")
|
|
47
|
+
_ENV_RE = re.compile(r"(^|/)\.env(\.|$)", re.I)
|
|
48
|
+
_ENV_EXAMPLE_RE = re.compile(r"\.env\.(example|sample|template|dist)$", re.I)
|
|
49
|
+
|
|
50
|
+
# Lockfiles are normally regenerated by the package manager, so a hand-edit is unusual enough to
|
|
51
|
+
# confirm. Routine manifests (package.json/tsconfig) are deliberately NOT gated — too frequent, not risky.
|
|
52
|
+
_ASK_BASENAMES = {"package-lock.json", "yarn.lock", "pnpm-lock.yaml", "poetry.lock", "cargo.lock",
|
|
53
|
+
"uv.lock", "composer.lock", "gemfile.lock"}
|
|
54
|
+
_CI_WORKFLOW_RE = re.compile(r"(^|/)(\.github|\.gitea)/workflows/", re.I)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def classify_write(file_path: str) -> tuple[str, str]:
|
|
58
|
+
"""Risk tier (deny|ask|allow) + reason for writing/editing ``file_path``."""
|
|
59
|
+
p = file_path.replace("\\", "/")
|
|
60
|
+
parts = [seg.lower() for seg in p.split("/") if seg]
|
|
61
|
+
base = parts[-1] if parts else ""
|
|
62
|
+
|
|
63
|
+
if any(seg in ("secrets", ".secrets") for seg in parts):
|
|
64
|
+
return "deny", f"writes into a secrets directory ({base})"
|
|
65
|
+
if ".git" in parts[:-1]:
|
|
66
|
+
return "deny", "writes into the .git internal directory"
|
|
67
|
+
if _ENV_RE.search(p) and not _ENV_EXAMPLE_RE.search(p):
|
|
68
|
+
return "deny", f"writes an environment/secret file ({base})"
|
|
69
|
+
if base in _SECRET_BASENAMES or any(base.endswith(s) for s in _SECRET_SUFFIXES):
|
|
70
|
+
return "deny", f"writes a credential/key file ({base})"
|
|
71
|
+
|
|
72
|
+
if base in _ASK_BASENAMES:
|
|
73
|
+
return "ask", f"edits a lockfile ({base})"
|
|
74
|
+
if _CI_WORKFLOW_RE.search(p):
|
|
75
|
+
return "ask", "edits a CI/CD workflow"
|
|
76
|
+
return "allow", ""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ── bash rules ───────────────────────────────────────────────────────────────
|
|
80
|
+
# Catastrophic delete targets: a filesystem/home root or a *bare* drive root (C:\ ), NOT a drive
|
|
81
|
+
# subpath (E:\proj\dist falls through to ask).
|
|
82
|
+
_CATASTROPHIC_TARGET = re.compile(
|
|
83
|
+
r"--no-preserve-root"
|
|
84
|
+
r"|(?:^|\s)(?:/|/\*|~/|~|\$\{?HOME\}?)(?:\s|$|;|&|\|)"
|
|
85
|
+
r"|(?:^|\s)[A-Za-z]:[\\/]?(?:\s|$|;|&|\|)",
|
|
86
|
+
re.I,
|
|
87
|
+
)
|
|
88
|
+
_FORK_BOMB = re.compile(r":\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _strip_quotes(s: str) -> str:
|
|
92
|
+
"""Drop quotes so quoted flags/targets still match (rm '-rf' \"/\" → rm -rf /)."""
|
|
93
|
+
return s.replace('"', "").replace("'", "")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _has_flag(cmd: str, letter: str, longname: str) -> bool:
|
|
97
|
+
"""True if `cmd` carries the short flag (e.g. -rf / -r) or the long flag (--recursive)."""
|
|
98
|
+
return bool(re.search(rf"(?:^|\s)-\w*{letter}\w*\b", cmd, re.I)) or longname in cmd.lower()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _is_rm_recursive_force(cmd: str) -> bool:
|
|
102
|
+
if not re.search(r"\brm\b", cmd):
|
|
103
|
+
return False
|
|
104
|
+
return _has_flag(cmd, "r", "--recursive") and _has_flag(cmd, "f", "--force")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _is_win_recursive_delete(cmd: str) -> bool:
|
|
108
|
+
"""Native Windows recursive delete — the POSIX ``rm`` path is handled by _is_rm_recursive_force.
|
|
109
|
+
|
|
110
|
+
Catches PowerShell ``Remove-Item -Recurse -Force`` (and the Remove-Item aliases ri/rmdir/rd/del/
|
|
111
|
+
erase) plus cmd.exe ``rmdir``/``rd``/``del``/``erase`` with the ``/s`` recurse-subdirectories switch.
|
|
112
|
+
This is a Windows-primary environment, so these are the real footguns the POSIX rules miss."""
|
|
113
|
+
if not re.search(r"(?:^|[\s;&|(])(?:remove-item|ri|rmdir|rd|del|erase)\b", cmd, re.I):
|
|
114
|
+
return False
|
|
115
|
+
ps_recurse_force = bool(re.search(r"(?:^|\s)-rec\w*", cmd, re.I)) and bool(re.search(r"(?:^|\s)-for\w*", cmd, re.I))
|
|
116
|
+
cmd_slash_s = bool(re.search(r"(?:^|\s)/s\b", cmd, re.I))
|
|
117
|
+
return ps_recurse_force or cmd_slash_s
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def classify_bash(command: str) -> tuple[str, str]:
|
|
121
|
+
"""Risk tier (deny|ask|allow) + reason for a shell command."""
|
|
122
|
+
c = command.strip()
|
|
123
|
+
cl = c.lower()
|
|
124
|
+
n = _strip_quotes(c) # quote-normalized: defeats `rm '-rf' "/"` evasion
|
|
125
|
+
nl = n.lower()
|
|
126
|
+
rm_rf = _is_rm_recursive_force(n)
|
|
127
|
+
catastrophic = bool(_CATASTROPHIC_TARGET.search(n))
|
|
128
|
+
find_delete = bool(re.search(r"\bfind\b.*?(?:-delete\b|-exec\s+rm\b)", nl))
|
|
129
|
+
win_del = _is_win_recursive_delete(n)
|
|
130
|
+
|
|
131
|
+
# ── deny: catastrophic ──
|
|
132
|
+
if rm_rf and catastrophic:
|
|
133
|
+
return "deny", "recursive force-delete of a root/home path (rm -rf)"
|
|
134
|
+
if find_delete and catastrophic:
|
|
135
|
+
return "deny", "find -delete from a root/home path"
|
|
136
|
+
if win_del and catastrophic:
|
|
137
|
+
return "deny", "recursive force-delete of a root/home path (Windows Remove-Item/rmdir)"
|
|
138
|
+
if re.search(r"\brmtree\s*\(\s*['\"]?(?:/|~|[A-Za-z]:\\?)['\"]?\s*\)", c, re.I):
|
|
139
|
+
return "deny", "recursive tree delete of a root/home path (rmtree)"
|
|
140
|
+
if _FORK_BOMB.search(c):
|
|
141
|
+
return "deny", "fork bomb"
|
|
142
|
+
if re.search(r"\bdd\b.*\bof=/dev/", nl) or re.search(r"\bmkfs", nl) or re.search(r">\s*/dev/sd[a-z]", nl):
|
|
143
|
+
return "deny", "writes directly to a disk device"
|
|
144
|
+
if (re.search(r"\bchmod\b", nl) and _has_flag(n, "r", "--recursive")
|
|
145
|
+
and re.search(r"\b0?777\b", nl) and catastrophic):
|
|
146
|
+
return "deny", "recursive chmod 777 on a root path"
|
|
147
|
+
|
|
148
|
+
# ── ask: destructive-but-legit ──
|
|
149
|
+
if rm_rf:
|
|
150
|
+
return "ask", "recursive force-delete (rm -rf)"
|
|
151
|
+
if find_delete:
|
|
152
|
+
return "ask", "find with -delete / -exec rm"
|
|
153
|
+
if win_del:
|
|
154
|
+
return "ask", "recursive delete (Windows Remove-Item -Recurse / rmdir /s)"
|
|
155
|
+
# git rules tolerate intervening options (e.g. `git -c k=v push --force`) but stay within one
|
|
156
|
+
# command segment (the [^;&|]* won't cross a ; && || into an unrelated command). The trailing
|
|
157
|
+
# `+\S` catches a force-push via refspec (`git push origin +main`), not just the --force flags.
|
|
158
|
+
if re.search(r"\bgit\b[^;&|]*\bpush\b[^;&|]*(--force\b|--force-with-lease\b|(?:^|\s)-f\b|(?:^|\s)\+\S)", c):
|
|
159
|
+
return "ask", "git force-push"
|
|
160
|
+
if re.search(r"\bgit\b[^;&|]*\breset\s+--hard\b", c):
|
|
161
|
+
return "ask", "git reset --hard"
|
|
162
|
+
if re.search(r"\bgit\b[^;&|]*\bclean\b[^;&|]*(?:^|\s)-\w*f", c):
|
|
163
|
+
return "ask", "git clean (force)"
|
|
164
|
+
if re.search(r"\bgit\b[^;&|]*\bfilter-(repo|branch)\b", c):
|
|
165
|
+
return "ask", "git history rewrite"
|
|
166
|
+
if re.search(r"\b(drop\s+(table|database|schema)|truncate\s+table)\b", cl):
|
|
167
|
+
return "ask", "destructive SQL (drop/truncate)"
|
|
168
|
+
if re.search(r"\b(npm|yarn|pnpm)\s+publish\b|\btwine\s+upload\b|\bcargo\s+publish\b", cl):
|
|
169
|
+
return "ask", "publishing a package"
|
|
170
|
+
if re.search(r"\b(curl|wget|iwr|invoke-webrequest)\b[^|]*\|\s*(sudo\s+)?(sh|bash|zsh|iex|python)\b", cl):
|
|
171
|
+
return "ask", "piping a download into a shell"
|
|
172
|
+
return "allow", ""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ── agy tool-shape mapping ───────────────────────────────────────────────────
|
|
176
|
+
# The shell tool: step type CORTEX_STEP_TYPE_RUN_COMMAND -> "run_command". The command lives at
|
|
177
|
+
# args.CommandLine. Extra spellings are accepted so a step-type rename can't silently un-guard the
|
|
178
|
+
# shell (a missed tool name = no guard at all, which is the failure we most want to avoid).
|
|
179
|
+
_COMMAND_TOOLS = {"run_command", "runcommand", "run_terminal_command", "bash", "shell", "terminal"}
|
|
180
|
+
_COMMAND_ARG_KEYS = ("CommandLine", "commandLine", "command_line", "command", "Command")
|
|
181
|
+
|
|
182
|
+
# Write-ish tool detection is heuristic until the live check pins agy's file-edit tool name.
|
|
183
|
+
_WRITE_NAME_RE = re.compile(r"(edit|write|create|replace|patch|append)", re.I)
|
|
184
|
+
_READONLY_NAME_RE = re.compile(r"(view|read|list|search|grep|find|browser|open)", re.I)
|
|
185
|
+
_PATH_ARG_KEYS = ("FilePath", "filePath", "file_path", "AbsolutePath", "absolutePath",
|
|
186
|
+
"TargetFile", "targetFile", "Path", "path")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _first_str(args: dict, keys) -> str:
|
|
190
|
+
for k in keys:
|
|
191
|
+
v = args.get(k)
|
|
192
|
+
if isinstance(v, str) and v.strip():
|
|
193
|
+
return v
|
|
194
|
+
return ""
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def decide(tool_name: str, args, allow_patterns: list[str]) -> tuple[str, str]:
|
|
198
|
+
"""Top-level decision. Classify FIRST; the allow escape hatch may only DOWNGRADE an `ask` to
|
|
199
|
+
`allow` (never override a `deny`), and it matches the command/path value — not the JSON envelope —
|
|
200
|
+
so a key name like "command" can't disable the guard. Never raises on a malformed payload."""
|
|
201
|
+
if not isinstance(args, dict):
|
|
202
|
+
return "allow", ""
|
|
203
|
+
name = (tool_name or "").strip().lower()
|
|
204
|
+
|
|
205
|
+
if name in _COMMAND_TOOLS:
|
|
206
|
+
target = _first_str(args, _COMMAND_ARG_KEYS)
|
|
207
|
+
tier, reason = classify_bash(target) if target else ("allow", "")
|
|
208
|
+
elif _WRITE_NAME_RE.search(name) and not _READONLY_NAME_RE.search(name):
|
|
209
|
+
target = _first_str(args, _PATH_ARG_KEYS)
|
|
210
|
+
tier, reason = classify_write(target) if target else ("allow", "")
|
|
211
|
+
else:
|
|
212
|
+
return "allow", ""
|
|
213
|
+
|
|
214
|
+
if tier == "ask" and any(pat and pat.lower() in target.lower() for pat in allow_patterns):
|
|
215
|
+
return "allow", ""
|
|
216
|
+
return tier, reason
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── config (per-repo escape hatch + kill switch) ─────────────────────────────
|
|
220
|
+
_TRUTHY = {"1", "true", "yes", "on"}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _config_path(root: str) -> str | None:
|
|
224
|
+
"""The repo's ``config.toml`` — ``.caddis`` preferred, legacy ``.claudster`` as fallback."""
|
|
225
|
+
for name in _ARTIFACT_DIRS:
|
|
226
|
+
cfg = os.path.join(str(root), name, "config.toml")
|
|
227
|
+
if os.path.isfile(cfg):
|
|
228
|
+
return cfg
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _load_guard_config(root: str) -> dict:
|
|
233
|
+
"""Parse the ``[guard]`` table from the repo's ``config.toml``; {} on any problem."""
|
|
234
|
+
cfg = _config_path(root)
|
|
235
|
+
if cfg is None:
|
|
236
|
+
return {}
|
|
237
|
+
try:
|
|
238
|
+
import tomllib
|
|
239
|
+
with open(cfg, "rb") as fh:
|
|
240
|
+
data = tomllib.load(fh)
|
|
241
|
+
section = data.get("guard", {})
|
|
242
|
+
return section if isinstance(section, dict) else {}
|
|
243
|
+
except Exception:
|
|
244
|
+
return {}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _load_allow(root: str) -> list[str]:
|
|
248
|
+
allow = _load_guard_config(root).get("allow", [])
|
|
249
|
+
return [str(x) for x in allow] if isinstance(allow, list) else []
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def guard_disabled(root: str) -> bool:
|
|
253
|
+
"""True when the guard is turned off entirely (all tiers bypassed).
|
|
254
|
+
|
|
255
|
+
Precedence: the env var wins (global); otherwise the per-repo `[guard]` table — `enabled = false`
|
|
256
|
+
or `mode = "off"`. Any parse problem → not disabled (fail safe). CADDIS_GUARD_DISABLED is the
|
|
257
|
+
current name; the pre-rename CLAUDSTER_GUARD_DISABLED is read only when the new name is unset."""
|
|
258
|
+
_disabled = os.environ.get(f"{_ENV_PREFIX}_GUARD_DISABLED")
|
|
259
|
+
if _disabled is None:
|
|
260
|
+
_disabled = os.environ.get(f"{_LEGACY_ENV_PREFIX}_GUARD_DISABLED", "")
|
|
261
|
+
if _disabled.strip().lower() in _TRUTHY:
|
|
262
|
+
return True
|
|
263
|
+
section = _load_guard_config(root)
|
|
264
|
+
if section.get("enabled") is False:
|
|
265
|
+
return True
|
|
266
|
+
if str(section.get("mode", "")).strip().lower() == "off":
|
|
267
|
+
return True
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def deny_only(root: str) -> bool:
|
|
272
|
+
"""True when the guard keeps DENY (silent hard-block) but suppresses the ASK tier (ask -> allow =
|
|
273
|
+
silence), so it NEVER prompts — for autonomous / no-prompt workflows that still want a catastrophe
|
|
274
|
+
net. Env `CADDIS_GUARD_MODE=deny-only` (`CLAUDSTER_GUARD_MODE` fallback); config `[guard] mode =
|
|
275
|
+
"deny-only"`. Read silently."""
|
|
276
|
+
_mode = os.environ.get(f"{_ENV_PREFIX}_GUARD_MODE")
|
|
277
|
+
if _mode is None:
|
|
278
|
+
_mode = os.environ.get(f"{_LEGACY_ENV_PREFIX}_GUARD_MODE", "")
|
|
279
|
+
if _mode.strip().lower() == "deny-only":
|
|
280
|
+
return True
|
|
281
|
+
section = _load_guard_config(root)
|
|
282
|
+
return str(section.get("mode", "")).strip().lower() == "deny-only"
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# ── hook I/O glue (agy envelope) ─────────────────────────────────────────────
|
|
286
|
+
_AGY_DECISION = {"deny": "deny", "ask": "force_ask"}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _emit(tier: str, reason: str) -> None:
|
|
290
|
+
"""Serialize the decision FIRST, then print once — a failure mid-build can never leave agy
|
|
291
|
+
reading half an object. Silence (allow) is the default for anything that isn't deny/ask."""
|
|
292
|
+
decision = _AGY_DECISION.get(tier)
|
|
293
|
+
if not decision:
|
|
294
|
+
return
|
|
295
|
+
tail = (" Blocked as too dangerous to run automatically — do it by hand if you are certain."
|
|
296
|
+
if tier == "deny" else " Confirm only if this is intended.")
|
|
297
|
+
payload = json.dumps({"decision": decision, "reason": f"[caddis guard] {reason}.{tail}"})
|
|
298
|
+
sys.stdout.write(payload)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def main() -> None:
|
|
302
|
+
tier, reason = "allow", ""
|
|
303
|
+
try:
|
|
304
|
+
_reconfig = getattr(sys.stdout, "reconfigure", None)
|
|
305
|
+
if _reconfig:
|
|
306
|
+
try:
|
|
307
|
+
_reconfig(encoding="utf-8")
|
|
308
|
+
except Exception:
|
|
309
|
+
pass
|
|
310
|
+
try:
|
|
311
|
+
data = json.load(sys.stdin)
|
|
312
|
+
except Exception:
|
|
313
|
+
return # malformed payload → fail open, silently
|
|
314
|
+
if not isinstance(data, dict):
|
|
315
|
+
return
|
|
316
|
+
call = data.get("toolCall")
|
|
317
|
+
if not isinstance(call, dict):
|
|
318
|
+
return
|
|
319
|
+
workspaces = data.get("workspacePaths") or []
|
|
320
|
+
root = workspaces[0] if isinstance(workspaces, list) and workspaces else os.getcwd()
|
|
321
|
+
if guard_disabled(str(root)):
|
|
322
|
+
return # kill switch: bypass every tier
|
|
323
|
+
tier, reason = decide(str(call.get("name") or ""), call.get("args"), _load_allow(str(root)))
|
|
324
|
+
if tier == "ask" and deny_only(str(root)):
|
|
325
|
+
tier = "allow" # deny-only: keep catastrophe blocks, never force_ask (no plan interruption)
|
|
326
|
+
except Exception:
|
|
327
|
+
return # ANY unexpected shape/error → no guard, never a crash and never a bad decision
|
|
328
|
+
finally:
|
|
329
|
+
try:
|
|
330
|
+
_emit(tier, reason)
|
|
331
|
+
except Exception:
|
|
332
|
+
pass
|
|
333
|
+
# Always exit 0: a non-zero code from a PreToolUse hook is an agy hook *error*.
|
|
334
|
+
sys.exit(0)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
if __name__ == "__main__":
|
|
338
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "*",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "python guard_agy.py",
|
|
10
|
+
"windows": "python guard_agy.py",
|
|
11
|
+
"linux": "python3 guard_agy.py",
|
|
12
|
+
"osx": "python3 guard_agy.py"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"PreInvocation": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "python warm_start_agy.py",
|
|
21
|
+
"windows": "python warm_start_agy.py",
|
|
22
|
+
"linux": "python3 warm_start_agy.py",
|
|
23
|
+
"osx": "python3 warm_start_agy.py"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"Stop": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "python session_end_agy.py",
|
|
30
|
+
"windows": "python session_end_agy.py",
|
|
31
|
+
"linux": "python3 session_end_agy.py",
|
|
32
|
+
"osx": "python3 session_end_agy.py"
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|