@iceinvein/agent-skills 0.23.9 → 0.23.13

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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/skills/index.json +2 -2
  3. package/skills/magpie/SKILL.md +21 -20
  4. package/skills/magpie/bin/magpie.ts +63 -2
  5. package/skills/magpie/evals/quality/README.md +74 -1
  6. package/skills/magpie/evals/quality/__tests__/probe-specialist.test.ts +498 -0
  7. package/skills/magpie/evals/quality/__tests__/replay-full.test.ts +9 -0
  8. package/skills/magpie/evals/quality/probe-specialist.ts +672 -0
  9. package/skills/magpie/evals/quality/replay-full.ts +6 -2
  10. package/skills/magpie/fixtures/fake-gh-with-lockfile.sh +37 -2
  11. package/skills/magpie/fixtures/fake-gh.sh +49 -2
  12. package/skills/magpie/references/critic.md +20 -4
  13. package/skills/magpie/references/proof.md +102 -0
  14. package/skills/magpie/references/scout.md +6 -1
  15. package/skills/magpie/references/specialists.md +15 -9
  16. package/skills/magpie/scripts/__tests__/cleanup-cmd.test.ts +103 -7
  17. package/skills/magpie/scripts/__tests__/cli.test.ts +69 -0
  18. package/skills/magpie/scripts/__tests__/critic-cmd.test.ts +46 -0
  19. package/skills/magpie/scripts/__tests__/critic.test.ts +11 -1
  20. package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +20 -1
  21. package/skills/magpie/scripts/__tests__/git-diff.test.ts +23 -4
  22. package/skills/magpie/scripts/__tests__/handoff-cmd.test.ts +68 -0
  23. package/skills/magpie/scripts/__tests__/history.test.ts +232 -0
  24. package/skills/magpie/scripts/__tests__/impact-cmd.test.ts +477 -0
  25. package/skills/magpie/scripts/__tests__/impact-render.test.ts +365 -0
  26. package/skills/magpie/scripts/__tests__/impact.test.ts +915 -0
  27. package/skills/magpie/scripts/__tests__/incremental.test.ts +59 -8
  28. package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +1 -1
  29. package/skills/magpie/scripts/__tests__/post-cmd.test.ts +58 -0
  30. package/skills/magpie/scripts/__tests__/pr-activity.test.ts +281 -0
  31. package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +4 -4
  32. package/skills/magpie/scripts/__tests__/previous-findings.test.ts +267 -0
  33. package/skills/magpie/scripts/__tests__/proof-cmd.test.ts +260 -0
  34. package/skills/magpie/scripts/__tests__/proof.test.ts +226 -0
  35. package/skills/magpie/scripts/__tests__/refresh.test.ts +71 -0
  36. package/skills/magpie/scripts/__tests__/render-annotation.test.ts +34 -0
  37. package/skills/magpie/scripts/__tests__/render-findings.test.ts +75 -0
  38. package/skills/magpie/scripts/__tests__/render-progress.test.ts +9 -0
  39. package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +376 -1
  40. package/skills/magpie/scripts/__tests__/shard-coverage.test.ts +79 -0
  41. package/skills/magpie/scripts/__tests__/skill-lint.test.ts +48 -2
  42. package/skills/magpie/scripts/__tests__/status-cmd.test.ts +20 -8
  43. package/skills/magpie/scripts/__tests__/types.test.ts +46 -0
  44. package/skills/magpie/scripts/cleanup-cmd.ts +88 -0
  45. package/skills/magpie/scripts/critic-cmd.ts +2 -0
  46. package/skills/magpie/scripts/critic.ts +4 -0
  47. package/skills/magpie/scripts/dedupe-cmd.ts +4 -0
  48. package/skills/magpie/scripts/evidence-filter.ts +10 -0
  49. package/skills/magpie/scripts/git-diff.ts +50 -19
  50. package/skills/magpie/scripts/handoff-cmd.ts +42 -3
  51. package/skills/magpie/scripts/history.ts +143 -0
  52. package/skills/magpie/scripts/impact-cmd.ts +202 -0
  53. package/skills/magpie/scripts/impact-render.ts +158 -0
  54. package/skills/magpie/scripts/impact.ts +622 -0
  55. package/skills/magpie/scripts/incremental.ts +44 -5
  56. package/skills/magpie/scripts/labels-cmd.ts +7 -3
  57. package/skills/magpie/scripts/post-cmd.ts +46 -0
  58. package/skills/magpie/scripts/pr-activity.ts +243 -0
  59. package/skills/magpie/scripts/preview-cmd.ts +3 -0
  60. package/skills/magpie/scripts/previous-findings.ts +181 -0
  61. package/skills/magpie/scripts/proof-cmd.ts +226 -0
  62. package/skills/magpie/scripts/proof.ts +150 -0
  63. package/skills/magpie/scripts/refresh.ts +11 -0
  64. package/skills/magpie/scripts/render-annotation.ts +17 -0
  65. package/skills/magpie/scripts/render-cmd.ts +12 -0
  66. package/skills/magpie/scripts/render-findings.ts +37 -1
  67. package/skills/magpie/scripts/render-progress.ts +3 -0
  68. package/skills/magpie/scripts/setup-cmd.ts +142 -11
  69. package/skills/magpie/scripts/shard-coverage.ts +53 -0
  70. package/skills/magpie/scripts/status-cmd.ts +2 -0
  71. package/skills/magpie/scripts/types.ts +39 -0
  72. package/skills/magpie/skill.json +1 -1
  73. package/skills/magpie/templates/styles.css +30 -0
  74. package/skills/sluice/scripts/status.sh +32 -13
  75. package/skills/sluice/skill.json +1 -1
@@ -0,0 +1,202 @@
1
+ import { appendFile, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+ import {
4
+ buildImpactMap,
5
+ extractDiffNames,
6
+ type GrepHit,
7
+ type GrepPlan,
8
+ type ImpactMap,
9
+ parseGrepOutput,
10
+ planSearches,
11
+ } from './impact.ts'
12
+ import {
13
+ IMPACT_BRIEF_CAP_BYTES,
14
+ IMPACT_BRIEF_MAX_CHILDREN,
15
+ IMPACT_FULL_CAP_BYTES,
16
+ renderImpactMarkdown,
17
+ } from './impact-render.ts'
18
+ import { loadPathFilterConfig, matchesAny, type PathFilterConfig } from './path-filter.ts'
19
+ import type { ShardEntry } from './shard.ts'
20
+ import { type ReviewTarget, readTarget } from './target.ts'
21
+
22
+ const IMPACT_FILE =
23
+ /^impact(?:\.brief)?\.md$|^impact\.json$|^impact\.shard-.*\.md$|^impact\..*\.tmp$/
24
+
25
+ async function logLine(runDir: string, entry: Record<string, unknown>): Promise<void> {
26
+ await appendFile(join(runDir, 'log.jsonl'), `${JSON.stringify({ ...entry, ts: Date.now() })}\n`)
27
+ }
28
+
29
+ function isMissingFile(err: unknown): boolean {
30
+ return (err as NodeJS.ErrnoException)?.code === 'ENOENT'
31
+ }
32
+
33
+ async function readDiff(runDir: string): Promise<string> {
34
+ const path = join(runDir, 'diff.patch')
35
+ try {
36
+ return await readFile(path, 'utf8')
37
+ } catch (err) {
38
+ if (isMissingFile(err)) throw new Error(`${path} not found`)
39
+ throw err
40
+ }
41
+ }
42
+
43
+ // git grep exits 1 when nothing matches, which is an empty result, not a failure.
44
+ async function runGrep(root: string, plan: GrepPlan): Promise<GrepHit[]> {
45
+ const proc = Bun.spawn(['git', ...plan.args], {
46
+ cwd: root,
47
+ stdin: new TextEncoder().encode(`${plan.patterns.join('\n')}\n`),
48
+ stdout: 'pipe',
49
+ stderr: 'pipe',
50
+ })
51
+ const [out, err, exit] = await Promise.all([
52
+ new Response(proc.stdout).text(),
53
+ new Response(proc.stderr).text(),
54
+ proc.exited,
55
+ ])
56
+ if (exit === 1) return []
57
+ if (exit !== 0) throw new Error(`git grep exited ${exit}: ${err.trim()}`)
58
+ return parseGrepOutput(out)
59
+ }
60
+
61
+ /** A run with no manifest was never sharded, which is the same as one shard. */
62
+ async function readShards(runDir: string): Promise<ShardEntry[]> {
63
+ const path = join(runDir, 'shards', 'manifest.json')
64
+ let text: string
65
+ try {
66
+ text = await readFile(path, 'utf8')
67
+ } catch (err) {
68
+ if (isMissingFile(err)) return []
69
+ throw err
70
+ }
71
+ const shards = (JSON.parse(text) as { shards?: unknown }).shards
72
+ if (!Array.isArray(shards)) throw new Error(`${path}: shards must be an array`)
73
+ for (const s of shards as Array<Partial<ShardEntry>>) {
74
+ if (typeof s?.id !== 'number' || !Array.isArray(s.files)) {
75
+ throw new Error(`${path}: each shard needs a numeric id and a files array`)
76
+ }
77
+ }
78
+ return shards as ShardEntry[]
79
+ }
80
+
81
+ // The same include/exclude decision filterDiff applies to the diff.
82
+ function isFilteredOut(file: string, config: PathFilterConfig): boolean {
83
+ if (config.include.length > 0) return matchesAny(file, config.include) === null
84
+ return matchesAny(file, config.exclude) !== null
85
+ }
86
+
87
+ async function removeImpactFiles(runDir: string): Promise<void> {
88
+ for (const name of await readdir(runDir)) {
89
+ if (IMPACT_FILE.test(name)) await rm(join(runDir, name), { force: true })
90
+ }
91
+ }
92
+
93
+ // Written beside the target and renamed over it, so a run cut off mid-write leaves
94
+ // either the whole file or none: a reader of a half-written file gets malformed JSON.
95
+ async function writeAtomic(path: string, text: string): Promise<void> {
96
+ const temp = `${path}.tmp`
97
+ await writeFile(temp, text)
98
+ await rename(temp, path)
99
+ }
100
+
101
+ async function writeIfNonEmpty(path: string, text: string): Promise<void> {
102
+ if (text.length > 0) await writeAtomic(path, `${text}\n`)
103
+ }
104
+
105
+ async function writeRenders(runDir: string, map: ImpactMap, prefix: string, shard: number | null) {
106
+ await writeIfNonEmpty(
107
+ join(runDir, `${prefix}.md`),
108
+ renderImpactMarkdown(map, { shard, capBytes: IMPACT_FULL_CAP_BYTES }),
109
+ )
110
+ await writeIfNonEmpty(
111
+ join(runDir, `${prefix}.brief.md`),
112
+ renderImpactMarkdown(map, {
113
+ shard,
114
+ capBytes: IMPACT_BRIEF_CAP_BYTES,
115
+ maxChildren: IMPACT_BRIEF_MAX_CHILDREN,
116
+ }),
117
+ )
118
+ }
119
+
120
+ // setup reads .magpie.json from the user's checkout. A PR run's root is a worktree
121
+ // at the PR head, so the checkout is the one owning the shared git dir.
122
+ async function configDir(target: ReviewTarget): Promise<string> {
123
+ if (target.mode === 'local') return target.root
124
+ const args = ['rev-parse', '--path-format=absolute', '--git-common-dir']
125
+ const proc = Bun.spawn(['git', ...args], { cwd: target.root, stdout: 'pipe', stderr: 'pipe' })
126
+ const [out, err, exit] = await Promise.all([
127
+ new Response(proc.stdout).text(),
128
+ new Response(proc.stderr).text(),
129
+ proc.exited,
130
+ ])
131
+ if (exit !== 0) throw new Error(`git ${args.join(' ')} exited ${exit}: ${err.trim()}`)
132
+ return dirname(out.trim())
133
+ }
134
+
135
+ async function buildMap(
136
+ runDir: string,
137
+ target: ReviewTarget,
138
+ shards: ShardEntry[],
139
+ ): Promise<ImpactMap> {
140
+ const root = target.root
141
+ const names = extractDiffNames(await readDiff(runDir))
142
+ const config = await loadPathFilterConfig(await configDir(target))
143
+ const hits: Record<GrepPlan['kind'], GrepHit[]> = {
144
+ references: [],
145
+ definitions: [],
146
+ writers: [],
147
+ routes: [],
148
+ docs: [],
149
+ }
150
+ for (const plan of planSearches(names)) hits[plan.kind].push(...(await runGrep(root, plan)))
151
+ const sharded = shards.length > 1
152
+ return buildImpactMap({
153
+ names,
154
+ hits,
155
+ shardOf: (file) => (sharded ? (shards.find((s) => s.files.includes(file))?.id ?? null) : null),
156
+ excluded: (file) => isFilteredOut(file, config),
157
+ })
158
+ }
159
+
160
+ export async function runImpact(runDir: string): Promise<number> {
161
+ const isDir = await stat(runDir)
162
+ .then((s) => s.isDirectory())
163
+ .catch(() => false)
164
+ if (!isDir) {
165
+ process.stderr.write(`impact: no such run directory: ${runDir}\n`)
166
+ return 2
167
+ }
168
+ let target: ReviewTarget
169
+ try {
170
+ target = await readTarget(runDir)
171
+ } catch (err) {
172
+ process.stderr.write(`impact: ${err instanceof Error ? err.message : String(err)}\n`)
173
+ return 2
174
+ }
175
+
176
+ try {
177
+ // Cleared before the search so a skipped re-run leaves no map from an older diff.
178
+ await removeImpactFiles(runDir)
179
+ const shards = await readShards(runDir)
180
+ const map = await buildMap(runDir, target, shards)
181
+ await writeAtomic(join(runDir, 'impact.json'), `${JSON.stringify(map, null, 2)}\n`)
182
+ await writeRenders(runDir, map, 'impact', null)
183
+ if (shards.length > 1) {
184
+ for (const s of shards) await writeRenders(runDir, map, `impact.shard-${s.id}`, s.id)
185
+ }
186
+ await logLine(runDir, {
187
+ stage: 'impact',
188
+ status: 'done',
189
+ symbols: map.symbols.length,
190
+ uses: map.uses.length,
191
+ stores: map.stores.length,
192
+ routes: map.routes.length,
193
+ docs: map.docs.length,
194
+ })
195
+ return 0
196
+ } catch (err) {
197
+ const message = err instanceof Error ? err.message : String(err)
198
+ await logLine(runDir, { stage: 'impact', status: 'skipped', error: message })
199
+ process.stderr.write(`impact: skipped: ${message}\n`)
200
+ return 0
201
+ }
202
+ }
@@ -0,0 +1,158 @@
1
+ import { COMMON_NAME_HITS, type ImpactLocation, type ImpactMap, type ImpactRef } from './impact.ts'
2
+
3
+ export const IMPACT_FULL_CAP_BYTES = 8192
4
+ export const IMPACT_BRIEF_CAP_BYTES = 2048
5
+ export const IMPACT_BRIEF_MAX_CHILDREN = 3
6
+
7
+ const SECTIONS = [
8
+ 'Other writers of the same data',
9
+ 'References to changed symbols',
10
+ 'References to added symbols',
11
+ 'Definitions this change relies on',
12
+ 'Sibling routes',
13
+ 'Docs naming what changed',
14
+ ] as const
15
+
16
+ type Entry = {
17
+ section: number
18
+ anchor: ImpactLocation
19
+ shards: number[]
20
+ head: string
21
+ children: ImpactRef[]
22
+ // Symbols report their hit count; every other kind reports its child count.
23
+ total: number
24
+ }
25
+
26
+ function codeSpan(text: string): string {
27
+ return text.includes('`') ? `\`\` ${text} \`\`` : `\`${text}\``
28
+ }
29
+
30
+ function head(name: string, at: ImpactLocation): string {
31
+ return `- \`${name}\` (diff: ${at.file}:${at.line})`
32
+ }
33
+
34
+ function firstLocation(list: ImpactLocation[], what: string): ImpactLocation {
35
+ const at = list[0]
36
+ if (!at) throw new Error(`Impact ${what} has no diff location`)
37
+ return at
38
+ }
39
+
40
+ function shardList(shard: number | null): number[] {
41
+ return shard === null ? [] : [shard]
42
+ }
43
+
44
+ // Entries with no facts outside the diff are left out: they tell a reviewer nothing.
45
+ function entriesOf(map: ImpactMap): Entry[] {
46
+ const entries: Entry[] = []
47
+ for (const s of map.stores) {
48
+ if (s.writers.length === 0) continue
49
+ const anchor = firstLocation(s.usedIn, `store ${s.name}`)
50
+ entries.push({
51
+ section: 0,
52
+ anchor,
53
+ shards: shardList(s.shard),
54
+ head: head(s.name, anchor),
55
+ children: s.writers,
56
+ total: s.writers.length,
57
+ })
58
+ }
59
+ for (const s of map.symbols) {
60
+ const common = s.hits > COMMON_NAME_HITS
61
+ if (!common && s.references.length === 0) continue
62
+ const anchor = firstLocation(s.definedAt, `symbol ${s.name}`)
63
+ entries.push({
64
+ section: s.kind === 'added' ? 2 : 1,
65
+ anchor,
66
+ shards: shardList(s.shard),
67
+ head: common ? `- \`${s.name}\`: ${s.hits} hits, too common to list` : head(s.name, anchor),
68
+ children: common ? [] : s.references,
69
+ total: common ? 0 : s.hits,
70
+ })
71
+ }
72
+ for (const u of map.uses) {
73
+ if (u.definedAt.length === 0) continue
74
+ entries.push({
75
+ section: 3,
76
+ anchor: u.usedIn,
77
+ shards: shardList(u.shard),
78
+ head: head(u.name, u.usedIn),
79
+ children: u.definedAt,
80
+ total: u.definedAt.length,
81
+ })
82
+ }
83
+ for (const r of map.routes) {
84
+ if (r.siblings.length === 0) continue
85
+ entries.push({
86
+ section: 4,
87
+ anchor: r.usedIn,
88
+ shards: shardList(r.shard),
89
+ head: head(r.path, r.usedIn),
90
+ children: r.siblings,
91
+ total: r.siblings.length,
92
+ })
93
+ }
94
+ for (const d of map.docs) {
95
+ entries.push({
96
+ section: 5,
97
+ anchor: { file: d.file, line: d.line },
98
+ shards: d.shards,
99
+ head: `- ${d.file}:${d.line} ${codeSpan(d.text)} (names: ${d.mentions.join(', ')})`,
100
+ children: [],
101
+ total: 0,
102
+ })
103
+ }
104
+ return entries.sort((a, b) => {
105
+ if (a.section !== b.section) return a.section - b.section
106
+ if (a.anchor.file !== b.anchor.file) return a.anchor.file < b.anchor.file ? -1 : 1
107
+ return a.anchor.line - b.anchor.line
108
+ })
109
+ }
110
+
111
+ function entryLines(e: Entry, maxChildren: number): string[] {
112
+ const shown = e.children.slice(0, maxChildren)
113
+ const lines = [e.head, ...shown.map((r) => ` - ${r.file}:${r.line} ${codeSpan(r.text)}`)]
114
+ if (e.total > shown.length) lines.push(` - ${shown.length} of ${e.total} shown`)
115
+ return lines
116
+ }
117
+
118
+ function renderEntries(kept: Entry[], left: number, maxChildren: number): string {
119
+ const lines: string[] = []
120
+ let section = -1
121
+ for (const e of kept) {
122
+ if (e.section !== section) {
123
+ if (lines.length > 0) lines.push('')
124
+ section = e.section
125
+ lines.push(`### ${SECTIONS[section]}`)
126
+ }
127
+ lines.push(...entryLines(e, maxChildren))
128
+ }
129
+ if (left > 0) {
130
+ if (lines.length > 0) lines.push('')
131
+ lines.push(`${left} more entries in impact.json`)
132
+ }
133
+ return lines.join('\n')
134
+ }
135
+
136
+ function renderCapped(entries: Entry[], capBytes: number, maxChildren: number): string {
137
+ const fits = (s: string): boolean => Buffer.byteLength(s, 'utf8') <= capBytes
138
+ let kept: Entry[] = []
139
+ for (const e of entries) {
140
+ const trial = [...kept, e]
141
+ // Counting every entry not yet kept bounds the final count line from above.
142
+ if (fits(renderEntries(trial, entries.length - trial.length, maxChildren))) kept = trial
143
+ }
144
+ const out = renderEntries(kept, entries.length - kept.length, maxChildren)
145
+ if (!fits(out)) {
146
+ throw new Error(`Impact render cap of ${capBytes} bytes cannot hold its own count line`)
147
+ }
148
+ return out
149
+ }
150
+
151
+ export function renderImpactMarkdown(
152
+ map: ImpactMap,
153
+ opts: { shard: number | null; capBytes: number; maxChildren?: number },
154
+ ): string {
155
+ const { shard } = opts
156
+ const entries = entriesOf(map).filter((e) => shard === null || e.shards.includes(shard))
157
+ return renderCapped(entries, opts.capBytes, opts.maxChildren ?? Number.POSITIVE_INFINITY)
158
+ }