From 0b08ceda97038d6d07c4de54f3704b8eb9fabea0 Mon Sep 17 00:00:00 2001 From: bchanot Date: Wed, 7 Oct 2026 14:48:07 +0200 Subject: [PATCH] =?UTF-8?q?chore(memory):=20BDR-113=20+=20LRN-197..199=20+?= =?UTF-8?q?=20journal=20=E2=80=94=20feat=20manual-push-mode=20run=20C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/memory/decisions.md | 7 ++ .claude/memory/journal.md | 1 + .claude/memory/learnings.md | 12 +++ .claude/tasks/TODO.md | 6 +- .../2026-10-07-manual-push-skills-c1-1304.md | 41 ++++++++++ .../2026-10-07-manual-push-skills-c2-1325.md | 43 ++++++++++ .../2026-10-07-manual-push-skills-c1-1304.md | 79 +++++++++++++++++++ .../2026-10-07-manual-push-skills-c2-1325.md | 33 ++++++++ 8 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 .claude/tasks/contracts/2026-10-07-manual-push-skills-c1-1304.md create mode 100644 .claude/tasks/contracts/2026-10-07-manual-push-skills-c2-1325.md create mode 100644 .claude/tasks/plans/2026-10-07-manual-push-skills-c1-1304.md create mode 100644 .claude/tasks/plans/2026-10-07-manual-push-skills-c2-1325.md diff --git a/.claude/memory/decisions.md b/.claude/memory/decisions.md index c4bc03e..8a3f2ee 100644 --- a/.claude/memory/decisions.md +++ b/.claude/memory/decisions.md @@ -1378,3 +1378,10 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate). - **Alternatives rejected**: no-jq fallback (greps whole payload, denies in auto, untestable without shim; jq hard dep); `-C` dir unresolvable → allow (fail-open; now skipped, cwd still checked); `rev-parse` work-tree gate (drops the global key); `--default true` read (hides git failure); narrowing deny to spare the read (impossible with end-matching globs). - **Gates**: 3 lenses CONCERNS(2)/CONCERNS(5)/FATAL(7) + confirmation FATAL(8) → r2 (BSD sed, oracle naming denied tokens, read loss); feater ×3 (58 → 61 → 71 checks); GATE 0 MET ×3; verifier CONFORME, then ECARTS(1) on hardening closed by gated clarification; security PASS ×2 (3 MEDIUM closed: 20k-token flood denied in 0.15 s, git absent → deny, `bash -c 'cd … && git push'` extracted; residuals → run D). - **Refs**: contract `.claude/tasks/contracts/2026-10-07-manual-push-guard-1003.md`, plan `.claude/tasks/plans/2026-10-07-manual-push-guard-1003.md`, commits a2ac018 + 6468eda (feature/manual-push-mode, UNMERGED). Links [[BDR-095]], [[BDR-100]], [[LRN-069]], [[LRN-196]]. Open: user probe `! git push --dry-run` under autopush=false (bang commands assumed hook-free). + +## BDR-113 — Skills never push; truth from `rev-list` facts, mode verb only words the reason [accepted] (2026-10-07) +- **Decision**: run C of [[BDR-111]]. New lib verb `gitflow.sh push-mode` (stdout `auto|manual|invalid`, rc 0, raw value on stderr, printable ≤64 chars; ignores GITFLOW_NO_PUSH) = the one reader skills may call (bare `git config … gitflow.*` denied, [[BDR-112]]). Skills push NOTHING on their own except the release tag in auto mode on explicit go. Every "on origin / not pushed" line comes from `git rev-list --count origin/
..
` (or `
--not --remotes=origin` for the tour) read AFTER the action, in its own Bash call; the verb only words the reason (manual vs push FAILED vs invalid). Removed as redundant since BDR-095: `/close` STEP 5C `git push origin develop` (finish pushes develop itself, mode-aware since run A) and `/client-handover`'s "Push to origin now?" question + push block (hooks had pushed; push-guard denies in manual). User hints are complete `! git …` commands (`-u`, `--atomic origin main develop vX`, `-C `), branch name allowlisted `^[A-Za-z0-9._/][A-Za-z0-9._/-]*$` before any interpolation. `/release-candidate` manual/invalid or any count ≠ 0 → one user command, STOP, no question; tag gate kept for auto with both counts 0. `/tour`: one `-C` fact per project after the report commit, suffix-aware branch name, summary row `on origin | local only → cmd`. +- **Why**: a shell gate `[ "$mode" = auto ] && git push …` is denied WHOLE by the text-only push-guard and `$mode` dies between Bash calls ([[LRN-199]]); the push it gated was redundant anyway ([[LRN-197]]); invalid value: lib/hooks still push (fail-open until run D) so "not pushed" from the mode word would lie — the count tells the truth. +- **Alternatives rejected**: gate the existing push on the verb (denied/stateless); keep the GO question (gated nothing); word invalid as manual (false in the lib); per-skill `git config` read (denied). +- **Gates**: C1: 3 lenses (1 BLOCKER: shell gate) + confirm CONCERNS(3); feater; GATE 0 MET; verifier CONFORME; security PASS. C2: 3 lenses (BLOCKER: deploy brief said "push done") + confirm CONCERNS(4); feater; verifier CONFORME; security BLOCK(1) branch-name injection ([[LRN-198]]) → fixed → CONFORME + PASS. Polish pass: CONFORME + PASS. +- **Refs**: contracts `.claude/tasks/contracts/2026-10-07-manual-push-skills-c1-1304.md`, `…-c2-1325.md`; plans same slugs; commits 5cf049d, 6104545, 3881f46 (feature/manual-push-mode, UNMERGED). Links [[BDR-068]], [[BDR-095]], [[BDR-042]], [[LRN-069]]. diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index e3460e0..2b3e474 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -572,4 +572,5 @@ rules: ## 2026-10-07 - /feat manual-push-mode run B (user: "enchaine"): `hooks/push-guard.sh` PreToolUse denies Claude's `git push` when autopush false/unparseable/unreadable in cwd or literal -C/cd dirs (global config counts). Challenge: 3 lenses + robustness confirm FATAL(8) → BSD sed `N` fold empty on 1 line (hook dead), AC3 oracle naming denied tokens, glob trailing ` *` matches end → bare read lost, run C needs lib verb. feater ×3 (impl 58, no-jq test 61, hardening 71: cap 20 dirs, git rc → deny, quoted cd). GATE 0 MET ×3; verifier CONFORME then ECARTS(1) → user gated fail-closed also in auto for pathological commands; security PASS ×2 (3 MEDIUM closed, 2 residual → run D). Commits a2ac018 + 6468eda on feature/manual-push-mode, UNMERGED. settings.json live: 18 deny entries, soft_deny, Bash|Monitor hook group. User probe pending: `! git push --dry-run` bypasses hooks? +- /feat manual-push-mode run C (user: "enchaine"), split C1+C2. C1: lib verb `gitflow.sh push-mode` (auto|manual|invalid, value on stderr, rc 0) + T11b; /close STEP 5C `git push origin develop` REMOVED (finish has pushed develop since BDR-095; shell gate would be denied whole by push-guard, `$mode` dies between Bash calls) → finish, verb, `rev-list --count origin/develop..develop`, prose outcomes incl. finish-failure; `--no-push` line from the branch's own count. Challenge 3 lenses (1 BLOCKER) + confirm CONCERNS(3); verifier CONFORME; security PASS. C2: client-handover GO question + push block removed (hooks pushed already in auto; guard denies in manual) → PUSH STATE READ re-run before every claim, branch-name allowlist (security BLOCK(1) → fixed, PASS); release-candidate: two counts + verb, `! git push --atomic origin main develop vX`, tag gate kept for auto/0/0; tour: `-C` fact per project, suffix-aware branch, `--remotes=origin`. Verifier CONFORME ×2. Commits 5cf049d + 6104545, UNMERGED. Polish pass in flight. diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index ef32021..69293c0 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -1724,3 +1724,15 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s ## LRN-196 — A fail-closed Claude Code hook: trap must `exit 0`, cap attacker-sized loops, read git's rc not its value - **Context**: push-guard hardening (security gate, 3 MEDIUM). (1) EXIT trap printed the static deny but kept the non-zero rc → Claude Code parses hook JSON only on exit 0 → deny ignored = allow. (2) Each literal `cd`/`-C` token cost a subshell + 3 git execs: 600 tokens = 12 s > 10 s hook timeout → timeout = non-blocking = allow. (3) `git config --bool --default true` returns empty on git absent / old git / unreadable dir → read as "auto" → allow. - **Apply**: `trap '… ; exit 0' EXIT`; deny path `out=$(jq …) || out=$STATIC; printf '%s' "$out"`; dedup (`sort -u`) + hard cap on command-controlled token counts, deny above the cap BEFORE any fork; distinguish `git config` rc 0/1/other (value / unset / failure → deny); record "decided" only after ≥1 clean evaluation. Lock each with a test (shim PATH without a tool, 25-token flood, chmod 000 dir with SKIP path). Measure the flood after the fix (20 000 tokens → 0.15 s). Links [[BDR-112]], [[BDR-087]], [[LRN-160]]. + +## LRN-197 — A skill's own `git push` after a lib finish or a hook-pushed commit is redundant since BDR-095: delete it, don't gate it +- **Context**: `/close` STEP 5C ran `gitflow finish` then `git push origin develop` (added 2026-07-16); the lib push landed 2026-09-22 and 5C was never revisited. `/client-handover` asked "Push to origin now?" after commit-change, whose commits the post-commit hook had already pushed. Both runs' first plan GATED the push on the mode; the simplicity lens found both pushes redundant. +- **Apply**: before gating an action, ask whether it still does anything. Grep every `git push` in skills/agents after any change to hooks/lib push behaviour (BDR-100 surface rule); replace a redundant push + its question by a FACT read afterwards (`git rev-list --count origin/
..
`) and a user hint. Links [[BDR-113]], [[BDR-095]], [[LRN-113]]. + +## LRN-198 — Text read from git and pasted into a later Bash call is an injection sink: allowlist before interpolating +- **Context**: C2 replaced one Bash block (`CURRENT_BRANCH=$(git branch --show-current); git push origin "$CURRENT_BRANCH"`, quoted variable, safe) by three separate calls where the branch name is pasted as text into `git rev-list --count origin/
..
` and into `! git push -u origin
`. `git check-ref-format --branch 'x$(id)y'` rc 0: a hostile branch (PR checkout, crafted remote) runs its payload. Security gate BLOCK(1). +- **Apply**: any name an agent READS (branch, tag, path from repo state) and later WRITES into command text must pass an allowlist first (`^[A-Za-z0-9._/][A-Za-z0-9._/-]*$`; leading `-` excluded = option injection); on mismatch interpolate nothing and say so. Prefer a quoted shell variable inside ONE call when the flow allows; when prose branching forces multi-call, the allowlist replaces the quotes. `` from user args: same class, lower trust gap. Links [[BDR-113]], [[LRN-196]]. + +## LRN-199 — Agent-level branching is prose on a printed word: shell state dies between Bash calls, and a text guard denies the whole call +- **Context**: plan wrote `mode=$(gitflow.sh push-mode)` then `[ "$mode" = auto ] && git push origin develop`. Two failures: (a) separate calls → `$mode` empty → silent skip, rc 1 misread as "push FAILED"; (b) one call → push-guard's STRICT regex matches `&& git push origin` in the TEXT and denies the WHOLE call, so even `finish` never runs. Three lenses hit it independently. +- **Apply**: a skill reads a word from a command's visible stdout, then branches in PROSE ("printed `auto` → run X as its own call; anything else → never issue X"). Never a shell variable across calls, never a conditional that contains a guarded token. Any text-only PreToolUse guard turns `cmd-you-wanted-to-avoid` inside a conditional into a denial of the surrounding command. Links [[BDR-112]], [[BDR-113]], [[LRN-191]]. diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 8cb8517..76bb3c5 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -2060,9 +2060,11 @@ dans un runner; capitalize reste main-loop. - [x] run A — `gitflow.autopush=false` honoured by `_gitflow_push_branch`, quiet unpushed-guard, doctrine line; plan `.claude/tasks/plans/2026-10-06-manual-push-mode-1632.md` → commit 2fc8830 on feature/manual-push-mode; verifier ECARTS(1) = AC6 only (design-tool-gate env red, pre-existing on develop) → human waiver; merge human-gated - [x] run B — `hooks/push-guard.sh` PreToolUse (deny `git push` in manual mode) + 71-check test + settings.json (own hook group Bash|Monitor timeout 10; 18 write-form deny entries on the toggle; soft_deny on manual-mode pushes with no per-turn clearance; prose) + banner → a2ac018 + hardening commit; verifier CONFORME then ECARTS(1) closed by gated clarification (fail-closed cap/unenterable dir also in auto mode); security PASS ×2 - [ ] run D also (push-guard residuals, security gate 2026-10-07): tokens with inner quotes/backslashes (`cd /m/'a b'`) resolve to the wrong dir → treat as unresolvable + deny or document; unparseable payload (lone surrogate) → jq fails → silent allow → grep raw payload for `push` and deny; `case "$mode"` default `*) deny`; up-front `command -v grep sed sort head jq` check; header line > 80 cols; T42 compares against HEAD (vacuous once committed) → compare against a pinned base or drop; no test sets the key to literal `true` -- [ ] run C — skills that push on their own, gate on the mode through a NEW lib verb `bash ~/.claude/lib/gitflow.sh push-mode` (prints auto|manual|invalid; the bare `git config … gitflow.autopush` read is denied for Claude after run B — a trailing ` *` glob also matches end-of-string): capitalize STEP 5C (`git push origin develop`), client-handover SKILL:48 + agents/client-handover-writer.md:586, release-candidate:96 + tour:273 "already on origin" claims +- [x] run C (C1 5cf049d, C2 6104545; split C1: lib verb `push-mode` + T11 tests + capitalize STEP 5C + close hint, plan `.claude/tasks/plans/2026-10-07-manual-push-skills-c1-1304.md`; C2: client-handover skill+agent, release-candidate STEP 6, tour rule) — skills that push on their own, gate on the mode through a NEW lib verb `bash ~/.claude/lib/gitflow.sh push-mode` (prints auto|manual|invalid; the bare `git config … gitflow.autopush` read is denied for Claude after run B — a trailing ` *` glob also matches end-of-string): capitalize STEP 5C (`git push origin develop`), client-handover SKILL:48 + agents/client-handover-writer.md:586, release-candidate:96 + tour:273 "already on origin" claims - [ ] run B also: fail-CLOSED on an unparseable `gitflow.autopush` value in every reader at once (lib `_gitflow_push_off`, the two emitted push hooks, unpushed-guard) — run A keeps fail-open for consistency with the untouched emitters (security gate MEDIUM, 2026-10-06); `--end-of-options`/`--` on refname args and `printf %q` in copy-paste hints (LOW); `gitflow_delete`: check `_gitflow_checkout_containing_base` rc before `--unset-upstream` (LOW, 2nd gate) -- [ ] ORDER: do not set `gitflow.autopush false` on the work machine before B + C are merged (until then `/close` still pushes develop) +- [x] C1/C2 polish pass → 3881f46 (verifier CONFORME, security PASS; items were: capitalize STEP 5C heading still says "(finish + push)"; :372 paragraph glued to the :371 bullet and tells the WORKING-branch path to read `origin/chore/..` (no such ref there; that path never uses the mode); on finish rc 5/2/6 calls 2-3 are skipped so a manual-mode user gets no `! git push origin develop` hint; the "unknown + manual" closing line (:377) lacks the `once a remote exists` hint present in 5C (:348); STEP 6 "auto-persisted … pushed" bullet (:371) not tied to `ahead = 0` (security MEDIUM); verb stderr: cap `raw` to 64 printable chars; T11b "default auto" relies on the Makefile's hermetic env (fine under `make test`, spurious when run bare on a global-manual machine) → export in the suite header like line 257. C2 polish (verifier non-gaps): client-handover-writer PUSH STATE READ states need explicit precedence (uncommitted/no-commits first, then no-origin, then ahead); tour STEP 3 item 5 only when a branch exists (report-only / dirty-tree rows have none); 9.7 `STATUS: BLOCKED` branch lacks the `- Push:` bullet; release-executor:85-86 line > 80 cols +- [x] ORDER constraint lifted: A + B + C all on feature/manual-push-mode; merge (human gate) then the work machine may set `gitflow.autopush false`. Still pending before relying on it at work: user probe `! git push --dry-run` in a scratch repo under autopush=false (bang commands assumed hook-free); run D below. +- [ ] run D — fail-CLOSED on an unparseable `gitflow.autopush` in every reader at once (lib `_gitflow_push_off`, the two emitted push hooks + githooks regen, unpushed-guard) so the "invalid → lib/hooks still push" caveat in CHANGELOG/SETTINGS/skills can be removed; push-guard residuals (inner-quote/backslash tokens, lone-surrogate payload, `*) deny` default, up-front tool check, T42 base, literal `true` test); verb stderr: `LC_ALL=C` done, truncation marker + sanitizer test; client-handover: stale "COMMIT + PUSH" headings, `` quoting in tour hints; release-executor own version regex ## test hermeticity (2026-10-06, found during manual-push-mode run A) - [ ] `lib/tests/design-tool-gate.test.sh` reds on any machine with the 21st CLI installed ("FAIL precondition: system-wide 21st present, CLI_ABSENT case not hermetic") — pre-existing on develop (fa67664), independent of the diff. Make the CLI_ABSENT case hermetic (PATH shim / stubbed probe) so `make test` is green on a design-profile machine. Until then full-suite oracles (`make test` exit 0) cannot be MET here. diff --git a/.claude/tasks/contracts/2026-10-07-manual-push-skills-c1-1304.md b/.claude/tasks/contracts/2026-10-07-manual-push-skills-c1-1304.md new file mode 100644 index 0000000..dc3d321 --- /dev/null +++ b/.claude/tasks/contracts/2026-10-07-manual-push-skills-c1-1304.md @@ -0,0 +1,41 @@ +# CONTRACT — manual-push-skills-c1 (run C1 of manual-push mode) +- date: 2026-10-07 | flow: feat | branch: feature/manual-push-mode (runs A 2fc8830, B a2ac018+6468eda landed; C2 = client-handover ×2, release-candidate, tour follows) +- status: active + +## REQUEST (verbatim — IMMUTABLE) +User (fr): "ok enchaine sur le run C" +Run C as scoped in `.claude/tasks/TODO.md` "manual-push-mode": skills that push on their own, gate on the mode through a NEW lib verb `bash ~/.claude/lib/gitflow.sh push-mode` (prints auto|manual|invalid; the bare `git config … gitflow.autopush` read is denied for Claude after run B): capitalize STEP 5C (`git push origin develop`), client-handover SKILL:48 + agents/client-handover-writer.md:586, release-candidate:96 + tour:273 "already on origin" claims. User's framing (2026-10-06): "Il faut tout faire pareil, juste rien push seul. Mais faire les branches localement, faire les commits localement etc. Juste il faut pas push. seulement manuel". + +## CLARIFICATIONS +Q: scope split / A: C1 (this contract) = lib verb + its test + `/capitalize` STEP 5C + the `--no-push` hints in capitalize and close; C2 = client-handover skill + agent, release-candidate STEP 6, tour rule. 8 files > the /feat cap of 5. [orchestrator — scope, surfaced] +Q: does `/close` still merge the memory chore branch into develop in manual mode? / A: yes — local merges are part of "tout faire pareil"; only the push is withheld, and the handoff line says so. [orchestrator, from the user's own words] +Q: `invalid` mode in STEP 5C / A: challenge r1 — the lib and hooks still PUSH on an invalid value (fail-open until run D), so the handoff never claims "not pushed" from the mode alone: it reports the real `origin/develop..develop` count and names the value from the verb's stderr. [orchestrator, revised] +Q: challenge r1 — explicit `git push origin develop` in STEP 5C / A: removed. `finish` has pushed develop itself since BDR-095 (mode-aware since run A); a shell gate containing `git push` would be denied whole by push-guard in manual mode and `$mode` does not persist across Bash calls. 5C = finish, then two read-only facts (verb, ahead count), then prose. [orchestrator — internal] +Q: challenge r1 — scope / A: `lib/gitflow-aiguillage.md` (one line, "finish → develop + push") joins FILE SCOPE (5 files). [orchestrator — scope] +Q: `_gitflow_push_off` refactor onto the new verb / A: no — unchanged this run (run D owns every reader's fail-closed semantics). [orchestrator — internal] + +## ACCEPTANCE CRITERIA +1. `bash ~/.claude/lib/gitflow.sh push-mode` prints exactly one word on stdout: `manual` when `git config --bool gitflow.autopush` returns false, `auto` when it returns true or the key is unset (rc 1), `invalid` for any other rc (unparseable value, corrupt config, git failure) with the raw value named on stderr; rc 0 in all cases; the usage line lists the verb; the verb never writes config. Locked by the new T11b block. + CHECK: out=$(make test suite=lib/gitflow-test.sh 2>&1); printf '%s' "$out" | grep -q ' FAIL ' && exit 1; for t in "cli push-mode default auto" "cli push-mode true auto" "cli push-mode manual" "cli push-mode invalid, rc 0, value on stderr" "cli push-mode corrupt config" "cli usage lists push-mode"; do grep -qF "ok $t" <<<"$out" || exit 1; done; echo PUSH-MODE-OK + EXPECT: PUSH-MODE-OK + EVIDENCE: MET exit=0 marker-found :: PUSH-MODE-OK +2. `skills/capitalize/SKILL.md` STEP 5C contains NO `git push` text and no `git config` read: three separate calls (finish; `gitflow.sh push-mode`; `git rev-list --count origin/develop..develop`), a finish failure outcome (rc≠0 → kept, NOT merged, no "merged" wording), and outcomes keyed on the ahead count + mode (`develop pushed` / `manual push mode: not pushed, you: ! git push origin develop` / `push FAILED` / invalid value named from stderr with the real ahead count). STEP 6 reads the mode on every 5B-committed path and the `--no-push` closing line has a manual-mode variant ("this disk only, not pushed"); the recap carries the new values. The `--no-push` argument-hint in capitalize AND close says "in auto-push mode"; `lib/gitflow-aiguillage.md` no longer says "+ push" unconditionally. + CHECK: grep -q 'gitflow.sh" push-mode' skills/capitalize/SKILL.md && grep -q 'manual push mode: not pushed' skills/capitalize/SKILL.md && grep -q 'rev-list --count origin/develop..develop' skills/capitalize/SKILL.md && grep -q 'this disk only' skills/capitalize/SKILL.md && ! grep -q 'git config.*gitflow' skills/capitalize/SKILL.md skills/close/SKILL.md && grep -q 'auto-push mode' skills/capitalize/SKILL.md && grep -q 'auto-push mode' skills/close/SKILL.md && grep -q 'auto-push mode' lib/gitflow-aiguillage.md && echo CAPITALIZE-OK + EXPECT: CAPITALIZE-OK + EVIDENCE: MET exit=0 marker-found :: CAPITALIZE-OK +3. Doctrine citers census green (skill prose changed). + CHECK: make test suite=lib/tests/doctrine-citers.test.sh >/dev/null 2>&1 && echo CITERS-OK + EXPECT: CITERS-OK + EVIDENCE: MET exit=0 marker-found :: CITERS-OK +4. shellcheck clean on lib/gitflow.sh and lib/gitflow-test.sh; no `# shellcheck disable` added; floor guard clean. + CHECK: shellcheck lib/gitflow.sh lib/gitflow-test.sh && [ "$(git diff -- lib/gitflow.sh lib/gitflow-test.sh | grep -c '^+.*shellcheck disable')" -eq 0 ] && echo SHELLCHECK-OK + EXPECT: SHELLCHECK-OK + EVIDENCE: MET exit=0 marker-found :: SHELLCHECK-OK +5. Every hermetic suite green except the declared environmental red `lib/tests/design-tool-gate.test.sh`. + CHECK: fail=0; for t in $(ls lib/tests/*.test.sh lib/seo-data/*.test.sh lib/gitflow-test.sh lib/tests/run-*.sh | grep -v design-tool-gate.test.sh); do make test suite="$t" >/dev/null 2>&1 || { fail=1; echo "RED $t"; }; done; [ $fail -eq 0 ] && echo SUITES-OK + EXPECT: SUITES-OK + EVIDENCE: MET exit=0 marker-found :: SUITES-OK +6. No behaviour change elsewhere in lib/gitflow.sh (`_gitflow_push_off`, hooks emitters, start/finish/delete untouched; T18/T19/T22/T24 green — covered by criterion 1's FAIL grep); no edits outside FILE SCOPE; the verb never writes config. INVARIANT judged by reading (a negative grep would itself carry the denied text): no `git push` inside any Bash call in skills/capitalize/SKILL.md or skills/close/SKILL.md — the `! git push …` user hints are prose on single lines; every 5C outcome (invalid first, ahead 0, unknown, >0 manual, >0 auto; finish rc 1/4 vs 5/2/6) has a STEP 6 line and a recap value. + +## FILE SCOPE +lib/gitflow.sh · lib/gitflow-test.sh · skills/capitalize/SKILL.md · skills/close/SKILL.md · lib/gitflow-aiguillage.md diff --git a/.claude/tasks/contracts/2026-10-07-manual-push-skills-c2-1325.md b/.claude/tasks/contracts/2026-10-07-manual-push-skills-c2-1325.md new file mode 100644 index 0000000..ae34ee6 --- /dev/null +++ b/.claude/tasks/contracts/2026-10-07-manual-push-skills-c2-1325.md @@ -0,0 +1,43 @@ +# CONTRACT — manual-push-skills-c2 (run C2 of manual-push mode) +- date: 2026-10-07 | flow: feat | branch: feature/manual-push-mode (after C1: lib verb `gitflow.sh push-mode`) +- status: active + +## REQUEST (verbatim — IMMUTABLE) +User (fr): "ok enchaine sur le run C" +Run C as scoped in `.claude/tasks/TODO.md` "manual-push-mode": skills that push on their own, gate on the mode through the lib verb `bash ~/.claude/lib/gitflow.sh push-mode` (auto|manual|invalid): client-handover SKILL:48 + agents/client-handover-writer.md (STEP 5 push GO), release-candidate STEP 6 ("main and develop are already on origin", tag push), tour rule ("pushed by the gitflow hooks … fixed with a plain git push -u"). User's framing (2026-10-06): "tout faire pareil, juste rien push seul … seulement manuel". + +## CLARIFICATIONS +Q: scope / A: C2 = the four remaining sites + the one-line claim in agents/release-executor.md ("ride the lib's hook pushes"). 5 files. Prose only, no shell code change; the verb is read in its own Bash call and the decision is prose. [orchestrator — scope] +Q: invalid mode in these flows / A: push-guard denies Claude's push on an invalid value (fail closed, run B), while the lib/hooks still push on it (fail-open until run D): the skills treat `invalid` like `manual` for what CLAUDE does (no push attempt, the user runs the command), and name the value from the verb's stderr. [orchestrator] +Q: release in manual mode / A: no push at all by Claude: main, develop and the tag are left local; the skill prints ONE user command `! git push origin main develop v` and stops (no AskUserQuestion, nothing to gate). The `hold` wording for auto mode stays. [orchestrator — visible wording derived from the user's rule] +Q: challenge r1 — truth source / A: every "on origin" / "not pushed" statement in these flows comes from `git rev-list --count origin/
..
` (or `
--not --remotes` for the tour), read in its own Bash call; the verb only words the reason. Invalid: the verb's stderr is quoted verbatim, never a templated value. [orchestrator] +Q: challenge r1 — client-handover push GO question / A: removed (it gated nothing: the hooks had already pushed in auto mode; push-guard denies it in manual mode). The pipeline never runs `git push`; the user is told to push BEFORE the deploy pause, and the deploy brief says "after your push". `Push:` line added to both end reports. [orchestrator — visible wording derived from the user's rule] +Q: challenge r1 — release command / A: `! git push --atomic origin main develop v`; also used in auto mode when a lib push did not reach origin. Tag-push gate kept for auto mode with both counts 0. `hold` wording notes `--follow-tags` publishes the held tag on the next push of main. [orchestrator] +Q: confirmation r2 / A: PUSH STATE READ is one reusable paragraph, re-run at the top of STEP 6, after "Deployed", and right before every `Push:` line (states: on origin / nothing to push / uncommitted (gitflow fallback) / not on origin, no origin remote / pending + reason); the red-flag box is kept and reworded, not deleted; anything other than `auto` from the verb is treated like manual; the tour uses the branch name `gitflow start` returned (suffix-aware) and reads the fact after the report commit; multi-line Edit anchors given to the executor. [orchestrator] +Q: tour `push FAILED` residual / A: in every mode the USER fixes it (BDR-095: a rejected push warns, the user decides); the rule no longer reads as Claude retrying. [orchestrator] + +## ACCEPTANCE CRITERIA +1. agents/client-handover-writer.md: the GO question and the push block are gone; a reusable PUSH STATE READ (branch, origin check, `git rev-list --count origin/
..
`, the verb when ahead ≠ 0) is defined in STEP 5 and re-run at the top of STEP 6, after "Deployed", and before every `Push:` line; `pending` tells the user `! git push -u origin
` BEFORE STEP 6 and the deploy brief opens with it and says "after your push"; `Push:` line (column 0) in the PIPELINE STOPPED template and a `- Push:` bullet in the 9.7 report; the red-flag box is kept and says the pipeline never pushes. No `git config` read. + CHECK: grep -q 'gitflow.sh" push-mode' agents/client-handover-writer.md && grep -q 'rev-list --count origin/
..
' agents/client-handover-writer.md && grep -q 'First push:' agents/client-handover-writer.md && [ "$(grep -c '^Push: ' agents/client-handover-writer.md)" -ge 1 ] && grep -q 'after your push' agents/client-handover-writer.md && grep -q 'PUSH STATE READ' agents/client-handover-writer.md && grep -q 'Red flag' agents/client-handover-writer.md && ! grep -q 'git config.*gitflow' agents/client-handover-writer.md && ! grep -q 'Push to origin now' agents/client-handover-writer.md && echo HANDOVER-AGENT-OK + EXPECT: HANDOVER-AGENT-OK + EVIDENCE: MET exit=0 marker-found :: HANDOVER-AGENT-OK +2. skills/client-handover/SKILL.md step 4 says the hooks push in auto-push mode and that otherwise the agent tells the user to push BEFORE the deploy pause. + CHECK: grep -q 'auto-push mode' skills/client-handover/SKILL.md && grep -q 'manual push mode' skills/client-handover/SKILL.md && grep -q 'BEFORE the deploy pause' skills/client-handover/SKILL.md && echo HANDOVER-SKILL-OK + EXPECT: HANDOVER-SKILL-OK + EVIDENCE: MET exit=0 marker-found :: HANDOVER-SKILL-OK +3. skills/release-candidate/SKILL.md STEP 6: reads two ahead counts + the verb (separate calls); manual/invalid or any count ≠ 0 → prints `! git push --atomic origin main develop v` and stops (no question; invalid quotes the verb's stderr); auto with both counts 0 → the existing tag-push gate; `hold` notes `--follow-tags`. Overview and common-mistakes qualified. agents/release-executor.md: both push claims (step 2 and the forbidden-span note) say "in auto-push mode". + CHECK: grep -q 'gitflow.sh" push-mode' skills/release-candidate/SKILL.md && grep -q -- '--atomic origin main develop v' skills/release-candidate/SKILL.md && grep -q 'rev-list --count origin/main..main' skills/release-candidate/SKILL.md && grep -q 'follow-tags' skills/release-candidate/SKILL.md && [ "$(grep -c 'auto-push mode' agents/release-executor.md)" -ge 2 ] && echo RELEASE-OK + EXPECT: RELEASE-OK + EVIDENCE: MET exit=0 marker-found :: RELEASE-OK +4. skills/tour/SKILL.md: the rule is mode-agnostic (hooks push in auto-push mode; otherwise the USER pushes with `! git -C push -u origin `; the tour never pushes or retries); STEP 3 item 5 reads one `git -C rev-list --count --not --remotes` fact per project after the report commit, with `` = the name gitflow start returned (suffix-aware); the summary row carries `on origin` / `local only → …`. + CHECK: grep -q 'auto-push mode' skills/tour/SKILL.md && grep -q 'manual push mode' skills/tour/SKILL.md && grep -q 'git -C push -u origin' skills/tour/SKILL.md && grep -q 'local only' skills/tour/SKILL.md && grep -q -- '--not --remotes' skills/tour/SKILL.md && echo TOUR-OK + EXPECT: TOUR-OK + EVIDENCE: MET exit=0 marker-found :: TOUR-OK +5. Doctrine citers census green; floor guard clean; every hermetic suite green except the declared environmental red. + CHECK: make test suite=lib/tests/doctrine-citers.test.sh >/dev/null 2>&1 && bash ~/.claude/lib/floor-guard.sh develop -- skills/client-handover/SKILL.md agents/client-handover-writer.md skills/release-candidate/SKILL.md skills/tour/SKILL.md agents/release-executor.md >/dev/null 2>&1 && fail=0 && for t in $(ls lib/tests/*.test.sh lib/seo-data/*.test.sh lib/gitflow-test.sh lib/tests/run-*.sh | grep -v design-tool-gate.test.sh); do make test suite="$t" >/dev/null 2>&1 || fail=1; done && [ $fail -eq 0 ] && echo SUITES-OK + EXPECT: SUITES-OK + EVIDENCE: MET exit=0 marker-found :: SUITES-OK +6. Judged by reading: the only `git push` left inside a Bash block in the five files is release-candidate's tag push, reached only in auto mode with both counts 0 on explicit go; every other push is a `! git …` user hint in prose (complete: `-u`, `--atomic`, `-C ` where needed); no "on origin" / "not pushed" claim derives from the mode word alone; no file outside FILE SCOPE changes; frontmatter, agent pins and headings unchanged (release-candidate description + STEP 6 heading accepted residuals). + +## FILE SCOPE +skills/client-handover/SKILL.md · agents/client-handover-writer.md · skills/release-candidate/SKILL.md · agents/release-executor.md · skills/tour/SKILL.md diff --git a/.claude/tasks/plans/2026-10-07-manual-push-skills-c1-1304.md b/.claude/tasks/plans/2026-10-07-manual-push-skills-c1-1304.md new file mode 100644 index 0000000..d2f0d7d --- /dev/null +++ b/.claude/tasks/plans/2026-10-07-manual-push-skills-c1-1304.md @@ -0,0 +1,79 @@ +# PLAN — manual-push-skills-c1 — REVISED r2 (3 lenses + correctness confirmation) +Contract: .claude/tasks/contracts/2026-10-07-manual-push-skills-c1-1304.md + +## Context +`/close` STEP 5C runs `gitflow.sh finish chore ` then `git push origin develop`. Since BDR-095 (9da5d8d, 2026-09-22) `finish` already pushes develop itself (`_gitflow_merge_into` → `_gitflow_push_branch`, mode-aware since run A), so the explicit push has been redundant for two weeks; in manual mode push-guard (run B) would deny it, and a shell gate `[ "$mode" = auto ] && git push …` is denied as a whole by the text-only guard while `$mode` does not survive between Bash calls. Fix = remove the push text entirely and REPORT from facts read after finish. Skills can no longer read `gitflow.autopush` via `git config` (BDR-112) → the lib verb `push-mode` is the sanctioned reader. Invalid value: lib/hooks still push (fail-open until run D), so the wording must not claim "not pushed" — the ahead count tells the truth. + +## Checklist +- [ ] lib/gitflow.sh — `gitflow_push_mode()` in the predicates section (after `gitflow_release_open`): + ``` + # gitflow_push_mode → stdout auto | manual | invalid, rc 0 always. The ONE + # reader skills may call: `git config … gitflow.*` is statically denied to + # Claude (BDR-112). manual = key reads false; auto = true or unset; invalid = + # anything else (unparseable value, git failure) — the raw value goes to + # stderr so the caller can name it. Reads only. Ignores GITFLOW_NO_PUSH (a + # test-repo switch, not a mode): a caller that pushes must not rely on this + # verb alone — the lib's own push sites use _gitflow_push_off. + gitflow_push_mode() { + local val rc raw + val=$(git config --bool gitflow.autopush 2>/dev/null); rc=$? + case "$rc:$val" in + 0:false) echo manual ;; + 0:true|1:*) echo auto ;; + *) raw=$(git config gitflow.autopush 2>/dev/null) + if [ -n "$raw" ]; then + echo "gitflow.sh push-mode: gitflow.autopush='$raw' is not a boolean (git rc $rc)" >&2 + else + echo "gitflow.sh push-mode: could not read gitflow.autopush (git rc $rc)" >&2 + fi + echo invalid ;; + esac + return 0 + } + ``` + CLI dispatcher: `push-mode) gitflow_push_mode ;;` after `merged`; add `push-mode` to the usage string. `_gitflow_push_off` UNCHANGED (run D). +- [ ] lib/gitflow-test.sh — NEW block after T11, own repo (hooks on from init, irrelevant: config reads/writes only): `echo "T11b — push-mode verb (the sanctioned reader for skills, BDR-112)"`; `newrepo pm; echo a>a; bash "$HERE/gitflow.sh" init >/dev/null 2>&1`; + `chk "cli push-mode default auto" '[ "$(bash "$HERE/gitflow.sh" push-mode)" = auto ]'`; + `git config gitflow.autopush true` → `chk "cli push-mode true auto" …= auto`; + `git config gitflow.autopush false` → `chk "cli push-mode manual" …= manual`; + `git config gitflow.autopush flase` → `pm_out=$(bash "$HERE/gitflow.sh" push-mode 2>"$WORK/pm.err"); pm_rc=$?` (same line) → `chk "cli push-mode invalid, rc 0, value on stderr" "[ $pm_rc -eq 0 ] && [ \"$pm_out\" = invalid ] && grep -q flase \"$WORK/pm.err\""`; + corrupt config: `printf '[gitflow\n' >> .git/config` → `pm2_out=$(bash "$HERE/gitflow.sh" push-mode 2>/dev/null); pm2_rc=$?` → `chk "cli push-mode corrupt config → invalid, rc 0" "[ $pm2_rc -eq 0 ] && [ \"$pm2_out\" = invalid ]"`; + `chk "cli usage lists push-mode" 'grep -q push-mode <<<"$(bash "$HERE/gitflow.sh" nope 2>&1)"'`. + Variables read in double-quoted assertions (no SC2034 suppression). Config writes live in the test FILE only. +- [ ] skills/capitalize/SKILL.md — STEP 5C (heading UNCHANGED; repo-wide grep shows no citer; the citers census does not cover skill headings). Body rewrite below the three fire-conditions: + "Skip this step entirely (go to STEP 6, which prints the hold note) on `--no-push`, on a WORKING branch, or when STEP 5B returned rc 3. + Otherwise, from the `chore/` branch, THREE separate Bash calls, never combined. INVARIANT: no `git push` inside any Bash call of this skill (push-guard reads command text; the lib pushes develop itself in auto-push mode). The hints that tell the USER what to type (`! git push …`) are prose, kept on single lines. + 1. `bash "$HOME/.claude/lib/gitflow.sh" finish chore ` — merge → develop, delete branch, push develop in auto-push mode. rc≠0 → skip calls 2-3, go to STEP 6 with the `finish failed` line: rc 4 = conflict, develop mid-merge, `chore/` kept, NOT merged; rc 1 = checkout failed, NOT merged; rc 5/2/6 come from the delete AFTER the merge: check `git merge-base --is-ancestor chore/ develop` and report `merged, branch not deleted (rc )` when it holds, `NOT merged` otherwise. Never say "merged" without that check. + 2. `bash "$HOME/.claude/lib/gitflow.sh" push-mode` → `auto | manual | invalid` (stderr names an invalid value). + 3. `git rev-list --count origin/develop..develop 2>/dev/null || echo unknown` → `ahead` (0 = on origin; `unknown` = no origin/develop ref, e.g. no origin remote). + Outcomes, evaluated IN THIS ORDER (all require finish rc 0): + - push mode `invalid` → `merged to develop — gitflow.autopush= is not a boolean: the lib and hooks still push on an invalid value until run D (origin/develop is commit(s) behind, or unknown); fix the value by hand`. + - `ahead` = 0 → `develop pushed` (auto-push mode did it). + - `ahead` = unknown → `merged to develop — not on origin (no origin/develop ref; no remote or never fetched)`; push mode manual → add `You: ! git push origin develop once a remote exists`. + - `ahead` > 0, push mode `manual` → `merged to develop — manual push mode: not pushed. You: ! git push origin develop`. + - `ahead` > 0, push mode `auto` → `merged to develop — push FAILED (see finish stderr); push manually`. Do NOT retry or reset the merge." + Keep the three existing bullets' intent inside the list above (the first qualified as auto-push mode). Recap line ~358 `persisted :` values → `develop pushed | merged, manual push mode: not pushed | merged, not on origin (no origin/develop) | merged, push FAILED | merged, gitflow.autopush invalid ( behind) | finish rc , not merged | merged, branch not deleted (rc ) | on chore/, not merged (--no-push)`. + STEP 6 (lines ~366-368): `` stays the session label (`Context flushed` / `Session closed`); the conditions below say "push mode". On the `--no-push` path (and on any 5B-committed path where 5C did not run) read TWO facts, each its own call: `bash "$HOME/.claude/lib/gitflow.sh" push-mode` and `git rev-list --count origin/chore/..chore/ 2>/dev/null || echo unknown` (`branch_ahead`). Lines (single-line bullets, as the existing ones): + - auto-persisted (ahead 0) — unchanged. + - `--no-push`, `branch_ahead` = 0 → `✅ + committed on chore/ — pushed to origin by the hooks (auto-push mode), NOT merged (--no-push). Merge when ready.` + - `--no-push`, `branch_ahead` > 0 or unknown → `✅ + committed on chore/ — this disk only, not pushed (), NOT merged. You: ! git push -u origin chore/; merge when ready.` With push mode `invalid`, append ` gitflow.autopush= is not a boolean: fix it by hand`. + - manual (merged, `ahead` > 0) → `✅ + merged to develop — manual push mode: not pushed. You: ! git push origin develop`. + - not on origin (merged, `ahead` unknown) → `✅ + merged to develop — not on origin (no origin/develop ref).` + - invalid (merged) → `⚠️ + merged to develop — gitflow.autopush= is not a boolean; lib/hooks still push on it until run D (origin/develop behind). Fix the value by hand.` + - push failed — unchanged. + - finish failed → `⚠️ + finish rc : — chore/ kept, NOT merged (or: merged, branch not deleted); resolve by hand.` + argument-hint (line 13): `pushed to origin by the hooks` → `pushed to origin by the hooks in auto-push mode`. Rules line ~403: append ` — the lib pushes develop in auto-push mode only; manual mode merges and leaves the push to the user`. +- [ ] skills/close/SKILL.md — argument-hint (line 12): same `in auto-push mode` wording; line 31 `STEP 5C auto-persist: finish + push, BDR-068` → `STEP 5C auto-persist: finish (push rides it in auto-push mode), BDR-068`. +- [ ] lib/gitflow-aiguillage.md — lines 40-42: `(finish → develop + push)` → `(finish → develop; the lib pushes develop in auto-push mode only)`. One line. + +## Edge cases +- INVARIANT: no `git push` inside any Bash CALL of capitalize/close (the user-facing `! git push …` hints are prose on single lines) → push-guard never fires on /close. The verifier judges it by reading; a negative grep would itself carry `git push` and be denied in manual mode (LRN-194 b). +- `origin/develop` ref absent (no origin, never fetched) → `unknown` → its own outcome ("not on origin"), never "push FAILED" (in auto mode without origin the lib is silently a no-op, lib/gitflow.sh:90). +- Invalid value: truth comes from `ahead`, not from the mode; wording never says "not pushed" without `ahead` > 0. +- The verb ignores GITFLOW_NO_PUSH by design (documented in its comment); 5C never runs in a test repo; C2 callers that push must gate on the verb AND respect push-guard (they will not contain `git push` text in manual mode anyway). +- Heading kept → no citer risk; BDR-100 census does not apply to skill headings (manual repo-wide grep done: none). + +## Disposition +- honors BDR-068 (auto-persist: merge always, push rides finish in auto mode) and BDR-111/BDR-112 (verb = sanctioned reader; zero `git config` in skills; zero `git push` inside Bash calls). +- honors BDR-095 (truth from the remote state, never from intent: `ahead` count) and LRN-104 (every new output string lives in the skill text; the verb's outputs locked in T11b incl. stderr and rc). +- honors LRN-191 (`grep -q … <<<"$(…)"`), LRN-194 (fixtures in files), LRN-193 (fresh confirmation pass after this revision). diff --git a/.claude/tasks/plans/2026-10-07-manual-push-skills-c2-1325.md b/.claude/tasks/plans/2026-10-07-manual-push-skills-c2-1325.md new file mode 100644 index 0000000..856e976 --- /dev/null +++ b/.claude/tasks/plans/2026-10-07-manual-push-skills-c2-1325.md @@ -0,0 +1,33 @@ +# PLAN — manual-push-skills-c2 — REVISED r2 (3 lenses + correctness confirmation) +Contract: .claude/tasks/contracts/2026-10-07-manual-push-skills-c2-1325.md + +## Context +Same pattern as C1: since BDR-095 the hooks push every commit in auto-push mode, so a skill's own `git push` (and the question that gates it) gates nothing in auto mode, and in manual/invalid mode push-guard denies it. Truth about "on origin" comes from a FACT read after the fact — `git rev-list --count origin/
..
` (0 = on origin; >0 = not; `unknown` = no remote-tracking ref) — never from the mode word (the lib still pushes on an invalid value until run D). The verb `gitflow.sh push-mode` (C1) only WORDS the explanation (manual vs push FAILED) and is read in its own Bash call; no shell variable crosses calls. Where a user must push, the hint is a complete `! git …` command with `-u` and, for multi-repo flows, `-C `. + +## Checklist +- [ ] agents/client-handover-writer.md — define ONE reusable paragraph "PUSH STATE READ" (insert it once, right after the commit-change dispatch in STEP 5, and REFER to it elsewhere): "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`. 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: `ahead` = 0 → `on origin`; 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`; `no-origin` → `not on origin (no origin remote: add one first)`; `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." Then: + STEP 5: replace ONLY lines ~570-578 (from "Then, **before pushing, STOP and ask for an explicit GO**" through the "Only on **A** … then continue." paragraph) with the PUSH STATE READ paragraph followed by: "`pending` → tell the user NOW: `Commits are local only. Push first: ! git push -u origin
`." KEEP the red-flag box (~580-582) and reword it (multi-line old_string, exact current text: `> **Red flag — STOP:** never \`git push\` without option-A GO; never\n> \`gitflow finish\`/\`merge\`. This pipeline commits and (on GO) pushes a working\n> branch — it never integrates into a protected branch.`) → `> **Red flag — STOP:** never \`git push\` (the hooks push in auto-push mode;\n> otherwise the user does); never \`gitflow finish\`/\`merge\`. This pipeline\n> commits a working branch — it never integrates into a protected branch.` Then DELETE lines ~584-598 (the `CURRENT_BRANCH=…/git push origin` bash block and the "If push fails …" AskUserQuestion block). + STEP 6: FIRST line of STEP 6 (before "Skip if PROJECT_TYPE != web"): "Re-run PUSH STATE READ (every path reaches STEP 6, some without STEP 5's read)." Deploy brief (lines ~626-631, multi-line anchors: `"Push has been\n done. The platform deploys automatically — usually 1-3 min. Watch the\n dashboard.`): when the state is `pending` the brief OPENS with `First push: ! git push -u origin
`; the Vercel/Netlify/Cloudflare line reads "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…"; the CI line "Workflow `` runs on your push…"; when `on origin`, keep "Push has been done. …". After option A "Deployed" (~648): "Re-run PUSH STATE READ; still `pending` → ask again (the live site cannot hold these commits)." + Reports: PIPELINE STOPPED template (~795-810) gains a line at column 0 `Push: ` after the Score table; the 9.7 user report gains a bullet `- Push: `; both re-run PUSH STATE READ right before printing (never a STEP 5 snapshot). Line ~65 `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).`; lines ~686-687 `(mini-commit\n+ push)` → `(mini-commit; push state read, never assumed)`. +- [ ] skills/client-handover/SKILL.md step 4 — `run /commit-change (atomic logical commits) then \`git push\`.` → `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.` +- [ ] skills/release-candidate/SKILL.md STEP 6 — replace the paragraph from "`main` and `develop` are already on origin" through the `hold` line (lines ~96-108) with: + "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`. Go → ```bash\ngit push origin v\n```. `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)." + Overview lines ~27-28 (multi-line anchor `and the two human gates (when to release, and\nthe tag push).`) → append " (auto-push mode; in manual push mode the user pushes main, develop and the tag in one command)". Common-mistakes bullet list: add `- Pushing anything in manual push mode → print the one user command, push nothing.` Frontmatter description ("tag it, and push") and the STEP 6 heading stay (frozen, residuals). +- [ ] agents/release-executor.md — lines ~80-82 (multi-line anchor: `Finish has already pushed \`main\` and\n \`develop\` through the lib's hooks (BDR-095); the tag stays local until\n the dispatcher's tag-push gate.`) → "In auto-push mode finish has already pushed `main` and `develop` through the lib (BDR-095); in manual push mode they stay local. The tag stays local"; lines ~85-86 (anchor `\`main\`/\`develop\` ride the lib's hook\npushes during finish;`) → "`main`/`develop` ride the lib's pushes during finish in auto-push mode". +- [ ] skills/tour/SKILL.md — Rules (lines ~273-275, multi-line anchor: ` The chore branch's own commits are pushed by the gitflow hooks\n (BDR-095); a \`push FAILED\` hook warning is a report residual, fixed\n 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." STEP 3 per-project closing list (~228-239): add item 5 AFTER the `docs(tour): report` commit (3.3, the last commit): "5. Push state, one read-only call: `git -C rev-list --count --not --remotes 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 ` (no origin remote → `local only (no origin remote)`)." Summary row format (~258-259): after `, commits` append ` | on origin` or ` | local only → ! git -C push -u origin `. Runner prompt (~92-100) unchanged: the row format carries the field and the runner already returns `BRANCH: `. No verb read in the tour. + +## Edge cases +- `unknown` (never fetched) → "not on origin (no remote-tracking ref)"; no origin remote → "add an origin remote first" (the `! git push … origin …` hint would fail); never "push FAILED". `ahead` = 0 → "on origin" with no claim about WHO pushed (in manual mode it was the user). +- Every `Push:`/deploy-brief statement re-reads the fact right before it prints (a STEP 5 snapshot is stale once the user pushed); STEP 6 reads it first because three paths reach STEP 6 without STEP 5's read (no pending changes; gitflow fallback; `--skip-audits`). +- AC substrings must each sit on ONE physical line (line-based greps); `Push:` at column 0 inside the PIPELINE STOPPED fence; `auto-push mode` on two distinct lines in release-executor.md. +- Invalid value: never "not pushed" from the mode; the counts decide; the verb's stderr is quoted verbatim (it may say "could not read" without a value). +- Release command is `--atomic`: a non-fast-forward on main rejects the whole set, so the tag never lands without its merge. +- client-handover-writer runs INLINE in the main loop (SKILL.md:29-33): the prose reaches the pusher. commit-change and handover-doc-writer never push. +- Tour runners are sub-agents using `git -C `: the fact call uses `-C` too; the user hint carries the path (same branch name across projects). +- Removed gates (client-handover GO question, release "on origin" claim) were gating nothing in auto mode: the hooks had pushed already (same redundancy C1 removed in /close). LRN-069's push gate now means: Claude never pushes in these flows except the release tag on explicit go in auto mode. +- Residual (frozen by AC6): release-candidate frontmatter "tag it, and push", STEP 6 heading "Tag push GATE (ASK)" — true in auto mode; listed in the CHANGELOG at doc-sync. + +## Disposition +- honors BDR-095 (truth from the remote state; a failed push is the user's decision), BDR-111/BDR-112 (verb for wording only, read in its own call; zero `git config` in skills; zero `git push` inside a Bash call reachable in manual mode), BDR-042 (tag + its gate stay in the dispatcher), LRN-069 (explicit go kept for the one push Claude still makes: the tag, auto mode), LRN-193 (fresh confirmation pass after this revision), LRN-104 (every user-facing string is in the skill text; no runtime test exists for prose — AC6 is the reading gate).