@prismatic-io/lux 0.0.2-preview.23 → 0.0.2-preview.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/lib/answerers/persona/index.d.ts.map +1 -1
  2. package/lib/answerers/persona/index.js +3 -2
  3. package/lib/answerers/persona/index.js.map +1 -1
  4. package/lib/cli/init-templates.d.ts +1 -1
  5. package/lib/cli/init-templates.d.ts.map +1 -1
  6. package/lib/cli/init-templates.js +1 -0
  7. package/lib/cli/init-templates.js.map +1 -1
  8. package/lib/cli/init.d.ts +1 -0
  9. package/lib/cli/init.d.ts.map +1 -1
  10. package/lib/cli/init.js +10 -2
  11. package/lib/cli/init.js.map +1 -1
  12. package/lib/cli/program.d.ts +1 -1
  13. package/lib/cli/program.d.ts.map +1 -1
  14. package/lib/cli/program.js +9 -1
  15. package/lib/cli/program.js.map +1 -1
  16. package/lib/core/harness-catalogs/grok-build.d.ts +7 -0
  17. package/lib/core/harness-catalogs/grok-build.d.ts.map +1 -0
  18. package/lib/core/harness-catalogs/grok-build.js +18 -0
  19. package/lib/core/harness-catalogs/grok-build.js.map +1 -0
  20. package/lib/core/harness-catalogs/index.d.ts +7 -1
  21. package/lib/core/harness-catalogs/index.d.ts.map +1 -1
  22. package/lib/core/harness-catalogs/index.js +3 -1
  23. package/lib/core/harness-catalogs/index.js.map +1 -1
  24. package/lib/drivers/grok-build/config.d.ts +65 -0
  25. package/lib/drivers/grok-build/config.d.ts.map +1 -0
  26. package/lib/drivers/grok-build/config.js +37 -0
  27. package/lib/drivers/grok-build/config.js.map +1 -0
  28. package/lib/drivers/grok-build/index.d.ts +83 -0
  29. package/lib/drivers/grok-build/index.d.ts.map +1 -0
  30. package/lib/drivers/grok-build/index.js +291 -0
  31. package/lib/drivers/grok-build/index.js.map +1 -0
  32. package/lib/index.d.ts +2 -1
  33. package/lib/index.d.ts.map +1 -1
  34. package/lib/index.js +2 -1
  35. package/lib/index.js.map +1 -1
  36. package/lib/orchestrator/config.d.ts.map +1 -1
  37. package/lib/orchestrator/config.js +2 -0
  38. package/lib/orchestrator/config.js.map +1 -1
  39. package/package.json +1 -1
  40. package/skills/lux-answerer/SKILL.md +1 -1
  41. package/src/answerers/persona/index.ts +3 -2
  42. package/src/cli/init-templates.ts +9 -1
  43. package/src/cli/init.ts +10 -2
  44. package/src/cli/program.ts +9 -1
  45. package/src/core/harness-catalogs/grok-build.ts +19 -0
  46. package/src/core/harness-catalogs/index.ts +10 -1
  47. package/src/drivers/grok-build/README.md +112 -0
  48. package/src/drivers/grok-build/config.ts +40 -0
  49. package/src/drivers/grok-build/index.ts +329 -0
  50. package/src/index.ts +7 -2
  51. package/src/orchestrator/config.ts +2 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prismatic-io/lux",
3
- "version": "0.0.2-preview.23",
3
+ "version": "0.0.2-preview.24",
4
4
  "description": "Coding-agent evaluation and improvement with deterministic assertions, optional LLM judges, and first-class human-in-the-loop runs.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/prismatic-io/lux",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lux-answerer
3
- version: 0.0.2-preview.23
3
+ version: 0.0.2-preview.24
4
4
  description: Play the persona for a running Lux orchestrator that uses the claude-code answerer. Read structured events on stdout, decide answers from persona + context, write JSON to its answer channel.
5
5
  user-invocable: false
6
6
  allowed-tools: Bash, Read
@@ -75,7 +75,7 @@ const responseInstruction = (interrupt: Interrupt): string => {
75
75
  return "Answer the free-text question directly. Output only the answer, without a follow-up question or appended conversation decision.";
76
76
  }
77
77
  if (interrupt.kind === "approve") {
78
- return 'Decide whether the persona approves this exact request. Output only JSON: {"approved":true|false,"reason":"..."}.';
78
+ return 'Decide whether the persona approves this exact request. Output only JSON, without Markdown fences or surrounding prose: {"approved":true|false,"reason":"..."}.';
79
79
  }
80
80
  if (isConversationTurnInterrupt(interrupt)) {
81
81
  return 'Decide whether the task needs another user reply. Output only {"action":"stop"} if it is complete, blocked on real user-side work outside the chat, or the persona would naturally end the conversation. Never pretend external work happened. Otherwise output only {"action":"reply","text":"<the persona\'s brief reply>"}.';
@@ -224,7 +224,8 @@ const parseApprovalResponse = (
224
224
  text: string,
225
225
  ): { approved: boolean; reason?: string | undefined } => {
226
226
  const trimmed = text.trim();
227
- const parsed = PersonaApprovalResponseSchema.safeParse(tryParseJson(trimmed));
227
+ const fenced = /^```(?:json)?\r?\n([\s\S]*?)\r?\n```$/.exec(trimmed);
228
+ const parsed = PersonaApprovalResponseSchema.safeParse(tryParseJson(fenced?.[1] ?? trimmed));
228
229
  if (parsed.success) return parsed.data;
229
230
  return { approved: false, reason: trimmed };
230
231
  };
@@ -1,4 +1,11 @@
1
- type StarterDriver = "subprocess" | "claude-code" | "codex" | "cursor" | "copilot" | "antigravity";
1
+ type StarterDriver =
2
+ | "subprocess"
3
+ | "claude-code"
4
+ | "codex"
5
+ | "cursor"
6
+ | "copilot"
7
+ | "antigravity"
8
+ | "grok-build";
2
9
 
3
10
  export const luxConfigBody = (
4
11
  withExperiment: boolean,
@@ -22,6 +29,7 @@ export const luxConfigBody = (
22
29
  "claude-code": 'reasoningEffort: "low"',
23
30
  codex: 'reasoningEffort: "low"',
24
31
  cursor: "",
32
+ "grok-build": 'reasoningEffort: "low", permissionMode: "auto"',
25
33
  copilot: 'reasoningEffort: "low"',
26
34
  antigravity: 'reasoningEffort: "low"',
27
35
  };
package/src/cli/init.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  STARTER_PROMPT_BODY,
14
14
  } from "./init-templates.js";
15
15
 
16
- const EXPLICIT_MODEL_DRIVERS = new Set(["cursor", "copilot", "antigravity"]);
16
+ const EXPLICIT_MODEL_DRIVERS = new Set(["cursor", "copilot", "antigravity", "grok-build"]);
17
17
 
18
18
  const InitOptionsSchema = z
19
19
  .object({
@@ -21,7 +21,15 @@ const InitOptionsSchema = z
21
21
  force: z.boolean().default(false),
22
22
  experiment: z.boolean().default(false),
23
23
  driver: z
24
- .enum(["subprocess", "claude-code", "codex", "cursor", "copilot", "antigravity"])
24
+ .enum([
25
+ "subprocess",
26
+ "claude-code",
27
+ "codex",
28
+ "cursor",
29
+ "copilot",
30
+ "antigravity",
31
+ "grok-build",
32
+ ])
25
33
  .optional(),
26
34
  model: z.string().min(1).optional(),
27
35
  })
@@ -167,7 +167,15 @@ export const buildCli = () =>
167
167
  force: z.boolean().optional().describe("Overwrite existing files"),
168
168
  experiment: z.boolean().optional().describe("Also scaffold an optimization campaign"),
169
169
  driver: z
170
- .enum(["subprocess", "claude-code", "codex", "cursor", "copilot", "antigravity"])
170
+ .enum([
171
+ "subprocess",
172
+ "claude-code",
173
+ "codex",
174
+ "cursor",
175
+ "copilot",
176
+ "antigravity",
177
+ "grok-build",
178
+ ])
171
179
  .optional()
172
180
  .describe("Starter driver (default: deterministic subprocess)"),
173
181
  model: nonEmptyString.optional().describe("Explicit subject model; required for Cursor"),
@@ -0,0 +1,19 @@
1
+ import { defineHarnessCatalog } from "../harness-catalog.js";
2
+
3
+ const efforts = ["low", "medium", "high", "xhigh"] as const;
4
+
5
+ export const grokBuildCatalog = defineHarnessCatalog({
6
+ driver: "grok-build",
7
+ verified: "2026-09-24",
8
+ sources: [
9
+ "grok 1.0.40: models and ACP initialize modelState",
10
+ "https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/15-agent-mode.md",
11
+ ],
12
+ efforts,
13
+ models: {
14
+ "grok-4.7": efforts,
15
+ "grok-4.7-build-fast": efforts,
16
+ "grok-4.6": efforts,
17
+ "grok-4.5": ["low", "medium", "high"],
18
+ },
19
+ });
@@ -4,8 +4,16 @@ import { claudeCodeCatalog } from "./claude-code.js";
4
4
  import { codexCatalog } from "./codex.js";
5
5
  import { copilotCatalog } from "./copilot.js";
6
6
  import { cursorCatalog } from "./cursor.js";
7
+ import { grokBuildCatalog } from "./grok-build.js";
7
8
 
8
- export { antigravityCatalog, claudeCodeCatalog, codexCatalog, copilotCatalog, cursorCatalog };
9
+ export {
10
+ antigravityCatalog,
11
+ claudeCodeCatalog,
12
+ codexCatalog,
13
+ copilotCatalog,
14
+ cursorCatalog,
15
+ grokBuildCatalog,
16
+ };
9
17
 
10
18
  export const builtInHarnessCatalogs = [
11
19
  claudeCodeCatalog,
@@ -13,6 +21,7 @@ export const builtInHarnessCatalogs = [
13
21
  copilotCatalog,
14
22
  cursorCatalog,
15
23
  antigravityCatalog,
24
+ grokBuildCatalog,
16
25
  ] as const;
17
26
 
18
27
  export type BuiltInHarnessCatalog = (typeof builtInHarnessCatalogs)[number];
@@ -0,0 +1,112 @@
1
+ # Grok.build driver
2
+
3
+ The `grok-build` driver runs the native Grok.build CLI over ACP (`grok agent
4
+ --no-leader stdio`). Install Grok.build and run `grok login` first. This is
5
+ separate from selecting a Grok model in the Copilot driver.
6
+
7
+ ```ts
8
+ {
9
+ defaultDriver: {
10
+ name: "grok-build",
11
+ config: {
12
+ model: "grok-4.5",
13
+ reasoningEffort: "low",
14
+ permissionMode: "auto",
15
+ sandbox: "workspace",
16
+ },
17
+ },
18
+ defaultAnswerer: "persona",
19
+ }
20
+ ```
21
+
22
+ Or scaffold an eval project:
23
+
24
+ ```sh
25
+ lux init --driver grok-build --model grok-4.5
26
+ ```
27
+
28
+ An explicit model is required; effort defaults to `low`. Lux selects both through
29
+ `session/set_config_option` and verifies the returned values before prompting.
30
+ Grok 1.0.40 can choose a different model on `session/new` despite the CLI model
31
+ flag, so passing that flag alone is insufficient.
32
+
33
+ ## Permissions
34
+
35
+ The default `auto` mode uses Grok's native safety checks, including its LLM
36
+ classifier, to approve routine operations. It is not blanket approval. `default`
37
+ (ask) is also supported. Lux does not expose always-approve, arbitrary extra
38
+ arguments, or the CLI `--allow`/`--deny` flags. In Grok 1.0.40, those rule flags
39
+ are ignored by the agent subcommand. Project permission rules require Grok folder trust. Lux leaves
40
+ `trustProject: false` by default; an untrusted fixture's rules are ignored by
41
+ Grok. Enable `trustProject: true` only for reviewed fixtures: it also permits
42
+ project configuration such as hooks and MCP servers. Then put native rules in
43
+ `.grok/config.toml` and assert the expected denial. CLI `dontAsk` is also omitted because the ACP launch path does not propagate that
44
+ policy.
45
+
46
+ For example, this native rule denies the smoke test's command:
47
+
48
+ ```toml
49
+ [[permission.rules]]
50
+ action = "deny"
51
+ tool = "bash"
52
+ pattern = "printf *"
53
+ ```
54
+
55
+ Change `action` to `"ask"` to exercise Lux's answerer explicitly. The live
56
+ smoke task opts into trust only for its own generated ask/deny fixtures.
57
+
58
+ ACP permission requests become Lux `approve` interrupts, including the tool
59
+ call details. The configured answerer handles those requests; `persona` uses an
60
+ LLM with the case's task and persona constraints. Lux selects only `allow_once`
61
+ or `reject_once`, never a remembered grant. The persona parser accepts strict
62
+ approval JSON either bare or in one complete JSON Markdown fence; ambiguous
63
+ text and invalid fields still deny the operation. If the agent offers no matching
64
+ one-operation option, Lux cancels and reports an error rather than widening the
65
+ grant. Native auto decisions are handled inside Grok and are not Lux interrupts.
66
+
67
+ Permission reviews have a separate `permissionTimeoutMs` (150 seconds by
68
+ default), while ordinary inactivity uses `idleTimeoutMs` (10 minutes). Startup
69
+ RPCs time out after 30 seconds. Cancelled turns, process failures, unanswered
70
+ reviews, and turns ending with pending approvals cannot count as successful runs.
71
+ A completed turn can still contain denied tool calls: assert the expected files
72
+ or tool results to determine whether the task succeeded.
73
+
74
+ ## Scope and limitations
75
+
76
+ Lux requests the native `workspace` sandbox and a private process with
77
+ `--no-leader`. `read-only` and `strict` profiles are available; disabling the
78
+ sandbox is not a driver option. The workspace profile allows filesystem reads,
79
+ network access, and writes to the working directory, Grok state, and temporary
80
+ directories. It is not configuration isolation: Grok still loads user/project
81
+ settings, compatible Claude settings, hooks, skills, and MCP configuration.
82
+ The driver reports configuration isolation, usage, and cost as unavailable;
83
+ controlled experiments requiring those capabilities remain unsupported.
84
+
85
+ The interaction surface currently supports tool permissions. Other ACP client
86
+ requests receive an explicit unsupported-method response. Run evidence includes
87
+ agent messages, reasoning, tool calls/results, and collected files. Use artifact
88
+ assertions alongside `run-succeeded`.
89
+
90
+ ## Validation
91
+
92
+ Synthetic tests cover explicit model/effort selection, one-operation approvals
93
+ and rejections, refusal to substitute remembered grants, permission timeouts,
94
+ process exit during review, cancellation, idle timeout, and cleanup.
95
+
96
+ Live smoke validation uses Grok.build 1.0.40 with `grok-4.5` at low effort.
97
+
98
+ The opt-in live smoke task runs three real evals: a local shell write under auto
99
+ review, an explicit ask rule answered by Lux's LLM persona, and an explicit deny
100
+ rule that must produce a failed tool result without creating the file. It
101
+ requires Grok and Claude Code logins and consumes model usage. Evidence stays
102
+ in the temporary directory printed by the task:
103
+
104
+ ```sh
105
+ LUX_GROK_MODEL=grok-4.5 mise run smoke:grok-build
106
+ ```
107
+
108
+ References:
109
+
110
+ - [Grok ACP guide](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/15-agent-mode.md)
111
+ - [Grok permissions](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/22-permissions-and-safety.md)
112
+ - [Grok sandbox](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/18-sandbox.md)
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ import { applyCatalogEffort, grokBuildCatalog } from "../../core/index.js";
3
+
4
+ export const GrokBuildDriverConfigSchema = z
5
+ .object({
6
+ command: z.string().min(1).default("grok"),
7
+ model: z.string().min(1),
8
+ reasoningEffort: z.enum(grokBuildCatalog.efforts).default("low"),
9
+ permissionMode: z.enum(["auto", "default"]).default("auto"),
10
+ trustProject: z.boolean().default(false),
11
+ sandbox: z.enum(["workspace", "read-only", "strict"]).default("workspace"),
12
+ cwd: z.string().optional(),
13
+ env: z.record(z.string(), z.string()).optional(),
14
+ startupTimeoutMs: z.number().int().positive().default(30_000),
15
+ idleTimeoutMs: z.number().int().positive().default(600_000),
16
+ permissionTimeoutMs: z.number().int().positive().default(150_000),
17
+ maxInterrupts: z.number().int().positive().default(20),
18
+ maxLineBytes: z.number().int().positive().default(10_000_000),
19
+ maxQueuedBytes: z.number().int().positive().default(20_000_000),
20
+ maxToolCalls: z.number().int().positive().default(10_000),
21
+ })
22
+ .strict()
23
+ .transform(applyCatalogEffort(grokBuildCatalog));
24
+
25
+ export type GrokBuildDriverConfig = z.infer<typeof GrokBuildDriverConfigSchema>;
26
+
27
+ export const buildGrokBuildArgs = (config: GrokBuildDriverConfig): string[] => [
28
+ "--permission-mode",
29
+ config.permissionMode,
30
+ "--sandbox",
31
+ config.sandbox,
32
+ ...(config.trustProject ? ["--trust"] : []),
33
+ "agent",
34
+ "--no-leader",
35
+ "--model",
36
+ config.model,
37
+ "--reasoning-effort",
38
+ config.reasoningEffort,
39
+ "stdio",
40
+ ];
@@ -0,0 +1,329 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import {
3
+ type AgentDriver,
4
+ type Answer,
5
+ type Artifact,
6
+ type DriverEventMap,
7
+ defineDriver,
8
+ discoverCliVersion,
9
+ type ReadyState,
10
+ type StartContext,
11
+ selectModelAndReasoningEffort,
12
+ TypedEmitter,
13
+ } from "../../core/index.js";
14
+ import { AcpEvents } from "../shared/acp-events.js";
15
+ import { permissionAnswer, permissionRequestText, records } from "../shared/acp-interaction.js";
16
+ import { AcpTransport, asRecord, type RpcId, type RpcRecord } from "../shared/acp-transport.js";
17
+ import { walkArtifacts } from "../shared/artifacts.js";
18
+ import {
19
+ buildGrokBuildArgs,
20
+ type GrokBuildDriverConfig,
21
+ GrokBuildDriverConfigSchema,
22
+ } from "./config.js";
23
+
24
+ class GrokBuildDriver implements AgentDriver {
25
+ private readonly emitter = new TypedEmitter<DriverEventMap>();
26
+ private readonly events: AcpEvents;
27
+ private transport: AcpTransport | undefined;
28
+ private sessionId: string | undefined;
29
+ private cwd: string | undefined;
30
+ private artifactsDir: string | undefined;
31
+ private ready = false;
32
+ private runPromise: Promise<void> | undefined;
33
+ private terminal = false;
34
+ private idleTimer: NodeJS.Timeout | undefined;
35
+ private interruptCount = 0;
36
+ private readonly interrupts = new Map<
37
+ string,
38
+ { id: RpcId; params: RpcRecord; timer: NodeJS.Timeout }
39
+ >();
40
+
41
+ private readonly config: GrokBuildDriverConfig;
42
+
43
+ constructor(config: GrokBuildDriverConfig) {
44
+ this.config = config;
45
+ this.events = new AcpEvents("grok-build", config.maxToolCalls, config.maxQueuedBytes);
46
+ }
47
+
48
+ on<K extends keyof DriverEventMap>(
49
+ event: K,
50
+ handler: (e: DriverEventMap[K]) => void | Promise<void>,
51
+ ): void {
52
+ this.emitter.on(event, handler);
53
+ }
54
+ off<K extends keyof DriverEventMap>(
55
+ event: K,
56
+ handler: (e: DriverEventMap[K]) => void | Promise<void>,
57
+ ): void {
58
+ this.emitter.off(event, handler);
59
+ }
60
+
61
+ async start(ctx: StartContext): Promise<ReadyState> {
62
+ if (this.transport || this.terminal)
63
+ throw new Error("Grok.build driver cannot be started twice or after close");
64
+ this.cwd = await realpath(this.config.cwd ?? ctx.artifactsDir);
65
+ this.artifactsDir = await realpath(ctx.artifactsDir);
66
+ const cliVersion = await discoverCliVersion(this.config.command);
67
+ const transport = new AcpTransport({
68
+ label: "Grok.build ACP",
69
+ command: this.config.command,
70
+ args: buildGrokBuildArgs(this.config),
71
+ cwd: this.cwd,
72
+ env: { ...process.env, ...this.config.env },
73
+ signal: ctx.abortSignal,
74
+ maxLineBytes: this.config.maxLineBytes,
75
+ maxQueuedBytes: this.config.maxQueuedBytes,
76
+ onMessage: (message) => this.message(message),
77
+ onFailure: (error) => this.fail(error),
78
+ onActivity: () => this.bumpIdleTimer(),
79
+ });
80
+ this.transport = transport;
81
+ try {
82
+ await transport.spawned();
83
+ const initialized = await this.request("initialize", {
84
+ protocolVersion: 1,
85
+ clientInfo: { name: "lux", version: "1" },
86
+ clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false },
87
+ });
88
+ if (initialized.protocolVersion !== 1)
89
+ throw new Error("Grok.build ACP returned an unsupported protocol version");
90
+ if (asRecord(initialized._meta)?.grokShell !== true)
91
+ throw new Error("Expected Grok.build ACP; check the configured command points to grok");
92
+ const session = await this.request("session/new", {
93
+ cwd: this.cwd,
94
+ mcpServers: [],
95
+ _meta: { autoMode: this.config.permissionMode === "auto", yoloMode: false },
96
+ });
97
+ if (typeof session.sessionId !== "string")
98
+ throw new Error("Grok.build ACP omitted sessionId");
99
+ this.sessionId = session.sessionId;
100
+ let nativeConfig = session.configOptions;
101
+ for (const [configId, value] of [
102
+ ["model", this.config.model],
103
+ ["reasoning_effort", this.config.reasoningEffort],
104
+ ] as const) {
105
+ const updated = await this.request("session/set_config_option", {
106
+ sessionId: this.sessionId,
107
+ configId,
108
+ value,
109
+ });
110
+ nativeConfig = updated.configOptions;
111
+ if (records(nativeConfig).find((option) => option.id === configId)?.currentValue !== value)
112
+ throw new Error(`Grok.build ACP did not apply requested ${configId} '${value}'`);
113
+ }
114
+ this.ready = true;
115
+ this.bumpIdleTimer();
116
+ this.runPromise = this.run(ctx.prompt).catch((error: unknown) => this.fail(error));
117
+ return {
118
+ agentId: "grok-build",
119
+ model: this.config.model,
120
+ ...(typeof asRecord(initialized._meta)?.agentVersion === "string"
121
+ ? { agentVersion: String(asRecord(initialized._meta)?.agentVersion) }
122
+ : {}),
123
+ detail: {
124
+ sessionId: this.sessionId,
125
+ transport: "acp",
126
+ protocolVersion: 1,
127
+ ...(cliVersion ? { cliVersion } : {}),
128
+ permissionMode: this.config.permissionMode,
129
+ sandbox: this.config.sandbox,
130
+ trustProject: this.config.trustProject,
131
+ reasoningEffort: this.config.reasoningEffort,
132
+ modelResolution: "reported",
133
+ nativeConfig: nativeConfig ?? [],
134
+ usageAvailability: "unknown",
135
+ isolation: "none",
136
+ },
137
+ };
138
+ } catch (error) {
139
+ await this.close();
140
+ throw error;
141
+ }
142
+ }
143
+
144
+ private request(method: string, params: RpcRecord): Promise<RpcRecord> {
145
+ if (!this.transport) throw new Error("Grok.build ACP has not started");
146
+ return this.transport.request(method, params, this.config.startupTimeoutMs);
147
+ }
148
+
149
+ private async run(prompt: string): Promise<void> {
150
+ const result = await this.transport?.request("session/prompt", {
151
+ sessionId: this.sessionId,
152
+ prompt: [{ type: "text", text: prompt }],
153
+ });
154
+ await this.transport?.drained();
155
+ if (this.terminal) return;
156
+ if (result?.stopReason !== "end_turn")
157
+ throw new Error(`Grok.build stopped with ${String(result?.stopReason ?? "unknown reason")}`);
158
+ if (this.interrupts.size > 0)
159
+ throw new Error("Grok.build ended with pending permission requests");
160
+ this.terminal = true;
161
+ clearTimeout(this.idleTimer);
162
+ await this.emitter.emit("done", {
163
+ exitReason: "done",
164
+ summary: {
165
+ sessionId: this.sessionId,
166
+ stopReason: result.stopReason,
167
+ usageAvailability: "unknown",
168
+ },
169
+ });
170
+ }
171
+
172
+ private async message(message: RpcRecord): Promise<void> {
173
+ if (this.terminal) return;
174
+ const method = String(message.method);
175
+ const params = asRecord(message.params) ?? {};
176
+ const id = message.id;
177
+ if (typeof id === "number" || typeof id === "string") {
178
+ if (params.sessionId !== this.sessionId) {
179
+ this.transport?.write({ id, error: { code: -32602, message: "Unknown session" } });
180
+ return;
181
+ }
182
+ if (method !== "session/request_permission") {
183
+ this.transport?.write({
184
+ id,
185
+ error: { code: -32601, message: `Unsupported method: ${method}` },
186
+ });
187
+ return;
188
+ }
189
+ if (++this.interruptCount > this.config.maxInterrupts) {
190
+ this.transport?.write({ id, result: { outcome: { outcome: "cancelled" } } });
191
+ throw new Error("Grok.build exceeded maxInterrupts");
192
+ }
193
+ const interruptId = `grok-build-${id}`;
194
+ if (this.interrupts.has(interruptId))
195
+ throw new Error("Grok.build reused a pending request ID");
196
+ const timer = setTimeout(() => {
197
+ this.transport?.write({ id, result: { outcome: { outcome: "cancelled" } } });
198
+ this.interrupts.delete(interruptId);
199
+ this.fail(
200
+ new Error(
201
+ `Grok.build permission review timed out after ${this.config.permissionTimeoutMs}ms`,
202
+ ),
203
+ );
204
+ }, this.config.permissionTimeoutMs);
205
+ this.interrupts.set(interruptId, { id, params, timer });
206
+ clearTimeout(this.idleTimer);
207
+ void this.emitter
208
+ .emit("interrupt", {
209
+ kind: "approve",
210
+ id: interruptId,
211
+ request: `${permissionRequestText(params)}\n\nOperation details:\n${JSON.stringify(params.toolCall)}`,
212
+ context: params,
213
+ })
214
+ .catch((error: unknown) => this.fail(error));
215
+ return;
216
+ }
217
+ if (method === "session/update") {
218
+ if (params.sessionId !== this.sessionId) return;
219
+ const update = asRecord(params.update);
220
+ if (update)
221
+ for (const event of this.events.update(update)) await this.emitter.emit("progress", event);
222
+ }
223
+ }
224
+
225
+ async respond(answer: Answer): Promise<void> {
226
+ if (this.terminal) throw new Error("Grok.build is closed");
227
+ const pending = this.interrupts.get(answer.id);
228
+ if (!pending) throw new Error(`Grok.build has no pending interrupt '${answer.id}'`);
229
+ const result = permissionAnswer(pending.params, answer);
230
+ this.transport?.write({ id: pending.id, result });
231
+ if (asRecord(result.outcome)?.outcome === "cancelled") {
232
+ this.fail(new Error("Grok.build offered no matching one-operation permission option"));
233
+ return;
234
+ }
235
+ clearTimeout(pending.timer);
236
+ this.interrupts.delete(answer.id);
237
+ this.bumpIdleTimer();
238
+ }
239
+
240
+ private bumpIdleTimer(): void {
241
+ clearTimeout(this.idleTimer);
242
+ if (!this.ready || this.terminal || this.interrupts.size > 0) return;
243
+ this.idleTimer = setTimeout(
244
+ () =>
245
+ this.fail(
246
+ new Error(`Grok.build emitted no event for ${this.config.idleTimeoutMs}ms`),
247
+ "idle-timeout",
248
+ ),
249
+ this.config.idleTimeoutMs,
250
+ );
251
+ }
252
+
253
+ private fail(error: unknown, exitReason: "error" | "idle-timeout" = "error"): void {
254
+ if (this.terminal) return;
255
+ this.terminal = true;
256
+ clearTimeout(this.idleTimer);
257
+ for (const pending of this.interrupts.values()) clearTimeout(pending.timer);
258
+ if (this.ready)
259
+ void this.transport?.drained().then(() =>
260
+ this.emitter.emit("error", {
261
+ exitReason,
262
+ reason: error instanceof Error ? error.message : String(error),
263
+ }),
264
+ );
265
+ void this.transport?.close();
266
+ }
267
+
268
+ async quiesce(): Promise<void> {
269
+ clearTimeout(this.idleTimer);
270
+ const cancelling = !this.terminal && this.sessionId !== undefined;
271
+ this.terminal = true;
272
+ for (const pending of this.interrupts.values()) clearTimeout(pending.timer);
273
+ if (cancelling) {
274
+ for (const pending of this.interrupts.values())
275
+ this.transport?.write({ id: pending.id, result: { outcome: { outcome: "cancelled" } } });
276
+ this.transport?.write({ method: "session/cancel", params: { sessionId: this.sessionId } });
277
+ let timer: NodeJS.Timeout | undefined;
278
+ try {
279
+ await Promise.race([
280
+ this.runPromise,
281
+ new Promise<void>((resolve) => {
282
+ timer = setTimeout(resolve, 200);
283
+ }),
284
+ ]);
285
+ } finally {
286
+ clearTimeout(timer);
287
+ }
288
+ }
289
+ for (const pending of this.interrupts.values()) clearTimeout(pending.timer);
290
+ this.interrupts.clear();
291
+ await this.transport?.close();
292
+ }
293
+
294
+ async collect(): Promise<Artifact[]> {
295
+ await this.quiesce();
296
+ if (!this.cwd) return [];
297
+ const artifacts = await walkArtifacts(this.cwd);
298
+ return this.cwd === this.artifactsDir
299
+ ? artifacts
300
+ : artifacts.map((artifact) => ({ ...artifact, root: this.cwd as string }));
301
+ }
302
+
303
+ async close(): Promise<void> {
304
+ await this.quiesce();
305
+ this.emitter.removeAllListeners();
306
+ }
307
+ }
308
+
309
+ export const grokBuildDriver = defineDriver({
310
+ name: "grok-build",
311
+ configSchema: GrokBuildDriverConfigSchema,
312
+ runtime: (config) => ({
313
+ commands: [config.command],
314
+ capabilities: {
315
+ interactive: true,
316
+ "tool-calls": true,
317
+ "tool-results": true,
318
+ artifacts: true,
319
+ usage: false,
320
+ cost: false,
321
+ isolation: false,
322
+ },
323
+ }),
324
+ selectHarness: selectModelAndReasoningEffort,
325
+ create: (config) => new GrokBuildDriver(config),
326
+ });
327
+
328
+ export type { GrokBuildDriverConfig };
329
+ export { buildGrokBuildArgs, GrokBuildDriverConfigSchema };
package/src/index.ts CHANGED
@@ -364,6 +364,7 @@ export {
364
364
  codexCatalog,
365
365
  copilotCatalog,
366
366
  cursorCatalog,
367
+ grokBuildCatalog,
367
368
  harnessCatalogFor,
368
369
  } from "./core/harness-catalogs/index.js";
369
370
  export {
@@ -528,6 +529,12 @@ export {
528
529
  CursorDriverConfigSchema,
529
530
  cursorDriver,
530
531
  } from "./drivers/cursor/index.js";
532
+ export {
533
+ buildGrokBuildArgs,
534
+ type GrokBuildDriverConfig,
535
+ GrokBuildDriverConfigSchema,
536
+ grokBuildDriver,
537
+ } from "./drivers/grok-build/index.js";
531
538
  export {
532
539
  type McpProbeDriverConfig,
533
540
  McpProbeDriverConfigSchema,
@@ -539,13 +546,11 @@ export {
539
546
  SubprocessDriverConfigSchema,
540
547
  subprocessDriver,
541
548
  } from "./drivers/subprocess/index.js";
542
-
543
549
  export {
544
550
  createReflectiveOptimizer,
545
551
  type ReflectiveOptimizerConfig,
546
552
  ReflectiveOptimizerConfigSchema,
547
553
  } from "./optimization/reflective.js";
548
-
549
554
  // Supported programmatic orchestration.
550
555
  export {
551
556
  applyCandidate,
@@ -29,6 +29,7 @@ import { claudeCodeDriver } from "../drivers/claude-code/index.js";
29
29
  import { codexDriver } from "../drivers/codex/index.js";
30
30
  import { copilotDriver } from "../drivers/copilot/index.js";
31
31
  import { cursorDriver } from "../drivers/cursor/index.js";
32
+ import { grokBuildDriver } from "../drivers/grok-build/index.js";
32
33
  import { mcpProbeDriver } from "../drivers/mcp/index.js";
33
34
  import { subprocessDriver } from "../drivers/subprocess/index.js";
34
35
 
@@ -188,6 +189,7 @@ const builtinDrivers: DriverPlugin<never>[] = [
188
189
  codexDriver,
189
190
  cursorDriver,
190
191
  copilotDriver,
192
+ grokBuildDriver,
191
193
  antigravityDriver,
192
194
  subprocessDriver,
193
195
  mcpProbeDriver,