diff --git a/.claude/memory/blockers.md b/.claude/memory/blockers.md index 3d04ef0..761c3e9 100644 --- a/.claude/memory/blockers.md +++ b/.claude/memory/blockers.md @@ -34,6 +34,7 @@ rules: | BLK-012 | 2026-06-29 | gitflow_init half-applied: socle-commit failure swallowed → hook activated on partial run → re-run self-blocks | resolved | | BLK-013 | 2026-06-30 | `make plugin` Error 127 — npm absent on apt-`nodejs` host (Step 4 gsd-pi aborts, Steps 5-10 + residual cleanup never run) | resolved (env) | | BLK-014 | 2026-07-01 | `make install` aborts npm EEXIST on `~/.local/bin/claude` when claude already installed via native installer — no presence guard | resolved | +| BLK-015 | 2026-07-03 | `gitflow_finish` ignored its ` ` args → merged the CHECKED-OUT branch not the one named → wrong-branch merge (audit LOT3) | resolved | --- @@ -179,3 +180,12 @@ rules: - **Status**: resolved. Fix `8dc4027`, branch `bugfix/install-claude-idempotent`, pending merge validation. - **Reference**: [[BLK-013]] npm prefix `~/.local` = contributing factor (npm bin over native bin). install-plugins.sh already pointed to code.claude.com (native) — install.sh was the npm outlier. Fresh-machine `elif npm` branch channel-consistency = open design question (potential BDR). Pattern → [[LRN-085]]. - **Update 2026-07-01**: MERGED `2393ca5` (bugfix/install-claude-idempotent → develop), pushed — supersedes "pending merge validation". The open channel-consistency question is RESOLVED by [[BDR-046]] (fresh install → native installer, npm dropped for claude); install.sh has no `elif npm` branch → nothing left to trancher. + +## BLK-015 — `gitflow_finish` ignored its args, merged the CURRENT branch not the one asked + +- **Date**: 2026-07-03 +- **Friction**: audit 2026-07-02 — `gitflow.sh finish bugfix audit-bugs` run while checked out on `feature/audit-tokens` merged audit-tokens (LOT3), NOT audit-bugs. Final develop state identical (disjoint hunks) so no data damage, but the merge order was silently wrong. UX trap: the command LOOKS like it targets `bugfix/audit-bugs`. +- **Real cause**: CLI dispatch (`lib/gitflow.sh:257` `finish) gitflow_finish "$@"`) forwards args, but the function derived its source from `HEAD` (`git symbolic-ref`) and NEVER read `$1/$2` → the ` ` were silently dropped. Merge source = ambient state (checked-out branch), not the named target. Design intended finish to always operate on HEAD (human gate = "be on the branch"), but nothing enforced that passed args, if any, MATCH the branch you're on. +- **Solution**: `gitflow_finish [ ]` — args now an optional safety ASSERTION: present AND `"$req_type/$req_name" != "$br"` → error `operates on the current branch 'X', but you asked 'Y' — checkout 'Y' first`, rc 2. No args = behavior unchanged (only real caller `skills/gitflow/SKILL.md:36` + every test pass none → zero regression). +7 regression assertions (`gitflow-test.sh` T12, numbered to dodge collision with reconcile's own T6c). +- **Status**: resolved. Commit `d9fdd4c`, branch `bugfix/gitflow-finish-args`. +- **Reference**: journal 2026-07-02 (trap noted, not fixed) → fixed 2026-07-03. Pattern → [[LRN-089]] (pass-through wrapper deriving target from ambient state = silent contract violation). diff --git a/.claude/memory/journal.md b/.claude/memory/journal.md index 49abc38..895c878 100644 --- a/.claude/memory/journal.md +++ b/.claude/memory/journal.md @@ -302,3 +302,9 @@ rules: - #11 rtk auto-allow DROPPED — permission control back in settings.json (rtk registry was a parallel authority bypassing deny/ask). #10 rules/context7.md deleted (−493 tok; find-docs survives, stable — regen keyed on its absence); faulty examples → upstream issue draft (upstash/context7, gh unauthenticated). plugin-dev uninstalled + dropped from installer. - Incidents: magic API key printed into transcript from ~/.claude.json → rotated, [[BDR-026]] update (copies of secrets); gitflow_finish ignores its args (operates on CURRENT branch, lib/gitflow.sh:104) → LOT 3 merged first by mistake, final develop state identical (disjoint hunks) — UX trap noted, not fixed. - Residuals (flagged, not built): doctor "Cargo not found (RTK unavailable)" parenthesis now misleading; doctor symlink-check false-warns on dir-level symlinks; doctor token constants stale; find-docs faulty examples ctx7-owned. + +## 2026-07-03 +- bugfix/gitflow-finish-args: `gitflow_finish` contract fix — args now optional safety ASSERTION (present + ≠ current branch → refuse rc2 "operates on current branch X, you asked Y — checkout Y first"); no-args unchanged (only real caller SKILL.md:36 + all tests pass none → zero regression). +7 T12 assertions. [[BLK-015]], [[LRN-089]]. Off-by-one caught at capitalize: next free BLK = 015 not 016 (gate proposal said 016) → gitflow.sh comment corrected pre-finish via soft-reset+redo of the 3 commits. +- Same branch, 3 doctor false-warns fixed ([[LRN-047]] corrob — a doctor that cries false is ignored): cargo "(RTK unavailable)" → optional info (RTK prebuilt, detect_rtk); check_symlink passes children of dir-level symlinks (hooks/session-start.sh); gstack counts 34 per-skill symlinks not a mythical skills/gstack link (link.sh removes it); token budget vs 200k context window not bogus 11k "session budget" → killed false "92% CRITICAL" (measured ~11.4k [[LRN-088]]; 200k confirmed by user — 1M pin revoked at audit #7, calibrate on default not the exceptional session). +- Suites green: gitflow 71/71 (+7), deterministic 13, doc-commit 32, doc-shape 19, reconcile 20, deploy-commit 13, release-candidate 5/5 tag-mode. doctor: 0 false-warn (1 legit survivor = gstack tracks branch=main advisory). shellcheck clean. T12 named to dodge collision with reconcile's own T6c (darwin path, audit #3). +- 3 atomic commits (fix gitflow / fix doctor / docs changelog Unreleased) + memory. finish bugfix→develop on GO; user pushes develop. diff --git a/.claude/memory/learnings.md b/.claude/memory/learnings.md index fe97b0d..16e7585 100644 --- a/.claude/memory/learnings.md +++ b/.claude/memory/learnings.md @@ -108,6 +108,7 @@ rules: | LRN-086 | 2026-07-02 | External-tool-generated skill: prove provenance by mtime (not repo grep), gitignore + regen via install-step; guard regen on ABSENCE when the tool co-writes a user-editable config | any untracked skill/dir a tool (ctx7, etc.) drops into the repo | | LRN-087 | 2026-07-02 | presence-flag ≠ capability — rtk silently dead after .bashrc wipe; emitted commands need ABSOLUTE bin paths (they run in another shell); integrity pin = live machinery, re-pin on hook edit | any PATH-dependent capability + hand-managed shell profile; hooks emitting commands for another shell | | LRN-088 | 2026-07-02 | token-cutting intuition inverts under measurement — verbosity beats cardinality (gstack 34 skills ≈ 592 tok vs pr-review 6 agents ≈ 2,183) | any "disable X to save tokens" — measure per-item bytes first; profiles toggle skills, not plugin payloads | +| LRN-089 | 2026-07-03 | pass-through wrapper (CLI `"$@"` → fn deriving target from ambient state: HEAD/cwd/env) silently ignores its args = silent contract violation; guard = args are an ASSERTION, refuse when they disagree with state | any dispatcher forwarding args to a callee that reads ambient state instead of the args | --- @@ -956,3 +957,11 @@ rules: - **actions taken**: pr-review-toolkit OFF by default (−2,183; audit.profile keeps it = reactivation channel), 10 fattest personal descriptions compressed 6,416→4,243 chars (−~540), context7 rule dropped for the find-docs skill (−493; skill body loads on-demand, stable — regen keyed on find-docs absence). Total ≈ −3.2k/session ≈ −22%. - **future application**: before any "disable X to save tokens" → measure per-item bytes FIRST (frontmatter extraction, plugin cache); expect the fat where descriptions are hand-written rich, not where items are many. Profiles toggle SKILLS only — plugin payloads (agents/skills in cache) need `enabledPlugins`. [[LRN-080]] measure-first corroborated on a new axis (cost, not behavior). - **Reference**: audit 2026-07-02 measurement + branch feature/audit-tokens. See [[BDR-014]], [[LRN-043]]. + +## LRN-089 — a pass-through wrapper whose callee reads ambient state silently ignores its args + +- **Date**: 2026-07-03 +- **pattern**: a CLI/dispatcher that forwards `"$@"` to a function which derives its TARGET from ambient state (HEAD, cwd, env, "current X") rather than from those args → the args are silently dropped. The call SITE looks parameterized (`finish bugfix audit-bugs`) but the callee acts on whatever state it's standing in → wrong-target action, NO error. `gitflow_finish` read `HEAD`, never `$1/$2`; `finish bugfix X` from another branch merged that other branch. +- **context**: audit 2026-07-02, `lib/gitflow.sh:257` `finish) gitflow_finish "$@"` passed args the function never consulted. Surfaced when a finish "for" one branch merged another (LOT3). [[BLK-015]]. +- **future application**: any wrapper/dispatcher forwarding args to a callee that resolves its target from ambient state — either (a) make the callee USE the args as the target, or (b) if the ambient-state contract is deliberate, treat passed args as an ASSERTION and refuse loudly when they disagree with the state. Never let forwarded args be silently dropped: silent-drop = the caller believes they steered, the callee ignored them. Sibling of "presence-flag ≠ capability" [[LRN-087]] — both = a visible signal lying about the real behavior. +- **Reference**: `lib/gitflow.sh` gitflow_finish arg-guard, `lib/gitflow-test.sh` T12. [[BLK-015]]. diff --git a/.claude/tasks/TODO.md b/.claude/tasks/TODO.md index 9a7d040..867f37d 100644 --- a/.claude/tasks/TODO.md +++ b/.claude/tasks/TODO.md @@ -393,3 +393,20 @@ Tension réelle proactif vs intrusif. Auto-déclencher feat/bugfix sur intention [done 2026-07-01 : unconditional npm guard after Node block (corepack enable npm → distro `install npm` fallback → fatal exit 1 w/ clear msg). Catches node>=22-present-but-npm-absent (NODE_OK short-circuit). shellcheck clean, bash -n OK. Fresh-apt live validation pending (no npm-less host to hand). branch bugfix/install-plugins-npm-guard.] - [x] (b) Re-baseline darwin on the 5 ex-broken gstack skills (`benchmark-models`, `context-restore`, `context-save`, `make-pdf`, `plan-tune`) — now repaired and back in scope ([[BDR-043]], trigger cleared). Verify `results.tsv` still marks them `status=error` first. (Promoted from BDR-043's action-field — not an item the user authored.) [resolved-MOOT 2026-06-30 : won't-run. BDR-043 cleared only motif (a) of BDR-015's TWO exclusion grounds (symlinks repaired ✅); motif (b) external-ownership INTACT — the 5 resolve to skills-external/gstack/ (submodule), darwin optimizes by EDITING SKILL.md → would dirty the submodule (forbidden [[LRN-070]]). Re-baseline = unactionable score. + results.tsv gone (wiped by 23/06 make-plugin reinstall) → not even a re-baseline, a fresh-from-zero one. Geometric trigger lifted, value trigger intact — twin of --help [[LRN-080]]. See [[LRN-082]]. Not "done", not "open": MOOT.] + +## 2026-07-03 — bugfix/gitflow-finish-args (contract fix + doctor false-warns) +Root: audit 2026-07-02 residuals. `gitflow_finish` ignores its args (merges CHECKED-OUT +branch) → LOT3 mis-merge trap; + 3 doctor false-warns (LRN-047 class). +- [x] (1) lib/gitflow.sh gitflow_finish — optional ; error rc2 if != current + branch ("operates on current branch X, you asked Y — checkout Y first"). No-args unchanged. + Commit d9fdd4c. [[BLK-015]] [[LRN-089]]. +- [x] (2) lib/gitflow-test.sh — T12 arg-guard: arg-mismatch → nonzero + message names both; + arg-match → merges as before. +7 assertions (71/71). T12 (not T6c — reconcile collision). +- [x] (3) doctor.sh cargo line — false "(RTK unavailable)" → optional info (RTK prebuilt). +- [x] (4) doctor.sh check_symlink — PASS iff canonical path under $REPO (direct OR via + symlinked ancestor dir); hooks/session-start.sh false-warn gone. Commit 6778b9f. +- [x] (5) doctor.sh §2 gstack — counts 34 per-skill symlinks; mythical [ -L skills/gstack ] dropped. +- [x] (6) doctor.sh token § — denominator 11000→CONTEXT_WINDOW=200000, thresholds 15/25, + comment anchored to measured ~11.4k (LRN-088). False "92% CRITICAL" → ~5% comfortable. +- [x] Verify — suites green (71/13/32/19/20/13 + RC 5/5); doctor 0 false-warn; shellcheck clean. + +docs(changelog) Unreleased entry (706abff). Gate passed on GO 2026-07-03. Finish pending. diff --git a/CHANGELOG.md b/CHANGELOG.md index db22ea4..1d73b6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] +### Fixed +- `gitflow_finish` ignored its ` ` arguments and always merged the checked-out branch — naming a different branch silently merged the wrong one. The arguments are now an optional safety assertion: if given and not equal to the current branch, `finish` refuses with a clear error instead of merging. No-argument calls (the only real caller) are unchanged. +- `doctor.sh` false-warnings removed (a check that cries wolf is one you learn to ignore): `cargo` absence no longer claims "RTK unavailable" (RTK ships as a prebuilt binary); `check_symlink` no longer flags files reached through directory-level symlinks (e.g. `hooks/session-start.sh`); the GStack check counts the per-skill symlinks instead of a `skills/gstack` link that `link.sh` deliberately removes; the token-budget estimate is measured against the ~200k context window instead of a mis-framed "~11k session budget" that produced a false "92% CRITICAL". + ## [4.0.0] — 2026-06-30 ### Added diff --git a/doctor.sh b/doctor.sh index 58442be..6cf9b81 100644 --- a/doctor.sh +++ b/doctor.sh @@ -42,18 +42,24 @@ check_symlink() { return fi - if [ -L "$target" ]; then - # readlink -f is not available on macOS BSD — use -f with fallback - local real - real=$(readlink -f "$target" 2>/dev/null) || real=$(readlink "$target") - if [ ! -e "$real" ]; then - fail "$HOME/.claude/$name → $real — BROKEN SYMLINK" - else - pass "$HOME/.claude/$name"; _LINK_PASS=$((_LINK_PASS + 1)) - fi - else - warn "$HOME/.claude/$name exists but is NOT a symlink (expected symlink to repo)" + # Broken symlink: points at a target that no longer exists. + if [ -L "$target" ] && [ ! -e "$target" ]; then + fail "$HOME/.claude/$name → $(readlink "$target") — BROKEN SYMLINK" + return fi + + # Correctly wired iff the canonical path lands inside the repo. This is true + # for a direct symlink (CLAUDE.md, settings.json) AND for a real file reached + # through a symlinked ANCESTOR dir (hooks/, skills/, agents/, lib/, templates/ + # are dir-level symlinks — their children are real files under $REPO). A stray + # real copy in ~/.claude resolves to itself (outside $REPO) → still flagged as + # drift. (LRN-047: the dir-symlink layout is legitimate, must not false-warn.) + local real + real=$(readlink -f "$target" 2>/dev/null) || real="$target" + case "$real" in + "$REPO"/*) pass "$HOME/.claude/$name"; _LINK_PASS=$((_LINK_PASS + 1)) ;; + *) warn "$HOME/.claude/$name resolves to $real (outside repo — expected a link into $REPO)" ;; + esac } check_symlink "CLAUDE.md" @@ -83,24 +89,17 @@ else warn "GStack submodule missing — run: git submodule update --init" fi -if [ -L "$HOME/.claude/skills/gstack" ]; then - real=$(readlink -f "$HOME/.claude/skills/gstack" 2>/dev/null || readlink "$HOME/.claude/skills/gstack") - if [ -d "$real" ]; then - pass "Symlink OK → $real" - # Check for skills/ subdirectory (referenced by plugin-advisor PHASE 1). - # `|| echo 0` is required because under `set -o pipefail`, a missing - # gstack/skills/ dir makes find exit non-zero, killing the script. - gstack_skills_count=$( { find "$HOME/.claude/skills/gstack/skills/" -maxdepth 1 -mindepth 1 2>/dev/null || true; } | wc -l | tr -d ' ') - if [ "${gstack_skills_count:-0}" -gt 0 ]; then - pass "GStack: ${gstack_skills_count} skills available" - else - warn "GStack symlink OK but no skills/ subdirectory found — may need: cd skills-external/gstack && ./setup" - fi - else - fail "Symlink broken → $real" - fi +# GStack skills are exposed as PER-SKILL symlinks directly under skills/ (browse, +# cso, review, …) pointing into skills-external/gstack/ — there is NO single +# skills/gstack symlink (link.sh deliberately removes it: it duplicated the +# top-level gstack SKILL.md alongside the per-skill entries). The bin/ + +# browse/dist/ helper links under skills/gstack/ are checked in §7 Consistency. +# `|| true` guards pipefail if skills/ is unexpectedly absent (checked above). +gstack_skill_links=$( { find "$HOME/.claude/skills/" -maxdepth 1 -type l -lname '*skills-external/gstack/*' 2>/dev/null || true; } | wc -l | tr -d ' ') +if [ "${gstack_skill_links:-0}" -gt 0 ]; then + pass "GStack: ${gstack_skill_links} skills linked (per-skill symlinks)" else - warn "GStack not symlinked — run: bash link.sh" + warn "GStack skills not linked — run: cd skills-external/gstack && ./setup" fi echo "" @@ -136,7 +135,10 @@ fi if command -v cargo &>/dev/null; then pass "Cargo $(cargo --version | awk '{print $2}')" else - warn "Cargo not found (RTK unavailable)" + # Cargo does NOT gate RTK: RTK ships as a prebuilt binary and detect_rtk finds + # it via ~/.cargo/bin or ~/.local/bin (RTK status is shown under Plugins). + # Cargo is only the Rust toolchain to BUILD RTK from source → optional, info. + info "Cargo not found (optional — only needed to build RTK from source)" fi if command -v python3 &>/dev/null; then @@ -239,8 +241,12 @@ echo "" # 6. Token budget estimate # ──────────────────────────────────────────────────────────── echo "── Token budget estimate ──" -# Reference: Claude Code Pro plan ~11k tokens/5h session (session budget, not context window). -# Seuils: WARNING >15%, CRITICAL >30% of session budget. +# The passive footprint (CLAUDE.md + skill descriptions + plugin session-injects) +# loads into the CONTEXT WINDOW every session — it competes with the ~200k default +# context, NOT a per-session token quota (the old "~11k/5h budget" denominator was +# a category error → false "92% CRITICAL", LRN-047). Measured ~11.4k post-audit +# 2026-07-02 (LRN-088); the chars/4 sum below is a coarse proxy of that footprint. +# Thresholds: WARNING >15% of context (~30k), CRITICAL >25% (~50k). CLAUDE_MD_CHARS=$(wc -c < "$REPO/CLAUDE.md" 2>/dev/null || echo 0) CLAUDE_MD_TOKENS=$((CLAUDE_MD_CHARS / 4)) @@ -264,25 +270,25 @@ if detect_context7 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 200)); if detect_graphifyy 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 300)); fi TOTAL_TOKENS=$((CLAUDE_MD_TOKENS + SKILL_DESC_TOKENS + PLUGIN_TOKENS)) -SESSION_BUDGET=11000 -PCT=$((TOTAL_TOKENS * 100 / SESSION_BUDGET)) +CONTEXT_WINDOW=200000 # Claude Code default context window (conservative; 1M is opt-in) +PCT=$((TOTAL_TOKENS * 100 / CONTEXT_WINDOW)) echo "" echo " CLAUDE.md: ~${CLAUDE_MD_TOKENS}t" echo " Skill descriptions: ~${SKILL_DESC_TOKENS}t (${SKILL_COUNT} skills)" echo " Plugin passive cost: ~${PLUGIN_TOKENS}t (active plugins)" echo " ─────────────────────────────────────────" -info " Total: ~${TOTAL_TOKENS}t" -info " Session budget (Pro): ${SESSION_BUDGET}t" -info " Usage: ~${PCT}%" +info " Total: ~${TOTAL_TOKENS}t (measured ~11.4k post-audit, LRN-088)" +info " Context window: ${CONTEXT_WINDOW}t (default; 1M opt-in)" +info " Usage: ~${PCT}% of context" echo "" -if [ "$PCT" -gt 30 ]; then - warn "CRITICAL: ${PCT}% of session budget — /plugin-check to disable unused plugins" +if [ "$PCT" -gt 25 ]; then + warn "CRITICAL: ~${PCT}% of the ${CONTEXT_WINDOW}t context — /plugin-check to disable unused plugins" elif [ "$PCT" -gt 15 ]; then - warn "WARNING: ${PCT}% of session budget — consider disabling unused toggle plugins" + warn "WARNING: ~${PCT}% of the ${CONTEXT_WINDOW}t context — consider disabling unused toggle plugins" else - pass "Budget: ${PCT}% (comfortable)" + pass "Budget: ~${PCT}% of context (comfortable)" fi # Per-file breakdown (skill bodies — loaded on demand, shown for awareness) diff --git a/lib/gitflow-test.sh b/lib/gitflow-test.sh index 9b46394..b3f37e8 100644 --- a/lib/gitflow-test.sh +++ b/lib/gitflow-test.sh @@ -156,6 +156,22 @@ if bash "$HERE/gitflow.sh" protected-base main; then ok "cli protected-bas if bash "$HERE/gitflow.sh" protected-base feature/x; then no "cli protected-base feature (rc0?)"; else ok "cli protected-base feature → rc1"; fi chk "cli base-for hotfix=main" '[ "$(bash "$HERE/gitflow.sh" base-for hotfix)" = main ]' +echo "T12 — finish arg-guard (named branch must equal current, else refuse)" +newrepo finargs; echo a>a; hookon; gitflow_init >/dev/null 2>&1 +gitflow_start feature standon >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m w +# mismatch: standing on feature/standon but asking to finish bugfix/other → refuse +# shellcheck disable=SC2034 # mism_out/mism_rc are used in the deferred chk eval strings +mism_out="$(gitflow_finish bugfix other 2>&1)"; mism_rc=$? +chk "arg-mismatch → nonzero rc" "[ $mism_rc -ne 0 ]" +chk "arg-mismatch → HEAD untouched" '[ "$(git symbolic-ref --short HEAD)" = feature/standon ]' +chk "arg-mismatch → branch kept" 'git rev-parse --verify -q refs/heads/feature/standon >/dev/null' +chk "arg-mismatch → develop NOT merged" '! git log develop --oneline | grep -q "Merge feature/standon into develop"' +chk "arg-mismatch → message names both" 'printf "%s" "$mism_out" | grep -q "current branch" && printf "%s" "$mism_out" | grep -q "bugfix/other"' +# match: naming the current branch explicitly finishes exactly like the no-arg path +gitflow_finish feature standon >/dev/null 2>&1 +chk "arg-match → merged into develop" 'git log develop --oneline | grep -q "Merge feature/standon into develop"' +chk "arg-match → branch deleted" '! git rev-parse --verify -q refs/heads/feature/standon >/dev/null' + echo echo "==== RESULT: $PASS passed, $FAIL failed ====" [ "$FAIL" -eq 0 ] diff --git a/lib/gitflow.sh b/lib/gitflow.sh index 54feec7..31f8ed1 100644 --- a/lib/gitflow.sh +++ b/lib/gitflow.sh @@ -97,11 +97,24 @@ _gitflow_delete() { # git branch -q -d "$br" || { echo "gitflow: '$br' not fully merged — branch kept" >&2; return 5; } } -# gitflow_finish → directed merge of the CURRENT branch per its type, then delete. -# WHEN to call this is the human gate (SKILL.md). This only performs the merge. +# gitflow_finish [ ] → directed merge of the CURRENT branch per its +# type, then delete. WHEN to call this is the human gate (SKILL.md). +# +# The merge source is ALWAYS the checked-out branch (HEAD) — that is the contract. +# The optional is a SAFETY ASSERTION, not a target selector: if you +# name a branch it MUST equal the current one, else finish refuses loudly instead +# of silently merging whatever you happen to be standing on. (Guards the audit UX +# trap: `finish bugfix audit-bugs` run from feature/audit-tokens merged the wrong +# branch — args were silently ignored. See BLK-015 / LRN-089.) No args = unchanged. gitflow_finish() { - local br type + local br type req_type="${1:-}" req_name="${2:-}" br="$(git symbolic-ref --short -q HEAD)" || { echo "gitflow_finish: detached HEAD" >&2; return 3; } + if [ -n "$req_type" ] || [ -n "$req_name" ]; then + [ "$req_type/$req_name" = "$br" ] || { + echo "gitflow_finish: operates on the current branch '$br', but you asked '$req_type/$req_name' — checkout '$req_type/$req_name' first (or run finish with no args)." >&2 + return 2 + } + fi type="$(gitflow_branch_type "$br")" case "$type" in feature|bugfix|chore)