---
name: az-descent
description: Azterra autonomous "descent" loop — one self-feeding work iteration on a LIVE app. Scan/recc to generate work, do the top backlog item (⚙️ build via executor, or 🎨 design-room → build to SAFE defaults), gate it (build + lint + tests + Vercel READY), auto-merge ONLY safe categories, PR-and-park everything that touches server/auth/Supabase/secrets/reveal-gates, append findings, then re-arm or go IDLE. Driven by `/loop /az-descent`. PROD-DEPLOY-AWARE: pushing to `main` auto-deploys to production. Never decides 🔒 reserved items.
triggers:
  - keywords: "az-descent", "azterra descent", "continue the descent", "/loop /az-descent"
---

# az-descent — one autonomous work iteration (PROD-DEPLOY-AWARE)

> **Source of truth:** `.claude/planning/AUTONOMOUS_BACKLOG.md` (the queue) + `Azterra/CLAUDE.md` + **`CLAUDE.local.md`
> (AZ_LAW + the Vercel build-verify protocol)** + `.claude/workflows/az-design-room.mjs` (creative specs). This
> skill defines **one iteration**. `/loop /az-descent` self-paces it via `ScheduleWakeup` (the "boulder"): each
> wake runs one iteration and re-arms the next — until the iteration goes **IDLE** (stops re-arming) or Nick
> stops it (interrupt the `/loop`). **You are the dispatcher: delegate reading/editing to `executor`; hold compact
> returns; keep main context < 60%.**
>
> ### 🔴 THE ONE RULE THAT MAKES THIS DIFFERENT FROM KTM: **Azterra is LIVE, and EVERY push to `main` auto-deploys
> to PRODUCTION on BOTH layers:** (1) **Vercel** rebuilds + deploys the **frontend**; (2) **Render** auto-deploys
> the backend that serves `/api/*` (**verified live 2026-06-09** via field probes against the live API; the old
> AWS-EB workflow is deleted — its creds were revoked). A `server/` change merged to `main` is RUNNING ON PROD
> within minutes, with zero buffer — and any schema/column the new code writes must exist in Supabase BEFORE the
> merge. **Therefore auto-merge is DOCS-AND-NET-NEW-TESTS-ONLY** (see the AUTO-MERGE POLICY). ALL logic — anything
> under `src/` OR `server/`, any data/config/schema/secrets/reveal-gate — is **PR-and-park: build it, gate it, open the
> PR, leave it for Nick.** **There is NO branch protection on `main` and merges use `--admin`, so NOTHING
> machine-enforces this — it is entirely on the loop's discipline.** When in doubt, PR-and-park. Shipping a broken or
> insecure change to live prod is the one unrecoverable failure here.

## ⚠️ Path note (Azterra-specific divergence from the KTM template)
In Azterra, **`docs/` is the Vite build outDir and is git-ignored**, and **`.omc/` is git-ignored** too. So the
KTM convention path `docs/planning/AUTONOMOUS_BACKLOG.md` **cannot be committed here.** The committed backlog
lives at **`.claude/planning/AUTONOMOUS_BACKLOG.md`** (`.claude/` is tracked). The per-run descent log lives at
**`docs/planning/<date>-descent-log.md`** (local-only / git-ignored — the room's bold/reserved forks are notes
for Nick, not shared-repo artifacts; that's fine, they're surfaced in chat too). `STATUS.md` and `CLAUDE.local.md`
are also git-excluded (local-only). **Never** try to `git add docs/`, `.omc/`, `.env`, `STATUS.md`, or
`CLAUDE.local.md` — stage explicit tracked paths only.

## Confirmed operating policy
1. **Build creative to SAFE defaults.** A design-room spec that SHIPs clean → build it to the room's safe-default
   forks. Park ONLY hard blockers (reserved fork: canon / auth-tier / non-additive migration / reveal-to-a-new-role,
   `[NEEDS-NEW]`/missing API, or anything in the 🔒 set). Log the bold/reserved forks; never pause for them.
2. **Self-driving** via `/loop /az-descent` — re-arm between iterations; run a scan each iteration to generate
   work; keep finding new work until only 🔒 reserved remains.
3. **Auto-merge DOCS + NET-NEW TESTS only** (see the AUTO-MERGE POLICY) — review-lane + all gates green + **Vercel
   READY after merge**. ALL logic (anything under `src/` or `server/`) is PR-and-park for Nick.
4. **Re-check before idle — never stop early.** When no unchecked ⚙️/🎨 remain, run a **full `/recc`** work-check
   (never idle on a lite scan). New actionable work → append + keep going. Only **TWO consecutive full `/recc`**
   passes that find nothing but 🔒 on a clean `main` → emit the IDLE sentinel and stop re-arming. A single empty
   pass re-arms one more confirming pass — it does NOT idle.

## Gates (ALL must pass before any merge — discovered + verified 2026-06-03, baseline real)
- **`npm run build`** (= `vite build`) — MUST exit 0. (Outputs to `docs/` — that's the Vite outDir, ignored by git.)
- **`npm run lint`** (= `eslint .`) — **no NEW errors over the 20-warning baseline** (0 errors / 20 warnings is
  the current floor; eslint ignores `server/`, `docs/`, `.claude/`, so this gate is frontend-only).
- **`npm test`** (= `vitest run`) — the Vitest suite (currently 46 tests across `src/**` + `server/**`, jsdom env;
  it EXCLUDES `server/authRedirect.test.js`). MUST pass.
- **`node --test server/authRedirect.test.js`** — the legacy `node:test` auth-redirect suite (6 tests) that Vitest
  excludes. MUST pass.
- **Vercel READY** — **after any merge to `main`**, verify the production deploy reaches **READY** (not ERROR) via
  the Vercel MCP: team `team_jOAmXzYm1OH7JHqvqgATTjZe`, project `prj_NE8JaAaJwfVBcxUSymyDfp0tHLLE`. `list_deployments`
  → check the newest `state`. **If a deploy ERRORs:** pull `get_deployment_build_logs`, and **fixing/reverting it is
  the NEXT iteration's job #1** (note the prior READY deploy as the rollback target — Step 1 self-heal handles it).

## 🔴 AUTO-MERGE POLICY (the critical prod-deploy catering — every merge to `main` auto-deploys to LIVE prod)
**`main` has NO branch protection, NO CI test gate, and merges use `--admin` — so NOTHING machine-enforces this
policy; it rests entirely on the loop's discipline.** Therefore the auto-merge set is deliberately TINY and PROD-INERT.

**Auto-merge to `main` (`gh pr merge <n> --squash --admin --delete-branch`, then verify Vercel READY) ONLY when the
change is PROD-INERT — it CANNOT change the deployed app's runtime behavior — AND all gates are green:**
- **Docs only** — `*.md`, `.claude/**`, code comments. (Vercel rebuilds with identical output; EB redeploys identical
  `server/`; harmless.)
- **Net-new test files only** — adding/fixing `*.test.{js,jsx}` / `*.spec.*` / `server/*.test.js` that modify **no
  non-test source file**. (Vite excludes tests from the bundle → the deployed app is byte-identical.)

**EVERYTHING ELSE → PR-and-park** (build it, gate it green, open the PR, mark the item `[~] PARKED-needs-Nick`, surface
it; Nick reviews + merges). This explicitly includes **ALL `src/**` logic** — yes, even a "small" frontend fix: it
auto-deploys to live prod via Vercel with no machine gate, and `src/**` contains the reveal gates — plus all
`server/**`, all `src/data/**`, and all config (`vite.config.js`, `eslint.config.js`, `vitest.config.js`, `package.json`).

**🔒 HARD DENY-LIST — never auto-merge, and an autonomous build/edit must NEVER target these (confirm your edit target
is NOT on this list BEFORE dispatching — §verify-before-build step 2). AUTHORITATIVE; do NOT relax:**
- **ANYTHING under `server/`** — `auth.js`, `utils.js` (JWT sign/verify + default-admin seed), `secretAccess.js`,
  `secretStore.js`, `middleware/supabaseAuth.js`, all routes. A `server/` change auto-deploys to AWS EB on push **and**
  leaves the Render serving backend stale → ALWAYS PR-park (AZ_LAW A — auth never regresses).
- **The reveal / true-name gates — which live inside `src/**`, NOT only on the server:**
  `src/components/pages/MagicSystemPage.jsx`, `src/components/pages/DukeDetailModal.jsx`,
  `src/components/pages/MagicHubPage.jsx`, and the gated data in `src/data/magicSystems.js` + `src/data/races.js`.
  Weakening a `revealName`/`revealPower` here ships true-name/true-power spoilers to live players, **undetected** (no
  test guards it) — the named unrecoverable failure (AZ_LAW D). ALWAYS PARK, even for an unrelated lint fix in the
  same file.
- **The content-hydration / permission modules** that feed reveal & role logic: `src/context/ContentContext.jsx`,
  `src/context/LabelDataContext.jsx`, `src/utils/permissions.js`. A change here is role-visible behavior → PARK.
- **Supabase schema** (additive+reversible included — `list_tables` first, then PARK; never `apply_migration`/
  `execute_sql` on prod autonomously), **secrets/env** (never read/print/edit/commit `.env`; the open prod-security
  item `JWT_SECRET`/`DEFAULT_ADMIN_PASSWORD` is Nick-ops), and **any auth/authz/role-gate** change.

If a PR mixes inert + reserved files it's mis-scoped — **split it**, auto-merge only the purely-inert part, park the rest.

> **To safely unlock frontend-logic auto-merge later** (so the loop can ship `src/**` fixes unattended): add GitHub
> branch protection on `main` with required CI status checks (build + lint + vitest + node:test as CI jobs — none run
> in CI today), add a **reveal-gate regression test** (assert a guest/editor gets `revealName === false` for a gated
> Duke), then drop `--admin`. Until then, logic stays PR-park.

## THE ITERATION (do exactly one unit, then decide continue vs idle)

**0 — Guard.**
- Any escalation trigger live (irreversible op / money / secret needed / scope-or-canon conflict the room can't
  fork around / 2× same-fix-fail / 3× build-fail / verifier-stalemate / context > 85% / token-budget)? → run the
  halt chain (open a `human-required` GH issue → append a local `ESCALATION.md` entry `az/escalation/<trigger>` → surface "ESCALATION: …") and
  **STOP — do not re-arm.**
- **A prod deploy is currently ERROR** (last merge didn't reach READY)? → that IS this iteration (Step 1).
- Context > ~60% → compact, re-read the backlog + this skill + `CLAUDE.local.md`, continue (NOT a stop).

**1 — Self-heal (a broken `main` / a broken prod deploy is always job #1).**
- If the newest Vercel deploy on `main` is **ERROR**: pull `get_deployment_build_logs`, diagnose. If it's a quick
  frontend fix → dispatch `executor` → gates → auto-merge → re-verify READY. If it's NOT a safe-category fix, or
  the fix is non-obvious → **revert to the noted prior-READY commit** (a safe, reversible frontend op: `git revert`
  the bad merge, PR, gates, merge, verify READY) and park the real fix for Nick.
- If `npm run build` / `npm test` / `node --test …` / `npm run lint` is RED on `main` for any other reason → that
  IS this iteration's work: fix it (safe-category → dispatch `executor` + review lane → gates → auto-merge; else
  PR-park). Then go to step 5 (re-arm).

**2 — Scan / replenish (the "/recc between tasks").**
- Lightweight grounded scan: grep new `TODO|FIXME|HACK|XXX`, "Coming Soon"/placeholder content, silent catch
  blocks, missing test coverage on `src/**` (the biggest gap — high-value SAFE work), doc-drift, failing gates.
  Run a **full `/recc`** every ~5th iteration; the lite scan otherwise.
- Append genuinely-NEW, **autonomously-actionable** items to `.claude/planning/AUTONOMOUS_BACKLOG.md` (⚙️ or 🎨).
  **Dedup hard (convergence guard, §dedup):** skip anything already in the backlog (checked OR unchecked) and any
  reword of a done `[x]` item — re-discovering finished work is the #1 way the loop fails to ever idle. Do NOT
  append 🔒/🚫 items as work. Commit the backlog append (docs-only: `git add .claude/planning/AUTONOMOUS_BACKLOG.md`).

**3 — Pick** the top unchecked **⚙️**; if none, the top unchecked **🎨**. Never a 🔒/🚫. Honor §2 ordering and any
per-item dependency note.

**4 — Do (one concern, one PR).** **§verify-before-build FIRST:** confirm the problem exists in live code at the
named path. Wrong path → grep for the real one. Doesn't exist → check off `[x] non-bug — <reason>`, build nothing.
- **⚙️:** dispatch `executor` in a worktree (one concern; **`model=opus` for complex / security-adjacent reads**,
  sonnet for mechanical) + a **code-review depth lane** on substantive PRs → gates (build → lint → vitest →
  node:test) → decide via the **AUTO-MERGE POLICY** above:
  - **PROD-INERT (docs or net-new tests) + all gates green** → `gh pr merge <n> --squash --admin --delete-branch` →
    **verify Vercel READY** (record the new READY deploy SHA to the Descent scan ledger as the rollback target) →
    check the item `[x] (#PR)`.
  - **ANY logic (`src/` or `server/`), a DENY-list path, OR a gate red** → leave the PR open, flag the reason in the
    PR body, mark the item `[~] PARKED-needs-Nick`, surface it. **Do NOT merge.** (This is the COMMON case — most real
    work parks for Nick; the loop's job is to deliver a clean, gated, ready-to-merge PR, not to ship it.)
- **🎨:** `Workflow({scriptPath:'.claude/workflows/az-design-room.mjs', args:{topic,problem,room,owning,references}})`
  (args is delivered as a JSON string — the script `JSON.parse`s it; pass a real object). `problem` must be a
  PROBLEM, not a solution. **Commit the spec + research regardless of verdict** (they land under `.omc/plans/`,
  which is git-ignored — so they're local artifacts; surface the spec path in the report). Then:
  - `shipReady` (SHIP + feasibility clean + no reveal/migration leak + no hard blocker) → **build to the spec's
    SAFE defaults** via `executor` (+ review lane) → gates → **AUTO-MERGE POLICY** (still SAFE-only: content JSON
    in `src/data/**` is frontend-safe; but if the spec's build touches `server/**`/schema/reveal it PARKS) → check `[x]`.
  - REVISE/BLOCK on a **reserved** fork, or a hard blocker, or a server/schema/reveal build → append the forks to
    `docs/planning/<date>-descent-log.md` (local), mark the item `[~] spec'd · build PARKED (Nick: <fork>)`,
    **check it off** (the spec is the delivered unit), continue.

**5 — Continue vs IDLE (always re-check for work before stopping; idle needs TWO empty passes).**
- Work done this iteration, OR an unchecked ⚙️/🎨 remains, OR the scan added one → **re-arm**: call
  `ScheduleWakeup` with prompt `<<autonomous-loop-dynamic>>` (or re-issue `/loop /az-descent`) at a short delay,
  **reset the empty-streak to 0**, and report ONE terse line (shipped: … / parked: … / next: …).
- **No unchecked ⚙️/🎨 left + all gates clean → run a full `/recc` work-check BEFORE idling** (never idle on a
  lite scan):
  - It surfaces NEW actionable ⚙️/🎨 (passes the §dedup guard) → append them, **reset the empty-streak to 0,
    re-arm and keep working.**
  - It surfaces nothing but 🔒/🚫 → append the pass to the **Descent scan ledger** at the foot of
    `AUTONOMOUS_BACKLOG.md` (one line: `YYYY-MM-DD HH:MM — recc: EMPTY`), then count the trailing run of EMPTY entries:
    - **streak < 2 → re-arm anyway** (one more confirming full-`/recc` pass next wake). **Do NOT idle on a single empty pass.**
    - **streak ≥ 2 → IDLE**: emit `🟢 IDLE — backlog clear, main clean + Vercel READY, 2 consecutive /recc empty, N reserved (see -descent-log.md)` and **do not re-arm.**
- After IDLE, manual pokes re-run steps 1-2 + a full `/recc`: still empty → re-emit IDLE (never invent work);
  new actionable work appears → reset the streak and resume.

## Hard rules (inherited + Azterra-specific, do not relax)
- **PROD-DEPLOY catering is law.** Auto-merge is DOCS-AND-NET-NEW-TESTS-ONLY; ALL logic (`src/` + `server/`) and the
  🔒 DENY-list PR-and-park. Every push to `main` auto-deploys live on BOTH layers — Vercel (frontend) + Render
  (the backend serving `/api/*`; verified auto-deploying 2026-06-09 — the old EB workflow is deleted). `server/`
  merges are live in minutes. No branch protection enforces any of this — the loop's discipline is the only gate.
- **AZ_LAW (A-D) is the floor:** (A) auth never regresses; (B) Supabase writes additive + reversible, read
  `list_tables` first then PARK; (C) build/deploy stays green (`npm run build` exit 0 + Vercel READY); (D) no
  true-name/true-power leak (gated fields absent from responses to weaker roles, server-side).
- **NEVER** read/print/edit/commit `.env` or any secret. Placeholder keys only.
- **NEVER** run `apply_migration` / `execute_sql` / any write against the prod Supabase autonomously.
- **Only the escalation triggers stop the loop.** Rate limit → wait + retry. One agent failure → log + retry.
  Compaction → continue. A prod-ERROR deploy → it's the next iteration's job #1, not a stop.
- **Reserved (🔒) = logged, never decided, never blocks.** Append to the `-descent-log.md` and move on.
- **Out-of-scope (🚫) = never built.** Building one = an escalation trigger.
- **Merge mechanics:** `--admin` for the up-to-date gate; **never force-push**; stage explicit tracked paths
  (never `git add docs/`, `.omc/`, `.env`, `STATUS.md`, `CLAUDE.local.md`).
- **Reporting:** terse — one line per iteration. No preamble.

## Start / stop
- **Start:** `/loop /az-descent` (self-paced). Or `/az-descent` for a single manual iteration. **Nick launches it
  — this tier is built but never run autonomously by the builder.**
- **Stop:** interrupt the `/loop` (Esc), the iteration's own IDLE (no re-arm), or Nick interrupts.
