The sentinel pattern: extracting plans from any agent
The constraint
Tetherlab coordinates on a plan: a {summary, steps} object the master can match against the concept registry before the agent acts. The hard part is getting that object out of an agent the shim wraps at the process level, with no SDK and no cooperation from the agent itself.
Design principle two: the agent is unconscious of the coordination system. It sees only normal turn-taking. So the shim cannot ask the agent to call a special API. It can only read what the agent emits and write into its input.
Two strings and a round-trip
The shim coaxes the agent into a planning turn, then watches for two sentinel strings the agent emits around its plan: ::tether-plan-ready:: and ::tether-plan-end::. When it sees the open sentinel, it does one JSON round-trip to pull the structured {summary, steps} and submits it as an intent. No screen-scraping of prose, no fragile natural-language parser.
The child runs under a PTY so its normal TUI renders for the developer. Coordination happens out-of-band on a separate read channel. The two never fight over the terminal.
{
"summary": "Refactor auth layer to use JWT instead of session cookies",
"steps": [
"Replace cookie-based session middleware with JWT verification",
"Deprecate the in-memory session store"
]
}Same shape, different transport
A ToolProfile classifies the wrapped binary and the orchestrator dispatches on it. The plan always comes back the same shape; only the transport changes.
- Claude Code runs under
claude --session-id <UUID>. The shim tails the JSONL transcript at~/.claude/projects/<encoded-cwd>/<UUID>.jsonlfor the sentinels. - OpenCode runs under
opencode --port=N. The shim coordinates over its local HTTP server API instead of a transcript file. - Anything unrecognized runs as Generic: plain PTY passthrough, no coordination. The shim stays transparent.
When the plan never arrives
Claude only writes its transcript after its first persisted turn, so the shim waits for the file to appear. The deadline is generous: DEFAULT_TRANSCRIPT_DEADLINE is 10 minutes, overridable with TETHER_TRANSCRIPT_TIMEOUT_SECS. An earlier 30-second gate fired before a human finished typing their first message, so coordination never started. The wait now also races the child, so a quit or crash unblocks at once.
If the deadline elapses, the shim degrades to plain passthrough: the live session keeps running, coordination just stops. A wedged plan extraction never takes the developer's session down with it.
The invariants that keep it honest
- No agent execution starts before the master returns
continue. - Fail closed on auth and network errors.
- No fabricated intents: if no plan can be extracted, the run exits without submitting anything.
Coordination injects are best-effort and log-on-error, so a stalled write degrades gracefully rather than hanging the shim. The result is a coordination layer that adds nothing to a session it cannot help, and blocks the moment it cannot guarantee safety.
Next steps
See the pattern in practice in wrapping Claude Code, or read what the master does with the plan in intent-level conflict detection.
Frequently asked questions
- Does the agent need to know about Tetherlab?
No. The agent sees only normal turn-taking. The shim coaxes a planning turn and reads two sentinel strings the agent emits around its plan; all system-aware behavior lives in the shim and master.
- What if the agent never produces a plan?
The shim waits up to a 10-minute deadline (overridable via
TETHER_TRANSCRIPT_TIMEOUT_SECS), then degrades to plain passthrough. The session keeps running; coordination simply stops. It never fabricates an intent.- Which agents are supported today?
Claude Code (via its session transcript) and OpenCode (via its HTTP server). Anything unrecognized runs as plain passthrough with no coordination. The Codex shim is on the roadmap.