tmb_recovery · diff
git:20261105.b918908 to git:20261105.e930e06
1 added, 0 removed. Audit A to A.
---
name: tmb_recovery
description: Bro's response when something fails — an MCP tool returns is_error=true (halt + surface), the trajectory-server is unreachable (degraded sqlite3 readonly fallback), or the world model is unavailable or empty (reconnect the server, or run /scan); when the failure is a TMB bug, offering an opt-in bug report the Human previews before bro files it. Loaded reactively on the first failure of a session.
allowed-tools: Bash(node --experimental-sqlite "${CLAUDE_PLUGIN_ROOT}/mcp/trajectory-server/dist/cli.js" bro-sqlite-readonly:*), mcp__plugin_tmb_trajectory-server__discussion_append, mcp__plugin_tmb_trajectory-server__bug_report_draft, mcp__plugin_tmb_trajectory-server__audit_append
+ effort: high
---
# Recovery — three failure modes, three responses, and a bug report
Bro keeps the user-visible flow moving on recoverable errors. Each failure class has a deterministic fallback path; the judgment is *which class applies*.
The bundled fallback `node --experimental-sqlite "${CLAUDE_PLUGIN_ROOT}/mcp/trajectory-server/dist/cli.js" bro-sqlite-readonly` is for §B (trajectory-server unreachable). Invoke it via Bash — use it as a black box, not by reading its source directly.
## A. MCP tool returns is_error=true
When any MCP call result has `is_error: true` or content includes `{"error": ...}`, the call was wrong — not the doctrine.
### Protocol
1. **Halt the current flow immediately.** Treat the failed call as the chain's last.
2. **Pick one path** based on the error class:
- **Surface verbatim** to the Human and ask how to proceed, OR
- **If recoverable AND you know the corrected call**, append a note recording the error and the corrected retry, then retry.
### Error classification
| Error shape | Meaning | Right response |
|---|---|---|
| `forbidden` | Bro called a tool scoped to another role. | Reconsider whether the action is bro's responsibility. Change the call signature before retrying. |
| `validation` | Input didn't match the schema (e.g. malformed branch_id). | Fix the input. Retry with the corrected payload. |
| `missing_argument` | The call omitted or misspelled a required argument such as `agent`. | Fix the call and retry. |
| Constraint failure | DB integrity violation (foreign key, unique). | Surface to Human; usually means a stale or duplicate write attempt. When the write was correct for the data, offer a bug report (§D). |
| Internal error | The server threw on input that matches its schema. | Surface to Human, then offer a bug report (§D). |
| `no matching deferred tools` | The MCP child process is dead. | Switch to degraded mode (§B). |
## B. Trajectory-server unreachable
Two distinct sub-cases — read the `additionalContext` warning from `mcp-health-check` at session start to tell which one you have.
### B.1 — MCP absent this session
The plugin's MCP server is missing from CC's resolved-plugin list — a full quit + relaunch won't fix it because CC persists the cached config to disk.
<!-- LOAD-BEARING-SAFETY: halt-on-B.1 (MCP absent) is mandatory — state-writing tools are unreachable and silent degradation corrupts the audit trail -->
**During the failure, bro halts.** State-writing tools are unreachable; halt to avoid corrupting the audit trail. Read-only sqlite3 fallback (`node --experimental-sqlite "${CLAUDE_PLUGIN_ROOT}/mcp/trajectory-server/dist/cli.js" bro-sqlite-readonly`) is still available for emergency reads.
Recovery escalation — try IN ORDER, stop at the first that brings MCP back:
1. From the project root, `claude plugin uninstall tmb@<your-marketplace> --scope <your-scope>`, quit CC fully, then reinstall with `claude plugin install tmb@<your-marketplace> --scope <your-scope>` and relaunch CC — the marketplace-valid remedy (a standard install has only `~/.claude/plugins/cache/`, no plugin source on disk).
2. Dev-only: if you have the plugin source checked out, `claude --plugin-dir <plugin-source>` re-resolves the MCP config from disk with the plugin cache cleared.
### B.2 — MCP died mid-session
The MCP child process was alive at session start but is now gone — CC's plugin list is correct; the process just needs to be re-spawned. (`forbidden`/`validation`/`constraint` errors are §A, not this — they mean the server IS running and rejected bad input.)
**Read fallback:** `node --experimental-sqlite "${CLAUDE_PLUGIN_ROOT}/mcp/trajectory-server/dist/cli.js" bro-sqlite-readonly <tool_name> [json_args]`. Run `--list` to see supported tools. Parse stdout as JSON — same shape as the corresponding MCP tool. Write tools are refused with a structured error; surface the refusal to the Human.
**Recovery:**
1. Kill only THIS project's zombie server — identify it by the `trajectory.db` it holds open, never a machine-wide match that would take down every project's server:
```bash
DB=$(node --experimental-sqlite "${CLAUDE_PLUGIN_ROOT}/mcp/trajectory-server/dist/cli.js" bro-sqlite-readonly --dry-run \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["db_path"])')
PIDS=$(lsof -t "$DB" 2>/dev/null); [ -n "$PIDS" ] && kill $PIDS
```
2. Restart Claude Code — server re-spawns on fresh session.
3. If the first `mcp__plugin_tmb_*` call still fails, the failure has escalated into the MCP-absent case — follow B.1.
Once the restarted server answers, offer a bug report for the death (§D).
## C. World model unavailable or empty
The world model is a set of tables in the trajectory DB, so it is reachable whenever the trajectory-server is.
1. **`warning: world-model-empty`** — no scan has filled it yet. Run `/scan` (`scan_run`).
2. **`warning: world-model-unavailable`**, or `scan_run` returning `graph_db_open_failed` — the server has no handle on the trajectory DB. Treat it as §B.2: fully quit and relaunch CC so a fresh server process opens the DB.
Scanned data survives a restart — only an empty world model needs `/scan`.
## D. Offer a TMB bug report
Reports go to github.com/trustmybot/plugin and are public. Offer one only for a bug in TMB itself, after the failure is surfaced and the Human's work is back on track.
### When
Offer a report for exactly these failures, with the `error_class` to pass:
| Failure | `error_class` |
|---|---|
| A §A constraint failure on a write that was correct for the data | `constraint` |
| A §A internal error: the server threw on input that matches its schema | `internal` |
| A §B.2 server death | `server_died` |
| A `trajectory_doctor` contract violation | `doctor` |
| A hook deny or gate that bro can show is wrong for the input it judged | `hook_deny` |
| The Human says the failure is a TMB bug | `user_reported` |
Everything else stays out of §D: a `validation` or `forbidden` error (bro's own call was wrong), a deny bro agrees with, and a failure in the Human's own code or tools.
Bro offers when the Human said yes in `/onboard` (`bug_report_consent` is `yes`), or when the Human asked for this report, which bro passes as `user_asked: true`. Each signature is offered once per session: after the first offer, a repeat of the same failure gets no second offer.
### Draft
Call `bug_report_draft(agent='bro', …)` with `tool` (the failing tool, or a hook as `hook:<name>`), `error` verbatim, `error_class`, a one-line `summary`, `what_happened` (expected vs actual), numbered `reproduce` steps from a fresh session, `frequency`, `impact`, and when known `args`, `doctor_violations`, `workaround`, `claude_code_version` and `session`. The tool redacts paths, tokens, remotes and project names, and returns `{title, body, signature, marker, prefill_url, prefill_truncated, consent, redactions, already_filed}`.
- A refusal with `reason: 'bug_report_consent_required'` means the Human has not opted in: carry on with the work and leave reports unmentioned.
- `already_filed` set: tell the Human this failure was already reported at `already_filed.url` and offer nothing new.
- Otherwise search for an open or closed report with the same signature:
```bash
gh issue list --repo trustmybot/plugin --state all --search "<signature>"
```
On a match, offer to add this session's environment block (the body's `### Environment` section) as a comment on that issue instead of a new report.
### Preview
<!-- LOAD-BEARING-SAFETY: bro files a report or comment only after the Human picks File it for that exact text — reports are public and the redaction is pattern-based, so the Human is the final check for secrets and private code -->
Show the Human the full title and body (or the full comment text) exactly as they would be filed, then ask one AskUserQuestion:
- **File it** — the Human read the report and found no secrets, private code or personal paths. This answer is the Human ticking the form's second check, so change `- [ ] I read this report and it contains no secrets, private code or personal paths.` to `- [x] …` in the body before filing.
- **Edit first** — apply the Human's edits, keep the `marker` line at the end, and show the preview again.
- **Don't file** — drop the report and carry on.
### File
On **File it**, write the body to a temp file under `$TMPDIR` and run:
```bash
gh issue create --repo trustmybot/plugin --title "<title>" --body-file "<temp file>"
gh issue comment <number> --repo trustmybot/plugin --body-file "<temp file>" # for a dedup match
```
Each `gh` call goes through the normal Bash permission prompt. When `gh` is missing or not authenticated, give the Human `prefill_url` to open in a browser, where they tick the checks and submit. When `prefill_truncated` is true, name the sections the link left out or shortened (compare the link's fields with the preview; `Minimal repro script`, `Doctor output` and `Workaround` are dropped first) and ask the Human to paste them from the preview.
### Record
After filing, call `audit_append(agent='bro', from_node='bro', issue_id=<in-flight issue>, event_type='bug_report_filed', summary=<title>, content_json={signature, url, mode})`, passing `content_json` as a JSON string, with `mode` `issue` (a new report), `comment` (added to a dedup match) or `link` (the Human got `prefill_url`; `url` is that link). A later draft of the same failure then reports it as `already_filed`.