@iceinvein/agent-skills 0.1.39 → 0.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/index.json +1 -1
- package/skills/magpie/README.md +2 -1
- package/skills/magpie/SKILL.md +27 -12
- package/skills/magpie/fixtures/example-pr/brief.json +18 -0
- package/skills/magpie/fixtures/fake-gh.sh +14 -0
- package/skills/magpie/package.json +1 -1
- package/skills/magpie/references/scout.md +90 -0
- package/skills/magpie/references/specialists.md +84 -4
- package/skills/magpie/scripts/__tests__/gh.test.ts +21 -0
- package/skills/magpie/scripts/__tests__/preview-cmd.test.ts +16 -0
- package/skills/magpie/scripts/__tests__/refresh.test.ts +45 -0
- package/skills/magpie/scripts/__tests__/render-cmd.test.ts +78 -1
- package/skills/magpie/scripts/__tests__/render-findings.test.ts +118 -1
- package/skills/magpie/scripts/__tests__/skill-lint.test.ts +111 -6
- package/skills/magpie/scripts/__tests__/types.test.ts +47 -0
- package/skills/magpie/scripts/gh.ts +4 -1
- package/skills/magpie/scripts/preview-cmd.ts +11 -1
- package/skills/magpie/scripts/refresh.ts +24 -3
- package/skills/magpie/scripts/render-cmd.ts +8 -3
- package/skills/magpie/scripts/render-findings.ts +67 -1
- package/skills/magpie/scripts/status-cmd.ts +3 -3
- package/skills/magpie/scripts/types.ts +50 -0
- package/skills/magpie/skill.json +1 -1
- package/skills/magpie/templates/styles.css +61 -0
package/package.json
CHANGED
package/skills/index.json
CHANGED
|
@@ -221,7 +221,7 @@
|
|
|
221
221
|
"name": "magpie",
|
|
222
222
|
"description": "Interactive PR review pipeline. Runs five parallel specialist subagents (security, bugs, performance, code-smells, architecture), dedupes findings, applies a critic rubric, peer-reviews via codex exec (falling back to a Claude second opinion when codex is unavailable), and serves an interactive HTML report for selecting findings to post via gh. Bundles a Bun CLI installed onto PATH via the skill's postinstall step. Use when the user asks to review a GitHub pull request.",
|
|
223
223
|
"type": "prompt",
|
|
224
|
-
"version": "0.
|
|
224
|
+
"version": "0.9.0"
|
|
225
225
|
},
|
|
226
226
|
{
|
|
227
227
|
"name": "module-secret-auditor",
|
package/skills/magpie/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Given a GitHub PR number, dispatches five specialist subagents in parallel (secu
|
|
|
12
12
|
- `gh` on PATH, authenticated (`gh auth status`)
|
|
13
13
|
- `git` on PATH
|
|
14
14
|
- `codex` on PATH, authenticated (optional; if absent the peer-review stage falls back to a Claude second-opinion subagent)
|
|
15
|
+
- the code-intelligence MCP server, with a completed index for the repo under review (optional; if absent the specialists review from the diff and worktree alone)
|
|
15
16
|
|
|
16
17
|
## Install
|
|
17
18
|
|
|
@@ -54,7 +55,7 @@ The fixture lives at `fixtures/example-pr/` (pr.json + findings.final.json + pos
|
|
|
54
55
|
## Layout
|
|
55
56
|
|
|
56
57
|
- `SKILL.md` is the agent-facing prompt: the stage walkthrough and nothing else. Installed by the agent-skills CLI.
|
|
57
|
-
- `references/` holds the prompt bodies the walkthrough loads on demand, one file per stage that needs one: `
|
|
58
|
+
- `references/` holds the prompt bodies the walkthrough loads on demand, one file per stage that needs one: `scout.md` (stage 3, the PR-brief prompt and the `brief.json` contract), `specialists.md` (stage 4, the five focus blocks plus the shared output contract and the codebase-intelligence block), `critic.md` (stage 6), `peer-review.md` (stage 7, including the Claude-fallback preamble). They ship in the bundle and sit next to `SKILL.md` once installed.
|
|
58
59
|
- `skill.json` is the agent-skills manifest.
|
|
59
60
|
- `bin/magpie` is the CLI invoked by the agent during stages; symlinked onto PATH by `install.sh`.
|
|
60
61
|
- `scripts/` holds the implementation (server, dedupe, render, setup, cleanup, etc.).
|
package/skills/magpie/SKILL.md
CHANGED
|
@@ -46,8 +46,6 @@ When a prior run exists for the same PR (active or archived under `~/.magpie/`),
|
|
|
46
46
|
|
|
47
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.
|
|
48
48
|
|
|
49
|
-
The pipeline has no separate context-indexing stage, but `magpie status` and the progress page track one. After setup succeeds, append `{stage: context, status: skipped}` to `$RUN_DIR/log.jsonl`; both treat a skipped stage as behind them, so the pipeline advances to `specialists`.
|
|
50
|
-
|
|
51
49
|
### 2. Serve
|
|
52
50
|
|
|
53
51
|
Start the HTML server in the background using the Bash tool with `run_in_background: true`:
|
|
@@ -66,7 +64,22 @@ Render the first progress paint:
|
|
|
66
64
|
magpie render "$RUN_DIR" progress
|
|
67
65
|
```
|
|
68
66
|
|
|
69
|
-
### 3.
|
|
67
|
+
### 3. Context
|
|
68
|
+
|
|
69
|
+
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
|
+
|
|
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.
|
|
72
|
+
|
|
73
|
+
- `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
|
+
- `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.
|
|
75
|
+
- A ready result sets `CODE_INTELLIGENCE=available`.
|
|
76
|
+
- Any other error sets `CODE_INTELLIGENCE=unavailable`. Do not retry.
|
|
77
|
+
|
|
78
|
+
**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
|
+
|
|
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.
|
|
81
|
+
|
|
82
|
+
### 4. Specialists
|
|
70
83
|
|
|
71
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.
|
|
72
85
|
|
|
@@ -74,9 +87,9 @@ Append `{stage: specialists, status: running}` to `$RUN_DIR/log.jsonl` and re-re
|
|
|
74
87
|
|
|
75
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`.)
|
|
76
89
|
|
|
77
|
-
If all five specialists fail (no findings files written), log `{stage: specialists, status: error}` and stop. Otherwise mark `{stage: specialists, status: done}`.
|
|
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}`.
|
|
78
91
|
|
|
79
|
-
###
|
|
92
|
+
### 5. Dedupe
|
|
80
93
|
|
|
81
94
|
```
|
|
82
95
|
magpie dedupe "$RUN_DIR" [--threshold <0-10>]
|
|
@@ -88,11 +101,11 @@ Each finding receives a derived 0-10 `score` from its risk fields. Findings belo
|
|
|
88
101
|
|
|
89
102
|
Re-render progress.
|
|
90
103
|
|
|
91
|
-
###
|
|
104
|
+
### 6. Critic
|
|
92
105
|
|
|
93
106
|
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.
|
|
94
107
|
|
|
95
|
-
###
|
|
108
|
+
### 7. Peer review
|
|
96
109
|
|
|
97
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.
|
|
98
111
|
|
|
@@ -112,7 +125,7 @@ If codex returns non-zero, do not abort: record `{stage: peer-review, provider:
|
|
|
112
125
|
|
|
113
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.
|
|
114
127
|
|
|
115
|
-
###
|
|
128
|
+
### 8. Report
|
|
116
129
|
|
|
117
130
|
```
|
|
118
131
|
magpie render "$RUN_DIR" findings
|
|
@@ -124,7 +137,7 @@ Print to the terminal: "Findings ready at <url>. Tick the ones you want and clic
|
|
|
124
137
|
|
|
125
138
|
End the turn.
|
|
126
139
|
|
|
127
|
-
###
|
|
140
|
+
### 9. Post
|
|
128
141
|
|
|
129
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.
|
|
130
143
|
|
|
@@ -147,12 +160,14 @@ Pass `--dry-run` to record the would-be gh commands without invoking gh. After p
|
|
|
147
160
|
magpie render "$RUN_DIR" findings
|
|
148
161
|
```
|
|
149
162
|
|
|
150
|
-
###
|
|
163
|
+
### 10. Cleanup
|
|
151
164
|
|
|
152
165
|
```
|
|
153
166
|
magpie cleanup "$RUN_DIR" --repo "$REPO"
|
|
154
167
|
```
|
|
155
168
|
|
|
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.
|
|
170
|
+
|
|
156
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.
|
|
157
172
|
|
|
158
173
|
The archived `findings.html` is self-contained and auto-switches to read-only "archived" mode when opened, so:
|
|
@@ -172,7 +187,7 @@ magpie status "$RUN_DIR"
|
|
|
172
187
|
|
|
173
188
|
The JSON output tells you `lastCompleted` and `next`. Resume from `next`:
|
|
174
189
|
|
|
175
|
-
- `context`
|
|
190
|
+
- `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.
|
|
176
191
|
- Any other stage: run it as written in the walkthrough.
|
|
177
192
|
- If a specialist focus has no findings file but its sibling stages are done, re-dispatch only that focus.
|
|
178
193
|
- Non-null `error` means the run stopped on a failed stage. Report which stage to the user and confirm before re-running it.
|
|
@@ -181,4 +196,4 @@ The server from the original run is gone. Restart it with `magpie serve "$RUN_DI
|
|
|
181
196
|
|
|
182
197
|
## Aborting
|
|
183
198
|
|
|
184
|
-
If the user types `abort` mid-run, run `magpie cleanup`
|
|
199
|
+
If the user types `abort` mid-run, rebind code intelligence to `$REPO` if bound (stage 10), then run `magpie cleanup` and exit.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"purpose": "Adds bounded retries to the upload path so transient S3 failures stop surfacing to end users as hard errors.",
|
|
3
|
+
"changes": [
|
|
4
|
+
"Wraps the S3 put call in a bounded retry with jittered backoff",
|
|
5
|
+
"Threads a per-request deadline through the storage client",
|
|
6
|
+
"Adds a metrics counter for exhausted retry budgets"
|
|
7
|
+
],
|
|
8
|
+
"subsystems": [
|
|
9
|
+
{ "name": "upload", "role": "owns the client-facing put path" },
|
|
10
|
+
{ "name": "storage-client", "role": "wraps the S3 SDK and owns timeouts" }
|
|
11
|
+
],
|
|
12
|
+
"watchItems": [
|
|
13
|
+
"The PR body claims the retry is idempotent, but no idempotency key is sent with the put"
|
|
14
|
+
],
|
|
15
|
+
"unclear": [
|
|
16
|
+
"Whether the new per-request deadline interacts correctly with the outer HTTP timeout"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -19,6 +19,20 @@ case "$1 $2" in
|
|
|
19
19
|
"deletions": 0,
|
|
20
20
|
"changeType": "modified"
|
|
21
21
|
}
|
|
22
|
+
],
|
|
23
|
+
"commits": [
|
|
24
|
+
{
|
|
25
|
+
"oid": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
|
|
26
|
+
"messageHeadline": "Add retry handling to the upload path",
|
|
27
|
+
"messageBody": ""
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"closingIssuesReferences": [
|
|
31
|
+
{
|
|
32
|
+
"number": 42,
|
|
33
|
+
"title": "Uploads fail intermittently under load",
|
|
34
|
+
"url": "https://github.com/octocat/Hello-World/issues/42"
|
|
35
|
+
}
|
|
22
36
|
]
|
|
23
37
|
}
|
|
24
38
|
JSON
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Scout prompt
|
|
2
|
+
|
|
3
|
+
Stage 3 of the walkthrough dispatches one subagent from this file, before the five
|
|
4
|
+
specialists. Send the fenced `magpie-scout` block below as the agent's entire task,
|
|
5
|
+
with `<<RUN_DIR>>` and `<<PR_NUMBER>>` replaced by the real values first. The
|
|
6
|
+
subagent has no shell variables from your session, so an unexpanded path means it
|
|
7
|
+
writes the brief where nothing will read it.
|
|
8
|
+
|
|
9
|
+
Substitute `<<CODE_INTELLIGENCE>>` with `available` when the bind probe succeeded and
|
|
10
|
+
`unavailable` otherwise. The scout still runs when code-intelligence is unavailable;
|
|
11
|
+
only the subsystem map degrades.
|
|
12
|
+
|
|
13
|
+
```magpie-scout
|
|
14
|
+
You are a senior engineer building the orienting brief that five specialist
|
|
15
|
+
reviewers will read before they review PR #<<PR_NUMBER>>. You are not reviewing the
|
|
16
|
+
code. You are answering "what is this PR for, and what does it actually do".
|
|
17
|
+
|
|
18
|
+
Working directory: <<RUN_DIR>>/worktree
|
|
19
|
+
PR metadata: <<RUN_DIR>>/pr.json
|
|
20
|
+
Diff: <<RUN_DIR>>/diff.patch
|
|
21
|
+
Code intelligence: <<CODE_INTELLIGENCE>>
|
|
22
|
+
|
|
23
|
+
## What to read
|
|
24
|
+
|
|
25
|
+
1. `pr.json` for the author's stated intent: `title`, `body`, `commits[].messageHeadline`,
|
|
26
|
+
and `closingIssuesReferences[].title`. This is the claim.
|
|
27
|
+
2. `diff.patch` for what the change actually does. This is the evidence.
|
|
28
|
+
3. The worktree for surrounding context on any file the diff changes but does not
|
|
29
|
+
explain.
|
|
30
|
+
|
|
31
|
+
## Code intelligence
|
|
32
|
+
|
|
33
|
+
When the line above says `available`, the code-intelligence MCP tools are indexed
|
|
34
|
+
against `<<RUN_DIR>>/worktree`, seeded from the base repository. Call `bind_workspace`
|
|
35
|
+
with `<<RUN_DIR>>/worktree` before your first query. Use them to map where the change
|
|
36
|
+
lands:
|
|
37
|
+
|
|
38
|
+
- `get_module_summary` on each directory the diff touches, to name the subsystem and
|
|
39
|
+
say what it is responsible for.
|
|
40
|
+
- `explore_dependency_graph` on the touched modules, to say what depends on them.
|
|
41
|
+
|
|
42
|
+
If a tool returns `indexing_in_progress`, finish reading the diff and retry once. If
|
|
43
|
+
it still is not ready, or the line above says `unavailable`, write `"subsystems": []`
|
|
44
|
+
and carry on. Never call `approve_indexing`. Never call `refresh_index`. Triggering a
|
|
45
|
+
full index is a consent-gated operation that is not yours to start.
|
|
46
|
+
|
|
47
|
+
## How to reason
|
|
48
|
+
|
|
49
|
+
1. State the purpose in your own words, not the author's. If you cannot restate it
|
|
50
|
+
without quoting the PR body, you have not understood it yet.
|
|
51
|
+
2. Group the diff into 3-7 concerns. A concern is a thing a reviewer would evaluate
|
|
52
|
+
as a unit, not a file.
|
|
53
|
+
3. Compare the claim against the evidence. Where the diff does something the stated
|
|
54
|
+
intent does not cover, or omits something the stated intent implies, that is a
|
|
55
|
+
watch item.
|
|
56
|
+
4. Be honest about what you could not determine. An empty `unclear` on a large PR is
|
|
57
|
+
not credible.
|
|
58
|
+
|
|
59
|
+
## Output contract
|
|
60
|
+
|
|
61
|
+
Write `<<RUN_DIR>>/brief.json` before returning. The file MUST be a JSON object with
|
|
62
|
+
exactly these five keys:
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
"purpose": string, // 1-3 sentences: what this PR is for, in your words. Required
|
|
66
|
+
// and non-empty; a brief with no purpose is discarded whole.
|
|
67
|
+
"changes": string[], // 3-7 entries: what it actually does, grouped by concern.
|
|
68
|
+
// One clause each, no trailing period needed.
|
|
69
|
+
"subsystems": [ { "name": string, "role": string } ],
|
|
70
|
+
// Code-intelligence derived. `name` is the subsystem, `role`
|
|
71
|
+
// is one clause on what it is responsible for. [] when
|
|
72
|
+
// code intelligence is unavailable.
|
|
73
|
+
"watchItems": string[], // Where the diff and the stated intent diverge, or where the
|
|
74
|
+
// intent implies a risk the diff does not address. Often
|
|
75
|
+
// empty. See the boundary below.
|
|
76
|
+
"unclear": string[] // What you could not determine from the bundle.
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
A watch item is not a finding. You do not assign severity, you do not assign risk,
|
|
80
|
+
and you do not recommend a fix. A watch item is a pointer a specialist may escalate
|
|
81
|
+
into a finding in its own domain, with its own risk fields, or dismiss. Write it as
|
|
82
|
+
an observation: "the PR body claims X, but the diff does Y".
|
|
83
|
+
|
|
84
|
+
Do not add keys. Do not omit keys; write `[]` for an empty list. Do not transcribe
|
|
85
|
+
commit messages or issue titles into the brief: the report reads those from `pr.json`
|
|
86
|
+
directly, and repeating them wastes the specialists' attention.
|
|
87
|
+
|
|
88
|
+
Return as your final tool result a single line:
|
|
89
|
+
`brief: <N> changes, <M> subsystems, <K> watch items`. Do not include other prose.
|
|
90
|
+
```
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# Specialist prompts
|
|
2
2
|
|
|
3
|
-
Stage
|
|
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:
|
|
4
5
|
|
|
5
6
|
1. The focus block for that focus (the fenced `magpie-specialist-<focus>` blocks below), verbatim.
|
|
6
7
|
2. The run header, with the two placeholders filled in:
|
|
@@ -12,10 +13,42 @@ Diff: <RUN_DIR>/diff.patch
|
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
3. The `## Output Contract` section below, verbatim.
|
|
16
|
+
4. The `magpie-codebase-intelligence` block below, verbatim, **only** when the context
|
|
17
|
+
stage logged `codeIntelligence: true`. Omit it entirely otherwise: telling a
|
|
18
|
+
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:
|
|
15
20
|
|
|
16
|
-
|
|
21
|
+
```
|
|
22
|
+
## What this PR is for
|
|
23
|
+
|
|
24
|
+
<purpose>
|
|
25
|
+
|
|
26
|
+
What it does:
|
|
27
|
+
- <each entry of changes>
|
|
28
|
+
|
|
29
|
+
Subsystems it lands in:
|
|
30
|
+
- <name>: <role> (omit this heading when subsystems is empty)
|
|
31
|
+
|
|
32
|
+
Watch items:
|
|
33
|
+
- <each entry of watchItems> (omit this heading when watchItems is empty)
|
|
34
|
+
|
|
35
|
+
Open questions the scout could not resolve:
|
|
36
|
+
- <each entry of unclear> (omit this heading when unclear is empty)
|
|
17
37
|
|
|
18
|
-
|
|
38
|
+
This brief is the author's claim as understood by a reader who has not reviewed the
|
|
39
|
+
code. It is not ground truth. Where the diff contradicts it, that is a finding in
|
|
40
|
+
your domain, not a correction to the brief. A watch item is a pointer, not a verdict:
|
|
41
|
+
escalate it into a finding with your own risk fields, or leave it alone.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Replace every `<RUN_DIR>` and `<PR_NUMBER>` with the real values before sending: the
|
|
45
|
+
subagent has no shell variables from your session, so an unexpanded path means it
|
|
46
|
+
writes its findings where nothing will read them. Leave `<focus>` as written; the
|
|
47
|
+
contract tells the subagent to substitute it.
|
|
48
|
+
|
|
49
|
+
Parts 1, 2, and 3 go every time. The contract is what makes the output parseable by
|
|
50
|
+
`magpie dedupe`, and the focus blocks are what keep the five reviews from collapsing
|
|
51
|
+
into the same generic pass. Do not paraphrase, summarise, or trim either one.
|
|
19
52
|
|
|
20
53
|
## Output Contract
|
|
21
54
|
|
|
@@ -66,7 +99,7 @@ Good:
|
|
|
66
99
|
- `Observation: <one idea, what the diff actually does and where>`
|
|
67
100
|
- `Why it matters: <impact at realistic scale or on a real user path>`
|
|
68
101
|
- `Suggested direction: <one concrete next step, optional if the fix isn't obvious>`
|
|
69
|
-
- `Needs verification: <what you couldn't confirm from the bundle, optional, low/medium severity only>` This labelled paragraph is the only channel for uncertainty: never hedge inside another section, and never raise `severity` to compensate for what you couldn't verify (a blocker/high you cannot stand behind is not a blocker/high). Use the exact `Needs verification:` prefix, not inline phrasing.
|
|
102
|
+
- `Needs verification: <what you couldn't confirm from the bundle, optional, low/medium severity only>` This labelled paragraph is the only channel for uncertainty: never hedge inside another section, and never raise `severity` to compensate for what you couldn't verify (a blocker/high you cannot stand behind is not a blocker/high). Use the exact `Needs verification:` prefix, not inline phrasing. When the codebase-intelligence tools are available and one of them could answer the question, look before you hedge. A question you resolved is not a `Needs verification:` paragraph, it is evidence: cite the file:line you found under `Observation:` and omit the paragraph entirely.
|
|
70
103
|
|
|
71
104
|
One idea per paragraph. Do not collapse them into a single wall of text. Do not invent extra labels. If a section doesn't apply, omit it. The interactive report and the GitHub comment both parse these labels and render them as section headers, so missing labels degrade the output.
|
|
72
105
|
|
|
@@ -79,6 +112,47 @@ One idea per paragraph. Do not collapse them into a single wall of text. Do not
|
|
|
79
112
|
|
|
80
113
|
If you have no findings, write []. Return as your final tool result a single line: `<focus>: <N> findings (<blocker>/<high>/<medium>/<low>)`. Do not include other prose.
|
|
81
114
|
|
|
115
|
+
## Codebase intelligence
|
|
116
|
+
|
|
117
|
+
Include this block as part 4 only when the context stage logged `codeIntelligence: true`.
|
|
118
|
+
|
|
119
|
+
```magpie-codebase-intelligence
|
|
120
|
+
## Codebase intelligence
|
|
121
|
+
|
|
122
|
+
You have code-intelligence MCP tools against an index of this exact worktree,
|
|
123
|
+
including the PR's own changes. Call `bind_workspace` with `<RUN_DIR>/worktree` before
|
|
124
|
+
your first query.
|
|
125
|
+
|
|
126
|
+
These answer the cross-file questions a diff cannot:
|
|
127
|
+
|
|
128
|
+
- `ask_code`: a natural-language question, answered with grounded evidence. Start here
|
|
129
|
+
when you do not yet know which symbol to pivot on.
|
|
130
|
+
- `find_references` / `get_call_hierarchy`: who calls this, and what does it call.
|
|
131
|
+
Use for reachability: is the path you are worried about actually reachable.
|
|
132
|
+
- `find_affected_code`: the full reverse-dependency set for a symbol. Use for blast
|
|
133
|
+
radius before claiming a change is safe or unsafe.
|
|
134
|
+
- `trace_data_flow`: follow a value from its origin to where it is used. Use to
|
|
135
|
+
confirm that untrusted input actually reaches the sink you are worried about.
|
|
136
|
+
- `search_code`: hybrid semantic and literal search. Use to check whether something
|
|
137
|
+
already exists before claiming the PR should add it.
|
|
138
|
+
- `get_definition`: the body of a symbol the diff calls but does not show.
|
|
139
|
+
- `explore_dependency_graph`: module-level edges. Use for cycles and boundaries.
|
|
140
|
+
- `find_tests_for_symbol`: whether the symbol you are flagging is covered.
|
|
141
|
+
|
|
142
|
+
Rules:
|
|
143
|
+
|
|
144
|
+
- Cite what you find as ordinary `file:line` evidence under `Observation:`. Do not
|
|
145
|
+
say "code intelligence told me"; the location is the evidence.
|
|
146
|
+
- Verify before you report. A finding you could have refuted with one query and did
|
|
147
|
+
not is worse than no finding: it costs the author trust and the critic a slot.
|
|
148
|
+
- Verify before you hedge. If a tool can answer the question, a `Needs verification:`
|
|
149
|
+
paragraph is a failure to look, not honest uncertainty.
|
|
150
|
+
- If a tool returns `indexing_in_progress`, finish reading the diff and retry once.
|
|
151
|
+
If it is still not ready, review from the diff and worktree alone. Do not block.
|
|
152
|
+
- Never call `approve_indexing`. Never call `refresh_index`. Starting a full index is
|
|
153
|
+
a consent-gated operation that is not yours to start.
|
|
154
|
+
```
|
|
155
|
+
|
|
82
156
|
## Focus blocks
|
|
83
157
|
|
|
84
158
|
### security
|
|
@@ -132,6 +206,7 @@ For each potential finding:
|
|
|
132
206
|
2. Identify the trust boundary: is this crossing from untrusted to trusted context?
|
|
133
207
|
3. Assess exploitability: can an attacker realistically trigger this?
|
|
134
208
|
4. Evaluate impact: what's the blast radius if exploited?
|
|
209
|
+
5. Confirm the flow with `trace_data_flow` from the entry point to the sink before reporting. A taint path you asserted but did not trace is a guess.
|
|
135
210
|
|
|
136
211
|
**Risk guide:**
|
|
137
212
|
- blocker: Realistic path to remote code execution, auth bypass, data breach, or privilege escalation
|
|
@@ -196,6 +271,8 @@ For each potential bug:
|
|
|
196
271
|
3. What's the consequence: crash, data corruption, silent wrong behavior?
|
|
197
272
|
4. Is there an existing guard I'm not seeing?
|
|
198
273
|
|
|
274
|
+
Question 4 is answerable: `get_call_hierarchy` on the changed symbol shows every caller, and `find_references` shows where the guard would have to live. Check before you file.
|
|
275
|
+
|
|
199
276
|
**Risk guide:**
|
|
200
277
|
- blocker: Data loss, data corruption, broken auth/session behavior, or consistently crashing a major workflow
|
|
201
278
|
- high: Reachable incorrect behavior, race, resource leak, or crash in a meaningful workflow
|
|
@@ -254,6 +331,7 @@ For each potential issue:
|
|
|
254
331
|
2. How often does this code path execute? (once on init vs. every keystroke)
|
|
255
332
|
3. What's the measurable impact? (milliseconds vs. seconds)
|
|
256
333
|
4. Is the optimization worth the complexity cost?
|
|
334
|
+
5. Establish the call frequency with `find_affected_code` before claiming a path is hot. "Called from one cold init path" and "called per keystroke" are different findings.
|
|
257
335
|
|
|
258
336
|
**Risk guide:**
|
|
259
337
|
- blocker: Change can make a major workflow unusable or cause unbounded production resource exhaustion
|
|
@@ -315,6 +393,7 @@ For each potential smell:
|
|
|
315
393
|
2. Confirm the smell is introduced or materially worsened by this PR, not merely pre-existing nearby code.
|
|
316
394
|
3. Suggest the smallest refactor that fits the surrounding codebase patterns.
|
|
317
395
|
4. Weigh the cost: do not ask for a new abstraction unless it reduces real duplication, coupling, or reasoning burden now.
|
|
396
|
+
5. Before claiming the PR duplicates something or should reuse an existing helper, find it with `search_code`. Name the file:line of the thing it should have reused, or do not make the claim.
|
|
318
397
|
|
|
319
398
|
**Risk guide:**
|
|
320
399
|
- blocker: Smell creates a high-risk maintenance trap likely to cause defects across modules soon
|
|
@@ -373,6 +452,7 @@ For each potential issue:
|
|
|
373
452
|
2. Is this coupling necessary or incidental?
|
|
374
453
|
3. Would a new team member understand where to make changes?
|
|
375
454
|
4. Is this over-engineered for the current requirements, or appropriately future-proofed?
|
|
455
|
+
5. Confirm boundary and cycle claims with `explore_dependency_graph` on the touched modules. A cycle you inferred from import statements in the diff may already be broken by an interface you cannot see.
|
|
376
456
|
|
|
377
457
|
**Risk guide:**
|
|
378
458
|
- blocker: Change introduces a serious boundary violation or contract break likely to cascade across subsystems
|
|
@@ -45,3 +45,24 @@ test('fetchPr returns ok=false when gh exits non-zero', async () => {
|
|
|
45
45
|
expect(result.error).toBeDefined()
|
|
46
46
|
}
|
|
47
47
|
})
|
|
48
|
+
|
|
49
|
+
test('PR_VIEW_FIELDS requests commit messages and linked issues', async () => {
|
|
50
|
+
const { PR_VIEW_FIELDS } = await import('../gh.ts')
|
|
51
|
+
const fields = PR_VIEW_FIELDS.split(',')
|
|
52
|
+
expect(fields).toContain('commits')
|
|
53
|
+
expect(fields).toContain('closingIssuesReferences')
|
|
54
|
+
// The pre-existing fields must survive the edit.
|
|
55
|
+
for (const field of ['number', 'title', 'body', 'url', 'files', 'headRefOid']) {
|
|
56
|
+
expect(fields).toContain(field)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('fetchPr carries commits and linked issues into pr.json', async () => {
|
|
61
|
+
const result = await fetchPr({ ghBin: FAKE_GH, prNumber: 1234, runDir })
|
|
62
|
+
expect(result.ok).toBe(true)
|
|
63
|
+
const pr = JSON.parse(await readFile(join(runDir, 'pr.json'), 'utf8'))
|
|
64
|
+
expect(Array.isArray(pr.commits)).toBe(true)
|
|
65
|
+
expect(pr.commits[0].messageHeadline).toBe('Add retry handling to the upload path')
|
|
66
|
+
expect(Array.isArray(pr.closingIssuesReferences)).toBe(true)
|
|
67
|
+
expect(pr.closingIssuesReferences[0]).toMatchObject({ number: 42 })
|
|
68
|
+
})
|
|
@@ -208,3 +208,19 @@ test('bundled fixture covers all five focus domains', async () => {
|
|
|
208
208
|
expect(html).toContain(`data-domain="${domain}"`)
|
|
209
209
|
}
|
|
210
210
|
})
|
|
211
|
+
|
|
212
|
+
test('bundled fixture brief.json renders the PR brief header', async () => {
|
|
213
|
+
await runPreview({
|
|
214
|
+
page: 'findings',
|
|
215
|
+
stage: DEFAULT_STAGE,
|
|
216
|
+
outDir: workDir,
|
|
217
|
+
openInBrowser: false,
|
|
218
|
+
})
|
|
219
|
+
const html = await readFile(join(workDir, 'findings.html'), 'utf8')
|
|
220
|
+
expect(html).toContain('class="pr-brief"')
|
|
221
|
+
// Unique to fixtures/example-pr/brief.json; would not appear from any other
|
|
222
|
+
// part of the fixture or the page shell.
|
|
223
|
+
expect(html).toContain(
|
|
224
|
+
'Adds bounded retries to the upload path so transient S3 failures stop surfacing to end users as hard errors.',
|
|
225
|
+
)
|
|
226
|
+
})
|
|
@@ -84,3 +84,48 @@ test('refreshFindings sets data-run-id from the run directory basename', async (
|
|
|
84
84
|
const expectedId = runDir.split('/').pop()
|
|
85
85
|
expect(fresh).toContain(`data-run-id="${expectedId}"`)
|
|
86
86
|
})
|
|
87
|
+
|
|
88
|
+
test('refreshFindings carries the brief header through, so archives keep it on re-render', async () => {
|
|
89
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
90
|
+
await writeFile(
|
|
91
|
+
join(runDir, 'brief.json'),
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
purpose: 'Adds bounded retries to the upload path.',
|
|
94
|
+
changes: ['Wraps the S3 put in a bounded retry'],
|
|
95
|
+
subsystems: [{ name: 'upload', role: 'owns the put path' }],
|
|
96
|
+
watchItems: [],
|
|
97
|
+
unclear: [],
|
|
98
|
+
}),
|
|
99
|
+
)
|
|
100
|
+
const result = await refreshFindings(runDir)
|
|
101
|
+
expect(result.refreshed).toBe(true)
|
|
102
|
+
const fresh = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
103
|
+
expect(fresh).toContain('class="pr-brief"')
|
|
104
|
+
expect(fresh).toContain('Adds bounded retries to the upload path.')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('refreshFindings omits the brief header when brief.json is absent', async () => {
|
|
108
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
109
|
+
await refreshFindings(runDir)
|
|
110
|
+
const fresh = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
111
|
+
expect(fresh).not.toContain('class="pr-brief"')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('refreshFindings carries linked issues from pr.json through the brief header', async () => {
|
|
115
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
116
|
+
await writeFile(
|
|
117
|
+
join(runDir, 'pr.json'),
|
|
118
|
+
JSON.stringify({
|
|
119
|
+
number: 1234,
|
|
120
|
+
headRefName: 'feature-x',
|
|
121
|
+
headRefOid: 'deadbeef',
|
|
122
|
+
closingIssuesReferences: [
|
|
123
|
+
{ number: 42, title: 'Uploads fail', url: 'https://example.test/issues/42' },
|
|
124
|
+
],
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
await writeFile(join(runDir, 'brief.json'), JSON.stringify({ purpose: 'Fixes uploads.' }))
|
|
128
|
+
await refreshFindings(runDir)
|
|
129
|
+
const fresh = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
130
|
+
expect(fresh).toContain('https://example.test/issues/42')
|
|
131
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, expect, test } from 'bun:test'
|
|
2
|
-
import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'
|
|
3
3
|
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { runRender } from '../render-cmd.ts'
|
|
@@ -115,3 +115,80 @@ test('findings render includes files and diff when provided', async () => {
|
|
|
115
115
|
expect(html).toContain('data-file-pane="src/app.ts"')
|
|
116
116
|
expect(html).toContain('data-file-pane="src/util.ts"')
|
|
117
117
|
})
|
|
118
|
+
|
|
119
|
+
test('findings render includes the brief header when brief.json is present', async () => {
|
|
120
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
121
|
+
await writeFile(
|
|
122
|
+
join(runDir, 'brief.json'),
|
|
123
|
+
JSON.stringify({
|
|
124
|
+
purpose: 'Adds bounded retries to the upload path.',
|
|
125
|
+
changes: ['Wraps the S3 put in a bounded retry'],
|
|
126
|
+
subsystems: [{ name: 'upload', role: 'owns the put path' }],
|
|
127
|
+
watchItems: [],
|
|
128
|
+
unclear: [],
|
|
129
|
+
}),
|
|
130
|
+
)
|
|
131
|
+
expect(await runRender(runDir, 'findings')).toBe(0)
|
|
132
|
+
const html = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
133
|
+
expect(html).toContain('class="pr-brief"')
|
|
134
|
+
expect(html).toContain('Adds bounded retries to the upload path.')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('findings render omits the brief header when brief.json is absent', async () => {
|
|
138
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
139
|
+
expect(await runRender(runDir, 'findings')).toBe(0)
|
|
140
|
+
const html = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
141
|
+
expect(html).not.toContain('class="pr-brief"')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('a malformed brief.json degrades to no header instead of failing the render', async () => {
|
|
145
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
146
|
+
await writeFile(join(runDir, 'brief.json'), '{ this is not json')
|
|
147
|
+
expect(await runRender(runDir, 'findings')).toBe(0)
|
|
148
|
+
const html = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
149
|
+
expect(html).not.toContain('class="pr-brief"')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('linked issues from pr.json render in the brief header', async () => {
|
|
153
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
154
|
+
await writeFile(
|
|
155
|
+
join(runDir, 'pr.json'),
|
|
156
|
+
JSON.stringify({
|
|
157
|
+
number: 1234,
|
|
158
|
+
headRefName: 'feature-x',
|
|
159
|
+
headRefOid: 'deadbeef',
|
|
160
|
+
closingIssuesReferences: [
|
|
161
|
+
{ number: 42, title: 'Uploads fail', url: 'https://example.test/issues/42' },
|
|
162
|
+
],
|
|
163
|
+
}),
|
|
164
|
+
)
|
|
165
|
+
await writeFile(join(runDir, 'brief.json'), JSON.stringify({ purpose: 'Fixes uploads.' }))
|
|
166
|
+
expect(await runRender(runDir, 'findings')).toBe(0)
|
|
167
|
+
const html = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
168
|
+
expect(html).toContain('https://example.test/issues/42')
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('issue entries with a missing, zero, or negative number are dropped, not defaulted to 0', async () => {
|
|
172
|
+
await writeFile(join(runDir, 'findings.final.json'), JSON.stringify([]))
|
|
173
|
+
await writeFile(
|
|
174
|
+
join(runDir, 'pr.json'),
|
|
175
|
+
JSON.stringify({
|
|
176
|
+
number: 1234,
|
|
177
|
+
headRefName: 'feature-x',
|
|
178
|
+
headRefOid: 'deadbeef',
|
|
179
|
+
closingIssuesReferences: [
|
|
180
|
+
{ number: 42, title: 'Uploads fail', url: 'https://example.test/issues/42' },
|
|
181
|
+
{ title: 'Missing number', url: 'https://example.test/issues/missing' },
|
|
182
|
+
{ number: 0, title: 'Zero number', url: 'https://example.test/issues/zero' },
|
|
183
|
+
{ number: -1, title: 'Negative number', url: 'https://example.test/issues/negative' },
|
|
184
|
+
],
|
|
185
|
+
}),
|
|
186
|
+
)
|
|
187
|
+
await writeFile(join(runDir, 'brief.json'), JSON.stringify({ purpose: 'Fixes uploads.' }))
|
|
188
|
+
expect(await runRender(runDir, 'findings')).toBe(0)
|
|
189
|
+
const html = await readFile(join(runDir, 'screen', 'findings.html'), 'utf8')
|
|
190
|
+
expect(html).toContain('https://example.test/issues/42')
|
|
191
|
+
expect(html).not.toContain('https://example.test/issues/missing')
|
|
192
|
+
expect(html).not.toContain('https://example.test/issues/zero')
|
|
193
|
+
expect(html).not.toContain('https://example.test/issues/negative')
|
|
194
|
+
})
|