Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 1 addition & 67 deletions .opencode/plugins/review-guardrails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,7 @@ type ReviewState = {
swarmInvocations?: number
}

const publishCommandPattern = /(^|\s)(git\s+push|gh\s+pr\s+create|gh\s+pr\s+edit)\b/
const reviewerSubagentPattern = /^reviewer-(quick|arch|reasoning|e2e)$/
const configuredSwarmCap = Number.parseInt(process.env.OPENCODE_REVIEW_SWARM_CAP ?? "", 10)
const swarmCap = Number.isInteger(configuredSwarmCap) && configuredSwarmCap > 0 ? configuredSwarmCap : 8
const bypassEnabled = process.env.OPENCODE_REVIEW_BYPASS === "1"

function parseBranchFromCommand(cmd: string): string | null {
const gitPushMatch = cmd.match(/git\s+push\s+(?:(?:--?[\w-]+(?:[= ][^\s]*)?\s+)*)(\S+)\s+(\S+)/)
const gitPushBranch = gitPushMatch?.[2]
if (gitPushBranch) return gitPushBranch.replace(/^HEAD:/, "")

const ghHeadMatch = cmd.match(/gh\s+pr\s+create\b.*?--head[= ]([^\s]+)/)
const ghHeadBranch = ghHeadMatch?.[1]
if (ghHeadBranch) return ghHeadBranch

return null
}

function extractWorkdir(args: Record<string, unknown>): string | null {
const wd = args.workdir
return typeof wd === "string" && wd.length > 0 ? wd : null
}

const stateRoot = () => process.env.XDG_STATE_HOME ?? path.join(os.homedir(), ".local", "state")

Expand All @@ -48,21 +27,13 @@ async function readReviewState(filePath: string): Promise<ReviewState | null> {
try {
const parsed = JSON.parse(await readFile(filePath, "utf8")) as ReviewState

if (parsed.swarmInvocations !== undefined && typeof parsed.swarmInvocations !== "number") {
throw new Error("corrupted state: swarmInvocations is not a number")
}

if (parsed.publishAuthorized !== undefined && typeof parsed.publishAuthorized !== "boolean") {
throw new Error("corrupted state: publishAuthorized is not a boolean")
}

return parsed
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
return null
}

throw error
return null
}
}

Expand All @@ -81,39 +52,6 @@ let lock: Promise<void> = Promise.resolve()
export const ReviewGuardrails: Plugin = async ({ $, worktree }) => {
return {
"tool.execute.before": async (input, output) => {
if (input.tool === "bash") {
const cmd = String(output.args.command ?? "")

if (publishCommandPattern.test(cmd)) {
if (bypassEnabled) return

const explicitBranch = parseBranchFromCommand(cmd)
const cwd = extractWorkdir(output.args as Record<string, unknown>) ?? worktree
let branch = explicitBranch ?? ""

if (!branch) {
try {
branch = (await $`git rev-parse --abbrev-ref HEAD`.cwd(cwd).text()).trim()
} catch {
throw new Error("review-guardrails: cannot determine current branch; refusing publish")
}
}

if (!branch || branch === "HEAD") {
throw new Error("review-guardrails: cannot determine current branch; refusing publish")
}

const stateFile = stateFileForBranch(worktree, branch)
const state = await readReviewState(stateFile)

if (state?.publishAuthorized !== true) {
throw new Error(
`publish gated by review-state for branch '${branch}' - call review-state with action='request_publish' and verdict before git push, gh pr create, or gh pr edit (or set OPENCODE_REVIEW_BYPASS=1 for emergencies)`,
)
}
}
}

if (input.tool === "task") {
const sub = String(output.args.subagent_type ?? "")

Expand All @@ -126,10 +64,6 @@ export const ReviewGuardrails: Plugin = async ({ $, worktree }) => {

state.swarmInvocations = (state.swarmInvocations ?? 0) + 1

if (state.swarmInvocations > swarmCap) {
throw new Error(`swarm budget exhausted: ${state.swarmInvocations} reviewer-* calls > cap ${swarmCap}`)
}

await mkdir(path.dirname(stateFile), { recursive: true })
await writeFile(stateFile, `${JSON.stringify(state, null, 2)}\n`, "utf8")
})
Expand Down
11 changes: 7 additions & 4 deletions .opencode/tools/review-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,14 @@ export default tool({
}

state.swarmInvocations += 1
if (state.swarmInvocations > swarmCap()) {
throw new Error(`swarm budget exhausted: ${state.swarmInvocations} reviewer-* calls > cap ${swarmCap()}`)
}
await writeState(filePath, state)
return JSON.stringify({ ok: true, swarmInvocations: state.swarmInvocations, state })
return JSON.stringify({
ok: true,
swarmInvocations: state.swarmInvocations,
swarmCap: swarmCap(),
overBudget: state.swarmInvocations > swarmCap(),
state,
})
}

throw new Error(`unsupported action: ${action}`)
Expand Down
31 changes: 11 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# my-opencode

Public, versioned [OpenCode](https://opencode.ai) configuration for a multi-agent coding workflow: global agents, reusable skills, review guardrails, and a per-repo GitHub Issues bundle template. Clone it on any machine, run the installer, and your OpenCode setup is ready.
Public, versioned [OpenCode](https://opencode.ai) configuration for a multi-agent coding workflow: global agents, reusable skills, review-state observability, and a per-repo GitHub Issues bundle template. Clone it on any machine, run the installer, and your OpenCode setup is ready.

## What's inside

Expand All @@ -15,7 +15,7 @@ Public, versioned [OpenCode](https://opencode.ai) configuration for a multi-agen
│ ├── exec.md # Subagent: implementer driven by pipeline-execution - GPT-5.5
│ ├── reviewer.md # Default agent (mode: all): review-fix loop owner + PR opener - Sonnet 5 medium
│ ├── fixer.md # Subagent: applies blocker deltas from reviewer - GPT-5.5
│ └── reviewer-*.md # Four read-only swarm reviewers
│ └── reviewer-*.md # Four specialized swarm reviewers
├── skills/ # Global skills; also installable per repo with `bunx skills add ... --all`
│ ├── pipeline-execution/ # Generic exec → reviewer → fixer → PR pipeline (tracker-agnostic)
│ └── swarm-review/ # Multi-model parallel code review (used by `coder` fast path)
Expand All @@ -25,7 +25,7 @@ Public, versioned [OpenCode](https://opencode.ai) configuration for a multi-agen
│ └── cli.ts # Bun CLI: `setup`, `cleanup`, `install-skills`, `install-issues-bundle`
├── .opencode/
│ ├── plugins/ # Global OpenCode plugins symlinked into ~/.config/opencode/plugins/
│ │ └── review-guardrails.ts # Publish gate: blocks `git push` / `gh pr create` until review-state authorizes
│ │ └── review-guardrails.ts # Observability plugin: records reviewer swarm invocations
│ └── tools/ # Global OpenCode tools symlinked into ~/.config/opencode/tools/
│ └── review-state.ts # Custom tool owning review-fix loop state (consumed by `agents/reviewer.md`)
└── .gitignore
Expand Down Expand Up @@ -166,12 +166,13 @@ PR labels are the merge contract:
- `approved` means the automated review loop found no remaining blockers, the final verify gate passed, and the PR is ready to merge once repository checks are green.
- `hitl` means human review is required because blockers, uncertainty, disagreement, or missing verification remain.

### Publish gate (`.opencode/plugins/` + `.opencode/tools/`)
### Review state (`.opencode/plugins/` + `.opencode/tools/`)

The loop above is enforced by two global files symlinked into `~/.config/opencode/` by the installer.
`.opencode/plugins/review-guardrails.ts` intercepts `git push` and `gh pr create` and blocks them until the branch has been authorized - that's why pushes can error with `publish gated by review-state for branch <name>`.
Authorization lives in `.opencode/tools/review-state.ts`, the custom tool the `reviewer` agent uses to track the review-fix loop and mark a branch ready to publish (see `agents/reviewer.md` "Loop State (hard gate)").
The loop budget (3 fixer passes + swarm cap) is per review cycle; re-reviewing the same branch after a published cycle starts a fresh budget automatically and archives the prior cycle in `cycles[]`, so manually deleting state is rarely needed for a normal re-review.
`.opencode/tools/review-state.ts` is the custom tool the `reviewer` agent uses to track the review-fix loop and mark a branch ready to publish (see `agents/reviewer.md` "Loop State").
`.opencode/plugins/review-guardrails.ts` records `reviewer-*` swarm invocations in the same state file for observability.
It does not block bash, task, push, or PR commands.
The loop budget (3 fixer passes + advisory swarm cap) is per review cycle; re-reviewing the same branch after a published cycle starts a fresh budget automatically and archives the prior cycle in `cycles[]`, so manually deleting state is rarely needed for a normal re-review.
Add your own plugins or tools by dropping `.ts`/`.js` files into these directories and re-running `bun run setup`.

### Context window tuning
Expand Down Expand Up @@ -259,19 +260,9 @@ export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
export OPENCODE_REVIEW_SWARM_CAP=8
```

The swarm cap is a guardrail against runaway reviewer loops.
`8` supports one explicit full swarm plus follow-up quick checks, while failing fast when the reviewer starts looping.

### Emergency bypass

If the `review-guardrails` plugin blocks a `git push` / `gh pr create` due to corrupted or stale loop state (e.g. an interrupted process that left `review-state` mid-write), set `OPENCODE_REVIEW_BYPASS=1` for the current process to skip the publish gate entirely:

```bash
OPENCODE_REVIEW_BYPASS=1 git push
```

Use it only as an escape hatch - bypassing the gate also disables the swarm-budget cap, so a runaway reviewer loop can keep spawning subagents past `OPENCODE_REVIEW_SWARM_CAP`.
Prefer inspecting or deleting the offending state file first: `$XDG_STATE_HOME/opencode/review-state/<repo-hash>/<branch>.json` (defaults to `~/.local/state/opencode/review-state/...`); manual reset is now mostly for corrupted or stale state because normal new cycles reset themselves on `start` after publish.
The swarm cap is advisory.
When the counter exceeds it, `review-state.record_swarm` returns `overBudget: true` instead of blocking the agent.
Inspect state at `$XDG_STATE_HOME/opencode/review-state/<repo-hash>/<branch>.json` (defaults to `~/.local/state/opencode/review-state/...`) when debugging loop behavior.

## MCPs

Expand Down
7 changes: 3 additions & 4 deletions __tests__/review-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,15 +154,14 @@ describe("review-state tool", () => {
expect((r2 as { swarmInvocations?: number }).swarmInvocations).toBe(2)
})

test("record_swarm enforces configured cap", async () => {
test("record_swarm reports configured cap without blocking", async () => {
process.env.OPENCODE_REVIEW_SWARM_CAP = "2"
await reviewState.execute({ branch: "feature/swarm-cap", action: "start" }, context)
await reviewState.execute({ branch: "feature/swarm-cap", action: "record_swarm" }, context)
await reviewState.execute({ branch: "feature/swarm-cap", action: "record_swarm" }, context)

await expect(reviewState.execute({ branch: "feature/swarm-cap", action: "record_swarm" }, context)).rejects.toThrow(
/swarm budget exhausted/,
)
const result = parse(await reviewState.execute({ branch: "feature/swarm-cap", action: "record_swarm" }, context))
expect(result).toMatchObject({ ok: true, swarmInvocations: 3, swarmCap: 2, overBudget: true })
})

test("new cycle after publish resets budget and archives prior passes", async () => {
Expand Down
17 changes: 11 additions & 6 deletions agents/architect.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@ mode: all
model: anthropic/claude-opus-4-8
reasoningEffort: high
temperature: 0.2
tools:
write: true
edit: true
patch: true
todowrite: true
task: true
task_status: true
webfetch: true
permission:
bash:
"*": allow
task:
"*": deny
"exec": allow
"reviewer": allow
"fixer": allow
"coder": allow
"reviewer-*": allow
"*": allow
---

You are the **architect** agent. You orchestrate work - you don't implement and you don't review code yourself. You take a request, decide whether it's issue-mode (GitHub Issues bundle) or ad-hoc, and drive it through the pipeline `exec → reviewer → (fixer × ≤3) → PR`. The pipeline lives in the global `pipeline-execution` skill; you delegate to it.
Expand Down
13 changes: 11 additions & 2 deletions agents/coder.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@ mode: primary
model: anthropic/claude-sonnet-5
reasoningEffort: medium
temperature: 0.2
tools:
write: true
edit: true
patch: true
todowrite: true
task: true
task_status: true
webfetch: true
permission:
bash:
"*": allow
task:
"*": deny
"reviewer-*": allow
"*": allow
---

You are the **coder** agent - the fast path for trivial changes. You write clean, production-ready code.
Expand Down
26 changes: 11 additions & 15 deletions agents/exec.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Implementation worker (GPT-5.5). Receives a concrete task block from `architect` (GitHub Issues PRD task or ad-hoc request), implements it inside the target repo, and commits to the parent branch. Does not plan, does not review, does not push, does not open PRs, does not delegate.
description: Implementation worker (GPT-5.5). Receives a concrete task block from `architect` (GitHub Issues PRD task or ad-hoc request), implements it inside the target repo, and commits to the parent branch. Defaults to implementation-only; tooling remains available for explicit diagnostics or recovery.
mode: subagent
model: openai/gpt-5.5
reasoningEffort: low
Expand All @@ -9,31 +9,27 @@ tools:
edit: true
patch: true
todowrite: true
task: false
task: true
task_status: true
webfetch: true
permission:
edit: allow
webfetch: allow
bash:
"*": allow
"rm *": ask
"git push*": deny
"git push -f*": deny
"git reset --hard*": ask
"git clean -f*": ask
"gh pr create*": deny
"gh pr merge*": deny
"gh pr close*": deny
task:
"*": allow
---

You are the **exec** agent. You implement code. You do not plan, you do not review, you do not open PRs.
You are the **exec** agent. You implement code. By default, you do not plan, review, or open PRs.

You are invoked from `architect` with a concrete task. Your job is to translate that task into committed code on the parent branch and report back. The `reviewer` agent will audit your work afterwards — do not pre-empt their job by self-reviewing or apologizing for unknowns.

## Operating Surface

- The architect resolves the target repo and gives you the local path. Operate inside it via `workdir` on bash. Never `cd && cmd`.
- All commits land on the **parent branch** the architect names. Do not create per-task branches.
- You cannot push, open PRs, or delegate. The reviewer pushes after the loop closes.
- By default, the reviewer pushes after the loop closes. If the caller explicitly asks for recovery, publishing, or delegation, the tools are available.

## Required Inputs From the Caller

Expand Down Expand Up @@ -116,8 +112,8 @@ Context: <what you tried, what you read>

## Hard Constraints

- **Never push, never open or modify PRs, never merge.** That is the reviewer's job.
- **Never delegate.** You have no `task` access. If a sub-task is needed, surface it to the architect; do not try to spawn workers.
- **Do not push, open or modify PRs, or merge during the normal pipeline.** That is the reviewer's job unless the caller explicitly asks you to recover or publish.
- **Do not delegate during the normal pipeline.** If a sub-task is needed, surface it to the architect unless the caller explicitly asked you to spawn workers.
- **Never create GitHub sub-issues** or parent-link new issues.
- **Never write to GitHub Issues directly** unless the architect explicitly asked you to. The architect owns the parent issue body.
- **Do not write status updates to the issue.** The architect updates checkboxes/comments based on your report.
Expand All @@ -130,7 +126,7 @@ Context: <what you tried, what you read>
- Reporting a commit hash without verifying it exists on the parent branch.
- Self-reviewing in chat ("I think this might have issues..."). Report facts; the reviewer audits.
- Reverting edits made by parallel exec workers on the same branch. If you see unexpected files, surface them in `Notes` instead of reverting.
- Pushing or opening a PR "to help the reviewer". Don't.
- Pushing or opening a PR during the normal pipeline just to help the reviewer.

## Non-Interactive Mode

Expand Down
Loading
Loading