lib/graphify-gate.sh counts tracked code files (graphify's AST extension set, vendored trees excluded) and, from 200 with no graphify-out/graph.json, prints one banner-sized line; session-start shows it with the /graphify hint. Nothing is built, installed or updated: the rule is the user's (BDR-097), grounded in the LRN-162 measurements (AST build 2.3 s, 0 tokens, a query 2 to 3k tokens). Doctrine section and plugin-advisor thresholds follow the same rule; graphify claude install stays rejected. Test: 11 checks. GRAPHIFY_MIN_CODE_FILES overrides the threshold.
`gitflow_delete` now ends with `_gitflow_delete_remote`: after the local
copy is gone, the remote tip is read with `ls-remote --exit-code`, checked
against develop/main with the same ancestor test, and only then removed
with `push origin --delete`. Same contract as the pushes (BDR-095): best
effort, warn never fail. No origin, `GITFLOW_NO_PUSH=1` or
`gitflow.autopush false` skip it; an unreachable origin or a remote tip
holding commits the bases lack keeps the remote branch, loudly. A base is
never targeted, by construction and by an explicit guard.
The static deny on hand `git push --delete` stays: it matches the Bash
tool's command string, the lib is the sanctioned path. Prose (hard_deny,
environment), doctrine, gitflow SKILL (table, op, warning row),
SETTINGS.md and CHANGELOG updated. T24: 9 checks (finish removes the
copy, bases untouched, unmerged remote tip kept, never pushed silent,
unreachable origin loud, autopush opt-out). 161/163, the 2 failures are
the pre-existing T16a (gitleaks absent on this host).
Since BDR-095 `start` sets an auto-pushed upstream, so `git branch -d`
checked "merged into origin/<branch>" (always true, the post-commit hook
keeps it in sync) instead of "merged into develop". T22a proves it: an
unmerged feature with its upstream in sync is deleted by `-d` alone.
- `gitflow_delete` is the single delete path (finish + CLI `delete`):
refuses main/develop (rc 6) and any branch that is not an ancestor of
develop or main (rc 5, `gitflow_merged_into_base`, fail closed when
neither base exists), then `-d` as a second layer. CLI `merged`, `hooks`.
- Fourth generated hook `reference-transaction`: in the `prepared` call,
a deletion of refs/heads/main or refs/heads/develop exits 1, whatever
issued it (branch -d/-D, update-ref -d, rename, script, sub-agent).
`git config gitflow.protect false` opts a foreign clone out.
- `GITFLOW_HOOKS` is the one hook list: write/emit/reconcile, T19d and
doctor.sh (`gitflow.sh hooks`) read it. `.githooks/` and `githooks/`
regenerated with the fourth hook.
- settings.json: static deny on hand `git branch -d/--delete/-dr/-rd` and
on renames of main/develop; hard_deny "Branch deletion by hand"; the
Disarming entry covers all four hooks and `gitflow.*` config; the
protected-branches environment line states the rule.
- Doctrine (CLAUDE.global.md gitflow section), gitflow SKILL (`delete`
op, rc 5/6 rows, common mistake), guard-bash spec T8w flips to deny,
SETTINGS.md, README, CHANGELOG.
- Tests: T22 (12) lib guard incl. the premise proof, T23 (11) hook;
T19 covers the fourth hook. 152/154, the 2 failures are the
pre-existing T16a (gitleaks absent on this host).
Global: `make link` generates githooks/ from lib/gitflow.sh and sets git's
global core.hooksPath to ~/.claude/githooks, so every repo on the machine
runs the pre-commit protection and the post-commit / post-merge push, even
one that never ran gitflow init. A repo's own local core.hooksPath still
wins, so hooks/session-start.sh calls `gitflow reconcile-hooks` once per
session and rewrites a .githooks/ that lags the lib (LRN-114 automated);
the pre-commit exemption now covers .githooks/** next to .claude/**.
Per-repo opt-outs for a foreign clone: `git config gitflow.protect false`
(branch model) and `git config gitflow.autopush false` (push). Both, and
the GIT_CONFIG_GLOBAL= / GIT_CONFIG= env bypass, are static deny rules.
`make test` and the two suites that commit on main export
GIT_CONFIG_GLOBAL=/dev/null so the machine's global hooks never fire in
throwaway repos. doctor gains "Git hooks" (global setting, githooks/ equal
to the emitters) and "Scratchpad" (warn when TMPDIR sits on a tmpfs with
usrquota: systemd caps each user at 80% of it, which killed two shells
today, BLK-021). Tests: T18h, T19d, T20 (reconcile), T21 (whitelist and
protect opt-out); this repo's own stale .githooks/ refreshed.
Layer C of the plan written after the 2026-09-21 wipe (BDR-095): a reviewer
sub-agent traced `lftp mirror --delete` against a local file:// tree, the
prose tiers named neither lftp nor a local trace, the brief had authorized
it, and four days of commits had never left the machine.
- gitflow: `start` pushes the branch with its upstream, merge targets are
pushed after each merge, and `init`/`install-hook` write post-commit and
post-merge hooks that push every commit as it lands (warn, never block;
GITFLOW_NO_PUSH=1 for throwaway repos). T18 + T19 (installed == emitted).
- hooks/unpushed-guard.sh on SessionStart and Stop: branch ahead of its
upstream, no upstream, or no origin. Non-blocking systemMessage.
- settings.json: static deny for transfer and mirror tools, rsync --delete,
xargs rm, pipe-to-shell, chmod/chown -R, sudo/doas/pkexec, disk tools,
chattr, docker volume drops/prune/--privileged/socket/-v /:, git history
destruction, --no-verify and core.hooksPath; new hard_deny "destructive
tool against a local path, brief carries no user authority"; soft_deny
reworded + discarding uncommitted work; environment records the incident.
- CLAUDE.global.md "Destructive tools & data loss"; the four report-only
agents trace by reading, never by running, whatever the brief says.
- lib/tests/guard-bash.test.sh: executable spec of the PreToolUse guard
(214 cases). The hook itself is not shipped (BLK-022); the spec skips.
make plugin never installed impeccable. The 3.2.0 pin had rotted upstream
(the CLI fetches its skill dist at install time; that release's zip is
gone), the --scope=project staging moved the skill dir alone and dropped
the 4 impeccable-* subagents, and /impeccable init was never announced.
Step 8d now installs at --scope=global straight through the
~/.claude/{skills,agents} symlinks into the repo (both paths gitignored),
guards on those symlinks existing, keeps a profile-parked copy parked,
falls back to @latest on a pin failure with a bump-the-lock warning, and
prints the per-project init hint. update-all.sh mirrors the shape.
Found while probing: with a copy already installed a rotted pin exits 0
("Could not check for skill updates ... left unchanged"), byte-identical
on disk to an up-to-date rerun, so imp_install reads the installer output
instead of trusting the exit code. Harness 4/4 in a sandbox HOME with the
real installer.
plugins.lock.json: impeccable 3.2.0 -> 4.1.0 (CLI only). link.sh drops
impeccable from EXTERNAL_SKILLS. lib/design-gate.md section 5: suggest-only
/impeccable init check when a frontend project has no PRODUCT.md.
Upstream supersedes `@21st-dev/magic` with `@21st-dev/cli` (bin `21st`):
same endpoint, `21st login` in place of an API key, no MCP process loaded
into every session.
- install-plugins.sh Step 8.7: `npm i -g @21st-dev/cli` (pinned in
plugins.lock.json), staged `21st skills install`, TTY-only login offer,
pack disabled by default. update-all.sh 7.4 refreshes both.
- The documented `21st install-skill` cannot be used: the installer refuses
to follow a symlink on the target path and `~/.claude/skills` is one. The
install runs under a throwaway HOME and the result moves into
skills-external/21st-* (gitignored), symlinked on demand.
- toggle-external.sh manages `21st` as a pack (names globbed from
skills-external/21st-*, parked under plain names). `magic` is gone.
- The 5 design skills join design/web/web-full/full and MANAGED_EXTERNALS;
21st-registry and 21st-design-sync stay parked. MANAGED_MCPS is now empty
and profile.sh's dead magic branches are removed.
- Design gate: GATE-BLOCK gains `21st` (required-manual, magic's old slot)
and `21st-ui-build`; PATH repair extended to the npm global bin.
- settings.json: the 4 mcp__magic__* ask entries go; the outward-facing
21st verbs land in autoMode.soft_deny, the tier that holds under auto
mode (LRN-153).
- Docs: README, CLAUDE.global.md, design-gate.md, profile SKILL.md,
.env.example, .gitleaks.toml, link.sh. BDR-093, LRN-158.
Tests: profile-set-managed 17/17, make test green except 2 pre-existing
gitflow FAILs (gitleaks binary absent on this host), shellcheck clean.
Every checklist command is emitted on exactly one line, however long; a
legacy backslash continuation in the runbook is joined at instantiation,
and bootstrap / learn patches write runbook lines the same way. After the
checklist the hand-back carries a Post-deploy tests block derived from the
delta diff: by-hand checks tied to delta files plus Suggestions for gaps.
Cold-resume re-display and re-hand-back regenerate both.
RED/GREEN on a scratch runbook: 4/4 baseline runs reproduced the
continuation verbatim and printed no tests; 4/4 runs on the edited skill
joined it and printed the block in the recipe's shape.
`permissions.ask` gates nothing under `defaultMode: auto` (LRN-146,
verified live), so the ten rules that left the static tiers had no cover
left: rsync / kill -9 / killall / pkill out of deny, and python3 -c /
python -c / xargs / sed / cp / mv out of ask.
autoMode.soft_deny (7 rules) takes over what an explicit instruction
should be able to clear: writes outside the working directory,
rsync --delete, SIGKILL and kill-by-name, in-place edits spanning more
than one file, directory moves, and inline interpreters or xargs that
delete or write outside the cwd. Intent clears a soft block for the
current turn only, stated as a rule since no setting expresses it.
autoMode.hard_deny (3 rules) takes the classes no command pattern can
express: secret exfiltration, production deployment, and disarming the
guardrails. Adding a restriction stays allowed, removing one does not.
permissions.deny gains ten .env reader rules (sed awk cut tr sort uniq
diff od xxd strings). Six of those tools sat in permissions.allow, so
reading a .env through them triggered nothing.
autoMode.environment named another project, its FTP deploy target and its
customer data, inside the file link.sh:21 symlinks to
~/.claude/settings.json, where it reached every repo and contradicted
this one's Gitea remote. Rewritten machine-generic; the project facts
moved to that project's gitignored .claude/settings.local.json. All three
lists now open with "$defaults", which the original omitted, so the
built-in classifier entries are inherited rather than replaced.
doctor.sh check_automode backstops both defects. SETTINGS.md documents
the block and a tier-choice table. README no longer claims the ask tier
makes every mcp__magic__* call require a live confirmation.
Two or more project paths dispatch one general-purpose runner per repo,
all in a single message, instead of processing repos one by one. The
runner inherits the session model — no pin, it carries tour's reflection
(fix decisions, convergence) — and every agent inside keeps its defined
tier (security-auditor sonnet, Phase B opus, doc-syncer sonnet). A dead
or mute runner becomes an explicit RUNNER FAILED summary row; the gated
capitalize offer stays in the main loop, never in a runner.
Bounded LRN-083 derogation recorded in BDR-084: the per-project fix loop
moves into its runner, but nothing a runner decides touches shared state
— independent repos, per-repo chore branches, branches left unmerged for
human review exactly as inline. Mechanics proven before building: nested
probe, 3 sub-agent windows all overlapping, 9.1s vs ~18s sequential.
Census §12: 6 locks, flip-tested. Single-project path unchanged.
The include is authoritative, but feat/bugfix/ship-feature/init-project
each restate the verify loop inline — an orchestrator following the
restatement alone would have skipped the floor. Each now carries the
GATE 0 bullet ahead of GATE 1 (4 new structure locks, flip-tested).
The contract-interview weight table stops promising a hotfix oracle
nothing executes: hotfix runs no floor, the hotfixer runs the suite
itself. CHANGELOG extended with the wiring + the RED result.
BDR-083 records what was taken from unlazy and, more usefully, what was
refused and why. LRN-141: an external skill's machinery encodes its threat
model, not yours — take the invariants, refuse the machinery. LRN-142:
structure locks are fixed-string, so reflowing a doctrine paragraph reds
them; fix the doc, not the lock.
- MANAGED_EXTERNALS (emil-design-eng, frontend-design,
design-motion-principles, impeccable) + MANAGED_MCPS (magic):
cmd_set now trims both when the profile does not list them —
design leftovers no longer survive a 'set backend'
- cmd_set refactored to 4 symmetric trim helpers; nothing outside
the MANAGED_* allowlists is ever auto-toggled (darwin-skill manual)
- enable_skill external: from-source fallback (ln -sf
skills-external/<name>), mirrors toggle-external.sh
- stale usage() NOTE + SKILL.md updated to the both-ways reality
- hermetic test: 16 checks, fixture repo + fake claude shim (gstack
on-demand, from-source, park/restore, magic add/remove, non-managed
untouched); shellcheck + full make test green
RC fix (soak day 1): `make update` step 7.5 failed on a 300s clone timeout
of alchaincyf/find-skills. The skill (search the skills.sh registry from
Claude) was never used; the discovery case stays reachable manually via
`npx -y skills find <query>`. Removed from install-plugins.sh (install list
+ summary + comment), update-all.sh (refresh list), link.sh
(NPX_EXTERNAL_SKILLS), lib/toggle-external.sh (MANAGED_TOOLS + case arms),
plugin-advisor.md, .gitignore; local skills/find-skills symlink deleted.
Memory-registry and test-fixture mentions kept — append-only history.
toggle-external `list` verified post-removal; suites 8/8.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpphkdTosUzokBDNG7PToS
Live failure (run 2): the checklist printed above AskUserQuestion never
reached the user. Fix is structural: the checklist is never written to a
file (throwaway — PENDING.json + live runbook regenerate it in any
session) and every hand-back/re-display ends the turn with the full
checklist as the FINAL text, no tool call after it. Cold resume without a
report regenerates + re-displays. Artifacts 5 -> 4 files; bootstrap
gitignore step drops NEXT.sh; mistakes/red-flags updated (no tool call
after the print, no file 'for reference').
Generator-owned files updated by the out-of-band 'make plugin' run
(SKILL.md, extraction-spec, query reference, version marker). Committed
deliberately after diff review; CHANGELOG Unreleased notes the bump.
First-real-run UX feedback (EVAL-016): one command per line as typed in an
interactive session (ssh opens the box, following lines run on it, local
steps flagged), never folded ssh compounds; the hand-back prints the full
checklist in the conversation (and every re-hand-back reprints it). Step
defined as a block (header + command lines to next blank line), @delta
governs the block. Template restyled to match.
Fresh installs and too-old hosts now get Node 24 (NodeSource setup_24.x /
brew node@24). GSD v2 (>=22) still satisfied. Next 'make plugin' on a
Node-22 host upgrades in place and unlocks impeccable Step 8d.
Complementary to frontend-design (kept: build-time aesthetic direction).
impeccable adds what the chain lacked: 45 deterministic anti-slop rules
(npx impeccable detect, exit 0/2, --json) — the design counterpart of the
semgrep gate — plus 23 design verbs under one /impeccable skill and
persistent per-project design context.
- plugins.lock.json: CLI pinned 3.2.0 (rules update = audit output change
on unchanged code, LRN-077 class); skill dist = its own release track
- install-plugins.sh Step 8d: staged npx install (tmpdir) -> moved to
skills-external/impeccable (machine-owned, gitignored, ctx7 pattern);
never writes through the ~/.claude/skills symlink into the tracked tree
- update-all.sh: pin-honored refresh, Node<24 or failure -> dist kept
- Node >= 24 required (host at 22): steps skip gracefully, activation
deferred to a deliberate Node bump
- link.sh EXTERNAL_SKILLS, profiles (design/web/web-full/full),
plugin-advisor, CLAUDE.md design routing, design-gate, README, CHANGELOG
- NOT in design GATE-BLOCK yet: promotion after first dogfood
version.txt 3.5.0→4.0.0; CHANGELOG [Unreleased]→[4.0.0] — 2026-06-30
(complete: gitflow universal + 10 other gaps filled before the cut), new
empty [Unreleased] re-opened. MAJOR = the gitflow universal workflow rupture.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6bUdvHnajCNzgVQefZowj
/release-candidate cuts a release by orchestrating the existing gitflow
release mechanic (start from develop; finish fan-out main+develop+delete)
and adding the one piece the lib lacks: the version tag.
- skills/release-candidate/SKILL.md: thin orchestrator — preconditions →
gitflow start release → prep (version.txt + CHANGELOG, breaking doc'd) →
run-tests gate → human WHEN-to-release gate → gitflow finish → git tag -a
vX.Y.Z (in the skill, lib untouched) → push (gated).
- lib/tests/run-release-candidate.sh: throwaway-repo flow replay. RC_TAG=0
reds the tag (gitflow fans out but never tags); RC_TAG=1 → 5/5.
- CLAUDE.md: Skill routing line. CHANGELOG [Unreleased]: /reconcile +
/release-candidate under Added (so the eventual v4.0.0 captures them).
Tag scheme vX.Y.Z continues the version.txt/CHANGELOG lineage. writing-skills TDD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6bUdvHnajCNzgVQefZowj
STEP 12 ran `gsd init` AFTER FINISH, creating ROADMAP.md + .gsd/ in the working
tree once the merge had already integrated committed history only — so the
artifacts stranded outside the merge/PR (BLK-011, 3rd post-FINISH artifact after
memory + docs).
Resolved by REMOVAL, not by plumbing a commit: STEP 12 speculatively bootstrapped
a heavy multi-session engine (state machine / crash recovery / cost tracking /
parallel workers) that is opt-in and rarely used. Deleting the producer means the
orphan is never created — a negative diff beats building a gsd-commit helper for an
artifact nobody commits to using.
Deliberate GSD use is untouched: initializable on-demand (`/onboard add gsd`, or
`gsd init` in a terminal), still recommended by plugin-advisor, still read by
/status. init-project is now an 11-step pipeline.
Coherence sweep (the "test" for a removal): zero dangling STEP-12 refs — header
12->11-step, the STEP 10c note, and 4 USAGE.md worked-example references all updated
to on-demand init.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6bUdvHnajCNzgVQefZowj
(1) lib/doc-shape.sh — deterministic shape oracle re-checks each LLM-classified
MINOR patch before the silent auto-commit; a shape-suspect patch (added heading
/ oversize / new file / non-doc) escalates to the EXISTING SIGNIFICANT gate.
Genuine MINOR still auto-commits (zero friction, BDR-036 preserved). A structural
floor under the LLM call, not a semantic SIGNIFICANT-detector (BDR-040).
(2) lib/doc-commit.sh — guard the commit itself: a rejected git commit (pre-commit
hook / protected branch / signing) now fails loud with exit 5 + empty stdout,
instead of a false "committed" + the previous HEAD's hash + exit 0 that left docs
silently uncommitted (LRN-071, 3rd occurrence of the swallowed-commit pattern
after LRN-066 and LRN-068/BLK-012).
Wired doc-syncer STEP A4 (whole-set escalation: no=revert all, select=keep subset)
+ doc-commit.md (rc-5 consumer row + ACKNOWLEDGMENTS coherence).
TDD RED->GREEN: run-doc-commit.sh 32/32, run-doc-shape.sh 19/19, behavioral C/D.
shellcheck clean. Branch-guard (3) deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C6bUdvHnajCNzgVQefZowj