diff --git a/agents/client-handover-writer.md b/agents/client-handover-writer.md
index 1ba9c0c..c9a7271 100644
--- a/agents/client-handover-writer.md
+++ b/agents/client-handover-writer.md
@@ -62,7 +62,7 @@ and degrading Google's NAP-consistency signal.
Pipeline (each step gates the next):
1. Baseline audits: SEO+GEO and security hardening in parallel.
2. Fix loops: apply each audit's bundle (AUTO items, ONE gate for GATED ones) and re-audit until ≥17/20 or `MAX_ITERATIONS` hit.
-3. Commit + push if files changed.
+3. Commit if files changed (the hooks push in auto-push mode; the push state is read, never assumed).
4. Deploy pause: list deploy artifacts + process, wait for user confirmation.
5. Live-site validation against the deployed URL.
6. Per-axis gate: every score ≥17/20 OR stop + roadmap.
@@ -567,40 +567,45 @@ If `PENDING_CHANGES` non-empty → invoke /commit-change skill via subagent:
> commit). Use Conventional Commits format. After committing, return the
> SHA list."
-Then, **before pushing, STOP and ask for an explicit GO** — the push is an
-outward-facing action and never fires autonomously:
+**PUSH STATE READ.** Three separate Bash calls, never combined, read-only:
+`git branch --show-current` → `
`;
+`git remote get-url origin >/dev/null 2>&1 && echo origin || echo no-origin`;
+`git rev-list --count origin/
..
2>/dev/null || echo unknown` →
+`ahead`. Validate `
` against `^[A-Za-z0-9._/-]+$` before using it
+anywhere (git accepts shell metacharacters in branch names). On mismatch:
+state = `unknown (branch name contains characters this pipeline refuses to
+interpolate: push by hand after renaming the branch)`, interpolate NOTHING,
+skip the rev-list and the verb. The validated `
` is the only name ever
+placed in a `! git push -u origin
` hint (STEP 5, deploy brief,
+reports); every re-run of PUSH STATE READ inherits this rule.
+If `ahead` ≠ 0 and origin exists:
+`bash "$HOME/.claude/lib/gitflow.sh" push-mode` → anything other than `auto`
+is treated like `manual` (stderr line kept verbatim when `invalid`).
+State, first match wins, in this order: (1) no commits were made this run
+or the gitflow fallback left changes uncommitted →
+`nothing to push (no commits this run)` / `uncommitted changes (no gitflow
+model): publish by hand`, stop; (2) `
` invalid → the unknown state
+above; (3) `no-origin` → `not on origin (no origin remote: add one first)`;
+(4) `ahead` = 0 → `on origin`; (5) otherwise (`ahead` > 0 or `unknown`)
+→ `pending — you: ! git push -u origin
` + reason: push mode `manual` →
+`(manual push mode)`, `auto` → `(not on origin: no remote-tracking ref or
+the hook push did not land)`, `invalid` → `()`.
+The pipeline never runs `git push` itself.
-> AskUserQuestion — "Changes committed on ``. Push to origin now?
-> - A) Yes — push `` to origin
-> - B) No — I'll push manually before confirming deploy"
+`pending` → tell the user NOW: `Commits are local only. Push first:
+! git push -u origin
`.
-Only on **A** run the push; on **B** skip it and note "push deferred to user"
-in the STEP 8 summary, then continue.
-
-> **Red flag — STOP:** never `git push` without option-A GO; never
-> `gitflow finish`/`merge`. This pipeline commits and (on GO) pushes a working
-> branch — it never integrates into a protected branch.
-
-```bash
-CURRENT_BRANCH=$(git branch --show-current)
-git push origin "$CURRENT_BRANCH" 2>&1
-```
-
-If push fails (no remote, auth issue, conflict): capture error, report to
-user via AskUserQuestion:
-
-```
-"Push failed: . Pipeline needs the changes published before deploy.
-Options:
-- A) Retry push (after I fix it manually)
-- B) Skip push — I'll publish manually before confirming deploy
-- C) Abort pipeline"
-```
+> **Red flag — STOP:** never `git push` (the hooks push in auto-push mode;
+> otherwise the user does); never `gitflow finish`/`merge`. This pipeline
+> commits a working branch — it never integrates into a protected branch.
---
## STEP 6 — DEPLOY PAUSE
+Re-run PUSH STATE READ (every path reaches STEP 6, some without STEP 5's
+read).
+
Skip if `PROJECT_TYPE != web` (non-web has no deploy-then-validate flow —
set `VALIDATE_SKIPPED=true` and jump to STEP 8).
@@ -625,10 +630,15 @@ Commits added in this session:
Tailor to project deploy method (use DEPLOY_HINTS):
-- **Vercel/Netlify/Cloudflare Pages auto-deploy from git**: "Push has been
- done. The platform deploys automatically — usually 1-3 min. Watch the
- dashboard. Tell me when the new version is live."
-- **GitHub Actions / GitLab CI**: "Workflow `` should run on push.
+When the state is `pending`, the brief OPENS with
+`First push: ! git push -u origin
`. When `on origin`, keep "Push has
+been done. …".
+
+- **Vercel/Netlify/Cloudflare Pages auto-deploy from git**: "The platform
+ deploys automatically after your push (a working branch gives a preview
+ at most; production builds from the production branch) — usually 1-3
+ min. Watch the dashboard. Tell me when the new version is live."
+- **GitHub Actions / GitLab CI**: "Workflow `` runs on your push.
Watch CI status. Tell me when it's green and live."
- **Manual upload (FTP / SSH)**: "Upload these files to the server: ``.
If using rsync, here's a template: `rsync -avz dist/ user@server:/path`."
@@ -650,6 +660,9 @@ AskUserQuestion:
- C) Skip /web-validate — proceed to handover doc with VALIDATE marked SKIPPED
```
+After option A "Deployed": re-run PUSH STATE READ; still `pending` → ask
+again (the live site cannot hold these commits).
+
If A → proceed to STEP 7. If B → exit cleanly with state report. If C →
mark `VALIDATE_SKIPPED=true` and jump to STEP 8.
@@ -683,8 +696,8 @@ SCORE_VALIDATE_AFTER=$(extract_score .claude/audits/VALIDATE.md)
Note: VALIDATE has no `_BEFORE` (first run is post-deploy). The before/after
table for VALIDATE shows `—` for before, `` for after.
-If /web-validate produced new fixes in source code, run STEP 5 again (mini-commit
-+ push) BEFORE moving to STEP 8 — but DO NOT loop /web-validate. The remaining
+If /web-validate produced new fixes in source code, run STEP 5 again (mini-commit;
+push state read, never assumed) BEFORE moving to STEP 8 — but DO NOT loop /web-validate. The remaining
deploy of those fixes is mentioned to the user in the final doc.
---
@@ -806,9 +819,14 @@ Below-threshold audits:
Roadmap written to .claude/audits/HANDOVER-ROADMAP.md.
Tasks appended to .claude/tasks/TODO.md.
+Push:
+
Resolve P0 items, then re-run /client-handover.
```
+Re-run PUSH STATE READ right before printing this report (never reuse
+the STEP 5 snapshot).
+
If `ALL_PASS = true` → proceed to STEP 9 (memory load + doc generation).
### Roadmap structure (when gate fails)
@@ -1170,9 +1188,13 @@ Parse the returned `HANDOVER-DOC REPORT`:
- `STATUS: DONE` → report the `MD` / `HTML` / `PDF` paths to the user,
plus the `GATES` line and any `NOTES` caveats (e.g. `[À COMPLÉTER]`
markers left in NAP, deploy chapter included/skipped).
+ Re-run PUSH STATE READ right before printing, then add the bullet:
+ - Push:
- `STATUS: BLOCKED` → surface the report verbatim (including which
PACKAGE field the doc-writer flagged) and stop — do not retry or
- patch the PACKAGE silently.
+ patch the PACKAGE silently. Re-run PUSH STATE READ and add the same
+ bullet:
+ - Push:
In BOTH branches, then clean the transient draft:
`rm -f ".audit/handover-draft-${RUNID}.md"` (run-scoped, gitignored —
diff --git a/agents/release-executor.md b/agents/release-executor.md
index f33a180..b86aa05 100644
--- a/agents/release-executor.md
+++ b/agents/release-executor.md
@@ -77,15 +77,17 @@ actual branch; never finish whatever happens to be checked out.
output verbatim; do not attempt to resolve it yourself.
2. **Tag AFTER finish, on `main`** — never before:
`git tag -a v main -m "release "` (annotated, so it lands on
- main's release-merge commit). Finish has already pushed `main` and
- `develop` through the lib's hooks (BDR-095); the tag stays local until
- the dispatcher's tag-push gate.
+ main's release-merge commit). In auto-push mode finish pushes `main`
+ and `develop` (best effort: the lib warns and returns 0 on a failed
+ push; the dispatcher re-verifies with ahead counts) (BDR-095); in
+ manual push mode they stay local. The tag stays local until the
+ dispatcher's tag-push gate.
### Forbidden in this span
-`git push` (any remote, any ref — `main`/`develop` ride the lib's hook
-pushes during finish; the dispatcher owns the tag-push gate), deciding the
-version number, the when-to-release decision, attribution trailers of any
-kind.
+`git push` (any remote, any ref — `main`/`develop` ride the lib's
+pushes during finish in auto-push mode; the dispatcher owns the tag-push
+gate), deciding the version number, the when-to-release decision,
+attribution trailers of any kind.
---
diff --git a/skills/client-handover/SKILL.md b/skills/client-handover/SKILL.md
index 73ebfaf..d7df8f0 100644
--- a/skills/client-handover/SKILL.md
+++ b/skills/client-handover/SKILL.md
@@ -45,7 +45,7 @@ The agent runs a **ship-and-handover pipeline** with explicit gates:
- Re-invoke the audit subagent in audit mode: it re-scores and returns the next FIX BUNDLE; it applies nothing (a dispatched child cannot hold a gate).
- Repeat up to `MAX_ITERATIONS` (default 5).
- If still < 17/20 after cap → escalate to user with concrete remaining issues; user decides continue / stop / manual intervention.
-4. **COMMIT + PUSH** — If files changed during fix loops, run /commit-change (atomic logical commits) then `git push`.
+4. **COMMIT + PUSH** — If files changed during fix loops, run /commit-change (atomic logical commits); the gitflow hooks push in auto-push mode, otherwise (manual push mode, or a hook push that failed) the agent tells the user to push with `! git push -u origin ` BEFORE the deploy pause.
5. **DEPLOY PAUSE** — List exact deploy artifacts: changed files since baseline, deploy hints from project (vercel.json, netlify.toml, Dockerfile, .github/workflows/deploy.yml, etc.), and the deploy process in plain words. Use AskUserQuestion: "Deploy done? (Yes / Not yet / Skip validate)". Block until Yes or Skip.
6. **/web-validate (live site)** — Run validator-analyzer against the deployed URL. Capture `SCORE_VALIDATE`.
7. **GATE — per-axis threshold ≥17/20** — Compute final `SCORE_*_AFTER` for SEO classique, GEO (IA), HARDEN, VALIDATE. If ANY < 17/20: STOP. Generate `.claude/audits/HANDOVER-ROADMAP.md` with prioritized analysis of what's blocking each below-threshold axis. Do NOT write the client deliverable. Report to user.
diff --git a/skills/release-candidate/SKILL.md b/skills/release-candidate/SKILL.md
index 7eddd35..4437664 100644
--- a/skills/release-candidate/SKILL.md
+++ b/skills/release-candidate/SKILL.md
@@ -25,8 +25,9 @@ The two mechanical spans (prep, finish+tag) run on the sonnet-pinned
gate needed here, dispatch does the job. This dispatcher keeps everything
the executor must never own: the version-NUMBER decision (judgment — derives
from semver change nature), and the two human gates (when to release, and
-the tag push). A human gate sits BETWEEN the two spans by construction, so the
-executor is never dispatched twice in one call.
+the tag push (auto-push mode; in manual push mode the user pushes main,
+develop and the tag in one command)). A human gate sits BETWEEN the two
+spans by construction, so the executor is never dispatched twice in one call.
## When to use
- `develop` is ahead of `main` and you want to publish a version.
@@ -54,6 +55,8 @@ Read the `## [Unreleased]` section of `CHANGELOG.md` and the commits on
`develop` since `main`. Apply the Versioning rule above (breaking → MAJOR,
features → MINOR, fixes → PATCH) and settle `` before dispatching
anything — the executor never derives or second-guesses this number.
+The version must match `^[0-9]+\.[0-9]+\.[0-9]+$` before it is placed in
+any command or tag; anything else stops the run.
### STEP 3 — Dispatch: prep
```
@@ -93,10 +96,22 @@ Parse the `RELEASE-EXEC REPORT`:
not an auto-retry.
### STEP 6 — Tag push GATE (ASK)
-`main` and `develop` are already on origin: the lib pushes every merge as
-it lands (`_gitflow_merge_into` + the post-merge hook, BDR-095). Only the
-tag is left. STOP. On explicit go only ([[LRN-069]]) — run the tag push
-HERE, in this dispatcher, never delegated to the executor:
+Read the state, separate Bash calls:
+`git rev-list --count origin/main..main 2>/dev/null || echo unknown`,
+`git rev-list --count origin/develop..develop 2>/dev/null || echo unknown`,
+`bash "$HOME/.claude/lib/gitflow.sh" push-mode`.
+- Anything other than `auto` from the verb (manual, invalid, empty, usage
+ error) OR either count ≠ 0 or `unknown` → Claude pushes nothing
+ (push-guard would refuse it in manual mode; a failed lib push is the
+ user's call, BDR-095). Print ONE command for the user and STOP, no
+ question: `! git push --atomic origin main develop v` (invalid:
+ quote the verb's stderr line verbatim; auto with a count ≠ 0 or unknown:
+ say `main/develop not on origin (no remote-tracking ref or the lib's push
+ did not land)`; no origin remote (`git remote get-url origin` fails): say
+ `add an origin remote first`).
+- Push mode `auto` and both counts 0 → main and develop are on origin; only
+ the tag is left. STOP. On explicit go only ([[LRN-069]]) — run the tag
+ push HERE, never delegated:
```
AskUserQuestion:
Push tag v to origin? — go / hold
@@ -105,13 +120,16 @@ Go →
```bash
git push origin v
```
-`hold` → stop; the release is on origin (main + develop), the tag stays local.
+`hold` → stop; the release is on origin (main + develop), the tag stays
+local until the next push of main (`--follow-tags` on every lib and hook
+push).
## Common mistakes
- Tagging before `gitflow finish` → tag wouldn't sit on main's merge commit. Tag AFTER, on main.
- Auto-firing finish because tests pass → finish is a HUMAN gate.
- Restarting the tag at v1.0.0 → desyncs from the CHANGELOG lineage. Continue it.
- Pushing the tag without the ASK gate → [[LRN-069]].
+- Pushing anything in manual push mode → print the one user command, push nothing.
## Validation
`RC_WORK=$(mktemp -d) RC_TAG=1 bash lib/tests/run-release-candidate.sh` → 5/5 (fan-out + tag on main). `RC_TAG=0` reds the tag assertion — proves the lib alone never tags (the gap this skill fills).
diff --git a/skills/tour/SKILL.md b/skills/tour/SKILL.md
index 3f7ca46..9f88245 100644
--- a/skills/tour/SKILL.md
+++ b/skills/tour/SKILL.md
@@ -237,6 +237,16 @@ order:
and says so in the summary.
4. Confirm `git status --porcelain` is clean (runtime junk the sandbox
cannot delete, e.g. `__pycache__/`, becomes a report residual line).
+5. Push state, only when a branch exists (report-only, skipped or
+ dirty-tree projects have none: their row keeps `no branch`, no push
+ column). Two read-only calls, probe first:
+ `git -C remote get-url origin >/dev/null 2>&1 || echo no-origin`
+ then
+ `git -C rev-list --count --not --remotes=origin 2>/dev/null || echo unknown`
+ (`` = the name `gitflow start` returned, suffixed `-2`/`-3` on a
+ same-day re-run — never the bare `chore/tour-`). 0 → `on origin`;
+ else `local only → ! git -C push -u origin ` (probe
+ printed `no-origin` → `local only (no origin remote)`).
```markdown
## Tour 2026-07-04 — branch chore/tour-2026-07-04 — 2 iterations — CONVERGED
@@ -255,8 +265,8 @@ any project line with contract-changing fixes left open for decision):
```
TOUR COMPLETE — 2026-07-04
- ~/proj/api : CONVERGED (2 it.) — 3 fixed, 1 suggested | chore/tour-2026-07-04, 4 commits
- ~/proj/site : NOT CONVERGED (3 it.) — 2 open residuals | chore/tour-2026-07-04, 6 commits
+ ~/proj/api : CONVERGED (2 it.) — 3 fixed, 1 suggested | chore/tour-2026-07-04, 4 commits | on origin
+ ~/proj/site : NOT CONVERGED (3 it.) — 2 open residuals | chore/tour-2026-07-04, 6 commits | local only → ! git -C ~/proj/site push -u origin chore/tour-2026-07-04
~/proj/lib : report-only (dirty tree) | no branch
Branches left UNMERGED — review each, then `gitflow finish` on your GO.
Reconcile suggestions pending — apply via /reconcile.
@@ -270,9 +280,10 @@ without that approval — neither this repo's nor any target project's.
- Branch via the gitflow lib; **never `gitflow finish`, never merge,
never push `main`/`develop`** — "the tour is green" is not a signal.
- The chore branch's own commits are pushed by the gitflow hooks
- (BDR-095); a `push FAILED` hook warning is a report residual, fixed
- with a plain `git push -u origin chore/tour-`.
+ The gitflow hooks push the chore branch in auto-push mode only; when it
+ is not on origin (manual push mode, or a `push FAILED` warning) the USER
+ pushes it — `! git -C push -u origin ` — the tour
+ never pushes or retries.
- Scoped pathspecs only; `git add -A` is forbidden.
- Target TODO.md and target `.claude/memory/` are READ-ONLY. Reconcile
produces suggestions, not edits.