diff --git a/.github/workflows/deps-bump.yml b/.github/workflows/deps-bump.yml
new file mode 100644
index 00000000..453dace1
--- /dev/null
+++ b/.github/workflows/deps-bump.yml
@@ -0,0 +1,194 @@
+name: Dependency Bump (Claude)
+
+on:
+ schedule:
+ - cron: '0 16 * * 1' # weekly full run, Mon 16:00 UTC (after the Monday Dependabot run)
+ - cron: '0 13 * * *' # daily Dependabot-alert poll
+ workflow_dispatch:
+ inputs:
+ branch:
+ description: 'Branch to bump (a branch name, or "all")'
+ default: 'main'
+ type: string
+
+permissions:
+ contents: read
+
+concurrency:
+ group: deps-bump-${{ github.workflow }}-${{ github.event_name }}
+ cancel-in-progress: false
+
+jobs:
+ discover:
+ if: ${{ github.repository == 'ericfitz/tmi' }}
+ runs-on: ubuntu-latest
+ outputs:
+ branches: ${{ steps.set.outputs.branches }}
+ steps:
+ - id: set
+ env:
+ GH_TOKEN: ${{ secrets.DEPS_ALERTS_TOKEN || github.token }}
+ REPO: ${{ github.repository }}
+ EVENT: ${{ github.event_name }}
+ SCHED: ${{ github.event.schedule }}
+ INPUT_BRANCH: ${{ github.event.inputs.branch }}
+ run: |
+ set -euo pipefail
+ all_branches() {
+ gh api "repos/$REPO/branches" --paginate --jq '.[].name' | grep -E '^(main|dev/.*)$' || true
+ }
+ open_alerts_count() {
+ gh api "repos/$REPO/dependabot/alerts?state=open&per_page=1" --jq 'length' 2>/dev/null || echo "ERR"
+ }
+ if [ "$EVENT" = "workflow_dispatch" ] && [ "$INPUT_BRANCH" != "all" ]; then
+ LIST="$INPUT_BRANCH"
+ elif [ "$EVENT" = "schedule" ] && [ "$SCHED" = "0 13 * * *" ]; then
+ CNT="$(open_alerts_count)"
+ if [ "$CNT" = "ERR" ]; then
+ echo "::warning::Could not read Dependabot alerts (token scope). Daily poll is a no-op until DEPS_ALERTS_TOKEN is set."
+ LIST=""
+ elif [ "$CNT" -gt 0 ] 2>/dev/null; then
+ echo "Open Dependabot alerts present — running the full branch set."
+ LIST="$(all_branches)"
+ else
+ echo "No open Dependabot alerts — nothing to do."
+ LIST=""
+ fi
+ else
+ LIST="$(all_branches)" # weekly schedule, or dispatch with branch=all
+ fi
+ BRANCHES="$(printf '%s\n' "$LIST" | grep -v '^$' | jq -R . | jq -cs .)"
+ echo "branches=$BRANCHES" >> "$GITHUB_OUTPUT"
+ echo "Discovered branches: $BRANCHES"
+
+ bump:
+ needs: discover
+ if: ${{ needs.discover.outputs.branches != '[]' && needs.discover.outputs.branches != '' }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ branch: ${{ fromJson(needs.discover.outputs.branches) }}
+ concurrency:
+ group: deps-bump-run-${{ matrix.branch }}
+ cancel-in-progress: false
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ matrix.branch }}
+ fetch-depth: 0
+ # The headless agent runs with --dangerously-skip-permissions; do NOT
+ # leave a push credential in its reach. The "Open PR" step authenticates
+ # explicitly with the App token instead.
+ persist-credentials: false
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Set up uv
+ uses: astral-sh/setup-uv@v5
+
+ - name: Set up pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9 # pnpm-lock.yaml is lockfileVersion 9.0; package.json has no packageManager field
+ run_install: false
+
+ - name: Install Go quality tools
+ run: |
+ set -euo pipefail
+ go install golang.org/x/vuln/cmd/govulncheck@latest
+ go install honnef.co/go/tools/cmd/staticcheck@latest
+ # golangci-lint via goinstall, pinned to the version tmi's security.yml uses
+ go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
+ echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
+
+ - name: Install Claude Code CLI
+ run: npm install -g @anthropic-ai/claude-code
+
+ - name: Vendor the bump skill into the runner
+ env:
+ # Pin ericfitz/skills to a reviewed commit so the high-privilege agent
+ # run below cannot execute an unreviewed change to the skill. Bump this
+ # SHA deliberately when adopting skill updates.
+ SKILL_SHA: "804ee39f7d594e896f6dab8501b59cb92556b8df"
+ run: |
+ set -euo pipefail
+ tmp="$(mktemp -d)"
+ git clone --filter=blob:none https://github.com/ericfitz/skills "$tmp/skills"
+ git -C "$tmp/skills" checkout -q "$SKILL_SHA"
+ mkdir -p .claude/skills/bump
+ cp -R "$tmp/skills/deps/skills/bump/." .claude/skills/bump/
+ test -f .claude/skills/bump/SKILL.md && echo "skill vendored @ $SKILL_SHA"
+
+ - name: Create working branch
+ id: wb
+ env:
+ BASE: ${{ matrix.branch }}
+ run: |
+ set -euo pipefail
+ WB="deps/auto-bump/${BASE}/${{ github.run_id }}"
+ git config user.name "deps-bot"
+ git config user.email "deps-bot@users.noreply.github.com"
+ git checkout -b "$WB"
+ {
+ echo "base=$BASE"
+ echo "wb=$WB"
+ } >> "$GITHUB_OUTPUT"
+
+ - name: Run headless bump (commit only — no push, no PR, no session-completion)
+ env:
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+ # Intentionally NO GH_TOKEN: the agent must not perform GitHub writes.
+ run: |
+ set -euo pipefail
+ if [ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]; then
+ echo "::error::CLAUDE_CODE_OAUTH_TOKEN is empty (secret missing on this ref)."
+ exit 1
+ fi
+ claude -p "Run the dependency bump skill (/bump) on the CURRENT checked-out git branch ONLY. You are running UNATTENDED in CI under a STRICT scope. DO: apply only safe patch/minor dependency updates; run the project build/test/lint; bisect out any update that breaks them; make exactly ONE local commit with the safe updates; then STOP. DO NOT: push; run any session-completion / 'landing the plane' / end-of-session workflow; file or close GitHub issues; run a security review; open a pull request; switch branches; ask questions. A separate CI step handles pushing and the PR. If there are no safe updates, make NO commit and say so. Finally, print the manual-review plan for anything not auto-applied." \
+ --dangerously-skip-permissions \
+ --allowedTools "Bash,Read,Write,Edit,Glob,Grep" | tee /tmp/bump-plan.txt
+
+ # Minted AFTER the agent step so the write-capable token never exists while
+ # the --dangerously-skip-permissions agent is running. Ephemeral (~1h),
+ # scoped to this App's Contents + Pull-requests permissions on this repo.
+ - name: Mint GitHub App token
+ id: app-token
+ uses: actions/create-github-app-token@v1
+ with:
+ app-id: ${{ secrets.DEPS_BOT_APP_ID }}
+ private-key: ${{ secrets.DEPS_BOT_APP_PRIVATE_KEY }}
+
+ - name: Open PR if there are changes
+ env:
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
+ BASE: ${{ steps.wb.outputs.base }}
+ WB: ${{ steps.wb.outputs.wb }}
+ REPO: ${{ github.repository }}
+ run: |
+ set -euo pipefail
+ if [ "$(git rev-list --count "origin/${BASE}..HEAD")" -eq 0 ]; then
+ echo "No commits relative to ${BASE} — no safe updates. Skipping PR."
+ exit 0
+ fi
+ # persist-credentials was false, so push with the App token explicitly.
+ git push "https://x-access-token:${GH_TOKEN}@github.com/${REPO}.git" "HEAD:refs/heads/${WB}"
+ {
+ echo "Automated dependency bump for \`${BASE}\` (safe patch/minor only)."
+ echo
+ echo "Generated by the \`deps:bump\` skill running headless in CI. Review before merging."
+ echo
+ echo 'Skill output / manual-review plan
'
+ echo
+ echo '```'
+ tail -c 60000 /tmp/bump-plan.txt
+ echo '```'
+ echo ' '
+ } > /tmp/pr-body.md
+ gh pr create --repo "${REPO}" --base "${BASE}" --head "${WB}" \
+ --title "chore(deps): automated bump for ${BASE}" \
+ --body-file /tmp/pr-body.md \
+ --label dependencies
diff --git a/.gitignore b/.gitignore
index 6bff6f92..3ff953e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -125,7 +125,6 @@ scripts/oci-env.sh
# OCI public deployment secrets
scripts/setup-oci-public.env
-docs/superpowers/**
test/integration/workflows.test
test/configs/google-drive-test-docs.json
test/configs/google-drive-credentials.json
diff --git a/CLAUDE.md b/CLAUDE.md
index 81b4ede7..20ce6497 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -500,10 +500,11 @@ When working with JSON files **larger than 100KB**, use streaming approaches wit
**IMPORTANT**: All project documentation is maintained in the GitHub Wiki. Do NOT update markdown files in the `docs/` directory - they are deprecated and will be removed.
-Do not update or add any content to the docs/ directory. Instead, update or add the content to the appropriate page on the tmi wiki.
+Do not update or add any content to the docs/ directory (except the `docs/superpowers/` subtree — see exception below). Instead, update or add the content to the appropriate page on the tmi wiki.
- **Authoritative documentation**: GitHub Wiki (https://github.com/ericfitz/tmi/wiki)
- **Local `docs/` directory**: Deprecated, do not update
+- **Exception — `docs/superpowers/`**: Superpowers skills (brainstorming, writing-plans, etc.) legitimately write specs and plans under `docs/superpowers/` (e.g., `docs/superpowers/specs/`). This subtree is allowed and is NOT covered by the "do not update docs/" rule. It is only for superpowers-generated artifacts — do not put hand-authored project documentation here (that still belongs in the wiki).
## Python Development
diff --git a/docs/superpowers/plans/2026-06-18-deps-bump-automation.md b/docs/superpowers/plans/2026-06-18-deps-bump-automation.md
new file mode 100644
index 00000000..9f05e35e
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-18-deps-bump-automation.md
@@ -0,0 +1,582 @@
+# Dependency Bump Automation — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Run the existing `deps:bump` Claude Code skill automatically on GitHub Actions — on a weekly schedule, on a daily Dependabot-alert poll, and on manual dispatch — producing one independent PR per target branch (main + `dev/*`), with no cross-branch cherry-pick or merge.
+
+**Architecture:** A single workflow `.github/workflows/deps-bump.yml`. A `discover` job computes the target-branch matrix from the trigger type. A `bump` job (matrix, `fail-fast: false`) checks out each branch, sets up the toolchain, vendors the `bump` skill into the runner, runs `claude -p` headless to apply safe updates + commit, then opens a PR targeting that branch. Build is de-risked by a spike (Task 1) before scheduling/matrix are added.
+
+**Tech Stack:** GitHub Actions, Claude Code CLI (`@anthropic-ai/claude-code`), `gh` CLI, Go toolchain (`make build-server`/`make test-unit`/`make lint`), uv, Node/pnpm, `actionlint` for static workflow validation.
+
+## Global Constraints
+
+- Validation commands come from tmi's CLAUDE.md / Makefile: `make build-server`, `make test-unit` (fast, **no external deps**), `make lint`. The skill discovers these automatically (CLAUDE.md → Makefile precedence).
+- Bump exclusions are honored by the skill from `## Bump Exclusions` in tmi CLAUDE.md (currently `github.com/golang/protobuf`), `.bump-config.json`, and `// pinned:` comments. Do not re-implement exclusion logic.
+- Output model: **PR per branch** to a working branch named `deps/auto-bump//`; never push to the source branch directly.
+- Branch set for "all": `main` + every `dev/*` branch. **No cherry-pick / merge between branches.**
+- Claude auth: subscription via `CLAUDE_CODE_OAUTH_TOKEN` secret (generated by `claude setup-token`). Never use an API key.
+- Skill source of truth: marketplace repo `ericfitz/skills`, path `deps/skills/bump/SKILL.md`. Vendor at runtime (shallow clone), do **not** commit a copy into tmi.
+- Headless permission: run `claude -p` with `--dangerously-skip-permissions` (autonomous CI; no human to approve). **VERIFY exact flag in Task 1** — fallback is `.claude/settings.json` `permissions.defaultMode: bypassPermissions`.
+- Workflow permissions: `contents: write`, `pull-requests: write`. Dependabot-alerts read may require a fine-grained PAT secret `DEPS_ALERTS_TOKEN` (verified in Task 5).
+- Spec: `docs/superpowers/specs/2026-06-18-deps-bump-automation-design.md`.
+
+---
+
+## Task 0: Prerequisites (one-time setup, mostly human actions)
+
+**Files:** none (repo/secret configuration)
+
+**Interfaces:**
+- Produces: repo secret `CLAUDE_CODE_OAUTH_TOKEN`; `actionlint` available locally.
+
+- [ ] **Step 1: Generate the Claude OAuth token (human runs locally)**
+
+```bash
+claude setup-token
+# Completes OAuth in browser; prints a ~1-year token to stdout. Copy it.
+```
+Expected: a token string printed (not saved to disk).
+
+- [ ] **Step 2: Add the token as a repo secret**
+
+```bash
+gh secret set CLAUDE_CODE_OAUTH_TOKEN --repo ericfitz/tmi
+# Paste the token when prompted.
+gh secret list --repo ericfitz/tmi | grep CLAUDE_CODE_OAUTH_TOKEN
+```
+Expected: `CLAUDE_CODE_OAUTH_TOKEN` appears in the list.
+
+- [ ] **Step 3: Install actionlint locally (for static validation in later tasks)**
+
+```bash
+go install github.com/rhysd/actionlint/cmd/actionlint@latest
+actionlint -version
+```
+Expected: a version string prints.
+
+- [ ] **Step 4: Confirm the skill path exists upstream**
+
+```bash
+gh api repos/ericfitz/skills/contents/deps/skills/bump/SKILL.md --jq '.path'
+```
+Expected: `deps/skills/bump/SKILL.md`.
+
+No commit (configuration only).
+
+---
+
+## Task 1: Spike — headless bump on one branch (de-risk)
+
+Prove the headless skill run works end-to-end on `dev/1.4.0` with a minimal, manually-dispatched workflow. This resolves the flagged unknowns (exact permission flag, OAuth in CI, toolchain completeness, skill discovery) before any scheduling/matrix work.
+
+**Files:**
+- Create: `.github/workflows/deps-bump.yml` (minimal, single-job, dispatch-only — grown in later tasks)
+
+**Interfaces:**
+- Produces: a verified `claude -p` invocation and the exact toolchain setup steps that later tasks reuse.
+
+- [ ] **Step 1: Create the minimal spike workflow**
+
+```yaml
+# .github/workflows/deps-bump.yml
+name: Dependency Bump (Claude)
+
+on:
+ # Iteration trigger: workflow_dispatch & schedule only fire from the default
+ # branch, so during development we trigger on pushes to the feature branch.
+ # Replaced by workflow_dispatch + schedule in Task 5 before merging to main.
+ push:
+ branches: [feature/deps-bump-automation]
+
+permissions:
+ contents: read # spike only reads + commits locally; no push yet
+
+jobs:
+ bump:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Set up uv
+ uses: astral-sh/setup-uv@v5
+
+ - name: Set up Node/pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ run_install: false
+
+ - name: Install Go quality tools
+ run: |
+ set -euo pipefail
+ go install golang.org/x/vuln/cmd/govulncheck@latest
+ go install honnef.co/go/tools/cmd/staticcheck@latest
+ curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "$(go env GOPATH)/bin"
+ echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
+
+ - name: Install Claude Code CLI
+ run: npm install -g @anthropic-ai/claude-code
+
+ - name: Vendor the bump skill into the runner
+ run: |
+ set -euo pipefail
+ tmp="$(mktemp -d)"
+ git clone --depth 1 https://github.com/ericfitz/skills "$tmp/skills"
+ mkdir -p .claude/skills/bump
+ cp -R "$tmp/skills/deps/skills/bump/." .claude/skills/bump/
+ test -f .claude/skills/bump/SKILL.md && echo "skill vendored"
+
+ - name: Run headless bump (commit only, no push)
+ env:
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ git config user.name "deps-bot"
+ git config user.email "deps-bot@users.noreply.github.com"
+ claude -p "/bump — run the dependency bump skill on the CURRENT checked-out branch only. Do NOT switch branches and do NOT ask me anything; you are running unattended in CI. Apply only safe patch/minor updates, run build/test/lint, bisect on failure, commit the safe subset, and print the manual-review plan. If there are no safe updates, make no commit and say so." \
+ --dangerously-skip-permissions \
+ --allowedTools "Bash,Read,Write,Edit,Glob,Grep"
+
+ - name: Show result
+ run: |
+ git log --oneline -3
+ git status -sb
+```
+
+- [ ] **Step 2: Statically validate the workflow**
+
+Run: `actionlint .github/workflows/deps-bump.yml`
+Expected: no output (exit 0). Fix any reported syntax/type issues.
+
+- [ ] **Step 3: Commit the spike workflow**
+
+```bash
+git add .github/workflows/deps-bump.yml
+git commit -m "ci(deps): spike headless deps bump on one branch"
+```
+
+- [ ] **Step 4: Push the branch the workflow lives on and dispatch the run**
+
+> The workflow must exist on the branch you dispatch from. Push to a CI branch (e.g. `dev/1.4.0`) per the project's normal push process, then:
+
+```bash
+gh workflow run deps-bump.yml --ref dev/1.4.0 -f branch=dev/1.4.0
+gh run watch "$(gh run list --workflow=deps-bump.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
+```
+Expected: the run completes green. In the "Run headless bump" log, the skill detects ecosystems, applies safe updates (or reports none), and build/test/lint pass. The "Show result" step shows either a new `chore(deps): ...` commit or a clean tree with a "no safe updates" message.
+
+- [ ] **Step 5: Record verified facts inline in the workflow**
+
+If `--dangerously-skip-permissions` was rejected or insufficient, switch to writing `.claude/settings.json` before the run:
+```bash
+mkdir -p .claude
+printf '{\n "permissions": { "defaultMode": "bypassPermissions" }\n}\n' > .claude/settings.json
+```
+Update the toolchain steps to match whatever the spike actually needed (e.g. if Node isn't a real ecosystem in tmi, drop the pnpm setup). Commit:
+```bash
+git add .github/workflows/deps-bump.yml .claude/settings.json
+git commit -m "ci(deps): pin verified headless invocation for bump spike"
+```
+Expected: a follow-up green dispatch confirming the pinned invocation.
+
+---
+
+## Task 2: Open a PR with the bump result
+
+Extend the spike so a successful bump lands as a PR against the source branch (the agreed output model), and a no-op exits cleanly.
+
+**Files:**
+- Modify: `.github/workflows/deps-bump.yml` (add working-branch + PR steps; raise `permissions`)
+
+**Interfaces:**
+- Consumes: the verified `claude -p` step from Task 1.
+- Produces: a PR per run when there are changes; the skill's plan in the PR body.
+
+- [ ] **Step 1: Raise workflow permissions**
+
+```yaml
+permissions:
+ contents: write
+ pull-requests: write
+```
+
+- [ ] **Step 2: Create a working branch before the bump runs**
+
+Insert before the "Run headless bump" step:
+```yaml
+ - name: Create working branch
+ id: wb
+ run: |
+ set -euo pipefail
+ WB="deps/auto-bump/${{ github.event.inputs.branch }}/${{ github.run_id }}"
+ git config user.name "deps-bot"
+ git config user.email "deps-bot@users.noreply.github.com"
+ git checkout -b "$WB"
+ echo "working_branch=$WB" >> "$GITHUB_OUTPUT"
+ echo "base_branch=${{ github.event.inputs.branch }}" >> "$GITHUB_OUTPUT"
+```
+
+- [ ] **Step 3: Capture the skill's plan output to a file**
+
+Change the bump step to tee Claude's stdout so the plan can go in the PR body:
+```yaml
+ - name: Run headless bump (commit only)
+ env:
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ claude -p "/bump — run the dependency bump skill on the CURRENT checked-out branch only. Do NOT switch branches and do NOT ask me anything; you are running unattended in CI. Apply only safe patch/minor updates, run build/test/lint, bisect on failure, commit the safe subset, and print the manual-review plan. If there are no safe updates, make no commit and say so." \
+ --dangerously-skip-permissions \
+ --allowedTools "Bash,Read,Write,Edit,Glob,Grep" | tee /tmp/bump-plan.txt
+```
+
+- [ ] **Step 4: Open a PR only if the bump produced commits**
+
+Replace the "Show result" step:
+```yaml
+ - name: Open PR if there are changes
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ BASE="${{ steps.wb.outputs.base_branch }}"
+ WB="${{ steps.wb.outputs.working_branch }}"
+ if git diff --quiet "origin/$BASE".."$WB" 2>/dev/null && [ "$(git rev-list --count "origin/$BASE".."$WB")" -eq 0 ]; then
+ echo "No commits relative to $BASE — no safe updates. Skipping PR."
+ exit 0
+ fi
+ git push origin "$WB"
+ {
+ echo "Automated dependency bump for \`$BASE\` (safe patch/minor only)."
+ echo
+ echo "Generated by the \`deps:bump\` skill running headless in CI."
+ echo
+ echo 'Manual-review plan from the skill
'
+ echo
+ echo '```'
+ tail -c 60000 /tmp/bump-plan.txt
+ echo '```'
+ echo ' '
+ } > /tmp/pr-body.md
+ gh pr create --base "$BASE" --head "$WB" \
+ --title "chore(deps): automated bump for $BASE" \
+ --body-file /tmp/pr-body.md \
+ --label dependencies
+```
+
+- [ ] **Step 5: Validate and dispatch**
+
+Run: `actionlint .github/workflows/deps-bump.yml`
+Expected: exit 0.
+Then commit, push, and dispatch as in Task 1 Step 4. Expected: a PR is opened against `dev/1.4.0` (if there are safe updates) with the plan in a collapsible section, OR the run logs "No commits … Skipping PR."
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add .github/workflows/deps-bump.yml
+git commit -m "ci(deps): open a PR per run with the bump result"
+```
+
+---
+
+## Task 3: Generalize the single-branch job (dispatch input)
+
+Make the job branch-agnostic so it can run against any single branch via `workflow_dispatch`, in preparation for the matrix.
+
+**Files:**
+- Modify: `.github/workflows/deps-bump.yml`
+
+**Interfaces:**
+- Consumes: the PR-producing job from Task 2.
+- Produces: a job parameterized by a single `branch` value (later supplied by the matrix as `${{ matrix.branch }}`).
+
+- [ ] **Step 1: Replace hardcoded `github.event.inputs.branch` with a job-level env**
+
+Add at the top of the `bump` job so the same expression works for both dispatch and (later) matrix:
+```yaml
+ bump:
+ runs-on: ubuntu-latest
+ env:
+ TARGET_BRANCH: ${{ matrix.branch || github.event.inputs.branch }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ env.TARGET_BRANCH }}
+ fetch-depth: 0
+```
+Replace every other `${{ github.event.inputs.branch }}` in the job with `${{ env.TARGET_BRANCH }}`.
+
+- [ ] **Step 2: Make the dispatch `branch` input free-form with a sane default**
+
+```yaml
+ workflow_dispatch:
+ inputs:
+ branch:
+ description: 'Branch to bump (a branch name, or "all")'
+ default: 'main'
+ type: string
+```
+
+- [ ] **Step 3: Validate, commit, dispatch against `main`**
+
+Run: `actionlint .github/workflows/deps-bump.yml` → exit 0.
+```bash
+git add .github/workflows/deps-bump.yml
+git commit -m "ci(deps): parameterize bump job by target branch"
+```
+Dispatch: `gh workflow run deps-bump.yml --ref dev/1.4.0 -f branch=main` and watch.
+Expected: runs against `main`, opens a PR targeting `main` (or clean no-op).
+
+---
+
+## Task 4: Branch discovery + matrix ("all branches independently")
+
+Add a `discover` job that emits the branch matrix, and drive `bump` from it. This delivers the "main / specific / all-independently, no cherry-pick or merge" requirement.
+
+**Files:**
+- Modify: `.github/workflows/deps-bump.yml`
+
+**Interfaces:**
+- Produces: `needs.discover.outputs.branches` — a JSON array of branch names.
+- Consumes (in `bump`): `${{ fromJson(needs.discover.outputs.branches) }}` as `matrix.branch`.
+
+- [ ] **Step 1: Add the `discover` job**
+
+```yaml
+ discover:
+ runs-on: ubuntu-latest
+ outputs:
+ branches: ${{ steps.set.outputs.branches }}
+ steps:
+ - id: set
+ env:
+ GH_TOKEN: ${{ github.token }}
+ REPO: ${{ github.repository }}
+ EVENT: ${{ github.event_name }}
+ INPUT_BRANCH: ${{ github.event.inputs.branch }}
+ run: |
+ set -euo pipefail
+ all_branches() {
+ gh api "repos/$REPO/branches" --paginate --jq '.[].name' \
+ | grep -E '^(main|dev/.*)$' || true
+ }
+ if [ "$EVENT" = "workflow_dispatch" ] && [ "$INPUT_BRANCH" != "all" ]; then
+ LIST="$INPUT_BRANCH"
+ else
+ LIST="$(all_branches)"
+ fi
+ BRANCHES="$(printf '%s\n' "$LIST" | grep -v '^$' | jq -R . | jq -cs .)"
+ echo "branches=$BRANCHES" >> "$GITHUB_OUTPUT"
+ echo "Discovered: $BRANCHES"
+```
+
+- [ ] **Step 2: Convert `bump` to a matrix job that needs `discover`**
+
+```yaml
+ bump:
+ needs: discover
+ if: ${{ needs.discover.outputs.branches != '[]' && needs.discover.outputs.branches != '' }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ branch: ${{ fromJson(needs.discover.outputs.branches) }}
+ env:
+ TARGET_BRANCH: ${{ matrix.branch }}
+```
+(Remove the `matrix.branch || github.event.inputs.branch` fallback now that the matrix always supplies it.)
+
+- [ ] **Step 3: Add per-branch concurrency so re-runs don't collide**
+
+```yaml
+ concurrency:
+ group: deps-bump-${{ matrix.branch }}
+ cancel-in-progress: false
+```
+
+- [ ] **Step 4: Validate, commit, dispatch with `all`**
+
+Run: `actionlint .github/workflows/deps-bump.yml` → exit 0.
+```bash
+git add .github/workflows/deps-bump.yml
+git commit -m "ci(deps): discover branches and fan out per-branch (matrix)"
+```
+Dispatch: `gh workflow run deps-bump.yml --ref dev/1.4.0 -f branch=all` and watch.
+Expected: `discover` lists `main` + `dev/*`; one independent `bump` job per branch; each opens its own PR (or no-ops). A failure on one branch does not abort the others (`fail-fast: false`).
+
+---
+
+## Task 5: Scheduling + daily Dependabot-alert poll
+
+Add the weekly full run and the daily alert-gated run.
+
+**Files:**
+- Modify: `.github/workflows/deps-bump.yml`
+
+**Interfaces:**
+- Consumes: the `discover`/`bump` jobs from Task 4.
+- Produces: scheduled triggers; `discover` returns `[]` on a daily poll with no open alerts (so `bump` is skipped by its `if`).
+
+- [ ] **Step 1: Add the schedule triggers**
+
+```yaml
+on:
+ schedule:
+ - cron: '0 16 * * 1' # weekly full run, Mon 16:00 UTC (after Dependabot Monday)
+ - cron: '0 13 * * *' # daily Dependabot-alert poll
+ workflow_dispatch:
+ inputs:
+ branch:
+ description: 'Branch to bump (a branch name, or "all")'
+ default: 'main'
+ type: string
+```
+
+- [ ] **Step 2: Teach `discover` the three trigger modes**
+
+Replace the `discover` `run:` body:
+```bash
+set -euo pipefail
+all_branches() {
+ gh api "repos/$REPO/branches" --paginate --jq '.[].name' \
+ | grep -E '^(main|dev/.*)$' || true
+}
+open_alerts_count() {
+ gh api "repos/$REPO/dependabot/alerts?state=open&per_page=1" --jq 'length' 2>/dev/null || echo "ERR"
+}
+
+if [ "$EVENT" = "workflow_dispatch" ] && [ "$INPUT_BRANCH" != "all" ]; then
+ LIST="$INPUT_BRANCH"
+elif [ "$EVENT" = "schedule" ] && [ "$SCHED" = "0 13 * * *" ]; then
+ CNT="$(open_alerts_count)"
+ if [ "$CNT" = "ERR" ]; then
+ echo "::error::Could not read Dependabot alerts (token scope?). See Task 5 Step 3."
+ LIST=""
+ elif [ "$CNT" -gt 0 ] 2>/dev/null; then
+ echo "Open Dependabot alerts present ($CNT) — running full set."
+ LIST="$(all_branches)"
+ else
+ echo "No open Dependabot alerts — nothing to do."
+ LIST=""
+ fi
+else
+ # weekly schedule, or dispatch with branch=all
+ LIST="$(all_branches)"
+fi
+BRANCHES="$(printf '%s\n' "$LIST" | grep -v '^$' | jq -R . | jq -cs .)"
+echo "branches=$BRANCHES" >> "$GITHUB_OUTPUT"
+echo "Discovered: $BRANCHES"
+```
+Add `SCHED: ${{ github.event.schedule }}` to the step's `env:`.
+
+- [ ] **Step 3: Verify the Dependabot-alerts token, add PAT only if needed**
+
+Test whether `github.token` can read alerts:
+```bash
+gh workflow run deps-bump.yml --ref dev/1.4.0 # fires as dispatch=all; does NOT exercise the poll path
+```
+To exercise the poll path directly, temporarily add `workflow_dispatch` input `force_poll` and branch on it, OR test the API locally:
+```bash
+GH_TOKEN=$(gh auth token) gh api "repos/ericfitz/tmi/dependabot/alerts?state=open&per_page=1" --jq 'length'
+```
+Expected: a number. If the in-workflow `github.token` call logs the `::error::` (403/empty), create a fine-grained PAT with **Dependabot alerts: read**, add it as secret `DEPS_ALERTS_TOKEN`, and set in the `discover` step env:
+```yaml
+ GH_TOKEN: ${{ secrets.DEPS_ALERTS_TOKEN || github.token }}
+```
+
+- [ ] **Step 4: Validate and commit**
+
+Run: `actionlint .github/workflows/deps-bump.yml` → exit 0.
+```bash
+git add .github/workflows/deps-bump.yml
+git commit -m "ci(deps): add weekly schedule + daily alert-gated poll"
+```
+Expected: manual `branch=all` dispatch still works; the daily/weekly crons are registered (visible under the repo Actions tab → the workflow → "This workflow has scheduled events").
+
+---
+
+## Task 6: Hardening, docs, and finalize
+
+**Files:**
+- Modify: `.github/workflows/deps-bump.yml`
+- Create: wiki page note (GitHub Wiki, per tmi docs policy — not `docs/`)
+
+**Interfaces:** none new.
+
+- [ ] **Step 1: Make a bump-step failure surface clearly without aborting siblings**
+
+Confirm `strategy.fail-fast: false` is set (Task 4). Add a guard so a Claude failure still reports:
+```yaml
+ - name: Run headless bump (commit only)
+ id: bump
+ continue-on-error: false
+ env:
+ CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ if [ -z "${CLAUDE_CODE_OAUTH_TOKEN:-}" ]; then
+ echo "::error::CLAUDE_CODE_OAUTH_TOKEN is empty (secret missing on this ref)."; exit 1
+ fi
+ claude -p "...prompt as in Task 2..." \
+ --dangerously-skip-permissions \
+ --allowedTools "Bash,Read,Write,Edit,Glob,Grep" | tee /tmp/bump-plan.txt
+```
+
+- [ ] **Step 2: Add a top-level guard against scheduled runs on forks**
+
+```yaml
+jobs:
+ discover:
+ if: ${{ github.repository == 'ericfitz/tmi' }}
+ runs-on: ubuntu-latest
+```
+
+- [ ] **Step 3: Document the workflow in the GitHub Wiki**
+
+Add a wiki page (e.g. "CI: Automated Dependency Bumps") covering: triggers, the `branch`/`all` dispatch input, where PRs land, the `CLAUDE_CODE_OAUTH_TOKEN`/`DEPS_ALERTS_TOKEN` secrets, and how to disable (comment out the `schedule:` block). Do **not** add this under `docs/` (tmi policy: wiki only; `docs/superpowers/` is for specs/plans only).
+
+- [ ] **Step 4: Final static validation + commit**
+
+Run: `actionlint .github/workflows/deps-bump.yml` → exit 0.
+```bash
+git add .github/workflows/deps-bump.yml
+git commit -m "ci(deps): harden deps-bump workflow (fork guard, token check)"
+```
+
+- [ ] **Step 5: Full acceptance dispatch**
+
+```bash
+gh workflow run deps-bump.yml --ref dev/1.4.0 -f branch=all
+gh run watch "$(gh run list --workflow=deps-bump.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
+```
+Expected (success criteria from the spec):
+- `discover` emits `main` + `dev/*`.
+- One independent PR per branch with safe updates (or a clean no-op log).
+- Existing PR checks (`security-deps-gate.yml`, `security.yml`, `codeql.yml`) run on each PR.
+
+---
+
+## Self-Review
+
+**Spec coverage:**
+- "main / specific / all-independently, no cherry-pick/merge" → Tasks 3–4 (matrix, per-branch jobs, no cross-branch ops). ✓
+- "schedule on GitHub" → Task 5 weekly cron. ✓
+- "triggered by Dependabot alerts" → Task 5 daily poll (documented approximation; spec Risk 3). ✓
+- PR-per-branch output → Task 2. ✓
+- OAuth auth → Tasks 0/1. ✓
+- Skill-in-CI risk → Task 1 spike (vendor at runtime). ✓
+- Alert-token risk → Task 5 Step 3. ✓
+- Toolchain for build/test/lint → Task 1 setup steps. ✓
+
+**Placeholder scan:** Prompts, YAML, and commands are concrete. The two explicitly-flagged unknowns (permission flag; alerts token) have verification steps and named fallbacks rather than silent TBDs. ✓
+
+**Type consistency:** `discover.outputs.branches` (JSON array) is produced in Tasks 4–5 and consumed via `fromJson(...)` as `matrix.branch`; `TARGET_BRANCH` env is introduced in Task 3 and used consistently thereafter; `steps.wb.outputs.{working_branch,base_branch}` defined and used in Task 2. ✓
diff --git a/docs/superpowers/specs/2026-06-18-deps-bump-automation-design.md b/docs/superpowers/specs/2026-06-18-deps-bump-automation-design.md
new file mode 100644
index 00000000..785e9bfa
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-18-deps-bump-automation-design.md
@@ -0,0 +1,126 @@
+# Design: Automate the `deps:bump` skill on GitHub Actions
+
+**Date:** 2026-06-18
+**Repo (first target):** `ericfitz/tmi` (pattern generalizes to `tmi-ux` later)
+**Status:** Awaiting user review → implementation plan
+
+## Goal
+
+Run the existing Claude Code `deps:bump` skill automatically, so dependency
+updates land as reviewable PRs without a human kicking off each run.
+
+### Requirements (from the user)
+
+1. Run against **main (default), a specific branch, or all branches
+ independently** — with **no cherry-pick and no merge** between branches.
+ Each branch's bump operates only on that branch.
+2. Run **automatically on a schedule** on GitHub, **and** be **triggered by
+ Dependabot alerts** (approximated — see Constraints).
+
+### Decisions (locked with the user)
+
+| Topic | Decision |
+|-------|----------|
+| Output model | **Open a PR per branch** (commit to a working branch, PR targets the source branch). |
+| "All branches" scope | **`main` + every `dev/*` release branch** (discovered dynamically). |
+| Claude auth | **`CLAUDE_CODE_OAUTH_TOKEN`** (user's subscription via `claude setup-token`); no API billing. |
+| Schedule | **Weekly full run** (Mon, after Dependabot's Monday run) + **daily alert poll**. |
+| Alert trigger | **Daily poll** that runs the bump only when open Dependabot alerts exist. |
+
+## What `deps:bump` does (and doesn't)
+
+The skill (`efitz-skills` marketplace, `deps/skills/bump/SKILL.md`):
+- Auto-detects ecosystems (Go / Python / Node).
+- Loads exclusions from `## Bump Exclusions` in `CLAUDE.md`, `.bump-config.json`,
+ and `// pinned:` / `# pinned:` comments.
+- Applies **safe patch/minor** updates only (majors → manual-review plan).
+- Runs build + test + lint; **bisects** to isolate a bad package on failure.
+- **Commits** the safe subset and prints a prioritized manual-review plan.
+
+It **does not push** and **does not open PRs** — automation supplies that step.
+Its Phase 1 is **interactive** (asks about switching branches) — the CI prompt
+pins it to the current branch and forbids prompting.
+
+tmi validation commands (per tmi `CLAUDE.md`): `make build-server`,
+`make test-unit` (fast, **no external deps** — CI-friendly), `make lint`
+(`uv run scripts/lint.py` + golangci-lint + staticcheck checks).
+
+## Architecture
+
+Single workflow: **`.github/workflows/deps-bump.yml`**.
+
+### Triggers (`on:`)
+- `schedule: '0 16 * * 1'` — weekly full run (Mon 16:00 UTC, after the Monday
+ Dependabot run).
+- `schedule: '0 13 * * *'` — daily alert poll.
+- `workflow_dispatch` — inputs: `branch` (specific name or `all`),
+ `ecosystem` (optional: go/node/python).
+
+Branch logic distinguishes the run type via `github.event.schedule` /
+`github.event_name`.
+
+### Job 1 — `discover` (matrix builder)
+Outputs a JSON array of target branches:
+- Weekly / dispatch `all` → `main` + `dev/*` (via `gh api repos/$REPO/branches`).
+- Dispatch specific branch → just that branch.
+- Daily poll → query open Dependabot alerts; map to affected branches; if none,
+ output `[]` so the bump job is skipped.
+
+### Job 2 — `bump` (matrix over `discover` output)
+`strategy.fail-fast: false`; `concurrency` keyed per branch (no colliding runs).
+Per branch:
+1. `actions/checkout` the target branch (full history).
+2. Set up toolchain: Go, uv, Node/pnpm; install `govulncheck`,
+ `golangci-lint`, `staticcheck`, `gh`.
+3. Make the `deps:bump` skill available to a headless Claude Code run
+ (see Risk 1 for the mechanism).
+4. `git checkout -b deps/auto-bump//`.
+5. Run Claude headless with a pinned prompt: *"Run the deps bump skill on the
+ current branch. Never switch branches, never prompt. Commit the safe
+ updates and output the manual-review plan."*
+6. If a commit was produced (working branch differs from base): push the
+ working branch and `gh pr create --base ` with the skill's
+ manual-review **plan in the PR body**. If no changes: skip.
+
+Existing PR checks (`security-deps-gate.yml`, `security.yml`, `codeql.yml`)
+run on the PR before merge — this is the review gate.
+
+### Auth & permissions
+- Repo secret `CLAUDE_CODE_OAUTH_TOKEN` (from `claude setup-token`).
+- Workflow `permissions: { contents: write, pull-requests: write }`.
+- Dependabot-alert read may require a fine-grained **PAT** secret rather than
+ the default `GITHUB_TOKEN` (see Risk 2).
+
+## Constraints & Risks (resolve in implementation plan)
+
+1. **Skill-in-CI (main risk).** The skill ships in the `efitz-skills`
+ marketplace plugin; it must be available to a headless runner. Candidate
+ approaches: (a) install the marketplace/plugin in CI, or (b) vendor the
+ `bump` SKILL.md into the runner workspace and point Claude at it. Pick and
+ validate one in the plan.
+2. **Dependabot alerts API auth.** Reading `repos/$REPO/dependabot/alerts` may
+ need a token with Dependabot-alerts: read, which the default `GITHUB_TOKEN`
+ may not grant. Plan: confirm; add a fine-grained PAT secret only if needed.
+3. **`dependabot_alert` is not a native Actions trigger** (webhook event only;
+ open feature request). Hence the daily poll. Acknowledged tradeoff:
+ up to ~24h latency from alert to bump run.
+4. **PRs opened with `GITHUB_TOKEN` don't trigger downstream workflows** by
+ default. Since merging is manual and the security gates run on `push`/PR via
+ their own triggers, confirm the gates fire; if not, use a PAT for `pr create`.
+5. **CI cost/usage.** Headless Claude runs consume subscription usage; weekly +
+ daily-when-alerts keeps volume modest. `fail-fast: false` isolates per-branch
+ failures.
+
+## Out of scope (this iteration)
+- Auto-merging PRs.
+- Major-version upgrades (skill defers these to the manual plan by design).
+- tmi-ux and other repos (replicate the pattern after tmi is proven).
+- Cross-branch propagation (explicitly excluded: no cherry-pick / merge).
+
+## Success criteria
+- Manual `workflow_dispatch` on a chosen branch opens a PR (or cleanly reports
+ "no safe updates") with passing toolchain setup.
+- Weekly schedule fans out across `main` + `dev/*`, one independent PR per
+ branch that has safe updates.
+- Daily poll is a no-op when there are no open alerts and opens PRs when there
+ are.