From 25300fbd145f4578ef65b59007639dc6ffba40c7 Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Sat, 17 Oct 2026 02:37:27 -0400 Subject: [PATCH 01/12] feat(install): resolve single skill/agent/hook names; skip optional edges Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WHwM2TcQBo9oHdCrYCtdMn --- scripts/workflow_deps.py | 91 +++++++++++++++++++++++++++++++++------- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/scripts/workflow_deps.py b/scripts/workflow_deps.py index 96e0f1f..4dd8f88 100644 --- a/scripts/workflow_deps.py +++ b/scripts/workflow_deps.py @@ -1,9 +1,13 @@ #!/usr/bin/env python3 -"""Resolve sample workflows (site/data/workflows.json) to what to install. -Stdlib only; used by install-skills.sh and install-skills.ps1. +"""Resolve sample workflows (site/data/workflows.json) or single skills, +agents and hooks to what to install. Stdlib only; used by install-skills.sh +and install-skills.ps1. workflow_deps.py list nametitle per workflow - workflow_deps.py resolve NAME [NAME ...] one " " line each: + workflow_deps.py list-names namekind per skill/agent/hook + workflow_deps.py resolve [WF ...] [--skill NAME ...] + workflow_deps.py resolve-names NAME [NAME ...] + one " " line each: skill e.g. "skill kanban/next" agent agents/.md hook hooks/.py (copied, never wired) @@ -12,9 +16,11 @@ settings.json snippet for hooks copied into HOOKS_DIR -Unknown workflow name: the list goes to stderr and the exit code is 2. +Names may be comma-separated. --skill takes any skill, agent or hook name. +Unknown name: the valid names go to stderr and the exit code is 2. -Closure, starting from every skill/agent/hook a workflow's steps name: +Closure, starting from every skill/agent/hook a workflow's steps name (and +each --skill name): preloads an agent's frontmatter `skills:` (site_map preloads edges) calls a site_map related edge whose target the source also names in its own SKILL.md (agent: its .md) as `name`, /name or name/. @@ -23,7 +29,9 @@ files a relative link in any non-README .md of a selected skill that points into another skill's dir (e.g. ../next/BOARD.md), so no installed skill links to a file that isn't there. -Hooks come only from workflow steps, never from edges. +Hooks come only from workflow steps or --skill, never from edges. +OPTIONAL lists "calls" edges whose source works without the target (it +only suggests it, or has a fallback); they are not followed. """ import glob import json @@ -48,10 +56,28 @@ } +# src -> targets it names but works without; see each source's SKILL.md +OPTIONAL = { + "find-me-a-skill": {"meta-orchestrator", "next", "pair"}, # suggests, runs nothing + "gap-scan": {"divide", "grill-docs", "kanban-setup"}, # offers, doesn't run + "handoff-auto": {"handoff"}, # credit line only + "pr-watch": {"pair"}, # fixes inline without it + "repo-scrub": {"repo-showcase"}, # either order, either alone + "repo-showcase": {"repo-scrub"}, + "score-loop": {"sitrep"}, # pointer only + "smartzone": {"divide", "session-handoff"}, # hook text suggests them + "wayfinder": {"divide", "meta-orchestrator"}, # offered at the end +} + + class UnknownWorkflow(Exception): pass +class UnknownName(Exception): + pass + + def _source_md(root, e, kind): if kind == "skill": return os.path.join(root, *e["source"].split("/"), "SKILL.md") @@ -104,7 +130,7 @@ def __init__(self, root=ROOT): elif "related" in kinds: if src not in texts: texts[src] = _read(_source_md(root, self.by_name[src], self.kinds[src])) - if _names(texts[src], dst): + if _names(texts[src], dst) and dst not in OPTIONAL.get(src, ()): self.deps[src].append(dst) for e in self.entries: if self.kinds[e["name"]] == "skill": @@ -113,13 +139,18 @@ def __init__(self, root=ROOT): def names(self): return [wf["name"] for wf in self.workflows] - def resolve(self, wf_names): - """{"skill": [...], "agent": [...], "hook": [...]} sorted names.""" + def resolve(self, wf_names=(), names=()): + """{"skill": [...], "agent": [...], "hook": [...]} sorted names, for + the union of workflows wf_names and skill/agent/hook names.""" known = {wf["name"]: wf for wf in self.workflows} bad = [n for n in wf_names if n not in known] - if bad or not wf_names: + if bad or not (wf_names or names): raise UnknownWorkflow(", ".join(bad)) + bad = [n for n in names if n not in self.kinds] + if bad: + raise UnknownName(", ".join(bad)) seeds = [s["name"] for n in wf_names for s in known[n]["steps"] if s["kind"] != "human"] + seeds += list(names) seen, todo = set(seeds), list(seeds) while todo: for d in self.deps[todo.pop()]: @@ -131,8 +162,8 @@ def resolve(self, wf_names): out[self.kinds[n]].append(n) return out - def lines(self, wf_names): - r = self.resolve(wf_names) + def lines(self, wf_names=(), names=()): + r = self.resolve(wf_names, names) out = ["skill " + self.by_name[n]["source"] for n in r["skill"]] out += ["agent " + n for n in r["agent"]] out += ["hook " + n for n in r["hook"]] @@ -150,6 +181,23 @@ def listing(g): return ["%s\t%s" % (wf["name"], wf["title"]) for wf in g.workflows] +def name_listing(g): + return ["%s\t%s" % (e["name"], g.kinds[e["name"]]) for e in g.entries] + + +def parse_resolve(args): + """['a', '--skill', 'x,y', 'b'] -> (['a', 'b'], ['x', 'y']).""" + wfs, names, it = [], [], iter(args) + for a in it: + if a == "--skill": + names.append(next(it, "")) + elif a.startswith("--skill="): + names.append(a[len("--skill="):]) + else: + wfs.append(a) + return split_names(wfs), split_names(names) + + def snippet(hooks_dir, hooks): """settings.json text wiring each hook at its path under hooks_dir.""" block = {} @@ -169,7 +217,7 @@ def main(argv): out = sys.stdout if hasattr(out, "reconfigure"): out.reconfigure(newline="\n") # shells read these lines; no \r on Windows - if not argv or argv[0] not in ("list", "resolve", "snippet"): + if not argv or argv[0] not in ("list", "list-names", "resolve", "resolve-names", "snippet"): sys.stderr.write(__doc__) return 1 cmd, rest = argv[0], argv[1:] @@ -183,14 +231,27 @@ def main(argv): if cmd == "list": out.write("\n".join(listing(g)) + "\n") return 0 - names = split_names(rest) + if cmd == "list-names": + out.write("\n".join(name_listing(g)) + "\n") + return 0 + if cmd == "resolve-names": + wfs, names = [], split_names(rest) + if not names: + sys.stderr.write("error: no skill, agent or hook name given\n") + return 2 + else: + wfs, names = parse_resolve(rest) try: - lines = g.lines(names) + lines = g.lines(wfs, names) except UnknownWorkflow as e: msg = "unknown workflow(s): %s" % e if str(e) else "no workflow name given" sys.stderr.write("error: %s\nWorkflows:\n" % msg) sys.stderr.write("".join(" %s\n" % ln for ln in listing(g))) return 2 + except UnknownName as e: + sys.stderr.write("error: unknown skill, agent or hook name(s): %s\nNames:\n" % e) + sys.stderr.write("".join(" %s\n" % ln for ln in name_listing(g))) + return 2 out.write("\n".join(lines) + "\n") return 0 From 816a5d4c8bb6224b9274b8d1a5f836302f88d12b Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Sat, 17 Oct 2026 02:37:27 -0400 Subject: [PATCH 02/12] test: resolve-names, union, standalone install has no dangling links Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WHwM2TcQBo9oHdCrYCtdMn --- scripts/test_workflow_deps.py | 88 +++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/scripts/test_workflow_deps.py b/scripts/test_workflow_deps.py index 450dfff..4ed96bd 100644 --- a/scripts/test_workflow_deps.py +++ b/scripts/test_workflow_deps.py @@ -94,6 +94,94 @@ def test_every_hook_has_wiring(self): self.assertEqual(sorted(hooks), sorted(workflow_deps.HOOK_WIRING)) +LINK_RE = re.compile(r"\]\(([^)\s]+)\)") + + +def copy_like_installer(g, r, dest): + """Lay out a resolve() result as the installers do: flat skill dirs, + agents/ (+ agents/hooks/ once any agent is in), hooks/.""" + for n in r["skill"]: + shutil.copytree(os.path.join(ROOT, g.by_name[n]["source"]), os.path.join(dest, "skills", n), + ignore=shutil.ignore_patterns("__pycache__")) + if r["agent"]: + shutil.copytree(os.path.join(ROOT, "agents", "hooks"), os.path.join(dest, "agents", "hooks"), + ignore=shutil.ignore_patterns("__pycache__")) + for n in r["agent"]: + shutil.copy(os.path.join(ROOT, "agents", n + ".md"), os.path.join(dest, "agents")) + for n in r["hook"]: + os.makedirs(os.path.join(dest, "hooks"), exist_ok=True) + shutil.copy(os.path.join(ROOT, "hooks", n + ".py"), os.path.join(dest, "hooks")) + + +def dangling_links(dest): + """'file: link' for each relative .md link in an installed non-README + file whose target is missing from the install.""" + out = [] + for base, _dirs, files in os.walk(dest): + for f in files: + if not f.endswith(".md") or f == "README.md": + continue + p = os.path.join(base, f) + with open(p, encoding="utf-8-sig") as fh: + text = fh.read() + for url in LINK_RE.findall(text): + path = url.split("#")[0] + if not path or re.match(r"^[a-z][a-z0-9+.-]*:", path, re.I) or path.startswith("/"): + continue + if not os.path.exists(os.path.normpath(os.path.join(base, path))): + out.append("%s: %s" % (os.path.relpath(p, dest).replace(os.sep, "/"), url)) + return out + + +class NamesTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.g = workflow_deps.Graph(ROOT) + + def test_each_kind_alone(self): + self.assertEqual(self.g.resolve(names=["switch"]), {"skill": ["switch"], "agent": [], "hook": []}) + r = self.g.resolve(names=["planner"]) + self.assertEqual(r["agent"], ["planner"]) + self.assertIn("next", r["skill"]) + self.assertIn("agent-hooks", self.g.lines(names=["planner"])) + self.assertEqual(self.g.resolve(names=["spend-guard"]), {"skill": [], "agent": [], "hook": ["spend-guard"]}) + + def test_unknown_name_raises(self): + with self.assertRaises(workflow_deps.UnknownName): + self.g.resolve(names=["switch", "no-such-thing"]) + + def test_union_with_workflow(self): + wf, one = self.g.resolve(["go-public"]), self.g.resolve(names=["switch"]) + both = self.g.resolve(["go-public"], ["switch"]) + for k in both: + self.assertEqual(set(both[k]), set(wf[k]) | set(one[k]), k) + + def test_optional_edges_are_real_and_unfollowed(self): + # each OPTIONAL entry names a real edge source/target, and none is followed + for src, dsts in workflow_deps.OPTIONAL.items(): + self.assertIn(src, self.g.kinds) + for d in dsts: + self.assertIn(d, self.g.kinds, src) + self.assertNotIn(d, self.g.deps[src], "%s still pulls %s" % (src, d)) + + def test_dangling_links_detects(self): + with tempfile.TemporaryDirectory() as tmp: + os.makedirs(os.path.join(tmp, "skills", "a")) + with open(os.path.join(tmp, "skills", "a", "SKILL.md"), "w") as f: + f.write("[ok](SKILL.md#x) [web](https://x.y) [bad](../b/BOARD.md)\n") + with open(os.path.join(tmp, "skills", "a", "README.md"), "w") as f: + f.write("[ignored](../../README.md)\n") + self.assertEqual(dangling_links(tmp), ["skills/a/SKILL.md: ../b/BOARD.md"]) + + def test_every_entry_standalone(self): + # each skill, agent and hook installed alone (with what it needs) + # links to nothing that isn't installed + for n in self.g.kinds: + with self.subTest(n), tempfile.TemporaryDirectory() as tmp: + copy_like_installer(self.g, self.g.resolve(names=[n]), tmp) + self.assertEqual(dangling_links(tmp), []) + + class CliTest(unittest.TestCase): def run_main(self, *argv): out, err = io.StringIO(), io.StringIO() From c9de9c5f2ef816e531036ed3dbf875d0b2afaae2 Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Sat, 17 Oct 2026 02:37:27 -0400 Subject: [PATCH 03/12] docs: pr-watch, find-me-a-skill fallbacks when companion missing Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WHwM2TcQBo9oHdCrYCtdMn --- find-me-a-skill/SKILL.md | 2 +- pr-watch/README.md | 4 ++-- pr-watch/SKILL.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/find-me-a-skill/SKILL.md b/find-me-a-skill/SKILL.md index 028d2ae..37f1775 100644 --- a/find-me-a-skill/SKILL.md +++ b/find-me-a-skill/SKILL.md @@ -12,7 +12,7 @@ Read-only: suggest, never invoke a skill, agent or tool that changes anything. T First that applies: - Text after the command. - The goal of this conversation so far. -- The top 1-3 board tasks, found as the `next` skill's `BOARD.md` says (`~/.claude/skills/next/` or, as the plugin, `kanban/next/`); give one suggestion per task. +- The top 1-3 board tasks, found as the `next` skill's `BOARD.md` says (`~/.claude/skills/next/` or, as the plugin, `kanban/next/`); give one suggestion per task. No `next` skill installed: skip this line. - No repo or no board: ask what the task is, then stop. ## 2. Options (at most 3) diff --git a/pr-watch/README.md b/pr-watch/README.md index 7638a3a..6dd02a9 100644 --- a/pr-watch/README.md +++ b/pr-watch/README.md @@ -36,7 +36,7 @@ Watches your open pull requests (your own repo, or a fork against its upstream) - Cites its thread URL. 3. **Read CI** (`gh pr checks`); a fork PR stuck at `action_required` is reported as waiting on maintainer approval, not as failing. 4. **Fix** the valid threads through [`pair`](../pair/) (one call per PR, every valid thread's path/line/body as the task), which reruns the tests. - - Where subagents can't be spawned, it fixes inline on the head branch with the same gates. + - Where subagents can't be spawned, or `pair` isn't installed, it fixes inline on the head branch with the same gates. 5. **Gate.** Shows the diff summary and drafted replies, and asks before pushing or replying. - A force push needs its own fresh yes even if the push itself was already approved. 6. **Push, then reply and resolve.** @@ -139,4 +139,4 @@ each wait for their own yes: - `gh` authenticated with `repo` scope. - `origin` on GitHub (otherwise it stops and says so). - Write access where you expect to resolve threads. -- `pair`'s agents installed for the fix step. +- Optional: `pair` and its agents for the fix step (without them it fixes inline). diff --git a/pr-watch/SKILL.md b/pr-watch/SKILL.md index 8630576..d375b43 100644 --- a/pr-watch/SKILL.md +++ b/pr-watch/SKILL.md @@ -53,7 +53,7 @@ Classify each unresolved thread, citing its URL: One `pair` call per PR on its existing head branch, every **valid** thread (path, line, body) as the task. Tell it the PR exists: no new PR, no push. disagree/question threads get replies, not diffs. -Can't spawn subagents (so `pair` can't run)? Fix inline on the PR's head branch instead: same valid threads, run the repo's tests, commit; the §5 gates still apply. +Can't spawn subagents, or `pair` isn't installed? Fix inline on the PR's head branch instead: same valid threads, run the repo's tests, commit; the §5 gates still apply. ## 5. Gate, then push and reply From abeba1315a82df89be8edfef2963d2142f481d2a Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Sat, 17 Oct 2026 02:39:56 -0400 Subject: [PATCH 04/12] feat(install): --skill/-Skill and --list-skills/-ListSkills Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WHwM2TcQBo9oHdCrYCtdMn --- scripts/install-skills.ps1 | 43 ++++++++++++++++++++++++-------- scripts/install-skills.sh | 50 +++++++++++++++++++++++++++++--------- 2 files changed, 72 insertions(+), 21 deletions(-) diff --git a/scripts/install-skills.ps1 b/scripts/install-skills.ps1 index cd6e2ea..6ef0989 100644 --- a/scripts/install-skills.ps1 +++ b/scripts/install-skills.ps1 @@ -21,12 +21,18 @@ # skills dir) and their settings.json snippet printed, never # wired. Needs python 3, even with -NoLint. # -ListWorkflows print the workflow names and exit +# -Skill a,b install only those skills, agents or hooks plus what they +# need, same rules as -Workflow; combines with -Workflow +# (union). +# -ListSkills print every skill, agent and hook name and exit param( [switch]$NoLint, [switch]$Force, [switch]$NoShCheck, [string[]]$Workflow, - [switch]$ListWorkflows + [switch]$ListWorkflows, + [string[]]$Skill, + [switch]$ListSkills ) $ErrorActionPreference = "Stop" @@ -70,6 +76,14 @@ if ($ListWorkflows) { & $py.Path @($py.Args) $deps list exit $LASTEXITCODE } +if ($ListSkills) { + if (-not $py) { + [Console]::Error.WriteLine("error: -ListSkills needs python 3 (checked python, python3, py -3).") + exit 1 + } + & $py.Path @($py.Args) $deps list-names + exit $LASTEXITCODE +} function Find-GitBash { # Where Claude Code gets a POSIX shell on Windows: CLAUDE_CODE_GIT_BASH_PATH, @@ -130,23 +144,27 @@ if ($py) { exit 1 } -# -Workflow: resolve before creating or copying anything; an unknown name -# makes the resolver list the workflows and exit non-zero. -$partial = $PSBoundParameters.ContainsKey("Workflow") +# -Workflow / -Skill: resolve before creating or copying anything; an +# unknown name makes the resolver list the valid names and exit non-zero. +$partial = $PSBoundParameters.ContainsKey("Workflow") -or $PSBoundParameters.ContainsKey("Skill") $selSkills = @() $selAgents = @() $selHooks = @() $agentHooks = $false $wfNames = @() +$skillNames = @() if ($partial) { $wfNames = @($Workflow | ForEach-Object { "$_" -split "," } | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $skillNames = @($Skill | ForEach-Object { "$_" -split "," } | ForEach-Object { $_.Trim() } | Where-Object { $_ }) if (-not $py) { - [Console]::Error.WriteLine("error: -Workflow needs python 3 to resolve the workflow (checked python, python3, py -3).") + [Console]::Error.WriteLine("error: -Workflow and -Skill need python 3 to resolve what to install (checked python, python3, py -3).") [Console]::Error.WriteLine("Install Python 3; -NoLint does not skip this step.") exit 1 } $ErrorActionPreference = "Continue" - $resolved = @(& $py.Path @($py.Args) $deps resolve @wfNames) + $resolveArgs = @($wfNames) + foreach ($n in $skillNames) { $resolveArgs += @("--skill", $n) } + $resolved = @(& $py.Path @($py.Args) $deps resolve @resolveArgs) $resolveExit = $LASTEXITCODE $ErrorActionPreference = "Stop" if ($resolveExit -ne 0) { exit $resolveExit } @@ -163,7 +181,9 @@ if ($partial) { } } -New-Item -ItemType Directory -Force -Path $target | Out-Null +if ((-not $partial) -or $selSkills.Count -gt 0) { + New-Item -ItemType Directory -Force -Path $target | Out-Null +} if ((-not $partial) -or $selAgents.Count -gt 0) { New-Item -ItemType Directory -Force -Path $agentsTarget | Out-Null } @@ -231,9 +251,12 @@ if ($partial) { $copiedHooks += $h } - Write-Host "Workflow(s): $($wfNames -join ' ')" - Write-Host "Installed $($installed.Count) skill(s) to $target :" - $installed | ForEach-Object { Write-Host " - $_" } + if ($wfNames.Count -gt 0) { Write-Host "Workflow(s): $($wfNames -join ' ')" } + if ($skillNames.Count -gt 0) { Write-Host "Skill(s): $($skillNames -join ' ')" } + if ($selSkills.Count -gt 0) { + Write-Host "Installed $($installed.Count) skill(s) to $target :" + $installed | ForEach-Object { Write-Host " - $_" } + } if ($skippedForeign.Count -gt 0) { Write-Warning "skipped $($skippedForeign.Count) foreign skill dir(s) without marker (use -Force): $($skippedForeign -join ', ')" } diff --git a/scripts/install-skills.sh b/scripts/install-skills.sh index 000d0f3..bdf24d0 100644 --- a/scripts/install-skills.sh +++ b/scripts/install-skills.sh @@ -23,6 +23,10 @@ # and their settings.json snippet printed, never wired. # Needs python 3, even with --no-lint. # --list-workflows print the workflow names and exit +# --skill install only that skill, agent or hook plus what it +# needs, same rules as --workflow. Repeatable, or +# comma-separated; combines with --workflow (union). +# --list-skills print every skill, agent and hook name and exit set -euo pipefail MARKER=".from-krishp147-skills" @@ -31,9 +35,11 @@ no_lint=0 force=0 no_sh_check=0 list_workflows=0 +list_skills=0 workflows=() +skill_names=() usage() { - echo "Usage: $0 [--no-lint] [--force] [--no-sh-check] [--workflow ]... [--list-workflows]" >&2 + echo "Usage: $0 [--no-lint] [--force] [--no-sh-check] [--workflow ]... [--skill ]... [--list-workflows] [--list-skills]" >&2 exit 1 } while (( $# )); do @@ -48,6 +54,13 @@ while (( $# )); do shift ;; --workflow=*) workflows+=("${1#--workflow=}") ;; + --list-skills) list_skills=1 ;; + --skill) + (( $# >= 2 )) || usage + skill_names+=("$2") + shift + ;; + --skill=*) skill_names+=("${1#--skill=}") ;; *) usage ;; esac shift @@ -84,6 +97,14 @@ if (( list_workflows )); then "$pybin" ${pyargs[@]+"${pyargs[@]}"} "${deps[@]}" list exit 0 fi +if (( list_skills )); then + if [[ -z "$pybin" ]]; then + echo "error: --list-skills needs python 3 (checked python3, python, py -3)." >&2 + exit 1 + fi + "$pybin" ${pyargs[@]+"${pyargs[@]}"} "${deps[@]}" list-names + exit 0 +fi # guard-git's hook command (agents/*.md frontmatter) is POSIX sh. Without # sh, Claude Code can't run the hook and nothing gets blocked, so check @@ -114,21 +135,25 @@ else exit 1 fi -# --workflow: resolve before creating or copying anything; an unknown name -# makes the resolver list the workflows and exit non-zero. +# --workflow / --skill: resolve before creating or copying anything; an +# unknown name makes the resolver list the valid names and exit non-zero. partial=0 sel_skills=() sel_agents=() sel_hooks=() agent_hooks=0 -if (( ${#workflows[@]} )); then +if (( ${#workflows[@]} || ${#skill_names[@]} )); then partial=1 if [[ -z "$pybin" ]]; then - echo "error: --workflow needs python 3 to resolve the workflow (checked python3, python, py -3)." >&2 + echo "error: --workflow and --skill need python 3 to resolve what to install (checked python3, python, py -3)." >&2 echo "Install Python 3; --no-lint does not skip this step." >&2 exit 1 fi - resolved="$("$pybin" ${pyargs[@]+"${pyargs[@]}"} "${deps[@]}" resolve "${workflows[@]}")" || exit $? + resolve_args=(${workflows[@]+"${workflows[@]}"}) + for n in ${skill_names[@]+"${skill_names[@]}"}; do + resolve_args+=(--skill "$n") + done + resolved="$("$pybin" ${pyargs[@]+"${pyargs[@]}"} "${deps[@]}" resolve "${resolve_args[@]}")" || exit $? while IFS= read -r line; do line="${line%$'\r'}" case "$line" in @@ -143,8 +168,8 @@ if (( ${#workflows[@]} )); then fi if (( partial )); then - mkdir -p "$target" - (( ${#sel_agents[@]} )) && mkdir -p "$agents_target" + if (( ${#sel_skills[@]} )); then mkdir -p "$target"; fi + if (( ${#sel_agents[@]} )); then mkdir -p "$agents_target"; fi else mkdir -p "$target" mkdir -p "$agents_target" @@ -211,9 +236,12 @@ if (( partial )); then copied_hooks+=("$h") done - printf 'Workflow(s): %s\n' "${workflows[*]}" - printf 'Installed %d skill(s) to %s:\n' "${#installed[@]}" "$target" - (( ${#installed[@]} )) && printf ' - %s\n' "${installed[@]}" + if (( ${#workflows[@]} )); then printf 'Workflow(s): %s\n' "${workflows[*]}"; fi + if (( ${#skill_names[@]} )); then printf 'Skill(s): %s\n' "${skill_names[*]}"; fi + if (( ${#sel_skills[@]} )); then + printf 'Installed %d skill(s) to %s:\n' "${#installed[@]}" "$target" + if (( ${#installed[@]} )); then printf ' - %s\n' "${installed[@]}"; fi + fi if (( ${#skipped_foreign[@]} )); then printf 'warning: skipped %d foreign skill dir(s) without marker (use --force): %s\n' \ "${#skipped_foreign[@]}" "${skipped_foreign[*]}" >&2 From 02583f6291f9837cb738a166d0f59584e6550d4b Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Sat, 17 Oct 2026 02:39:56 -0400 Subject: [PATCH 05/12] test: installers with --skill alone, hook-only, union, unknown Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WHwM2TcQBo9oHdCrYCtdMn --- scripts/test_workflow_deps.py | 50 +++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/scripts/test_workflow_deps.py b/scripts/test_workflow_deps.py index 4ed96bd..2ed522c 100644 --- a/scripts/test_workflow_deps.py +++ b/scripts/test_workflow_deps.py @@ -311,6 +311,37 @@ def test_unknown_installs_nothing(self): self.assertFalse(os.path.exists(self.tmp + "/skills")) self.assertFalse(os.path.exists(self.agents)) + def test_skill_alone(self): + self.seed() + rc, out, err = self.install(None, "switch") + self.assertEqual(rc, 0, out + err) + self.assertEqual(self.ls(self.skills), ["old-skill", "switch"]) + self.assertEqual(self.ls(self.agents), ["mine.md"]) + self.assertFalse(os.path.exists(self.hooks)) + self.assertNotIn("stale", (out + err).lower()) + + def test_hook_alone_no_skills_dir(self): + rc, out, err = self.install(None, "spend-guard") + self.assertEqual(rc, 0, out + err) + self.assertEqual(self.ls(self.hooks), ["spend-guard.py"]) + self.assertFalse(os.path.exists(self.skills)) + self.assertFalse(os.path.exists(self.agents)) + + def test_skill_and_workflow_union(self): + rc, out, err = self.install("lazy-fix-handoff", "switch,planner") + self.assertEqual(rc, 0, out + err) + want = workflow_deps.Graph(ROOT).resolve(["lazy-fix-handoff"], ["switch", "planner"]) + self.assertEqual(self.ls(self.skills), want["skill"]) + self.assertEqual(self.ls(self.agents), ["hooks", "planner.md"]) + + def test_unknown_skill_installs_nothing(self): + rc, out, err = self.install(None, "no-such-thing") + self.assertEqual(rc, 2) + self.assertIn("switch", out + err) + self.assertFalse(os.path.exists(self.skills)) + self.assertFalse(os.path.exists(self.agents)) + self.assertFalse(os.path.exists(self.hooks)) + def test_full_install_unchanged(self): self.seed() rc, out, err = self.install(None) @@ -325,10 +356,12 @@ def test_full_install_unchanged(self): @unittest.skipUnless(find_bash(), "bash not available") class BashInstallerTest(InstallerMixin, unittest.TestCase): - def install(self, wf): + def install(self, wf, skill=None): args = [find_bash(), os.path.join(ROOT, "scripts", "install-skills.sh").replace("\\", "/")] if wf: args += ["--workflow", wf] + if skill: + args += ["--skill", skill] p = subprocess.run(args, env=self.env(), capture_output=True, text=True, encoding="utf-8") return p.returncode, p.stdout, p.stderr @@ -345,14 +378,27 @@ def test_list_and_repeat_and_equals(self): self.assertIn("repo-scrub", self.ls(self.skills)) self.assertIn("sitrep", self.ls(self.skills)) + def test_list_skills_and_skill_equals(self): + sh = os.path.join(ROOT, "scripts", "install-skills.sh").replace("\\", "/") + p = subprocess.run([find_bash(), sh, "--list-skills"], env=self.env(), capture_output=True, text=True) + self.assertEqual(p.returncode, 0) + self.assertIn("spend-guard\thook", p.stdout) + self.assertFalse(os.path.exists(self.skills)) + p = subprocess.run([find_bash(), sh, "--skill=grilling,ponytail", "--skill", "switch"], + env=self.env(), capture_output=True, text=True) + self.assertEqual(p.returncode, 0, p.stderr) + self.assertEqual(self.ls(self.skills), ["grilling", "ponytail", "switch"]) + @unittest.skipUnless(os.name == "nt" and find_powershell(), "Windows PowerShell not available") class PowerShellInstallerTest(InstallerMixin, unittest.TestCase): - def install(self, wf): + def install(self, wf, skill=None): args = [find_powershell(), "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", os.path.join(ROOT, "scripts", "install-skills.ps1")] if wf: args += ["-Workflow", wf] + if skill: + args += ["-Skill", skill] p = subprocess.run(args, env=self.env(), capture_output=True, text=True) return p.returncode, p.stdout, p.stderr From 25b25e3fbbcf54805edacc5493477ff253233496 Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Sat, 17 Oct 2026 02:41:02 -0400 Subject: [PATCH 06/12] test: README needs line matches resolver Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WHwM2TcQBo9oHdCrYCtdMn --- scripts/test_workflow_deps.py | 14 ++++++++++++++ scripts/workflow_deps.py | 9 +++++++++ 2 files changed, 23 insertions(+) diff --git a/scripts/test_workflow_deps.py b/scripts/test_workflow_deps.py index 2ed522c..a42cb36 100644 --- a/scripts/test_workflow_deps.py +++ b/scripts/test_workflow_deps.py @@ -164,6 +164,20 @@ def test_optional_edges_are_real_and_unfollowed(self): self.assertIn(d, self.g.kinds, src) self.assertNotIn(d, self.g.deps[src], "%s still pulls %s" % (src, d)) + def test_readme_needs_line_matches_resolver(self): + # skills: line right after the italic note; agents: first line of + # their agents/README.md section + with open(os.path.join(ROOT, "agents", "README.md"), encoding="utf-8") as f: + agents_md = f.read() + for n, k in self.g.kinds.items(): + want = workflow_deps.needs_line(self.g, n) + if k == "skill": + with open(os.path.join(ROOT, self.g.by_name[n]["source"], "README.md"), encoding="utf-8") as f: + lines = f.read().split("\n") + self.assertEqual(lines[4], want, "%s/README.md line 5" % n) + elif k == "agent": + self.assertIn("\n## %s\n\n%s\n" % (n, want), agents_md, n) + def test_dangling_links_detects(self): with tempfile.TemporaryDirectory() as tmp: os.makedirs(os.path.join(tmp, "skills", "a")) diff --git a/scripts/workflow_deps.py b/scripts/workflow_deps.py index 4dd8f88..3899f34 100644 --- a/scripts/workflow_deps.py +++ b/scripts/workflow_deps.py @@ -181,6 +181,15 @@ def listing(g): return ["%s\t%s" % (wf["name"], wf["title"]) for wf in g.workflows] +def needs_line(g, name): + """The README line saying what installing `name` alone brings along.""" + r = g.resolve(names=[name]) + others = [n for k in ("skill", "agent", "hook") for n in r[k] if n != name] + if not others: + return "Works on its own." + return "Needs: %s (installed with it)." % ", ".join("`%s`" % n for n in others) + + def name_listing(g): return ["%s\t%s" % (e["name"], g.kinds[e["name"]]) for e in g.entries] From b98ae97424425ce7b39bd36ed7b34821cae92a54 Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Sat, 17 Oct 2026 02:41:02 -0400 Subject: [PATCH 07/12] docs: each README says what it needs, or that it works alone Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WHwM2TcQBo9oHdCrYCtdMn --- agents/README.md | 8 ++++++++ find-me-a-skill/README.md | 2 ++ gap-scan/README.md | 2 ++ grill-docs/README.md | 2 ++ grill-me/README.md | 2 ++ grilling/README.md | 2 ++ handoff-auto/README.md | 2 ++ handoff/README.md | 2 ++ hooks/README.md | 4 ++-- kanban/consult-plan/README.md | 2 ++ kanban/divide/README.md | 2 ++ kanban/kanban-setup/README.md | 2 ++ kanban/next/README.md | 2 ++ kanban/session-handoff/README.md | 2 ++ kanban/update-progress/README.md | 2 ++ lanes/README.md | 2 ++ meta-orchestrator/README.md | 2 ++ pair/README.md | 2 ++ ponytail/README.md | 2 ++ pr-watch/README.md | 2 ++ progress-report/README.md | 2 ++ remind/README.md | 2 ++ repo-scrub/README.md | 2 ++ repo-showcase/README.md | 2 ++ score-loop/README.md | 2 ++ session-bridge/README.md | 2 ++ sitrep/README.md | 2 ++ spend-gate/README.md | 2 ++ switch/README.md | 2 ++ teammate-brief/README.md | 2 ++ wayfinder/README.md | 2 ++ 31 files changed, 68 insertions(+), 2 deletions(-) diff --git a/agents/README.md b/agents/README.md index 4730638..165d0f3 100644 --- a/agents/README.md +++ b/agents/README.md @@ -18,6 +18,8 @@ Installed by `scripts/install-skills.*` into `~/.claude/agents/`, with `hooks/` ## planner +Needs: `divide`, `next` (installed with it). + - **Role:** picks the next single unit of work from the board and writes the kickoff brief. - Read-only: `disallowedTools: Edit, Write, NotebookEdit`; never creates issues. - `maxTurns: 40`. @@ -57,6 +59,8 @@ Installed by `scripts/install-skills.*` into `~/.claude/agents/`, with `hooks/` ## manager +Needs: `handoff-auto` (installed with it). + - **Role:** owns one task end to end in a worktree. - Never implements itself: briefs an implementer, then reviews its work as if it didn't trust it. - `maxTurns: 150`. @@ -109,6 +113,8 @@ Installed by `scripts/install-skills.*` into `~/.claude/agents/`, with `hooks/` ## implementer +Needs: `handoff-auto`, `kanban-setup`, `next`, `session-handoff` (installed with it). + - **Role:** implements one well-specified issue from a kickoff brief on its own branch. - `maxTurns: 200`. - **You pass:** the kickoff brief verbatim + branch prefix + protected branch + `decisions` (from `manager` or the orchestrator). @@ -143,6 +149,8 @@ Installed by `scripts/install-skills.*` into `~/.claude/agents/`, with `hooks/` ## verifier +Needs: `consult-plan`, `next`, `update-progress` (installed with it). + - **Role:** verifies an implementation session and closes the loop. - `maxTurns: 120`. - No guard hook. diff --git a/find-me-a-skill/README.md b/find-me-a-skill/README.md index 36d806e..bd18fef 100644 --- a/find-me-a-skill/README.md +++ b/find-me-a-skill/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Works on its own. + ## What it does Tells you which skill or sample workflow fits a task, which model and diff --git a/gap-scan/README.md b/gap-scan/README.md index d0d2589..e2347d6 100644 --- a/gap-scan/README.md +++ b/gap-scan/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `handoff-auto`, `next` (installed with it). + ## What it does Surveys one or more repos — code, recent commits, docs, open issues, and diff --git a/grill-docs/README.md b/grill-docs/README.md index e81be24..86d0be2 100644 --- a/grill-docs/README.md +++ b/grill-docs/README.md @@ -2,6 +2,8 @@ *Original. Wraps Matt Pocock's `grilling` (MIT). `SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `consult-plan`, `handoff-auto`, `next` (installed with it). + ## What it does Runs the `grilling` interview on a plan or decision and leaves a paper trail in the repo: the full question → recommendation → answer transcript, and one row per settled decision in an append-only register. Everything else about the interview is unchanged. diff --git a/grill-me/README.md b/grill-me/README.md index 6247519..5a69589 100644 --- a/grill-me/README.md +++ b/grill-me/README.md @@ -2,6 +2,8 @@ *By Matt Pocock (MIT), included verbatim. `SKILL.md` is the prompt Claude follows; this file is for you.* +Works on its own. + ## What it does Short alias for [`grilling`](../grilling/): a relentless interview to sharpen a plan or design. diff --git a/grilling/README.md b/grilling/README.md index 3d05817..50f2013 100644 --- a/grilling/README.md +++ b/grilling/README.md @@ -2,6 +2,8 @@ *By Matt Pocock (MIT), included verbatim. `SKILL.md` is the prompt Claude follows; this file is for you.* +Works on its own. + ## What it does Interviews you, one round at a time, until a plan or decision has no unexamined branches left. Every question comes with Claude's recommended answer, so you can just say "yes" to most. diff --git a/handoff-auto/README.md b/handoff-auto/README.md index f03665b..5d953fb 100644 --- a/handoff-auto/README.md +++ b/handoff-auto/README.md @@ -2,6 +2,8 @@ *Derived from Matt Pocock's `handoff` (MIT). `SKILL.md` is the prompt Claude follows; this file is for you.* +Works on its own. + ## What it does Same handoff document as [`handoff`](../handoff/), but Claude (or a skill or subagent) can call it without you typing a command. diff --git a/handoff/README.md b/handoff/README.md index e232012..bf12b97 100644 --- a/handoff/README.md +++ b/handoff/README.md @@ -2,6 +2,8 @@ *By Matt Pocock (MIT); one sentence modified so the file saves under `skilleddocs/handoffs/`, one added for whether to commit it. `SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `handoff-auto` (installed with it). + ## What it does Turns this conversation into a handoff document. A fresh session can pick up the work from it, without the old context. diff --git a/hooks/README.md b/hooks/README.md index 3252ce6..6abcaf8 100644 --- a/hooks/README.md +++ b/hooks/README.md @@ -1,8 +1,8 @@ # hooks/ — optional per-repo hooks -Not installed by a full `install-skills` run; copy what you want into a repo's `.claude/hooks/` and wire it in that repo's `.claude/settings.json`. +Not installed by a full `install-skills` run; copy what you want into a repo's `.claude/hooks/` and wire it in that repo's `.claude/settings.json`. Each hook works on its own. -- `install-skills --workflow ` with a workflow that uses a hook copies it to `~/.claude/hooks/` and prints the user-level `settings.json` snippet below; it never wires it in. +- `install-skills --skill `, or `--workflow ` with a workflow that uses a hook, copies it to `~/.claude/hooks/` and prints the user-level `settings.json` snippet below; it never wires it in. - The agents' own `guard-git.py` lives in [`../agents/hooks/`](../agents/hooks/) and *is* installed, into `~/.claude/agents/hooks/`, or wired by the plugin. - None of these ship with the plugin either. diff --git a/kanban/consult-plan/README.md b/kanban/consult-plan/README.md index f2d5694..b660d88 100644 --- a/kanban/consult-plan/README.md +++ b/kanban/consult-plan/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you. Loop overview: [`kanban/README.md`](../README.md).* +Needs: `next` (installed with it). + ## What it does Grills a proposed change, deviation or new idea against the repo's existing plan (open issues, board, roadmap doc), then records the outcome where the repo already keeps decisions. diff --git a/kanban/divide/README.md b/kanban/divide/README.md index c1dd50e..7bdeafe 100644 --- a/kanban/divide/README.md +++ b/kanban/divide/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you. Loop overview: [`kanban/README.md`](../README.md).* +Needs: `next` (installed with it). + ## What it does Splits an issue (or a described task) that's too big for one session into two or more session-sized issues (about the ~75K-token smart zone, one PR). diff --git a/kanban/kanban-setup/README.md b/kanban/kanban-setup/README.md index 8454710..2641f2a 100644 --- a/kanban/kanban-setup/README.md +++ b/kanban/kanban-setup/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you. Loop overview: [`kanban/README.md`](../README.md).* +Needs: `next` (installed with it). + ## What it does Creates a GitHub Projects (v2) board for a repo if it has none, then backfills issues from work already documented in the repo: planned work, already-done work (created and immediately closed, for history), or both. diff --git a/kanban/next/README.md b/kanban/next/README.md index 1165ba5..61beec9 100644 --- a/kanban/next/README.md +++ b/kanban/next/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you. Loop overview: [`kanban/README.md`](../README.md).* +Works on its own. + ## What it does Shows the next 1–3 tasks to work on, pulled from the repo's GitHub kanban board, with a size and a suggested model for each. diff --git a/kanban/session-handoff/README.md b/kanban/session-handoff/README.md index 54a6a93..d951e60 100644 --- a/kanban/session-handoff/README.md +++ b/kanban/session-handoff/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you. Loop overview: [`kanban/README.md`](../README.md).* +Needs: `handoff-auto`, `kanban-setup`, `next` (installed with it). + ## What it does Ends an implementation session tied to a GitHub issue: writes the handoff document (via `handoff-auto`), adds a `## Board status` section, and moves the issue's card to reflect reality. diff --git a/kanban/update-progress/README.md b/kanban/update-progress/README.md index 56b9905..9cf2991 100644 --- a/kanban/update-progress/README.md +++ b/kanban/update-progress/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you. Loop overview: [`kanban/README.md`](../README.md).* +Needs: `next` (installed with it). + ## What it does Takes a handoff document, **verifies** its claims (git log, tests, code review), then updates the repo's docs, the board and the issue to match what actually happened. diff --git a/lanes/README.md b/lanes/README.md index c61beae..da13155 100644 --- a/lanes/README.md +++ b/lanes/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `consult-plan`, `divide`, `handoff-auto`, `kanban-setup`, `meta-orchestrator`, `next`, `pair`, `progress-report`, `session-handoff`, `update-progress`, `implementer`, `manager`, `planner`, `verifier` (installed with it). + ## What it does Sets how many issues a [`meta-orchestrator`](../meta-orchestrator/) run works on at once. It predicts which files each queued issue will touch, groups issues that share files into the same **lane**, proposes a lane count with a cost line, and on your yes writes the lanes into the run's `scope.md`. diff --git a/meta-orchestrator/README.md b/meta-orchestrator/README.md index bcb6830..37b1268 100644 --- a/meta-orchestrator/README.md +++ b/meta-orchestrator/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; `SETUP.md` (first-run interview + every option) and `ROTATION.md` (handoff / resume) are loaded only when needed. This file is for you.* +Needs: `consult-plan`, `divide`, `handoff-auto`, `kanban-setup`, `lanes`, `next`, `pair`, `progress-report`, `session-handoff`, `update-progress`, `implementer`, `manager`, `planner`, `verifier` (installed with it). + ## What it does Works through a repo's backlog unattended. Claude becomes the **orchestrator**: it never implements. diff --git a/pair/README.md b/pair/README.md index af23a1a..fae20a2 100644 --- a/pair/README.md +++ b/pair/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `handoff-auto`, `manager` (installed with it). + ## What it does Runs **one task** (a GitHub issue or free text) through a `manager` subagent in an isolated git worktree. The manager spawns and reviews an `implementer`, retrying up to 3 rounds. You stay in one interactive session; only your session pushes and opens the PR, at the end. diff --git a/ponytail/README.md b/ponytail/README.md index 89c25ba..b324171 100644 --- a/ponytail/README.md +++ b/ponytail/README.md @@ -2,6 +2,8 @@ *By DietrichGebert (MIT), included verbatim from [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail/blob/84740e3d344b69b4a133201fa519eb37a62a2b2a/skills/ponytail/SKILL.md) (v5.2.0). `SKILL.md` is the prompt Claude follows; this file is for you.* +Works on its own. + ## What it does Puts Claude in "lazy senior developer" mode for the whole session: it solves the task with the smallest change that fully works, and ends its reply with one or two lines saying what it skipped or did not check, plus any risk you must know. diff --git a/pr-watch/README.md b/pr-watch/README.md index 6dd02a9..9fa9472 100644 --- a/pr-watch/README.md +++ b/pr-watch/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `handoff-auto` (installed with it). + ## What it does Watches your open pull requests (your own repo, or a fork against its upstream) until they merge. diff --git a/progress-report/README.md b/progress-report/README.md index 1012afc..3fe8e47 100644 --- a/progress-report/README.md +++ b/progress-report/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `handoff-auto` (installed with it). + ## What it does Turns a run's ledger into a short progress report (1–2 pages) covering **only what happened since the last report** — never overlapping. diff --git a/remind/README.md b/remind/README.md index 5cf8190..2d08f46 100644 --- a/remind/README.md +++ b/remind/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Works on its own. + ## What it does Sets reminders tied to your Claude accounts or to a time. A due reminder shows once at your next prompt, and timed ones also pop up on your desktop at the exact time. diff --git a/repo-scrub/README.md b/repo-scrub/README.md index 0d6055c..6af13e8 100644 --- a/repo-scrub/README.md +++ b/repo-scrub/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Works on its own. + ## What it does Scans a GitHub repo's **full git history** for secrets and oversized files. diff --git a/repo-showcase/README.md b/repo-showcase/README.md index e4544cd..f29eec0 100644 --- a/repo-showcase/README.md +++ b/repo-showcase/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `handoff-auto` (installed with it). + ## What it does Gets a repo's working tree ready to be seen. diff --git a/score-loop/README.md b/score-loop/README.md index 33b3756..2a157ae 100644 --- a/score-loop/README.md +++ b/score-loop/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `handoff-auto`, `spend-gate` (installed with it). + ## What it does Pushes one numeric score up (or down) unattended. Claude tries one idea per commit: diff --git a/session-bridge/README.md b/session-bridge/README.md index a745306..1cfcd11 100644 --- a/session-bridge/README.md +++ b/session-bridge/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `handoff-auto` (installed with it). + ## What it does Coordinates with another running Claude Code session or agent that might be working on the same repo. diff --git a/sitrep/README.md b/sitrep/README.md index d762007..d865b40 100644 --- a/sitrep/README.md +++ b/sitrep/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `handoff-auto` (installed with it). + ## What it does Answers "how's it going" in **10 lines or fewer**. It's a live, spoken answer, not a document. diff --git a/spend-gate/README.md b/spend-gate/README.md index d1f05b5..2f7ef1a 100644 --- a/spend-gate/README.md +++ b/spend-gate/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `handoff-auto` (installed with it). + ## What it does Stops Claude from running a billable action (spinning up a GPU pod, calling a paid API, buying a paid tier, spending cloud credits) without first: diff --git a/switch/README.md b/switch/README.md index 3683d5c..9db4249 100644 --- a/switch/README.md +++ b/switch/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Works on its own. + ## What it does Runs several Claude Code accounts side by side, each with its own config dir, and moves a live conversation from one to another. diff --git a/teammate-brief/README.md b/teammate-brief/README.md index 8344b3a..7b412e3 100644 --- a/teammate-brief/README.md +++ b/teammate-brief/README.md @@ -2,6 +2,8 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `handoff-auto` (installed with it). + ## What it does Writes a handoff document for a person joining or picking up a project. It covers: diff --git a/wayfinder/README.md b/wayfinder/README.md index 5f6bd32..e553d07 100644 --- a/wayfinder/README.md +++ b/wayfinder/README.md @@ -2,6 +2,8 @@ *Adapted from Matt Pocock's `wayfinder` (MIT): GitHub Issues only, our ticket types, smart-zone ticket size, and a hand-off to `divide`. `SKILL.md` is the prompt Claude follows; this file is for you.* +Needs: `consult-plan`, `grill-docs`, `handoff-auto`, `next` (installed with it). + ## What it does Plans an effort too big for one session. It charts a **map** (a GitHub issue labelled `wayfinder:map`) of **decision tickets** (its sub-issues), then resolves one ticket per session until nothing is left to decide. When the way is clear it hands off to `divide`, which turns the destination into board issues. From 344495dda7254baef9525aa31fa31c5d9b2ab31e Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Sat, 17 Oct 2026 02:42:05 -0400 Subject: [PATCH 08/12] feat(site): one-line install per detail page Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WHwM2TcQBo9oHdCrYCtdMn --- scripts/site_detail.py | 20 +++++++++++++++++++- scripts/test_site_detail.py | 9 +++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/scripts/site_detail.py b/scripts/site_detail.py index 5364f85..8ded677 100644 --- a/scripts/site_detail.py +++ b/scripts/site_detail.py @@ -524,7 +524,7 @@ def build_pages(root, entries, meta, template, data_json, site_base="", workflow for fixed in ("term", "cmd", "menu", "echo", "hint", "status", "prompt-box", "comments", "skill-data", "source"): rd.unique_id(fixed) - body = [] + body = [install_section(e, rd.unique_id("install"))] for key, title, md in ordered: body.append(_section(rd, title, md, example=key == "example")) for heading, md in extras: @@ -618,6 +618,24 @@ def missing_example(ordered): return not any(k == "example" for k, _t, _b in ordered) +def install_cmds(name): + """(bash, PowerShell) one-liners that install just this entry from a clone.""" + return ("./scripts/install-skills.sh --skill %s" % name, + ".\\scripts\\install-skills.ps1 -Skill %s" % name) + + +def install_section(e, sid): + """'Install' section, first on every detail page.""" + sh, ps = install_cmds(e["name"]) + note = ("Install just this, from a clone of the repo. It is copied, not wired: " + "the installer prints the settings.json snippet to add." + if e["group"] == "hooks" else + "Install just this, from a clone of the repo. Anything it needs comes with it.") + return ('
\n

Install

\n

%s

\n' + '
%s   # bash, Git Bash, macOS, Linux\n%s   # Windows PowerShell
\n
' + %(esc(sid), esc(sid), esc(note), esc(sh), esc(ps))) + + def _source_section(e): links = [] if e["group"] == "agents": diff --git a/scripts/test_site_detail.py b/scripts/test_site_detail.py index f5f22f8..5780c27 100644 --- a/scripts/test_site_detail.py +++ b/scripts/test_site_detail.py @@ -117,6 +117,15 @@ def test_giscus_embed(self): self.assertNotRegex(page, r'theme[^,;]*https?://', name) self.assertIn("Use cases & tips from the community", page, name) + def test_install_section_first(self): + # Every page opens with its own one-line install, bash + PowerShell. + for name, page in self.pages.items(): + first = re.search(r'

(.*?)

', page) + self.assertEqual(first.groups(), ("install", "Install"), name) + self.assertIn("./scripts/install-skills.sh --skill %s #" % name, page, name) + self.assertIn(".\\scripts\\install-skills.ps1 -Skill %s #" % name, page, name) + self.assertEqual("copied, not wired" in page, self.by[name]["group"] == "hooks", name) + def test_sections_ordered_none_empty(self): titles = [t for _k, t in site_detail.SECTIONS] + ["Source"] h2 = re.compile(r'

(.*?)

') From ad26f7face2fc15027c7b7b52548c4b18fca03d1 Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Sat, 17 Oct 2026 02:42:18 -0400 Subject: [PATCH 09/12] docs(readme): install one skill Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WHwM2TcQBo9oHdCrYCtdMn --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index dab7271..2c59149 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,18 @@ The lint step needs a real Python 3 on `PATH` (`python`, `python3`, or — on Wi If a directory with the same name as one of this repo's skills already exists under `~/.claude/skills/` but has no `.from-krishp147-skills` marker (i.e. you didn't get it from here), the install leaves it untouched, warns, and exits non-zero at the end so you notice. Pass `--force` (sh) / `-Force` (ps1) to replace it anyway. Upgrading from an install made before the marker existed? Every old skill dir looks foreign, so run once with `--force` / `-Force`. +#### Install one skill + +Want just one skill, agent or hook, like `switch`? Name it: + +```bash +./scripts/install-skills.sh --list-skills # every name and its kind +./scripts/install-skills.sh --skill switch # repeat --skill for more +.\scripts\install-skills.ps1 -Skill switch,planner # Windows PowerShell +``` + +Anything it needs comes with it, and its README says what that is on the line under the title ("Works on its own." or "Needs: ..."). `--skill` and `--workflow` can be combined; you get both. Everything below about partial installs applies to `--skill` too. + #### Install one workflow To take only what one [sample workflow](https://www.krishpunjabi.com/skills/workflows) uses instead of everything: From abec80949a5efeb31aacfc5eafd2fbd26c728038 Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Sat, 17 Oct 2026 02:42:18 -0400 Subject: [PATCH 10/12] docs(changelog): install one skill Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WHwM2TcQBo9oHdCrYCtdMn --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5830811..c5fd2bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ version in [`.claude-plugin/plugin.json`](.claude-plugin/plugin.json); ## [0.1.0] - 2026-10-15 +- `install-skills --skill ` / `-Skill ` (#233) installs one skill, agent or hook plus what it needs; `--list-skills` / `-ListSkills` lists the names. Combines with `--workflow`. Optional companions are no longer pulled in (e.g. `find-me-a-skill` no longer brings `meta-orchestrator`; `pr-watch` fixes inline without `pair`). Each README says "Works on its own." or what it needs, each site page shows its one-line install, and a test installs every entry alone and checks it links to nothing missing. - Skills split into categories (#227): Plan and decide, Build and orchestrate, Sessions and accounts, Review and report, Repo and safety, Kanban board, plus agents and hooks. One list in `site/data/meta.json` (`categories`, was `map_groups`) drives the site home, `/map`, each page's tag and the README "What's here" tables; a test fails if they drift. - skill pages: more spacing, bullets - New `remind` skill (#226): `/remind when 's session|weekly limit resets`, `when is under N%`, `in 2h ...`, `at 3pm ...`, `list`, `cancel `. Reset times come from `switch`'s usage cache (unknown account, no cache or an already-reset window is an error, never a guess). Due reminders show once at the next prompt on any account through `switch`'s hook (a one-line hook without switch); timed ones also get a one-shot desktop notification (Windows scheduled task with a PowerShell toast, `msg` fallback; `at` on macOS/Linux). `reminders.json` sits next to the switch registry (`~/.claude-remind/` without it). From 3476cca0744d115bc1d355beaa57c9ddf18d6e0f Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Sat, 17 Oct 2026 02:52:56 -0400 Subject: [PATCH 11/12] fix(install): follow subagent_type spawns; manager, pair pull implementer Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WHwM2TcQBo9oHdCrYCtdMn --- agents/README.md | 2 +- pair/README.md | 2 +- scripts/test_workflow_deps.py | 32 ++++++++++++++++++++++++++++++++ scripts/workflow_deps.py | 19 +++++++++++++++---- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/agents/README.md b/agents/README.md index 165d0f3..6accd23 100644 --- a/agents/README.md +++ b/agents/README.md @@ -59,7 +59,7 @@ Needs: `divide`, `next` (installed with it). ## manager -Needs: `handoff-auto` (installed with it). +Needs: `handoff-auto`, `kanban-setup`, `next`, `session-handoff`, `implementer` (installed with it). - **Role:** owns one task end to end in a worktree. - Never implements itself: briefs an implementer, then reviews its work as if it didn't trust it. diff --git a/pair/README.md b/pair/README.md index fae20a2..8e14cf9 100644 --- a/pair/README.md +++ b/pair/README.md @@ -2,7 +2,7 @@ *`SKILL.md` is the prompt Claude follows; this file is for you.* -Needs: `handoff-auto`, `manager` (installed with it). +Needs: `handoff-auto`, `kanban-setup`, `next`, `session-handoff`, `implementer`, `manager` (installed with it). ## What it does diff --git a/scripts/test_workflow_deps.py b/scripts/test_workflow_deps.py index a42cb36..17bbdf5 100644 --- a/scripts/test_workflow_deps.py +++ b/scripts/test_workflow_deps.py @@ -133,6 +133,23 @@ def dangling_links(dest): return out +def spawned_agents(dest): + """(owner, agent) for each subagent_type="[krish:]agent" in an installed + .md file; owner is the skill dir name or the agent file's name.""" + out = [] + for base, _dirs, files in os.walk(dest): + for f in files: + if not f.endswith(".md"): + continue + p = os.path.join(base, f) + parts = os.path.relpath(p, dest).split(os.sep) + owner = parts[1] if parts[0] == "skills" else os.path.splitext(f)[0] + with open(p, encoding="utf-8-sig") as fh: + for a in re.findall(r'subagent_type="(?:krish:)?([\w-]+)"', fh.read()): + out.append((owner, a)) + return out + + class NamesTest(unittest.TestCase): @classmethod def setUpClass(cls): @@ -178,6 +195,21 @@ def test_readme_needs_line_matches_resolver(self): elif k == "agent": self.assertIn("\n## %s\n\n%s\n" % (n, want), agents_md, n) + def test_manager_spawns_implementer(self): + for n in ("manager", "pair"): + self.assertIn("implementer", self.g.resolve(names=[n])["agent"], n) + + def test_every_spawned_agent_installed(self): + # each agent an installed .md names via subagent_type="..." is in the + # closure, unless OPTIONAL says its owner only suggests it + for n in self.g.kinds: + with self.subTest(n), tempfile.TemporaryDirectory() as tmp: + r = self.g.resolve(names=[n]) + copy_like_installer(self.g, r, tmp) + for owner, a in spawned_agents(tmp): + if self.g.kinds.get(a) == "agent" and a not in workflow_deps.OPTIONAL.get(owner, ()): + self.assertIn(a, r["agent"], "%s: %s spawns %s" % (n, owner, a)) + def test_dangling_links_detects(self): with tempfile.TemporaryDirectory() as tmp: os.makedirs(os.path.join(tmp, "skills", "a")) diff --git a/scripts/workflow_deps.py b/scripts/workflow_deps.py index 3899f34..bd03632 100644 --- a/scripts/workflow_deps.py +++ b/scripts/workflow_deps.py @@ -26,11 +26,13 @@ its own SKILL.md (agent: its .md) as `name`, /name or name/. Related links alone pull in most of the repo; this keeps the ones the source actually calls. + spawns an agent the source's own SKILL.md (agent: its .md) passes as + subagent_type="name" or "krish:name" (manager -> implementer). files a relative link in any non-README .md of a selected skill that points into another skill's dir (e.g. ../next/BOARD.md), so no installed skill links to a file that isn't there. Hooks come only from workflow steps or --skill, never from edges. -OPTIONAL lists "calls" edges whose source works without the target (it +OPTIONAL lists "calls"/"spawns" edges whose source works without the target (it only suggests it, or has a fallback); they are not followed. """ import glob @@ -48,6 +50,7 @@ ROOT = os.path.dirname(HERE) _FILE_LINK_RE = re.compile(r"\]\(((?:\.\./)+[^)\s#]*)") +_SPAWN_RE = re.compile(r'subagent_type="(?:krish:)?([\w-]+)"') # hooks/.py -> (event, matcher or None, command suffix); per hooks/README.md HOOK_WIRING = { @@ -58,7 +61,8 @@ # src -> targets it names but works without; see each source's SKILL.md OPTIONAL = { - "find-me-a-skill": {"meta-orchestrator", "next", "pair"}, # suggests, runs nothing + "find-me-a-skill": {"implementer", "manager", "meta-orchestrator", "next", "pair", "planner", + "verifier"}, # suggests, runs nothing "gap-scan": {"divide", "grill-docs", "kanban-setup"}, # offers, doesn't run "handoff-auto": {"handoff"}, # credit line only "pr-watch": {"pair"}, # fixes inline without it @@ -133,8 +137,15 @@ def __init__(self, root=ROOT): if _names(texts[src], dst) and dst not in OPTIONAL.get(src, ()): self.deps[src].append(dst) for e in self.entries: - if self.kinds[e["name"]] == "skill": - self.deps[e["name"]].extend(_file_links(root, e, by_dir)) + n, k = e["name"], self.kinds[e["name"]] + if k == "skill": + self.deps[n].extend(_file_links(root, e, by_dir)) + if k in ("skill", "agent"): + if n not in texts: + texts[n] = _read(_source_md(root, e, k)) + for a in _SPAWN_RE.findall(texts[n]): + if self.kinds.get(a) == "agent" and a != n and a not in OPTIONAL.get(n, ()): + self.deps[n].append(a) def names(self): return [wf["name"] for wf in self.workflows] From 34ee6793c7186aebb586a8cd9b3ca229a5c7911e Mon Sep 17 00:00:00 2001 From: KrishP147 Date: Sat, 17 Oct 2026 02:55:40 -0400 Subject: [PATCH 12/12] feat(site): needs line under install cmds, not in What it does Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01WHwM2TcQBo9oHdCrYCtdMn --- scripts/site_detail.py | 29 ++++++++++++++++++++++------- scripts/test_site_detail.py | 21 +++++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/scripts/site_detail.py b/scripts/site_detail.py index 8ded677..7a20c7a 100644 --- a/scripts/site_detail.py +++ b/scripts/site_detail.py @@ -57,6 +57,8 @@ _README_NOTE_RE = re.compile(r"^\*.*`SKILL\.md` is the prompt Claude follows") _FENCE_RE = re.compile(r"^\s*(```|~~~)") _TRANSCRIPT_FENCE_RE = re.compile(r"^ {0,3}(```|~~~)\s*transcript\s*$", re.M) +# README line saying what an install of this alone brings (workflow_deps.needs_line) +_NEEDS_RE = re.compile(r"^(?:Works on its own\.|Needs: .+ \(installed with it\)\.)$", re.M) def esc(s): @@ -482,6 +484,7 @@ def build_pages(root, entries, meta, template, data_json, site_base="", workflow backlinks = {e["name"]: set() for e in entries} for e in entries: pre, pairs, base = raw_sections(root, e, agents_readme) + pre, needs = pop_needs(pre) ordered, extras = organise(pre, pairs) if e["group"] == "agents": base_dir, agents_md = "agents", True @@ -491,7 +494,7 @@ def build_pages(root, entries, meta, template, data_json, site_base="", workflow base_dir, agents_md = e["source"], False targets = set() rw = index.rewriter(base_dir, targets.add, agents_readme=agents_md) - rendered[e["name"]] = (ordered, extras, rw, prereq_hint(pairs)) + rendered[e["name"]] = (ordered, extras, rw, prereq_hint(pairs), needs) if missing_example(ordered): sys.stderr.write("WARN: %s: no Example section (its page has no transcript)\n" % e["name"]) # Pre-render to collect outgoing page links (for backlinks). @@ -511,7 +514,7 @@ def build_pages(root, entries, meta, template, data_json, site_base="", workflow pages = {} names = [e["name"] for e in entries] for i, e in enumerate(entries): - ordered, extras, rw, hint = rendered[e["name"]] + ordered, extras, rw, hint, needs = rendered[e["name"]] ordered = list(ordered) if e["group"] == "agents": ordered = _with_agent_related(ordered, e, backlinks[e["name"]], site_base) @@ -524,7 +527,7 @@ def build_pages(root, entries, meta, template, data_json, site_base="", workflow for fixed in ("term", "cmd", "menu", "echo", "hint", "status", "prompt-box", "comments", "skill-data", "source"): rd.unique_id(fixed) - body = [install_section(e, rd.unique_id("install"))] + body = [install_section(e, rd.unique_id("install"), rd.inline(needs) if needs else "")] for key, title, md in ordered: body.append(_section(rd, title, md, example=key == "example")) for heading, md in extras: @@ -624,16 +627,28 @@ def install_cmds(name): ".\\scripts\\install-skills.ps1 -Skill %s" % name) -def install_section(e, sid): - """'Install' section, first on every detail page.""" +def pop_needs(md): + """(md without its README "Works on its own." / "Needs: ..." line, that + line or ""). The line belongs under the install commands on the site.""" + m = _NEEDS_RE.search(md) + if not m: + return md, "" + return (md[:m.start()] + md[m.end():].lstrip("\n")).strip(), m.group(0) + + +def install_section(e, sid, needs_html=""): + """'Install' section, first on every detail page; needs_html is the + rendered README needs line, shown under the commands.""" sh, ps = install_cmds(e["name"]) note = ("Install just this, from a clone of the repo. It is copied, not wired: " "the installer prints the settings.json snippet to add." if e["group"] == "hooks" else "Install just this, from a clone of the repo. Anything it needs comes with it.") + needs = '

%s

\n' % needs_html if needs_html else "" return ('
\n

Install

\n

%s

\n' - '
%s   # bash, Git Bash, macOS, Linux\n%s   # Windows PowerShell
\n
' - %(esc(sid), esc(sid), esc(note), esc(sh), esc(ps))) + '
%s   # bash, Git Bash, macOS, Linux\n%s   # Windows PowerShell
\n' + '%s' + % (esc(sid), esc(sid), esc(note), esc(sh), esc(ps), needs)) def _source_section(e): diff --git a/scripts/test_site_detail.py b/scripts/test_site_detail.py index 5780c27..979de8d 100644 --- a/scripts/test_site_detail.py +++ b/scripts/test_site_detail.py @@ -126,6 +126,27 @@ def test_install_section_first(self): self.assertIn(".\\scripts\\install-skills.ps1 -Skill %s #" % name, page, name) self.assertEqual("copied, not wired" in page, self.by[name]["group"] == "hooks", name) + def test_needs_line_in_install_not_body(self): + # the README's "Works on its own." / "Needs: ..." line sits under the + # install commands, and nowhere else on the page + sec = re.compile(r'

Install

.*?', re.S) + for name, page in self.pages.items(): + if self.by[name]["group"] == "hooks": + continue + install = sec.search(page).group(0) + self.assertRegex(install, r'\n

(Works on its own\.|Needs: .+ \(installed with it\)\.)

', name) + rest = page.replace(install, "") + self.assertNotIn("Works on its own.", rest, name) + self.assertNotIn("(installed with it).", rest, name) + self.assertIn('

Needs: divide, next (installed with it).

', + self.pages["planner"]) + + def test_pop_needs(self): + self.assertEqual(site_detail.pop_needs("Intro.\n\nNeeds: `a` (installed with it).\n\nMore."), + ("Intro.\n\nMore.", "Needs: `a` (installed with it).")) + self.assertEqual(site_detail.pop_needs("Works on its own.\n\nIntro."), ("Intro.", "Works on its own.")) + self.assertEqual(site_detail.pop_needs("Needs a token."), ("Needs a token.", "")) + def test_sections_ordered_none_empty(self): titles = [t for _k, t in site_detail.SECTIONS] + ["Source"] h2 = re.compile(r'

(.*?)

')