fix(catalog): preserve routing prose when first sentence exceeds 200 chars

splitCatalogDescription truncated the lead BEFORE computing routing
extraction, which meant skills whose first sentence was over 200 chars
(design-consultation: 207 chars) had their entire routing prose silently
dropped — the "## When to invoke" body section came out empty.

Root cause: routing was extracted via `collapsed.indexOf(lead)` after lead
was suffixed with "...". The "..." never appeared in the original string,
so indexOf returned -1 and routingProse fell back to empty.

Fix: compute routing from sentenceLead (the untruncated first sentence)
BEFORE truncating the displayed lead. The displayed lead still gets "..."
when over 200 chars, but the routing extraction uses the real boundary.

Also: refresh golden snapshots for claude/codex/factory ship and update
two unit tests that asserted v1.44 behavior:
- skill-validation.test.ts: trigger-phrase + proactive-routing tests now
  search whole content, not just frontmatter (T4 moved them to a body
  "## When to invoke" section)
- writing-style-resolver.test.ts: jargon-list assertion now expects the
  T3 reference pointer, not the inline list

Test plan:
- bun test test/skill-validation.test.ts test/writing-style-resolver.test.ts
  test/host-config.test.ts test/skill-size-budget.test.ts
  test/parity-suite.test.ts test/skill-coverage-matrix.test.ts
  test/skill-coverage-floor.test.ts test/cso-preserved.test.ts
  test/resolver-entry.test.ts test/helpers/capture-parity-baseline.test.ts
  test/gen-skill-docs.test.ts: 1134 pass, 0 fail
- Manual verify: design-consultation/SKILL.md "## When to invoke this skill"
  body section now contains "Use when asked to..." + "Proactively suggest..."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan 2026-05-25 21:09:59 -07:00
parent 43e18af4ad
commit 2aff29e956
No known key found for this signature in database
GPG Key ID: C1F69E85C74EFE1D
8 changed files with 57 additions and 264 deletions

View File

@ -46,6 +46,12 @@ gbrain:
## When to invoke this skill ## When to invoke this skill
Creates DESIGN.md as your project's design source
of truth. For existing sites, use /plan-design-review to infer the system instead.
Use when asked to "design system", "brand guidelines", or "create DESIGN.md".
Proactively suggest when starting a new project's UI with no existing
design system or DESIGN.md.
## Preamble (run first) ## Preamble (run first)
```bash ```bash

View File

@ -245,29 +245,38 @@ export function splitCatalogDescription(description: string): CatalogParts {
// First normalize to single-line for sentence detection, then back out. // First normalize to single-line for sentence detection, then back out.
const collapsed = working.replace(/\s+/g, ' ').trim(); const collapsed = working.replace(/\s+/g, ' ').trim();
const sentenceMatch = collapsed.match(/^([^.!?]*[.!?])(?:\s|$)/); const sentenceMatch = collapsed.match(/^([^.!?]*[.!?])(?:\s|$)/);
let lead = sentenceMatch ? sentenceMatch[1].trim() : collapsed.split(/\s/).slice(0, 20).join(' '); // sentenceLead is the FULL first sentence (no truncation). We compute routing
// from this position, then optionally truncate the displayed lead afterwards.
// Truncating first then computing routing was the v1.45.0.0 bug — when the
// first sentence exceeded 200 chars, the routing extraction would lose the
// entire tail of the description (design-consultation's "Use when..."
// routing prose silently dropped).
const sentenceLead = sentenceMatch ? sentenceMatch[1].trim() : collapsed.split(/\s/).slice(0, 20).join(' ');
// If the lead would be too long, trim to the first 140 chars at a word boundary // Routing prose: everything AFTER the first sentence boundary in the collapsed view.
const leadInCollapsed = collapsed.indexOf(sentenceLead);
const routingCollapsed = leadInCollapsed >= 0
? collapsed.slice(leadInCollapsed + sentenceLead.length).trim()
: '';
// Now produce the displayed lead — truncated if too long. The original
// sentenceLead is preserved for routing extraction below.
let lead = sentenceLead;
if (lead.length > 200) { if (lead.length > 200) {
const trunc = lead.slice(0, 197); const trunc = lead.slice(0, 197);
const lastSpace = trunc.lastIndexOf(' '); const lastSpace = trunc.lastIndexOf(' ');
lead = (lastSpace > 60 ? trunc.slice(0, lastSpace) : trunc) + '...'; lead = (lastSpace > 60 ? trunc.slice(0, lastSpace) : trunc) + '...';
} }
const leadInCollapsed = collapsed.indexOf(lead);
const routingCollapsed = leadInCollapsed >= 0
? collapsed.slice(leadInCollapsed + lead.length).trim()
: '';
// Restore line breaks for routing prose by mapping back to original layout. // Restore line breaks for routing prose by mapping back to original layout.
// Use original whitespace structure where possible; fall back to collapsed. // Use original whitespace structure where possible; fall back to collapsed.
// Anchor recovery on sentenceLead (the untruncated first sentence) — not
// `lead` (which may have a "..." suffix and won't substring-match `working`).
let routingProse = routingCollapsed; let routingProse = routingCollapsed;
// Try to recover the multi-line layout: split working at the lead boundary. const collapsedLeadIdx = working.replace(/\s+/g, ' ').indexOf(sentenceLead);
const collapsedLeadIdx = working.replace(/\s+/g, ' ').indexOf(lead);
if (collapsedLeadIdx >= 0) { if (collapsedLeadIdx >= 0) {
// Walk the original working string until we've consumed lead.length collapsed chars
let consumed = 0; let consumed = 0;
let cut = 0; let cut = 0;
for (let i = 0; i < working.length && consumed < collapsedLeadIdx + lead.length; i++) { for (let i = 0; i < working.length && consumed < collapsedLeadIdx + sentenceLead.length; i++) {
if (/\s/.test(working[i])) { if (/\s/.test(working[i])) {
if (i === 0 || /\s/.test(working[i - 1])) continue; if (i === 0 || /\s/.test(working[i - 1])) continue;
consumed += 1; consumed += 1;

View File

@ -45,7 +45,7 @@
}, },
"design-consultation": { "design-consultation": {
"lead": "Design consultation: understands your product, researches the landscape, proposes a complete design system (aesthetic, typography, color, layout, spacing, motion), and generates font+color preview...", "lead": "Design consultation: understands your product, researches the landscape, proposes a complete design system (aesthetic, typography, color, layout, spacing, motion), and generates font+color preview...",
"routing": "", "routing": "Creates DESIGN.md as your project's design source\nof truth. For existing sites, use /plan-design-review to infer the system instead.\nUse when asked to \"design system\", \"brand guidelines\", or \"create DESIGN.md\".\nProactively suggest when starting a new project's UI with no existing\ndesign system or DESIGN.md.",
"voice_line": null "voice_line": null
}, },
"learn": { "learn": {

View File

@ -2,12 +2,7 @@
name: ship name: ship
preamble-tier: 4 preamble-tier: 4
version: 1.0.0 version: 1.0.0
description: | description: Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION, update CHANGELOG, commit, push, create PR. (gstack)
Ship workflow: detect + merge base branch, run tests, review diff, bump VERSION,
update CHANGELOG, commit, push, create PR. Use when asked to "ship", "deploy",
"push to main", "create a PR", "merge and push", or "get it deployed".
Proactively invoke this skill (do NOT push/PR directly) when the user says code
is ready, asks about deploying, wants to push code up, or asks to create a PR. (gstack)
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
@ -27,6 +22,14 @@ triggers:
<!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly --> <!-- AUTO-GENERATED from SKILL.md.tmpl — do not edit directly -->
<!-- Regenerate: bun run gen:skill-docs --> <!-- Regenerate: bun run gen:skill-docs -->
## When to invoke this skill
Use when asked to "ship", "deploy",
"push to main", "create a PR", "merge and push", or "get it deployed".
Proactively invoke this skill (do NOT push/PR directly) when the user says code
is ready, asks about deploying, wants to push code up, or asks to create a PR.
## Preamble (run first) ## Preamble (run first)
```bash ```bash
@ -553,84 +556,7 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
- User-turn override wins: if the current message asks for terse / no explanations / just the answer, skip this section. - User-turn override wins: if the current message asks for terse / no explanations / just the answer, skip this section.
- Terse mode (EXPLAIN_LEVEL: terse): no glosses, no outcome-framing layer, shorter responses. - Terse mode (EXPLAIN_LEVEL: terse): no glosses, no outcome-framing layer, shorter responses.
Jargon list, gloss on first use if the term appears: Curated jargon list lives at `~/.claude/skills/gstack/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
- idempotent
- idempotency
- race condition
- deadlock
- cyclomatic complexity
- N+1
- N+1 query
- backpressure
- memoization
- eventual consistency
- CAP theorem
- CORS
- CSRF
- XSS
- SQL injection
- prompt injection
- DDoS
- rate limit
- throttle
- circuit breaker
- load balancer
- reverse proxy
- SSR
- CSR
- hydration
- tree-shaking
- bundle splitting
- code splitting
- hot reload
- tombstone
- soft delete
- cascade delete
- foreign key
- composite index
- covering index
- OLTP
- OLAP
- sharding
- replication lag
- quorum
- two-phase commit
- saga
- outbox pattern
- inbox pattern
- optimistic locking
- pessimistic locking
- thundering herd
- cache stampede
- bloom filter
- consistent hashing
- virtual DOM
- reconciliation
- closure
- hoisting
- tail call
- GIL
- zero-copy
- mmap
- cold start
- warm start
- green-blue deploy
- canary deploy
- feature flag
- kill switch
- dead letter queue
- fan-out
- fan-in
- debounce
- throttle (UI)
- hydration mismatch
- memory leak
- GC pause
- heap fragmentation
- stack overflow
- null pointer
- dangling pointer
- buffer overflow
## Completeness Principle — Boil the Lake ## Completeness Principle — Boil the Lake

View File

@ -542,84 +542,7 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
- User-turn override wins: if the current message asks for terse / no explanations / just the answer, skip this section. - User-turn override wins: if the current message asks for terse / no explanations / just the answer, skip this section.
- Terse mode (EXPLAIN_LEVEL: terse): no glosses, no outcome-framing layer, shorter responses. - Terse mode (EXPLAIN_LEVEL: terse): no glosses, no outcome-framing layer, shorter responses.
Jargon list, gloss on first use if the term appears: Curated jargon list lives at `$GSTACK_ROOT/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
- idempotent
- idempotency
- race condition
- deadlock
- cyclomatic complexity
- N+1
- N+1 query
- backpressure
- memoization
- eventual consistency
- CAP theorem
- CORS
- CSRF
- XSS
- SQL injection
- prompt injection
- DDoS
- rate limit
- throttle
- circuit breaker
- load balancer
- reverse proxy
- SSR
- CSR
- hydration
- tree-shaking
- bundle splitting
- code splitting
- hot reload
- tombstone
- soft delete
- cascade delete
- foreign key
- composite index
- covering index
- OLTP
- OLAP
- sharding
- replication lag
- quorum
- two-phase commit
- saga
- outbox pattern
- inbox pattern
- optimistic locking
- pessimistic locking
- thundering herd
- cache stampede
- bloom filter
- consistent hashing
- virtual DOM
- reconciliation
- closure
- hoisting
- tail call
- GIL
- zero-copy
- mmap
- cold start
- warm start
- green-blue deploy
- canary deploy
- feature flag
- kill switch
- dead letter queue
- fan-out
- fan-in
- debounce
- throttle (UI)
- hydration mismatch
- memory leak
- GC pause
- heap fragmentation
- stack overflow
- null pointer
- dangling pointer
- buffer overflow
## Completeness Principle — Boil the Lake ## Completeness Principle — Boil the Lake

View File

@ -544,84 +544,7 @@ Applies to AskUserQuestion, user replies, and findings. AskUserQuestion Format i
- User-turn override wins: if the current message asks for terse / no explanations / just the answer, skip this section. - User-turn override wins: if the current message asks for terse / no explanations / just the answer, skip this section.
- Terse mode (EXPLAIN_LEVEL: terse): no glosses, no outcome-framing layer, shorter responses. - Terse mode (EXPLAIN_LEVEL: terse): no glosses, no outcome-framing layer, shorter responses.
Jargon list, gloss on first use if the term appears: Curated jargon list lives at `$GSTACK_ROOT/scripts/jargon-list.json` (80+ terms). On the first jargon term you encounter this session, Read that file once; treat the `terms` array as the canonical list. The list is repo-owned and may grow between releases.
- idempotent
- idempotency
- race condition
- deadlock
- cyclomatic complexity
- N+1
- N+1 query
- backpressure
- memoization
- eventual consistency
- CAP theorem
- CORS
- CSRF
- XSS
- SQL injection
- prompt injection
- DDoS
- rate limit
- throttle
- circuit breaker
- load balancer
- reverse proxy
- SSR
- CSR
- hydration
- tree-shaking
- bundle splitting
- code splitting
- hot reload
- tombstone
- soft delete
- cascade delete
- foreign key
- composite index
- covering index
- OLTP
- OLAP
- sharding
- replication lag
- quorum
- two-phase commit
- saga
- outbox pattern
- inbox pattern
- optimistic locking
- pessimistic locking
- thundering herd
- cache stampede
- bloom filter
- consistent hashing
- virtual DOM
- reconciliation
- closure
- hoisting
- tail call
- GIL
- zero-copy
- mmap
- cold start
- warm start
- green-blue deploy
- canary deploy
- feature flag
- kill switch
- dead letter queue
- fan-out
- fan-in
- debounce
- throttle (UI)
- hydration mismatch
- memory leak
- GC pause
- heap fragmentation
- stack overflow
- null pointer
- dangling pointer
- buffer overflow
## Completeness Principle — Boil the Lake ## Completeness Principle — Boil the Lake

View File

@ -1480,14 +1480,15 @@ describe('Skill trigger phrases', () => {
const skillPath = path.join(ROOT, skill, 'SKILL.md'); const skillPath = path.join(ROOT, skill, 'SKILL.md');
if (!fs.existsSync(skillPath)) return; if (!fs.existsSync(skillPath)) return;
const content = fs.readFileSync(skillPath, 'utf-8'); const content = fs.readFileSync(skillPath, 'utf-8');
// Extract description from frontmatter // v1.45.0.0 catalog trim moved trigger prose out of frontmatter into a
const frontmatterEnd = content.indexOf('---', 4); // body "## When to invoke" section. Search the full file content, not
const frontmatter = content.slice(0, frontmatterEnd); // just frontmatter. The trigger phrase must still appear somewhere in
expect(frontmatter).toMatch(/Use when/i); // the skill so agents can match user requests to the skill.
expect(content).toMatch(/Use when/i);
}); });
} }
// Skills with proactive triggers should have "Proactively suggest" in description // Skills with proactive triggers should have "Proactively suggest" somewhere in the skill.
const SKILLS_REQUIRING_PROACTIVE = [ const SKILLS_REQUIRING_PROACTIVE = [
'qa', 'qa-only', 'ship', 'review', 'investigate', 'office-hours', 'qa', 'qa-only', 'ship', 'review', 'investigate', 'office-hours',
'plan-ceo-review', 'plan-eng-review', 'plan-design-review', 'plan-ceo-review', 'plan-eng-review', 'plan-design-review',
@ -1499,9 +1500,8 @@ describe('Skill trigger phrases', () => {
const skillPath = path.join(ROOT, skill, 'SKILL.md'); const skillPath = path.join(ROOT, skill, 'SKILL.md');
if (!fs.existsSync(skillPath)) return; if (!fs.existsSync(skillPath)) return;
const content = fs.readFileSync(skillPath, 'utf-8'); const content = fs.readFileSync(skillPath, 'utf-8');
const frontmatterEnd = content.indexOf('---', 4); // Same catalog-trim consideration — search the full file content.
const frontmatter = content.slice(0, frontmatterEnd); expect(content).toMatch(/Proactively (suggest|invoke)/i);
expect(frontmatter).toMatch(/Proactively (suggest|invoke)/i);
}); });
} }
}); });

View File

@ -49,11 +49,17 @@ describe('Writing Style preamble section', () => {
expect(out).toMatch(/terse|no explanations|user-turn override|current message/i); expect(out).toMatch(/terse|no explanations|user-turn override|current message/i);
}); });
test('tier 2+ preamble inlines jargon list', () => { test('tier 2+ preamble references jargon list by path (v1.45.0.0 T3 — pointer, not inline)', () => {
const out = generatePreamble(makeCtx('claude', 2)); const out = generatePreamble(makeCtx('claude', 2));
// Spot-check a few terms from scripts/jargon-list.json // T3 dedup: the 80-term jargon list lives in scripts/jargon-list.json.
expect(out).toContain('idempotent'); // The Writing Style section points at the file rather than inlining it,
expect(out).toContain('race condition'); // saving ~70 KB across the corpus. Agents Read the JSON on first
// jargon term encountered per session.
expect(out).toContain('jargon-list.json');
expect(out).toContain('Curated jargon list');
// Negative check: the literal term lines should NOT be inlined any more.
expect(out).not.toMatch(/^- idempotent$/m);
expect(out).not.toMatch(/^- race condition$/m);
}); });
test('tier 2+ preamble includes terse-mode gate condition', () => { test('tier 2+ preamble includes terse-mode gate condition', () => {