@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.
- package/package.json +1 -1
- package/skills/index.json +2 -2
- package/skills/magpie/SKILL.md +21 -20
- package/skills/magpie/bin/magpie.ts +63 -2
- package/skills/magpie/evals/quality/README.md +74 -1
- package/skills/magpie/evals/quality/__tests__/probe-specialist.test.ts +498 -0
- package/skills/magpie/evals/quality/__tests__/replay-full.test.ts +9 -0
- package/skills/magpie/evals/quality/probe-specialist.ts +672 -0
- package/skills/magpie/evals/quality/replay-full.ts +6 -2
- package/skills/magpie/fixtures/fake-gh-with-lockfile.sh +37 -2
- package/skills/magpie/fixtures/fake-gh.sh +49 -2
- package/skills/magpie/references/critic.md +20 -4
- package/skills/magpie/references/proof.md +102 -0
- package/skills/magpie/references/scout.md +6 -1
- package/skills/magpie/references/specialists.md +15 -9
- package/skills/magpie/scripts/__tests__/cleanup-cmd.test.ts +103 -7
- package/skills/magpie/scripts/__tests__/cli.test.ts +69 -0
- package/skills/magpie/scripts/__tests__/critic-cmd.test.ts +46 -0
- package/skills/magpie/scripts/__tests__/critic.test.ts +11 -1
- package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +20 -1
- package/skills/magpie/scripts/__tests__/git-diff.test.ts +23 -4
- package/skills/magpie/scripts/__tests__/handoff-cmd.test.ts +68 -0
- package/skills/magpie/scripts/__tests__/history.test.ts +232 -0
- package/skills/magpie/scripts/__tests__/impact-cmd.test.ts +477 -0
- package/skills/magpie/scripts/__tests__/impact-render.test.ts +365 -0
- package/skills/magpie/scripts/__tests__/impact.test.ts +915 -0
- package/skills/magpie/scripts/__tests__/incremental.test.ts +59 -8
- package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +1 -1
- package/skills/magpie/scripts/__tests__/post-cmd.test.ts +58 -0
- package/skills/magpie/scripts/__tests__/pr-activity.test.ts +281 -0
- package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +4 -4
- package/skills/magpie/scripts/__tests__/previous-findings.test.ts +267 -0
- package/skills/magpie/scripts/__tests__/proof-cmd.test.ts +260 -0
- package/skills/magpie/scripts/__tests__/proof.test.ts +226 -0
- package/skills/magpie/scripts/__tests__/refresh.test.ts +71 -0
- package/skills/magpie/scripts/__tests__/render-annotation.test.ts +34 -0
- package/skills/magpie/scripts/__tests__/render-findings.test.ts +75 -0
- package/skills/magpie/scripts/__tests__/render-progress.test.ts +9 -0
- package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +376 -1
- package/skills/magpie/scripts/__tests__/shard-coverage.test.ts +79 -0
- package/skills/magpie/scripts/__tests__/skill-lint.test.ts +48 -2
- package/skills/magpie/scripts/__tests__/status-cmd.test.ts +20 -8
- package/skills/magpie/scripts/__tests__/types.test.ts +46 -0
- package/skills/magpie/scripts/cleanup-cmd.ts +88 -0
- package/skills/magpie/scripts/critic-cmd.ts +2 -0
- package/skills/magpie/scripts/critic.ts +4 -0
- package/skills/magpie/scripts/dedupe-cmd.ts +4 -0
- package/skills/magpie/scripts/evidence-filter.ts +10 -0
- package/skills/magpie/scripts/git-diff.ts +50 -19
- package/skills/magpie/scripts/handoff-cmd.ts +42 -3
- package/skills/magpie/scripts/history.ts +143 -0
- package/skills/magpie/scripts/impact-cmd.ts +202 -0
- package/skills/magpie/scripts/impact-render.ts +158 -0
- package/skills/magpie/scripts/impact.ts +622 -0
- package/skills/magpie/scripts/incremental.ts +44 -5
- package/skills/magpie/scripts/labels-cmd.ts +7 -3
- package/skills/magpie/scripts/post-cmd.ts +46 -0
- package/skills/magpie/scripts/pr-activity.ts +243 -0
- package/skills/magpie/scripts/preview-cmd.ts +3 -0
- package/skills/magpie/scripts/previous-findings.ts +181 -0
- package/skills/magpie/scripts/proof-cmd.ts +226 -0
- package/skills/magpie/scripts/proof.ts +150 -0
- package/skills/magpie/scripts/refresh.ts +11 -0
- package/skills/magpie/scripts/render-annotation.ts +17 -0
- package/skills/magpie/scripts/render-cmd.ts +12 -0
- package/skills/magpie/scripts/render-findings.ts +37 -1
- package/skills/magpie/scripts/render-progress.ts +3 -0
- package/skills/magpie/scripts/setup-cmd.ts +142 -11
- package/skills/magpie/scripts/shard-coverage.ts +53 -0
- package/skills/magpie/scripts/status-cmd.ts +2 -0
- package/skills/magpie/scripts/types.ts +39 -0
- package/skills/magpie/skill.json +1 -1
- package/skills/magpie/templates/styles.css +30 -0
- package/skills/sluice/scripts/status.sh +32 -13
- package/skills/sluice/skill.json +1 -1
|
@@ -31,6 +31,12 @@ async function sh(cwd: string, ...cmd: string[]): Promise<void> {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
async function revParse(cwd: string, ref: string): Promise<string> {
|
|
35
|
+
const proc = Bun.spawn(['git', 'rev-parse', ref], { cwd, stdout: 'pipe', stderr: 'pipe' })
|
|
36
|
+
if ((await proc.exited) !== 0) throw new Error(await new Response(proc.stderr).text())
|
|
37
|
+
return (await new Response(proc.stdout).text()).trim()
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
beforeEach(async () => {
|
|
35
41
|
repo = await mkdtemp(join(tmpdir(), 'magpie-setup-repo-'))
|
|
36
42
|
runDir = await mkdtemp(join(tmpdir(), 'magpie-setup-run-'))
|
|
@@ -140,7 +146,11 @@ test('runSetup writes incremental.json when a prior run for the same PR exists',
|
|
|
140
146
|
await sh(magpieHome, 'mkdir', '-p', 'pr-1234-100')
|
|
141
147
|
await writeFile(
|
|
142
148
|
join(magpieHome, 'pr-1234-100', 'pr.json'),
|
|
143
|
-
JSON.stringify({
|
|
149
|
+
JSON.stringify({
|
|
150
|
+
headRefOid: 'oldsha',
|
|
151
|
+
number: 1234,
|
|
152
|
+
url: 'https://github.com/octocat/Hello-World/pull/1234',
|
|
153
|
+
}),
|
|
144
154
|
)
|
|
145
155
|
const originalHome = process.env.MAGPIE_HOME
|
|
146
156
|
process.env.MAGPIE_HOME = magpieHome
|
|
@@ -164,6 +174,166 @@ test('runSetup writes incremental.json when a prior run for the same PR exists',
|
|
|
164
174
|
}
|
|
165
175
|
})
|
|
166
176
|
|
|
177
|
+
test('runSetup carries the previous run posted findings forward with their anchor status', async () => {
|
|
178
|
+
const magpieHome = await mkdtemp(join(tmpdir(), 'magpie-home-prev-'))
|
|
179
|
+
const prev = join(magpieHome, 'pr-1234-100')
|
|
180
|
+
await mkdir(prev, { recursive: true })
|
|
181
|
+
await writeFile(
|
|
182
|
+
join(prev, 'pr.json'),
|
|
183
|
+
JSON.stringify({
|
|
184
|
+
headRefOid: 'oldsha',
|
|
185
|
+
number: 1234,
|
|
186
|
+
url: 'https://github.com/octocat/Hello-World/pull/1234',
|
|
187
|
+
}),
|
|
188
|
+
)
|
|
189
|
+
await writeFile(
|
|
190
|
+
join(prev, 'findings.final.json'),
|
|
191
|
+
JSON.stringify([
|
|
192
|
+
{
|
|
193
|
+
id: 'bugs-1',
|
|
194
|
+
file: 'a.txt',
|
|
195
|
+
line: 1,
|
|
196
|
+
evidence: 'one',
|
|
197
|
+
severity: 'high',
|
|
198
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'should-fix' },
|
|
199
|
+
title: 'a.txt says one',
|
|
200
|
+
description: 'Observation: x',
|
|
201
|
+
domain: 'bugs',
|
|
202
|
+
},
|
|
203
|
+
]),
|
|
204
|
+
)
|
|
205
|
+
await writeFile(join(prev, 'post-status.json'), JSON.stringify({ 'bugs-1': 'posted' }))
|
|
206
|
+
const originalHome = process.env.MAGPIE_HOME
|
|
207
|
+
process.env.MAGPIE_HOME = magpieHome
|
|
208
|
+
try {
|
|
209
|
+
const exit = await runSetup({
|
|
210
|
+
mode: 'pr',
|
|
211
|
+
runDir,
|
|
212
|
+
prNumber: 1234,
|
|
213
|
+
repoPath: repo,
|
|
214
|
+
deps: { bun: 'bun', gh: FAKE_GH, codex: 'echo', git: 'git' },
|
|
215
|
+
})
|
|
216
|
+
expect(exit).toBe(0)
|
|
217
|
+
const previous = JSON.parse(await readFile(join(runDir, 'previous-findings.json'), 'utf8'))
|
|
218
|
+
expect(previous).toEqual({
|
|
219
|
+
previousRunId: 'pr-1234-100',
|
|
220
|
+
findings: [
|
|
221
|
+
{
|
|
222
|
+
id: 'bugs-1',
|
|
223
|
+
fromRunId: 'pr-1234-100',
|
|
224
|
+
file: 'a.txt',
|
|
225
|
+
line: 1,
|
|
226
|
+
title: 'a.txt says one',
|
|
227
|
+
domain: 'bugs',
|
|
228
|
+
label: 'posted',
|
|
229
|
+
anchor: 'unchanged',
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
})
|
|
233
|
+
const incremental = JSON.parse(await readFile(join(runDir, 'incremental.json'), 'utf8'))
|
|
234
|
+
// oldsha is not a commit in the clone, so there is nothing to diff from.
|
|
235
|
+
expect(incremental.changedSincePrevious).toBeNull()
|
|
236
|
+
} finally {
|
|
237
|
+
if (originalHome === undefined) delete process.env.MAGPIE_HOME
|
|
238
|
+
else process.env.MAGPIE_HOME = originalHome
|
|
239
|
+
await rm(magpieHome, { recursive: true, force: true })
|
|
240
|
+
}
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('runSetup writes the PR checks, comments and review threads to pr-activity.json', async () => {
|
|
244
|
+
const exit = await runSetup({
|
|
245
|
+
mode: 'pr',
|
|
246
|
+
runDir,
|
|
247
|
+
prNumber: 1234,
|
|
248
|
+
repoPath: repo,
|
|
249
|
+
deps: { bun: 'bun', gh: FAKE_GH, codex: 'echo', git: 'git' },
|
|
250
|
+
})
|
|
251
|
+
expect(exit).toBe(0)
|
|
252
|
+
const activity = JSON.parse(await readFile(join(runDir, 'pr-activity.json'), 'utf8'))
|
|
253
|
+
expect(activity.checks).toEqual([
|
|
254
|
+
{ name: 'build', conclusion: 'FAILURE', url: 'https://ci.example/1' },
|
|
255
|
+
])
|
|
256
|
+
expect(activity.comments).toEqual([
|
|
257
|
+
{ author: 'alice', body: 'Does this retry past three attempts?', magpie: false },
|
|
258
|
+
])
|
|
259
|
+
expect(activity.threads.map((t: { file: string }) => t.file)).toEqual(['src/a.ts'])
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('runSetup fails and cleans up when the PR activity cannot be fetched', async () => {
|
|
263
|
+
const gh = join(runDir, '..', `gh-noactivity-${Date.now()}.sh`)
|
|
264
|
+
await writeFile(
|
|
265
|
+
gh,
|
|
266
|
+
[
|
|
267
|
+
'#!/usr/bin/env bash',
|
|
268
|
+
'if [[ "$1 $2" == "api graphql" ]]; then echo "HTTP 502" >&2; exit 1; fi',
|
|
269
|
+
`exec "${FAKE_GH}" "$@"`,
|
|
270
|
+
].join('\n'),
|
|
271
|
+
)
|
|
272
|
+
await sh(runDir, 'chmod', '+x', gh)
|
|
273
|
+
try {
|
|
274
|
+
const exit = await runSetup({
|
|
275
|
+
mode: 'pr',
|
|
276
|
+
runDir,
|
|
277
|
+
prNumber: 1234,
|
|
278
|
+
repoPath: repo,
|
|
279
|
+
deps: { bun: 'bun', gh, codex: 'echo', git: 'git' },
|
|
280
|
+
})
|
|
281
|
+
expect(exit).not.toBe(0)
|
|
282
|
+
const contents = await readdir(runDir)
|
|
283
|
+
expect(contents).not.toContain('pr.json')
|
|
284
|
+
expect(contents).not.toContain('pr-activity.json')
|
|
285
|
+
const log = await readFile(join(runDir, 'log.jsonl'), 'utf8')
|
|
286
|
+
expect(log).toContain('HTTP 502')
|
|
287
|
+
} finally {
|
|
288
|
+
await rm(gh, { force: true })
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
test('runSetup fails and cleans up when diffing against the previous head fails', async () => {
|
|
293
|
+
const magpieHome = await mkdtemp(join(tmpdir(), 'magpie-home-gitfail-'))
|
|
294
|
+
const prev = join(magpieHome, 'pr-1234-100')
|
|
295
|
+
await mkdir(prev, { recursive: true })
|
|
296
|
+
await writeFile(
|
|
297
|
+
join(prev, 'pr.json'),
|
|
298
|
+
JSON.stringify({
|
|
299
|
+
headRefOid: 'oldsha',
|
|
300
|
+
url: 'https://github.com/octocat/Hello-World/pull/1234',
|
|
301
|
+
}),
|
|
302
|
+
)
|
|
303
|
+
// Both commits probe as present, then the diff itself fails, as a corrupt object would.
|
|
304
|
+
const git = join(magpieHome, 'git.sh')
|
|
305
|
+
await writeFile(
|
|
306
|
+
git,
|
|
307
|
+
[
|
|
308
|
+
'#!/usr/bin/env bash',
|
|
309
|
+
'if [[ "$1" == "cat-file" ]]; then exit 0; fi',
|
|
310
|
+
'if [[ "$1 $2" == "diff --name-only" ]]; then echo "fatal: bad object" >&2; exit 128; fi',
|
|
311
|
+
'exec git "$@"',
|
|
312
|
+
].join('\n'),
|
|
313
|
+
)
|
|
314
|
+
await sh(magpieHome, 'chmod', '+x', git)
|
|
315
|
+
const originalHome = process.env.MAGPIE_HOME
|
|
316
|
+
process.env.MAGPIE_HOME = magpieHome
|
|
317
|
+
try {
|
|
318
|
+
const exit = await runSetup({
|
|
319
|
+
mode: 'pr',
|
|
320
|
+
runDir,
|
|
321
|
+
prNumber: 1234,
|
|
322
|
+
repoPath: repo,
|
|
323
|
+
deps: { bun: 'bun', gh: FAKE_GH, codex: 'echo', git },
|
|
324
|
+
})
|
|
325
|
+
expect(exit).not.toBe(0)
|
|
326
|
+
const contents = await readdir(runDir)
|
|
327
|
+
expect(contents).not.toContain('pr.json')
|
|
328
|
+
const log = await readFile(join(runDir, 'log.jsonl'), 'utf8')
|
|
329
|
+
expect(log).toContain('fatal: bad object')
|
|
330
|
+
} finally {
|
|
331
|
+
if (originalHome === undefined) delete process.env.MAGPIE_HOME
|
|
332
|
+
else process.env.MAGPIE_HOME = originalHome
|
|
333
|
+
await rm(magpieHome, { recursive: true, force: true })
|
|
334
|
+
}
|
|
335
|
+
})
|
|
336
|
+
|
|
167
337
|
test('runSetup omits incremental.json when no prior run exists', async () => {
|
|
168
338
|
const magpieHome = await mkdtemp(join(tmpdir(), 'magpie-home-incr-empty-'))
|
|
169
339
|
const originalHome = process.env.MAGPIE_HOME
|
|
@@ -584,3 +754,208 @@ test('local setup with --repo outside any git repository fails with exit 2 and c
|
|
|
584
754
|
await rm(notRepo, { recursive: true, force: true })
|
|
585
755
|
}
|
|
586
756
|
})
|
|
757
|
+
|
|
758
|
+
test('the worktree is the PR head commit even when the local branch has moved past it', async () => {
|
|
759
|
+
const prHead = await revParse(repo, 'feature-x')
|
|
760
|
+
await sh(repo, 'git', 'tag', 'fake-pr-head', prHead)
|
|
761
|
+
await sh(repo, 'git', 'checkout', '-q', 'feature-x')
|
|
762
|
+
await writeFile(join(repo, 'a.txt'), 'unpushed\n')
|
|
763
|
+
await sh(repo, 'git', 'commit', '-q', '-am', 'local only')
|
|
764
|
+
await sh(repo, 'git', 'checkout', '-q', 'main')
|
|
765
|
+
|
|
766
|
+
const exit = await runSetup({
|
|
767
|
+
mode: 'pr',
|
|
768
|
+
runDir,
|
|
769
|
+
prNumber: 1234,
|
|
770
|
+
repoPath: repo,
|
|
771
|
+
deps: { bun: 'bun', gh: FAKE_GH, codex: 'echo', git: 'git' },
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
expect(exit).toBe(0)
|
|
775
|
+
expect(await revParse(join(runDir, 'worktree'), 'HEAD')).toBe(prHead)
|
|
776
|
+
expect(await readFile(join(runDir, 'worktree', 'a.txt'), 'utf8')).toBe('one\n')
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
test('setup works when no local branch carries the PR head name, as on a fork PR', async () => {
|
|
780
|
+
const prHead = await revParse(repo, 'feature-x')
|
|
781
|
+
await sh(repo, 'git', 'tag', 'fake-pr-head', prHead)
|
|
782
|
+
await sh(repo, 'git', 'branch', '-q', '-D', 'feature-x')
|
|
783
|
+
|
|
784
|
+
const exit = await runSetup({
|
|
785
|
+
mode: 'pr',
|
|
786
|
+
runDir,
|
|
787
|
+
prNumber: 1234,
|
|
788
|
+
repoPath: repo,
|
|
789
|
+
deps: { bun: 'bun', gh: FAKE_GH, codex: 'echo', git: 'git' },
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
expect(exit).toBe(0)
|
|
793
|
+
expect(await revParse(join(runDir, 'worktree'), 'HEAD')).toBe(prHead)
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
test('setup fails and cleans up when the PR head commit is not in the clone', async () => {
|
|
797
|
+
await sh(repo, 'git', 'branch', '-q', '-D', 'feature-x')
|
|
798
|
+
|
|
799
|
+
const exit = await runSetup({
|
|
800
|
+
mode: 'pr',
|
|
801
|
+
runDir,
|
|
802
|
+
prNumber: 1234,
|
|
803
|
+
repoPath: repo,
|
|
804
|
+
deps: { bun: 'bun', gh: FAKE_GH, codex: 'echo', git: 'git' },
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
expect(exit).toBe(5)
|
|
808
|
+
const log = await readFile(join(runDir, 'log.jsonl'), 'utf8')
|
|
809
|
+
expect(log).toContain('cannot resolve PR head deadbeefdeadbeefdeadbeefdeadbeefdeadbeef locally')
|
|
810
|
+
const contents = await readdir(runDir)
|
|
811
|
+
expect(contents.filter((c) => c !== 'log.jsonl')).toHaveLength(0)
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
test('a re-review after the author force-pushed checks out the new PR head', async () => {
|
|
815
|
+
const origin = await mkdtemp(join(tmpdir(), 'magpie-setup-origin-'))
|
|
816
|
+
try {
|
|
817
|
+
await sh(origin, 'git', 'init', '-q', '--bare')
|
|
818
|
+
await sh(repo, 'git', 'remote', 'add', 'origin', origin)
|
|
819
|
+
await sh(repo, 'git', 'checkout', '-q', 'feature-x')
|
|
820
|
+
await writeFile(join(repo, 'a.txt'), 'first\n')
|
|
821
|
+
await sh(repo, 'git', 'commit', '-q', '-am', 'first')
|
|
822
|
+
const first = await revParse(repo, 'HEAD')
|
|
823
|
+
await sh(repo, 'git', 'checkout', '-q', 'main')
|
|
824
|
+
await sh(repo, 'git', 'push', '-q', 'origin', `${first}:refs/pull/1234/head`)
|
|
825
|
+
await sh(repo, 'git', 'tag', 'fake-pr-head', first)
|
|
826
|
+
expect(
|
|
827
|
+
await runSetup({
|
|
828
|
+
mode: 'pr',
|
|
829
|
+
runDir,
|
|
830
|
+
prNumber: 1234,
|
|
831
|
+
repoPath: repo,
|
|
832
|
+
deps: { bun: 'bun', gh: FAKE_GH, codex: 'echo', git: 'git' },
|
|
833
|
+
}),
|
|
834
|
+
).toBe(0)
|
|
835
|
+
|
|
836
|
+
// The amended head does not descend from the first, as after `git commit --amend`.
|
|
837
|
+
await sh(repo, 'git', 'checkout', '-q', '--detach', 'main')
|
|
838
|
+
await writeFile(join(repo, 'a.txt'), 'amended\n')
|
|
839
|
+
await sh(repo, 'git', 'commit', '-q', '-am', 'amended')
|
|
840
|
+
const amended = await revParse(repo, 'HEAD')
|
|
841
|
+
await sh(repo, 'git', 'checkout', '-q', 'main')
|
|
842
|
+
await sh(repo, 'git', 'push', '-q', '-f', 'origin', `${amended}:refs/pull/1234/head`)
|
|
843
|
+
await sh(repo, 'git', 'tag', '-f', 'fake-pr-head', amended)
|
|
844
|
+
const secondRun = await mkdtemp(join(tmpdir(), 'magpie-setup-run-'))
|
|
845
|
+
try {
|
|
846
|
+
const exit = await runSetup({
|
|
847
|
+
mode: 'pr',
|
|
848
|
+
runDir: secondRun,
|
|
849
|
+
prNumber: 1234,
|
|
850
|
+
repoPath: repo,
|
|
851
|
+
deps: { bun: 'bun', gh: FAKE_GH, codex: 'echo', git: 'git' },
|
|
852
|
+
})
|
|
853
|
+
expect(exit).toBe(0)
|
|
854
|
+
expect(await revParse(join(secondRun, 'worktree'), 'HEAD')).toBe(amended)
|
|
855
|
+
} finally {
|
|
856
|
+
await rm(secondRun, { recursive: true, force: true })
|
|
857
|
+
}
|
|
858
|
+
} finally {
|
|
859
|
+
await rm(origin, { recursive: true, force: true })
|
|
860
|
+
}
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
test('offline, a PR head commit already in the clone wins over a stale PR ref', async () => {
|
|
864
|
+
const stale = await revParse(repo, 'main')
|
|
865
|
+
await sh(repo, 'git', 'checkout', '-q', 'feature-x')
|
|
866
|
+
await writeFile(join(repo, 'a.txt'), 'new head\n')
|
|
867
|
+
await sh(repo, 'git', 'commit', '-q', '-am', 'new head')
|
|
868
|
+
const head = await revParse(repo, 'HEAD')
|
|
869
|
+
await sh(repo, 'git', 'checkout', '-q', 'main')
|
|
870
|
+
await sh(repo, 'git', 'update-ref', 'refs/magpie/pr-1234', stale)
|
|
871
|
+
|
|
872
|
+
const exit = await runSetup({
|
|
873
|
+
mode: 'pr',
|
|
874
|
+
runDir,
|
|
875
|
+
prNumber: 1234,
|
|
876
|
+
repoPath: repo,
|
|
877
|
+
deps: { bun: 'bun', gh: FAKE_GH, codex: 'echo', git: 'git' },
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
expect(exit).toBe(0)
|
|
881
|
+
expect(await revParse(join(runDir, 'worktree'), 'HEAD')).toBe(head)
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
test('setup fetches nothing when the PR head commit is already in the clone', async () => {
|
|
885
|
+
const origin = await mkdtemp(join(tmpdir(), 'magpie-setup-origin-'))
|
|
886
|
+
try {
|
|
887
|
+
await sh(origin, 'git', 'init', '-q', '--bare')
|
|
888
|
+
await sh(repo, 'git', 'remote', 'add', 'origin', origin)
|
|
889
|
+
await sh(repo, 'git', 'push', '-q', 'origin', 'feature-x:refs/pull/1234/head')
|
|
890
|
+
|
|
891
|
+
const exit = await runSetup({
|
|
892
|
+
mode: 'pr',
|
|
893
|
+
runDir,
|
|
894
|
+
prNumber: 1234,
|
|
895
|
+
repoPath: repo,
|
|
896
|
+
deps: { bun: 'bun', gh: FAKE_GH, codex: 'echo', git: 'git' },
|
|
897
|
+
})
|
|
898
|
+
|
|
899
|
+
expect(exit).toBe(0)
|
|
900
|
+
const ref = Bun.spawn(['git', 'rev-parse', '--verify', '-q', 'refs/magpie/pr-1234'], {
|
|
901
|
+
cwd: repo,
|
|
902
|
+
stdout: 'pipe',
|
|
903
|
+
})
|
|
904
|
+
expect(await ref.exited).toBe(1)
|
|
905
|
+
} finally {
|
|
906
|
+
await rm(origin, { recursive: true, force: true })
|
|
907
|
+
}
|
|
908
|
+
})
|
|
909
|
+
|
|
910
|
+
test('a re-review lists the files changed since the last review when the new head is not yet local', async () => {
|
|
911
|
+
const origin = await mkdtemp(join(tmpdir(), 'magpie-setup-origin-'))
|
|
912
|
+
const other = await mkdtemp(join(tmpdir(), 'magpie-setup-other-'))
|
|
913
|
+
const magpieHome = await mkdtemp(join(tmpdir(), 'magpie-home-fetch-'))
|
|
914
|
+
const originalHome = process.env.MAGPIE_HOME
|
|
915
|
+
try {
|
|
916
|
+
await sh(origin, 'git', 'init', '-q', '--bare')
|
|
917
|
+
await sh(repo, 'git', 'remote', 'add', 'origin', origin)
|
|
918
|
+
await sh(repo, 'git', 'push', '-q', 'origin', 'feature-x')
|
|
919
|
+
const previousHead = await revParse(repo, 'feature-x')
|
|
920
|
+
// The author pushes from another clone, so this clone has never seen the new head.
|
|
921
|
+
await sh(other, 'git', 'clone', '-q', '-b', 'feature-x', origin, '.')
|
|
922
|
+
await sh(other, 'git', 'config', 'user.email', 't@t.t')
|
|
923
|
+
await sh(other, 'git', 'config', 'user.name', 't')
|
|
924
|
+
await sh(other, 'git', 'config', 'commit.gpgsign', 'false')
|
|
925
|
+
await writeFile(join(other, 'b.txt'), 'new\n')
|
|
926
|
+
await sh(other, 'git', 'add', '.')
|
|
927
|
+
await sh(other, 'git', 'commit', '-q', '-m', 'new')
|
|
928
|
+
const newHead = await revParse(other, 'HEAD')
|
|
929
|
+
await sh(other, 'git', 'push', '-q', 'origin', 'HEAD:refs/pull/1234/head')
|
|
930
|
+
await writeFile(join(repo, '.git', 'fake-pr-head'), newHead)
|
|
931
|
+
|
|
932
|
+
await mkdir(join(magpieHome, 'pr-1234-100'))
|
|
933
|
+
await writeFile(
|
|
934
|
+
join(magpieHome, 'pr-1234-100', 'pr.json'),
|
|
935
|
+
JSON.stringify({
|
|
936
|
+
headRefOid: previousHead,
|
|
937
|
+
number: 1234,
|
|
938
|
+
url: 'https://github.com/octocat/Hello-World/pull/1234',
|
|
939
|
+
}),
|
|
940
|
+
)
|
|
941
|
+
process.env.MAGPIE_HOME = magpieHome
|
|
942
|
+
|
|
943
|
+
const exit = await runSetup({
|
|
944
|
+
mode: 'pr',
|
|
945
|
+
runDir,
|
|
946
|
+
prNumber: 1234,
|
|
947
|
+
repoPath: repo,
|
|
948
|
+
deps: { bun: 'bun', gh: FAKE_GH, codex: 'echo', git: 'git' },
|
|
949
|
+
})
|
|
950
|
+
|
|
951
|
+
expect(exit).toBe(0)
|
|
952
|
+
const incremental = JSON.parse(await readFile(join(runDir, 'incremental.json'), 'utf8'))
|
|
953
|
+
expect(incremental.changedSincePrevious).toEqual(['b.txt'])
|
|
954
|
+
} finally {
|
|
955
|
+
if (originalHome === undefined) delete process.env.MAGPIE_HOME
|
|
956
|
+
else process.env.MAGPIE_HOME = originalHome
|
|
957
|
+
await rm(origin, { recursive: true, force: true })
|
|
958
|
+
await rm(other, { recursive: true, force: true })
|
|
959
|
+
await rm(magpieHome, { recursive: true, force: true })
|
|
960
|
+
}
|
|
961
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { describeUnreviewed, readShardCoverage } from '../shard-coverage.ts'
|
|
6
|
+
|
|
7
|
+
let runDir: string
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
runDir = await mkdtemp(join(tmpdir(), 'magpie-coverage-'))
|
|
11
|
+
await mkdir(join(runDir, 'shards'))
|
|
12
|
+
await writeFile(
|
|
13
|
+
join(runDir, 'shards', 'manifest.json'),
|
|
14
|
+
JSON.stringify({
|
|
15
|
+
budget: 6000,
|
|
16
|
+
maxFiles: 80,
|
|
17
|
+
totalFiles: 4,
|
|
18
|
+
totalLines: 40,
|
|
19
|
+
shards: [
|
|
20
|
+
{ id: 1, path: 'shards/shard-1.patch', files: ['src/a.ts'], lines: 10 },
|
|
21
|
+
{ id: 2, path: 'shards/shard-2.patch', files: ['src/b.ts', 'src/c.ts'], lines: 20 },
|
|
22
|
+
{ id: 3, path: 'shards/shard-3.patch', files: ['docs/d.md'], lines: 10 },
|
|
23
|
+
],
|
|
24
|
+
}),
|
|
25
|
+
)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await rm(runDir, { recursive: true, force: true })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
async function writeLog(entries: Array<Record<string, unknown>>): Promise<void> {
|
|
33
|
+
await writeFile(join(runDir, 'log.jsonl'), entries.map((e) => `${JSON.stringify(e)}\n`).join(''))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('the skipped shards and their files come from the shard-coverage entry and the manifest', async () => {
|
|
37
|
+
await writeLog([
|
|
38
|
+
{ stage: 'setup', status: 'done' },
|
|
39
|
+
{ stage: 'shard-coverage', status: 'partial', reviewed: [1], skipped: [2, 3] },
|
|
40
|
+
])
|
|
41
|
+
expect(await readShardCoverage(runDir)).toEqual({
|
|
42
|
+
skipped: [
|
|
43
|
+
{ id: 2, files: ['src/b.ts', 'src/c.ts'] },
|
|
44
|
+
{ id: 3, files: ['docs/d.md'] },
|
|
45
|
+
],
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('the last shard-coverage entry wins', async () => {
|
|
50
|
+
await writeLog([
|
|
51
|
+
{ stage: 'shard-coverage', status: 'partial', reviewed: [1], skipped: [2, 3] },
|
|
52
|
+
{ stage: 'shard-coverage', status: 'partial', reviewed: [1, 2], skipped: [3] },
|
|
53
|
+
])
|
|
54
|
+
expect(await readShardCoverage(runDir)).toEqual({ skipped: [{ id: 3, files: ['docs/d.md'] }] })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('a run that reviewed every shard has no coverage gap', async () => {
|
|
58
|
+
await writeLog([{ stage: 'specialists', status: 'done' }])
|
|
59
|
+
expect(await readShardCoverage(runDir)).toBeNull()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('a skipped shard the manifest does not have is rejected, naming it', async () => {
|
|
63
|
+
await writeLog([{ stage: 'shard-coverage', status: 'partial', reviewed: [1], skipped: [9] }])
|
|
64
|
+
await expect(readShardCoverage(runDir)).rejects.toThrow('shard 9')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('the gap reads as the shard ids and the file count', () => {
|
|
68
|
+
expect(
|
|
69
|
+
describeUnreviewed({
|
|
70
|
+
skipped: [
|
|
71
|
+
{ id: 2, files: ['src/b.ts', 'src/c.ts'] },
|
|
72
|
+
{ id: 3, files: ['docs/d.md'] },
|
|
73
|
+
],
|
|
74
|
+
}),
|
|
75
|
+
).toBe('Not reviewed: shards 2, 3 (3 files)')
|
|
76
|
+
expect(describeUnreviewed({ skipped: [{ id: 3, files: ['docs/d.md'] }] })).toBe(
|
|
77
|
+
'Not reviewed: shard 3 (1 file)',
|
|
78
|
+
)
|
|
79
|
+
})
|
|
@@ -24,7 +24,7 @@ test('references/ ships in the install bundle', async () => {
|
|
|
24
24
|
}
|
|
25
25
|
const covers = (p: string) =>
|
|
26
26
|
manifest.bundle.include.some((inc) => inc === p || p.startsWith(`${inc}/`))
|
|
27
|
-
for (const name of ['specialists.md', 'critic.md', 'peer-review.md', 'scout.md']) {
|
|
27
|
+
for (const name of ['specialists.md', 'critic.md', 'peer-review.md', 'scout.md', 'proof.md']) {
|
|
28
28
|
expect(covers(`references/${name}`)).toBe(true)
|
|
29
29
|
}
|
|
30
30
|
})
|
|
@@ -88,6 +88,7 @@ test('references/critic.md carries every placeholder critic-prompt fills', async
|
|
|
88
88
|
'<<CANDIDATES>>',
|
|
89
89
|
'<<MERGE_CANDIDATES>>',
|
|
90
90
|
'<<REVIEW_RULES>>',
|
|
91
|
+
'<<ALREADY_RAISED>>',
|
|
91
92
|
'<<WORKTREE>>',
|
|
92
93
|
'<<DIFF_PATH>>',
|
|
93
94
|
'<<OUTPUT_PATH>>',
|
|
@@ -96,6 +97,43 @@ test('references/critic.md carries every placeholder critic-prompt fills', async
|
|
|
96
97
|
}
|
|
97
98
|
})
|
|
98
99
|
|
|
100
|
+
test('references/specialists.md has the orchestrator fill in the focus id', async () => {
|
|
101
|
+
// A bugs specialist left to fill in <focus> itself wrote correctness.shard-4.json,
|
|
102
|
+
// which nothing reads.
|
|
103
|
+
const text = await readFile(ref('specialists.md'), 'utf8')
|
|
104
|
+
expect(text).toMatch(/Replace every `<RUN_DIR>`, `<WORKTREE>`, `<TARGET>` and `<focus>`/)
|
|
105
|
+
expect(text).not.toContain('Leave `<focus>` as written')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('references/proof.md carries every placeholder proof-prompt fills', async () => {
|
|
109
|
+
const text = await readFile(ref('proof.md'), 'utf8')
|
|
110
|
+
const block = text.match(/^(`{3,4})magpie-proof\n([\s\S]*?)\n\1[ \t]*$/m)?.[2] ?? ''
|
|
111
|
+
expect(block.length).toBeGreaterThan(0)
|
|
112
|
+
for (const ph of ['<<FINDING>>', '<<WORKTREE>>', '<<DIFF_PATH>>', '<<OUTPUT_PATH>>']) {
|
|
113
|
+
expect(block).toContain(ph)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test("references/proof.md keeps each prover's scratch output in a directory of its own", async () => {
|
|
118
|
+
// On PR 254 a prover picked up another prover's run output from the session
|
|
119
|
+
// scratchpad every subagent shares, and nearly reported it as its own.
|
|
120
|
+
const text = await readFile(ref('proof.md'), 'utf8')
|
|
121
|
+
const block = text.match(/^(`{3,4})magpie-proof\n([\s\S]*?)\n\1[ \t]*$/m)?.[2] ?? ''
|
|
122
|
+
expect(block).toContain('mktemp -d')
|
|
123
|
+
expect(block).toMatch(/never a scratchpad or\s+temp path another session shares/)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('references/proof.md asks for the result shape proof-apply accepts', async () => {
|
|
127
|
+
const text = await readFile(ref('proof.md'), 'utf8')
|
|
128
|
+
const block = text.match(/^(`{3,4})magpie-proof\n([\s\S]*?)\n\1[ \t]*$/m)?.[2] ?? ''
|
|
129
|
+
for (const outcome of ['"reproduced"', '"disproved"', '"not-proven"']) {
|
|
130
|
+
expect(block).toContain(outcome)
|
|
131
|
+
}
|
|
132
|
+
for (const field of ['"path"', '"command"', '"source"', '"output"']) {
|
|
133
|
+
expect(block).toContain(field)
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
99
137
|
test('references/critic.md asks for the verdict shape critic-apply accepts', async () => {
|
|
100
138
|
const text = await readFile(ref('critic.md'), 'utf8')
|
|
101
139
|
const block = text.match(/^(`{3,4})magpie-critic\n([\s\S]*?)\n\1[ \t]*$/m)?.[2] ?? ''
|
|
@@ -210,6 +248,7 @@ test('SKILL.md sends each stage to the reference file it needs', async () => {
|
|
|
210
248
|
expect(section('### 4. Specialists')).toContain('references/specialists.md')
|
|
211
249
|
expect(section('### 6. Critic')).toContain('references/critic.md')
|
|
212
250
|
expect(section('### 7. Peer review')).toContain('references/peer-review.md')
|
|
251
|
+
expect(section('### 7b. Proof')).toContain('references/proof.md')
|
|
213
252
|
})
|
|
214
253
|
|
|
215
254
|
test('a bare /magpie is routed to local mode and the hint says so', async () => {
|
|
@@ -222,7 +261,13 @@ test('a bare /magpie is routed to local mode and the hint says so', async () =>
|
|
|
222
261
|
|
|
223
262
|
test('SKILL.md no longer inlines the prompt bodies it moved out', async () => {
|
|
224
263
|
const text = await readFile(SKILL, 'utf8')
|
|
225
|
-
for (const tag of [
|
|
264
|
+
for (const tag of [
|
|
265
|
+
'magpie-specialist-',
|
|
266
|
+
'magpie-critic',
|
|
267
|
+
'magpie-peer-review',
|
|
268
|
+
'magpie-scout',
|
|
269
|
+
'magpie-proof',
|
|
270
|
+
]) {
|
|
226
271
|
expect(text).not.toContain(`\`\`\`${tag}`)
|
|
227
272
|
}
|
|
228
273
|
// The walkthrough is the always-read part; keep it small enough to be cheap.
|
|
@@ -273,6 +318,7 @@ test('rendered HTML opts into light/dark with a color-scheme meta tag', async ()
|
|
|
273
318
|
dedupe: 'pending',
|
|
274
319
|
critic: 'pending',
|
|
275
320
|
'peer-review': 'pending',
|
|
321
|
+
proof: 'pending',
|
|
276
322
|
report: 'pending',
|
|
277
323
|
post: 'pending',
|
|
278
324
|
},
|
|
@@ -67,12 +67,12 @@ const throughPeerReview: [string, string][] = [
|
|
|
67
67
|
['peer-review', 'done'],
|
|
68
68
|
]
|
|
69
69
|
|
|
70
|
-
test('a local run after peer-review resumes at
|
|
70
|
+
test('a local run after peer-review resumes at proof', async () => {
|
|
71
71
|
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'local', root: runDir }))
|
|
72
72
|
await writeFile(join(runDir, 'log.jsonl'), logLines(throughPeerReview))
|
|
73
73
|
const result = await runStatus(runDir)
|
|
74
74
|
expect(result.lastCompleted).toBe('peer-review')
|
|
75
|
-
expect(result.next).toBe('
|
|
75
|
+
expect(result.next).toBe('proof')
|
|
76
76
|
})
|
|
77
77
|
|
|
78
78
|
test('a local run with handoff done goes to cleanup', async () => {
|
|
@@ -83,17 +83,29 @@ test('a local run with handoff done goes to cleanup', async () => {
|
|
|
83
83
|
expect(result.next).toBe('cleanup')
|
|
84
84
|
})
|
|
85
85
|
|
|
86
|
-
test('a PR or archived run with no target.json resumes at report after
|
|
87
|
-
await writeFile(join(runDir, 'log.jsonl'), logLines(throughPeerReview))
|
|
86
|
+
test('a PR or archived run with no target.json resumes at report after proof', async () => {
|
|
87
|
+
await writeFile(join(runDir, 'log.jsonl'), logLines([...throughPeerReview, ['proof', 'done']]))
|
|
88
88
|
const result = await runStatus(runDir)
|
|
89
|
-
expect(result.lastCompleted).toBe('
|
|
89
|
+
expect(result.lastCompleted).toBe('proof')
|
|
90
90
|
expect(result.next).toBe('report')
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
-
test('a PR run with target.json resumes at report after
|
|
93
|
+
test('a PR run with target.json resumes at report after proof', async () => {
|
|
94
94
|
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'pr', root: runDir }))
|
|
95
|
-
await writeFile(join(runDir, 'log.jsonl'), logLines(throughPeerReview))
|
|
95
|
+
await writeFile(join(runDir, 'log.jsonl'), logLines([...throughPeerReview, ['proof', 'done']]))
|
|
96
96
|
const result = await runStatus(runDir)
|
|
97
|
-
expect(result.lastCompleted).toBe('
|
|
97
|
+
expect(result.lastCompleted).toBe('proof')
|
|
98
98
|
expect(result.next).toBe('report')
|
|
99
99
|
})
|
|
100
|
+
|
|
101
|
+
test('a PR run after peer-review resumes at proof', async () => {
|
|
102
|
+
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'pr', root: runDir }))
|
|
103
|
+
await writeFile(join(runDir, 'log.jsonl'), logLines(throughPeerReview))
|
|
104
|
+
expect((await runStatus(runDir)).next).toBe('proof')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('a local run after proof resumes at handoff', async () => {
|
|
108
|
+
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'local', root: runDir }))
|
|
109
|
+
await writeFile(join(runDir, 'log.jsonl'), logLines([...throughPeerReview, ['proof', 'done']]))
|
|
110
|
+
expect((await runStatus(runDir)).next).toBe('handoff')
|
|
111
|
+
})
|
|
@@ -418,3 +418,49 @@ test('parseBrief keeps review rules with a non-empty rule and source', () => {
|
|
|
418
418
|
test('parseBrief yields no review rules when the key is absent', () => {
|
|
419
419
|
expect(parseBrief({ purpose: 'Does a thing.' })?.reviewRules).toEqual([])
|
|
420
420
|
})
|
|
421
|
+
|
|
422
|
+
test('parseFinding keeps a reproduced proof with its repro', () => {
|
|
423
|
+
const proof = {
|
|
424
|
+
status: 'reproduced',
|
|
425
|
+
reason: 'fails as described',
|
|
426
|
+
repro: { path: 'a.test.ts', command: 'bun test a.test.ts', source: 'src', output: 'out' },
|
|
427
|
+
}
|
|
428
|
+
const f = parseFinding({
|
|
429
|
+
id: 'a',
|
|
430
|
+
file: 'a.ts',
|
|
431
|
+
line: 1,
|
|
432
|
+
title: 't',
|
|
433
|
+
description: 'd',
|
|
434
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'must-fix' },
|
|
435
|
+
proof,
|
|
436
|
+
})
|
|
437
|
+
expect(f.proof).toEqual(proof as never)
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
test('parseFinding rejects a proof that is neither reproduced nor not-proven', () => {
|
|
441
|
+
expect(() =>
|
|
442
|
+
parseFinding({
|
|
443
|
+
id: 'a',
|
|
444
|
+
file: 'a.ts',
|
|
445
|
+
line: 1,
|
|
446
|
+
title: 't',
|
|
447
|
+
description: 'd',
|
|
448
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'must-fix' },
|
|
449
|
+
proof: { status: 'disproved', reason: 'r' },
|
|
450
|
+
}),
|
|
451
|
+
).toThrow('Finding.proof.status must be reproduced or not-proven')
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
test('parseFinding rejects a proof with an empty reason', () => {
|
|
455
|
+
expect(() =>
|
|
456
|
+
parseFinding({
|
|
457
|
+
id: 'a',
|
|
458
|
+
file: 'a.ts',
|
|
459
|
+
line: 1,
|
|
460
|
+
title: 't',
|
|
461
|
+
description: 'd',
|
|
462
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'high', action: 'must-fix' },
|
|
463
|
+
proof: { status: 'not-proven', reason: '' },
|
|
464
|
+
}),
|
|
465
|
+
).toThrow('Finding.proof.reason must be a non-empty string')
|
|
466
|
+
})
|