First-real-run UX feedback (EVAL-016): one command per line as typed in an interactive session (ssh opens the box, following lines run on it, local steps flagged), never folded ssh compounds; the hand-back prints the full checklist in the conversation (and every re-hand-back reprints it). Step defined as a block (header + command lines to next blank line), @delta governs the block. Template restyled to match.
23 KiB
name, description, allowed-tools
| name | description | allowed-tools | |||||||
|---|---|---|---|---|---|---|---|---|---|
| deploy | 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". |
|
/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 writes the checklist (NEXT.sh), 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 NEXT.sh, 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/ (five 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 |
NEXT.sh |
no (gitignored) | instantiated checklist; run BY HAND, never bash NEXT.sh |
Schemas (document of record — recover the shapes from here):
// STATE.json — overwritten each successful deploy (the diff oracle)
{ "deployed_sha": "<sha>", "deployed_at": "<ISO-8601>", "outcome": "ok", "tag": "deploy/<YYYY-MM-DD>" }
// 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 NEXT.sh 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, and the runbook_rev staleness guard is the real
cold-resume regenerate trigger.
runbook_rev = the commit sha of PROCEDURE.md at instantiation; a mismatch
versus the live runbook means NEXT.sh is stale and must be regenerated.
@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.
| 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.jsonpresent → 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.- Staleness guard: if
NEXT.shis absent ORrunbook_rev≠ the live runbook commit (git log -1 --format=%H -- .claude/deploy/PROCEDURE.md), the on-diskNEXT.shis stale or missing (a patch landed, or a cold resume without regeneration) — regenerate it fromstep_reached(STEP 2's expansion) before reacting.
- Staleness guard: if
PENDING.jsonabsent +PROCEDURE.mdabsent → BOOTSTRAP. No runbook yet: interview the project and scaffold an annotatedPROCEDURE.md(or adopt one the user pastes), then continue at STEP 1. (See STEP 0-B below.)PENDING.jsonabsent +PROCEDURE.mdpresent → 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)
- Receive the runbook (paste, path → Read, or URL). Accept as-is.
- Prepend the standard header:
#!/usr/bin/env bash # === deploy runbook (reference) — NOT run directly. Instantiated to NEXT.sh per delta. === # Fixed steps run every deploy; annotated steps (@delta lines) re-instantiate from the delta. # @config push_deploy_tags=false - 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
- Migration steps (
- 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 |
| Push deploy tags | "push_deploy_tags? (true / false)" |
false |
Using templates/deploy/PROCEDURE.md as base, populate fields from interview answers + detected artifacts:
- Substitute
$DEPLOY_HOSTwith the supplied host (keep literal$DEPLOY_HOSTif none given). - Include only the annotated steps whose artifact was detected; keep all fixed steps.
- Set
# @config push_deploy_tags=<answer>in the header. - 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/deploywhen ready.
On approve — write + seed + commit:
- Write
.claude/deploy/PROCEDURE.md(Write tool — the approved draft). - Seed
.claude/deploy/INCIDENTS.mdfromtemplates/deploy/INCIDENTS.md(Write tool). - Ensure the target project's
.gitignorecontains.claude/deploy/NEXT.shand.claude/deploy/PENDING.json(append both if missing — these are the transient artifacts that must not be committed). - 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.gitignorewholesale — 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. - Commit via the allowlist helper:
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
bash lib/deploy-commit.sh commit \ "feat(deploy): bootstrap runbook" \ .claude/deploy/PROCEDURE.md .claude/deploy/INCIDENTS.md.gitignore· 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.jsonabsent → 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.jsonpresent → readdeployed_shaasbase, then:This is the literal tree difference deployed→HEAD. Nevergit diff --name-only <base_sha> HEAD # two explicit endpoints, no dotsgit rev-listancestry (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 NEXT.sh (the recipe — it IS this shape):
- Walk
PROCEDURE.mdin 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.
- Read
INCIDENTS.md; for eachDEP-NNNwhose step matches an emitted step, prepend# PRE-WARN: DEP-NNN <one-line summary>above it. - Keep every
# VERIFY:gate. Header the file: "Run by hand, step by step. Neverbash NEXT.shunattended." - Preserve the runbook's shape: one command per line, session style (see the
@delta:grammar section) — instantiation never re-folds lines. - Write
.claude/deploy/NEXT.sh.
[GATE] — present NEXT.sh → all / edit / skip-all.
all→ proceed.edit→ revise the listed steps, re-present.skip-all→ abort: write noPENDING.json, discard the draftNEXT.sh, stop.
On approve: write .claude/deploy/PENDING.json:
{ "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 checklist lands in the conversation, not just on disk.
Print the FULL final NEXT.sh content inline (fenced code block) so the user
sees exactly what to run without opening the file — the gate preview is not
enough (an edit round may have changed it; the hand-back shows the final
text). Then (AskUserQuestion): "Run NEXT.sh step by step against prod.
Report back: Deployed OK / Failed at step X: / Not yet." Then
stop — control is the user's; PENDING.json on disk now marks the wait.
The same rule applies to every re-hand-back (STEP 4.3): regenerated NEXT.sh
⇒ reprinted in full.
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: " → STEP 4.
- "Not yet" → restate what is pending (
step_reached, target, the command to run) and stop.PENDING.jsonstays; 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.mdso the next run cannot repeat the failure; - (b) an append to
INCIDENTS.md— a newDEP-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→ leavePENDING.jsonas-is, stop, nothing learned (the deploy stays failed-and-pending).
On approve — one ATOMIC commit of both files:
bash 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 · 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:
- Bump
PENDING.json.runbook_revtogit rev-parse HEAD(full sha — not the helper's short-hash stdout); keepstep_reached=X. - Regenerate
NEXT.shfromstep_reachedagainst the PATCHED runbook (steps X…end — X+1…end never ran). This is NOT replaying one step: the bumpedrunbook_revis exactly the staleness trigger — runbook changed ⇒ priorNEXT.shis stale ⇒ regenerate. - Re-present via STEP 2's [GATE] + hand-back (the regenerated
NEXT.sh;PENDING.jsonkeepsbase/target/delta,step_reachedback toawaiting-user).
STEP 5 — MARK (success)
The deploy succeeded. Lay the oracle and close out.
- Read
# @config push_deploy_tags=from thePROCEDURE.mdheader (defaultfalse). Pickdate = today(YYYY-MM-DD); ifdeploy/<date>exists, suffix-N. - Write
.claude/deploy/STATE.json(overwrite):{ "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. git tag -a deploy/<date> <PENDING.target_sha> -m "<summary>".- If
push_deploy_tags=true→git push origin deploy/<date>— best-effort, non-fatal: a push failure logs a warning, never blocks the mark (the tag is a bookmark;STATE.jsonis the oracle). - Commit the oracle:
bash lib/deploy-commit.sh commit "chore(deploy): mark <date> @ <short>" \ .claude/deploy/STATE.json - Delete
.claude/deploy/PENDING.jsonand.claude/deploy/NEXT.sh— the deploy is no longer in flight; the bridge is consumed. - Report: deployed SHA, tag (+ push result), state committed, any
DEP-NNNlearned this deploy. Then offer to capitalize per CLAUDE.md (recurring failure pattern →learnings.md; deploy verdict →evals.md), gated, never silent.
Rules
PENDING.jsonis the only memory across the gap. Read it first, every run.- On RESUME, never recompute
{base, target, delta}— the bridge is authoritative. deployed_shaisPENDING.target_sha, never live HEAD.- Delta is
git diff --name-only <base> HEAD(two endpoints). Norev-list, no three-dot, no date ranges. - First-deploy / fresh detection is file existence only — never
git describe. - Claude never executes the deploy.
NEXT.shis hand-run;# VERIFY:gates stay. - Patch + incident commit atomically, one
deploy-commit.shcall, both files. - A learn bumps
runbook_revand regeneratesNEXT.shfromstep_reached; it never replays a single step. - Tag push is best-effort;
STATE.jsonis the oracle. - JSON is read natively (Read tool), never parsed with
jq/shell. STATE.jsonwritten only on confirmed success (STEP 5). A failed/partial deploy leaves the oracle untouched,PENDING.jsonalive — 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. |
bash NEXT.sh 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 NEXT.sh from step_reached. |
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.jsonexists. - About to run
git describe,git rev-list, or a three-dot diff for the delta. - About to
bash NEXT.shor run any prod command yourself. - About to commit
PROCEDURE.mdwithoutINCIDENTS.mdin the same call. - About to write
STATE.jsonbefore 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 NEXT.sh 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 — the failure modes were taken from the design spec, not a fresh
RED run.