opencode-plugin-zooplankton
Advanced tools
| --- | ||
| description: Review an existing pull request — run a Phase 4 review round on a PR you did not author, with consolidated feedback posted back to the PR. Re-running the command on the same PR advances the round counter and posts resolution replies to prior-round comments. | ||
| --- | ||
| # /zooplankton-pr-review | ||
| Review an existing GitHub pull request. Unlike `/zooplankton-orchestrate`, this command does **not** implement, revise, merge, or run a retrospective — it executes a Phase-4-style review round against an arbitrary PR (typically authored by another contributor or another agent run) and posts one consolidated review per role-group. Re-running the command against the same PR advances the round counter (Round 2, 3, …), applies false-positive triage, and posts threaded resolution replies to prior-round inline comments. | ||
| ## Usage | ||
| ```text | ||
| /zooplankton-pr-review <PR_NUMBER> | ||
| ``` | ||
| `<PR_NUMBER>` is the PR number on the configured repo (e.g. `1234`). The command reads `project.repo` from `zooplankton.json` to identify the GitHub repository. | ||
| ## Preconditions | ||
| - Platform must be `github` (resolved per `references/agent-config.md`). Local mode is not supported — abort with a clear message if `project.platform` resolves to `local`. | ||
| - `zooplankton.json` must be present and parseable; `project.repo` must be set (used to scope `gh pr` calls). | ||
| - The orchestrator must have read access to the PR (`gh pr view <PR_NUMBER>` succeeds) and write access for posting reviews (`gh api -X POST .../pulls/:n/reviews`). Use the `coreReviewer` GitHub account from `zooplankton-local.json` if mapped; otherwise the `default` account. | ||
| - The PR must not be in `MERGED` or `CLOSED` state — abort with `PR <n> is <state>; nothing to review.` | ||
| - An `origin` remote must exist locally (the orchestrate worktree-setup model assumes it). | ||
| ## Phase A — Resolve PR and Working Tree | ||
| 1. **Capture metadata.** Run `gh pr view <PR_NUMBER> --json number,headRefName,headRefOid,baseRefName,state,author,title,url` and bind: | ||
| - `PR_NUMBER`, `PR_URL`, `PR_TITLE` | ||
| - `BRANCH = headRefName` | ||
| - `HEAD_SHA = headRefOid` | ||
| - `BASE_BRANCH = baseRefName` | ||
| 2. **Fetch the head ref.** `git fetch origin "$BRANCH"` so the orchestrator and reviewer worktrees can resolve `origin/$BRANCH` and `HEAD_SHA`. | ||
| 3. **Capture the diff once.** `gh pr diff <PR_NUMBER> > /tmp/zooplankton-pr-<PR_NUMBER>-diff.patch` for use in anchor-validation remediation prompts (see `references/review-protocol.md` → **Anchor Validation**). | ||
| 4. **Detect round number.** Read `.zooplankton-state.json` at the repo root. Find the entry keyed by `pr-review.<PR_NUMBER>` (containing the rounds array `[{round, fingerprint, postedAt, clusterCommentMap, findings, reviewerSessions}, …]`). If no entry exists, set `CURRENT_ROUND = 1` and `PRIOR_ROUND_STATE = null`. If an entry exists, set `CURRENT_ROUND = max(rounds[].round) + 1` and bind `PRIOR_ROUND_STATE` to the most-recent rounds entry (its `clusterCommentMap` and `findings` drive Phase D.5 resolution replies; its `reviewerSessions` drives Phase C session reuse). If the file is absent or malformed, treat as no prior state, log `pr-review: state file missing/invalid; treating as Round 1`, and create/repair on next write in Phase D.9. | ||
| > **Note:** Reviewer worktree setup is deferred to **after** Phase B step 3 builds `REVIEWERS_THIS_ROUND`, so we only create worktrees for the reviewers actually invoked this round. See Phase B step 4. | ||
| ## Phase B — Pre-flight Reviewer Enumeration | ||
| Follow the same enumeration protocol as orchestrate Phase 4a.0 (see `skills/orchestrate/SKILL.md` → **4a.0 — Pre-flight Reviewer Enumeration**), with one adaptation: the touched-files check must use the PR diff, not a local branch comparison. | ||
| 1. **Resolve roster** from `agents.coreReviewers`, `agents.reviewers`, `agents.frontendReviewers`, and `agents.securityReviewers` in `zooplankton.json`. Drop entries with `disabled: true`. Treat `null` roles as empty. | ||
| 2. **Decide frontend inclusion** by inspecting the touched-files list: `gh pr view <PR_NUMBER> --json files --jq '.files[].path'`. Apply the same heuristic as orchestrate (`src/components/`, `src/pages/`, `app/`, `.tsx`/`.vue`/`.svelte`/`.css`/`.scss`). | ||
| 3. **Build `REVIEWERS_THIS_ROUND`** in order: core reviewers (blocking), invoked frontend reviewers (blocking), security reviewers (blocking), normal reviewers (non-blocking). Security reviewers are invoked as a regular review role here — the BLOCK feedback loop (orchestrate Phase 5) is not run, but their findings are still posted as a consolidated review like any other role-group. | ||
| 4. **Set up reviewer worktrees** (deferred from Phase A). For each reviewer in `REVIEWERS_THIS_ROUND`, follow `skills/orchestrate/references/worktree-setup.md` to create `.worktrees/<branch>/<reviewer-name>` checked out at `HEAD_SHA`. Reuse an existing worktree if it is already at `HEAD_SHA`; otherwise recreate. | ||
| 5. **Emit the enumeration line** verbatim, substituting `CURRENT_ROUND` from Phase A step 4: | ||
| ```text | ||
| Phase 4a.0: round=<CURRENT_ROUND> reviewers=[…] blocking=[…] non_blocking=[…] frontend_touched=<true|false> pr=<PR_NUMBER> | ||
| ``` | ||
| 6. **Single-message-invocation rule applies.** All Task calls in Phase C must be issued in one assistant message. Splitting or dropping a reviewer is an error — abort and re-enter Phase B. | ||
| ## Phase C — Parallel Review Invocation | ||
| Issue exactly one assistant message containing one `Task` tool call per entry in `REVIEWERS_THIS_ROUND`. Each Task prompt must include: | ||
| - The reviewer's worktree path (`.worktrees/<branch>/<reviewer-name>`). | ||
| - `PR_NUMBER`, `PR_URL`, `BRANCH`, `HEAD_SHA`, `BASE_BRANCH`. | ||
| - The reviewer's role-specific guide reference (`guides/core-reviewer-guide.md`, `guides/reviewer-guide.md`, `guides/frontend-reviewer-guide.md`, or `guides/security-reviewer-guide.md`). | ||
| - `round = CURRENT_ROUND` and `review_kind: "code"`. | ||
| - Instruction to emit the structured JSON result block (see `references/review-protocol.md` → **Structured Reviewer Results**) including the REQUIRED `position` field on every line-anchored finding. | ||
| - **Round ≥ 2 only:** the prior-round findings list (cluster IDs, summaries, anchor info) extracted from `PRIOR_ROUND_STATE.findings`. The reviewer guide's Round 2+ protocol requires emitting `prior_issue_resolution[]` evaluations (`ADDRESSED` / `NOT_ADDRESSED` / `PARTIALLY_ADDRESSED`) for each prior finding, in addition to any new findings discovered in the current round. | ||
| Use `references/invocation-templates.md` → **Review Invocation Template** as the prompt skeleton, substituting `PR_NUMBER`, `HEAD_SHA`, and `CURRENT_ROUND` for the orchestrator-computed values from Phase A. Session reuse: pass `task_id` from `PRIOR_ROUND_STATE.reviewerSessions[<reviewer-name>]` when present (round ≥ 2) so reviewers retain context; fall back to a fresh session if the resumed session fails. Round 1 always starts a fresh session. | ||
| After the parallel-invocation message returns, perform the post-invocation self-check from 4a.0 step 6 (count of dispatched Task calls equals `len(REVIEWERS_THIS_ROUND)`). | ||
| ## Phase D — Consolidate, Anchor, Triage, Reply, Post | ||
| Run the standard Phase 4b/4d pipeline against the collected reviewer JSON results, with no changes to the protocol — `/zooplankton-pr-review` reuses orchestrate's review-posting and resolution-reply machinery exactly: | ||
| 1. **Parse** each reviewer's JSON via `parseReviewerResult()` from `.opencode/plugins/lib.js`. Apply fail-closed handling (see `references/review-protocol.md` → **Structured Reviewer Results**). For Round ≥ 2, also parse `prior_issue_resolution[]` from each reviewer. | ||
| 2. **Validate anchors on raw per-reviewer findings (pre-dedup).** For each reviewer, reject findings with missing/invalid `position` and re-invoke that reviewer once with the diff from `/tmp/zooplankton-pr-<PR_NUMBER>-diff.patch` per **Anchor Validation** — using the reviewer's own `ephemeral_id` values, which are still meaningful at this stage because dedup has not yet run. On second failure, drop those findings (do not abort). This step **must complete before** step 3 because consolidation discards `ephemeral_id`. | ||
| 3. **Dedup, sort, and assign cluster IDs** with the current round number: `R<CURRENT_ROUND>-C<nn>` (see **Finding Consolidation**). Findings carried forward from prior rounds receive **new** cluster IDs in this round's namespace — they are not reused from the prior round. The carry-forward link is recorded in the state file (Phase D.9) by mapping new cluster ID → prior cluster ID for traceability. | ||
| 4. **False-positive triage (§4d.FP).** Run the same conservative-bias FP triage as orchestrate Phase 4d before posting or replying. See `references/review-protocol.md` → **False-Positive Triage**. Build the triage map (`cluster_id → {CONFIRMED|FALSE_POSITIVE|<rationale>}`) — it drives both Phase D.5 reply rendering and Phase D.7 posting (FALSE_POSITIVE findings are excluded from the consolidated review body). | ||
| 5. **Round 2+ resolution replies (§4d.Replies).** *(Skip when `CURRENT_ROUND == 1` or `PRIOR_ROUND_STATE == null`.)* For each entry in `PRIOR_ROUND_STATE.clusterCommentMap` (`prior_cluster_id → comment_id`), look up the comment thread on the PR and post one threaded reply via `gh api -X POST .../pulls/<PR_NUMBER>/comments/<comment_id>/replies`. Reply template selection follows `references/review-protocol.md` → **Round 2+ Resolution Replies** exactly: | ||
| - If any current-round reviewer marked the prior cluster as `ADDRESSED` in `prior_issue_resolution[]` → "Resolved" template. | ||
| - If `NOT_ADDRESSED` AND the §4d.FP triage map flags the corresponding carried-forward cluster as `FALSE_POSITIVE` → "Dismissed as false positive" template, including the FP rationale. | ||
| - If `NOT_ADDRESSED` AND triage is `CONFIRMED` → "Carried forward" template referencing the new round's cluster ID. | ||
| - If `PARTIALLY_ADDRESSED` → "Partially addressed" template per protocol. | ||
| 6. **Compute fingerprint and check the marker** for idempotency (see **Fingerprint And Idempotency**). The fingerprint must include `CURRENT_ROUND` so re-runs on different rounds produce different fingerprints. If the same `(PR_NUMBER, fingerprint)` already exists in `.zooplankton-state.json`, skip posting and emit `pr-review: idempotent skip (round=<n> fingerprint=<hash>)`. | ||
| 7. **Render §2 narrative** as LLM-authored prose per **Consolidated Review Posting** §2 spec, **before** posting so the rendered narrative can be embedded in the review body. No bullet lists, no severity tables, no "Total findings" lines. For Round ≥ 2, the narrative should reference what changed since the prior round (which clusters were resolved, which were carried forward, which new issues surfaced). Render one narrative per role-group. | ||
| 8. **Post one consolidated review per role-group** via `gh api -X POST .../pulls/<PR_NUMBER>/reviews` per **Consolidated Review Posting**, forwarding reviewer-supplied `position` values directly and using the §2 narrative rendered in step 7 as the review body. Role-groups: `core-reviewers`, `frontend-reviewers` (if invoked), `security-reviewers` (if invoked), `normal-reviewers`. Exclude FALSE_POSITIVE findings from the body. Event selection: | ||
| - For `security-reviewers`: use `event: "REQUEST_CHANGES"` when any reviewer in the group emitted `security_verdict: "BLOCK"`, OR any (post-FP-triage) finding has severity `critical` or `high`. Otherwise use `event: "COMMENT"`. Security reviewers do not use `blocking` severity labels — `security_verdict` is the authoritative signal. | ||
| - For all other role-groups (`core-reviewers`, `frontend-reviewers`, `normal-reviewers`): use `event: "REQUEST_CHANGES"` when any (post-FP-triage) finding has severity `blocking`. Otherwise use `event: "COMMENT"`. | ||
| 9. **Persist round state.** After successful posting, build the new round's `clusterCommentMap` by fetching inline comments separately (the `POST /pulls/{n}/reviews` response returns the Review object only — it does **not** include inline comment IDs). For each role-group review just posted, capture the returned `review.id`, then run: | ||
| ```bash | ||
| gh api "repos/<REPO>/pulls/$PR_NUMBER/comments" --paginate \ | ||
| --jq "[.[] | select(.pull_request_review_id == <returned_review_id>)]" | ||
| ``` | ||
| Match each returned comment to its cluster by parsing the `[<cluster_id>]` prefix in the comment body, or by `(path, position)` against the posted findings — the same pattern documented in `references/review-protocol.md` → **Consolidated Review Posting**. Atomically update `.zooplankton-state.json` by appending a new entry under `pr-review.<PR_NUMBER>.rounds[]` containing: `{round: CURRENT_ROUND, fingerprint, postedAt: <ISO timestamp>, clusterCommentMap, findings: <array of {cluster_id, prior_cluster_id?, severity, summary, anchor_scope, path, line, position, fp_status}>, reviewerSessions: <map of name → task_id when known>}`. Use a temp-file-then-rename write so a crash mid-write cannot corrupt the file. **If the rename step fails (e.g. ENOENT, EACCES, cross-device), log a warning `pr-review: state persist failed (<error>); review already posted, next run will re-enter as Round 1` and continue — the consolidated review is already live on the PR. Note that the fingerprint includes `CURRENT_ROUND` (per Phase D.6), so the next run will compute a *different* fingerprint when it re-enters as Round 1 with no prior state. The fingerprint-idempotency check only prevents same-round duplicates; it does **not** prevent posting a new Round 1 review after a state-persist failure. The trade-off is intentional: a duplicate Round 1 review on the PR is preferable to silently dropping subsequent rounds because state was corrupted.** | ||
| ## Phase E — Final Status | ||
| After posting, emit a one-paragraph status summary to the user (LLM-authored prose, not a table): | ||
| - The current round number (`CURRENT_ROUND`) and how it was determined (fresh PR vs. continuation from prior round). | ||
| - How many findings were posted per role-group, and (for Round ≥ 2) how many prior-round clusters were resolved, carried forward, or dismissed as false positives. | ||
| - Which reviewers contributed. | ||
| - The PR URL. | ||
| - A reminder that this command does not request a revision — if blocking findings exist, the PR author must address them via their own implementation flow (or run `/zooplankton-orchestrate` against the same branch). Re-running `/zooplankton-pr-review <PR_NUMBER>` after the author pushes new commits will advance to the next round. | ||
| Then halt. Do not loop, do not invoke a coder, do not re-run reviewers within the same invocation. | ||
| ## Out of Scope | ||
| - **No coder dispatch.** This command never invokes coders, never revises code, never amends commits. | ||
| - **No security gate / BLOCK loop.** Security reviewers are still invoked as part of Phase C and their findings are posted as a consolidated review, but the orchestrate Phase 5 BLOCK feedback loop (re-dispatch to coder until clean) is not run. Use `/zooplankton-orchestrate` if a full security gate with BLOCK iteration is required. | ||
| - **No merge.** This command never merges, never approves, never closes the PR. | ||
| - **No retrospective.** No `retro.md` is written. | ||
| - **No automatic looping.** Each invocation runs exactly one round. To advance to the next round, re-invoke the command after the PR author pushes new commits. | ||
| - **No plan review.** This command operates on an existing PR; it does not read or write plans. | ||
| For the full multi-round implement → review → merge → retrospective cycle, use `/zooplankton-orchestrate` instead. |
@@ -143,5 +143,10 @@ # Core Reviewer Guide | ||
| > **GitHub mode:** For each finding with `anchor_scope: "line"`, confirm the target line appears in the diff. Run `gh pr diff $PR_NUMBER` and find the file's hunk. Only lines with a `+` prefix (added) or ` ` prefix (context) on the RIGHT side are valid targets. Lines with `-` prefix (removed) are LEFT-side only and will be rejected by GitHub. If the line number is not in any hunk, downgrade the finding to `anchor_scope: "file"` with `line: null` rather than guessing — the orchestrator will re-validate but cannot recover an invented line. | ||
| > **GitHub mode:** For each finding with `anchor_scope: "line"`, you MUST do TWO things before emitting the finding: | ||
| > | ||
| > 1. **Confirm the line is postable.** Run `gh pr diff $PR_NUMBER` and find the file's hunk. Only lines with a `+` prefix (added) or ` ` prefix (context) on the RIGHT side are valid targets. Lines with `-` prefix (removed) are LEFT-side only and will be rejected by GitHub. If the line number is not in any hunk, downgrade the finding to `anchor_scope: "file"` with `line: null` and `position: null` rather than guessing. | ||
| > 2. **Compute the diff `position` integer.** GitHub's `POST /pulls/{n}/reviews` endpoint requires `position` (a diff offset), not `line`. From the same `gh pr diff $PR_NUMBER` output, locate the file's first `@@ ... @@` hunk header. The line **immediately below** that header is `position: 1`. The next line is `position: 2`. Continue counting context, `+`, and `-` lines (and additional `@@` separator lines within the same file) through to your target line. The `position` you record in the JSON is that count. Re-counting is fine — accuracy matters more than speed. | ||
| > | ||
| > Both `line` (file line number, right side, post-change) and `position` (diff offset integer ≥ 1) MUST be populated for every `anchor_scope: "line"` finding. **The orchestrator will reject any line-anchored finding whose `position` is missing, `null`, zero, or non-integer, and re-invoke you with a remediation prompt asking for a complete re-output of the JSON. There is no silent fallback to file-scope.** | ||
| > **Local mode:** No external posting, so anchor validation is best-effort. Still prefer `anchor_scope: "file"` when you cannot confirm the line is in the diff. | ||
| > **Local mode:** No external posting, so `position` computation is unnecessary — set `position: null` for line-anchored findings. Anchor validation is best-effort. Still prefer `anchor_scope: "file"` when you cannot confirm the line is in the diff. | ||
@@ -195,5 +200,6 @@ ### Step 9: Emit structured output | ||
| - `anchor_scope`: | ||
| - `"line"` → string `path` + integer `line` (right-side, post-change) **required**. | ||
| - `"file"` → string `path`; `line` **must be `null`**. | ||
| - `"branch"` → `path` and `line` **must both be `null`**. | ||
| - `"line"` → string `path` + integer `line` (right-side, post-change) + integer `position` (diff offset, ≥ 1) **all required in GitHub mode**. In local mode, `position` may be `null`. | ||
| - `"file"` → string `path`; `line` and `position` **must both be `null`**. | ||
| - `"branch"` → `path`, `line`, and `position` **must all be `null`**. | ||
| - `position`: GitHub diff offset integer (≥ 1). REQUIRED for `anchor_scope: "line"` in GitHub mode — the orchestrator passes this directly to `POST /pulls/{n}/reviews` and rejects findings missing a valid integer `position`. See Step 8 for how to compute it from `gh pr diff $PR_NUMBER`. MUST be `null` for `"file"` and `"branch"` scopes. | ||
| - `area`: `logic | tests | docs | perf | style | deps`. | ||
@@ -239,2 +245,3 @@ - `ephemeral_id`: per-payload-only (e.g. `"f1"`). The orchestrator assigns cross-reviewer cluster IDs (`R<round>-C<nn>`) during consolidation — do **not** attempt to assign global IDs yourself. | ||
| "line": 42, | ||
| "position": 17, | ||
| "area": "logic", | ||
@@ -241,0 +248,0 @@ "summary": "Null deref when `config.repo` is absent", |
@@ -64,2 +64,9 @@ # Frontend Reviewer Guide | ||
| ### Step 7: Validate anchors and compute diff position (TWO things) | ||
| Same as the other reviewer roles — follow `core-reviewer-guide.md` **Step 8: Validate anchors and compute diff position** exactly. The TWO things you must do for every `anchor_scope: "line"` finding: | ||
| 1. **Validate the anchor** is on the right side of the PR diff (post-change), inside a hunk reachable from `gh pr diff $PR_NUMBER`. If not, downgrade the finding to `anchor_scope: "file"` (with `line: null`, `position: null`) or `"branch"` (also `path: null`). | ||
| 2. **Compute the integer `position`** (≥ 1) by counting from the file's first `@@` hunk header — line immediately below = `position: 1`, next = `position: 2`, etc. Include context, `+`, `-`, and additional `@@` separators within the same file. The orchestrator forwards this value verbatim to GitHub's `POST /pulls/{n}/reviews` and **rejects findings missing a valid integer `position`** — there is no silent file-scope fallback. | ||
| > **GitHub mode:** **You do not post to GitHub** — the orchestrator consolidates all reviewer JSON and posts one review per round. | ||
@@ -95,5 +102,6 @@ | ||
| - `anchor_scope`: | ||
| - `"line"` → string `path` + integer `line` required (right-side, post-change). | ||
| - `"file"` → string `path`; `line` must be `null`. | ||
| - `"branch"` → `path` and `line` must both be `null`. | ||
| - `"line"` → string `path` + integer `line` (right-side, post-change) + integer `position` (diff offset, ≥ 1) **all required in GitHub mode**. In local mode, `position` may be `null`. | ||
| - `"file"` → string `path`; `line` and `position` must both be `null`. | ||
| - `"branch"` → `path`, `line`, and `position` must all be `null`. | ||
| - `position`: GitHub diff offset integer (≥ 1). REQUIRED for `anchor_scope: "line"` in GitHub mode — the orchestrator passes this directly to `POST /pulls/{n}/reviews` and rejects findings missing a valid integer `position`. To compute, run `gh pr diff $PR_NUMBER`, locate the file's first `@@` hunk header, and count: line immediately below = `position: 1`, next = `position: 2`, etc. (counting context, `+`, `-`, and additional `@@` separators within the same file). MUST be `null` for `"file"` and `"branch"` scopes. | ||
| - `area`: `logic | tests | docs | perf | style | deps`. | ||
@@ -124,2 +132,3 @@ - `ephemeral_id`: per-payload-only. The orchestrator assigns cross-reviewer cluster IDs during consolidation. | ||
| "line": null, | ||
| "position": null, | ||
| "area": "style", | ||
@@ -126,0 +135,0 @@ "summary": "Function mixes tabs and spaces" |
@@ -128,5 +128,10 @@ # Reviewer Guide | ||
| > **GitHub mode:** For each finding with `anchor_scope: "line"`, confirm the target line appears in the diff. Run `gh pr diff $PR_NUMBER` and find the file's hunk. Only lines with a `+` prefix (added) or ` ` prefix (context) on the RIGHT side are valid targets. Lines with `-` prefix (removed) are LEFT-side only and will be rejected by GitHub. If the line number is not in any hunk, downgrade the finding to `anchor_scope: "file"` with `line: null` — the orchestrator will re-validate but cannot recover an invented line. | ||
| > **GitHub mode:** For each finding with `anchor_scope: "line"`, you MUST do TWO things before emitting the finding: | ||
| > | ||
| > 1. **Confirm the line is postable.** Run `gh pr diff $PR_NUMBER` and find the file's hunk. Only lines with a `+` prefix (added) or ` ` prefix (context) on the RIGHT side are valid targets. Lines with `-` prefix (removed) are LEFT-side only and will be rejected by GitHub. If the line number is not in any hunk, downgrade the finding to `anchor_scope: "file"` with `line: null` and `position: null`. | ||
| > 2. **Compute the diff `position` integer.** GitHub's `POST /pulls/{n}/reviews` endpoint requires `position` (a diff offset), not `line`. From the same `gh pr diff $PR_NUMBER` output, locate the file's first `@@ ... @@` hunk header. The line **immediately below** that header is `position: 1`. The next line is `position: 2`. Continue counting context, `+`, and `-` lines (and additional `@@` separator lines within the same file) through to your target line. | ||
| > | ||
| > Both `line` (file line number, right side) and `position` (diff offset integer ≥ 1) MUST be populated for every `anchor_scope: "line"` finding. **The orchestrator will reject any line-anchored finding whose `position` is missing, `null`, zero, or non-integer, and re-invoke you with a remediation prompt asking for a complete re-output. There is no silent fallback to file-scope.** | ||
| > **Local mode:** No external posting, so anchor validation is best-effort. Still prefer `anchor_scope: "file"` when you cannot confirm the line is in the diff. | ||
| > **Local mode:** No external posting, so `position` computation is unnecessary — set `position: null` for line-anchored findings. Anchor validation is best-effort. Still prefer `anchor_scope: "file"` when you cannot confirm the line is in the diff. | ||
@@ -176,5 +181,6 @@ ### Step 7: Emit structured output | ||
| - `anchor_scope`: | ||
| - `"line"` → string `path` + integer `line` required (right-side, post-change). | ||
| - `"file"` → string `path`; `line` must be `null`. | ||
| - `"branch"` → `path` and `line` must both be `null`. | ||
| - `"line"` → string `path` + integer `line` (right-side, post-change) + integer `position` (diff offset, ≥ 1) **all required in GitHub mode**. In local mode, `position` may be `null`. | ||
| - `"file"` → string `path`; `line` and `position` must both be `null`. | ||
| - `"branch"` → `path`, `line`, and `position` must all be `null`. | ||
| - `position`: GitHub diff offset integer (≥ 1). REQUIRED for `anchor_scope: "line"` in GitHub mode — the orchestrator passes this directly to `POST /pulls/{n}/reviews` and rejects findings missing a valid integer `position`. See Step 6 for how to compute it from `gh pr diff $PR_NUMBER`. MUST be `null` for `"file"` and `"branch"` scopes. | ||
| - `area`: `logic | tests | docs | perf | style | deps`. | ||
@@ -214,2 +220,3 @@ - `ephemeral_id`: per-payload-only. | ||
| "line": null, | ||
| "position": null, | ||
| "area": "style", | ||
@@ -216,0 +223,0 @@ "summary": "Function mixes tabs and spaces" |
@@ -119,4 +119,9 @@ # Security Reviewer Guide | ||
| For each finding with `anchor_scope: "line"`, confirm the target line appears in the diff. Run `gh pr diff $PR_NUMBER` and find the file's hunk. Only lines with a `+` prefix (added) or ` ` prefix (context) on the RIGHT side are valid targets. Lines with `-` prefix (removed) are LEFT-side only and will be rejected by GitHub. If the line number is not in any hunk, downgrade the finding to `anchor_scope: "file"` with `line: null`. | ||
| For each finding with `anchor_scope: "line"`, you MUST do TWO things before emitting the finding: | ||
| 1. **Confirm the line is postable.** Run `gh pr diff $PR_NUMBER` and find the file's hunk. Only lines with a `+` prefix (added) or ` ` prefix (context) on the RIGHT side are valid targets. Lines with `-` prefix (removed) are LEFT-side only and will be rejected by GitHub. If the line number is not in any hunk, downgrade the finding to `anchor_scope: "file"` with `line: null` and `position: null`. | ||
| 2. **Compute the diff `position` integer.** GitHub's `POST /pulls/{n}/reviews` endpoint requires `position` (a diff offset), not `line`. From the same `gh pr diff $PR_NUMBER` output, locate the file's first `@@ ... @@` hunk header. The line **immediately below** that header is `position: 1`. The next line is `position: 2`. Continue counting context, `+`, and `-` lines (and additional `@@` separator lines within the same file) through to your target line. | ||
| Both `line` (file line number, right side) and `position` (diff offset integer ≥ 1) MUST be populated for every `anchor_scope: "line"` finding. **The orchestrator will reject any line-anchored finding whose `position` is missing, `null`, zero, or non-integer, and re-invoke you with a remediation prompt asking for a complete re-output. There is no silent fallback to file-scope.** | ||
| ### Step 6: Emit structured output | ||
@@ -167,3 +172,4 @@ | ||
| - `security_verdict` is **required** and must be `"BLOCK"` or `"PASS"` (never `null` for a security reviewer). BLOCK implies at least one critical or high finding. | ||
| - `anchor_scope`: `"line"` (path + line required), `"file"` (path required, line null), or `"branch"` (both null). | ||
| - `anchor_scope`: `"line"` (path + line + integer `position` ≥ 1 required in GitHub mode; `position` may be `null` in local mode), `"file"` (path required, `line` and `position` both null), or `"branch"` (`path`, `line`, `position` all null). | ||
| - `position`: GitHub diff offset integer (≥ 1). REQUIRED for `anchor_scope: "line"` in GitHub mode — the orchestrator passes this directly to `POST /pulls/{n}/reviews` and rejects findings missing a valid integer `position`. See Step 5 for how to compute it from `gh pr diff $PR_NUMBER`. MUST be `null` for `"file"` and `"branch"` scopes. | ||
| - `area` is free-form for security (e.g. `"authz"`, `"crypto"`, `"injection"`, `"supply-chain"`). | ||
@@ -195,2 +201,3 @@ - `ephemeral_id`: per-payload-only. The orchestrator assigns cluster IDs during consolidation. | ||
| "line": 88, | ||
| "position": 23, | ||
| "area": "injection", | ||
@@ -197,0 +204,0 @@ "summary": "Unescaped shell concatenation", |
@@ -5,25 +5,48 @@ # ZooplanktonAI Coding Standards | ||
| ## 1. Skeleton-then-Replace for Large Files | ||
| ## 1. Skeleton-then-Replace for Large Files (HARD STOP) | ||
| When writing or substantially rewriting a file that exceeds ~80 lines: | ||
| When writing or substantially rewriting a file that exceeds **~80 lines**, you MUST follow the skeleton-then-replace protocol. This is a hard rule, not a guideline. | ||
| 1. **First pass — skeleton:** Write the file structure with stub implementations (empty function bodies, placeholder comments, `// TODO` markers, `PLACEHOLDER_SECTION_X` blocks). This establishes the shape and makes review easy. | ||
| 2. **Second pass — fill in:** Replace each stub with the real implementation, one section at a time, using the Edit tool. | ||
| **Mandatory pre-Write self-check.** Before EVERY call to the Write tool, run this 3-step check in your reasoning. If you skip it, you have already violated this rule: | ||
| This prevents context-window blowout from dumping 200+ lines in a single Write call, and makes each chunk independently reviewable. | ||
| 1. **Estimate line count.** Count or honestly estimate the number of lines in the content you are about to write. Round up. | ||
| 2. **Compare to threshold.** Is the count > 80? | ||
| - **No** → proceed with Write in one shot. | ||
| - **Yes** → STOP. Do not call Write with the full content. Go to step 3. | ||
| 3. **Switch to skeleton-then-replace:** | ||
| 1. **First Write call:** Write only the file structure — section headings, function signatures with empty bodies, and `PLACEHOLDER_<UPPERCASE_NAME>` markers (one per section to be filled). The skeleton itself should be well under 80 lines; if it isn't, your sections are too coarse — split them further. | ||
| 2. **Subsequent Edit calls:** Replace each `PLACEHOLDER_<NAME>` with its real implementation, one at a time, using the Edit tool. Each Edit's `oldString` is the placeholder line; `newString` is the filled-in section. | ||
| 3. **Final verification:** Run `grep -n "PLACEHOLDER" <file>` and `wc -l <file>` to confirm zero remaining placeholders. | ||
| **Exception:** Files under ~80 lines can be written in one shot. | ||
| **Why this rule exists.** Single Write calls of 200+ lines cause: | ||
| - Context-window blowout in the orchestrator (the full file body is echoed in tool output). | ||
| - Loss of reviewability — there is no way to spot a subtle bug in a 300-line write that lands in one tool call. | ||
| - Silent truncation risk if the model output approaches its token limit mid-write. | ||
| **⚠ Common failure mode — do not do this:** Agents often violate this rule when the full content is "already in mind" during a large implementation task. Before calling Write on any file, count the expected lines. If > 80, you MUST write the skeleton first — no exceptions, even if the content feels ready. Writing 300 lines in one Write call is always wrong regardless of how clear the content is. | ||
| **⚠ Imperative — read this before every Write:** Agents most often violate this rule when "the full content is already in mind" during a large task. The feeling that the content is ready is NOT a license to skip the skeleton. It is the exact moment when you are most likely to violate the rule. Before EVERY Write call, perform the 3-step check above as an explicit reasoning step. Writing >80 lines in one Write call is **always** a rule violation, regardless of how clear, how short the timeline feels, or how confident you are. There are NO exceptions for "small" overruns (e.g. 100 lines is not "basically 80"). The threshold is 80, full stop. | ||
| ## 2. Prefer Edit Over Write for Existing Files | ||
| **Counter-pattern to avoid:** "I'll just write the whole thing this once because the structure is simple" → this is the exact rationalization that produces the violation. Do not. Use skeleton-then-replace. | ||
| When modifying an existing file: | ||
| ## 2. Prefer Edit Over Write for Existing Files (HARD STOP) | ||
| - **Always use the Edit tool** (targeted find-and-replace) instead of the Write tool (full file rewrite). | ||
| - Read the file first, identify the exact lines to change, then apply a surgical edit. | ||
| - Only fall back to Write when the changes are so pervasive that targeted edits would be harder to follow than a full rewrite (rare — typically >60% of lines changing). | ||
| When modifying an existing file, you MUST use the Edit tool unless a strict exception applies. This is a hard rule. | ||
| **Why:** Edit preserves unchanged lines exactly, avoids accidental deletions, and produces minimal diffs that are easy to review. | ||
| **Mandatory pre-modification self-check.** Before modifying any existing file, run this 4-step check: | ||
| 1. **Does the file already exist on disk?** Check with `ls` or your prior Read. If yes, go to step 2. If no (creating fresh), Rule 1 applies instead. | ||
| 2. **Default to Edit.** Plan the change as one or more targeted Edit calls. Read the file first if you have not already, identify the exact lines to change, and prepare `oldString`/`newString` pairs. | ||
| 3. **Quantify scope.** Estimate what fraction of total lines change. | ||
| - **≤ 60% of lines change** → use Edit (possibly multiple Edit calls). No Write fallback permitted. | ||
| - **> 60% of lines change** → Write fallback is *allowed but not required*. Prefer Edit even at high churn if the unchanged lines are non-trivial (e.g. preserved imports, license headers, exported symbols). | ||
| 4. **If you fall back to Write,** Rule 1's >80-line check still applies — write a skeleton first if the result will exceed 80 lines. | ||
| **Why this rule exists.** Edit: | ||
| - Preserves unchanged lines byte-for-byte (no accidental whitespace, comment, or import drift). | ||
| - Produces minimal, reviewable diffs. | ||
| - Avoids the catastrophic failure mode where a full-file Write silently drops a section the model "forgot" to include. | ||
| **⚠ Imperative — read this before every modification:** The temptation to "just rewrite the whole file" almost always indicates that you have not Read the file recently enough to know its current state. The correct response is to Read first, then Edit. Treat full-file Write on existing files as a code smell that requires explicit justification (the >60% threshold) — not a default. If you find yourself drafting a full-file Write to a file that exists, STOP and re-plan as a sequence of Edit calls. | ||
| **Counter-pattern to avoid:** "It's easier to just rewrite the file from scratch than figure out the exact edit." → that ease is illusory. The full-file rewrite hides exactly the kind of subtle drift (deleted import, renamed export, dropped comment) that breaks downstream code without an obvious diff signal. Use Edit. | ||
| ## 3. Prefer `master` Over `main` as Default Branch | ||
@@ -30,0 +53,0 @@ |
+1
-1
| { | ||
| "name": "opencode-plugin-zooplankton", | ||
| "version": "0.6.3", | ||
| "version": "0.6.4", | ||
| "description": "Unified global OpenCode plugin for ZooplanktonAI — coding standards, multi-agent skills (brainstorm / plan / orchestrate), commands, guides, and the autonomous-mode permission hook.", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -108,4 +108,67 @@ # Planning Gate Reference | ||
| 4. Local mode: skip PR creation and leave `PR_NUMBER` / `PR_URL` unset. | ||
| 5. Proceed to Phase 3 implementation. | ||
| 5. Proceed to Phase 2d. | ||
| PR creation must be idempotent: first check for an open PR with matching head branch and base branch. If more than one exists, warn and use the first. | ||
| ### PR description authoring (GitHub mode only) | ||
| The PR description (`gh pr create --body …` or `--body-file …`) MUST be **authored by the orchestrator's LLM as natural narrative prose** — it is not a fixed template, not a bullet list, and not a paste of the plan file's frontmatter. The PR author and reviewers should be able to read the description and immediately understand what the PR does, why, and what the review surface looks like. | ||
| **Required structure (3–6 paragraphs of prose, no headings other than the optional Plan link):** | ||
| 1. **What and why** (1–2 paragraphs) — explain the user-facing or developer-facing problem this PR addresses, and the chosen approach in plain language. Reference concrete components, files, or modules where it helps the reader. Avoid restating the task description verbatim — synthesize. | ||
| 2. **How** (1–2 paragraphs) — describe the implementation strategy at a high level: the key modules touched, any non-obvious design decisions, sequencing across commits if relevant. Do not enumerate every file changed — that is what the diff is for. | ||
| 3. **Risk and verification** (1 paragraph) — call out the riskiest part of the change, what testing was performed, and what the reviewer should pay closest attention to. If `plan_scope` is `simple` and there is little risk, a single sentence is fine. | ||
| 4. **Plan link** (optional final line) — a single line like `Plan: .plans/<plan-dir>/plan.md` for traceability. No other metadata block. | ||
| **Hard rules:** | ||
| - No bullet lists, no checklists, no `### Summary` / `### Changes` / `### Test Plan` headings, no severity tables, no "Reviewers: …" lines, no copy-paste of the plan file. Those defeat the purpose of a narrative description. | ||
| - No emoji prefixes (`✅`/`⚠️`/`🚀`). | ||
| - Do not include the zooplankton review marker — that lives in consolidated review bodies, not the PR description. | ||
| - Length: roughly 150–500 words. Shorter for trivial changes; longer for medium/large `plan_scope`. | ||
| - Pass the body via heredoc or `--body-file -` to avoid shell expansion of `$(...)`, backticks, or `$VAR` from the LLM-authored prose: | ||
| ```bash | ||
| gh pr create \ | ||
| --base master \ | ||
| --head "$BRANCH" \ | ||
| --title "$PR_TITLE" \ | ||
| --body-file - <<'EOF' | ||
| This PR introduces … | ||
| The implementation … | ||
| The riskiest part is … | ||
| Plan: .plans/2026-04-30-add-payments/plan.md | ||
| EOF | ||
| ``` | ||
| Always use `<<'EOF'` (single-quoted) — never `<<EOF`. The single-quoted heredoc prevents the shell from interpreting any character in the LLM-authored prose. | ||
| - **Never use `gh pr edit`** to mutate this description after creation. If the description needs updating after Phase 2c (e.g. scope changed mid-implementation), abort and re-run from Phase 2c rather than mutating the description out-of-band. | ||
| ## Phase 2d: Pre-Implementation Confirmation | ||
| After Phase 2c writes the plan, opens the PR (in GitHub mode), and posts the PR description, the orchestrator pauses for one final user confirmation **before any implementation work begins**. This is the last cheap escape hatch — once Phase 3 starts, code changes accumulate. | ||
| **Procedure:** | ||
| 1. **Compose a one-paragraph LLM summary** (3–5 sentences) capturing: | ||
| - What the plan will implement (1 sentence). | ||
| - The shape of the work — files/modules touched, rough task count, `plan_scope` (1–2 sentences). | ||
| - The PR link (GitHub mode) or branch name (local mode), so the user can preview the artifact before approving. | ||
| - The implicit ask: "Ready to proceed with implementation? (yes/no)". | ||
| This summary is **LLM-authored prose**, not a bullet list. It should be skim-able in 10 seconds. | ||
| 2. **Ask the user.** Print the summary, then ask `Ready to proceed with implementation? (yes/no)`. Wait for input. | ||
| 3. **Decision:** | ||
| - User responds `yes` (or any clearly-affirmative variant) → set `APPROVED_TO_IMPLEMENT = true`, log `Phase 2d: human confirmed → proceeding`, and continue to Phase 3. | ||
| - User responds with **anything other than a clearly-affirmative `yes`** (including `no`, a question, a comment, a request to amend the plan, or silence) → halt orchestration, log `Phase 2d: human declined → halting`, and surface a one-line message to the user: `Plan halted at Phase 2d. Amend the plan file (and the PR description, if applicable) directly, then re-run /zooplankton-orchestrate to restart from Phase 1. The next run will reuse the existing plan and PR.` Do **not** loop back to Phase 2a within the current session — the orchestrator's session has no mechanism to re-enter the planning gate; the user owns the amendment loop. | ||
| 4. **Auto-skip path.** When `AUTO_MODE=true` (set by the user via env var `ZOOPLANKTON_AUTO_MODE=1` or by passing `--auto` to the orchestrate command — distinct from `autonomousMode` in `zooplankton.json` which only governs permission auto-allow), skip the prompt entirely: log `Phase 2d: AUTO_MODE → confirmation skipped` and proceed. `AUTO_MODE` is opt-in per-run; it is not the default even when `autonomousMode: true`. | ||
| **Why this is separate from Phase 2b approval:** Phase 2b approves the *plan* (the document). Phase 2d approves *starting implementation* now (commit to spending review-loop budget). The two questions are different: the user may approve a plan but want to defer execution (e.g. wait for an upstream PR to merge first). |
@@ -46,7 +46,13 @@ # Review Protocol Reference | ||
| > **Ordering (critical):** Anchor validation runs on the **raw per-reviewer findings** *before* dedup/consolidation (see **Finding Consolidation** above), because the re-invocation step below references each finding's `ephemeral_id` — which is per-reviewer-payload and is discarded once findings are merged into cross-reviewer clusters. The orchestrator must: (a) validate every reviewer's findings independently, (b) re-invoke any reviewer whose payload contained rejected line-anchored findings (using *that* reviewer's `ephemeral_id`s in the remediation prompt), (c) re-validate the re-emitted payload, then (d) only then proceed to dedup/cluster-ID assignment. Skipping this order will cause the re-invocation prompt to reference IDs the reviewer no longer recognizes. | ||
| 1. Fetch the PR diff once per round with `gh pr diff $PR_NUMBER`. | ||
| 2. For `anchor_scope: "line"`, confirm the line is postable on the RIGHT side of the diff. | ||
| 3. If a line anchor is not postable, downgrade to `anchor_scope: "file"` and `line = null`. | ||
| 2. For `anchor_scope: "line"`, **reject the finding outright** if `position` is missing, `null`, zero, or non-integer. The reviewer must supply a valid integer `position` (≥ 1) computed from the diff. **There is no silent fallback to file-scope.** When this happens: | ||
| - Log a visible warning: `[anchor-validation] reviewer=<name> finding=<ephemeral_id> rejected: missing/invalid position`. | ||
| - Re-invoke **the same reviewer** (still operating on its raw, pre-consolidation payload) with a remediation prompt that (a) lists each rejected `ephemeral_id` and the reason, (b) includes the current `gh pr diff $PR_NUMBER` output verbatim, and (c) instructs the reviewer to re-emit a complete v1 JSON block with valid `position` integers for all `anchor_scope: "line"` findings (or to legitimately downgrade those findings to `anchor_scope: "file"` with `line: null` and `position: null` if no diff anchor exists). | ||
| - On the re-invocation's return, re-parse and re-validate the per-reviewer payload. If `position` is still missing/invalid on any line-anchored finding, drop those findings with an error log and proceed with the rest of the round; do not abort the orchestrate run. | ||
| 3. For `anchor_scope: "line"` findings whose `position` parses as a valid integer, additionally confirm the line is postable on the RIGHT side of the diff. If not postable (e.g. on the LEFT side or outside any hunk), drop the finding with a warning — again, no silent file-scope fallback. | ||
| 4. For file-scope findings, confirm the file appears in `gh pr view $PR_NUMBER --json files`; otherwise downgrade to branch-scope. | ||
| 5. Branch-scope findings render in the review body only. | ||
| 6. Only after every reviewer's payload has passed steps 1–5 (and any required re-invocations have been resolved) should the orchestrator move on to **Finding Consolidation** (dedup, clustering, cluster-ID assignment). Once consolidation has run, `ephemeral_id` is no longer meaningful — there is no second chance to re-request a reviewer for a missing position. | ||
@@ -119,3 +125,3 @@ ## Fingerprint And Idempotency | ||
| - Each `anchor_scope: "line"` finding becomes one entry in `comments[]`. **The `POST /pulls/{pull_number}/reviews` endpoint requires `comments[].position` (the diff position offset), not the file's `line`/`side`.** To resolve a file path + line number to a `position`, fetch the PR's unified diff with `gh pr diff $PR_NUMBER` and count: `position` is the count of lines from the file's first hunk header (`@@ ... @@`) through the target line in that diff, where the line **immediately below** the `@@` line is `position` 1, the next is `position` 2, and so on. Counting includes context, addition (`+`), and deletion (`-`) lines, and continues across additional hunks within the same file (the `@@` separator lines themselves are also counted). When the same line appears across re-hunks (e.g. on a force-push), recompute against the current diff before posting. | ||
| - Each `anchor_scope: "line"` finding becomes one entry in `comments[]`. **The `POST /pulls/{pull_number}/reviews` endpoint requires `comments[].position` (the diff position offset), not the file's `line`/`side`.** Reviewers compute `position` themselves from `gh pr diff $PR_NUMBER` and supply it in the JSON payload (see **Anchor Validation** above) — the orchestrator forwards each finding's `position` directly into `comments[].position` with no additional resolution step. When a reviewer omits or supplies an invalid `position`, the orchestrator rejects the finding and re-invokes the reviewer rather than guessing the offset itself. When the diff changes (e.g. on a force-push) so previously-valid positions are no longer accurate, treat all line-anchored findings on the affected files as needing revalidation in the next round; the current round's already-validated positions remain authoritative for the post-in-flight. | ||
| - Each `anchor_scope: "file"` and `anchor_scope: "branch"` finding is rendered into the review `body` only — GitHub has no per-file anchor for review comments outside hunks, so file/branch-scope findings live in the body's "File-/branch-scope findings" section. | ||
@@ -130,15 +136,14 @@ | ||
| 1. **Zooplankton marker** — `<!-- zooplankton-orchestrate: pr=<n> round=<n> fingerprint=<sha> -->`. Always first, on its own line. | ||
| 2. **Summary header** — derived deterministically (no LLM) from the consolidated cluster list: | ||
| - **Total finding count** — `Total findings: <n>`. | ||
| - **Severity breakdown** — one row per non-zero severity. Only the two valid values `blocking` and `advisory` (the reviewer schema defines no others). Omit a row whose count is zero. | ||
| - **Why it matters** — a single fixed-template sentence generated from the **blocking** findings only: | ||
| - Collect the unique `area` values from all `severity === "blocking"` findings, preserving first-occurrence order. Fall back to the finding's `path` when `area` is absent; if both are absent, skip that finding for this computation only (it still counts toward the severity total). | ||
| - If the resulting list is non-empty: emit `Blocking issues span: <areas>. See inline comments for details.` (`<areas>` is the comma-separated list, no Oxford comma, no trailing period inside the list). | ||
| - If the list is empty (zero blocking findings): omit this sentence entirely. Do **not** emit a placeholder like "No blocking issues" — the severity-count line already conveys this. | ||
| - **Reviewer attribution line** — `Reviewers: <name> (<model-id>), <name> (<model-id>), …`. Names + model IDs only, no per-reviewer prose. | ||
| - **Pointer line** — `See inline comments below for line-by-line details. File-/branch-scope findings are listed in the next section.` | ||
| 2. **Narrative summary** — an LLM-authored prose paragraph (3–8 sentences, single paragraph; up to two short paragraphs only when the round has both code and security verdicts to weave together). The orchestrator generates this directly from the consolidated cluster list — **not** from a fixed template. It must: | ||
| - Convey the round's overall verdict in plain language (e.g. "blocking issues remain", "advisory polish only", "ready to merge once X is addressed"). | ||
| - Mention the rough shape of the work (which areas, which files or modules) so the PR author understands the scope at a glance. | ||
| - Acknowledge cross-reviewer agreement or disagreement when present (e.g. "two reviewers flagged the same concurrency hazard"). | ||
| - Point readers to the inline comments and the file-/branch-scope section that follow. | ||
| - Avoid bullet lists, fixed-format severity-count rows, "Total findings: N" lines, "Reviewers: …" attribution lines, or any other deterministic-template residue. The narrative must read as natural human prose written for the PR author. | ||
| - Stay under ~8 sentences. Verbose dumps defeat the purpose of the high-level summary. | ||
| - When there are zero findings (LGTM), this section is a 1–2 sentence prose acknowledgement (e.g. "All four reviewers verified the build and tests pass cleanly; no blocking or advisory issues found this round.") — still no template, still no severity table. | ||
| 3. **File-/branch-scope findings section** — every `anchor_scope: "file"` and `anchor_scope: "branch"` finding rendered in full, each prefixed with its cluster ID and severity (`[<cluster_id>] [<severity>] <summary> — <path-or-branch-scope>`). Required because GitHub has no per-file anchor outside hunks. | ||
| 4. **Line-anchored findings (Tier 3 only)** — when the orchestrator falls back to a body-only review (Tier 3 retry, see below), every `anchor_scope: "line"` finding is appended here in full prose so nothing is lost. In the default (non-fallback) body, this section is omitted. | ||
| The per-reviewer prose dump (`**[Round N] <model-id>:** …` followed by per-finding prose) used in the previous spec is **removed** — the attribution line in §2 replaces the per-reviewer headers, and the per-finding prose lives only in `comments[]` for line-anchored findings or in §3 for file/branch-scope findings. | ||
| The per-reviewer prose dump (`**[Round N] <reviewer-name>:** …` followed by per-finding prose) used in the previous spec is **removed** — the narrative summary in §2 is the orchestrator's high-level voice, and the per-finding prose lives only in `comments[]` for line-anchored findings or in §3 for file/branch-scope findings. | ||
@@ -150,12 +155,4 @@ Example body (default, non-fallback): | ||
| Total findings: 5 | ||
| - blocking: 2 | ||
| - advisory: 3 | ||
| This round surfaced two blocking concerns concentrated in the auth and billing flows, plus three advisory polish items spread across docs and tests. Both `core-reviewer` and `reviewer` independently flagged the missing null-guard on `config.repo`, which is the highest-impact item to fix first. The advisory README and release-tagging notes are listed below as file- and branch-scope findings; everything line-anchored is in the inline comments. Once the two blockers are addressed, the PR should be ready for a final pass. | ||
| Blocking issues span: auth, billing. See inline comments for details. | ||
| Reviewers: core-reviewer (claude-sonnet-4.6), reviewer (glm-4.6) | ||
| See inline comments below for line-by-line details. File-/branch-scope findings are listed in the next section. | ||
| ### File-/branch-scope findings | ||
@@ -167,3 +164,3 @@ | ||
| Example invocation (single-quoted heredoc to prevent shell expansion): | ||
| Example invocation (single-quoted heredoc to prevent shell expansion; the `body` is a JSON string with `\n` line breaks — the LLM-authored narrative is escaped into a single-line JSON string): | ||
@@ -175,3 +172,3 @@ ```bash | ||
| "event": "COMMENT", | ||
| "body": "<!-- zooplankton-orchestrate: pr=123 round=2 fingerprint=abc -->\n\nTotal findings: 5\n- blocking: 2\n- advisory: 3\n\nBlocking issues span: auth, billing. See inline comments for details.\n\nReviewers: core-reviewer (claude-sonnet-4.6), reviewer (glm-4.6)\n\nSee inline comments below for line-by-line details. File-/branch-scope findings are listed in the next section.\n\n### File-/branch-scope findings\n\n- [R2-C04] [advisory] Update README to mention new flag — README.md", | ||
| "body": "<!-- zooplankton-orchestrate: pr=123 round=2 fingerprint=abc -->\n\nThis round surfaced two blocking concerns concentrated in the auth and billing flows, plus three advisory polish items spread across docs and tests. Both `core-reviewer` and `reviewer` independently flagged the missing null-guard on `config.repo`, which is the highest-impact item to fix first. The advisory README and release-tagging notes are listed below as file- and branch-scope findings; everything line-anchored is in the inline comments.\n\n### File-/branch-scope findings\n\n- [R2-C04] [advisory] Update README to mention new flag — README.md", | ||
| "comments": [ | ||
@@ -184,2 +181,4 @@ { "path": "src/foo.ts", "position": 17, "body": "[R2-C03] [blocking] Null deref when `config.repo` is absent." } | ||
| > **Authoring note:** Generate the §2 narrative *after* consolidation and cluster-ID assignment so the prose can reference cluster IDs and reviewer names accurately. Prefer concrete observations ("both reviewers flagged the same null-guard issue") over abstract qualifiers ("several issues found"). Never paste a fixed severity table or "Total findings: N" line — if your draft contains those, rewrite it as natural prose. | ||
| Review marker format (in the review `body`): | ||
@@ -200,4 +199,4 @@ | ||
| 1. Build the body with `renderConsolidatedReviewBody(findings, reviewers, { marker })` (default `{ includeLineAnchored: false }`). `marker` is the string returned by `createReviewMarker({ pr, round, fingerprint })` and is required — the renderer throws `TypeError` if it is missing or empty. | ||
| 2. Build endpoint-neutral line anchors with `buildInlineCommentAnchors(findings)`. Each anchor has shape `{ path, position: null, _line, body }` — `position` is `null` as a placeholder; `_line` is the file line number retained as a resolution hint. Do NOT forward `_line` to GitHub. | ||
| 3. Before calling GitHub, resolve each anchor's `position`. `POST /pulls/{pull_number}/reviews` review comments require `path` + `position` (with `commit_id` at the top level) — `position` is the diff-position integer described above, not the file line number. Use `_line` to find the target line in the unified diff, then count positions from the hunk header. The legacy `POST /pulls/{pull_number}/comments` per-comment endpoint (which does accept `path` + `line` + `side`) is **not** used by the consolidated post. | ||
| 2. Build inline comment anchors from the validated findings. Each `anchor_scope: "line"` finding's `position` (already an integer ≥ 1, supplied by the reviewer and validated in **Anchor Validation**) is forwarded directly to GitHub. Each anchor has shape `{ path, position, body }`. The legacy `_line` resolution hint is no longer needed — the orchestrator does not compute `position` itself. | ||
| 3. Before calling GitHub, re-confirm each anchor's `position` is a positive integer and that the corresponding `path` is in the PR's file list. `POST /pulls/{pull_number}/reviews` review comments require `path` + `position` (with `commit_id` at the top level). The legacy `POST /pulls/{pull_number}/comments` per-comment endpoint (which does accept `path` + `line` + `side`) is **not** used by the consolidated post. | ||
| 4. Tier 1: re-validate anchors against current `headRefOid`, then retry with `planReviewPostRetry(comments, 1)`. | ||
@@ -244,3 +243,3 @@ 5. Tier 2: deterministically halve inline comments and move the rest into the body with `planReviewPostRetry(comments, 2)`. | ||
| - The zooplankton marker in the parent review body (look up via the comment's `pull_request_review_id`), **or** | ||
| - A `[R<N>-C<nn>]` cluster-ID prefix at the start of the inline comment body, with `N < currentRound` (e.g. `[R1-C03] [blocking] …`). Note: the `**[Round N]**` per-reviewer header lives in the consolidated review `body`, not in individual inline comment bodies — do not use it as the filter pattern for inline comments. | ||
| - A `[R<N>-C<nn>]` cluster-ID prefix at the start of the inline comment body, with `N < currentRound` (e.g. `[R1-C03] [blocking] …`). Note: the zooplankton marker (`<!-- zooplankton-orchestrate: … -->`) lives in the consolidated review `body`, not in individual inline comment bodies — do not use it as the filter pattern for inline comments. | ||
@@ -247,0 +246,0 @@ Use the `(pr, round) → (cluster_id → comment_id)` map persisted by previous rounds (see **Consolidated Review Posting**) as the primary source. Fall back to API enumeration only when the map is missing or stale. |
@@ -102,3 +102,4 @@ --- | ||
| - **Phase 2b — Plan Review Gate.** Simple plans skip reviewer plan review. Medium/large plans run core-reviewer plan review regardless of autonomous mode (autonomous mode only skips human approval). See `references/planning-gate.md` → **Phase 2b: Decision Table**, **Simple Path**, **Medium/Large Path**, and **Required Logging**. | ||
| - **Phase 2c — Branch and PR Preparation.** Precondition: `APPROVED_TO_IMPLEMENT == true`. Commit and push the plan. In GitHub mode, create or reuse the PR idempotently and capture `PR_NUMBER`/`PR_URL`; in local mode, skip PR creation. See `references/planning-gate.md` → **Phase 2c: Branch And PR Preparation**. | ||
| - **Phase 2c — Branch and PR Preparation.** Precondition: `APPROVED_TO_IMPLEMENT == true`. Commit and push the plan. In GitHub mode, create or reuse the PR idempotently and capture `PR_NUMBER`/`PR_URL`; in local mode, skip PR creation. The PR description must be LLM-authored narrative prose (3–6 paragraphs covering what/why, how, risk/verification, and an optional plan link) — never a fixed template, bullet list, or paste of the plan file. See `references/planning-gate.md` → **Phase 2c: Branch And PR Preparation** and **PR description authoring**. | ||
| - **Phase 2d — Pre-Implementation Confirmation.** After the plan is committed/pushed and the PR (if any) exists, the orchestrator presents a one-paragraph LLM-authored summary (what will be built, shape of work, PR link) and asks `Ready to proceed with implementation? (yes/no)`. Only a clearly-affirmative `yes` continues to Phase 3; **anything else (including `no`, questions, or amendment requests) halts the run and asks the user to amend the plan and re-run `/zooplankton-orchestrate`** — there is no in-session loop-back to Phase 2a. Skipped only when `AUTO_MODE=true` (env `ZOOPLANKTON_AUTO_MODE=1` or `--auto` flag on the orchestrate run — distinct from `autonomousMode`). See `references/planning-gate.md` → **Phase 2d: Pre-Implementation Confirmation**. | ||
@@ -121,19 +122,24 @@ --- | ||
| Invoke **all reviewers in a single parallel message** (one Task call per reviewer). | ||
| #### 4a.0 — Pre-flight Reviewer Enumeration (REQUIRED) | ||
| **Agent discovery (run fresh every round — do NOT rely on memory or prior-round lists):** | ||
| Before invoking any reviewer, the orchestrator MUST perform an explicit enumeration step and emit it visibly so partial-invocation bugs cannot hide: | ||
| 1. Read `agents.coreReviewers`, `agents.reviewers`, and `agents.frontendReviewers` from `zooplankton.json` if the `agents` block is present. | ||
| 2. If any role key is absent from `zooplankton.json` (or the `agents` block is missing entirely), discover agents by globbing agent MD files: | ||
| - Core reviewers: `~/.config/opencode/agents/core-reviewer-*.md` and `.opencode/agents/core-reviewer-*.md` | ||
| - Normal reviewers: `~/.config/opencode/agents/reviewer-*.md` and `.opencode/agents/reviewer-*.md` | ||
| - Frontend reviewers: `~/.config/opencode/agents/frontend-reviewer-*.md` and `.opencode/agents/frontend-reviewer-*.md` | ||
| 3. **De-duplicate** across global and project paths by filename stem (e.g. `reviewer-glm` from either location counts once). | ||
| 4. **Always enumerate all discovered agents** — never omit an agent because it returned empty in a prior round. Empty results in a prior round are not a reason to skip in subsequent rounds. | ||
| 5. Each entry is a `{ name }` object — use `.name` for `@<name>`. Skip the role if `null`; skip individual entries with `"disabled": true`. | ||
| 1. **Resolve roster.** Read `agents.coreReviewers`, `agents.reviewers`, and `agents.frontendReviewers` from `zooplankton.json` (fall back to MD-file discovery if the keys are absent). Each entry is a `{ name, disabled? }` object. Treat the role as empty if the value is `null`. Drop any entry whose `disabled` is truthy. | ||
| 2. **Decide frontend inclusion.** Run the touched-files check (`git diff --name-only origin/master...HEAD`). If at least one path matches the frontend heuristic (`src/components/`, `src/pages/`, `app/`, or extensions `.tsx`/`.vue`/`.svelte`/`.css`/`.scss`) AND `agents.frontendReviewers` is non-empty after disabled-filtering, mark frontend reviewers as **invoked-this-round**. | ||
| 3. **Build `REVIEWERS_THIS_ROUND`.** Concatenate the surviving entries into one ordered list of `@<name>` handles: core reviewers first (blocking), then invoked frontend reviewers (blocking), then normal reviewers (non-blocking). | ||
| 4. **Emit the enumeration line** before the parallel-invocation message: | ||
| - **Core reviewers** (blocking quorum): for each entry in `agents.coreReviewers` (or discovered `core-reviewer-*`), invoke `@<entry.name>` and assign worktree `.worktrees/<branch>/<name>`. | ||
| - **Normal reviewers** (non-blocking, opportunistic): for each `agents.reviewers` entry (or discovered `reviewer-*`), invoke `@<entry.name>` and assign 1–2 review areas via adaptive round-robin. | ||
| - **Frontend reviewers** (blocking when frontend files are touched): if `agents.frontendReviewers` is not null **and** the change touches frontend files, invoke each non-disabled entry. They follow `guides/frontend-reviewer-guide.md` and join the quorum. | ||
| ```text | ||
| Phase 4a.0: round=<n> reviewers=[@core-reviewer-primary, @frontend-reviewer-glm, @reviewer-deepseek] blocking=[@core-reviewer-primary, @frontend-reviewer-glm] non_blocking=[@reviewer-deepseek] frontend_touched=<true|false> | ||
| ``` | ||
| 5. **Single-message invocation rule.** The orchestrator MUST issue exactly one assistant message that contains one `Task` tool call per entry in `REVIEWERS_THIS_ROUND` — no more, no fewer. Splitting invocations across multiple messages, dropping a reviewer silently, or "saving" a reviewer for a later round is an **orchestrator error** that invalidates the round. If for any reason a reviewer in `REVIEWERS_THIS_ROUND` cannot be invoked in this single message, abort the round, log the cause, and re-enter Phase 4a.0 from the top. | ||
| 6. **Post-invocation self-check.** Immediately after the parallel-invocation message returns, count the dispatched Task calls and assert the count equals `len(REVIEWERS_THIS_ROUND)`. If they differ, abort the round and re-run 4a.0. | ||
| #### 4a.1 — Per-Role Mechanics | ||
| - **Core reviewers** (blocking quorum): assign worktree `.worktrees/<branch>/<name>` per entry. | ||
| - **Normal reviewers** (non-blocking, opportunistic): assign 1–2 review areas via adaptive round-robin per `references/reviewer-assignment.md`. | ||
| - **Frontend reviewers** (blocking when invoked-this-round per 4a.0 step 2): they follow `guides/frontend-reviewer-guide.md` and join the blocking quorum for the round. | ||
| Each reviewer follows their unified guide (`guides/core-reviewer-guide.md`, `guides/reviewer-guide.md`, `guides/frontend-reviewer-guide.md`). | ||
@@ -146,4 +152,4 @@ | ||
| - **4b.1** Parse reviewer JSON via `parseReviewerResult(text)` from `.opencode/plugins/lib.js` — the authoritative review surface. See `references/review-protocol.md` → **Structured Reviewer Results**. | ||
| - **4b.2** Dedup (fuzzy + exact + deterministic sort) and assign cluster IDs `R<round>-C<nn>` (code), `S<round>-C<nn>` (security), `P<round>-C<nn>` (plan review). See `references/review-protocol.md` → **Finding Consolidation**. | ||
| - **4b.3** Validate anchors. *(Local mode: skip.)* See `references/review-protocol.md` → **Anchor Validation**. | ||
| - **4b.2** Validate anchors on raw per-reviewer findings (pre-dedup). *(Local mode: skip.)* This MUST run before consolidation because anchor remediation uses each reviewer's own `ephemeral_id` values, which are discarded once dedup runs. See `references/review-protocol.md` → **Anchor Validation**. | ||
| - **4b.3** Dedup (fuzzy + exact + deterministic sort) and assign cluster IDs `R<round>-C<nn>` (code), `S<round>-C<nn>` (security), `P<round>-C<nn>` (plan review). See `references/review-protocol.md` → **Finding Consolidation**. | ||
| - **4b.4** Compute fingerprint and check marker idempotency. *(Local mode: skip.)* See `references/review-protocol.md` → **Fingerprint And Idempotency**. | ||
@@ -150,0 +156,0 @@ - **4b.5** Post one consolidated review per role-group per round (GitHub mode); render the same body locally otherwise and feed it into coder revision. See `references/review-protocol.md` → **Consolidated Review Posting**. |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
418462
9.33%63
1.61%0
-100%