chore(memory): BDR-096 branch deletion guard, LRN-161 -d checks the upstream, journal + TODO 2026-09-24

This commit is contained in:
bastien
2026-09-24 11:35:01 +02:00
parent 32d8f981df
commit 0d717d9bfc
4 changed files with 59 additions and 0 deletions
+8
View File
@@ -1205,3 +1205,11 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
- **Status**: accepted, on feature/destructive-guardrails. `gitflow-test.sh` T18 7/7 + T19 3/3, unpushed-guard 9/9, `make test` green minus 2 pre-existing T16a, shellcheck clean, doctor 0 errors. NOT DONE: `hooks/guard-bash.sh` ([[BLK-022]]). Existing projects need `gitflow install-hook` re-run for the push hooks.
- **Reference**: `settings.json`, `lib/gitflow.sh`, `.githooks/{post-commit,post-merge}`, `hooks/unpushed-guard.sh`, `lib/tests/{guard-bash,unpushed-guard}.test.sh`, `CLAUDE.global.md`, `templates/settings/SETTINGS.md`, `agents/{verifier,plan-challenger,analyzer,security-auditor}.md`. Extends [[BDR-090]] [[BDR-092]] (ask inert, soft_deny doctrine); links [[LRN-114]] (T19 drift gate), [[LRN-155]], [[BDR-083]].
- **Amendment 2026-09-22 (user go: "je valide les deux")**: no per-project `install-hook` step. (a) GLOBAL: `make link` runs `gitflow global-hooks` → generates `githooks/` from the emitters + `git config --global core.hooksPath ~/.claude/githooks` (symlinked into the repo) → every repo on the machine is protected + auto-pushed, gitflow-initialized or not (faunosteo class). Git precedence: a repo's local `core.hooksPath` wins. (b) RECONCILE: `hooks/session-start.sh` calls `gitflow reconcile-hooks` once per session → rewrites a lagging `.githooks/` (LRN-114 automated), banner line + commit reminder; pre-commit whitelist extended to `.githooks/**` so that refresh commits on develop. (c) Opt-outs per repo (foreign clone): `git config gitflow.protect false`, `gitflow.autopush false` — human-only, static deny on `git config gitflow.*` and on the `GIT_CONFIG_GLOBAL=`/`GIT_CONFIG=` env bypass. (d) Hermetic tests: `make test` + the two suites committing on `main` export `GIT_CONFIG_GLOBAL=/dev/null`, else the machine's global hooks fire in throwaway repos. (e) doctor: global setting + `githooks/` == emitted. Tests T18h, T19d, T20, T21. Rejected: `init.templateDir` (new repos only, ignored once hooksPath is set); dropping the per-repo `.githooks/` (portability to a machine without claude-config). Status: verified once /tmp was freed — gitflow 127/129 (2 pre-existing T16a), review-guards G5 flagged this repo's own stale `.githooks/` (the LRN-114 gate doing its job; refreshed via install-hook), shellcheck clean. `make link` (global `core.hooksPath`) refused to the agent by the classifier twice → user runs it. Doctor gained a "Scratchpad" check ([[BLK-021]] mechanism).
## BDR-096 — Branch deletion guard: lib-only delete after verified merge, main/develop undeletable at the ref layer
- **Date**: 2026-09-24
- **Decision**: user rule "auto-delete OK only once merged into develop or main; main/develop never". (1) `gitflow_delete` = single delete path (finish + CLI `delete <br>`): rc 2 unknown, rc 6 protected base, rc 5 not ancestor of develop or main (`gitflow_merged_into_base`, fail closed when neither base exists), then `git branch -d` kept as 2nd layer. (2) 4th generated hook `reference-transaction`: `prepared` call, `refs/heads/main|develop` with all-zero new value → exit 1, whatever issued it (branch -d/-D, update-ref -d, rename, script, sub-agent); `gitflow.protect false` opt-out checked only on a hit. `GITFLOW_HOOKS` array = single hook list (write/emit/reconcile, T19d, doctor via `gitflow.sh hooks`). (3) static deny `git branch -d|--delete|-dr|-rd *`, `-m|-M main|develop*`; hard_deny "Branch deletion by hand"; Disarming entry now covers all 4 hooks + `gitflow.*` config; environment protected-branches line. (4) doctrine CLAUDE.global.md gitflow §, SKILL.md `delete` op + rc 5/6 rows, guard-bash spec T8w flips to deny, SETTINGS.md/README/CHANGELOG.
- **Why**: since [[BDR-095]] `start` pushes `-u origin` → `git branch -d` checks merge into the UPSTREAM (origin/<br>, kept in sync by post-commit), not HEAD → its valve is dead; T22a proves it. `_gitflow_delete` survived only because finish chained it after a successful merge. Same doctrine as 21/09 ([[LRN-160]]): mechanical + static before prose; ref-layer hook holds for nested commands and sub-agents where the Bash deny cannot see.
- **Alternatives rejected**: hook also refusing UNMERGED deletion → files backend rename = delete + create in separate transactions, `branch -d` passes zeros as old oid → merged check impossible/false positives on `branch -m`, user-shell friction; lib + deny carry that rule. Remote cleanup after finish (`push --delete origin/<br>`) → in static deny since BDR-095, not requested; origin/<br> accumulates, flagged to user. `-D` in the lib → no, `-d` stays as defense in depth. Interactive brainstorm → user absent (autonomous run), request unambiguous; trade-off (hook blast radius) stated in the report instead.
- **Status**: accepted, on feature/branch-delete-guard, UNMERGED (human gate). gitflow-test 152/154 (2 pre-existing T16a, gitleaks absent), T22 12/12 + T23 11/11, shellcheck clean incl. emitted hook, doctor 4/4 hooks match. Hook LIVE machine-wide via global `githooks/` while the branch is checked out (symlink follows the checkout).
- **Reference**: `lib/gitflow.sh`, `lib/gitflow-test.sh` T22/T23, `githooks/reference-transaction`, `.githooks/reference-transaction`, `settings.json`, `doctor.sh`, `skills/gitflow/SKILL.md`, `CLAUDE.global.md`, `templates/settings/SETTINGS.md`. Extends [[BDR-095]]; links [[LRN-161]], [[LRN-114]].
+4
View File
@@ -492,3 +492,7 @@ rules:
- /tmp freed by the user → shell back. G8 verified (gitflow 127/129, review-guards G5 caught the repo's stale `.githooks/`, refreshed). Quota mechanism found: systemd's stock `tmp.mount` carries `x-systemd.graceful-option=usrquota` and each user is capped at 80% of the tmpfs (5.9 GB of 7.4 GB = the exact volume that killed both shells); no override on this machine. Durable fix = `TMPDIR=$HOME/.cache/claude-tmp` in the `dtach_claude()` launcher + a tmpfiles age rule; doctor "Scratchpad" check added. `make link` denied to the agent → user.
- feature/destructive-guardrails merged into develop on user go, `gitflow finish` → cbb87f6, pushed by the lib itself (first live run of the merge-target push). Branch deleted. OPEN for the user: `make link`, TMPDIR in the launcher, layers A/B, guard hook ([[BLK-022]]).
## 2026-09-24
- User rule: auto-delete of a branch only once merged into develop/main; main/develop never deleted. Found `git branch -d` guard dead since BDR-095's `-u` push (checks the upstream, always in sync) — T22a proves it ([[LRN-161]]).
- Shipped [[BDR-096]] on feature/branch-delete-guard: `gitflow_delete` (rc 5 unmerged / rc 6 protected; CLI `delete` `merged` `hooks`), 4th hook `reference-transaction` vetoing delete/rename of main/develop (live via global `githooks/`), `GITFLOW_HOOKS` single list, static deny on hand `branch -d/--delete` + base renames, hard_deny entry, doctrine + SKILL + docs. 152/154 (2 pre-existing T16a), doctor 4/4, shellcheck clean. UNMERGED — human gate.
- Inline probes denied 4× by the guardrails themselves (deny strings in command text) → probe = test file, content via Write. Open for the user: `origin/<br>` accumulates after finish (`push --delete` denied), CLAUDE.global.md 352L (>320 budget), user's `feedbackDrafts` settings line left uncommitted on purpose.
+7
View File
@@ -1513,3 +1513,10 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
- **Pattern**: (a) an LLM classifier reads intent; the orchestrator's brief IS the sub-agent's user voice, so a reasoned authorization passes. Only static deny rules (resolve first, inherited by sub-agents, per-segment on `&&`) and OS rights are boundaries. (b) "Trace what it would do" is execution; a scratch target from a variable is one unset var away from `/`. (c) The event deletes its own evidence when the agent's uid owns the logs, the config and the transcripts. (d) A remote backs up only what it holds: push at branch creation and at every commit, from a hook, not from discipline. (e) `git merge` fires post-merge, not post-commit.
- **Future application**: any new destructive capability → static deny first, prose second, doctrine third. Any orchestrator brief → never "X is allowed outside the repo". Sub-agent tools: report-only agents trace by reading. Probe a guard with the real sub-agent path (auto mode inherited), not the main session.
- **Reference**: [[BDR-095]], `/mnt/cloudpex/RECOVERY/00-incident/`, atlast transcript `26e76a0b…` + stub `agent-a7d9119…`. Links [[BDR-090]], [[BDR-092]], [[LRN-155]], [[LRN-114]].
## LRN-161 — `git branch -d` guards against the UPSTREAM once one is set: auto-push turns it into a no-op guard
- **Date**: 2026-09-24
- **Context**: audit of `_gitflow_delete` for the user rule "never delete unmerged". git-branch(1): `-d` requires the branch merged into its upstream if set, else into HEAD; when merged to upstream but not HEAD it only WARNS. [[BDR-095]] made `start` push `-u origin` and post-commit keeps origin/<br> == <br> → `-d` always succeeds. T22a: unmerged feature, upstream in sync, `git branch -q -d` rc 0, branch gone.
- **Pattern**: (a) a safety check whose reference point is configurable changes meaning when config moves elsewhere — auto-push broke `-d` with zero diff in the delete code. Verify "merged" explicitly against the NAMED base: `git merge-base --is-ancestor <br> <base>`. (b) probing a guardrail inline gets blocked BY the guardrail: deny strings (`core.hooksPath`, `GIT_CONFIG_GLOBAL=`, `rm -rf "$VAR"`, `branch -D develop`) are matched in the command text, heredocs included → 4 denials this session. Probe = a test in the suite (file, run via `make test`), the TDD path anyway; file content via the Write tool, command line clean. (c) `reference-transaction` hook: line `<old> <new> <ref>` in `prepared`; `branch -d` passes an all-zero old oid ("force" semantics) → ref NAME + all-zero NEW is the only reliable deletion signal; a merged check cannot live there.
- **Future application**: any change to upstream/push config → re-read every `-d`, `--ff-only`, `@{u}`-relative guard. New destructive capability → static deny + mechanical check + prose, in that order ([[LRN-160]]). Guardrail probes → test file, never inline; a denied probe is the guard working, not a bug to route around.
- **Reference**: [[BDR-096]], [[BDR-095]], `lib/gitflow-test.sh` T22a/T23, git-branch(1), githooks(5) reference-transaction.
+40
View File
@@ -1,5 +1,45 @@
# TODO
## 2026-09-24 — branch deletion guard: never main/develop, never unmerged (feature/branch-delete-guard)
User rule (after the 21/09 wipe, same family as BDR-095): auto-delete of a branch
is accepted ONLY once it is merged into develop or main; main and develop are
never deleted. Finding that motivates it: since BDR-095 `start` pushes `-u origin`,
so `git branch -d` now checks "merged into its UPSTREAM" (origin/<br>, always in
sync via post-commit) instead of "merged into HEAD" — its safety valve is dead.
`_gitflow_delete` only survived because finish chains it after a successful merge.
- [x] D1 `lib/gitflow-test.sh` T22 (lib): `-d` alone deletes an unmerged branch
whose upstream is in sync (premise proof); `gitflow_delete` refuses
main/develop (rc 6) and an unmerged branch (rc 5), deletes a merged one;
`gitflow_merged_into_base` predicate; T23 (hook): `git branch -D
develop|main`, `update-ref -d`, `branch -m develop` all BLOCKED from a
working branch; a merged feature deletes fine; `gitflow.protect false`
opt-out; `commit`/`checkout` unaffected; T19d/T20 iterate the 4 hooks.
- [x] D2 `lib/gitflow.sh`: `gitflow_merged_into_base <br>` (ancestor of develop
OR main, fail closed when neither exists); `gitflow_delete` = protected
refusal + merged check + `git branch -d`; CLI verbs `delete <br>`,
`merged <br>`, `hooks`; 4th hook `reference-transaction` (refuses deletion
of refs/heads/main|develop in `prepared` state, sh, opt-out
gitflow.protect); hook names in one `GITFLOW_HOOKS` array (write, emit,
reconcile, T19d, doctor all read it).
- [x] D3 `settings.json`: static deny `git branch -d|--delete|-dr|-rd *`,
`git branch -m|-M main|develop *`; hard_deny "branch deletion outside
`gitflow.sh delete/finish`, any deletion/rename of main/develop, local or
remote"; "Disarming" entry covers every hook file; `environment`
protected-branches line updated. `guard-bash.test.sh` T8w flips to deny.
Leave the user's uncommitted `feedbackDrafts` line out of the commit.
- [x] D4 doctrine: `CLAUDE.global.md` gitflow section (delete only via the lib,
main/develop never, `-d` no longer protects); `skills/gitflow/SKILL.md`
table + `delete` op + failure rows rc 5/6.
- [x] D5 `doctor.sh` hook loop reads `gitflow.sh hooks`; regenerate `.githooks/`
+ `githooks/` (both tracked) with the 4th hook.
- [x] D6 docs: `templates/settings/SETTINGS.md`, README line, CHANGELOG.
- [x] D7 `make test`, shellcheck, doctor; BDR-096 + LRN + journal.
Verified 2026-09-24: gitflow-test 152/154 (2 pre-existing T16a),
T22 12/12 + T23 11/11, shellcheck clean incl. emitted hook, doctor
4/4 hooks match. Branch UNMERGED — human gate. BDR-096, LRN-161.
Out of scope, flagged: remote branch cleanup after finish (`git push --delete`
is in static deny since BDR-095; origin/<br> now accumulates — user's call).
## 2026-09-22 — destructive guardrails after the 21/09 wipe (feature/destructive-guardrails)
Incident 2026-09-21 00:21 on the old server: a reviewer sub-agent (atlast SDD, opus)
traced `lftp mirror --reverse --delete` against a local `file://` tree; the target