forked from bchanot/claude
chore(memory): BDR-114 + LRN-200..202 + journal — feat manual-push-mode run D
This commit is contained in:
@@ -1385,3 +1385,10 @@ Branch feature/user-writing-web-rules, UNMERGED (human gate).
|
||||
- **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]].
|
||||
|
||||
## BDR-114 — Every `gitflow.autopush` reader fails closed and names the stop; the lib verb is the single reader [accepted] (2026-10-07)
|
||||
- **Decision**: run D of [[BDR-111]]. Rule for every reader: unset or `true` → auto (push); `false` → manual (no push, silent); anything else (unparseable value, corrupt config, git failure) → NO push and one named line. Readers: lib `_gitflow_push_off` = `[ "$(gitflow_push_mode)" != auto ]` (verb stderr passes through); emitted post-commit/post-merge hooks (standalone POSIX sh, mirror of the verb: `case "$rc:$v" in 0:true|1:*) ;; 0:false) exit 0 ;; *) echo "… NOT pushed, treated as manual push mode" >&2; exit 0 ;; esac`); unpushed-guard, session-start banner (`autopush bad` lock line) and push-guard (sources the lib once, `gitflow_push_mode` per candidate) all read the verb. push-guard residuals: whole-word dir tokens (mixed quoting → deny; fully quoted + inner other-quote and `\ ` escapes resolved), payload jq cannot parse → raw scan with JSON escapes folded → static deny, cap BEFORE any per-token fork, T42 base = main. Skill prose: invalid outcome split on the ahead count (0 → "pushed anyway, likely a stale fail-open hook or a manual push"), labels COMMIT + PUSH STATE READ, release-executor version format check by reading. Doctrine: invalid = manual; "never pushes, even when asked".
|
||||
- **Why**: `--bool --default true` covers a MISSING key only; a typo re-enabled every push silently (the 21-09 hazard class on a work machine). The user chose fail-closed for the guard in run B; D extends it everywhere and keeps a terminal signal (git's own "fatal: bad boolean" used to be the only one; the hooks now print theirs).
|
||||
- **Alternatives rejected**: regen via `install-hook` (writes a LOCAL hooks-path entry) or `global-hooks` (writes the GLOBAL config when the value is missing — it was, [[LRN-200]]) → `emit-hook > file` only; temp file for the verb's stderr in a hook (fail-open on a full TMPDIR, predictable path) → `2>&1` capture ([[LRN-202]]); classification before the token cap (13 s flood) → cap first ([[LRN-201]]); shell check of the release version string (interpolation sink) → by reading.
|
||||
- **Gates**: D1 3 lenses + confirm FATAL(1) (global-config write) → emit-hook; feater; GATE 0 MET; verifier CONFORME; security PASS. D2 3 lenses + confirm FATAL(3) (escape alternative, same-quote-inside, `bare=$one` when unparsed); feater; GATE 0 MET; verifier CONFORME; security BLOCK(1) cap-after-fork → fixed (20k tokens 0.13 s) → CONFORME + PASS. D3 3 lenses + confirm CONCERNS(3); feater; CONFORME + PASS.
|
||||
- **Refs**: contracts/plans `2026-10-07-manual-push-failclosed-d1-1522`, `…-guard-residuals-d2-1526`, `…-prose-d3-1530`; commits 472cccb, 3c59333, 64ca0f8 (feature/manual-push-mode, UNMERGED). Supersedes the "fail-open on invalid value" line of [[BDR-111]]. Links [[BDR-112]], [[BDR-113]], [[LRN-114]], [[LRN-196]]. Residuals: TODO "post-run-D residuals" (soft_deny names only `false`; stale `.githooks/` in onboarded repos until reconcile).
|
||||
|
||||
@@ -573,4 +573,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.
|
||||
- /feat manual-push-mode run D (user: "enchaine"), split D1/D2/D3, 9 lenses + 3 confirmations. D1: every autopush reader fails closed AND names an invalid value (lib `_gitflow_push_off` via the verb; emitted hooks POSIX `case "$rc:$v"` + stderr line; unpushed-guard via the verb, no temp file); regen files-only via `emit-hook >` after the confirmation showed `install-hook` writes a local hooks-path and `global-hooks` writes the GLOBAL config when it lacks the value — which it did: user's dotfiles installer had overwritten `~/.gitconfig` (@USER@ placeholders, no hooksPath) + `~/.zshrc` at 15:39; user confirmed (own machine setup) and restored from the installer's backup before execution. D2: push-guard sources the lib, whole-word tokens (mixed quoting → deny), payload fallback, cap BEFORE classification (security BLOCK(1) caught the reorder: 1,600 tokens = 13 s > 10 s timeout → fixed, 20k tokens 0.13 s), T42 base main, banner `autopush bad`. D3: prose aligned (invalid outcome split on the ahead count, labels COMMIT + PUSH STATE READ, executor version check by reading). Commits 472cccb, 3c59333, 64ca0f8 on feature/manual-push-mode, UNMERGED. Residuals in TODO.
|
||||
|
||||
|
||||
@@ -1736,3 +1736,15 @@ Rule: when editing a doctrine file under structure locks, grep the test's lock s
|
||||
## 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]].
|
||||
|
||||
## LRN-200 — `install-hook` and `global-hooks` write git config as a side effect; only `emit-hook > file` regenerates hooks config-free
|
||||
- **Context**: D1 planned `install-hook` (= write hooks + a LOCAL hooks-path entry this repo must not gain: it runs on the global hooks) then `global-hooks <dir> <value>` ("returns before any write because the global value already matches"). The confirmation challenger read `~/.gitconfig`: rewritten 2 min earlier by the user's dotfiles installer (`@USER@` placeholders, no hooks path) → `global-hooks` WOULD have written the global config through a lib call, and `md5 .git/config` could not see it. User confirmed (own machine setup) and restored from the installer's backup; commits waited.
|
||||
- **Apply**: regenerate generated hooks with `bash lib/gitflow.sh emit-hook <name> > <dir>/<name>` (no config read/write, mode preserved); never `install-hook`/`global-hooks` from a flow. Evidence = md5 of `.git/config` AND a Read of `~/.gitconfig` before/after. Before any commit, check `git var GIT_COMMITTER_IDENT` is not a placeholder: an external installer can rewrite dotfiles mid-session. Links [[BDR-114]], [[LRN-114]], [[BLK-027]].
|
||||
|
||||
## LRN-201 — A flood cap must run before ANY per-token fork; a reorder reopened the timeout fail-open and only a timed 2 000-token test catches it
|
||||
- **Context**: run B put the 20-token cap before resolution (20k tokens 0.15 s). D2 added `classify_tok` (one subshell + sed per token) and the executor ran it BEFORE the cap: 1,600 tokens = 13 s > the 10 s hook timeout = allow. Verifier CONFORME (suite max 21 tokens), security re-scan BLOCK(1). Fix: `arg_tokens | sort -u | grep -c .` then `> 20` deny, then classification on ≤20 survivors; T58 = 2,000 tokens, deny, `$SECONDS` < 5.
|
||||
- **Apply**: in any guard, order = count → cap → everything else; a cap without a timed flood test in the suite is a comment, not a guard. Re-measure the flood after every change to the token pipeline (security gate did: 0.13 s at 20k). Links [[BDR-114]], [[LRN-196]], [[LRN-104]].
|
||||
|
||||
## LRN-202 — Reading a stderr-then-stdout verb from a hook: `out=$(cmd 2>&1)`, last line = word, prefix line = reason; no temp file; lib path absolute before any cd
|
||||
- **Context**: unpushed-guard plan used `2>"${TMPDIR:-/tmp}/x.$$"` + cat + rm: fail-OPEN when TMPDIR is full (`|| mode=auto`), predictable path, symlink-followable on shared /tmp, leaked on kill; `mode=$(cmd 2>&1 >/dev/null)` captures ONLY stderr. push-guard's `mktemp` variant added `set -u` trap hazards. The verb writes its stderr line BEFORE its stdout word in one process, so `${out##*$'\n'}` is the word and the `gitflow.sh push-mode:` line is the reason (select by prefix, not `head -1`: a bash startup warning could precede it). Resolve the lib path to an absolute one BEFORE the hook's `cd "$cwd"` (a relative invocation otherwise resolves into the target repo).
|
||||
- **Apply**: hooks never touch temp files for a one-line capture; anything but the expected word is treated as the fail-closed state, never as the default. Links [[BDR-114]], [[LRN-196]], [[LRN-199]].
|
||||
|
||||
Reference in New Issue
Block a user