feat(coding-agent): list a large skill catalog one line per skill

Merged
CO
7d ago
+228-18

Fixes #4449

What

A large skill catalog is listed one line per skill instead of a five-line XML block:

<available_skills>
- smart-rebase [r0]: Rebase the current branch onto the right base branch with local conflict handling…
- gpt-image-gen [r1/bundle/SKILL-3R5ZD4J5.md]: MUST read before generating images…
</available_skills>
  • Location is the root alias alone when the file is <root>/<name>/SKILL.md (100 of 101 skills on a real catalog), else the path under the root, so every skill stays resolvable through the existing <skill_roots> table; one header line states the rule.
  • One-line descriptions are capped at 160 characters and keep the description's when-to-use sentence (Use when/for/whenever ..., MUST USE ..., USE ONLY WHEN, This skill should be used when, Triggers: ..., Korean triggers: ..., Triggers - ...): a cut keeps that sentence whole (or its start, when it alone is longer than the line) and fills the remaining room with the leading what-it-does text, then the text after it, cut at word boundaries. Multi-line descriptions are flattened first, so a trigger on a later line still counts. A catalog whose 160-character lines overflow the 48 KiB cap tries 100-character lines before dropping descriptions.
  • Compact mode also triggers from 16 KiB of descriptions (was 64 KiB); the 100-skill count trigger is unchanged. A ~100-skill personal library no longer falls back to full multi-sentence descriptions when a few skills are hidden.
  • The names-only fallback (over 48 KiB) uses the same line form without descriptions, and its header says - name [location] (no : when to use it). Small catalogs render exactly as before.
  • The <available_skills> wrapper and the catalog marker sentence are kept, so the Claude-subscription prompt extractor still finds the block.

Measured: when-to-use clause survival vs tokens (review FIX_FIRST at 7da8afe7b9)

Real libraries on a dev machine: ~/.agents/skills (71 skills, 65 with a trigger clause) and the same plus the omo plugin skills (97 skills, 89 with a clause). Tokens are an o200k estimate (js-tiktoken) of the whole rendered skills section. "Whole" = the full trigger sentence appears in the line; "start" = its first 40 characters appear.

setversionclause wholeclause starttokensbytes
~/.agents/skills (71)main64/6564/6510,33541,211
~/.agents/skills (71)pr-7da8afe7b94/6516/652,2708,938
~/.agents/skills (71)new39/6564/653,25213,111
+ omo plugin skills (97)main19/8952/897,57328,445
+ omo plugin skills (97)pr-7da8afe7b94/8917/893,05712,112
+ omo plugin skills (97)new62/8988/894,37417,735

main renders the 71-skill set in full XML (below its thresholds) and the 97-skill set with a 160-character head cut (over the 48 KiB cap). The previous head of this PR (100-character head cut) kept the whole clause for 4 skills.

Candidate rules, whole-clause survival / tokens (71-skill set · 97-skill set):

rule100120140160180
head cut4/65 / 2,261 · 4/89 / 3,0436/65 / 2,554 · 7/89 / 3,4499/65 / 2,877 · 11/89 / 3,87413/65 / 3,146 · 19/89 / 4,26517/65 / 3,398 · 23/89 / 4,602
trigger-first25/65 / 2,288 · 36/89 / 3,09628/65 / 2,598 · 44/89 / 3,50733/65 / 2,920 · 56/89 / 3,92839/65 / 3,219 · 62/89 / 4,33242/65 / 3,480 · 66/89 / 4,680

The bar was the 160-character baseline: a clause survives whenever it fits in 160 characters (39/65 and 62/89 clauses are that short). Trigger-first at 160 meets it exactly at the lowest cost of any rule that does; 140 falls to 33/65. A head cut never comes close at any cap.

Tests

  • RED -> GREEN test/suite/regressions/4449-compact-skill-catalog-lines.test.ts: on main both original cases fail (expected false to be true for the 16 KiB trigger, expected [] to have a length of 98 for the line form). The new table-driven case keeps the when-to-use clause: $shape covers 11 description shapes (Use when / Use for / Use this whenever / MUST USE first / USE ONLY WHEN / This skill should be used when / Triggers: / Korean triggers / Triggers - Korean list / trigger on a later line / clause longer than the line). Against the previous head 7da8afe7b9, 11 of 13 cases failed (RED); all 13 pass now (GREEN). It also resolves every listed line back to the skill's file through the roots table. The short-trigger case also asserts that the lead fill survives (the line starts with Operates the deployment toolchain); replacing the lead fill with "" makes that case fail.
  • Updated 3460-context-file-size-cap.test.ts for the line form and the new byte threshold. Its 400-skill case also covers the 100-character fallback tier.
  • 3483-prompt-goldens.json: only prompts.compact changed, and only between </skill_roots> and </available_skills>. The fixture's two-line descriptions are now flattened ("... verify behavior. Keep the full description."); every other key is byte-identical.
  • docs/settings.md now states the real thresholds (16 KiB, 160 characters), the one-line format, and the trigger-sentence rule.
  • 8 related files, 164 tests green. Push gates (each its own process): git diff --check origin/main...HEAD, bun run check, those tests, and node scripts/check-pr-changelog.mjs.

Follow-up

  • #4520: the generic verb branch of the trigger detection (Run/Load/Call ... for/before/if) can pick imperative prose over the real when-to-use sentence (review LOW 1; 0 misfires on real libraries, synthetic repro in the issue).
  • #4518: rewriteSkillsLocations (Anthropic subscription lane) has not rewritten any location since the roots-table aliases landed. It is pre-existing and out of scope here.