@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
@@ -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
- cat <<'JSON'
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": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
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
- cat <<'JSON'
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": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
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 six placeholders
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 six placeholders must stay in the block, or critic-prompt exits 1.
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 when the code refutes it, and say how in `reason`:
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 what the code refutes.
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. The repository's written conventions, for the review rules. At the worktree root:
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 five parts, in this order, and send it as the agent's entire
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
- Replace every `<RUN_DIR>`, `<WORKTREE>` and `<TARGET>` with the real values before
72
- sending. `<WORKTREE>` is what `magpie target "$RUN_DIR"` prints. `<TARGET>` is `PR #<n>`,
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. Leave `<focus>` as written; the contract tells the subagent to substitute it.
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(runCleanup({ runDir, repoPath: repo, gitBin: 'git' })).rejects.toThrow('target.json')
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({ runDir: localRun, repoPath: localRepo, gitBin: 'git' })
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
+ })