Adds two checks to the shared-palette section, extending the existing
colored list with the functions converted in this branch (config-help,
fish-deps, gi, git-clean, mkrep):
- Every listed function's --help must contain the session's own c_head
and c_cmd escapes -- not just some escape sequence, which the existing
presence check already covered but which a wrong-role bug (still some
color, just not the right one) sails through undetected.
- Every escape sequence found in --help output must be one of
c_reset/c_head/c_cmd/c_flag/c_arg/c_dim. c_warn/c_err/c_ok/c_accent/
c_sel/c_hi are legitimate elsewhere but never in --help text; reaching
for one there is always a bug, which is exactly what logs and
smart_exit did (static c_accent green for the command name).
Verified against the pre-fix smart_exit.fish (58d5cf3~1): both checks
fail on it -- missing c_head on "Options:", and a disallowed \e[32m for
the command name -- confirming this isn't a tautological pass.
Both link paths -- --remote and the --server/$GIT_SERVER link-existing
branch -- called `git remote add origin` bare. That fails with "remote
origin already exists" whenever the target is already linked, which is
the normal case for `mkrep .` against an existing checkout and for any
rerun against the same target. The failure took the whole call down with
exit 1.
Route both through a new _mkrep_add_origin helper: add when there is no
origin, accept and report when origin already points at the requested
URL, and refuse when it points elsewhere. A different URL is a different
repo, so repointing stays the caller's explicit decision rather than a
silent rewrite of a checkout mkrep may have been aimed at by mistake.
The gitignore-anchoring check quoted arguments fish_indent removes, so
run-tests.fish failed its indent lint and exited 1 on main even though all
753 assertions passed.
The non-git fallback used a substring match, so a negation line such as
!AGENTS/foo counted as the pattern AGENTS/ already being ignored and the
real rule was never appended.
Resolving the root to (pwd) meant running an agent CLI anywhere created an
AGENTS/ git repo, an AGENTS.md, two root symlinks, and a docs/ tree in that
directory. Scaffolding now requires a git repo or a pre-existing agent file
(AGENTS.md, CLAUDE.md, or AGENTS/); elsewhere it is a no-op.
Two related defects on the `$GIT_SERVER` auto-create path added in #150.
The test suite was creating repositories on a live forge. mkrep resolves a
server from `$GIT_SERVER` plus `$GITEA_URL`/`$GITEA_HOST`, and this repo
doubles as a real ~/.config/fish where all of them are exported, so the six
sections that call a bare `mkrep <dir>` took the auto-create branch and
contacted the server. That is how an empty `rootiest/repo` came to exist on
git.rootiest.dev on 2026-09-14 (since deleted). It also explains why those
cases looked flaky rather than broken: standalone they passed, because the
repo existed and mkrep linked instead of creating, while under
run-tests.fish they failed with `Error: no available login` — a throwaway
XDG_CONFIG_HOME leaves `tea` with no credentials. Passing for that reason is
worse than failing.
The suite now neutralizes all five variables once at setup, the same
shadow-to-empty pattern already used per-section for --check-existing, whose
comment flagged this exact hazard. Nothing depended on the ambient value:
every section wanting a server sets its own `set -lx GIT_SERVER`, and one
section asserts the opposite outright.
mkrep itself now confirms before an implicit create. Creating a repository
on a forge is the only outward-facing thing mkrep does, and on this path an
exported variable is all it takes to reach it, so `mkrep foo` — which reads
as purely local — would make a repo on a server without saying so. It asks
first, defaulting to no; declining leaves the local repo with no remote and
still exits 0. Provenance is what gates the prompt, not the resolved value:
--server, --remote and --new-remote all state outright what they will do, so
none of them prompts, and --yes skips the question. Where it cannot be asked
(a script, a pipe, any non-interactive shell) creation is skipped rather than
assumed, with a note on stderr naming the flags that would allow it.
Nine new assertions cover the skip, its stderr note, --yes, and --server not
prompting. The interactive read is verified by hand under a PTY, both
answers, but is not in the suite: that needs a pseudo-terminal, and the
answer parsing it guards is a single `string match`.
`md` forwards every argument to marktext untouched except two flags of its
own: `--read-only`/`-r` and `--foreground`. By default it detaches via
`bkg`, so the shell stays usable and the editor outlives the window that
launched it.
MarkText has no read-only mode, so `-r` sandboxes it with firejail, binding
each named file read-only. The subtlety is that MarkText is single-instance:
a plain launch hands the file to an already-running, unsandboxed, writable
window and exits, silently defeating the sandbox. `-r` therefore also passes
a private `--user-data-dir`, which forces an independent instance the
read-only bind actually covers, plus `--no-sandbox`, since Electron's own
sandbox needs the user namespaces firejail has already taken away.
Flags whose entire purpose is terminal output (`--version`, `-v`/`--verbose`,
`--debug`) imply `--foreground`; backgrounding them would send the output
you asked for to /dev/null.
The function is autoloaded and so never shadows an `md` function or alias
defined elsewhere -- fish only looks in functions/ when nothing named `md`
exists. A real `md` *binary* would be shadowed, so the body hands off to it
verbatim whenever marktext is not installed. No conf.d file and no
opinionated guard: `md` is a novel name rather than a command shadow, the
same as `bkg` and `detach`.
mkrep only ever runs git init, never a commit, so a freshly created
repo has no HEAD yet. The gitea/gitlab default templates chained
`git push -u origin HEAD` unconditionally after linking the remote,
which fails immediately regardless of the remote ("src refspec HEAD
does not match any") -- reproduced by a real user hitting it on the
first mkrep --server call. Guard the push on HEAD actually resolving
to a commit; skipping it is the correct outcome (nothing to push yet),
and a real push failure once a commit exists still propagates.
$GITEA_HOST/$GITLAB_HOST are bare hostnames (git.example.com); only
$GITEA_URL/$GITLAB_URL are expected to carry a scheme. mkrep was using
_HOST values as-is, producing a schemeless clone URL when only the
_HOST var was set.
mkrep can now pick a git host from --server, $GIT_SERVER, and
$GITEA_URL/$GITEA_HOST/$GITLAB_URL/$GITLAB_HOST (base URL only -- type
must come from --server/$GIT_SERVER, so setting those URL vars for an
unrelated tool can't silently turn a plain mkrep call into a
remote-creating one). Before creating anything it checks via
gh/glab/tea whether the repo already exists and links instead of
recreating it; --check-existing runs just that check and reports
without touching remotes.
git branch -vv marks column 1 with '+' (not '*') for a branch checked
out in another linked worktree. Only '*' was stripped from $1, so a
gone branch shown with '+' left a bogus "+" entry in $gone_branches
that then failed to delete: error: branch '+' not found.
Add a regression case reproducing the '+'-marked gone-branch line via
the existing git-clean mock handler in tests/test-network-fish.fish.
mkcd's create-and-cd behavior plus git init, remote linking, and
optional remote creation via a user-configured command template
($MKREP_REMOTE_CMD, {name}/{user} placeholders).
The guard added in 208ad95 had no test. This is the failure worth
covering, because it is the one that does not announce itself: with an
empty dump every row renders as DEFAULT, indistinguishable from a config
where nothing is set, so the user toggles from a false baseline.
Reaching the guard needs a real terminal -- the isatty check sits in
front of it -- so the case runs fish under a pty via python3's stdlib
pty module, which this suite already depends on. An empty
__config_settings_state is shadowed in to fake the failure.
The pty reader's 15s deadline is load-bearing rather than defensive: if
the guard regresses, config-settings does not fail, it opens the TUI and
blocks on getch(), so an unbounded read would hang the suite instead of
failing it. Verified both ways -- passes with the guard, and with the
guard removed the deadline fires and the case fails with a legible
excerpt rather than a screenful of escape sequences.
config-settings is now a launcher for scripts/config-settings-tui.py,
drawn with Python's stdlib curses. The seven fish files that hand-rolled
the ANSI renderer are gone, along with the golden harness that had to pin
their byte-exact output.
The TUI is a child process, so it can neither read the session's global
variables nor write them. State goes in as a dump from the new
__config_settings_state; the edits come back as a fish script that
config-settings sources, which is what lets the Session page's `set -g`
land in the caller's shell instead of in a child that is about to exit.
Every edit is emitted as a call to __config_settings_apply or
__config_settings_set_value, so list splitting, the SCROLLBACK_HISTORY_*
export mirror and the shadow-warning suppression all stay in the fish
layer that already owned them.
The consequence, and the one behaviour change: edits are applied in one
batch on exit rather than on each keypress. The status bar shows a
pending count.
New: `/` filters the current page, and on the Universal and Session pages
it reaches into every category's sub-categories, listing hits as
"Category › Sub" so a sub-category can be toggled without drilling into
its parent first. Also a `?` help overlay, mouse selection, and a
drill-down page that leads with the category's own toggle.
Gone with the renderer: the four width tiers, the wrap-aware erase
arithmetic, the stty/dd/od raw key reader, the panel-height bookkeeping
and the hand-written redraw differ. curses owns all of it, and the alt
screen plus absolute addressing makes the desync class behind 608b022,
4210f3b, 93fc5e0 and 3c4f720 unreachable.
The sub-category taxonomy is NOT duplicated in Python: it travels in the
state dump, still sourced from __config_settings_subcats. The category,
Sponge and Paths row tables move into Python, consolidating the two
copies the fish renderers kept.
Dependency: python3 with curses. Stdlib on Arch, Fedora and a full
Debian/Ubuntu python3; python3-minimal alone lacks _curses. The launcher
checks for both and names what is missing. Called out in the README.
Verified: 416/416 assertions, plus a live end-to-end in a sandbox HOME
confirming the Universal page writes universal scope only (U1/G0) and the
Session page global scope only (U0/G1).
Add scripts/config-settings-tui.py, a stdlib-curses prototype of the
config-settings interface, plus a gate that runs its --self-test.
The backend is stubbed: values live in an in-memory dict and nothing is
read from or written to fish variables. This is here to evaluate the
render engine and the interaction model before committing to a rewrite.
Why curses rather than more ANSI arithmetic:
- No flicker, structurally. curses diffs its virtual screen against the
physical one and emits only the changed cells, which is what
__config_settings_diff_redraw.fish reimplements by hand.
- Alternate screen plus absolute addressing. Stray output cannot desync
the display, so the bug class behind 608b022 (fish's read prompt),
4210f3b (a shadow warning on stderr), 93fc5e0 and 3c4f720 (line wrap
breaking the erase height) cannot occur at all.
- Resize is a repaint rather than wrap-factor arithmetic.
- Overlays, panes, live filtering and mouse input cost a few lines each.
The layout departs from the current single panel: a page sidebar with a
live filter on the left, a scrolling detail pane on the right, a help
overlay on '?', and mouse selection. Sub-category drill-down, the
tri-state badges and the Sponge/Paths value rows all carry over.
Dependency note: python3 with the curses module. That is stdlib on Arch,
Fedora and a full Debian/Ubuntu python3; python3-minimal alone does not
carry _curses, so the test asserts the import.
The prototype is not wired into config-settings and nothing existing
changed.
Comments in tracked files pointed readers at AGENTS/specs/*.md,
AGENTS.md, and JOB-BRIEF-FINDINGS.md -- all gitignored, transient
dev notes not part of the base repo. Rewrote each to be self-contained
or point at tracked docs (docs/fish-config.md) instead.
- Print the missing exit-repaint after the inline editor's
__cs_dispatch_draw call (was silently changed to capture-only earlier
in the branch; this call site was missed, blanking the panel on
commit/cancel of an inline edit).
- Hoist (count $prev_edit_frame) out of a quoted math string in the
inline editor's per-keystroke redraw -- fish doesn't expand a command
substitution there, so math silently errored.
- Add a >= 52 column floor to both diff-path guards: below the
narrowest tier's own 52-column box width, lines wrap and the diff
path's one-physical-row-per-logical-line walk corrupts the display.
- Reword a stale test comment that described panel_h as mirroring a
hand-set constant in __cs_dispatch_draw; it derives panel_h from real
output now.
- Declare prev_edit_frame with -l alongside edit_frame instead of a
bare set, matching the file's convention.
- Move prev_frame's declaration to its point of use instead of an
empty top-level placeholder, matching old_h in the same block.
A byte-identity gate CI never runs will rot, and a rotted gate is worse
than no gate. Adds the render harness as Phase 4, in the same shape as the
vault suite: its own process, its own sandbox, no loaded config needed.
Kept to one self-contained block so it can be dropped or re-applied by
hand if the runner is restructured. The functional suite's 317/317 count
is untouched; the render cases report separately.
Runtime escape-presence check over 24 user-facing functions.
Deliberately not a static grep for __fish_palette: measured on a broken
logs.fish whose --help block lost its declarations without gaining a call,
`fish -n` exited 0 and `grep -c __fish_palette` returned 1 while the help
output went 431 B -> 150 B with every escape stripped. The full suite was
green too. Only running the function and looking for an \e byte catches it;
that demonstration is recorded as a comment above the test.
Verified red as well as green -- deleting the __fish_palette call from
functions/mkcd.fish yields:
--help lost its colour: mkcd
FAIL test_functions_keep_their_palette
16/17 passed
fish_prompt.fish is excluded by name (its own hex palette); qc is excluded
because its --help shells out to aichat, which CI lacks.
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.
bd-pull, branch, check_fish_deps, claude-docs, claude-pr, fast, poke, qr
and parur take the standard call site. fish-deps instead routes -h/--help
to its existing __fish_deps_help, which previously answered only via the
unknown-subcommand path, printing "Unknown subcommand: --help" and
exiting 1.
This completes the 40 conversions and lands
test_every_user_facing_function_has_help -- acceptance criterion 6, the
check that fails when a new user-facing function ships without help. It
was developed red against the pre-conversion tree and reported exactly
the 40 names of the design's CONVERT table:
bd-pull bkg branch check_fish_deps claude-docs claude-pr cleanup
fast fc fish-deps fzf-update gip gip4 gip6 hist lD ld limine-edit
lock lsr lss lstree lt ltr lx parur poke ports qr sbver screensleep
split spwin steam-dl sudo-toggle swapstat tab tmux-clean upgrade
wake-lock
each as "<name>: no -h/--help handling and not in $__help_exempt", with
the guard exiting 1. The count fell 40 -> 32 -> 24 -> 17 -> 10 -> 0
across the conversion commits. It is committed green so every commit on
this branch passes the suite.
$__help_exempt is the single machine-readable exemption record; a second
assertion fails if an exempt name stops being a published function, so a
rename or deletion upstream cannot silently exempt nothing.
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.
These eight ignore $argv entirely, so passing --help ran the command
instead of describing it. The new check shadows every external binary
they reach with a recording stub on PATH and fails if anything is
invoked. Before the fix it reported, verbatim:
cleanup --help EXECUTED: pacman -Qtdq
fzf-update --help EXECUTED: git clone --depth 1 https://github.com/junegunn/fzf.git /tmp/.../.fzf
limine-edit --help EXECUTED: sudoedit /boot/limine.conf sudo limine-enroll-config sudo limine-mkinitcpio sudo sbctl sign-all
lock --help EXECUTED: loginctl lock-session
screensleep --help EXECUTED: busctl --user call org.kde.kglobalaccel ... invokeShortcut s Turn Off Screen
sudo-toggle --help EXECUTED: sudo stat -c %s /etc/sudoers.d/nofail-toggle sudo tee /etc/sudoers.d/nofail-toggle
tmux-clean --help EXECUTED: tmux list-sessions -F #{session_name} #{session_attached}
upgrade --help EXECUTED: paru -Syu --noconfirm
cleanup's log line is the read that precedes `sudo pacman -Rns $orphans`,
which the stub suppressed by returning no orphans; on a real machine with
orphans present the removal ran.
Each now answers --help from its own comment header. The call site is the
first statement of the body, above the C4 guard, so help stays reachable
when the component is disabled and nothing side-effecting runs first.
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.
The helper is a new file, so its diff is entirely additions and can never
be "purely structural". It declares colours rather than rendering any, and
tests/functional.fish asserts its 12 roles directly.
The width tier, title-border arithmetic, ON/OFF/DEFAULT badge, cursor cell
and table row are currently hand-copied across the three draw functions.
This adds them once, with the two geometry identities derived rather than
hand-maintained: a row's chrome is a fixed 21 columns, so field_w is
iw - 21 - label_w (reproducing both iw-33 and iw-34), and a title border is
dashes = iw - visible(segment) - 1 (reproducing all three of iw-23,
iw-len-3 and iw-L-S-22).
The frame owns no page height. Every verb prints exactly one line or
fragment, so the fixed-16 category and value pages and the dynamic
7+n sub-category page keep their heights, and config-settings.fish's
erase is unaffected.
No caller yet, so rendering cannot move: the golden's existing page
section is byte-identical (verified with cmp -n over its previous size)
and the file only gains frame-verb cases appended after it.
A missing or label-less header must still print and exit 0. Returning 1
would hand control back to the caller's body, which for upgrade(1) is a
full system upgrade.
Both fixtures reach the renderer's bottom `count $out -eq 0` fallback,
not the unreadable-source branch. Mutating that fallback to `return 1`
turns the test red with:
headerless executed its body despite --help
malformed executed its body despite --help
The mutation was reverted before this commit.
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.
Declared --no-scope-shadowing with a bare set, so the 12 colour roles land
in the caller's scope under the same short names the consuming functions
already interpolate. Keeping the names means the conversion never edits an
output string in 33 of the 38 consumers.
No consumer is converted yet; byte-identity harness still reports 29/29.
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.
Parses a function's own man-page comment header at call time and prints
it as a help menu on stdout, so the documentation that already generates
Section 5 of the manual becomes reachable from the shell.
Reads the .fish source rather than the generated docs/fish-config.md, so
it cannot go stale between a header edit and a docs rebuild. Walks
backwards from the `function` line to collect the header, which resolves
multi-header files (fish-deps, gi, y) without reimplementing
manualtools._block_identity.
Returns 1 only when argv[1] is not a help flag; every other path prints
and returns 0. A return of 1 hands control back to the caller's body.
Nothing calls it yet.
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.