guard: read command position, refuse unreadable URLs, survive pipefail
ci/woodpecker/pr/ci Pipeline was canceled
ci/woodpecker/pr/ci Pipeline was canceled
Round two of the same independent review. Three findings, all real, and the first two share a root cause: the guard was reading command TEXT as though it were a command. 1. Splitting the endpoint token itself defeats fragment matching outright — `a=/api/v1/repos/o/r/iss; b=ues/1/comments` leaves no fragment contiguous. Round one fixed one spelling of this and the reviewer produced the general form immediately. It is not winnable by more fragments: the endpoint does not exist until the shell expands it, and this hook runs first. So the guard stops pretending to read it. A write whose URL contains an expansion, on a visibly forge-shaped command, is now BLOCKED as unreadable — because "I could not find an endpoint" must not mean "there is no endpoint". Opaque URLs that are not forge-shaped (webhooks, artifact stores) still pass. 2. The broadened body detection false-blocked ordinary work: `grep -R "curl -d https://.../issues" docs/`, `echo "curl -d ..." > note.txt`, printing an example from python. Talking about a call is not making one, and this is the direction that actually kills a control — an over-blocking hook gets turned off, and an off hook permits everything. The client must now appear at COMMAND POSITION: line start or after a shell operator, optionally behind VAR=value. In every false positive it sat behind a quote instead. Quotes are deliberately NOT stripped before matching; real calls quote their URLs. 3. `wt_precious()` aborted `cmd_rm` under `set -euo pipefail`: `grep -v` exits 1 when it filters everything out, which is exactly the disposable-only case, so a SAFE worktree failed to remove with no message. Fixed, and the same defect was latent one step upstream in `wt_dirty()`, where `head -200` SIGPIPEs git on any worktree with 201 changed files. The cap is gone — counting is cheap and the cap only ever truncated output that is no longer printed. Seven new fixtures pin all of it, in both directions. 27/27.
This commit is contained in:
@@ -19,51 +19,51 @@ from a wrapper that was never written — which is how the APPROVE/APPROVED inci
|
||||
Every command takes `--help`. All of them accept `--login <account>` to pin the acting identity;
|
||||
supply it explicitly on any host where the provider CLI's default account is an admin.
|
||||
|
||||
| Issues | |
|
||||
| --- | --- |
|
||||
| `issue-create.sh` | Create an issue (Gitea or GitHub) |
|
||||
| `issue-view.sh` | Show one issue |
|
||||
| `issue-list.sh` | List issues |
|
||||
| `issue-edit.sh` | Edit title/body/labels/milestone |
|
||||
| `issue-comment.sh` | Add a comment |
|
||||
| `issue-assign.sh` | Assign or unassign |
|
||||
| `issue-close.sh` | Close an issue |
|
||||
| `issue-reopen.sh` | Reopen a closed issue |
|
||||
| Issues | |
|
||||
| ------------------ | --------------------------------- |
|
||||
| `issue-create.sh` | Create an issue (Gitea or GitHub) |
|
||||
| `issue-view.sh` | Show one issue |
|
||||
| `issue-list.sh` | List issues |
|
||||
| `issue-edit.sh` | Edit title/body/labels/milestone |
|
||||
| `issue-comment.sh` | Add a comment |
|
||||
| `issue-assign.sh` | Assign or unassign |
|
||||
| `issue-close.sh` | Close an issue |
|
||||
| `issue-reopen.sh` | Reopen a closed issue |
|
||||
|
||||
| Pull requests | |
|
||||
| --- | --- |
|
||||
| `pr-create.sh` | Open a pull request |
|
||||
| `pr-view.sh` | Show one PR |
|
||||
| `pr-list.sh` | List PRs |
|
||||
| `pr-diff.sh` | Fetch a PR's diff |
|
||||
| Pull requests | |
|
||||
| ---------------- | --------------------------------------------------------- |
|
||||
| `pr-create.sh` | Open a pull request |
|
||||
| `pr-view.sh` | Show one PR |
|
||||
| `pr-list.sh` | List PRs |
|
||||
| `pr-diff.sh` | Fetch a PR's diff |
|
||||
| `pr-metadata.sh` | PR metadata as JSON (head SHA, base, state, mergeability) |
|
||||
| `pr-review.sh` | **Place a review verdict — see the dialect note below** |
|
||||
| `pr-ci-wait.sh` | Block until the PR's CI reaches a terminal state |
|
||||
| `pr-merge.sh` | Merge a PR |
|
||||
| `pr-close.sh` | Close a PR without merging |
|
||||
| `pr-review.sh` | **Place a review verdict — see the dialect note below** |
|
||||
| `pr-ci-wait.sh` | Block until the PR's CI reaches a terminal state |
|
||||
| `pr-merge.sh` | Merge a PR |
|
||||
| `pr-close.sh` | Close a PR without merging |
|
||||
|
||||
| Milestones | |
|
||||
| --- | --- |
|
||||
| Milestones | |
|
||||
| --------------------- | ------------------ |
|
||||
| `milestone-create.sh` | Create a milestone |
|
||||
| `milestone-list.sh` | List milestones |
|
||||
| `milestone-close.sh` | Close a milestone |
|
||||
| `milestone-list.sh` | List milestones |
|
||||
| `milestone-close.sh` | Close a milestone |
|
||||
|
||||
| Gates and guards | |
|
||||
| --- | --- |
|
||||
| `ci-queue-wait.sh` | CI queue guard — required before push/merge (see below) |
|
||||
| `push-guard.sh` | Refuse verifications that pass for the wrong reason (e.g. green against an unpushed tree) |
|
||||
| `mutate-push-guard.sh` | Regenerate the guard's mutation-coverage table from measurement, so the table cannot drift from the guard |
|
||||
| `verify-clean-clone.sh` | Prove the **committed** artifact runs, from a clean clone — not the working tree |
|
||||
| Gates and guards | |
|
||||
| ----------------------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `ci-queue-wait.sh` | CI queue guard — required before push/merge (see below) |
|
||||
| `push-guard.sh` | Refuse verifications that pass for the wrong reason (e.g. green against an unpushed tree) |
|
||||
| `mutate-push-guard.sh` | Regenerate the guard's mutation-coverage table from measurement, so the table cannot drift from the guard |
|
||||
| `verify-clean-clone.sh` | Prove the **committed** artifact runs, from a clean clone — not the working tree |
|
||||
|
||||
| Context | |
|
||||
| --- | --- |
|
||||
| Context | |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `detect-platform.sh` | Resolve the provider (Gitea vs GitHub) for the current repo; every other wrapper uses it |
|
||||
| `lane-brief.sh` | Live dispatch brief for a repo "lane" (milestone/label) straight from the provider |
|
||||
| `lane-brief.sh` | Live dispatch brief for a repo "lane" (milestone/label) straight from the provider |
|
||||
|
||||
| Workspace | |
|
||||
| --- | --- |
|
||||
| Workspace | |
|
||||
| -------------------- | ------------------------------------------------------------------------ |
|
||||
| `mosaic-worktree.sh` | Create/list/remove git worktrees — **the only supported way**; see below |
|
||||
| `wrapper-guard.sh` | PreToolUse hook that enforces the two rules above; not called by hand |
|
||||
| `wrapper-guard.sh` | PreToolUse hook that enforces the two rules above; not called by hand |
|
||||
|
||||
**Workspace placement is derived, not chosen.** `mosaic-worktree.sh new <branch>` takes a branch
|
||||
name and nothing else. Every path comes out of `git worktree list --porcelain` — main worktree,
|
||||
@@ -83,7 +83,7 @@ anyway, under five simultaneous conventions on a single host.
|
||||
Worktrees rather than clones, because `git worktree list` makes every checkout enumerable — a bare
|
||||
clone dropped somewhere on disk can never be safely reclaimed, so it is never reclaimed. `rm` and
|
||||
`gc` decide by **evidence, never by size or age**: a worktree is reclaimable only when
|
||||
`git status --porcelain` is empty *and* `git rev-list --count HEAD --not --remotes` is 0. Anything
|
||||
`git status --porcelain` is empty _and_ `git rev-list --count HEAD --not --remotes` is 0. Anything
|
||||
else is preserved and reported. `--force` exists and is yours to type deliberately.
|
||||
|
||||
`wrapper-guard.sh` is registered as a Claude Code `PreToolUse` hook on `Bash` (see
|
||||
|
||||
Reference in New Issue
Block a user