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
|
|
5676
|
-
const
|
|
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
|
-
|
|
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}
|
|
5764
|
-
if (totalFail > 0) console.log(`${DIM$1}Failed to update ${totalFail}
|
|
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
|
@@ -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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
The PR is **always a draft**; a human marks it
|
|
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
|
|
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
|
|
33
|
-
|
|
34
|
-
lifts the author's own "judge this" questions for the human
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
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
|
|
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 |
|