diff --git a/diagram/SKILL.md b/diagram/SKILL.md index 9e5a41066..a17f0adee 100644 --- a/diagram/SKILL.md +++ b/diagram/SKILL.md @@ -1,7 +1,7 @@ --- name: diagram version: 1.0.0 -description: "Turn an English description (or mermaid source) into a diagram triplet: the source, an editable .excalidraw file you can open (gstack)" +description: "Turn an English description (or mermaid source) into a diagram triplet: the source, an editable .excalidraw file you can open on excalidraw.com, and rendered SVG + PNG. (gstack)" allowed-tools: - Bash - Read @@ -21,9 +21,8 @@ triggers: ## When to invoke this skill -on excalidraw.com, -and rendered SVG + PNG (clean mermaid style; the .excalidraw carries the -hand-drawn aesthetic). Fully offline. +The SVG/PNG use clean mermaid style; the +.excalidraw carries the hand-drawn aesthetic. Fully offline. Use when asked to "make a diagram", "draw the architecture", "create a flowchart", "diagram this", or "visualize this flow". diff --git a/diagram/SKILL.md.tmpl b/diagram/SKILL.md.tmpl index 9e19a52c6..38acdf16c 100644 --- a/diagram/SKILL.md.tmpl +++ b/diagram/SKILL.md.tmpl @@ -4,8 +4,8 @@ version: 1.0.0 description: | Turn an English description (or mermaid source) into a diagram triplet: the source, an editable .excalidraw file you can open on excalidraw.com, - and rendered SVG + PNG (clean mermaid style; the .excalidraw carries the - hand-drawn aesthetic). Fully offline. + and rendered SVG + PNG. The SVG/PNG use clean mermaid style; the + .excalidraw carries the hand-drawn aesthetic. Fully offline. Use when asked to "make a diagram", "draw the architecture", "create a flowchart", "diagram this", or "visualize this flow". (gstack) allowed-tools: diff --git a/gstack/llms.txt b/gstack/llms.txt index efe522f90..979acf3e7 100644 --- a/gstack/llms.txt +++ b/gstack/llms.txt @@ -26,7 +26,7 @@ Conventions: - [/design-review](design-review/SKILL.md): Designer's eye QA: finds visual inconsistency, spacing issues, hierarchy problems, AI slop patterns, and slow interactions — then fixes them. - [/design-shotgun](design-shotgun/SKILL.md): Design shotgun: generate multiple AI design variants, open a comparison board, collect structured feedback, and iterate. - [/devex-review](devex-review/SKILL.md): Live developer experience audit. -- [/diagram](diagram/SKILL.md): Turn an English description (or mermaid source) into a diagram triplet: the source, an editable .excalidraw file you can open on excalidraw.com, and rendered SVG + PNG (clean mermaid style; the .excalidraw carries the hand-drawn aesthetic). +- [/diagram](diagram/SKILL.md): Turn an English description (or mermaid source) into a diagram triplet: the source, an editable .excalidraw file you can open on excalidraw.com, and rendered SVG + PNG. - [/document-generate](document-generate/SKILL.md): Generate missing documentation from scratch for a feature, module, or entire project. - [/document-release](document-release/SKILL.md): Post-ship documentation update. - [/freeze](freeze/SKILL.md): Restrict file edits to a specific directory for the session. diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 71aa1a34c..fab7e97cc 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -317,12 +317,16 @@ export function splitCatalogDescription(description: string): CatalogParts { const hasGstackTag = /\(gstack\)/.test(working); if (hasGstackTag) working = working.replace(/\(gstack\)/, '').trim(); - // Lead = first sentence (up to first period followed by space or end of string). - // We tolerate sentences with embedded periods (URLs, "v1.45.0.0") by requiring - // the period to be followed by whitespace OR end-of-text. + // Lead = first sentence, ending at the first `.`/`!`/`?` that is followed by + // whitespace or end-of-text. Terminator chars NOT followed by whitespace/end + // (embedded periods in "TODOS.md", URLs, "v1.45.0.0") are consumed by the + // second alternative `[.!?](?!\s|$)` and do NOT end the sentence. The two + // alternatives are disjoint character classes, so there is no ambiguity and + // no catastrophic-backtracking risk. If no terminator-followed-by-boundary + // exists at all, we fall back to a 20-word cut below. // First normalize to single-line for sentence detection, then back out. const collapsed = working.replace(/\s+/g, ' ').trim(); - const sentenceMatch = collapsed.match(/^([^.!?]*[.!?])(?:\s|$)/); + const sentenceMatch = collapsed.match(/^((?:[^.!?]|[.!?](?!\s|$))*[.!?])(?:\s|$)/); // 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 diff --git a/scripts/proactive-suggestions.json b/scripts/proactive-suggestions.json index d08c60853..f4c49e56e 100644 --- a/scripts/proactive-suggestions.json +++ b/scripts/proactive-suggestions.json @@ -79,8 +79,8 @@ "voice_line": "Voice triggers (speech-to-text aliases): \"dx audit\", \"test the developer experience\", \"try the onboarding\", \"developer experience test\"." }, "diagram": { - "lead": "Turn an English description (or mermaid source) into a diagram triplet: the source, an editable .excalidraw file you can open", - "routing": "on excalidraw.com,\nand rendered SVG + PNG (clean mermaid style; the .excalidraw carries the\nhand-drawn aesthetic). Fully offline.\nUse when asked to \"make a diagram\", \"draw the architecture\", \"create a\nflowchart\", \"diagram this\", or \"visualize this flow\".", + "lead": "Turn an English description (or mermaid source) into a diagram triplet: the source, an editable .excalidraw file you can open on excalidraw.com, and rendered SVG + PNG.", + "routing": "The SVG/PNG use clean mermaid style; the\n.excalidraw carries the hand-drawn aesthetic. Fully offline.\nUse when asked to \"make a diagram\", \"draw the architecture\", \"create a\nflowchart\", \"diagram this\", or \"visualize this flow\".", "voice_line": null }, "document-generate": { @@ -264,8 +264,8 @@ "voice_line": null }, "sync-gbrain": { - "lead": "Keep gbrain current with this repo's code and refresh agent search guidance in CLAUDE.md. Wraps the gstack-gbrain-sync orchestrator with state", - "routing": "probing, native code-surface registration, capability checks,\nand a verdict block. Re-runnable, idempotent. Use when: \"sync gbrain\",\n\"refresh gbrain\", \"re-index this repo\", \"gbrain search isn't finding\nthings\".", + "lead": "Keep gbrain current with this repo's code and refresh agent search guidance in CLAUDE.md.", + "routing": "Wraps the gstack-gbrain-sync orchestrator with\nstate probing, native code-surface registration, capability checks,\nand a verdict block. Re-runnable, idempotent. Use when: \"sync gbrain\",\n\"refresh gbrain\", \"re-index this repo\", \"gbrain search isn't finding\nthings\".", "voice_line": null }, "unfreeze": { diff --git a/sync-gbrain/SKILL.md b/sync-gbrain/SKILL.md index bfaf291d2..8c00ab5e3 100644 --- a/sync-gbrain/SKILL.md +++ b/sync-gbrain/SKILL.md @@ -2,7 +2,7 @@ name: sync-gbrain preamble-tier: 2 version: 1.0.0 -description: Keep gbrain current with this repo's code and refresh agent search guidance in CLAUDE.md. Wraps the gstack-gbrain-sync orchestrator with state (gstack) +description: Keep gbrain current with this repo's code and refresh agent search guidance in CLAUDE.md. (gstack) triggers: - sync gbrain - refresh gbrain @@ -23,7 +23,8 @@ allowed-tools: ## When to invoke this skill -probing, native code-surface registration, capability checks, +Wraps the gstack-gbrain-sync orchestrator with +state probing, native code-surface registration, capability checks, and a verdict block. Re-runnable, idempotent. Use when: "sync gbrain", "refresh gbrain", "re-index this repo", "gbrain search isn't finding things". diff --git a/test/catalog-trim.test.ts b/test/catalog-trim.test.ts index 82e46bdfe..2b56b5a62 100644 --- a/test/catalog-trim.test.ts +++ b/test/catalog-trim.test.ts @@ -95,31 +95,59 @@ describe('splitCatalogDescription', () => { expect(parts.routingProse).toBe('With routing prose afterward.'); }); - test('embedded-period descriptions: known limitation falls back to first-20-words', () => { - // KNOWN LIMITATION: the sentence regex `^([^.!?]*[.!?])(?:\\s|$)` stops - // at the FIRST `.`-then-non-whitespace because [^.!?]* is greedy and - // can't backtrack past a non-period char. For "DESIGN.md and v1.45.0.0 - // in the lead. Use when..." the regex fails entirely and the lead falls - // back to the first 20 words (~the whole short input). - // - // The real-world impact is small: descriptions like "DESIGN.md" or "v1.45" - // appearing in the middle of the FIRST sentence are rare. When they do - // occur, the lead simply becomes the full description (no body section - // generated) — same as a description without a period. The trim CI gate - // still keeps the per-skill size budget honest. - // - // If this gap matters later, replace the regex with a sentence tokenizer - // (compromise.js / Intl.Segmenter) — until then we accept the fallback. + test('REGRESSION: embedded-period first sentence splits at real boundary', () => { + // The old regex `^([^.!?]*[.!?])(?:\s|$)` could not cross ANY period — + // [^.!?]* stops at the first `.` even mid-token ("DESIGN.md"), the + // boundary check then fails, and the whole match failed. The code + // silently fell back to a 20-word cut mid-phrase (observed with a + // description mentioning "TODOS.md"). The fixed regex + // `^((?:[^.!?]|[.!?](?!\s|$))*[.!?])(?:\s|$)` consumes terminators NOT + // followed by whitespace/end, so embedded periods no longer break it. const desc = 'Skill that mentions DESIGN.md and v1.45.0.0 in the lead. ' + 'Use when asked to do something.'; const parts = splitCatalogDescription(desc); - // Actual behavior: lead absorbs the whole input via the word-count fallback. - expect(parts.lead.length).toBeGreaterThan(0); - // routingProse may be empty when the fallback consumes everything. - // The test exists to detect REGRESSIONS (lead becoming oddly short like - // "Skill that mentions DESIGN.") not to assert ideal behavior. - expect(parts.lead).toContain('Skill that mentions'); + expect(parts.lead).toBe('Skill that mentions DESIGN.md and v1.45.0.0 in the lead.'); + expect(parts.routingProse).toBe('Use when asked to do something.'); + }); + + test('REGRESSION: "TODOS.md backlog" style sentence keeps full lead + routing', () => { + const desc = + 'Drive an approved plan to completion on a TODOS.md backlog. ' + + 'Use when asked to "autobuilder" or "run the build loop". ' + + 'Proactively suggest after a plan is approved. (gstack)'; + const parts = splitCatalogDescription(desc); + expect(parts.lead).toBe('Drive an approved plan to completion on a TODOS.md backlog.'); + expect(parts.routingProse).toContain('Use when asked to "autobuilder"'); + expect(parts.routingProse).toContain('Proactively suggest'); + expect(parts.hasGstackTag).toBe(true); + }); + + test('URL in first sentence does not end the lead early', () => { + const desc = + 'See https://example.com/docs/v2 for the workflow. ' + + 'Use when asked to consult the docs.'; + const parts = splitCatalogDescription(desc); + expect(parts.lead).toBe('See https://example.com/docs/v2 for the workflow.'); + expect(parts.routingProse).toBe('Use when asked to consult the docs.'); + }); + + test('>200 char first sentence WITH embedded periods still truncates with ellipsis and keeps routing', () => { + const firstSentence = + 'Regenerate the iOS debug bridge against files like Bridge.swift and TODOS.md, ' + + 'walking every target listed in Project.xcodeproj while preserving v1.45.0.0 ' + + 'compatibility shims, custom entitlements, and the long tail of per-target ' + + 'build settings nobody remembers configuring.'; + expect(firstSentence.length).toBeGreaterThan(200); + const desc = firstSentence + ' Use when asked to resync the bridge. (gstack)'; + const parts = splitCatalogDescription(desc); + // Lead is the truncated first sentence (ellipsis path), not a 20-word cut. + expect(parts.lead.endsWith('...')).toBe(true); + expect(parts.lead.length).toBeLessThanOrEqual(200); + expect(parts.lead).toContain('TODOS.md'); + // Routing prose survives intact. + expect(parts.routingProse).toContain('Use when asked to resync the bridge.'); + expect(parts.hasGstackTag).toBe(true); }); test('description without a period uses first ~20 words as lead', () => {