__fish_config_sync_logging carried its own inferior copy of the
paru/yay wrapper generator (tee-based, no PTY, no progress-bar
rendering, hard-coded /usr/bin/paru|yay) alongside the canonical
version-6 generators in conf.d/paru-wrapper.fish and
conf.d/yay-wrapper.fish. Both wrote the same file with different
version markers and the same misattributed provenance comment, so
whichever ran last won and a subsequent C5 toggle would flip it back.
See startup-latency-JOB-BRIEF-FINDINGS.md §2.
__fish_config_sync_logging now delegates entirely to the canonical
generators instead of carrying a copy: they already resolve the real
binary via __fish_real_command (never /usr/bin-assumed) and
independently gate on their own C2/C5 keys, covering both the
enabled-regenerate and disabled-remove cases.
One behavior change falls out of delegating rather than special-casing
around it: a wrapper is no longer generated when C2 (paru-autoexec /
yay-autoexec) is disabled, even if C5 logging is on. The removed
sync-logging copy never checked C2, so it could reinstall a wrapper
the user had explicitly turned auto-exec off for.
Adds functions/_fish_source_scoped.fish: a small helper that runs
source inside its own function-call boundary. source itself runs in
the caller's scope, so a bare return inside a sourced conf.d guard
(both files have several) would otherwise unwind whatever function
called source directly -- verified with a minimal repro before relying
on it. Routing through this helper contains the return to just that
call, so calling paru's generator and then yay's actually reaches the
second call.
Manually verified end-to-end in an isolated HOME/XDG sandbox with a
stubbed paru/yay: enable generates both v6 wrappers, disable removes
both and drops the sentinel, re-enable regenerates them and clears the
sentinel.
tricks.fish is sourced twice per shell on CachyOS: once by the conf.d
autoload, once forced by config.fish to re-win over the distro's own
tricks.fish (measured 4.55ms for the second pass, see
startup-latency-JOB-BRIEF-FINDINGS.md §1). The bang-bang functions,
aliases, and history override further down need to re-run on both
passes since those are what re-assert over the distro config, but the
PATH/MANPAGER setup does not.
Gates fish_add_path and the type -q bat probe behind a once-per-session
global, guarding only that block. Everything else in the file, and the
coupling with config.fish's own cachyos-tricks guard, is unchanged.
14 cases. The production registry has 65 entries all carrying exactly one
tag and no always/* anywhere, so these three documented semantics are
unreachable from real data -- the synthetic keys/values table is the only
way to reach them. Reasoning recorded at the fixture.
Also pins the degenerate no-slash tag and reaches C5 through
__fish_config_op_enabled, which is the path production code takes.
8 cases on the real __fish_config_op_logging name. AGENTS.md records this
as a deliberate special case agents keep trying to 'fix' out; three of
these cases exist specifically to turn that into a test failure.
Includes the three subcategory cases proving the opt-in is inherited
through chain[-1] rather than special-cased per subcategory.
13 cases on invented variable names, so nothing ambient can perturb them:
default-on, explicit truthy/falsy, unrecognized-defers, subcategory
overriding the category in both directions, and the four cases that
together pin the master as an off switch only.
21 cases: a precondition asserting the fork's registry is loaded, then
truthy/falsy/unset/empty/unrecognized/no-argument/list.
Runs isolated, which is what lets these cases manipulate real guard
variable names without being able to reach the user's universal variables.
Spawns one non-interactive child against the sandboxed config and asserts on
what loaded -- abbreviations, key bindings, bang-expansions, the C5 event
handlers and the tailscale completion machinery must all be absent. Exit
codes name which guard regressed. No wall-clock assertions, so it cannot
flake.
run-tests.fish globs tests/test-*.fish and runs each in the mode the suite
declares in its own header. Isolated is the default and no typo can
promote a suite to in-session: detection is case-insensitive so a
near-miss is caught, the value comparison is exact.
In-session suites share one sandboxed session; isolated suites each get a
--no-config child with temp XDG dirs. Counts come back through a file so
output keeps streaming.
functional.fish becomes test-session.fish, its 15 predicates rewritten as
check calls. 332 assertions, unchanged per-phase.
docs/fish-config.1 is left for CI to regenerate: the local pandoc is 3.10.2
against CI's 3.1.3, so rebuilding it here emits ~4700 lines of formatter
churn unrelated to this change. The component registry rebuilt identically.
Follows the relocation out of conf.d/. The completions/ branch of the file
tree is expanded from the real directory at build time, so it needs no
hand-written children. Generated docs are regenerated separately.
check/section/report and the counters move out of the vault suite
unchanged. report also writes its counts to $FISH_CONFIG_TEST_COUNTS so a
driver can aggregate without parsing stdout, and ends on an explicit
boolean per AGENTS.md item 5.
All 317 vault assertions and every fixture helper are untouched.
Neither file defines a function or sets a global; their only effect is
writing ~/.local/bin/<tool>, which every interactive session does anyway.
Combined 10.7 ms off every fish -c.
__fish_config_sync_logging ran on every fish -c, mkdir+touching the C5
sentinel on disk from every subshell. Its consumers — the Kitty watcher and
the paru/yay wrappers — are interactive-context, and every interactive shell
still reconciles the state.
run-tests.fish executes under the config it tests, which shadows cp, rm
and cat. The real hazard is cp: the config aliases it to 'cp -i', which on
a non-empty destination reads EOF in a non-interactive runner, silently
skips the copy and exits 0 -- a sandbox missing config files, reported as
success.
rm -rf and cat were measured and behave correctly as-is (the rm wrapper
bails to command rm on any non-recursive flag, so -rf really deletes and
does not trash). Prefixed anyway: a test runner must not depend on the
configuration under test.
The --on-variable PWD handler backgrounds a git fast-forward. A script that
cd's was firing it, which is also where AGENTS.md Task #4's credential
prompt could surface from a background job.
fish_color_* is consumed only by the syntax highlighter. The guard sits
below the existing cleanup branch so stale-FZF_DEFAULT_OPTS cleanup keeps
running where it does today; the FZF value itself is a persisted universal
and survives regardless.
The suite ran under a plain `fish`, which loads the user's real
~/.config/fish and their universal variables -- this repo doubles as that
config. A test manipulating a guard variable could erase a real universal
variable out of the running shell.
Override XDG_CONFIG_HOME/XDG_DATA_HOME and pass --no-config. HOME stays
real on purpose: overriding it makes the suite's two hermeticity
assertions vacuous. Reasoning recorded at the call site.
Vault suite still 317/317 with byte-identical stderr.
Defines fish_prompt only. The guard precedes the op-guard and the type -q
PATH scan so both are skipped in scripts. Scripts fall back to the repo's
autoloadable functions/fish_prompt.fish, which nothing invokes anyway.
Thirteen lines, every one a `complete -c cheat` registration, sourced on
every shell start from the wrong directory. Startup cost was already ~0
because its command substitutions are lazy, but completions/ is where fish
expects the file and the move is free.
The Cobra-generated block ran `complete --do-complete "tailscale "` to flush
pre-existing completions before erasing them, which executed the tailscale
binary. From completions/ it has no job: fish autoloads only the first match
on $fish_complete_path and the repo's completions/ precedes the vendor dir,
so the vendor file is never sourced.
Verified byte-identical completion output across four probes with the vendor
file present. A comment at the deletion site records the reasoning.
conf.d/tailscale.fish is 252 lines of Cobra-generated completion that fish
sourced on every shell start, and its self-priming block executed the
tailscale binary to warm the completion cache. completions/ is the directory
fish autoloads on first <TAB>.
Measured: 19.2 ms off both interactive and non-interactive startup.
UnoCSS's .i-pajamas:gitea rule is unlayered CSS; our override lived in
@layer starlight.core, and unlayered rules always beat layered ones
regardless of specificity or source order. The Gitea icon has silently
stayed at UnoCSS's 1em default since it was first added -- confirmed via
computed style in the browser, not just reading the stylesheet source.
!important restores the override across the layer boundary.
Also drops the GitHub icon from 2x to 1.5x per visual feedback.
pandoc wasn't available when this branch's earlier commit ran
build-manual.py --concat; regenerate docs/fish-config.1 from the
current docs/fish-config.md now that it is.
Adds a GitHub social icon to the docs site header alongside the existing
Gitea one, and documents in the README's Contributing section that
git.rootiest.dev is the base repo while the GitHub copy is a one-way
mirror, so forks/issues/PRs should go through Gitea.
Adds Testing, Contributing, Attribution, and License sections to the
manual/man page/site, sourced directly from README.md via a new
`<!-- README: Heading -->` placeholder mechanism in build-manual.py, so
the README stays the single source of truth for those sections instead
of a hand-maintained copy drifting out of sync.
Also converts docs/manual/00-table-of-contents.md from a hand-typed list
to a generated one (mt.walk()-driven), fixing a numbering drift where
Components Reference was omitted and every section after it was off by
one relative to its own manTitle.
Five fixtures forced a failure with chmod 500 on a parent directory. That
is silently useless for root: uid 0 bypasses the mode bits, the operation
succeeds, and a test asserting a failure path then reports the tool as
broken rather than the injection as ineffective.
CI runs the suite as root inside a container, so 21 checks failed there
while passing for every developer and every reviewer -- the same shape as
the git-identity gap in the previous commit. A fixture that quietly stops
injecting is worse than one that never existed: it reads as coverage.
Shim the single command each failure hinges on, matched by a path fragment
so nothing else in the run is disturbed. A command that exits 1 on purpose
does so for every uid. Which command matters: ensure_symlink removes an
existing link before it creates the new one, so the two adopt fixtures
have to fail at that rm to leave the live link in place, and shimming ln
would be too late. The restore fixture deletes the link first and so does
hinge on ln, and the failing --remote fixture hinges on git set-url.
The one remaining chmod 500 asserts a success, not a failure, and its real
proof is the direct assertions on where the stash landed.
Reproduce the root case locally without a container:
unshare -r fish tests/test-agents-vault.fish
Verified: 296/317 as root before, 317/317 after; 317/317 unchanged as an
ordinary user; and neutering the shims reproduces exactly 296/317, the
count CI reported.
The suite was hermetic against $HOME and ~/.claude but not against git
config. new_repo set an identity on the repos the tests create; nothing
set one on the repos the tool creates -- the scaffolded vault and the
AGENTS/ sub-repo. Those inherited a global user.name/user.email on a
developer machine and committed fine, so the gap was invisible to every
local run and every review; on a runner with no gitconfig they died with
"Author identity unknown", and the suite reported the tool own correct
"nothing recorded" handling as 71 failures.
Supply the identity through the environment, which reaches the git calls
inside agents-vault and agents-init as well as the ones the tests make.
Pin commit.gpgsign off for the mirror-image reason: a developer with
signing enabled would otherwise have tool-created repos reach for a key,
and a hardware token would prompt for a touch partway through the run.
Pin init.defaultBranch because the rebase fixtures build an upstream and a
clone and need the two to agree on a branch name. A global saying main and
a bare default of master disagree, and the fixture then fails to create the
rebase the test is about, reporting a tool failure that never happened.
Verified with GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null:
267/317 before, 317/317 after; unchanged at 317/317 with a normal config.
The connect bound was delivered by injecting GIT_SSH_COMMAND, and an
environment variable outranks git's core.sshCommand -- so the guard,
which read only the environment, did not merely miss a configured ssh
command, it overruled one. A vault remote reachable only as
`ssh -i ~/.ssh/vault_key` failed to authenticate on every push, autopush
and --push alike, for the sake of a ten-second timeout. Both spellings
now count, and `set -qx` rather than `set -q` on the environment side so
an unexported fish variable -- which git never sees -- does not leave the
push with neither the user's ssh command nor a bound.
The agy knowledge walk appended each find with `set -a`, which rewrites
the whole variable every time; 500 files cost 21ms but 20,000 cost 58s,
on a path that runs in front of every agent launch. The walk now prints
NUL-separated and the list is built once, which is flat: the same 20,000
files take 707ms. NUL rather than newline because a filename may legally
contain one. What the walk collects, and its symlink and dot-led
semantics, are byte-for-byte unchanged.
Autopush is bounded by timeout(1) alone, so without it the launch path
was quietly back to an open-ended network call. It now says so and skips
the push instead; --push was never wrapped and is unaffected.
The agy knowledge allowlist walked the store with `**` and copied with
plain cp, so a symlink inside the store was both followed and dereferenced.
The extension rule still bounded what kind of file was collected, but not
whose: a link to a home directory hands over settings.json, CLAUDE.md and
every cached .json in it, and those reached a commit. A link to / made the
walk itself unbounded, on the path that runs before every agent launch.
The tree is now walked a level at a time and nothing that is a symlink is
followed or copied.
Autopush had the same shape one layer out. Neither GIT_TERMINAL_PROMPT nor
GIT_ASKPASS closes a socket, and git has no connect timeout to set: against
a blackholed address a push took 135s with http.lowSpeedLimit and
http.lowSpeedTime set as well as without them. ssh can time itself out and
is now told to; the autopush pull and push are additionally capped with
timeout(1). An explicit --push stays uncapped, since it is watched and has
to report what a real transfer really did.
Also: scaffold /.migrate-stash into .gitignore beside /.adopt-stash, which
the comment already claimed was covered; and drop the live memory path
during a slug migration only when it is a link. Reached from the
path-derived fallback candidate it can be a real populated directory, where
rm -f correctly refuses -- but said so in rm's voice, so a --silent run that
had succeeded printed what read as an error.
Six findings from the whole-branch review, all of which end in the same
place: a backup tool reporting success while nothing was backed up.
Slug migration nested the old entry inside the new one. The clear before
the rename was gated on the destination's claude/memory subdirectory
rather than on the destination itself, so an entry that exists without
one survived, `git mv A B` moved A *inside* B, and the mkdir below
fabricated a fresh empty memory directory for the live link to point at.
The real memory ended up one level deeper than --status and --restore
ever look, and the run returned 0. That shape is not exotic: git cannot
track an empty directory, so an entry committed while its memory was
empty comes back from a clone as projects/<slug>/origin and nothing
else -- and cloning the vault is this feature's own recovery path. The
destination is now moved aside the way --adopt already does it rather
than deleted (widening the rm -rf would have destroyed the clone's
origin log), its provenance is folded into the migrated entry, and every
failure path rolls back and reports.
The launch path pulled over the network. Both wrappers call agents-vault
synchronously before starting an agent, and the pull in the shared sync
helper was unguarded once an upstream existed: against a blackholed
remote it blocked the launch indefinitely and then aborted the commit,
so an offline laptop silently stopped being backed up at all. Committing
never needed a remote, so the pull moved to the push path, which was
already opt-in for exactly this reason. A failure there now distinguishes
a real rebase conflict (rebase-merge/ or rebase-apply/ present) from an
unreachable remote instead of calling both a conflict, and both network
calls set GIT_TERMINAL_PROMPT=0 and GIT_ASKPASS so they fail fast rather
than prompt with nobody watching. The helper still refuses to commit a
rebase in progress, and leaves it standing rather than aborting one it
did not start. This also restores agents-init's pre-refactor ability to
commit while offline.
The agy knowledge copy was unfiltered. The allowlist held at the agy root
and nowhere below it, so a planted .credentials.json inside knowledge/
was committed verbatim while the documentation promised nothing new
upstream added could leak in. Only *.md and *.json are copied now --
which is what the store actually holds -- so lock files, transcripts and
conversation databases are excluded by having no business in a backup
rather than by being known about. The scaffolded .gitignore also ignored
only the SQLite sidecars and not the databases, which is worse than
ignoring neither: a torn database landed in history with the write-ahead
log that would have completed it deliberately excluded. Both changes are
template-only, on a feature that has never shipped.
agents-init reported success when nothing was committed. It ended on a
branchless `if` with no arm for a failed commit, which fish resolves to
0 -- the same false zero already fixed in agents-vault, left in the
function the refactor was rewriting. It now has the arm and an explicit
final status.
The --adopt forward-failure path with no stash left a raw coreutils `mv:`
line and no statement that the adopt had been abandoned cleanly; it is
branded like every other error exit in the function.
Tests: the suite now clones a vault with git and runs agents-vault
against the clone, instead of trusting hand-built fixtures to have shapes
git can actually produce -- that blind spot shipped both of the merge
blockers. The "present but empty" migration fixture is rebuilt as the
origin-only directory a clone leaves behind, with the hand-built shape
kept as a separate case. Reverting each fix drops the suite from 285 to
279 (migration), 261 (network), 275 (knowledge allowlist) and 283
(agents-init status).