perf(skill-quality): measure the listing budget in one awk pass, byte-identical and ~100x faster - #2322
Conversation
…-identical and ~100x faster `bash check-listing-budget.sh plugins/*/skills` took 289s on Windows (Git Bash) and, run in the foreground by an agent, was killed at 180s with exit 143 and zero output. Re-measured at this branch's merge base before the port: 232s. That command is verbatim what #2023's procedure and .github/recurring-schedule.json's listing-budget-watch row instruct an operator to run each cycle, so on the one machine where the routine is actually driven, a report-only drift watch silently produced nothing. The cause is process-spawn cost, not the machine: the per-file loop spent at least eleven forked subshells and five external process execs (4x awk, 1x tr) on every one of ~200 SKILL.md files — on the order of 2,000 spawns. Process creation costs roughly two orders of magnitude more on Windows than on Linux, which is why CI (ubuntu-24.04) never surfaced it. The same run now takes 2.2s, and the output is byte-identical. Proved by diffing the PER-FILE contribution rows of both implementations over the same tree, not just their reports: 144/144 rows identical, aggregate 94,468 identical, top-10 ordering identical. Per-file rows are the load-bearing comparison — two files with offsetting extraction errors produce a matching aggregate and a clean report diff while the parser is broken. This is a port, not a rewrite. The awk program reimplements, behaviour for behaviour, the four helpers the loop shelled out to: extract, field (block-scalar unfolding for | and >, quote-aware trailing-comment strip including the doubled-single-quote case), strip_quotes (one outer layer, double OR single), and normalize_bool/trim_ws. Two behaviours the old pipeline got free from command substitution are reproduced explicitly and commented: trailing newlines stripped from the extracted frontmatter, and from each field's value. Corpus equivalence is not parser equivalence, so four fixtures pin the shapes most likely to diverge, including a multibyte description asserted as an EQUIVALENCE against the shell's own ${#var} rather than a hardcoded count — awk length() and ${#var} can disagree where awk counts bytes and the shell counts characters. A fifth bounds wall clock over a 200-file corpus at 30s: the pre-port script takes 194s on that corpus, the single pass 1s, so the bound discriminates while staying far too loose to flake in required CI. The file list reaches awk on stdin rather than as operands, because awk reads an operand containing `=` as a variable assignment and would swallow such a path. skill-frontmatter.sh is no longer sourced here; the library is unchanged and check-skill.sh remains its consumer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Claude finished @kyle-sexton's task in 2m 0s —— View job Security review complete
No security issues found. This PR is a behavior-preserving performance refactor of a local, report-only CLI script (bash loop → single What I checked specifically
One non-security robustness note (not reported as a finding per the review scope): the new tab/newline-delimited transport between bash and awk ( |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a8335a11f1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude finished @kyle-sexton's task in 6m 18s —— View job Review complete
Environment note: repo CI/test execution (the sandbox in this review lane) requires interactive approval for running shell scripts, git init, etc., and none was available here — so I could not execute What I checkedI traced the new single-pass
I didn't find a functional/correctness bug in the port itself. Two inline nits, both cosmetic/low severity:
Also confirmed:
Nothing here blocks merging; the two nits above are optional polish. |
|
Claude has reviewed this PR 1 time. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
…strophe-less comment (#2348) ## Summary Follow-up to #2322, which merged before its two advisory review findings were addressed. Both are on `plugins/skill-quality/scripts/check-listing-budget.sh` and both are live on `main` in 0.15.3. They are nits, not correctness bugs — but this is the file whose entire point is byte-identical output, so neither was taken on faith and the identity was re-proved rather than assumed. ### 1. `:400` — a redundant `strip_trailing_nl` `normalize_bool` opens with `trim_ws`, whose trailing `sub` uses `[[:space:]]` — a class that matches a newline. So the strip removed a strict subset of what the very next call removed, on every scanned file's `disable-model-invocation` field. Verified rather than reasoned: ``` $ awk 'BEGIN{ v = " True\n\n" a = v; sub(/^[[:space:]]+/,"",a); sub(/[[:space:]]+$/,"",a) b = v; sub(/\n+$/,"",b); sub(/^[[:space:]]+/,"",b); sub(/[[:space:]]+$/,"",b) printf "trim_ws only -> [%s] len=%d\n", a, length(a) printf "strip_trailing_nl+trim -> [%s] len=%d\n", b, length(b) printf "identical: %s\n", (a==b ? "YES" : "NO") t = "x\n"; sub(/[[:space:]]+$/,"",t) printf "[[:space:]] strips a bare newline: %s\n", (t=="x" ? "YES" : "NO") }' trim_ws only -> [True] len=4 strip_trailing_nl+trim -> [True] len=4 identical: YES [[:space:]] strips a bare newline: YES ``` **The reviewer's own scoping is honoured: it is NOT removed at `:401–402`.** There it runs before `strip_quotes`, which trims nothing — a value still ending in a newline has that newline as its last character, so the closing quote never matches and the quote marks would survive into the measured length. Confirmed the same way (`substr(s, length(s), 1)` on `"abc"\n` is the newline, not `"`). That asymmetry now carries a comment at the site, because otherwise it reads as an oversight worth "fixing" — which would silently inflate every quoted description by two characters. ### 2. `:407` — an apostrophe dropped to survive the awk quoting `"check 2 own FAIL"` reads as a typo. **Rephrased** to `"the FAIL check 2 already raises"` rather than escaped: one apostrophe does not justify a `'"'"'` sequence inside an awk program, which is a readability cost with no payoff. ### The byte-identity re-proof, against a fresh baseline The original proof's baseline was captured at `a0abaf81`. `main` has since **gained a skill** (`plugins/claude-ops/skills/inventory`, absent at `a0abaf81`) and edited two descriptions (`discovery/research` 666→900, `docs-hygiene/audit-encapsulation` 350→408), so re-using it would have measured tree drift and reported a false difference. Instead the **last pre-port revision of the script** (`def5f67b`, the parent of the port merge) was run over **today's tree**, with both implementations instrumented to dump their per-file contribution rows: ``` PRE-PORT (def5f67) : rc=0 324s CURRENT (0.15.4) : rc=0 2s --- per-file contribution rows --- IDENTICAL (145 rows) --- report --- IDENTICAL Shared listing-budget estimate over 145 listing-eligible skill(s) across 65 root(s): aggregate: 95735 chars budget: 8000 chars (documented default (SLASH_COMMAND_TOOL_CHAR_BUDGET fallback)) ``` Per-file rows are the load-bearing comparison, not the report: two files with offsetting extraction errors produce a matching aggregate and a clean report diff while the parser is broken. `skill-quality` 0.15.3 → **0.15.4** (patch — output is unchanged). The shipped 0.15.3 entry keeps the wording it shipped with; the CHANGELOG diff is **27 insertions, 0 deletions**. ## Test plan ``` $ bash plugins/skill-quality/scripts/check-listing-budget.test.sh ... ok - a trailing YAML comment is excluded from the measured description ok - a plain scalar drops its trailing comment but keeps a non-comment # ok - a # inside a block scalar is content and is still measured ok - a literal block scalar joins with newlines and drops the trailing blank ok - a doubled single quote is content; the trailing comment is still cut ok - multibyte description measures the same as the shell's own ${#var} (5 chars) ok - 200-file corpus measured in 2s (bound: 30s) all assertions passed $ shellcheck -x plugins/skill-quality/scripts/check-listing-budget.sh (no output) $ git diff --numstat origin/main -- plugins/skill-quality/CHANGELOG.md 27 0 plugins/skill-quality/CHANGELOG.md # pure insertion: 0.15.3's entry untouched ``` Comment-only and expression-only changes to one awk program; no flag, no option, no output format, and no new read or write. No security review note applies. ## Related - No linked issue — these are the two advisory review findings from #2322, which merged before they were addressed; they are nits with no separate tracked issue. - Follow-up to #2322 (merged), which closed #2216. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Summary
bash plugins/skill-quality/scripts/check-listing-budget.sh plugins/*/skills— verbatim what#2023's procedure and
.github/recurring-schedule.json'slisting-budget-watchrow tell anoperator to run each cycle — took 289s on Windows (Git Bash) and, run in the foreground by an
agent, was killed at 180s with exit 143 and zero output. On the one machine where that quarterly
routine is actually driven, a report-only drift watch silently produced nothing.
The cause is process-spawn cost, not the machine. The per-file loop spent at least eleven forked
subshells and five external process execs (4×
awk, 1×tr) on every one of the repo's ~200SKILL.mdfiles — on the order of 2,000 spawns. Process creation costs roughly two orders ofmagnitude more on Windows than on Linux, which is why CI (
ubuntu-24.04) never surfaced it.The measurement is now one
awkpass. Same command, same tree: 232s → 2.2s, outputbyte-identical.
The byte-identical claim, and how it was actually checked
Diffing the two reports is not sufficient — two files with offsetting extraction errors produce a
matching aggregate, a matching entry count, and a matching top-10, so the report diff comes back
clean while the parser is broken. Both implementations were therefore instrumented to dump their
CONTRIB_FILE(the per-fileentry_len ⇥ skill ⇥ rootrows) and those were diffed first:144/144 per-file rows identical, aggregate 94,468 identical, top-10 ordering identical. Emission
order into the contribution file is preserved by construction (roots in argv order, glob order per
root), so
sort -t $'\t' -k1,1nr's tie-breaking cannot shift.A port, not a rewrite
The awk program reimplements, behaviour for behaviour, the four helpers the loop shelled out to:
skill_frontmatter::extract;::field— including block-scalar unfolding for|and>and thequote-aware trailing-comment strip with its doubled-single-quote case;
::strip_quotes(ONE outerlayer, double or single, never both, and never a lone quote character); and
normalize_bool/trim_ws, whose deliberate refusal to fold YAML 1.1'syes/onaliases iscarried across with its reasoning.
Two behaviours the old pipeline got for free from command substitution are reproduced explicitly
and commented as such, because they were load-bearing rather than incidental:
fm="$(…extract…)"stripped trailing newlines from the extracted frontmatter, so a trailingblank line could never append a separator inside a block scalar — and an all-blank block counted
as no frontmatter at all.
"$(…field…)"stripped trailing newlines from each field's value. A folded (>) scalar joinswith spaces, which command substitution does not strip, so only newlines are removed.
The file list reaches awk on stdin, not as operands: awk reads an operand containing
=as avariable assignment, so a path with
=in it would be silently swallowed instead of read. The roottravels alongside each path rather than being re-derived from it, because the report prints the root
string the caller passed and a derivation would have to reproduce its exact spelling to stay
byte-identical.
skill-frontmatter.shis no longer sourced here — its helpers are the per-call execs that causedthis. The library file itself is unchanged and
check-skill.shremains its consumer.No flag, no option, and no output format changed.
skill-quality0.15.2 → 0.15.3 (patch —byte-identical output is the reason).
The locale trap, handled rather than hoped past
The pre-port script measured with bash
${#var}; the port measures with awklength(). Both arelocale-dependent and they can disagree — mawk ignores the locale and counts bytes while gawk does
not. A fixture asserting a hardcoded character count would encode one environment's answer and
fail elsewhere for the wrong reason, so case 18 asserts an equivalence: the script's reported
aggregate for a multibyte description must equal
${#UTF8_DESC}computed in the same shell. Adivergence is then reported as exactly what it is. On this machine both say 5
(
GNU Awk 5.4.0,LANG=en_US.UTF-8).Test plan
Five new cases; the full suite:
Byte-identity over the real tree, both implementations run over the same worktree:
The wall-clock bound discriminates — measured, not assumed. Same 200-file fixture corpus the test
builds, both implementations:
The 30s bound is deliberately very loose against a ~1s target: this runs in required CI, and a tight
timing assertion is a flaky gate — worse than the defect it guards. It fails only on a return to
per-file process spawning, which is two orders of magnitude away.
Lint:
This change reads the same files the script already read and writes only its own temp files — no new
external read or write, no new grant, no hook. No security review note applies.
Related
.github/recurring-schedule.jsonlisting-budget-watch— the routine whose own commandthis makes runnable. Neither carries a "run detached" note that this would make stale (checked).
spawn-cost class, different plugin and file)
20260810-225907-melodic-software-skill-listing-budget-overflow(ledger
I4-listing-budget.md§ I4-V1; adjudicationRECONCILE.md§ AD-6)