ia-linux-bash-scripting
Defensive Bash scripting for Linux: safe foundations, argument parsing, production patterns, ShellCheck compliance. Use when writing bash scripts, shell scripts, cron jobs, or CLI tools in bash.
Install
npx skills add https://github.com/iliaal/whetstone/tree/master/plugins/whetstone/skills/ia-linux-bash-scripting
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install iliaal-whetstone@llmmart
git clone https://github.com/iliaal/whetstone.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole iliaal/whetstone collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Linux Bash Scripting
Produce bash scripts that pass shellcheck --enable=all and shfmt -d with zero warnings.
Target: GNU Bash 4.4+ on Linux. No macOS/BSD workarounds, no Windows paths, no POSIX-only restrictions.
Script Foundation
#!/usr/bin/env bash
set -Eeuo pipefail
shopt -s inherit_errexit
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
trap 'printf "Error at %s:%d\n" "${BASH_SOURCE[0]}" "$LINENO" >&2' ERR
trap 'rm -rf -- "${_tmpdir:-}"' EXIT
-Epropagates ERR traps into functionsinherit_errexitpropagates errexit into$()command substitutions- Resolve the script's own data files against
SCRIPT_DIR, never the caller's cwd orgit rev-parse --show-toplevel. A shared linter invoked from another project's git hook, a cron job, or a wrapper runs with someone else's cwd, so a caller-relative rules path resolves to a file that does not exist: the rule set loads empty, zero violations are found, exit 0. It is a silent no-op, not an error, and running it from inside its own repo passes for the wrong reason. Exercise it once from a scratch directory that is not the script's own tree - Always create temp dirs under the EXIT trap:
_tmpdir=$(mktemp -d) - Wrap body in
main() { ... }with source guard:[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"-- enables sourcing for testing
Core rules
- Quote expansions, use arrays for commands, and never evaluate external data as shell code.
- Validate numeric syntax, sign, and application bounds before arithmetic. Convert unsigned digits with
10#before applying the sign;10#-08is invalid. - Keep secrets out of process arguments and tracing. Feed them through stdin and use a JSON-aware encoder.
- Check exact exit statuses where “absent” differs from “failed to inspect.” Separate
localdeclarations from command substitutions. - Use NUL-delimited file iteration, validate required flag values, and reject conflicting output/target combinations.
- For atomic replacement, stage beside the destination; for multi-file activation, switch a single staged release reference.
- Preserve unrelated files and report the actual signal or command status after cleanup.
- Do not assume Bash options work in
sh, GNU utility modes behave like syscall modes, or a configured fallback path is usable.
Task-specific references
Read the relevant reference before implementing the matching behavior:
- For command execution, external input, numeric conversion, argument parsing, iteration, or subprocess status handling: input-and-process-safety.md.
- For file activation, secrets, locking, retries, cleanup, permissions, logging, or restartable automation: production-patterns.md.
Performance
- Parameter expansion over externals:
${path%/*}notdirname,${path##*/}notbasename,${var//old/new}notsed (( ))overexpr;[[ =~ ]]overecho | grep- Cache results:
val=$(cmd)once, reuse$val xargs -0 -P "$(nproc)"for parallel workdeclare -A mapfor lookups instead of repeated grep
Bash 4.4+ / 5.x
${var@Q}shell-quoted,${var@U}uppercase,${var@L}lowercasedeclare -n ref=varnamenameref for indirect accesswait -nwait for any background job$EPOCHSECONDS,$EPOCHREALTIME-- timestamps without forkingdate
Linux-Specific
- GNU coreutils differ from macOS:
sed -i(no''suffix),grep -P(PCRE support),readlink -f(canonical path) timeout 30s cmdto prevent automation hangs
ShellCheck
Run shellcheck --enable=all script.sh. Key rules:
- SC2155: Separate declaration from assignment
- SC2086: Double-quote variables
- SC2046: Quote command substitutions
- SC2164:
cd dir || exit - SC2327/SC2328: Use
${BASH_REMATCH[n]}not$nfor regex captures
Pre-commit: shellcheck *.sh && shfmt -i 2 -ci -d *.sh
Verify
Run shellcheck --enable=all and shfmt -d with zero warnings before declaring done. Test edge cases: empty input, missing files, spaces in paths.
If shellcheck or shfmt is not installed (command -v shellcheck fails), the check was skipped, not passed: report "static analysis not run: shellcheck unavailable" and fall back to bash -n for syntax only. A skipped linter is the same silent no-op as the empty rules file above.
Files (whetstone)
-
references
-
input-and-process-safety.md 6.3 KB
# Input and process safety ## Core Rules - Quote every expansion: `"$var"`, `"$(cmd)"`, `"${array[@]}"` - `local` for function variables, `local -r` for function constants, `readonly` for script constants - `printf '%s\n'` over `echo` -- predictable behavior, no flag interpretation - `[[ ]]` for conditionals; `(( ))` for arithmetic; `$()` over backticks - End options with `--`: `rm -rf -- "$path"`, `grep -- "$pattern" "$file"` - Require env vars: `: "${VAR:?must be set}"` - Never `eval` user input; build commands as arrays: `cmd=("grep" "--" "$pat" "$f"); "${cmd[@]}"` - Keep untrusted/derived bytes off the command line: never build a heredoc body or an `sh -c` string from external data. An unquoted `<<EOF` command-substitutes `$(...)`/backticks in the content, and even a quoted `<<'EOF'` breaks if a content line equals the delimiter (the heredoc ends early and the rest runs as shell). Write the data to a file with a non-shell writer and have the consumer read the file - Allowlisting a command? Match the whole command against an anchored pattern (`^…$`), never inspect individual arguments — shell operators (`;`, `&&`, `|`, `#`, newline) smuggle a second command past a per-argument check (`rm -rf node_modules; rm -rf /`). Unrecognized syntax must fail closed to deny/ask - Validating a path component before it reaches a destructive command? Anchor it against an allowlist (`[[ "$name" =~ ^[a-z0-9][a-z0-9._-]*$ ]]`) before `rm -rf -- "$base/$name"` -- a prefix/`startswith` check on the joined path is defeated by `../` (`$base/../x` still starts with `$base`) and by a sibling directory sharing the prefix (`/srv/app` matches `/srv/app2`). When a full path must be accepted, `realpath -e` it and compare against the resolved base plus a trailing slash - Validate external numeric text before arithmetic: array subscripts can execute commands, and leading zeros select octal. Require `[[ "$v" =~ ^-?[0-9]+$ ]]`, separate an optional minus from the digits, reject magnitudes outside the application's range before arithmetic conversion, convert the unsigned digits with `10#`, then apply the sign. `10#-08` is invalid. See Bounded signed decimal conversion below. - Separate `local` from assignment to preserve exit codes: `local val; val=$(cmd)` - Debug tracing: `PS4='+${BASH_SOURCE[0]}:${LINENO}: '` with `bash -x` -- shows file:line per command - Named exit codes: `readonly EX_USAGE=64 EX_CONFIG=78` -- no magic numbers in `exit` - Pipeline diagnostics: `"${PIPESTATUS[@]}"` shows exit code of each pipe stage, not just last failure - Branch on a probe's exact exit status, not on nonzero-versus-zero. A tool that exits 2 for "ran, found nothing" and 128 for "could not run" collapses into a single negative under `if ! cmd`, and stderr is often empty for both. Treating every silent nonzero as "absent" converts a network, permission, or spawn failure into a confident false diagnosis - `A || B` is a fallback only when `A` **fails** on the case `B` exists for. When `A` succeeds while doing the wrong thing -- resolving a different tool, default, or directory -- `B` is dead code and the wrong behavior is silent. Same trap in `${VAR:-default}` on a path two processes must agree on: whoever lacks `VAR` gets a different location, the two silently stop sharing state, and neither errors. Pick one resolution and fail loudly when it is unavailable. A fallback chain must also test *usability*, not presence: `${XDG_RUNTIME_DIR:-/tmp}` falls through only when the variable is unset, so a variable pointing at an unwritable directory takes `mkdir` to `EACCES` and aborts on the step the chain made optional. Treat a permission or existence failure on a configured location the same as an unconfigured one, and log which candidate was chosen - A `;` list exits with its *last* command's status, so appending a status echo guarantees success: `./run.sh > log 2>&1; echo "EXIT=$?"` exits 0 no matter what `run.sh` did. Capture and re-raise: `rc=$?; printf 'EXIT=%d\n' "$rc"; exit "$rc"` - A wait loop that greps for the process it waits on matches itself: `pgrep -f` tests the full argv and the pattern sits in the waiter's own command line, so `until ! pgrep -f build_step; do sleep 10; done` never exits. A pipeline feeding `ps` straight into a matcher includes the matcher's own process the same way, and an empty substitution collapses `/proc/$(pgrep -f cmd | head -1)` to `/proc/`, which always exists. Match the exact process name (`pgrep -x`), drop your own PID (`pgrep -f "$pat" | grep -vx "$$"`), take the snapshot in one command and filter the saved output in the next, and prefer waiting on the process directly (`wait`, `flock`) over polling for it ## Safe Iteration ```bash # NUL-delimited file processing while IFS= read -r -d '' f; do process "$f" done < <(find /path -type f -name '*.log' -print0) # Array from command output readarray -t lines < <(command) readarray -d '' files < <(find . -print0) # Glob with no-match guard for f in *.txt; do [[ -e "$f" ]] || continue; process "$f"; done ``` ## Argument Parsing ```bash verbose=false; output="" while [[ $# -gt 0 ]]; do case "$1" in -v|--verbose) verbose=true; shift ;; -o|--output) output="$2"; shift 2 ;; -h|--help) usage; exit 0 ;; --) shift; break ;; -*) printf 'Unknown: %s\n' "$1" >&2; exit 1 ;; *) break ;; esac done ``` A single-destination override flag (`--out FILE`) combined with more than one positional target clobbers silently -- last write wins, no error, no diagnostic. Detect the combination (`(( ${#targets[@]} > 1 )) && [[ -n "$output" ]]`) and exit `EX_USAGE` instead of letting the last target overwrite every prior one. ## Bounded signed decimal conversion Accept integers from -999999999 to 999999999 in this example, including zero padding. Choose and check the application's actual range before arithmetic; Bash integers do not detect overflow. ```bash parse_decimal() { local raw=${1-} digits sign=1 [[ "$raw" =~ ^-?[0-9]+$ ]] || return 64 digits=${raw#-} [[ "$raw" != -* ]] || sign=-1 while [[ ${#digits} -gt 1 && "$digits" == 0* ]]; do digits=${digits#0} done [[ ${#digits} -le 9 ]] || return 64 printf '%d\n' "$((sign * 10#$digits))" } ``` Check `08 → 8`, `-08 → -8`, `-0 → 0`, and reject empty text, expression syntax, and magnitudes beyond the bound. -
production-patterns.md 6.6 KB
# Production patterns ## Production Patterns **Dependency check:** ```bash require() { command -v "$1" &>/dev/null || { printf 'Missing: %s\n' "$1" >&2; exit 1; }; } require jq; require curl ``` **Dry-run wrapper:** ```bash run() { if [[ "${DRY_RUN:-}" == "1" ]]; then printf '[dry] %s\n' "$*" >&2; else "$@"; fi; } run cp "$src" "$dst" ``` **Atomic file write** -- run the producer into a temp file, then rename only after it succeeds: ```bash atomic_write() { local target=$1 tmp shift tmp=$(mktemp -- "${target}.tmp.XXXXXXXX") || return if "$@" >"$tmp" && mv -fT -- "$tmp" "$target"; then return 0 else local rc=$? rm -f -- "$tmp" return "$rc" fi } atomic_write /etc/app/config.yml generate_config ``` The producer must return nonzero on generation failure; explicitly propagate failures inside shell functions because Bash suppresses `errexit` in this conditional context. Pipeline producers must enable `pipefail`. Do not pipe into this helper: an upstream failure cannot prevent a rename that already happened. **Atomic multi-file activation** -- N individually atomic copies are not an atomic interface: a failure after replacing the second of three leaves the old entry point running against a mixed set. Stage the release into a fresh uniquely-named directory, then swap one relative `current` symlink (`ln -sfn` onto a temp name, then `mv -T` it into place). A component that cannot join the swap -- a separately installed helper that an already-running caller invokes -- is installed *first*, so an interrupted run lands on old-caller/new-helper, and the helper's interface stays backward compatible. The failure fixture seeds a complete prior release, fails after one new component is staged, and asserts every prior component is still active. **Retry with backoff:** ```bash retry() { local n=0 max=5 delay=1; until "$@"; do ((++n>=max)) && return 1; sleep $delay; ((delay*=2)); done; } retry curl -fsSL "$url" ``` **Script locking** -- prevent concurrent runs: ```bash exec 9>/var/lock/"${0##*/}".lock flock -n 9 || { printf 'Already running\n' >&2; exit 1; } ``` **Idempotent operations** -- safe to rerun: ```bash ensure_dir() { [[ -d "$1" ]] || mkdir -p -- "$1"; } ensure_link() { [[ -L "$2" ]] || ln -s -- "$1" "$2"; } ``` A linear script with irreversible steps (commit, push, tag, publish) must be re-runnable from any failure point, not just idempotent per primitive: make each step check-and-skip (`release_exists "$tag" || create_release "$tag"`) so a failure at step 4 is repaired by one re-invocation instead of a hand-reconstruction of steps 4-6. **Input validation:** `[[ "$1" =~ ^[1-9][0-9]*$ ]] || die "Invalid: $1"` -- validate at script boundaries with `[[ =~ ]]`. The leading `[1-9]` also excludes zero-padded input, which arithmetic would read as octal; widening this to `^[0-9]+$` to admit `0` reintroduces that trap unless the value goes through `10#` - `umask 077` for scripts creating sensitive files - Distinguish syscall modes from utility options: umask masks a `mkdir(2)` mode, but GNU `mkdir -m 755` explicitly sets the resulting directory to `0755` even under `umask 077`. For a mode-preservation test, set the fixture's mode explicitly and assert the observed result; do not loosen the service's umask. - Staging a file across users through a world-writable directory fails on the rename, not the read: `/tmp`'s sticky bit lets only the file's owner rename or unlink it, so a second user's `mv /tmp/f "$dest"` fails with `Operation not permitted` while `cp` succeeds. Copy as the destination user (`sudo -u <dest> cp -- /tmp/f "$target"`), then remove the staging copy as its creator - Moving a secret out of argv into a temp file closes the `ps` / `/proc/<pid>/cmdline` exposure and nothing else. Bash stores a multi-line command as **one** history entry, heredoc body included, and the single-line form `printf %s '<value>' >"$tmp"` puts the value on the command line too. Take it from stdin and let a JSON-aware writer escape it: ```bash umask 077; tmp=$(mktemp); trap 'rm -f -- "$tmp"' EXIT read -rs SECRET # stdin: never a command line, never in history printf '%s' "$SECRET" | jq -Rs '{Password:.}' >"$tmp" ``` Bash's builtin `printf` sends the value through stdin, avoiding `jq --arg`'s process-argument exposure; `jq -Rs` escapes JSON characters. Use `mktemp` rather than a predictable path that an attacker can replace with a symlink. - Generate secret/token files with no trailing newline. `cmd >"$f"` keeps the `\n`, `$(cat "$f")` strips it, and CLI arguments of the `file://$f` shape transmit it verbatim -- so one generated value installed into two consumers differs by one byte while both sides *display* the same characters and every constant-time comparison on the far side just returns false. Fix at the generator (`printf %s "$(cmd)" >"$f"`), never per reader, and verify with `wc -c < "$f"` - Signal cleanup: use `trap 'cleanup; exit 130' INT` and `trap 'cleanup; exit 143' TERM` to report the conventional signal-specific exit status. ## Logging ```bash log() { printf '[%s] [%s] %s\n' "$(date -Iseconds)" "$1" "${*:2}" >&2; } info() { log INFO "$@"; } warn() { log WARN "$@"; } error() { log ERROR "$@"; } die() { error "$@"; exit 1; } ``` ## Anti-Patterns | Bad | Fix | |-----|-----| | `for f in $(ls)` | `for f in *; do` or `find -print0 \| while read` | | `local x=$(cmd)` | `local x; x=$(cmd)` -- preserves exit code | | `x=$(cmd)` then an `[[ -z $x ]]` fallback check | `x=$(cmd) \|\| true` -- under `set -e` a failed `$()` in a bare assignment aborts the script there, so the fallback never runs (opposite of the `local` case: `local` masks the failure, a bare assignment propagates it) | | `x=$(cmd 2>/dev/null \|\| echo MISSING)` | Capture and test separately -- a tool that prints to stdout *and* exits nonzero (some echo their unresolved argument before failing) contributes both strings, so `x` becomes `<junk>` + `MISSING` and every comparison built on it reports a spurious difference. The `2>/dev/null` that quiets the loop is also what hides the error line | | `echo "$data"` | `printf '%s\n' "$data"` | | `cat file \| grep` | `grep pat file` | | `kill -9 $pid` first | `kill "$pid"` first, `-9` as last resort | | `cd dir; cmd` | `cd dir || exit 1` or subshell `(cd dir && cmd)` | | A multi-command shell block embedded in YAML or a `RUN` line | Select Bash explicitly before using `set -Eeuo pipefail`; these options are not portable to `sh`. Match the production interpreter and flags in tests. Capture failures before a trailing successful command can hide them, and keep intentional fallbacks explicit. |
-
-
SKILL.md 4.6 KB
--- name: ia-linux-bash-scripting class: language description: >- Defensive Bash scripting for Linux: safe foundations, argument parsing, production patterns, ShellCheck compliance. Use when writing bash scripts, shell scripts, cron jobs, or CLI tools in bash. paths: "**/*.sh,**/*.bash" --- # Linux Bash Scripting Produce bash scripts that pass `shellcheck --enable=all` and `shfmt -d` with zero warnings. Target: GNU Bash 4.4+ on Linux. No macOS/BSD workarounds, no Windows paths, no POSIX-only restrictions. ## Script Foundation ```bash #!/usr/bin/env bash set -Eeuo pipefail shopt -s inherit_errexit readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" trap 'printf "Error at %s:%d\n" "${BASH_SOURCE[0]}" "$LINENO" >&2' ERR trap 'rm -rf -- "${_tmpdir:-}"' EXIT ``` - `-E` propagates ERR traps into functions - `inherit_errexit` propagates errexit into `$()` command substitutions - Resolve the script's own data files against `SCRIPT_DIR`, never the caller's cwd or `git rev-parse --show-toplevel`. A shared linter invoked from another project's git hook, a cron job, or a wrapper runs with someone else's cwd, so a caller-relative rules path resolves to a file that does not exist: the rule set loads empty, zero violations are found, exit 0. It is a silent no-op, not an error, and running it from inside its own repo passes for the wrong reason. Exercise it once from a scratch directory that is not the script's own tree - Always create temp dirs under the EXIT trap: `_tmpdir=$(mktemp -d)` - Wrap body in `main() { ... }` with source guard: `[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"` -- enables sourcing for testing ## Core rules - Quote expansions, use arrays for commands, and never evaluate external data as shell code. - Validate numeric syntax, sign, and application bounds before arithmetic. Convert unsigned digits with `10#` before applying the sign; `10#-08` is invalid. - Keep secrets out of process arguments and tracing. Feed them through stdin and use a JSON-aware encoder. - Check exact exit statuses where “absent” differs from “failed to inspect.” Separate `local` declarations from command substitutions. - Use NUL-delimited file iteration, validate required flag values, and reject conflicting output/target combinations. - For atomic replacement, stage beside the destination; for multi-file activation, switch a single staged release reference. - Preserve unrelated files and report the actual signal or command status after cleanup. - Do not assume Bash options work in `sh`, GNU utility modes behave like syscall modes, or a configured fallback path is usable. ## Task-specific references Read the relevant reference before implementing the matching behavior: - For command execution, external input, numeric conversion, argument parsing, iteration, or subprocess status handling: [input-and-process-safety.md](./references/input-and-process-safety.md). - For file activation, secrets, locking, retries, cleanup, permissions, logging, or restartable automation: [production-patterns.md](./references/production-patterns.md). ## Performance - Parameter expansion over externals: `${path%/*}` not `dirname`, `${path##*/}` not `basename`, `${var//old/new}` not `sed` - `(( ))` over `expr`; `[[ =~ ]]` over `echo | grep` - Cache results: `val=$(cmd)` once, reuse `$val` - `xargs -0 -P "$(nproc)"` for parallel work - `declare -A map` for lookups instead of repeated grep ## Bash 4.4+ / 5.x - `${var@Q}` shell-quoted, `${var@U}` uppercase, `${var@L}` lowercase - `declare -n ref=varname` nameref for indirect access - `wait -n` wait for any background job - `$EPOCHSECONDS`, `$EPOCHREALTIME` -- timestamps without forking `date` ## Linux-Specific - GNU coreutils differ from macOS: `sed -i` (no `''` suffix), `grep -P` (PCRE support), `readlink -f` (canonical path) - `timeout 30s cmd` to prevent automation hangs ## ShellCheck Run `shellcheck --enable=all script.sh`. Key rules: - **SC2155**: Separate declaration from assignment - **SC2086**: Double-quote variables - **SC2046**: Quote command substitutions - **SC2164**: `cd dir || exit` - **SC2327/SC2328**: Use `${BASH_REMATCH[n]}` not `$n` for regex captures Pre-commit: `shellcheck *.sh && shfmt -i 2 -ci -d *.sh` ## Verify Run `shellcheck --enable=all` and `shfmt -d` with zero warnings before declaring done. Test edge cases: empty input, missing files, spaces in paths. If `shellcheck` or `shfmt` is not installed (`command -v shellcheck` fails), the check was skipped, not passed: report "static analysis not run: shellcheck unavailable" and fall back to `bash -n` for syntax only. A skipped linter is the same silent no-op as the empty rules file above. -
SPEC.md 4.5 KB
# ia-linux-bash-scripting Specification ## Intent `ia-linux-bash-scripting` is a `language`-class skill (stack-specific patterns and idioms). Defensive Bash scripting for Linux: safe foundations, argument parsing, production patterns, ShellCheck compliance. Use when writing bash scripts, shell scripts, cron jobs, or CLI tools in bash. ## Scope In scope: - Behaviors described in `SKILL.md` and routed via the should_trigger phrasings in `distillery/tests/fixtures/triggers/ia-linux-bash-scripting.jsonl`. - Updates to runtime behavior, structure, trigger precision, references, and validation. Out of scope: - Acting as the runtime instructions themselves (those live in `SKILL.md`). - Trigger phrasings already covered by adjacent `ia-*` skills (`validate-plugin` flags >70% description overlap as DUPLICATE_TRIGGER). - <!-- to fill in: domain-specific exclusions when the skill drifts --> ## Trigger Context - Class: `language` - Hook regex: `plugins/whetstone/hooks/skill-patterns.sh` -> `SKILL_PATTERNS[ia-linux-bash-scripting]` - Common requests (from fixture should_trigger): - "write a bash script to automate the database backup" - "create a deployment script for the production servers" - "write a bash script to rotate the nightly backups" - Should not trigger for (from fixture should_not_trigger): - "build a React form with validation" - "add a new Laravel middleware for API throttling" - "write a Python CLI for log parsing" ## Source And Evidence Model Authoritative sources: - `SKILL.md` -- runtime instructions and reference routing. - `references/*.md` -- bundled supplementary content (0 file(s)). - `distillery/tests/fixtures/triggers/ia-linux-bash-scripting.jsonl` -- positive and negative trigger phrasings under regression test. - `plugins/whetstone/hooks/skill-patterns.sh` -- regex pattern that fires this skill. - `distillery/.eval-data/ia-linux-bash-scripting/` -- harvested session examples (when present). Data that must not be stored in this skill or its references: - Secrets, credentials, tokens. - Machine-specific filesystem paths (`/home/...`, `/Users/...`, `~/ai/...`). The validator (`MACHINE_PATH_LEAK`) flags these as HIGH. - Private URLs, customer data, or unredacted personal information. ### Coverage matrix | Dimension | Status | Evidence | |---|---|---| | Trigger fixtures | complete | distillery/tests/fixtures/triggers/ia-linux-bash-scripting.jsonl (>=5 should_trigger, >=5 should_not_trigger) | | Hook regex pattern | complete | plugins/whetstone/hooks/skill-patterns.sh (`SKILL_PATTERNS[ia-linux-bash-scripting]`) | | Reference architecture | n/a | no references; SKILL.md is self-contained | | Real-usage signal | <!-- populated by harvest-sessions when sessions exist --> | distillery/.eval-data/ia-linux-bash-scripting/ (created by harvest-sessions) | ## Evaluation Lightweight (run on every change): ```bash python3 distillery/scripts/distiller.py validate-plugin --component ia-linux-bash-scripting python3 distillery/scripts/distiller.py test-triggers --skill ia-linux-bash-scripting ``` Deeper (when behavior risk warrants): ```bash python3 distillery/scripts/distiller.py dspy-eval ia-linux-bash-scripting python3 distillery/scripts/distiller.py diagnose-negatives ia-linux-bash-scripting ``` Acceptance gates: - `validate-plugin --component ia-linux-bash-scripting` returns 0 HIGH findings. - `test-triggers --skill ia-linux-bash-scripting` returns F1 = 1.0 with floors of 5 should_trigger and 5 should_not_trigger. - For dspy-eval, the composite score does not regress against the most recent saved baseline (see `distillery/.eval-data/ia-linux-bash-scripting/history.json`). ## Known Limitations <!-- to fill in over time as drift surfaces. Default rule: any time diagnose-negatives surfaces a recurring failure pattern, document it here so future maintainers understand the trade-off the current implementation accepts. --> ## Maintenance Notes - Update `SKILL.md` when the runtime workflow, branch conditions, or output contract changes. - Update this `SPEC.md` when intent, scope, evidence model, evaluation gates, or maintenance expectations change. - Update the trigger fixture when adding new positive phrasings, removing stale ones, or expanding scope (the 5/5 floor is a hard validator gate). - Update the hook regex in `skill-patterns.sh` whenever fixture positives expose a missed phrasing; verify F1 = 1.0 with `eval-triggers` before committing. - Run the full release pipeline via `/release` -- never bump versions or update CHANGELOG.md from a per-skill edit.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.