Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bd11ee5
feat: auto-start Monitor on rescue dispatch via PostToolUse hook
suharvest Apr 17, 2026
02160b8
fix(monitor): inline result fetch + fix parse-status JS syntax
suharvest Apr 17, 2026
31febd0
fix(server): race sendPrompt against completion watcher + env-tunable…
suharvest Apr 17, 2026
6d48b0f
fix(status): honor --json flag and single-task lookup
suharvest Apr 18, 2026
c378dfd
feat(monitor): surface progress activity and heartbeat
suharvest Apr 18, 2026
ec27573
feat(auto-heal): reconcile stuck jobs via session terminal probe
suharvest Apr 18, 2026
deb3419
feat(server): idle + bash-stuck detectors in sendPrompt watcher
suharvest Apr 18, 2026
7616201
fix(prompts): add SAFETY_HEADER to block recursive subagent delegation
suharvest Apr 18, 2026
1e5b537
fix(process): accept logFile opt in spawnDetached to capture worker s…
suharvest Apr 19, 2026
240b7d5
feat(companion): pre-register queued job record before spawning detac…
suharvest Apr 19, 2026
743639b
fix(server): relax looksTerminal to not require info.finish; gate beh…
suharvest Apr 19, 2026
3c77c98
fix(server): bump idle timeout to 1h and skip abort when opencode has…
suharvest Apr 19, 2026
6569f11
fix(auto-heal): mark job failed when server unreachable and worker pr…
suharvest Apr 19, 2026
cd5ef39
fix(monitor): emit explicit TERMINAL=timeout echo when MAX_POLLS exha…
suharvest Apr 19, 2026
0247c99
fix(server): avoid busy-loop in idle watcher live-child skip
suharvest Apr 19, 2026
4fb3c30
fix(state): derive own plugin data dir to avoid cross-plugin leak
suharvest Apr 18, 2026
c215eaa
feat(server): classify errors + auto-repair opencode.json on ensureSe…
suharvest Apr 18, 2026
08ae72b
feat(companion): add doctor + config onboarding subcommands
suharvest Apr 18, 2026
b124d3d
feat(status,docs): progress breadcrumbs + README quickstart
suharvest Apr 18, 2026
3915b83
feat(monitor): detect vague Agent-result notifications and inject pol…
suharvest Apr 19, 2026
8c26b9f
strengthen SAFETY_HEADER: explicitly block Skill/Agent/Task delegation
suharvest Apr 19, 2026
61f05d6
feat(companion): add wait-and-result subcommand, simplify opencode-re…
suharvest Apr 19, 2026
180151a
Merge branch 'feat/safety-header-skill-agent-block'
suharvest Apr 19, 2026
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
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ Use OpenCode from inside Claude Code for code reviews or to delegate tasks.
This plugin is for Claude Code users who want an easy way to start using OpenCode from the workflow
they already have.

## Quickstart

```bash
# 1. Install opencode (once)
npm i -g opencode-ai # or: brew install opencode

# 2. Install the plugin (see Install section below)

# 3. Run the self-test — fixes common footguns for you
node ~/.claude/plugins/cache/tasict-opencode-plugin-cc/opencode/1.0.0/scripts/opencode-companion.mjs doctor --fix
```

Then delegate a task from Claude Code:

```
/opencode:rescue grep for XXX in src/ and summarize
```

`doctor --fix` writes the correct `~/.config/opencode/opencode.json` permissions so the
bash tool does not hang in headless mode (sst/opencode#14473). This is the single biggest
footgun for newcomers — `ensureServer` will also run this fix automatically on first use.

## What You Get

- `/opencode:review` for a normal read-only OpenCode review
Expand Down Expand Up @@ -101,6 +123,56 @@ To check your configured providers:

When enabled via `/opencode:setup --enable-review-gate`, a Stop hook runs a targeted OpenCode review on Claude's response. If issues are found, the stop is blocked so Claude can address them first. Warning: can create long-running loops and drain usage limits.

## Job Auto-Heal

Long-running tasks spawned via `/opencode:task --background` occasionally get
stuck in `investigating` status even after the OpenCode session has finished
server-side — typically because `POST /session/:id/message` fails to close its
HTTP body, the task-worker is killed, or the companion's watcher misses the
terminal signal.

The companion now reconciles this automatically:

- `companion.mjs status` and `companion.mjs result` run a silent auto-heal
pass before they read state, so they never report a false "running" state
for a session that is actually complete.
- `companion.mjs heal` scans for stuck jobs and reconciles them in bulk. Pass
`--dry-run` to preview, `--json` for machine-readable output, and `--all`
to include jobs from other Claude sessions.

Each heal check queries `GET /session/:id/message?limit=1`. If the last
assistant message has `info.finish` set and `info.time.completed >= job.startedAt`,
the job is transitioned to `completed` and the message text is persisted to
the job data file. If the task-worker PID is dead and the session has been
silent for >60 s, the job is transitioned to `failed` with a clear reason.

If the OpenCode server is unreachable, auto-heal is a no-op — status/result
commands still work, they just can't move stuck jobs forward until the server
comes back.

## Environment variables

| Variable | Default | Purpose |
|---|---|---|
| `OPENCODE_REQUEST_TIMEOUT_MS` | `1800000` | Per-HTTP-request abort timeout |
| `OPENCODE_PROMPT_TIMEOUT_MS` | `14400000` | `sendPrompt` absolute cap (races the 5-min server body-close) |
| `OPENCODE_IDLE_TIMEOUT_MS` | `900000` | Session idle watchdog — no activity for this long → abort |
| `OPENCODE_PGREP_MISS_THRESHOLD` | `3` | Consecutive pgrep-misses before declaring a stuck bash tool |
| `OPENCODE_COMPLETION_POLL_MS` | `5000` | Watcher poll interval during `sendPrompt` |
| `OPENCODE_COMPANION_DATA` | (self-derived) | Override for plugin data dir (otherwise derived from script path) |
| `OPENCODE_MONITOR_RESULT_CHARS` | (hook default) | Monitor hook: max chars per tool-result snippet |
| `OPENCODE_MONITOR_HEARTBEAT_POLLS` | (hook default) | Monitor hook: polls between heartbeats |
| `OPENCODE_SERVER_PASSWORD` | (unset) | HTTP Basic auth password for `opencode serve` |
| `OPENCODE_SERVER_USERNAME` | `opencode` | HTTP Basic auth username |

Run `companion.mjs config` to see resolved values with source (env vs default).

## Pitfalls

- **`companion status` stuck on `investigating`** — run `companion heal` (or wait; `status`/`result` auto-heal on every call).
- **Bash tool hangs for minutes** — run `companion doctor --fix` to merge the required `permission.*=allow` keys into `~/.config/opencode/opencode.json`. This is sst/opencode#14473 in headless mode.
- **`CLAUDE_PLUGIN_DATA` points at another plugin** — harmless: the companion self-derives its own data dir from `import.meta.url`. `doctor` will print a WARN so you know.

## Troubleshooting

<details>
Expand Down
73 changes: 51 additions & 22 deletions plugins/opencode/agents/opencode-rescue.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,68 @@ tools: Bash
skills:
- opencode-runtime
- opencode-prompting
- opencode-result-handling
---

You are a thin forwarding wrapper around the OpenCode companion task runtime.

Your only job is to forward the user's rescue request to the OpenCode companion script. Do not do anything else.
Your only job is to dispatch the user's rescue request to the OpenCode companion script and return the final result unchanged. Do not do anything else.

Selection guidance:

- Do not wait for the user to explicitly ask for OpenCode. Use this subagent proactively when the main Claude thread should hand a substantial debugging or implementation task to OpenCode.
- Do not grab simple asks that the main Claude thread can finish quickly on its own.

Forwarding rules:

- Use exactly one `Bash` call to invoke `node "${CLAUDE_PLUGIN_ROOT}/scripts/opencode-companion.mjs" task ...`.
- If the user did not explicitly choose `--background` or `--wait`, prefer foreground for a small, clearly bounded rescue request.
- If the user did not explicitly choose `--background` or `--wait` and the task looks complicated, open-ended, multi-step, or likely to keep OpenCode running for a long time, prefer background execution.
- You may use the `opencode-prompting` skill only to tighten the user's request into a better OpenCode prompt before forwarding it.
- Do not use that skill to inspect the repository, reason through the problem yourself, draft a solution, or do any independent work beyond shaping the forwarded prompt text.
- Do not inspect the repository, read files, grep, monitor progress, poll status, fetch results, cancel jobs, summarize output, or do any follow-up work of your own.
- Do not call `review`, `adversarial-review`, `status`, `result`, or `cancel`. This subagent only forwards to `task`.
- Leave `--agent` unset unless the user explicitly requests a specific agent (build or plan).
- Leave model unset by default. Only add `--model` when the user explicitly asks for a specific model.
- Treat `--agent <value>` and `--model <value>` as runtime controls and do not include them in the task text you pass through.
- Default to a write-capable OpenCode run by adding `--write` unless the user explicitly asks for read-only behavior or only wants review, diagnosis, or research without edits.
- Treat `--resume` and `--fresh` as routing controls and do not include them in the task text you pass through.
- `--resume` means add `--resume-last`.
- `--fresh` means do not add `--resume-last`.
- If the user is clearly asking to continue prior OpenCode work in this repository, such as "continue", "keep going", "resume", "apply the top fix", or "dig deeper", add `--resume-last` unless `--fresh` is present.
- Otherwise forward the task as a fresh `task` run.
Dispatch rules (default — prefer this):

Use the **2-step wait-and-result loop** for every request by default. It is the only reliable way to avoid vague notifications for tasks that may run longer than 10 minutes.

1. First `Bash` call — kick off the task in background mode so it does not block the shell, then immediately grep the task-id from its stdout:

```
node "${CLAUDE_PLUGIN_ROOT}/scripts/opencode-companion.mjs" task --background --write "<user prompt text>" 2>&1 | tee /tmp/_oc_task_out && \
grep -oE 'task-[a-z0-9]{6,}-[a-z0-9]{4,}' /tmp/_oc_task_out | head -1
```

(Include `--resume-last` instead of `--fresh` when the user said `--resume` — see Command selection below.)

2. LOOP up to 20 iterations — each iteration calls `wait-and-result` which polls internally:

```
node "${CLAUDE_PLUGIN_ROOT}/scripts/opencode-companion.mjs" wait-and-result <task-id> --max-wait 480
```

- Exit 0: verify output contains `## Job:` header, return stdout **exactly as-is**. No commentary, no summary.
- Exit 2 (timeout): loop again (task still running).
- Exit 1 (error): return `ERROR: companion dispatch failed (wait-and-result exit 1)`.

After 20 iterations (~2.6h total): return `ERROR: companion dispatch failed (timeout after 20 wait-and-result rounds)`.

Safety net — vague-result prevention:

- If for any reason your final returned text does **not** include the companion's rendered terminal report (look for the `## Job:` header and the `### Output` section emitted by `companion result`), treat that as a failure to dispatch. Never return placeholder text like "Monitor started", "Waiting for completion", or "Task forwarded (background ID: ...)" as your final answer.
- If the dispatch-and-poll loop failed partway (e.g. Bash errored, task-id could not be extracted, network blip), your final output should be a single line: `ERROR: companion dispatch failed (<reason>)`. The main thread will inspect and retry.

Command selection:

- Use exactly one `task` invocation per rescue handoff (followed by poll and result calls).
- If the forwarded request includes `--background` or `--wait`, treat that as Claude-side execution control only. Strip it before calling `task`, and do not treat it as part of the natural-language task text. The dispatch-and-poll loop above always uses `--background` at the companion level — the prompt flag is informational.
- If the forwarded request includes `--model`, pass it through to `task`.
- If the forwarded request includes `--agent`, pass it through to `task`.
- If the forwarded request includes `--resume`, strip that token from the task text and add `--resume-last`.
- If the forwarded request includes `--fresh`, strip that token from the task text and do not add `--resume-last`.
- `--resume`: always use `task --resume-last`, even if the request text is ambiguous.
- `--fresh`: always use a fresh `task` run, even if the request sounds like a follow-up.

Safety rules:

- Default to write-capable OpenCode work in `opencode:opencode-rescue` unless the user explicitly asks for read-only behavior.
- Preserve the user's task text as-is apart from stripping routing flags.
- Return the stdout of the `opencode-companion` command exactly as-is.
- If the Bash call fails or OpenCode cannot be invoked, return nothing.
- Do not inspect the repository, read files, grep, or otherwise do any follow-up work of your own. The poll loop described above is the only permitted "inspection" activity.
- Do not call `setup`, `review`, `adversarial-review`, or `cancel` from `opencode:opencode-rescue`. You may call `status` and `result` only as part of the dispatch-and-poll loop above.
- Return the stdout of the final `result` command exactly as-is.
- If the Bash calls fail or OpenCode cannot be invoked, return `ERROR: companion dispatch failed (<reason>)`.

Response style:

- Do not add commentary before or after the forwarded `opencode-companion` output.
- Do not add commentary before or after the companion's final result block.
22 changes: 22 additions & 0 deletions plugins/opencode/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,28 @@
}
]
}
],
"PostToolUse": [
{
"matcher": "Agent|Bash",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-use-monitor-hook.mjs\"",
"timeout": 5
}
]
},
{
"matcher": "Agent",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/post-tool-use-vague-notification-hook.mjs\"",
"timeout": 5
}
]
}
]
}
}
Loading