Engine CLI Compatibility
VibeFlow dispatches work to external AI coding CLIs (claude, codex, copilot, opencode, antigravity) and drives auxiliary tools (bun). Those CLIs change their flags and output shapes without notice — a silent breaking change there becomes a silent breaking change in VibeFlow. This file records which CLI versions the current code was verified against, and what shape each integration assumes, so that when a CLI is bumped you know exactly what to re-check.
When you bump any engine CLI, re-run the verification steps below and update the “Verified” version + date. If a shape changed, fix the parser/invocation AND the fixture in the same PR.
Verified versions
| Tool | Verified version | Date | Install source |
|---|---|---|---|
| claude | 2.1.207 | 2026-07-12 | npm @anthropic-ai/claude-code |
| codex | 0.144.1 | 2026-07-12 | brew codex |
| copilot | 1.0.83 | 2026-09-08 | brew copilot (GitHub Copilot CLI) |
| opencode | 1.18.22 | 2026-08-27 | brew anomalyco/tap/opencode |
| agy | 1.1.4 | 2026-07-19 | %LOCALAPPDATA%\\agy\\bin\\agy.exe |
| bun | 1.4.0 | 2026-08-26 | (runtime) |
Per-engine integration contract
Source of truth: src/dispatch.ts (engineCommand) and src/dispatch/prompt.ts
(parseEngineSummary, parseSessionId).
claude
- Fresh invocation:
claude -p --output-format json - Resume:
claude -p -r <session_id> --output-format json(-ronly works with-p/print) - Skip perms: append
--dangerously-skip-permissions - Output shape: a single JSON envelope
{ "type": "result", "session_id": "...", "result": "<text>", "num_turns": N, "subtype": "success" }. The VibeFlow summary is a fenced ```json block inside.result. - Session id:
.session_idon thetype: "result"envelope (last JSON object → scanned in reverse).
codex
- Fresh invocation:
codex exec --json -(prompt on stdin via-) - Resume:
codex exec resume <thread_id> --json - - Output shape: JSONL — one JSON event per line. Key events:
{"type":"thread.started","thread_id":"<uuid>"}— first line, carries the session id.{"type":"item.completed","item":{"type":"agent_message","text":"```json\n{...}\n```"}}— the model’s answer; the VibeFlow summary is the fenced block insideitem.text.{"type":"item.completed","item":{"type":"reasoning","text":"..."}}— ⚠️ TRAP: the reasoning event often echoes the same json. The parser MUST targetitem.type === "agent_message"specifically and neverreasoning.{"type":"turn.completed","usage":{...}}— last line; NOT a summary.
- Session id:
thread_idon thethread.startedevent (first JSON object → scanned forward). - Why
--jsonmatters: without it,codex exec -emits plain text andparseEngineSummaryreturns garbage (verified: it picked up theturn.completedevent). The JSONL path inparseEngineSummaryexists solely to dig the summary out ofagent_messageand bail (return undefined) if a codex stream had noagent_message, so the reasoning echo is never mistaken for the answer. - Fixture:
test/fixtures/codex-json-stream.txt— a real 6-line--jsonstream containing BOTH anagent_messageand areasoningecho, so the reasoning-trap regression is covered.
copilot
- Fresh invocation:
copilot --allow-all -p <prompt>— every option (including--allow-all) must precede the-pflag; copilot 1.0.83 rejects any flag placed after-por after the prompt value. The prompt is an argv value, not stdin; argv is ~32K-capped so large prompts are written to.vibeflow/dispatch/<unit>.mdand a short pointerRead <abs path> and follow itis passed instead. - Tool flags:
--available-tools/--excluded-toolsaccept MCP tool names only in 1.0.83 (e.g.Neon-*from the user’s MCP config). Built-in tool intents (Read,Grep,Glob,WebFetch, …) are never passed to these flags — they are always treated as unknown names and silently ignored. Copilot has no read-only mode flag, so the conversation read-only sandbox is unenforced for copilot (enforcement flags are dropped rather than sent as invalid values). - Resume: exact by-id resume is not supported by VibeFlow. Latest-session shortcuts are not accepted as exact authority; a turn without a valid exact binding uses bounded structured own-history replay instead of silently omitting context.
- Version guard: the CLI has a history of silent breaking auto-updates (github/copilot-cli#1606 removed
--headless --stdio); whencopilot --versioncan’t be read, dispatch proceeds with a warning. The verified contract (2026-09-08, copilot 1.0.83) iscopilot [options...] -p <prompt>.
antigravity
- Fresh invocation:
agy -p <prompt>; prompt is one argv value and output is plain text. VibeFlow parses a fenced JSON block when present; other prose has no structured summary. - Prompt limit: VibeFlow rejects a UTF-8 prompt at or above 30 KiB before spawn.
agyhas no supported prompt-file/stdin replacement for print mode. - Resume: unavailable in VibeFlow’s exact-session authority. The adapter has no primary evidence for a safely captured and validated exact binding, so an exact claim fails closed. Fresh turn delivery uses bounded structured own-history replay; VibeFlow does not use latest-workspace shortcuts as exact authority.
- Workspace files:
AGENTS.md,.agents/agents/<name>/agent.md,.agents/skills/,.agents/mcp_config.json. - Hooks (unproven):
.agents/hooks.jsonusesPreToolUse/PostToolUsein the emitted config, but theagy 1.1.4PreToolUse deny canary did not fire in headless test. VibeFlow classifies antigravity as post-hoc-only until native enforcement is proven. Hook config generation is preserved (forward-compatible if agy later honors it), but no native guardrail is advertised. - Auth / reliability: Google OAuth/keyring is required;
vf doctor --probeis the live readiness check. Authenticatedagy 1.1.4fresh print and workspace-agent canaries passed on 2026-07-19. No safe exact-session binding is evidenced.
opencode
- Fresh work-unit invocation:
opencode run --format json --auto(prompt on stdin; no positional prompt sentinel) - Exact conversation resume:
opencode run --session <validated ses_...> --format jsonwith the prompt on stdin. The opaque id must pass VibeFlow’s native-session validator; latest-session--continueis not exact authority. - Auto perms: work-unit dispatch may use
--auto; the conversation session adapter removes it and rejects tool/sandbox claims it cannot enforce. - Output shape: JSONL — one JSON event per line. Key events:
{"type":"step_start","sessionID":"ses_..."}— carries the session id.{"type":"text","part":{"type":"text","text":"..."}}— the model’s text response; VibeFlow summary is the fenced json block insidetext.{"type":"step_finish","part":{"tokens":{...}}}— last line, carries token usage.
- Session id:
sessionIDon the firststep_startevent (forward scan). - Fixture: N/A (opencode output format is stable, no known traps).
Attachment support matrix
Uploads are stored as .vibeflow/attachments/<name>. The UI classifies allowed
extensions into text, image, and document kinds, then projects the repo-relative
path into engine argv. This table is the user-facing contract:
| Engine | Kinds | CLI flag | Notes |
|---|---|---|---|
| claude | text | --append-system-prompt-file <path> | Repeatable; not an image/document reader |
| copilot | image, document | --attachment <path> | Repeatable; flags must precede -p |
| codex | image | --image <path> | Repeatable; text files use another engine |
| opencode | text, image, document | --file <path> | Broad fallback for mixed selections |
| antigravity | none verified | none | Attach surface stays hidden until support is proven |
Auto mode accepts the union of all support-matrix extensions so the native file picker cannot hide a file before its engine is chosen. For one file, Auto chooses the first ready capable engine, then falls back to a support-matrix candidate while readiness is unknown. For multiple selected files, Auto keeps one engine capable of every selected kind; mixed image + text therefore uses OpenCode when available. Readiness gates dispatch, not picker visibility.
Private ranges are separate: they require text and stage an exact line window through
VF-PRIVATE-FILE-RANGES/1; they do not upload or pass the whole attachment to an
engine. In Web UI, select an uploaded text attachment from Private range file,
preview numbered lines, and select start/end. Repo path remains explicit fallback.
The conversation runtime prefixes every delivered turn with VF-TURN/1 and materializes a
canonical JSON envelope for the selected participant. When exact resume authority is proven for
the same participant and interaction cursor, the runtime uses delivery_mode: "exact-delta" and
only re-sends newly applicable public user messages plus concise peer deltas. When that proof is
missing or stale, it falls back to delivery_mode: "full-history" and re-sends the full public
context. Exact by-id authority is limited to Claude, Codex, and OpenCode. Copilot and
Antigravity never silently claim exact resume. Native session histories remain inside the
selected CLI; VibeFlow only changes which public material is re-delivered.
Private file-range context is staged separately from the public turn envelope and is cleared after
use so the next turn does not inherit it accidentally. Its wire form is canonical JSON prefixed by
VF-PRIVATE-FILE-RANGES/1; it is never folded into public trace or browser persistence.
For an exact native resume, the recipient’s own prior response is not repeated: it already
exists in that CLI’s session. The envelope contains only newly applicable user messages and
peer-agent responses/reactions. Without valid exact authority, a full turn also includes a
bounded replay of the recipient’s last eight public responses. Each summary is at most 2 KiB
UTF-8 and carries source digest, provenance, source/replayed counts, and truncation counts.
The turn may also include the content-addressed VF-HANDOFF/1 shared handoff.
Prompt transport is not conversation memory. Claude, Codex, and OpenCode read stdin;
Copilot and Antigravity use native prompt argv. Copilot’s large work-unit fallback writes
.vibeflow/dispatch/<unit>.md and passes a short absolute read pointer. Antigravity instead
rejects UTF-8 prompts at or above 30 KiB because its print mode has no supported file/stdin
replacement.
Bridge command string (VIBEFLOW_AI)
VIBEFLOW_AI / opts.bridgeCmd is a command string; how it is launched differs by platform:
| Platform | Launch form | Semantics |
|---|---|---|
| POSIX | /bin/sh -c "<string> <args>" | shell: >, &&, globs are interpreted |
| Windows | tokenized argv, launched directly | no shell: >/&& reach the program as literal argv |
Windows + first token resolves to .cmd/.bat | cmd.exe /c <argv…>, plus call when that program token is launcher-quoted | CreateProcess cannot execute a batch file, so cmd.exe runs the shim |
Quoting rule (Windows): one double-quoted token is one argv element — quote the program path and
any argument containing spaces. When the shim path itself must be quoted, the launch gains a
call: cmd.exe /d /c call "C:\Program Files\My Tools\shim tool.cmd" "arg with space". Without
it cmd.exe strips the leading and trailing quote of its /c remainder and re-splits the path at
the first space (#819). A command string is never handed to cmd.exe as one argv element: the
launcher re-escapes the quotes and cmd.exe reads them as the program name (#805).
Owned process portability
Every canonical owned launch persists supervisor and CLI PIDs, host, operation/attempt, and
exact process-start identity. Terminal release waits for exit/quiescence plus the
streams-drained stdout/stderr barrier.
| Platform | Scope | Proof strength | Process identity / containment |
|---|---|---|---|
| Windows | windows-job | kernel-contained | Kill-on-close Job Object established before receipt/spawn; PowerShell/CIM creation ticks; no /bin/ps. |
| Linux | posix-process-group | cooperative-lineage | Isolated process group, boot id, and /proc start ticks. |
| macOS | posix-process-group | cooperative-lineage | Isolated process group and exact Darwin libproc seconds/microseconds. |
The POSIX proof is intentionally weaker because descendants can leave the process group.
vf doctor --fix repairs only exact proved orphans; live or identity-unprovable owners fail
closed. Injected platform tests cover the Windows Job Object and identity contracts. Live Windows
evidence is accepted only from a green, exact-SHA windows-latest smoke job; local macOS/Linux
evidence is not a Windows canary.
Windows portability is intentionally narrow at the persistence boundary. Owned-process records
use a stable deny-delete CreateFileW handle with LockFileEx, exact owner metadata, local-drive
path authority, and write-through atomic replacement through MoveFileExW. General POSIX
durability primitives remain unsupported on Win32 and continue to fail closed; this port does not
claim that the rest of the durability layer has native Windows semantics.
Crash-resume (vf orchestrate --resume)
- Capture: dispatch persists a validated engine session id into
DispatchMarker.engineSessionIdfor Claude (session_id), Codex (thread_id), and OpenCode (sessionID). Copilot and Antigravity persist no exact binding. - Resume policy:
src/orchestrator/resume-policy.tsresolveResumeIdresumes only when--resumeis set, the marker is non-terminal (running/blocked/failed, neverdone/pending), and it carries a valid id for an exact-resume engine. Unsupported engines never claim exact continuation. - Conversation delivery separately guarantees bounded own-history replay whenever exact native proof is absent.
How to re-verify after a CLI bump
-
claude — confirm resume flag + envelope shape:
claude --help | grep -E "resume|output-format" echo "reply with a fenced json: {\"confidence\":1.0}" | claude -p --output-format json | tail -1Check the envelope still has
type: "result"+session_id, and the summary lives in.result. -
codex — confirm JSONL event names (the critical one):
echo 'Reply with EXACTLY a fenced json block: {"confidence":1.0,"files_changed":[]}' \ | codex exec --json --skip-git-repo-check -Confirm
thread.started/thread_id(first line) anditem.completed/item.type=="agent_message"still hold. If the event names changed, updateparseSessionId+parseEngineSummary+test/fixtures/codex-json-stream.txttogether. Also confirmcodex exec resume --helpstill accepts a[SESSION_ID]positional and--json. -
copilot — confirm
-p+--allow-allstill exist; do not adopt a resume path without a captured by-id contract and tests:copilot --help | grep -E "allow-all|continue|resume|-p" -
opencode — confirm
--format jsonplus exact--session <ses_...>work:echo 'Reply with exactly READY' | opencode run --format jsonCapture the first event’s
sessionID, validate itsses_...shape, then pipe a second prompt toopencode run --session <captured-id> --format json. Confirm the response text appears in atype: "text"event withpart.textand belongs to that exact session. -
agy — run authenticated scratch-directory canaries for fresh
agy -p,--agent <name>with.agents/agents/<name>/agent.md, and aPreToolUsedeny hook. Do not advertise exact resume unless a safe captured by-id binding and primary evidence are added together. Confirm plain output, native deny behavior, and no scratch files remain. -
Run
bun run check. The dispatch tests + codex fixture assert the shapes above; a red suite after a bump means the CLI changed its contract.
Related surfaces that can drift with a CLI change
- Hooks:
src/commands/hooks.ts— engine invocation is wrapped by dispatch, but permission/stall behavior depends on the engine’s flags. - Skills / rules:
.agents/skills/vf/— the vf skill documents engine usage for the agent surface; keep it in sync with the flags here. - System One judge (optional, off by default):
src/typesafe.tsposts to the third-party endpointhttps://api.typesafe.ai/v1/systemone. A vendor-side request or response shape change is a response contract, so re-verifyparseSystemOneResponse(envelope shape, finite in-range numbers, unknown answer ids dropped) whenever the model id behindsettings.typesafe.modelis bumped, the same way an engine CLI bump requires re-verifying its parser. See TYPESAFE.md. - Schema:
DispatchMarker(src/orchestrator/marker.ts) —engineSessionIdis the persisted contract for resume.