From cbcb1e1a88b6a85c27dbc547aa00d11997fd0c1c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Sep 2026 13:21:34 +0000 Subject: [PATCH 1/3] fix(rate-limit-guard): align unwrap-before-compose with context-guard Stop treating bare quoting as an sh -c wrap trigger. Share the peel and guard rules through a synced spoke so the twins cannot drift again. Co-authored-by: ksextonmelodic --- .github/workflows/ci.yml | 19 +++ .../context-guard/.claude-plugin/plugin.json | 2 +- plugins/context-guard/CHANGELOG.md | 9 ++ plugins/context-guard/skills/setup/SKILL.md | 9 +- .../skills/setup/reference/statusline-edit.md | 112 ++------------ .../setup/reference/unwrap-before-compose.md | 109 +++++++++++++ .../.claude-plugin/plugin.json | 2 +- plugins/rate-limit-guard/CHANGELOG.md | 9 ++ .../rate-limit-guard/skills/setup/SKILL.md | 27 ++-- .../skills/setup/evals/evals.json | 52 +++++++ .../setup/reference/unwrap-before-compose.md | 109 +++++++++++++ scripts/cross-plugin-source-registry.txt | 3 + scripts/sync-unwrap-before-compose.sh | 40 +++++ scripts/sync-unwrap-before-compose.test.sh | 143 ++++++++++++++++++ 14 files changed, 522 insertions(+), 123 deletions(-) create mode 100644 plugins/context-guard/skills/setup/reference/unwrap-before-compose.md create mode 100644 plugins/rate-limit-guard/skills/setup/reference/unwrap-before-compose.md create mode 100755 scripts/sync-unwrap-before-compose.sh create mode 100755 scripts/sync-unwrap-before-compose.test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a174ed4f..dc88ccc5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -667,6 +667,24 @@ jobs: BASE_REF: ${{ github.base_ref }} run: scripts/sync-legacy-statusline-detect.sh --check-bump "origin/$BASE_REF" + unwrap-before-compose-sync: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - name: Check out + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Fetch base + uses: ./.github/actions/checkout-with-base + - name: Verify unwrap-before-compose cluster matches canonical + run: scripts/sync-unwrap-before-compose.sh --check + - name: Verify carrying plugins bumped when canonical changed + if: github.event_name == 'pull_request' + env: + BASE_REF: ${{ github.base_ref }} + run: scripts/sync-unwrap-before-compose.sh --check-bump "origin/$BASE_REF" + resolve-convention-home-sync: runs-on: ubuntu-24.04 timeout-minutes: 15 @@ -1819,6 +1837,7 @@ jobs: - spawn-noise-sync - check-retirements-sync - legacy-statusline-detect-sync + - unwrap-before-compose-sync - resolve-convention-home-sync - resolve-convention-pattern-sync - index-regen-sync diff --git a/plugins/context-guard/.claude-plugin/plugin.json b/plugins/context-guard/.claude-plugin/plugin.json index 46692570b..fdb3fc4a0 100644 --- a/plugins/context-guard/.claude-plugin/plugin.json +++ b/plugins/context-guard/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "context-guard", - "version": "0.7.30", + "version": "0.7.31", "description": "Per-session context-window observability plus the first shipped consumer: a statusline wrapper tees each session's context_window fields to a per-session snapshot file, a zone resolver classifies usage into smart/acceptable/dumb bands (percentage bands plus window-class token bands, conservative-min combination, zones.json SSOT with shipped defaults), a reader contract fixes how consuming sessions interpret the snapshots, and zone-crossing hooks report once per transition into a worse zone across two channels \u2014 the continuation menu to the operator, who owns that choice, and to the model only the zone determination plus the counter-steer that a zone word is not a decay signal (advisory by default; an optional blocking mode gates new mutating work on a fresh dumb-zone snapshot with handoff-writing exempt), with a PostCompact hook persisting an evidence-degraded marker.", "author": { "name": "Melodic Software", diff --git a/plugins/context-guard/CHANGELOG.md b/plugins/context-guard/CHANGELOG.md index 77bf98840..bf0771431 100644 --- a/plugins/context-guard/CHANGELOG.md +++ b/plugins/context-guard/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to the `context-guard` plugin. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.7.31] + +### Changed + +- **setup: extract unwrap-before-compose to a synced spoke.** Peel rules and the + shell-syntax guard move to `reference/unwrap-before-compose.md`, canonical here + and synced into rate-limit-guard so the twins cannot drift on quoting-as-trigger + again. + ## [0.7.30] ### Changed diff --git a/plugins/context-guard/skills/setup/SKILL.md b/plugins/context-guard/skills/setup/SKILL.md index 580643481..f131a63b1 100644 --- a/plugins/context-guard/skills/setup/SKILL.md +++ b/plugins/context-guard/skills/setup/SKILL.md @@ -162,10 +162,11 @@ zone bands, zones.json shape) are owned by settings. Those branches already forbade printing wiring the operator cannot make run. When this step does print, the wiring target is the SHIM's fixed path, never `${CLAUDE_PLUGIN_ROOT}`. Read - [`reference/statusline-edit.md`](reference/statusline-edit.md) now, before composing: it owns - the peel rules, the shell-syntax guard, both JSON edit blocks, and the Windows note, and - composing without it is what produced `context -> rate -> rate -> renderer` and the - compounding `sh -c` wrap. + [`reference/unwrap-before-compose.md`](reference/unwrap-before-compose.md) for the peel + rules and the shell-syntax guard (shared with rate-limit-guard), then + [`reference/statusline-edit.md`](reference/statusline-edit.md) for this plugin's JSON + edit blocks and the Windows note. Composing without those is what produced + `context -> rate -> rate -> renderer` and the compounding `sh -c` wrap. 8. **Dotfiles tracking proposal**, the printed edit changes a durable user-scope file the operator maintains. When the operator's home directory is managed by a dotfiles system (chezmoi, yadm, a bare-repo setup, ...), surface the reminder to capture the `settings.json` change through that diff --git a/plugins/context-guard/skills/setup/reference/statusline-edit.md b/plugins/context-guard/skills/setup/reference/statusline-edit.md index 66194114c..be0a1b831 100644 --- a/plugins/context-guard/skills/setup/reference/statusline-edit.md +++ b/plugins/context-guard/skills/setup/reference/statusline-edit.md @@ -10,59 +10,17 @@ ineffective remediation those branches exist to withhold. ## Contents -- [Unwrap before you compose](#unwrap-before-you-compose) +- [Unwrap and wrap rules](#unwrap-and-wrap-rules) - [The edit blocks](#the-edit-blocks) -- [The shell-syntax guard](#the-shell-syntax-guard) - [Sibling shims compose by nesting](#sibling-shims-compose-by-nesting) - [Windows note](#windows-note) -## Unwrap before you compose - -`` below means the operator's OWN -renderer, never the raw effective `command` string. Recover it by peeling off the wrapping this -skill itself prints, applying BOTH rules repeatedly until a pass strips nothing: - -1. **Guard-shim prefixes**. Every leading `bash /context-guard/bin/statusline-shim.sh` and - `bash /rate-limit-guard/bin/statusline-shim.sh`, in whatever order they appear, plus any - legacy `bash /…/statusline-tee.sh` prefix. -2. **A generated `sh -c` adapter**, when what remains is EXACTLY `sh -c ''` - with nothing after the closing quote, AND, once that string is unescaped, ANY of the following - holds, that is an adapter a previous run printed, not the renderer. Unescape it back: drop the - leading `sh -c` and the outer quotes, then replace every `'\''` with `'`. - - - **A. It is itself EXACTLY `sh -c ''`, nothing after the closing - quote.** A nested `sh -c` is always a layer some run added: an operator's own renderer is at - most one `sh -c` deep. Apply the same strictness here as to the outer shape, so two readers - peel the same number of layers. - - **B. It begins with a guard-shim prefix from rule 1.** This skill never puts a shim inside - an adapter, and an operator would not write one inside their own `sh -c`. Leaving it sealed - there hides it from rule 1, which strips only LEADING prefixes, and the composed wiring then - names that shim a second time. - - **C. It is a command the guard below would send for wrapping.** That is the only shape this - skill's own adapter ever carries. - - Branches A and B must NOT inherit the guard's top-level scoping. Their evidence is the shape - of the carried string, not the syntax in it. Absent all three, the `sh -c` was written by the - operator and must be preserved: peeling `sh -c 'ulimit -n'` to `ulimit -n` would leave the - shim `exec`-ing a shell builtin that no longer has a shell, and the statusline would exit 127 - instead of rendering. A trailing word (`sh -c '…' extra`) makes it a real command, not an - adapter. Leave that alone too. - - One shape stays ambiguous on purpose: a single `sh -c` over a merely-quoted command, which a - version of this skill that wrongly counted quoting as a trigger also emitted. Nothing in it - distinguishes that from an operator's own, so it is preserved. The cost is one spurious shell - per refresh; peeling on a guess costs a broken statusline. - -One pass is not enough: an operator may already carry several layers from earlier reruns, and a -single peel over three layers leaves two. - -Substituting the raw string instead is what produces `context → rate → rate → renderer` when the -sibling plugin was configured first, or a doubled self-wrap on a re-run: each duplicated tee runs -and writes on EVERY refresh and costs another 0.6–0.9 s (below). Skipping rule 2 compounds the -shell-syntax guard instead, the leftover adapter still contains shell syntax, so it is wrapped in -ANOTHER `sh -c` layer, one more on every run. Unwrapping both makes the printed edit idempotent: -re-running `check` on already-correct wiring prints byte-identical wiring, with exactly one shim -invocation per plugin and at most one `sh -c` layer. +## Unwrap and wrap rules + +Read [`unwrap-before-compose.md`](unwrap-before-compose.md) now, before composing. It owns +the peel rules and the shell-syntax guard, shared byte-identical with rate-limit-guard. +Composing without it is what produced `context -> rate -> rate -> renderer` and the +compounding `sh -c` wrap. The JSON blocks below are this plugin's printed paths only. ## The edit blocks @@ -89,27 +47,7 @@ No statusline configured (standalone minimal statusline): } ``` -## The shell-syntax guard - -The wrapped form passes the user's command as ARGV, the shell that runs -the `statusLine` command splits the whole line into words and consumes its quotes, and the shim -`exec`s those words unchanged. It therefore only works for plain `executable arg…` commands. Test -the UNWRAPPED renderer, never the raw effective `command` string, the rules above run first. -Print the shell-wrapped variant instead when EITHER of these holds: - -- It carries, UNQUOTED, at the top level, shell syntax no ARGV word can express: an inline env - assignment like `THEME=dark my-statusline`, a redirection, or any control operator (`|`, `|&`, - `&&`, `||`, `;`, `&`, a newline). -- **Its command word is not an executable**, a shell builtin, function, or alias, which `exec` - cannot run because there is no file to exec. `ulimit '-n'` is the standing example: `ulimit` - exists only as a builtin, so the plain wrapped form reaches `exec ulimit -n` and the statusline - dies with exit 127 on every refresh. Resolve it the way the shim will: `type -P ` finding nothing while `type -t ` reports `builtin`, `function`, or - `alias` is the test, not by matching a hardcoded list of builtin names. - - This trigger is load-bearing precisely because it is *not* about syntax. Such a renderer often - carries none at all, and before the guard was scoped to real syntax the bare presence of quotes - wrapped it by accident. That accident was doing real work, and dropping it without this - replacement is what turns a working statusline into exit 127. +When the shared guard selects the shell-wrapped form: ```json { @@ -120,33 +58,6 @@ Print the shell-wrapped variant instead when EITHER of these holds: } ``` -`` is that same unwrapped renderer, POSIX-escaped for single-quote -embedding: replace every `'` in it with `'\''` before substituting (then JSON-escape the whole -`command` string as usual). Show the final, fully escaped line, never hand the operator a -template with raw quotes left to fix. Verify your printed edit round-trips: run -`printf '%s\n' ''` and confirm the output matches the renderer -byte-for-byte. The single-quoted argument reproduces exactly the quoting context the emitted -`sh -c ''` uses; a double-quoted wrapper would instead let the outer shell -expand any `$(...)` or backticks in the operator's own renderer before the check ever ran. - -**Syntax inside a quoted argument does not count, and bare quoting is never itself a trigger.** -The quotes make it one ordinary ARGV word that reaches the renderer intact through the plain -wrapped form. So an operator's own `sh -c ''`, the one shape rule 2 preserves, is -ALREADY a plain `executable arg…` command: `sh` is the executable, `-c` and the carried string -are two ordinary ARGV words. Substitute it VERBATIM. - -For an input that is ITSELF `sh -c ''`, rule 2 and this guard therefore never both wrap -it, and leave exactly one layer: rule 2 peels every generated layer before the guard runs, and -what rule 2 preserves is a renderer this guard declines. Do not generalize that to a layer count -for every input, the guard adds whatever the renderer genuinely needs, which is NONE for a plain -command and ONE for top-level syntax, and that one is a layer more when the operator's own -`sh -c` sits inside it. `sh -c 'ulimit -n' && echo ok` correctly prints TWO: the `&&` cannot be an -ARGV word, so the adapter is mandatory, and peeling the inner `sh -c` would strand the builtin. -What is invariant is that peel and wrap are inverses, which is what makes a re-run byte-identical -at whatever count the renderer needs. Firing on the quotes instead is what turned an operator's -`sh -c 'ulimit -n'` into `sh -c 'sh -c '\''ulimit -n'\'''`, one more shell on every refresh and -the same compounding rule 2 exists to prevent. - ## Sibling shims compose by nesting Sibling tees compose by nesting, each through its OWN shim, the tees are transparent wrappers, @@ -168,9 +79,10 @@ installed but its shim is absent, print the single-shim form above and say that } ``` -The shell-syntax guard applies UNCHANGED to this form: `` is the -innermost ARGV here too, so run the same test above on the same unwrapped renderer and substitute -whichever of the two forms it selects, never the raw string. Substituting `THEME=dark my-statusline` raw +The shell-syntax guard in [`unwrap-before-compose.md`](unwrap-before-compose.md) applies +UNCHANGED to this form: `` is the innermost ARGV here too, so run +that test on the same unwrapped renderer and substitute whichever of the two forms it selects, +never the raw string. Substituting `THEME=dark my-statusline` raw makes `THEME=dark` the executable, which fails `command not found` (127) instead of setting the variable. The shim paths are the only part that nests; the innermost substitution rule never changes: diff --git a/plugins/context-guard/skills/setup/reference/unwrap-before-compose.md b/plugins/context-guard/skills/setup/reference/unwrap-before-compose.md new file mode 100644 index 000000000..5fdab03af --- /dev/null +++ b/plugins/context-guard/skills/setup/reference/unwrap-before-compose.md @@ -0,0 +1,109 @@ +# Unwrap before you compose — shared peel and wrap rules + +The shared, plugin-name-free half of the two statusline guard plugins' compose +rules, synced byte-identical between them by `scripts/sync-unwrap-before-compose.sh` +and registered in `scripts/cross-plugin-source-registry.txt`. The hub setup skill +supplies every concrete shim path for the printed edit. These rules target +machine-scope surfaces under `~/.claude/`, outside the repo-scope +retirement-manifest schema (ADR 0018, decision 6), so they stay prose and are +deduplicated here instead. + +## Unwrap before you compose + +`` below means the operator's OWN +renderer, never the raw effective `command` string. Recover it by peeling off the wrapping this +skill itself prints, applying BOTH rules repeatedly until a pass strips nothing: + +1. **Guard-shim prefixes**. Every leading `bash /context-guard/bin/statusline-shim.sh` and + `bash /rate-limit-guard/bin/statusline-shim.sh`, in whatever order they appear, plus any + legacy `bash /…/statusline-tee.sh` prefix. +2. **A generated `sh -c` adapter**, when what remains is EXACTLY `sh -c ''` + with nothing after the closing quote, AND, once that string is unescaped, ANY of the following + holds, that is an adapter a previous run printed, not the renderer. Unescape it back: drop the + leading `sh -c` and the outer quotes, then replace every `'\''` with `'`. + + - **A. It is itself EXACTLY `sh -c ''`, nothing after the closing + quote.** A nested `sh -c` is always a layer some run added: an operator's own renderer is at + most one `sh -c` deep. Apply the same strictness here as to the outer shape, so two readers + peel the same number of layers. + - **B. It begins with a guard-shim prefix from rule 1.** This skill never puts a shim inside + an adapter, and an operator would not write one inside their own `sh -c`. Leaving it sealed + there hides it from rule 1, which strips only LEADING prefixes, and the composed wiring then + names that shim a second time. + - **C. It is a command the guard below would send for wrapping.** That is the only shape this + skill's own adapter ever carries. + + Branches A and B must NOT inherit the guard's top-level scoping. Their evidence is the shape + of the carried string, not the syntax in it. Absent all three, the `sh -c` was written by the + operator and must be preserved: peeling `sh -c 'ulimit -n'` to `ulimit -n` would leave the + shim `exec`-ing a shell builtin that no longer has a shell, and the statusline would exit 127 + instead of rendering. A trailing word (`sh -c '…' extra`) makes it a real command, not an + adapter. Leave that alone too. + + One shape stays ambiguous on purpose: a single `sh -c` over a merely-quoted command, which a + version of this skill that wrongly counted quoting as a trigger also emitted. Nothing in it + distinguishes that from an operator's own, so it is preserved. The cost is one spurious shell + per refresh; peeling on a guess costs a broken statusline. + +One pass is not enough: an operator may already carry several layers from earlier reruns, and a +single peel over three layers leaves two. + +Substituting the raw string instead is what produces `context → rate → rate → renderer` when the +sibling plugin was configured first, or a doubled self-wrap on a re-run: each duplicated tee runs +and writes on EVERY refresh and costs another 0.6–0.9 s. Skipping rule 2 compounds the +shell-syntax guard instead, the leftover adapter still contains shell syntax, so it is wrapped in +ANOTHER `sh -c` layer, one more on every run. Unwrapping both makes the printed edit idempotent: +re-running `check` on already-correct wiring prints byte-identical wiring, with exactly one shim +invocation per plugin and at most one `sh -c` layer. + +## The shell-syntax guard + +The wrapped form passes the user's command as ARGV, the shell that runs +the `statusLine` command splits the whole line into words and consumes its quotes, and the shim +`exec`s those words unchanged. It therefore only works for plain `executable arg…` commands. Test +the UNWRAPPED renderer, never the raw effective `command` string, the rules above run first. +Print the shell-wrapped variant instead when EITHER of these holds: + +- It carries, UNQUOTED, at the top level, shell syntax no ARGV word can express: an inline env + assignment like `THEME=dark my-statusline`, a redirection, or any control operator (`|`, `|&`, + `&&`, `||`, `;`, `&`, a newline). +- **Its command word is not an executable**, a shell builtin, function, or alias, which `exec` + cannot run because there is no file to exec. `ulimit '-n'` is the standing example: `ulimit` + exists only as a builtin, so the plain wrapped form reaches `exec ulimit -n` and the statusline + dies with exit 127 on every refresh. Resolve it the way the shim will: `type -P ` finding nothing while `type -t ` reports `builtin`, `function`, or + `alias` is the test, not by matching a hardcoded list of builtin names. + + This trigger is load-bearing precisely because it is *not* about syntax. Such a renderer often + carries none at all, and before the guard was scoped to real syntax the bare presence of quotes + wrapped it by accident. That accident was doing real work, and dropping it without this + replacement is what turns a working statusline into exit 127. + +The hub prints this plugin's own shim path around `sh -c ''` when the guard +fires, and around the renderer verbatim when it does not. + +`` is that same unwrapped renderer, POSIX-escaped for single-quote +embedding: replace every `'` in it with `'\''` before substituting (then JSON-escape the whole +`command` string as usual). Show the final, fully escaped line, never hand the operator a +template with raw quotes left to fix. Verify your printed edit round-trips: run +`printf '%s\n' ''` and confirm the output matches the renderer +byte-for-byte. The single-quoted argument reproduces exactly the quoting context the emitted +`sh -c ''` uses; a double-quoted wrapper would instead let the outer shell +expand any `$(...)` or backticks in the operator's own renderer before the check ever ran. + +**Syntax inside a quoted argument does not count, and bare quoting is never itself a trigger.** +The quotes make it one ordinary ARGV word that reaches the renderer intact through the plain +wrapped form. So an operator's own `sh -c ''`, the one shape rule 2 preserves, is +ALREADY a plain `executable arg…` command: `sh` is the executable, `-c` and the carried string +are two ordinary ARGV words. Substitute it VERBATIM. + +For an input that is ITSELF `sh -c ''`, rule 2 and this guard therefore never both wrap +it, and leave exactly one layer: rule 2 peels every generated layer before the guard runs, and +what rule 2 preserves is a renderer this guard declines. Do not generalize that to a layer count +for every input, the guard adds whatever the renderer genuinely needs, which is NONE for a plain +command and ONE for top-level syntax, and that one is a layer more when the operator's own +`sh -c` sits inside it. `sh -c 'ulimit -n' && echo ok` correctly prints TWO: the `&&` cannot be an +ARGV word, so the adapter is mandatory, and peeling the inner `sh -c` would strand the builtin. +What is invariant is that peel and wrap are inverses, which is what makes a re-run byte-identical +at whatever count the renderer needs. Firing on the quotes instead is what turned an operator's +`sh -c 'ulimit -n'` into `sh -c 'sh -c '\''ulimit -n'\'''`, one more shell on every refresh and +the same compounding rule 2 exists to prevent. diff --git a/plugins/rate-limit-guard/.claude-plugin/plugin.json b/plugins/rate-limit-guard/.claude-plugin/plugin.json index cc3115916..7f35bc111 100644 --- a/plugins/rate-limit-guard/.claude-plugin/plugin.json +++ b/plugins/rate-limit-guard/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "rate-limit-guard", - "version": "0.7.24", + "version": "0.7.25", "description": "Shared rate-limit guard for loop lanes: a statusline wrapper tees the subscription rate-limit windows to a fixed machine-scope file, a StopFailure hook records rate-limit stops reactively, and a reader contract fixes how consuming sessions pause and resume.", "author": { "name": "Melodic Software", diff --git a/plugins/rate-limit-guard/CHANGELOG.md b/plugins/rate-limit-guard/CHANGELOG.md index e40bd44bc..6becca603 100644 --- a/plugins/rate-limit-guard/CHANGELOG.md +++ b/plugins/rate-limit-guard/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to the `rate-limit-guard` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.7.25] + +### Changed + +- **setup: unwrap-before-compose matches context-guard, then shares the spoke.** The inline + shell-syntax guard treated bare quoting as a wrap trigger, the bug context-guard already + fixed with `type -P` / `type -t`. Peel and wrap rules now live in + `reference/unwrap-before-compose.md`, synced byte-identical from context-guard. + ## [0.7.24] ### Changed diff --git a/plugins/rate-limit-guard/skills/setup/SKILL.md b/plugins/rate-limit-guard/skills/setup/SKILL.md index 59bba7fc0..1bd9fd297 100644 --- a/plugins/rate-limit-guard/skills/setup/SKILL.md +++ b/plugins/rate-limit-guard/skills/setup/SKILL.md @@ -110,16 +110,11 @@ owned by `${CLAUDE_PLUGIN_ROOT}/reference/reader-contract.md`. marked clearly as the operator's to apply. The wiring target is the SHIM's fixed path, never `${CLAUDE_PLUGIN_ROOT}`, which is version-pinned and belongs in no operator file: - **Unwrap before you compose.** `` below means the operator's OWN - renderer, never the raw effective `command` string. Before substituting, strip every leading - guard-shim invocation from that string: `bash /rate-limit-guard/bin/statusline-shim.sh` - and `bash /context-guard/bin/statusline-shim.sh`, in whatever order they appear, plus any - legacy `bash /…/statusline-tee.sh` prefix, and treat what remains as the renderer. - Substituting the raw string instead is what produces `context → rate → rate → renderer` when the - sibling plugin was configured first, or a doubled self-wrap on a re-run: each duplicated tee runs - and writes on EVERY refresh and costs another 0.6–0.9 s (below). Unwrapping also makes the - printed edit idempotent. Re-running `check` on already-correct wiring prints the wiring it - already has. + Read [`reference/unwrap-before-compose.md`](reference/unwrap-before-compose.md) now, before + composing: it owns the peel rules and the shell-syntax guard, shared byte-identical with + context-guard. Bare quoting is never a wrap trigger; `type -P` / `type -t` is how a builtin + renderer is detected. Composing without it is what produced `context -> rate -> rate -> + renderer` and the compounding `sh -c` wrap. Wrapping an existing statusline command (preserve the user's unwrapped command verbatim as the trailing arguments): @@ -144,10 +139,7 @@ owned by `${CLAUDE_PLUGIN_ROOT}/reference/reader-contract.md`. } ``` - Shell-syntax guard: the wrapped form passes the user's command as ARGV. It only works for - plain `executable arg…` commands. If the current command contains shell syntax (an inline env - assignment like `THEME=dark my-statusline`, a pipe, `&&`, `;`, or quoting), print the - shell-wrapped variant instead: + When the shared guard selects the shell-wrapped form: ```json { @@ -184,9 +176,10 @@ owned by `${CLAUDE_PLUGIN_ROOT}/reference/reader-contract.md`. } ``` - The shell-syntax guard applies UNCHANGED to this form: `` is the - innermost ARGV here too, so a command carrying shell syntax must be substituted as - `sh -c ''`, never raw. Substituting `THEME=dark my-statusline` raw + The shell-syntax guard in [`reference/unwrap-before-compose.md`](reference/unwrap-before-compose.md) + applies UNCHANGED to this form: `` is the innermost ARGV here too, + so run that test on the same unwrapped renderer and substitute whichever of the two forms it + selects, never raw. Substituting `THEME=dark my-statusline` raw makes `THEME=dark` the executable, which fails `command not found` (127) instead of setting the variable. The shim paths are the only part that nests; the innermost substitution rule never changes: diff --git a/plugins/rate-limit-guard/skills/setup/evals/evals.json b/plugins/rate-limit-guard/skills/setup/evals/evals.json index c2c58d71c..fbe23bc4a 100644 --- a/plugins/rate-limit-guard/skills/setup/evals/evals.json +++ b/plugins/rate-limit-guard/skills/setup/evals/evals.json @@ -87,6 +87,58 @@ "Does not present the two steps as order-independent", "Notes that uninstalling the plugin itself is safe on its own — the shim degrades to running the wrapped command" ] + }, + { + "id": 8, + "name": "rerun-does-not-compound-the-sh-c-wrap", + "prompt": "/rate-limit-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c 'THEME=dark my-statusline --flag' — I ran this skill once before and applied what it printed. Both the shim and the plugin are installed.", + "expected_output": "Unwraps the shim prefix AND the previously-generated sh -c adapter back to THEME=dark my-statusline --flag, then re-applies the shell-syntax guard to that renderer, printing wiring that carries exactly one sh -c layer — byte-identical to the wiring already in settings.json, so the operator has nothing to apply.", + "files": [], + "expectations": [ + "Treats the existing `sh -c '...'` as a previously-generated adapter to unwrap, never as the operator's own renderer", + "Prints exactly ONE `sh -c` layer and one rate-limit-guard shim invocation — never a nested `sh -c 'sh -c '\\''...'\\'''`", + "Recovers `THEME=dark my-statusline --flag` byte-for-byte, unescaping `'\\''` back to `'`", + "Reports the wiring as already correct rather than presenting an identical line as a change to apply" + ] + }, + { + "id": 9, + "name": "genuine-sh-c-renderer-is-not-peeled", + "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: sh -c 'ulimit -n' — I wrote that myself because ulimit is a shell builtin and will not run any other way. The plugin is installed.", + "expected_output": "Treats sh -c 'ulimit -n' as the operator's OWN renderer, not a generated adapter, because the string it carries has no shell syntax and this skill only ever emits the adapter for a renderer that has some. Wraps it intact: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c 'ulimit -n'.", + "files": [], + "expectations": [ + "Does NOT peel the `sh -c` layer — the carried string `ulimit -n` contains no shell syntax, so the adapter cannot have been generated by this skill", + "Preserves `sh -c 'ulimit -n'` verbatim as the wrapped command, so the shim never `exec`s a shell builtin with no shell", + "Names the provenance test (adapter is emitted only for a renderer carrying shell syntax) rather than peeling any `sh -c` on sight", + "Prints exactly one shim invocation and exactly one `sh -c` layer" + ] + }, + { + "id": 10, + "name": "bare-quoting-is-not-a-wrap-trigger", + "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: my-statusline --format 'a | b' — the pipe is part of the format string, not a pipeline. The plugin is installed.", + "expected_output": "Prints the PLAIN wrapped form: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh my-statusline --format 'a | b'. The pipe is sealed inside a quoted argument, so it is one ordinary ARGV word and needs no adapter.", + "files": [], + "expectations": [ + "Emits the PLAIN wrapped form with no `sh -c` adapter", + "Does NOT treat the `|` as a control operator — the guard tests for syntax that is UNQUOTED and at the TOP LEVEL, and this pipe is neither", + "Does not justify a wrap by the presence of quotes — bare quoting is explicitly not a trigger", + "Reads `my-statusline` as an ordinary executable, so the not-an-executable trigger does not fire either" + ] + }, + { + "id": 11, + "name": "bare-builtin-renderer-gets-a-shell", + "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: ulimit '-n' — I wrote it that way myself; the quotes are just habit. The plugin is installed.", + "expected_output": "Recognizes that the command word `ulimit` resolves as a shell builtin and not as an executable, so the plain wrapped form would reach `exec ulimit -n` and die with exit 127. Prints the shell-wrapped variant: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c 'ulimit '''-n''''.", + "files": [], + "expectations": [ + "Emits the SHELL-WRAPPED variant, not the plain wrapped one — `exec` cannot run a builtin, so the plain form would exit 127 on every refresh", + "Reaches that decision through the not-an-executable trigger, NOT through the shell-syntax trigger — the renderer carries no unquoted top-level shell syntax at all", + "Does not justify the wrap by the presence of quotes — bare quoting is explicitly not a trigger", + "Resolves builtin-ness the way the shim will (`type -P` finding nothing while `type -t` reports `builtin`), never by matching a hardcoded list of builtin names" + ] } ] } diff --git a/plugins/rate-limit-guard/skills/setup/reference/unwrap-before-compose.md b/plugins/rate-limit-guard/skills/setup/reference/unwrap-before-compose.md new file mode 100644 index 000000000..5fdab03af --- /dev/null +++ b/plugins/rate-limit-guard/skills/setup/reference/unwrap-before-compose.md @@ -0,0 +1,109 @@ +# Unwrap before you compose — shared peel and wrap rules + +The shared, plugin-name-free half of the two statusline guard plugins' compose +rules, synced byte-identical between them by `scripts/sync-unwrap-before-compose.sh` +and registered in `scripts/cross-plugin-source-registry.txt`. The hub setup skill +supplies every concrete shim path for the printed edit. These rules target +machine-scope surfaces under `~/.claude/`, outside the repo-scope +retirement-manifest schema (ADR 0018, decision 6), so they stay prose and are +deduplicated here instead. + +## Unwrap before you compose + +`` below means the operator's OWN +renderer, never the raw effective `command` string. Recover it by peeling off the wrapping this +skill itself prints, applying BOTH rules repeatedly until a pass strips nothing: + +1. **Guard-shim prefixes**. Every leading `bash /context-guard/bin/statusline-shim.sh` and + `bash /rate-limit-guard/bin/statusline-shim.sh`, in whatever order they appear, plus any + legacy `bash /…/statusline-tee.sh` prefix. +2. **A generated `sh -c` adapter**, when what remains is EXACTLY `sh -c ''` + with nothing after the closing quote, AND, once that string is unescaped, ANY of the following + holds, that is an adapter a previous run printed, not the renderer. Unescape it back: drop the + leading `sh -c` and the outer quotes, then replace every `'\''` with `'`. + + - **A. It is itself EXACTLY `sh -c ''`, nothing after the closing + quote.** A nested `sh -c` is always a layer some run added: an operator's own renderer is at + most one `sh -c` deep. Apply the same strictness here as to the outer shape, so two readers + peel the same number of layers. + - **B. It begins with a guard-shim prefix from rule 1.** This skill never puts a shim inside + an adapter, and an operator would not write one inside their own `sh -c`. Leaving it sealed + there hides it from rule 1, which strips only LEADING prefixes, and the composed wiring then + names that shim a second time. + - **C. It is a command the guard below would send for wrapping.** That is the only shape this + skill's own adapter ever carries. + + Branches A and B must NOT inherit the guard's top-level scoping. Their evidence is the shape + of the carried string, not the syntax in it. Absent all three, the `sh -c` was written by the + operator and must be preserved: peeling `sh -c 'ulimit -n'` to `ulimit -n` would leave the + shim `exec`-ing a shell builtin that no longer has a shell, and the statusline would exit 127 + instead of rendering. A trailing word (`sh -c '…' extra`) makes it a real command, not an + adapter. Leave that alone too. + + One shape stays ambiguous on purpose: a single `sh -c` over a merely-quoted command, which a + version of this skill that wrongly counted quoting as a trigger also emitted. Nothing in it + distinguishes that from an operator's own, so it is preserved. The cost is one spurious shell + per refresh; peeling on a guess costs a broken statusline. + +One pass is not enough: an operator may already carry several layers from earlier reruns, and a +single peel over three layers leaves two. + +Substituting the raw string instead is what produces `context → rate → rate → renderer` when the +sibling plugin was configured first, or a doubled self-wrap on a re-run: each duplicated tee runs +and writes on EVERY refresh and costs another 0.6–0.9 s. Skipping rule 2 compounds the +shell-syntax guard instead, the leftover adapter still contains shell syntax, so it is wrapped in +ANOTHER `sh -c` layer, one more on every run. Unwrapping both makes the printed edit idempotent: +re-running `check` on already-correct wiring prints byte-identical wiring, with exactly one shim +invocation per plugin and at most one `sh -c` layer. + +## The shell-syntax guard + +The wrapped form passes the user's command as ARGV, the shell that runs +the `statusLine` command splits the whole line into words and consumes its quotes, and the shim +`exec`s those words unchanged. It therefore only works for plain `executable arg…` commands. Test +the UNWRAPPED renderer, never the raw effective `command` string, the rules above run first. +Print the shell-wrapped variant instead when EITHER of these holds: + +- It carries, UNQUOTED, at the top level, shell syntax no ARGV word can express: an inline env + assignment like `THEME=dark my-statusline`, a redirection, or any control operator (`|`, `|&`, + `&&`, `||`, `;`, `&`, a newline). +- **Its command word is not an executable**, a shell builtin, function, or alias, which `exec` + cannot run because there is no file to exec. `ulimit '-n'` is the standing example: `ulimit` + exists only as a builtin, so the plain wrapped form reaches `exec ulimit -n` and the statusline + dies with exit 127 on every refresh. Resolve it the way the shim will: `type -P ` finding nothing while `type -t ` reports `builtin`, `function`, or + `alias` is the test, not by matching a hardcoded list of builtin names. + + This trigger is load-bearing precisely because it is *not* about syntax. Such a renderer often + carries none at all, and before the guard was scoped to real syntax the bare presence of quotes + wrapped it by accident. That accident was doing real work, and dropping it without this + replacement is what turns a working statusline into exit 127. + +The hub prints this plugin's own shim path around `sh -c ''` when the guard +fires, and around the renderer verbatim when it does not. + +`` is that same unwrapped renderer, POSIX-escaped for single-quote +embedding: replace every `'` in it with `'\''` before substituting (then JSON-escape the whole +`command` string as usual). Show the final, fully escaped line, never hand the operator a +template with raw quotes left to fix. Verify your printed edit round-trips: run +`printf '%s\n' ''` and confirm the output matches the renderer +byte-for-byte. The single-quoted argument reproduces exactly the quoting context the emitted +`sh -c ''` uses; a double-quoted wrapper would instead let the outer shell +expand any `$(...)` or backticks in the operator's own renderer before the check ever ran. + +**Syntax inside a quoted argument does not count, and bare quoting is never itself a trigger.** +The quotes make it one ordinary ARGV word that reaches the renderer intact through the plain +wrapped form. So an operator's own `sh -c ''`, the one shape rule 2 preserves, is +ALREADY a plain `executable arg…` command: `sh` is the executable, `-c` and the carried string +are two ordinary ARGV words. Substitute it VERBATIM. + +For an input that is ITSELF `sh -c ''`, rule 2 and this guard therefore never both wrap +it, and leave exactly one layer: rule 2 peels every generated layer before the guard runs, and +what rule 2 preserves is a renderer this guard declines. Do not generalize that to a layer count +for every input, the guard adds whatever the renderer genuinely needs, which is NONE for a plain +command and ONE for top-level syntax, and that one is a layer more when the operator's own +`sh -c` sits inside it. `sh -c 'ulimit -n' && echo ok` correctly prints TWO: the `&&` cannot be an +ARGV word, so the adapter is mandatory, and peeling the inner `sh -c` would strand the builtin. +What is invariant is that peel and wrap are inverses, which is what makes a re-run byte-identical +at whatever count the renderer needs. Firing on the quotes instead is what turned an operator's +`sh -c 'ulimit -n'` into `sh -c 'sh -c '\''ulimit -n'\'''`, one more shell on every refresh and +the same compounding rule 2 exists to prevent. diff --git a/scripts/cross-plugin-source-registry.txt b/scripts/cross-plugin-source-registry.txt index 2431466ca..38e6e56e0 100644 --- a/scripts/cross-plugin-source-registry.txt +++ b/scripts/cross-plugin-source-registry.txt @@ -45,5 +45,8 @@ lib/check-retirements.sh # Dedicated check: scripts/sync-legacy-statusline-detect.sh --check (CI: legacy-statusline-detect-sync) skills/setup/reference/legacy-statusline-detect.md +# Dedicated check: scripts/sync-unwrap-before-compose.sh --check (CI: unwrap-before-compose-sync) +skills/setup/reference/unwrap-before-compose.md + # Dedicated check: scripts/sync-resolve-convention-home.sh --check (CI: resolve-convention-home-sync) lib/resolve-convention-home.sh diff --git a/scripts/sync-unwrap-before-compose.sh b/scripts/sync-unwrap-before-compose.sh new file mode 100755 index 000000000..57d50d701 --- /dev/null +++ b/scripts/sync-unwrap-before-compose.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Sync or verify the cross-plugin skills/setup/reference/unwrap-before-compose.md cluster. +# +# scripts/sync-unwrap-before-compose.sh copy the canonical file into each carrier +# scripts/sync-unwrap-before-compose.sh --check fail if any carrier differs from canonical +# scripts/sync-unwrap-before-compose.sh --check-bump fail if the canonical changed vs but a +# carrying plugin's manifest version did not +# scripts/sync-unwrap-before-compose.sh --print-manifest emit src and copies as data (for affected-tests) +# +# Canonical copy: plugins/context-guard/skills/setup/reference/unwrap-before-compose.md (see +# scripts/cross-plugin-source-registry.txt). The twin statusline guard plugins carry the shared, +# plugin-name-free half of their unwrap-before-compose and shell-syntax-guard rules here; the +# surfaces they classify are machine-scope (~/.claude/), so ADR 0018 decision 6 fixes their twin +# drift with this sync gate rather than the repo-scope retirement schema. +# +# The three modes live in scripts/lib/sync-cluster.sh, shared with the sibling +# sync-*.sh gates; this file supplies the unwrap-before-compose cluster's parameters. +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$script_dir/.." +# shellcheck source=lib/sync-cluster.sh +. "$script_dir/lib/sync-cluster.sh" + +sync_cluster_script="sync-unwrap-before-compose.sh" +src="plugins/context-guard/skills/setup/reference/unwrap-before-compose.md" +copies=(plugins/rate-limit-guard/skills/setup/reference/unwrap-before-compose.md) +sync_cluster_manifest_strip='/skills/*' +sync_cluster_noun="Canonical" +sync_cluster_carrier="carrying" +sync_cluster_sync_summary=0 + +mode="${1:-sync}" +base="" +# Raised here, not in the shared engine: bash prefixes a ${var:?} diagnostic with +# the path and line of the expansion, so the message has to come from the script +# the user actually ran. +[[ "$mode" == "--check-bump" ]] && base="${2:?usage: sync-unwrap-before-compose.sh --check-bump }" + +sync_cluster::run "$mode" "$base" diff --git a/scripts/sync-unwrap-before-compose.test.sh b/scripts/sync-unwrap-before-compose.test.sh new file mode 100755 index 000000000..42a95eed1 --- /dev/null +++ b/scripts/sync-unwrap-before-compose.test.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Unit tests for sync-unwrap-before-compose.sh. Builds a tiny synthetic repo +# tree per scenario in a temp dir and invokes the script against it directly. +# The load-bearing case is "--check discriminates": a drift gate that reports +# clean whether or not the copies match is worse than no gate. +set -uo pipefail + +SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPT="$SELF_DIR/sync-unwrap-before-compose.sh" +. "$SELF_DIR/test-git-helpers.sh" + +# shellcheck source=lib/test-harness.sh +. "$SELF_DIR/lib/test-harness.sh" + +CANONICAL="plugins/context-guard/skills/setup/reference/unwrap-before-compose.md" +COPY="plugins/rate-limit-guard/skills/setup/reference/unwrap-before-compose.md" + +canonical_v1() { + printf '# Unwrap before you compose\n\nshared spoke v1\n' +} +canonical_v2() { + printf '# Unwrap before you compose\n\nshared spoke v2\n' +} + +new_fixture() { + local dir + dir="$(mktemp -d)" + mkdir -p "$dir/scripts/lib" \ + "$dir/plugins/context-guard/skills/setup/reference" \ + "$dir/plugins/context-guard/.claude-plugin" \ + "$dir/plugins/rate-limit-guard/skills/setup/reference" \ + "$dir/plugins/rate-limit-guard/.claude-plugin" + cp "$SCRIPT" "$dir/scripts/sync-unwrap-before-compose.sh" + cp "$SELF_DIR/lib/sync-cluster.sh" "$dir/scripts/lib/sync-cluster.sh" + chmod +x "$dir/scripts/sync-unwrap-before-compose.sh" + printf '%s' "$dir" +} + +manifest() { + printf '{"name":"%s","version":"%s"}\n' "$2" "$3" >"$1/plugins/$2/.claude-plugin/plugin.json" +} + +base_fixture() { + local dir + dir="$(new_fixture)" + canonical_v1 >"$dir/$CANONICAL" + canonical_v1 >"$dir/$COPY" + manifest "$dir" context-guard 0.1.0 + manifest "$dir" rate-limit-guard 0.1.0 + printf '%s' "$dir" +} + +git_fixture() { + local fixture="$1" + git_init_test_repo "$fixture" || return 1 + git -C "$fixture" add -A + git -C "$fixture" commit -qm base + git -C "$fixture" rev-parse HEAD +} + +run_mode() ( + local fixture="$1" + shift + cd "$fixture" && bash scripts/sync-unwrap-before-compose.sh "$@" +) + +f="$(new_fixture)" +canonical_v1 >"$f/$CANONICAL" +printf '# drifted\n' >"$f/$COPY" +manifest "$f" context-guard 0.1.0 +manifest "$f" rate-limit-guard 0.1.0 +if out="$(run_mode "$f" 2>&1)" && cmp -s "$f/$CANONICAL" "$f/$COPY"; then + ok "sync makes the carrying copy byte-identical to the canonical" +else + fail "sync should copy the canonical into rate-limit-guard, got: $out" +fi +rm -rf "$f" + +f="$(base_fixture)" +if run_mode "$f" --check >/dev/null 2>&1; then + clean_verdict=pass +else + clean_verdict=fail +fi +printf '# drifted\n' >"$f/$COPY" +if run_mode "$f" --check >/dev/null 2>&1; then + drifted_verdict=pass +else + drifted_verdict=fail +fi +if [[ "$clean_verdict" != "$drifted_verdict" ]]; then + ok "--check discriminates: matching copies '$clean_verdict', drifted copies '$drifted_verdict'" +else + fail "--check returned '$clean_verdict' for BOTH matching and drifted copies" +fi +if [[ "$clean_verdict" == pass && "$drifted_verdict" == fail ]]; then + ok "--check passes on a matching cluster and fails on a drifted one" +else + fail "expected clean=pass drifted=fail, got clean=$clean_verdict drifted=$drifted_verdict" +fi +rm -rf "$f" + +f="$(base_fixture)" +printf '# drifted\n' >"$f/$COPY" +out="$(run_mode "$f" --check 2>&1)" || true +if [[ "$out" == *"$COPY"* && "$out" == *"$CANONICAL"* ]]; then + ok "the drift message names both the drifted copy and the canonical" +else + fail "drift message should name both paths, got: $out" +fi +rm -rf "$f" + +f="$(base_fixture)" +out="$(run_mode "$f" --print-manifest 2>&1)" +if [[ "$out" == *"src"*"$CANONICAL"* && "$out" == *"copy"*"$COPY"* ]]; then + ok "--print-manifest publishes src and copy, so affected-tests can derive the fan-out" +else + fail "--print-manifest should publish src and copy, got: $out" +fi +rm -rf "$f" + +f="$(base_fixture)" +if base="$(git_fixture "$f")"; then + canonical_v2 >"$f/$CANONICAL" + canonical_v2 >"$f/$COPY" + if run_mode "$f" --check-bump "$base" >/dev/null 2>&1; then + fail "--check-bump should fail when the canonical changed but no carrier version moved" + else + ok "--check-bump fails when the canonical changed but no carrier version moved" + fi + manifest "$f" rate-limit-guard 0.2.0 + manifest "$f" context-guard 0.2.0 + if run_mode "$f" --check-bump "$base" >/dev/null 2>&1; then + ok "--check-bump passes once the carrying plugins bumped" + else + fail "--check-bump should pass after both carriers bumped" + fi +else + fail "could not init a git fixture; --check-bump arms did not run" +fi +rm -rf "$f" + +test_harness::report From 1b907672a87c6d0d417d0cb891d61e19a28b957d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Sep 2026 13:37:14 +0000 Subject: [PATCH 2/3] fix: make the builtin-renderer eval escape round-trip The expected sh -c argument for ulimit '-n' now uses the POSIX '\'' form so printf recovers the quoted renderer. Co-authored-by: ksextonmelodic --- .../skills/setup/evals/evals.json | 54 +++++++++---------- .../skills/setup/evals/evals.json | 40 +++++++------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/plugins/context-guard/skills/setup/evals/evals.json b/plugins/context-guard/skills/setup/evals/evals.json index 4027b3f70..21c090f77 100644 --- a/plugins/context-guard/skills/setup/evals/evals.json +++ b/plugins/context-guard/skills/setup/evals/evals.json @@ -17,7 +17,7 @@ { "id": 2, "name": "flags-legacy-version-pinned-wiring", - "prompt": "/context-guard:setup check\n\nMy settings.json statusLine command is: bash \"~/.claude/plugins/cache/melodic-software/context-guard/0.1.0/scripts/statusline-tee.sh\" ~/.claude/statusline/render.sh — and that file exists right now.", + "prompt": "/context-guard:setup check\n\nMy settings.json statusLine command is: bash \"~/.claude/plugins/cache/melodic-software/context-guard/0.1.0/scripts/statusline-tee.sh\" ~/.claude/statusline/render.sh \u2014 and that file exists right now.", "expected_output": "Names the wiring as legacy version-pinned even though the file exists today: it stops teeing at the next version bump and breaks the whole statusline once the old version directory is pruned. Prints the shim wiring as the fix, with apply first if the shim is not installed.", "files": [], "expectations": [ @@ -31,7 +31,7 @@ "id": 3, "name": "reads-null-fields-as-expected-state", "prompt": "/context-guard:setup check\n\nThe live session's snapshot is fresh but used_percentage is null.", - "expected_output": "Reports the fresh-but-null snapshot as INFO — a documented early-session or post-/compact statusline state in which the resolver correctly answers unknown — not as a failure to fix.", + "expected_output": "Reports the fresh-but-null snapshot as INFO \u2014 a documented early-session or post-/compact statusline state in which the resolver correctly answers unknown \u2014 not as a failure to fix.", "files": [], "expectations": [ "Classifies null used_percentage / current_usage on a fresh snapshot as documented fail-open behavior", @@ -43,10 +43,10 @@ "id": 4, "name": "apply-preserves-unrecognized-keys-and-custom-bands", "prompt": "/context-guard:setup apply\n\nMy zones.json already exists with custom valid bands (40/70) and carries a custom display_color key my statusline reads.", - "expected_output": "Bare apply is repair-only: leaves the valid custom bands (40/70) untouched and reports them, preserves the unrecognized display_color key and its value (semantic preservation — lexical formatting may normalize), names 'apply defaults' as the explicit route to the shipped defaults, and confirms a second identical apply would be a no-op.", + "expected_output": "Bare apply is repair-only: leaves the valid custom bands (40/70) untouched and reports them, preserves the unrecognized display_color key and its value (semantic preservation \u2014 lexical formatting may normalize), names 'apply defaults' as the explicit route to the shipped defaults, and confirms a second identical apply would be a no-op.", "files": [], "expectations": [ - "Writes only inside ~/.claude/context-guard/ — zones.json plus the shim — and nothing else (a fully valid zones file may mean no zones write at all)", + "Writes only inside ~/.claude/context-guard/ \u2014 zones.json plus the shim \u2014 and nothing else (a fully valid zones file may mean no zones write at all)", "Does NOT overwrite valid custom band values on a bare apply; names apply defaults as the explicit shipped-defaults mode", "Preserves unrecognized keys rather than rewriting the file from the template", "States idempotency (second run = no content change)" @@ -59,7 +59,7 @@ "expected_output": "Copies the shipped statusline-shim.sh byte-identically to ~/.claude/context-guard/bin/statusline-shim.sh alongside the zones.json seed/repair, reports each write (or reports no change on a second identical run), states that the shim is inert until the operator wires it, and prints the wiring edit as the next step.", "files": [], "expectations": [ - "Writes only inside ~/.claude/context-guard/ — never settings.json, the snapshot directory, or the sibling rate-limit-guard directory", + "Writes only inside ~/.claude/context-guard/ \u2014 never settings.json, the snapshot directory, or the sibling rate-limit-guard directory", "Installed shim is byte-identical to the shipped source, not a rewritten or templated variant", "Second identical apply reports no content change", "States that installing the shim starts nothing until the settings.json wiring is applied by the operator" @@ -68,14 +68,14 @@ { "id": 6, "name": "composes-without-double-wrapping", - "prompt": "/context-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh ~/.claude/statusline/render.sh — I configured rate-limit-guard first. Both plugins are installed, but I have never run /context-guard:setup apply.", + "prompt": "/context-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh ~/.claude/statusline/render.sh \u2014 I configured rate-limit-guard first. Both plugins are installed, but I have never run /context-guard:setup apply.", "expected_output": "Recognizes the existing sibling-shim wrapper, unwraps it to the operator's own renderer before composing, and prints wiring that runs each tee exactly once. Because this plugin's own shim is not installed yet, it reports that and names apply as the remediation rather than printing wiring naming a file that does not exist.", "files": [], "expectations": [ - "Treats ~/.claude/statusline/render.sh as the wrapped command — never re-wraps the full existing command, so the rate-limit-guard shim appears exactly once in the printed wiring", + "Treats ~/.claude/statusline/render.sh as the wrapped command \u2014 never re-wraps the full existing command, so the rate-limit-guard shim appears exactly once in the printed wiring", "Never prints a combined form naming a shim path that is not installed; gates the combined wiring on the sibling shim existing, or directs the operator to run that plugin's apply first", "Explains that a duplicated tee runs and writes on every refresh and adds another ~0.6-0.9 s per refresh", - "Still edits nothing — the wiring is printed for the operator to apply" + "Still edits nothing \u2014 the wiring is printed for the operator to apply" ] }, { @@ -87,18 +87,18 @@ "expectations": [ "States the order explicitly and gives the reason: deleting the directory first leaves settings.json invoking a missing file, so bash exits 127 and the whole statusline goes down", "Does not present the two steps as order-independent", - "Notes that uninstalling the plugin itself is safe on its own — the shim degrades to running the wrapped command" + "Notes that uninstalling the plugin itself is safe on its own \u2014 the shim degrades to running the wrapped command" ] }, { "id": 8, "name": "rerun-does-not-compound-the-sh-c-wrap", - "prompt": "/context-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'THEME=dark my-statusline --flag' — I ran this skill once before and applied what it printed. Both the shim and the plugin are installed.", - "expected_output": "Unwraps the shim prefix AND the previously-generated sh -c adapter back to THEME=dark my-statusline --flag, then re-applies the shell-syntax guard to that renderer, printing wiring that carries exactly one sh -c layer — byte-identical to the wiring already in settings.json, so the operator has nothing to apply.", + "prompt": "/context-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'THEME=dark my-statusline --flag' \u2014 I ran this skill once before and applied what it printed. Both the shim and the plugin are installed.", + "expected_output": "Unwraps the shim prefix AND the previously-generated sh -c adapter back to THEME=dark my-statusline --flag, then re-applies the shell-syntax guard to that renderer, printing wiring that carries exactly one sh -c layer \u2014 byte-identical to the wiring already in settings.json, so the operator has nothing to apply.", "files": [], "expectations": [ "Treats the existing `sh -c '...'` as a previously-generated adapter to unwrap, never as the operator's own renderer", - "Prints exactly ONE `sh -c` layer and one context-guard shim invocation — never a nested `sh -c 'sh -c '\\''...'\\'''`", + "Prints exactly ONE `sh -c` layer and one context-guard shim invocation \u2014 never a nested `sh -c 'sh -c '\\''...'\\'''`", "Recovers `THEME=dark my-statusline --flag` byte-for-byte, unescaping `'\\''` back to `'`", "Reports the wiring as already correct rather than presenting an identical line as a change to apply" ] @@ -106,11 +106,11 @@ { "id": 9, "name": "genuine-sh-c-renderer-is-not-peeled", - "prompt": "/context-guard:setup check\n\nI have never run this skill. My statusLine command is: sh -c 'ulimit -n' — I wrote that myself because ulimit is a shell builtin and will not run any other way. The plugin is installed.", + "prompt": "/context-guard:setup check\n\nI have never run this skill. My statusLine command is: sh -c 'ulimit -n' \u2014 I wrote that myself because ulimit is a shell builtin and will not run any other way. The plugin is installed.", "expected_output": "Treats sh -c 'ulimit -n' as the operator's OWN renderer, not a generated adapter, because the string it carries has no shell syntax and this skill only ever emits the adapter for a renderer that has some. Wraps it intact: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'ulimit -n'.", "files": [], "expectations": [ - "Does NOT peel the `sh -c` layer — the carried string `ulimit -n` contains no shell syntax, so the adapter cannot have been generated by this skill", + "Does NOT peel the `sh -c` layer \u2014 the carried string `ulimit -n` contains no shell syntax, so the adapter cannot have been generated by this skill", "Preserves `sh -c 'ulimit -n'` verbatim as the wrapped command, so the shim never `exec`s a shell builtin with no shell", "Names the provenance test (adapter is emitted only for a renderer carrying shell syntax) rather than peeling any `sh -c` on sight", "Prints exactly one shim invocation and exactly one `sh -c` layer" @@ -119,11 +119,11 @@ { "id": 10, "name": "multiple-generated-layers-collapse-in-one-run", - "prompt": "/context-guard:setup check\n\nMy statusLine command is: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'sh -c '\\''THEME=dark my-statusline'\\''' — two earlier runs of this skill each added a layer. Both the shim and the plugin are installed.", + "prompt": "/context-guard:setup check\n\nMy statusLine command is: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'sh -c '\\''THEME=dark my-statusline'\\''' \u2014 two earlier runs of this skill each added a layer. Both the shim and the plugin are installed.", "expected_output": "Applies the unwrap rules repeatedly rather than once: the first pass strips the shim prefix and recovers sh -c 'THEME=dark my-statusline', the second recovers THEME=dark my-statusline, and a third strips nothing. Re-applies the shell-syntax guard to that renderer and prints wiring carrying exactly one sh -c layer: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'THEME=dark my-statusline'.", "files": [], "expectations": [ - "Peels BOTH generated layers in ONE invocation — stopping after a single peel and treating `sh -c '\\''THEME=dark my-statusline'\\''` as the renderer is a failure", + "Peels BOTH generated layers in ONE invocation \u2014 stopping after a single peel and treating `sh -c '\\''THEME=dark my-statusline'\\''` as the renderer is a failure", "Peels the OUTER layer on the nested-`sh -c` branch of the provenance test, never concluding the layer is operator-written because its carried string has no UNQUOTED top-level shell syntax", "Recovers `THEME=dark my-statusline` byte-for-byte, unescaping `'\\''` back to `'` at each layer", "Prints exactly ONE `sh -c` layer and one context-guard shim invocation", @@ -133,38 +133,38 @@ { "id": 11, "name": "adapter-hiding-a-sibling-shim-is-peeled", - "prompt": "/context-guard:setup check\n\nMy statusLine command is: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh my-renderer --format '\\''a b'\\''' — I wired rate-limit-guard first and ran this skill once, back when it wrapped anything quoted. Both plugins are installed and both shims are present.", - "expected_output": "Peels the generated sh -c even though its carried string has no unquoted top-level shell syntax, because that string BEGINS WITH a guard-shim prefix — a shape this skill never emits inside an adapter. That exposes the rate-limit-guard shim to rule 1, which strips it, leaving the renderer my-renderer --format 'a b'. Prints the combined wiring naming each shim exactly once: bash ~/.claude/context-guard/bin/statusline-shim.sh bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh my-renderer --format 'a b'.", + "prompt": "/context-guard:setup check\n\nMy statusLine command is: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh my-renderer --format '\\''a b'\\''' \u2014 I wired rate-limit-guard first and ran this skill once, back when it wrapped anything quoted. Both plugins are installed and both shims are present.", + "expected_output": "Peels the generated sh -c even though its carried string has no unquoted top-level shell syntax, because that string BEGINS WITH a guard-shim prefix \u2014 a shape this skill never emits inside an adapter. That exposes the rate-limit-guard shim to rule 1, which strips it, leaving the renderer my-renderer --format 'a b'. Prints the combined wiring naming each shim exactly once: bash ~/.claude/context-guard/bin/statusline-shim.sh bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh my-renderer --format 'a b'.", "files": [], "expectations": [ - "Peels the adapter on the shim-prefix branch of the provenance test — treating it as the operator's own renderer because the carried string has no unquoted top-level syntax is a failure", + "Peels the adapter on the shim-prefix branch of the provenance test \u2014 treating it as the operator's own renderer because the carried string has no unquoted top-level syntax is a failure", "Names the rate-limit-guard shim EXACTLY ONCE in the printed wiring, never once from the sealed adapter and again from the composed sibling form", - "Recovers `my-renderer --format 'a b'` byte-for-byte and wraps it plainly — the quoted argument is not itself a reason to re-emit an `sh -c`", + "Recovers `my-renderer --format 'a b'` byte-for-byte and wraps it plainly \u2014 the quoted argument is not itself a reason to re-emit an `sh -c`", "Explains that a duplicated tee runs and writes on every refresh and adds another ~0.6-0.9 s per refresh" ] }, { "id": 12, "name": "bare-builtin-renderer-gets-a-shell", - "prompt": "/context-guard:setup check\n\nI have never run this skill. My statusLine command is: ulimit '-n' — I wrote it that way myself; the quotes are just habit. The plugin is installed.", - "expected_output": "Recognizes that the command word `ulimit` resolves as a shell builtin and not as an executable, so the plain wrapped form would reach `exec ulimit -n` and die with exit 127. Prints the shell-wrapped variant: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'ulimit '''-n''''.", + "prompt": "/context-guard:setup check\n\nI have never run this skill. My statusLine command is: ulimit '-n' \u2014 I wrote it that way myself; the quotes are just habit. The plugin is installed.", + "expected_output": "Recognizes that the command word `ulimit` resolves as a shell builtin and not as an executable, so the plain wrapped form would reach `exec ulimit -n` and die with exit 127. Prints the shell-wrapped variant: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'ulimit '\\''-n'\\'''.", "files": [], "expectations": [ - "Emits the SHELL-WRAPPED variant, not the plain wrapped one — `exec` cannot run a builtin, so the plain form would exit 127 on every refresh", - "Reaches that decision through the not-an-executable trigger, NOT through the shell-syntax trigger — the renderer carries no unquoted top-level shell syntax at all", - "Does not justify the wrap by the presence of quotes — bare quoting is explicitly not a trigger", + "Emits the SHELL-WRAPPED variant, not the plain wrapped one \u2014 `exec` cannot run a builtin, so the plain form would exit 127 on every refresh", + "Reaches that decision through the not-an-executable trigger, NOT through the shell-syntax trigger \u2014 the renderer carries no unquoted top-level shell syntax at all", + "Does not justify the wrap by the presence of quotes \u2014 bare quoting is explicitly not a trigger", "Resolves builtin-ness the way the shim will (`type -P` finding nothing while `type -t` reports `builtin`), never by matching a hardcoded list of builtin names" ] }, { "id": 13, "name": "shell-syntax-sealed-inside-quotes-is-not-a-trigger", - "prompt": "/context-guard:setup check\n\nI have never run this skill. My statusLine command is: my-statusline --format 'a | b' — the pipe is part of the format string, not a pipeline. The plugin is installed.", + "prompt": "/context-guard:setup check\n\nI have never run this skill. My statusLine command is: my-statusline --format 'a | b' \u2014 the pipe is part of the format string, not a pipeline. The plugin is installed.", "expected_output": "Prints the PLAIN wrapped form: bash ~/.claude/context-guard/bin/statusline-shim.sh my-statusline --format 'a | b'. The pipe is sealed inside a quoted argument, so it is one ordinary ARGV word and needs no adapter.", "files": [], "expectations": [ "Emits the PLAIN wrapped form with no `sh -c` adapter", - "Does NOT treat the `|` as a control operator — the guard tests for syntax that is UNQUOTED and at the TOP LEVEL, and this pipe is neither", + "Does NOT treat the `|` as a control operator \u2014 the guard tests for syntax that is UNQUOTED and at the TOP LEVEL, and this pipe is neither", "Reads `my-statusline` as an ordinary executable, so the not-an-executable trigger does not fire either" ] } diff --git a/plugins/rate-limit-guard/skills/setup/evals/evals.json b/plugins/rate-limit-guard/skills/setup/evals/evals.json index fbe23bc4a..ecf0787e9 100644 --- a/plugins/rate-limit-guard/skills/setup/evals/evals.json +++ b/plugins/rate-limit-guard/skills/setup/evals/evals.json @@ -30,7 +30,7 @@ "id": 3, "name": "reads-absent-rate-limits-as-reactive-only", "prompt": "/rate-limit-guard:setup check\n\nThe tee file is fresh but has no rate_limits key.", - "expected_output": "Reports the fresh-but-windowless snapshot as INFO — this auth exposes no subscription windows, consumers correctly run reactive-only — not as a failure to fix.", + "expected_output": "Reports the fresh-but-windowless snapshot as INFO \u2014 this auth exposes no subscription windows, consumers correctly run reactive-only \u2014 not as a failure to fix.", "files": [], "expectations": [ "Classifies absent rate_limits on a fresh snapshot as expected fail-open behavior", @@ -41,7 +41,7 @@ { "id": 4, "name": "flags-legacy-version-pinned-wiring", - "prompt": "/rate-limit-guard:setup check\n\nMy settings.json statusLine command is: bash \"~/.claude/plugins/cache/melodic-software/rate-limit-guard/0.1.0/scripts/statusline-tee.sh\" ~/.claude/statusline/render.sh — and that file exists right now.", + "prompt": "/rate-limit-guard:setup check\n\nMy settings.json statusLine command is: bash \"~/.claude/plugins/cache/melodic-software/rate-limit-guard/0.1.0/scripts/statusline-tee.sh\" ~/.claude/statusline/render.sh \u2014 and that file exists right now.", "expected_output": "Names the wiring as legacy version-pinned even though the file exists today: it stops teeing at the next version bump and breaks the whole statusline once the old version directory is pruned. Prints the shim wiring as the fix, with apply first if the shim is not installed.", "files": [], "expectations": [ @@ -57,7 +57,7 @@ "expected_output": "Copies the shipped statusline-shim.sh byte-identically to ~/.claude/rate-limit-guard/bin/statusline-shim.sh, reports the write (or reports no change on a second identical run), states that the shim is inert until the operator wires it, and prints the wiring edit as the next step.", "files": [], "expectations": [ - "Writes only ~/.claude/rate-limit-guard/bin/statusline-shim.sh — never settings.json, rate-limits.json, stop-events.jsonl, or the sibling context-guard directory", + "Writes only ~/.claude/rate-limit-guard/bin/statusline-shim.sh \u2014 never settings.json, rate-limits.json, stop-events.jsonl, or the sibling context-guard directory", "Installed copy is byte-identical to the shipped source, not a rewritten or templated variant", "Second identical apply reports no content change", "States that installing the shim starts nothing until the settings.json wiring is applied by the operator" @@ -66,14 +66,14 @@ { "id": 6, "name": "composes-without-double-wrapping", - "prompt": "/rate-limit-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/context-guard/bin/statusline-shim.sh ~/.claude/statusline/render.sh — I configured context-guard first. Both plugins are installed, but I have never run /rate-limit-guard:setup apply.", + "prompt": "/rate-limit-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/context-guard/bin/statusline-shim.sh ~/.claude/statusline/render.sh \u2014 I configured context-guard first. Both plugins are installed, but I have never run /rate-limit-guard:setup apply.", "expected_output": "Recognizes the existing sibling-shim wrapper, unwraps it to the operator's own renderer before composing, and prints wiring that runs each tee exactly once. Because this plugin's own shim is not installed yet, it reports that and names apply as the remediation rather than printing wiring naming a file that does not exist.", "files": [], "expectations": [ - "Treats ~/.claude/statusline/render.sh as the wrapped command — never re-wraps the full existing command, so the context-guard shim appears exactly once in the printed wiring", + "Treats ~/.claude/statusline/render.sh as the wrapped command \u2014 never re-wraps the full existing command, so the context-guard shim appears exactly once in the printed wiring", "Never prints a combined form naming a shim path that is not installed; gates the combined wiring on the sibling shim existing, or directs the operator to run that plugin's apply first", "Explains that a duplicated tee runs and writes on every refresh and adds another ~0.6-0.9 s per refresh", - "Still edits nothing — the wiring is printed for the operator to apply" + "Still edits nothing \u2014 the wiring is printed for the operator to apply" ] }, { @@ -85,18 +85,18 @@ "expectations": [ "States the order explicitly and gives the reason: deleting the directory first leaves settings.json invoking a missing file, so bash exits 127 and the whole statusline goes down", "Does not present the two steps as order-independent", - "Notes that uninstalling the plugin itself is safe on its own — the shim degrades to running the wrapped command" + "Notes that uninstalling the plugin itself is safe on its own \u2014 the shim degrades to running the wrapped command" ] }, { "id": 8, "name": "rerun-does-not-compound-the-sh-c-wrap", - "prompt": "/rate-limit-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c 'THEME=dark my-statusline --flag' — I ran this skill once before and applied what it printed. Both the shim and the plugin are installed.", - "expected_output": "Unwraps the shim prefix AND the previously-generated sh -c adapter back to THEME=dark my-statusline --flag, then re-applies the shell-syntax guard to that renderer, printing wiring that carries exactly one sh -c layer — byte-identical to the wiring already in settings.json, so the operator has nothing to apply.", + "prompt": "/rate-limit-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c 'THEME=dark my-statusline --flag' \u2014 I ran this skill once before and applied what it printed. Both the shim and the plugin are installed.", + "expected_output": "Unwraps the shim prefix AND the previously-generated sh -c adapter back to THEME=dark my-statusline --flag, then re-applies the shell-syntax guard to that renderer, printing wiring that carries exactly one sh -c layer \u2014 byte-identical to the wiring already in settings.json, so the operator has nothing to apply.", "files": [], "expectations": [ "Treats the existing `sh -c '...'` as a previously-generated adapter to unwrap, never as the operator's own renderer", - "Prints exactly ONE `sh -c` layer and one rate-limit-guard shim invocation — never a nested `sh -c 'sh -c '\\''...'\\'''`", + "Prints exactly ONE `sh -c` layer and one rate-limit-guard shim invocation \u2014 never a nested `sh -c 'sh -c '\\''...'\\'''`", "Recovers `THEME=dark my-statusline --flag` byte-for-byte, unescaping `'\\''` back to `'`", "Reports the wiring as already correct rather than presenting an identical line as a change to apply" ] @@ -104,11 +104,11 @@ { "id": 9, "name": "genuine-sh-c-renderer-is-not-peeled", - "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: sh -c 'ulimit -n' — I wrote that myself because ulimit is a shell builtin and will not run any other way. The plugin is installed.", + "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: sh -c 'ulimit -n' \u2014 I wrote that myself because ulimit is a shell builtin and will not run any other way. The plugin is installed.", "expected_output": "Treats sh -c 'ulimit -n' as the operator's OWN renderer, not a generated adapter, because the string it carries has no shell syntax and this skill only ever emits the adapter for a renderer that has some. Wraps it intact: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c 'ulimit -n'.", "files": [], "expectations": [ - "Does NOT peel the `sh -c` layer — the carried string `ulimit -n` contains no shell syntax, so the adapter cannot have been generated by this skill", + "Does NOT peel the `sh -c` layer \u2014 the carried string `ulimit -n` contains no shell syntax, so the adapter cannot have been generated by this skill", "Preserves `sh -c 'ulimit -n'` verbatim as the wrapped command, so the shim never `exec`s a shell builtin with no shell", "Names the provenance test (adapter is emitted only for a renderer carrying shell syntax) rather than peeling any `sh -c` on sight", "Prints exactly one shim invocation and exactly one `sh -c` layer" @@ -117,26 +117,26 @@ { "id": 10, "name": "bare-quoting-is-not-a-wrap-trigger", - "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: my-statusline --format 'a | b' — the pipe is part of the format string, not a pipeline. The plugin is installed.", + "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: my-statusline --format 'a | b' \u2014 the pipe is part of the format string, not a pipeline. The plugin is installed.", "expected_output": "Prints the PLAIN wrapped form: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh my-statusline --format 'a | b'. The pipe is sealed inside a quoted argument, so it is one ordinary ARGV word and needs no adapter.", "files": [], "expectations": [ "Emits the PLAIN wrapped form with no `sh -c` adapter", - "Does NOT treat the `|` as a control operator — the guard tests for syntax that is UNQUOTED and at the TOP LEVEL, and this pipe is neither", - "Does not justify a wrap by the presence of quotes — bare quoting is explicitly not a trigger", + "Does NOT treat the `|` as a control operator \u2014 the guard tests for syntax that is UNQUOTED and at the TOP LEVEL, and this pipe is neither", + "Does not justify a wrap by the presence of quotes \u2014 bare quoting is explicitly not a trigger", "Reads `my-statusline` as an ordinary executable, so the not-an-executable trigger does not fire either" ] }, { "id": 11, "name": "bare-builtin-renderer-gets-a-shell", - "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: ulimit '-n' — I wrote it that way myself; the quotes are just habit. The plugin is installed.", - "expected_output": "Recognizes that the command word `ulimit` resolves as a shell builtin and not as an executable, so the plain wrapped form would reach `exec ulimit -n` and die with exit 127. Prints the shell-wrapped variant: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c 'ulimit '''-n''''.", + "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: ulimit '-n' \u2014 I wrote it that way myself; the quotes are just habit. The plugin is installed.", + "expected_output": "Recognizes that the command word `ulimit` resolves as a shell builtin and not as an executable, so the plain wrapped form would reach `exec ulimit -n` and die with exit 127. Prints the shell-wrapped variant: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c 'ulimit '\\''-n'\\'''.", "files": [], "expectations": [ - "Emits the SHELL-WRAPPED variant, not the plain wrapped one — `exec` cannot run a builtin, so the plain form would exit 127 on every refresh", - "Reaches that decision through the not-an-executable trigger, NOT through the shell-syntax trigger — the renderer carries no unquoted top-level shell syntax at all", - "Does not justify the wrap by the presence of quotes — bare quoting is explicitly not a trigger", + "Emits the SHELL-WRAPPED variant, not the plain wrapped one \u2014 `exec` cannot run a builtin, so the plain form would exit 127 on every refresh", + "Reaches that decision through the not-an-executable trigger, NOT through the shell-syntax trigger \u2014 the renderer carries no unquoted top-level shell syntax at all", + "Does not justify the wrap by the presence of quotes \u2014 bare quoting is explicitly not a trigger", "Resolves builtin-ness the way the shim will (`type -P` finding nothing while `type -t` reports `builtin`), never by matching a hardcoded list of builtin names" ] } From ee307a8b73e57b11e687a6f1c11063e9dd95bbf0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Sep 2026 13:37:46 +0000 Subject: [PATCH 3/3] fix: keep eval JSON bytes and only correct the builtin escape Restore the eval files and change only the ulimit '-n' expected sh -c argument to the POSIX '\'' form. Co-authored-by: ksextonmelodic --- .../skills/setup/evals/evals.json | 52 +++++++++---------- .../skills/setup/evals/evals.json | 38 +++++++------- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/plugins/context-guard/skills/setup/evals/evals.json b/plugins/context-guard/skills/setup/evals/evals.json index 21c090f77..44e16d881 100644 --- a/plugins/context-guard/skills/setup/evals/evals.json +++ b/plugins/context-guard/skills/setup/evals/evals.json @@ -17,7 +17,7 @@ { "id": 2, "name": "flags-legacy-version-pinned-wiring", - "prompt": "/context-guard:setup check\n\nMy settings.json statusLine command is: bash \"~/.claude/plugins/cache/melodic-software/context-guard/0.1.0/scripts/statusline-tee.sh\" ~/.claude/statusline/render.sh \u2014 and that file exists right now.", + "prompt": "/context-guard:setup check\n\nMy settings.json statusLine command is: bash \"~/.claude/plugins/cache/melodic-software/context-guard/0.1.0/scripts/statusline-tee.sh\" ~/.claude/statusline/render.sh — and that file exists right now.", "expected_output": "Names the wiring as legacy version-pinned even though the file exists today: it stops teeing at the next version bump and breaks the whole statusline once the old version directory is pruned. Prints the shim wiring as the fix, with apply first if the shim is not installed.", "files": [], "expectations": [ @@ -31,7 +31,7 @@ "id": 3, "name": "reads-null-fields-as-expected-state", "prompt": "/context-guard:setup check\n\nThe live session's snapshot is fresh but used_percentage is null.", - "expected_output": "Reports the fresh-but-null snapshot as INFO \u2014 a documented early-session or post-/compact statusline state in which the resolver correctly answers unknown \u2014 not as a failure to fix.", + "expected_output": "Reports the fresh-but-null snapshot as INFO — a documented early-session or post-/compact statusline state in which the resolver correctly answers unknown — not as a failure to fix.", "files": [], "expectations": [ "Classifies null used_percentage / current_usage on a fresh snapshot as documented fail-open behavior", @@ -43,10 +43,10 @@ "id": 4, "name": "apply-preserves-unrecognized-keys-and-custom-bands", "prompt": "/context-guard:setup apply\n\nMy zones.json already exists with custom valid bands (40/70) and carries a custom display_color key my statusline reads.", - "expected_output": "Bare apply is repair-only: leaves the valid custom bands (40/70) untouched and reports them, preserves the unrecognized display_color key and its value (semantic preservation \u2014 lexical formatting may normalize), names 'apply defaults' as the explicit route to the shipped defaults, and confirms a second identical apply would be a no-op.", + "expected_output": "Bare apply is repair-only: leaves the valid custom bands (40/70) untouched and reports them, preserves the unrecognized display_color key and its value (semantic preservation — lexical formatting may normalize), names 'apply defaults' as the explicit route to the shipped defaults, and confirms a second identical apply would be a no-op.", "files": [], "expectations": [ - "Writes only inside ~/.claude/context-guard/ \u2014 zones.json plus the shim \u2014 and nothing else (a fully valid zones file may mean no zones write at all)", + "Writes only inside ~/.claude/context-guard/ — zones.json plus the shim — and nothing else (a fully valid zones file may mean no zones write at all)", "Does NOT overwrite valid custom band values on a bare apply; names apply defaults as the explicit shipped-defaults mode", "Preserves unrecognized keys rather than rewriting the file from the template", "States idempotency (second run = no content change)" @@ -59,7 +59,7 @@ "expected_output": "Copies the shipped statusline-shim.sh byte-identically to ~/.claude/context-guard/bin/statusline-shim.sh alongside the zones.json seed/repair, reports each write (or reports no change on a second identical run), states that the shim is inert until the operator wires it, and prints the wiring edit as the next step.", "files": [], "expectations": [ - "Writes only inside ~/.claude/context-guard/ \u2014 never settings.json, the snapshot directory, or the sibling rate-limit-guard directory", + "Writes only inside ~/.claude/context-guard/ — never settings.json, the snapshot directory, or the sibling rate-limit-guard directory", "Installed shim is byte-identical to the shipped source, not a rewritten or templated variant", "Second identical apply reports no content change", "States that installing the shim starts nothing until the settings.json wiring is applied by the operator" @@ -68,14 +68,14 @@ { "id": 6, "name": "composes-without-double-wrapping", - "prompt": "/context-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh ~/.claude/statusline/render.sh \u2014 I configured rate-limit-guard first. Both plugins are installed, but I have never run /context-guard:setup apply.", + "prompt": "/context-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh ~/.claude/statusline/render.sh — I configured rate-limit-guard first. Both plugins are installed, but I have never run /context-guard:setup apply.", "expected_output": "Recognizes the existing sibling-shim wrapper, unwraps it to the operator's own renderer before composing, and prints wiring that runs each tee exactly once. Because this plugin's own shim is not installed yet, it reports that and names apply as the remediation rather than printing wiring naming a file that does not exist.", "files": [], "expectations": [ - "Treats ~/.claude/statusline/render.sh as the wrapped command \u2014 never re-wraps the full existing command, so the rate-limit-guard shim appears exactly once in the printed wiring", + "Treats ~/.claude/statusline/render.sh as the wrapped command — never re-wraps the full existing command, so the rate-limit-guard shim appears exactly once in the printed wiring", "Never prints a combined form naming a shim path that is not installed; gates the combined wiring on the sibling shim existing, or directs the operator to run that plugin's apply first", "Explains that a duplicated tee runs and writes on every refresh and adds another ~0.6-0.9 s per refresh", - "Still edits nothing \u2014 the wiring is printed for the operator to apply" + "Still edits nothing — the wiring is printed for the operator to apply" ] }, { @@ -87,18 +87,18 @@ "expectations": [ "States the order explicitly and gives the reason: deleting the directory first leaves settings.json invoking a missing file, so bash exits 127 and the whole statusline goes down", "Does not present the two steps as order-independent", - "Notes that uninstalling the plugin itself is safe on its own \u2014 the shim degrades to running the wrapped command" + "Notes that uninstalling the plugin itself is safe on its own — the shim degrades to running the wrapped command" ] }, { "id": 8, "name": "rerun-does-not-compound-the-sh-c-wrap", - "prompt": "/context-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'THEME=dark my-statusline --flag' \u2014 I ran this skill once before and applied what it printed. Both the shim and the plugin are installed.", - "expected_output": "Unwraps the shim prefix AND the previously-generated sh -c adapter back to THEME=dark my-statusline --flag, then re-applies the shell-syntax guard to that renderer, printing wiring that carries exactly one sh -c layer \u2014 byte-identical to the wiring already in settings.json, so the operator has nothing to apply.", + "prompt": "/context-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'THEME=dark my-statusline --flag' — I ran this skill once before and applied what it printed. Both the shim and the plugin are installed.", + "expected_output": "Unwraps the shim prefix AND the previously-generated sh -c adapter back to THEME=dark my-statusline --flag, then re-applies the shell-syntax guard to that renderer, printing wiring that carries exactly one sh -c layer — byte-identical to the wiring already in settings.json, so the operator has nothing to apply.", "files": [], "expectations": [ "Treats the existing `sh -c '...'` as a previously-generated adapter to unwrap, never as the operator's own renderer", - "Prints exactly ONE `sh -c` layer and one context-guard shim invocation \u2014 never a nested `sh -c 'sh -c '\\''...'\\'''`", + "Prints exactly ONE `sh -c` layer and one context-guard shim invocation — never a nested `sh -c 'sh -c '\\''...'\\'''`", "Recovers `THEME=dark my-statusline --flag` byte-for-byte, unescaping `'\\''` back to `'`", "Reports the wiring as already correct rather than presenting an identical line as a change to apply" ] @@ -106,11 +106,11 @@ { "id": 9, "name": "genuine-sh-c-renderer-is-not-peeled", - "prompt": "/context-guard:setup check\n\nI have never run this skill. My statusLine command is: sh -c 'ulimit -n' \u2014 I wrote that myself because ulimit is a shell builtin and will not run any other way. The plugin is installed.", + "prompt": "/context-guard:setup check\n\nI have never run this skill. My statusLine command is: sh -c 'ulimit -n' — I wrote that myself because ulimit is a shell builtin and will not run any other way. The plugin is installed.", "expected_output": "Treats sh -c 'ulimit -n' as the operator's OWN renderer, not a generated adapter, because the string it carries has no shell syntax and this skill only ever emits the adapter for a renderer that has some. Wraps it intact: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'ulimit -n'.", "files": [], "expectations": [ - "Does NOT peel the `sh -c` layer \u2014 the carried string `ulimit -n` contains no shell syntax, so the adapter cannot have been generated by this skill", + "Does NOT peel the `sh -c` layer — the carried string `ulimit -n` contains no shell syntax, so the adapter cannot have been generated by this skill", "Preserves `sh -c 'ulimit -n'` verbatim as the wrapped command, so the shim never `exec`s a shell builtin with no shell", "Names the provenance test (adapter is emitted only for a renderer carrying shell syntax) rather than peeling any `sh -c` on sight", "Prints exactly one shim invocation and exactly one `sh -c` layer" @@ -119,11 +119,11 @@ { "id": 10, "name": "multiple-generated-layers-collapse-in-one-run", - "prompt": "/context-guard:setup check\n\nMy statusLine command is: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'sh -c '\\''THEME=dark my-statusline'\\''' \u2014 two earlier runs of this skill each added a layer. Both the shim and the plugin are installed.", + "prompt": "/context-guard:setup check\n\nMy statusLine command is: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'sh -c '\\''THEME=dark my-statusline'\\''' — two earlier runs of this skill each added a layer. Both the shim and the plugin are installed.", "expected_output": "Applies the unwrap rules repeatedly rather than once: the first pass strips the shim prefix and recovers sh -c 'THEME=dark my-statusline', the second recovers THEME=dark my-statusline, and a third strips nothing. Re-applies the shell-syntax guard to that renderer and prints wiring carrying exactly one sh -c layer: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'THEME=dark my-statusline'.", "files": [], "expectations": [ - "Peels BOTH generated layers in ONE invocation \u2014 stopping after a single peel and treating `sh -c '\\''THEME=dark my-statusline'\\''` as the renderer is a failure", + "Peels BOTH generated layers in ONE invocation — stopping after a single peel and treating `sh -c '\\''THEME=dark my-statusline'\\''` as the renderer is a failure", "Peels the OUTER layer on the nested-`sh -c` branch of the provenance test, never concluding the layer is operator-written because its carried string has no UNQUOTED top-level shell syntax", "Recovers `THEME=dark my-statusline` byte-for-byte, unescaping `'\\''` back to `'` at each layer", "Prints exactly ONE `sh -c` layer and one context-guard shim invocation", @@ -133,38 +133,38 @@ { "id": 11, "name": "adapter-hiding-a-sibling-shim-is-peeled", - "prompt": "/context-guard:setup check\n\nMy statusLine command is: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh my-renderer --format '\\''a b'\\''' \u2014 I wired rate-limit-guard first and ran this skill once, back when it wrapped anything quoted. Both plugins are installed and both shims are present.", - "expected_output": "Peels the generated sh -c even though its carried string has no unquoted top-level shell syntax, because that string BEGINS WITH a guard-shim prefix \u2014 a shape this skill never emits inside an adapter. That exposes the rate-limit-guard shim to rule 1, which strips it, leaving the renderer my-renderer --format 'a b'. Prints the combined wiring naming each shim exactly once: bash ~/.claude/context-guard/bin/statusline-shim.sh bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh my-renderer --format 'a b'.", + "prompt": "/context-guard:setup check\n\nMy statusLine command is: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh my-renderer --format '\\''a b'\\''' — I wired rate-limit-guard first and ran this skill once, back when it wrapped anything quoted. Both plugins are installed and both shims are present.", + "expected_output": "Peels the generated sh -c even though its carried string has no unquoted top-level shell syntax, because that string BEGINS WITH a guard-shim prefix — a shape this skill never emits inside an adapter. That exposes the rate-limit-guard shim to rule 1, which strips it, leaving the renderer my-renderer --format 'a b'. Prints the combined wiring naming each shim exactly once: bash ~/.claude/context-guard/bin/statusline-shim.sh bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh my-renderer --format 'a b'.", "files": [], "expectations": [ - "Peels the adapter on the shim-prefix branch of the provenance test \u2014 treating it as the operator's own renderer because the carried string has no unquoted top-level syntax is a failure", + "Peels the adapter on the shim-prefix branch of the provenance test — treating it as the operator's own renderer because the carried string has no unquoted top-level syntax is a failure", "Names the rate-limit-guard shim EXACTLY ONCE in the printed wiring, never once from the sealed adapter and again from the composed sibling form", - "Recovers `my-renderer --format 'a b'` byte-for-byte and wraps it plainly \u2014 the quoted argument is not itself a reason to re-emit an `sh -c`", + "Recovers `my-renderer --format 'a b'` byte-for-byte and wraps it plainly — the quoted argument is not itself a reason to re-emit an `sh -c`", "Explains that a duplicated tee runs and writes on every refresh and adds another ~0.6-0.9 s per refresh" ] }, { "id": 12, "name": "bare-builtin-renderer-gets-a-shell", - "prompt": "/context-guard:setup check\n\nI have never run this skill. My statusLine command is: ulimit '-n' \u2014 I wrote it that way myself; the quotes are just habit. The plugin is installed.", + "prompt": "/context-guard:setup check\n\nI have never run this skill. My statusLine command is: ulimit '-n' — I wrote it that way myself; the quotes are just habit. The plugin is installed.", "expected_output": "Recognizes that the command word `ulimit` resolves as a shell builtin and not as an executable, so the plain wrapped form would reach `exec ulimit -n` and die with exit 127. Prints the shell-wrapped variant: bash ~/.claude/context-guard/bin/statusline-shim.sh sh -c 'ulimit '\\''-n'\\'''.", "files": [], "expectations": [ - "Emits the SHELL-WRAPPED variant, not the plain wrapped one \u2014 `exec` cannot run a builtin, so the plain form would exit 127 on every refresh", - "Reaches that decision through the not-an-executable trigger, NOT through the shell-syntax trigger \u2014 the renderer carries no unquoted top-level shell syntax at all", - "Does not justify the wrap by the presence of quotes \u2014 bare quoting is explicitly not a trigger", + "Emits the SHELL-WRAPPED variant, not the plain wrapped one — `exec` cannot run a builtin, so the plain form would exit 127 on every refresh", + "Reaches that decision through the not-an-executable trigger, NOT through the shell-syntax trigger — the renderer carries no unquoted top-level shell syntax at all", + "Does not justify the wrap by the presence of quotes — bare quoting is explicitly not a trigger", "Resolves builtin-ness the way the shim will (`type -P` finding nothing while `type -t` reports `builtin`), never by matching a hardcoded list of builtin names" ] }, { "id": 13, "name": "shell-syntax-sealed-inside-quotes-is-not-a-trigger", - "prompt": "/context-guard:setup check\n\nI have never run this skill. My statusLine command is: my-statusline --format 'a | b' \u2014 the pipe is part of the format string, not a pipeline. The plugin is installed.", + "prompt": "/context-guard:setup check\n\nI have never run this skill. My statusLine command is: my-statusline --format 'a | b' — the pipe is part of the format string, not a pipeline. The plugin is installed.", "expected_output": "Prints the PLAIN wrapped form: bash ~/.claude/context-guard/bin/statusline-shim.sh my-statusline --format 'a | b'. The pipe is sealed inside a quoted argument, so it is one ordinary ARGV word and needs no adapter.", "files": [], "expectations": [ "Emits the PLAIN wrapped form with no `sh -c` adapter", - "Does NOT treat the `|` as a control operator \u2014 the guard tests for syntax that is UNQUOTED and at the TOP LEVEL, and this pipe is neither", + "Does NOT treat the `|` as a control operator — the guard tests for syntax that is UNQUOTED and at the TOP LEVEL, and this pipe is neither", "Reads `my-statusline` as an ordinary executable, so the not-an-executable trigger does not fire either" ] } diff --git a/plugins/rate-limit-guard/skills/setup/evals/evals.json b/plugins/rate-limit-guard/skills/setup/evals/evals.json index ecf0787e9..06f431628 100644 --- a/plugins/rate-limit-guard/skills/setup/evals/evals.json +++ b/plugins/rate-limit-guard/skills/setup/evals/evals.json @@ -30,7 +30,7 @@ "id": 3, "name": "reads-absent-rate-limits-as-reactive-only", "prompt": "/rate-limit-guard:setup check\n\nThe tee file is fresh but has no rate_limits key.", - "expected_output": "Reports the fresh-but-windowless snapshot as INFO \u2014 this auth exposes no subscription windows, consumers correctly run reactive-only \u2014 not as a failure to fix.", + "expected_output": "Reports the fresh-but-windowless snapshot as INFO — this auth exposes no subscription windows, consumers correctly run reactive-only — not as a failure to fix.", "files": [], "expectations": [ "Classifies absent rate_limits on a fresh snapshot as expected fail-open behavior", @@ -41,7 +41,7 @@ { "id": 4, "name": "flags-legacy-version-pinned-wiring", - "prompt": "/rate-limit-guard:setup check\n\nMy settings.json statusLine command is: bash \"~/.claude/plugins/cache/melodic-software/rate-limit-guard/0.1.0/scripts/statusline-tee.sh\" ~/.claude/statusline/render.sh \u2014 and that file exists right now.", + "prompt": "/rate-limit-guard:setup check\n\nMy settings.json statusLine command is: bash \"~/.claude/plugins/cache/melodic-software/rate-limit-guard/0.1.0/scripts/statusline-tee.sh\" ~/.claude/statusline/render.sh — and that file exists right now.", "expected_output": "Names the wiring as legacy version-pinned even though the file exists today: it stops teeing at the next version bump and breaks the whole statusline once the old version directory is pruned. Prints the shim wiring as the fix, with apply first if the shim is not installed.", "files": [], "expectations": [ @@ -57,7 +57,7 @@ "expected_output": "Copies the shipped statusline-shim.sh byte-identically to ~/.claude/rate-limit-guard/bin/statusline-shim.sh, reports the write (or reports no change on a second identical run), states that the shim is inert until the operator wires it, and prints the wiring edit as the next step.", "files": [], "expectations": [ - "Writes only ~/.claude/rate-limit-guard/bin/statusline-shim.sh \u2014 never settings.json, rate-limits.json, stop-events.jsonl, or the sibling context-guard directory", + "Writes only ~/.claude/rate-limit-guard/bin/statusline-shim.sh — never settings.json, rate-limits.json, stop-events.jsonl, or the sibling context-guard directory", "Installed copy is byte-identical to the shipped source, not a rewritten or templated variant", "Second identical apply reports no content change", "States that installing the shim starts nothing until the settings.json wiring is applied by the operator" @@ -66,14 +66,14 @@ { "id": 6, "name": "composes-without-double-wrapping", - "prompt": "/rate-limit-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/context-guard/bin/statusline-shim.sh ~/.claude/statusline/render.sh \u2014 I configured context-guard first. Both plugins are installed, but I have never run /rate-limit-guard:setup apply.", + "prompt": "/rate-limit-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/context-guard/bin/statusline-shim.sh ~/.claude/statusline/render.sh — I configured context-guard first. Both plugins are installed, but I have never run /rate-limit-guard:setup apply.", "expected_output": "Recognizes the existing sibling-shim wrapper, unwraps it to the operator's own renderer before composing, and prints wiring that runs each tee exactly once. Because this plugin's own shim is not installed yet, it reports that and names apply as the remediation rather than printing wiring naming a file that does not exist.", "files": [], "expectations": [ - "Treats ~/.claude/statusline/render.sh as the wrapped command \u2014 never re-wraps the full existing command, so the context-guard shim appears exactly once in the printed wiring", + "Treats ~/.claude/statusline/render.sh as the wrapped command — never re-wraps the full existing command, so the context-guard shim appears exactly once in the printed wiring", "Never prints a combined form naming a shim path that is not installed; gates the combined wiring on the sibling shim existing, or directs the operator to run that plugin's apply first", "Explains that a duplicated tee runs and writes on every refresh and adds another ~0.6-0.9 s per refresh", - "Still edits nothing \u2014 the wiring is printed for the operator to apply" + "Still edits nothing — the wiring is printed for the operator to apply" ] }, { @@ -85,18 +85,18 @@ "expectations": [ "States the order explicitly and gives the reason: deleting the directory first leaves settings.json invoking a missing file, so bash exits 127 and the whole statusline goes down", "Does not present the two steps as order-independent", - "Notes that uninstalling the plugin itself is safe on its own \u2014 the shim degrades to running the wrapped command" + "Notes that uninstalling the plugin itself is safe on its own — the shim degrades to running the wrapped command" ] }, { "id": 8, "name": "rerun-does-not-compound-the-sh-c-wrap", - "prompt": "/rate-limit-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c 'THEME=dark my-statusline --flag' \u2014 I ran this skill once before and applied what it printed. Both the shim and the plugin are installed.", - "expected_output": "Unwraps the shim prefix AND the previously-generated sh -c adapter back to THEME=dark my-statusline --flag, then re-applies the shell-syntax guard to that renderer, printing wiring that carries exactly one sh -c layer \u2014 byte-identical to the wiring already in settings.json, so the operator has nothing to apply.", + "prompt": "/rate-limit-guard:setup check\n\nMy statusLine command is already: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c 'THEME=dark my-statusline --flag' — I ran this skill once before and applied what it printed. Both the shim and the plugin are installed.", + "expected_output": "Unwraps the shim prefix AND the previously-generated sh -c adapter back to THEME=dark my-statusline --flag, then re-applies the shell-syntax guard to that renderer, printing wiring that carries exactly one sh -c layer — byte-identical to the wiring already in settings.json, so the operator has nothing to apply.", "files": [], "expectations": [ "Treats the existing `sh -c '...'` as a previously-generated adapter to unwrap, never as the operator's own renderer", - "Prints exactly ONE `sh -c` layer and one rate-limit-guard shim invocation \u2014 never a nested `sh -c 'sh -c '\\''...'\\'''`", + "Prints exactly ONE `sh -c` layer and one rate-limit-guard shim invocation — never a nested `sh -c 'sh -c '\\''...'\\'''`", "Recovers `THEME=dark my-statusline --flag` byte-for-byte, unescaping `'\\''` back to `'`", "Reports the wiring as already correct rather than presenting an identical line as a change to apply" ] @@ -104,11 +104,11 @@ { "id": 9, "name": "genuine-sh-c-renderer-is-not-peeled", - "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: sh -c 'ulimit -n' \u2014 I wrote that myself because ulimit is a shell builtin and will not run any other way. The plugin is installed.", + "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: sh -c 'ulimit -n' — I wrote that myself because ulimit is a shell builtin and will not run any other way. The plugin is installed.", "expected_output": "Treats sh -c 'ulimit -n' as the operator's OWN renderer, not a generated adapter, because the string it carries has no shell syntax and this skill only ever emits the adapter for a renderer that has some. Wraps it intact: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c 'ulimit -n'.", "files": [], "expectations": [ - "Does NOT peel the `sh -c` layer \u2014 the carried string `ulimit -n` contains no shell syntax, so the adapter cannot have been generated by this skill", + "Does NOT peel the `sh -c` layer — the carried string `ulimit -n` contains no shell syntax, so the adapter cannot have been generated by this skill", "Preserves `sh -c 'ulimit -n'` verbatim as the wrapped command, so the shim never `exec`s a shell builtin with no shell", "Names the provenance test (adapter is emitted only for a renderer carrying shell syntax) rather than peeling any `sh -c` on sight", "Prints exactly one shim invocation and exactly one `sh -c` layer" @@ -117,26 +117,26 @@ { "id": 10, "name": "bare-quoting-is-not-a-wrap-trigger", - "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: my-statusline --format 'a | b' \u2014 the pipe is part of the format string, not a pipeline. The plugin is installed.", + "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: my-statusline --format 'a | b' — the pipe is part of the format string, not a pipeline. The plugin is installed.", "expected_output": "Prints the PLAIN wrapped form: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh my-statusline --format 'a | b'. The pipe is sealed inside a quoted argument, so it is one ordinary ARGV word and needs no adapter.", "files": [], "expectations": [ "Emits the PLAIN wrapped form with no `sh -c` adapter", - "Does NOT treat the `|` as a control operator \u2014 the guard tests for syntax that is UNQUOTED and at the TOP LEVEL, and this pipe is neither", - "Does not justify a wrap by the presence of quotes \u2014 bare quoting is explicitly not a trigger", + "Does NOT treat the `|` as a control operator — the guard tests for syntax that is UNQUOTED and at the TOP LEVEL, and this pipe is neither", + "Does not justify a wrap by the presence of quotes — bare quoting is explicitly not a trigger", "Reads `my-statusline` as an ordinary executable, so the not-an-executable trigger does not fire either" ] }, { "id": 11, "name": "bare-builtin-renderer-gets-a-shell", - "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: ulimit '-n' \u2014 I wrote it that way myself; the quotes are just habit. The plugin is installed.", + "prompt": "/rate-limit-guard:setup check\n\nI have never run this skill. My statusLine command is: ulimit '-n' — I wrote it that way myself; the quotes are just habit. The plugin is installed.", "expected_output": "Recognizes that the command word `ulimit` resolves as a shell builtin and not as an executable, so the plain wrapped form would reach `exec ulimit -n` and die with exit 127. Prints the shell-wrapped variant: bash ~/.claude/rate-limit-guard/bin/statusline-shim.sh sh -c 'ulimit '\\''-n'\\'''.", "files": [], "expectations": [ - "Emits the SHELL-WRAPPED variant, not the plain wrapped one \u2014 `exec` cannot run a builtin, so the plain form would exit 127 on every refresh", - "Reaches that decision through the not-an-executable trigger, NOT through the shell-syntax trigger \u2014 the renderer carries no unquoted top-level shell syntax at all", - "Does not justify the wrap by the presence of quotes \u2014 bare quoting is explicitly not a trigger", + "Emits the SHELL-WRAPPED variant, not the plain wrapped one — `exec` cannot run a builtin, so the plain form would exit 127 on every refresh", + "Reaches that decision through the not-an-executable trigger, NOT through the shell-syntax trigger — the renderer carries no unquoted top-level shell syntax at all", + "Does not justify the wrap by the presence of quotes — bare quoting is explicitly not a trigger", "Resolves builtin-ness the way the shim will (`type -P` finding nothing while `type -t` reports `builtin`), never by matching a hardcoded list of builtin names" ] }