Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .claude/commands/commit.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ Co-Authored-By: Claude <noreply@anthropic.com>

## Steps

1. `git status` + `git diff` (and `git diff --staged` if anything's staged). **Abort if on `master`** — commit on a feature branch instead.
1. `git status` + `git diff` (and `git diff --staged` if anything's staged), and **check the current branch is the right place for this commit**:
- **Never commit on `master`/`main`.** If you're on it, stop and **offer to create a feature branch** from the current HEAD — `git switch -c <type>/<short-desc>` carries the uncommitted changes onto the new branch — then commit there. Don't proceed on `master` even if the user didn't mention branching; confirm first.
- If you're on a feature branch, glance at its name. If it looks **unrelated** to the change you're about to commit, flag it and offer to branch off (so you don't pile an unrelated commit onto someone else's WIP); otherwise proceed.
2. Stage the files that belong in this commit — be specific, don't `git add -A`.
3. Commit with `HUSKY=0` to skip the interactive husky prompt:

Expand Down
76 changes: 76 additions & 0 deletions .claude/commands/implement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
description: Take a triaged bug or feature request through to a verified change on a branch, ready for /commit and /pr — using this session's investigation plus the Jira ticket
---

Implement a fix or feature that's already been triaged (typically by `/slack-triage` earlier in this
same session) and land it as a **verified change on a branch**, stopping before commit. This is the
bridge between a Jira ticket and `/commit` → `/pr`. Works for both bugs and feature requests — it's
not always a "fix".

Usage: `/implement [INSTUI-1234]` — pass a ticket key if you have one. If `$ARGUMENTS` is empty, use
the ticket and investigation from this session.

## Step 1 — Establish context (prefer this session over the ticket)

A Jira ticket is a **short summary** of a much richer investigation. When `/slack-triage` ran earlier
in this conversation, the full picture is already in context — root cause, exact `file:line`, the
introducing commit/PR, the confirmed reproduction recipe, version/v1-v2 notes. **Use that. Do not
downgrade to the ticket's summary when the fuller investigation is in the session.**

Source context in this priority order:

1. **This session's investigation** (primary). If triage already happened here, carry forward its root
cause, the precise code location, and the confirmed repro. Treat these as the working spec.
2. **The Jira ticket** (durable anchor). Read it (Jira MCP / `$ARGUMENTS` key) to pin the scope,
acceptance criteria, and the Slack-thread link — but as a *reference point*, not a replacement for
the session's detail. If the ticket and your session findings conflict, prefer the verified session
findings and note the discrepancy to the user.
3. **Cold start fallback.** If there's *no* prior investigation in this session (fresh conversation, or
you only have a ticket key), reconstruct it before coding: read the ticket, then re-run the
investigation — mirror `/slack-triage` Step 3 (read the README/`props.ts`/`theme.ts`, trace the
introducing commit/PR for a regression, cross-check the published docs) and rebuild the repro. Don't
start editing off a one-paragraph ticket.

State briefly which sources you're working from before you start changing code.

## Step 2 — Branch

Branch from `master` (CLAUDE.md: branch from master, integrate by **rebasing**, never merge). Use a
descriptive branch name; include the ticket key if there is one.

## Step 3 — Implement, honoring InstUI conventions

Make the change at the location identified in Step 1. Enforce the rules the commit/PR skills don't:

- **No hardcoded user-facing strings** — all UI text comes from props for i18n (the most common review
comment).
- **New components: functional + hooks only.** Styling via the co-located Emotion `theme.ts`.
- **No breaking changes unless the user explicitly asked** — removing/renaming a prop, component, theme
variable, or exported util; changing a prop type or a behavior-altering default. Adding optional
props / new components / new theme variables is fine. If a break is genuinely required, flag it and
get explicit sign-off; it must carry `BREAKING CHANGE:` in the commit.
- **v1/v2:** if the component has both, change the right version (default to v2 for new work; confirm
before touching deprecated v1).
- **Docs & tests in the same change:** if you add/change a prop, update the component **README**. Add or
extend co-located **unit tests** (`*.test.tsx`), a **regression-test page** under
`/regression-test/src/app/<name>/page.tsx`, and a **Cypress entry** in
`/regression-test/cypress/e2e/spec.cy.ts`. Maintain WCAG 2.1 AA and RTL support.

## Step 4 — Verify against the original repro

A change isn't done until it's shown to work:

- Run the package's unit tests: `pnpm run test:vitest <pkg>`.
- Re-run the **confirmed reproduction from triage** and show it now behaves as expected (use the
`verify` skill / `pnpm run dev`). For a bug, the exact repro that demonstrated the failure should now
pass; for a feature, the use case from the ticket should work. Report what you observed.

## Step 5 — Hand off (stop before commit)

Stop at a **verified diff on the branch** and summarize: what changed and why, files touched, test/repro
results, and any follow-ups. **Do not commit or open a PR automatically** — the diff needs human review.
Tell the user to run `/commit` then `/pr` (the PR body should reference the `INSTUI-` ticket) once they're
happy.

If the change is non-trivial and you only got partway, **say so plainly** — leave a documented WIP branch
and list what's left, rather than forcing a wrong or incomplete change to look finished.
40 changes: 37 additions & 3 deletions .claude/commands/pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,56 @@ Open a PR for the current branch.
2. `git log master..HEAD` and `git diff master...HEAD` — read **all** commits in the branch (not just the latest) so the summary covers everything that's changed.
3. If not pushed: `git push -u origin <branch>`.
4. Create the PR (see invocation below).
5. Return the PR URL.
5. Suggest reviewers and assign the user's pick (see **Reviewer assignment**).
6. Return the PR URL and the assigned reviewer (if any).

If the branch name or any commit references a Jira ticket (e.g. `INSTUI-1234`), include it. If you can't find one, ask the user once before opening — don't invent one.

## gh invocation

Use `--body-file -` with a heredoc on stdin. This avoids shell-quoting issues and is the form supported by current `gh` versions. If unsure about flags, run `gh pr create --help` first — do **not** fall back to older forms like `gh pr create -t ... -b ...` with inline `-b`.
Use `--body-file -` with a heredoc on stdin. This avoids shell-quoting issues and is the form supported by current `gh` versions. `--assignee @me` self-assigns the PR to its author (`gh` doesn't do this by default — it only records authorship). If unsure about flags, run `gh pr create --help` first — do **not** fall back to older forms like `gh pr create -t ... -b ...` with inline `-b`.

```bash
gh pr create --title "<title>" --body-file - <<'EOF'
gh pr create --title "<title>" --assignee @me --body-file - <<'EOF'
<body>
EOF
```

Open as draft (`--draft`) if the work is in progress.

## Reviewer assignment

After the PR is created, help the user choose a reviewer from the CODEOWNERS roster, ranked by **fewest reviews in the last 60 days** (lightest load first). Don't auto-assign — the counts can't see who's on PTO or heads-down, so the human makes the call. **Run this verbatim** to gather the ranked list:

```bash
set -euo pipefail
[ -f .github/CODEOWNERS ] || { echo "no CODEOWNERS — skip assignment"; exit 0; }
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner)
SINCE=$(date -v-60d +%F 2>/dev/null || date -d '60 days ago' +%F) # macOS || GNU
AUTHOR=$(gh api user --jq .login)

# Roster: individual @handles from CODEOWNERS — drop comments, globs, and @org/team handles.
grep -v '^[[:space:]]*#' .github/CODEOWNERS \
| grep -oE '@[A-Za-z0-9_-]+' | sed 's/@//' | grep -v '/' | sort -u > /tmp/pr_roster.txt

: > /tmp/pr_counts.txt
while IFS= read -r U; do # real loop — this shell won't word-split $var
[ -z "$U" ] && continue
[ "$U" = "$AUTHOR" ] && continue # can't review own PR
N=$(gh search prs --repo "$REPO" --reviewed-by "$U" --updated ">=$SINCE" \
--limit 1000 --json number --jq 'length') || { echo "WARN: count failed for $U" >&2; continue; }
printf '%s %s\n' "$N" "$U" >> /tmp/pr_counts.txt
done < /tmp/pr_roster.txt

[ -s /tmp/pr_counts.txt ] || { echo "no eligible reviewers — skip assignment"; exit 0; }
echo "reviews(60d) per candidate:"; sort -n /tmp/pr_counts.txt
MIN=$(sort -n /tmp/pr_counts.txt | head -1 | awk '{print $1}')
WINNER=$(awk -v m="$MIN" '$1==m{print $2}' /tmp/pr_counts.txt | sort -R | head -1) # random tie-break
echo "==> suggested (lightest load): $WINNER (count=$MIN)"
```

Then **present the ranked list to the user** — each candidate with their 60-day review count, lightest first — and recommend the top one as the default. Ask them who to assign (they may pick a heavier-loaded person who's actually available, or skip entirely). Only after they choose, assign with `gh pr edit <pr> --add-reviewer <their-pick>` and confirm. If the script printed a skip message (no CODEOWNERS / no eligible reviewers), say so and leave the PR unassigned.

## Body format

Keep it **short**. No preamble, no restating the title, no "this PR does X" filler.
Expand Down
90 changes: 90 additions & 0 deletions .claude/commands/slack-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
description: One-time setup for the Slack bot token used by /slack-triage — create/scope the Slack app, store the credentials, and verify they work
---

Set up (or repair) the Slack credentials that `/slack-triage` needs to read threads. This is a
one-time concern; once it's working, run `/slack-triage` directly.

The `slack` server reads `SLACK_BOT_TOKEN` and `SLACK_TEAM_ID` from `.claude/mcp.local.env`, which
`.mcp.json` sources when it launches the server. **That file is the single source of truth** for
these secrets (gitignored via `.claude/*.local.*`). The bot is **read-only** — it does not post — so
it never needs `chat:write`.

## Step 1 — Check what's already there

Verify whether credentials are present **without printing their values** (source the env file first,
since that's what the server uses — they won't be in the plain shell otherwise):

```sh
[ -f ./.claude/mcp.local.env ] && . ./.claude/mcp.local.env
( [ -n "${SLACK_BOT_TOKEN:-}" ] && [ -n "${SLACK_TEAM_ID:-}" ] \
&& ! printf '%s' "${SLACK_BOT_TOKEN:-}" | grep -q REPLACE ) && echo creds-present || echo creds-missing
```

- If `creds-missing` → go to Step 2 (create/configure the app and store the token).
- If `creds-present` → skip to Step 5 to verify scopes are actually sufficient. (A token can be set
but lack scopes — e.g. name resolution fails — so verifying is worthwhile even when present.)

## Step 2 — Create or open the Slack app and set scopes

If the user doesn't have a bot token yet, walk them through it:

- Go to https://api.slack.com/apps → *Create New App* → *From scratch*, pick the workspace (or open
the existing app).
- *OAuth & Permissions* → *Bot Token Scopes*. Add these **read-only** scopes:
- `channels:history`, `channels:read` — read public channels
- `groups:history`, `groups:read` — read private channels
- `users:read`, `users.profile:read` — resolve reporter display names
- Do **not** add `chat:write` — the user posts the reply themselves; the bot only reads.
- `SLACK_TEAM_ID` is the workspace id (`T…`), e.g. from the URL `app.slack.com/client/T0XXXXXX/…`.

## Step 3 — Install (or reinstall) to the workspace

- *OAuth & Permissions* → *Install to Workspace* → *Allow*. Copy the **Bot User OAuth Token**
(`xoxb-…`).
- **Adding scopes later requires a *Reinstall to Workspace*** — Slack only grants newly-added scopes
on (re)install, which mints a **new** token. Changing the scope list in the config alone does
nothing until you reinstall. After reinstalling, copy the new `xoxb-…` token.

## Step 4 — Invite the bot to the channel

In the target Slack channel, run `/invite @<app-name>`. This is required to read a **private**
channel even with the scopes above (and harmless for public channels).

## Step 5 — Store the credentials

Ask the user to paste their `SLACK_BOT_TOKEN` (`xoxb-…`) and `SLACK_TEAM_ID` (`T…`). **Never echo the
token back** in your replies. Then write them into `.claude/mcp.local.env` as `KEY=value` lines —
this is the file `.mcp.json` sources for the slack server:

```
SLACK_BOT_TOKEN=xoxb-…
SLACK_TEAM_ID=T…
```

- If the file already exists, **preserve its other lines** — only set/replace the `SLACK_BOT_TOKEN`
and `SLACK_TEAM_ID` lines.
- It's gitignored via `.claude/*.local.*` — never commit it or write the token anywhere else.

Tell the user to **restart Claude Code** afterward — `.mcp.json` sources this file only when it
launches the server at startup, so a new/changed token is picked up only after a restart.

> Note: if the token string is unchanged and you only *reinstalled* to grant new scopes, Slack grants
> the new scopes to the existing token server-side, so a restart isn't strictly required for scope
> changes. A restart is required whenever the **token value** changes.

## Step 6 — Verify

After the restart, confirm the credentials actually work and carry the right scopes:

- Re-run the Step 1 check (expect `creds-present`).
- Discover the read tools with `ToolSearch` `slack thread replies conversation history permalink`,
then make one real read call — e.g. fetch a user profile or the users list. A successful response
means scopes are sufficient.
- If a call fails with `missing_scope`, the error names the scope it `needed` and lists what the token
currently `provided`. Add the missing scope in Step 2, **reinstall** (Step 3), and re-verify. Common
cases: `users.profile:read` (resolve a single user's name) and `users:read` (list users).
- The `atlassian` server (used by `/slack-triage` for Jira) authenticates via OAuth, not a token — if
Jira calls error with auth, run `/mcp` and finish the Atlassian login. No token to store here.

When the read call succeeds, setup is done — run `/slack-triage <thread-link>`.
Loading
Loading