feat: CLASSIFICATION function-header field + shadow-classification lint #163

Merged
rootiest merged 7 commits from feat/classification-header-field into main 2026-09-22 02:12:58 +00:00
91 changed files with 556 additions and 35 deletions
+12
View File
@@ -391,6 +391,7 @@ all optional except where noted:
| `CATEGORY` | **Required to appear in the manual at all** — see below. |
| `COMPONENT` | Only for functions gated by the [opinionated-component system](#opinionated-components). |
| `DEPENDENCIES` | Other functions this one calls that a reader may want to look up. |
| `CLASSIFICATION` | Hazard/shadow-interaction tags — see below. |
| `SYNOPSIS` | One-line usage form. |
| `DESCRIPTION` | Prose description; can span multiple paragraphs. |
| `ARGUMENTS` | Flags/positional args, one per line. |
@@ -414,6 +415,9 @@ A full example (`functions/claude.fish`):
# DEPENDENCIES
# agents-init
#
# CLASSIFICATION
# bypasses-shadow(claude)
#
# SYNOPSIS
# claude [ARGS...]
#
@@ -443,6 +447,14 @@ If your function genuinely doesn't fit any of these, add a new
`docs/manual/05-functions/NN-your-category.md` stub (with frontmatter
matching its siblings) rather than force-fitting it into an existing one.
**`CLASSIFICATION` flags hazards and shadow interactions, optional and
omitted when nothing applies:** whether the function calls a
[C1-shadowed command](docs/manual/08-components-reference/01-c1-command-shadows.md)
bare wanting the override (`uses-shadow(ls)`) or bypasses it deliberately
via `command`/`builtin` (`bypasses-shadow(cat)`), and general hazards —
`destructive`, `network`, `blocking-prompt`. Full tag definitions and
placement rule: [`docs/function-classification-schema.md`](docs/function-classification-schema.md).
### Private/internal helper functions
Functions named with a leading `_` (e.g. `_agents_init_ensure_gitignore`,
+3
View File
@@ -24,6 +24,9 @@ __fish_config_op_enabled (status basename); or exit
# COMPONENT
# autoexec/sync
#
# CLASSIFICATION
# bypasses-shadow(cat)
#
# SYNOPSIS
# __auto_pull_on_pwd (event handler, --on-variable PWD)
#
+6 -4
View File
@@ -97,11 +97,13 @@ if __fish_config_op_enabled (status basename) tricks-bang
end
end
# Fish command history override to show timestamps
# Shadowing the history command is opinionated (C1 aliasing); when disabled,
# the function is never defined and fish's stock history behavior applies.
# Timestamped history view. Named pretty-history (not history) so it never
# shadows the fish builtin -- every function in this config that expects
# stock `history` semantics (search, --max, merge, ...) would otherwise
# silently break, which has happened more than once. Opinionated (C1
# aliasing); when disabled, the function is never defined.
if __fish_config_op_enabled (status basename) aliases-tricks
function history
function pretty-history --description 'History with timestamps prepended to every entry'
builtin history --show-time='%F %T '
end
end
+25
View File
@@ -703,6 +703,29 @@ ENTRY_HEADS = {
}
def _classification_tags(raw: list[str]) -> list[str]:
"""Split a CLASSIFICATION body into its comma-separated tags.
A plain comma split (as `names()` uses for DEPENDENCIES) would break on
the commas inside `uses-shadow(rm, cp)`-style tags, so this only splits
on commas at paren depth 0.
"""
text = " ".join(raw)
tags: list[str] = []
depth = 0
start = 0
for i, ch in enumerate(text):
if ch == "(":
depth += 1
elif ch == ")":
depth = max(0, depth - 1)
elif ch == "," and depth == 0:
tags.append(text[start:i].strip())
start = i + 1
tags.append(text[start:].strip())
return [t for t in tags if t]
def render_entry(fn: dict[str, list[str]], used_by: list[str], link=None) -> str:
"""Render one parsed function header as a manual entry body.
@@ -739,6 +762,7 @@ def render_entry(fn: dict[str, list[str]], used_by: list[str], link=None) -> str
refs = []
for label, values in (
("Dependencies", names(fn.get("DEPENDENCIES", []))),
("Classification", _classification_tags(fn.get("CLASSIFICATION", []))),
("Used by", sorted(used_by)),
):
if values:
@@ -884,6 +908,7 @@ def render_entry_site(fn: dict[str, list[str]], used_by: list[str], link=None) -
refs = []
for label, values in (
("Dependencies", names(fn.get("DEPENDENCIES", []))),
("Classification", _classification_tags(fn.get("CLASSIFICATION", []))),
("Used by", sorted(used_by)),
):
if values:
+2
View File
@@ -291,6 +291,8 @@ network-shadow=### network
monitor=### monitor
shell-tools=### shell-tools
dev-tools=### dev-tools
classification=### For function authors
shadow-classification=### For function authors
c2=## C2 — Startup Side-Effects
autoexec=## C2 — Startup Side-Effects
startup=## C2 — Startup Side-Effects
+101
View File
@@ -0,0 +1,101 @@
# Function CLASSIFICATION schema
This is the canonical definition of the `# CLASSIFICATION` function
doc-header label. It's referenced from code comments and commit messages —
link here, not to anything under `AGENTS/` (that tree is git-ignored local
agent state, not part of the repo).
See [Public function documentation header](../CONTRIBUTING.md#public-function-documentation-header)
in `CONTRIBUTING.md` for where `CLASSIFICATION` fits among the other header
labels, and [C1 — Command Shadows](manual/08-components-reference/01-c1-command-shadows.md)
for the full list of C1-shadowed commands this schema's shadow tags refer to.
## Format
Optional. Comma-separated tags from the closed set below, on the indented
body line directly under the label:
```fish
# CLASSIFICATION
# uses-shadow(ls), destructive
```
Omit the label entirely when nothing applies — omission means "nothing to
flag," not "not yet audited," so don't add it speculatively, and don't add
it empty as a placeholder.
## Tags
- **`uses-shadow(name[,name...])`** — calls a C1-shadowed command (see the
C1 doc linked above) bare, deliberately wanting the overridden behavior
(e.g. `ls` wanting eza's icons for a human to read).
- **`bypasses-shadow(name[,name...])`** — calls `command <name>`,
`builtin <name>`, or (for `help` specifically) `__original_help $argv`,
deliberately forcing stock behavior because the shadow's override would
break this function's logic: timestamps leaking into a parsed capture,
`-i` prompting on a path meant to run unattended, structural output
changes breaking a `string`/`sed` parse, etc.
- **`self-limiting(name[,name...])`** — calls a shadowed command bare, and
it's safe not because the caller did anything but because *the shadow's
own logic* already neutralizes the override for this call. Verify the
actual condition per shadow, it's not the same check for each one:
- `rm` falls back to `command rm` for any flag **except** a bare `-r`,
`-R`, or `--recursive` (those still route to `trash put`) — so
`rm -f`/`rm -rf` qualify, but `rm -r $dir` alone does not.
- `mkdir` falls back to `command mkdir -p` for *any* flag at all, no
exception.
- `--color=auto`/`bat`'s own tty auto-detection (`grep`, `fgrep`,
`egrep`, `dir`, `vdir`, `cat` — verified byte-identical to stock when
piped or captured, since none of these force color on a
non-terminal).
Document it explicitly rather than leaving the bare call untagged: if a
shadow's bypass condition is ever weakened, narrowed, or removed, every
`self-limiting` site is one grep away instead of silently wrong.
Don't use this for `ls` — eza's long-format/icon layout is structural,
not tty-gated, so it stays different from stock `ls` even piped; a
bare `ls` call still needs `uses-shadow(ls)` or a real bypass.
- **`destructive`** — can irreversibly delete or overwrite data: `rm -f`,
`rm -rf`, truncating or force-overwriting a file, `git push --force`.
Routine cleanup of the function's own `$tmpdir`/`$_tmpdir`/`mktemp`
output (or other output it just created in this same call) is expected
behavior, not a hazard — don't tag it.
- **`network`** — makes an outbound network call: `curl`, `wget`, `ssh`,
`git fetch`/`pull`/`push`/`clone`, `paru`/`yay` (package-manager network
ops), talking to an API, etc.
- **`blocking-prompt`** — can block waiting on interactive confirmation
with no non-interactive escape hatch: a shadow's forced `-i`, fish's
`read` (genuinely waiting on a terminal — not a `string split | read`
or `while read` consuming a pipe, which never blocks), a `confirm`-style
prompt with no `--yes`/`--force`/`--silent` bypass. Don't tag a function
that's only ever meant to be run interactively at a prompt (a keybinding
handler, an fzf-driven picker) — the hazard this tag exists for is a
script or another function calling it unexpectedly, not a human running
it themselves.
## Placement
Directly under `# DEPENDENCIES` if the header has one; otherwise directly
under `# COMPONENT`; otherwise directly under `# CATEGORY`; otherwise as
the first label in the header block (this is the common case for internal
`_`-prefixed helpers, which usually carry none of the three).
## Judgment calls
`uses-shadow` vs `bypasses-shadow` is the easiest place to get subtly
wrong — verify against the actual code, not just whether the name appears
in the file. A function that only calls a *helper* which itself interacts
with a shadow does not get the tag; the tag belongs on the helper. When
generating these tags in bulk (e.g. delegating the sweep to another
model), review every result against the source before trusting it — this
schema's own rollout caught several false positives this way: a piped
`read` misread as an interactive prompt, a documented `--yes` flag missed
as an escape hatch, and cleanup of a function's own temp output flagged
as `destructive` despite the explicit exclusion above.
`rm` specifically has its own internal flag check (any flag other than
`-r`/`-R`/`--recursive` falls back to `command rm` *inside the shadow
itself*, before it ever touches trash) — a caller writing plain `rm -f`
or `rm -rf` is not bypassing anything itself, the shadow is. Only tag
`bypasses-shadow(rm)` when the caller explicitly writes `command rm` or
`builtin rm`; a bare `rm -f`/`rm -rf` call gets no shadow tag at all.
@@ -19,7 +19,6 @@ all of these commands.
rg rg --hyperlink-format=kitty system rg
mkdir verbose path-tree display on creation mkdir -p silently
bash XDG bashrc + $SHELL reset on exit system bash
history timestamps prepended to every entry fish builtin history
cp / mv forced -i confirmation prompt cp / mv unmodified
wget forced --continue (resume downloads) system wget
grep/fgrep/egrep forced --color=auto system grep variants
@@ -31,6 +30,11 @@ all of these commands.
When C1 is disabled, `rm` uses bare `command rm` with no wrapper — files
are permanently deleted, not trashed. There is no intermediate safety net.
`history` itself is never shadowed — every function in this config that
reads history depends on its stock builtin semantics. `pretty-history`
(same `aliases-tricks` toggle) is a separate command that prints history
with a timestamp prepended to every entry.
## Sub-categories
`__fish_config_op_aliases` sub-divides into six sub-categories, each with
@@ -63,3 +67,48 @@ and the `help config` interception.
`claude` (AGENTS.md/CLAUDE.md auto-linking) and `edit` (multi-editor
launcher), plus `agy`.
## For function authors
Calling one of these names bare from inside your own function means the
override runs whenever C1 (or its sub-category) is on — which may not be
what your function wants: a shadow can change stdout (`cat`'s syntax
highlighting, `mkdir`'s tree display), prompt interactively where none is
expected (`cp`/`mv`'s forced `-i`), or reshape output structurally (`ls`'s
icons/columns, `rg`'s hyperlink markers). If your function's logic depends
on stock behavior, bypass the shadow deterministically, regardless of the
toggle state:
Shadow Bypass Why
─────────────────────────────────────────────────────────────────────────
ls, cat, rm, less, du, command <name> Real external
top, ping, ssh, rg, binaries — a
mkdir, bash, cp, mv, real system command
wget, grep/fgrep/egrep, exists to fall
dir/vdir, claude back to.
cd builtin cd The one true
fish builtin
in this table.
help config __original_help $argv `help` is neither
a builtin nor an
external binary
(embedded in the
fish binary
itself) — see
conf.d/help.fish
for why the
wrapper keeps its
own backup copy.
edit (nothing to bypass to) Purely our own
invention, no
stock command
exists. Call
$EDITOR/$VISUAL
yourself if you
want a plain
editor launch.
A function's own doc header records which of these it depends on: see the
`CLASSIFICATION` label (`uses-shadow(...)` / `bypasses-shadow(...)`),
documented in full at
[`docs/function-classification-schema.md`](../../function-classification-schema.md).
+1
View File
@@ -64,6 +64,7 @@ SECTIONS = (
"CATEGORY",
"COMPONENT",
"DEPENDENCIES",
"CLASSIFICATION",
"SYNOPSIS",
"DESCRIPTION",
"ARGUMENTS",
@@ -4,6 +4,9 @@
# COMPONENT
# logging/terminal-capture
#
# CLASSIFICATION
# self-limiting(rm,mkdir)
#
# SYNOPSIS
# __fish_config_sync_logging
#
+3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# bypasses-shadow(cat)
#
# SYNOPSIS
# __fish_help_header <name> [args...]
#
+3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# self-limiting(grep)
#
# SYNOPSIS
# __fish_real_command <name>
#
+3
View File
@@ -4,6 +4,9 @@
# COMPONENT
# autoexec/sync
#
# CLASSIFICATION
# self-limiting(rm), destructive
#
# SYNOPSIS
# __fish_user_dots_link
#
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# bypasses-shadow(grep)
#
# SYNOPSIS
# __kitty_logging_has_watcher
#
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# self-limiting(grep)
#
# SYNOPSIS
# _agents_init_ensure_gitignore <root> <label> <pattern>...
#
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# self-limiting(rm,mkdir)
#
# SYNOPSIS
# _agents_repo_ensure_symlink <link> <target>
#
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# uses-shadow(mkdir), bypasses-shadow(cp,grep)
#
# SYNOPSIS
# _agents_repo_install_tools <repo_dir>
#
+6 -3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# self-limiting(rm,mkdir,cat), bypasses-shadow(cp,bash), destructive, network, blocking-prompt
#
# SYNOPSIS
# _fish_deps_install
#
@@ -281,7 +284,7 @@ function _fish_deps_install
end
test $_go_status -eq 0
case special-lazydocker
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | command bash
case special-marktext-paru
paru -S --noconfirm marktext-bin
case special-marktext-yay
@@ -309,7 +312,7 @@ function _fish_deps_install
-o "$_tmpdir/$_zip"
and unzip -o "$_tmpdir/$_zip" -d "$_tmpdir"
and mkdir -p "$_wt_dir" "$HOME/.local/bin"
and cp "$_tmpdir/$_bin_src" "$_wt_bin"
and command cp "$_tmpdir/$_bin_src" "$_wt_bin"
and chmod +x "$_wt_bin"
and ln -sf "$_wt_bin" "$HOME/.local/bin/wakatime"
rm -rf "$_tmpdir"
@@ -320,7 +323,7 @@ function _fish_deps_install
and curl -fL "https://github.com/equalsraf/win32yank/releases/latest/download/$_zip" \
-o "$_tmpdir/$_zip"
and unzip -o "$_tmpdir/$_zip" -d "$_tmpdir"
and cp "$_tmpdir/win32yank.exe" "$HOME/.local/bin/win32yank.exe"
and command cp "$_tmpdir/win32yank.exe" "$HOME/.local/bin/win32yank.exe"
and chmod +x "$HOME/.local/bin/win32yank.exe"
set -l _dl_status $status
rm -rf "$_tmpdir"
+4 -1
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# self-limiting(rm,mkdir), bypasses-shadow(mv), destructive, network
#
# SYNOPSIS
# _fish_deps_marktext_appimage
#
@@ -54,7 +57,7 @@ function _fish_deps_marktext_appimage
and chmod +x "$tmp/marktext"
# Replace via mv, not a write into $dest: overwriting a running AppImage
# in place corrupts the live mount.
and mv -f "$tmp/marktext" "$dest"
and command mv -f "$tmp/marktext" "$dest"
and set ok 1
rm -rf $tmp
+6 -3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# self-limiting(rm), bypasses-shadow(cp,bash), destructive, network
#
# SYNOPSIS
# _fish_deps_update
#
@@ -90,7 +93,7 @@ function _fish_deps_update
# lazydocker: re-run the official install/update script
if test "$special" = curl-lazydocker
echo "Updating $bin..."
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | bash
curl https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | command bash
set updated_any 1
set i (math $i + 1)
continue
@@ -138,7 +141,7 @@ function _fish_deps_update
curl -L "https://github.com/wakatime/wakatime-cli/releases/latest/download/$_zip" \
-o "$_tmpdir/$_zip"
and unzip -o "$_tmpdir/$_zip" -d "$_tmpdir"
and cp "$_tmpdir/$_bin_src" "$_wt_bin"
and command cp "$_tmpdir/$_bin_src" "$_wt_bin"
and chmod +x "$_wt_bin"
rm -rf "$_tmpdir"
and set updated_any 1
@@ -155,7 +158,7 @@ function _fish_deps_update
curl -fL "https://github.com/equalsraf/win32yank/releases/latest/download/$_zip" \
-o "$_tmpdir/$_zip"
and unzip -o "$_tmpdir/$_zip" -d "$_tmpdir"
and cp "$_tmpdir/win32yank.exe" "$HOME/.local/bin/win32yank.exe"
and command cp "$_tmpdir/win32yank.exe" "$HOME/.local/bin/win32yank.exe"
and chmod +x "$HOME/.local/bin/win32yank.exe"
set -l _up_status $status
rm -rf "$_tmpdir"
+3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# uses-shadow(mkdir)
#
# SYNOPSIS
# _fish_mkdir_p [--path|--tree|--silent] <dir>
#
+3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# network
#
# SYNOPSIS
# _mkrep_default_remote_cmd <type>
#
+3
View File
@@ -4,6 +4,9 @@
# DEPENDENCIES
# gh, glab, tea
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# _mkrep_repo_exists <type> <user> <name>
#
+3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# bypasses-shadow(ls,rm), destructive
#
# SYNOPSIS
# _prune_terminal_logs <prefix>
#
+6 -3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# bypasses-shadow(cat,rm), self-limiting(grep), destructive
#
# SYNOPSIS
# _scrollback_prune_junk [dir]
#
@@ -26,7 +29,7 @@ function _scrollback_prune_junk --description 'Remove empty, trivial, and Kitty
# Remove any completely empty log file regardless of source
for f in $dir/*.log $dir/*.txt
test -f $f || continue
not test -s $f; and rm $f
not test -s $f; and command rm -f $f
end
# Remove any log with only a single meaningful line (e.g. [exited], a lone prompt, or a trivial error)
@@ -34,7 +37,7 @@ function _scrollback_prune_junk --description 'Remove empty, trivial, and Kitty
test -f $f || continue
set -l line_count (command cat $f | sed 's/\x1b\[[0-9;:]*[a-zA-Z]//g' | grep -cv '^\s*$')
if test $line_count -le 1
rm $f
command rm -f $f
end
end
@@ -42,7 +45,7 @@ function _scrollback_prune_junk --description 'Remove empty, trivial, and Kitty
for f in $dir/scrollback_*.log $dir/scrollback_*.txt
test -f $f || continue
if command cat $f | sed 's/\x1b\[[0-9;:]*[a-zA-Z]//g' | grep -q 'Enter the new title for this tab below'
rm $f
command rm -f $f
end
end
end
+3
View File
@@ -1,6 +1,9 @@
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
# CLASSIFICATION
# uses-shadow(mkdir)
#
# SYNOPSIS
# _tmux_pipe_log
#
+3
View File
@@ -4,6 +4,9 @@
# COMPONENT
# logging/multiplexer-capture
#
# CLASSIFICATION
# uses-shadow(mkdir), bypasses-shadow(rm)
#
# SYNOPSIS
# _zellij_dump_log
#
+7 -4
View File
@@ -7,6 +7,9 @@
# DEPENDENCIES
# _agents_repo_install_tools, _agents_repo_sync, _agents_init_ensure_gitignore
#
# CLASSIFICATION
# self-limiting(rm,mkdir), bypasses-shadow(mv)
#
# SYNOPSIS
# agents-init [-a | --agents] [-p | --plugins] [-v | --verbose]
# [-q | --quiet] [-s | --silent] [-h | --help]
@@ -212,7 +215,7 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
if test $has_agents -eq 1; and test $has_claude -eq 1
# Both exist: preserve each as its own file in AGENTS/
if not test -f "$agents_dir/AGENTS.md"
if not mv "$root/AGENTS.md" "$agents_dir/AGENTS.md"
if not command mv "$root/AGENTS.md" "$agents_dir/AGENTS.md"
echo "$c_err""Error: could not move AGENTS.md → AGENTS/AGENTS.md$c_reset" >&2
return 1
end
@@ -220,7 +223,7 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
test $verbose -eq 1; and echo "$c_ok→ Moved AGENTS.md → AGENTS/AGENTS.md$c_reset"
end
if not test -f "$agents_dir/CLAUDE.md"; and not test -L "$agents_dir/CLAUDE.md"
if not mv "$root/CLAUDE.md" "$agents_dir/CLAUDE.md"
if not command mv "$root/CLAUDE.md" "$agents_dir/CLAUDE.md"
echo "$c_err""Error: could not move CLAUDE.md → AGENTS/CLAUDE.md$c_reset" >&2
return 1
end
@@ -229,7 +232,7 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
end
else if test $has_agents -eq 1
if not test -f "$agents_dir/AGENTS.md"
if not mv "$root/AGENTS.md" "$agents_dir/AGENTS.md"
if not command mv "$root/AGENTS.md" "$agents_dir/AGENTS.md"
echo "$c_err""Error: could not move AGENTS.md → AGENTS/AGENTS.md$c_reset" >&2
return 1
end
@@ -239,7 +242,7 @@ function agents-init --description 'scaffold AGENTS/ sub-repo with agent spec fi
else if test $has_claude -eq 1
# Only CLAUDE.md: treat it as the agent spec
if not test -f "$agents_dir/AGENTS.md"
if not mv "$root/CLAUDE.md" "$agents_dir/AGENTS.md"
if not command mv "$root/CLAUDE.md" "$agents_dir/AGENTS.md"
echo "$c_err""Error: could not move CLAUDE.md → AGENTS/AGENTS.md$c_reset" >&2
return 1
end
+3
View File
@@ -9,6 +9,9 @@
# _agents_repo_ensure_symlink, _agents_repo_sync,
# _agents_repo_install_tools, git, hostname
#
# CLASSIFICATION
# self-limiting(rm,mkdir)
#
# SYNOPSIS
# agents-vault [--link] [--push] [--restore] [--status]
# [--adopt=SLUG] [--remote=URL]
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 12-ai-and-developer-tools
#
# CLASSIFICATION
# self-limiting(grep)
#
# SYNOPSIS
# antigravity-ide [args...]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/shell-tools
#
# CLASSIFICATION
# bypasses-shadow(bash)
#
# SYNOPSIS
# bash [args...]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/filesystem
#
# CLASSIFICATION
# uses-shadow(ls), bypasses-shadow(cat)
#
# SYNOPSIS
# cat [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 14-miscellaneous
#
# CLASSIFICATION
# uses-shadow(ls)
#
# SYNOPSIS
# cffetch [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 12-ai-and-developer-tools
#
# CLASSIFICATION
# uses-shadow(claude)
#
# SYNOPSIS
# claude-docs
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 12-ai-and-developer-tools
#
# CLASSIFICATION
# uses-shadow(claude)
#
# SYNOPSIS
# claude-pr
#
+3
View File
@@ -10,6 +10,9 @@
# DEPENDENCIES
# agents-init, agents-vault
#
# CLASSIFICATION
# bypasses-shadow(claude)
#
# SYNOPSIS
# claude [ARGS...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 05-package-management
#
# CLASSIFICATION
# self-limiting(grep)
#
# SYNOPSIS
# cleanup
#
+4 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 14-miscellaneous
#
# CLASSIFICATION
# self-limiting(grep), bypasses-shadow(less)
#
# SYNOPSIS
# config-help [section]
# config-help --html
@@ -347,7 +350,7 @@ function config-help --description 'Open the offline fish shell configuration ma
else if type -q less
string replace -ra $span_raw $span_bold <"$doc_file" \
| less -R +"$start_line"
| command less -R +"$start_line"
else
string replace -ra $span_raw $span_bold <"$doc_file"
+3
View File
@@ -8,6 +8,9 @@
# __fish_palette, __config_settings_state, __config_settings_apply,
# __config_settings_set_value, python3
#
# CLASSIFICATION
# bypasses-shadow(rm)
#
# SYNOPSIS
# config-settings [-h | --help]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# bypasses-shadow(cp)
#
# SYNOPSIS
# copy <source> <dest>
#
+4 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 13-media-and-utilities
#
# CLASSIFICATION
# self-limiting(rm)
#
# SYNOPSIS
# dng2avif [-h] [-i <file>] [-o <file>] [-q <n>] [-s <n>] [input.dng]
#
@@ -119,7 +122,7 @@ function dng2avif --description 'Convert DNG raw to 10-bit HDR AVIF'
end
# Final Cleanup
test -f "$temp_pnm"; and rm "$temp_pnm"
test -f "$temp_pnm"; and rm -f "$temp_pnm"
set -l size (stat -c '%s' "$output" | numfmt --to=iec)
echo (set_color yellow)"Complete: $output ($size)"(set_color normal)
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 14-miscellaneous
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# dockup [-h] [directory]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/filesystem
#
# CLASSIFICATION
# bypasses-shadow(du)
#
# SYNOPSIS
# du [--disk|--dir|--dua] [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# uses-shadow(du)
#
# SYNOPSIS
# dusize [dir]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/dev-tools
#
# CLASSIFICATION
# bypasses-shadow(rm)
#
# SYNOPSIS
# edit [-V|-t] [-e EDITOR] [-c] [-x TEXT] [-n] [-v|-s] [FILE...]
#
+6 -3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 03-editors-and-viewers
#
# CLASSIFICATION
# bypasses-shadow(rm), self-limiting(cat)
#
# SYNOPSIS
# fc [command_prefix]
#
@@ -50,15 +53,15 @@ function fc --description 'Edit and execute the last command (Bash-style fc)'
# Final check if user cleared the file in the editor
if test -s $tmpfile
set -l command (cat $tmpfile)
rm $tmpfile
command rm -f $tmpfile
commandline -r "$command"
commandline -f execute
else
rm $tmpfile
command rm -f $tmpfile
echo "fc: Aborted (empty file)"
end
else
rm $tmpfile
command rm -f $tmpfile
echo "fc: Could not retrieve history"
end
end
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 14-miscellaneous
#
# CLASSIFICATION
# uses-shadow(ls)
#
# SYNOPSIS
# ffetch [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 06-dependency-management
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# fzf-update
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 04-git-and-version-control
#
# CLASSIFICATION
# self-limiting(grep,cat), network, blocking-prompt
#
# SYNOPSIS
# gi [-h] [-b] [-p] [-s] [-l] [targets...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 10-network
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# gip
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 10-network
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# gip4
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 10-network
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# gip6
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 04-git-and-version-control
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# git-clean [-h] [-f]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 04-git-and-version-control
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# gitup [args...]
#
+1 -1
View File
@@ -34,7 +34,7 @@ function hist --description 'Search fish history and put it in the prompt'
return 1
end
set -l selected (history | fzf --reverse --height 40% --with-nth 3..)
set -l selected (builtin history --show-time='%F %T ' | fzf --reverse --height 40% --with-nth 3..)
if test -n "$selected"
# Strip the timestamp for the final output
+3
View File
@@ -7,6 +7,9 @@
# DEPENDENCIES
# tmux, screen, __jobrunner_sessions
#
# CLASSIFICATION
# bypasses-shadow(cat,rm)
#
# SYNOPSIS
# jobrunner [-t <tool>] [<subcommand>] [<name>] [<command>...]
# jr [-t <tool>] [<subcommand>] [<name>] [<command>...]
+3
View File
@@ -7,6 +7,9 @@
# DEPENDENCIES
# gpg, tar
#
# CLASSIFICATION
# bypasses-shadow(rm), destructive
#
# SYNOPSIS
# key-crypt [options] <input> [output]
# key-crypt -i <input> -o <output> [options]
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# logging/terminal-capture
#
# CLASSIFICATION
# bypasses-shadow(grep,mkdir,rm)
#
# SYNOPSIS
# kitty-logging [install | uninstall | status | dismiss] [-h]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# bypasses-shadow(ls)
#
# SYNOPSIS
# lD [args...]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/shell-tools
#
# CLASSIFICATION
# bypasses-shadow(cat,less)
#
# SYNOPSIS
# less [args...]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# integrations/history-logs
#
# CLASSIFICATION
# bypasses-shadow(cat), self-limiting(rm), network
#
# SYNOPSIS
# logs [-h] [-c <category>]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/filesystem
#
# CLASSIFICATION
# bypasses-shadow(ls)
#
# SYNOPSIS
# ls [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# bypasses-shadow(ls)
#
# SYNOPSIS
# lsr [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# bypasses-shadow(ls)
#
# SYNOPSIS
# lss [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# bypasses-shadow(ls)
#
# SYNOPSIS
# lstree [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# bypasses-shadow(ls)
#
# SYNOPSIS
# lt [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# bypasses-shadow(ls)
#
# SYNOPSIS
# ltr [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# bypasses-shadow(ls)
#
# SYNOPSIS
# lx [args...]
#
+3
View File
@@ -7,6 +7,9 @@
# DEPENDENCIES
# marktext, firejail, bkg
#
# CLASSIFICATION
# uses-shadow(mkdir)
#
# SYNOPSIS
# md [-r] [--foreground] [marktext-args...] [FILE...]
#
+4 -1
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# bypasses-shadow(cd)
#
# SYNOPSIS
# mkcd [-s | --silent] <dir>
#
@@ -65,7 +68,7 @@ function mkcd --description 'Create a directory (with parents) and cd into it'
_fish_mkdir_p --tree $dir; or return $status
end
cd $dir
builtin cd $dir
or return $status
if test $is_new -eq 1
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/filesystem
#
# CLASSIFICATION
# bypasses-shadow(mkdir)
#
# SYNOPSIS
# mkdir [args...]
#
+12 -9
View File
@@ -9,6 +9,9 @@
# _mkrep_add_origin, _mkrep_default_remote_cmd, _mkrep_remote_url,
# _mkrep_repo_exists, git
#
# CLASSIFICATION
# bypasses-shadow(cd), self-limiting(rm), destructive, network
#
# SYNOPSIS
# mkrep [--cd | --no-cd] [--mkdir | --no-mkdir] [--git | --no-git]
# [-c | --clean | --no-clean] [--strict] [-v | --verbose]
@@ -304,7 +307,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it'
_mkrep_say $silent "$c_warn""→$c_reset $c_arg$dir$c_reset already exists"
end
cd $dir
builtin cd $dir
or begin
echo "$c_err""✘$c_reset Failed to enter $c_arg$dir$c_reset" >&2
return 1
@@ -326,7 +329,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it'
end
or begin
echo "$c_err""✘$c_reset git init failed in $c_arg$dir$c_reset" >&2
cd $orig_pwd
builtin cd $orig_pwd
return 1
end
@@ -341,7 +344,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it'
_mkrep_verbose $silent $verbose "$c_dim""Running: git remote add origin $_flag_remote$c_reset"
_mkrep_add_origin $silent $_flag_remote
or begin
cd $orig_pwd
builtin cd $orig_pwd
return 1
end
end
@@ -351,7 +354,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it'
test -z "$cmd"; and set cmd $MKREP_REMOTE_CMD
if test -z "$cmd"
echo "$c_err""✘$c_reset --new-remote given no command and \$MKREP_REMOTE_CMD is unset" >&2
cd $orig_pwd
builtin cd $orig_pwd
return 1
end
@@ -368,7 +371,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it'
end
or begin
echo "$c_err""✘$c_reset Remote-create command failed" >&2
cd $orig_pwd
builtin cd $orig_pwd
return 1
end
_mkrep_say $silent "$c_ok""✔$c_reset Ran remote-create command"
@@ -390,7 +393,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it'
_mkrep_say $silent "$c_warn""→$c_reset $c_arg$USER/$name$c_reset already exists on $srv_type; linking instead of creating"
_mkrep_add_origin $silent $url
or begin
cd $orig_pwd
builtin cd $orig_pwd
return 1
end
else
@@ -421,7 +424,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it'
if string match -q '*{server}*' -- $cmd
if test -z "$srv_url"
echo "$c_err""✘$c_reset No base URL resolved for $srv_type (set \$GITEA_URL/\$GITEA_HOST or \$GITLAB_URL/\$GITLAB_HOST)" >&2
cd $orig_pwd
builtin cd $orig_pwd
return 1
end
set cmd (string replace -a '{server}' $srv_url -- $cmd)
@@ -437,7 +440,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it'
end
or begin
echo "$c_err""✘$c_reset Remote-create command failed" >&2
cd $orig_pwd
builtin cd $orig_pwd
return 1
end
set -l url (_mkrep_remote_url $srv_type $USER $name $srv_url)
@@ -448,7 +451,7 @@ function mkrep --description 'Create a directory, cd into it, and git init it'
end
if test $do_cd -eq 0
cd $orig_pwd
builtin cd $orig_pwd
else
_mkrep_say $silent "$c_ok""✔$c_reset Entered $c_arg$dir$c_reset"
end
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/filesystem
#
# CLASSIFICATION
# bypasses-shadow(mv)
#
# SYNOPSIS
# mv [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 05-package-management
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# parur
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/network
#
# CLASSIFICATION
# bypasses-shadow(ping)
#
# SYNOPSIS
# ping [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 05-package-management
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# pkg [-h] [-i|-u] <package> [package...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 10-network
#
# CLASSIFICATION
# self-limiting(cat), network
#
# SYNOPSIS
# qr [text...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 14-miscellaneous
#
# CLASSIFICATION
# bypasses-shadow(cat)
#
# SYNOPSIS
# rand_string [COMPONENTS/MODIFIERS]...
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 14-miscellaneous
#
# CLASSIFICATION
# bypasses-shadow(bash,cd)
#
# SYNOPSIS
# replay <commands>
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/search
#
# CLASSIFICATION
# bypasses-shadow(rg)
#
# SYNOPSIS
# rg [args...]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/filesystem
#
# CLASSIFICATION
# bypasses-shadow(rm), destructive
#
# SYNOPSIS
# rm [-e [options] | -S | args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 07-system-and-monitoring
#
# CLASSIFICATION
# self-limiting(grep)
#
# SYNOPSIS
# sbver [--brief]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 01-file-and-directory
#
# CLASSIFICATION
# uses-shadow(rm), bypasses-shadow(rm), destructive
#
# SYNOPSIS
# scrub [-a] [-d] [-h]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 05-package-management
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# search [args...]
#
+3
View File
@@ -8,6 +8,9 @@
# site exit-plain: overrides/key-bindings
# site logging-guard: logging/terminal-capture
#
# CLASSIFICATION
# self-limiting(rm,mkdir), destructive
#
# SYNOPSIS
# smart_exit [-h] [-n]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/network
#
# CLASSIFICATION
# bypasses-shadow(ssh), network
#
# SYNOPSIS
# ssh [args...]
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 12-ai-and-developer-tools
#
# CLASSIFICATION
# uses-shadow(claude)
#
# SYNOPSIS
# superpowers [on|off] [-g]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/monitor
#
# CLASSIFICATION
# bypasses-shadow(top)
#
# SYNOPSIS
# top [args...]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# integrations/pkg-upgrade
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# upgrade
#
+3
View File
@@ -4,6 +4,9 @@
# CATEGORY
# 03-editors-and-viewers
#
# CLASSIFICATION
# uses-shadow(less)
#
# SYNOPSIS
# view [args...]
#
+3
View File
@@ -7,6 +7,9 @@
# COMPONENT
# aliases/network
#
# CLASSIFICATION
# network
#
# SYNOPSIS
# yt-dlp [args...] URL [URL...]
#
+83
View File
@@ -53,6 +53,89 @@ if test $syntax_failed -ne 0 -o $indent_failed -ne 0
set overall_failed 1
end
# ---- Phase 1b: shadow-classification lint --------------------------------
# Catches a bare C1-shadowed-command call in a function body with no
# matching uses-shadow(name) or self-limiting(name) in that function's own
# CLASSIFICATION header -- the exact bug class fixed across fc.fish,
# dng2avif.fish, _scrollback_prune_junk.fish, mkcd.fish, and mkrep.fish. A
# bare call is either declared (uses-shadow: wanted; self-limiting: safe
# because the shadow's own logic neutralizes it, e.g. rm/mkdir's flag check
# or grep/cat's tty-auto-detected color) or it's undocumented at best, a bug
# at worst -- the lint never guesses which on its own; see
# docs/function-classification-schema.md for the full tag definitions and
# why the reasoning belongs in a tag, not in this script.
#
# Scoped to functions/*.fish only: the one-function-per-file convention
# there makes "everything after the function line is its body" exact, with
# no block-depth parser needed. conf.d/*.fish can define several functions
# in one file and isn't covered -- see docs/function-classification-schema.md.
echo
echo "== Shadow-classification lint =="
# help and edit are deliberately excluded: help's real bypass is
# __original_help (not command/builtin), and edit has no backing binary at
# all to bypass to -- see docs/manual/08-components-reference/01-c1-command-shadows.md.
set -l shadow_names ls cat cd rm less du top ping ssh rg mkdir bash cp mv wget grep fgrep egrep dir vdir claude
set -l class_checked 0
set -l class_files_failed 0
set -l class_issues 0
for f in $repo_root/functions/*.fish
set -l lines (cat $f)
# Find the function line; everything before it is header, everything
# from it onward is body (one function per file).
set -l func_idx 0
for i in (seq (count $lines))
if string match -qr '^function ' -- $lines[$i]
set func_idx $i
break
end
end
test $func_idx -eq 0; and continue
set class_checked (math $class_checked + 1)
# Pull uses-shadow(...) and self-limiting(...) names from the
# CLASSIFICATION tag line, if any -- either one accounts for a bare call.
set -l declared
for i in (seq (math $func_idx - 1))
if test "$lines[$i]" = "# CLASSIFICATION"; and test $i -lt $func_idx
set -l tagline $lines[(math $i + 1)]
for tag in uses-shadow self-limiting
set -l m (string match -r "$tag"'\(([^)]*)\)' -- $tagline)
test -n "$m[2]"; and set -a declared (string trim -- (string split ',' -- $m[2]))
end
break
end
end
set -l file_failed 0
for i in (seq $func_idx (count $lines))
set -l line $lines[$i]
# Strip quoted spans and comments so string literals (error
# messages, --description text) never masquerade as a call.
set -l stripped (string replace -ra '"[^"]*"' '' -- $line)
set stripped (string replace -ra "'[^']*'" '' -- $stripped)
set stripped (string replace -r '#.*$' '' -- $stripped)
for name in $shadow_names
if string match -qr '(^|[;|(]|\band\b|\bor\b|\bnot\b|\bif\b|\bwhile\b|\bbegin\b)\s*'"$name"'(\s|$)' -- $stripped
if not contains -- $name $declared
echo " FAIL (shadow) "(string replace $repo_root/ '' $f)": line $i calls bare '$name' with no uses-shadow($name)/self-limiting($name)"
set class_issues (math $class_issues + 1)
set file_failed 1
end
end
end
end
test $file_failed -eq 1; and set class_files_failed (math $class_files_failed + 1)
end
echo (math $class_checked - $class_files_failed)"/$class_checked functions passed shadow-classification check"
if test $class_issues -ne 0
set overall_failed 1
end
# ---- Phase 2: discover suites --------------------------------------------
# Mode is declared by the suite, not by this driver. Detection is
# case-insensitive so a near-miss like "# Mode: in-session" is caught rather