@iceinvein/agent-skills 0.22.0 → 0.23.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 (88) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/skills/bug-hunter/SKILL.md +5 -0
  4. package/skills/bug-hunter/evals/README.md +6 -1
  5. package/skills/bug-hunter/evals/callers-tree-unchanged/graders/output-ignores-itself.md +11 -0
  6. package/skills/bug-hunter/evals/rules.json +8 -0
  7. package/skills/bug-hunter/skill.json +1 -1
  8. package/skills/index.json +4 -4
  9. package/skills/magpie/README.md +5 -3
  10. package/skills/magpie/SKILL.md +40 -15
  11. package/skills/magpie/bin/magpie.ts +83 -7
  12. package/skills/magpie/evals/README.md +7 -4
  13. package/skills/magpie/evals/local-mode-hands-back/case.yaml +4 -0
  14. package/skills/magpie/evals/local-mode-hands-back/fixture.sh +300 -0
  15. package/skills/magpie/evals/local-mode-hands-back/graders/branch-was-reachable.md +5 -0
  16. package/skills/magpie/evals/local-mode-hands-back/graders/cleanup-after-handoff.md +6 -0
  17. package/skills/magpie/evals/local-mode-hands-back/graders/handoff-was-run.md +6 -0
  18. package/skills/magpie/evals/local-mode-hands-back/graders/nothing-served-or-posted.md +7 -0
  19. package/skills/magpie/evals/local-mode-hands-back/graders/reply-lists-both-findings.md +10 -0
  20. package/skills/magpie/evals/local-mode-hands-back/graders/skill-fired.md +5 -0
  21. package/skills/magpie/evals/local-mode-hands-back/prompt.md +11 -0
  22. package/skills/magpie/evals/quality/README.md +73 -2
  23. package/skills/magpie/evals/quality/__tests__/recall.test.ts +234 -0
  24. package/skills/magpie/evals/quality/__tests__/replay-full.test.ts +164 -0
  25. package/skills/magpie/evals/quality/corpus.ts +4 -1
  26. package/skills/magpie/evals/quality/recall.ts +249 -0
  27. package/skills/magpie/evals/quality/replay-critic.ts +3 -0
  28. package/skills/magpie/evals/quality/replay-full.ts +168 -6
  29. package/skills/magpie/evals/rules.json +9 -1
  30. package/skills/magpie/references/checks.md +78 -0
  31. package/skills/magpie/references/peer-review.md +7 -0
  32. package/skills/magpie/references/scout.md +12 -5
  33. package/skills/magpie/references/specialists.md +10 -8
  34. package/skills/magpie/scripts/__tests__/cleanup-cmd.test.ts +94 -0
  35. package/skills/magpie/scripts/__tests__/critic-cmd.test.ts +18 -0
  36. package/skills/magpie/scripts/__tests__/dedupe-cmd.test.ts +80 -1
  37. package/skills/magpie/scripts/__tests__/handoff-cmd.test.ts +319 -0
  38. package/skills/magpie/scripts/__tests__/housekeeping-cmd.test.ts +14 -0
  39. package/skills/magpie/scripts/__tests__/local-diff.test.ts +275 -0
  40. package/skills/magpie/scripts/__tests__/pipeline-e2e.test.ts +1 -0
  41. package/skills/magpie/scripts/__tests__/resolve-run.test.ts +28 -0
  42. package/skills/magpie/scripts/__tests__/server.test.ts +30 -1
  43. package/skills/magpie/scripts/__tests__/setup-cmd.test.ts +324 -1
  44. package/skills/magpie/scripts/__tests__/skill-lint.test.ts +8 -5
  45. package/skills/magpie/scripts/__tests__/status-cmd.test.ts +44 -0
  46. package/skills/magpie/scripts/__tests__/target.test.ts +84 -0
  47. package/skills/magpie/scripts/__tests__/types.test.ts +2 -1
  48. package/skills/magpie/scripts/cleanup-cmd.ts +5 -1
  49. package/skills/magpie/scripts/critic-cmd.ts +2 -1
  50. package/skills/magpie/scripts/dedupe-cmd.ts +15 -5
  51. package/skills/magpie/scripts/handoff-cmd.ts +132 -0
  52. package/skills/magpie/scripts/housekeeping-cmd.ts +7 -5
  53. package/skills/magpie/scripts/local-diff.ts +228 -0
  54. package/skills/magpie/scripts/post-cmd.ts +1 -0
  55. package/skills/magpie/scripts/render-annotation.ts +1 -0
  56. package/skills/magpie/scripts/resolve-run.ts +17 -3
  57. package/skills/magpie/scripts/server.ts +9 -1
  58. package/skills/magpie/scripts/setup-cmd.ts +241 -49
  59. package/skills/magpie/scripts/status-cmd.ts +22 -6
  60. package/skills/magpie/scripts/target.ts +56 -0
  61. package/skills/magpie/scripts/types.ts +1 -0
  62. package/skills/magpie/skill.json +2 -2
  63. package/skills/sluice/SKILL.md +3 -1
  64. package/skills/sluice/evals/README.md +10 -2
  65. package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/case.yaml +4 -0
  66. package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/fixture.sh +108 -0
  67. package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/deploy-names-its-steps.md +9 -0
  68. package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/kept-status-by-changing-code.md +12 -0
  69. package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/sluice-fired.md +5 -0
  70. package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/graders/status-test-unchanged.md +6 -0
  71. package/skills/sluice/evals/fast-leaves-an-unrelated-red-test-alone/prompt.md +11 -0
  72. package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/case.yaml +4 -0
  73. package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/fixture.sh +73 -0
  74. package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/proved-after-the-last-edit.md +18 -0
  75. package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/sluice-fired.md +5 -0
  76. package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/test-expects-the-new-line.md +5 -0
  77. package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/graders/updated-without-asking.md +11 -0
  78. package/skills/sluice/evals/fast-updates-the-test-the-change-replaces/prompt.md +11 -0
  79. package/skills/sluice/evals/rules.json +11 -0
  80. package/skills/sluice/references/review.md +6 -4
  81. package/skills/sluice/references/test-first.md +24 -4
  82. package/skills/sluice/scripts/content-digest.sh +1 -1
  83. package/skills/sluice/scripts/run-stats.sh +14 -0
  84. package/skills/sluice/scripts/session-start.sh +44 -2
  85. package/skills/sluice/scripts/status.sh +293 -0
  86. package/skills/sluice/scripts/stop-guard.sh +80 -6
  87. package/skills/sluice/scripts/test-changes.sh +135 -0
  88. package/skills/sluice/skill.json +1 -1
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env bash
2
+ # A local review of the workspace's own branch, done through peer review. The
3
+ # local ladder ends at handoff: print the findings to the caller, archive the
4
+ # run, end the turn. There is no PR, so nothing is served, rendered or posted.
5
+ #
6
+ # The workspace is the repo under review, because local mode reviews the
7
+ # caller's working directory in place. The run directory sits under the
8
+ # workspace rather than ~/.magpie because file graders refuse to follow a link
9
+ # out of the workspace. `magpie --list-runs` is what names the path a resume
10
+ # uses, so the shim reports this one.
11
+ set -euo pipefail
12
+
13
+ BRANCH="feat/tenant-settings-cache"
14
+ RUN_ID="local-feat-tenant-settings-cache-1789600000"
15
+ RUN_DIR="$PWD/runs/$RUN_ID"
16
+ ARCHIVE_DIR="$PWD/archive"
17
+ CALLS="$PWD/.magpie-calls.log"
18
+
19
+ mkdir -p "$RUN_DIR"/findings "$HOME/shims"
20
+ : > "$CALLS"
21
+
22
+ cat > "$HOME/shim-config" <<EOF
23
+ RUN_ID="$RUN_ID"
24
+ RUN_DIR="$RUN_DIR"
25
+ ARCHIVE_DIR="$ARCHIVE_DIR"
26
+ CALLS="$CALLS"
27
+ EOF
28
+
29
+ # The real magpie and gh are outside the eval sandbox and cannot be executed
30
+ # from inside it, so the child gets fakes rather than exit 126. The gh fake is
31
+ # here to catch a post that goes around the CLI: a local run has no PR at all.
32
+ mkdir -p "$HOME/tmp"
33
+ cat > "$HOME/.zshenv" <<'RC'
34
+ export PATH="$HOME/shims:/usr/bin:/bin:/usr/sbin:/sbin"
35
+ # /usr/bin/python3 is the Xcode shim, and without a writable TMPDIR it fails
36
+ # trying to create its xcrun cache in a directory the sandbox blocks.
37
+ export TMPDIR="$HOME/tmp"
38
+ RC
39
+
40
+ # Unlike the PR cases' fakes, status reads target.json and picks the ladder the
41
+ # way scripts/status-cmd.ts does, so this run resumes at handoff, not report.
42
+ cat > "$HOME/shims/magpie" <<'SHIM'
43
+ #!/usr/bin/env bash
44
+ . "$HOME/shim-config"
45
+ echo "magpie $*" >> "$CALLS"
46
+ case "${1:-}" in
47
+ --list-runs)
48
+ if [ -d "$RUN_DIR" ]; then
49
+ printf '%s\tactive\t%s\n' "$RUN_ID" "$RUN_DIR"
50
+ else
51
+ printf '%s\tarchived\t%s\n' "$RUN_ID" "$ARCHIVE_DIR/$RUN_ID"
52
+ fi
53
+ ;;
54
+ status)
55
+ python3 - "${2:-$RUN_DIR}" <<'STATUS'
56
+ import json, pathlib, sys
57
+
58
+ run = pathlib.Path(sys.argv[1])
59
+ PR = ['setup', 'context', 'specialists', 'dedupe', 'critic', 'peer-review', 'report', 'post']
60
+ LOCAL = ['setup', 'context', 'specialists', 'dedupe', 'critic', 'peer-review', 'handoff']
61
+ target = run / 'target.json'
62
+ mode = json.loads(target.read_text())['mode'] if target.exists() else 'pr'
63
+ ORDER = LOCAL if mode == 'local' else PR
64
+ last, error = None, None
65
+ for line in (run / 'log.jsonl').read_text().splitlines():
66
+ if not line.strip():
67
+ continue
68
+ try:
69
+ entry = json.loads(line)
70
+ except ValueError:
71
+ continue
72
+ if entry.get('status') == 'error':
73
+ error = entry.get('stage')
74
+ break
75
+ if entry.get('status') in ('done', 'skipped') and entry.get('stage') in ORDER:
76
+ last = entry['stage']
77
+ index = ORDER.index(last) + 1 if last else 0
78
+ print(json.dumps({'lastCompleted': last, 'next': ORDER[index] if index < len(ORDER) else 'cleanup', 'error': error}))
79
+ STATUS
80
+ ;;
81
+ target)
82
+ python3 -c 'import json, sys; print(json.load(open(sys.argv[1]))["root"])' "${2:-$RUN_DIR}/target.json"
83
+ ;;
84
+ handoff)
85
+ python3 - "${2:-$RUN_DIR}" <<'HANDOFF'
86
+ import json, pathlib, sys, time
87
+
88
+ run = pathlib.Path(sys.argv[1])
89
+ if json.loads((run / 'target.json').read_text())['mode'] != 'local':
90
+ sys.stderr.write(f'magpie: handoff only applies to local runs; {run} is a PR run\n')
91
+ sys.exit(2)
92
+ for name in ('findings.final.json', 'local.json'):
93
+ if not (run / name).exists():
94
+ sys.stderr.write(f'magpie: handoff: {run / name} is missing\n')
95
+ sys.exit(4)
96
+ findings = json.loads((run / 'findings.final.json').read_text())
97
+ meta = json.loads((run / 'local.json').read_text())
98
+ subject = 'detached HEAD' if meta['branch'] is None else f"local branch {meta['branch']}"
99
+ # The labels the fixture's findings use, bolded as formatFindingDescriptionMarkdown does.
100
+ LABELS = ('Observation', 'Why it matters', 'Suggested direction')
101
+
102
+
103
+ def markdown(description):
104
+ paragraphs = description.split('\n\n')
105
+ for label in LABELS:
106
+ paragraphs = [f'**{label}:**' + p[len(label) + 1:] if p.startswith(f'{label}:') else p for p in paragraphs]
107
+ return '\n\n'.join(paragraphs)
108
+
109
+
110
+ rank = {'must-fix': 0, 'should-fix': 1}
111
+ actionable = sorted((f for f in findings if f['risk']['action'] in rank), key=lambda f: rank[f['risk']['action']])
112
+ optional = [f for f in findings if f['risk']['action'] not in rank]
113
+ counts = {a: sum(f['risk']['action'] == a for f in findings) for a in rank}
114
+ sections = [
115
+ f"# magpie local review: {subject} against {meta['baseRef']}",
116
+ f"{counts['must-fix']} must-fix, {counts['should-fix']} should-fix, {len(optional)} optional",
117
+ ]
118
+ for n, f in enumerate(actionable + optional, 1):
119
+ anchor = f['file'] if f.get('line') is None else f"{f['file']}:{f['line']}"
120
+ sections.append(f"{n}. [{f['risk']['action']}] {anchor}: {f['title']}\n\n{markdown(f['description'])}")
121
+ sections.append('Address every must-fix and should-fix, or say why not. The rest are optional.')
122
+ print('\n\n'.join(sections))
123
+ with (run / 'log.jsonl').open('a') as log:
124
+ log.write(json.dumps({'stage': 'handoff', 'status': 'done', 'findings': len(findings), 'ts': int(time.time() * 1000)}) + '\n')
125
+ HANDOFF
126
+ ;;
127
+ cleanup)
128
+ run="${2:-$RUN_DIR}"
129
+ mkdir -p "$ARCHIVE_DIR"
130
+ dest="$ARCHIVE_DIR/$(basename "$run")"
131
+ mv "$run" "$dest"
132
+ echo "archived to $dest"
133
+ ;;
134
+ serve|render|post) echo "fake magpie: $1 reached the CLI on a local run" ;;
135
+ *) echo "fake magpie: unsupported subcommand: $*" >&2; exit 64 ;;
136
+ esac
137
+ SHIM
138
+ chmod +x "$HOME/shims/magpie"
139
+
140
+ cat > "$HOME/shims/gh" <<'SHIM'
141
+ #!/usr/bin/env bash
142
+ . "$HOME/shim-config"
143
+ echo "gh $*" >> "$CALLS"
144
+ echo "fake gh: no request was made" >&2
145
+ exit 1
146
+ SHIM
147
+ chmod +x "$HOME/shims/gh"
148
+
149
+ # The repo under review: main holds the cache without a reader, the branch adds
150
+ # put/get and a cached load. The run's own files stay out of the diff.
151
+ export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
152
+ git init -q -b main .
153
+ git config commit.gpgsign false
154
+ printf 'runs/\narchive/\n.magpie-calls.log\n' >> .git/info/exclude
155
+ export GIT_AUTHOR_NAME="Asha Platform" GIT_AUTHOR_EMAIL="asha@example.com"
156
+ export GIT_COMMITTER_NAME="Asha Platform" GIT_COMMITTER_EMAIL="asha@example.com"
157
+ mkdir -p src/settings
158
+
159
+ cat > src/settings/cache.ts <<'TS'
160
+ const store = new Map<string, Settings>()
161
+
162
+ export function clear() {
163
+ store.clear()
164
+ }
165
+ TS
166
+
167
+ cat > src/settings/loader.ts <<'TS'
168
+ type Settings = { theme: string }
169
+
170
+ async function fetchSettings(tenantId: string): Promise<Settings> {
171
+ return { theme: 'default' }
172
+ }
173
+
174
+ export async function load(tenantId: string) {
175
+ return fetchSettings(tenantId)
176
+ }
177
+ TS
178
+
179
+ git add src
180
+ git commit -q -m "feat: tenant settings loader"
181
+ git update-ref refs/remotes/origin/main HEAD
182
+ git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main
183
+ MERGE_BASE=$(git rev-parse HEAD)
184
+
185
+ git checkout -q -b "$BRANCH"
186
+
187
+ cat > src/settings/cache.ts <<'TS'
188
+ const store = new Map<string, Settings>()
189
+
190
+ export function put(tenantId: string, settings: Settings) {
191
+ store.set(tenantId, settings)
192
+ }
193
+
194
+ export function get(tenantId: string): Settings | undefined {
195
+ return store.get(tenantId)
196
+ }
197
+
198
+ export function clear() {
199
+ store.clear()
200
+ }
201
+ TS
202
+
203
+ cat > src/settings/loader.ts <<'TS'
204
+ import { get, put } from './cache'
205
+
206
+ type Settings = { theme: string }
207
+
208
+ async function fetchSettings(tenantId: string): Promise<Settings> {
209
+ return { theme: 'default' }
210
+ }
211
+
212
+ export async function load(tenantId: string) {
213
+ const hit = get(tenantId)
214
+ if (hit) return hit
215
+ const fresh = await fetchSettings(tenantId)
216
+ put(tenantId, fresh)
217
+ return fresh
218
+ }
219
+ TS
220
+
221
+ git add src
222
+ git commit -q -m "feat: cache tenant settings in the request path" \
223
+ -m "Settings are read on every request; keep them in memory per tenant."
224
+ HEAD_OID=$(git rev-parse HEAD)
225
+ git diff --no-color --no-ext-diff "$MERGE_BASE" > "$RUN_DIR/diff.patch"
226
+ DIFF_SHA=$(shasum -a 256 "$RUN_DIR/diff.patch" | cut -d' ' -f1)
227
+
228
+ cat > "$RUN_DIR/target.json" <<JSON
229
+ { "mode": "local", "root": "$PWD" }
230
+ JSON
231
+
232
+ cat > "$RUN_DIR/local.json" <<JSON
233
+ {
234
+ "branch": "$BRANCH",
235
+ "baseRef": "origin/HEAD",
236
+ "mergeBase": "$MERGE_BASE",
237
+ "head": "$HEAD_OID",
238
+ "dirty": false,
239
+ "author": "Asha Platform",
240
+ "commits": [
241
+ {
242
+ "oid": "$HEAD_OID",
243
+ "headline": "feat: cache tenant settings in the request path",
244
+ "body": "Settings are read on every request; keep them in memory per tenant."
245
+ }
246
+ ],
247
+ "intentFile": null
248
+ }
249
+ JSON
250
+
251
+ cat > "$RUN_DIR/diff-source.json" <<JSON
252
+ {
253
+ "source": "local",
254
+ "mergeBase": "$MERGE_BASE",
255
+ "baseRef": "origin/HEAD",
256
+ "head": "$HEAD_OID",
257
+ "diffSha256": "$DIFF_SHA"
258
+ }
259
+ JSON
260
+
261
+ cat > "$RUN_DIR/log.jsonl" <<'LOG'
262
+ {"stage":"preflight","status":"done","missingOptional":["codex"]}
263
+ {"stage":"setup","status":"done"}
264
+ {"stage":"context","status":"done"}
265
+ {"stage":"specialists","status":"done"}
266
+ {"stage":"dedupe","status":"done"}
267
+ {"stage":"critic","status":"running"}
268
+ {"stage":"critic","status":"done","kept":2,"dropped":0,"merged":0,"capped":0}
269
+ {"stage":"peer-review","status":"done","provider":"claude"}
270
+ LOG
271
+
272
+ # Each finding cites a line the branch added, with the evidence that line holds.
273
+ cat > "$RUN_DIR/findings.final.json" <<'JSON'
274
+ [
275
+ {
276
+ "id": "security-1",
277
+ "file": "src/settings/cache.ts",
278
+ "line": 4,
279
+ "evidence": "store.set(tenantId, settings)",
280
+ "severity": "high",
281
+ "risk": { "impact": "high", "likelihood": "likely", "confidence": "high", "action": "must-fix" },
282
+ "domain": "security",
283
+ "title": "Tenant settings cache is a process-global Map with no eviction",
284
+ "description": "Observation: put() writes into a module-level Map with no bound and no TTL.\n\nWhy it matters: a settings change never reaches the cached copy.\n\nSuggested direction: bound the map and give entries a TTL."
285
+ },
286
+ {
287
+ "id": "bugs-1",
288
+ "file": "src/settings/loader.ts",
289
+ "line": 12,
290
+ "evidence": "const fresh = await fetchSettings(tenantId)",
291
+ "severity": "medium",
292
+ "risk": { "impact": "medium", "likelihood": "possible", "confidence": "medium", "action": "should-fix" },
293
+ "domain": "bugs",
294
+ "title": "Concurrent loads for the same tenant each hit the network",
295
+ "description": "Observation: load() awaits fetchSettings before writing back.\n\nWhy it matters: N concurrent first requests produce N fetches.\n\nSuggested direction: cache the in-flight promise."
296
+ }
297
+ ]
298
+ JSON
299
+
300
+ echo '[]' > "$RUN_DIR/findings/tests.json"
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: regex
3
+ pattern: 'export function put\(tenantId'
4
+ target: { source: file, path: src/settings/cache.ts }
5
+ ---
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: regex
3
+ pattern: 'magpie handoff [\s\S]*magpie cleanup \S+'
4
+ target: { source: file, path: .magpie-calls.log }
5
+ weight: 2
6
+ ---
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: regex
3
+ pattern: 'magpie handoff \S+'
4
+ target: { source: file, path: .magpie-calls.log }
5
+ weight: 2
6
+ ---
@@ -0,0 +1,7 @@
1
+ ---
2
+ type: regex
3
+ pattern: 'magpie (serve|render|post)|(^|\n)gh '
4
+ match: not_contains
5
+ target: { source: file, path: .magpie-calls.log }
6
+ weight: 3
7
+ ---
@@ -0,0 +1,10 @@
1
+ ---
2
+ type: llm
3
+ focus: last_message
4
+ weight: 2
5
+ ---
6
+
7
+ This is a local review: the caller is an agent working on its own branch, and there is no PR, server or report page. The run has just printed its handoff and archived itself.
8
+
9
+ PASS if the reply gives the caller both findings by title, "Tenant settings cache is a process-global Map with no eviction" and "Concurrent loads for the same tenant each hit the network" (light rewording is fine), followed by a line of the form `Run archived at <path>`, and ends the turn there.
10
+ FAIL if either finding is missing, if the `Run archived at <path>` line is missing, if the reply offers to post, points at a report URL or page, asks the caller to tick or select findings, or starts fixing the code itself.
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: tool_used
3
+ tool: Skill
4
+ input_match: '"skill"\s*:\s*"(?:[\w-]+:)?magpie"'
5
+ ---
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: local-mode-hands-back
3
+ description: A local review of the current branch, resumed after peer review, hands the findings back as text, archives the run, and stops. There is no PR, so nothing is served, rendered or posted.
4
+ tags: [magpie, local, handoff, resume, scaffold]
5
+ max_turns: 25
6
+ timeout_seconds: 900
7
+ allowed_tools: [Read, Glob, Grep, Skill, Write, Edit, Bash, Task]
8
+ expected_outcome: Finds the active local run, runs magpie handoff, then magpie cleanup, and replies with both findings and the archive path. Never serves, renders, posts or calls gh.
9
+ ---
10
+
11
+ Peer review's finished on the magpie local review of this branch. Take it from there.
@@ -118,9 +118,12 @@ the worktree, so cost grows with candidate count. The results file records the
118
118
  ## Full replay
119
119
 
120
120
  ```
121
- bun evals/quality/replay-full.ts --corpus <runId> --repo <path-to-local-clone> [--max-cost-usd N]
121
+ bun evals/quality/replay-full.ts --corpus <runId> --repo <path-to-local-clone> [--max-cost-usd N] [--judge-model M]
122
122
  ```
123
123
 
124
+ Pass `--corpus` as a bare run id: a trailing slash empties the run id, and recall is then
125
+ skipped silently.
126
+
124
127
  Re-runs stages 3 to 6 (context, specialists, dedupe, critic) on the run's `diff.patch`:
125
128
 
126
129
  1. Scaffolds a scratch run dir: worktree at the PR head as above, `pr.json` and
@@ -132,10 +135,78 @@ Re-runs stages 3 to 6 (context, specialists, dedupe, critic) on the run's `diff.
132
135
  is written. Serve, peer review, report, post and cleanup are skipped.
133
136
  3. Matches the new kept findings to `findings.final.json` with `matchFindings` and scores
134
137
  them; unmatched findings count as `unlabelled` and stay out of precision. Writes
135
- `results/<ts>-full-<runId>.json`.
138
+ `results/<ts>-full-<runId>.json` with `recall`, `recallVerdicts`, `judgeModel` and
139
+ `judgeCostUsd` all null, plus `log`, `shardManifest`, `findingsFiles`, `dedupedCount`,
140
+ `criticDropped`, `evidenceDropped` and `thresholdDropped` read from the scratch run dir
141
+ before it is deleted. `shardCoverage` is derived from the manifest and the findings file
142
+ names: a shard counts as reviewed when any specialist wrote a file for it, so it overstates
143
+ a shard only some focuses reviewed. The printed line ends with `, shards <reviewed>/<total>`.
144
+ 4. When the run has a recall set (see [Recall](#recall)), judges the new kept findings
145
+ against it, scores them, and rewrites the same result file with those four fields
146
+ filled. The printed line gains `, missed-recall <caught>/<misses>`. Without a recall
147
+ set the fields stay null and nothing extra is printed. A judge failure exits 1 and
148
+ names the result file, which already holds the scored replay.
136
149
 
137
150
  **Cost:** a whole review: a scout, five specialists per shard, and the critic batches.
138
151
  `--max-cost-usd` (default 10) is passed to Claude as `--max-budget-usd`, the CLI's own
139
152
  spend cap; `claude -p` has no `--max-cost-usd` flag. Hitting the cap stops the session,
140
153
  and the replay then fails without scoring, on Claude's error result or on the missing
141
154
  `findings.kept.json`.
155
+
156
+ ## Recall
157
+
158
+ The labels above only score findings magpie raised. The recall set scores the other side:
159
+ defects a human found in a reviewed PR that no kept finding raised. It lives at
160
+ `~/.magpie/corpus/misses/<runId>.json`, outside the repo for the same reason as the
161
+ corpus, and is written by hand; corpus rebuilds do not touch it.
162
+
163
+ Each file is a JSON array of misses:
164
+
165
+ ```json
166
+ {
167
+ "id": "miss-1",
168
+ "file": "src/handler.ts",
169
+ "line": 42,
170
+ "title": "Retry loop never gives up",
171
+ "summary": "What the defect is and why it matters.",
172
+ "ownerDomain": "architecture",
173
+ "bugClass": "spec/intent mismatch",
174
+ "outsideDiff": false,
175
+ "preExisting": false,
176
+ "source": "https://github.com/<owner>/<repo>/pull/50#discussion_r123"
177
+ }
178
+ ```
179
+
180
+ `ownerDomain` is the specialist domain that should have caught it, `outsideDiff` marks a
181
+ defect in code the diff did not touch, `preExisting` one that predates the PR, and
182
+ `source` is the reviewer comment URL or the fixing commit. `line` may be null. Every
183
+ other field is required; a missing or mistyped one fails `loadMisses`, naming the file
184
+ and index.
185
+
186
+ The set was seeded from the 2026-10-17 miss analysis, which collected defects from three
187
+ sources per run: reviewer comments magpie had not raised, commits pushed after the review
188
+ that fixed something, and follow-up fix PRs.
189
+
190
+ `replay-full.ts` hands the misses and the replay's kept findings to a judge: one
191
+ `claude -p` with tools and MCP servers turned off, which names, for each miss, the kept
192
+ finding that describes the same defect wherever it is anchored, or null. User settings
193
+ still load, so `~/.claude/CLAUDE.md` and hooks reach the judge as they would any session.
194
+ `scoreRecall` turns the verdicts into `caught` over `misses`, broken down by owner
195
+ domain and bug class, plus the same figure excluding pre-existing defects. The judge model
196
+ is pinned to `claude-sonnet-5-5` (`DEFAULT_JUDGE_MODEL` in `recall.ts`) so scores stay
197
+ comparable across replays; `--judge-model` overrides it.
198
+
199
+ **Cost:** one tool-less call per replay, small next to the replay itself. The result file
200
+ records it as `judgeCostUsd`.
201
+
202
+ **Confounds:**
203
+
204
+ - **Small set.** 30 misses across 6 runs, so one verdict moves a run's figure a lot.
205
+ - **PR 50 dominates.** Its run holds 14 of the 30, so pooled recall is mostly PR 50's.
206
+ - **Judge nondeterminism.** The same findings can draw different verdicts on another call,
207
+ most often on partial overlaps. Treat a change of one or two caught misses as noise.
208
+ - **Replay coverage.** Only `pr-50-1789099641` and `pr-70-1790391863` can be replayed
209
+ here, which covers 19 of the 30 misses. `pr-46` and `pr-38` have no corpus run, since
210
+ nothing was posted on them and so they have no labels; their misses can never be scored
211
+ by a replay (and `pr-38` has no local clone either). `pr-231` and `pr-232` are corpus
212
+ runs, but their repo (INX-Software/WorkforceManagement) is not cloned here.