@iceinvein/agent-skills 0.1.40 → 0.3.0

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 (141) hide show
  1. package/README.md +18 -2
  2. package/dist/cli/index.js +115 -34
  3. package/package.json +1 -1
  4. package/skills/index.json +14 -2
  5. package/skills/magpie/SKILL.md +118 -40
  6. package/skills/magpie/bin/magpie.ts +43 -0
  7. package/skills/magpie/fixtures/fake-gh-nodiff.sh +38 -0
  8. package/skills/magpie/package.json +1 -1
  9. package/skills/magpie/references/peer-review.md +7 -2
  10. package/skills/magpie/references/specialists.md +38 -7
  11. package/skills/magpie/scripts/__tests__/cli.test.ts +101 -1
  12. package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +187 -0
  13. package/skills/magpie/scripts/__tests__/diff-chunks.test.ts +51 -0
  14. package/skills/magpie/scripts/__tests__/filter-diff-preservation.test.ts +54 -0
  15. package/skills/magpie/scripts/__tests__/findings-files.test.ts +35 -0
  16. package/skills/magpie/scripts/__tests__/gh.test.ts +69 -0
  17. package/skills/magpie/scripts/__tests__/git-diff.test.ts +83 -0
  18. package/skills/magpie/scripts/__tests__/helpers/git-fixture.ts +47 -0
  19. package/skills/magpie/scripts/__tests__/path-filter.test.ts +27 -0
  20. package/skills/magpie/scripts/__tests__/render-cmd.test.ts +95 -0
  21. package/skills/magpie/scripts/__tests__/render-findings.test.ts +33 -0
  22. package/skills/magpie/scripts/__tests__/render-progress.test.ts +42 -0
  23. package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +83 -1
  24. package/skills/magpie/scripts/__tests__/shard.test.ts +165 -0
  25. package/skills/magpie/scripts/__tests__/skill-lint.test.ts +96 -1
  26. package/skills/magpie/scripts/dedupe-cmd.ts +58 -3
  27. package/skills/magpie/scripts/diff-chunks.ts +28 -0
  28. package/skills/magpie/scripts/findings-files.ts +32 -0
  29. package/skills/magpie/scripts/gh.ts +64 -13
  30. package/skills/magpie/scripts/git-diff.ts +111 -0
  31. package/skills/magpie/scripts/path-filter.ts +9 -5
  32. package/skills/magpie/scripts/refresh.ts +8 -0
  33. package/skills/magpie/scripts/render-cmd.ts +28 -9
  34. package/skills/magpie/scripts/render-findings.ts +11 -1
  35. package/skills/magpie/scripts/render-progress.ts +6 -1
  36. package/skills/magpie/scripts/setup-cmd.ts +38 -1
  37. package/skills/magpie/scripts/shard.ts +171 -0
  38. package/skills/magpie/scripts/status-cmd.ts +4 -1
  39. package/skills/magpie/skill.json +2 -2
  40. package/skills/magpie/templates/styles.css +5 -0
  41. package/skills/migrate/README.md +194 -0
  42. package/skills/migrate/SKILL.md +197 -0
  43. package/skills/migrate/bin/migrate +15 -0
  44. package/skills/migrate/bin/migrate.ts +309 -0
  45. package/skills/migrate/biome.json +35 -0
  46. package/skills/migrate/bun.lock +24 -0
  47. package/skills/migrate/docs/architecture.md +294 -0
  48. package/skills/migrate/docs/reference.md +590 -0
  49. package/skills/migrate/fixtures/tiny-express/GROUND-TRUTH.md +39 -0
  50. package/skills/migrate/fixtures/tiny-express/app.js +29 -0
  51. package/skills/migrate/fixtures/tiny-express/cron.js +6 -0
  52. package/skills/migrate/fixtures/tiny-express/reports/daily-users.json +6 -0
  53. package/skills/migrate/fixtures/tiny-express/schema.sql +12 -0
  54. package/skills/migrate/fixtures/tiny-express/settings.json +4 -0
  55. package/skills/migrate/fixtures/tiny-express/views/users.html +9 -0
  56. package/skills/migrate/fixtures/tiny-webforms/Controllers/UsersController.cs +68 -0
  57. package/skills/migrate/fixtures/tiny-webforms/Default.aspx +7 -0
  58. package/skills/migrate/fixtures/tiny-webforms/Default.aspx.cs +14 -0
  59. package/skills/migrate/fixtures/tiny-webforms/GROUND-TRUTH.md +50 -0
  60. package/skills/migrate/fixtures/tiny-webforms/Integrations/BillingClient.cs +16 -0
  61. package/skills/migrate/fixtures/tiny-webforms/Jobs/NightlyDigestJob.cs +33 -0
  62. package/skills/migrate/fixtures/tiny-webforms/Reports/DailyUsers.rdl +11 -0
  63. package/skills/migrate/fixtures/tiny-webforms/Schema.sql +12 -0
  64. package/skills/migrate/fixtures/tiny-webforms/Site.master +16 -0
  65. package/skills/migrate/fixtures/tiny-webforms/Users.aspx +8 -0
  66. package/skills/migrate/fixtures/tiny-webforms/Users.aspx.cs +14 -0
  67. package/skills/migrate/fixtures/tiny-webforms/web.config +10 -0
  68. package/skills/migrate/install.sh +68 -0
  69. package/skills/migrate/package.json +17 -0
  70. package/skills/migrate/references/phases/enumerate.md +291 -0
  71. package/skills/migrate/references/phases/extract.md +652 -0
  72. package/skills/migrate/references/phases/parity.md +275 -0
  73. package/skills/migrate/references/phases/probe.md +135 -0
  74. package/skills/migrate/references/phases/queue.md +242 -0
  75. package/skills/migrate/references/phases/seam.md +416 -0
  76. package/skills/migrate/references/recipes/README.md +116 -0
  77. package/skills/migrate/references/recipes/aspnet.md +287 -0
  78. package/skills/migrate/references/run-ops.md +280 -0
  79. package/skills/migrate/scripts/__tests__/census.test.ts +775 -0
  80. package/skills/migrate/scripts/__tests__/check.test.ts +458 -0
  81. package/skills/migrate/scripts/__tests__/citations.test.ts +156 -0
  82. package/skills/migrate/scripts/__tests__/cli.test.ts +183 -0
  83. package/skills/migrate/scripts/__tests__/concurrency.test.ts +164 -0
  84. package/skills/migrate/scripts/__tests__/config.test.ts +112 -0
  85. package/skills/migrate/scripts/__tests__/e2e-express.test.ts +1093 -0
  86. package/skills/migrate/scripts/__tests__/e2e-webforms.test.ts +1276 -0
  87. package/skills/migrate/scripts/__tests__/e2e.test.ts +320 -0
  88. package/skills/migrate/scripts/__tests__/ids.test.ts +38 -0
  89. package/skills/migrate/scripts/__tests__/import.test.ts +155 -0
  90. package/skills/migrate/scripts/__tests__/init.test.ts +192 -0
  91. package/skills/migrate/scripts/__tests__/leaks.test.ts +176 -0
  92. package/skills/migrate/scripts/__tests__/lock.test.ts +183 -0
  93. package/skills/migrate/scripts/__tests__/paths.test.ts +129 -0
  94. package/skills/migrate/scripts/__tests__/phase-cmd.test.ts +151 -0
  95. package/skills/migrate/scripts/__tests__/phases.test.ts +70 -0
  96. package/skills/migrate/scripts/__tests__/queue.test.ts +475 -0
  97. package/skills/migrate/scripts/__tests__/report.test.ts +150 -0
  98. package/skills/migrate/scripts/__tests__/run-state.test.ts +136 -0
  99. package/skills/migrate/scripts/__tests__/status-reset.test.ts +318 -0
  100. package/skills/migrate/scripts/__tests__/store.test.ts +132 -0
  101. package/skills/migrate/scripts/__tests__/validate.test.ts +54 -0
  102. package/skills/migrate/scripts/census-cmd.ts +109 -0
  103. package/skills/migrate/scripts/census.ts +342 -0
  104. package/skills/migrate/scripts/check-cmd.ts +24 -0
  105. package/skills/migrate/scripts/check.ts +376 -0
  106. package/skills/migrate/scripts/citations.ts +92 -0
  107. package/skills/migrate/scripts/config.ts +237 -0
  108. package/skills/migrate/scripts/ids.ts +31 -0
  109. package/skills/migrate/scripts/import-cmd.ts +141 -0
  110. package/skills/migrate/scripts/init-cmd.ts +118 -0
  111. package/skills/migrate/scripts/leaks.ts +184 -0
  112. package/skills/migrate/scripts/lock.ts +188 -0
  113. package/skills/migrate/scripts/paths.ts +103 -0
  114. package/skills/migrate/scripts/phase-cmd.ts +63 -0
  115. package/skills/migrate/scripts/phases.ts +113 -0
  116. package/skills/migrate/scripts/queue-cmd.ts +98 -0
  117. package/skills/migrate/scripts/queue.ts +258 -0
  118. package/skills/migrate/scripts/report-cmd.ts +47 -0
  119. package/skills/migrate/scripts/report.ts +131 -0
  120. package/skills/migrate/scripts/reset-cmd.ts +120 -0
  121. package/skills/migrate/scripts/status-cmd.ts +52 -0
  122. package/skills/migrate/scripts/store.ts +159 -0
  123. package/skills/migrate/scripts/types.ts +137 -0
  124. package/skills/migrate/scripts/validate.ts +221 -0
  125. package/skills/migrate/skill.json +33 -0
  126. package/skills/migrate/templates/config.toml +27 -0
  127. package/skills/migrate/templates/queue-item.md +17 -0
  128. package/skills/migrate/tsconfig.json +18 -0
  129. package/skills/migrate/uninstall.sh +31 -0
  130. package/skills/sluice/SKILL.md +95 -0
  131. package/skills/sluice/references/deep-channel.md +114 -0
  132. package/skills/sluice/references/finish.md +37 -0
  133. package/skills/sluice/references/intent.md +29 -0
  134. package/skills/sluice/references/meter.md +38 -0
  135. package/skills/sluice/references/review.md +42 -0
  136. package/skills/sluice/references/root-cause.md +38 -0
  137. package/skills/sluice/references/show-or-say.md +36 -0
  138. package/skills/sluice/references/test-first.md +35 -0
  139. package/skills/sluice/references/verify.md +26 -0
  140. package/skills/sluice/scripts/run-stats.sh +236 -0
  141. package/skills/sluice/skill.json +33 -0
@@ -7,7 +7,7 @@ description: Use when the user asks to review a GitHub pull request (a PR number
7
7
 
8
8
  ## Prerequisites
9
9
 
10
- The skill pre-flights `bun`, `gh`, `git` (required) and `codex` (optional). If a required binary is missing the run aborts with a single install hint line. `codex` is the preferred peer reviewer, but it is optional: if it is missing the run continues and the peer-review stage falls back to a Claude second-opinion subagent (setup prints a one-line notice and logs `{stage: preflight, status: done, missingOptional: ["codex"]}`).
10
+ The skill pre-flights `bun`, `gh`, `git` (required) and `codex` (optional). A missing required binary aborts the run with a single install hint line. Without `codex` the run continues and peer review falls back to a Claude second-opinion subagent (setup prints a one-line notice and logs `{stage: preflight, status: done, missingOptional: ["codex"]}`).
11
11
 
12
12
  ## Stage walkthrough
13
13
 
@@ -15,15 +15,15 @@ Stop reading and follow these steps in order. Do not skip stages. Use the exact
15
15
 
16
16
  ### 0. Identify the PR
17
17
 
18
- Parse the user's request for a PR number, URL, or "this PR" (current branch). If ambiguous, ask one clarifying terminal question. Capture the PR number into `$PR_NUMBER` and the repository path into `$REPO` (default: current working directory).
18
+ Parse the user's request for a PR number, URL, or "this PR" (current branch). If ambiguous, ask one clarifying terminal question. Capture the PR number into `$PR_NUMBER` and the repo path into `$REPO` (default: the current working directory).
19
19
 
20
- Then check whether an earlier run on this PR is still unfinished, before minting a new id:
20
+ Before minting a new id, check whether an earlier run on this PR is unfinished:
21
21
 
22
22
  ```
23
23
  magpie --list-runs
24
24
  ```
25
25
 
26
- Each line is `<id>\t<active|archived>\t<path>`. If an `active` id matches `pr-${PR_NUMBER}-*`, that run was interrupted rather than cleaned up. Set `RUN_DIR` to its path and go to "Resuming a crashed run" instead of starting over; ask the user first if it is unclear whether they want to resume or review from scratch. (`archived` ids are finished runs, not resumable.)
26
+ Each line is `<id>\t<active|archived>\t<path>`. If an `active` id matches `pr-${PR_NUMBER}-*`, that run was interrupted rather than cleaned up. Set `RUN_DIR` to its path and go to "Resuming a crashed run"; ask the user first if it is unclear whether they want to resume or start over. (`archived` ids are finished runs, not resumable.)
27
27
 
28
28
  Otherwise compute a fresh run directory:
29
29
 
@@ -38,13 +38,30 @@ RUN_DIR="$HOME/.magpie/$RUN_ID"
38
38
  magpie setup "$RUN_DIR" --pr $PR_NUMBER --repo "$REPO"
39
39
  ```
40
40
 
41
- If exit is non-zero, surface stderr verbatim and stop. The CLI removes the worktree and subdirs on failure; the run directory itself plus `log.jsonl` are kept for diagnostics.
41
+ If exit is non-zero, surface stderr verbatim and stop. The CLI removes the worktree and subdirs on failure, keeping the run directory and `log.jsonl` for diagnostics.
42
42
 
43
- Setup automatically filters lockfiles, build output, generated source, and snapshot fixtures from `diff.patch` before specialists see it. Users can override by placing `.magpie.json` at the repo root: `{"exclude": [...glob], "include": [...glob], "useDefaults": true|false}`. When anything is filtered, the raw diff is preserved as `$RUN_DIR/diff.full.patch` and the exclusion list as `$RUN_DIR/excluded-files.json`.
43
+ Setup filters lockfiles, build output, generated source, and snapshot fixtures from `diff.patch` before specialists see it. Users override with `.magpie.json` at the repo root: `{"exclude": [...glob], "include": [...glob], "useDefaults": true|false}`. When anything is filtered, the raw diff is kept as `$RUN_DIR/diff.full.patch` and the exclusion list as `$RUN_DIR/excluded-files.json`.
44
+
45
+ When `gh pr diff` refuses the diff (HTTP 406 above roughly 300 files) or returns an
46
+ empty diff for a PR with changed files, setup rebuilds it from the local clone instead
47
+ of aborting: it fetches `pull/<n>/head` and diffs from the merge base against the PR's
48
+ base branch, reproducing the three-dot semantics `gh pr diff` uses. A local head that
49
+ does not match the PR's `headRefOid` is a hard error, not a silently stale review. The
50
+ `fetch-pr` log entry records `source: "gh" | "git"` and the merge base, and
51
+ `$RUN_DIR/diff-source.json` carries the same for the report.
52
+
53
+ Setup then splits the filtered diff into shards, writing `$RUN_DIR/shards/manifest.json`
54
+ and, when more than one shard results, `$RUN_DIR/shards/shard-<n>.patch`. `diff.patch`
55
+ itself is never modified: shards are views over it. Re-split with a different budget
56
+ using `magpie shard "$RUN_DIR" --budget <lines> --max-files <n>` (defaults: 6000 patch
57
+ lines, 80 files). Re-splitting invalidates every existing
58
+ `findings/<focus>.shard-<n>.json`, since a shard id then names a different file set:
59
+ delete those files first, or stage 4's resume rule counts a pair as covered that
60
+ nothing reviewed.
44
61
 
45
62
  When a prior run exists for the same PR (active or archived under `~/.magpie/`), setup writes `$RUN_DIR/incremental.json` with `{previousRunId, previousSha, currentSha, sameSha}`. The post stage surfaces this as a "Incremental review since `<sha>`" trailer on the summary comment.
46
63
 
47
- Setup also runs a deterministic test-coverage check: when the diff contains zero test or spec files anywhere, each non-test source file with `>= 10` added code lines gets a `domain: "tests"` finding written to `$RUN_DIR/findings/tests.json`. This is a sixth domain that flows through dedupe/critic/peer-review alongside the five LLM specialists. No specialist subagent is dispatched for it.
64
+ Setup also runs a deterministic test-coverage check: when the diff contains zero test or spec files anywhere, each non-test source file with `>= 10` added code lines gets a `domain: "tests"` finding in `$RUN_DIR/findings/tests.json`. That sixth domain flows through dedupe/critic/peer-review alongside the five LLM specialists; no subagent is dispatched for it.
48
65
 
49
66
  ### 2. Serve
50
67
 
@@ -54,9 +71,9 @@ Start the HTML server in the background using the Bash tool with `run_in_backgro
54
71
  magpie serve "$RUN_DIR"
55
72
  ```
56
73
 
57
- Read `$RUN_DIR/state/server-info` for the URL; the server writes it asynchronously at startup, so if the file doesn't exist yet, wait a moment and re-read (it appears within ~1s). Print to the user: "Open <url> in your browser to follow along."
74
+ Read `$RUN_DIR/state/server-info` for the URL; the server writes it asynchronously, so if it is missing, wait a moment and re-read (it appears within ~1s). Print to the user: "Open <url> in your browser to follow along."
58
75
 
59
- The server shuts down after 30 minutes with no requests (an open report tab heartbeats every 30s, so it stays up while the user is looking at it) and deletes `state/server-info` on the way out. Nothing in the pipeline depends on it staying alive: re-run `magpie serve "$RUN_DIR"` to bring the report back.
76
+ The server shuts down after 30 idle minutes (an open report tab heartbeats every 30s, so it stays up while the user is looking) and deletes `state/server-info` on the way out. Nothing in the pipeline depends on it: re-run `magpie serve "$RUN_DIR"` to bring the report back.
60
77
 
61
78
  Render the first progress paint:
62
79
 
@@ -68,7 +85,7 @@ magpie render "$RUN_DIR" progress
68
85
 
69
86
  Append `{stage: context, status: running}` to `$RUN_DIR/log.jsonl` and re-render progress. This stage has two steps and never aborts the run.
70
87
 
71
- **Bind probe.** If the `mcp__code-intelligence__*` tools are not in your tool list, skip straight to the scout with `CODE_INTELLIGENCE=unavailable`. Otherwise call `bind_workspace` with `$RUN_DIR/worktree`. The worktree is a linked git worktree, so an already-indexed base repo seeds its index instead of re-indexing.
88
+ **Bind probe.** If the `mcp__code-intelligence__*` tools are not in your tool list, skip straight to the scout with `CODE_INTELLIGENCE=unavailable`. Otherwise call `bind_workspace` with `$RUN_DIR/worktree`: it is a linked git worktree, so an already-indexed base repo seeds its index instead of re-indexing.
72
89
 
73
90
  - `consent_required` means the base repo has never completed an index. **Never call `approve_indexing`**: that is a full GPU pass the user did not ask for. Set `CODE_INTELLIGENCE=unavailable`, and print one line: "Code intelligence is unavailable (the base repo has no index); specialists will review from the diff alone."
74
91
  - `indexing_started` or `indexing_in_progress` means the seed took. Poll `get_index_stats` every 5s for at most 60s, then set `CODE_INTELLIGENCE=available` either way. Do not block the pipeline on completion; the specialist contract handles a still-indexing tool.
@@ -77,17 +94,61 @@ Append `{stage: context, status: running}` to `$RUN_DIR/log.jsonl` and re-render
77
94
 
78
95
  **Scout.** Read `references/scout.md` and dispatch one subagent (Agent tool, `general-purpose`) carrying the `magpie-scout` block with `<<RUN_DIR>>`, `<<PR_NUMBER>>`, and `<<CODE_INTELLIGENCE>>` substituted. It writes `$RUN_DIR/brief.json`.
79
96
 
80
- Append `{stage: context, status: done, codeIntelligence: true|false}` and re-render progress. If the scout returned without writing `brief.json`, append `{stage: context, status: skipped, codeIntelligence: true|false}` instead and continue: the brief is optional everywhere it is read. The bind probe's result is known regardless of what the scout did, so both entries carry it.
97
+ Append `{stage: context, status: done, codeIntelligence: true|false}` and re-render progress. If the scout returned without writing `brief.json`, append `{stage: context, status: skipped, codeIntelligence: true|false}` instead and continue: the brief is optional everywhere it is read. Both entries carry the probe's result, which is known whatever the scout did.
81
98
 
82
99
  ### 4. Specialists
83
100
 
84
- Read `references/specialists.md` now, before dispatching anything. It holds the five focus blocks and the output contract that every specialist prompt is built from. Assemble the prompts from that file verbatim: prompts written from memory drift off the JSON contract, and `magpie dedupe` drops findings it cannot parse.
85
-
86
- Append `{stage: specialists, status: running}` to `$RUN_DIR/log.jsonl` and re-render progress, so the served page shows the stage as active rather than "Paused". Then dispatch the five specialist subagents in a single message using five Agent tool calls in parallel, one per focus in (security, bugs, performance, code-smells, architecture), each carrying the prompt that `references/specialists.md` describes.
87
-
88
- After each subagent returns, append `{stage: specialist, focus: <focus>, status: done, findings: <count>}` to `$RUN_DIR/log.jsonl` and re-render progress. (Per-focus `specialist` entries are diagnostic; only the aggregate `specialists` entry advances `magpie status`.)
89
-
90
- If all five specialists fail (no findings files written), log `{stage: specialists, status: error}`, rebind code intelligence to `$REPO` if bound (stage 10), and stop. Otherwise mark `{stage: specialists, status: done}`.
101
+ Read `references/specialists.md` now, before dispatching anything: it holds the five focus blocks and the output contract every specialist prompt is built from. Assemble prompts from that file verbatim; written from memory they drift off the JSON contract, and `magpie dedupe` drops findings it cannot parse.
102
+
103
+ Append `{stage: specialists, status: running}` to `$RUN_DIR/log.jsonl` and re-render
104
+ progress, so the served page shows the stage as active rather than "Paused". Then read
105
+ `$RUN_DIR/shards/manifest.json`.
106
+
107
+ **One shard, zero shards, or no manifest** (a diff filtered down to nothing, e.g. a
108
+ lockfile-only PR, yields `shards: []` in an otherwise normal manifest; a run predating
109
+ this feature has no manifest at all): dispatch the five specialists in a single message,
110
+ five parallel Agent calls, one per focus in (security, bugs, performance, code-smells,
111
+ architecture), each carrying the prompt `references/specialists.md` describes with the
112
+ unsharded run header. The shard gate and the wave dispatch below do not apply; the
113
+ logging and the file check at the end of this stage still do.
114
+
115
+ **More than one shard:** each focus reviews every shard, so the run dispatches
116
+ `5 × <shard count>` subagents in total.
117
+
118
+ **More than four shards: stop and ask the user once, before dispatching anything.**
119
+ State the shard count, the resulting agent count, and the three options: proceed as
120
+ sharded; re-shard for fewer, larger chunks with
121
+ `magpie shard "$RUN_DIR" --budget <lines> --max-files <n>`, raising both flags (a PR of
122
+ many small files is split by the 80-file cap, so a larger `--budget` alone changes
123
+ nothing); or review only the highest-risk shards, which means appending
124
+ `{stage: shard-coverage, status: partial, reviewed: [<ids>], skipped: [<ids>]}` to
125
+ `$RUN_DIR/log.jsonl` and telling the user in the terminal which shards go unreviewed.
126
+ That log entry is the only record of the gap: the report has no unreviewed marker.
127
+ Wait for the answer. This is the only interactive gate in the pipeline before the
128
+ report, and it exists so that neither the cost nor a coverage gap is ever silent.
129
+
130
+ Dispatch by wave, one shard per wave, the five focuses in parallel within a wave,
131
+ re-rendering progress between waves. That holds in-flight agents at five and makes a
132
+ crash cheap to resume: only the `(focus, shard)` pairs whose findings file is missing
133
+ need re-dispatching.
134
+
135
+ After each subagent returns, append
136
+ `{stage: specialist, focus: <focus>, shard: <n>, status: done, findings: <count>}` to
137
+ `$RUN_DIR/log.jsonl` and re-render progress. Omit `shard` on the unsharded path (one
138
+ shard, zero shards, or no manifest).
139
+ (Per-focus `specialist` entries are diagnostic; only the aggregate `specialists` entry
140
+ advances `magpie status`.)
141
+
142
+ Before leaving this stage, list `$RUN_DIR/findings` and confirm one file per expected
143
+ `(focus, shard)` pair: `5 × <shard count>` named `findings/<focus>.shard-<n>.json` when
144
+ sharded, five `findings/<focus>.json` otherwise, plus `findings/tests.json` from setup.
145
+ Re-dispatch any pair missing from a shard you meant to review; a shard skipped at the
146
+ gate is expected to have none. `magpie dedupe` re-checks this against the manifest and
147
+ names every missing pair on stdout, as a backstop rather than a substitute.
148
+
149
+ If every specialist fails (no findings files written), log
150
+ `{stage: specialists, status: error}`, rebind code intelligence to `$REPO` if bound
151
+ (stage 10), and stop. Otherwise mark `{stage: specialists, status: done}`.
91
152
 
92
153
  ### 5. Dedupe
93
154
 
@@ -95,9 +156,9 @@ If all five specialists fail (no findings files written), log `{stage: specialis
95
156
  magpie dedupe "$RUN_DIR" [--threshold <0-10>]
96
157
  ```
97
158
 
98
- `magpie dedupe` also runs a deterministic evidence check against the worktree: findings whose `file` is missing or whose `line` is out of range are dropped. Drops are logged and recorded to `$RUN_DIR/evidence-dropped.json`. The check is skipped if the worktree is no longer present (archived run replay).
159
+ `magpie dedupe` also runs a deterministic evidence check against the worktree: findings whose `file` is missing or whose `line` is out of range are dropped, logged, and recorded to `$RUN_DIR/evidence-dropped.json`. The check is skipped when the worktree is gone (archived run replay).
99
160
 
100
- Each finding receives a derived 0-10 `score` from its risk fields. Findings below `--threshold` (default 3) are dropped before the critic LLM runs and recorded to `$RUN_DIR/threshold-dropped.json`. Pass `--threshold 0` to keep everything.
161
+ Each finding gets a derived 0-10 `score` from its risk fields; those below `--threshold` (default 3) are dropped before the critic LLM runs and recorded to `$RUN_DIR/threshold-dropped.json`. Pass `--threshold 0` to keep everything.
101
162
 
102
163
  Re-render progress.
103
164
 
@@ -105,25 +166,38 @@ Re-render progress.
105
166
 
106
167
  Read `references/critic.md` and `$RUN_DIR/findings.deduped.json`. Substitute both placeholders in the critic rubric (the compact candidate list including each finding's `onChangedLine`, and the `<<DIFF_EXCERPT>>` hunks for the referenced files), then apply the rubric verbatim (one verdict per finding). Write the kept subset to `$RUN_DIR/findings.kept.json`. Append `{stage: critic, status: done}` and re-render progress.
107
168
 
169
+ When `findings.deduped.json` holds more than 40 findings, run the rubric in batches of
170
+ 30 rather than one prompt: a sharded run can produce more candidates than fit alongside
171
+ their diff excerpts. Apply the same rubric verbatim per batch and concatenate the kept
172
+ subsets into `findings.kept.json`.
173
+
108
174
  ### 7. Peer review
109
175
 
110
- Append `{stage: peer-review, status: running}` to `$RUN_DIR/log.jsonl` and re-render progress. This stage always runs. `codex` is the preferred reviewer because it is a different model from the Claude agents that produced the findings; when `codex` is unavailable, a Claude second-opinion subagent stands in.
176
+ Append `{stage: peer-review, status: running}` to `$RUN_DIR/log.jsonl` and re-render progress. This stage always runs. `codex` is preferred because it is a different model from the Claude agents that produced the findings; without it, a Claude second-opinion subagent stands in.
177
+
178
+ Build the peer-review prompt first: read `references/peer-review.md`, take the `magpie-peer-review` block from it, and substitute the placeholders listed in that file's `## Substitute before use` preamble.
111
179
 
112
- Build the peer-review prompt first: read `references/peer-review.md`, take the `magpie-peer-review` block from it, and substitute the placeholders listed in that file's `## Substitute before use` preamble. Write the substituted prompt to `$RUN_DIR/peer-prompt.md`.
180
+ One batch carries up to 40 findings; above that, split them 30 at a time, as in stage 6.
181
+ Write each batch's prompt, its `<<KEPT_FINDINGS_COMPACT>>` narrowed to that batch, to
182
+ `$RUN_DIR/peer-prompt-<k>.md`, `<k>` counting from 1. **When there is a single batch, drop `-<k>` throughout** (`peer-prompt.md`,
183
+ `peer.out`), which is the common case. Keep the `add` id counter running across batches
184
+ (`peer-1`, `peer-2`, ...): restarting it per batch produces colliding ids, and every
185
+ finding in `findings.final.json` must have a unique one or the report and post stages
186
+ crash.
113
187
 
114
- **Codex path (preferred).** If `codex` is available (setup did not log `missingOptional: ["codex"]` and `command -v codex` succeeds), set `<<PEER_PROVIDER>>` to `codex` and run codex with the prompt piped on stdin:
188
+ **Codex path (preferred).** If `codex` is available (setup did not log `missingOptional: ["codex"]` and `command -v codex` succeeds), set `<<PEER_PROVIDER>>` to `codex` and run codex once per batch, that batch's prompt piped on stdin:
115
189
 
116
190
  ```
117
- codex exec < "$RUN_DIR/peer-prompt.md" > "$RUN_DIR/peer.out"
191
+ codex exec < "$RUN_DIR/peer-prompt-<k>.md" > "$RUN_DIR/peer-<k>.out"
118
192
  ```
119
193
 
120
- `peer.out` is codex's full transcript; extract the fenced JSON block tagged `review-peer-review` from it to get the verdicts array. Write that verdicts array to `$RUN_DIR/peer.json`, append `{stage: peer-review, status: done, provider: codex}`, then apply the verdicts as described below.
194
+ Each `peer-<k>.out` is codex's full transcript; extract the fenced JSON block tagged `review-peer-review` from each. Write the concatenated verdict arrays to `$RUN_DIR/peer.json` once, after the last batch: writing `peer.json` per batch keeps only the last batch's verdicts and silently discards the rest. Then append `{stage: peer-review, status: done, provider: codex}` and apply the verdicts as described below.
121
195
 
122
- If codex returns non-zero, do not abort: record `{stage: peer-review, provider: codex, status: fallback, error: "<first line of stderr>"}` and fall through to the Claude path. (Never log `status: error` for a recoverable codex failure: `magpie status` stops at the first `error` entry and would report the run as poisoned even after the Claude fallback succeeds.)
196
+ If codex returns non-zero on a batch, do not abort: record `{stage: peer-review, provider: codex, status: fallback, batch: <k>, error: "<first line of stderr>"}` and take the Claude path for that batch. (Never log `status: error` for a recoverable codex failure: `magpie status` stops at the first `error` entry and would report the run as poisoned even after the Claude fallback succeeds.)
123
197
 
124
- **Claude path (fallback).** When `codex` is unavailable or failed, get the second opinion from a Claude subagent instead. Set `<<PEER_PROVIDER>>` to `claude`, then prepend the `magpie-peer-review-claude-preamble` block from `references/peer-review.md` to the substituted peer-review prompt (the preamble forces genuine independence, since the reviewer shares a model family with the primary reviewers). Dispatch one subagent (Agent tool, `general-purpose`) whose entire task is that combined prompt, and instruct it to return only the fenced `review-peer-review` JSON block. Write its output to `$RUN_DIR/peer.out`, extract the `review-peer-review` block to `$RUN_DIR/peer.json`, and append `{stage: peer-review, status: done, provider: claude}`.
198
+ **Claude path (fallback).** When `codex` is unavailable or failed, get the second opinion from a Claude subagent instead, one per batch. Set `<<PEER_PROVIDER>>` to `claude`, then prepend the `magpie-peer-review-claude-preamble` block from `references/peer-review.md` to each batch's substituted prompt (the preamble forces genuine independence, since the reviewer shares a model family with the primary reviewers). Dispatch one subagent (Agent tool, `general-purpose`) per batch whose entire task is that combined prompt, and instruct it to return only the fenced `review-peer-review` JSON block. Write each output to `$RUN_DIR/peer-<k>.out`, extract each `review-peer-review` block, merge into `$RUN_DIR/peer.json` after the last batch as above, and append `{stage: peer-review, status: done, provider: claude}` (`provider: mixed` if codex handled some batches).
125
199
 
126
- **Apply the verdicts (both paths).** Parse the verdicts JSON and apply the `update` / `add` entries (an empty array means no change). For each `add`, mint a unique `id` on the new finding before merging (`peer-1`, `peer-2`, ...): the peer contract does not include ids, but every finding in `findings.final.json` must carry one or the report render and post stages will crash. Then write `findings.final.json`. Re-render progress.
200
+ **Apply the verdicts (both paths).** Parse the merged verdicts and apply the `update` / `add` entries (an empty array means no change). Mint each `add`'s `id` as above before merging, since the peer contract does not carry ids. Then write `findings.final.json`. Re-render progress.
127
201
 
128
202
  ### 8. Report
129
203
 
@@ -139,9 +213,9 @@ End the turn.
139
213
 
140
214
  ### 9. Post
141
215
 
142
- Most users will tick the checkboxes in the served report and click **Post Selected** (or **Post Recommended**, which takes every finding whose `risk.action` is `must-fix` or `should-fix`, skipping the `consider`/`optional` ones); the report server handles the rest and posts the batch as one GitHub review with inline threads. The agent only handles posts when the user explicitly types `post` (optionally `post 1,3,7` for indices) in the conversation, which takes the CLI path below: separate inline comments plus a top-level summary comment. Either path records posted ids in `post-status.json`, so the two cannot double-post the same finding.
216
+ Most users tick the checkboxes in the served report and click **Post Selected** (or **Post Recommended**, which takes every `must-fix`/`should-fix` finding and skips the `consider`/`optional` ones); the server posts that batch as one GitHub review with inline threads. The agent posts only when the user types `post` (optionally `post 1,3,7` for indices), which takes the CLI path below: separate inline comments plus a top-level summary comment. Either path records posted ids in `post-status.json`, so the two cannot double-post the same finding.
143
217
 
144
- When the user types `post`, read `$RUN_DIR/state/events`. Fold the events in order, keeping the LAST event per finding id; ids whose last event is `select` are selected. (Not union-minus: the UI emits one event per toggle, so select then deselect then select again must resolve to selected.) Merge with any explicit indices the user named (1-based, against `findings.final.json` in file order). If that leaves nothing selected, say so and ask rather than posting an empty batch. Then post via the CLI:
218
+ When the user types `post`, read `$RUN_DIR/state/events` and fold them in order, keeping the LAST event per finding id; ids whose last event is `select` are selected. (Not union-minus: the UI emits one event per toggle, so select, deselect, select again resolves to selected.) Merge any explicit indices the user named (1-based, against `findings.final.json` in file order). If nothing is selected, say so and ask rather than posting an empty batch. Then post via the CLI:
145
219
 
146
220
  ```
147
221
  magpie post "$RUN_DIR" --ids id1,id2,id3
@@ -149,12 +223,12 @@ magpie post "$RUN_DIR" --ids id1,id2,id3
149
223
 
150
224
  That delegates to `runPost`, which:
151
225
 
152
- - Picks `formatInlineBody` (severity heading, `<sub>` risk metaline, parsed `Observation`/`Why it matters`/`Suggested direction`/`Needs verification` sections, optional `` ```suggestion `` block, hidden `magpie:finding` marker) when the finding has a `line`, and uses `gh api repos/<owner>/<repo>/pulls/<n>/comments` to open an inline review thread.
153
- - Falls back to `formatConversationBody` (same shape plus a `Location · <file>:<line>` metaline) posted via `gh pr comment <n>` when there is no anchor, or when GitHub rejects the inline anchor with 422.
154
- - When at least one new finding is being posted in this batch (default `auto` mode), prepends one top-level summary comment (verdict line, "Needs Attention" top three, `<details>` risk breakdown) and persists the sentinel `__summary__` in `post-status.json` so re-runs don't duplicate it. Override with `--include-summary always|never` if you need to force or suppress it.
226
+ - For a finding with a `line`, picks `formatInlineBody` (severity heading, `<sub>` risk metaline, parsed `Observation`/`Why it matters`/`Suggested direction`/`Needs verification` sections, optional `` ```suggestion `` block, hidden `magpie:finding` marker) and opens an inline review thread via `gh api repos/<owner>/<repo>/pulls/<n>/comments`.
227
+ - Falls back to `formatConversationBody` (same shape plus a `Location · <file>:<line>` metaline) via `gh pr comment <n>` when there is no anchor, or GitHub rejects the inline anchor with 422.
228
+ - When at least one new finding is in the batch (default `auto` mode), prepends one top-level summary comment (verdict line, "Needs Attention" top three, `<details>` risk breakdown) and persists the `__summary__` sentinel in `post-status.json` so re-runs don't duplicate it. Override with `--include-summary always|never`.
155
229
  - Appends `{stage: post, ...}` events to `log.jsonl` and updates `$RUN_DIR/post-status.json` per finding id.
156
230
 
157
- Pass `--dry-run` to record the would-be gh commands without invoking gh. After posting, append `{stage: post, status: done}` to `$RUN_DIR/log.jsonl` (`runPost` logs per-finding `ok`/`failed` events but not the stage-complete marker, and `magpie status` counts only `done`), then re-render the report so the badges update:
231
+ Pass `--dry-run` to record the would-be gh commands without invoking gh. After posting, append `{stage: post, status: done}` to `$RUN_DIR/log.jsonl` (`runPost` logs per-finding `ok`/`failed` events but not the stage marker, and `magpie status` counts only `done`), then re-render the report so the badges update:
158
232
 
159
233
  ```
160
234
  magpie render "$RUN_DIR" findings
@@ -166,15 +240,15 @@ magpie render "$RUN_DIR" findings
166
240
  magpie cleanup "$RUN_DIR" --repo "$REPO"
167
241
  ```
168
242
 
169
- If the context stage bound code intelligence, rebind the session to the repository now: call `bind_workspace` with `$REPO`. Binding is per session with no per-call override, so a run that ends without this leaves your session pointed at a worktree `cleanup` just deleted. The daemon prunes the seeded index on its own once the worktree is gone.
243
+ If the context stage bound code intelligence, rebind the session now: call `bind_workspace` with `$REPO`. Binding is per session with no per-call override, so ending a run without this leaves the session pointed at a worktree `cleanup` just deleted. The daemon prunes the seeded index once the worktree is gone.
170
244
 
171
- The run directory is renamed to `<run-dir>.archived-<timestamp>` and the worktree is removed. The CLI prints two lines on success: `archived to <path>` and `view later: magpie open <archived-id>`. Surface that second line to the user verbatim so they have a one-command path back to the report.
245
+ The run directory is renamed to `<run-dir>.archived-<timestamp>` and the worktree is removed. The CLI prints two lines on success: `archived to <path>` and `view later: magpie open <archived-id>`. Surface that second line verbatim so the user has a one-command path back to the report.
172
246
 
173
247
  The archived `findings.html` is self-contained and auto-switches to read-only "archived" mode when opened, so:
174
248
 
175
- - `magpie open` (no args) opens the latest run in the user's default browser via `open`/`xdg-open`. Add `--dry-run` to see the command without spawning.
249
+ - `magpie open` (no args) opens the latest run in the default browser via `open`/`xdg-open`; `--dry-run` prints the command instead of spawning it.
176
250
  - `magpie open <id>` opens a specific archived run.
177
- - `magpie serve <id>` re-spins the Bun server against an archived run if the user wants the live interactive surface back (posts still work because `pr.json` retains the head SHA).
251
+ - `magpie serve <id>` re-spins the Bun server against an archived run for the live interactive surface (posts still work: `pr.json` retains the head SHA).
178
252
  - `magpie --list-runs` enumerates all runs in `~/.magpie/`.
179
253
 
180
254
  ## Resuming a crashed run
@@ -189,10 +263,14 @@ The JSON output tells you `lastCompleted` and `next`. Resume from `next`:
189
263
 
190
264
  - `context` re-runs by redoing the bind probe, then dispatching the scout only if `$RUN_DIR/brief.json` is missing. The seeded index survives a crash, so the rebind is near-instant.
191
265
  - Any other stage: run it as written in the walkthrough.
192
- - If a specialist focus has no findings file but its sibling stages are done, re-dispatch only that focus.
266
+ - If a specialist focus has no findings file but its sibling stages are done,
267
+ re-dispatch only that focus. On a sharded run the unit is the `(focus, shard)` pair:
268
+ read `shards/manifest.json`, and re-dispatch only the pairs with no
269
+ `findings/<focus>.shard-<n>.json`. Never re-shard mid-run without deleting those
270
+ files first (stage 1): the check would otherwise trust ids that moved.
193
271
  - Non-null `error` means the run stopped on a failed stage. Report which stage to the user and confirm before re-running it.
194
272
 
195
- The server from the original run is gone. Restart it with `magpie serve "$RUN_DIR"` (step 2) before re-rendering, so the user gets a live URL again.
273
+ The original server is gone. Restart it with `magpie serve "$RUN_DIR"` (step 2) before re-rendering, so the user gets a live URL again.
196
274
 
197
275
  ## Aborting
198
276
 
@@ -10,6 +10,8 @@ Subcommands:
10
10
  setup <run-dir> --pr <n> Pre-flight, fetch PR, create worktree
11
11
  serve <run-dir-or-id> Start the HTML server (accepts active or archived run id)
12
12
  dedupe <run-dir> Merge specialist findings into deduped set
13
+ shard <run-dir> [--budget N] [--max-files N]
14
+ Re-split diff.patch into budgeted shards
13
15
  render <run-dir> <page> Render progress.html or findings.html
14
16
  cleanup <run-dir> Remove worktree, stop server, archive run
15
17
  status <run-dir> Print highest completed stage
@@ -116,6 +118,47 @@ const HANDLERS: Record<string, Handler> = {
116
118
  const { runDedupe } = await import('../scripts/dedupe-cmd.ts')
117
119
  return runDedupe(runDir, threshold !== undefined ? { threshold } : {})
118
120
  },
121
+ shard: async (args) => {
122
+ const runDir = args[0]
123
+ if (!runDir) {
124
+ process.stderr.write('shard: missing <run-dir> [--budget <n>] [--max-files <n>]\n')
125
+ return 2
126
+ }
127
+ const numFlag = (name: string): number | undefined | null => {
128
+ const idx = args.indexOf(name)
129
+ if (idx === -1) return undefined
130
+ const n = Number(args[idx + 1])
131
+ if (!Number.isFinite(n) || n <= 0) {
132
+ process.stderr.write(`shard: invalid ${name} ${args[idx + 1]} (want a positive number)\n`)
133
+ return null
134
+ }
135
+ return n
136
+ }
137
+ const budget = numFlag('--budget')
138
+ if (budget === null) return 2
139
+ const maxFiles = numFlag('--max-files')
140
+ if (maxFiles === null) return 2
141
+ // `shardDiff` tolerates a missing diff.patch (setup calls it before the diff
142
+ // can exist) and mkdirs `shards/` on the way, so a typo'd run directory would
143
+ // otherwise be invented on disk and reported as a successful zero-shard split.
144
+ const { stat } = await import('node:fs/promises')
145
+ const isRunDir = await stat(runDir)
146
+ .then((s) => s.isDirectory())
147
+ .catch(() => false)
148
+ if (!isRunDir) {
149
+ process.stderr.write(`shard: no such run directory: ${runDir}\n`)
150
+ return 2
151
+ }
152
+ const { shardDiff } = await import('../scripts/shard.ts')
153
+ const manifest = await shardDiff(runDir, {
154
+ ...(budget !== undefined ? { budget } : {}),
155
+ ...(maxFiles !== undefined ? { maxFiles } : {}),
156
+ })
157
+ process.stdout.write(
158
+ `${manifest.shards.length} shard(s), ${manifest.totalFiles} files, ${manifest.totalLines} patch lines\n`,
159
+ )
160
+ return 0
161
+ },
119
162
  render: async (args) => {
120
163
  const runDir = args[0]
121
164
  const page = args[1]
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # Fake gh whose `pr diff` refuses to serve, the way GitHub does above ~300
3
+ # files. MAGPIE_FAKE_HEAD_OID lets a test point pr.json at a real local commit.
4
+ # MAGPIE_FAKE_DIFF_MODE=fail (default) exits non-zero; =empty exits 0 with no
5
+ # output, which is the truncation case fetchPr also has to catch.
6
+ case "$1 $2" in
7
+ "pr view")
8
+ cat <<JSON
9
+ {
10
+ "number": 7,
11
+ "title": "Fake big PR",
12
+ "headRefName": "feature-x",
13
+ "baseRefName": "main",
14
+ "headRefOid": "${MAGPIE_FAKE_HEAD_OID:-deadbeefdeadbeefdeadbeefdeadbeefdeadbeef}",
15
+ "baseRefOid": "cafebabecafebabecafebabecafebabecafebabe",
16
+ "author": { "login": "octocat" },
17
+ "body": "Fake body",
18
+ "url": "https://github.com/octocat/Hello-World/pull/7",
19
+ "files": [
20
+ { "path": "a.ts", "additions": 1, "deletions": 1, "changeType": "modified" }
21
+ ],
22
+ "commits": [],
23
+ "closingIssuesReferences": []
24
+ }
25
+ JSON
26
+ ;;
27
+ "pr diff")
28
+ if [ "${MAGPIE_FAKE_DIFF_MODE:-fail}" = "empty" ]; then
29
+ exit 0
30
+ fi
31
+ echo "the diff exceeded the maximum number of files (300) (HTTP 406)" >&2
32
+ exit 1
33
+ ;;
34
+ *)
35
+ echo "fake-gh-nodiff: unsupported args: $*" >&2
36
+ exit 1
37
+ ;;
38
+ esac
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magpie",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,9 +1,14 @@
1
1
  # Peer-review prompt
2
2
 
3
- The agent substitutes the placeholders below and writes the result to `<run-dir>/peer-prompt.md`. Step 6 then feeds that prompt to the peer reviewer: `codex exec < <run-dir>/peer-prompt.md > <run-dir>/peer.out` when codex is available, or a Claude `general-purpose` subagent (with the `magpie-peer-review-claude-preamble` prepended) writing to `<run-dir>/peer.out` when it is not.
3
+ The agent substitutes the placeholders below and writes the result to `<run-dir>/peer-prompt.md`. Stage 7 then feeds that prompt to the peer reviewer: `codex exec < <run-dir>/peer-prompt.md > <run-dir>/peer.out` when codex is available, or a Claude `general-purpose` subagent (with the `magpie-peer-review-claude-preamble` prepended) writing to `<run-dir>/peer.out` when it is not.
4
4
 
5
5
  Either way, extract the fenced `review-peer-review` block from `peer.out` and save it to `<run-dir>/peer.json`.
6
6
 
7
+ The filenames above are the single-batch case. Stage 7 batches above 40 findings, and
8
+ those runs use `peer-prompt-<k>.md` and `peer-<k>.out` per batch, one reviewer
9
+ invocation each, with every batch's verdicts merged into one `peer.json` after the
10
+ last batch. Everything below is per batch and otherwise unchanged.
11
+
7
12
  ## Substitute before use
8
13
 
9
14
  Replace each `<<NAME>>` placeholder in the block below:
@@ -72,7 +77,7 @@ Rules:
72
77
 
73
78
  ## Claude peer-review preamble
74
79
 
75
- Used only by the Claude fallback path in step 6. Prepend this block verbatim (no substitutions) to the substituted `magpie-peer-review` prompt before dispatching the subagent. Its job is to buy back the independence you lose by using the same model family that produced the findings: the reviewer must re-derive each verdict from the diff rather than trusting the finding text, and must actively resist rubber-stamping.
80
+ Used only by the Claude fallback path in stage 7. Prepend this block verbatim (no substitutions) to the substituted `magpie-peer-review` prompt before dispatching the subagent. Its job is to buy back the independence you lose by using the same model family that produced the findings: the reviewer must re-derive each verdict from the diff rather than trusting the finding text, and must actively resist rubber-stamping.
76
81
 
77
82
  ````magpie-peer-review-claude-preamble
78
83
  You are a fresh, independent second-opinion reviewer. You have no memory of, and no stake in, how the findings below were produced. They were generated by other agents that share your model family, so they may carry the same blind spots you would: do not defer to them, and do not assume they are correct because they sound confident.
@@ -1,10 +1,12 @@
1
1
  # Specialist prompts
2
2
 
3
- Stage 4 of the walkthrough dispatches five subagents from this file. Build each prompt
4
- from up to five parts, in this order, and send it as the agent's entire task:
3
+ Stage 4 of the walkthrough dispatches five subagents per shard from this file. Build
4
+ each prompt from up to six parts, in this order, and send it as the agent's entire
5
+ task. Each part is a fenced block or the named section it points at; the prose around
6
+ them is instruction to you, not text for the specialist.
5
7
 
6
8
  1. The focus block for that focus (the fenced `magpie-specialist-<focus>` blocks below), verbatim.
7
- 2. The run header, with the two placeholders filled in:
9
+ 2. The run header, with the placeholders filled in. Unsharded:
8
10
 
9
11
  ```
10
12
  You are reviewing PR #<PR_NUMBER>.
@@ -12,11 +14,37 @@ Working directory: <RUN_DIR>/worktree
12
14
  Diff: <RUN_DIR>/diff.patch
13
15
  ```
14
16
 
17
+ When stage 4 is fanning this focus across shards, use instead:
18
+
19
+ ```
20
+ You are reviewing PR #<PR_NUMBER>.
21
+ Working directory: <RUN_DIR>/worktree
22
+ Shard: <n> of <N>
23
+ Diff: <RUN_DIR>/shards/shard-<n>.patch
24
+
25
+ This shard is your review scope. The other shards belong to other agents working
26
+ in parallel: do not review files outside your shard, and do not report findings
27
+ anchored to them. Reading any file in the worktree for context is expected and
28
+ encouraged.
29
+ ```
30
+
15
31
  3. The `## Output Contract` section below, verbatim.
16
- 4. The `magpie-codebase-intelligence` block below, verbatim, **only** when the context
32
+ 4. The excluded-files block below, verbatim, when `<RUN_DIR>/excluded-files.json`
33
+ exists (setup writes it only when the path filter removed something, and without
34
+ it the two paths the block names do not exist):
35
+
36
+ ```
37
+ Files excluded by the path filter are absent from your diff but present in the
38
+ worktree, with their full patches in <RUN_DIR>/diff.full.patch and the exclusion
39
+ list in <RUN_DIR>/excluded-files.json. When a change in your scope implies a change
40
+ in an excluded file (a migration implies a model snapshot, a schema change implies
41
+ generated types), open it and cross-check rather than treating it as out of scope.
42
+ ```
43
+
44
+ 5. The `magpie-codebase-intelligence` block below, verbatim, **only** when the context
17
45
  stage logged `codeIntelligence: true`. Omit it entirely otherwise: telling a
18
46
  specialist to use tools it does not have wastes a turn per specialist on discovery.
19
- 5. The brief, when `<RUN_DIR>/brief.json` exists, rendered as:
47
+ 6. The brief, when `<RUN_DIR>/brief.json` exists, rendered as:
20
48
 
21
49
  ```
22
50
  ## What this PR is for
@@ -52,7 +80,10 @@ into the same generic pass. Do not paraphrase, summarise, or trim either one.
52
80
 
53
81
  ## Output Contract
54
82
 
55
- Write findings to <RUN_DIR>/findings/<focus>.json before returning. The file MUST be a JSON array. Each entry MUST conform to this schema exactly (no extra top-level keys, no renamed keys):
83
+ Write findings to <RUN_DIR>/findings/<focus>.json before returning, or to
84
+ <RUN_DIR>/findings/<focus>.shard-<n>.json when your run header names a shard. The file
85
+ MUST be a JSON array. Each entry MUST conform to this schema exactly (no extra
86
+ top-level keys, no renamed keys):
56
87
 
57
88
  ```
58
89
  {
@@ -114,7 +145,7 @@ If you have no findings, write []. Return as your final tool result a single lin
114
145
 
115
146
  ## Codebase intelligence
116
147
 
117
- Include this block as part 4 only when the context stage logged `codeIntelligence: true`.
148
+ Include this block as part 5 only when the context stage logged `codeIntelligence: true`.
118
149
 
119
150
  ```magpie-codebase-intelligence
120
151
  ## Codebase intelligence
@@ -1,5 +1,6 @@
1
1
  import { expect, test } from 'bun:test'
2
- import { mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises'
2
+ import { existsSync } from 'node:fs'
3
+ import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises'
3
4
  import { tmpdir } from 'node:os'
4
5
  import { join } from 'node:path'
5
6
 
@@ -274,3 +275,102 @@ test('top-level --help surfaces the preview subcommand', async () => {
274
275
  expect(stdout).toContain('preview')
275
276
  expect(stdout).toContain('--help-preview')
276
277
  })
278
+
279
+ test('shard rejects missing <run-dir>', async () => {
280
+ const proc = Bun.spawn(['bun', CLI, 'shard'], { stdout: 'pipe', stderr: 'pipe' })
281
+ const stderr = await new Response(proc.stderr).text()
282
+ const exit = await proc.exited
283
+ expect(exit).toBe(2)
284
+ expect(stderr).toContain('shard: missing <run-dir>')
285
+ })
286
+
287
+ test('shard rejects a zero, negative, or non-numeric --budget, naming the flag and value', async () => {
288
+ for (const bad of ['0', '-5', 'abc']) {
289
+ const proc = Bun.spawn(['bun', CLI, 'shard', '/tmp/does-not-matter', '--budget', bad], {
290
+ stdout: 'pipe',
291
+ stderr: 'pipe',
292
+ })
293
+ const stderr = await new Response(proc.stderr).text()
294
+ const exit = await proc.exited
295
+ expect(exit).toBe(2)
296
+ expect(stderr).toContain(`shard: invalid --budget ${bad}`)
297
+ }
298
+ })
299
+
300
+ test('shard rejects an invalid --max-files, naming the flag and value', async () => {
301
+ const proc = Bun.spawn(['bun', CLI, 'shard', '/tmp/does-not-matter', '--max-files', '-1'], {
302
+ stdout: 'pipe',
303
+ stderr: 'pipe',
304
+ })
305
+ const stderr = await new Response(proc.stderr).text()
306
+ const exit = await proc.exited
307
+ expect(exit).toBe(2)
308
+ expect(stderr).toContain('shard: invalid --max-files -1')
309
+ })
310
+
311
+ test('shard rejects a run directory that does not exist instead of creating it', async () => {
312
+ const missing = join(tmpdir(), `magpie-cli-no-run-${Date.now()}`, 'nested')
313
+ const proc = Bun.spawn(['bun', CLI, 'shard', missing], { stdout: 'pipe', stderr: 'pipe' })
314
+ const stdout = await new Response(proc.stdout).text()
315
+ const stderr = await new Response(proc.stderr).text()
316
+ const exit = await proc.exited
317
+ expect(exit).toBe(2)
318
+ expect(stderr).toContain(`shard: no such run directory: ${missing}`)
319
+ // The old behaviour mkdir -p'd the typo and reported success.
320
+ expect(stdout).not.toContain('shard(s)')
321
+ expect(existsSync(missing)).toBe(false)
322
+ })
323
+
324
+ test('--help shows the shard budget flags', async () => {
325
+ const proc = Bun.spawn(['bun', CLI, '--help'], { stdout: 'pipe' })
326
+ const stdout = await new Response(proc.stdout).text()
327
+ expect(await proc.exited).toBe(0)
328
+ expect(stdout).toContain('shard <run-dir> [--budget N] [--max-files N]')
329
+ })
330
+
331
+ test('shard with a custom --budget and --max-files reaches shardDiff with those values', async () => {
332
+ const runDir = await mkdtemp(join(tmpdir(), 'magpie-cli-shard-'))
333
+ try {
334
+ // Two files under the same two-segment directory group ('src'), each with
335
+ // its own diff --git block, so a --max-files 1 cap forces the group apart
336
+ // into two shards regardless of --budget: proof the flags actually reach
337
+ // shardDiff rather than the defaults (budget 6000, max-files 80) taking
338
+ // over silently.
339
+ const diff = [
340
+ 'diff --git a/src/a.ts b/src/a.ts',
341
+ 'index 0000000..1111111 100644',
342
+ '--- a/src/a.ts',
343
+ '+++ b/src/a.ts',
344
+ '@@ -1 +1 @@',
345
+ '-x',
346
+ '+y',
347
+ 'diff --git a/src/b.ts b/src/b.ts',
348
+ 'index 0000000..1111111 100644',
349
+ '--- a/src/b.ts',
350
+ '+++ b/src/b.ts',
351
+ '@@ -1 +1 @@',
352
+ '-x',
353
+ '+y',
354
+ '',
355
+ ].join('\n')
356
+ await writeFile(join(runDir, 'diff.patch'), diff)
357
+
358
+ const proc = Bun.spawn(['bun', CLI, 'shard', runDir, '--budget', '3', '--max-files', '1'], {
359
+ stdout: 'pipe',
360
+ stderr: 'pipe',
361
+ })
362
+ const stdout = await new Response(proc.stdout).text()
363
+ const exit = await proc.exited
364
+ expect(exit).toBe(0)
365
+ expect(stdout).toContain('2 shard(s)')
366
+
367
+ const manifest = JSON.parse(
368
+ await readFile(join(runDir, 'shards', 'manifest.json'), 'utf8'),
369
+ ) as { budget: number; maxFiles: number; shards: unknown[] }
370
+ expect(manifest.budget).toBe(3)
371
+ expect(manifest.maxFiles).toBe(1)
372
+ expect(manifest.shards).toHaveLength(2)
373
+ } finally {
374
+ await rm(runDir, { recursive: true, force: true })
375
+ }
376
+ })