# Battle Test — forgejo-action-code-review **Date:** 2026-05-12 **Targets:** proposal.md, design.md, specs/action-code-review/spec.md **Team:** adversary (devils-advocate, opus), defender (proponent, opus) ## Verdict **Patches applied; C4 spike resolved; C5 spike deferred. Proceed to tasks.md.** Of 9 critiques: 7 patched, 1 resolved by spike (C4), 1 deferred via spec split (C5 — Path A copy-paste is supported; Path B cross-repo `uses:` is recommended-when-supported and tracked in bd for follow-up). The change was written against the dep change's **design**, not its **shipped artifact**. The `/code-review` skill drifted from its archived design before this action layered on top, and this change inherited the drift. The single most damaging finding (C1): the action invoked the skill with `--comment` to enable posting; the shipped skill posts by **default** and accepts `--dry-run` to suppress. Every action artifact referenced the wrong flag. C2 was similarly load-bearing: the D6 dedup design assumed the skill embeds head-SHA in review bodies — it does not. Both fixed by patches and a follow-up tasks.md entry to amend skill Step 12 body templates. C4 spike (2026-05-13) validated D2 (BASE workflow file executes under `pull_request_target` on fork PRs; Forgejo's display label `pull_request` is cosmetic, not semantic) and D7 (ephemeral `GITEA_TOKEN` + `permissions: pull-requests: write` permits `create_pull_review` on fork PRs on Codeberg v11.x). Spike artifacts retained under `openspec/changes/forgejo-action-code-review/spikes/c4-ephemeral-token-fork-pr/` for reproducibility. ## Surviving Critiques ### C1. `--comment` flag does not exist; posting is the skill's default **Severity:** blocker **Failure mode:** workflow passes `--comment` to a skill that does not recognize it; or omits it and posts anyway. Either way the action ships against a non-existent flag contract. The proposal's "supports `--comment` flag to post findings" is fiction relative to the shipped `.claude/commands/code-review.md`. **Hits:** proposal.md "What Changes" bullet 2; design.md D6 "Tagging mechanism" paragraph; spec.md "Findings posted as a Forgejo PR review" requirement. **Canonical conflict:** `.claude/commands/code-review.md:3` declares `argument-hint: [PR#] [--dry-run]`; line 12 reads "By default, findings are posted as a PR review with inline comments. Use `--dry-run` for terminal-only output." **Falsifier:** `grep -n "dry-run\|--comment" .claude/commands/code-review.md` — done; confirms inversion. **Disposition:** patch (concede-patch) ### C2. D6 dedup tagging is fabricated **Severity:** blocker **Failure mode:** D6 promises action-side dedup via "current head SHA tag in review body footer." Skill's body templates (Step 12 of `.claude/commands/code-review.md`) use fixed strings with no SHA and no placeholder. Without a contract-level mechanism, dedup either no-ops (every run posts a new review) or relies on soft prompt-injection via `additional_instructions` — unreliable for a mechanism the spec promises to honor. **Hits:** design.md D6 paragraph beginning "Tagging mechanism"; the change's "duplicate-review suppression" goal in design.md "Goals" list. **Canonical conflict:** skill design.md (archived dep change) marks dedup as future work; shipped skill provides no tagging surface. **Falsifier:** `grep -n "sha\|SHA\|` trailer, requires skill amendment via separate tasks.md entry). User must choose. - **D7** (C4): retain "Verification owed" — but mark explicitly **stalemate pending spike**. After spike, either remove note (ephemeral works) or flip default to PAT-required (ephemeral does not work for fork PRs). - **D9** (C5): lead with copy-paste as the supported distribution path; describe cross-repo `uses:` as recommended **when supported by the target Forgejo version**. List specific cross-repo concerns: `secrets: inherit` behavior, tag resolution, token grants. Mark `uses:` path as **stalemate pending spike**. - **D10** (C6): clarify "max_turns exhausted" row rationale — "trips most often mid-pipeline before Step 12, so partial-review-not-posted is the expected outcome. Surface `max_turns_exhausted` in step summary so consumers raise the limit. Recommended floor: 25." Add new failure row: `| inconclusive run (max_turns trip before posting) | review | exit 0 | step summary records max_turns_exhausted with zero findings |`. - **Risks → "`additional_instructions` injection vector" bullet** (C3): rename to **"Prompt-injection vectors"**. Expand to note every PR-controlled byte (diff, file contents, PR title/body) reaches Claude's context. Mitigation: scope FORGEJO_TOKEN to PR read + review-write on the single target repo, no org-wide scope. - **Risks → "Cost runaway despite max_turns" bullet** (C7): strengthen — recommend `max_turns: 20` ceiling and document worst-case cost range `$0.50-$5.00 per large-PR review`. - **New decision D11** (C8): **"Skill invocation prompt shape"** — invocation is the literal string `/code-review ${{ github.event.pull_request.number }}`. PR# from event context. No `--dry-run`. Skill's branch-based auto-detection bypassed because base-only checkout makes it unreliable. - **New decision D12** (C9): **"`--allowedTools` whitelist: MCP-only, no Bash fallback"** — enumerate exact allowed tools (`mcp__codeberg__get_pull_request_by_index`, `mcp__codeberg__list_pull_request_files`, `mcp__codeberg__get_pull_request_diff`, `mcp__codeberg__get_file_content`, `mcp__codeberg__list_pull_reviews`, `mcp__codeberg__create_pull_review`, `Task`). No `Bash(...)`, no `Read`, no `AskUserQuestion`. Trade-off: brittleness (no fallback) for tighter blast-radius (no shell escape from prompt injection). Bounded further by FORGEJO_TOKEN scope. ### spec.md - **"Findings posted as a Forgejo PR review"** (C1): add sentence `The workflow MUST invoke the skill without the --dry-run flag.` - **"Required secrets"** (C3): add scenario `Scenario: FORGEJO_TOKEN broader than PR read + PR-review write` → `WHEN the workflow runs with a FORGEJO_TOKEN whose scope exceeds PR read + PR-review write on the target repository THEN preflight SHALL fail with a clear error.` - **"Reusable Forgejo Actions workflow definition"** (C5): split the existing scenario into two — `Scenario: PR opened on a repository that copies the workflow file` (the supported path) and `Scenario: PR opened on a repository that references via uses:` (gated on `workflow_call` support, marked uncertain until spike). - **"Cost cap via max-turns"** (C6 + C7): rewrite first sentence to `The workflow SHALL impose a soft upper bound on Claude Code's main-loop turn count via the --max-turns flag. The cap bounds the number of subagent dispatches, not the token spend within each subagent.` Add requirement `The workflow SHALL surface per-run cost_usd from --output-format json in the step summary.` Rewrite Scenario "max_turns reached" to `WHEN Claude Code consumes its configured max_turns budget without finishing organically THEN Claude Code SHALL terminate AND the workflow step SHALL exit zero AND the step summary SHALL record a max_turns_exhausted status with whatever findings were posted (which may be none).` Drop the unenforceable "SHALL still attempt to submit." - **"Non-interactive Claude Code invocation" → Scenario "/code-review skill is the entry point"** (C8): replace `against the PR identified by the workflow event context` with `the prompt SHALL be the literal string "/code-review ${{ github.event.pull_request.number }}". The workflow MUST NOT rely on the skill's branch-based auto-detection.` - **New requirement** (C2 Option B only): "Dedup contract" — `The workflow SHALL append a machine-parseable trailer of the form to every posted review body. The workflow's pre-flight step SHALL parse trailers from list_pull_reviews results and skip if the current head SHA matches an existing bot review's trailer.` Plus a tasks.md entry to amend skill Step 12 templates. - **New requirement** (C9): "Allowed tools" — `The workflow SHALL invoke Claude Code with --allowedTools restricted to the MCP tools enumerated in design D12. The list MUST NOT include any Bash(...) entries.` ## Future Work - **C5 spike** is deferred to a separate bd issue. Path A (copy-paste) is the certain distribution; Path B (cross-repo `uses:`) is recommended-when-supported and gated on a future spike. Implementation may proceed on Path A without blocking. ## Lead Recommendation **Status (2026-05-13):** C4 resolved by spike. C5 deferred but documentation already split into Path A + Path B. C2 decision made (Option B: lift dedup to spec + skill amendment). C1, C3, C6, C7, C8, C9 patches applied. **Resolved before tasks.md:** 1. ✅ **C4 spike** complete. Ephemeral `GITEA_TOKEN` works for `create_pull_review` on fork PRs on Codeberg Forgejo v11.x. D2's BASE-workflow-file guarantee verified (`pull_request_target` semantics honored despite displayed `pull_request` label). 2. **C5 spike** deferred. Repos created (`goern/c5-spike-lib`, `goern/c5-spike-consumer`); tag + secrets + trigger commit not yet applied. Tracked in bd. Implementation proceeds on Path A; Path B documented as conditional. 3. ✅ **C2 decision**: Option B chosen. Spec gained "Dedup contract" requirement; skill amendment for SHA trailer is a tasks.md entry (forward dependency on `forgejo-code-review-skill` follow-up). 4. ✅ **Mechanical patches** applied for C1, C3, C6, C7, C8, C9 to proposal.md, design.md, spec.md. **Change is now hardened enough for tasks.md** with the following caveats: the C5 spike result may demote D9 Path B from "recommended" to "experimental" if cross-repo `workflow_call` semantics turn out unreliable on Codeberg Forgejo. That demotion is doc-only and does not block implementation. **Most embarrassing findings (recorded for future battle-tests):** C1 (`--comment` vs `--dry-run` flag inversion) and C2 (fabricated dedup tagging mechanism). Both indicate the change set was written against the dep change's design rather than the shipped skill artifact. Future battle-tests should explicitly include "code-read the shipped artifact" as a pre-flight, not just "read the dep change's design."