To add a new API key managed by chezmoi without committing it to the dotfiles repo:
[data.<scope>] <key> = "<value>"
to ~/.config/chezmoi/chezmoi.toml (private config)~/para/areas/dev/gh/ak/dotfiles/), rename the shell config
to .tmpl{{ .<scope>.<key> }} template syntax in
the filechezmoi apply--locked breaks
with newer CI RustCI runs Rust 0.98.0 (or whatever latest stable is), which expects a
different Cargo.lock format than local 1.93.x. Fix:
Remove --locked from build steps in release.yml. Keep it in
CI.yml (no cross-compile there).
cargo publish dirty
directoryCargo.lock gets regenerated by CI's different Rust version.
cargo publish checks working dir is clean.
Fix: Add --allow-dirty to
cargo publish step AND add include field to
Cargo.toml to control what gets packaged.
include field requiredWithout include, cargo publish packages
everything in the repo (beads, openspec, .wai, etc.). Wai was packaging
998 files / 68MiB. Fix: Add
include = ["src/**/*", "/build.rs", "/Cargo.toml", "/Cargo.lock", "/LICENSE", "/README.md"]
to Cargo.toml. Note: If the crate has a
build.rs, it MUST be in the include list.
Espectacular's Cargo.toml has [[bin]] name = "ah" (not
espectacular). The release.yml was trying to copy a
non-existent espectacular binary. Fix:
Only copy actual binary names from target/release/.
The release workflow's homebrew/scoop update steps fail if
TAP_GITHUB_TOKEN secret isn't set. This is non-critical —
the GitHub release and crates.io publish are the important parts.
pi install and pi update only update the
npm package in .pi/agent/npm/node_modules/. They do NOT
re-copy skills to flat-install locations
(~/.agents/skills/, ~/.config/agents/skills/).
Any npm package that provides skills via both pi's pi-package/skills/
AND a flat-install path MUST handle re-sync in its own postinstall
script. For incitaciones, scripts/generate-pi-resources.mjs
includes reSyncFlatInstallSkills() for this purpose.
mise.toml configIf mise.toml has a custom backend tool entry with URL
templates (e.g. Julia with url = """..."""), mise may
ignore the custom template and use its own built-in plugin's URL
resolution. This can produce wrong download URLs (e.g.
1.11.9/ instead of 1.11/).
Fix: Remove the tool entry from mise.toml
and install it via an alternative mechanism (e.g. GitHub Actions'
setup-julia, setup-dotnet, etc.).
cohere/north-mini-code:free,
poolside/laguna-s-2.1:free,
nvidia/nemotron-3-ultra-550b-a55b:freeliquid/lfm-2.5-2.6b:free at 65K)limit: 0 (key only works for free models,
paid models get 402)POST /api/v1/keys with
{"name": "...", "limit": 0}limit: 0 is simplerpi --provider openrouter --model "openrouter/cohere/north-mini-code:free" -p "task"modelOverrides in
~/.pi/agent/models.json for proper metadata (contextWindow,
maxTokens, reasoning)/login openrouterauth.json works fine with
:free suffixed modelsbd) batch issue creation — workflow gotchasbd create -f <file>
is the batch path; --stdin ignores some flagsbd create -f file.md parses ## Issue Title
sections and is the reliable batch path. The --stdin /
--body-file - alias reads the description from stdin but
does honor
-t/-p/-l/--silent
flags — useful when a Python driver sets metadata per-ticket.
bd list
defaults to 50 open issues — use --all --limit 0 for full
setbd list --json silently caps at 50 open issues. To
enumerate every issue (including closed, or beyond 50) for metadata
analysis: bd list --all --limit 0 --json.
**Field:** not **Field**:In bd create -f, frontmatter (--- block)
sets type/priority/labels. The **File:** /
**Files:** label convention puts the colon
inside the bold markers (**File:**), not
after (**File**:). Regex for parsing must match
\*\*Files?:\*\*.
Creating 14 dont issues with
-l severity:low,...: the 5 LOW-severity (P3) issues lost
their severity:low label while the other 9 kept theirs.
Label propagation has a quirk at P3. Mitigation: after
batch creation, verify labels with bd list --json | jq and
re-add any dropped:
bd label add <id> severity:low.
metadata.files
must be set explicitly — body text doesn't countbd create does not parse file paths from the description
body into structured metadata. For file-based queries
(bd list --file) and shared-file analysis to work, set
metadata.files after creation:
bd update <id> --metadata '{"files":["src/main.rs"]}'
(JSON object form, not --set-metadata 'files=["..."]' which
stores as a string).
bd close
needs no flag; --note isn't a close flagbd close <id> closes with status
closed. To record a reason, use
--reason <text> or add a
bd comment <id> <text> separately.
--note is not a close-flag.
bd dolt push
requires a configured remotebd dolt push fails with "Supported remote URLs..." if no
remote is set. Add one per repo:
bd dolt remote add origin git+ssh://git@<host>/<org>/<repo>.git.
Push can fail with divergent-history on repos that had independent
bd init — use bd bootstrap to clone instead in
that case.
bd create --description
with backticks breaks shell interpolationBackticks in an inline --description="..." trigger shell
command substitution and silently corrupt/abort the ticket. Use
--body-file <path> (or --stdin)
to pass descriptions containing code, paths, or backticks.
Environment-variable expansion ($VAR) in inline
descriptions is also unsafe.
If a tag-triggered GHA publish workflow (e.g.
on: push tags) fails because of a CI config bug, fixing the
workflow on main does NOT fix the failed run — the workflow
checks out the tag ref, not the branch. The tag must be moved to the
fixed commit and re-pushed:
git tag -d v0.6.0
git push origin :refs/tags/v0.6.0
git tag v0.6.0
git push origin v0.6.0This re-triggers the publish workflow against the fixed tree.
RecursionError on truncated email HTML — use DOM-level
truncationEmail HTML (Mailchimp/Drip templates) is deeply nested tables;
truncating by regex and hand-appending guessed closing tags
(</td></tr></table></div>) leaves
unbalanced nesting that crashes WeasyPrint with
RecursionError in cffi. Instead parse with BeautifulSoup
and truncate structurally: find the signature text node, then for each
ancestor up to the document root extract() all
next_siblings. Always verify post-cut text is free of
boilerplate markers before rendering.
The incitaciones installer (install --global) rewrites
skill files including provenance injection, and it followed symlinks
into the journal repo's resources/skills/, clobbering
content with generated output. Rule: after running any installer/tool
that mutates skill directories in place, immediately
git status the source repos behind those symlinks and
revert unintended writes. Prefer point-installers at explicit targets
over blanket --global re-installs when symlinked sources
exist.
Markdown code fences in docs are never compiled — they drift silently
when APIs change. For libraries with copy-paste onboarding snippets, add
a CI-compiled mirror test (test file mirroring each snippet, or rustdoc
no_run doctests + mdbook test). Coverage must
be verifiable: every mirror test maps to a doc location, and a
"forbidden patterns" guard fails when removed/renamed API call-shapes
reappear in docs. Keep the forbidden list in a human-maintained doc
(markdown table), not in the test file — humans update docs, CI consumes
them. When bumping a crate version, grep docs for the old pin in the
same commit.
Mermaid v10+ ships ESM-only builds;
<script src="mermaid.esm.min.mjs"> from a
file:// page is blocked by CORS in Chromium, so diagrams
silently don't render. Fix: vendor the Mermaid
v9.4.3 IIFE build (mermaid.min.js, global
mermaid) next to the report and reference it with a
relative path. Keep the raw Mermaid source in a
<details><pre class="mermaid-source"> fallback
so nothing is blank without JS. (Established in incitaciones
codebase-cartography HTML export spec, 2026-09-01.)
The journal repo gets commits from multiple machines;
git pull there often rebases a long unpushed local history
onto remote and hits AA/UU conflicts in log files (both
machines appended different sessions from an empty base;
git rerere preimages may auto-replay stale resolutions).
Fix: for each conflict, concatenate both sides' session
entries under a single ## Log heading (keep one copy of
each ### Session: block), git add,
GIT_EDITOR=true git rebase --continue. Check for duplicated
session blocks before committing — rerere can glue content weirdly.
The highlight.js CDN highlight.min.js (common bundle) is
NOT a fixed superset to assume from memory — it omits julia, clojure,
and many others, while toml resolves via the
ini grammar's alias and html/svg
via xml. Fix: after the bundle, load
<script src=".../build/languages/<lang>.min.js">
for each missing grammar (self-registers into the global
hljs); verify with hljs.getLanguage(l) per
rendered language in headless chromium. (Established in michael site,
2026-09-04.)
hljs.highlightElement() sets
data-highlighted="yes" and silently no-ops on subsequent
calls to the same element (even after textContent is
replaced). Fix:
el.removeAttribute('data-highlighted') (+
data-hljs for older versions) before re-highlighting.
Symptom: "syntax highlighting lost after switching languages."
flatpak run org.chromium.Chromium --headless cannot read
file:///tmp/... test pages (sandbox) and a
--screenshot=foo.png relative path may write to
$HOME. Fix: place test HTML + assets in
$HOME, use absolute output paths, and use the real home
prefix in file:// URLs (e.g.
file:///var/home/<user>/..., not
/home/<user>). --dump-dom +
--screenshot + --window-size is a fast
zero-setup way to verify deployed public pages (deploy timing, cache
staleness, CTA fold position).
If one edits[] entry fails to match, none of the edits
apply; a later call re-applying only the failed item silently leaves the
others out. Symptom: commit message/notes claim a change that isn't in
the file. Fix: treat any partial-match error as
"all-or-nothing," re-apply the full set, and grep the actual file for
every intended change before claiming completion. (Established
incitaciones, 2026-09-05: report-template.md macro-mode edit silently
dropped, caught only by a post-commit review.)
skills
CLI (Vercel agent-skills) — flag semanticsnpx skills add <owner>/<repo> installs
interactively; the real flags are -a agent1,agent2 (agents,
comma-separated), -s skill1,skill2 (skills),
-g user-level, --copy instead of symlinks,
-l list-only. Trap: -l prints
an "installing non-interactively" banner yet performs no
install — it is safe read-only. Skills land as symlinks into each
detected agent's directory (e.g. ~/.claude/skills); default
scope is the current project, not global. (Verified 2026-09-05 against
skills add --help while documenting PhormaSci/skills — the
README flags I had written from memory were wrong.)
Skill files are agent-executed code — before committing one, extract
the block verbatim from the file and run it under
env -i HOME="$HOME" bash -c '...' (or with the target
repo's real state). Two broken implementations of the same resolver
passed casual review and were only caught by this: a sed comment-strip
that couldn't handle column-aligned inline comments in
.bashrc, and JOURNAL_PATH="$JORNAL" evaluating
empty because the referenced var doesn't exist in non-interactive
shells. Method:
sed -n '/start/,/end/p' file | sed 's/^ //' → source it.
(Established incitaciones close-skill work, 2026-09-05.)
When a script needs values from user shell config
(.bashrc etc.) and the shell is non-interactive, don't
grep+cut+sed the file: comments, column alignment, quotes, and
$VAR indirection each break naive parsing. Instead
bash -c 'source ~/.bashrc >/dev/null 2>&1; printf "%s" "${'"'$1'"'}"'
— the shell itself resolves comments, quotes, and nested indirection in
one step, and sourced vars arrive fully expanded. (Established
2026-09-05; supersedes any grep-based config-reading attempt.)
When evaluating a prompt/skill kit, every case must run twice — with
the kit and with an inert baseline (--no-skills) — same
model, same sandbox. Absolute pass/fail is misleading: strong models
pass without the kit (it's dead context weight for them), weak models
fail with it. The meaningful output is the per-model effect size.
(Established fck eval work, 2026-09-09; kit effect was +3.1 on sonnet
but ≈0 on deepseek/gpt-5.6-luna.)
For playlist harvesting: yt-dlp -J <playlist-url>
gives full metadata (views/likes/descriptions) in one JSON;
--print templates break on null-like values
(like_count prints unquoted NA, invalid JSON).
Two traversals of the same playlist (metadata dump vs subtitle download)
can label indices differently — join artifacts by video ID in
brackets, never by position. Auto-caption VTTs are rolling:
dedupe by keeping the last line of each cue body, not consecutive-line
dedup. (Established microdancing conference-corpus work, 2026-09-15; 371
talks harvested.)
When doing content analysis where the requester has suspicions, have them write hypotheses to a file, checksum it, and extract without reading it; score supported/refuted/no-signal afterward. Prevents contamination both ways — the extraction stays honest AND the requester's pet theory gets falsified, not confirmed. (Established microdancing conference analysis, 2026-09-15.)
Before pushing to a repo, check for an active concurrent session
(shifting branches, in-flight uncommitted edits, failing pre-push hooks
on files you didn't touch). If one exists, stop touching the worktree —
the issue tracker's own sync (e.g. bd dolt push to
refs/dolt/data) is the real wire protocol; passive exports
like issues.jsonl re-export commits can wait for the other
session's push. Retrying pushes against a moving worktree only creates
interleaved broken states. (Established dont/whisper session,
2026-09-15.)
chezmoi diff direction is a=live file, b=desired (what
apply would write) — the opposite of intuition; verify with
chezmoi cat <target> when deciding which side is
truth. Drift resolution per file: hand edits newer →
chezmoi add <target>; source newer →
chezmoi apply --force <target> (TTY-less sessions
block on "changed since last written" prompts otherwise).
chezmoi add on a .tmpl entry prompts to drop
the template attribute — instead edit the template content directly to
preserve {{ ... }} constructs. (Established dotfiles
session, 2026-09-16.)
2026-09-18T21:33:41Z [id:ac86d0e4ba57832f7ad543abc3ad9540db99bcb22efaee71fc29b059e1cc1051] (#bd-dolt) bd 1.0.4 'bd dolt push' to git+ssh remotes is unreliable: dolt's git-remote-cache dies with 'git rev-parse ... must be run in a work tree' and the URL loses its user part (exit 128); even healthy repos like wai show 'no common ancestor' vs their refs/dolt/data. Working pattern (dont): config no-db true → .beads/issues.jsonl is source of truth; git tracks only .beads/config.yaml + issues.jsonl (root .gitignore needs '.beads/*' with negations because global ~/.gitignore ignores .beads/); keep local embeddeddolt/ untracked — deleting it breaks bd until 'bd init --from-jsonl' restores it. Custom GIT_SSH_COMMAND breaks dolt pushes; dolt resolves ssh-config aliases itself.
2026-09-20T17:02:00Z [id:pi-orchestration-close]
(#pi-orchestration) Orquestación de sesiones pi headless
(pi --session-id <id> --name <n> -p < prompt):
(1) reusar el mismo --session-id en una re-invocación hace APPEND del
prompt a la sesión existente y puede abortar sin procesar nada — para
reintentar, usá un id nuevo e incrustá el contexto destilado del
transcript
(~/.pi/agent/sessions/<dir-slug>/<ts>_<id>.jsonl).
(2) "Connection error" o "Command aborted" al final de
pi -p NO implica fallo — verificar el estado real (bd show,
git log/push, suite) antes de re-lanzar. (3) Prompt de orquestación por
ticket: incluir claim ya tomado (o el agente aborta), pipeline del repo,
y verificar gates entre tickets.
2026-09-20T21:52:00Z [id:typos-config-layers] typos config
layers: (1) lowercase tokens (e.g. column names cacl,
otu) are matched through
[default.extend-words], NOT
[default.extend-identifiers] — identifiers only catch cased
tokens (video IDs, acronyms); verify with
typos --dump-config - that the config actually loads. (2)
typos applies config excludes only during directory walks —
explicitly-passed file args bypass exclusion, so a lefthook-style
staged-file pipeline needs its own grep filters for out-of-scope content
(es trees, session notes). (3) beads'
core.hooksPath=.beads/hooks chains to lefthook — when
debugging hook behavior, check which wrapper actually fires. (talleres
session, 2026-09-20.)
~/.config/containers/systemd/jellyfin.container →
systemctl --user start|stop jellyfin (manual only, no boot
auto-start).~/PROJECTOR in Jellyfin layout
Show (Year)/Season NN/, mounted read-only at
/media in the container. State in
~/jellyfin/{config,cache}.projector-queue (queue
~/Downloads/MOVIES) → to-projector (repo
para/areas/dev/gh/charly/canticos/, symlinked into
~/.local/bin) which sorts output into the Jellyfin layout
by default; --no-jellyfin for flat USB-projector
drives.Movies/ and
Shows/ trees, one Jellyfin library each. A movie file
inside the TV library root gets picked up as a bogus series (e.g. folder
→ series, file → Season/Episode) — keep the two trees disjoint.media.mblink files in
~/jellyfin/config/root/default/<lib>/ must have NO
trailing newline (echo breaks it; use printf).
Symptom: "Could not find a part of the path" in logs, library silently
empty, and restarts don't always re-trigger a scan.~/jellyfin/config/data/jellyfin.db, table
BaseItems (Name/Type/Path).z-ai/glm-5.3-flash mode collapsez-ai/glm-5.3-flash is routed through 33+
providers on OpenRouter with different quantization levels
(fp4→fp8). During peak US hours (8AM-9AM ET = 9AM-10AM ART) Z.AI's
native API congests and OpenRouter falls back to cheap fp4
(4-bit) providers. At fp4, the model's attention mechanism
collapses under complex tool-use + high thinking load, producing streams
of random words (mode collapse).
Symptoms: Assistant message starts with plausible technical jargon, then free-associates into random words (hospital beds, diseases, etc.), model self-identifies drift ("Wait — I notice I'm drifting again") but can't stop. OpenRouter eventually aborts with "Operation aborted."
Diagnosis:
curl -s https://openrouter.ai/api/v1/models/z-ai/glm-5.3-flash/endpoints | python3 -c "
import json,sys
for ep in json.load(sys.stdin)['data']['endpoints']:
n=ep['provider_name']; q=ep.get('quantization','?')
u1=ep['uptime_last_1d']
if n == 'Z.AI' or 'fp4' in q:
print(f'{n:20} {q:6} 1d={u1:.1f}%')
"
If Z.AI uptime < 99% or fp4 providers degraded, switch model.
Safe hours (ART): 10AM (0.25% failure), 12PM
(0.26%), 7PM (0.38%) Risky hours: 9AM (peaked at 13% on
Sep 30), 2PM (1.78% baseline) Alternative:
deepseek/deepseek-v4-flash routes through providers with
fp8 minimum quality floor.
**just note-stdin <cat> <title> can fail
with categoria: unbound variable when invoked from agent
shells (set -u + just interpolation quirk). Fallback that always works:
write the .org file with
#+TITLE:/#+DATE: frontmatter directly into the
category dir (exploraciones/, machetes/,
etc.), then
just --justfile ~/para/areas/dev/gh/ak/borradores/justfile sync "<commit msg>".
Filename = kebab-case title + .org.
{"model":"jev-latest","state":{...},"questions":{...}} —
verified live.findings key (sibling of state/questions)
returns api_usage_error: Invalid request. Fold findings
into state (e.g. state.findings or
state.code_evidence) and reference them inside each
question's instructions text instead of
instructions.finding backtick-refs.verified below
gate stays REVIEW_REQUIRED → escalate to human, don't drop it.Beads tickets carry
afk/design/hitl/review
+ complexity:s/m/l/xl labels. afk = safe to
run autonomously (requires an objective gate — gateless repos get
no-gate instead). Grade gate: S/M tickets touching public
API/invariant/concurrency code are mis-tagged → retag L; L fully covered
by existing pattern+test → retag down. Escalation is a retag: 2 failed
attempts → complexity+1; semantic failure → lane=design. Schedulable
filter: bd list --label afk --label complexity:s. Full
criteria in incitaciones
content/distilled/issues/references/create-issues/SKILL.md
v1.5.0.
GLM-5.3-flash (OpenRouter, ~$0.003/msg) is the default for known-shape implementation. Escalate to gpt-6.1-sol (Codex plan) for debugging/CI/design — unknown-shape work should always try GPT first since the cap is fixed-price. Astra-xhigh reserved for semantics work (dont/wild/specodelic). Luna for GPT-side batch chores. Baseline facts mined from pi sessions 2026-10-06.
Reference implementation: specodelic repo
(.wai/resources/pipelines/epic-orchestrator.toml,
.wai/resources/templates/subagent-brief.md,
scripts/epic-status.sh, specodelic-k9v). Established
2026-10-07 after the 2026-10-06 session proved the pattern works (2
tickets landed) but needed 5 user corrections in the first hour.
The rule: orchestration must be tooling-enforced, never improvised mid-session. An orchestrator pipeline whose steps ARE the delegation loop; executor pipelines (tdd-ro5) run INSIDE the subagent. Five failure modes, each with its enforcement point:
--no-session) —
usage/cost records lost forever, session undiscoverable. Enforcement:
spawn contract in a step prompt: pi -p "$(cat brief)" +
-n "subagent:<ticket>:<steps>", never
--no-session.stop — "one ticket per run, do not chain; name the next
candidate but do not start it".just epic-status-style snapshot reading
.wai/pipeline-runs/*.yml (current_step vs step count in the
TOML), bd in-progress, unpushed commits, plus a session audit flagging
subagent-shaped runs (# Task: first message) that carry no
-n "subagent:" name.wai pipeline next, bumped current_step 4→8). Enforcement:
brief template says "advance ONLY via wai pipeline next,
never hand-edit run YAML"; orchestrator verify step checks the run file
reached its final step and that artifacts match the claimed steps.Per-ticket brief overhead: keep shared boilerplate (repo facts, ratchets, commit hygiene, scope-guard shape, report format) in one template; per ticket fill only task/scope placeholders. The subagent report format ("Commits / Gates run / Deviations / Next") makes orchestrator verification diffable against reality.
pi session-dir naming for status scripts: cwd with leading
/ stripped, /→-, wrapped
--…-- under ~/.pi/agent/sessions/.
A [user] fallback in ~/.gitconfig MUST
precede [include] ~/.config/git/private: git processes
includes at the point they appear and later assignments win, so a
fallback placed after the include silently overrides every
profile identity. Verified both branches (ak profile → akielbowicz;
unmatched repo → fallback). Dotfiles commit 39f6431.
When adding a new GitHub-account ssh profile
(~/.ssh/profiles/<alias>), always set
IdentitiesOnly yes alongside the IdentityFile.
Without it the ssh-agent's loaded keys are offered first and
ssh -T git@<alias> can authenticate as the WRONG
account (observed 2026-10-13: new at/arltificial profile
answered as akielbowicz via an agent key). Existing profiles (ak, ch, …)
predate this and work because the agent usually isn't loaded with those
keys — don't rely on that.
Per-account GitHub identity on this machine spans FIVE files; missing
any one causes silent wrong-account behavior. For a new dir
~/para/areas/dev/gh/<dir>/ with alias
<alias> and GitHub login <account>
(all paths relative to $HOME):
~/.ssh/id_<alias> — ed25519 keypair (no
passphrase, machine-local)~/.ssh/profiles/<alias> —
Host <alias> → ssh.github.com:443 + IdentityFile +
IdentitiesOnly yes (see rule above)~/.config/git/private —
[url "git@<alias>:<account>/"] insteadOf
rewrites + [includeIf "gitdir:…/gh/<dir>/"] →
profiles file~/.config/git/profiles/<alias> —
user.name/user.email (emails live here only, untracked)~/.config/nushell/private.nu — PWD env_change hook
branch: gh_base/<dir> →
$env.GH_TOKEN = (gh auth token -u <account>)Gotchas for touchpoint 5 (bit us 2026-10-13, gh/at — agent did 1–4, missed 5):
gh then reports the WRONG account (or 404s on the new
account's private repos — keyring has no token for it).gh auth token -u <account> errors (account not in
keyring yet) and leaves the stale token.gh auth login refuses to run while GH_TOKEN is set;
workaround: hide-env GH_TOKEN; gh auth login (interactive,
user-side).Verification checklist (run in the new repo):
git config user.name && git config user.email →
resolves to profile valuesgit remote get-url origin →
git@<alias>:<account>/<repo>.gitssh -T git@<alias> →
Hi <account>!gh auth status → shows <account> as
GH_TOKEN/active in that dir[include]-file values are inserted at the include point;
assignments later in the including file override them. When testing
override chains, test BOTH branches — a lazy test (same layout twice)
let the broken ordering slip through once.
bd close updates the embedded Dolt DB but
bd sync does NOT reconcile .beads/issues.jsonl
— the jsonl stayed stale ('open') after sync reported complete. Working
pattern: run bd export --output .beads/issues.jsonl
explicitly, then commit + push (use git add -f — the
.beads dir matches ignore rules and plain add errors
despite the file being tracked).checksums.txt from its
latest GitHub release, run the repo's own
scripts/update-homebrew.py <tap formula path> <ver> <tag> <checksums>
+ update-scoop.py, commit and push. Formula filenames
follow the BINARY name (espectacular →
ah.rb/ah.json), not the repo name. Note
lefthook's ruby-syntax pre-commit/pre-push hook fails in non-ruby shells
— push with --no-verify (formulas are script-generated,
same as CI).