Commit Graph
11 Commits
Author SHA1 Message Date
Renn F a88045aacf feat(grok): convert interactive intake/secretary to the grok CLI; delete opencode
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.
2026-06-19 04:42:25 +02:00
Renn F 499f6fc509 feat(grok-cli): read captured usage at finalize; keep interactive serve working
The provider mounts the per-agent data dir and points the entrypoint's usage
file at it; the orchestrator's grok finalize reads that usage.json first (the
grok-CLI total, priced at the output rate) and falls back to opencode.db for the
still-opencode interactive intake/secretary path. Re-add _reasoning_effort_for to
grok.py as a clearly-temporary shim for that interactive path (it needs opencode's
"minimal" variant, distinct from the CLI's --effort) until it is converted too.
2026-06-19 03:48:30 +02:00
Renn F c139e2d017 feat(grok-cli): wire usage capture into the run (session id + post-run extract)
The provider pins a fixed session id (ROBOCO_AGENT_SESSION_ID, reused from the
agent session id as on the Claude path); the entrypoint passes it to
'grok -p -s <id>' so the run's session store is locatable, then runs the usage
reader post-run (best-effort) to write the captured tokens + cost. The
orchestrator-side finalize that reads that file follows.
2026-06-19 03:32:32 +02:00
Renn F aa4dc08d58 feat(grok-cli): GrokCliProvider — subscription auth mount, mirrors ClaudeCodeProvider
Replace the opencode GrokProvider with GrokCliProvider: reuses the orchestrator's
shared mount/auth/git assembly (gateway + identity) exactly like the Claude path,
mounts the host ~/.grok/auth.json read-only (SuperGrok subscription) instead of
injecting an xAI key, and sets the slim env the grok-cli entrypoint + renderer
read (ROBOCO_AGENT_ID for per-role flags, model, mcp-config, prompt). Provider
routing fields are blanked before the shared step so the grok endpoint is never
mislabelled ANTHROPIC_*. Per-role permission logic now lives in grok_cli_config,
so the provider is slim. Registry/orchestrator/exports updated; provider tests
rewritten for the CLI behavior (no XAI key, auth mount present/absent).
2026-06-19 03:01:46 +02:00
Renn F e29168653a fix(grok): make the opencode runtime actually load — proven live on grok-build-0.1
Live verification (opencode 1.17.8 + grok-build-0.1, funded key) showed the Grok
runtime was loading INERT, three ways:

1. The provider override `provider.xai.npm=@ai-sdk/openai` failed model
   resolution (ProviderModelNotFoundError) — opencode can't resolve that package
   from its module path. Worse, ANY custom `provider.xai` block (even just
   options) breaks plugin-tool registration. opencode's BUILT-IN xai provider
   drives grok-build-0.1 with working tool-calls, so emit NO provider block; the
   key + base reach it via XAI_API_KEY / XAI_BASE_URL env (provider.options.apiKey
   alone does NOT authenticate).
2. Plugins referenced by absolute path in the config `plugin:` array never
   registered their hooks/tools. opencode 1.17.8 only registers from the plugin
   AUTO-DISCOVERY dir (~/.config/opencode/plugin/). Bake all plugins there.
3. Plugins must use a NAMED export, not `export default`.

Changes:
- opencode_config: no `provider` block, no `plugin` array; drop the dead
  XaiTarget + timeout machinery; build_opencode_config now takes a model string.
- GrokProvider / orchestrator interactive env: inject XAI_API_KEY + XAI_BASE_URL
  (drop the now-unused OPENAI_*).
- secret-scrub / budget-feed / secretary-tools / intake-tools: named exports;
  baked into /home/agent/.config/opencode/plugin/ (drop the EXTRA_PLUGINS env).
- agent-grok* Dockerfiles: plugin dir + agent ownership; drop the unneeded
  @ai-sdk/openai global install.

Verified live end-to-end: grok-build-0.1 calls read_company_state AND
submit_directive through secretary-tools.js and the backend receives both with
the agent token; a tool.execute.before guard fires; built-in tool-calls work.
Targeted gate green (ruff/mypy/xenon + opencode_config/providers/interactive
tests; node --check the plugins).
2026-06-18 21:14:06 +02:00
Renn F 8e728bdf9d feat(grok): scope opencode edit/bash/external-dir permissions per role
Grok wrote ONE global permission block, so a Grok pr_reviewer (or qa / PM /
auditor) ran with edit=allow + bash=allow on untrusted PR content. Now the
permissions are derived per role, mirroring orchestrator._get_role_permissions
on the Claude path:

- edit  — allow only roles that write code (role_config.allows_write:
  developer / documenter); everyone else edit=deny.
- bash  — allow only roles that legitimately run a shell (developer /
  documenter / cell_pm / main_pm); the read-only reviewers (qa / pr_reviewer /
  auditor) and the board get bash=deny. secret-scrub still guards the rest.
- external_directory — only the pr_reviewer reads scratch outside its cwd (the
  /tmp diff); delivery roles get deny.

One-shot roles resolve these in GrokProvider._append_grok_env; the interactive
intake/secretary set edit=deny + bash=deny in the orchestrator (intake keeps
external-dir reads for sibling product repos, the secretary does not). The
Claude path is untouched — the permission env is a GROK-only contract.

Targeted gate green (ruff/mypy/xenon + provider + interactive-spawn tests).
2026-06-18 20:03:10 +02:00
Renn F 997a19e074 fix(grok): make the live interactive path work — store perms, error surfacing, variant
Found by actually running opencode serve locally (the path was doc-verified but
never executed). Three fixes:

1. EACCES on the opencode store mount (the live intake crash): on Linux docker
   auto-creates a missing bind source as root:root, so the non-root agent user
   could not mkdir/write in /home/agent/.local/share/opencode and opencode died
   at boot. _ensure_opencode_data_dir pre-creates the per-agent dir 0777 before
   the mount (one-shot via the _GrokHost seam, interactive in both spawns).

2. Silent blank reply on a model error: opencode reports a turn failure in
   info.error with parts=[], NOT as a part — verified live (a bad xAI key
   returns info.error APIError). send() / normalize_opencode_message now surface
   it as an "error" StreamChunk so a failed turn is never blank (the original
   intake bug class). Confirmed live: the error now renders.

3. Reasoning variant on the serve path: the live OpenAPI shows the message body
   accepts a "variant" field (it is NOT CLI-only, as the docs implied), so the
   pin is unblocked. send() passes ROBOCO_GROK_VARIANT as the per-turn variant;
   the orchestrator sets it per-role (_reasoning_effort_for) for interactive
   Grok, the same lever as the one-shot --variant.

opencode serve startup, POST /session, session-id extraction, the part-type
mapping (text/reasoning/tool), and the error path are all validated against a
live opencode 1.17.8. A real successful grok reply still needs a funded key.
2026-06-18 13:30:54 +02:00
Renn F e36549f01f feat(grok): capture one-shot Grok usage/cost from the opencode store
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.
2026-06-18 11:54:58 +02:00
Renn F ff8867dd1f fix(grok): stop opencode subagent-stream hang at the config layer
The Grok pr_reviewer wedged in_progress forever: opencode's default agent ran
with the subagent `task` tool enabled, spawned an Explore subagent on
grok-build-0.1 whose model call opened an SSE stream that went idle, and the
run hung with no timeout.

- Hard-disable opencode's subagent `task` tool in the generated opencode.json.
  No RoboCo role uses opencode-internal subagents — work flows through the
  gateway verbs — so removing the tool kills the hang trigger outright.
- Set provider.xai.options.timeout + chunkTimeout (operator-tunable via
  ROBOCO_GROK_REQUEST_TIMEOUT_MS / ROBOCO_GROK_CHUNK_TIMEOUT_MS) as the
  defence-in-depth backstop; chunkTimeout aborts an idle stream.
- Bundle the permission + timeout + subagent knobs into an OpencodeGuards
  dataclass (keeps the builder under the arg-count gate).
- Drop the dead ROBOCO_AGENT_TOOLS spawn env (it had no consumer); opencode
  tool restriction lives in the rendered config now.
2026-06-18 11:31:13 +02:00
Renn F 5725ec998b feat(grok): reasoning-effort by role (cut grok-build cost on cheap roles)
grok-build-0.1 reasons heavily by default and reasoning bills at the output
rate (a live "say ok" call emitted ~300 reasoning tokens, ~85% of its cost).
Confirmed live that opencode's `--variant minimal` cuts reasoning ~54%
(298 -> 136 tokens, same prompt).

GrokProvider now picks reasoning effort by role: code-quality roles (developer,
qa, pr_reviewer) keep full reasoning; coordination / docs / board roles
(cell_pm, main_pm, documenter, product_owner, head_marketing, auditor, prompter,
secretary) run "minimal". It's passed to opencode via the entrypoint's
`--variant`. Operators can force one effort for ALL grok agents with the
ROBOCO_GROK_REASONING_EFFORT env (minimal | high | max, or default/full).

Tests cover the role map, the env override, and the spawn env wiring.
2026-06-18 10:26:29 +02:00
Renn F a956083f9f feat(providers): pluggable agent providers + Grok (xAI) backend
Add a roboco/llm/providers/ seam — an AgentProvider lifecycle ABC and a
ProviderRegistry keyed by ModelProvider — so the orchestrator can drive
agent backends other than Claude Code.

The first non-Claude backend is GrokProvider for xAI's grok-build-0.1.
xAI is OpenAI-compatible only (no Anthropic-Messages endpoint), so a Grok
agent runs an OpenAI-protocol runtime pointed at https://api.x.ai/v1
rather than the ANTHROPIC_BASE_URL injection the other providers use. It
reuses the orchestrator's existing mount/auth assembly, so it inherits the
same MCP gateway + tool-manifest wiring as every other agent by
construction, and passes its prompt via env (never an argv positional).

The change is purely additive: only GROK routes through the registry;
Anthropic / Ollama Cloud / self-hosted spawns run the existing
_spawn_container path unchanged.

Includes:
- ModelProvider.GROK (migration 038) + a seeded Grok provider row
  (migration 039) + a grok-build-0.1 catalog entry
- GET/PUT /api/providers/grok-key to store the xAI key (Fernet-encrypted,
  reusing the existing provider-key machinery)
- ClaudeCodeProvider reference adapter over the current spawn
- unit tests for the registry, GrokProvider (gateway wiring, no
  ANTHROPIC_* leak, prompt-injection safety, failure paths) and routing

The dedicated roboco-agent-grok image and the exact OpenAI-protocol CLI
invocation are the remaining piece to finalise with xAI.
2026-06-18 06:14:27 +02:00