pruff-skills 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -4026,6 +4026,78 @@ async function fetchAgentFile(source, name) {
4026
4026
  return null;
4027
4027
  }
4028
4028
  }
4029
+ async function listCatalogAgents(source) {
4030
+ const toNames = (files) => files.filter((f) => f.endsWith(".md")).map((f) => f.slice(0, -3));
4031
+ if (source.startsWith(".") || isAbsolute(source)) try {
4032
+ return toNames(await readdir(join(source, "agents")));
4033
+ } catch {
4034
+ return null;
4035
+ }
4036
+ if (/^[^/]+\/[^/]+$/.test(source)) try {
4037
+ const { stdout } = await execFileAsync$4("gh", [
4038
+ "api",
4039
+ `repos/${source}/contents/agents`,
4040
+ "--jq",
4041
+ ".[].name"
4042
+ ], { env: {
4043
+ ...process.env,
4044
+ GIT_TERMINAL_PROMPT: "0"
4045
+ } });
4046
+ return toNames(stdout.trim().split("\n").filter(Boolean));
4047
+ } catch {
4048
+ return null;
4049
+ }
4050
+ try {
4051
+ return toNames(await readdir(join(await cloneRepo$1(source), "agents")));
4052
+ } catch {
4053
+ return null;
4054
+ }
4055
+ }
4056
+ async function listInstalledAgents(targetDir) {
4057
+ try {
4058
+ return (await readdir(targetDir, { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name.slice(0, -3));
4059
+ } catch {
4060
+ return [];
4061
+ }
4062
+ }
4063
+ async function updateAgents(options = {}) {
4064
+ const source = PRUFF_CATALOG;
4065
+ const targetDir = claudeAgentsDir(options.global ?? false);
4066
+ const result = {
4067
+ updated: [],
4068
+ failed: [],
4069
+ localOnly: [],
4070
+ available: [],
4071
+ catalogUnavailable: false
4072
+ };
4073
+ const installed = await listInstalledAgents(targetDir);
4074
+ if (installed.length === 0) return result;
4075
+ const catalog = await listCatalogAgents(source);
4076
+ if (!catalog) {
4077
+ result.catalogUnavailable = true;
4078
+ return result;
4079
+ }
4080
+ const filter = options.names?.map((n) => n.toLowerCase());
4081
+ const matches = (name) => !filter || filter.length === 0 || filter.includes(name.toLowerCase());
4082
+ const catalogSet = new Set(catalog);
4083
+ const installedSet = new Set(installed);
4084
+ result.available = catalog.filter((n) => !installedSet.has(n) && matches(n));
4085
+ for (const name of installed) {
4086
+ if (!matches(name)) continue;
4087
+ if (!catalogSet.has(name)) {
4088
+ result.localOnly.push(name);
4089
+ continue;
4090
+ }
4091
+ const content = await fetchAgentFile(source, name);
4092
+ if (content === null) {
4093
+ result.failed.push(name);
4094
+ continue;
4095
+ }
4096
+ await writeFile(join(targetDir, `${sanitizeName(name)}.md`), content);
4097
+ result.updated.push(name);
4098
+ }
4099
+ return result;
4100
+ }
4029
4101
  function parseAgentsOptions(args) {
4030
4102
  const names = [];
4031
4103
  const options = {};
@@ -5672,8 +5744,9 @@ async function updateProjectSkills(options = {}) {
5672
5744
  }
5673
5745
  for (const [source, skillsForSource] of bySource) {
5674
5746
  const firstEntry = skillsForSource[0].entry;
5675
- const sourceUrl = firstEntry.source;
5676
- const ref = firstEntry.ref;
5747
+ const parsedSource = parseSource(firstEntry.source);
5748
+ const sourceUrl = parsedSource.url;
5749
+ const ref = firstEntry.ref ?? parsedSource.ref;
5677
5750
  const allLockedForSource = Object.entries(localLock.skills).filter(([_, entry]) => entry.source === source).map(([name, _]) => name);
5678
5751
  let tempDir = null;
5679
5752
  let deletedSkills = [];
@@ -5684,7 +5757,8 @@ async function updateProjectSkills(options = {}) {
5684
5757
  });
5685
5758
  deletedSkills = await checkAndPromptForDeletions(source, allLockedForSource, localLock.skills, false, options, discoveredPaths);
5686
5759
  } catch (error) {
5687
- console.log(`${DIM$1}✗ Failed to check for deleted skills from ${source}${RESET$1}`);
5760
+ const reason = error instanceof Error ? error.message : String(error);
5761
+ console.log(`${DIM$1}✗ Failed to check for deleted skills from ${source}: ${sanitizeMetadata(reason)}${RESET$1}`);
5688
5762
  } finally {
5689
5763
  if (tempDir) await cleanupTempDir(tempDir);
5690
5764
  }
@@ -5734,6 +5808,29 @@ function printLegacyProjectSkills(legacy) {
5734
5808
  console.log(` ${DIM$1}To refresh: ${TEXT$1}npx pruff-skills add ${reinstall} -y${RESET$1}`);
5735
5809
  }
5736
5810
  }
5811
+ async function syncAgentsForScope(isGlobal, options) {
5812
+ const result = await updateAgents({
5813
+ global: isGlobal,
5814
+ names: options.skills
5815
+ });
5816
+ const label = isGlobal ? "global" : "project";
5817
+ if (result.catalogUnavailable) {
5818
+ console.log(` ${DIM$1}✗ Could not read the subagent catalog — skipped ${label} subagents${RESET$1}`);
5819
+ return result;
5820
+ }
5821
+ for (const name of result.updated) console.log(` ${TEXT$1}✓${RESET$1} Synced ${sanitizeMetadata(name)}`);
5822
+ for (const name of result.failed) console.log(` ${DIM$1}✗ Failed to sync ${sanitizeMetadata(name)}${RESET$1}`);
5823
+ if (result.localOnly.length > 0) {
5824
+ const names = result.localOnly.map((n) => sanitizeMetadata(n)).join(", ");
5825
+ console.log(` ${DIM$1}• Not in the catalog, left untouched: ${names}${RESET$1}`);
5826
+ }
5827
+ if (result.available.length > 0) {
5828
+ const names = result.available.map((n) => sanitizeMetadata(n)).join(", ");
5829
+ console.log(` ${DIM$1}• Available but not installed: ${names}${RESET$1}`);
5830
+ console.log(` ${DIM$1}To install: ${TEXT$1}npx pruff-skills agents add <name>${isGlobal ? " -g" : ""}${RESET$1}`);
5831
+ }
5832
+ return result;
5833
+ }
5737
5834
  async function runUpdate(args = []) {
5738
5835
  const options = parseUpdateOptions(args);
5739
5836
  const scope = await resolveUpdateScope(options);
@@ -5758,10 +5855,25 @@ async function runUpdate(args = []) {
5758
5855
  totalFail += failCount;
5759
5856
  totalFound += foundCount;
5760
5857
  }
5858
+ console.log();
5859
+ console.log(`${BOLD$1}Subagents${RESET$1}`);
5860
+ console.log(`${DIM$1}ALL installed subagents will be updated and synced with the repo — local edits are overwritten.${RESET$1}`);
5861
+ const agentScopes = scope === "both" ? [true, false] : scope === "global" ? [true] : [false];
5862
+ let agentsSynced = 0;
5863
+ let agentsFailed = 0;
5864
+ for (const isGlobal of agentScopes) {
5865
+ const agentResult = await syncAgentsForScope(isGlobal, options);
5866
+ agentsSynced += agentResult.updated.length;
5867
+ agentsFailed += agentResult.failed.length;
5868
+ }
5869
+ if (agentsSynced === 0 && agentsFailed === 0) console.log(` ${DIM$1}No installed subagents to sync.${RESET$1}`);
5870
+ totalSuccess += agentsSynced;
5871
+ totalFail += agentsFailed;
5872
+ totalFound += agentsSynced + agentsFailed;
5761
5873
  if (options.skills && totalFound === 0) console.log(`${DIM$1}No installed skills found matching: ${options.skills.join(", ")}${RESET$1}`);
5762
5874
  console.log();
5763
- if (totalSuccess > 0) console.log(`${TEXT$1}✓ Updated ${totalSuccess} skill(s)${RESET$1}`);
5764
- if (totalFail > 0) console.log(`${DIM$1}Failed to update ${totalFail} skill(s)${RESET$1}`);
5875
+ if (totalSuccess > 0) console.log(`${TEXT$1}✓ Updated ${totalSuccess} item(s)${RESET$1}`);
5876
+ if (totalFail > 0) console.log(`${DIM$1}Failed to update ${totalFail} item(s)${RESET$1}`);
5765
5877
  String(totalSuccess + totalFail);
5766
5878
  console.log();
5767
5879
  }
@@ -7896,9 +8008,7 @@ const TEAM_SKILLS = {
7896
8008
  "pruff-logs",
7897
8009
  "pruff-sgsi",
7898
8010
  "infra-healthcheck",
7899
- "pr-visualizer-llm",
7900
8011
  "pr-visualizer",
7901
- "create-pr-llm",
7902
8012
  "prod-parity",
7903
8013
  "review-pr-comments"
7904
8014
  ]
@@ -8734,7 +8844,7 @@ async function showBanner() {
8734
8844
  console.log(` ${DIM}$${RESET} ${TEXT}npx pruff-skills agents add ${DIM}<name>${RESET} ${DIM}Add a Pruff subagent${RESET}`);
8735
8845
  console.log(` ${DIM}$${RESET} ${TEXT}npx pruff-skills rules add ${DIM}<name>${RESET} ${DIM}Add a Pruff rule (.claude/rules/)${RESET}`);
8736
8846
  console.log();
8737
- console.log(` ${DIM}$${RESET} ${TEXT}npx pruff-skills update${RESET} ${DIM}Update installed skills${RESET}`);
8847
+ console.log(` ${DIM}$${RESET} ${TEXT}npx pruff-skills update${RESET} ${DIM}Update installed skills + subagents${RESET}`);
8738
8848
  console.log();
8739
8849
  console.log(` ${DIM}$${RESET} ${TEXT}npx pruff-skills experimental_install${RESET} ${DIM}Restore from skills-lock.json${RESET}`);
8740
8850
  console.log(` ${DIM}$${RESET} ${TEXT}npx pruff-skills init ${DIM}[name]${RESET} ${DIM}Create a new skill${RESET}`);
@@ -8789,6 +8899,8 @@ ${BOLD}Onboarding Options:${RESET}
8789
8899
 
8790
8900
  ${BOLD}Updates:${RESET}
8791
8901
  update [skills...] Update skills to latest versions (alias: upgrade)
8902
+ Also syncs ALL installed subagents with the catalog,
8903
+ overwriting local edits.
8792
8904
 
8793
8905
  ${BOLD}Update Options:${RESET}
8794
8906
  -g, --global Update global skills only
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pruff-skills",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "The official Pruff agent skills CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -43,11 +43,12 @@ human directly. Your output is always a **draft PR** — never a merge, never a
43
43
  individual edit. Chain the repo's typecheck/lint/tests into a single command
44
44
  (`&&`) rather than separate calls. If the fix touches Prisma or raw SQL, run the
45
45
  `db-query-reviewer` subagent on your own diff before anyone else has to.
46
- 5. **Commit and open the PR** using the `/commit` and `/create-pr-llm` skills — never a
47
- raw `git commit`/`gh pr create`, and never the legacy `/create-pr` — your PRs always
48
- go to `reviewer-shared` next, so the description needs `/create-pr-llm`'s anchored,
49
- verifiable claims and authored "judge this" questions for that skill to work with.
50
- The PR is **always a draft**; a human marks it ready.
46
+ 5. **Commit and open the PR** using the `/commit` and `/create-pr` skills — never a raw
47
+ `git commit`/`gh pr create`. Your PRs always go to `reviewer-shared` next, so anchor
48
+ every solution bullet to `file:symbol` and give every decision a real "judge this"
49
+ question: that is what the review artifact verifies against the diff and reuses as the
50
+ "after" pane of its before/after pairs. The PR is **always a draft**; a human marks it
51
+ ready.
51
52
  6. **Post the PR link to `#pr-review` — always, not optionally.** `reviewer-shared`
52
53
  only listens in `#pr-review`; it does not see `#fixer-queue` at all, so replying
53
54
  there, or having a human type `?reviewer` in a `#fixer-queue` thread, reaches no
@@ -78,4 +79,4 @@ human directly. Your output is always a **draft PR** — never a merge, never a
78
79
  | Review your own Prisma/SQL diff before opening the PR | the `db-query-reviewer` subagent |
79
80
  | Trace a request/cron to confirm a root cause | the `pruff-logs-investigator` subagent |
80
81
  | Commit | the `/commit` skill — never raw `git commit` |
81
- | Open the PR | the `/create-pr-llm` skill — never raw `gh pr create`, never legacy `/create-pr`; always draft |
82
+ | Open the PR | the `/create-pr` skill — never raw `gh pr create`; always draft, always anchored |
@@ -29,21 +29,27 @@ don't guess which one they mean.
29
29
  2. **Read the touched repo's own rules** before judging anything —
30
30
  `~/Desktop/pruff/<repo>/CLAUDE.md` and `.claude/rules/*.md`. A finding that
31
31
  contradicts a rule you didn't read is not a finding, it's noise.
32
- 3. **Run the `pr-visualizer-llm` skill** to generate the review artifact — it verifies
33
- the PR description's anchored claims (`file:symbol`) directly against the diff and
34
- lifts the author's own "judge this" questions for the human, rather than
35
- rediscovering everything from scratch. This is the deliverable; don't hand-roll an
36
- equivalent by reading the diff and writing prose. If the PR body isn't in that
37
- format (no anchored claims, no ⚖️ column — i.e. it wasn't written by `fixer-shared`
38
- with `/create-pr-llm`), fall back to the legacy `pr-visualizer` skill instead.
39
- **Never let it run in `light` mode, regardless of how small the diff looks.**
40
- `light` is 0-agents — it only reads exactly what the PR description anchors, and a
41
- real review missed a confirmed blocker this way: the PR claimed "no backend changes
42
- needed" for an autocomplete filter, and `light` mode has no mechanism to check
43
- whether that claim is true, because it never traces the value into the actual
44
- service layer. Always pass `--standard` explicitly. Go to `--deep` only if a human
45
- asks for a deeper look in `#pr-review` — never on your own initiative, and never
46
- drop back to `light` for a PR that "looks contained."
32
+ 3. **Run the `pr-visualizer` skill** to generate the review artifact — it builds the
33
+ change as before/after diagram pairs, consolidates the specialist findings you pass
34
+ it, and lifts the author's own "judge this" questions for the human. This is the
35
+ deliverable; don't hand-roll an equivalent by reading the diff and writing prose.
36
+ It handles both anchored and legacy PR bodies; an anchored body (one written with
37
+ `/create-pr`) just makes it cheaper and sharper.
38
+ **Pass a mode explicitly; let the diff choose which one.** `light` is 0-agents and
39
+ is the right call for a genuinely contained change — a copy fix, a config bump, one
40
+ component's internals. It still builds the before/after pairs (it reads the base
41
+ state with `git show <base>:<path>`); what it does not do is trace a value beyond
42
+ the files the diff touches.
43
+
44
+ That limit is the one that bit us, so escalate on **the shape of the claim, not the
45
+ size of the diff**: a real review missed a confirmed blocker when a PR claimed "no
46
+ backend changes needed" for an autocomplete filter and nothing checked whether that
47
+ was true — the param was validated in the Zod schema and never wired into
48
+ `buildWhere`. Whenever the description asserts something about code the diff does
49
+ **not** contain ("no backend changes", "no migration needed", "behavior unchanged"),
50
+ `light` structurally cannot check it. Pass `--standard` for those, and for anything
51
+ touching schema, auth, money, or concurrency. Go to `--deep` only if a human asks in
52
+ `#pr-review` — never on your own initiative.
47
53
  4. **Comment the dossier on the PR.** The skill reports a markdown dossier and a link to
48
54
  an interactive HTML version. **Post the markdown as the comment body** — that is the
49
55
  durable record. Then add the link on its own last line, with the absolute expiry date
@@ -88,7 +94,7 @@ worse than asking once.
88
94
 
89
95
  | Task | Reach for |
90
96
  |---|---|
91
- | Generate the review artifact | the `pr-visualizer-llm` skill — this is your primary tool, use it every time. Fall back to `pr-visualizer` only for a legacy (non-anchored) PR body |
97
+ | Generate the review artifact | the `pr-visualizer` skill — this is your primary tool, use it every time |
92
98
  | Prisma / raw SQL in the diff | the `db-query-reviewer` subagent |
93
99
  | Find where a changed symbol is used elsewhere | the `codebase-navigator` subagent |
94
100
  | The full multi-dimension review pipeline (rules + db + react-doctor) | the `pruff-pr-review` skill, if the PR is large enough to warrant it |
@@ -9,7 +9,7 @@ belongs in that agent's own persona file.
9
9
 
10
10
  | Display name | Owns | What it does | Writes code? | Responds to |
11
11
  |---|---|---|---|---|
12
- | `reviewer-shared` | `#pr-review` | Runs `pr-visualizer-llm` on a PR, posts a summary, requests a human reviewer via `gh`. Never approves or merges. | No | allowlist |
12
+ | `reviewer-shared` | `#pr-review` | Runs `pr-visualizer` on a PR, posts a summary, requests a human reviewer via `gh`. Never approves or merges. | No | allowlist |
13
13
  | `marketing-shared` | `#marketing` | Answers questions using Linear + GitHub history + the brand guide. Drafts newsletter/comms/campaign copy **in chat only**. | No | allowlist |
14
14
  | `investigator-shared` | `#triage` | Triages a bug report using CloudWatch, GitHub history and Linear. Hands confirmed root causes to `fixer-shared` via `#fixer-queue`. | No | allowlist |
15
15
  | `fixer-shared` | `#fixer-queue` | Takes a triaged job (from `investigator-shared` or a human) and opens a **draft PR**. Never merges, never pushes to `dev`/`main`. | Yes | allowlist |