diff --git a/README.md b/README.md index 6af2108f46f4ad85ad8917681275d536177c3841..8d0bf65712e75819c7699a7f09849947810b9cc8 100644 --- a/README.md +++ b/README.md @@ -6,157 +6,18 @@ SPDX-License-Identifier: CC0-1.0 # agent-skills -A collection of [Agent Skills]; structured markdown that extends LLM -agent capabilities through a standardised format. +A collection of [Agent Skills]: structured markdown that extends LLM agent +capabilities through a standardised format. See [§ Available +skills](#available-skills) for the full list. [Agent Skills]: https://agentskills.io/ -## Available Skills - -Refer to [§ Token stats](#token-stats) for a detailed breakdown of each skill's -token count, plus overall metadata usage. I've used and tested them most with -[Crush], [Amp], and [Shelley], and to a lesser degree, [Octofriend] and [Pi]. +I've used and tested them most with [Crush], [Pi], [Amp], and [Shelley]. [Crush]: https://github.com/charmbracelet/crush +[Pi]: https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent [Amp]: https://ampcode.com [Shelley]: https://github.com/boldsoftware/shelley -[Octofriend]: https://github.com/synthetic-lab/octofriend -[Pi]: https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent - -- [addressing-code-review-comments](skills/addressing-code-review-comments/SKILL.md): - Finds `CR:` comments in code, gathers feedback, and carries out requested - changes. -- [amoliths-opinions](skills/amoliths-opinions/SKILL.md): Applies Amolith's - opinions across project setup, tooling, licensing, dependencies, mise tasks, - CLIs, TUIs, MCP servers, web apps, packaging/releases, testing, and - Go-specific ecosystem defaults. -- [ast-grep](skills/ast-grep/SKILL.md): Writes [ast-grep] rules for structural - code search and modification. -- [auditing-repositories](skills/auditing-repositories/SKILL.md): Audits open - source repositories for security, privacy, and unexpected behavior. -- [authoring-skills](skills/authoring-skills/SKILL.md): Creates and reviews - Agent Skills following best practices. Covers skill structure, frontmatter, - and progressive disclosure patterns. -- [backing-up-with-keld](skills/backing-up-with-keld/SKILL.md): Writes and - manages keld configuration for restic backups. Covers TOML preset structure, - split preset composition (`home@cloud`), config file discovery, environment - variables, and systemd timer setup. -- [collaborating-through-pr-pico-sh](skills/collaborating-through-pr-pico-sh/SKILL.md): - Collaborates on git patches via [pr.pico.sh], a minimal patchbin service. - Covers both contributing and reviewing patch requests using `git format-patch` - and `git am`. -- [computing-golden-ratio-typography](skills/computing-golden-ratio-typography/SKILL.md): - Computes Golden Ratio Typography line heights, spacing units, type scales, and - readable measures from a font file or explicit font metrics. -- [cooking](skills/cooking/SKILL.md): Guides home cooking as a technically - grounded collaborator. Helps plan meals, use odds and ends, troubleshoot - techniques, handle substitutions, and reason through cuisine-specific flavor - logic without flattening regional traditions. -- [creating-tasks-through-lunatask](skills/creating-tasks-through-lunatask/SKILL.md): - Creates tasks and handoffs in [Lunatask] via [lune]. Tasks are just tasks. - Handoffs capture work to resume later across sessions without filling context - windows. -- [fallback-code-review](skills/fallback-code-review/SKILL.md): Provides a - fallback-only external review flow via CLI tools like Amp, CodeRabbit, or - Kodus. -- [formatting-commits](skills/formatting-commits/SKILL.md): Detects a project's - commit style from recent history and formats messages accordingly. Supports - Conventional Commits and kernel-style imperative commits. -- [frontend-accessibility](skills/frontend-accessibility/SKILL.md): Strives to - generate accessible HTML, React, and frontend code following WCAG 2.2 AA. - Prioritizes semantic HTML over ARIA, keyboard navigation, and screen reader - compatibility. -- [handling-customer-data](skills/handling-customer-data/SKILL.md): Queries - customer data responsibly—the agent answers questions about the data without - ever seeing it directly. -- [humanize](skills/humanize/SKILL.md): Removes AI-generated patterns from - text like promotional fluff, weasel words, and mechanical sentence structures. - Based on Wikipedia's AI Cleanup research. Originally from [blader/humanizer]. -- [ideating-with-bdd](skills/ideating-with-bdd/SKILL.md): Guides collaborative - discovery of behaviour through structured conversation, iterating with the user - to refine ideas into user stories and Gherkin scenarios. Works for any - language or framework. -- [in-session-review](skills/in-session-review/SKILL.md): Runs an extremely - strict maintainability review for abstraction quality, giant files, and - spaghetti-condition growth. -- [initialising-and-updating-agents-md](skills/initialising-and-updating-agents-md/SKILL.md): - Analyses a codebase and creates or updates `AGENTS.md` to help future agents - work effectively. Discovers commands, conventions, patterns, and gotchas from - the project's source and config files. -- [invoking-subagents](skills/invoking-subagents/SKILL.md): Spawns subagents - with restricted tool access for parallel tasks across repositories. Requires - [the Pi coding agent][Pi]. Useful for summarizing git history or processing - large diffs without filling the main context window. -- [licensing-with-reuse](skills/licensing-with-reuse/SKILL.md): Manages - REUSE-compliant licensing with the `reuse` CLI. Covers `reuse annotate`, - `.license` sidecars for prompt files, custom `LicenseRef-...` identifiers, - `LICENSES/`, project conventions, and `reuse lint`. -- [maintaining-aur-packages](skills/maintaining-aur-packages/SKILL.md): Creates - and updates AUR packages following Arch packaging standards. Covers PKGBUILDs - for source, `-bin`, and `-git` package types, checksums with `updpkgsums`, - linting with `namcap`, and `.SRCINFO` generation. -- [managing-personal-knowledge](skills/managing-personal-knowledge/SKILL.md): - Works inside a personal knowledge base as an exacting steward, shaping atomic, - concept-oriented notes, a dense wikilink graph, and disciplined tags instead - of dumping transcripts. Tool-agnostic across Markdown vaults like Obsidian, - Logseq, Roam, and SilverBullet. -- [monitoring-with-munin](skills/monitoring-with-munin/SKILL.md): Deploys and - manages Munin monitoring across servers. Sets up munin-node on hosts, writes - plugins, configures masters, and handles alerts. -- [notifying-through-ntfy](skills/notifying-through-ntfy/SKILL.md): Sends push - notifications via [ntfy.sh] when requested, such as at the end of its turn. -- [rebasing-with-git](skills/rebasing-with-git/SKILL.md): Manages git rebase - workflows from simple rebases to the drop-and-repick integration branch - pattern. Assumes `upstream` remote for forks and `rerere` enabled. Supports - worktrees including `wt`-managed repos. -- [researching-with-rumilo](skills/researching-with-rumilo/SKILL.md): Dispatches - AI research subagents via [rumilo] for web search and repository exploration. - Research topics, look up library usage, or explore external codebases without - filling the main context window. -- [scripting-with-go](skills/scripting-with-go/SKILL.md): Creates executable Go - scripts using a shell trick (not a true shebang). For automation and tooling - outside of Go projects. -- [testing-with-gocuke-and-gherkin](skills/testing-with-gocuke-and-gherkin/SKILL.md): - Drives BDD, red/green TDD, and property-based testing in Go projects using - [gocuke] and Gherkin feature files. -- [toki-pona-dictionary](skills/toki-pona-dictionary/SKILL.md): Searches the - [nimi.li] toki pona dictionary by English meaning. Caches word data locally - after a one-time fetch, then runs offline searches across definitions and - community usage tags. -- [updating-llm-client-model-lists](skills/updating-llm-client-model-lists/SKILL.md): - Synchronizes model configurations across Zed, Crush, Octofriend, and Pi from - Plexus' /v1/models endpoint. -- [using-exe-dev](skills/using-exe-dev/SKILL.md): Guides working with [exe.dev] - VMs, which provide Linux VMs with persistent disks, instant HTTPS, and - built-in auth via SSH. -- [using-jujutsu](skills/using-jujutsu/SKILL.md): Guides [jujutsu] (jj) - workflows, covering commit selection, change creation, diff reading, and - common pitfalls that trip up agents. -- [working-with-zmx](skills/working-with-zmx/SKILL.md): Manages persistent - terminal sessions for background processes, dev servers, and long-running tasks. - Creates sessions, runs commands detached, checks output, and writes files into - sessions over SSH. -- [writing-git-tags](skills/writing-git-tags/SKILL.md): Generates git tag - annotations from commit history following Semantic Versioning and Conventional - Commits via [git-format]. -- [writing-rust](skills/writing-rust/SKILL.md): Writes idiomatic Rust with - strict quality gates: `cargo fmt`, `clippy -- -D warnings`, and required - tests. Enforces `thiserror` for libs, `anyhow` for apps, warns against common - anti-patterns. - -[blader/humanizer]: https://github.com/blader/humanizer -[ast-grep]: https://github.com/ast-grep/ast-grep -[git-format]: https://git.secluded.site/git-format -[gocuke]: https://github.com/tenntenn/gocuke -[lune]: https://git.secluded.site/lune -[Lunatask]: https://lunatask.app/ -[nimi.li]: https://nimi.li -[ntfy.sh]: https://ntfy.sh -[rumilo]: https://git.secluded.site/rumilo -[synu]: https://git.secluded.site/synu -[wt]: https://git.secluded.site/wt -[exe.dev]: https://exe.dev -[jujutsu]: https://jj-vcs.github.io/jj/latest/ ## Installation @@ -173,9 +34,9 @@ cd agent-skills ### With Make -Skills install to `~/.agents/skills/` by default. Override by -setting `SKILL_DIR`. Tab completion works so you don't have to remember -skill names or copy/paste. +Skills install to `~/.agents/skills/` by default. Override by setting +`SKILL_DIR`. Tab completion works so you don't have to remember skill names or +copy/paste. ```sh # List available skills @@ -209,12 +70,12 @@ rm ~/.agents/skills/formatting-commits ### Agent skill directories -| Agent | Dir | -| --------------------------------------- | --------------------- | -| [Crush], [Octofriend], [Shelley], [Amp] | `~/.agents/skills/` | -| [Pi] | `~/.pi/agent/skills/` | -| Anthropic's Claude Code | `~/.claude/skills/` | -| OpenAI's Codex | `~/.codex/skills/` | +| Agent | Dir | +| ------------------------- | --------------------- | +| [Crush], [Shelley], [Amp] | `~/.agents/skills/` | +| [Pi] | `~/.pi/agent/skills/` | +| Anthropic's Claude Code | `~/.claude/skills/` | +| OpenAI's Codex | `~/.codex/skills/` | ## Updating @@ -225,6 +86,441 @@ git pull Symlinked skills update automatically. +## Available skills + +Each skill below lists its install command, source path, and token counts. The +counts are computed by [./skill-stats.go](./skill-stats.go) against +[Synthetic.new]'s [/messages/count_tokens] endpoint. + +[Synthetic.new]: https://synthetic.new +[/messages/count_tokens]: https://dev.synthetic.new/docs/anthropic/messages/count-tokens + +### addressing-code-review-comments + +Finds `CR:` comments in code, gathers feedback, and carries out requested +changes. + +- Install with: `make addressing-code-review-comments` +- Source: [skills/addressing-code-review-comments/](skills/addressing-code-review-comments/) +- Name/desc: 28 tok +- SKILL.md: 133 tok +- Full skill: 161 tok (all `.md` files in the folder) + +### amoliths-opinions + +Applies Amolith's opinions across project setup, tooling, licensing, +dependencies, mise tasks, CLIs, TUIs, MCP servers, web apps, packaging/releases, +testing, and Go-specific ecosystem defaults. + +- Install with: `make amoliths-opinions` +- Source: [skills/amoliths-opinions/](skills/amoliths-opinions/) +- Name/desc: 50 tok +- SKILL.md: 271 tok +- Full skill: 5210 tok (all `.md` files in the folder) + +### ast-grep + +Writes [ast-grep] rules for structural code search and modification. + +[ast-grep]: https://github.com/ast-grep/ast-grep + +- Install with: `make ast-grep` +- Source: [skills/ast-grep/](skills/ast-grep/) +- Name/desc: 43 tok +- SKILL.md: 1272 tok +- Full skill: 2457 tok (all `.md` files in the folder) + +### auditing-repositories + +Audits open source repositories for security, privacy, and unexpected behavior. + +- Install with: `make auditing-repositories` +- Source: [skills/auditing-repositories/](skills/auditing-repositories/) +- Name/desc: 32 tok +- SKILL.md: 480 tok +- Full skill: 707 tok (all `.md` files in the folder) + +### authoring-skills + +Creates and reviews Agent Skills following best practices. Covers skill +structure, frontmatter, and progressive disclosure patterns. + +- Install with: `make authoring-skills` +- Source: [skills/authoring-skills/](skills/authoring-skills/) +- Name/desc: 24 tok +- SKILL.md: 401 tok +- Full skill: 1787 tok (all `.md` files in the folder) + +### backing-up-with-keld + +Writes and manages keld configuration for restic backups. Covers TOML preset +structure, split preset composition (`home@cloud`), config file discovery, +environment variables, and systemd timer setup. + +- Install with: `make backing-up-with-keld` +- Source: [skills/backing-up-with-keld/](skills/backing-up-with-keld/) +- Name/desc: 27 tok +- SKILL.md: 796 tok +- Full skill: 1478 tok (all `.md` files in the folder) + +### collaborating-through-pr-pico-sh + +Collaborates on git patches via [pr.pico.sh], a minimal patchbin service. Covers +both contributing and reviewing patch requests using `git format-patch` and +`git am`. + +[pr.pico.sh]: https://pr.pico.sh + +- Install with: `make collaborating-through-pr-pico-sh` +- Source: [skills/collaborating-through-pr-pico-sh/](skills/collaborating-through-pr-pico-sh/) +- Name/desc: 32 tok +- SKILL.md: 239 tok +- Full skill: 775 tok (all `.md` files in the folder) + +### computing-golden-ratio-typography + +Computes Golden Ratio Typography line heights, spacing units, type scales, and +readable measures from a font file or explicit font metrics. + +- Install with: `make computing-golden-ratio-typography` +- Source: [skills/computing-golden-ratio-typography/](skills/computing-golden-ratio-typography/) +- Name/desc: 38 tok +- SKILL.md: 560 tok +- Full skill: 598 tok (all `.md` files in the folder) + +### cooking + +Guides home cooking as a technically grounded collaborator. Helps plan meals, +use odds and ends, troubleshoot techniques, handle substitutions, and reason +through cuisine-specific flavor logic without flattening regional traditions. + +- Install with: `make cooking` +- Source: [skills/cooking/](skills/cooking/) +- Name/desc: 86 tok +- SKILL.md: 1292 tok +- Full skill: 12751 tok (all `.md` files in the folder) + +### creating-tasks-through-lunatask + +Creates tasks and handoffs in [Lunatask] via [lune]. Tasks are just tasks. +Handoffs capture work to resume later across sessions without filling context +windows. + +[lune]: https://git.secluded.site/lune +[Lunatask]: https://lunatask.app/ + +- Install with: `make creating-tasks-through-lunatask` +- Source: [skills/creating-tasks-through-lunatask/](skills/creating-tasks-through-lunatask/) +- Name/desc: 29 tok +- SKILL.md: 343 tok +- Full skill: 980 tok (all `.md` files in the folder) + +### fallback-code-review + +Provides a fallback-only external review flow via CLI tools like Amp, +CodeRabbit, or Kodus. + +- Install with: `make fallback-code-review` +- Source: [skills/fallback-code-review/](skills/fallback-code-review/) +- Name/desc: 44 tok +- SKILL.md: 228 tok +- Full skill: 272 tok (all `.md` files in the folder) + +### formatting-commits + +Detects a project's commit style from recent history and formats messages +accordingly. Supports Conventional Commits and kernel-style imperative commits. + +- Install with: `make formatting-commits` +- Source: [skills/formatting-commits/](skills/formatting-commits/) +- Name/desc: 34 tok +- SKILL.md: 142 tok +- Full skill: 596 tok (all `.md` files in the folder) + +### frontend-accessibility + +Strives to generate accessible HTML, React, and frontend code following WCAG +2.2 AA. Prioritizes semantic HTML over ARIA, keyboard navigation, and screen +reader compatibility. + +- Install with: `make frontend-accessibility` +- Source: [skills/frontend-accessibility/](skills/frontend-accessibility/) +- Name/desc: 32 tok +- SKILL.md: 498 tok +- Full skill: 2173 tok (all `.md` files in the folder) + +### handling-customer-data + +Queries customer data responsibly—the agent answers questions about the data +without ever seeing it directly. + +- Install with: `make handling-customer-data` +- Source: [skills/handling-customer-data/](skills/handling-customer-data/) +- Name/desc: 29 tok +- SKILL.md: 331 tok +- Full skill: 360 tok (all `.md` files in the folder) + +### humanize + +Removes AI-generated patterns from text like promotional fluff, weasel words, +and mechanical sentence structures. Based on Wikipedia's AI Cleanup research. +Originally from [blader/humanizer]. + +[blader/humanizer]: https://github.com/blader/humanizer + +- Install with: `make humanize` +- Source: [skills/humanize/](skills/humanize/) +- Name/desc: 55 tok +- SKILL.md: 860 tok +- Full skill: 2289 tok (all `.md` files in the folder) + +### ideating-with-bdd + +Guides collaborative discovery of behaviour through structured conversation, +iterating with the user to refine ideas into user stories and Gherkin +scenarios. Works for any language or framework. + +- Install with: `make ideating-with-bdd` +- Source: [skills/ideating-with-bdd/](skills/ideating-with-bdd/) +- Name/desc: 69 tok +- SKILL.md: 730 tok +- Full skill: 1253 tok (all `.md` files in the folder) + +### in-session-review + +Runs an extremely strict maintainability review for abstraction quality, giant +files, and spaghetti-condition growth. + +- Install with: `make in-session-review` +- Source: [skills/in-session-review/](skills/in-session-review/) +- Name/desc: 32 tok +- SKILL.md: 1015 tok +- Full skill: 1047 tok (all `.md` files in the folder) + +### initialising-and-updating-agents-md + +Analyses a codebase and creates or updates `AGENTS.md` to help future agents +work effectively. Discovers commands, conventions, patterns, and gotchas from +the project's source and config files. + +- Install with: `make initialising-and-updating-agents-md` +- Source: [skills/initialising-and-updating-agents-md/](skills/initialising-and-updating-agents-md/) +- Name/desc: 38 tok +- SKILL.md: 395 tok +- Full skill: 433 tok (all `.md` files in the folder) + +### invoking-subagents + +Spawns subagents with restricted tool access for parallel tasks across +repositories. Requires [the Pi coding agent][Pi]. Useful for summarizing git +history or processing large diffs without filling the main context window. + +- Install with: `make invoking-subagents` +- Source: [skills/invoking-subagents/](skills/invoking-subagents/) +- Name/desc: 27 tok +- SKILL.md: 210 tok +- Full skill: 237 tok (all `.md` files in the folder) + +### licensing-with-reuse + +Manages REUSE-compliant licensing with the `reuse` CLI. Covers `reuse annotate`, +`.license` sidecars for prompt files, custom `LicenseRef-...` identifiers, +`LICENSES/`, project conventions, and `reuse lint`. + +- Install with: `make licensing-with-reuse` +- Source: [skills/licensing-with-reuse/](skills/licensing-with-reuse/) +- Name/desc: 33 tok +- SKILL.md: 560 tok +- Full skill: 593 tok (all `.md` files in the folder) + +### maintaining-aur-packages + +Creates and updates AUR packages following Arch packaging standards. Covers +PKGBUILDs for source, `-bin`, and `-git` package types, checksums with +`updpkgsums`, linting with `namcap`, and `.SRCINFO` generation. + +- Install with: `make maintaining-aur-packages` +- Source: [skills/maintaining-aur-packages/](skills/maintaining-aur-packages/) +- Name/desc: 35 tok +- SKILL.md: 689 tok +- Full skill: 1274 tok (all `.md` files in the folder) + +### managing-personal-knowledge + +Works inside a personal knowledge base as an exacting steward, shaping atomic, +concept-oriented notes, a dense wikilink graph, and disciplined tags instead of +dumping transcripts. Tool-agnostic across Markdown vaults like Obsidian, +Logseq, Roam, and SilverBullet. + +- Install with: `make managing-personal-knowledge` +- Source: [skills/managing-personal-knowledge/](skills/managing-personal-knowledge/) +- Name/desc: 68 tok +- SKILL.md: 859 tok +- Full skill: 2172 tok (all `.md` files in the folder) + +### monitoring-with-munin + +Deploys and manages Munin monitoring across servers. Sets up munin-node on +hosts, writes plugins, configures masters, and handles alerts. + +- Install with: `make monitoring-with-munin` +- Source: [skills/monitoring-with-munin/](skills/monitoring-with-munin/) +- Name/desc: 38 tok +- SKILL.md: 907 tok +- Full skill: 1470 tok (all `.md` files in the folder) + +### notifying-through-ntfy + +Sends push notifications via [ntfy.sh] when requested, such as at the end of its +turn. + +[ntfy.sh]: https://ntfy.sh + +- Install with: `make notifying-through-ntfy` +- Source: [skills/notifying-through-ntfy/](skills/notifying-through-ntfy/) +- Name/desc: 26 tok +- SKILL.md: 478 tok +- Full skill: 1541 tok (all `.md` files in the folder) + +### rebasing-with-git + +Manages git rebase workflows from simple rebases to the drop-and-repick +integration branch pattern. Assumes `upstream` remote for forks and `rerere` +enabled. Supports worktrees including `wt`-managed repos. + +- Install with: `make rebasing-with-git` +- Source: [skills/rebasing-with-git/](skills/rebasing-with-git/) +- Name/desc: 41 tok +- SKILL.md: 1057 tok +- Full skill: 1098 tok (all `.md` files in the folder) + +### researching-with-rumilo + +Dispatches AI research subagents via [rumilo] for web search and repository +exploration. Research topics, look up library usage, or explore external +codebases without filling the main context window. + +[rumilo]: https://git.secluded.site/rumilo + +- Install with: `make researching-with-rumilo` +- Source: [skills/researching-with-rumilo/](skills/researching-with-rumilo/) +- Name/desc: 37 tok +- SKILL.md: 557 tok +- Full skill: 594 tok (all `.md` files in the folder) + +### scripting-with-go + +Creates executable Go scripts using a shell trick (not a true shebang). For +automation and tooling outside of Go projects. For scripting in a non-Go +project, copy the header comment and adjust as needed. + +- Install with: `make scripting-with-go` +- Source: [skills/scripting-with-go/](skills/scripting-with-go/) +- Name/desc: 29 tok +- SKILL.md: 333 tok +- Full skill: 362 tok (all `.md` files in the folder) + +### testing-with-gocuke-and-gherkin + +Drives BDD, red/green TDD, and property-based testing in Go projects using +[gocuke] and Gherkin feature files. + +[gocuke]: https://github.com/tenntenn/gocuke + +- Install with: `make testing-with-gocuke-and-gherkin` +- Source: [skills/testing-with-gocuke-and-gherkin/](skills/testing-with-gocuke-and-gherkin/) +- Name/desc: 47 tok +- SKILL.md: 299 tok +- Full skill: 1123 tok (all `.md` files in the folder) + +### toki-pona-dictionary + +Searches the [nimi.li] toki pona dictionary by English meaning. Caches word +data locally after a one-time fetch, then runs offline searches across +definitions and community usage tags. + +[nimi.li]: https://nimi.li + +- Install with: `make toki-pona-dictionary` +- Source: [skills/toki-pona-dictionary/](skills/toki-pona-dictionary/) +- Name/desc: 48 tok +- SKILL.md: 114 tok +- Full skill: 162 tok (all `.md` files in the folder) + +### updating-llm-client-model-lists + +Synchronizes model configurations across Zed, Crush, and Pi from Plexus' +/v1/models endpoint. + +- Install with: `make updating-llm-client-model-lists` +- Source: [skills/updating-llm-client-model-lists/](skills/updating-llm-client-model-lists/) +- Name/desc: 32 tok +- SKILL.md: 1009 tok +- Full skill: 1041 tok (all `.md` files in the folder) + +### using-exe-dev + +Guides working with [exe.dev] VMs, which provide Linux VMs with persistent +disks, instant HTTPS, and built-in auth via SSH. + +[exe.dev]: https://exe.dev + +- Install with: `make using-exe-dev` +- Source: [skills/using-exe-dev/](skills/using-exe-dev/) +- Name/desc: 24 tok +- SKILL.md: 176 tok +- Full skill: 200 tok (all `.md` files in the folder) + +### using-jujutsu + +Guides [jujutsu] (jj) workflows, covering commit selection, change creation, +diff reading, and common pitfalls that trip up agents. + +[jujutsu]: https://jj-vcs.github.io/jj/latest/ + +- Install with: `make using-jujutsu` +- Source: [skills/using-jujutsu/](skills/using-jujutsu/) +- Name/desc: 35 tok +- SKILL.md: 337 tok +- Full skill: 372 tok (all `.md` files in the folder) + +### working-with-zmx + +Manages persistent terminal sessions for background processes, dev servers, and +long-running tasks. Creates sessions, runs commands detached, checks output, and +writes files into sessions over SSH. + +- Install with: `make working-with-zmx` +- Source: [skills/working-with-zmx/](skills/working-with-zmx/) +- Name/desc: 35 tok +- SKILL.md: 241 tok +- Full skill: 276 tok (all `.md` files in the folder) + +### writing-git-tags + +Generates git tag annotations from commit history following Semantic Versioning +and Conventional Commits via [git-format]. + +[git-format]: https://git.secluded.site/git-format + +- Install with: `make writing-git-tags` +- Source: [skills/writing-git-tags/](skills/writing-git-tags/) +- Name/desc: 26 tok +- SKILL.md: 386 tok +- Full skill: 425 tok (all `.md` files in the folder) + +### writing-rust + +Writes idiomatic Rust with strict quality gates: `cargo fmt`, `clippy -- -D +warnings`, and required tests. Enforces `thiserror` for libs, `anyhow` for apps, +warns against common anti-patterns. + +- Install with: `make writing-rust` +- Source: [skills/writing-rust/](skills/writing-rust/) +- Name/desc: 29 tok +- SKILL.md: 607 tok +- Full skill: 1425 tok (all `.md` files in the folder) + ## Contributions Patch requests are in [amolith/llm-projects] on [pr.pico.sh]. You don't need a @@ -254,462 +550,6 @@ complete example workflow. [amolith/llm-projects]: https://pr.pico.sh/r/amolith/llm-projects [pr.pico.sh]: https://pr.pico.sh -## Token stats - -Real token stats as reported by [Synthetic.new]'s [/messages/count_tokens] -endpoint with [./skill-stats.go](./skill-stats.go). - -[Synthetic.new]: https://synthetic.new -[/messages/count_tokens]: https://dev.synthetic.new/docs/anthropic/messages/count-tokens - -``` -=== addressing-code-review-comments === - -Token breakdown: - Name: 9 tokens - Description: 40 tokens - Body: 290 tokens (38 lines) - ─────────────────────────────────────────────── - Total: 339 tokens - -=== amoliths-opinions === - -Token breakdown: - Name: 10 tokens - Description: 89 tokens - Body: 590 tokens (51 lines) - References: - cli.md 385 tokens - golang/baseline.md 860 tokens - golang/cli.md 777 tokens - golang/index.md 599 tokens - golang/licencing.md 206 tokens - golang/mcp.md 524 tokens - golang/mise.md 1331 tokens - golang/packaging.md 947 tokens - golang/preferred-libraries.md 621 tokens - golang/testing.md 486 tokens - golang/tui.md 381 tokens - golang/wails.md 621 tokens - golang/web.md 405 tokens - licencing.md 493 tokens - mcp.md 326 tokens - mise.md 350 tokens - packaging.md 462 tokens - testing.md 345 tokens - tui.md 222 tokens - web.md 400 tokens - ─────────────────────────────────────────────── - Total: 11430 tokens - -=== ast-grep === - -Token breakdown: - Name: 7 tokens - Description: 76 tokens - Body: 2765 tokens (469 lines) - References: - rule_reference.md 2515 tokens - ─────────────────────────────────────────────── - Total: 5363 tokens - -=== auditing-repositories === - -Token breakdown: - Name: 8 tokens - Description: 51 tokens - Body: 1046 tokens (156 lines) - References: - llm-security.md 427 tokens - ─────────────────────────────────────────────── - Total: 1532 tokens - -=== authoring-skills === - -Token breakdown: - Name: 8 tokens - Description: 32 tokens - Body: 861 tokens (107 lines) - References: - checklist.md 301 tokens - patterns.md 880 tokens - process.md 311 tokens - specification.md 1441 tokens - ─────────────────────────────────────────────── - Total: 3834 tokens - -=== backing-up-with-keld === - -Token breakdown: - Name: 9 tokens - Description: 39 tokens - Body: 1742 tokens (235 lines) - References: - config-examples.md 1415 tokens - installation.md 19 tokens - ─────────────────────────────────────────────── - Total: 3224 tokens - -=== collaborating-through-pr-pico-sh === - -Token breakdown: - Name: 12 tokens - Description: 47 tokens - Body: 521 tokens (43 lines) - References: - contributing.md 162 tokens - jujutsu.md 516 tokens - reviewing.md 406 tokens - ─────────────────────────────────────────────── - Total: 1664 tokens - -=== computing-golden-ratio-typography === - -Token breakdown: - Name: 12 tokens - Description: 59 tokens - Body: 1208 tokens (108 lines) - ─────────────────────────────────────────────── - Total: 1279 tokens - -=== cooking === - -Token breakdown: - Name: 6 tokens - Description: 166 tokens - Body: 2805 tokens (117 lines) - References: - cuisines/_index.md 1274 tokens - cuisines/caribbean/dish-boundaries.md 232 tokens - cuisines/caribbean/overview.md 730 tokens - cuisines/caribbean/pantry-techniques.md 399 tokens - cuisines/caribbean/regions.md 450 tokens - cuisines/chinese-regional/overview.md 1140 tokens - cuisines/chinese-regional/regions.md 500 tokens - cuisines/chinese-regional/substitutions.md 225 tokens - cuisines/chinese-regional/techniques-pantry.md 278 tokens - cuisines/german.md 642 tokens - cuisines/hawaiian.md 931 tokens - cuisines/indian/overview.md 873 tokens - cuisines/indian/pantry-substitutions.md 267 tokens - cuisines/indian/regions-communities.md 524 tokens - cuisines/indian/techniques.md 243 tokens - cuisines/irish-british.md 641 tokens - cuisines/japanese-home.md 673 tokens - cuisines/korean.md 761 tokens - cuisines/levantine.md 659 tokens - cuisines/mexican-regional/overview.md 761 tokens - cuisines/mexican-regional/pantry-substitutions.md 288 tokens - cuisines/mexican-regional/regions.md 402 tokens - cuisines/mexican-regional/techniques.md 237 tokens - cuisines/north-african-middle-eastern/egypt.md 146 tokens - cuisines/north-african-middle-eastern/gulf.md 149 tokens - cuisines/north-african-middle-eastern/maghreb.md 218 tokens - cuisines/north-african-middle-eastern/overview.md 820 tokens - cuisines/north-african-middle-eastern/pantry-techniques.md 266 tokens - cuisines/north-african-middle-eastern/persian.md 159 tokens - cuisines/sichuan/flavor-profiles.md 301 tokens - cuisines/sichuan/overview.md 774 tokens - cuisines/sichuan/pantry.md 225 tokens - cuisines/sichuan/pivots-history.md 151 tokens - cuisines/sichuan/techniques.md 195 tokens - example-conversations.md 1873 tokens - foundations.md 1335 tokens - introductions.md 532 tokens - searching-sources.md 693 tokens - substitutions-and-pivots.md 1086 tokens - techniques.md 2584 tokens - ─────────────────────────────────────────────── - Total: 27614 tokens - -=== creating-tasks-through-lunatask === - -Token breakdown: - Name: 12 tokens - Description: 41 tokens - Body: 741 tokens (88 lines) - References: - cli.md 218 tokens - handoff.md 1035 tokens - installing-lune.md 65 tokens - ─────────────────────────────────────────────── - Total: 2112 tokens - -=== fallback-code-review === - -Token breakdown: - Name: 7 tokens - Description: 79 tokens - Body: 489 tokens (56 lines) - ─────────────────────────────────────────────── - Total: 575 tokens - -=== formatting-commits === - -Token breakdown: - Name: 8 tokens - Description: 57 tokens - Body: 310 tokens (34 lines) - References: - conventional-commits.md 362 tokens - installing-git-format.md 22 tokens - kernel-style.md 526 tokens - ─────────────────────────────────────────────── - Total: 1285 tokens - -=== frontend-accessibility === - -Token breakdown: - Name: 7 tokens - Description: 52 tokens - Body: 1091 tokens (172 lines) - References: - antipatterns.md 1341 tokens - patterns.md 2279 tokens - ─────────────────────────────────────────────── - Total: 4770 tokens - -=== handling-customer-data === - -Token breakdown: - Name: 9 tokens - Description: 46 tokens - Body: 715 tokens (115 lines) - ─────────────────────────────────────────────── - Total: 770 tokens - -=== humanize === - -Token breakdown: - Name: 6 tokens - Description: 100 tokens - Body: 1886 tokens (152 lines) - References: - DETAILED_PATTERNS.md 1903 tokens - REPLACEMENTS.md 1125 tokens - ─────────────────────────────────────────────── - Total: 5020 tokens - -=== ideating-with-bdd === - -Token breakdown: - Name: 9 tokens - Description: 133 tokens - Body: 1582 tokens (177 lines) - References: - gherkin-reference.md 984 tokens - ─────────────────────────────────────────────── - Total: 2708 tokens - -=== in-session-review === - -Token breakdown: - Name: 7 tokens - Description: 48 tokens - Body: 2248 tokens (182 lines) - ─────────────────────────────────────────────── - Total: 2303 tokens - -=== initialising-and-updating-agents-md === - -Token breakdown: - Name: 12 tokens - Description: 59 tokens - Body: 850 tokens (63 lines) - ─────────────────────────────────────────────── - Total: 921 tokens - -=== invoking-subagents === - -Token breakdown: - Name: 8 tokens - Description: 42 tokens - Body: 450 tokens (20 lines) - ─────────────────────────────────────────────── - Total: 500 tokens - -=== licensing-with-reuse === - -Token breakdown: - Name: 9 tokens - Description: 51 tokens - Body: 1234 tokens (135 lines) - ─────────────────────────────────────────────── - Total: 1294 tokens - -=== maintaining-aur-packages === - -Token breakdown: - Name: 9 tokens - Description: 58 tokens - Body: 1481 tokens (105 lines) - References: - build-patterns.md 1120 tokens - ─────────────────────────────────────────────── - Total: 2668 tokens - -=== managing-personal-knowledge === - -Token breakdown: - Name: 10 tokens - Description: 129 tokens - Body: 1867 tokens (64 lines) - References: - philosophy.md 1426 tokens - scenarios.md 1294 tokens - ─────────────────────────────────────────────── - Total: 4726 tokens - -=== monitoring-with-munin === - -Token breakdown: - Name: 10 tokens - Description: 62 tokens - Body: 1981 tokens (238 lines) - References: - writing-plugins.md 1151 tokens - ─────────────────────────────────────────────── - Total: 3204 tokens - -=== notifying-through-ntfy === - -Token breakdown: - Name: 10 tokens - Description: 37 tokens - Body: 1052 tokens (89 lines) - References: - ht.md 383 tokens - httpie.md 385 tokens - nodejs.md 541 tokens - python.md 517 tokens - wget.md 430 tokens - ─────────────────────────────────────────────── - Total: 3355 tokens - -=== rebasing-with-git === - -Token breakdown: - Name: 8 tokens - Description: 71 tokens - Body: 2317 tokens (239 lines) - ─────────────────────────────────────────────── - Total: 2396 tokens - -=== researching-with-rumilo === - -Token breakdown: - Name: 10 tokens - Description: 60 tokens - Body: 1217 tokens (112 lines) - ─────────────────────────────────────────────── - Total: 1287 tokens - -=== scripting-with-go === - -Token breakdown: - Name: 8 tokens - Description: 46 tokens - Body: 727 tokens (141 lines) - ─────────────────────────────────────────────── - Total: 781 tokens - -=== testing-with-gocuke-and-gherkin === - -Token breakdown: - Name: 14 tokens - Description: 77 tokens - Body: 633 tokens (67 lines) - References: - bdd-practices.md 261 tokens - gocuke-api.md 1427 tokens - ─────────────────────────────────────────────── - Total: 2412 tokens - -=== toki-pona-dictionary === - -Token breakdown: - Name: 11 tokens - Description: 86 tokens - Body: 250 tokens (24 lines) - ─────────────────────────────────────────────── - Total: 347 tokens - -=== updating-llm-client-model-lists === - -Token breakdown: - Name: 13 tokens - Description: 47 tokens - Body: 2210 tokens (264 lines) - ─────────────────────────────────────────────── - Total: 2270 tokens - -=== using-exe-dev === - -Token breakdown: - Name: 8 tokens - Description: 32 tokens - Body: 380 tokens (45 lines) - ─────────────────────────────────────────────── - Total: 420 tokens - -=== using-jujutsu === - -Token breakdown: - Name: 9 tokens - Description: 55 tokens - Body: 735 tokens (92 lines) - ─────────────────────────────────────────────── - Total: 799 tokens - -=== working-with-zmx === - -Token breakdown: - Name: 8 tokens - Description: 58 tokens - Body: 522 tokens (69 lines) - ─────────────────────────────────────────────── - Total: 588 tokens - -=== writing-git-tags === - -Token breakdown: - Name: 7 tokens - Description: 38 tokens - Body: 846 tokens (109 lines) - References: - installing-git-format.md 22 tokens - ─────────────────────────────────────────────── - Total: 913 tokens - -=== writing-rust === - -Token breakdown: - Name: 7 tokens - Description: 45 tokens - Body: 1324 tokens (139 lines) - References: - idioms.md 1708 tokens - ─────────────────────────────────────────────── - Total: 3084 tokens - -============================================================ -SUMMARY -============================================================ - -Skills: 35 -Metadata: 2525 tokens -Combined bodies: 40966 tokens -Overall: 107787 tokens -Validation errors: 0 - -Largest skills (by total tokens): - 1. cooking 27614 tokens - 2. amoliths-opinions 11430 tokens - 3. ast-grep 5363 tokens - 4. humanize 5020 tokens - 5. frontend-accessibility 4770 tokens -``` - --- Some other tools if these interested you diff --git a/skill-stats.go b/skill-stats.go index 88606eed5bac184e0aaf926ac730158249fdde7e..3983234e8de648fd0e4dee777e7639fdd1317c44 100755 --- a/skill-stats.go +++ b/skill-stats.go @@ -9,7 +9,6 @@ import ( "io" "net/http" "os" - "os/exec" "path/filepath" "regexp" "sort" @@ -19,9 +18,13 @@ import ( ) const ( - syntheticAPI = "https://api.synthetic.new/anthropic/v1/messages/count_tokens" - model = "hf:moonshotai/Kimi-K2.6" - workerCount = 5 // Number of parallel API workers + syntheticAPI = "https://api.synthetic.new/anthropic/v1/messages/count_tokens" + model = "syn:large:text" + workerCount = 5 // Number of parallel API workers + readmePath = "README.md" + skillsHeading = "## Available skills" + todoParagraph = "TODO: describe me" + maxBodyLines = 500 ) var httpClient = &http.Client{ @@ -33,11 +36,13 @@ type Frontmatter struct { Description string } +// TokenCount breaks down a skill's token usage. Extra is the sum of every +// .md file in the skill folder other than SKILL.md itself. type TokenCount struct { Name int Description int Body int - References map[string]int + Extra int Total int } @@ -60,19 +65,7 @@ type TokenResult struct { Err error } -type SkillComparison struct { - PrevTotal int - PrevMetadata int - PrevBody int - Delta int - MetadataDelta int - BodyDelta int - Percent float64 - IsNew bool -} - func main() { - compare := flag.Bool("compare", false, "Compare with HEAD commit") workers := flag.Int("workers", workerCount, "Number of parallel API workers") flag.Parse() @@ -82,7 +75,6 @@ func main() { os.Exit(1) } - // Start worker pool counter := newTokenCounter(apiKey, *workers) defer counter.Close() @@ -92,33 +84,20 @@ func main() { os.Exit(1) } - // Build comparison map if requested - var comparisons map[string]SkillComparison - if *compare { - comparisons = buildComparisons(skills, counter) - } - - // Sort skills by name for consistent output sort.Slice(skills, func(i, j int) bool { return skills[i].Dir < skills[j].Dir }) - // Print reports - for _, skill := range skills { - var comp *SkillComparison - if comparisons != nil { - if c, ok := comparisons[skill.Dir]; ok { - comp = &c - } - } - printSkillReport(skill, comp) + stats, err := updateReadme(readmePath, skills) + if err != nil { + fmt.Fprintf(os.Stderr, "Error updating README: %v\n", err) + os.Exit(1) } - // Print summary - printSummary(skills, comparisons) + fmt.Fprintf(os.Stderr, "README updated: %d skills maintained, %d errored, %d present in README but missing on disk\n", + stats.Maintained, stats.Errored, stats.Missing) } -// TokenCounter manages a pool of workers for parallel token counting type TokenCounter struct { apiKey string jobs chan TokenJob @@ -132,13 +111,14 @@ func newTokenCounter(apiKey string, workers int) *TokenCounter { jobs: make(chan TokenJob, 100), results: make(chan TokenResult, 100), } - - // Start workers + tc.wg.Add(workers) for i := 0; i < workers; i++ { - tc.wg.Add(1) go tc.worker() } - + go func() { + tc.wg.Wait() + close(tc.results) + }() return tc } @@ -155,13 +135,20 @@ func (tc *TokenCounter) Count(id, text string) { } func (tc *TokenCounter) GetResult() TokenResult { - return <-tc.results + result, ok := <-tc.results + if !ok { + return TokenResult{Err: fmt.Errorf("results channel closed")} + } + return result } func (tc *TokenCounter) TryGetResult() (TokenResult, bool) { select { - case r := <-tc.results: - return r, true + case result, ok := <-tc.results: + if !ok { + return TokenResult{}, false + } + return result, true default: return TokenResult{}, false } @@ -169,8 +156,6 @@ func (tc *TokenCounter) TryGetResult() (TokenResult, bool) { func (tc *TokenCounter) Close() { close(tc.jobs) - tc.wg.Wait() - close(tc.results) } func analyzeSkills(counter *TokenCounter) ([]SkillInfo, error) { @@ -199,12 +184,7 @@ func analyzeSkills(counter *TokenCounter) ([]SkillInfo, error) { } func analyzeSkill(path string, counter *TokenCounter) (SkillInfo, error) { - skill := SkillInfo{ - Dir: filepath.Base(path), - Tokens: TokenCount{ - References: make(map[string]int), - }, - } + skill := SkillInfo{Dir: filepath.Base(path)} // Read SKILL.md skillMdPath := filepath.Join(path, "SKILL.md") @@ -233,40 +213,46 @@ func analyzeSkill(path string, counter *TokenCounter) (SkillInfo, error) { fmt.Fprintf(os.Stderr, "Analyzing %s...\n", skill.Dir) - // Collect all jobs first (no channel use yet) + // Collect token-counting jobs. SKILL.md is counted as its frontmatter + // name+description fields plus the body; every other .md file in the + // folder is counted wholesale. var jobs []TokenJob jobs = append(jobs, TokenJob{ID: fmt.Sprintf("%s:name", skill.Dir), Text: fm.Name}) jobs = append(jobs, TokenJob{ID: fmt.Sprintf("%s:description", skill.Dir), Text: fm.Description}) jobs = append(jobs, TokenJob{ID: fmt.Sprintf("%s:body", skill.Dir), Text: body}) - // Collect reference file jobs (recursively walk subdirectories) - refsPath := filepath.Join(path, "references") - filepath.WalkDir(refsPath, func(refPath string, d os.DirEntry, err error) error { - if err != nil { - if os.IsNotExist(err) { - return nil - } - skill.Errors = append(skill.Errors, fmt.Sprintf("Cannot access references path %s: %v", refPath, err)) + err = filepath.WalkDir(path, func(p string, d os.DirEntry, walkErr error) error { + if walkErr != nil { + skill.Errors = append(skill.Errors, fmt.Sprintf("Cannot walk %s: %v", p, walkErr)) return nil } if d.IsDir() { return nil } - refContent, err := os.ReadFile(refPath) + if !strings.HasSuffix(p, ".md") { + return nil + } + // SKILL.md is already covered by the name/description/body jobs. + if p == skillMdPath { + return nil + } + fileContent, err := os.ReadFile(p) if err != nil { - skill.Errors = append(skill.Errors, fmt.Sprintf("Cannot read reference %s: %v", refPath, err)) + skill.Errors = append(skill.Errors, fmt.Sprintf("Cannot read %s: %v", p, err)) return nil } - // Use path relative to references/ dir as the key - relPath, _ := filepath.Rel(refsPath, refPath) + relPath, _ := filepath.Rel(path, p) jobs = append(jobs, TokenJob{ - ID: fmt.Sprintf("%s:ref:%s", skill.Dir, relPath), - Text: string(refContent), + ID: fmt.Sprintf("%s:file:%s", skill.Dir, relPath), + Text: string(fileContent), }) return nil }) + if err != nil { + skill.Errors = append(skill.Errors, fmt.Sprintf("Walk error: %v", err)) + } - // Interleave enqueue and drain to prevent deadlock + // Interleave enqueue and drain to avoid backpressure on the buffered channels. processResult := func(result TokenResult) { if result.Err != nil { skill.Errors = append(skill.Errors, fmt.Sprintf("Token count failed for %s: %v", result.ID, result.Err)) @@ -283,10 +269,8 @@ func analyzeSkill(path string, counter *TokenCounter) (SkillInfo, error) { skill.Tokens.Description = result.Count case "body": skill.Tokens.Body = result.Count - case "ref": - if len(parts) == 3 { - skill.Tokens.References[parts[2]] = result.Count - } + case "file": + skill.Tokens.Extra += result.Count } } @@ -294,7 +278,6 @@ func analyzeSkill(path string, counter *TokenCounter) (SkillInfo, error) { for _, job := range jobs { counter.Count(job.ID, job.Text) outstanding++ - // Drain any available results to prevent backpressure for { if result, ok := counter.TryGetResult(); ok { processResult(result) @@ -304,19 +287,13 @@ func analyzeSkill(path string, counter *TokenCounter) (SkillInfo, error) { } } } - - // Drain remaining results for outstanding > 0 { result := counter.GetResult() processResult(result) outstanding-- } - // Calculate total - skill.Tokens.Total = skill.Tokens.Name + skill.Tokens.Description + skill.Tokens.Body - for _, count := range skill.Tokens.References { - skill.Tokens.Total += count - } + skill.Tokens.Total = skill.Tokens.Name + skill.Tokens.Description + skill.Tokens.Body + skill.Tokens.Extra return skill, nil } @@ -403,14 +380,14 @@ func validateSkill(skill SkillInfo) []string { } // Check body line count - if skill.BodyLines > 500 { - errors = append(errors, fmt.Sprintf("body has %d lines (recommended: < 500)", skill.BodyLines)) + if skill.BodyLines > maxBodyLines { + errors = append(errors, fmt.Sprintf("body has %d lines (recommended: < %d)", skill.BodyLines, maxBodyLines)) } return errors } -func countTokensAPI(apiKey string, text string) (int, error) { +func countTokensAPI(apiKey, text string) (int, error) { reqBody := map[string]interface{}{ "model": model, "messages": []map[string]string{ @@ -430,7 +407,6 @@ func countTokensAPI(apiKey string, text string) (int, error) { if err != nil { return 0, fmt.Errorf("create request: %w", err) } - req.Header.Set("Authorization", "Bearer "+apiKey) req.Header.Set("Content-Type", "application/json") req.Header.Set("anthropic-version", "2023-06-01") @@ -446,10 +422,7 @@ func countTokensAPI(apiKey string, text string) (int, error) { return 0, fmt.Errorf("API status %d: %s", resp.StatusCode, string(body)) } - var result struct { - InputTokens int `json:"input_tokens"` - } - + var result InputTokens if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return 0, fmt.Errorf("decode response: %w", err) } @@ -457,322 +430,244 @@ func countTokensAPI(apiKey string, text string) (int, error) { return result.InputTokens, nil } -type PrevTokens struct { - Total int - Metadata int - Body int +type InputTokens struct { + InputTokens int `json:"input_tokens"` } -func buildComparisons(currentSkills []SkillInfo, counter *TokenCounter) map[string]SkillComparison { - comparisons := make(map[string]SkillComparison) - - for _, skill := range currentSkills { - prev, err := getSkillTokensFromGit(skill.Dir, counter) - if err != nil { - // Skill is new - if skill.Tokens.Total > 0 { - comparisons[skill.Dir] = SkillComparison{ - PrevTotal: 0, - PrevMetadata: 0, - PrevBody: 0, - Delta: skill.Tokens.Total, - MetadataDelta: skill.Tokens.Name + skill.Tokens.Description, - BodyDelta: skill.Tokens.Body, - Percent: 100.0, - IsNew: true, - } - } - continue - } - - delta := skill.Tokens.Total - prev.Total - metadataDelta := (skill.Tokens.Name + skill.Tokens.Description) - prev.Metadata - bodyDelta := skill.Tokens.Body - prev.Body - var percent float64 - if prev.Total > 0 { - percent = (float64(delta) / float64(prev.Total)) * 100 - } - - if delta != 0 || metadataDelta != 0 || bodyDelta != 0 { - comparisons[skill.Dir] = SkillComparison{ - PrevTotal: prev.Total, - PrevMetadata: prev.Metadata, - PrevBody: prev.Body, - Delta: delta, - MetadataDelta: metadataDelta, - BodyDelta: bodyDelta, - Percent: percent, - IsNew: false, - } - } - } +// updateStats summarises what updateReadme did. +type updateStats struct { + Maintained int // skills on disk whose section was (re)written + Errored int // skills with one or more errors + Missing int // sections in README with no matching skill on disk +} - return comparisons +// parsedSection is one ### skill-name block from the existing README. The +// script rewrites the heading and bullets of every maintained section but +// preserves the paragraph verbatim. For sections whose skill has disappeared +// from disk (orphans), raw is emitted unchanged. +type parsedSection struct { + name string + paragraph []string // prose between heading and first bullet (preserved) + raw []string // full original section text (used for orphan passthrough) } -func getSkillTokensFromGit(skillDir string, counter *TokenCounter) (PrevTokens, error) { - // Get file from HEAD - skillPath := fmt.Sprintf("skills/%s/SKILL.md", skillDir) - cmd := exec.Command("git", "show", fmt.Sprintf("HEAD:%s", skillPath)) - output, err := cmd.Output() - if err != nil { - return PrevTokens{}, err - } +// updateReadme rewrites the ## Available Skills region of the README: it +// regenerates each section's heading and bullet list, preserving the +// author-written paragraph; inserts a TODO scaffold for skills present on disk +// but not in the README; and leaves sections whose skill is missing from disk +// untouched (with a stderr warning). Everything outside the region is passed +// through byte-for-byte. +func updateReadme(path string, skills []SkillInfo) (updateStats, error) { + var stats updateStats - // Parse frontmatter and body - fm, body, err := parseFrontmatter(string(output)) + data, err := os.ReadFile(path) if err != nil { - return PrevTokens{}, err + return stats, fmt.Errorf("cannot read %s: %w", path, err) } + lines := strings.Split(string(data), "\n") - // Collect all jobs first (no channel use yet) - var jobs []TokenJob - jobs = append(jobs, TokenJob{ID: fmt.Sprintf("prev:%s:name", skillDir), Text: fm.Name}) - jobs = append(jobs, TokenJob{ID: fmt.Sprintf("prev:%s:description", skillDir), Text: fm.Description}) - jobs = append(jobs, TokenJob{ID: fmt.Sprintf("prev:%s:body", skillDir), Text: body}) - - // Get reference files from HEAD - refsPath := fmt.Sprintf("skills/%s/references", skillDir) - cmd = exec.Command("git", "ls-tree", "-r", "--name-only", "HEAD", refsPath) - output, err = cmd.Output() - if err != nil { - // Log non-fatal git errors (e.g., refs directory doesn't exist in HEAD) - if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() != 0 { - fmt.Fprintf(os.Stderr, "Warning: cannot list git refs for %s (may not exist in HEAD)\n", refsPath) + headingIdx := -1 + for i, l := range lines { + if strings.TrimSpace(l) == skillsHeading { + headingIdx = i + break } - } else { - refPaths := strings.Split(strings.TrimSpace(string(output)), "\n") - for _, refPath := range refPaths { - if refPath == "" { - continue - } - cmd = exec.Command("git", "show", fmt.Sprintf("HEAD:%s", refPath)) - refContent, err := cmd.Output() - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: cannot read %s from HEAD: %v\n", refPath, err) - continue - } - jobs = append(jobs, TokenJob{ID: fmt.Sprintf("prev:%s:ref", skillDir), Text: string(refContent)}) + } + if headingIdx == -1 { + return stats, fmt.Errorf("could not find %q in %s", skillsHeading, path) + } + + regionEnd := len(lines) + for i := headingIdx + 1; i < len(lines); i++ { + if strings.HasPrefix(strings.TrimSpace(lines[i]), "## ") { + regionEnd = i + break } } - // Interleave enqueue and drain to prevent deadlock - prev := PrevTokens{} - processResult := func(result TokenResult) { - if result.Err != nil { - fmt.Fprintf(os.Stderr, "Warning: token count failed for %s: %v\n", result.ID, result.Err) - return + // Leading = lines between the heading and the first ### section. + firstSectionIdx := regionEnd + for i := headingIdx + 1; i < regionEnd; i++ { + if strings.HasPrefix(strings.TrimSpace(lines[i]), "### ") { + firstSectionIdx = i + break } - parts := strings.SplitN(result.ID, ":", 3) - if len(parts) < 3 { - prev.Total += result.Count - return + } + leading := lines[headingIdx+1 : firstSectionIdx] + + // Parse sections and whatever follows them (trailing). + var existing []parsedSection + i := firstSectionIdx + for i < regionEnd { + if !strings.HasPrefix(strings.TrimSpace(lines[i]), "### ") { + break // non-section content ends the section run; rest is trailing } - switch parts[2] { - case "name": - prev.Metadata += result.Count - case "description": - prev.Metadata += result.Count - case "body": - prev.Body += result.Count - case "ref": - // References are counted in total but not metadata or body + name := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(lines[i]), "### ")) + + bodyStart := i + 1 + bodyEnd := regionEnd + for j := bodyStart; j < regionEnd; j++ { + if strings.HasPrefix(strings.TrimSpace(lines[j]), "### ") { + bodyEnd = j + break + } } - prev.Total += result.Count - } + raw := lines[i:bodyEnd] + body := lines[bodyStart:bodyEnd] - outstanding := 0 - for _, job := range jobs { - counter.Count(job.ID, job.Text) - outstanding++ - // Drain any available results to prevent backpressure - for { - if result, ok := counter.TryGetResult(); ok { - processResult(result) - outstanding-- - } else { + bulletStart := -1 + for k, bl := range body { + if strings.HasPrefix(strings.TrimSpace(bl), "- ") { + bulletStart = k break } } - } + var paragraph []string + if bulletStart == -1 { + paragraph = body + } else { + paragraph = body[:bulletStart] + } + paragraph = trimLeadingBlanks(trimTrailingBlanks(paragraph)) - // Drain remaining results - for outstanding > 0 { - result := counter.GetResult() - processResult(result) - outstanding-- + existing = append(existing, parsedSection{name: name, paragraph: paragraph, raw: raw}) + i = bodyEnd } + trailing := lines[i:regionEnd] - return prev, nil -} - -func printSkillReport(skill SkillInfo, comp *SkillComparison) { - fmt.Printf("\n=== %s ===\n", skill.Dir) - - if len(skill.Errors) > 0 { - fmt.Println("\nValidation errors:") - for _, err := range skill.Errors { - fmt.Printf(" ✗ %s\n", err) - } + existingByPos := existing + existingByName := make(map[string]parsedSection, len(existing)) + for _, e := range existing { + existingByName[e.name] = e } - fmt.Println("\nToken breakdown:") - fmt.Printf(" Name: %5d tokens\n", skill.Tokens.Name) - fmt.Printf(" Description: %5d tokens\n", skill.Tokens.Description) - fmt.Printf(" Body: %5d tokens (%d lines)\n", skill.Tokens.Body, skill.BodyLines) - - if len(skill.Tokens.References) > 0 { - fmt.Println(" References:") - // Sort reference names for consistent output - refNames := make([]string, 0, len(skill.Tokens.References)) - for name := range skill.Tokens.References { - refNames = append(refNames, name) + desired := make(map[string]bool, len(skills)) + for _, s := range skills { + desired[s.Dir] = true + if len(s.Errors) > 0 { + stats.Errored++ } - sort.Strings(refNames) - - for _, name := range refNames { - count := skill.Tokens.References[name] - fmt.Printf(" %-40s %5d tokens\n", name, count) + } + stats.Maintained = len(skills) + for _, e := range existingByPos { + if !desired[e.name] { + stats.Missing++ } } - fmt.Println(" ───────────────────────────────────────────────") + // Build the rebuilt region. + var regionLines []string + lead := trimTrailingBlanks(leading) + regionLines = append(regionLines, lead...) - // Print total with comparison if available - if comp != nil { - sign := "+" - if comp.Delta < 0 { - sign = "" - } - indicator := "" - if comp.IsNew { - indicator = " [NEW]" - } else if comp.Percent > 20 { - indicator = " ⚠️" - } else if comp.Percent < -20 { - indicator = " ✓" + for _, s := range skills { + paragraph, ok := existingByName[s.Dir] + if ok && len(paragraph.paragraph) > 0 { + regionLines = appendBlankSep(regionLines) + regionLines = append(regionLines, renderSection(s, paragraph.paragraph)...) + } else { + // New skill: scaffold with a TODO placeholder. + if !ok { + fmt.Fprintf(os.Stderr, "Warning: inserting new section for %s (no description yet)\n", s.Dir) + } + regionLines = appendBlankSep(regionLines) + regionLines = append(regionLines, renderSection(s, []string{todoParagraph})...) } - fmt.Printf(" Total: %5d tokens (%s%d, %s%.1f%% from HEAD)%s\n", - skill.Tokens.Total, sign, comp.Delta, sign, comp.Percent, indicator) - } else { - fmt.Printf(" Total: %5d tokens\n", skill.Tokens.Total) } - // Warn if approaching budget - if skill.Tokens.Body > 5000 { - fmt.Println(" ⚠️ Body exceeds recommended 5000 token budget!") - } else if skill.Tokens.Body > 4000 { - fmt.Println(" ⚠️ Body approaching 5000 token budget") + // Orphans: sections in the README whose skill is gone from disk. Emit + // their raw text unchanged and warn. + for _, e := range existingByPos { + if desired[e.name] { + continue + } + fmt.Fprintf(os.Stderr, "Warning: section %q is in README but missing from disk; leaving untouched\n", e.name) + regionLines = appendBlankSep(regionLines) + regionLines = append(regionLines, e.raw...) } -} -func printSummary(skills []SkillInfo, comparisons map[string]SkillComparison) { - fmt.Println("\n" + strings.Repeat("=", 60)) - fmt.Println("SUMMARY") - fmt.Println(strings.Repeat("=", 60)) - - totalTokens := 0 - totalMetadataTokens := 0 - totalBodyTokens := 0 - totalErrors := 0 - totalDelta := 0 - metadataDelta := 0 - bodyDelta := 0 - - for _, skill := range skills { - totalTokens += skill.Tokens.Total - totalMetadataTokens += skill.Tokens.Name + skill.Tokens.Description - totalBodyTokens += skill.Tokens.Body - totalErrors += len(skill.Errors) - if comp, ok := comparisons[skill.Dir]; ok { - totalDelta += comp.Delta - metadataDelta += comp.MetadataDelta - bodyDelta += comp.BodyDelta - } + trail := trimLeadingBlanks(trailing) + if len(trail) > 0 { + regionLines = appendBlankSep(regionLines) + regionLines = append(regionLines, trail...) } - fmt.Printf("\nSkills: %d\n", len(skills)) - if comparisons != nil && metadataDelta != 0 { - fmt.Printf("Metadata: %d tokens (%+d)\n", totalMetadataTokens, metadataDelta) - } else { - fmt.Printf("Metadata: %d tokens\n", totalMetadataTokens) + // Ensure a blank line separates the region from the following ## heading + // (if any), matching markdown convention. + if len(regionLines) > 0 && regionLines[len(regionLines)-1] != "" && regionEnd < len(lines) { + regionLines = append(regionLines, "") } - if comparisons != nil && bodyDelta != 0 { - fmt.Printf("Combined bodies: %d tokens (%+d)\n", totalBodyTokens, bodyDelta) - } else { - fmt.Printf("Combined bodies: %d tokens\n", totalBodyTokens) + + var newLines []string + newLines = append(newLines, lines[:headingIdx]...) + newLines = append(newLines, lines[headingIdx]) // "## Available Skills" + newLines = append(newLines, regionLines...) + newLines = append(newLines, lines[regionEnd:]...) + + tmp := path + ".tmp" + if err := os.WriteFile(tmp, []byte(strings.Join(newLines, "\n")), 0o644); err != nil { + return stats, fmt.Errorf("write temp: %w", err) } - if comparisons != nil && totalDelta != 0 { - fmt.Printf("Overall: %d tokens (%+d from HEAD)\n", totalTokens, totalDelta) - } else { - fmt.Printf("Overall: %d tokens\n", totalTokens) + if err := os.Rename(tmp, path); err != nil { + return stats, fmt.Errorf("rename: %w", err) } - fmt.Printf("Validation errors: %d\n", totalErrors) + return stats, nil +} - // Find largest skills - sort.Slice(skills, func(i, j int) bool { - return skills[i].Tokens.Total > skills[j].Tokens.Total - }) +// renderSection produces the deterministic heading + (preserved) paragraph + +// bullets + optional warning block. When the skill has errors, all three +// token bullets show 0 and a blockquote warning is appended. +func renderSection(s SkillInfo, paragraph []string) []string { + var out []string + out = append(out, "### "+s.Dir, "") + out = append(out, paragraph...) + out = append(out, "") + + nameDesc, bodyTk, totalTk := 0, 0, 0 + if len(s.Errors) == 0 { + nameDesc = s.Tokens.Name + s.Tokens.Description + bodyTk = s.Tokens.Body + totalTk = s.Tokens.Total + } + out = append(out, + fmt.Sprintf("- Install with: `make %s`", s.Dir), + fmt.Sprintf("- Source: [skills/%s/](skills/%s/)", s.Dir, s.Dir), + fmt.Sprintf("- Name/desc: %d tok", nameDesc), + fmt.Sprintf("- SKILL.md: %d tok", bodyTk), + fmt.Sprintf("- Full skill: %d tok (all `.md` files in the folder)", totalTk), + ) + + if len(s.Errors) > 0 { + out = append(out, "", "> ⚠️ **Errors:**") + for _, e := range s.Errors { + out = append(out, "> - "+e) + } + } + return out +} - fmt.Println("\nLargest skills (by total tokens):") - for i := 0; i < 5 && i < len(skills); i++ { - skill := skills[i] - if comp, ok := comparisons[skill.Dir]; ok { - sign := "+" - if comp.Delta < 0 { - sign = "" - } - fmt.Printf(" %d. %-40s %5d tokens (%s%d)\n", - i+1, skill.Dir, skill.Tokens.Total, sign, comp.Delta) - } else { - fmt.Printf(" %d. %-40s %5d tokens\n", i+1, skill.Dir, skill.Tokens.Total) - } +func appendBlankSep(lines []string) []string { + if len(lines) == 0 { + return lines } + if lines[len(lines)-1] != "" { + return append(lines, "") + } + return lines +} - // Show biggest changes if comparing - if comparisons != nil && len(comparisons) > 0 { - type changeEntry struct { - name string - comp SkillComparison - } - var changes []changeEntry - for name, comp := range comparisons { - changes = append(changes, changeEntry{name, comp}) - } - - sort.Slice(changes, func(i, j int) bool { - absI := changes[i].comp.Delta - if absI < 0 { - absI = -absI - } - absJ := changes[j].comp.Delta - if absJ < 0 { - absJ = -absJ - } - return absI > absJ - }) +func trimTrailingBlanks(lines []string) []string { + end := len(lines) + for end > 0 && strings.TrimSpace(lines[end-1]) == "" { + end-- + } + return lines[:end] +} - fmt.Println("\nBiggest changes:") - displayed := 0 - for _, change := range changes { - if displayed >= 5 { - break - } - sign := "+" - if change.comp.Delta < 0 { - sign = "" - } - indicator := "" - if change.comp.IsNew { - indicator = " [NEW]" - } else if change.comp.Percent > 20 { - indicator = " ⚠️" - } else if change.comp.Percent < -20 { - indicator = " ✓" - } - fmt.Printf(" %-40s %s%-5d tokens (%s%.1f%%)%s\n", - change.name, sign, change.comp.Delta, sign, change.comp.Percent, indicator) - displayed++ - } +func trimLeadingBlanks(lines []string) []string { + start := 0 + for start < len(lines) && strings.TrimSpace(lines[start]) == "" { + start++ } + return lines[start:] }