codex-opencode-pi
Skill72 starsChanged 37 days agoMITView on GitHub
Codex CLI, OpenCode CLI, and Pi CLI runner protocols for Untether. Covers JSONL event types, event translation, resume mechanisms, and key differences between engines. All three are non-interactive (no control channel).
In this repository11 other files it gives its agents.
- AGENTS.mdAGENTS.md
- .codex/AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- llms-full.txtllms.txt
- llms.txtllms.txt
- claude-stream-jsonSkill
- jsonl-subprocess-runnerSkill
- release-coordinationSkill
- telegram-bot-apiSkill
- untether-architectureSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 89
Prefer item with phase == "final_answer"
- Very specific: line 210
If a session header arrives with a UUID, the resume token is promoted to the 8-char short ID
- Very specific: line 253
Add reference docs in docs/reference/runners/myengine/
- Very specific: line 254
Add tests mirroring existing tests/test_*_runner.py patterns
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Codex, OpenCode, and Pi Runner Protocols
These three engines are non-interactive only — no control channel, no permission prompts. They extend JsonlSubprocessRunner and use its run_impl (ClaudeRunner replaces it; OpenCodeRunner only wraps it with a pre-spawn version check, #970).
A failed resumed run keeps the chat's saved session unless the error names the resume itself (_RESUME_FAILURE_RE in runner_bridge.py: Codex no rollout found, OpenCode Session not found, Pi No session found, …) — none of the three reports num_turns, so the Claude-style 0-turn rule can't apply (#952). A new "session not found" wording must be added there.
Quick comparison
| Aspect | Codex | OpenCode | Pi |
| CLI | codex exec --json | opencode run --format json | pi --print --mode json |
| Event model | Turn-based (items in turns) | Step-based (tools in steps) | Agent-based (messages + tools) |
| Resume line | `codex resume <thread_id>` | `opencode --session ses_XXX` | `pi --session <token>` |
| Resume token | thread_id (UUID) | ses_XXXX (26+ chars) | UUID short (8 chars) or path |
| Final answer | agent_message item | Accumulated text events | message_end assistant content |
| Error signal | turn.failed | error event / missing step_finish | stopReason in message_end |
Codex
Key files
| File | Purpose |
src/untether/runners/codex.py | CodexRunner implementation |
src/untether/schemas/codex.py | msgspec structs for Codex events |
docs/reference/runners/codex/exec-json-cheatsheet.md | JSONL event shapes |
docs/reference/runners/codex/untether-events.md | Event mapping spec |
CLI invocation
codex [extra_args…] [--profile P] [--model MODEL] [-c model_reasoning_effort=LEVEL] \
exec --json --skip-git-repo-check --color=never \
[--sandbox read-only] [resume <thread_id> | resume --last] -- Prompt on stdin (trailing
-means read stdin) - Resume:
resume <thread_id> -(resume --last -for/continue) - Safe mode is genuinely read-only:
--sandbox read-only(exec-level, so it outranks a root-level sandbox flag), only insafepermission mode and beforeresume(codex exec resumehas no--sandbox); no--ask-for-approval—codex execforces approvalneveritself (#830) extra_argssit at the root (beforeexec) and are checked byrunners/extra_args_guard.py: bypass/sandbox-escape flags (--yolo,--sandbox danger-full-access,-c …danger…,-C/--cd, …) disable the engine at load (#209)--skip-git-repo-check --color=neverfor clean output
JSONL events
| Event | Untether mapping |
thread.started | StartedEvent(resume=thread_id) |
item.started | ActionEvent(phase="started") |
item.updated | ActionEvent(phase="updated") |
item.completed | ActionEvent(phase="completed") |
turn.completed | CompletedEvent(ok=True, answer=final_answer) |
turn.failed | CompletedEvent(ok=False, error=message) |
error (transient) | Progress note (reconnect handling) |
Item types
| Item type | ActionKind | Notes |
command_execution | command | ok = (status=="completed" && exit_code==0) |
mcp_tool_call | tool | Title: server.tool |
file_change | file_change | detail.changes = [{path, kind}] |
web_search | web_search | Title: query / url / pattern from the untyped action; placeholder web search on started (#419) |
todo_list | note | detail.done, detail.total |
reasoning | note | Reasoning text |
agent_message | (not emitted) | Stored as final answer candidate |
error (item) | warning | Non-fatal warning: ok=True, title prefixed ⚠️ (one ⚠️ row, not ✗); Codex emits each config warning twice, so a repeated message is dropped per run (#987) |
Final answer selection
Multiple agent_message items may appear. Selection:
- Prefer item with
phase == "final_answer" - Fall back to last unnamed
agent_message - Used in
CompletedEvent.answer
Config keys
[codex]
profile = "Codex" # Codex profile name
extra_args = [] # additional CLI flagsOpenCode
Key files
| File | Purpose |
src/untether/runners/opencode.py | OpenCodeRunner implementation |
src/untether/schemas/opencode.py | msgspec structs for OpenCode events |
docs/reference/runners/opencode/runner.md | Runner spec |
docs/reference/runners/opencode/stream-json-cheatsheet.md | JSONL event shapes |
docs/reference/runners/opencode/untether-events.md | Event mapping spec |
CLI invocation
opencode run --format json [--session SESSION_ID] [--model MODEL] -- <prompt>- Prompt as positional arg after
-- - Resume:
--session ses_XXX - Session IDs:
ses_prefix + 20+ chars - Supported CLI: OpenCode 1.x (npm
opencode-ai) only (#970).run_implprobesopencode --versionoff the event loop (cached per binary path + mtime; failed probes aren't cached) and refuses 2.x (@opencode/cli, which runs prompts on a shared background service outside Untether's process control) with onecompleted_errorcarryingPRESPAWN_BLOCKED_KEY, so nothing spawns and the saved session is kept. An unparsable version fails open (opencode.version.unknown). Tests stub the probe via an autouse fixture intests/conftest.py runauto-rejectsaskpermissions (never auto-approves; only--dangerously-skip-permissions, which Untether doesn't pass) and deniesquestion/plan_enter/plan_exit. OpenCode does auto-compact, butrun --format jsondoesn't report it (#969)
JSONL events
| Event | Untether mapping |
step_start (first, with sessionID) | StartedEvent(resume=sessionID) |
tool_use (status="completed") | ActionEvent(phase="completed", ok=exit==0) |
tool_use (status="error") | ActionEvent(phase="completed", ok=False) |
text | Final-answer part (no action): each event is one complete part, keyed by part.id and joined with a blank line (#955) |
step_finish (reason="stop") | CompletedEvent(ok=True, answer=text) |
error | CompletedEvent(ok=False, error=message) |
Tool mapping
| Tool | ActionKind |
bash, shell | command |
edit, write, multiedit | file_change |
read, glob, grep | tool |
websearch, web_search, webfetch, web_fetch | web_search |
todowrite, todoread | note |
task | tool |
| (other) | tool |
Usage
Each step_finish's cost and tokens accumulate into OpenCodeStreamState; _build_usage() puts them on
CompletedEvent.usage (total_cost_usd + usage{input,output,reasoning,cache_*}_tokens). The bridge accumulates
them per session for /usage and /export (#417).
Config keys
[opencode]
model = "anthropic/claude-sonnet-5-5" # must be OpenCode's provider/model form; bare names failPi
Key files
| File | Purpose |
src/untether/runners/pi.py | PiRunner implementation |
src/untether/schemas/pi.py | msgspec structs for Pi events |
docs/reference/runners/pi/runner.md | Runner spec |
docs/reference/runners/pi/stream-json-cheatsheet.md | JSONL event shapes |
docs/reference/runners/pi/untether-events.md | Event mapping spec |
CLI invocation
pi --print --mode json [--session SESSION_PATH] \
[--provider PROVIDER] [--model MODEL] <prompt>- Prompt as positional arg (prefixed with space if starts with
-) - Resume:
--session <token>(short ID or full path) - Minimum version: 0.45.1
- Environment:
NO_COLOR=1,CI=1(set by runner)
JSONL events
| Event | Untether mapping |
session | Session ID extraction, possible ID promotion |
agent_start | StartedEvent(resume=session_token) |
tool_execution_start | ActionEvent(phase="started") |
tool_execution_end | ActionEvent(phase="completed", ok=!isError) |
message_end (assistant) | Final answer + usage stored |
agent_end | CompletedEvent(ok=..., answer=last_text) |
Session ID promotion
Pi has a unique resume mechanism:
- For new runs, Untether generates a session
.jsonlfile path - If a
sessionheader arrives with a UUID, the resume token is promoted to the 8-char short ID allow_id_promotionflag ensures this only happens once- This gives a user-friendly resume token instead of a long path
Session path format: ~/.pi/agent/sessions/--<sanitized-cwd>--/<date>-<uuid>.jsonl
Tool mapping
| Tool | ActionKind | Title source |
bash | command | args.command |
edit, write | file_change | args.path |
read, grep, find, ls | tool | tool: <path> or tool: <pattern> |
| (other) | tool | tool name |
Error detection
stopReasoninmessage_end:"error"or"aborted"->ok=False- No
agent_endreceived ->CompletedEvent(ok=False, error="stream ended...")
Config keys
[pi]
provider = "anthropic" # or "openai", "google", etc.
model = "claude-sonnet-4-5-20250929"
extra_args = []Adding a new engine
To add a new engine runner:
- Create
src/untether/runners/myengine.py - Define schemas in
src/untether/schemas/myengine.py - Implement
MyEngineRunner(JsonlSubprocessRunner)with required template methods - Export
BACKEND = EngineBackend(id="myengine", build_runner=..., cli_cmd="myengine") - Register in
pyproject.tomlentry points:myengine = "untether.runners.myengine:BACKEND" - Add reference docs in
docs/reference/runners/myengine/ - Add tests mirroring existing
tests/test_*_runner.pypatterns
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

