Skip to content
159 changes: 159 additions & 0 deletions .claude/commands/pr-address.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
description: Triage PR review comments, fix the valid ones, then reply to each comment (manual commit + push)
allowed-tools: Bash(gh pr view:*), Bash(gh pr checks:*), Bash(gh pr comment:*), Bash(gh api:*), Bash(gh repo view:*), Bash(gh run view:*), Bash(gh run list:*), Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git add:*), Bash(git commit:*), Bash(git push:*), Bash(git rev-parse:*), Bash(git branch:*), Bash(pnpm:*)
argument-hint: [PR-number]
---

Triage PR review comments, fix the valid ones, then reply to each comment. The command will NOT commit or push — that's a manual step. Three confirmation gates: never proceed past one without my explicit "yes".

Arguments: `$ARGUMENTS` — optional PR number. If omitted, use the PR for the current branch.

## 1. Resolve PR (read-only)

If `$ARGUMENTS` is a number, use that PR. Otherwise `gh pr view --json number,headRefName,state,title,url,baseRefName`. Stop if no PR exists for the current branch.

Capture `<owner>` and `<repo>` from `gh repo view --json owner,name`.

## 2. Fetch comments + checks (read-only)

Comments — pull all three sources, retaining each comment's `id` (needed to reply):

- General PR comments: `gh api repos/<owner>/<repo>/issues/<PR>/comments`
- Review / line comments: `gh api repos/<owner>/<repo>/pulls/<PR>/comments`
- Review summaries / approvals: `gh api repos/<owner>/<repo>/pulls/<PR>/reviews`

Skip any comment authored by the current `gh api user --jq .login` (don't re-engage your own past replies).

Checks — run `gh pr checks` (exit code 8 means "not all complete", that's fine, parse the output anyway). For each check that finished with `failure`, fetch the tail of the failed job log:

```
gh run view <runId> --log-failed | tail -60
```

Cap at 60 lines per failure; truncate longer with a note pointing at the check URL.

## 3. Triage

First, print a one-line check summary: `Checks: <N> ✓ · <N> ✗ · <N> ⏳`. If everything is passing or pending, say so.

Then classify each comment AND each failed check:

- **valid** — concrete claim or actionable suggestion grounded in the diff. Will be fixed. Replies posted.
- **question** — clarifying question; no code change, but reply needed.
- **noise** — bot welcome / "review enabled" / auto-generated PR overview / "show summary per file" / known-flaky CI failure. No fix, no reply by default.
- **approved** — APPROVED review with no concrete request. Acknowledge with `👍 thanks`, no fix.
- **ci-failure** — a failed Action / check that points at a real problem in the diff (test/typecheck/lint/format). Will be fixed. NO reply (CI re-runs after push).

For ci-failure rows use the check name as `AUTHOR` and the failed-job URL as the `id`. Put a one-line root cause (parsed from the failed log tail) in the summary.

Show one table:

```
ID AUTHOR FILE:LINE CLASS ONE-LINE SUMMARY
<id> <user | check-name> <path:line | --> <class> <≤ 90 chars>
```

Then propose a fix plan for everything classified `valid` or `ci-failure`:

```
[id <id>] <file>(:line)
→ <one-line description of the change>
```

Group related fixes (same file or shared root cause) into a single edit when natural.

**GATE 1.** Ask exactly: `Triage + fix plan look right? (yes / edit / cancel)`. Wait.

If `edit`: let me reclassify (e.g. "id 1234 is noise") or adjust the plan, then re-confirm.

## 4. Apply fixes (only after Gate 1 = yes)

1. If `git status --porcelain` is non-empty, STOP — don't mix the user's WIP into the fix commit. Tell me to stash/commit first.
2. Make minimal edits per the confirmed plan. Don't fix things that weren't reported.
3. Run validation, only scripts that exist in `package.json`:
```
pnpm typecheck
pnpm lint
pnpm test
pnpm format
```
If any fail, STOP and show the failure. Do not commit broken code, do not post replies promising a non-existent fix.
4. Show me the diff (`git diff`) for review.

**GATE 2.** Ask: `Fixes look right? (yes / edit / cancel)`. Wait.

On `yes`, do NOT commit and do NOT push. Instead:

5. Print a suggested commit message I can copy-paste:

```
fix: address PR review feedback

addresses: <comment-id>, <comment-id>
```

6. STOP. Wait for me to commit + push manually. When I reply with `pushed` (or equivalent), continue to section 5 below — not before.
7. Once I confirm, run `git rev-parse HEAD` to capture the new commit SHA. It goes in the reply text.

## 5. Draft replies (only after Gate 2 = yes)

For each row classified `valid`, `question`, or `approved`, write a reply. Skip `ci-failure` (CI re-runs after push) and `noise` (silence).

Style: terse, one or two sentences max. Per-class template:

- `valid` → `Fixed in <sha>: <one-line of what changed>.` Optionally a brief follow-up if the fix isn't 1:1 to the suggestion.
- `question` → answer directly. Don't speculate. If the answer is "this is intentional because X", say that.
- `approved` → `👍 thanks`.
- `ci-failure` → no reply.
- `noise` → no reply.

Show all drafts in one block, grouped by comment id.

**GATE 3.** Ask: `Post these replies (and resolve threads for fix-landed rows)? (yes / edit / cancel)`. Wait.

## 6. Post replies + resolve threads (only after Gate 3 = yes)

### 6.1 Post replies

- **Review / line comment** (came from `pulls/<PR>/comments`): reply in-thread:
`gh api --method POST repos/<owner>/<repo>/pulls/<PR>/comments/<id>/replies -f body="<reply>"`
- **General PR comment** (came from `issues/<PR>/comments`): `gh pr comment <PR> --body "<reply>"`, quoting the original with `> @<user>` for context.
- **Review summary** (came from `pulls/<PR>/reviews`): no per-thread reply API. If a reply is genuinely warranted (rare), post a general comment @-mentioning the reviewer; otherwise skip.

After each post, confirm it returned a 200/201. If any fail, list exactly which — don't claim success for a failed post.

### 6.2 Resolve threads for fix-landed rows

A row is "fix-landed" iff its reply starts with `Fixed in <sha>:` — i.e. the fix actually shipped in the commit. Mark each such review thread resolved. Do NOT resolve `noise`, `question`, `approved`, `ci-failure`, or deferred-fix `valid` threads.

1. List the PR's review threads with their constituent comment ids:
```
gh api graphql -F owner=<owner> -F name=<repo> -F num=<PR> -f query='query($owner:String!,$name:String!,$num:Int!){repository(owner:$owner,name:$name){pullRequest(number:$num){reviewThreads(first:100){nodes{id isResolved comments(first:10){nodes{databaseId}}}}}}}'
```
2. For each fix-landed reply, find the thread whose `comments.databaseId` list includes the original comment id. If `isResolved` is already `true`, skip. Otherwise resolve:
```
gh api graphql -F threadId=<thread-node-id> -f query='mutation($threadId:ID!){resolveReviewThread(input:{threadId:$threadId}){thread{isResolved}}}'
```
3. Confirm each response shows `thread.isResolved == true`. List any failures by original comment id.

General PR comments (issues/comments) and review summaries don't have line-thread structure — nothing to resolve there.

## 7. Final summary

Print:

- `Triaged: <N> Fixed: <N> (review + <N> ci) Replied: <N> Resolved: <N> Skipped (noise): <N>`
- Commit SHA(s)
- PR URL

Stop.

## Hard rules

- Never edit code before Gate 1 confirms.
- Never run `git commit` or `git push`. Those are manual — print the suggested commit message, then wait for me to confirm `pushed`.
- Never post replies before Gate 3 confirms.
- Never promise a fix that didn't actually land. Validation must pass AND I must confirm `pushed` before drafting replies.
- Never address a comment that wasn't in the confirmed set (no scope creep).
- Never reply to your own past comments.
- Never resolve a thread for a deferred-fix `valid` row (reply was "Agreed, tracking" rather than "Fixed in <sha>"). Only resolve threads where the fix actually shipped in the commit.
53 changes: 53 additions & 0 deletions .claude/commands/pr-open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
description: Open a GitHub PR for the current branch with an auto-generated title and body
allowed-tools: Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(git branch:*), Bash(git push:*), Bash(gh auth status:*), Bash(gh pr create:*), Bash(gh pr view:*), Bash(gh repo view:*)
argument-hint: [--draft]
---

Open a GitHub PR for the current branch. Generate the title + body, show them to me, confirm, then push + create.

Arguments: `$ARGUMENTS` — if it contains `--draft`, create the PR as draft.

## Pre-flight

Stop with a clear message if any of these fail:

1. `gh auth status` — gh must be authenticated.
2. `git rev-parse --abbrev-ref HEAD` — must not be on `main`/`master`.
3. Determine base branch: try `main`, fall back to `master`. If neither exists, stop.
4. `git log <base>..HEAD --oneline` — if empty, stop ("nothing to PR").
5. `gh pr view --json url,state` on the current branch — if a PR already exists, print its URL and stop. Do NOT create a duplicate.
6. `git status --porcelain` — if there are uncommitted changes, warn me but do NOT commit them.

## Generate

Run `git diff <base>...HEAD` and `git log <base>..HEAD --oneline`, then produce:

- **Title**: plain English sentence (NOT `<type>: <subject>` semantic format). Capitalized, present tense, ≤ 72 chars, no trailing period.
- **Body**: three sections, total ~15–25 lines, skim-friendly.
- `### What`: 2–4 short bullets. Lead with the _why_ (bug, gap, user need), then key changes. No opening paragraph. Skip anything obvious from the diff.
- `### How to test`: commands one per line, no prose, no "expected results". Use only scripts that exist in `package.json` (verify first). Prefix manual steps with `Recommended:` if you didn't run them.
- `### Security review`: default to single line `No security-impacting changes.` Expand to checklist only when an item is actually affected. Format when expanded:
- **Secrets / env vars:** <what changed>
- **Auth / session:** <what changed>
- **Network / API calls:** <what changed>
- **Data handling / PII:** <what changed>
- **Dependencies:** <what changed>

Omit lines that didn't change.

## Confirm

Show me the title and body in two separate fenced code blocks. Then ASK explicitly: `Open this PR? (yes / edit / cancel)`. Wait for my reply. Do NOT push or create the PR without my confirmation.

If I say "edit", let me edit, then re-confirm.

## Create

On confirmation:

1. Push the branch with `git push -u origin <current-branch>` (skip if `git status -sb` shows the branch is already published and up to date).
2. Run `gh pr create --base <base> --title "<title>" --body "$(cat <<'EOF' ... EOF)"` — pass the body via heredoc so newlines survive. Add `--draft` if requested.
3. Print the PR URL that `gh` returns.

Do not commit on my behalf. Do not push without confirmation. Do not skip the confirmation step.
69 changes: 69 additions & 0 deletions .claude/commands/pr-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
description: Show CI checks (Actions) and comments for the current branch's PR (or a given PR number)
allowed-tools: Bash(gh pr view:*), Bash(gh pr checks:*), Bash(gh run list:*), Bash(gh run view:*), Bash(gh api:*), Bash(git rev-parse:*), Bash(git branch:*)
argument-hint: [PR-number]
---

Show CI status and comments for a PR. Read-only — never approves, merges, comments, or reruns anything.

Arguments: `$ARGUMENTS` — optional PR number. If omitted, use the PR for the current branch.

## Resolve PR

1. If `$ARGUMENTS` is a number, use that PR.
2. Otherwise, run `git rev-parse --abbrev-ref HEAD` and `gh pr view --json number,headRefName,state,isDraft,mergeable,title,url,updatedAt`. If no PR exists for the current branch, stop and say so.

## Checks (Actions)

Run `gh pr checks <PR>` (or `gh pr checks` if using current branch). Show each check on one line:

```
<icon> <name> <conclusion> <elapsed> <url>
```

Use `✅` for success, `❌` for failure, `⏳` for in_progress/queued/pending, `⏭️` for skipped, `❔` for anything else. Sort failures first.

If any check failed, ALSO run `gh run view <runId> --log-failed | tail -60` for the most recent failed run and print that log block under the check list (fenced as `text`). Cap at 60 lines — if longer, say `(truncated, see <url> for full log)`.

## Comments

Fetch both kinds:

- General PR comments: `gh api repos/{owner}/{repo}/issues/<PR>/comments --jq '.[] | {user: .user.login, body: .body, at: .updated_at, url: .html_url}'`
- Review/line comments: `gh api repos/{owner}/{repo}/pulls/<PR>/comments --jq '.[] | {user: .user.login, body: .body, at: .updated_at, file: .path, line: (.line // .original_line), url: .html_url}'`
- Reviews (approvals/changes-requested): `gh api repos/{owner}/{repo}/pulls/<PR>/reviews --jq '.[] | {user: .user.login, state: .state, body: .body, at: .submitted_at}'`

Print them merged in chronological order. One block per comment:

```
@<user> <state-or-tag> <relative-time>
[<file>:<line>] ← only if a line comment
<body — wrapped, max ~6 lines; truncate the rest with "…">
```

Tags: `[review:APPROVED]`, `[review:CHANGES_REQUESTED]`, `[review:COMMENTED]`, `[line]`, `[comment]`.

If there are zero comments, say `No comments.` and stop the section.

## Output structure

Print in this order, with one blank line between sections:

```
PR #<num> <state> <draft?> <mergeable?>
<title>
<url>
updated <relative-time>

Checks:
<one line per check>

[failed-log block if applicable]

Comments (<count>):
<one block per comment>
```

Keep it skim-friendly. No prose preamble, no closing summary. Don't quote bodies in full if they're long.

Do NOT post comments, approve, request changes, merge, close, or rerun any workflow. This command is read-only.
5 changes: 1 addition & 4 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
"Bash(pnpm typecheck:*)",
"Bash(pnpm test:*)",
"Bash(pnpm format:*)",
"Bash(pnpm --filter * test*)",
"Bash(pnpm --filter * lint*)",
"Bash(pnpm --filter * typecheck*)",
"Bash(pnpm --filter * format*)"
"Bash(pnpm --filter:*)"
]
}
}
1 change: 1 addition & 0 deletions apps/operator/migrations/0003_amused_marvex.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `schedules` ADD `claimed_at` text;
Loading
Loading