From 21a02fd0e40e4d90a288c09f9204c95e0f87b5cc Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 03:06:30 -0400 Subject: [PATCH 01/39] docs(specs): add header-driven --help design Design for making the man-page-style comment header above each function reachable at runtime via -h/--help, instead of hand-writing 40 more menus. One renderer (__fish_help_header) parses the .fish source at call time and prints the header to stdout; each call site is a single line. Audits all 79 published functions that lack help today into 35 shadow/pass-through exempt, 4 prompt-hook exempt, and 40 to convert, with a one-line reason each. Corrects the baseline counts (109 published functions, 30 with menus, 79 without) and records two pre-existing defects found while auditing. Note: AGENTS/ is gitignored upstream, so this file is force-added. --- .../2026-09-07-header-driven-help-design.md | 446 ++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100644 AGENTS/specs/2026-09-07-header-driven-help-design.md diff --git a/AGENTS/specs/2026-09-07-header-driven-help-design.md b/AGENTS/specs/2026-09-07-header-driven-help-design.md new file mode 100644 index 0000000..dc259d0 --- /dev/null +++ b/AGENTS/specs/2026-09-07-header-driven-help-design.md @@ -0,0 +1,446 @@ +# Design: Header-Driven `--help` + +**Date:** 2026-09-07 +**Status:** Proposed +**Job:** 3 of the parallel improvement effort (fork of `08e66c8`, branch `work`) + +--- + +## Overview + +`functions/CLAUDE.md` requires every user-facing function to accept `-h`/`--help` +and print a formatted menu to stdout, "unless it strictly wraps another tool's +help." Most do not. + +The documentation those menus would contain already exists. Every documented +function carries a man-page-style comment header — `SYNOPSIS`, `DESCRIPTION`, +`ARGUMENTS`, `EXIT STATUS`, `RETURNS`, `EXAMPLE`, `NOTES` — which +`2026-07-26-function-headers-as-ssot-design.md` established as the SSOT for +Section 5 of the manual, and which `docs/verify-manual.py` already enforces. + +The header is written; it is simply not reachable at runtime. This spec makes it +reachable with one renderer function and one line per call site. It authors no +new documentation prose, and it changes nothing in the docs build pipeline. + +### Why not write the menus + +Writing a menu per function creates a second copy of facts the header already +holds, in a place no verifier checks. The 40 functions in scope would cost +roughly 600 lines of `echo` that immediately begin drifting from the header that +generates the manual. The 30 functions that already have hand-rolled menus are +the evidence: several of them already say things their headers do not. + +--- + +## Measured Baseline + +Taken 2026-09-07 against `functions/` at `08e66c8`. Counts come from +`manualtools.parse_functions()`, not from grep, so they match what the manual +actually publishes. + +| Fact | Value | +|---|---| +| `.fish` files in `functions/` | 185 | +| Files with a `# CATEGORY` comment | 110 | +| Functions the parser actually publishes | **109** | +| ...that print their own help menu today | **30** | +| ...that do not | **79** | +| Of those 79: exempt (see §4) | 39 | +| Of those 79: to convert | 40 | +| `fish tests/run-tests.fish` | 317/317 pass | +| `python3 docs/verify-manual.py` | 74/74 pass | + +The job brief quoted 110 / 34 / 78. Three corrections, all verified: + +- **110 → 109.** `zoxide.fish` carries two `# CATEGORY` blocks, but both resolve + to `_zoxide_*` names, which `parse_functions` drops as private. It publishes + nothing. +- **34 → 30.** The grep for `--help` counted `fisher.fish` and + `_fzf_configure_bindings_help.fish` (neither is a published function), and + `jr.fish` and `yt-dlp.fish`, which mention `--help` only inside their comment + headers and do not handle it in the body. +- **78 → 79.** `split` matched the grep because it uses `-h` for + `--horizontal`. It has no help menu, and its `-h` is spoken for. It belongs in + the target set as a special case (§4.3). + +### Pre-existing defects found, not fixed here + +- **`dops.fish` defines `function docker`, not `dops`.** The single header block + in that file resolves to the file stem, so the manual publishes an entry for + `dops` — a function that does not exist — while `docker` is undocumented. The + renderer resolves this correctly by accident (§3.2), but the manual entry + stays wrong. Out of scope: fixing it means authoring header content. +- **`sponge_filter_secrets.fish`** has a blank line between its header block and + its `function` line. The renderer tolerates it (§3.2); noted because it is the + only file that does. + +--- + +## 1. Requirements + +1. Every non-exempt published function responds to `-h` and `--help` with its own + documentation on **stdout**, exit 0. +2. Functions that pass `--help` through to a wrapped tool keep doing so. +3. `fish tests/run-tests.fish` stays at 317/317 plus the new checks. +4. `python3 docs/verify-manual.py` passes unchanged. +5. A test fails when a new user-facing function ships without help. +6. The docs build pipeline is not modified. +7. No header documentation content is added or rewritten. + +--- + +## 2. Renderer: `__fish_help_header` + +One new autoloaded function, `functions/__fish_help_header.fish`. + +```fish +__fish_help_header [args...] +``` + +It returns **1 — and prints nothing — only when `args[1]` is not a help flag.** +In every other case it prints something useful to stdout and returns 0. That +asymmetry is the safety-critical invariant: a return of 1 means "carry on with +the real work", so a parse failure must never produce it. `upgrade --help` +falling through to `paru -Syu --noconfirm` is the failure this rule exists to +prevent. + +### 2.1 Call site + +One line, as the **first statement of the function body**, above any opinionated +guard: + +```fish +function poke --description 'touch with automatic parent directory creation' + __fish_help_header (status current-function) $argv; and return 0 + ... +end +``` + +`status current-function` is evaluated in the caller's scope, so it yields the +caller's name. Placing the line above the C1/C4 guard means documentation stays +reachable when a component is disabled, and guarantees nothing side-effecting +runs before the flag is inspected. + +The `; and return 0` form leaves `$status` at 1 when no help flag was passed. +No function in scope reads `$status` as its first act, so this is inert — but +new call sites must not be added above a `$status` read. + +### 2.2 First-argument-only interception + +The renderer fires only when **`$argv[1]`** is `-h` or `--help`. Not anywhere in +`$argv`. + +This is not a simplification, it is a correctness requirement. `bkg`, +`wake-lock`, `split` and `spwin` take a command to run as their arguments. +Scanning all of `$argv` would make `wake-lock rsync --help` print `wake-lock`'s +help instead of running rsync. First-argument-only is also the pattern +`jobrunner` and `rand_string` already use. + +The 30 existing argparse-based menus keep accepting `--help` in any position. +This spec does not touch them (§5). + +### 2.3 Output format + +Matches `config-help`, the richest and most recent house menu: + +``` + bold + +USAGE bold brblue + two-space indent, deeper indent preserved + +DESCRIPTION + ... + +ARGUMENTS + ... +``` + +- `SYNOPSIS` renders as `USAGE`; `EXAMPLE` renders as `EXAMPLES`. Every other + label renders verbatim. +- `CATEGORY`, `COMPONENT` and `DEPENDENCIES` are build metadata and are skipped. +- `EXIT STATUS`, `RETURNS` and `NOTES` render when present. +- Blank lines inside a section are preserved (20 headers have multi-paragraph + `DESCRIPTION`s); trailing blanks are trimmed, mirroring + `manualtools._trailing_blanks`. +- **Body text is not colorized.** Only the name and the section headings carry + color. Guessing which words in a `DESCRIPTION` are flags or commands is a + heuristic with no upside; `config-help`'s hand-tuned coloring stays hand-tuned. + +### 2.4 Degraded output + +If `functions -D` yields no readable file, or the file holds no renderable +section, print the function name, its `--description`, and a pointer to +`help config ` — then return 0. Never return 1. + +--- + +## 3. Where the help text comes from + +### 3.1 Decision: parse the `.fish` source at call time + +Rejected alternatives: + +- **Read the generated `docs/fish-config.md`.** It is committed and + pre-formatted, but it is a *generated* artifact: a header edited without a + docs rebuild makes `--help` lie, which is the exact drift this job exists to + remove. It also costs a 163 KB scan and a `### ` heading match that can + collide with prose headings elsewhere in the manual. +- **Precompute a fish data file at build time**, mirroring the component + registry. This is the fastest option at runtime, but it adds a build artifact, + a new staleness class, and a step in the docs pipeline — which requirement 6 + forbids touching. +- **Reuse `config-help`.** It is a pager launcher over the whole manual; it + cannot emit one function's entry to stdout, and bending it into that shape + would cost more than the renderer. + +Source parsing wins because the source cannot be stale, the parse is ~15 lines +of `string match`, and the cost is one small file read on a keypress — never at +startup. + +### 3.2 Parsing rule + +Locate `function ` in the file, then walk **backwards** collecting the +contiguous run of `#` lines above it (skipping a blank separator line, for +`sponge_filter_secrets`). Within that run, apply `manualtools`' own grammar: + +- `^#\s+([A-Z][A-Z ]*[A-Z])\s*$` starts a section. +- Any other `#` line is body: strip `#`, then strip exactly three leading spaces + if present, so deeper indentation in nested `ARGUMENTS` tables survives. +- `#` lines before the first label (the copyright preamble) are ignored. + +Backward-walking from the `function` line replaces `manualtools._block_identity` +and is strictly more accurate at runtime: it resolves the three multi-header +files that publish a function (`fish-deps`, `gi`, `y`) correctly, and it gives +`docker` its real header in `dops.fish` where the Python parser attributes that +block to the file stem. + +### 3.3 Verified mechanics + +Proven in fish 4.9.1, not assumed. An autoloaded function calling +`(status current-function)` and `functions -D` on the result resolves to its own +defining file, both when called directly and when called from another function: + +``` +status current-function: probefn +functions -D self: .../functions/probefn.fish +status filename: .../functions/probefn.fish +``` + +Called *inside* the renderer, `status current-function` returns `__fish_help_header` +and `status stack-trace` returns the caller's path only as `~`-abbreviated prose. +Hence the name is passed as an argument rather than discovered. A working +prototype rendered `poke --help` in the format of §2.3 and left `poke`'s normal +path untouched. + +--- + +## 4. Classification of the 79 + +The exemption in `functions/CLAUDE.md` is "strictly wraps another tool's help." +Applied literally it splits into two classes, plus one collision case. + +**EXEMPT-A — shadow or pass-through.** The function shadows a real same-named +binary, or its entire body forwards `$argv` to one named tool. `ls --help` must +reach `ls`. Intercepting is a regression, and for the C1-guarded shadows it also +breaks the disabled-fallback contract, where the bare tool is supposed to answer. + +**EXEMPT-B — not a command.** Invoked by fish, never typed. Published in the +manual, but `--help` is meaningless. + +### 4.1 EXEMPT-A (35) + +| Function | Reason | +|---|---| +| `agy` | Shadows `agy`; body ends in `command agy $argv`. | +| `antigravity-ide` | Shadows `antigravity-ide`; only filters one stderr line. | +| `bash` | Shadows `bash`; adds `--rcfile`, forwards the rest. | +| `cat` | Shadows `cat`; C1 guard falls back to `command cat $argv`. | +| `cdi` | Whole body is `zi $argv`; zoxide owns the help. | +| `cffetch` | `clear` then `fastfetch $argv`. | +| `cheat` | `command cheat -c $argv`, else `tldr`/`man`. | +| `claude` | Shadows `claude`; body ends in `command claude $argv`. | +| `clone` | Whole body is `clone-in-kitty $argv`. | +| `clonet` | Whole body is `clone-in-kitty --type=tab $argv`. | +| `config-toggle` | Deprecated alias; forwards to `config-settings`, which has help. | +| `copy` | Whole body is `command cp $argv` with one directory special case. | +| `dops` | The file defines `docker`, which shadows `docker`. | +| `du` | Shadows `du`; unmatched args reach `command du`. | +| `dusize` | `du -sh $argv[1]` through the `du` shadow. | +| `fast-cli` | Whole body is `command fast $argv`. | +| `ffetch` | Whole body is `fastfetch $argv`. | +| `gitui` | `command gitui -t frappe.ron $argv`. | +| `gitup` | Its own `SYNOPSIS` is `gitup [args...]`, forwarded to `git fetch`. | +| `jr` | Whole body is `jobrunner $argv`; `jobrunner` has a help menu. | +| `joplin` | Whole body is the real `joplin` binary with `NODE_OPTIONS` set. | +| `less` | Shadows `less`; resolves a pager and forwards `$argv`. | +| `ls` | Shadows `ls`; forwards to `eza`/`lsd`/`command ls`. | +| `mkdir` | Shadows `mkdir`; any flag argument goes to `command mkdir -p`. | +| `mv` | Shadows `mv`; falls through to `command mv $argv`. | +| `paste` | Shadows coreutils `paste`; forwards `$argv` to `wl-paste`/`xclip`. | +| `ping` | Shadows `ping`; forwards to `prettyping` or `command ping`. | +| `rawfish` | Whole body is `env NO_TMUX=1 fish $argv`. | +| `rg` | Shadows `rg`; adds one flag under Kitty, forwards the rest. | +| `rm` | Shadows `rm`; unmatched args reach `command rm $argv`. | +| `search` | Whole body is `$aur $argv` (paru or yay). | +| `ssh` | Shadows `ssh`; forwards to `kitten ssh` or `command ssh`. | +| `top` | Shadows `top`; forwards to `btop` or `command top`. | +| `view` | Shadows vim's `view`; whole body is `nvim -R $argv`. | +| `yt-dlp` | Shadows `yt-dlp`; injects defaults, forwards `$argv` last. | + +### 4.2 EXEMPT-B (4) + +| Function | Reason | +|---|---| +| `fish_prompt` | Prompt hook; fish calls it, users never do. | +| `fish_right_prompt` | Prompt hook. | +| `fish_mode_prompt` | Prompt hook; body is empty. | +| `sponge_filter_secrets` | `sponge` plugin filter callback, invoked per command. | + +### 4.3 CONVERT (40) + +All take the standard call site of §2.1 unless noted. + +| Function | Reason it is not exempt | +|---|---| +| `bd-pull` | Own Gitea/Beads logic; `$argv[1]` is a repo slug. | +| `bkg` | Command runner; `$argv` is a command, not flags. First-arg rule applies. | +| `branch` | Own git logic; `branch --help` currently feeds `--help` to `git checkout -b`. | +| `check_fish_deps` | Ignores `$argv`; runs `fish-deps status`. | +| `claude-docs` | Ignores `$argv`; fires a fixed prompt. | +| `claude-pr` | Ignores `$argv`; fires a fixed prompt. | +| `cleanup` | Ignores `$argv`; `--help` currently runs `sudo pacman -Rns`. | +| `fast` | Ignores `$argv`; placeholder that prints a notice. | +| `fc` | Own history logic; `--help` currently becomes a history search term. | +| `fish-deps` | **Reuse:** wire `case -h --help` to the existing `__fish_deps_help`, return 0. Today `--help` hits `case '*'`, prints "Unknown subcommand", exits 1. | +| `fzf-update` | Ignores `$argv`; `--help` currently clones and installs fzf. | +| `gip` | Ignores `$argv`. | +| `gip4` | Ignores `$argv`. | +| `gip6` | Ignores `$argv`. | +| `hist` | Ignores `$argv`; opens an fzf picker. | +| `lD` | Not a binary; three-branch body whose value is the `--only-dirs` preset. | +| `ld` | Ignores `$argv`; launches lazydocker with a computed `DOCKER_HOST`. | +| `limine-edit` | Ignores `$argv`; `--help` currently runs `sudoedit` and re-enrolls Limine. | +| `lock` | Ignores `$argv`; `lock --help` currently locks the screen. | +| `lsr` | Not a binary; value is the reverse-time preset. | +| `lss` | Not a binary; value is the size-sort preset. | +| `lstree` | Not a binary; value is the recursive-tree preset. | +| `lt` | Not a binary; value is the depth-2 tree preset. | +| `ltr` | Not a binary; value is the reverse-time preset. | +| `lx` | Not a binary; value is the extension-sort preset. | +| `parur` | Ignores `$argv`; opens an fzf package picker. | +| `poke` | Own logic; `--help` would be created as a file. | +| `ports` | Ignores `$argv`; runs `sudo lsof`. | +| `qr` | `--help` would be encoded into a QR code. | +| `sbver` | Own `--brief` flag; runs `sudo sbctl verify`. | +| `screensleep` | Ignores `$argv`; `--help` currently blanks the display. | +| `split` | **`--help` only, no `-h`.** Its own `ARGUMENTS` documents `-h, --horizontal`. | +| `spwin` | Own terminal dispatch; first-arg rule protects `spwin --help`. | +| `steam-dl` | Ignores `$argv`. | +| `sudo-toggle` | Ignores `$argv`; `--help` currently rewrites `/etc/sudoers.d`. | +| `swapstat` | Ignores `$argv`. | +| `tab` | Own terminal dispatch; first-arg rule protects the forwarded command. | +| `tmux-clean` | Ignores `$argv`; `--help` currently kills tmux sessions. | +| `upgrade` | Ignores `$argv`; `--help` currently runs `paru -Syu --noconfirm`. | +| `wake-lock` | Command runner; first-arg rule protects `wake-lock --help`. | + +Eight of these currently perform a destructive or irreversible action when handed +`--help`, because they ignore `$argv` entirely and just run: `cleanup` +(`sudo pacman -Rns`), `fzf-update` (clone and install), `limine-edit` (`sudoedit` +and re-enroll), `lock` (locks the session), `screensleep` (blanks the display), +`sudo-toggle` (rewrites `/etc/sudoers.d`), `tmux-clean` (kills tmux sessions) and +`upgrade` (`paru -Syu --noconfirm`). For these, converting is a safety fix before +it is a documentation one. + +`split` is the only `-h` collision in the whole set; every other CONVERT function +was checked and uses `-h` for nothing. + +--- + +## 5. The 30 existing menus stay + +Not converted in this job. The risk is asymmetric: + +- Three of them carry content that **does not exist in any header**, so + conversion would delete documentation: `config-help`'s ov navigation keys and + pager fallback chain, `qc`'s appended `aichat --help`, `superpowers`' inline + usage block. Recovering them means authoring header prose, which requirement 7 + forbids. +- Thirteen use `argparse`/`_flag_help` and accept `--help` in any position. + Moving them to the first-argument-only rule of §2.2 is a real behavior change + for menus people already use. +- No acceptance criterion requires it. The duplication they represent is + pre-existing and static — it does not grow as new functions land, because new + functions will use the renderer. + +Thirty conversions is thirty independent chances to change output somebody +relies on, bought for no test. Recommended as a separate follow-up branch, where +each diff can be reviewed against its own before/after output. + +--- + +## 6. Testing + +Two additions to `tests/functional.fish`, which runs inside the sandboxed loaded +session where `functions -D` resolves to the sandbox copy. + +**`test_help_renderer`** — behavioral. Renders a fixture function with a known +header and asserts the section order, the `SYNOPSIS`→`USAGE` rename, indentation +survival, and exit 0. Then spot-checks two real, side-effect-free functions +(`poke`, `gip4`) end to end. Runs under `TERM=dumb`, where `set_color` emits +nothing, so assertions match plain text. + +**`test_every_user_facing_function_has_help`** — the guard required by +acceptance 5. For each function published by the header parser, assert its body +contains a help entry point (`__fish_help_header`, `_flag_help`, or a +`-h`/`--help` branch) *or* appears in an explicit exempt list. + +The exempt list lives in `tests/functional.fish` as one array with a one-line +comment per entry, seeded from §4.1 and §4.2. Rejected: a `# NOHELP` marker +comment in each exempt file — 39 file edits for no runtime benefit, and a marker +is the kind of thing that gets copy-pasted into a new function and silently +exempts it. A central list makes adding an exemption a reviewable act. + +The wiring check is deliberately static. Running all 40 functions with `--help` +would be a stronger test, but it executes `upgrade`, `sudo-toggle` and +`limine-edit` on the CI machine, and it is precisely wrong when the code is +broken — the case the test exists to catch. The renderer's behavior is proven +once against a fixture; the wiring is proven by inspection across all 40. + +--- + +## 7. Line delta + +| Change | Lines | +|---|---:| +| `functions/__fish_help_header.fish` (with license + header) | ~55 | +| 39 call sites × 1 line | +39 | +| `fish-deps` `case -h --help` | +2 | +| `tests/functional.fish` (2 tests + exempt list) | ~60 | +| Hand-rolled menus deleted | 0 | +| **Net** | **~+156** | + +The number that matters is not in the table: 40 menus not written. At the ~15 +lines the existing menus average, hand-writing them would have cost ~600 lines +of `echo` and a permanent second copy of the manual. + +--- + +## 8. Risks accepted + +- **First-argument-only interception** means `poke somefile --help` does not + print help. Correct for command runners, mildly surprising elsewhere. Accepted: + the alternative silently breaks `wake-lock rsync --help`. +- **Uncolored body text.** Generated menus will look plainer than the + hand-tuned ones. Accepted over a guessing heuristic. +- **A header missing `ARGUMENTS`** yields a menu without an arguments section. + `verify-manual.py` enforces only `SYNOPSIS`, `DESCRIPTION` and `EXAMPLE`. + Filling the gaps is authoring prose — out of scope. Note them, do not write them. +- **The seven `ls` variants** (`lD`, `lsr`, `lss`, `lstree`, `lt`, `ltr`, `lx`) + currently pass `--help` to `eza`, incidentally. After conversion they print + their own preset's documentation and `eza --help` reaches eza directly. This + is the largest judgment cluster in §4.3 and the most likely place for the + classification to be wrong. +- **`functions/CLAUDE.md` will still read as violated.** Its one-line rule names + only the wrapper exemption, not the prompt-hook class. A two-line amendment is + proposed separately; this spec does not change it. From 8a2731d411f4a8c9957bdf168676b8feff9bea9c Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 07:22:51 -0400 Subject: [PATCH 02/39] revert: untrack the spec AGENTS/ is gitignored upstream; a tracked file there pollutes git diff main..work, which is what the user reads when choosing which job to merge. Spec stays on disk, browsable, out of history. --- .../2026-09-07-header-driven-help-design.md | 446 ------------------ 1 file changed, 446 deletions(-) delete mode 100644 AGENTS/specs/2026-09-07-header-driven-help-design.md diff --git a/AGENTS/specs/2026-09-07-header-driven-help-design.md b/AGENTS/specs/2026-09-07-header-driven-help-design.md deleted file mode 100644 index dc259d0..0000000 --- a/AGENTS/specs/2026-09-07-header-driven-help-design.md +++ /dev/null @@ -1,446 +0,0 @@ -# Design: Header-Driven `--help` - -**Date:** 2026-09-07 -**Status:** Proposed -**Job:** 3 of the parallel improvement effort (fork of `08e66c8`, branch `work`) - ---- - -## Overview - -`functions/CLAUDE.md` requires every user-facing function to accept `-h`/`--help` -and print a formatted menu to stdout, "unless it strictly wraps another tool's -help." Most do not. - -The documentation those menus would contain already exists. Every documented -function carries a man-page-style comment header — `SYNOPSIS`, `DESCRIPTION`, -`ARGUMENTS`, `EXIT STATUS`, `RETURNS`, `EXAMPLE`, `NOTES` — which -`2026-07-26-function-headers-as-ssot-design.md` established as the SSOT for -Section 5 of the manual, and which `docs/verify-manual.py` already enforces. - -The header is written; it is simply not reachable at runtime. This spec makes it -reachable with one renderer function and one line per call site. It authors no -new documentation prose, and it changes nothing in the docs build pipeline. - -### Why not write the menus - -Writing a menu per function creates a second copy of facts the header already -holds, in a place no verifier checks. The 40 functions in scope would cost -roughly 600 lines of `echo` that immediately begin drifting from the header that -generates the manual. The 30 functions that already have hand-rolled menus are -the evidence: several of them already say things their headers do not. - ---- - -## Measured Baseline - -Taken 2026-09-07 against `functions/` at `08e66c8`. Counts come from -`manualtools.parse_functions()`, not from grep, so they match what the manual -actually publishes. - -| Fact | Value | -|---|---| -| `.fish` files in `functions/` | 185 | -| Files with a `# CATEGORY` comment | 110 | -| Functions the parser actually publishes | **109** | -| ...that print their own help menu today | **30** | -| ...that do not | **79** | -| Of those 79: exempt (see §4) | 39 | -| Of those 79: to convert | 40 | -| `fish tests/run-tests.fish` | 317/317 pass | -| `python3 docs/verify-manual.py` | 74/74 pass | - -The job brief quoted 110 / 34 / 78. Three corrections, all verified: - -- **110 → 109.** `zoxide.fish` carries two `# CATEGORY` blocks, but both resolve - to `_zoxide_*` names, which `parse_functions` drops as private. It publishes - nothing. -- **34 → 30.** The grep for `--help` counted `fisher.fish` and - `_fzf_configure_bindings_help.fish` (neither is a published function), and - `jr.fish` and `yt-dlp.fish`, which mention `--help` only inside their comment - headers and do not handle it in the body. -- **78 → 79.** `split` matched the grep because it uses `-h` for - `--horizontal`. It has no help menu, and its `-h` is spoken for. It belongs in - the target set as a special case (§4.3). - -### Pre-existing defects found, not fixed here - -- **`dops.fish` defines `function docker`, not `dops`.** The single header block - in that file resolves to the file stem, so the manual publishes an entry for - `dops` — a function that does not exist — while `docker` is undocumented. The - renderer resolves this correctly by accident (§3.2), but the manual entry - stays wrong. Out of scope: fixing it means authoring header content. -- **`sponge_filter_secrets.fish`** has a blank line between its header block and - its `function` line. The renderer tolerates it (§3.2); noted because it is the - only file that does. - ---- - -## 1. Requirements - -1. Every non-exempt published function responds to `-h` and `--help` with its own - documentation on **stdout**, exit 0. -2. Functions that pass `--help` through to a wrapped tool keep doing so. -3. `fish tests/run-tests.fish` stays at 317/317 plus the new checks. -4. `python3 docs/verify-manual.py` passes unchanged. -5. A test fails when a new user-facing function ships without help. -6. The docs build pipeline is not modified. -7. No header documentation content is added or rewritten. - ---- - -## 2. Renderer: `__fish_help_header` - -One new autoloaded function, `functions/__fish_help_header.fish`. - -```fish -__fish_help_header [args...] -``` - -It returns **1 — and prints nothing — only when `args[1]` is not a help flag.** -In every other case it prints something useful to stdout and returns 0. That -asymmetry is the safety-critical invariant: a return of 1 means "carry on with -the real work", so a parse failure must never produce it. `upgrade --help` -falling through to `paru -Syu --noconfirm` is the failure this rule exists to -prevent. - -### 2.1 Call site - -One line, as the **first statement of the function body**, above any opinionated -guard: - -```fish -function poke --description 'touch with automatic parent directory creation' - __fish_help_header (status current-function) $argv; and return 0 - ... -end -``` - -`status current-function` is evaluated in the caller's scope, so it yields the -caller's name. Placing the line above the C1/C4 guard means documentation stays -reachable when a component is disabled, and guarantees nothing side-effecting -runs before the flag is inspected. - -The `; and return 0` form leaves `$status` at 1 when no help flag was passed. -No function in scope reads `$status` as its first act, so this is inert — but -new call sites must not be added above a `$status` read. - -### 2.2 First-argument-only interception - -The renderer fires only when **`$argv[1]`** is `-h` or `--help`. Not anywhere in -`$argv`. - -This is not a simplification, it is a correctness requirement. `bkg`, -`wake-lock`, `split` and `spwin` take a command to run as their arguments. -Scanning all of `$argv` would make `wake-lock rsync --help` print `wake-lock`'s -help instead of running rsync. First-argument-only is also the pattern -`jobrunner` and `rand_string` already use. - -The 30 existing argparse-based menus keep accepting `--help` in any position. -This spec does not touch them (§5). - -### 2.3 Output format - -Matches `config-help`, the richest and most recent house menu: - -``` - bold - -USAGE bold brblue - two-space indent, deeper indent preserved - -DESCRIPTION - ... - -ARGUMENTS - ... -``` - -- `SYNOPSIS` renders as `USAGE`; `EXAMPLE` renders as `EXAMPLES`. Every other - label renders verbatim. -- `CATEGORY`, `COMPONENT` and `DEPENDENCIES` are build metadata and are skipped. -- `EXIT STATUS`, `RETURNS` and `NOTES` render when present. -- Blank lines inside a section are preserved (20 headers have multi-paragraph - `DESCRIPTION`s); trailing blanks are trimmed, mirroring - `manualtools._trailing_blanks`. -- **Body text is not colorized.** Only the name and the section headings carry - color. Guessing which words in a `DESCRIPTION` are flags or commands is a - heuristic with no upside; `config-help`'s hand-tuned coloring stays hand-tuned. - -### 2.4 Degraded output - -If `functions -D` yields no readable file, or the file holds no renderable -section, print the function name, its `--description`, and a pointer to -`help config ` — then return 0. Never return 1. - ---- - -## 3. Where the help text comes from - -### 3.1 Decision: parse the `.fish` source at call time - -Rejected alternatives: - -- **Read the generated `docs/fish-config.md`.** It is committed and - pre-formatted, but it is a *generated* artifact: a header edited without a - docs rebuild makes `--help` lie, which is the exact drift this job exists to - remove. It also costs a 163 KB scan and a `### ` heading match that can - collide with prose headings elsewhere in the manual. -- **Precompute a fish data file at build time**, mirroring the component - registry. This is the fastest option at runtime, but it adds a build artifact, - a new staleness class, and a step in the docs pipeline — which requirement 6 - forbids touching. -- **Reuse `config-help`.** It is a pager launcher over the whole manual; it - cannot emit one function's entry to stdout, and bending it into that shape - would cost more than the renderer. - -Source parsing wins because the source cannot be stale, the parse is ~15 lines -of `string match`, and the cost is one small file read on a keypress — never at -startup. - -### 3.2 Parsing rule - -Locate `function ` in the file, then walk **backwards** collecting the -contiguous run of `#` lines above it (skipping a blank separator line, for -`sponge_filter_secrets`). Within that run, apply `manualtools`' own grammar: - -- `^#\s+([A-Z][A-Z ]*[A-Z])\s*$` starts a section. -- Any other `#` line is body: strip `#`, then strip exactly three leading spaces - if present, so deeper indentation in nested `ARGUMENTS` tables survives. -- `#` lines before the first label (the copyright preamble) are ignored. - -Backward-walking from the `function` line replaces `manualtools._block_identity` -and is strictly more accurate at runtime: it resolves the three multi-header -files that publish a function (`fish-deps`, `gi`, `y`) correctly, and it gives -`docker` its real header in `dops.fish` where the Python parser attributes that -block to the file stem. - -### 3.3 Verified mechanics - -Proven in fish 4.9.1, not assumed. An autoloaded function calling -`(status current-function)` and `functions -D` on the result resolves to its own -defining file, both when called directly and when called from another function: - -``` -status current-function: probefn -functions -D self: .../functions/probefn.fish -status filename: .../functions/probefn.fish -``` - -Called *inside* the renderer, `status current-function` returns `__fish_help_header` -and `status stack-trace` returns the caller's path only as `~`-abbreviated prose. -Hence the name is passed as an argument rather than discovered. A working -prototype rendered `poke --help` in the format of §2.3 and left `poke`'s normal -path untouched. - ---- - -## 4. Classification of the 79 - -The exemption in `functions/CLAUDE.md` is "strictly wraps another tool's help." -Applied literally it splits into two classes, plus one collision case. - -**EXEMPT-A — shadow or pass-through.** The function shadows a real same-named -binary, or its entire body forwards `$argv` to one named tool. `ls --help` must -reach `ls`. Intercepting is a regression, and for the C1-guarded shadows it also -breaks the disabled-fallback contract, where the bare tool is supposed to answer. - -**EXEMPT-B — not a command.** Invoked by fish, never typed. Published in the -manual, but `--help` is meaningless. - -### 4.1 EXEMPT-A (35) - -| Function | Reason | -|---|---| -| `agy` | Shadows `agy`; body ends in `command agy $argv`. | -| `antigravity-ide` | Shadows `antigravity-ide`; only filters one stderr line. | -| `bash` | Shadows `bash`; adds `--rcfile`, forwards the rest. | -| `cat` | Shadows `cat`; C1 guard falls back to `command cat $argv`. | -| `cdi` | Whole body is `zi $argv`; zoxide owns the help. | -| `cffetch` | `clear` then `fastfetch $argv`. | -| `cheat` | `command cheat -c $argv`, else `tldr`/`man`. | -| `claude` | Shadows `claude`; body ends in `command claude $argv`. | -| `clone` | Whole body is `clone-in-kitty $argv`. | -| `clonet` | Whole body is `clone-in-kitty --type=tab $argv`. | -| `config-toggle` | Deprecated alias; forwards to `config-settings`, which has help. | -| `copy` | Whole body is `command cp $argv` with one directory special case. | -| `dops` | The file defines `docker`, which shadows `docker`. | -| `du` | Shadows `du`; unmatched args reach `command du`. | -| `dusize` | `du -sh $argv[1]` through the `du` shadow. | -| `fast-cli` | Whole body is `command fast $argv`. | -| `ffetch` | Whole body is `fastfetch $argv`. | -| `gitui` | `command gitui -t frappe.ron $argv`. | -| `gitup` | Its own `SYNOPSIS` is `gitup [args...]`, forwarded to `git fetch`. | -| `jr` | Whole body is `jobrunner $argv`; `jobrunner` has a help menu. | -| `joplin` | Whole body is the real `joplin` binary with `NODE_OPTIONS` set. | -| `less` | Shadows `less`; resolves a pager and forwards `$argv`. | -| `ls` | Shadows `ls`; forwards to `eza`/`lsd`/`command ls`. | -| `mkdir` | Shadows `mkdir`; any flag argument goes to `command mkdir -p`. | -| `mv` | Shadows `mv`; falls through to `command mv $argv`. | -| `paste` | Shadows coreutils `paste`; forwards `$argv` to `wl-paste`/`xclip`. | -| `ping` | Shadows `ping`; forwards to `prettyping` or `command ping`. | -| `rawfish` | Whole body is `env NO_TMUX=1 fish $argv`. | -| `rg` | Shadows `rg`; adds one flag under Kitty, forwards the rest. | -| `rm` | Shadows `rm`; unmatched args reach `command rm $argv`. | -| `search` | Whole body is `$aur $argv` (paru or yay). | -| `ssh` | Shadows `ssh`; forwards to `kitten ssh` or `command ssh`. | -| `top` | Shadows `top`; forwards to `btop` or `command top`. | -| `view` | Shadows vim's `view`; whole body is `nvim -R $argv`. | -| `yt-dlp` | Shadows `yt-dlp`; injects defaults, forwards `$argv` last. | - -### 4.2 EXEMPT-B (4) - -| Function | Reason | -|---|---| -| `fish_prompt` | Prompt hook; fish calls it, users never do. | -| `fish_right_prompt` | Prompt hook. | -| `fish_mode_prompt` | Prompt hook; body is empty. | -| `sponge_filter_secrets` | `sponge` plugin filter callback, invoked per command. | - -### 4.3 CONVERT (40) - -All take the standard call site of §2.1 unless noted. - -| Function | Reason it is not exempt | -|---|---| -| `bd-pull` | Own Gitea/Beads logic; `$argv[1]` is a repo slug. | -| `bkg` | Command runner; `$argv` is a command, not flags. First-arg rule applies. | -| `branch` | Own git logic; `branch --help` currently feeds `--help` to `git checkout -b`. | -| `check_fish_deps` | Ignores `$argv`; runs `fish-deps status`. | -| `claude-docs` | Ignores `$argv`; fires a fixed prompt. | -| `claude-pr` | Ignores `$argv`; fires a fixed prompt. | -| `cleanup` | Ignores `$argv`; `--help` currently runs `sudo pacman -Rns`. | -| `fast` | Ignores `$argv`; placeholder that prints a notice. | -| `fc` | Own history logic; `--help` currently becomes a history search term. | -| `fish-deps` | **Reuse:** wire `case -h --help` to the existing `__fish_deps_help`, return 0. Today `--help` hits `case '*'`, prints "Unknown subcommand", exits 1. | -| `fzf-update` | Ignores `$argv`; `--help` currently clones and installs fzf. | -| `gip` | Ignores `$argv`. | -| `gip4` | Ignores `$argv`. | -| `gip6` | Ignores `$argv`. | -| `hist` | Ignores `$argv`; opens an fzf picker. | -| `lD` | Not a binary; three-branch body whose value is the `--only-dirs` preset. | -| `ld` | Ignores `$argv`; launches lazydocker with a computed `DOCKER_HOST`. | -| `limine-edit` | Ignores `$argv`; `--help` currently runs `sudoedit` and re-enrolls Limine. | -| `lock` | Ignores `$argv`; `lock --help` currently locks the screen. | -| `lsr` | Not a binary; value is the reverse-time preset. | -| `lss` | Not a binary; value is the size-sort preset. | -| `lstree` | Not a binary; value is the recursive-tree preset. | -| `lt` | Not a binary; value is the depth-2 tree preset. | -| `ltr` | Not a binary; value is the reverse-time preset. | -| `lx` | Not a binary; value is the extension-sort preset. | -| `parur` | Ignores `$argv`; opens an fzf package picker. | -| `poke` | Own logic; `--help` would be created as a file. | -| `ports` | Ignores `$argv`; runs `sudo lsof`. | -| `qr` | `--help` would be encoded into a QR code. | -| `sbver` | Own `--brief` flag; runs `sudo sbctl verify`. | -| `screensleep` | Ignores `$argv`; `--help` currently blanks the display. | -| `split` | **`--help` only, no `-h`.** Its own `ARGUMENTS` documents `-h, --horizontal`. | -| `spwin` | Own terminal dispatch; first-arg rule protects `spwin --help`. | -| `steam-dl` | Ignores `$argv`. | -| `sudo-toggle` | Ignores `$argv`; `--help` currently rewrites `/etc/sudoers.d`. | -| `swapstat` | Ignores `$argv`. | -| `tab` | Own terminal dispatch; first-arg rule protects the forwarded command. | -| `tmux-clean` | Ignores `$argv`; `--help` currently kills tmux sessions. | -| `upgrade` | Ignores `$argv`; `--help` currently runs `paru -Syu --noconfirm`. | -| `wake-lock` | Command runner; first-arg rule protects `wake-lock --help`. | - -Eight of these currently perform a destructive or irreversible action when handed -`--help`, because they ignore `$argv` entirely and just run: `cleanup` -(`sudo pacman -Rns`), `fzf-update` (clone and install), `limine-edit` (`sudoedit` -and re-enroll), `lock` (locks the session), `screensleep` (blanks the display), -`sudo-toggle` (rewrites `/etc/sudoers.d`), `tmux-clean` (kills tmux sessions) and -`upgrade` (`paru -Syu --noconfirm`). For these, converting is a safety fix before -it is a documentation one. - -`split` is the only `-h` collision in the whole set; every other CONVERT function -was checked and uses `-h` for nothing. - ---- - -## 5. The 30 existing menus stay - -Not converted in this job. The risk is asymmetric: - -- Three of them carry content that **does not exist in any header**, so - conversion would delete documentation: `config-help`'s ov navigation keys and - pager fallback chain, `qc`'s appended `aichat --help`, `superpowers`' inline - usage block. Recovering them means authoring header prose, which requirement 7 - forbids. -- Thirteen use `argparse`/`_flag_help` and accept `--help` in any position. - Moving them to the first-argument-only rule of §2.2 is a real behavior change - for menus people already use. -- No acceptance criterion requires it. The duplication they represent is - pre-existing and static — it does not grow as new functions land, because new - functions will use the renderer. - -Thirty conversions is thirty independent chances to change output somebody -relies on, bought for no test. Recommended as a separate follow-up branch, where -each diff can be reviewed against its own before/after output. - ---- - -## 6. Testing - -Two additions to `tests/functional.fish`, which runs inside the sandboxed loaded -session where `functions -D` resolves to the sandbox copy. - -**`test_help_renderer`** — behavioral. Renders a fixture function with a known -header and asserts the section order, the `SYNOPSIS`→`USAGE` rename, indentation -survival, and exit 0. Then spot-checks two real, side-effect-free functions -(`poke`, `gip4`) end to end. Runs under `TERM=dumb`, where `set_color` emits -nothing, so assertions match plain text. - -**`test_every_user_facing_function_has_help`** — the guard required by -acceptance 5. For each function published by the header parser, assert its body -contains a help entry point (`__fish_help_header`, `_flag_help`, or a -`-h`/`--help` branch) *or* appears in an explicit exempt list. - -The exempt list lives in `tests/functional.fish` as one array with a one-line -comment per entry, seeded from §4.1 and §4.2. Rejected: a `# NOHELP` marker -comment in each exempt file — 39 file edits for no runtime benefit, and a marker -is the kind of thing that gets copy-pasted into a new function and silently -exempts it. A central list makes adding an exemption a reviewable act. - -The wiring check is deliberately static. Running all 40 functions with `--help` -would be a stronger test, but it executes `upgrade`, `sudo-toggle` and -`limine-edit` on the CI machine, and it is precisely wrong when the code is -broken — the case the test exists to catch. The renderer's behavior is proven -once against a fixture; the wiring is proven by inspection across all 40. - ---- - -## 7. Line delta - -| Change | Lines | -|---|---:| -| `functions/__fish_help_header.fish` (with license + header) | ~55 | -| 39 call sites × 1 line | +39 | -| `fish-deps` `case -h --help` | +2 | -| `tests/functional.fish` (2 tests + exempt list) | ~60 | -| Hand-rolled menus deleted | 0 | -| **Net** | **~+156** | - -The number that matters is not in the table: 40 menus not written. At the ~15 -lines the existing menus average, hand-writing them would have cost ~600 lines -of `echo` and a permanent second copy of the manual. - ---- - -## 8. Risks accepted - -- **First-argument-only interception** means `poke somefile --help` does not - print help. Correct for command runners, mildly surprising elsewhere. Accepted: - the alternative silently breaks `wake-lock rsync --help`. -- **Uncolored body text.** Generated menus will look plainer than the - hand-tuned ones. Accepted over a guessing heuristic. -- **A header missing `ARGUMENTS`** yields a menu without an arguments section. - `verify-manual.py` enforces only `SYNOPSIS`, `DESCRIPTION` and `EXAMPLE`. - Filling the gaps is authoring prose — out of scope. Note them, do not write them. -- **The seven `ls` variants** (`lD`, `lsr`, `lss`, `lstree`, `lt`, `ltr`, `lx`) - currently pass `--help` to `eza`, incidentally. After conversion they print - their own preset's documentation and `eza --help` reaches eza directly. This - is the largest judgment cluster in §4.3 and the most likely place for the - classification to be wrong. -- **`functions/CLAUDE.md` will still read as violated.** Its one-line rule names - only the wrapper exemption, not the prompt-hook class. A two-line amendment is - proposed separately; this spec does not change it. From 321b80f1f8735aa0da52c50567a5e04b24c8b565 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 07:32:03 -0400 Subject: [PATCH 03/39] perf(conf.d): load tailscale completions lazily from completions/ 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 . Measured: 19.2 ms off both interactive and non-interactive startup. --- {conf.d => completions}/tailscale.fish | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {conf.d => completions}/tailscale.fish (100%) diff --git a/conf.d/tailscale.fish b/completions/tailscale.fish similarity index 100% rename from conf.d/tailscale.fish rename to completions/tailscale.fish From fb21fa550aface2555be7e38d10402d80ca7f438 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:53:35 -0400 Subject: [PATCH 04/39] perf(completions): drop tailscale self-priming completion call 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. --- completions/tailscale.fish | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/completions/tailscale.fish b/completions/tailscale.fish index dce3602..b682131 100644 --- a/completions/tailscale.fish +++ b/completions/tailscale.fish @@ -228,16 +228,15 @@ function __tailscale_prepare_completions return 0 end -# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves -# so we can properly delete any completions provided by another script. -# Only do this if the program can be found, or else fish may print some errors; besides, -# the existing completions will only be loaded if the program can be found. -if type -q "tailscale" - # The space after the program name is essential to trigger completion for the program - # and not completion of the program name itself. - # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. - complete --do-complete "tailscale " > /dev/null 2>&1 -end +# REMOVED (2026-09-07): Cobra's self-priming block — +# if type -q "tailscale"; complete --do-complete "tailscale " >/dev/null 2>&1; end +# It existed to force any pre-existing tailscale completions to load so the +# `complete -c tailscale -e` below could erase them. From completions/ it has +# no job: fish autoloads only the FIRST match on $fish_complete_path, and +# $__fish_config_dir/completions precedes /usr/share/fish/vendor_completions.d, +# so the vendor file is never sourced and there is nothing to erase. It also +# executed the tailscale binary at startup. Verified: completion output is +# byte-identical with and without it. See AGENTS/specs/2026-09-07-startup-latency-design.md D2. # Remove any pre-existing completions for the program since we will be handling all of them. complete -c tailscale -e From 3d8d6a44689c101bd40c28a3ae1961c0d56b28dd Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:54:38 -0400 Subject: [PATCH 05/39] refactor(conf.d): move cheat completions to completions/ 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. --- {conf.d => completions}/cheat.fish | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {conf.d => completions}/cheat.fish (100%) diff --git a/conf.d/cheat.fish b/completions/cheat.fish similarity index 100% rename from conf.d/cheat.fish rename to completions/cheat.fish From c3a2b6fa0c9ee7574f9167bb83b83debde42c3ae Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:55:31 -0400 Subject: [PATCH 06/39] perf(conf.d): skip abbr.fish in non-interactive shells 61 abbreviations were declared on every fish -c. Abbreviations expand only in the line editor, so nothing outside an interactive session can use them. --- conf.d/abbr.fish | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conf.d/abbr.fish b/conf.d/abbr.fish index 5771e32..b8016d1 100644 --- a/conf.d/abbr.fish +++ b/conf.d/abbr.fish @@ -12,6 +12,9 @@ # site abbr-integrations: integrations/terminal-abbrs # site abbr-overrides: overrides/key-bindings +# Abbreviations only expand in the line editor; a script can never use one. +status is-interactive; or return + # Neovim # @category Editors # @desc nvim From 13d415db2bbbbc72ab5db54049f3fbede46b50f7 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:56:16 -0400 Subject: [PATCH 07/39] perf(conf.d): skip bash_expands.fish in non-interactive shells The six expand_* functions are only ever reached through abbr --function, which fires during interactive expansion. --- conf.d/bash_expands.fish | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conf.d/bash_expands.fish b/conf.d/bash_expands.fish index d81dc91..89c5bf7 100644 --- a/conf.d/bash_expands.fish +++ b/conf.d/bash_expands.fish @@ -7,6 +7,10 @@ # Provides bash-style history expansion functions for abbreviations. # These functions are gated by the C3 overrides switch. +# The six expand_* functions are reachable only through abbr --function +# (conf.d/abbr.fish:677-697), i.e. only during interactive expansion. +status is-interactive; or return + # Execute expand_bang_all function expand_bang_all --description 'Execute expand_bang_all' # Opinionated guard (C3): no expansion when overrides are disabled. From bb5b6b361abf8b1593f39e5758d73ea439094d2e Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:57:02 -0400 Subject: [PATCH 08/39] perf(conf.d): skip key_bindings.fish in non-interactive shells fish_user_key_bindings is invoked only by the interactive reader. --- conf.d/key_bindings.fish | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conf.d/key_bindings.fish b/conf.d/key_bindings.fish index 588e073..e6143ab 100644 --- a/conf.d/key_bindings.fish +++ b/conf.d/key_bindings.fish @@ -47,6 +47,10 @@ # This allows for rapid-fire math without leaving the current shell. # ────────────────────────────────────────────────────────────────────── +# Defines only fish_user_key_bindings, which fish calls from the interactive +# reader and nowhere else. +status is-interactive; or return + function fish_user_key_bindings # Custom key chords are opinionated (C3 overrides); skip them entirely From 4d6b99fb28a9a45bb1a2e53558cc26b61ae72b79 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:58:18 -0400 Subject: [PATCH 09/39] perf(conf.d): skip starship.fish in non-interactive shells 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. --- conf.d/starship.fish | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conf.d/starship.fish b/conf.d/starship.fish index fc9673e..adee42b 100644 --- a/conf.d/starship.fish +++ b/conf.d/starship.fish @@ -9,6 +9,10 @@ # Without starship, fish's built-in prompt already emits OSC 133;A # on the prompt line itself, so no wrapper is needed. +# Defines fish_prompt; no script renders a prompt. Checked before the +# op-guard so the builtin short-circuits ahead of three function autoloads. +status is-interactive; or return + # Replacing the prompt is opinionated (C3 overrides) __fish_config_op_enabled (status basename); or return From dd672ded3b3ba1d034739b92d62fb29f189ab616 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:59:05 -0400 Subject: [PATCH 10/39] fix(tests): isolate the vault suite from the live config and universal variables 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. --- tests/run-tests.fish | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/tests/run-tests.fish b/tests/run-tests.fish index 59bc260..35336af 100755 --- a/tests/run-tests.fish +++ b/tests/run-tests.fish @@ -93,11 +93,43 @@ end # the suite builds its own throwaway git repos and binds the vault, claude # and agy roots to them, so it needs no loaded config and must never see # the real ~/.claude. +# +# HOME is deliberately NOT overridden here. Read this before "improving" it. +# +# Overriding XDG_CONFIG_HOME/XDG_DATA_HOME plus --no-config is what makes +# this run isolated: the universal-variable file fish can reach is a fresh +# empty one, and no config.fish/conf.d is loaded. Without that, an +# "isolated" suite runs against the user's LIVE config and real universal +# variables -- this repo doubles as a real ~/.config/fish -- so a test +# doing `set -e __fish_config_op_logging` would erase a real universal +# variable out of the running shell. Measured: +# $__fish_config_op_registry_keys has 65 entries under a plain `fish`, 0 +# under `fish --no-config`. +# +# `env -i HOME=$sandbox` was tried and REJECTED. It looks strictly more +# hermetic, but test-agents-vault.fish's hermeticity floor snapshots the +# real $HOME/.claude/memory and $HOME/.gemini/antigravity-cli and asserts +# them unchanged at the end. Point HOME at a sandbox and both snapshots +# read "absent" before and after: the assertions still pass while +# asserting nothing. A change that turns a real assertion into a tautology +# without turning anything red is the worst failure mode a test harness +# has. Keeping HOME real is what keeps those two assertions biting. +# +# Overriding XDG_DATA_HOME is a hermeticity gain on top of the isolation: +# _agents_vault_dir falls back to +# ${XDG_DATA_HOME:-$HOME/.local/share}/agent-vault, so a vault path that +# no test overrode lands in a temp dir instead of the user's real +# ~/.local/share/agent-vault. echo "" echo "== Vault helper tests ==" -fish $repo_root/tests/test-agents-vault.fish +set -l vault_xdg (mktemp -d) +env XDG_CONFIG_HOME=$vault_xdg/cfg XDG_DATA_HOME=$vault_xdg/data \ + fish --no-config $repo_root/tests/test-agents-vault.fish if test $status -ne 0 set overall_failed 1 end +# `command rm`, not bare `rm`: this driver runs under the config it tests, +# which shadows rm/cp/cat. See the Phase 2 note for the full reasoning. +command rm -rf $vault_xdg exit $overall_failed From 9077d9837e9fed4c29cea84998c8d965e133d3f2 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:59:05 -0400 Subject: [PATCH 11/39] feat(help): add __fish_help_header runtime renderer 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. --- functions/__fish_help_header.fish | 141 ++++++++++++++++++++++++++++++ tests/functional.fish | 85 ++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 functions/__fish_help_header.fish diff --git a/functions/__fish_help_header.fish b/functions/__fish_help_header.fish new file mode 100644 index 0000000..68920be --- /dev/null +++ b/functions/__fish_help_header.fish @@ -0,0 +1,141 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# __fish_help_header [args...] +# +# DESCRIPTION +# Prints 's man-page comment header as a help menu on stdout. +# Intended as the first statement of a user-facing function's body: +# +# __fish_help_header (status current-function) $argv; and return 0 +# +# Returns 1 -- printing nothing -- ONLY when args[1] is not a help flag. +# Every other outcome, including an unreadable or headerless source +# file, prints something and returns 0. That asymmetry is load-bearing: +# a return of 1 means "run the real body", and the real body of upgrade +# is `paru -Syu --noconfirm`. A parse failure must never return 1. +# +# Only args[1] is inspected, never the whole list. wake-lock, bkg, +# split and spwin take a command to run as their arguments, so +# scanning all of $argv would make `wake-lock rsync --help` print +# wake-lock's own help instead of running rsync. +# +# The header is read from the caller's source at call time rather than +# from the generated manual, so it cannot go stale between a header +# edit and a docs rebuild. +# +# ARGUMENTS +# name The calling function's name, from (status current-function) +# args... The caller's $argv, forwarded verbatim +# +# EXIT STATUS +# 0 Help was printed, including the degraded fallback +# 1 args[1] is not -h/--help; the caller should carry on +# +# EXAMPLE +# __fish_help_header (status current-function) $argv; and return 0 +# +# NOTES +# Section labels are those of the manual SSOT parser in +# docs/manualtools.py. CATEGORY, COMPONENT and DEPENDENCIES are build +# metadata and are suppressed; SYNOPSIS renders as USAGE and EXAMPLE as +# EXAMPLES. +function __fish_help_header --argument-names name + # First argument only -- see DESCRIPTION. + contains -- "$argv[2]" -h --help; or return 1 + + set -l c_ttl (set_color --bold) + set -l c_sec (set_color --bold brblue) + set -l c_rst (set_color normal) + set -l miss " No documentation header found. Try: help config $name" + + set -l file (functions -D -- $name 2>/dev/null) + if not test -f "$file" + # Quoted: set_color yields an EMPTY LIST under TERM=dumb, and an + # unquoted empty list in a concatenation annihilates the whole + # word -- the title line would silently vanish wherever colour is + # off, which is exactly where a test would be reading it. + echo "$c_ttl$name$c_rst" + echo $miss + return 0 + end + + # Collect the contiguous comment run directly above `function `, + # walking backwards. This resolves multi-header files (fish-deps, gi, + # y) without reimplementing manualtools._block_identity, and is more + # accurate at runtime: in dops.fish it finds the header above + # `function docker` rather than attributing it to the file stem. + # One blank separator line is tolerated -- sponge_filter_secrets.fish + # is the only file that has one, and JOB-BRIEF-FINDINGS.md records it + # so this skip is not mistaken for dead code. + set -l lines (string split \n -- (command cat $file)) + set -l pat '^\s*function\s+'(string escape --style=regex -- $name)'(\s|$)' + set -l start 0 + for i in (seq (count $lines)) + if string match -qr -- $pat $lines[$i] + set start $i + break + end + end + + set -l header + if test $start -gt 1 + set -l j (math $start - 1) + if test -z (string trim -- "$lines[$j]") + set j (math $j - 1) + end + while test $j -ge 1; and string match -q '#*' -- $lines[$j] + set -p header $lines[$j] + set j (math $j - 1) + end + end + + # Render. Comment lines before the first `# LABEL` -- the copyright + # preamble -- carry no label and are dropped, matching + # manualtools._header_blocks. + set -l skip CATEGORY COMPONENT DEPENDENCIES + set -l label "" + set -l out + for line in $header + set -l m (string match -r -- '^#\s+([A-Z][A-Z ]*[A-Z])\s*$' $line) + if set -q m[2] + set label $m[2] + contains -- $label $skip; and continue + set -l shown (string replace SYNOPSIS USAGE -- $label) + set shown (string replace EXAMPLE EXAMPLES -- $shown) + # One blank line before a heading, never two: the header's own + # `#` separator has usually already emitted one. + if set -q out[1]; and test -n (string trim -- "$out[-1]") + set -a out "" + end + set -a out "$c_sec$shown$c_rst" + continue + end + test -n "$label"; or continue + contains -- $label $skip; and continue + set -l body (string sub -s 2 -- $line) + if string match -q ' *' -- $body + set -a out " "(string sub -s 4 -- $body) + else + set -a out (string trim -- $body) + end + end + + # Trim the trailing blank separator, mirroring + # manualtools._trailing_blanks. + while set -q out[-1]; and test -z (string trim -- "$out[-1]") + set -e out[-1] + end + + echo "$c_ttl$name$c_rst" + if test (count $out) -eq 0 + echo $miss + else + # out[1] is always a heading -- a body line cannot precede the + # first label -- so this blank is never doubled. + echo "" + printf '%s\n' $out + end + return 0 +end diff --git a/tests/functional.fish b/tests/functional.fish index e4d1651..4641cc1 100644 --- a/tests/functional.fish +++ b/tests/functional.fish @@ -97,6 +97,91 @@ function test_vault_dir_honors_override test "$got" = /tmp/vault-override-check end +# ── Header-driven --help ───────────────────────────────────────────── +# Helper: run ` $argv` in a throwaway fish that can see both $dir and +# the loaded session's function path, so a fixture function can call the +# real __fish_help_header. Paths here are mktemp -d output, never spaced. +function _help_probe --argument-names dir + env TERM=dumb fish --no-config -c \ + "set -g fish_function_path $dir $fish_function_path; $argv[2..]" +end + +function test_help_renderer + set -l tmp (mktemp -d) + printf '%s\n' \ + '# Copyright (C) 2026 Rootiest' \ + '' \ + '# CATEGORY' \ + '# 99-fixture' \ + '#' \ + '# SYNOPSIS' \ + '# fixturefn [options]' \ + '#' \ + '# DESCRIPTION' \ + '# First paragraph.' \ + '#' \ + '# Second paragraph.' \ + '#' \ + '# ARGUMENTS' \ + '# -x Do the thing' \ + '# more Indented continuation' \ + '#' \ + '# EXAMPLE' \ + '# fixturefn -x' \ + 'function fixturefn' \ + ' __fish_help_header (status current-function) $argv; and return 0' \ + ' echo RAN-BODY' \ + 'end' >$tmp/fixturefn.fish + + set -l out (_help_probe $tmp 'fixturefn --help') + set -l code $status + set -l text (string join \n $out) + rm -rf $tmp + + set -l failed 0 + if test $code -ne 0 + echo " renderer exited $code, expected 0" + set failed 1 + end + if contains -- RAN-BODY $out + echo " body executed despite --help" + set failed 1 + end + if not contains -- USAGE $out + echo " missing USAGE heading (SYNOPSIS should render as USAGE)" + set failed 1 + end + if contains -- CATEGORY $out + echo " CATEGORY leaked into the menu" + set failed 1 + end + if not string match -q '* more Indented continuation*' -- $text + echo " nested ARGUMENTS indentation lost" + set failed 1 + end + # Index-based, not a glob: fish's `string match` glob `*` does not + # span newlines, so a pattern straddling two lines silently never + # matches and the assertion would pass for the wrong reason. + set -l i (contains -i -- " First paragraph." $out) + if test -z "$i" + echo " DESCRIPTION body missing entirely" + set failed 1 + else + # Indices hoisted: a command substitution inside a quoted index + # ("$out[(math ...)]") is a fish parse error, not an expansion. + set -l gap (math $i + 1) + set -l nxt (math $i + 2) + if test -n "$out[$gap]" + echo " multi-paragraph DESCRIPTION lost its blank line" + set failed 1 + else if test "$out[$nxt]" != " Second paragraph." + echo " second paragraph missing after the blank" + set failed 1 + end + end + test $failed -eq 0 +end + function functional_test_main set -l names (functions -a | string match 'test_*' | sort) set -l failed 0 From 5edfb5b72018de162c8a653a5f9fc60ec6ef76fe Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:59:10 -0400 Subject: [PATCH 12/39] perf(conf.d): skip theme colors in non-interactive shells 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. --- conf.d/theme.fish | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/conf.d/theme.fish b/conf.d/theme.fish index 5944728..26bc834 100644 --- a/conf.d/theme.fish +++ b/conf.d/theme.fish @@ -20,6 +20,11 @@ if not __fish_config_op_enabled (status basename) return end +# Below the cleanup block on purpose: that branch erases a stale universal +# FZF_DEFAULT_OPTS and must keep running wherever it runs today. Everything +# past here is fish_color_* for the syntax highlighter, interactive-only. +status is-interactive; or return + # ────────────────────── Syntax highlighting colors ────────────────────── set --global fish_color_autosuggestion 6c7086 set --global fish_color_cancel f38ba8 From 2edea59ca33f2120b1d0e89aba3ef0f578491b2a Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 15:00:01 -0400 Subject: [PATCH 13/39] perf(conf.d): skip auto-pull handler in non-interactive shells 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. --- conf.d/auto-pull.fish | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/conf.d/auto-pull.fish b/conf.d/auto-pull.fish index 852559a..db4f7f5 100644 --- a/conf.d/auto-pull.fish +++ b/conf.d/auto-pull.fish @@ -13,6 +13,11 @@ # # Manage the registry with: auto-pull add / remove / list / status +# Registers an --on-variable PWD handler that backgrounds a git fetch. In a +# script that cd's, that is both wasted work and AGENTS.md Task #4's +# credential-prompt hazard fired from a background job. +status is-interactive; or return + # C2 guard: when auto-execution is disabled, do not register the handler. __fish_config_op_enabled (status basename); or exit From 8fc27fa9c276f9a43a54747873016cce7d410fe8 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 15:00:10 -0400 Subject: [PATCH 14/39] fix(tests): route the runner's utilities through command 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. --- tests/run-tests.fish | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/run-tests.fish b/tests/run-tests.fish index 35336af..4ebb2d1 100755 --- a/tests/run-tests.fish +++ b/tests/run-tests.fish @@ -53,12 +53,22 @@ mkdir -p $sandbox_cfg # no-op on missing paths), so give it $HOME/.local/bin to find. mkdir -p $sandbox/home/.local/bin -cp $repo_root/config.fish $sandbox_cfg/ +# Every utility below goes through `command`. This driver runs under the +# very config it tests, which shadows these: `cp` is an alias for `cp -i`, +# `rm` is a trash wrapper, `cat` resolves to bat. Only `cp` is an actual +# hazard today -- `-i` on a non-empty destination reads EOF in a +# non-interactive runner and SILENTLY SKIPS the copy while exiting 0, +# which would leave the sandbox missing config files and report 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. +command cp $repo_root/config.fish $sandbox_cfg/ test -f $repo_root/fish_plugins -and cp $repo_root/fish_plugins $sandbox_cfg/ +and command cp $repo_root/fish_plugins $sandbox_cfg/ for d in functions conf.d completions integrations themes data test -d $repo_root/$d - and cp -r $repo_root/$d $sandbox_cfg/ + and command cp -r $repo_root/$d $sandbox_cfg/ end set -l err_file (mktemp) @@ -72,8 +82,8 @@ env -i \ 2>$err_file set -l functional_status $status -set -l stderr_out (cat $err_file) -rm -rf $sandbox $err_file +set -l stderr_out (command cat $err_file) +command rm -rf $sandbox $err_file if test -n "$stderr_out" # Diagnostic only, not a gate: on machines with vendor fish configs From 5fa849fd81763bc22ec106d1bfb6f8253d14d7d1 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 15:00:57 -0400 Subject: [PATCH 15/39] perf(conf.d): skip wakatime hook in non-interactive shells fish_postexec is emitted only by the interactive reader (verified), so the handler could never fire in a script. No telemetry behaviour changes. --- conf.d/wakatime.fish | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conf.d/wakatime.fish b/conf.d/wakatime.fish index db97a43..e4568bf 100644 --- a/conf.d/wakatime.fish +++ b/conf.d/wakatime.fish @@ -9,6 +9,10 @@ # site wakatime-autoexec: autoexec/telemetry # site wakatime-hook: integrations/notifications +# Registers a fish_postexec handler; that event is emitted only by the +# interactive reader, so the handler is dead weight in a script. +status is-interactive; or return + # Local modification: opinionated guard (AGENTS.md Task #3). WakaTime # reporting is classified under both C2 auto-execution and C4 integrations; # disabling either category skips registering the hook. From c998d1b8d7fbdef4eec32153872f1727dfbbfdc9 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 19:56:45 -0400 Subject: [PATCH 16/39] perf(conf.d): skip C5 logging sync in non-interactive shells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit __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. --- conf.d/logging-events.fish | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/conf.d/logging-events.fish b/conf.d/logging-events.fish index 892f518..53e1287 100644 --- a/conf.d/logging-events.fish +++ b/conf.d/logging-events.fish @@ -13,6 +13,12 @@ # solely in functions/ are never registered and their --on-variable triggers # never fire. +# Calls __fish_config_sync_logging at every shell start, which mkdir+touches +# the C5 sentinel on disk. Its only consumers — the Kitty watcher and the +# paru/yay wrappers — are interactive-context; every interactive shell still +# refreshes it. +status is-interactive; or return + function __fish_config_logging_changed --on-variable __fish_config_op_logging \ --description 'C5 event handler: sync logging state when __fish_config_op_logging changes' __fish_config_sync_logging From 52711a42a2c06644936bd4c923075ac6a7996ecc Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 19:58:10 -0400 Subject: [PATCH 17/39] perf(conf.d): skip paru/yay wrapper generation in non-interactive shells Neither file defines a function or sets a global; their only effect is writing ~/.local/bin/, which every interactive session does anyway. Combined 10.7 ms off every fish -c. --- conf.d/paru-wrapper.fish | 4 ++++ conf.d/yay-wrapper.fish | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/conf.d/paru-wrapper.fish b/conf.d/paru-wrapper.fish index bef1e54..e5e629e 100644 --- a/conf.d/paru-wrapper.fish +++ b/conf.d/paru-wrapper.fish @@ -10,6 +10,10 @@ # site paru-autoexec: autoexec/pkg-wrappers # site paru-logging: logging/pkg-logs +# Defines nothing; its only effect is generating ~/.local/bin/paru, an +# idempotent write every interactive session already performs. +status is-interactive; or return + # Auto-generating a wrapper in ~/.local/bin is opinionated (C2 auto-exec). # Wrapper generation is also gated by C5 (Logging & Capture). __fish_config_op_enabled (status basename) paru-autoexec; or return diff --git a/conf.d/yay-wrapper.fish b/conf.d/yay-wrapper.fish index 53da589..6f43360 100644 --- a/conf.d/yay-wrapper.fish +++ b/conf.d/yay-wrapper.fish @@ -10,6 +10,10 @@ # site yay-autoexec: autoexec/pkg-wrappers # site yay-logging: logging/pkg-logs +# Defines nothing; its only effect is generating ~/.local/bin/yay, an +# idempotent write every interactive session already performs. +status is-interactive; or return + # Auto-generating a wrapper in ~/.local/bin is opinionated (C2 auto-exec). # Wrapper generation is also gated by C5 (Logging & Capture). __fish_config_op_enabled (status basename) yay-autoexec; or return From d3028d770389f314c00b7fece724a8980490a117 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 19:59:23 -0400 Subject: [PATCH 18/39] refactor(tests): extract the shared assertion core into tests/lib.fish 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. --- tests/lib.fish | 62 ++++++++++++++++++++++++++++++++++++ tests/test-agents-vault.fish | 21 ++---------- 2 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 tests/lib.fish diff --git a/tests/lib.fish b/tests/lib.fish new file mode 100644 index 0000000..2edfc91 --- /dev/null +++ b/tests/lib.fish @@ -0,0 +1,62 @@ +#!/usr/bin/env fish +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Shared assertion and reporting core for tests/test-*.fish. +# +# One assertion: `check