Files
roboco/docker/grok/intake-tools.js
T
Renn F eac065e504 fix(grok): deliver intake draft via the relay + correct opencode-mechanism docs
Live end-to-end verification (opencode 1.17.8 + grok-build-0.1) of the WHOLE
integration, then fixes for what it surfaced:

1) Intake draft card (FUNCTIONAL): opencode's synchronous serve reply
   (POST /session/:id/message) returns only [step-start, text, step-finish] — it
   does NOT include tool-call parts, so the driver could never extract the
   propose_draft draft. intake-tools.js now POSTs the draft straight to the
   prompter-live relay (/api/prompter/live/{session}/events, the same endpoint
   the driver's relay sink uses), so the panel renders the card regardless.
   Verified live: grok calls propose_draft -> the relay receives the draft.

2) Correct misattributed opencode "bugs" (DOCS): earlier comments asserted as
   general opencode behavior that a provider.xai block / npm override / config
   plugin:-array "break" registration. Re-testing showed those were artifacts of
   a PROJECT-level .opencode/opencode.json; from the GLOBAL config (which
   opencode_config writes) the built-in provider, model resolution, the plugin
   array AND the auto-discovery dir all work, and MCP gateway verbs register
   (delivery agents verified). Reframed the comments as design choices (built-in
   provider + XAI_API_KEY env + plugins baked in the auto-discovery dir with
   named exports) and dropped the false claims.

3) Reasoning --variant: passing it does not error, but whether opencode applies a
   named reasoning variant to grok-build-0.1 (no provider-defined variants) is
   UNVERIFIED — comment softened from a "~54% cut" claim to best-effort,
   measure-on-NAS.

Verified live this session: one-shot delivery (model + MCP verbs + plugins +
hooks), secretary tools (read_company_state + submit_directive -> backend with
token), intake draft (relay), grok built-in-provider tool-calling. Remaining
NAS-only: full container assembly (SDK :9000 startup, entrypoint hooks, 429
parking) + the --variant cost measurement. Gate green (ruff/mypy + 51 tests;
node --check the plugins).
2026-06-18 21:59:25 +02:00

71 lines
2.9 KiB
JavaScript

// opencode plugin — the Intake interviewer's propose_draft tool, on Grok.
//
// The model calls propose_draft once the task spec is ready; this delivers the
// draft to the panel's reviewable draft card.
//
// WHY IT POSTS DIRECTLY (not via the driver): opencode's synchronous serve reply
// (POST /session/:id/message) returns only the final assistant text + step
// markers — NOT the tool-CALL parts. So OpencodeServeSession cannot intercept
// this call to emit a `draft` chunk (verified live: a propose_draft call comes
// back as parts=[step-start, text, step-finish], no tool part). Instead the tool
// POSTs the draft straight to the prompter-live relay — the same
// /api/prompter/live/{session}/events endpoint the driver's relay sink uses — so
// the panel renders the card regardless. (The Claude intake path differs: the
// Claude SDK DOES expose the tool-use block, so its driver intercepts it.)
//
// Loaded from the plugin auto-discovery dir (~/.config/opencode/plugin/), baked
// into the grok-prompter image only (the one-shot delivery roles never draft).
// The container provides ROBOCO_API_URL + ROBOCO_PROMPTER_SESSION_ID.
import { tool } from "@opencode-ai/plugin";
const API_BASE = (
process.env.ROBOCO_API_URL || "http://roboco-orchestrator:8000"
).replace(/\/+$/, "");
export const RobocoIntakeTools = async () => ({
tool: {
propose_draft: tool({
description:
"Submit the finished task draft for the human to review and confirm. " +
"Call this once the spec is complete. Pass a JSON object: title, " +
"objective, what_this_builds[], the_work[] ({team, summary, items}), " +
"notes[], acceptance_criteria[], team, scale, task_type, nature, " +
"estimated_complexity, priority.",
args: {
draft: tool.schema
.record(tool.schema.string(), tool.schema.any())
.describe("The task draft object"),
},
async execute(args) {
const session = process.env.ROBOCO_PROMPTER_SESSION_ID || "";
if (!session) {
return "No live session id (ROBOCO_PROMPTER_SESSION_ID) — cannot surface the draft.";
}
try {
const res = await fetch(
`${API_BASE}/api/prompter/live/${encodeURIComponent(session)}/events`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
kind: "draft",
text: "",
tool: "propose_draft",
data: args.draft || {},
}),
signal: AbortSignal.timeout(15000),
},
);
if (!res.ok) {
return `Draft relay returned HTTP ${res.status}; the human may not see the card.`;
}
} catch (e) {
return "Could not submit the draft to the panel: " + String(e);
}
return "Draft submitted — the human can review it in the panel.";
},
}),
},
});