@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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// again, capped by --max-cost-usd.
|
|
6
6
|
|
|
7
7
|
import { existsSync } from 'node:fs'
|
|
8
|
-
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
8
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'
|
|
9
9
|
import { tmpdir } from 'node:os'
|
|
10
10
|
import { join } from 'node:path'
|
|
11
11
|
import { detectMissingTests } from '../../scripts/tests-check.ts'
|
|
@@ -46,6 +46,7 @@ export type ReplayDiagnostics = {
|
|
|
46
46
|
criticDropped: unknown | null
|
|
47
47
|
evidenceDropped: unknown | null
|
|
48
48
|
thresholdDropped: unknown | null
|
|
49
|
+
impactBytes: number | null
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
async function readJsonIfPresent(path: string): Promise<unknown | null> {
|
|
@@ -92,6 +93,9 @@ export async function replayDiagnostics(runDir: string): Promise<ReplayDiagnosti
|
|
|
92
93
|
criticDropped: await readJsonIfPresent(join(runDir, 'critic-dropped.json')),
|
|
93
94
|
evidenceDropped: await readJsonIfPresent(join(runDir, 'evidence-dropped.json')),
|
|
94
95
|
thresholdDropped: await readJsonIfPresent(join(runDir, 'threshold-dropped.json')),
|
|
96
|
+
impactBytes: existsSync(join(runDir, 'impact.md'))
|
|
97
|
+
? (await stat(join(runDir, 'impact.md'))).size
|
|
98
|
+
: null,
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
|
|
@@ -149,7 +153,7 @@ export function replaySummary(input: {
|
|
|
149
153
|
)
|
|
150
154
|
}
|
|
151
155
|
|
|
152
|
-
async function exec(
|
|
156
|
+
export async function exec(
|
|
153
157
|
cmd: string[],
|
|
154
158
|
options: { cwd?: string; stdinText?: string; claudeLabel?: string } = {},
|
|
155
159
|
): Promise<string> {
|
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# The activity fetch asks pr view for different fields than setup's first call.
|
|
3
|
+
case "$*" in
|
|
4
|
+
*statusCheckRollup*)
|
|
5
|
+
cat <<'JSON'
|
|
6
|
+
{
|
|
7
|
+
"statusCheckRollup": [
|
|
8
|
+
{ "__typename": "CheckRun", "name": "build", "status": "COMPLETED", "conclusion": "FAILURE", "detailsUrl": "https://ci.example/1" }
|
|
9
|
+
],
|
|
10
|
+
"comments": [
|
|
11
|
+
{ "author": { "login": "alice" }, "body": "Does this retry past three attempts?" }
|
|
12
|
+
],
|
|
13
|
+
"reviews": []
|
|
14
|
+
}
|
|
15
|
+
JSON
|
|
16
|
+
exit 0
|
|
17
|
+
;;
|
|
18
|
+
esac
|
|
2
19
|
case "$1 $2" in
|
|
3
20
|
"pr view")
|
|
4
|
-
|
|
21
|
+
# setup runs gh in the repo, so the PR head is read from it: a fake-pr-head file
|
|
22
|
+
# in the git dir for a head the clone does not have yet, a fake-pr-head tag,
|
|
23
|
+
# else feature-x, else a SHA no clone has.
|
|
24
|
+
head=$(cat "$(git rev-parse --git-dir)/fake-pr-head" 2>/dev/null ||
|
|
25
|
+
git rev-parse --verify -q 'refs/tags/fake-pr-head^{commit}' ||
|
|
26
|
+
git rev-parse --verify -q 'refs/heads/feature-x' ||
|
|
27
|
+
echo deadbeefdeadbeefdeadbeefdeadbeefdeadbeef)
|
|
28
|
+
cat <<JSON
|
|
5
29
|
{
|
|
6
30
|
"number": 1234,
|
|
7
31
|
"title": "Fake PR",
|
|
8
32
|
"headRefName": "feature-x",
|
|
9
33
|
"baseRefName": "main",
|
|
10
|
-
"headRefOid": "
|
|
34
|
+
"headRefOid": "$head",
|
|
11
35
|
"baseRefOid": "cafebabecafebabecafebabecafebabecafebabe",
|
|
12
36
|
"author": { "login": "octocat" },
|
|
13
37
|
"body": "Fake body",
|
|
@@ -18,6 +42,17 @@ case "$1 $2" in
|
|
|
18
42
|
{ "path": "dist/x.js", "additions": 1, "deletions": 0, "changeType": "modified" }
|
|
19
43
|
]
|
|
20
44
|
}
|
|
45
|
+
JSON
|
|
46
|
+
;;
|
|
47
|
+
"api graphql")
|
|
48
|
+
cat <<'JSON'
|
|
49
|
+
{ "data": { "repository": { "pullRequest": { "reviewThreads": {
|
|
50
|
+
"pageInfo": { "hasNextPage": false, "endCursor": null },
|
|
51
|
+
"nodes": [
|
|
52
|
+
{ "path": "src/a.ts", "line": 1, "originalLine": 1, "isResolved": false, "isOutdated": false,
|
|
53
|
+
"comments": { "totalCount": 1, "nodes": [ { "author": { "login": "dave" }, "body": "Why 2?" } ] } }
|
|
54
|
+
]
|
|
55
|
+
} } } } }
|
|
21
56
|
JSON
|
|
22
57
|
;;
|
|
23
58
|
"pr diff")
|
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# The activity fetch asks pr view for different fields than setup's first call.
|
|
3
|
+
case "$*" in
|
|
4
|
+
# cleanup asks each saved PR ref's state: 1234 is open, 9 closed, 5 in a state
|
|
5
|
+
# gh never reports, 404 errors, and any other PR is merged.
|
|
6
|
+
*"--json state"*)
|
|
7
|
+
case "$3" in
|
|
8
|
+
1234) echo '{"state":"OPEN"}' ;;
|
|
9
|
+
9) echo '{"state":"CLOSED"}' ;;
|
|
10
|
+
5) echo '{"state":"WEIRD"}' ;;
|
|
11
|
+
404) echo 'GraphQL: Could not resolve to a PullRequest' >&2; exit 1 ;;
|
|
12
|
+
*) echo '{"state":"MERGED"}' ;;
|
|
13
|
+
esac
|
|
14
|
+
exit 0
|
|
15
|
+
;;
|
|
16
|
+
*statusCheckRollup*)
|
|
17
|
+
cat <<'JSON'
|
|
18
|
+
{
|
|
19
|
+
"statusCheckRollup": [
|
|
20
|
+
{ "__typename": "CheckRun", "name": "build", "status": "COMPLETED", "conclusion": "FAILURE", "detailsUrl": "https://ci.example/1" }
|
|
21
|
+
],
|
|
22
|
+
"comments": [
|
|
23
|
+
{ "author": { "login": "alice" }, "body": "Does this retry past three attempts?" }
|
|
24
|
+
],
|
|
25
|
+
"reviews": []
|
|
26
|
+
}
|
|
27
|
+
JSON
|
|
28
|
+
exit 0
|
|
29
|
+
;;
|
|
30
|
+
esac
|
|
2
31
|
case "$1 $2" in
|
|
3
32
|
"pr view")
|
|
4
|
-
|
|
33
|
+
# setup runs gh in the repo, so the PR head is read from it: a fake-pr-head file
|
|
34
|
+
# in the git dir for a head the clone does not have yet, a fake-pr-head tag,
|
|
35
|
+
# else feature-x, else a SHA no clone has.
|
|
36
|
+
head=$(cat "$(git rev-parse --git-dir)/fake-pr-head" 2>/dev/null ||
|
|
37
|
+
git rev-parse --verify -q 'refs/tags/fake-pr-head^{commit}' ||
|
|
38
|
+
git rev-parse --verify -q 'refs/heads/feature-x' ||
|
|
39
|
+
echo deadbeefdeadbeefdeadbeefdeadbeefdeadbeef)
|
|
40
|
+
cat <<JSON
|
|
5
41
|
{
|
|
6
42
|
"number": 1234,
|
|
7
43
|
"title": "Fake PR",
|
|
8
44
|
"headRefName": "feature-x",
|
|
9
45
|
"baseRefName": "main",
|
|
10
|
-
"headRefOid": "
|
|
46
|
+
"headRefOid": "$head",
|
|
11
47
|
"baseRefOid": "cafebabecafebabecafebabecafebabecafebabe",
|
|
12
48
|
"author": { "login": "octocat" },
|
|
13
49
|
"body": "Fake body",
|
|
@@ -35,6 +71,17 @@ case "$1 $2" in
|
|
|
35
71
|
}
|
|
36
72
|
]
|
|
37
73
|
}
|
|
74
|
+
JSON
|
|
75
|
+
;;
|
|
76
|
+
"api graphql")
|
|
77
|
+
cat <<'JSON'
|
|
78
|
+
{ "data": { "repository": { "pullRequest": { "reviewThreads": {
|
|
79
|
+
"pageInfo": { "hasNextPage": false, "endCursor": null },
|
|
80
|
+
"nodes": [
|
|
81
|
+
{ "path": "src/a.ts", "line": 1, "originalLine": 1, "isResolved": false, "isOutdated": false,
|
|
82
|
+
"comments": { "totalCount": 1, "nodes": [ { "author": { "login": "dave" }, "body": "Why 2?" } ] } }
|
|
83
|
+
]
|
|
84
|
+
} } } } }
|
|
38
85
|
JSON
|
|
39
86
|
;;
|
|
40
87
|
"pr diff")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Critic prompt
|
|
2
2
|
|
|
3
3
|
Stage 6 of the walkthrough does not read this file by hand. `magpie critic-prompt
|
|
4
|
-
"$RUN_DIR"` takes the fenced `magpie-critic` block below, fills its
|
|
4
|
+
"$RUN_DIR"` takes the fenced `magpie-critic` block below, fills its seven placeholders
|
|
5
5
|
from the run directory, and writes one prompt file per batch of candidates:
|
|
6
6
|
|
|
7
7
|
- `<<CANDIDATES>>`: the batch's candidates from `findings.deduped.json`, with `id`,
|
|
@@ -11,6 +11,9 @@ from the run directory, and writes one prompt file per batch of candidates:
|
|
|
11
11
|
in this batch (a group always rides in one batch).
|
|
12
12
|
- `<<REVIEW_RULES>>`: `brief.json`'s `reviewRules`, one `- <rule> (<source>)` line each,
|
|
13
13
|
or `(none)`.
|
|
14
|
+
- `<<ALREADY_RAISED>>`: from `pr-activity.json` and `previous-findings.json`, the PR's
|
|
15
|
+
unresolved review threads, its top-level comments, and the findings a reviewer
|
|
16
|
+
dismissed in an earlier magpie run on this PR, or `(none)`.
|
|
14
17
|
- `<<WORKTREE>>`, `<<DIFF_PATH>>`: the run's worktree and `diff.patch`.
|
|
15
18
|
- `<<OUTPUT_PATH>>`: where this batch's verdicts go, `critic.json` or `critic-<k>.json`.
|
|
16
19
|
|
|
@@ -18,7 +21,7 @@ Each prompt file is the entire task of one `general-purpose` subagent. `magpie
|
|
|
18
21
|
critic-apply "$RUN_DIR"` then reads every output file, rejects the set if any verdict
|
|
19
22
|
is malformed, and writes `findings.kept.json` and `critic-dropped.json`. The output
|
|
20
23
|
contract in the block is exactly what critic-apply accepts; change one only with the
|
|
21
|
-
other. All
|
|
24
|
+
other. All seven placeholders must stay in the block, or critic-prompt exits 1.
|
|
22
25
|
|
|
23
26
|
````magpie-critic
|
|
24
27
|
You are a senior code reviewer auditing candidate review findings that other agents
|
|
@@ -53,7 +56,7 @@ Treat every candidate as a claim to verify, not a conclusion to grade. For each
|
|
|
53
56
|
|
|
54
57
|
## When to drop
|
|
55
58
|
|
|
56
|
-
Drop a candidate only
|
|
59
|
+
Drop a candidate only for one of these reasons, and say which in `reason`:
|
|
57
60
|
- The code does not do what the description says, or an existing guard already
|
|
58
61
|
prevents it (name the guard's file:line).
|
|
59
62
|
- The path it describes cannot be reached: no caller passes the value, or the
|
|
@@ -68,6 +71,11 @@ Drop a candidate only when the code refutes it, and say how in `reason`:
|
|
|
68
71
|
assertion that can no longer fail, cleanup that leaks into other tests) is a
|
|
69
72
|
correctness finding, not maintainability: judge it like any other defect.
|
|
70
73
|
- It asks for something a repository review rule below forbids.
|
|
74
|
+
- It repeats an entry under "Already raised" below: an open review thread or PR
|
|
75
|
+
comment that makes the same point, or a finding the reviewer dismissed in an earlier
|
|
76
|
+
run. Name who raised it, or the dismissal reason, in `reason`. A candidate that adds
|
|
77
|
+
something material the earlier entry lacks (a second path, a worse consequence, the
|
|
78
|
+
fix the thread asked for) is not a repeat: judge it on its own.
|
|
71
79
|
|
|
72
80
|
None of these is a refutation, so none is a reason to drop:
|
|
73
81
|
- The same mistake already exists elsewhere in the codebase. A pre-existing instance
|
|
@@ -101,7 +109,7 @@ about. Downgrade rather than drop when:
|
|
|
101
109
|
request body, a file name) is not an unlikely precondition.
|
|
102
110
|
|
|
103
111
|
Downgrade only for a reason on this list, and name it in `reason`. Everything else you
|
|
104
|
-
keep is at full weight. Drop only
|
|
112
|
+
keep is at full weight. Drop only for a reason under "When to drop".
|
|
105
113
|
|
|
106
114
|
## Repository review rules
|
|
107
115
|
|
|
@@ -111,6 +119,14 @@ it; a candidate that asks for something one of these rules forbids should be dro
|
|
|
111
119
|
|
|
112
120
|
<<REVIEW_RULES>>
|
|
113
121
|
|
|
122
|
+
## Already raised
|
|
123
|
+
|
|
124
|
+
What was already said on this pull request before this review, and what the reviewer
|
|
125
|
+
dismissed in an earlier magpie run on it. A candidate that repeats one of these is
|
|
126
|
+
dropped, as above.
|
|
127
|
+
|
|
128
|
+
<<ALREADY_RAISED>>
|
|
129
|
+
|
|
114
130
|
## Merge candidates
|
|
115
131
|
|
|
116
132
|
Each group below lists candidate ids anchored close together in one file by more than
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Proof prompt
|
|
2
|
+
|
|
3
|
+
Stage 7b of the walkthrough does not read this file by hand. `magpie proof-prompt
|
|
4
|
+
"$RUN_DIR"` picks the findings worth proving from `findings.final.json` (action
|
|
5
|
+
must-fix or should-fix, at most 8, highest score first),
|
|
6
|
+
takes the fenced `magpie-proof` block below, fills its four placeholders, and writes
|
|
7
|
+
one prompt file per finding:
|
|
8
|
+
|
|
9
|
+
- `<<FINDING>>`: the finding's `id`, `file`, `line`, `risk`, `domain`, `title`,
|
|
10
|
+
`description` and `evidence`, as JSON.
|
|
11
|
+
- `<<WORKTREE>>`, `<<DIFF_PATH>>`: the run's worktree and `diff.patch`. In local mode
|
|
12
|
+
the worktree is the caller's own repo.
|
|
13
|
+
- `<<OUTPUT_PATH>>`: where this finding's result goes, `proof-<id>.json`.
|
|
14
|
+
|
|
15
|
+
Each prompt file is the entire task of one `general-purpose` subagent. `magpie
|
|
16
|
+
proof-apply "$RUN_DIR"` then reads every result, rejects the set if any is missing or
|
|
17
|
+
malformed, drops the disproved findings to `proof-dropped.json`, and marks the rest in
|
|
18
|
+
`findings.final.json`. The output contract in the block is exactly what proof-apply
|
|
19
|
+
accepts; change one only with the other. All four placeholders must stay in the
|
|
20
|
+
block, or proof-prompt exits 1.
|
|
21
|
+
|
|
22
|
+
````magpie-proof
|
|
23
|
+
You are proving or disproving one finding from a code review of a change, by writing a
|
|
24
|
+
test that runs it. A finding that a failing test reproduces is shown to the author with
|
|
25
|
+
the test; a finding the test shows to be wrong is removed. Getting this right matters
|
|
26
|
+
more than getting a result: when you cannot run a fair test, say so.
|
|
27
|
+
|
|
28
|
+
Time matters here: do not spend time that can be avoided, and the earlier a correct
|
|
29
|
+
result is obtained, the better.
|
|
30
|
+
|
|
31
|
+
Working directory: <<WORKTREE>>
|
|
32
|
+
Diff: <<DIFF_PATH>>
|
|
33
|
+
|
|
34
|
+
## The finding
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
<<FINDING>>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How to prove it
|
|
41
|
+
|
|
42
|
+
1. Read the code at `file` and `line`, and the diff, until you can say exactly which
|
|
43
|
+
input or sequence of calls shows the defect the description claims.
|
|
44
|
+
2. Find the repository's own test runner from its manifests (`package.json`,
|
|
45
|
+
`pyproject.toml`, `Cargo.toml`, `go.mod`, ...) and its `CLAUDE.md` or `AGENTS.md`.
|
|
46
|
+
Use it as the repository's existing tests do. Install nothing: dependencies were
|
|
47
|
+
installed before you started, and if the runner still cannot run, stop with
|
|
48
|
+
`not-proven`.
|
|
49
|
+
3. Write one test file whose name contains `magpie-proof-<id>` (the finding's id), or
|
|
50
|
+
`MagpieProof<id>` where a file name must be an identifier, where the runner
|
|
51
|
+
discovers it, next to the existing tests for that code. The test asserts the
|
|
52
|
+
correct behaviour, so it fails while the defect is present. Take the expected value
|
|
53
|
+
from the finding's description, the diff's intent or the existing tests, never from
|
|
54
|
+
what you assume the code should do. Call the real code on the path the finding
|
|
55
|
+
describes. Mock only what the existing tests mock; a mock that returns what the
|
|
56
|
+
real dependency never would proves nothing.
|
|
57
|
+
4. Run only that file, with colour off (`--no-color`, `CI=1`), and in a way that does
|
|
58
|
+
not compile or type-check the whole project: other provers' half-written files sit
|
|
59
|
+
beside yours. Read the output. Anything you save outside the test file, such as the
|
|
60
|
+
run's output, goes in a directory of your own from `mktemp -d`, never a scratchpad or
|
|
61
|
+
temp path another session shares: other provers write there too, and their output
|
|
62
|
+
is not your result.
|
|
63
|
+
5. Copy the test's full source and the last 40 lines of output (at most) into your
|
|
64
|
+
result. Then, whatever the outcome, `not-proven` included, delete the test file and
|
|
65
|
+
anything its run created (snapshots, fixtures). Change no other file in the working
|
|
66
|
+
directory: it may be the author's own checkout, and other provers are running in it
|
|
67
|
+
at the same time.
|
|
68
|
+
|
|
69
|
+
## Which outcome
|
|
70
|
+
|
|
71
|
+
- `"reproduced"`: the test ran and failed on the assertion, with the wrong behaviour
|
|
72
|
+
the finding describes.
|
|
73
|
+
- `"disproved"`: the test ran, exercised the path the finding describes, and passed,
|
|
74
|
+
so the code behaves correctly there. Say in `reason` what stops the defect (a guard,
|
|
75
|
+
a caller that never passes the value), with its file:line.
|
|
76
|
+
- `"not-proven"`: anything else. The defect needs what a test here cannot set up (a
|
|
77
|
+
live service, real concurrency, a browser, a security boundary outside the process),
|
|
78
|
+
the runner cannot run, or the test failed for a reason other than the assertion (an
|
|
79
|
+
import error, a missing fixture, a type error). A test that fails for the wrong
|
|
80
|
+
reason is not a reproduction and a test that never reached the path is not a
|
|
81
|
+
disproof: fix it, or report `not-proven` and say why.
|
|
82
|
+
|
|
83
|
+
## Output contract
|
|
84
|
+
|
|
85
|
+
Write one JSON object to <<OUTPUT_PATH>> with the Write tool:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
{
|
|
89
|
+
"id": string, // the finding's id, verbatim
|
|
90
|
+
"outcome": "reproduced" | "disproved" | "not-proven",
|
|
91
|
+
"reason": string, // one sentence, under 25 words
|
|
92
|
+
"repro": { // REQUIRED with "reproduced" and "disproved"; omit with "not-proven"
|
|
93
|
+
"path": string, // the test file you wrote, relative to the working directory
|
|
94
|
+
"command": string, // the exact command that ran it
|
|
95
|
+
"source": string, // the test file's full source
|
|
96
|
+
"output": string // the last 40 lines of output, at most
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Then reply with one line: `proof <id>: <outcome>`.
|
|
102
|
+
````
|
|
@@ -29,7 +29,12 @@ Diff: <<RUN_DIR>>/diff.patch
|
|
|
29
29
|
2. `diff.patch` for what the change actually does. This is the evidence.
|
|
30
30
|
3. The worktree for surrounding context on any file the diff changes but does not
|
|
31
31
|
explain.
|
|
32
|
-
4.
|
|
32
|
+
4. `<<RUN_DIR>>/pr-activity.json`, when it exists, for what has happened on the PR so
|
|
33
|
+
far: `checks` (CI results), `threads` (review threads with file, line and whether
|
|
34
|
+
they are resolved) and `comments`. A failing check, or an unresolved question from a
|
|
35
|
+
human reviewer that bears on the diff, is a watch item. Leave out what magpie itself
|
|
36
|
+
posted (`magpie: true`) and anything already resolved.
|
|
37
|
+
5. The repository's written conventions, for the review rules. At the worktree root:
|
|
33
38
|
`CLAUDE.md`, `AGENTS.md`, `CONTRIBUTING*`, `.github/copilot-instructions.md`, and
|
|
34
39
|
every file under `.cursor/rules/`. Then `CLAUDE.md` and `AGENTS.md` in each
|
|
35
40
|
directory the diff touches. Skip any that do not exist.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Specialist prompts
|
|
2
2
|
|
|
3
3
|
Stage 4 of the walkthrough dispatches five subagents per shard from this file. Build
|
|
4
|
-
each prompt from up to
|
|
4
|
+
each prompt from up to six parts, in this order, and send it as the agent's entire
|
|
5
5
|
task. Each part is a fenced block or the named section it points at; the prose around
|
|
6
6
|
them is instruction to you, not text for the specialist.
|
|
7
7
|
|
|
@@ -68,14 +68,24 @@ review rule is a convention the maintainers wrote down: a change that breaks one
|
|
|
68
68
|
finding in your domain when the rule bears on it, citing the rule's source.
|
|
69
69
|
```
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
6. What is already on the PR: the stdout of `magpie history "<RUN_DIR>"`, verbatim,
|
|
72
|
+
when it prints anything. It lists the PR's open review threads and comments, the
|
|
73
|
+
findings a reviewer dismissed in an earlier magpie run, failing CI checks, and on a
|
|
74
|
+
re-review the files changed since the last one. Run it once per run; every
|
|
75
|
+
specialist gets the same text. It prints nothing on a local run or a first review
|
|
76
|
+
of a quiet PR, and then the part is left out.
|
|
77
|
+
|
|
78
|
+
Replace every `<RUN_DIR>`, `<WORKTREE>`, `<TARGET>` and `<focus>` with the real values
|
|
79
|
+
before sending. `<focus>` is the focus id exactly as in the block's tag (`security`,
|
|
80
|
+
`bugs`, `performance`, `code-smells`, `architecture`): a subagent left to fill it in names
|
|
81
|
+
the file after its own reading of the focus (a bugs specialist wrote
|
|
82
|
+
`correctness.shard-4.json`), which nothing reads. `<WORKTREE>` is what `magpie target "$RUN_DIR"` prints. `<TARGET>` is `PR #<n>`,
|
|
73
83
|
or on a local run `local branch <branch> against <baseRef>` (`detached HEAD
|
|
74
84
|
against <baseRef>` when HEAD has no branch). The subagent has no shell variables from
|
|
75
85
|
your session, so an unexpanded path means it writes its findings where nothing will
|
|
76
|
-
read them.
|
|
86
|
+
read them.
|
|
77
87
|
|
|
78
|
-
Parts 1, 2, and 3 go every time. The contract is what makes the output parseable by
|
|
88
|
+
Parts 1, 2, and 3 go every time; 4, 5 and 6 only when their source exists. The contract is what makes the output parseable by
|
|
79
89
|
`magpie dedupe`, and the focus blocks are what keep the five reviews from collapsing
|
|
80
90
|
into the same generic pass. Do not paraphrase, summarise, or trim either one.
|
|
81
91
|
|
|
@@ -398,8 +408,6 @@ For each potential smell:
|
|
|
398
408
|
|
|
399
409
|
Do not flag formatting, naming, or stylistic preference unless it is evidence of a deeper maintainability problem. Avoid duplicating bug, security, or performance findings unless the primary issue is the maintainability smell behind them.
|
|
400
410
|
|
|
401
|
-
Report at most 3 findings for the scope your run header names, strongest first. If you have more candidates, keep the three whose breaking change is nearest and most concrete and drop the rest.
|
|
402
|
-
|
|
403
411
|
Use the JSON schema defined in the orchestrator's `## Output Contract` block; do not invent fields.
|
|
404
412
|
```
|
|
405
413
|
|
|
@@ -464,8 +472,6 @@ Boundary with Security: flag validation gaps as design/contract issues (where va
|
|
|
464
472
|
|
|
465
473
|
Focus on design decisions introduced or materially worsened by this PR that affect the long-term health of the codebase. Don't flag things that are "technically impure" but work well in practice.
|
|
466
474
|
|
|
467
|
-
Report at most 3 findings for the scope your run header names, strongest first. If you have more candidates, keep the three whose breaking change is nearest and most concrete and drop the rest.
|
|
468
|
-
|
|
469
475
|
Use the JSON schema defined in the orchestrator's `## Output Contract` block; do not invent fields.
|
|
470
476
|
```
|
|
471
477
|
|
|
@@ -47,7 +47,7 @@ afterEach(async () => {
|
|
|
47
47
|
})
|
|
48
48
|
|
|
49
49
|
test('cleanup removes worktree and archives run dir', async () => {
|
|
50
|
-
const exit = await runCleanup({ runDir, repoPath: repo, gitBin: 'git' })
|
|
50
|
+
const exit = await runCleanup({ runDir, repoPath: repo, gitBin: 'git', ghBin: FAKE_GH })
|
|
51
51
|
expect(exit).toBe(0)
|
|
52
52
|
const original = await Bun.file(join(runDir, 'worktree', 'a.txt')).exists()
|
|
53
53
|
expect(original).toBe(false)
|
|
@@ -65,7 +65,7 @@ test('cleanup prints a view-later hint line with the archived id', async () => {
|
|
|
65
65
|
return true
|
|
66
66
|
}) as typeof process.stdout.write
|
|
67
67
|
try {
|
|
68
|
-
await runCleanup({ runDir, repoPath: repo, gitBin: 'git' })
|
|
68
|
+
await runCleanup({ runDir, repoPath: repo, gitBin: 'git', ghBin: FAKE_GH })
|
|
69
69
|
} finally {
|
|
70
70
|
process.stdout.write = origWrite
|
|
71
71
|
}
|
|
@@ -85,7 +85,7 @@ test('cleanup of a local run prints the archive path but no view-later hint', as
|
|
|
85
85
|
return true
|
|
86
86
|
}) as typeof process.stdout.write
|
|
87
87
|
try {
|
|
88
|
-
await runCleanup({ runDir, repoPath: repo, gitBin: 'git' })
|
|
88
|
+
await runCleanup({ runDir, repoPath: repo, gitBin: 'git', ghBin: FAKE_GH })
|
|
89
89
|
} finally {
|
|
90
90
|
process.stdout.write = origWrite
|
|
91
91
|
}
|
|
@@ -98,7 +98,9 @@ test('cleanup with a malformed target.json rejects before archiving or writing l
|
|
|
98
98
|
await writeFile(join(runDir, 'log.jsonl'), '')
|
|
99
99
|
await writeFile(join(runDir, 'findings.final.json'), '[]')
|
|
100
100
|
await writeFile(join(runDir, 'target.json'), '{not json')
|
|
101
|
-
await expect(
|
|
101
|
+
await expect(
|
|
102
|
+
runCleanup({ runDir, repoPath: repo, gitBin: 'git', ghBin: FAKE_GH }),
|
|
103
|
+
).rejects.toThrow('target.json')
|
|
102
104
|
const entries = await readdir(dirname(runDir))
|
|
103
105
|
expect(entries.some((e) => e.startsWith(`${basename(runDir)}.archived-`))).toBe(false)
|
|
104
106
|
expect(await Bun.file(join(runDir, 'labels.json')).exists()).toBe(false)
|
|
@@ -169,7 +171,7 @@ test('cleanup leaves labels.json in the archived dir when findings.final.json ex
|
|
|
169
171
|
join(runDir, 'state', 'events'),
|
|
170
172
|
`${JSON.stringify({ type: 'dismiss', findingId: 'sec-1', reason: 'wrong', timestamp: 1 })}\n`,
|
|
171
173
|
)
|
|
172
|
-
const exit = await runCleanup({ runDir, repoPath: repo, gitBin: 'git' })
|
|
174
|
+
const exit = await runCleanup({ runDir, repoPath: repo, gitBin: 'git', ghBin: FAKE_GH })
|
|
173
175
|
expect(exit).toBe(0)
|
|
174
176
|
const parent = dirname(runDir)
|
|
175
177
|
const archived = (await readdir(parent)).find((e) =>
|
|
@@ -183,7 +185,7 @@ test('cleanup leaves labels.json in the archived dir when findings.final.json ex
|
|
|
183
185
|
|
|
184
186
|
test('runCleanup logs kill outcome to log.jsonl', async () => {
|
|
185
187
|
await writeFile(join(runDir, 'log.jsonl'), '')
|
|
186
|
-
const exit = await runCleanup({ runDir, repoPath: repo, gitBin: 'git' })
|
|
188
|
+
const exit = await runCleanup({ runDir, repoPath: repo, gitBin: 'git', ghBin: FAKE_GH })
|
|
187
189
|
expect(exit).toBe(0)
|
|
188
190
|
const parent = dirname(runDir)
|
|
189
191
|
const entries = await readdir(parent)
|
|
@@ -222,7 +224,12 @@ test('cleanup after a local setup archives the run and leaves the repo as it was
|
|
|
222
224
|
})
|
|
223
225
|
expect(setupExit).toBe(0)
|
|
224
226
|
|
|
225
|
-
const exit = await runCleanup({
|
|
227
|
+
const exit = await runCleanup({
|
|
228
|
+
runDir: localRun,
|
|
229
|
+
repoPath: localRepo,
|
|
230
|
+
gitBin: 'git',
|
|
231
|
+
ghBin: FAKE_GH,
|
|
232
|
+
})
|
|
226
233
|
expect(exit).toBe(0)
|
|
227
234
|
|
|
228
235
|
const parent = dirname(localRun)
|
|
@@ -245,3 +252,92 @@ test('cleanup after a local setup archives the run and leaves the repo as it was
|
|
|
245
252
|
await rm(localRun, { recursive: true, force: true })
|
|
246
253
|
}
|
|
247
254
|
})
|
|
255
|
+
|
|
256
|
+
async function refExists(ref: string): Promise<boolean> {
|
|
257
|
+
const proc = Bun.spawn(['git', 'rev-parse', '--verify', '-q', ref], { cwd: repo, stdout: 'pipe' })
|
|
258
|
+
return (await proc.exited) === 0
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
test('cleanup of a PR run deletes saved refs of merged or closed PRs and keeps open ones', async () => {
|
|
262
|
+
await sh(repo, 'git', 'update-ref', 'refs/magpie/pr-1234', 'HEAD')
|
|
263
|
+
await sh(repo, 'git', 'update-ref', 'refs/magpie/pr-7', 'HEAD')
|
|
264
|
+
|
|
265
|
+
const exit = await runCleanup({ runDir, repoPath: repo, gitBin: 'git', ghBin: FAKE_GH })
|
|
266
|
+
|
|
267
|
+
expect(exit).toBe(0)
|
|
268
|
+
expect(await refExists('refs/magpie/pr-1234')).toBe(true)
|
|
269
|
+
expect(await refExists('refs/magpie/pr-7')).toBe(false)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
test('cleanup of a local run leaves saved PR refs alone', async () => {
|
|
273
|
+
await sh(repo, 'git', 'worktree', 'remove', '--force', join(runDir, 'worktree'))
|
|
274
|
+
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'local', root: repo }))
|
|
275
|
+
await sh(repo, 'git', 'update-ref', 'refs/magpie/pr-7', 'HEAD')
|
|
276
|
+
|
|
277
|
+
await runCleanup({ runDir, repoPath: repo, gitBin: 'git', ghBin: FAKE_GH })
|
|
278
|
+
|
|
279
|
+
expect(await refExists('refs/magpie/pr-7')).toBe(true)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('cleanup still archives the run and keeps the refs when gh cannot say a PR state', async () => {
|
|
283
|
+
await sh(repo, 'git', 'update-ref', 'refs/magpie/pr-7', 'HEAD')
|
|
284
|
+
const err: string[] = []
|
|
285
|
+
const origWrite = process.stderr.write.bind(process.stderr)
|
|
286
|
+
process.stderr.write = ((s: string) => {
|
|
287
|
+
err.push(s)
|
|
288
|
+
return true
|
|
289
|
+
}) as typeof process.stderr.write
|
|
290
|
+
let exit: number
|
|
291
|
+
try {
|
|
292
|
+
exit = await runCleanup({ runDir, repoPath: repo, gitBin: 'git', ghBin: '/usr/bin/false' })
|
|
293
|
+
} finally {
|
|
294
|
+
process.stderr.write = origWrite
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
expect(exit).toBe(0)
|
|
298
|
+
expect(await refExists('refs/magpie/pr-7')).toBe(true)
|
|
299
|
+
expect(err.join('')).toContain(
|
|
300
|
+
'magpie: warning: could not prune refs/magpie: gh pr view 7 exit 1',
|
|
301
|
+
)
|
|
302
|
+
const entries = await readdir(dirname(runDir))
|
|
303
|
+
expect(entries.some((e) => e.startsWith(`${basename(runDir)}.archived-`))).toBe(true)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
test('cleanup prunes the other refs when gh cannot answer for one, and names the one it skipped', async () => {
|
|
307
|
+
for (const n of [404, 5, 7, 9, 1234]) {
|
|
308
|
+
await sh(repo, 'git', 'update-ref', `refs/magpie/pr-${n}`, 'HEAD')
|
|
309
|
+
}
|
|
310
|
+
const err: string[] = []
|
|
311
|
+
const origWrite = process.stderr.write.bind(process.stderr)
|
|
312
|
+
process.stderr.write = ((s: string) => {
|
|
313
|
+
err.push(s)
|
|
314
|
+
return true
|
|
315
|
+
}) as typeof process.stderr.write
|
|
316
|
+
let exit: number
|
|
317
|
+
try {
|
|
318
|
+
exit = await runCleanup({ runDir, repoPath: repo, gitBin: 'git', ghBin: FAKE_GH })
|
|
319
|
+
} finally {
|
|
320
|
+
process.stderr.write = origWrite
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
expect(exit).toBe(0)
|
|
324
|
+
expect(await refExists('refs/magpie/pr-7')).toBe(false)
|
|
325
|
+
expect(await refExists('refs/magpie/pr-9')).toBe(false)
|
|
326
|
+
expect(await refExists('refs/magpie/pr-1234')).toBe(true)
|
|
327
|
+
expect(await refExists('refs/magpie/pr-404')).toBe(true)
|
|
328
|
+
expect(await refExists('refs/magpie/pr-5')).toBe(true)
|
|
329
|
+
expect(err.join('')).toBe(
|
|
330
|
+
'magpie: warning: could not prune refs/magpie: gh pr view 404 exit 1: GraphQL: Could not resolve to a PullRequest; gh pr view 5: unexpected state "WEIRD"\n',
|
|
331
|
+
)
|
|
332
|
+
const archived = (await readdir(dirname(runDir))).find((e) =>
|
|
333
|
+
e.startsWith(`${basename(runDir)}.archived-`),
|
|
334
|
+
) as string
|
|
335
|
+
const log = await readFile(join(dirname(runDir), archived, 'log.jsonl'), 'utf8')
|
|
336
|
+
const entry = log
|
|
337
|
+
.split('\n')
|
|
338
|
+
.filter(Boolean)
|
|
339
|
+
.map((l) => JSON.parse(l) as Record<string, unknown>)
|
|
340
|
+
.find((e) => e.status === 'prune-refs')
|
|
341
|
+
expect(entry?.deleted).toEqual(['refs/magpie/pr-7', 'refs/magpie/pr-9'])
|
|
342
|
+
expect(entry?.failed).toEqual(['refs/magpie/pr-404', 'refs/magpie/pr-5'])
|
|
343
|
+
})
|
|
@@ -374,3 +374,72 @@ test('shard with a custom --budget and --max-files reaches shardDiff with those
|
|
|
374
374
|
await rm(runDir, { recursive: true, force: true })
|
|
375
375
|
}
|
|
376
376
|
})
|
|
377
|
+
|
|
378
|
+
test('history prints the already-on-this-PR section for a run with PR activity', async () => {
|
|
379
|
+
const runDir = await mkdtemp(join(tmpdir(), 'magpie-cli-history-'))
|
|
380
|
+
try {
|
|
381
|
+
await writeFile(
|
|
382
|
+
join(runDir, 'pr-activity.json'),
|
|
383
|
+
JSON.stringify({
|
|
384
|
+
checks: [],
|
|
385
|
+
threads: [],
|
|
386
|
+
comments: [{ author: 'alice', body: 'Why three retries?', magpie: false }],
|
|
387
|
+
}),
|
|
388
|
+
)
|
|
389
|
+
const proc = Bun.spawn(['bun', CLI, 'history', runDir], { stdout: 'pipe', stderr: 'pipe' })
|
|
390
|
+
const exit = await proc.exited
|
|
391
|
+
const stdout = await new Response(proc.stdout).text()
|
|
392
|
+
expect(exit).toBe(0)
|
|
393
|
+
expect(stdout.startsWith('## Already on this PR\n')).toBe(true)
|
|
394
|
+
expect(stdout).toContain('PR comments:\n- alice: Why three retries?\n')
|
|
395
|
+
} finally {
|
|
396
|
+
await rm(runDir, { recursive: true, force: true })
|
|
397
|
+
}
|
|
398
|
+
})
|
|
399
|
+
|
|
400
|
+
test('history prints nothing for a run with no history', async () => {
|
|
401
|
+
const runDir = await mkdtemp(join(tmpdir(), 'magpie-cli-history-'))
|
|
402
|
+
try {
|
|
403
|
+
const proc = Bun.spawn(['bun', CLI, 'history', runDir], { stdout: 'pipe', stderr: 'pipe' })
|
|
404
|
+
const exit = await proc.exited
|
|
405
|
+
expect(exit).toBe(0)
|
|
406
|
+
expect(await new Response(proc.stdout).text()).toBe('')
|
|
407
|
+
} finally {
|
|
408
|
+
await rm(runDir, { recursive: true, force: true })
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('proof-prompt fills the bundled prover prompt for a serious finding', async () => {
|
|
413
|
+
const runDir = await mkdtemp(join(tmpdir(), 'magpie-cli-proof-'))
|
|
414
|
+
try {
|
|
415
|
+
await writeFile(join(runDir, 'log.jsonl'), '')
|
|
416
|
+
await writeFile(join(runDir, 'target.json'), JSON.stringify({ mode: 'pr', root: '/wt' }))
|
|
417
|
+
await writeFile(
|
|
418
|
+
join(runDir, 'findings.final.json'),
|
|
419
|
+
JSON.stringify([
|
|
420
|
+
{
|
|
421
|
+
id: 'bugs-1',
|
|
422
|
+
file: 'src/a.ts',
|
|
423
|
+
line: 3,
|
|
424
|
+
title: 'Cache never evicts',
|
|
425
|
+
description: 'Observation: x',
|
|
426
|
+
domain: 'bugs',
|
|
427
|
+
risk: { impact: 'high', likelihood: 'likely', confidence: 'medium', action: 'must-fix' },
|
|
428
|
+
},
|
|
429
|
+
]),
|
|
430
|
+
)
|
|
431
|
+
const proc = Bun.spawn(['bun', CLI, 'proof-prompt', runDir], { stdout: 'pipe', stderr: 'pipe' })
|
|
432
|
+
const exit = await proc.exited
|
|
433
|
+
const stdout = await new Response(proc.stdout).text()
|
|
434
|
+
expect(exit).toBe(0)
|
|
435
|
+
const promptPath = join(runDir, 'proof-prompt-bugs-1.md')
|
|
436
|
+
expect(stdout).toBe(`${promptPath}\t${join(runDir, 'proof-bugs-1.json')}\n`)
|
|
437
|
+
const prompt = await readFile(promptPath, 'utf8')
|
|
438
|
+
expect(prompt).toContain('"title": "Cache never evicts"')
|
|
439
|
+
expect(prompt).toContain('/wt')
|
|
440
|
+
expect(prompt).toContain(join(runDir, 'proof-bugs-1.json'))
|
|
441
|
+
expect(prompt).not.toContain('<<')
|
|
442
|
+
} finally {
|
|
443
|
+
await rm(runDir, { recursive: true, force: true })
|
|
444
|
+
}
|
|
445
|
+
})
|