Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
29228cf
chore: clean drift in AI rules + meta; remove TODO.md
EricAndrechek May 15, 2026
3156c88
docs(contributing): drop vestigial "tenant isolation" wording
EricAndrechek May 15, 2026
9687ea9
Merge remote-tracking branch 'origin/main' into claude-native
EricAndrechek May 15, 2026
b8a7a25
feat(claude): add agent PR discipline hooks + review gate
EricAndrechek May 19, 2026
6c79315
fix(claude): tighten no-verify regex to skip quoted message bodies
EricAndrechek May 19, 2026
30094ba
Merge remote-tracking branch 'origin/main' into claude-native
EricAndrechek May 19, 2026
94e6fd7
fix(claude): add name field so pre-push-reviewer agent registers
EricAndrechek May 19, 2026
c03edcd
chore(claude): tighten ship_it rule + drop porous marker-forgery regex
EricAndrechek May 19, 2026
ab2f414
fix(claude): doc-sync the honest-agent + strict-ship_it policy shifts
EricAndrechek May 19, 2026
849ffce
fix(claude): drop residual marker-write overclaim in claude-code.md:43
EricAndrechek May 19, 2026
f3018ed
fix(claude-code docs): three hooks wired, not four
EricAndrechek May 19, 2026
8fbd7db
fix(claude): strip quotes before gate matching + correct enforcement …
EricAndrechek May 19, 2026
f5e03bb
fix(claude): address coderabbit findings on PR #147
EricAndrechek May 19, 2026
dc91aa7
fix(claude): move review-marker to SubagentStop + 2 coderabbit findings
EricAndrechek May 19, 2026
3449478
chore(claude): doc-sync the SubagentStop hook event rename
EricAndrechek May 19, 2026
92e2c6a
fix(claude): emit diagnostic stderr from review-marker.sh on jq failure
EricAndrechek May 19, 2026
c873153
fix(claude): guard the marker write so failures surface as errors
EricAndrechek May 19, 2026
42d08f5
chore(claude-review): drop auto-trigger, manual-only via comment or d…
EricAndrechek May 19, 2026
bc81178
docs(changelog): record the claude-review manual-only switch
EricAndrechek May 19, 2026
c141476
Merge remote-tracking branch 'origin/main' into claude-native
EricAndrechek May 19, 2026
acd6c04
fix(docs): use absolute path for the claude-code link
EricAndrechek May 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .claude/agents/pre-push-reviewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
name: pre-push-reviewer
description: Reviews the current branch's full delta against main using the canonical WaveHouse review prompt (.github/prompts/pr-review.md). Use before pushing to any PR branch (mandatory per AGENTS.md §Agent PR Discipline) or to audit someone else's PR after `wt switch pr:N` / `gh pr checkout N`. Considers full PR diff, latest commit, all open PR comments + reviews, and CI / failing-test status. Runs in fresh context for objectivity. Returns [MUST]/[SHOULD]/[MAY] findings plus a parseable verdict line that drives the pre-push marker.
tools: Bash, Read, Glob, Grep
model: opus
---

You are reviewing the current branch's delta against main, using the same prompt as the CI Claude review action — but locally, on the working state, before push (or on someone else's PR after checking it out locally).

## Source of truth

Read `.github/prompts/pr-review.md` first. That file is the canonical WaveHouse review prompt and applies here verbatim **for the focus areas (correctness → security → performance → testing → docs/sdk-sync), the severity tags `[MUST]`/`[SHOULD]`/`[MAY]`, and the noise filter**. The verdict rules below override the CI prompt's — WaveHouse pre-push runs a stricter rubric (any finding forces iterate; see §Verdict mapping below).

The diff source here is the local working state, computed as `git diff main...HEAD` (three dots — equivalent to `git diff $(git merge-base main HEAD) HEAD`, i.e. merge-base vs HEAD). Pre-push self-review wants the same range so uncommitted edits are NOT included (commit them first; markers are SHA-pinned anyway).

## Process

1. Read `.github/prompts/pr-review.md` and `AGENTS.md` (especially §Documentation Sync, §SDK Sync, §Branch Maintenance, §Agent PR Discipline).

2. Compute the branch diff:

```bash
git diff main...HEAD
```

3. For each changed file, read its current state. Don't just look at the diff — context matters.

4. **If this branch has an open PR, fetch PR context.** Get the PR number from the branch name (`gh pr view --json number,state,comments,reviews,statusCheckRollup`). When there's a PR, the review must consider:

- **All open PR comments and reviews** — top-level comments (`gh pr view <num> --json comments,reviews`) AND inline review comments (`gh api repos/<repo>/pulls/<num>/comments`). If a reviewer already flagged something, don't re-flag; either acknowledge and add nuance, or skip. If the author replied to a concern, factor in the reply.
- **Failing CI checks** — `gh pr checks <num>` and `gh pr view <num> --json statusCheckRollup`. Surface failures that look like real bugs (not env flakes).
- **Linked issues** — `Closes #N` / `Fixes #N` in PR body. Acceptance criteria live in the issue.
- **Latest commit specifically** — `git show HEAD` — sometimes the most recent push introduced a regression worth highlighting.

If there's no open PR for this branch (e.g., pre-PR self-review), skip the PR-context fetch but still review the merge-base diff thoroughly.

5. Apply the focus areas from `pr-review.md` in order:

- **Correctness** — Go concurrency (goroutine leaks, data races, missing context propagation, channel leaks, `sync.Once` / `sync.Map` misuse, handlers ignoring `r.Context()`), error wrapping with `%w`, resource cleanup on every error path, broken invariants per AGENTS.md §Key Design Decisions.
- **Security** — OWASP Top 10 walked against the diff (SQL injection in CH paths, JWT/role handling, sensitive data exposure, CORS, hardcoded secrets, TOCTOU). Severity-tag CRITICAL / HIGH / MEDIUM / LOW.
- **Performance** — hot-path allocations, unbounded goroutines, unbatched DB work, locks across I/O, N+1, singleflight misuse.
- **Testing** — new code on critical paths without tests, missing edge cases, mocks where integration would catch more.
- **Documentation sync** — per AGENTS.md §Documentation Sync table.
- **SDK sync** — per AGENTS.md §SDK Sync table. Did `internal/api/` change without `clients/ts/src/` consideration?

6. Apply the noise filter from `pr-review.md` before finalizing: drop findings you wouldn't personally ask the author to change in-person.

7. Tag each finding `[MUST]` / `[SHOULD]` / `[MAY]` per the styleguide.

8. End with a verdict per the styleguide (`Ship it` / `Iterate` / `Block`), **followed immediately by the parseable verdict line** on its own line:

```text
VERDICT: ship_it
```

or `VERDICT: iterate` or `VERDICT: block`. The line is consumed by `.claude/hooks/review-marker.sh` to gate the pre-push marker — incorrect formatting means no marker, no push.

## Output format

```markdown
## Pre-push review — <branch> vs main

(Optional: brief paragraph on PR scope + linked issues if applicable.)

### [MUST] Findings

- `internal/api/handler.go:42` — <concrete issue + suggested fix>
Severity: CRITICAL/HIGH/MEDIUM/LOW (security findings only)

### [SHOULD] Findings

- ...

### [MAY] Findings

- ...

## Verdict

**Ship it** / **Iterate** / **Block** — <one-line headline of the most important thing>

VERDICT: ship_it
```

(or `VERDICT: iterate` / `VERDICT: block`)

## Verdict mapping

WaveHouse uses a stricter rule than `.gemini/styleguide.md` / `.github/prompts/pr-review.md`: **`ship_it` requires zero findings at any severity**. If there is anything left to do, the PR isn't shippable — "ship it, just do this one thing first" is iteration, not shipping.

- **`Ship it`** + `VERDICT: ship_it` — `[MUST]`, `[SHOULD]`, and `[MAY]` sections are all empty. Pre-push marker auto-writes, push proceeds.
- **`Iterate`** + `VERDICT: iterate` — any `[MUST]` / `[SHOULD]` / `[MAY]` finding exists, but none are block-level. The orchestrator fixes the findings and re-invokes this subagent (always in fresh context) until ship_it.
- **`Block`** + `VERDICT: block` — a `[MUST]` that's CRITICAL/HIGH security, data-loss risk, broken core invariant, or otherwise needs human/maintainer attention (architectural disagreement, missing CI signal, etc.). Cannot proceed without addressing.

### What this means for `[MAY]`

Under this rubric, **`[MAY]` is a real commitment** — "I'd actually do this before merge," not "optional polish." If you're tempted to raise a finding because it's nice-to-have but you wouldn't ask the author to act on it before merge, drop it from the findings list. Put it in the prose preamble as an observation, or leave it out entirely. The noise filter from `pr-review.md` is even stronger here: any finding in the list is a blocker to ship_it.

## Framing

This is a SELF-review or PR-audit run by an agent. Frame findings as "things to consider fixing before pushing / before this PR merges" — direct and skeptical, but constructive. The user reads these and decides what to act on.

**Do not make code changes.** Review only. The orchestrator agent (or a human) decides what to fix; you just surface the findings.

**Do not post comments on the PR.** This is a local review. To make a bot comment on a PR remotely, the workflow is `gh workflow run "Claude PR review" -f pr_number=<N>` (which fires the CI claude-review action — that's the bot that comments).
24 changes: 24 additions & 0 deletions .claude/commands/cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
description: Render coverage HTML for a suite and surface drops below threshold
argument-hint: [unit|integration|e2e|sdk|merge|all] (default: merge whatever exists)
---

Generate the coverage report and surface anything below threshold from `.testcoverage.yml`.

Suite to run: $ARGUMENTS

Behavior:
- **no argument or "merge"**: just `make cov` (merges whatever covdata exists in `tmp/coverage/*/data/` and gates against `threshold.total`)
- **unit**: `make test-unit` (gates per-suite + writes `tmp/coverage/unit/`)
- **integration**: `make test-integration` (requires Docker)
- **e2e**: `make test-e2e` (requires Docker; orchestrator + cover binary)
- **sdk**: `make test-sdk` (vitest)
- **all**: `make test-all` (all four suites sequentially + `make cov`)

After the run completes:
1. Parse `tmp/coverage/<suite>/coverage.txt` (Go) or `tmp/coverage/sdk/index.html` (SDK) for per-package coverage.
2. Identify packages below the suite's threshold from `.testcoverage.yml`. Report as a sorted list.
3. Surface the suite total + delta vs. the threshold.
4. Print the path to the HTML report — don't auto-open (lets the user open it themselves if they want).

If the suite errors before producing covdata, report what failed without trying to render coverage.
210 changes: 210 additions & 0 deletions .claude/hooks/agent-bash-gate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
#!/usr/bin/env bash
# PreToolUse Bash gate — enforces AGENTS.md §"Agent PR Discipline".
#
# This hook is what the deny-list can't be (deny patterns are prefix-glob only).
# Here we regex over the whole command to catch:
#
# 1. --no-verify on git push/commit (no agent bypass; humans can use it intentionally)
# 2. gh pr create without --draft (only humans publish ready-for-review PRs)
# 3. gh pr ready (humans only — draft→ready is a deliberate human signal)
# 4. gh pr edit --add-reviewer / --add-assignee (human reviewer assignment is humans-only;
# bot reviewers are re-triggered via PR comments, not the reviewer API)
# 5. gh api .../requested_reviewers POST/PUT (the API form of --add-reviewer)
# 6. gh pr review --approve / --request-changes (only humans take formal review actions;
# bot reviewers use inline review comments + sticky summaries instead)
# 7. git push without required markers (ci-passed always; review-passed on PR branches)
#
# Marker forgery (writing tmp/(ci|review)-passed-* by any means) is NOT blocked
# here. The .claude/settings.json deny list catches the obvious tool-level
# attempts (Bash(touch tmp/...:*), Write/Edit on the paths); everything else
# relies on the honest-agent model documented in AGENTS.md §"Agent PR
# Discipline" — Bash can write a file by a dozen paths and regex enforcement
# becomes a porous game of whack-a-mole that oversells what it delivers.
#
# Anything blocked here can typically be re-run by a human from terminal — these
# rules are for the agent path, not the underlying git/gh capabilities.

set -uo pipefail

# --- Helper: block with a structured reason ---------------------------------
# Declared early because the parse step below may need it.
block() {
local reason="$1"
cat >&2 <<EOF

🛑 Claude PR discipline gate: ${reason}

See AGENTS.md §"Agent PR Discipline" for the full ruleset. If you genuinely
need to bypass, ask the human user to run the command themselves.
EOF
exit 2
}

# Fail-closed on missing jq or malformed JSON — silently exiting 0 here would
# disable every discipline check below, which is exactly what this hook is
# supposed to prevent. A valid Bash PreToolUse payload always has
# .tool_input.command, so a parse failure means something is wrong, not benign.
if ! command -v jq >/dev/null 2>&1; then
block "jq is required for the PR discipline gate but is not installed. Install jq (brew install jq) or remove the PreToolUse hook from .claude/settings.json."
fi

input=$(cat)
if ! cmd=$(printf '%s' "$input" | jq -r '.tool_input.command // ""' 2>/dev/null); then
block "Could not parse hook payload as JSON; refusing to fail open."
fi
[ -z "$cmd" ] && exit 0
Comment thread
EricAndrechek marked this conversation as resolved.

cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null || exit 0

# Strip single- and double-quoted segments before matching, so legitimate
# commands that *mention* a blocked pattern inside a string (e.g.
# `gh pr comment -b "we will git push after CI"`, `echo "use --no-verify"`)
# don't false-positive. Same intent as commit 6c79315 (which fixed the
# no-verify regex specifically), generalized to every check below. Doesn't
# cover escaped quotes or heredocs — accept the corner case; agents
# constructing such commands are doing something weird.
stripped=$(printf '%s' "$cmd" | sed -E "s/'[^']*'//g; s/\"[^\"]*\"//g")

# Boundary helper: detects a `git <subcommand>` invocation anywhere in the
# (quote-stripped) command (including after && / ; / | / cd ... &&). Used
# by multiple checks.
git_subcmd() {
printf '%s\n' "$stripped" | grep -qE "(^|[[:space:];|&]+)git[[:space:]]+$1\b"
Comment thread
EricAndrechek marked this conversation as resolved.
}

# True if `git <subcommand>` is followed by -h / --help anywhere before a
# separator (so `git push --help`, `git push origin main --help`, `git push -h`
# all return true). Help invocations don't actually run the subcommand, so
# discipline gates should skip them.
git_subcmd_is_help() {
printf '%s\n' "$stripped" | grep -qE "(^|[[:space:];|&]+)git[[:space:]]+$1([[:space:]]+[^;|&]*)?[[:space:]]+(-h|--help)([[:space:]]|\$|[;|&])"
}

# --- 1. --no-verify on git push/commit --------------------------------------
# Quote-stripping above already excludes false positives where the literal
# string sits inside a quoted commit-message body. Honest-agent defense, not
# adversarial — eval / sh -c wrappers around `git push --no-verify` could
# still bypass; AGENTS.md §"Agent PR Discipline" makes the rule explicit.
if printf '%s\n' "$stripped" | grep -qE '(^|[[:space:];|&])git[[:space:]]+(push|commit)\b[[:space:]][^&|;]*--no-verify\b'; then
block "git push/commit with --no-verify is not permitted for agents. Run the gates."
fi

# --- 2. gh pr create without --draft ----------------------------------------
if printf '%s\n' "$stripped" | grep -qE '(^|[[:space:];|&]+)gh[[:space:]]+pr[[:space:]]+create\b'; then
if ! printf '%s\n' "$stripped" | grep -qE '(^|[[:space:]])(--draft|-d)\b'; then
block "Agent-opened PRs must be created with --draft. Only humans publish ready-for-review PRs."
fi
fi

# --- 3. gh pr ready ---------------------------------------------------------
if printf '%s\n' "$stripped" | grep -qE '(^|[[:space:];|&]+)gh[[:space:]]+pr[[:space:]]+ready\b'; then
block "Only humans transition PRs from draft to ready-for-review. Ask the user to do this manually when the PR is ready."
fi

# --- 4. gh pr edit with reviewer/assignee changes ---------------------------
if printf '%s\n' "$stripped" | grep -qE '(^|[[:space:];|&]+)gh[[:space:]]+pr[[:space:]]+edit\b'; then
if printf '%s\n' "$stripped" | grep -qE '(^|[[:space:]])--(add|remove)-(reviewer|assignee)\b'; then
block "Adding/removing reviewers or assignees is humans-only. To re-trigger bot reviewers, post a PR comment mentioning them (@coderabbitai review, @gemini-code-assist /gemini review, @claude / /review)."
fi
fi

# --- 5. gh api .../requested_reviewers any write verb (humans-only API) -----
# Both POST (add) and PUT (replace) on /pulls/<n>/requested_reviewers are
# reviewer-write operations. Neither has a legitimate agent use case — bot
# reviewers are re-triggered via PR comments. Match any reviewer-write idiom.
if printf '%s\n' "$stripped" | grep -qE '(^|[[:space:];|&]+)gh[[:space:]]+api\b' && \
printf '%s\n' "$stripped" | grep -qE 'requested_reviewers'; then
if printf '%s\n' "$stripped" | grep -qE '(-X[[:space:]]*(POST|PUT|PATCH)|--method[[:space:]]*(POST|PUT|PATCH)|[[:space:]]-f[[:space:]]+reviewers=|[[:space:]]-F[[:space:]]+reviewers=)'; then
block "Write requests to /requested_reviewers are the API form of --add-reviewer; humans-only. For bot reviewers, post a PR comment mentioning them."
fi
fi

# --- 6. gh pr review --approve / --request-changes --------------------------
if printf '%s\n' "$stripped" | grep -qE '(^|[[:space:];|&]+)gh[[:space:]]+pr[[:space:]]+review\b'; then
if printf '%s\n' "$stripped" | grep -qE '(^|[[:space:]])(--approve|-a)\b'; then
block "Only humans approve PRs (--approve)."
fi
if printf '%s\n' "$stripped" | grep -qE '(^|[[:space:]])(--request-changes|-r)\b'; then
block "Agents don't use --request-changes. Post inline review comments via the GitHub inline-comment MCP tool, or use gh pr comment for top-level comments."
fi
fi

# --- 7. git push: check markers ---------------------------------------------
# Only on actual `git push` invocations (not `git push --help`, not `gh pr push`).
if git_subcmd 'push' && ! git_subcmd_is_help 'push'; then
head_sha=$(git rev-parse HEAD 2>/dev/null || echo "")
if [ -n "$head_sha" ]; then
short_sha="${head_sha:0:8}"
ci_marker="tmp/ci-passed-${head_sha}"
review_marker="tmp/review-passed-${head_sha}"

# 7a. ci-passed required for every push (mirrors the universal git pre-push hook;
# firing here too gives Claude a more actionable error inside its session).
if [ ! -f "$ci_marker" ]; then
cat >&2 <<EOF

🛑 Claude PR discipline gate: 'make ci' has not been run for HEAD (${short_sha}).

Per AGENTS.md §"Local-First Validation", every push must have passing local CI
for the exact HEAD being published. Run:

make ci

The 'ci' Makefile target writes tmp/ci-passed-${short_sha} on success. Then retry
'git push'.
EOF
exit 2
fi

# 7b. review-passed required when HEAD branch has an open PR.
branch=$(git symbolic-ref --short HEAD 2>/dev/null || echo "")
if [ -n "$branch" ] && [ "$branch" != "main" ]; then
if ! command -v gh >/dev/null 2>&1; then
block "gh CLI is required to determine PR state for branch '${branch}' (needed to enforce pre-push review on PR branches). Install gh or push from main."
fi
# gh pr view exits 1 for both "no PR for this branch" (benign — no review-marker
# enforcement needed) and "auth/network error" (NOT benign — would silently bypass
# the review gate). Differentiate by capturing stderr and grepping for the
# well-known "no pull requests found" message; anything else is treated as a
# real failure and blocks.
pr_state=""
if pr_view_out=$(gh pr view "$branch" --json state --jq .state 2>&1); then
pr_state="$pr_view_out"
elif printf '%s' "$pr_view_out" | grep -qiE 'no (open )?pull request'; then
pr_state=""
else
block "Could not determine PR state for branch '${branch}': ${pr_view_out}. Refusing to silently skip review-marker enforcement. Run 'gh auth status', 'gh auth login', or retry."
fi
if [ "$pr_state" = "OPEN" ]; then
if [ ! -f "$review_marker" ]; then
cat >&2 <<EOF

🛑 Claude PR discipline gate: no review marker for HEAD (${short_sha}) on PR branch '${branch}'.

Invoke the pre-push-reviewer subagent in fresh context before pushing:

Use the Agent tool with subagent_type="pre-push-reviewer" and a prompt
asking it to review the current branch's full diff vs main, the latest
commit, open PR comments, and CI status.

When the agent's response ends with "VERDICT: ship_it", a SubagentStop hook
auto-writes tmp/review-passed-${short_sha} and this push will succeed.

If the agent returns VERDICT: iterate or VERDICT: block, address the findings
and re-invoke the agent (always in fresh context — never the same session)
until ship_it.

Per AGENTS.md §"Agent PR Discipline", agents do not bypass this with
--no-verify, and you do not write the marker file directly by any means —
the marker is wrong-shaped if you're the one writing it. CI's claude-review
will also fire on push, but pre-push review catches issues before consuming
shared capacity.
EOF
exit 2
fi
fi
fi
fi
fi

exit 0
25 changes: 25 additions & 0 deletions .claude/hooks/gofumpt-on-save.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# PostToolUse hook: auto-format Go files with gofumpt + goimports after edits.
#
# Why a hook (vs running `make fix` periodically): catches the format drift
# at write time so commits never carry fmt-failing files, and works in
# bypassPermissions mode where teammates might not see prompts for `make fix`.
#
# Safety: best-effort. If gofumpt can't parse the file (mid-edit syntax error),
# it leaves the file alone — we silently skip rather than blocking the edit.

set -uo pipefail

input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null)

# Only Go files
[[ "$file_path" == *.go ]] || exit 0
[ -f "$file_path" ] || exit 0

cd "${CLAUDE_PROJECT_DIR:-.}" 2>/dev/null || exit 0

go tool gofumpt -w "$file_path" 2>/dev/null || true
go tool goimports -w "$file_path" 2>/dev/null || true

exit 0
Loading
Loading