From 21a02fd0e40e4d90a288c09f9204c95e0f87b5cc Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 03:06:30 -0400 Subject: [PATCH 01/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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/50] 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 51da3e9d83b426c98a9c784baec9bb2b557edb1a Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:57:07 -0400 Subject: [PATCH 09/50] test: add byte-identity harness for the shared palette Compares rendered stdout and stderr of 29 colour-bearing cases between a pristine baseline ref and the working tree, in isolated XDG_CONFIG_HOMEs that carry the gitignored fish_variables so the opinionated guards resolve. Two traps this harness exists to avoid, both of which silently produce a meaningless comparison rather than an error: - `git archive main` omits fish_variables because it is untracked. Without it __fish_config_op_enabled is unresolvable and every guarded function short-circuits, so all cases render empty and trivially "match". - `qc --help` shells out to aichat and never reaches its colour path unless aichat is on PATH; the harness stubs it. --- tests/palette-bytes.fish | 157 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 tests/palette-bytes.fish diff --git a/tests/palette-bytes.fish b/tests/palette-bytes.fish new file mode 100644 index 0000000..b94a289 --- /dev/null +++ b/tests/palette-bytes.fish @@ -0,0 +1,157 @@ +#!/usr/bin/env fish +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# Byte-identity harness for the shared output palette (__fish_palette). +# +# Compares rendered output of every colour-bearing function between a +# pristine checkout of a baseline git ref and the current working tree. +# This is a one-time acceptance harness, not part of run-tests.fish's +# permanent suite -- the permanent check lives in tests/functional.fish. +# +# Usage: +# fish tests/palette-bytes.fish [--baseline REF] byte-diff stdout+stderr +# fish tests/palette-bytes.fish --structural diff-shape assertion +# fish tests/palette-bytes.fish --startup startup medians +# +# Both sandboxes get an isolated XDG_CONFIG_HOME carrying a copy of the +# working tree's fish_variables. That file is gitignored, so `git archive` +# omits it; without it __fish_config_op_enabled is unresolvable and every +# opinionated-guarded function silently short-circuits. + +set -l repo (realpath (dirname (status filename))/..) +set -l mode bytes +set -l baseline main +for i in (seq (count $argv)) + switch $argv[$i] + case --structural; set mode structural + case --startup; set mode startup + case --baseline; set baseline $argv[(math $i + 1)] + end +end + +set -l tmp (mktemp -d) +function __pb_cleanup --on-event fish_exit --inherit-variable tmp + test -n "$tmp"; and rm -rf $tmp +end + +# ── Build the two sandboxes ──────────────────────────────────────────── +set -l A $tmp/base/fish # pristine baseline ref +set -l B $tmp/work/fish # current working tree +mkdir -p $A $B +git -C $repo archive $baseline | tar -x -C $A +or begin + echo "palette-bytes: cannot archive baseline ref '$baseline'" >&2 + exit 2 +end +for d in functions conf.d completions integrations themes data + test -d $repo/$d; and cp -r $repo/$d $B/ +end +cp $repo/config.fish $B/ 2>/dev/null +# fish_variables is gitignored -- copy it into BOTH sandboxes by hand. +for d in $A $B + cp $repo/fish_variables $d/ 2>/dev/null +end +# qc --help shells out to aichat; stub it so its colour path is reachable. +set -l stub $tmp/stub +mkdir -p $stub +printf '#!/bin/sh\necho "aichat stub"\n' >$stub/aichat +chmod +x $stub/aichat + +# ── The cases ────────────────────────────────────────────────────────── +# 25 --help paths (every converted function that has one) plus 4 error +# paths, two of which write to stderr. Side-effect-free by construction: +# --help returns before doing work, and each error path fails on argument +# validation. Do NOT add a case that mutates the filesystem. +set -l cases \ + "agents-init --help" "agents-vault --help" "auto-pull --help" \ + "config-settings --help" "config-update --help" "detach --help" \ + "dng2avif --help" "dockup --help" "edit --help" "jobrunner --help" \ + "kitty-logging --help" "logs --help" "mkcd --help" "open-url --help" \ + "p --help" "pkg --help" "play-media --help" "qc --help" \ + "rand_string --help" "replay --help" "repo-open --help" "scrub --help" \ + "smart_exit --help" "spark --help" "y --help" \ + "mkcd" "auto-pull remove __no_such_repo__" \ + "agents-init --no-such-flag" "pkg __no_such_subcommand__" + +function __pb_run --argument-names cfg stub cmd out + env XDG_CONFIG_HOME=(dirname $cfg) PATH="$stub:$PATH" TERM=xterm-256color \ + HOME=$HOME fish -c "$cmd" >$out.out 2>$out.err +end + +# ── Mode: bytes ──────────────────────────────────────────────────────── +if test $mode = bytes + echo "== palette byte-identity vs $baseline ==" + set -l failed 0 + set -l n 0 + for cmd in $cases + set n (math $n + 1) + __pb_run $A $stub "$cmd" $tmp/a$n + __pb_run $B $stub "$cmd" $tmp/b$n + set -l so ok + set -l se ok + cmp -s $tmp/a$n.out $tmp/b$n.out; or set so DIFF + cmp -s $tmp/a$n.err $tmp/b$n.err; or set se DIFF + if test $so = DIFF -o $se = DIFF + set failed (math $failed + 1) + printf ' FAIL %-34s stdout=%s stderr=%s\n' "$cmd" $so $se + test $so = DIFF; and diff -u (xxd $tmp/a$n.out | psub) (xxd $tmp/b$n.out | psub) | head -12 + test $se = DIFF; and diff -u (xxd $tmp/a$n.err | psub) (xxd $tmp/b$n.err | psub) | head -12 + else + printf ' ok %-34s (%s B out, %s B err)\n' "$cmd" (wc -c <$tmp/a$n.out | string trim) (wc -c <$tmp/a$n.err | string trim) + end + end + echo (math $n - $failed)"/$n cases byte-identical" + test $failed -eq 0 + exit $status +end + +# ── Mode: structural ─────────────────────────────────────────────────── +# For files converted WITHOUT drift renames, the whole diff must be +# declaration removals plus inserted __fish_palette calls. If that holds, +# the file's output strings are provably untouched. +if test $mode = structural + echo "== structural diff shape vs $baseline ==" + set -l bad 0 + for f in (git -C $repo diff --name-only $baseline -- functions/) + # fish_prompt.fish keeps its own hex palette -- see Task 10. + string match -q '*fish_prompt.fish' $f; and continue + set -l offenders + for line in (git -C $repo diff -U0 $baseline -- $f | string match -r '^[+-][^+-].*') + set -l body (string sub -s 2 -- $line) + string match -qr '^\s*set -l c_[a-z]+\s+\(set_color[^)]*\)\s*$' -- $body; and continue + string match -qr '^\s*__fish_palette\s*$' -- $body; and continue + set -a offenders $line + end + if test (count $offenders) -gt 0 + set bad (math $bad + 1) + echo " NOT PURELY STRUCTURAL $f" + printf ' %s\n' $offenders[1..3] + end + end + if test $bad -eq 0 + echo " all changed files are purely structural" + else + echo " $bad file(s) changed rendering text -- expected only for the drift-rename batch" + end + test $bad -eq 0 + exit $status +end + +# ── Mode: startup ────────────────────────────────────────────────────── +echo "== fish -c true, 31 interleaved pairs, median ==" +set -l ta +set -l tb +for i in (seq 31) + set -l s (date +%s%N) + env XDG_CONFIG_HOME=$tmp/base fish -c true >/dev/null 2>&1 + set -a ta (math "("(date +%s%N)" - $s) / 1000") + set s (date +%s%N) + env XDG_CONFIG_HOME=$tmp/work fish -c true >/dev/null 2>&1 + set -a tb (math "("(date +%s%N)" - $s) / 1000") +end +set -l sa (printf '%s\n' $ta | sort -n) +set -l sb (printf '%s\n' $tb | sort -n) +printf ' baseline median=%.2f ms p10=%.2f p90=%.2f\n' (math $sa[16]/1000) (math $sa[4]/1000) (math $sa[28]/1000) +printf ' working median=%.2f ms p10=%.2f p90=%.2f\n' (math $sb[16]/1000) (math $sb[4]/1000) (math $sb[28]/1000) +echo " (p10-p90 spread is ~15 ms; treat any delta inside it as noise)" From 4d6b99fb28a9a45bb1a2e53558cc26b61ae72b79 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 14:58:18 -0400 Subject: [PATCH 10/50] 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 11/50] 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 12/50] 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 13/50] 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 14/50] 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 15/50] 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 a31a46bdc2933c866ae5e898528ac350857190b2 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 15:00:32 -0400 Subject: [PATCH 16/50] feat: add __fish_palette shared output palette helper 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. --- functions/__fish_palette.fish | 63 +++++++++++++++++++++++++++++++++++ tests/functional.fish | 27 +++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 functions/__fish_palette.fish diff --git a/functions/__fish_palette.fish b/functions/__fish_palette.fish new file mode 100644 index 0000000..7097f39 --- /dev/null +++ b/functions/__fish_palette.fish @@ -0,0 +1,63 @@ +# Copyright (C) 2026 Rootiest +# SPDX-License-Identifier: AGPL-3.0-or-later + +# SYNOPSIS +# __fish_palette +# +# DESCRIPTION +# Defines the shared terminal-output colour roles used by the +# user-facing functions in this configuration. Declared +# --no-scope-shadowing and using a bare `set`, so the variables are +# created in the CALLER's scope -- a consumer just calls it and then +# interpolates $c_head, $c_err and friends exactly as it did when the +# declarations were inline. +# +# Call it where the local declarations used to sit, once per contiguous +# block that needs the palette. set_color runs at call time, so the +# values track $TERM exactly as inline declarations did. (Measured: +# set_color output is identical across every TERM tested except +# TERM=dumb, which yields empty strings, and is unaffected by whether +# stdout is a tty or a pipe.) +# +# A role is a semantic slot, not a colour. c_flag and c_warn are both +# yellow but stay separate, as do c_ok and c_accent (both green) -- +# merging either pair would foreclose ever restyling one without the +# other. c_accent is the command name in logs and smart_exit, which +# style it green where the rest of the config styles it bold. +# +# ARGUMENTS +# none +# +# EXIT STATUS +# 0 always +# +# EXAMPLE +# function mytool +# __fish_palette +# echo "$c_head""Usage:$c_reset $c_cmd""mytool$c_reset" +# end +# +# NOTES +# Calling this at top level (outside any function) creates GLOBAL +# variables. Every consumer calls it from inside a function, where the +# variables stay function-local and do not leak. +# +# functions/fish_prompt.fish deliberately does NOT use this palette. Its +# c_* values are Catppuccin hex strings passed as ARGUMENTS to set_color +# (`set_color --bold $c_green`), not captured escape sequences -- colour +# inputs rather than rendered output, a different concern. + +function __fish_palette --no-scope-shadowing --description 'Define the shared output colour palette in the caller scope' + set c_reset (set_color normal) + set c_head (set_color --bold cyan) + set c_cmd (set_color --bold) + set c_arg (set_color cyan) + set c_flag (set_color yellow) + set c_warn (set_color yellow) + set c_err (set_color red) + set c_ok (set_color green) + set c_accent (set_color green) + set c_dim (set_color brblack) + set c_sel (set_color --bold magenta) + set c_hi (set_color --bold white) +end diff --git a/tests/functional.fish b/tests/functional.fish index e4d1651..68c64e8 100644 --- a/tests/functional.fish +++ b/tests/functional.fish @@ -97,6 +97,33 @@ function test_vault_dir_honors_override test "$got" = /tmp/vault-override-check end +function test_palette_roles_defined + functions -q __fish_palette + or begin + echo " __fish_palette is not defined" + return 1 + end + # Called from inside a function, the palette must land in THIS scope. + __fish_palette + set -l missing + for role in c_reset c_head c_cmd c_arg c_flag c_warn c_err c_ok \ + c_accent c_dim c_sel c_hi + if not set -q $role; or test -z "$$role" + set -a missing $role + end + end + if test (count $missing) -gt 0 + echo " palette roles empty or unset: $missing" + return 1 + end + # Nothing may leak to global scope. + if set -q -g c_reset + echo " __fish_palette leaked c_reset into global scope" + return 1 + end + return 0 +end + function functional_test_main set -l names (functions -a | string match 'test_*' | sort) set -l failed 0 From 5fa849fd81763bc22ec106d1bfb6f8253d14d7d1 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 15:00:57 -0400 Subject: [PATCH 17/50] 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 18/50] 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 19/50] 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 20/50] 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