10 KiB
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 <name> 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 (aftergitflow_release_open):CLI dispatcher:# 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 }push-mode) gitflow_push_mode ;;aftermerged; addpush-modeto the usage string._gitflow_push_offUNCHANGED (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 thechore/<name>branch, THREE separate Bash calls, never combined. INVARIANT: nogit pushinside 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.bash "$HOME/.claude/lib/gitflow.sh" finish chore <name>— merge → develop, delete branch, push develop in auto-push mode. rc≠0 → skip calls 2-3, go to STEP 6 with thefinish failedline: rc 4 = conflict, develop mid-merge,chore/<name>kept, NOT merged; rc 1 = checkout failed, NOT merged; rc 5/2/6 come from the delete AFTER the merge: checkgit merge-base --is-ancestor chore/<name> developand reportmerged, branch not deleted (rc <n>)when it holds,NOT mergedotherwise. Never say "merged" without that check.bash "$HOME/.claude/lib/gitflow.sh" push-mode→auto | manual | invalid(stderr names an invalid value).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=<value from stderr> is not a boolean: the lib and hooks still push on an invalid value until run D (origin/develop is <ahead> commit(s) behind, or unknown); fix the value by hand. ahead= 0 →develop <short> 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 → addYou: ! git push origin develop once a remote exists.ahead> 0, push modemanual→merged to develop — manual push mode: not pushed. You: ! git push origin develop.ahead> 0, push modeauto→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 ~358persisted :values →develop <short> pushed | merged, manual push mode: not pushed | merged, not on origin (no origin/develop) | merged, push FAILED | merged, gitflow.autopush invalid (<ahead> behind) | finish rc <n>, not merged | merged, branch not deleted (rc <n>) | on chore/<name>, not merged (--no-push). STEP 6 (lines ~366-368):<mode>stays the session label (Context flushed/Session closed); the conditions below say "push mode". On the--no-pushpath (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-modeandgit rev-list --count origin/chore/<name>..chore/<name> 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 →✅ <mode> + committed on chore/<name> — pushed to origin by the hooks (auto-push mode), NOT merged (--no-push). Merge when ready.--no-push,branch_ahead> 0 or unknown →✅ <mode> + committed on chore/<name> — this disk only, not pushed (<push mode manual | no origin/chore ref>), NOT merged. You: ! git push -u origin chore/<name>; merge when ready.With push modeinvalid, appendgitflow.autopush=<value> is not a boolean: fix it by hand.- manual (merged,
ahead> 0) →✅ <mode> + merged to develop — manual push mode: not pushed. You: ! git push origin develop. - not on origin (merged,
aheadunknown) →✅ <mode> + merged to develop — not on origin (no origin/develop ref). - invalid (merged) →
⚠️ <mode> + merged to develop — gitflow.autopush=<value> is not a boolean; lib/hooks still push on it until run D (origin/develop <ahead> behind). Fix the value by hand. - push failed — unchanged.
- finish failed →
⚠️ <mode> + finish rc <n>: <stderr> — chore/<name> 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 modewording; line 31STEP 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 pushinside 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 carrygit pushand be denied in manual mode (LRN-194 b). origin/developref 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" withoutahead> 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 pushtext 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 configin skills; zerogit pushinside Bash calls). - honors BDR-095 (truth from the remote state, never from intent:
aheadcount) 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).