@iceinvein/agent-skills 0.1.26 → 0.1.28
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/README.md +27 -0
- package/dist/cli/index.js +201 -4
- package/package.json +2 -1
- package/skills/index.json +153 -123
- package/skills/magpie/README.md +7 -8
- package/skills/magpie/install.sh +13 -22
- package/skills/magpie/scripts/post-cmd.ts +85 -15
- package/skills/magpie/scripts/refresh.ts +15 -3
- package/skills/magpie/scripts/server.ts +12 -1
- package/skills/magpie/skill.json +22 -3
- package/skills/magpie/uninstall.sh +31 -0
package/README.md
CHANGED
|
@@ -126,6 +126,29 @@ bunx @iceinvein/agent-skills info <skill>
|
|
|
126
126
|
| `--activation <mode>` | For skills that support it: `session` (manual `/skill`) or `global` (auto via `SessionStart` hook). Claude Code only. |
|
|
127
127
|
| `-g, --global` | Install to home directory (available in all projects) |
|
|
128
128
|
|
|
129
|
+
## Updating Skills
|
|
130
|
+
|
|
131
|
+
Each skill is versioned independently (see `skill.json` inside each skill). The CLI tracks what you installed in `.agent-skills.lock` and can pull newer versions from GitHub.
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
bunx @iceinvein/agent-skills update <skill> # update one skill
|
|
135
|
+
bunx @iceinvein/agent-skills update --all # update everything in the lockfile
|
|
136
|
+
bunx @iceinvein/agent-skills list # show installed versions
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`update` re-fetches the latest manifest and files from this repo, removes the old install, and re-installs cleanly. Your chosen activation mode (session vs. global) is preserved across updates, as is the set of tools you installed for. Use `-g` to update globally-installed skills.
|
|
140
|
+
|
|
141
|
+
After any command other than `update`, the CLI runs a background freshness check (at most once every 24 hours) and prints a one-line notice if anything is out of date:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
some-skill (v0.2.0 → v0.3.0)
|
|
145
|
+
Run: agent-skills update --all
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The check is best-effort: it silently skips on network failure and only hits GitHub once per day per project. To force a re-check sooner, delete the `lastUpdateCheck` field in `.agent-skills.lock` (or just run any command after 24 hours have elapsed).
|
|
149
|
+
|
|
150
|
+
To pin to the current version, do nothing: skills are not auto-updated. `update` is opt-in.
|
|
151
|
+
|
|
129
152
|
## Activation Modes (Claude Code)
|
|
130
153
|
|
|
131
154
|
Some skills — like `terse` — support activation modes. Pick one at install time:
|
|
@@ -157,6 +180,10 @@ The CLI fetches skills from GitHub and installs them into the right locations fo
|
|
|
157
180
|
|
|
158
181
|
Skills install to the **current project** by default. Use `-g` to install to your **home directory** so the skill is available everywhere. A `.agent-skills.lock` file tracks installations for update and remove.
|
|
159
182
|
|
|
183
|
+
## Contributing
|
|
184
|
+
|
|
185
|
+
- [Releasing](docs/RELEASING.md): how versions are bumped, tagged, and published to npm
|
|
186
|
+
|
|
160
187
|
## License
|
|
161
188
|
|
|
162
189
|
MIT
|
package/dist/cli/index.js
CHANGED
|
@@ -248,6 +248,30 @@ function validateManifest(data) {
|
|
|
248
248
|
if (typeof d.install !== "object" || d.install === null) {
|
|
249
249
|
return { ok: false, error: "Missing 'install' configuration" };
|
|
250
250
|
}
|
|
251
|
+
if (d.bundle !== undefined) {
|
|
252
|
+
if (typeof d.bundle !== "object" || d.bundle === null) {
|
|
253
|
+
return { ok: false, error: "'bundle' must be an object" };
|
|
254
|
+
}
|
|
255
|
+
const b = d.bundle;
|
|
256
|
+
if (!Array.isArray(b.include) || b.include.length === 0) {
|
|
257
|
+
return { ok: false, error: "'bundle.include' must be a non-empty array" };
|
|
258
|
+
}
|
|
259
|
+
for (const p of b.include) {
|
|
260
|
+
if (typeof p !== "string") {
|
|
261
|
+
return { ok: false, error: "'bundle.include' entries must be strings" };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (b.exclude !== undefined) {
|
|
265
|
+
if (!Array.isArray(b.exclude)) {
|
|
266
|
+
return { ok: false, error: "'bundle.exclude' must be an array" };
|
|
267
|
+
}
|
|
268
|
+
for (const p of b.exclude) {
|
|
269
|
+
if (typeof p !== "string") {
|
|
270
|
+
return { ok: false, error: "'bundle.exclude' entries must be strings" };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
251
275
|
if (d.activation !== undefined) {
|
|
252
276
|
if (typeof d.activation !== "object" || d.activation === null) {
|
|
253
277
|
return { ok: false, error: "'activation' must be an object" };
|
|
@@ -318,12 +342,87 @@ async function fetchAllSkillFiles(skillName, manifest) {
|
|
|
318
342
|
files.set(supportFile, result.content);
|
|
319
343
|
}
|
|
320
344
|
}
|
|
345
|
+
if (manifest.bundle) {
|
|
346
|
+
const treeResult = await fetchSkillTree(skillName);
|
|
347
|
+
if (!treeResult.ok)
|
|
348
|
+
return { error: treeResult.error };
|
|
349
|
+
const bundlePaths = resolveBundlePaths(treeResult.entries, manifest.bundle);
|
|
350
|
+
for (const relPath of bundlePaths) {
|
|
351
|
+
if (files.has(relPath))
|
|
352
|
+
continue;
|
|
353
|
+
const result = await fetchSkillFile(skillName, relPath);
|
|
354
|
+
if (!result.ok)
|
|
355
|
+
return { error: result.error };
|
|
356
|
+
files.set(relPath, result.content);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
321
359
|
return files;
|
|
322
360
|
}
|
|
361
|
+
async function fetchSkillTree(skillName) {
|
|
362
|
+
const url = `https://api.github.com/repos/${REPO}/git/trees/${BRANCH}?recursive=1`;
|
|
363
|
+
const res = await fetch(url, {
|
|
364
|
+
headers: { Accept: "application/vnd.github+json" }
|
|
365
|
+
});
|
|
366
|
+
if (!res.ok) {
|
|
367
|
+
return { ok: false, error: `Failed to fetch repo tree: HTTP ${res.status}` };
|
|
368
|
+
}
|
|
369
|
+
const data = await res.json();
|
|
370
|
+
if (data.truncated) {
|
|
371
|
+
return { ok: false, error: "GitHub tree response was truncated; skill bundle too large to enumerate" };
|
|
372
|
+
}
|
|
373
|
+
const prefix = `skills/${skillName}/`;
|
|
374
|
+
const entries = data.tree.filter((e) => e.path.startsWith(prefix) && (e.type === "blob" || e.type === "tree")).map((e) => ({ path: e.path.slice(prefix.length), type: e.type }));
|
|
375
|
+
return { ok: true, entries };
|
|
376
|
+
}
|
|
377
|
+
function resolveBundlePaths(entries, bundle) {
|
|
378
|
+
const excludes = bundle.exclude ?? [];
|
|
379
|
+
const isExcluded = (p) => excludes.some((ex) => p === ex || p.startsWith(ex));
|
|
380
|
+
const dirs = new Set;
|
|
381
|
+
const files = new Set;
|
|
382
|
+
for (const entry of entries) {
|
|
383
|
+
if (entry.type === "tree")
|
|
384
|
+
dirs.add(entry.path);
|
|
385
|
+
else
|
|
386
|
+
files.add(entry.path);
|
|
387
|
+
}
|
|
388
|
+
const result = new Set;
|
|
389
|
+
for (const include of bundle.include) {
|
|
390
|
+
if (dirs.has(include)) {
|
|
391
|
+
const prefix = include.endsWith("/") ? include : include + "/";
|
|
392
|
+
for (const f of files) {
|
|
393
|
+
if (f.startsWith(prefix) && !isExcluded(f))
|
|
394
|
+
result.add(f);
|
|
395
|
+
}
|
|
396
|
+
} else if (files.has(include)) {
|
|
397
|
+
if (!isExcluded(include))
|
|
398
|
+
result.add(include);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return [...result].sort();
|
|
402
|
+
}
|
|
323
403
|
|
|
324
404
|
// src/cli/adapters/claude.ts
|
|
325
|
-
import { existsSync as existsSync2, mkdirSync, rmSync, unlinkSync } from "node:fs";
|
|
405
|
+
import { chmodSync, existsSync as existsSync2, mkdirSync, rmSync, statSync, unlinkSync } from "node:fs";
|
|
326
406
|
import { dirname, join as join2 } from "node:path";
|
|
407
|
+
function shouldBeExecutable(relPath, content) {
|
|
408
|
+
if (relPath.endsWith(".sh"))
|
|
409
|
+
return true;
|
|
410
|
+
if (relPath.startsWith("bin/") || relPath.includes("/bin/"))
|
|
411
|
+
return true;
|
|
412
|
+
if (content.startsWith("#!"))
|
|
413
|
+
return true;
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
function runScript(scriptPath, cwd) {
|
|
417
|
+
if (!existsSync2(scriptPath))
|
|
418
|
+
return { ok: true, code: 0 };
|
|
419
|
+
const result = Bun.spawnSync(["bash", scriptPath], {
|
|
420
|
+
cwd,
|
|
421
|
+
stdout: "inherit",
|
|
422
|
+
stderr: "inherit"
|
|
423
|
+
});
|
|
424
|
+
return { ok: result.exitCode === 0, code: result.exitCode ?? 1 };
|
|
425
|
+
}
|
|
327
426
|
function matchesSkillDirective(command, skillName) {
|
|
328
427
|
return command.includes(`Activate ${skillName} skill`);
|
|
329
428
|
}
|
|
@@ -394,6 +493,21 @@ var claudeAdapter = {
|
|
|
394
493
|
}
|
|
395
494
|
}
|
|
396
495
|
}
|
|
496
|
+
if (config.bundleRoot && manifest.bundle) {
|
|
497
|
+
const promptPath = manifest.files?.prompt;
|
|
498
|
+
for (const [relPath, content] of files) {
|
|
499
|
+
if (relPath === promptPath)
|
|
500
|
+
continue;
|
|
501
|
+
const targetRel = join2(config.bundleRoot, relPath);
|
|
502
|
+
const targetPath = join2(cwd, targetRel);
|
|
503
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
504
|
+
await Bun.write(targetPath, content);
|
|
505
|
+
if (shouldBeExecutable(relPath, content)) {
|
|
506
|
+
chmodSync(targetPath, 493);
|
|
507
|
+
}
|
|
508
|
+
installed.push(targetRel);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
397
511
|
if (config.mcpServers) {
|
|
398
512
|
const settingsPath = join2(cwd, ".claude/settings.json");
|
|
399
513
|
let settings = {};
|
|
@@ -416,22 +530,49 @@ var claudeAdapter = {
|
|
|
416
530
|
installed.push(".claude/settings.json");
|
|
417
531
|
}
|
|
418
532
|
}
|
|
533
|
+
if (config.postinstall && config.bundleRoot) {
|
|
534
|
+
const scriptPath = join2(cwd, config.bundleRoot, config.postinstall);
|
|
535
|
+
const result = runScript(scriptPath, join2(cwd, config.bundleRoot));
|
|
536
|
+
if (!result.ok) {
|
|
537
|
+
console.error(`postinstall script '${config.postinstall}' exited with code ${result.code}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
419
540
|
return installed;
|
|
420
541
|
},
|
|
421
542
|
async remove(cwd, manifest, installedFiles) {
|
|
422
543
|
const config = manifest.install.claude;
|
|
423
544
|
if (!config)
|
|
424
545
|
return;
|
|
546
|
+
if (config.postremove && config.bundleRoot) {
|
|
547
|
+
const scriptPath = join2(cwd, config.bundleRoot, config.postremove);
|
|
548
|
+
const result = runScript(scriptPath, join2(cwd, config.bundleRoot));
|
|
549
|
+
if (!result.ok) {
|
|
550
|
+
console.error(`postremove script '${config.postremove}' exited with code ${result.code}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
425
553
|
for (const file of installedFiles) {
|
|
426
554
|
if (file === ".claude/settings.json")
|
|
427
555
|
continue;
|
|
428
556
|
const fullPath = join2(cwd, file);
|
|
429
557
|
if (existsSync2(fullPath)) {
|
|
430
|
-
unlinkSync(fullPath);
|
|
431
|
-
const dir = dirname(fullPath);
|
|
432
558
|
try {
|
|
433
|
-
|
|
559
|
+
const stat = statSync(fullPath);
|
|
560
|
+
if (stat.isDirectory()) {
|
|
561
|
+
rmSync(fullPath, { recursive: true, force: true });
|
|
562
|
+
} else {
|
|
563
|
+
unlinkSync(fullPath);
|
|
564
|
+
}
|
|
434
565
|
} catch {}
|
|
566
|
+
let dir = dirname(fullPath);
|
|
567
|
+
const stopAt = config.bundleRoot ? join2(cwd, config.bundleRoot, "..") : cwd;
|
|
568
|
+
while (dir !== stopAt && dir !== "/" && dir.startsWith(cwd)) {
|
|
569
|
+
try {
|
|
570
|
+
rmSync(dir, { recursive: false });
|
|
571
|
+
} catch {
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
dir = dirname(dir);
|
|
575
|
+
}
|
|
435
576
|
}
|
|
436
577
|
}
|
|
437
578
|
if (config.mcpServers) {
|
|
@@ -768,8 +909,64 @@ async function fetchAllSkillFiles2(skillName, manifest) {
|
|
|
768
909
|
files.set(supportFile, result.content);
|
|
769
910
|
}
|
|
770
911
|
}
|
|
912
|
+
if (manifest.bundle) {
|
|
913
|
+
const treeResult = await fetchSkillTree2(skillName);
|
|
914
|
+
if (!treeResult.ok)
|
|
915
|
+
return { error: treeResult.error };
|
|
916
|
+
const bundlePaths = resolveBundlePaths2(treeResult.entries, manifest.bundle);
|
|
917
|
+
for (const relPath of bundlePaths) {
|
|
918
|
+
if (files.has(relPath))
|
|
919
|
+
continue;
|
|
920
|
+
const result = await fetchSkillFile2(skillName, relPath);
|
|
921
|
+
if (!result.ok)
|
|
922
|
+
return { error: result.error };
|
|
923
|
+
files.set(relPath, result.content);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
771
926
|
return files;
|
|
772
927
|
}
|
|
928
|
+
async function fetchSkillTree2(skillName) {
|
|
929
|
+
const url = `https://api.github.com/repos/${REPO2}/git/trees/${BRANCH2}?recursive=1`;
|
|
930
|
+
const res = await fetch(url, {
|
|
931
|
+
headers: { Accept: "application/vnd.github+json" }
|
|
932
|
+
});
|
|
933
|
+
if (!res.ok) {
|
|
934
|
+
return { ok: false, error: `Failed to fetch repo tree: HTTP ${res.status}` };
|
|
935
|
+
}
|
|
936
|
+
const data = await res.json();
|
|
937
|
+
if (data.truncated) {
|
|
938
|
+
return { ok: false, error: "GitHub tree response was truncated; skill bundle too large to enumerate" };
|
|
939
|
+
}
|
|
940
|
+
const prefix = `skills/${skillName}/`;
|
|
941
|
+
const entries = data.tree.filter((e) => e.path.startsWith(prefix) && (e.type === "blob" || e.type === "tree")).map((e) => ({ path: e.path.slice(prefix.length), type: e.type }));
|
|
942
|
+
return { ok: true, entries };
|
|
943
|
+
}
|
|
944
|
+
function resolveBundlePaths2(entries, bundle) {
|
|
945
|
+
const excludes = bundle.exclude ?? [];
|
|
946
|
+
const isExcluded = (p) => excludes.some((ex) => p === ex || p.startsWith(ex));
|
|
947
|
+
const dirs = new Set;
|
|
948
|
+
const files = new Set;
|
|
949
|
+
for (const entry of entries) {
|
|
950
|
+
if (entry.type === "tree")
|
|
951
|
+
dirs.add(entry.path);
|
|
952
|
+
else
|
|
953
|
+
files.add(entry.path);
|
|
954
|
+
}
|
|
955
|
+
const result = new Set;
|
|
956
|
+
for (const include of bundle.include) {
|
|
957
|
+
if (dirs.has(include)) {
|
|
958
|
+
const prefix = include.endsWith("/") ? include : include + "/";
|
|
959
|
+
for (const f of files) {
|
|
960
|
+
if (f.startsWith(prefix) && !isExcluded(f))
|
|
961
|
+
result.add(f);
|
|
962
|
+
}
|
|
963
|
+
} else if (files.has(include)) {
|
|
964
|
+
if (!isExcluded(include))
|
|
965
|
+
result.add(include);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return [...result].sort();
|
|
969
|
+
}
|
|
773
970
|
|
|
774
971
|
// src/cli/commands/remove.ts
|
|
775
972
|
async function removeSkill(cwd, skillName) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iceinvein/agent-skills",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"description": "Install agent skills into AI coding tools",
|
|
5
5
|
"author": "iceinvein",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"skill:bump": "bun run src/cli/index.ts bump",
|
|
20
20
|
"skill:bump:all": "bun run src/cli/index.ts bump --all",
|
|
21
21
|
"skill:bump:check": "bun run src/cli/index.ts bump --all --dry-run",
|
|
22
|
+
"build:index": "bun run scripts/build-index.ts",
|
|
22
23
|
"release": "bash scripts/release.sh"
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
package/skills/index.json
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
|
-
"name": "
|
|
4
|
-
"description": "
|
|
5
|
-
"type": "prompt",
|
|
6
|
-
"version": "1.0.0",
|
|
7
|
-
"applies": ["any"],
|
|
8
|
-
"quick": false
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
"name": "codebase-architecture",
|
|
12
|
-
"description": "Architecture review for existing codebases or structured design for new projects, with patterns reference",
|
|
3
|
+
"name": "bounded-context-auditor",
|
|
4
|
+
"description": "Evans-inspired bounded context analysis — detect linguistic fractures, draw context maps, identify leaking language and shared model pollution",
|
|
13
5
|
"type": "prompt",
|
|
14
6
|
"version": "1.0.0",
|
|
15
|
-
"applies": [
|
|
7
|
+
"applies": [
|
|
8
|
+
"domain"
|
|
9
|
+
],
|
|
16
10
|
"quick": false
|
|
17
11
|
},
|
|
18
12
|
{
|
|
@@ -22,107 +16,124 @@
|
|
|
22
16
|
"version": "1.0.0"
|
|
23
17
|
},
|
|
24
18
|
{
|
|
25
|
-
"name": "
|
|
26
|
-
"description": "
|
|
19
|
+
"name": "codebase-architecture",
|
|
20
|
+
"description": "Architecture review for existing codebases or structured design for new projects, with patterns reference",
|
|
27
21
|
"type": "prompt",
|
|
28
22
|
"version": "1.0.0",
|
|
29
|
-
"applies": [
|
|
23
|
+
"applies": [
|
|
24
|
+
"any",
|
|
25
|
+
"architecture"
|
|
26
|
+
],
|
|
30
27
|
"quick": false
|
|
31
28
|
},
|
|
32
29
|
{
|
|
33
|
-
"name": "
|
|
34
|
-
"description": "
|
|
30
|
+
"name": "cognitive-load-auditor",
|
|
31
|
+
"description": "Jeff Johnson-inspired cognitive load analysis — evaluate UI against Miller's Law, Hick's Law, Fitts's Law, and working memory limits",
|
|
35
32
|
"type": "prompt",
|
|
36
33
|
"version": "1.0.0",
|
|
37
|
-
"applies": [
|
|
34
|
+
"applies": [
|
|
35
|
+
"ui"
|
|
36
|
+
],
|
|
38
37
|
"quick": false
|
|
39
38
|
},
|
|
40
39
|
{
|
|
41
|
-
"name": "
|
|
42
|
-
"description": "
|
|
43
|
-
"type": "prompt",
|
|
44
|
-
"version": "1.0.0",
|
|
45
|
-
"applies": ["architecture"],
|
|
46
|
-
"quick": true
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"name": "seam-finder",
|
|
50
|
-
"description": "Feathers-inspired legacy code modification — find seams, make minimal incisions, preserve existing behavior",
|
|
40
|
+
"name": "cohesion-analyzer",
|
|
41
|
+
"description": "Constantine & Yourdon-inspired cohesion analysis — classify module focus on the 7-level spectrum, find split lines, reduce mixed responsibilities",
|
|
51
42
|
"type": "prompt",
|
|
52
43
|
"version": "1.0.0",
|
|
53
|
-
"applies": [
|
|
44
|
+
"applies": [
|
|
45
|
+
"architecture"
|
|
46
|
+
],
|
|
54
47
|
"quick": true
|
|
55
48
|
},
|
|
56
49
|
{
|
|
57
|
-
"name": "
|
|
58
|
-
"description": "
|
|
50
|
+
"name": "complexity-accountant",
|
|
51
|
+
"description": "Ousterhout-inspired complexity analysis — deep vs shallow modules, complexity budget, justified abstractions",
|
|
59
52
|
"type": "prompt",
|
|
60
53
|
"version": "1.0.0",
|
|
61
|
-
"applies": [
|
|
54
|
+
"applies": [
|
|
55
|
+
"any"
|
|
56
|
+
],
|
|
62
57
|
"quick": false
|
|
63
58
|
},
|
|
64
59
|
{
|
|
65
|
-
"name": "
|
|
66
|
-
"description": "
|
|
60
|
+
"name": "composability-auditor",
|
|
61
|
+
"description": "Unix Philosophy-inspired composability analysis — identify reuse blockers, decompose self-sufficient units into composable pieces with standard interfaces",
|
|
67
62
|
"type": "prompt",
|
|
68
63
|
"version": "1.0.0",
|
|
69
|
-
"applies": [
|
|
70
|
-
|
|
64
|
+
"applies": [
|
|
65
|
+
"any"
|
|
66
|
+
],
|
|
67
|
+
"quick": false
|
|
71
68
|
},
|
|
72
69
|
{
|
|
73
|
-
"name": "
|
|
74
|
-
"description": "
|
|
70
|
+
"name": "contract-enforcer",
|
|
71
|
+
"description": "Meyer-inspired Design by Contract — preconditions, postconditions, invariants, and failure contracts for non-trivial functions",
|
|
75
72
|
"type": "prompt",
|
|
76
73
|
"version": "1.0.0",
|
|
77
|
-
"applies": [
|
|
74
|
+
"applies": [
|
|
75
|
+
"any"
|
|
76
|
+
],
|
|
78
77
|
"quick": false
|
|
79
78
|
},
|
|
80
79
|
{
|
|
81
|
-
"name": "
|
|
82
|
-
"description": "
|
|
80
|
+
"name": "coupling-auditor",
|
|
81
|
+
"description": "Constantine & Yourdon-inspired coupling analysis — classify, measure, and reduce interdependence between modules",
|
|
83
82
|
"type": "prompt",
|
|
84
83
|
"version": "1.0.0",
|
|
85
|
-
"applies": [
|
|
86
|
-
|
|
84
|
+
"applies": [
|
|
85
|
+
"architecture"
|
|
86
|
+
],
|
|
87
|
+
"quick": true
|
|
87
88
|
},
|
|
88
89
|
{
|
|
89
|
-
"name": "
|
|
90
|
-
"description": "
|
|
90
|
+
"name": "cqs-auditor",
|
|
91
|
+
"description": "Meyer-inspired Command-Query Separation — classify functions as commands or queries, detect mixed violations, separate side effects from return values",
|
|
91
92
|
"type": "prompt",
|
|
92
93
|
"version": "1.0.0",
|
|
93
|
-
"applies": [
|
|
94
|
+
"applies": [
|
|
95
|
+
"architecture"
|
|
96
|
+
],
|
|
94
97
|
"quick": true
|
|
95
98
|
},
|
|
96
99
|
{
|
|
97
|
-
"name": "
|
|
98
|
-
"description": "
|
|
100
|
+
"name": "demeter-enforcer",
|
|
101
|
+
"description": "Lieberherr-inspired Law of Demeter analysis — detect chain violations, parameter drilling, and hidden traversal; fix with tell-don't-ask or parameter narrowing",
|
|
99
102
|
"type": "prompt",
|
|
100
103
|
"version": "1.0.0",
|
|
101
|
-
"applies": [
|
|
102
|
-
|
|
104
|
+
"applies": [
|
|
105
|
+
"architecture"
|
|
106
|
+
],
|
|
107
|
+
"quick": true
|
|
103
108
|
},
|
|
104
109
|
{
|
|
105
|
-
"name": "
|
|
106
|
-
"description": "
|
|
110
|
+
"name": "dependency-direction-auditor",
|
|
111
|
+
"description": "Martin-inspired dependency direction analysis — trace imports across layers, classify violations by severity, recommend inversion",
|
|
107
112
|
"type": "prompt",
|
|
108
113
|
"version": "1.0.0",
|
|
109
|
-
"applies": [
|
|
114
|
+
"applies": [
|
|
115
|
+
"architecture"
|
|
116
|
+
],
|
|
110
117
|
"quick": true
|
|
111
118
|
},
|
|
112
119
|
{
|
|
113
|
-
"name": "
|
|
114
|
-
"description": "
|
|
120
|
+
"name": "design-review",
|
|
121
|
+
"description": "Brooks-inspired design integrity review — tests conceptual integrity, constraint exploitation, removal discipline, and scope control",
|
|
115
122
|
"type": "prompt",
|
|
116
123
|
"version": "1.0.0",
|
|
117
|
-
"applies": [
|
|
124
|
+
"applies": [
|
|
125
|
+
"any"
|
|
126
|
+
],
|
|
118
127
|
"quick": false
|
|
119
128
|
},
|
|
120
129
|
{
|
|
121
|
-
"name": "
|
|
122
|
-
"description": "
|
|
130
|
+
"name": "error-strategist",
|
|
131
|
+
"description": "Duffy & Abrahams-inspired error handling — classify errors (bug/recoverable/fatal), assign safety guarantees, design recovery boundaries",
|
|
123
132
|
"type": "prompt",
|
|
124
133
|
"version": "1.0.0",
|
|
125
|
-
"applies": [
|
|
134
|
+
"applies": [
|
|
135
|
+
"errors"
|
|
136
|
+
],
|
|
126
137
|
"quick": false
|
|
127
138
|
},
|
|
128
139
|
{
|
|
@@ -130,129 +141,148 @@
|
|
|
130
141
|
"description": "Evans/Vernon/Dahan-inspired event design — domain-meaningful naming, fat payloads, schema evolution, and the domain expert test",
|
|
131
142
|
"type": "prompt",
|
|
132
143
|
"version": "1.0.0",
|
|
133
|
-
"applies": [
|
|
144
|
+
"applies": [
|
|
145
|
+
"integration",
|
|
146
|
+
"domain"
|
|
147
|
+
],
|
|
134
148
|
"quick": false
|
|
135
149
|
},
|
|
136
150
|
{
|
|
137
|
-
"name": "
|
|
138
|
-
"description": "
|
|
139
|
-
"type": "prompt",
|
|
140
|
-
"version": "1.0.0",
|
|
141
|
-
"applies": ["architecture"],
|
|
142
|
-
"quick": true
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
"name": "cohesion-analyzer",
|
|
146
|
-
"description": "Constantine & Yourdon-inspired cohesion analysis — classify module focus on the 7-level spectrum, find split lines, reduce mixed responsibilities",
|
|
151
|
+
"name": "evolution-analyzer",
|
|
152
|
+
"description": "Lehman-inspired software evolution analysis — trajectory assessment, debt visibility, and change impact on system health",
|
|
147
153
|
"type": "prompt",
|
|
148
154
|
"version": "1.0.0",
|
|
149
|
-
"applies": [
|
|
150
|
-
|
|
155
|
+
"applies": [
|
|
156
|
+
"any"
|
|
157
|
+
],
|
|
158
|
+
"quick": false
|
|
151
159
|
},
|
|
152
160
|
{
|
|
153
|
-
"name": "
|
|
154
|
-
"description": "
|
|
161
|
+
"name": "gestalt-reviewer",
|
|
162
|
+
"description": "Gestalt-inspired visual perception audit — proximity, similarity, closure, continuity, and figure-ground analysis for UI layouts",
|
|
155
163
|
"type": "prompt",
|
|
156
164
|
"version": "1.0.0",
|
|
157
|
-
"applies": [
|
|
165
|
+
"applies": [
|
|
166
|
+
"ui"
|
|
167
|
+
],
|
|
158
168
|
"quick": true
|
|
159
169
|
},
|
|
160
170
|
{
|
|
161
|
-
"name": "
|
|
162
|
-
"description": "
|
|
171
|
+
"name": "idempotency-guardian",
|
|
172
|
+
"description": "Helland-inspired idempotency analysis — classify mutation points, check protection mechanisms, evaluate side effect safety for retry-safe systems",
|
|
163
173
|
"type": "prompt",
|
|
164
174
|
"version": "1.0.0",
|
|
165
|
-
"applies": [
|
|
175
|
+
"applies": [
|
|
176
|
+
"integration"
|
|
177
|
+
],
|
|
166
178
|
"quick": false
|
|
167
179
|
},
|
|
168
180
|
{
|
|
169
|
-
"name": "
|
|
170
|
-
"description": "
|
|
181
|
+
"name": "improve-my-codebase",
|
|
182
|
+
"description": "Orchestrator skill that runs every applicable audit skill in parallel and produces a prioritized, convergence-ranked improvement report",
|
|
171
183
|
"type": "prompt",
|
|
172
|
-
"version": "1.0.
|
|
173
|
-
"applies": ["architecture"],
|
|
174
|
-
"quick": false
|
|
184
|
+
"version": "1.0.1"
|
|
175
185
|
},
|
|
176
186
|
{
|
|
177
|
-
"name": "
|
|
178
|
-
"description": "
|
|
187
|
+
"name": "integration-pattern-auditor",
|
|
188
|
+
"description": "Hohpe & Woolf-inspired messaging analysis — name the integration pattern, verify delivery guarantees, identify missing infrastructure",
|
|
179
189
|
"type": "prompt",
|
|
180
190
|
"version": "1.0.0",
|
|
181
|
-
"applies": [
|
|
191
|
+
"applies": [
|
|
192
|
+
"integration"
|
|
193
|
+
],
|
|
182
194
|
"quick": false
|
|
183
195
|
},
|
|
184
196
|
{
|
|
185
|
-
"name": "
|
|
186
|
-
"description": "
|
|
197
|
+
"name": "magpie",
|
|
198
|
+
"description": "Interactive PR review pipeline. Runs five parallel specialist subagents (security, bugs, performance, code-smells, architecture), dedupes findings, applies a critic rubric, peer-reviews via codex exec, and serves an interactive HTML report for selecting findings to post via gh. Bundles a Bun CLI installed onto PATH via the skill's postinstall step. Use when the user asks to review a GitHub pull request.",
|
|
187
199
|
"type": "prompt",
|
|
188
|
-
"version": "
|
|
189
|
-
"applies": ["any"],
|
|
190
|
-
"quick": false
|
|
200
|
+
"version": "0.3.0"
|
|
191
201
|
},
|
|
192
202
|
{
|
|
193
|
-
"name": "
|
|
194
|
-
"description": "
|
|
203
|
+
"name": "module-secret-auditor",
|
|
204
|
+
"description": "Parnas-inspired information hiding analysis — module boundaries drawn by change-reason, not by noun or technical layer",
|
|
195
205
|
"type": "prompt",
|
|
196
206
|
"version": "1.0.0",
|
|
197
|
-
"applies": [
|
|
207
|
+
"applies": [
|
|
208
|
+
"architecture"
|
|
209
|
+
],
|
|
198
210
|
"quick": true
|
|
199
211
|
},
|
|
200
212
|
{
|
|
201
|
-
"name": "
|
|
202
|
-
"description": "
|
|
213
|
+
"name": "port-adapter-auditor",
|
|
214
|
+
"description": "Cockburn-inspired hexagonal architecture analysis — identify ports and adapters, classify boundary health, ensure core testability and swappability",
|
|
203
215
|
"type": "prompt",
|
|
204
216
|
"version": "1.0.0",
|
|
205
|
-
"applies": [
|
|
217
|
+
"applies": [
|
|
218
|
+
"architecture"
|
|
219
|
+
],
|
|
206
220
|
"quick": false
|
|
207
221
|
},
|
|
208
222
|
{
|
|
209
|
-
"name": "
|
|
210
|
-
"description": "
|
|
223
|
+
"name": "rams-design-audit",
|
|
224
|
+
"description": "Dieter Rams-inspired design audit — every visual element must earn its presence, less but better, clarity through restraint",
|
|
211
225
|
"type": "prompt",
|
|
212
226
|
"version": "1.0.0",
|
|
213
|
-
"applies": [
|
|
227
|
+
"applies": [
|
|
228
|
+
"ui"
|
|
229
|
+
],
|
|
214
230
|
"quick": true
|
|
215
231
|
},
|
|
216
232
|
{
|
|
217
|
-
"name": "
|
|
218
|
-
"description": "
|
|
219
|
-
"type": "prompt",
|
|
220
|
-
"version": "1.1.0"
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
"name": "cover-letter",
|
|
224
|
-
"description": "Cover letter suite router. Routes to write, audit, rewrite, or persona subcommands; shares conventions for file I/O, output formats, and writing principles.",
|
|
233
|
+
"name": "seam-finder",
|
|
234
|
+
"description": "Feathers-inspired legacy code modification — find seams, make minimal incisions, preserve existing behavior",
|
|
225
235
|
"type": "prompt",
|
|
226
|
-
"version": "1.0.0"
|
|
236
|
+
"version": "1.0.0",
|
|
237
|
+
"applies": [
|
|
238
|
+
"legacy"
|
|
239
|
+
],
|
|
240
|
+
"quick": true
|
|
227
241
|
},
|
|
228
242
|
{
|
|
229
|
-
"name": "
|
|
230
|
-
"description": "
|
|
243
|
+
"name": "simplicity-razor",
|
|
244
|
+
"description": "Hickey-inspired simplicity analysis — simple vs easy, complecting detection, strand decomposition",
|
|
231
245
|
"type": "prompt",
|
|
232
|
-
"version": "1.0.0"
|
|
246
|
+
"version": "1.0.0",
|
|
247
|
+
"applies": [
|
|
248
|
+
"any"
|
|
249
|
+
],
|
|
250
|
+
"quick": false
|
|
233
251
|
},
|
|
234
252
|
{
|
|
235
|
-
"name": "
|
|
236
|
-
"description": "
|
|
253
|
+
"name": "temporal-coupling-detector",
|
|
254
|
+
"description": "Hidden ordering dependency analysis — detect two-phase init, method order dependencies, invisible preconditions, and resource lifecycle violations; fix with types, parameters, and factory patterns",
|
|
237
255
|
"type": "prompt",
|
|
238
|
-
"version": "1.0.0"
|
|
256
|
+
"version": "1.0.0",
|
|
257
|
+
"applies": [
|
|
258
|
+
"any"
|
|
259
|
+
],
|
|
260
|
+
"quick": true
|
|
239
261
|
},
|
|
240
262
|
{
|
|
241
|
-
"name": "
|
|
242
|
-
"description": "
|
|
263
|
+
"name": "terse",
|
|
264
|
+
"description": "Professional output compression. Cuts ~20-30% of output tokens with proper grammar and semantic accuracy. Three levels: clean, tight, sharp.",
|
|
243
265
|
"type": "prompt",
|
|
244
|
-
"version": "1.
|
|
266
|
+
"version": "1.2.0"
|
|
245
267
|
},
|
|
246
268
|
{
|
|
247
|
-
"name": "
|
|
248
|
-
"description": "
|
|
269
|
+
"name": "type-driven-designer",
|
|
270
|
+
"description": "Wlaschin & Minsky-inspired type design — make illegal states unrepresentable through branded types, discriminated unions, and domain-encoded constraints",
|
|
249
271
|
"type": "prompt",
|
|
250
|
-
"version": "1.0.0"
|
|
272
|
+
"version": "1.0.0",
|
|
273
|
+
"applies": [
|
|
274
|
+
"any"
|
|
275
|
+
],
|
|
276
|
+
"quick": false
|
|
251
277
|
},
|
|
252
278
|
{
|
|
253
|
-
"name": "
|
|
254
|
-
"description": "
|
|
279
|
+
"name": "unidirectional-flow-enforcer",
|
|
280
|
+
"description": "Elm Architecture-inspired data flow analysis — enforce unidirectional state flow, detect bidirectional mutations, trace state lifecycle in UI applications",
|
|
255
281
|
"type": "prompt",
|
|
256
|
-
"version": "1.0.0"
|
|
282
|
+
"version": "1.0.0",
|
|
283
|
+
"applies": [
|
|
284
|
+
"ui"
|
|
285
|
+
],
|
|
286
|
+
"quick": false
|
|
257
287
|
}
|
|
258
288
|
]
|
package/skills/magpie/README.md
CHANGED
|
@@ -15,15 +15,13 @@ Given a GitHub PR number, dispatches five specialist subagents in parallel (secu
|
|
|
15
15
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
18
|
-
This skill ships in two parts: the prompt (SKILL.md) and a companion Bun CLI (bin + scripts). Both are needed, and `install.sh` handles both in one step.
|
|
19
|
-
|
|
20
18
|
```
|
|
21
|
-
|
|
19
|
+
bunx @iceinvein/agent-skills install magpie -g
|
|
22
20
|
```
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
This skill ships in two parts: the prompt (SKILL.md) and a companion Bun CLI (bin + scripts). The agent-skills installer writes both into `~/.claude/skills/magpie/` and then runs the bundled `install.sh` as a postinstall step, which symlinks `bin/magpie` onto your PATH (preferring `/usr/local/bin`, falling back to `~/.local/bin`). Removing the skill with `agent-skills remove magpie -g` runs `uninstall.sh` first to undo the PATH symlink.
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
If you cloned this repo and want to run from source, you can also invoke `./install.sh` directly: it does the PATH-link step against the local source tree.
|
|
27
25
|
|
|
28
26
|
## Use
|
|
29
27
|
|
|
@@ -57,11 +55,12 @@ The fixture lives at `fixtures/example-pr/` (pr.json + findings.final.json + pos
|
|
|
57
55
|
|
|
58
56
|
- `SKILL.md` is the agent-facing prompt; installed by the agent-skills CLI.
|
|
59
57
|
- `skill.json` is the agent-skills manifest.
|
|
60
|
-
- `bin/magpie` is the CLI invoked by the agent during stages;
|
|
58
|
+
- `bin/magpie` is the CLI invoked by the agent during stages; symlinked onto PATH by `install.sh`.
|
|
61
59
|
- `scripts/` holds the implementation (server, dedupe, render, setup, cleanup, etc.).
|
|
62
60
|
- `templates/styles.css` is the HTML report stylesheet.
|
|
63
|
-
- `fixtures/` holds canned PR data for tests.
|
|
64
|
-
- `install.sh`
|
|
61
|
+
- `fixtures/` holds canned PR data for tests (not shipped to users via the registry).
|
|
62
|
+
- `install.sh` is run as a postinstall step by the agent-skills installer; symlinks `bin/magpie` onto PATH and records the location.
|
|
63
|
+
- `uninstall.sh` is run as a postremove step; removes the PATH symlink if it still points back into this bundle.
|
|
65
64
|
|
|
66
65
|
## Design docs
|
|
67
66
|
|
package/skills/magpie/install.sh
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
|
+
# This script is run by agent-skills as a postinstall step after the magpie
|
|
5
|
+
# skill bundle has been written to disk. Its only job is to put the magpie
|
|
6
|
+
# CLI on PATH and record where it linked, so uninstall.sh can clean up.
|
|
7
|
+
|
|
4
8
|
SOURCE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
5
|
-
|
|
9
|
+
BIN_SOURCE="$SOURCE_DIR/bin/magpie"
|
|
10
|
+
STATE_FILE="$SOURCE_DIR/.installed-cli-path"
|
|
6
11
|
|
|
7
|
-
if [
|
|
8
|
-
echo "
|
|
9
|
-
echo "Remove or move it manually, then re-run."
|
|
12
|
+
if [ ! -f "$BIN_SOURCE" ]; then
|
|
13
|
+
echo "magpie postinstall: bin/magpie missing at $BIN_SOURCE" >&2
|
|
10
14
|
exit 1
|
|
11
15
|
fi
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
ln -snf "$SOURCE_DIR" "$TARGET"
|
|
15
|
-
|
|
16
|
-
echo "Installed magpie skill at $TARGET"
|
|
17
|
-
|
|
18
|
-
# Install the magpie CLI onto PATH. Try /usr/local/bin first (no sudo
|
|
19
|
-
# needed if the user owns it, common on Homebrew Macs), fall back to
|
|
20
|
-
# ~/.local/bin (which most modern shells already have on PATH; we print a
|
|
21
|
-
# nudge if it isn't).
|
|
22
|
-
BIN_SOURCE="$SOURCE_DIR/bin/magpie"
|
|
17
|
+
chmod +x "$BIN_SOURCE" || true
|
|
23
18
|
|
|
24
19
|
install_link() {
|
|
25
20
|
local dest_dir="$1"
|
|
@@ -28,11 +23,9 @@ install_link() {
|
|
|
28
23
|
if [ ! -d "$dest_dir" ]; then
|
|
29
24
|
mkdir -p "$dest_dir" 2>/dev/null || return 1
|
|
30
25
|
fi
|
|
31
|
-
|
|
32
26
|
if [ ! -w "$dest_dir" ]; then
|
|
33
27
|
return 1
|
|
34
28
|
fi
|
|
35
|
-
|
|
36
29
|
if [ -e "$dest" ] && [ ! -L "$dest" ]; then
|
|
37
30
|
echo " skipping $dest_dir (a non-symlink magpie already exists there)"
|
|
38
31
|
return 2
|
|
@@ -40,23 +33,21 @@ install_link() {
|
|
|
40
33
|
|
|
41
34
|
ln -snf "$BIN_SOURCE" "$dest"
|
|
42
35
|
echo " linked $dest -> $BIN_SOURCE"
|
|
43
|
-
|
|
36
|
+
printf '%s\n' "$dest" > "$STATE_FILE"
|
|
44
37
|
return 0
|
|
45
38
|
}
|
|
46
39
|
|
|
47
|
-
echo ""
|
|
48
40
|
echo "Installing magpie CLI on PATH:"
|
|
49
41
|
|
|
50
42
|
CLI_INSTALL_DIR=""
|
|
51
43
|
if install_link "/usr/local/bin"; then
|
|
52
|
-
|
|
44
|
+
CLI_INSTALL_DIR="/usr/local/bin"
|
|
53
45
|
elif install_link "$HOME/.local/bin"; then
|
|
54
|
-
|
|
46
|
+
CLI_INSTALL_DIR="$HOME/.local/bin"
|
|
55
47
|
else
|
|
56
48
|
echo " Could not write to /usr/local/bin or ~/.local/bin."
|
|
57
|
-
echo " Add this to your shell rc instead:"
|
|
49
|
+
echo " Add this alias to your shell rc instead:"
|
|
58
50
|
echo " alias magpie='bun $BIN_SOURCE.ts'"
|
|
59
|
-
CLI_INSTALL_DIR=""
|
|
60
51
|
fi
|
|
61
52
|
|
|
62
53
|
if [ -n "$CLI_INSTALL_DIR" ]; then
|
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto'
|
|
2
2
|
import { appendFile, readFile, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
|
+
import { parseUnifiedDiffToHunks, splitDiffByFile } from './diff-utils.ts'
|
|
4
5
|
import { formatFindingDescriptionMarkdown } from './finding-description.ts'
|
|
5
6
|
import { type FocusId, parseFinding, type ReviewFinding, type Severity } from './types.ts'
|
|
6
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Build a per-file set of RIGHT-side line numbers that GitHub's PR Reviews API
|
|
10
|
+
* will accept as inline-comment anchors (added or context lines within hunks).
|
|
11
|
+
* Returns an empty map when the diff is empty/unavailable.
|
|
12
|
+
*/
|
|
13
|
+
function buildValidRightLines(diff: string): Map<string, Set<number>> {
|
|
14
|
+
const result = new Map<string, Set<number>>()
|
|
15
|
+
if (!diff) return result
|
|
16
|
+
for (const [file, chunk] of splitDiffByFile(diff)) {
|
|
17
|
+
const hunks = parseUnifiedDiffToHunks(chunk)
|
|
18
|
+
const set = new Set<number>()
|
|
19
|
+
for (const h of hunks) {
|
|
20
|
+
for (const l of h.lines) {
|
|
21
|
+
if (l.newLineNo != null && (l.type === 'added' || l.type === 'context')) {
|
|
22
|
+
set.add(l.newLineNo)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
result.set(file, set)
|
|
27
|
+
}
|
|
28
|
+
return result
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
export type PostInput = {
|
|
8
32
|
runDir: string
|
|
9
33
|
findingIds: string[]
|
|
@@ -289,6 +313,12 @@ export type PostReviewInput = {
|
|
|
289
313
|
findingIds: string[]
|
|
290
314
|
prNumber: number
|
|
291
315
|
headSha: string
|
|
316
|
+
/**
|
|
317
|
+
* Target repo as "owner/name". When omitted, the `{owner}/{repo}` gh
|
|
318
|
+
* placeholder is used, which resolves from the gh process's cwd. The server
|
|
319
|
+
* always passes this explicitly so the call does not depend on cwd.
|
|
320
|
+
*/
|
|
321
|
+
repo?: string
|
|
292
322
|
reviewBody?: string
|
|
293
323
|
ghBin?: string
|
|
294
324
|
dryRun?: boolean
|
|
@@ -364,25 +394,59 @@ export async function postFindingsAsReview(input: PostReviewInput): Promise<Post
|
|
|
364
394
|
// findings.final.json missing or unparseable; strictById stays empty
|
|
365
395
|
}
|
|
366
396
|
|
|
367
|
-
//
|
|
397
|
+
// Build the set of (file, RIGHT-side line) pairs GitHub will accept as inline
|
|
398
|
+
// anchors. Inline comments on lines outside this set get 422'd and would
|
|
399
|
+
// poison the whole batch; instead we demote them to the review body.
|
|
400
|
+
let validRightLines: Map<string, Set<number>> | null = null
|
|
401
|
+
try {
|
|
402
|
+
const diff = await readFile(join(input.runDir, 'diff.patch'), 'utf8')
|
|
403
|
+
validRightLines = buildValidRightLines(diff)
|
|
404
|
+
} catch {
|
|
405
|
+
// diff.patch absent (archived/legacy runs); skip validation and trust caller.
|
|
406
|
+
validRightLines = null
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Partition into inline (has file + line in diff) and unplaced (everything else).
|
|
368
410
|
type InlineComment = { path: string; line: number; side: 'RIGHT'; body: string }
|
|
369
411
|
const inlineComments: InlineComment[] = []
|
|
370
412
|
const unplacedBodies: string[] = []
|
|
413
|
+
const demotedIds: string[] = []
|
|
371
414
|
|
|
372
415
|
for (const id of input.findingIds) {
|
|
373
|
-
const f = byId.get(id)
|
|
374
|
-
if (!f) continue
|
|
375
416
|
const strict = strictById.get(id)
|
|
376
|
-
|
|
417
|
+
const f =
|
|
418
|
+
byId.get(id) ??
|
|
419
|
+
(strict
|
|
420
|
+
? {
|
|
421
|
+
id: strict.id,
|
|
422
|
+
file: strict.file,
|
|
423
|
+
line: strict.line,
|
|
424
|
+
title: strict.title,
|
|
425
|
+
description: strict.description,
|
|
426
|
+
}
|
|
427
|
+
: null)
|
|
428
|
+
if (!f) continue
|
|
429
|
+
const lineInDiff =
|
|
430
|
+
f.line != null &&
|
|
431
|
+
f.file != null &&
|
|
432
|
+
(validRightLines == null || validRightLines.get(f.file)?.has(f.line) === true)
|
|
433
|
+
if (lineInDiff) {
|
|
377
434
|
const body = strict
|
|
378
435
|
? formatInlineBody(strict)
|
|
379
436
|
: formatFindingDescriptionMarkdown(f.description)
|
|
380
437
|
inlineComments.push({
|
|
381
|
-
path: f.file,
|
|
382
|
-
line: f.line,
|
|
438
|
+
path: f.file as string,
|
|
439
|
+
line: f.line as number,
|
|
383
440
|
side: 'RIGHT',
|
|
384
441
|
body,
|
|
385
442
|
})
|
|
443
|
+
} else if (f.line != null && f.file != null) {
|
|
444
|
+
demotedIds.push(id)
|
|
445
|
+
const anchor = `\`${f.file}:${f.line}\` (anchor not in PR diff, posted in review body)`
|
|
446
|
+
const inner = strict
|
|
447
|
+
? formatConversationBody(strict)
|
|
448
|
+
: `**${f.title}**\n\n${formatFindingDescriptionMarkdown(f.description)}`
|
|
449
|
+
unplacedBodies.push(`${anchor}\n\n${inner}`)
|
|
386
450
|
} else {
|
|
387
451
|
const body = strict
|
|
388
452
|
? formatConversationBody(strict)
|
|
@@ -406,20 +470,30 @@ export async function postFindingsAsReview(input: PostReviewInput): Promise<Post
|
|
|
406
470
|
}
|
|
407
471
|
const payload = JSON.stringify(payloadObj)
|
|
408
472
|
|
|
473
|
+
const repoSlug = input.repo ?? '{owner}/{repo}'
|
|
409
474
|
const command = [
|
|
410
475
|
bin,
|
|
411
476
|
'api',
|
|
412
|
-
`repos
|
|
477
|
+
`repos/${repoSlug}/pulls/${input.prNumber}/reviews`,
|
|
413
478
|
'--method',
|
|
414
479
|
'POST',
|
|
415
480
|
'--input',
|
|
416
481
|
'-',
|
|
417
482
|
]
|
|
418
483
|
|
|
484
|
+
const demoted = new Set(demotedIds)
|
|
485
|
+
const buildCommentResults = (status: 'posted' | 'failed', message?: string) =>
|
|
486
|
+
input.findingIds.map((id) => {
|
|
487
|
+
const base: PostReviewCommentResult = { id, status }
|
|
488
|
+
const m =
|
|
489
|
+
message ?? (demoted.has(id) ? 'anchor not in PR diff, posted in review body' : undefined)
|
|
490
|
+
return m ? { ...base, message: m } : base
|
|
491
|
+
})
|
|
492
|
+
|
|
419
493
|
if (input.dryRun) {
|
|
420
494
|
return {
|
|
421
495
|
reviewId: null,
|
|
422
|
-
comments:
|
|
496
|
+
comments: buildCommentResults('posted'),
|
|
423
497
|
command,
|
|
424
498
|
payload,
|
|
425
499
|
}
|
|
@@ -444,7 +518,7 @@ export async function postFindingsAsReview(input: PostReviewInput): Promise<Post
|
|
|
444
518
|
const msg = (err as Error).message ?? `cannot spawn ${bin}`
|
|
445
519
|
return {
|
|
446
520
|
reviewId: null,
|
|
447
|
-
comments:
|
|
521
|
+
comments: buildCommentResults('failed', msg),
|
|
448
522
|
command,
|
|
449
523
|
payload,
|
|
450
524
|
}
|
|
@@ -453,11 +527,7 @@ export async function postFindingsAsReview(input: PostReviewInput): Promise<Post
|
|
|
453
527
|
if (exit !== 0) {
|
|
454
528
|
return {
|
|
455
529
|
reviewId: null,
|
|
456
|
-
comments:
|
|
457
|
-
id,
|
|
458
|
-
status: 'failed' as const,
|
|
459
|
-
message: stderrText.trim(),
|
|
460
|
-
})),
|
|
530
|
+
comments: buildCommentResults('failed', stderrText.trim()),
|
|
461
531
|
command,
|
|
462
532
|
payload,
|
|
463
533
|
}
|
|
@@ -476,7 +546,7 @@ export async function postFindingsAsReview(input: PostReviewInput): Promise<Post
|
|
|
476
546
|
|
|
477
547
|
return {
|
|
478
548
|
reviewId,
|
|
479
|
-
comments:
|
|
549
|
+
comments: buildCommentResults('posted'),
|
|
480
550
|
command,
|
|
481
551
|
payload,
|
|
482
552
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { readdir, unlink } from 'node:fs/promises'
|
|
1
|
+
import { readdir, readFile, unlink } from 'node:fs/promises'
|
|
2
2
|
import { basename, join } from 'node:path'
|
|
3
3
|
import { type PostStatusMap, renderFindingsToDisk } from './render-findings.ts'
|
|
4
|
-
import { parseFinding } from './types.ts'
|
|
4
|
+
import { type PrFileEntry, parseFinding } from './types.ts'
|
|
5
5
|
|
|
6
6
|
export type RefreshResult = {
|
|
7
7
|
refreshed: boolean
|
|
@@ -60,6 +60,7 @@ export async function refreshFindings(runDir: string): Promise<RefreshResult> {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
let pr: { number: number; branch: string; headSha: string } | undefined
|
|
63
|
+
let files: PrFileEntry[] = []
|
|
63
64
|
try {
|
|
64
65
|
const prJson = (await Bun.file(join(runDir, 'pr.json')).json()) as Record<string, unknown>
|
|
65
66
|
const prNumber = Number(prJson.number ?? 0)
|
|
@@ -70,12 +71,23 @@ export async function refreshFindings(runDir: string): Promise<RefreshResult> {
|
|
|
70
71
|
headSha: String(prJson.headRefOid ?? '?'),
|
|
71
72
|
}
|
|
72
73
|
}
|
|
74
|
+
const filesArray = Array.isArray(prJson.files) ? (prJson.files as unknown[]) : []
|
|
75
|
+
files = filesArray.map((f) => {
|
|
76
|
+
const entry = f as Record<string, unknown>
|
|
77
|
+
return {
|
|
78
|
+
path: String(entry.path ?? ''),
|
|
79
|
+
additions: Number(entry.additions ?? 0),
|
|
80
|
+
deletions: Number(entry.deletions ?? 0),
|
|
81
|
+
}
|
|
82
|
+
})
|
|
73
83
|
} catch {
|
|
74
84
|
// optional file; archived runs may not include pr.json
|
|
75
85
|
}
|
|
76
86
|
|
|
87
|
+
const diff = await readFile(join(runDir, 'diff.patch'), 'utf8').catch(() => '')
|
|
88
|
+
|
|
77
89
|
await renderFindingsToDisk(
|
|
78
|
-
{ findings, postStatus, runId: basename(runDir), pr },
|
|
90
|
+
{ findings, postStatus, runId: basename(runDir), pr, files, diff },
|
|
79
91
|
join(screenDir, 'findings.html'),
|
|
80
92
|
)
|
|
81
93
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFile, readdir, readFile, stat, unlink, writeFile } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { postFindingsAsReview } from './post-cmd.ts'
|
|
3
|
+
import { parseRepoFromUrl, postFindingsAsReview } from './post-cmd.ts'
|
|
4
4
|
|
|
5
5
|
export type ServerHandle = {
|
|
6
6
|
url: string
|
|
@@ -130,12 +130,23 @@ export async function startServer(input: StartServerInput): Promise<ServerHandle
|
|
|
130
130
|
const prJson = JSON.parse(await readFile(join(runDir, 'pr.json'), 'utf8')) as {
|
|
131
131
|
number: number
|
|
132
132
|
headRefOid: string
|
|
133
|
+
url?: string
|
|
134
|
+
}
|
|
135
|
+
const repo = prJson.url ? parseRepoFromUrl(prJson.url) : null
|
|
136
|
+
if (!repo) {
|
|
137
|
+
return new Response(
|
|
138
|
+
JSON.stringify({
|
|
139
|
+
error: 'Cannot resolve target repo from pr.json (missing or unparseable url).',
|
|
140
|
+
}),
|
|
141
|
+
{ status: 500, headers: { 'content-type': 'application/json' } },
|
|
142
|
+
)
|
|
133
143
|
}
|
|
134
144
|
const result = await postFindingsAsReview({
|
|
135
145
|
runDir,
|
|
136
146
|
findingIds: ids,
|
|
137
147
|
prNumber: prJson.number,
|
|
138
148
|
headSha: prJson.headRefOid,
|
|
149
|
+
repo,
|
|
139
150
|
dryRun: process.env.MAGPIE_DRY_RUN_POST === '1',
|
|
140
151
|
})
|
|
141
152
|
const status =
|
package/skills/magpie/skill.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magpie",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Interactive PR review pipeline. Runs five parallel specialist subagents (security, bugs, performance, code-smells, architecture), dedupes findings, applies a critic rubric, peer-reviews via codex exec, and serves an interactive HTML report for selecting findings to post via gh. Bundles a Bun CLI installed
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Interactive PR review pipeline. Runs five parallel specialist subagents (security, bugs, performance, code-smells, architecture), dedupes findings, applies a critic rubric, peer-reviews via codex exec, and serves an interactive HTML report for selecting findings to post via gh. Bundles a Bun CLI installed onto PATH via the skill's postinstall step. Use when the user asks to review a GitHub pull request.",
|
|
5
5
|
"author": "iceinvein",
|
|
6
6
|
"type": "prompt",
|
|
7
7
|
"tools": [
|
|
@@ -10,9 +10,28 @@
|
|
|
10
10
|
"files": {
|
|
11
11
|
"prompt": "SKILL.md"
|
|
12
12
|
},
|
|
13
|
+
"bundle": {
|
|
14
|
+
"include": [
|
|
15
|
+
"bin",
|
|
16
|
+
"scripts",
|
|
17
|
+
"templates",
|
|
18
|
+
"install.sh",
|
|
19
|
+
"uninstall.sh",
|
|
20
|
+
"package.json",
|
|
21
|
+
"bun.lock",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"exclude": [
|
|
26
|
+
"scripts/__tests__/"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
13
29
|
"install": {
|
|
14
30
|
"claude": {
|
|
15
|
-
"prompt": ".claude/skills/magpie/SKILL.md"
|
|
31
|
+
"prompt": ".claude/skills/magpie/SKILL.md",
|
|
32
|
+
"bundleRoot": ".claude/skills/magpie",
|
|
33
|
+
"postinstall": "install.sh",
|
|
34
|
+
"postremove": "uninstall.sh"
|
|
16
35
|
}
|
|
17
36
|
}
|
|
18
37
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Run by agent-skills as a postremove step before the magpie skill bundle is
|
|
5
|
+
# deleted. Reads the path recorded by install.sh and removes the PATH symlink
|
|
6
|
+
# if it still points back into this bundle.
|
|
7
|
+
|
|
8
|
+
SOURCE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
|
+
STATE_FILE="$SOURCE_DIR/.installed-cli-path"
|
|
10
|
+
|
|
11
|
+
if [ ! -f "$STATE_FILE" ]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
CLI_PATH="$(head -n 1 "$STATE_FILE")"
|
|
16
|
+
[ -z "$CLI_PATH" ] && exit 0
|
|
17
|
+
|
|
18
|
+
if [ -L "$CLI_PATH" ]; then
|
|
19
|
+
TARGET="$(readlink "$CLI_PATH")"
|
|
20
|
+
case "$TARGET" in
|
|
21
|
+
"$SOURCE_DIR"/*)
|
|
22
|
+
rm -f "$CLI_PATH"
|
|
23
|
+
echo "Removed magpie CLI symlink at $CLI_PATH"
|
|
24
|
+
;;
|
|
25
|
+
*)
|
|
26
|
+
echo "magpie uninstall: leaving $CLI_PATH alone (points to $TARGET, not this bundle)"
|
|
27
|
+
;;
|
|
28
|
+
esac
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
rm -f "$STATE_FILE"
|