@iceinvein/agent-skills 0.20.0 → 0.21.1

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 (139) hide show
  1. package/dist/cli/index.js +6 -2
  2. package/package.json +3 -2
  3. package/skills/index.json +2 -2
  4. package/skills/magpie/README.md +1 -2
  5. package/skills/magpie/SKILL.md +7 -18
  6. package/skills/magpie/evals/README.md +150 -0
  7. package/skills/magpie/evals/codex-missing-falls-back/case.yaml +4 -0
  8. package/skills/magpie/evals/codex-missing-falls-back/fixture.sh +310 -0
  9. package/skills/magpie/evals/codex-missing-falls-back/graders/final-findings-were-written.md +6 -0
  10. package/skills/magpie/evals/codex-missing-falls-back/graders/kept-findings-were-reachable.md +5 -0
  11. package/skills/magpie/evals/codex-missing-falls-back/graders/missing-codex-is-not-an-error.md +7 -0
  12. package/skills/magpie/evals/codex-missing-falls-back/graders/peer-prompt-carries-the-preamble.md +6 -0
  13. package/skills/magpie/evals/codex-missing-falls-back/graders/provider-logged-as-claude.md +6 -0
  14. package/skills/magpie/evals/codex-missing-falls-back/graders/skill-fired.md +5 -0
  15. package/skills/magpie/evals/codex-missing-falls-back/prompt.md +11 -0
  16. package/skills/magpie/evals/post-folds-selection-events/case.yaml +4 -0
  17. package/skills/magpie/evals/post-folds-selection-events/fixture.sh +290 -0
  18. package/skills/magpie/evals/post-folds-selection-events/graders/deselected-finding-was-left-alone.md +7 -0
  19. package/skills/magpie/evals/post-folds-selection-events/graders/events-were-reachable.md +5 -0
  20. package/skills/magpie/evals/post-folds-selection-events/graders/post-stage-logged-done.md +5 -0
  21. package/skills/magpie/evals/post-folds-selection-events/graders/posts-the-last-event-selection.md +6 -0
  22. package/skills/magpie/evals/post-folds-selection-events/graders/report-was-re-rendered-after-posting.md +5 -0
  23. package/skills/magpie/evals/post-folds-selection-events/graders/skill-fired.md +5 -0
  24. package/skills/magpie/evals/post-folds-selection-events/prompt.md +11 -0
  25. package/skills/magpie/evals/report-ends-the-turn/case.yaml +4 -0
  26. package/skills/magpie/evals/report-ends-the-turn/fixture.sh +246 -0
  27. package/skills/magpie/evals/report-ends-the-turn/graders/findings-report-was-rendered.md +6 -0
  28. package/skills/magpie/evals/report-ends-the-turn/graders/findings-were-reachable.md +5 -0
  29. package/skills/magpie/evals/report-ends-the-turn/graders/hands-back-for-selection.md +10 -0
  30. package/skills/magpie/evals/report-ends-the-turn/graders/nothing-was-posted.md +7 -0
  31. package/skills/magpie/evals/report-ends-the-turn/graders/report-stage-logged-done.md +6 -0
  32. package/skills/magpie/evals/report-ends-the-turn/graders/skill-fired.md +5 -0
  33. package/skills/magpie/evals/report-ends-the-turn/prompt.md +11 -0
  34. package/skills/magpie/evals/resume-finds-active-run/case.yaml +4 -0
  35. package/skills/magpie/evals/resume-finds-active-run/fixture.sh +234 -0
  36. package/skills/magpie/evals/resume-finds-active-run/graders/existing-runs-were-checked.md +5 -0
  37. package/skills/magpie/evals/resume-finds-active-run/graders/no-fresh-run-was-started.md +7 -0
  38. package/skills/magpie/evals/resume-finds-active-run/graders/skill-fired.md +5 -0
  39. package/skills/magpie/evals/resume-finds-active-run/graders/surfaces-the-interrupted-run.md +10 -0
  40. package/skills/magpie/evals/resume-finds-active-run/prompt.md +11 -0
  41. package/skills/magpie/evals/shard-gate-stops-and-asks/case.yaml +4 -0
  42. package/skills/magpie/evals/shard-gate-stops-and-asks/fixture.sh +215 -0
  43. package/skills/magpie/evals/shard-gate-stops-and-asks/graders/asks-before-dispatching.md +16 -0
  44. package/skills/magpie/evals/shard-gate-stops-and-asks/graders/manifest-was-read.md +5 -0
  45. package/skills/magpie/evals/shard-gate-stops-and-asks/graders/no-findings-were-written.md +6 -0
  46. package/skills/magpie/evals/shard-gate-stops-and-asks/graders/no-specialist-was-dispatched.md +7 -0
  47. package/skills/magpie/evals/shard-gate-stops-and-asks/graders/skill-fired.md +5 -0
  48. package/skills/magpie/evals/shard-gate-stops-and-asks/prompt.md +11 -0
  49. package/skills/magpie/fixtures/example-pr/brief.json +0 -4
  50. package/skills/magpie/references/scout.md +2 -42
  51. package/skills/magpie/references/specialists.md +8 -115
  52. package/skills/magpie/scripts/__tests__/refresh.test.ts +0 -1
  53. package/skills/magpie/scripts/__tests__/render-cmd.test.ts +0 -1
  54. package/skills/magpie/scripts/__tests__/render-findings.test.ts +4 -27
  55. package/skills/magpie/scripts/__tests__/render-progress.test.ts +1 -1
  56. package/skills/magpie/scripts/__tests__/skill-lint.test.ts +19 -171
  57. package/skills/magpie/scripts/__tests__/types.test.ts +16 -9
  58. package/skills/magpie/scripts/render-findings.ts +0 -7
  59. package/skills/magpie/scripts/render-progress.ts +1 -1
  60. package/skills/magpie/scripts/types.ts +0 -16
  61. package/skills/magpie/skill.json +1 -1
  62. package/skills/magpie/templates/styles.css +0 -2
  63. package/skills/sluice/SKILL.md +20 -11
  64. package/skills/sluice/agents/sluice-implementer-low.md +31 -0
  65. package/skills/sluice/evals/README.md +218 -12
  66. package/skills/sluice/evals/announcement-reaches-the-ledger/case.yaml +4 -0
  67. package/skills/sluice/evals/announcement-reaches-the-ledger/fixture.sh +112 -0
  68. package/skills/sluice/evals/announcement-reaches-the-ledger/graders/escalates-fast-to-main.md +12 -0
  69. package/skills/sluice/evals/announcement-reaches-the-ledger/graders/sluice-fired.md +5 -0
  70. package/skills/sluice/evals/announcement-reaches-the-ledger/graders/suite-was-run.md +6 -0
  71. package/skills/sluice/evals/announcement-reaches-the-ledger/graders/verbose-flag-implemented.md +5 -0
  72. package/skills/sluice/evals/announcement-reaches-the-ledger/prompt.md +11 -0
  73. package/skills/sluice/evals/deep-plan-across-subsystems/fixture.sh +9 -1
  74. package/skills/sluice/evals/deep-plan-across-subsystems/graders/announces-deep-channel.md +5 -4
  75. package/skills/sluice/evals/deep-plan-asks-the-fork/case.yaml +4 -0
  76. package/skills/sluice/evals/deep-plan-asks-the-fork/fixture.sh +108 -0
  77. package/skills/sluice/evals/deep-plan-asks-the-fork/graders/announces-deep-channel.md +12 -0
  78. package/skills/sluice/evals/deep-plan-asks-the-fork/graders/asks-one-fork-question.md +20 -0
  79. package/skills/sluice/evals/deep-plan-asks-the-fork/graders/no-design-before-the-answer.md +8 -0
  80. package/skills/sluice/evals/deep-plan-asks-the-fork/graders/no-implementation-yet.md +10 -0
  81. package/skills/sluice/evals/deep-plan-asks-the-fork/graders/sluice-fired.md +5 -0
  82. package/skills/sluice/evals/deep-plan-asks-the-fork/prompt.md +11 -0
  83. package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/fixture.sh +26 -26
  84. package/skills/sluice/evals/deep-run-blocks-on-a-real-decision/prompt.md +1 -1
  85. package/skills/sluice/evals/deep-run-decides-a-non-blocking-choice/case.yaml +4 -0
  86. package/skills/sluice/evals/deep-run-decides-a-non-blocking-choice/fixture.sh +216 -0
  87. package/skills/sluice/evals/deep-run-decides-a-non-blocking-choice/graders/every-task-done.md +7 -0
  88. package/skills/sluice/evals/deep-run-decides-a-non-blocking-choice/graders/hands-back-not-a-decision-list.md +23 -0
  89. package/skills/sluice/evals/deep-run-decides-a-non-blocking-choice/graders/nothing-blocked.md +7 -0
  90. package/skills/sluice/evals/deep-run-decides-a-non-blocking-choice/graders/record-names-the-prefix.md +7 -0
  91. package/skills/sluice/evals/deep-run-decides-a-non-blocking-choice/graders/sluice-fired.md +5 -0
  92. package/skills/sluice/evals/deep-run-decides-a-non-blocking-choice/prompt.md +11 -0
  93. package/skills/sluice/evals/deep-run-fans-out/case.yaml +4 -0
  94. package/skills/sluice/evals/deep-run-fans-out/fixture.sh +250 -0
  95. package/skills/sluice/evals/deep-run-fans-out/graders/disjoint-tasks-in-one-message.md +15 -0
  96. package/skills/sluice/evals/deep-run-fans-out/graders/every-task-done.md +7 -0
  97. package/skills/sluice/evals/deep-run-fans-out/graders/implementers-dispatched.md +6 -0
  98. package/skills/sluice/evals/deep-run-fans-out/graders/sluice-fired.md +5 -0
  99. package/skills/sluice/evals/deep-run-fans-out/prompt.md +11 -0
  100. package/skills/sluice/evals/deep-run-finishes-every-task/fixture.sh +26 -26
  101. package/skills/sluice/evals/deep-run-finishes-every-task/graders/did-not-check-in-between-tasks.md +4 -0
  102. package/skills/sluice/evals/deep-run-survives-a-milestone/case.yaml +4 -0
  103. package/skills/sluice/evals/deep-run-survives-a-milestone/fixture.sh +310 -0
  104. package/skills/sluice/evals/deep-run-survives-a-milestone/graders/every-task-done.md +7 -0
  105. package/skills/sluice/evals/deep-run-survives-a-milestone/graders/hands-back-finished-work.md +19 -0
  106. package/skills/sluice/evals/deep-run-survives-a-milestone/graders/sluice-fired.md +5 -0
  107. package/skills/sluice/evals/deep-run-survives-a-milestone/graders/stop-hook-never-refused.md +9 -0
  108. package/skills/sluice/evals/deep-run-survives-a-milestone/prompt.md +11 -0
  109. package/skills/sluice/evals/deep-run-waits-for-a-running-agent/case.yaml +4 -0
  110. package/skills/sluice/evals/deep-run-waits-for-a-running-agent/fixture.sh +310 -0
  111. package/skills/sluice/evals/deep-run-waits-for-a-running-agent/graders/every-task-done.md +7 -0
  112. package/skills/sluice/evals/deep-run-waits-for-a-running-agent/graders/hands-back-after-the-last-result.md +23 -0
  113. package/skills/sluice/evals/deep-run-waits-for-a-running-agent/graders/sluice-fired.md +5 -0
  114. package/skills/sluice/evals/deep-run-waits-for-a-running-agent/graders/suite-was-run.md +9 -0
  115. package/skills/sluice/evals/deep-run-waits-for-a-running-agent/graders/t2-dispatched-with-label.md +6 -0
  116. package/skills/sluice/evals/deep-run-waits-for-a-running-agent/prompt.md +11 -0
  117. package/skills/sluice/evals/explicit-instruction-collapses-to-fast/graders/announces-fast-channel.md +6 -1
  118. package/skills/sluice/evals/fast-flag-on-existing-command/graders/announces-fast-channel.md +6 -1
  119. package/skills/sluice/evals/fast-reads-the-unmentioned-convention/case.yaml +4 -0
  120. package/skills/sluice/evals/fast-reads-the-unmentioned-convention/fixture.sh +149 -0
  121. package/skills/sluice/evals/fast-reads-the-unmentioned-convention/graders/help-test-lists-verbose.md +8 -0
  122. package/skills/sluice/evals/fast-reads-the-unmentioned-convention/graders/sluice-fired.md +5 -0
  123. package/skills/sluice/evals/fast-reads-the-unmentioned-convention/graders/suite-was-run.md +6 -0
  124. package/skills/sluice/evals/fast-reads-the-unmentioned-convention/graders/verbose-registered-in-flags-table.md +9 -0
  125. package/skills/sluice/evals/fast-reads-the-unmentioned-convention/prompt.md +11 -0
  126. package/skills/sluice/evals/main-new-interface/graders/announces-main-channel.md +5 -4
  127. package/skills/sluice/evals/main-new-interface/graders/shape-agreed-before-building.md +7 -2
  128. package/skills/sluice/references/deep-channel.md +107 -59
  129. package/skills/sluice/references/meter.md +8 -0
  130. package/skills/sluice/references/status.md +15 -8
  131. package/skills/sluice/scripts/plan.sh +31 -18
  132. package/skills/sluice/scripts/run-stats.sh +19 -3
  133. package/skills/sluice/scripts/status.sh +42 -14
  134. package/skills/sluice/scripts/stop-guard.sh +24 -11
  135. package/skills/sluice/skill.json +8 -2
  136. package/skills/sluice/evals/results/2026-09-20T01-51-28-540Z/aggregate-result.json +0 -105
  137. package/skills/sluice/evals/results/2026-09-20T01-51-28-540Z/report.html +0 -300
  138. package/skills/sluice/evals/results/2026-09-20T01-52-06-287Z/aggregate-result.json +0 -122
  139. package/skills/sluice/evals/results/2026-09-20T01-52-06-287Z/report.html +0 -324
@@ -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: codex-missing-falls-back
3
+ description: With codex off the machine, peer review still runs. A Claude subagent stands in, carrying the independence preamble, and the stage never logs an error.
4
+ tags: [magpie, peer-review, fallback, scaffold]
5
+ max_turns: 40
6
+ timeout_seconds: 1200
7
+ allowed_tools: [Read, Glob, Grep, Skill, Write, Edit, Bash, Task]
8
+ expected_outcome: Peer review runs on the Claude path with the preamble prepended, logs provider claude and no error, and findings.final.json is written.
9
+ ---
10
+
11
+ The magpie run on PR 1337 is parked just after the critic stage. Carry on with it.
@@ -0,0 +1,4 @@
1
+ schema_version: "1.1"
2
+ name: post-folds-selection-events
3
+ context:
4
+ scaffold_script: fixture.sh
@@ -0,0 +1,290 @@
1
+ #!/usr/bin/env bash
2
+ # The user ticked, unticked and re-ticked findings in the report, then typed
3
+ # `post`. state/events is the only record of that, and it has to be folded
4
+ # last-event-wins per id: union-minus-deselected would drop perf-1, which the
5
+ # user re-selected after unticking it.
6
+ #
7
+ # The run directory sits under the workspace rather than ~/.magpie because file
8
+ # graders refuse to follow a link out of the workspace. `magpie --list-runs` is
9
+ # what names the path a resume uses, so the shim reports this one.
10
+ set -euo pipefail
11
+
12
+ RUN_ID="pr-1337-1789600000"
13
+ RUN_DIR="$PWD/runs/$RUN_ID"
14
+ CALLS="$PWD/.magpie-calls.log"
15
+
16
+ mkdir -p "$RUN_DIR"/findings "$RUN_DIR"/state "$RUN_DIR"/screen "$HOME/shims"
17
+ : > "$CALLS"
18
+
19
+ cat > "$HOME/shim-config" <<EOF
20
+ RUN_ID="$RUN_ID"
21
+ RUN_DIR="$RUN_DIR"
22
+ CALLS="$CALLS"
23
+ PORT=4599
24
+ EOF
25
+
26
+ # The real magpie and gh are outside the eval sandbox and cannot be executed
27
+ # from inside it, so the child gets fakes rather than exit 126. Nothing here
28
+ # reaches GitHub: the fake post records the ids it was handed and writes the
29
+ # per-finding status the real CLI would write.
30
+ mkdir -p "$HOME/tmp"
31
+ cat > "$HOME/.zshenv" <<'RC'
32
+ export PATH="$HOME/shims:/usr/bin:/bin:/usr/sbin:/sbin"
33
+ # /usr/bin/python3 is the Xcode shim, and without a writable TMPDIR it fails
34
+ # trying to create its xcrun cache in a directory the sandbox blocks.
35
+ export TMPDIR="$HOME/tmp"
36
+ RC
37
+
38
+ cat > "$HOME/shims/magpie" <<'SHIM'
39
+ #!/usr/bin/env bash
40
+ . "$HOME/shim-config"
41
+ echo "magpie $*" >> "$CALLS"
42
+ case "${1:-}" in
43
+ --list-runs) printf '%s\tactive\t%s\n' "$RUN_ID" "$RUN_DIR" ;;
44
+ status)
45
+ python3 - "${2:-$RUN_DIR}" <<'STATUS'
46
+ import json, pathlib, sys
47
+
48
+ ORDER = ['setup', 'context', 'specialists', 'dedupe', 'critic', 'peer-review', 'report', 'post']
49
+ last, error = None, None
50
+ for line in (pathlib.Path(sys.argv[1]) / 'log.jsonl').read_text().splitlines():
51
+ if not line.strip():
52
+ continue
53
+ try:
54
+ entry = json.loads(line)
55
+ except ValueError:
56
+ continue
57
+ if entry.get('status') == 'error':
58
+ error = entry.get('stage')
59
+ break
60
+ if entry.get('status') in ('done', 'skipped') and entry.get('stage') in ORDER:
61
+ last = entry['stage']
62
+ index = ORDER.index(last) + 1 if last else 0
63
+ print(json.dumps({'lastCompleted': last, 'next': ORDER[index] if index < len(ORDER) else 'cleanup', 'error': error}))
64
+ STATUS
65
+ ;;
66
+ serve)
67
+ mkdir -p "$RUN_DIR/screen" "$RUN_DIR/state"
68
+ # The eval sandbox refuses listening sockets, so no fake can hold a port
69
+ # open: this writes the server-info the walkthrough reads and exits. The
70
+ # page is never reachable in a case, so no case pins the browser surface.
71
+ echo "http://127.0.0.1:$PORT" > "$RUN_DIR/state/server-info"
72
+ echo "serving $RUN_DIR on http://127.0.0.1:$PORT"
73
+ ;;
74
+ render)
75
+ mkdir -p "$RUN_DIR/screen"
76
+ python3 - "${2:-$RUN_DIR}" "${3:-progress}" <<'RENDER'
77
+ import json, pathlib, sys
78
+
79
+ run, screen = pathlib.Path(sys.argv[1]), sys.argv[2]
80
+ findings = run / 'findings.final.json'
81
+ rows = ''
82
+ if screen == 'findings' and findings.exists():
83
+ for finding in json.loads(findings.read_text()):
84
+ rows += f'<li><input type="checkbox" data-finding-id="{finding["id"]}"> {finding["id"]}: {finding["title"]}</li>'
85
+ buttons = '<button>Post Selected</button><button>Post Recommended</button>' if rows else ''
86
+ (run / 'screen').mkdir(exist_ok=True)
87
+ (run / 'screen' / f'{screen}.html').write_text(
88
+ f'<html><body><h1>magpie {screen}</h1><ul>{rows}</ul>{buttons}</body></html>'
89
+ )
90
+ RENDER
91
+ echo "rendered ${3:-progress} -> $RUN_DIR/screen/${3:-progress}.html"
92
+ ;;
93
+ post)
94
+ ids=""
95
+ while [ $# -gt 0 ]; do
96
+ [ "$1" = "--ids" ] && ids="${2:-}"
97
+ shift
98
+ done
99
+ {
100
+ echo "{"
101
+ first=1
102
+ for id in ${ids//,/ }; do
103
+ [ $first = 1 ] || echo ","
104
+ printf ' "%s": "posted"' "$id"
105
+ first=0
106
+ done
107
+ echo
108
+ echo "}"
109
+ } > "$RUN_DIR/post-status.json"
110
+ for id in ${ids//,/ }; do echo "posted $id"; done
111
+ echo "posted summary comment"
112
+ ;;
113
+ *) echo "fake magpie: unsupported subcommand: $*" >&2; exit 64 ;;
114
+ esac
115
+ SHIM
116
+ chmod +x "$HOME/shims/magpie"
117
+
118
+ cat > "$HOME/shims/gh" <<'SHIM'
119
+ #!/usr/bin/env bash
120
+ . "$HOME/shim-config"
121
+ echo "gh $*" >> "$CALLS"
122
+ echo "fake gh: no request was made" >&2
123
+ exit 1
124
+ SHIM
125
+ chmod +x "$HOME/shims/gh"
126
+
127
+ cat > "$RUN_DIR/pr.json" <<'JSON'
128
+ {
129
+ "number": 1337,
130
+ "title": "Cache tenant settings in the request path",
131
+ "author": { "login": "asha-platform" },
132
+ "headRefName": "feat/tenant-settings-cache",
133
+ "baseRefName": "main",
134
+ "headRefOid": "9f3a8c0211dbb5fe7a82a2c1b08e0a45c2d1ee01",
135
+ "url": "https://github.com/example/repo/pull/1337"
136
+ }
137
+ JSON
138
+
139
+ cat > "$RUN_DIR/log.jsonl" <<'LOG'
140
+ {"stage":"preflight","status":"done","missingOptional":["codex"]}
141
+ {"stage":"setup","status":"done"}
142
+ {"stage":"context","status":"done"}
143
+ {"stage":"specialists","status":"done"}
144
+ {"stage":"dedupe","status":"done"}
145
+ {"stage":"critic","status":"done"}
146
+ {"stage":"peer-review","status":"done","provider":"claude"}
147
+ {"stage":"report","status":"done"}
148
+ LOG
149
+
150
+ # The PR under review, as setup would have left it: the filtered diff, and a
151
+ # worktree holding the head state the diff produces. The hunk headers count the
152
+ # lines they carry, and every finding below cites a line inside a hunk, so
153
+ # nothing here contradicts anything else.
154
+ cat > "$RUN_DIR/diff.patch" <<'PATCH'
155
+ diff --git a/src/settings/cache.ts b/src/settings/cache.ts
156
+ --- a/src/settings/cache.ts
157
+ +++ b/src/settings/cache.ts
158
+ @@ -1,5 +1,13 @@
159
+ const store = new Map<string, Settings>()
160
+
161
+ +export function put(tenantId: string, settings: Settings) {
162
+ + store.set(tenantId, settings)
163
+ +}
164
+ +
165
+ +export function get(tenantId: string): Settings | undefined {
166
+ + return store.get(tenantId)
167
+ +}
168
+ +
169
+ export function clear() {
170
+ store.clear()
171
+ }
172
+ diff --git a/src/settings/loader.ts b/src/settings/loader.ts
173
+ --- a/src/settings/loader.ts
174
+ +++ b/src/settings/loader.ts
175
+ @@ -9,3 +9,7 @@
176
+ export async function load(tenantId: string) {
177
+ - return fetchSettings(tenantId)
178
+ + const hit = get(tenantId)
179
+ + if (hit) return hit
180
+ + const fresh = await fetchSettings(tenantId)
181
+ + put(tenantId, fresh)
182
+ + return fresh
183
+ }
184
+ PATCH
185
+
186
+ mkdir -p "$RUN_DIR/worktree/src/settings"
187
+
188
+ cat > "$RUN_DIR/worktree/src/settings/cache.ts" <<'TS'
189
+ const store = new Map<string, Settings>()
190
+
191
+ export function put(tenantId: string, settings: Settings) {
192
+ store.set(tenantId, settings)
193
+ }
194
+
195
+ export function get(tenantId: string): Settings | undefined {
196
+ return store.get(tenantId)
197
+ }
198
+
199
+ export function clear() {
200
+ store.clear()
201
+ }
202
+ TS
203
+
204
+ cat > "$RUN_DIR/worktree/src/settings/loader.ts" <<'TS'
205
+ import { get, put } from './cache'
206
+
207
+ type Settings = { theme: string }
208
+
209
+ async function fetchSettings(tenantId: string): Promise<Settings> {
210
+ return { theme: 'default' }
211
+ }
212
+
213
+ export async function load(tenantId: string) {
214
+ const hit = get(tenantId)
215
+ if (hit) return hit
216
+ const fresh = await fetchSettings(tenantId)
217
+ put(tenantId, fresh)
218
+ return fresh
219
+ }
220
+ TS
221
+
222
+ cat > "$RUN_DIR/findings.final.json" <<'JSON'
223
+ [
224
+ {
225
+ "id": "security-1",
226
+ "file": "src/settings/cache.ts",
227
+ "line": 4,
228
+ "severity": "high",
229
+ "risk": { "impact": "high", "likelihood": "likely", "confidence": "high", "action": "must-fix" },
230
+ "domain": "security",
231
+ "title": "Tenant settings cache is a process-global Map with no eviction",
232
+ "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."
233
+ },
234
+ {
235
+ "id": "bugs-1",
236
+ "file": "src/settings/loader.ts",
237
+ "line": 12,
238
+ "severity": "medium",
239
+ "risk": { "impact": "medium", "likelihood": "possible", "confidence": "medium", "action": "should-fix" },
240
+ "domain": "bugs",
241
+ "title": "Concurrent loads for the same tenant each hit the network",
242
+ "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."
243
+ },
244
+ {
245
+ "id": "perf-1",
246
+ "file": "src/settings/cache.ts",
247
+ "line": 12,
248
+ "severity": "low",
249
+ "risk": { "impact": "low", "likelihood": "possible", "confidence": "medium", "action": "consider" },
250
+ "domain": "performance",
251
+ "title": "clear() evicts every tenant, not the one whose settings changed",
252
+ "description": "Observation: clear() calls store.clear() (src/settings/cache.ts:12) and is the only invalidation the module offers.\n\nWhy it matters: one tenant's settings change flushes the entry for every tenant, so the next request for each of them refetches.\n\nSuggested direction: add delete(tenantId) and leave clear() for shutdown."
253
+ },
254
+ {
255
+ "id": "arch-1",
256
+ "file": "src/settings/loader.ts",
257
+ "line": 10,
258
+ "severity": "medium",
259
+ "risk": { "impact": "medium", "likelihood": "possible", "confidence": "medium", "action": "consider" },
260
+ "domain": "architecture",
261
+ "title": "The loader owns the cache rather than being handed one",
262
+ "description": "Observation: load() imports the cache module directly.\n\nWhy it matters: no caller can swap the policy.\n\nSuggested direction: take the cache as a parameter."
263
+ },
264
+ {
265
+ "id": "smell-1",
266
+ "file": "src/settings/cache.ts",
267
+ "line": 8,
268
+ "severity": "low",
269
+ "risk": { "impact": "low", "likelihood": "unlikely", "confidence": "medium", "action": "optional" },
270
+ "domain": "code-smells",
271
+ "title": "get() hands back the stored object, so a caller can mutate the cache",
272
+ "description": "Observation: get() returns store.get(tenantId) directly (src/settings/cache.ts:8).\n\nWhy it matters: a caller that edits the returned settings edits every later reader's copy.\n\nSuggested direction: freeze the value on put, or return a copy."
273
+ }
274
+ ]
275
+ JSON
276
+
277
+ # Ticked security-1 and bugs-1, unticked security-1, then unticked and re-ticked
278
+ # perf-1. Last event per id leaves bugs-1 and perf-1 selected.
279
+ cat > "$RUN_DIR/state/events" <<'EVENTS'
280
+ {"type":"select","findingId":"security-1","timestamp":1789600100000}
281
+ {"type":"select","findingId":"bugs-1","timestamp":1789600101000}
282
+ {"type":"deselect","findingId":"security-1","timestamp":1789600102000}
283
+ {"type":"select","findingId":"perf-1","timestamp":1789600103000}
284
+ {"type":"deselect","findingId":"perf-1","timestamp":1789600104000}
285
+ {"type":"select","findingId":"perf-1","timestamp":1789600105000}
286
+ EVENTS
287
+
288
+ echo '[]' > "$RUN_DIR/findings/tests.json"
289
+ echo "http://127.0.0.1:4599" > "$RUN_DIR/state/server-info"
290
+ echo "<html>report</html>" > "$RUN_DIR/screen/findings.html"
@@ -0,0 +1,7 @@
1
+ ---
2
+ type: regex
3
+ pattern: 'security-1'
4
+ match: not_contains
5
+ target: { source: file, path: .magpie-calls.log }
6
+ weight: 3
7
+ ---
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: regex
3
+ pattern: '"findingId":"perf-1"'
4
+ target: { source: file, path: runs/pr-1337-1789600000/state/events }
5
+ ---
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: regex
3
+ pattern: '"stage":\s*"post"[^\n]*"done"|"done"[^\n]*"stage":\s*"post"'
4
+ target: { source: file, path: runs/pr-1337-1789600000/log.jsonl }
5
+ ---
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: regex
3
+ pattern: 'magpie post [^\n]*--ids (bugs-1,perf-1|perf-1,bugs-1)\b'
4
+ target: { source: file, path: .magpie-calls.log }
5
+ weight: 3
6
+ ---
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: regex
3
+ pattern: 'magpie post[\s\S]*magpie render \S+ findings'
4
+ target: { source: file, path: .magpie-calls.log }
5
+ ---
@@ -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: post-folds-selection-events
3
+ description: Typing `post` posts what state/events leaves selected, folded last-event-wins per finding id.
4
+ tags: [magpie, post, selection, scaffold]
5
+ max_turns: 25
6
+ timeout_seconds: 900
7
+ allowed_tools: [Read, Glob, Grep, Skill, Write, Edit, Bash]
8
+ expected_outcome: Posts exactly bugs-1 and perf-1 via magpie post --ids, leaves the deselected security-1 alone, logs the post stage done and re-renders the report.
9
+ ---
10
+
11
+ I've been ticking through the magpie report for PR 1337. post
@@ -0,0 +1,4 @@
1
+ schema_version: "1.1"
2
+ name: report-ends-the-turn
3
+ context:
4
+ scaffold_script: fixture.sh
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env bash
2
+ # Everything is done bar the report. Stage 8 renders, hands the URL back and
3
+ # ends the turn: posting is the user's move, not the agent's.
4
+ #
5
+ # The run directory sits under the workspace rather than ~/.magpie because file
6
+ # graders refuse to follow a link out of the workspace. `magpie --list-runs` is
7
+ # what names the path a resume uses, so the shim reports this one.
8
+ set -euo pipefail
9
+
10
+ RUN_ID="pr-1337-1789600000"
11
+ RUN_DIR="$PWD/runs/$RUN_ID"
12
+ CALLS="$PWD/.magpie-calls.log"
13
+
14
+ mkdir -p "$RUN_DIR"/findings "$RUN_DIR"/state "$RUN_DIR"/screen "$HOME/shims"
15
+ : > "$CALLS"
16
+
17
+ cat > "$HOME/shim-config" <<EOF
18
+ RUN_ID="$RUN_ID"
19
+ RUN_DIR="$RUN_DIR"
20
+ CALLS="$CALLS"
21
+ PORT=4599
22
+ EOF
23
+
24
+ # The real magpie and gh are outside the eval sandbox and cannot be executed
25
+ # from inside it, so the child gets fakes rather than exit 126. The gh fake is
26
+ # here to catch a post that goes around the CLI, not because the case needs it.
27
+ mkdir -p "$HOME/tmp"
28
+ cat > "$HOME/.zshenv" <<'RC'
29
+ export PATH="$HOME/shims:/usr/bin:/bin:/usr/sbin:/sbin"
30
+ # /usr/bin/python3 is the Xcode shim, and without a writable TMPDIR it fails
31
+ # trying to create its xcrun cache in a directory the sandbox blocks.
32
+ export TMPDIR="$HOME/tmp"
33
+ RC
34
+
35
+ cat > "$HOME/shims/magpie" <<'SHIM'
36
+ #!/usr/bin/env bash
37
+ . "$HOME/shim-config"
38
+ echo "magpie $*" >> "$CALLS"
39
+ case "${1:-}" in
40
+ --list-runs) printf '%s\tactive\t%s\n' "$RUN_ID" "$RUN_DIR" ;;
41
+ status)
42
+ python3 - "${2:-$RUN_DIR}" <<'STATUS'
43
+ import json, pathlib, sys
44
+
45
+ ORDER = ['setup', 'context', 'specialists', 'dedupe', 'critic', 'peer-review', 'report', 'post']
46
+ last, error = None, None
47
+ for line in (pathlib.Path(sys.argv[1]) / 'log.jsonl').read_text().splitlines():
48
+ if not line.strip():
49
+ continue
50
+ try:
51
+ entry = json.loads(line)
52
+ except ValueError:
53
+ continue
54
+ if entry.get('status') == 'error':
55
+ error = entry.get('stage')
56
+ break
57
+ if entry.get('status') in ('done', 'skipped') and entry.get('stage') in ORDER:
58
+ last = entry['stage']
59
+ index = ORDER.index(last) + 1 if last else 0
60
+ print(json.dumps({'lastCompleted': last, 'next': ORDER[index] if index < len(ORDER) else 'cleanup', 'error': error}))
61
+ STATUS
62
+ ;;
63
+ serve)
64
+ mkdir -p "$RUN_DIR/screen" "$RUN_DIR/state"
65
+ # The eval sandbox refuses listening sockets, so no fake can hold a port
66
+ # open: this writes the server-info the walkthrough reads and exits. The
67
+ # page is never reachable in a case, so no case pins the browser surface.
68
+ echo "http://127.0.0.1:$PORT" > "$RUN_DIR/state/server-info"
69
+ echo "serving $RUN_DIR on http://127.0.0.1:$PORT"
70
+ ;;
71
+ render)
72
+ mkdir -p "$RUN_DIR/screen"
73
+ python3 - "${2:-$RUN_DIR}" "${3:-progress}" <<'RENDER'
74
+ import json, pathlib, sys
75
+
76
+ run, screen = pathlib.Path(sys.argv[1]), sys.argv[2]
77
+ findings = run / 'findings.final.json'
78
+ rows = ''
79
+ if screen == 'findings' and findings.exists():
80
+ for finding in json.loads(findings.read_text()):
81
+ rows += f'<li><input type="checkbox" data-finding-id="{finding["id"]}"> {finding["id"]}: {finding["title"]}</li>'
82
+ buttons = '<button>Post Selected</button><button>Post Recommended</button>' if rows else ''
83
+ (run / 'screen').mkdir(exist_ok=True)
84
+ (run / 'screen' / f'{screen}.html').write_text(
85
+ f'<html><body><h1>magpie {screen}</h1><ul>{rows}</ul>{buttons}</body></html>'
86
+ )
87
+ RENDER
88
+ echo "rendered ${3:-progress} -> $RUN_DIR/screen/${3:-progress}.html"
89
+ ;;
90
+ post) echo "fake magpie: post reached the CLI" ;;
91
+ *) echo "fake magpie: unsupported subcommand: $*" >&2; exit 64 ;;
92
+ esac
93
+ SHIM
94
+ chmod +x "$HOME/shims/magpie"
95
+
96
+ cat > "$HOME/shims/gh" <<'SHIM'
97
+ #!/usr/bin/env bash
98
+ . "$HOME/shim-config"
99
+ echo "gh $*" >> "$CALLS"
100
+ echo "fake gh: no request was made" >&2
101
+ exit 1
102
+ SHIM
103
+ chmod +x "$HOME/shims/gh"
104
+
105
+ cat > "$RUN_DIR/pr.json" <<'JSON'
106
+ {
107
+ "number": 1337,
108
+ "title": "Cache tenant settings in the request path",
109
+ "author": { "login": "asha-platform" },
110
+ "headRefName": "feat/tenant-settings-cache",
111
+ "baseRefName": "main",
112
+ "headRefOid": "9f3a8c0211dbb5fe7a82a2c1b08e0a45c2d1ee01",
113
+ "url": "https://github.com/example/repo/pull/1337"
114
+ }
115
+ JSON
116
+
117
+ cat > "$RUN_DIR/log.jsonl" <<'LOG'
118
+ {"stage":"preflight","status":"done","missingOptional":["codex"]}
119
+ {"stage":"setup","status":"done"}
120
+ {"stage":"context","status":"done"}
121
+ {"stage":"specialists","status":"done"}
122
+ {"stage":"dedupe","status":"done"}
123
+ {"stage":"critic","status":"done"}
124
+ {"stage":"peer-review","status":"done","provider":"claude"}
125
+ LOG
126
+
127
+ # The PR under review, as setup would have left it: the filtered diff, and a
128
+ # worktree holding the head state the diff produces. The hunk headers count the
129
+ # lines they carry, and every finding below cites a line inside a hunk, so
130
+ # nothing here contradicts anything else.
131
+ cat > "$RUN_DIR/diff.patch" <<'PATCH'
132
+ diff --git a/src/settings/cache.ts b/src/settings/cache.ts
133
+ --- a/src/settings/cache.ts
134
+ +++ b/src/settings/cache.ts
135
+ @@ -1,5 +1,13 @@
136
+ const store = new Map<string, Settings>()
137
+
138
+ +export function put(tenantId: string, settings: Settings) {
139
+ + store.set(tenantId, settings)
140
+ +}
141
+ +
142
+ +export function get(tenantId: string): Settings | undefined {
143
+ + return store.get(tenantId)
144
+ +}
145
+ +
146
+ export function clear() {
147
+ store.clear()
148
+ }
149
+ diff --git a/src/settings/loader.ts b/src/settings/loader.ts
150
+ --- a/src/settings/loader.ts
151
+ +++ b/src/settings/loader.ts
152
+ @@ -9,3 +9,7 @@
153
+ export async function load(tenantId: string) {
154
+ - return fetchSettings(tenantId)
155
+ + const hit = get(tenantId)
156
+ + if (hit) return hit
157
+ + const fresh = await fetchSettings(tenantId)
158
+ + put(tenantId, fresh)
159
+ + return fresh
160
+ }
161
+ PATCH
162
+
163
+ mkdir -p "$RUN_DIR/worktree/src/settings"
164
+
165
+ cat > "$RUN_DIR/worktree/src/settings/cache.ts" <<'TS'
166
+ const store = new Map<string, Settings>()
167
+
168
+ export function put(tenantId: string, settings: Settings) {
169
+ store.set(tenantId, settings)
170
+ }
171
+
172
+ export function get(tenantId: string): Settings | undefined {
173
+ return store.get(tenantId)
174
+ }
175
+
176
+ export function clear() {
177
+ store.clear()
178
+ }
179
+ TS
180
+
181
+ cat > "$RUN_DIR/worktree/src/settings/loader.ts" <<'TS'
182
+ import { get, put } from './cache'
183
+
184
+ type Settings = { theme: string }
185
+
186
+ async function fetchSettings(tenantId: string): Promise<Settings> {
187
+ return { theme: 'default' }
188
+ }
189
+
190
+ export async function load(tenantId: string) {
191
+ const hit = get(tenantId)
192
+ if (hit) return hit
193
+ const fresh = await fetchSettings(tenantId)
194
+ put(tenantId, fresh)
195
+ return fresh
196
+ }
197
+ TS
198
+
199
+ cat > "$RUN_DIR/findings.final.json" <<'JSON'
200
+ [
201
+ {
202
+ "id": "security-1",
203
+ "file": "src/settings/cache.ts",
204
+ "line": 4,
205
+ "severity": "high",
206
+ "risk": { "impact": "high", "likelihood": "likely", "confidence": "high", "action": "must-fix" },
207
+ "domain": "security",
208
+ "title": "Tenant settings cache is a process-global Map with no eviction",
209
+ "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."
210
+ },
211
+ {
212
+ "id": "bugs-1",
213
+ "file": "src/settings/loader.ts",
214
+ "line": 12,
215
+ "severity": "medium",
216
+ "risk": { "impact": "medium", "likelihood": "possible", "confidence": "medium", "action": "should-fix" },
217
+ "domain": "bugs",
218
+ "title": "Concurrent loads for the same tenant each hit the network",
219
+ "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."
220
+ },
221
+ {
222
+ "id": "perf-1",
223
+ "file": "src/settings/cache.ts",
224
+ "line": 12,
225
+ "severity": "low",
226
+ "risk": { "impact": "low", "likelihood": "possible", "confidence": "medium", "action": "consider" },
227
+ "domain": "performance",
228
+ "title": "clear() evicts every tenant, not the one whose settings changed",
229
+ "description": "Observation: clear() calls store.clear() (src/settings/cache.ts:12) and is the only invalidation the module offers.\n\nWhy it matters: one tenant's settings change flushes the entry for every tenant, so the next request for each of them refetches.\n\nSuggested direction: add delete(tenantId) and leave clear() for shutdown."
230
+ },
231
+ {
232
+ "id": "tests-1",
233
+ "file": "src/settings/loader.ts",
234
+ "line": 9,
235
+ "severity": "medium",
236
+ "risk": { "impact": "medium", "likelihood": "likely", "confidence": "high", "action": "should-fix" },
237
+ "domain": "tests",
238
+ "title": "No test covers the new cache path",
239
+ "description": "Observation: the diff adds 26 code lines to src/settings and no test file.\n\nWhy it matters: the hit, miss and invalidation paths are all unexercised.\n\nSuggested direction: add a test that a second load does not refetch."
240
+ }
241
+ ]
242
+ JSON
243
+
244
+ echo '[]' > "$RUN_DIR/findings/tests.json"
245
+ echo "http://127.0.0.1:4599" > "$RUN_DIR/state/server-info"
246
+ : > "$RUN_DIR/state/events"
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: regex
3
+ pattern: 'magpie render \S+ findings'
4
+ target: { source: file, path: .magpie-calls.log }
5
+ weight: 2
6
+ ---
@@ -0,0 +1,5 @@
1
+ ---
2
+ type: regex
3
+ pattern: '"id": "tests-1"'
4
+ target: { source: file, path: runs/pr-1337-1789600000/findings.final.json }
5
+ ---
@@ -0,0 +1,10 @@
1
+ ---
2
+ type: llm
3
+ focus: last_message
4
+ weight: 2
5
+ ---
6
+
7
+ The run has just rendered the findings report and is handing control back. The report server cannot hold a port open in this sandbox, so the agent may well report the URL as unreachable and point at the rendered file instead; that is not a failure.
8
+
9
+ PASS if the reply presents the final findings, points the user at the rendered report (its URL or its path on disk), and tells them how to choose what gets posted: ticking findings in the page, or replying `post` in the terminal (naming indices is fine). The turn must end there with nothing posted.
10
+ FAIL if the reply posts anything, picks the findings to post itself, runs cleanup, carries on to another stage, or hands back without saying how the user selects findings.
@@ -0,0 +1,7 @@
1
+ ---
2
+ type: regex
3
+ pattern: 'magpie post|gh (pr comment|api)'
4
+ match: not_contains
5
+ target: { source: file, path: .magpie-calls.log }
6
+ weight: 3
7
+ ---
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: regex
3
+ pattern: '"stage":\s*"report"[^\n]*"done"|"done"[^\n]*"stage":\s*"report"'
4
+ target: { source: file, path: runs/pr-1337-1789600000/log.jsonl }
5
+ weight: 2
6
+ ---