A 7-dimension adversarial review (find -> independently refute) surfaced 14 real
issues; fixed each:
Runtime bugs
- GrokCliSession.send drained stdout fully BEFORE stderr — a >64KB stderr burst
would deadlock the turn forever (spinner never clears). Drain stderr
concurrently, and add a per-turn watchdog (ROBOCO_GROK_TURN_TIMEOUT_SECONDS,
default 600s) that kills a wedged process and emits error+turn_end.
- Crash-restarted grok agents launched `grok -p ""` (empty prompt) — Claude gets
a scan-for-work fallback. Default the prompt in _spawn_container so every
dedicated provider gets it too.
- _grok_usage_json read /data/grok-usage unconditionally while its writers branch
compose-vs-local, so a local-mode agent finalized at $0 and the cost-cap was
inert. Single-source the path in a new _grok_usage_dir helper (read == write).
- GrokCliSession secretary role fell through to "unknown" (get_agent_role returns
a truthy sentinel, never None), defeating the ROBOCO_AGENT_ROLE fallback.
Parity / hardening
- --deny set was missing `git tag -d` / `git reflog delete` that the Claude
bash-guard blocks — added them (the "same set" claim is now true).
- Interactive mains now install the bash-guard hook too (defense-in-depth).
- Compose: collapse the GROK_AUTH_DIR / ROBOCO_HOST_GROK_DIR auth-mount pair into
one canonical var so a partial override can't silently break agent auth.
Docs / comments
- Panel routing card + architecture security doc no longer say Grok runs on the
deleted opencode runtime; orchestrator comments point at the renamed entrypoint.
Tests
- Cover the interactive _render_grok_config MCP wiring (ModuleNotFound guard +
secretary HMAC env), the cost-cap kill-failure + interactive relay-close paths,
the local-mode usage read, the role fallback, the turn timeout, and the new
git denies. (#13 — a separate grok "Write" tool — investigated: grok's only
built-in file-mutation tool is search_replace, already removed; no gap.)
Gate green: ruff, mypy, xenon, tests.
Move the last Grok runtime off opencode onto xAI's official `grok` CLI, for full
parity with the Claude path. The intake/secretary chat now runs per-turn headless
`grok -p` invocations that resume one session id (proven live: context carries
across runs), with streaming-json deltas mapped to the existing panel StreamChunk
kinds — the IntakeDriver loop, message source, relay, and idle reaper are reused
unchanged; only the SessionFactory differs (GrokCliSession replaces the
opencode-serve session).
- GrokCliSession + a pure, unit-tested streaming-json -> StreamChunk assembler
(thought coalesced to one block, text streamed live, end captures the session
id for -r, fenced-draft fallback, clear errors incl. rate-limit).
- intake propose_draft and secretary read_company_state/read_task/submit_directive
are now FastMCP servers (roboco-intake / roboco-secretary) wired into
~/.grok/config.toml, launched via `uv run --directory /app` to resolve the
installed package. The secretary tools reuse the shared backend helpers.
- Orchestrator: interactive spawn mounts the subscription auth + per-agent usage
dir (no metered xAI key, no permission env — grok flags carry per-role perms);
usage/cost now read a captured usage.json (drop the opencode.db reader, the
_opencode_db_path/_grok_usage_from_opencode methods, and the cost-cap's
opencode read). hosts["opencode"] -> hosts["grok_usage"]; OPENCODE_DATA_DIR ->
GROK_USAGE_DATA_DIR.
- Fix one-shot usage capture: `-s` does not pin the session id (grok generates
its own), so the entrypoint now reads the real id back from the JSON run log
and the reader uses it; usage is captured per-turn on the interactive path.
- Delete the opencode layer: opencode_config/opencode_usage/opencode_session, the
docker/grok/*.js plugins, the old one-shot entrypoint, and their tests.
- Compose (all three files), .env.example, and stale comments updated to the
grok-CLI runtime; add the SuperGrok auth mount + grok-usage dir.
Gate green: ruff, mypy (296 files), xenon, tests. NAS build/verify pending.
A GROK agent runs opencode, not Claude Code: it has no SDK /usage/status
server and writes no Claude transcript, so _resolve_final_token_usage found
nothing and every Grok agent finalized at 0 tokens / $0 — the opencode_usage
reader existed but had no caller.
- Mount a per-agent opencode data dir ($DATA/opencode/<agent_id> →
/home/agent/.local/share/opencode) so opencode.db is captured, and mount the
same host dir into the orchestrator (/data/opencode) in all three compose
files so the finalizer can read it back — the opencode analogue of the
mounted Claude transcript.
- _resolve_final_token_usage branches on provider_type: GROK reads opencode.db
via opencode_usage (reasoning folded into output, billed at the output rate)
and skips the SDK/transcript path. A 0-token read logs a WARNING so a silent
mount failure isn't mistaken for a real zero-cost run.
- ROBOCO_OPENCODE_DATA_DIR overrides the in-orchestrator path for local runs.