forked from bchanot/claude
One ask policy; mandated executors exempt from the delegation rule; skill plan satisfies the planning rule; journal line exempt from the approval gate; chore = maintenance without new behaviour; small fix on develop = bugfix; BDR-068 written as the one auto-finish exception; deploy routes to /deploy. Skills and agents follow: hotfix types by base + skips the design gate on trivial; capitalize/close create missing registries; commit-change asks the branch type; doc/seo/web-validate/refactor branch through the aiguillage; tour reports BREAKING fixes as needs-decision and runs doc-syncer two-mode; client-handover applies audit bundles from its main loop behind one gate; init-project/onboard use the 200-file graphify signal and bootstrap memory; release-candidate gates the tag push only; push wording aligned with the BDR-095 hooks; stale pointers fixed (§ Language, .gsd/ROADMAP.md, handover script path, design-gate lists).
531 lines
29 KiB
Markdown
531 lines
29 KiB
Markdown
---
|
||
name: deploy
|
||
description: |
|
||
Use when deploying a project via its per-project runbook — instantiates the delta
|
||
since last deploy, hands off for out-of-band execution, resumes cold, learns from errors.
|
||
Triggers: "deploy", "déploie", "run the deploy", "ship to prod", "deploy runbook".
|
||
allowed-tools: [Read, Write, Edit, Bash, Grep, Glob, AskUserQuestion]
|
||
---
|
||
|
||
# /deploy — per-project runbook, instantiated from the delta, resumed cold
|
||
|
||
Run a project's deploy from its committed runbook (`.claude/deploy/PROCEDURE.md`):
|
||
instantiate only the steps the delta-since-last-deploy needs, hand the checklist
|
||
to the user for **out-of-band** execution, then **resume on their report — even
|
||
in a different session with no conversation memory** — and patch the runbook in
|
||
place when a step fails.
|
||
|
||
**Core principle — the disk is the only memory between the two moments.** This
|
||
skill runs in two moments split by a manual deploy you do not control. The report
|
||
that closes it may land in a fresh session. So everything moment 3 needs lives on
|
||
disk in `.claude/deploy/`, never in conversation context. Never reconstruct the
|
||
deploy from memory, commit messages, or `git describe`.
|
||
|
||
**Claude never runs the deploy.** Prod commands run by hand, out-of-band. This
|
||
skill only composes the checklist and the post-deploy tests — **displayed in
|
||
the conversation, never written to a file** (throwaway: valid for one delta,
|
||
worthless after) — reacts to the user's report, and records the outcome.
|
||
|
||
## The two-moment contract — cold cross-session resume
|
||
|
||
This is the skill's defining form. No other skill resumes with the context gone.
|
||
|
||
| | |
|
||
|---|---|
|
||
| **Moment 1 (BEFORE)** | STEP 0–2: detect the delta, instantiate the checklist, write the `PENDING.json` bridge, hand back. |
|
||
| **the gap** | The user deploys by hand. May take minutes or days. **May cross sessions.** |
|
||
| **Moment 2 (AFTER)** | STEP 3–5: on the user's report, react — mark success, or learn from a failure and re-hand-back. |
|
||
|
||
**How the wait is marked:** a `PENDING.json` on disk with `step_reached`
|
||
recorded **IS** the marker "a deploy is in flight, I am waiting for your report
|
||
here." Its presence is the whole signal — no flag in memory, no open question in
|
||
context. `STATE.json` is the deployed-up-to-here oracle; `PENDING.json` is the
|
||
in-flight bridge that outlives the session.
|
||
|
||
**How a cold resume detects + resumes (STEP 0):** every invocation reads
|
||
`PENDING.json` FIRST. Present ⇒ a deploy is mid-flight ⇒ jump straight to STEP 3
|
||
using the bridge's `{base_sha, target_sha, delta, step_reached}`. **Do not
|
||
recompute any of them** — HEAD may have moved during the gap, so "current HEAD"
|
||
is wrong; the bridge holds the truth captured at instantiation.
|
||
|
||
**Read the JSON natively.** Open `PENDING.json` / `STATE.json` with the Read
|
||
tool and parse the fields directly. NO `jq`, NO shell JSON parsing — there is no
|
||
jq dependency.
|
||
|
||
## When to use / When NOT to use
|
||
|
||
| Situation | Skill |
|
||
|-----------|-------|
|
||
| Run this project's deploy runbook, delta-instantiated, learning | **this skill** |
|
||
| Project has no `.claude/deploy/PROCEDURE.md` yet | this skill's **bootstrap** branch (see STEP 0) |
|
||
| Merge a branch + trigger CI deploy (gstack) | `/land-and-deploy` |
|
||
| Configure deployment settings | `/setup-deploy` |
|
||
| Document a release after shipping | `/document-release`, `/doc` |
|
||
|
||
## Artifacts — `.claude/deploy/` (four files)
|
||
|
||
| File | Committed? | Role |
|
||
|------|-----------|------|
|
||
| `PROCEDURE.md` | yes | reference runbook — fixed shell + `# @delta:` steps; edited IN PLACE |
|
||
| `INCIDENTS.md` | yes | `DEP-NNN` ledger, append-only; read at instantiation for pre-warns |
|
||
| `STATE.json` | yes | deploy oracle — the SHA deployed up to here |
|
||
| `PENDING.json` | **no (gitignored)** | in-flight bridge; written at hand-back, deleted on success |
|
||
|
||
The instantiated checklist is **NOT a file**: it is displayed in the
|
||
conversation (run BY HAND, step by step, never executed by Claude) and
|
||
regenerated on demand from `PENDING.json` + the live runbook. Throwaway by
|
||
design — once deployed, it has no value.
|
||
|
||
**Schemas (document of record — recover the shapes from here):**
|
||
|
||
```jsonc
|
||
// STATE.json — overwritten each successful deploy (the diff oracle)
|
||
{ "deployed_sha": "<sha>", "deployed_at": "<ISO-8601>", "outcome": "ok", "tag": "deploy/<YYYY-MM-DD>" }
|
||
```
|
||
|
||
```jsonc
|
||
// PENDING.json — the cold-resume bridge; gitignored; deleted on success
|
||
{ "base_sha": "<deployed STATE sha>", "target_sha": "<HEAD at instantiation>",
|
||
"delta": ["<path>", ...], "step_reached": "awaiting-user", "started_at": "<ISO-8601>",
|
||
"runbook_rev": "<PROCEDURE.md commit sha>" }
|
||
```
|
||
|
||
`step_reached` = where the next checklist must start: `"awaiting-user"` = run
|
||
from the top. A numeric `X` is used **transiently within a learn** to regenerate
|
||
from step X; **persisted on disk it is always `"awaiting-user"`** — STEP 4
|
||
resets to `awaiting-user` at re-hand-back.
|
||
`runbook_rev` = the commit sha of `PROCEDURE.md` at instantiation; on resume, a
|
||
mismatch versus the live runbook means the runbook changed mid-flight — flag it
|
||
and regenerate the checklist against the LIVE runbook.
|
||
|
||
## `@delta:` grammar (PROCEDURE.md)
|
||
|
||
A directive sits on the comment line **above** the step it governs; patterns are
|
||
matched against the delta file list. Un-annotated step = **fixed**, always
|
||
emitted verbatim.
|
||
|
||
**A step is a block**: its `# n)` comment header plus every command line below
|
||
it, up to the next blank line. A directive governs the whole block. Steps are
|
||
written **one command per line, interactive-session style** — an early fixed
|
||
step opens the box (`ssh "$DEPLOY_HOST"`), the lines after it run *on* the box
|
||
as you would type them; a step that runs locally says `(from your machine)` in
|
||
its header. Never fold `ssh host "cd … && …"` compounds: the user copy-pastes
|
||
line by line. Each `# VERIFY:` sits at the end of the command line it gates.
|
||
|
||
**One command = one physical line.** A command occupies exactly one line of
|
||
the file, however long it gets: no `\` continuation, no heredoc, no wrapped
|
||
argument list. The user copies one line and presses Enter; a continuation
|
||
pastes as two half-commands. The `# VERIFY:` comment ends that same line. This
|
||
holds wherever a runbook line is written — bootstrap, a learn patch, a manual
|
||
edit — and the instantiation joins any legacy continuation it still meets.
|
||
|
||
| Directive | Meaning | Instantiation |
|
||
|-----------|---------|---------------|
|
||
| `# @delta:<kind> glob=<pat>:each` | per-file command | repeat the command once **per** matching delta file (file substituted in) |
|
||
| `# @delta:<kind> glob=<pat>:list` | one command, many inputs | emit the command **once**; list matching files as `# VERIFY:` items |
|
||
| `# @delta:<kind> when=<pat,...>` | conditional | include the step **only if** the delta intersects a pattern |
|
||
|
||
`<kind>` is a human label. `<pat>` is a git-pathspec / shell glob; `when=`
|
||
comma-separates alternatives. Zero matches → omit that step. Both `:each` and
|
||
`:list` are first-class (e.g. apply each new migration with its own command vs.
|
||
one `migration up` that lists which migrations to verify).
|
||
|
||
---
|
||
|
||
## STEP 0 — PRE-FLIGHT + RESUME BRANCH
|
||
|
||
Read `.claude/deploy/PENDING.json` **first** (it is the only memory between runs).
|
||
|
||
- **`PENDING.json` present → RESUME.** A deploy is in flight. Parse its
|
||
`{base_sha, target_sha, delta, step_reached}` and jump to **STEP 3**. Announce:
|
||
"A deploy started `<started_at>` is awaiting your report (target `<target_sha>`)."
|
||
**Do not** recompute the delta, re-read HEAD, or re-instantiate from scratch —
|
||
the bridge is authoritative.
|
||
- *Cold resume without a report yet* (the user just re-invoked /deploy):
|
||
regenerate the checklist AND the post-deploy tests from the bridge + the
|
||
live runbook (STEP 2's expansion, from `step_reached`; the tests from the
|
||
bridge's `base_sha`/`target_sha`/`delta`) and RE-DISPLAY both — neither is
|
||
a file, the conversation that held them is gone. If `runbook_rev` ≠ the live
|
||
runbook commit (`git log -1 --format=%H -- .claude/deploy/PROCEDURE.md`),
|
||
say so: the runbook changed mid-flight and the regenerated checklist
|
||
follows the LIVE version.
|
||
- **`PENDING.json` absent + `PROCEDURE.md` absent → BOOTSTRAP.** No runbook yet:
|
||
interview the project and scaffold an annotated `PROCEDURE.md` (or adopt one
|
||
the user pastes), then continue at STEP 1. *(See STEP 0-B below.)*
|
||
- **`PENDING.json` absent + `PROCEDURE.md` present → FRESH.** Continue to STEP 1.
|
||
|
||
First-deploy / fresh detection is **file existence only**. Never `git describe`
|
||
(it errors when no `deploy/*` tag exists and is not the detection path).
|
||
|
||
## STEP 0-B — BOOTSTRAP (no runbook yet)
|
||
|
||
Entered from STEP 0 when both `PENDING.json` and `PROCEDURE.md` are absent.
|
||
Author a runbook, seed the incident ledger, commit both, then proceed to STEP 1.
|
||
|
||
**AskUserQuestion — choose path:**
|
||
|
||
> "No runbook found in `.claude/deploy/PROCEDURE.md`. How do you want to create it?
|
||
>
|
||
> **A — Paste:** share an existing runbook (paste text, file path, or URL). I adopt
|
||
> it verbatim and propose `@delta:` annotations for migration, build, and dep steps.
|
||
>
|
||
> **B — Scaffold:** I detect deploy artifacts in this repo, ask a few questions, and
|
||
> fill the standard template."
|
||
|
||
---
|
||
|
||
### Path A — Paste (adopt existing runbook)
|
||
|
||
1. Receive the runbook (paste, path → Read, or URL). Accept as-is.
|
||
2. Prepend the standard header:
|
||
```
|
||
#!/usr/bin/env bash
|
||
# === deploy runbook (reference) — NOT run directly. Instantiated into the deploy checklist per delta. ===
|
||
# Fixed steps run every deploy; annotated steps (@delta lines) re-instantiate from the delta.
|
||
```
|
||
3. Scan for migration, rebuild, and dependency steps; propose `@delta:` annotations inline:
|
||
- Migration steps (`psql -f`, `migrate up`, `supabase migration`) →
|
||
`# @delta:migrations glob=supabase/migrations/*.sql:list`
|
||
- Build/restart steps (`docker compose`, `make build`, image push) →
|
||
`# @delta:rebuild when=docker-compose*.yml,Dockerfile,Dockerfile.*`
|
||
- Dep-install steps (`npm ci`, `pip install -r`, `bundle install`) →
|
||
`# @delta:deps when=package.json,*lock*,requirements.txt,pyproject.toml`
|
||
4. Rewrite any `\`-continued, heredoc or wrapped command into one physical
|
||
line (the `@delta:` grammar's one-command-one-line rule).
|
||
5. Present the annotated draft; invite corrections before the gate.
|
||
|
||
→ **[GATE]** below.
|
||
|
||
---
|
||
|
||
### Path B — Scaffold (detect + interview)
|
||
|
||
**Detect artifacts** (Glob / Read only — never shell `find /`):
|
||
|
||
| Check | If found | Step emitted |
|
||
|-------|---------|--------------|
|
||
| `supabase/migrations/*.sql` | yes | migration step with `:list` annotation |
|
||
| `docker-compose*.yml` or `Dockerfile` | yes | rebuild step with `when=` annotation |
|
||
| `package.json` or `*lock*` | yes | deps step with `when=package.json,*lock*` |
|
||
| `requirements.txt` or `pyproject.toml` | yes | deps step with `when=requirements.txt,pyproject.toml` |
|
||
| `.env*` (not `.env.example`) | yes | add `# NOTE: inject env vars` to smoke-test step |
|
||
|
||
**Interview (AskUserQuestion — one prompt, all fields):**
|
||
|
||
| Field | Prompt | Default / placeholder |
|
||
|-------|--------|-----------------------|
|
||
| SSH host | "SSH host or deploy target?" | keep as `$DEPLOY_HOST` if blank |
|
||
| Backup command | "Backup command before migrations?" | `pg_dump "$DB" > ~/backups/pre-deploy-$(date +%F-%H%M).sql` |
|
||
| Health-check URL | "Health-check URL (expects HTTP 200)?" | `https://$DEPLOY_HOST/health` |
|
||
| Rollback note | "One-line rollback note (optional)?" | omit if blank |
|
||
|
||
**Using** `~/.claude/templates/deploy/PROCEDURE.md` **as base, populate** fields from interview answers + detected artifacts:
|
||
- Substitute `$DEPLOY_HOST` with the supplied host (keep literal `$DEPLOY_HOST` if none given).
|
||
- Include only the annotated steps whose artifact was detected; keep all fixed steps.
|
||
- Append the rollback note as `# ROLLBACK: <note>` at the end if provided.
|
||
|
||
→ **[GATE]** below.
|
||
|
||
---
|
||
|
||
### [GATE] — approve PROCEDURE.md draft (`all / edit / skip-all`)
|
||
|
||
Present the full draft `PROCEDURE.md`.
|
||
|
||
- `all` → approve: write files and commit (see below).
|
||
- `edit` → revise the listed steps or annotations, re-present.
|
||
- `skip-all` → abort bootstrap: write nothing, stop. Re-invoke `/deploy` when ready.
|
||
|
||
**On approve — write + seed + commit:**
|
||
|
||
1. Write `.claude/deploy/PROCEDURE.md` (Write tool — the approved draft).
|
||
2. Seed `.claude/deploy/INCIDENTS.md` from `~/.claude/templates/deploy/INCIDENTS.md` (Write tool).
|
||
3. Ensure the target project's `.gitignore` contains
|
||
`.claude/deploy/PENDING.json` (append if missing — the transient bridge must
|
||
not be committed).
|
||
4. Check that `.claude/deploy/` is NOT git-ignored: `git check-ignore -q .claude/deploy/PROCEDURE.md`
|
||
(rc 0 = ignored). If ignored — e.g. the project has `.claude/` in its `.gitignore` wholesale —
|
||
**ABORT bootstrap**: warn the user that the runbook/oracle/ledger cannot be committed,
|
||
and tell them to un-ignore `.claude/deploy/` (e.g. add `!.claude/deploy/` after the
|
||
`.claude/` rule). Do NOT commit anything further.
|
||
5. Commit via the allowlist helper:
|
||
```bash
|
||
bash ~/.claude/lib/deploy-commit.sh commit \
|
||
"feat(deploy): bootstrap runbook" \
|
||
.claude/deploy/PROCEDURE.md .claude/deploy/INCIDENTS.md
|
||
```
|
||
Return codes: **0** committed · **1** no-op (investigate — both files should be new) ·
|
||
**3** unsafe git state (STOP, tell user) · **4** out-of-scope path ·
|
||
**5** a passed path is git-ignored (won't persist) — STOP, fix the target's `.gitignore` ·
|
||
**6** commit rejected — pre-commit hook/protected branch/signing (STOP, investigate) ·
|
||
**2** usage error OR not a git repo.
|
||
|
||
**On rc=0: continue to STEP 1.** `STATE.json` absent → first deploy →
|
||
STEP 1 sets `base_sha: null` and the full runbook fires (every fixed step and
|
||
every detected `@delta:` step instantiates). Correct and expected — no special
|
||
handling needed.
|
||
|
||
---
|
||
|
||
## STEP 1 — DELTA
|
||
|
||
Set the base, compute the changed-file list, capture the target.
|
||
|
||
- **`STATE.json` absent → FIRST DEPLOY.** No base (`PENDING.json.base_sha: null`).
|
||
The **full runbook** fires: delta = the entire tracked tree (`git ls-files`), so
|
||
every fixed step and every applicable `@delta:` step instantiates.
|
||
- **`STATE.json` present →** read `deployed_sha` as `base`, then:
|
||
```bash
|
||
git diff --name-only <base_sha> HEAD # two explicit endpoints, no dots
|
||
```
|
||
This is the literal tree difference deployed→HEAD. **Never** `git rev-list`
|
||
ancestry (phantom deltas after a rebase) and **never** three-dot `<base>...HEAD`
|
||
(merge-base undercounts).
|
||
- `target = git rev-parse HEAD` — the SHA this deploy carries to prod.
|
||
|
||
## STEP 2 — INSTANTIATE + [GATE] + HAND BACK
|
||
|
||
**Build the checklist (the recipe — it IS this shape):**
|
||
|
||
1. Walk `PROCEDURE.md` in order. For each step:
|
||
- un-annotated (fixed) → emit verbatim;
|
||
- `@delta:…:each` → emit the command once per matching delta file, file
|
||
substituted; zero matches → omit;
|
||
- `@delta:…:list` → if any delta file matches, emit the command once and list
|
||
the matches as `# VERIFY:` items; zero matches → omit;
|
||
- `@delta:…when=` → emit verbatim only if the delta intersects a pattern.
|
||
2. Read `INCIDENTS.md`; for each `DEP-NNN` whose step matches an emitted step,
|
||
prepend `# PRE-WARN: DEP-NNN <one-line summary>` above it.
|
||
3. Keep every `# VERIFY:` gate. Header the checklist: *"Run by hand, step by
|
||
step. Never executed by Claude."* + base → target SHAs + the delta.
|
||
4. **One physical line per command.** Emit each command on exactly one line,
|
||
however long — the terminal wraps it on screen, the clipboard does not. A
|
||
runbook line ending in `\` is a legacy continuation: join it with the
|
||
line(s) below into one command before emitting (drop the `\` and the
|
||
indent). Never split a long command, never fold two commands into one
|
||
compound. Session style otherwise, as the `@delta:` grammar says.
|
||
5. **Derive the post-deploy tests from the delta** — the recipe is the next
|
||
section. They follow the checklist in the same hand-back.
|
||
6. **Write NO file.** The checklist and the tests exist in the conversation
|
||
only — `PENDING.json` is the sole on-disk artifact of the wait, and any
|
||
future session regenerates both from it + the live runbook.
|
||
|
||
**[GATE] — present the checklist + the post-deploy tests → `all / edit / skip-all`.**
|
||
- `all` → proceed. `edit` → revise the listed steps or tests, re-present.
|
||
- `skip-all` → abort: write no `PENDING.json`, discard the draft, stop.
|
||
|
||
**On approve:** write `.claude/deploy/PENDING.json`:
|
||
```jsonc
|
||
{ "base_sha": "<STEP 1 base>", "target_sha": "<STEP 1 target>",
|
||
"delta": [<STEP 1 file list>], "step_reached": "awaiting-user",
|
||
"started_at": "<now, ISO-8601>",
|
||
"runbook_rev": "<git log -1 --format=%H -- .claude/deploy/PROCEDURE.md>" }
|
||
```
|
||
**Then HAND BACK — the hand-back IS the last text of the turn.** End the turn
|
||
with, in this order: (1) the FULL final checklist in a fenced code block,
|
||
(2) the post-deploy tests block (outside the fence, its own shape), (3) the
|
||
one-line report request: *"Run it step by step against prod, then report:
|
||
**Deployed OK** / **Failed at step X: <err>** / **Not yet**."* **No tool call
|
||
comes after the print — none.** Do NOT wrap the report request in a blocking
|
||
question tool: text printed before a tool call may never reach the user
|
||
(observed live — a checklist printed above an AskUserQuestion was invisible;
|
||
the user had to open the file this rule exists to make unnecessary). The report
|
||
arrives as the user's next message; `PENDING.json` on disk marks the wait.
|
||
The same rule applies to every re-hand-back (STEP 4.3) and every cold-resume
|
||
re-display: regenerated checklist + tests ⇒ full print as the turn's final text.
|
||
|
||
### Post-deploy tests — the recipe (it IS this shape)
|
||
|
||
The tests come from the delta and nothing else: read the diff of each delta
|
||
file (`git diff <base_sha> <target_sha> -- <file>`); commit subjects serve the
|
||
wording only. Every delta file that changes behaviour observable from outside
|
||
— a route, a query, a policy, a UI element, a config value, a scheduled job —
|
||
yields at least one manual check. Docs-only and `.claude/`-only files yield
|
||
none. A gap between two delta files (a new client write with no matching
|
||
grant, a migration no code reads yet, a removed route still linked) becomes a
|
||
Suggestion phrased as a check to run — never a fix applied during the deploy.
|
||
|
||
~~~markdown
|
||
## Post-deploy tests — <n> delta files
|
||
### By hand, on prod, in this order
|
||
- [ ] <what the user does> → <what they must observe> (<delta file>)
|
||
- [ ] …
|
||
### Suggestions
|
||
- <a check the runbook does not do yet: a curl or query worth adding to the
|
||
smoke-test step, a log or metric to watch for the next hour, a rollback trigger>
|
||
- …
|
||
~~~
|
||
|
||
One line per item, action → observable result, each tied to a delta file.
|
||
"By hand" is what a person does in the browser, the app or a shell on prod;
|
||
"Suggestions" holds the optional and the tooling. Zero behaviour-changing
|
||
files (a docs-only delta) ⇒ one "By hand" item, the smoke test, and no
|
||
Suggestions section.
|
||
|
||
## STEP 3 — RESUME / REACT
|
||
|
||
Entry point on the user's report — reached inline after STEP 2, **or cold via
|
||
STEP 0** in a later session. Branch on the report:
|
||
|
||
- **"Deployed OK"** → STEP 5.
|
||
- **"Failed at step X: <err>"** → STEP 4.
|
||
- **"Not yet"** → restate what is pending (`step_reached`, target, the command to
|
||
run) and stop. `PENDING.json` stays; the wait continues.
|
||
|
||
## STEP 4 — LEARN + [GATE] + ATOMIC COMMIT
|
||
|
||
Diagnose the root cause of the step-X failure, then draft a **coupled pair**:
|
||
|
||
- **(a)** an in-place patch to step X in `PROCEDURE.md` so the next run cannot
|
||
repeat the failure — every command in the patch on one physical line;
|
||
- **(b)** an append to `INCIDENTS.md` — a new `DEP-NNN`
|
||
(`next = grep '^## DEP-' INCIDENTS.md | max+1`) with date, step, **error
|
||
verbatim**, root cause, and fix.
|
||
|
||
**[GATE] — `all / pick <IDs> / edit <ID> / skip-all`** (significant edit — it
|
||
changes a prod path).
|
||
- **Coupling invariant:** the patch and the incident are **one unit** — never
|
||
commit one without the other. `pick <IDs>` / `edit <ID>` apply only when
|
||
diagnosis yields **multiple** incidents (several failing steps); each selected
|
||
incident still commits its own patch+append together.
|
||
- `skip-all` → leave `PENDING.json` as-is, stop, nothing learned (the deploy stays
|
||
failed-and-pending).
|
||
|
||
**On approve — one ATOMIC commit of both files:**
|
||
```bash
|
||
bash ~/.claude/lib/deploy-commit.sh commit \
|
||
"docs(deploy): patch <step> — recovered from <err>" \
|
||
.claude/deploy/PROCEDURE.md .claude/deploy/INCIDENTS.md
|
||
```
|
||
Return codes: **0** committed (short-hash on stdout) · **1** nothing staged — you
|
||
wrote neither file · **3** unsafe git state (detached/merge/rebase — STOP, tell
|
||
the user) · **4** out-of-scope path (you passed a non-`.claude/deploy/` path — fix
|
||
the call) · **5** a passed path is git-ignored (won't persist) — STOP, fix the
|
||
target's `.gitignore` · **6** commit rejected — pre-commit hook/protected branch/
|
||
signing (STOP, investigate) · **2** usage error OR not a git repo. The helper
|
||
commits whatever subset actually changed;
|
||
patch+incident coupling is **Claude-discipline, not helper-enforced**.
|
||
|
||
**This commit IS the resolution** — the commit that introduces `DEP-NNN` is its
|
||
fix (patch + incident committed atomically). Recover later via
|
||
`git log -S '<DEP-NNN>' -- .claude/deploy/INCIDENTS.md`. No backfill needed.
|
||
|
||
Then:
|
||
1. Bump `PENDING.json.runbook_rev` to `git rev-parse HEAD` (full sha — not the helper's short-hash stdout); keep `step_reached` = `X`.
|
||
2. **Regenerate the checklist from `step_reached` against the PATCHED runbook**
|
||
(steps X…end — X+1…end never ran). This is NOT replaying one step: the
|
||
runbook changed ⇒ the prior checklist is stale ⇒ regenerate.
|
||
3. Re-present via **STEP 2's [GATE] + hand-back** (the regenerated checklist
|
||
+ the post-deploy tests, full print as the turn's final text;
|
||
`PENDING.json` keeps `base/target/delta`, `step_reached` back to
|
||
`awaiting-user`).
|
||
|
||
## STEP 5 — MARK (success)
|
||
|
||
The deploy succeeded. Lay the oracle and close out.
|
||
|
||
1. Pick `date = today` (`YYYY-MM-DD`); if `deploy/<date>` exists, suffix `-N`.
|
||
2. Write `.claude/deploy/STATE.json` (overwrite):
|
||
```jsonc
|
||
{ "deployed_sha": "<PENDING.target_sha>", "deployed_at": "<now ISO-8601>",
|
||
"outcome": "ok", "tag": "deploy/<date>" }
|
||
```
|
||
**`deployed_sha` = `PENDING.target_sha`, NOT current HEAD** — HEAD may have
|
||
moved during the gap; the bridge's target is the deployed truth.
|
||
3. `git tag -a deploy/<date> <PENDING.target_sha> -m "<summary>"`.
|
||
4. No separate tag push: the oracle commit (step 5) fires the gitflow
|
||
post-commit hook, which pushes with `--follow-tags`, so `deploy/<date>`
|
||
rides along (BDR-095). A hook `push FAILED` warning never blocks the mark
|
||
(the tag is a bookmark; `STATE.json` is the oracle).
|
||
5. Commit the oracle:
|
||
```bash
|
||
bash ~/.claude/lib/deploy-commit.sh commit "chore(deploy): mark <date> @ <short>" \
|
||
.claude/deploy/STATE.json
|
||
```
|
||
6. **Delete `.claude/deploy/PENDING.json`** — the deploy is no longer in
|
||
flight; the bridge is consumed. (Also remove any legacy `NEXT.sh` left by
|
||
an older skill version.)
|
||
7. Report: deployed SHA, tag (+ push result), state committed, any `DEP-NNN`
|
||
learned this deploy. Then offer to capitalize per CLAUDE.md (recurring failure
|
||
pattern → `learnings.md`; deploy verdict → `evals.md`), gated, never silent.
|
||
|
||
---
|
||
|
||
## Rules
|
||
|
||
- `PENDING.json` is the only memory across the gap. Read it first, every run.
|
||
- On RESUME, never recompute `{base, target, delta}` — the bridge is authoritative.
|
||
- `deployed_sha` is `PENDING.target_sha`, never live HEAD.
|
||
- Delta is `git diff --name-only <base> HEAD` (two endpoints). No `rev-list`, no
|
||
three-dot, no date ranges.
|
||
- First-deploy / fresh detection is file existence only — never `git describe`.
|
||
- Claude never executes the deploy. The checklist is hand-run; `# VERIFY:`
|
||
gates stay.
|
||
- The checklist is displayed, never written to a file; every hand-back and
|
||
re-display ends the turn with it — no tool call after the print.
|
||
- One command = one physical line — in the runbook, in a learn patch, in the
|
||
checklist. A legacy `\` continuation is joined at instantiation.
|
||
- The hand-back is checklist → post-deploy tests → report request. The tests
|
||
come from the delta diff: one manual check per behaviour-changing file,
|
||
gaps as Suggestions.
|
||
- Patch + incident commit **atomically**, one `deploy-commit.sh` call, both files.
|
||
- A learn bumps `runbook_rev` and **regenerates** the checklist from
|
||
`step_reached`; it never replays a single step.
|
||
- Tag push is best-effort; `STATE.json` is the oracle.
|
||
- JSON is read natively (Read tool), never parsed with `jq`/shell.
|
||
- `STATE.json` written only on confirmed success (STEP 5). A failed/partial deploy
|
||
leaves the oracle untouched, `PENDING.json` alive — fail closed, resume later.
|
||
|
||
## Common mistakes
|
||
|
||
| Mistake | Fix |
|
||
|---------|-----|
|
||
| On resume, recomputing delta from current HEAD | HEAD moved during the gap. Use `PENDING.json.{base,target,delta}` verbatim. |
|
||
| `git describe` to detect first deploy | Errors with no tag. Detect by `STATE.json` / `PENDING.json` existence. |
|
||
| `git rev-list` or three-dot for the delta | Phantom/undercounted deltas. Two-dot `<base> HEAD` only. |
|
||
| Executing the checklist yourself to "just run it" | Claude never deploys. Hand back; user runs by hand with `# VERIFY:` gates. |
|
||
| Committing the patch without the incident (or vice versa) | Coupling invariant. One atomic `deploy-commit.sh` call, both files. |
|
||
| Replaying only the failed step after a patch | Steps X…end never ran. Regenerate the checklist from `step_reached`. |
|
||
| Ending a hand-back with a blocking question tool after the checklist | Text before a tool call may never render. The checklist is the turn's FINAL text; the report comes as the user's next message. |
|
||
| Writing the checklist to a file "for reference" | Throwaway artifact — display only; PENDING.json + the runbook regenerate it anywhere. |
|
||
| Emitting a runbook `\` continuation as two lines | Join into one physical line. The clipboard pastes lines, not commands. |
|
||
| Wrapping a long command to fit a column width | One physical line, however long. The terminal wraps on screen; a wrapped paste runs two half-commands. |
|
||
| Ending the hand-back at the checklist | Checklist → post-deploy tests → report request. The delta says what changed; the tests say what to check. |
|
||
| Deriving the tests from commit messages | Read the delta diff. Subjects serve the wording only. |
|
||
| Fixing a gap the tests revealed, mid-deploy | It is a Suggestion (a check to run). The app is patched after the deploy, on its own branch. |
|
||
| Writing `STATE.json` before the user confirms success | Oracle marks success only. Failed deploy leaves it untouched. |
|
||
| Setting `deployed_sha` to HEAD at MARK time | Use `PENDING.target_sha` — the SHA actually deployed. |
|
||
| Parsing the JSON bridges with `jq` | Read them natively. No jq dependency. |
|
||
| Deleting `PENDING.json` before STEP 5 | The bridge is the resume marker — delete it only on confirmed success. |
|
||
|
||
## Red flags — STOP
|
||
|
||
- About to recompute the delta or re-read HEAD while a `PENDING.json` exists.
|
||
- About to run `git describe`, `git rev-list`, or a three-dot diff for the delta.
|
||
- About to execute the checklist or run any prod command yourself.
|
||
- About to call ANY tool after printing the checklist in a hand-back.
|
||
- About to write the checklist to a file.
|
||
- About to print a command across two lines (`\`, heredoc, wrapped).
|
||
- About to end a hand-back without the post-deploy tests block.
|
||
- About to commit `PROCEDURE.md` without `INCIDENTS.md` in the same call.
|
||
- About to write `STATE.json` before the user reported "Deployed OK".
|
||
- About to replay one failed step instead of regenerating from `step_reached`.
|
||
|
||
## Note on this skill (authoring)
|
||
|
||
Shaped via `superpowers:writing-skills`. The **cold cross-session resume** is the
|
||
novel form (design §10): the disk alone must carry the deploy across the
|
||
out-of-band gap, so `PENDING.json`'s presence marks the wait and STEP 0 resumes
|
||
from it without conversation memory — the `audit-delta` "state file is the only
|
||
memory between runs" convention, extended to a *mid-flow* pause. The forms here
|
||
match the failure modes the design identified: **discipline** failures
|
||
(recompute-on-resume, run-the-deploy, advance-the-oracle-early) get the
|
||
rationalization table + red flags; the **shape** of the checklist and the schemas get
|
||
positive recipes; the patch↔incident **omission** is a structural atomic-commit
|
||
requirement. Pressure-scenario baseline testing per the writing-skills Iron Law
|
||
is a follow-up for the two-moment core — those failure modes were taken from
|
||
the design spec, not a fresh RED run. The hand-back shape (one physical line
|
||
per command, post-deploy tests block) was RED/GREEN tested on 2026-09-17: 4/4
|
||
fresh agents on a scratch runbook carrying a `\`-continued psql reproduced the
|
||
continuation verbatim and printed no test list; the recipe above closed both.
|