@caddis/cli 0.4.56 → 0.4.57

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.
Files changed (31) hide show
  1. package/bundles/antigravity-plugin/guard_agy.py +700 -68
  2. package/bundles/antigravity-plugin/hooks.json +2 -1
  3. package/bundles/antigravity-plugin/plugin.json +1 -1
  4. package/bundles/antigravity-plugin/scripts/caddis_checkout_guard.py +63 -12
  5. package/bundles/antigravity-plugin/scripts/caddis_gate.py +21 -0
  6. package/bundles/antigravity-plugin/scripts/caddis_lanes.py +35 -0
  7. package/bundles/antigravity-plugin/scripts/caddis_merge_chain.py +67 -22
  8. package/bundles/antigravity-plugin/scripts/caddis_review.py +59 -9
  9. package/bundles/antigravity-plugin/scripts/caddis_roadmap.py +120 -24
  10. package/bundles/antigravity-plugin/scripts/caddis_roadmap_page.py +80 -7
  11. package/bundles/antigravity-plugin/scripts/oss_review.py +300 -79
  12. package/bundles/antigravity-plugin/scripts/setup_project_ai.py +4 -0
  13. package/bundles/antigravity-plugin/session_end_agy.py +13 -10
  14. package/bundles/antigravity-plugin/skills/cloud-phases/SKILL.md +53 -8
  15. package/bundles/antigravity-plugin/skills/cross-review/SKILL.md +11 -8
  16. package/bundles/antigravity-plugin/skills/feature-plan/SKILL.md +1 -1
  17. package/bundles/antigravity-plugin/skills/gh-cli/SKILL.md +9 -5
  18. package/bundles/antigravity-plugin/skills/implement/SKILL.md +18 -11
  19. package/bundles/antigravity-plugin/skills/mmr/SKILL.md +1 -1
  20. package/bundles/antigravity-plugin/skills/pr-merge/SKILL.md +44 -4
  21. package/bundles/antigravity-plugin/skills/review-pr/SKILL.md +13 -4
  22. package/bundles/antigravity-plugin/skills/ship/SKILL.md +26 -7
  23. package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +31 -0
  24. package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +42 -4
  25. package/bundles/antigravity-plugin/warm_start_agy.py +26 -15
  26. package/bundles/antigravity-plugin-extras/plugin.json +1 -1
  27. package/bundles/antigravity-plugin-extras/skills/agent-orchestration/SKILL.md +5 -5
  28. package/bundles/antigravity-plugin-extras/skills/cross-review/SKILL.md +1 -1
  29. package/bundles/codex/.codex/skills/devops/gh-cli/SKILL.md +9 -5
  30. package/bundles/manifest.json +3 -3
  31. package/package.json +1 -1
@@ -61,7 +61,8 @@ def _repo_root(path: str):
61
61
 
62
62
 
63
63
  def _user_artifact_root() -> str:
64
- """Where usage goes when the workspace is in no repository: the user's own folder, never the cwd."""
64
+ """Where usage goes when the workspace is in no repository: the user's own folder, never the cwd.
65
+ The caller writes there only when it already exists."""
65
66
  return _artifact_root(os.path.expanduser("~"))
66
67
 
67
68
 
@@ -121,21 +122,24 @@ def main() -> None:
121
122
  try:
122
123
  roots = _workspace_roots(data) or [os.getcwd()]
123
124
  # A workspace folder may sit inside a repo: the artifact dir is the REPO ROOT's, not the sub-folder's.
124
- resolved = [(_repo_root(r) or str(r)) for r in roots]
125
- # Write the record to every root that ALREADY has a .caddis dir, rather than to
126
- # whichever root happened to be first. A multi-root session's usage belongs to each
127
- # repo that participates in it, and creating .caddis in a repo that never opted in
125
+ # A folder in no repository is skipped: it has no project, and its cwd is never a fallback.
126
+ resolved = [top for top in (_repo_root(r) for r in roots) if top]
127
+ # Write the record only to roots that ALREADY have a .caddis dir. A multi-root session's usage
128
+ # belongs to each repo that participates in it, and creating .caddis in a repo that never opted in
128
129
  # would be caddis littering someone else's tree.
129
130
  targets = []
130
131
  for root in resolved:
131
132
  art = _artifact_root(root)
132
133
  if os.path.isdir(art) and art not in targets:
133
134
  targets.append(art)
135
+ if not resolved:
136
+ # No workspace is in a repository. Usage goes to the user's own folder, and only when
137
+ # that folder already exists. It is never created and never the cwd.
138
+ user_art = _user_artifact_root()
139
+ if os.path.isdir(user_art):
140
+ targets = [user_art]
134
141
  if not targets:
135
- # Nothing has opted in. In a repo, the first root's repo root is the default. Outside any
136
- # repo there is no project to attach usage to, so it goes to the user's folder, not the cwd.
137
- first_repo = _repo_root(roots[0])
138
- targets = [_artifact_root(first_repo) if first_repo else _user_artifact_root()]
142
+ return # nothing has opted in: write nothing
139
143
  record = {
140
144
  "ts": datetime.now(timezone.utc).isoformat(),
141
145
  "event": "session_end",
@@ -146,7 +150,6 @@ def main() -> None:
146
150
  "model": data.get("modelName"),
147
151
  }
148
152
  for art in targets:
149
- os.makedirs(art, exist_ok=True)
150
153
  with open(os.path.join(art, "usage-log.jsonl"), "a", encoding="utf-8") as fh:
151
154
  fh.write(json.dumps(record) + "\n")
152
155
  except Exception as _exc:
@@ -23,6 +23,35 @@ Two modes:
23
23
  With no plan argument, use the active plan (`python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_todo.py" active-plan`,
24
24
  or the one `status: current` plan in `.caddis/plans/`). If there is none, or more than one, ask.
25
25
 
26
+ ## Step 0 — Is this checkout safe to move?
27
+
28
+ <!-- checkout-safety: keep byte-identical in pr-merge, ship-pr, ship-merge and cloud-phases; a test pins it -->
29
+ Some steps below move the files of this checkout (`git rebase`, `switch`, `checkout <branch>`, `pull`, `merge`,
30
+ `stash`, `reset --hard`). Before the first of them, run:
31
+ ```
32
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_checkout_guard.py" shared --root .
33
+ ```
34
+ It prints `SHARED: yes <reason>` or `SHARED: no` and exits 0. It says yes when the repo's `AGENTS.md` has a line
35
+ `Shared checkout: yes` (a dev service serves from this checkout), or when `git status` lists a change. With no
36
+ `--own-files`, every change counts as another session's. A linked worktree is never shared.
37
+
38
+ - **On `SHARED: no`,** do the steps as written, in this checkout.
39
+ - **On `SHARED: yes`,** never move this checkout. Leave its changes as they are, and report them. Instead:
40
+ - **Rebase:** `git worktree add --detach "$TEMP/<repo>-rebase" <branch>`, then
41
+ `git -C <worktree> rebase origin/<default>` and `git -C <worktree> push --force-with-lease origin HEAD:<branch>`.
42
+ - **Pull a branch to test it:** `git fetch origin`, then
43
+ `git worktree add --detach "$TEMP/<repo>-<branch>" origin/<branch>` (for the default branch,
44
+ `origin/<default>`), and run the gates there. Install dependencies fresh; never link `node_modules` or a venv.
45
+ - **Clean up after a merge:** do not switch or pull. Run `git fetch --prune`, then
46
+ `git fetch origin <default>:<default>`: it fast-forwards the default branch with no checkout. Git refuses when a
47
+ checkout has the default branch out; then leave it and report it. Delete the local feature branch only when no
48
+ checkout has it out.
49
+ - Remove each worktree you made when its step ends: `git worktree remove <worktree>`.
50
+
51
+ `$TEMP` is `%TEMP%` (`$env:TEMP` in PowerShell). The caddis guard checks only the `Shared checkout: yes` line in
52
+ `AGENTS.md`, not a dirty tree: with that line, it asks before a tree move, and denies it in a headless session.
53
+ <!-- /checkout-safety -->
54
+
26
55
  ## Step 1 — check the preconditions (stop on any failure)
27
56
 
28
57
  ```
@@ -35,7 +64,11 @@ gh auth status
35
64
  - `origin` must be a GitHub repository. Cloud sessions clone from GitHub; a repo with no GitHub
36
65
  remote cannot use this command. Say so and stop.
37
66
  - The current branch must be the plan's feature branch, never the default branch. The cloud PRs
38
- target this branch, and it goes to `main` later through the normal `/caddis:ship-pr`.
67
+ target this branch, and it goes to the default branch later through the normal `/caddis:ship-pr`.
68
+ Read the default branch as a bare name: `git rev-parse --abbrev-ref origin/HEAD` prints
69
+ `origin/<default>`, so strip the `origin/` prefix before you compare
70
+ (`d=$(git rev-parse --abbrev-ref origin/HEAD) && d=${d#origin/}`; when origin/HEAD is not set,
71
+ `gh repo view --json defaultBranchRef -q .defaultBranchRef.name`).
39
72
  - Tracked changes must be committed. A cloud session cannot see uncommitted work.
40
73
  - `gh` must be signed in as an account that can push to the repository and open PRs.
41
74
 
@@ -152,14 +185,26 @@ gh pr list --base <feature branch> --state open --json number,title,headRefName,
152
185
  For each PR from a `<prefix>/phase-<NN>` branch, one at a time:
153
186
  1. Read the whole diff (`gh pr diff <N>`). Check it against the phase's Design and Touches. A file
154
187
  outside Touches is a finding.
155
- 2. Wait for CI: `gh pr checks <N> --watch` (or the ci-watch lane, if this caddis has it).
188
+ 2. Wait for CI outside the session:
189
+ `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_lanes.py" ci-watch --pr <N>`. Only `RESULT: pass`
190
+ counts; `HEAD:` names the watched head. A `gh pr checks` table is not proof: it can show an older
191
+ head's checks.
156
192
  3. Review under the three rules (rule 3, review by tier): stop at the first CLEAN; after 3 rounds,
157
- ask the owner. A fix goes back to the same cloud session or is made locally on the PR branch.
158
- 4. Just before merging, re-read `gh pr view <N> --json headRefOid,mergeStateStatus`. Merge only if
159
- the head SHA is the one you reviewed and CI passed on it:
160
- `gh pr merge <N> --squash --delete-branch`.
161
- 5. Pull: `git pull --ff-only origin <feature branch>`.
162
- 6. Run the phase's exit gate locally, and after the last PR, the full test suite.
193
+ ask the owner. A fix goes back to the same cloud session or is made locally on the PR branch. A
194
+ fix pushes a new head: run ci-watch again.
195
+ 4. Just before merging, check all three, then merge that head only:
196
+ - the last ci-watch printed `RESULT: pass`, and its `HEAD:` is the head you reviewed;
197
+ - `gh pr view <N> --json headRefOid,mergeStateStatus` still shows that head;
198
+ - no CI run on the default branch is in progress:
199
+ `gh run list --branch <default branch> --json status` lists no `in_progress` or `queued` run
200
+ (wait, then check again).
201
+
202
+ `gh pr merge <N> --squash --delete-branch --match-head-commit <watched head>` (the full SHA).
203
+ GitHub refuses the merge if the head moved. A merge on a head match alone merged a red PR (#34).
204
+ 5. Pull: `git pull --ff-only origin <feature branch>`. In a shared checkout (Step 0 says `SHARED: yes`), never
205
+ pull here: make a detached worktree of `origin/<feature branch>`, as Step 0 says, and run point 6 there.
206
+ 6. Run the phase's exit gate locally: the tests of the touched files, in the foreground, each command under
207
+ 10 minutes. Do not start the full suite in the background. CI runs it on every PR.
163
208
  7. Update the phase's Tracker row: status, PR number, merge commit, review result. Set the prompt's
164
209
  frontmatter `status: done` so `/caddis:handoff` tidies it.
165
210
 
@@ -86,23 +86,26 @@ now means the narrower case — a **single file** too big to split, or more batc
86
86
  see it, the fix is usually to review that one file separately, not to narrow `--range`; don't raise
87
87
  `--max-diff-chars` blind.
88
88
 
89
- **Do not alternate providers on a schedule.** DeepSeek is the default because it has actually found real
90
- bugs that same-vendor passes missed; GLM is the documented fallback, used automatically when DeepSeek is
91
- unavailable and you did not name a provider. The policy lives in `oss_review.py`'s `FALLBACK_PROVIDERS`
92
- comment — a rota that sends half your phases to a provider observed timing out three phases running buys
93
- variety it never actually collects, and a review that silently does not happen is the same failure class
94
- as a diff that silently is not read. Name a provider explicitly only when you have a reason.
89
+ **Do not alternate providers on a schedule.** The order is the owner policy: **DeepSeek, then agy Argon,
90
+ then GLM** (`REVIEW_ORDER` in `oss_review.py`). A named `--provider` goes first; on a provider failure
91
+ (HTTP 429, any 5xx, a timeout, a connection error, an empty reply) the tool moves to the next one in
92
+ that order and prints which provider gave the verdict. A provider with no key is skipped. agy runs
93
+ through `caddis_spend.py run` (the spend cap); if the cap refuses, agy is skipped. On a 429 the tool
94
+ waits for `Retry-After` once (at most 60 s); it never splits the diff because of a 429 or an empty
95
+ reply. A custom endpoint (`--base-url`/`--model`) never falls back. Name a provider only when you have
96
+ a reason.
95
97
 
96
98
  ## Interpret the exit code
97
99
  - **0 — REVIEW: CLEAN** → no blocking issues. Proceed.
98
100
  - **1 — REVIEW: BLOCKING** → read the printed findings, then **FIX each blocking item** (or explicitly
99
101
  justify why one is a false positive). Fix it, then re-review only the fix diff. After 3 rounds, ask the owner (rule 3).
100
102
  - **4 — error** → no verdict parsed, or a git/endpoint failure. Read stderr; do NOT treat as clean.
101
- - **3 — misconfigured** → `REVIEW_API_KEY` is unset. Set it (see Prerequisite) and re-run.
103
+ - **3 — could not run** → no key (see Prerequisite), the secret filter refused, or every provider
104
+ failed (429, 5xx, timeout, empty reply). Read stderr. A provider failure is not a verdict: re-run later.
102
105
 
103
106
  ## Three rules: one attempt, one review
104
107
  1. **Think before you send.** Before a task goes to any coder, write its edge cases and check the facts it depends on (one query or one file read).
105
- 2. **Run it before review.** The coder runs the same checks as CI (name the exact commands in the task). Then run the change once on real data.
108
+ 2. **Run it before review.** The coder runs only the tests of the files it touched, in the foreground, each command under 10 minutes (split into parts if needed). Name the exact commands in the task. Never start the full suite in the background and never wait on a monitor for it: CI runs the full suite on every PR. Then run the change once on real data.
106
109
  3. **Review by tier.** Run `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_gate.py" review-tier --range <base>...HEAD` (add `--plan <plan> --phase <N>` inside a plan; no `TIER:` line means tier 2).
107
110
  Tier 0: gates only. Tier 1-small: your self-check only. Tier 1: one review by another vendor; stop at
108
111
  the first CLEAN. Tier 2: two reviewers of two other vendors, in one round. Fix blocking findings only;
@@ -270,7 +270,7 @@ yourself (relay + Tracker) before stopping — do not hand back "run `/caddis:ha
270
270
  ## Execution protocol (standing rules — this plan is the memory, not anyone's head)
271
271
  Three rules: one attempt, one review.
272
272
  1. **Think before you send.** Before a task goes to any coder, write its edge cases and check the facts it depends on (one query or one file read).
273
- 2. **Run it before review.** The coder runs the same checks as CI (name the exact commands in the task). Then run the change once on real data.
273
+ 2. **Run it before review.** The coder runs only the tests of the files it touched, in the foreground, each command under 10 minutes (split into parts if needed). Name the exact commands in the task. Never start the full suite in the background and never wait on a monitor for it: CI runs the full suite on every PR. Then run the change once on real data.
274
274
  3. **Review by tier.** Run `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_gate.py" review-tier --range <base>...HEAD` (add `--plan <plan> --phase <N>` inside a plan; no `TIER:` line means tier 2).
275
275
  Tier 0: gates only. Tier 1-small: your self-check only. Tier 1: one review by another vendor; stop at
276
276
  the first CLEAN. Tier 2: two reviewers of two other vendors, in one round. Fix blocking findings only;
@@ -73,7 +73,10 @@ gh pr diff 55
73
73
  gh pr checkout 55
74
74
  gh pr review 55 --approve --body "LGTM!"
75
75
  gh pr review 55 --request-changes --body "Please fix error handling"
76
- gh pr merge 55 --squash --delete-branch
76
+ # merge only on ci-watch RESULT: pass, only its HEAD:, with no default-branch run in progress
77
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_lanes.py" ci-watch --pr 55
78
+ gh run list --branch main --json status # no in_progress or queued run (main = the default branch)
79
+ gh pr merge 55 --squash --delete-branch --match-head-commit <watched head>
77
80
 
78
81
  # Manage
79
82
  gh pr ready 55 # Mark draft as ready
@@ -170,8 +173,9 @@ gh issue develop 42 --branch fix/issue-42
170
173
  git add src/auth/login.py tests/test_login.py # named files only, never `git add .`
171
174
  git commit -m "fix: resolve login bug" -- src/auth/login.py tests/test_login.py && git push -u origin HEAD
172
175
  gh pr create --title "fix: resolve login bug" --body "Closes #42"
173
- gh pr checks --watch
174
- gh pr merge --squash --delete-branch
176
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_lanes.py" ci-watch --pr <n> # RESULT: pass, HEAD: <watched head>
177
+ gh run list --branch main --json status # wait while a default-branch run is in progress
178
+ gh pr merge <n> --squash --delete-branch --match-head-commit <watched head>
175
179
  ```
176
180
 
177
181
  ### Release Workflow
@@ -189,8 +193,8 @@ gh release upload v1.3.0 ./dist/*.tar.gz
189
193
  | Create repo | `gh repo create name --public` |
190
194
  | Create issue | `gh issue create --title "..." --labels bug` |
191
195
  | Create PR | `gh pr create --title "..." --reviewer user` |
192
- | Merge PR | `gh pr merge 55 --squash --delete-branch` |
196
+ | Merge PR | `gh pr merge 55 --squash --delete-branch --match-head-commit <watched head>`, only after ci-watch `RESULT: pass` |
193
197
  | Create release | `gh release create v1.0.0 --generate-notes` |
194
- | Watch CI | `gh run watch` |
198
+ | Watch CI | `caddis_lanes.py ci-watch --pr 55` (fallback: `gh run watch`) |
195
199
  | Set secret | `echo "val" \| gh secret set KEY` |
196
200
  | API call | `gh api /repos/o/r --jq '.description'` |
@@ -32,8 +32,8 @@ These override everything below. They exist because branch isolation and the run
32
32
  depend on them:
33
33
 
34
34
  - **Work ONLY on the current branch.** You are already on the feature branch (the current branch,
35
- `git branch --show-current`; that read is the only `git branch` form allowed). **NEVER** run
36
- `git checkout`, `git switch`, `git branch <name>`, `git switch -c`, or any command that changes,
35
+ `git branch --show-current`; that read is the only `git branch` form allowed).
36
+ **NEVER** run `git checkout`, `git switch`, `git branch <name>`, `git switch -c`, or any command that changes,
37
37
  creates, or leaves the current branch. Read the default branch with
38
38
  `git symbolic-ref --short refs/remotes/origin/HEAD`, which prints `origin/<default>`: remove the
39
39
  `origin/` prefix before you compare. If that command fails (origin/HEAD is not set), the default
@@ -57,7 +57,7 @@ depend on them:
57
57
 
58
58
  ## Three rules: one attempt, one review
59
59
  1. **Think before you send.** Before a task goes to any coder, write its edge cases and check the facts it depends on (one query or one file read).
60
- 2. **Run it before review.** The coder runs the same checks as CI (name the exact commands in the task). Then run the change once on real data.
60
+ 2. **Run it before review.** The coder runs only the tests of the files it touched, in the foreground, each command under 10 minutes (split into parts if needed). Name the exact commands in the task. Never start the full suite in the background and never wait on a monitor for it: CI runs the full suite on every PR. Then run the change once on real data.
61
61
  3. **Review by tier.** Run `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_gate.py" review-tier --range <base>...HEAD` (add `--plan <plan> --phase <N>` inside a plan; no `TIER:` line means tier 2).
62
62
  Tier 0: gates only. Tier 1-small: your self-check only. Tier 1: one review by another vendor; stop at
63
63
  the first CLEAN. Tier 2: two reviewers of two other vendors, in one round. Fix blocking findings only;
@@ -185,12 +185,17 @@ row, visible to any later reader without anyone having to catch it live.
185
185
  this wording without `--allow-load`. Only the owner gives that flag.
186
186
  7b. **Write test economy into every agent brief, not only the lane prompt.** The script already adds it
187
187
  to its lane prompts. A brief you write yourself (an Agent-tool subagent, a peer session) needs the same
188
- five lines: after each step run only the tests for the file or module you changed; run the full suite only
189
- for the baseline and once at the end; run no review loop inside the agent (you review); report progress
190
- every 20 minutes; never start a second heavy test run while one is still running. **Two parallel agents
191
- must not both run full suites.** Measured 2026-10-02 on one shared box: a 2,063-test suite took 2.7 min
188
+ five lines: after each step run only the tests for the file or module you changed, in the foreground, each
189
+ command under 10 minutes; never start the full suite, and never wait on a monitor for it (CI runs it on every PR);
190
+ run no review loop inside the agent (you review); report progress
191
+ every 20 minutes; never start a second heavy test run while one is still running. **Agents never run
192
+ the full suite.** Measured 2026-10-02 on one shared box: a 2,063-test suite took 2.7 min
192
193
  alone and 6.7 min beside a second test-heavy agent, and one agent's brief (baseline, tests after each of 9
193
194
  module switches, full gates, in-agent review) cost 30-40 of its 59 minutes.
195
+ On 18-19 Oct 2026 agents started the full suite (about 40 min) in the background and waited through
196
+ monitors with their own timeouts. One missed the finish and sat "Waiting" for over 1 h. Four parallel
197
+ suites also slowed the box. The orchestrator checks a stalled agent by process (pytest alive, CPU moving),
198
+ not by its monitor.
194
199
  8. Record these Tracker values:
195
200
  - `Model` = the packet's `model`.
196
201
  - `Lane` = the lanes tried, joined with ` -> ` and followed by the reason. For example,
@@ -288,10 +293,12 @@ You: /clear, then paste ↓
288
293
  **In headless/runner mode, never ask — halt, write the block, and exit.** The block is how a
289
294
  non-interactive run tells a human what it needs; asking is still forbidden.
290
295
 
291
- **4. Run the tests yourself.** After the last phase you complete, run the project's full test command once
292
- more and record the real result. The runner will re-run it independently and that decides success — but
293
- you run it too so your reported `"tests"` value is truthful, not assumed. Never report `"passed"` on a
294
- suite you did not see go green.
296
+ **4. Run the tests yourself.** After the last phase you complete, run the tests of the files you touched
297
+ once more, in the foreground, each command under 10 minutes (split into parts if needed), and record the
298
+ real result. Never start the full suite in the background and never wait on a monitor for it. CI runs the
299
+ full suite on every PR and is the gate before merge. The runner re-runs its own gate independently and
300
+ that decides success — but you run your tests too so your reported `"tests"` value is truthful, not
301
+ assumed. Never report `"passed"` on tests you did not see go green.
295
302
 
296
303
  **4b. If every phase is now done, tidy the plan into `done/` IN THE SAME COMMIT.** Only when the
297
304
  whole plan is finished (every `## Tracker` row is `done`, no halt, no blocker) — a mid-plan halt is
@@ -22,7 +22,7 @@ The exit codes follow `caddis_exit.py`: 0 clean, 1 blocked, 2 advisory, 3 could
22
22
 
23
23
  ## Three rules: one attempt, one review
24
24
  1. **Think before you send.** Before a task goes to any coder, write its edge cases and check the facts it depends on (one query or one file read).
25
- 2. **Run it before review.** The coder runs the same checks as CI (name the exact commands in the task). Then run the change once on real data.
25
+ 2. **Run it before review.** The coder runs only the tests of the files it touched, in the foreground, each command under 10 minutes (split into parts if needed). Name the exact commands in the task. Never start the full suite in the background and never wait on a monitor for it: CI runs the full suite on every PR. Then run the change once on real data.
26
26
  3. **Review by tier.** Run `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_gate.py" review-tier --range <base>...HEAD` (add `--plan <plan> --phase <N>` inside a plan; no `TIER:` line means tier 2).
27
27
  Tier 0: gates only. Tier 1-small: your self-check only. Tier 1: one review by another vendor; stop at
28
28
  the first CLEAN. Tier 2: two reviewers of two other vendors, in one round. Fix blocking findings only;
@@ -9,7 +9,7 @@ End-to-end delivery of feature-branch work through the reviewed lane in one shot
9
9
  push safely, create or update the Pull Request, monitor CI checks to green, merge into the default
10
10
  branch, validate the deployment, and clean up the feature branch.
11
11
 
12
- For a hotfix straight to the default branch with no PR ceremony, use `/ship` (the express lane) instead.
12
+ For a quick commit and push of a feature branch, use `/ship` (the express lane). It never pushes the default branch.
13
13
  To open a PR and stop at green for human review without merging, use `/ship-pr`.
14
14
  To merge an already-opened PR, use `/ship-merge`.
15
15
 
@@ -29,11 +29,47 @@ can be provided.
29
29
  Read `AGENTS.md` + the detected workflow file(s) first for the repo's real gate names, default
30
30
  branch, and merge conventions — never assume `main`, never assume job names.
31
31
 
32
+ ## Step 0b — Is this checkout safe to move?
33
+
34
+ <!-- checkout-safety: keep byte-identical in pr-merge, ship-pr, ship-merge and cloud-phases; a test pins it -->
35
+ Some steps below move the files of this checkout (`git rebase`, `switch`, `checkout <branch>`, `pull`, `merge`,
36
+ `stash`, `reset --hard`). Before the first of them, run:
37
+ ```
38
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_checkout_guard.py" shared --root .
39
+ ```
40
+ It prints `SHARED: yes <reason>` or `SHARED: no` and exits 0. It says yes when the repo's `AGENTS.md` has a line
41
+ `Shared checkout: yes` (a dev service serves from this checkout), or when `git status` lists a change. With no
42
+ `--own-files`, every change counts as another session's. A linked worktree is never shared.
43
+
44
+ - **On `SHARED: no`,** do the steps as written, in this checkout.
45
+ - **On `SHARED: yes`,** never move this checkout. Leave its changes as they are, and report them. Instead:
46
+ - **Rebase:** `git worktree add --detach "$TEMP/<repo>-rebase" <branch>`, then
47
+ `git -C <worktree> rebase origin/<default>` and `git -C <worktree> push --force-with-lease origin HEAD:<branch>`.
48
+ - **Pull a branch to test it:** `git fetch origin`, then
49
+ `git worktree add --detach "$TEMP/<repo>-<branch>" origin/<branch>` (for the default branch,
50
+ `origin/<default>`), and run the gates there. Install dependencies fresh; never link `node_modules` or a venv.
51
+ - **Clean up after a merge:** do not switch or pull. Run `git fetch --prune`, then
52
+ `git fetch origin <default>:<default>`: it fast-forwards the default branch with no checkout. Git refuses when a
53
+ checkout has the default branch out; then leave it and report it. Delete the local feature branch only when no
54
+ checkout has it out.
55
+ - Remove each worktree you made when its step ends: `git worktree remove <worktree>`.
56
+
57
+ `$TEMP` is `%TEMP%` (`$env:TEMP` in PowerShell). The caddis guard checks only the `Shared checkout: yes` line in
58
+ `AGENTS.md`, not a dirty tree: with that line, it asks before a tree move, and denies it in a headless session.
59
+ <!-- /checkout-safety -->
60
+
32
61
  ## Step 1 — Refuse on the default branch
33
62
 
34
- A PR is *branch → default*. If HEAD **is** the default branch, stop and point at `/ship` — there is
35
- nothing to open a PR from. (Offer to create a feature branch from the current work if the changes
36
- are uncommitted.)
63
+ A PR is *branch → default*. Read the default branch as a bare name: `git rev-parse --abbrev-ref
64
+ origin/HEAD` prints `origin/<default>`, so strip the `origin/` prefix before you compare.
65
+ ```bash
66
+ d=$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null) && d=${d#origin/} # prints origin/<default>: strip origin/
67
+ [ -n "$d" ] || d=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name) # origin/HEAD not set
68
+ git branch --show-current # compare with "$d"
69
+ ```
70
+ If HEAD **is** the default branch, stop: there is nothing to open a PR from. Offer to move the
71
+ uncommitted work to a feature branch (`git switch -c <branch>`), or run `/ship`, which does that and
72
+ hands back here.
37
73
 
38
74
  ## Step 2 — Preflight (local gates mirroring CI)
39
75
 
@@ -69,6 +105,8 @@ Run `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_gate.py" review-tier --range <
69
105
  git rebase <default-branch>
70
106
  git push --force-with-lease origin <branch> # never bare --force
71
107
  ```
108
+ - In a shared checkout (Step 0b says `SHARED: yes`), never rebase here: rebase in a detached worktree, as
109
+ Step 0b says.
72
110
 
73
111
  ## Step 5 — Push + create or update the PR
74
112
 
@@ -141,6 +179,8 @@ If the default branch triggers an automated deployment pipeline:
141
179
  **Only after** the merge is complete and deployment (if any) is validated green:
142
180
  - Run `git status --short`. If it shows changes you did not make, do not switch, stash, reset or commit:
143
181
  leave them, report them, and stop cleanup here (a shared checkout often holds a peer's edits).
182
+ - In a shared checkout (Step 0b says `SHARED: yes`), skip the switch and the pull below. Clean up as Step 0b
183
+ says: `git fetch --prune`, `git fetch origin <default-branch>:<default-branch>`, then delete the branches.
144
184
  - Fetch and prune: `git fetch --prune`.
145
185
  - Switch to the default branch: `git switch <default-branch>`.
146
186
  - Fast-forward to the merge commit: `git pull --ff-only origin <default-branch>`.
@@ -34,8 +34,11 @@ gh pr view <n> --repo <owner/repo> --json number,title,author,state,isDraft,merg
34
34
  - the PR is **merged or closed** — there is nothing to decide;
35
35
  - it is a **draft** — say so and ask whether to continue;
36
36
  - **the operator is the author.** GitHub does not allow approving your own PR. Say it plainly, and
37
- say the alternatives: have someone else approve it, or merge with an admin bypass
38
- (`gh pr merge <n> --squash --admin`). Do not silently produce a review they cannot submit.
37
+ say the alternatives: have someone else approve it, or merge with an admin bypass. An admin
38
+ bypass skips the approval only, never the CI rule: merge only after `caddis_lanes.py ci-watch
39
+ --pr <n>` prints `RESULT: pass`, with no default-branch run in progress (section 4)
40
+ (`gh pr merge <n> --squash --admin --match-head-commit <watched head>`). Do not silently produce a
41
+ review they cannot submit.
39
42
 
40
43
  ## 2. Gather what the decision needs
41
44
 
@@ -85,12 +88,18 @@ Lead with the verdict. Keep it short enough to read before deciding:
85
88
  - **CI** — green, red, or still running. Name the failing check.
86
89
  - **Merge state** — and what would clear it.
87
90
 
88
- Then print the exact commands, and stop:
91
+ Then print the exact commands, and stop. The merge follows the CI rule: wait with ci-watch, and
92
+ merge only on `RESULT: pass`, only the head it watched (`HEAD:`), and never while a CI run on the
93
+ default branch is in progress.
89
94
 
90
95
  ```bash
91
96
  # approve
92
97
  gh pr review <n> --repo <owner/repo> --approve
93
- gh pr merge <n> --repo <owner/repo> --squash --delete-branch
98
+ # wait for CI; merge only on RESULT: pass. HEAD: is the watched head
99
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_lanes.py" ci-watch --pr <n> --repo <owner/repo>
100
+ # no in_progress or queued run may be listed
101
+ gh run list --repo <owner/repo> --branch <default branch> --json status
102
+ gh pr merge <n> --repo <owner/repo> --squash --delete-branch --match-head-commit <watched head>
94
103
 
95
104
  # or ask for changes
96
105
  gh pr review <n> --repo <owner/repo> --request-changes --body "..."
@@ -9,11 +9,12 @@ Ship the current working changes via this repo's actual delivery pipeline. The p
9
9
  repo, so **detect it first**, then follow the matching lane. Preflight, staging, and commit are
10
10
  identical across lanes; only monitoring and validation differ.
11
11
 
12
- > **This is the express lane** — it pushes straight to the default branch with no review pause.
13
- > Right for a hotfix. On a repo where that push deploys to production, Step 2c stops it before the
14
- > push and the owner merges. **For feature work, use the reviewed lane instead: `/ship-pr`** (open a
15
- > PR, monitor CI, stop at green) **→ `/ship-merge`** (merge the green PR behind a deploy-confirm,
16
- > validate, clean up).
12
+ > **This is the express lane** — commit, push the current feature branch and watch its CI, with no
13
+ > review pause. **It never pushes to the default branch.** On the default branch (Gitea or GitHub lane)
14
+ > it moves the change to a new branch and hands over to the reviewed lane: **`/caddis:ship-pr`** (open
15
+ > a PR, monitor CI, stop at green) **→ `/caddis:ship-merge`** (merge the green PR behind a
16
+ > deploy-confirm, validate, clean up). Why: a direct push skips the PR, the ci-watch `RESULT: pass` on
17
+ > the watched head and the check that no default-branch run is in progress.
17
18
 
18
19
  Optional message: **$ARGUMENTS** (if empty, derive a conventional commit message from staged changes)
19
20
 
@@ -36,6 +37,22 @@ names, branch, and service identifiers — never assume.
36
37
  - Tests: run only if the user says they're needed or a gate explicitly requires them
37
38
  - Fix any failures before proceeding. Do not commit broken code.
38
39
 
40
+ **1b. BRANCH CHECK** — Read the default branch as a bare name (don't assume `main`) and the current
41
+ branch. `git rev-parse --abbrev-ref origin/HEAD` prints `origin/<default>`, so strip the `origin/`
42
+ prefix before you compare, or the check never matches:
43
+ ```bash
44
+ d=$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null) && d=${d#origin/} # prints origin/<default>: strip origin/
45
+ [ -n "$d" ] || d=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name) # origin/HEAD not set
46
+ git branch --show-current # compare with "$d"
47
+ ```
48
+ - **On a non-default branch:** continue at Step 2, then Step 3 pushes that branch.
49
+ - **On the default branch, Gitea or GitHub lane:** do not commit here. Run `git switch -c ship/<slug>`
50
+ (the uncommitted changes go with you) and commit on it (Step 2). Then **STOP: `/ship` ends here.**
51
+ Do not run Step 3 or later. Hand over to `/caddis:ship-pr` → `/caddis:ship-merge`.
52
+ A direct push to the default branch is refused: it skips the PR, the CI pass on the watched head
53
+ and the in-progress check.
54
+ - **Local-only lane:** there is no CI and no PR; commit where you are.
55
+
39
56
  **2. STAGE + COMMIT** — Stage only the intended files, by name (never `git add -A`, `git add .`, `git add -u`, a folder, or `git commit -a`):
40
57
  ```
41
58
  git status --short # confirm what's staged
@@ -55,10 +72,12 @@ Run `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_gate.py" review-tier --range <
55
72
  `<base>` is `origin/<default branch>` (on the default branch itself, the commit before this change). Tier 0 or 1-small: no model review. Tier 1: one `/caddis:cross-review`. Tier 2: two reviewers of two other vendors in one round. No `TIER:` line means tier 2.
56
73
  <!-- /shared:cross-review-trigger -->
57
74
 
58
- **3. PUSH** — to the repo's default branch (confirm the branch first; don't assume `main`):
75
+ **3. PUSH** — only when Step 1b found a non-default branch (on the default branch, `/ship` stopped at Step 2):
59
76
  ```
60
- git push origin <branch>
77
+ git push -u origin <feature branch>
61
78
  ```
79
+ Never `git push origin <default branch>`: the change reaches the default branch only through
80
+ `/caddis:ship-pr` → `/caddis:ship-merge`, which merge on ci-watch `RESULT: pass` at the watched head.
62
81
  Confirm the push succeeded and the SHA is accepted by the remote. (Local-only lane: skip if there is
63
82
  no remote, and note that nothing was pushed.)
64
83
 
@@ -28,6 +28,35 @@ Resolve the target PR from `$ARGUMENTS`, else the current branch's open PR. No o
28
28
  that and point at `/ship-pr`. Read `AGENTS.md` + the workflow file(s) for the repo's real deploy
29
29
  job names and default branch — never assume.
30
30
 
31
+ ## Step 0b — Is this checkout safe to move?
32
+
33
+ <!-- checkout-safety: keep byte-identical in pr-merge, ship-pr, ship-merge and cloud-phases; a test pins it -->
34
+ Some steps below move the files of this checkout (`git rebase`, `switch`, `checkout <branch>`, `pull`, `merge`,
35
+ `stash`, `reset --hard`). Before the first of them, run:
36
+ ```
37
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_checkout_guard.py" shared --root .
38
+ ```
39
+ It prints `SHARED: yes <reason>` or `SHARED: no` and exits 0. It says yes when the repo's `AGENTS.md` has a line
40
+ `Shared checkout: yes` (a dev service serves from this checkout), or when `git status` lists a change. With no
41
+ `--own-files`, every change counts as another session's. A linked worktree is never shared.
42
+
43
+ - **On `SHARED: no`,** do the steps as written, in this checkout.
44
+ - **On `SHARED: yes`,** never move this checkout. Leave its changes as they are, and report them. Instead:
45
+ - **Rebase:** `git worktree add --detach "$TEMP/<repo>-rebase" <branch>`, then
46
+ `git -C <worktree> rebase origin/<default>` and `git -C <worktree> push --force-with-lease origin HEAD:<branch>`.
47
+ - **Pull a branch to test it:** `git fetch origin`, then
48
+ `git worktree add --detach "$TEMP/<repo>-<branch>" origin/<branch>` (for the default branch,
49
+ `origin/<default>`), and run the gates there. Install dependencies fresh; never link `node_modules` or a venv.
50
+ - **Clean up after a merge:** do not switch or pull. Run `git fetch --prune`, then
51
+ `git fetch origin <default>:<default>`: it fast-forwards the default branch with no checkout. Git refuses when a
52
+ checkout has the default branch out; then leave it and report it. Delete the local feature branch only when no
53
+ checkout has it out.
54
+ - Remove each worktree you made when its step ends: `git worktree remove <worktree>`.
55
+
56
+ `$TEMP` is `%TEMP%` (`$env:TEMP` in PowerShell). The caddis guard checks only the `Shared checkout: yes` line in
57
+ `AGENTS.md`, not a dirty tree: with that line, it asks before a tree move, and denies it in a headless session.
58
+ <!-- /checkout-safety -->
59
+
31
60
  ## Step 1 — Refuse unless green + mergeable + reviewed
32
61
 
33
62
  All of, per the repo's own rules:
@@ -102,6 +131,8 @@ attempt a silent re-deploy — and **skip cleanup** (Step 6).
102
131
  **Only after** Step 5 is green:
103
132
  - Run `git status --short`. If it shows changes you did not make, do not switch, stash, reset or commit:
104
133
  leave them, report them, and stop cleanup here.
134
+ - In a shared checkout (Step 0b says `SHARED: yes`), do not switch or pull. Clean up as Step 0b says:
135
+ `git fetch --prune`, `git fetch origin <default-branch>:<default-branch>`, then delete the branches.
105
136
  - `git fetch --prune`, then switch to the default branch and fast-forward it to the merge commit.
106
137
  - Delete the **remote** feature branch; delete the **local** feature branch (for a squash merge, compare
107
138
  the branch tree with the merged tree first, not ancestry).
@@ -10,7 +10,7 @@ after. This command does the mechanics only: push safely, open (or update) the P
10
10
  and **stop at green** with a mergeability verdict. It **never merges and never deploys** — on these
11
11
  repos merge == deploy, so the merge stays a separate, deliberate act: `/ship-merge`.
12
12
 
13
- For a hotfix straight to the default branch, use `/ship` (the express lane) instead.
13
+ For a quick commit and push of a feature branch, use `/ship` (the express lane). It never pushes the default branch.
14
14
 
15
15
  Optional argument: **$ARGUMENTS** (PR title override; default = derived from the branch's
16
16
  conventional-commit summary).
@@ -27,11 +27,47 @@ conventional-commit summary).
27
27
  Read `AGENTS.md` + the detected workflow file(s) first for the repo's real gate names, default
28
28
  branch, and merge conventions — never assume `main`, never assume job names.
29
29
 
30
+ ## Step 0b — Is this checkout safe to move?
31
+
32
+ <!-- checkout-safety: keep byte-identical in pr-merge, ship-pr, ship-merge and cloud-phases; a test pins it -->
33
+ Some steps below move the files of this checkout (`git rebase`, `switch`, `checkout <branch>`, `pull`, `merge`,
34
+ `stash`, `reset --hard`). Before the first of them, run:
35
+ ```
36
+ python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_checkout_guard.py" shared --root .
37
+ ```
38
+ It prints `SHARED: yes <reason>` or `SHARED: no` and exits 0. It says yes when the repo's `AGENTS.md` has a line
39
+ `Shared checkout: yes` (a dev service serves from this checkout), or when `git status` lists a change. With no
40
+ `--own-files`, every change counts as another session's. A linked worktree is never shared.
41
+
42
+ - **On `SHARED: no`,** do the steps as written, in this checkout.
43
+ - **On `SHARED: yes`,** never move this checkout. Leave its changes as they are, and report them. Instead:
44
+ - **Rebase:** `git worktree add --detach "$TEMP/<repo>-rebase" <branch>`, then
45
+ `git -C <worktree> rebase origin/<default>` and `git -C <worktree> push --force-with-lease origin HEAD:<branch>`.
46
+ - **Pull a branch to test it:** `git fetch origin`, then
47
+ `git worktree add --detach "$TEMP/<repo>-<branch>" origin/<branch>` (for the default branch,
48
+ `origin/<default>`), and run the gates there. Install dependencies fresh; never link `node_modules` or a venv.
49
+ - **Clean up after a merge:** do not switch or pull. Run `git fetch --prune`, then
50
+ `git fetch origin <default>:<default>`: it fast-forwards the default branch with no checkout. Git refuses when a
51
+ checkout has the default branch out; then leave it and report it. Delete the local feature branch only when no
52
+ checkout has it out.
53
+ - Remove each worktree you made when its step ends: `git worktree remove <worktree>`.
54
+
55
+ `$TEMP` is `%TEMP%` (`$env:TEMP` in PowerShell). The caddis guard checks only the `Shared checkout: yes` line in
56
+ `AGENTS.md`, not a dirty tree: with that line, it asks before a tree move, and denies it in a headless session.
57
+ <!-- /checkout-safety -->
58
+
30
59
  ## Step 1 — Refuse on the default branch
31
60
 
32
- A PR is *branch → default*. If HEAD **is** the default branch, stop and point at `/ship` — there is
33
- nothing to open a PR from. (Offer to create a feature branch from the current work if the changes
34
- are uncommitted.)
61
+ A PR is *branch → default*. Read the default branch as a bare name: `git rev-parse --abbrev-ref
62
+ origin/HEAD` prints `origin/<default>`, so strip the `origin/` prefix before you compare.
63
+ ```bash
64
+ d=$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null) && d=${d#origin/} # prints origin/<default>: strip origin/
65
+ [ -n "$d" ] || d=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name) # origin/HEAD not set
66
+ git branch --show-current # compare with "$d"
67
+ ```
68
+ If HEAD **is** the default branch, stop: there is nothing to open a PR from. Offer to move the
69
+ uncommitted work to a feature branch (`git switch -c <branch>`), or run `/ship`, which does that and
70
+ hands back here.
35
71
 
36
72
  ## Step 2 — Preflight (local gates mirroring CI)
37
73
 
@@ -108,6 +144,8 @@ Run `python "${CLAUDE_PLUGIN_ROOT}/scripts/caddis_gate.py" review-tier --range <
108
144
  git rebase <default-branch>
109
145
  git push --force-with-lease origin <branch> # never bare --force
110
146
  ```
147
+ - In a shared checkout (Step 0b says `SHARED: yes`), never rebase here: rebase in a detached worktree, as
148
+ Step 0b says.
111
149
 
112
150
  ## Step 5 — Push + create/update the PR
113
151