From f6ead99f487a61c5fcd58471af512acd5a062e17 Mon Sep 17 00:00:00 2001 From: Rootiest Date: Mon, 7 Sep 2026 20:09:30 -0400 Subject: [PATCH] feat(help): header-driven --help for development functions, plus the wiring guard bd-pull, branch, check_fish_deps, claude-docs, claude-pr, fast, poke, qr and parur take the standard call site. fish-deps instead routes -h/--help to its existing __fish_deps_help, which previously answered only via the unknown-subcommand path, printing "Unknown subcommand: --help" and exiting 1. This completes the 40 conversions and lands test_every_user_facing_function_has_help -- acceptance criterion 6, the check that fails when a new user-facing function ships without help. It was developed red against the pre-conversion tree and reported exactly the 40 names of the design's CONVERT table: bd-pull bkg branch check_fish_deps claude-docs claude-pr cleanup fast fc fish-deps fzf-update gip gip4 gip6 hist lD ld limine-edit lock lsr lss lstree lt ltr lx parur poke ports qr sbver screensleep split spwin steam-dl sudo-toggle swapstat tab tmux-clean upgrade wake-lock each as ": no -h/--help handling and not in $__help_exempt", with the guard exiting 1. The count fell 40 -> 32 -> 24 -> 17 -> 10 -> 0 across the conversion commits. It is committed green so every commit on this branch passes the suite. $__help_exempt is the single machine-readable exemption record; a second assertion fails if an exempt name stops being a published function, so a rename or deletion upstream cannot silently exempt nothing. --- functions/bd-pull.fish | 2 ++ functions/branch.fish | 2 ++ functions/check_fish_deps.fish | 2 ++ functions/claude-docs.fish | 2 ++ functions/claude-pr.fish | 2 ++ functions/fast.fish | 2 ++ functions/fish-deps.fish | 7 ++++ functions/parur.fish | 2 ++ functions/poke.fish | 2 ++ functions/qr.fish | 2 ++ tests/functional.fish | 66 ++++++++++++++++++++++++++++++++++ 11 files changed, 91 insertions(+) diff --git a/functions/bd-pull.fish b/functions/bd-pull.fish index 87b60de..0def40d 100644 --- a/functions/bd-pull.fish +++ b/functions/bd-pull.fish @@ -23,6 +23,8 @@ # bd-pull myuser/myproject # bd-pull rootiest/fish-config function bd-pull --description 'Pull new Gitea issues into local Beads and link them' + __fish_help_header (status current-function) $argv; and return 0 + if not set -q argv[1]; echo "Need repo owner/name"; return 1; end if not set -q GITEA_TOKEN; echo "\$GITEA_TOKEN not set"; return 1; end diff --git a/functions/branch.fish b/functions/branch.fish index 836320d..6143685 100644 --- a/functions/branch.fish +++ b/functions/branch.fish @@ -21,6 +21,8 @@ # EXAMPLE # branch feature/new-ui function branch --description 'Switch to or create a git branch' + __fish_help_header (status current-function) $argv; and return 0 + if not git rev-parse --is-inside-work-tree >/dev/null 2>&1 echo "Not a git repo." return 1 diff --git a/functions/check_fish_deps.fish b/functions/check_fish_deps.fish index 9bae21b..fd24581 100644 --- a/functions/check_fish_deps.fish +++ b/functions/check_fish_deps.fish @@ -14,5 +14,7 @@ # EXAMPLE # check_fish_deps function check_fish_deps --description 'Check all fish-related dependencies' + __fish_help_header (status current-function) $argv; and return 0 + fish-deps status end diff --git a/functions/claude-docs.fish b/functions/claude-docs.fish index a4979b5..1d7717b 100644 --- a/functions/claude-docs.fish +++ b/functions/claude-docs.fish @@ -15,5 +15,7 @@ # EXAMPLE # claude-docs function claude-docs --description 'Claude-code: Sync README with recent changes' + __fish_help_header (status current-function) $argv; and return 0 + claude "Analyze the recent changes and update the README.md to ensure all features, setup instructions, and examples are 100% accurate. Prune any obsolete information." end diff --git a/functions/claude-pr.fish b/functions/claude-pr.fish index 507d753..d38f36e 100644 --- a/functions/claude-pr.fish +++ b/functions/claude-pr.fish @@ -15,5 +15,7 @@ # EXAMPLE # claude-pr function claude-pr --description 'Claude-code: New branch, commit, push, and PR' + __fish_help_header (status current-function) $argv; and return 0 + claude "Act as a senior engineer. Execute this sequence: 1. Create a new git branch (kebab-case). 2. Stage changes and write a Conventional Commit message. 3. Self-verify the changes by running relevant build/test commands or linting. 4. Push to remote. 5. Create a PR to 'main' including a summary of changes and a 'Manual Verification' section containing a Markdown checklist (- [ ]) of specific, bite-sized steps required to manually verify the functionality." end diff --git a/functions/fast.fish b/functions/fast.fish index 5cc0dfe..7fec3cb 100644 --- a/functions/fast.fish +++ b/functions/fast.fish @@ -36,6 +36,8 @@ # EXAMPLE # fast function fast --description 'Placeholder for future fast utility' + __fish_help_header (status current-function) $argv; and return 0 + # ANSI Escape Codes (Standard 16-color palette) set -l bold "\e[1m" set -l italic "\e[3m" diff --git a/functions/fish-deps.fish b/functions/fish-deps.fish index bf9839e..1e244ab 100644 --- a/functions/fish-deps.fish +++ b/functions/fish-deps.fish @@ -66,6 +66,13 @@ function fish-deps --description 'Manage fish shell dependencies' _fish_deps_status case install _fish_deps_install $flags + case -h --help + # Reuse the existing menu rather than the header renderer: it + # is richer, and it is already the text the unknown-subcommand + # path prints. Previously --help fell into `case '*'` and + # exited 1 with "Unknown subcommand: --help". + __fish_deps_help + return 0 case update _fish_deps_update case sync diff --git a/functions/parur.fish b/functions/parur.fish index 62a65e1..6f6f92c 100644 --- a/functions/parur.fish +++ b/functions/parur.fish @@ -19,6 +19,8 @@ # EXAMPLE # parur function parur --description 'Interactively search and remove an installed package using fzf' + __fish_help_header (status current-function) $argv; and return 0 + set -l aur "" if type -q paru set aur paru diff --git a/functions/poke.fish b/functions/poke.fish index 8cff1c5..ea9bd59 100644 --- a/functions/poke.fish +++ b/functions/poke.fish @@ -21,6 +21,8 @@ # EXAMPLE # poke ~/projects/new/src/main.fish function poke --description 'touch with automatic parent directory creation' + __fish_help_header (status current-function) $argv; and return 0 + if test (count $argv) -eq 0 echo (set_color red)"poke: no file specified"(set_color normal) >&2 return 1 diff --git a/functions/qr.fish b/functions/qr.fish index 804ded5..50af5d7 100644 --- a/functions/qr.fish +++ b/functions/qr.fish @@ -19,6 +19,8 @@ # qr "https://example.com" # echo "hello" | qr function qr --description 'Generate a QR code from text or pipe' + __fish_help_header (status current-function) $argv; and return 0 + if type -q qrencode if set -q argv[1] echo $argv | qrencode -t utf8 diff --git a/tests/functional.fish b/tests/functional.fish index f027194..759188b 100644 --- a/tests/functional.fish +++ b/tests/functional.fish @@ -289,6 +289,72 @@ function test_help_never_executes_destructive_path test $failed -eq 0 end +# Functions published in the manual that are exempt from the -h/--help +# rule. Rationale per entry: AGENTS/specs/2026-09-07-header-driven-help-design.md +# §4. This array is the ONLY machine-readable copy of the exempt set. +# +# EXEMPT-A -- shadows a same-named binary, or forwards $argv to one named +# tool that owns its own --help. Intercepting would hide that tool's help, +# and for the C1-guarded shadows it also breaks the disabled-fallback +# contract, where the bare tool is supposed to answer. +set -g __help_exempt \ + agy antigravity-ide bash cat cdi cffetch cheat claude clone clonet \ + config-toggle copy docker du dusize fast-cli ffetch gitui gitup jr \ + joplin less ls mkdir mv paste ping rawfish rg rm search ssh top \ + view yt-dlp +# EXEMPT-B -- invoked by fish, never typed by a user. +set -a __help_exempt fish_prompt fish_right_prompt fish_mode_prompt \ + sponge_filter_secrets + +function test_every_user_facing_function_has_help + set -l root (realpath (dirname (status filename))/..) + set -l failed 0 + set -l published + + for f in $root/functions/*.fish + set -l lines (string split \n -- (command cat $f)) + # Published == carries a `# CATEGORY` block, matching + # manualtools.parse_functions. + contains -- "# CATEGORY" (string trim -- $lines); or continue + # Resolve the real defined name; the file stem can disagree + # (dops.fish defines `docker` -- see JOB-BRIEF-FINDINGS.md §1). + set -l name (string match -rg '^\s*function\s+(\S+)' -- $lines)[1] + test -n "$name"; or continue + set name (string trim -c "'\"" -- $name) + string match -q '_*' -- $name; and continue + set -a published $name + + contains -- $name $__help_exempt; and continue + + # Body == everything from the `function` line down, comment lines + # dropped, so a header that merely mentions --help cannot pass. + set -l body + set -l in_body 0 + for l in $lines + test $in_body -eq 1; or string match -qr '^\s*function\s' -- $l; and set in_body 1 + test $in_body -eq 1; or continue + string match -qr '^\s*#' -- $l; and continue + set -a body $l + end + if not string match -qr -- '__fish_help_header|_flag_help|h/help|--help' \ + (string join \n -- $body) + echo " $name: no -h/--help handling and not in \$__help_exempt" + set failed 1 + end + end + + # Guard against a stale exempt list: every exempt name must still be a + # published function. Catches renames and deletions. + for e in $__help_exempt + if not contains -- $e $published + echo " \$__help_exempt lists '$e', which is no longer published" + 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