From 4818a8efa26d7d4f5d196f83adb051cffa554c65 Mon Sep 17 00:00:00 2001 From: Greg Kaleka Date: Wed, 5 Aug 2026 12:34:49 -0400 Subject: [PATCH 1/2] Repair the paths the tools-wg21 reorg left behind Moving tools/wg21 to tools-wg21 broke three things that all silently kept working well enough to hide it. install.sh resolved its command list under tools/, so 11 of the 14 TOP_LEVEL entries failed their existence check and were skipped. All three family parents failed too: the parent lives at tools/voice.md, not tools/voice/voice.md. And normalize.md had been renamed to normalize-prompt.md. A run installed 21 commands instead of 35. Both lists are now paths relative to the repo root, matching how skills are already declared, so an entry says where its file actually is. A listed entry that does not resolve now prints a warning naming it, since silently skipping is what let this drift for months. README linked 30 WG21 tools at the old path and 6 how-to files at names they were renamed away from. Arno was retired to tools-wg21/retired/ but its entry stayed in the active section, so it now sits in WG21 Retired with the others. All 97 README links resolve. AGENTS.md documented tools/wg21/ and a tools-retired/ that never existed. --- AGENTS.md | 3 +- README.md | 78 +++++++++++++++++++-------------------- install.sh | 105 +++++++++++++++++++++++++++++++++++------------------ 3 files changed, 111 insertions(+), 75 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 71ddca2..9f7b045 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,8 @@ ## Structure - When any tool is added, moved, or removed, update README.md to reflect the change. -- Tools live in `tools/` or in subdirectory groups (e.g., `tools/wg21/`). Retired tools live in `tools-retired/`. +- Tools live in `tools/` or in sibling groups (`tools-wg21/`) and subdirectory groups (`tools/code/`, `tools/voice/`). Retired tools live in the group's `retired/` subdirectory (e.g., `tools-wg21/retired/`), and their README entry moves to the matching Retired section rather than being deleted. +- A tool is only installable once it is listed in `install.sh`, by its path relative to the repo root. Moving or renaming a tool means updating that list and its README link in the same commit. - Dossiers live in `dossiers/`. These are composite behavioral models - aggregate profiles of populations, not individuals. - Every directory that contains tools or dossiers also contains an `images/` subdirectory for their paired images. - Every `.md` file pairs with `images/.png` in the same directory group. The image filename always matches the `.md` filename (minus extension). diff --git a/README.md b/README.md index 8cfc653..d743950 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ _[how-to/how-to-read-papers.md](how-to/how-to-read-papers.md)_\ Evaluate a paper's quality in three sequential passes, each with exit criteria, from the general idea to the depth a verdict needs. **Vibe-Coding Planner**\ -_[how-to/how-to-vibe-code.md](how-to/how-to-vibe-code.md)_\ +_[how-to/vibe-how-to.md](how-to/vibe-how-to.md)_\ Drive a design document to a complete, tested implementation, scaling planning, research, and review to what the change actually touches, with subagent-only execution and a review-and-amend cycle on every commit. **Fiction Rulebook**\ @@ -129,53 +129,49 @@ _[how-to/how-to-write-news.md](how-to/how-to-write-news.md)_\ Write or audit a news article, feature, analysis, briefing, or press release for structure, leads, sourcing, style, and objectivity. **How to Write a Paper**\ -_[how-to/how-to-write-papers.md](how-to/how-to-write-papers.md)_\ +_[how-to/papers-how-to.md](how-to/papers-how-to.md)_\ Draft a WG21 paper for a delegate who reads in passes and stops when one fails: show, then assert. **Prompt Rulebook**\ -_[how-to/how-to-write-prompts.md](how-to/how-to-write-prompts.md)_\ +_[how-to/prompts-how-to.md](how-to/prompts-how-to.md)_\ Write or audit a prompt, plan, tool, or rule file for unambiguous instructions and efficient context management. **Prose Rulebook**\ -_[how-to/how-to-write-prose.md](how-to/how-to-write-prose.md)_\ +_[how-to/prose-how-to.md](how-to/prose-how-to.md)_\ Revise model-generated prose so it reads as human-written, in sequential editing passes from structure to wording. **Report Rulebook**\ -_[how-to/how-to-write-reports.md](how-to/how-to-write-reports.md)_\ +_[how-to/reports-how-to.md](how-to/reports-how-to.md)_\ Write or audit a report of any type for structure, evidence, uncertainty, sourcing, and format discipline. **Rust Coding Rulebook**\ -_[how-to/how-to-write-rust.md](how-to/how-to-write-rust.md)_\ +_[how-to/rust-how-to.md](how-to/rust-how-to.md)_\ Reference for a model writing or maintaining Rust: layout, naming, ownership, API design, errors, crates, documentation, testing, tooling, performance, async, and unsafe. ## WG21 **Advocatus Diaboli**\ -_[tools/wg21/advocatus.md](tools/wg21/advocatus.md)_\ +_[tools-wg21/advocatus.md](tools-wg21/advocatus.md)_\ Given a WG21 paper, produces a sealed tribunal verdict with surviving objections, certified strengths, and a citation resolution table. -**The Arno**\ -_[tools/wg21/arno.md](tools/wg21/arno.md)_\ -Transforms a WG21 paper into a severity-ranked readability and argumentation findings report. - **Assay**\ -_[tools/wg21/assay.md](tools/wg21/assay.md)_\ +_[tools-wg21/assay.md](tools-wg21/assay.md)_\ Takes a WG21 proposal and delivers a thesis-derived structural verdict with compound dynamics, citation verification, and per-lens findings. **The Auditor**\ -_[tools/wg21/auditor.md](tools/wg21/auditor.md)_\ +_[tools-wg21/auditor.md](tools-wg21/auditor.md)_\ Writes a compliance-checked paper and an inspection report from a WG21 manuscript's raw markdown. **The Herald**\ -_[tools/wg21/herald.md](tools/wg21/herald.md)_\ +_[tools-wg21/herald.md](tools-wg21/herald.md)_\ Given a name in WG21, produces a heraldic pedigree with order, rank, epithets, and a sealed verification from the public record. **Is This C++?**\ -_[tools/wg21/is-this-cpp.md](tools/wg21/is-this-cpp.md)_\ +_[tools-wg21/is-this-cpp.md](tools-wg21/is-this-cpp.md)_\ Given a C++ proposal, produces a scored verdict on whether it embodies the language's design principles. **The Papersmith**\ -_[tools/wg21/papersmith.md](tools/wg21/papersmith.md)_\ +_[tools-wg21/papersmith.md](tools-wg21/papersmith.md)_\ Writes WG21 papers through a six-step pipeline (commission, research, skeleton, body, surface, review) and reviews any paper through a reusable Review Process: mechanical scans, citation integrity, fact check, adversarial evaluation, resolution. **Pick a PR to Review** (skill)\ @@ -183,97 +179,101 @@ _[tools-wg21/pick-pr-review/SKILL.md](tools-wg21/pick-pr-review/SKILL.md)_\ Scans the open PRs across the wg21 repos and names the single one worth reviewing next, ranked by whether the author is waiting on you and broken on how close the PR sits to your recent work. **Reform Reviewer**\ -_[tools/wg21/reform-reviewer.md](tools/wg21/reform-reviewer.md)_\ +_[tools-wg21/reform-reviewer.md](tools-wg21/reform-reviewer.md)_\ Takes a reform document and delivers a green/red framing report with rewrites and a verdict. **Review Paper**\ -_[tools/wg21/review-paper.md](tools/wg21/review-paper.md)_\ +_[tools-wg21/review-paper.md](tools-wg21/review-paper.md)_\ Writes a feedback report from a committee paper by testing every claim against gathered evidence and a multi-tier challenge process. **The Room**\ -_[tools/wg21/room.md](tools/wg21/room.md)_\ +_[tools-wg21/room.md](tools-wg21/room.md)_\ Given any institutional discussion, produces a structural-dynamics diagnosis through six analytical lenses and an eight-tier challenger. **Scribe**\ -_[tools/wg21/scribe.md](tools/wg21/scribe.md)_\ +_[tools-wg21/scribe.md](tools-wg21/scribe.md)_\ Transforms a raw transcript into two-layer minutes - executive summary for the chair, full attributed record for the implementer. **Summarize Papers**\ -_[tools/wg21/summarize-papers.md](tools/wg21/summarize-papers.md)_\ +_[tools-wg21/summarize-papers.md](tools-wg21/summarize-papers.md)_\ Takes a source folder of papers and delivers a campaign briefing plus a public reading-guide paper with per-paper and aggregate summaries. **The Table Read**\ -_[tools/wg21/table-read.md](tools/wg21/table-read.md)_\ +_[tools-wg21/table-read.md](tools-wg21/table-read.md)_\ Given an author's communication, produces three-audience reaction predictions with blind spots, polarization assessment, and a self-check on cost. **The Threadalyzer**\ -_[tools/wg21/threadalyzer.md](tools/wg21/threadalyzer.md)_\ +_[tools-wg21/threadalyzer.md](tools-wg21/threadalyzer.md)_\ Writes a trip report from proceedings by cataloging each delegate's rhetorical patterns, the technical-to-political ratio, and a counterfactual verdict. **Tighten**\ -_[tools/wg21/tighten.md](tools/wg21/tighten.md)_\ +_[tools-wg21/tighten.md](tools-wg21/tighten.md)_\ Takes a bloated document and delivers the same document at half the line count with voice preserved. **The Vasa**\ -_[tools/wg21/vasa.md](tools/wg21/vasa.md)_\ +_[tools-wg21/vasa.md](tools-wg21/vasa.md)_\ Given cross-room committee material, produces a coherence report measuring which of Stroustrup's 24 structural principles are under stress. ## WG21 Retired +**The Arno**\ +_[tools-wg21/retired/arno.md](tools-wg21/retired/arno.md)_\ +Transforms a WG21 paper into a severity-ranked readability and argumentation findings report. + **Chud Whisperer**\ -_[tools/wg21/retired/chud-whisperer.md](tools/wg21/retired/chud-whisperer.md)_\ +_[tools-wg21/retired/chud-whisperer.md](tools-wg21/retired/chud-whisperer.md)_\ On load, becomes a Discord-native senior C++ engineer who drops deep technical knowledge in bro-casual register while trash-talking the committee and recommending Boost anyway. **Dry Sorter**\ -_[tools/wg21/retired/dry-sorter.md](tools/wg21/retired/dry-sorter.md)_\ +_[tools-wg21/retired/dry-sorter.md](tools-wg21/retired/dry-sorter.md)_\ On load, becomes a terse, deadpan C++ expert who corrects you in fewer words than you used to be wrong. **Grand Chamberlain**\ -_[tools/wg21/retired/grand-chamberlain.md](tools/wg21/retired/grand-chamberlain.md)_\ +_[tools-wg21/retired/grand-chamberlain.md](tools-wg21/retired/grand-chamberlain.md)_\ Given a political landscape with shifting factions, produces a survival strategy built on reframing weakness as principle, outlasting opponents through delay, and making the audience believe the outcome was their idea. **Legendary Lecturer**\ -_[tools/wg21/retired/legendary-lecturer.md](tools/wg21/retired/legendary-lecturer.md)_\ +_[tools-wg21/retired/legendary-lecturer.md](tools-wg21/retired/legendary-lecturer.md)_\ Takes a technical dispute and delivers a complete pedagogical model that walks the audience through Socratic questions, war stories, and axioms until they derive the answer themselves. **Plovdiv Assassin**\ -_[tools/wg21/retired/plovdiv-assassin.md](tools/wg21/retired/plovdiv-assassin.md)_\ +_[tools-wg21/retired/plovdiv-assassin.md](tools-wg21/retired/plovdiv-assassin.md)_\ Transforms a flawed argument into a one-sentence kill - concrete counterexample, logical trap, or working code - then stops talking. **Shenyueguan**\ -_[tools/wg21/retired/shenyueguan.md](tools/wg21/retired/shenyueguan.md)_\ +_[tools-wg21/retired/shenyueguan.md](tools-wg21/retired/shenyueguan.md)_\ Given a committee paper, produces a formal multi-phase review record with verified citations, adversarial self-challenge, and a sealed verdict of approved, returned, or deferred. **Der Werkprüfer**\ -_[tools/wg21/retired/werkprufer.md](tools/wg21/retired/werkprufer.md)_\ +_[tools-wg21/retired/werkprufer.md](tools-wg21/retired/werkprufer.md)_\ Writes a German-flavored formal inspection report - complete with Werkmeister counter-challenge and Prufsiegel verdict - from a committee paper's claims tested against public evidence and author testimony. **WG21-Host**\ -_[tools/wg21/retired/wg21-host.md](tools/wg21/retired/wg21-host.md)_\ +_[tools-wg21/retired/wg21-host.md](tools-wg21/retired/wg21-host.md)_\ Takes evidence and arranges it so the reader draws the conclusion in the white space between paragraphs, feeling smart rather than argued at. **WG21-Lawyer**\ -_[tools/wg21/retired/wg21-lawyer.md](tools/wg21/retired/wg21-lawyer.md)_\ +_[tools-wg21/retired/wg21-lawyer.md](tools-wg21/retired/wg21-lawyer.md)_\ Given a technical position and its evidence, produces a precision-formatted persuasive paper that frames the argument, quotes the opposition into agreement, and writes the straw polls the room wants to pass. **WG21-Notary**\ -_[tools/wg21/retired/wg21-notary.md](tools/wg21/retired/wg21-notary.md)_\ +_[tools-wg21/retired/wg21-notary.md](tools-wg21/retired/wg21-notary.md)_\ Transforms a specification into a record of verified exhibits - quotes checked character by character, code traced to repositories, facts placed adjacent - where the reader's own conclusion is the only verdict delivered. **WG21-Persuader**\ -_[tools/wg21/retired/wg21-persuader.md](tools/wg21/retired/wg21-persuader.md)_\ +_[tools-wg21/retired/wg21-persuader.md](tools-wg21/retired/wg21-persuader.md)_\ Given a proposal, produces a coalition strategy that maps the room's existing positions, decomposes the ask into independently agreeable steps, and makes consensus feel like the committee's own idea. **WG21-Scribe**\ -_[tools/wg21/retired/wg21-scribe.md](tools/wg21/retired/wg21-scribe.md)_\ +_[tools-wg21/retired/wg21-scribe.md](tools-wg21/retired/wg21-scribe.md)_\ Writes a sourced, neutral committee paper - evidence presented, tradeoffs named, contributors credited, nothing recommended - from a design question or specification gap. **WG21-Supplicant**\ -_[tools/wg21/retired/wg21-supplicant.md](tools/wg21/retired/wg21-supplicant.md)_\ +_[tools-wg21/retired/wg21-supplicant.md](tools-wg21/retired/wg21-supplicant.md)_\ Transforms a request to a WG21 authority into a precisely calibrated diplomatic message that the recipient simply reads as "what a polite email." **WG21-Vintner**\ -_[tools/wg21/retired/wg21-vintner.md](tools/wg21/retired/wg21-vintner.md)_\ +_[tools-wg21/retired/wg21-vintner.md](tools-wg21/retired/wg21-vintner.md)_\ Takes a body of evidence and bottles it as a patient, zero-ask informational paper that sits in the committee record demanding nothing, expiring never, and delegating all conclusions to the reader. ## Novelist diff --git a/install.sh b/install.sh index a8de432..737ddd5 100644 --- a/install.sh +++ b/install.sh @@ -45,24 +45,33 @@ for arg in "$@"; do done [[ "${UNINSTALL:-}" == "1" ]] && MODE="uninstall" +# Commands, as paths relative to the repo root. The slash command is named after +# the file, so tools-wg21/tighten.md installs as /tighten regardless of where it +# sits in the tree. TOP_LEVEL=( - advocatus.md - auditor.md - boost-review.md - btc-talk.md - code-cleanup.md - code-review.md - herald.md - is-this-cpp.md - lib-review.md - normalize.md - refine-plan.md - research.md - review-paper.md - tighten.md + tools/btc-talk.md + tools/normalize-prompt.md + tools/refine-plan.md + tools/research.md + tools/code/boost-review.md + tools/code/code-cleanup.md + tools/code/code-review.md + tools/code/lib-review.md + tools-wg21/advocatus.md + tools-wg21/auditor.md + tools-wg21/herald.md + tools-wg21/is-this-cpp.md + tools-wg21/review-paper.md + tools-wg21/tighten.md ) -FAMILIES=(voice interview tutor) +# Families are a parent prompt plus a directory of sub-prompts: tools/voice.md +# installs as /voice, and tools/voice/*.md as /voice:. +FAMILIES=( + tools/voice + tools/interview + tools/tutor +) # Skills, as paths relative to the repo root. # @@ -131,37 +140,50 @@ extract_description() { # TARGETS[i] = absolute path under $DEST # SKILL_NAMES[i] / SKILL_SOURCES[i] = skill command name and its source directory plan() { - local src="$1" - local root="$2" + local root="$1" NAMES=() SOURCES=() TARGETS=() SKILL_NAMES=() SKILL_SOURCES=() + MISSING=() + local f base for f in "${TOP_LEVEL[@]}"; do - [[ -f "$src/$f" ]] || continue - NAMES+=("/${f%.md}") - SOURCES+=("$src/$f") - TARGETS+=("$DEST/$f") + # A listed command that does not resolve is a repo error, not a normal + # condition. Silently skipping is how this list drifted out of sync with the + # tree in the first place, so collect it and report at the end. + if [[ ! -f "$root/$f" ]]; then + MISSING+=("$f") + continue + fi + base="$(basename "$f")" + NAMES+=("/${base%.md}") + SOURCES+=("$root/$f") + TARGETS+=("$DEST/$base") done - for family in "${FAMILIES[@]}"; do - if [[ -f "$src/$family/$family.md" ]]; then + local family_path family + for family_path in "${FAMILIES[@]}"; do + family="$(basename "$family_path")" + if [[ -f "$root/$family_path.md" ]]; then NAMES+=("/$family") - SOURCES+=("$src/$family/$family.md") + SOURCES+=("$root/$family_path.md") TARGETS+=("$DEST/$family.md") + else + MISSING+=("$family_path.md") fi - if [[ -d "$src/$family" ]]; then - for f in "$src/$family"/*.md; do + if [[ -d "$root/$family_path" ]]; then + for f in "$root/$family_path"/*.md; do [[ -e "$f" ]] || continue - local base base="$(basename "$f" .md)" [[ "$base" == "$family" ]] && continue NAMES+=("/$family:$base") SOURCES+=("$f") TARGETS+=("$DEST/$family/$base.md") done + else + MISSING+=("$family_path/") fi done @@ -350,7 +372,9 @@ do_uninstall() { done # Drop empty family subdirs we may have created. - for family in "${FAMILIES[@]}"; do + local family_path family + for family_path in "${FAMILIES[@]}"; do + family="$(basename "$family_path")" if [[ -d "$DEST/$family" ]]; then rmdir "$DEST/$family" 2>/dev/null || true fi @@ -381,9 +405,8 @@ do_uninstall() { acquire_source() { if [[ -n "$LOCAL_SRC" ]]; then - SRC="$LOCAL_SRC/tools" ROOT="$LOCAL_SRC" - [[ -d "$SRC" ]] || die "LOCAL_SRC=$LOCAL_SRC has no tools/ subdirectory" + [[ -d "$ROOT/tools" ]] || die "LOCAL_SRC=$LOCAL_SRC has no tools/ subdirectory" echo "Source: local checkout at $LOCAL_SRC" return fi @@ -397,10 +420,11 @@ acquire_source() { echo "Source: downloading ${REPO}@${BRANCH}..." curl -fsSL "$TARBALL_URL" | tar -xz -C "$TMP" - SRC="$(find "$TMP" -maxdepth 2 -type d -name tools | head -n 1)" - [[ -n "$SRC" && -d "$SRC" ]] || die "could not locate tools/ in extracted tarball" - # Skills are listed relative to the repo root, which is tools/'s parent. - ROOT="$(dirname "$SRC")" + # Everything is listed relative to the repo root, which is tools/'s parent. + local tools_dir + tools_dir="$(find "$TMP" -maxdepth 2 -type d -name tools | head -n 1)" + [[ -n "$tools_dir" && -d "$tools_dir" ]] || die "could not locate tools/ in extracted tarball" + ROOT="$(dirname "$tools_dir")" } main() { @@ -410,9 +434,20 @@ main() { acquire_source - plan "$SRC" "$ROOT" + plan "$ROOT" [[ $(( ${#NAMES[@]} + ${#SKILL_NAMES[@]} )) -gt 0 ]] || die "nothing to process" + # Loud, because a missing entry means the lists have drifted from the tree and + # someone is quietly not getting a tool they should have. + if (( ${#MISSING[@]} > 0 )); then + echo >&2 + echo "warning: ${#MISSING[@]} listed entries were not found and will be skipped:" >&2 + for entry in "${MISSING[@]}"; do + echo " $entry" >&2 + done + echo "Fix the TOP_LEVEL or FAMILIES list in install.sh." >&2 + fi + echo if [[ "$MODE" == "install" ]]; then print_plan "install" From 60e1c0a14d02e0109c1df1b16d60a249857e1283 Mon Sep 17 00:00:00 2001 From: Greg Kaleka Date: Wed, 5 Aug 2026 12:49:46 -0400 Subject: [PATCH 2/2] Publish the repo as a Claude Code plugin marketplace Adds .claude-plugin/marketplace.json and plugin.json so Claude Code users can install with /plugin marketplace add cppalliance/tools-public and get updates and uninstall handled for them. No files move: plugin.json points at the existing tools/ and tools-wg21/ layout through its custom path fields. This does not replace install.sh, which stays the only path that installs into ~/.cursor/skills/ for Cursor users. The manifest lists the same tools install.sh does, and two hand-maintained copies of that list is exactly how the installer drifted from the tree in the first place. install.sh stays the source of truth and scripts/sync_plugin_manifest.py regenerates the manifest from it. Verify with sync_plugin_manifest.py --check, which catches a stale manifest, and claude plugin validate --strict, which catches a listed path that is not in the tree. Both are documented in AGENTS.md and INSTALL.md and run by hand, since the repo has no CI. --- .claude-plugin/marketplace.json | 15 +++++++ .claude-plugin/plugin.json | 35 +++++++++++++++ AGENTS.md | 2 + INSTALL.md | 18 ++++++++ scripts/sync_plugin_manifest.py | 78 +++++++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .claude-plugin/plugin.json create mode 100755 scripts/sync_plugin_manifest.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..afaf3fc --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,15 @@ +{ + "name": "cppalliance", + "owner": { + "name": "C++ Alliance", + "url": "https://github.com/cppalliance" + }, + "plugins": [ + { + "name": "tools-public", + "source": "./", + "description": "Prompt-based tools and skills from the C++ Alliance" + } + ], + "description": "Prompt-based tools, skills, and how-to guides from the C++ Alliance" +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..79e88a5 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,35 @@ +{ + "name": "tools-public", + "description": "Prompt-based tools and skills from the C++ Alliance", + "version": "1.0.0", + "author": { + "name": "C++ Alliance", + "url": "https://github.com/cppalliance" + }, + "repository": "https://github.com/cppalliance/tools-public", + "commands": [ + "./tools/btc-talk.md", + "./tools/normalize-prompt.md", + "./tools/refine-plan.md", + "./tools/research.md", + "./tools/code/boost-review.md", + "./tools/code/code-cleanup.md", + "./tools/code/code-review.md", + "./tools/code/lib-review.md", + "./tools-wg21/advocatus.md", + "./tools-wg21/auditor.md", + "./tools-wg21/herald.md", + "./tools-wg21/is-this-cpp.md", + "./tools-wg21/review-paper.md", + "./tools-wg21/tighten.md", + "./tools/voice.md", + "./tools/voice/", + "./tools/interview.md", + "./tools/interview/", + "./tools/tutor.md", + "./tools/tutor/" + ], + "skills": [ + "./tools-wg21/pick-pr-review/" + ] +} diff --git a/AGENTS.md b/AGENTS.md index 9f7b045..2f26622 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,8 @@ - When any tool is added, moved, or removed, update README.md to reflect the change. - Tools live in `tools/` or in sibling groups (`tools-wg21/`) and subdirectory groups (`tools/code/`, `tools/voice/`). Retired tools live in the group's `retired/` subdirectory (e.g., `tools-wg21/retired/`), and their README entry moves to the matching Retired section rather than being deleted. - A tool is only installable once it is listed in `install.sh`, by its path relative to the repo root. Moving or renaming a tool means updating that list and its README link in the same commit. +- `install.sh` is the single source of truth for what ships. After changing `TOP_LEVEL`, `FAMILIES`, or `SKILLS`, run `./scripts/sync_plugin_manifest.py` to regenerate `.claude-plugin/plugin.json` and commit it. Never hand-edit the `commands` or `skills` arrays in that file. +- Before committing a change to any of those lists, verify both: `./scripts/sync_plugin_manifest.py --check` catches a manifest that has fallen behind `install.sh`, and `claude plugin validate . --strict` catches an entry pointing at a file that is not in the tree. There is no CI, so this check is yours to run. - Dossiers live in `dossiers/`. These are composite behavioral models - aggregate profiles of populations, not individuals. - Every directory that contains tools or dossiers also contains an `images/` subdirectory for their paired images. - Every `.md` file pairs with `images/.png` in the same directory group. The image filename always matches the `.md` filename (minus extension). diff --git a/INSTALL.md b/INSTALL.md index 2d299ac..610fb29 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,5 +1,15 @@ # Installing tools-public +Two ways to install. **Claude Code users can use the plugin marketplace**, which handles updates and uninstall for you: + +``` +/plugin marketplace add cppalliance/tools-public +/plugin install tools-public@cppalliance +``` + +Refresh later with `/plugin marketplace update`. **Cursor users, and anyone who prefers a plain copy, use the installer below**, which is the only path that installs into `~/.cursor/skills/`. + + The installer registers the tools in this repo as user-level Claude Code slash commands by writing them into `~/.claude/commands/`. After install, each tool is invoked as `/` from any Claude Code session. It also installs **skills**, the directory-based tools that ship a script alongside the prompt. Those go to `~/.claude/skills/` and `~/.cursor/skills/`, since Claude Code and Cursor both read the `SKILL.md` format. They are invoked as `/` in either agent. @@ -85,6 +95,14 @@ SKILLS=( The installer skips any entry without a `SKILL.md`, copies the whole directory to each root in `SKILL_DEST`, and clears the previous copy first so a file dropped upstream does not linger. Uninstall removes a directory only if it exists and still contains a `SKILL.md`. +After editing `TOP_LEVEL`, `FAMILIES`, or `SKILLS`, regenerate the plugin manifest from them and check both agree with the tree: + +```bash +./scripts/sync_plugin_manifest.py # rewrite .claude-plugin/plugin.json +./scripts/sync_plugin_manifest.py --check # exit 1 if it is stale +claude plugin validate . --strict # exit 1 if any listed path is missing +``` + Skills that shell out to a tool the user may not have (`gh`, `python3`) should say so in the `SKILL.md` and fail with a clear message rather than a stack trace. ## What's not included diff --git a/scripts/sync_plugin_manifest.py b/scripts/sync_plugin_manifest.py new file mode 100755 index 0000000..583b380 --- /dev/null +++ b/scripts/sync_plugin_manifest.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Generate .claude-plugin/plugin.json from the lists in install.sh. + +The installer and the plugin manifest describe the same set of tools. Keeping +two hand-maintained lists is what let install.sh drift out of sync with the tree +for months, so install.sh stays the single source of truth and this regenerates +the manifest from it. + + ./scripts/sync_plugin_manifest.py rewrite the manifest + ./scripts/sync_plugin_manifest.py --check exit 1 if it is stale (used in CI) +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +INSTALL_SH = ROOT / "install.sh" +MANIFEST = ROOT / ".claude-plugin" / "plugin.json" + + +def bash_array(source: str, name: str) -> list[str]: + """Pull the entries out of a NAME=( ... ) block, ignoring comments.""" + match = re.search(rf"^{name}=\((.*?)^\)", source, re.S | re.M) + if not match: + raise SystemExit(f"{INSTALL_SH.name}: could not find the {name} array") + return [ + line.strip() + for line in match.group(1).splitlines() + if line.strip() and not line.strip().startswith("#") + ] + + +def build_manifest() -> dict: + source = INSTALL_SH.read_text() + + commands = [f"./{path}" for path in bash_array(source, "TOP_LEVEL")] + for family in bash_array(source, "FAMILIES"): + # The parent prompt sits next to the directory of its sub-prompts. + commands.append(f"./{family}.md") + commands.append(f"./{family}/") + skills = [f"./{path}/" for path in bash_array(source, "SKILLS")] + + existing = json.loads(MANIFEST.read_text()) if MANIFEST.exists() else {} + # Preserve hand-edited metadata, replace only the generated lists. + return {**existing, "commands": commands, "skills": skills} + + +def main(argv: list[str]) -> int: + manifest = build_manifest() + rendered = json.dumps(manifest, indent=2) + "\n" + + if "--check" in argv: + current = MANIFEST.read_text() if MANIFEST.exists() else "" + if current != rendered: + print( + f"{MANIFEST.relative_to(ROOT)} is out of sync with {INSTALL_SH.name}.\n" + f"Run ./scripts/{Path(__file__).name} and commit the result.", + file=sys.stderr, + ) + return 1 + print(f"{MANIFEST.relative_to(ROOT)} is up to date.") + return 0 + + MANIFEST.parent.mkdir(exist_ok=True) + MANIFEST.write_text(rendered) + print( + f"Wrote {MANIFEST.relative_to(ROOT)}: " + f"{len(manifest['commands'])} command entries, {len(manifest['skills'])} skills." + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:]))