@prismatic-io/lux 0.0.2-preview.23 → 0.0.2-preview.25
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.
- package/lib/answerers/persona/index.d.ts.map +1 -1
- package/lib/answerers/persona/index.js +3 -2
- package/lib/answerers/persona/index.js.map +1 -1
- package/lib/cli/init-templates.d.ts +1 -1
- package/lib/cli/init-templates.d.ts.map +1 -1
- package/lib/cli/init-templates.js +1 -0
- package/lib/cli/init-templates.js.map +1 -1
- package/lib/cli/init.d.ts +1 -0
- package/lib/cli/init.d.ts.map +1 -1
- package/lib/cli/init.js +10 -2
- package/lib/cli/init.js.map +1 -1
- package/lib/cli/program.d.ts +1 -1
- package/lib/cli/program.d.ts.map +1 -1
- package/lib/cli/program.js +9 -1
- package/lib/cli/program.js.map +1 -1
- package/lib/core/harness-catalogs/grok-build.d.ts +7 -0
- package/lib/core/harness-catalogs/grok-build.d.ts.map +1 -0
- package/lib/core/harness-catalogs/grok-build.js +18 -0
- package/lib/core/harness-catalogs/grok-build.js.map +1 -0
- package/lib/core/harness-catalogs/index.d.ts +7 -1
- package/lib/core/harness-catalogs/index.d.ts.map +1 -1
- package/lib/core/harness-catalogs/index.js +3 -1
- package/lib/core/harness-catalogs/index.js.map +1 -1
- package/lib/drivers/claude-code/deferred-process.d.ts +17 -0
- package/lib/drivers/claude-code/deferred-process.d.ts.map +1 -0
- package/lib/drivers/claude-code/deferred-process.js +108 -0
- package/lib/drivers/claude-code/deferred-process.js.map +1 -0
- package/lib/drivers/claude-code/index.d.ts +7 -0
- package/lib/drivers/claude-code/index.d.ts.map +1 -1
- package/lib/drivers/claude-code/index.js +45 -42
- package/lib/drivers/claude-code/index.js.map +1 -1
- package/lib/drivers/grok-build/config.d.ts +65 -0
- package/lib/drivers/grok-build/config.d.ts.map +1 -0
- package/lib/drivers/grok-build/config.js +37 -0
- package/lib/drivers/grok-build/config.js.map +1 -0
- package/lib/drivers/grok-build/index.d.ts +83 -0
- package/lib/drivers/grok-build/index.d.ts.map +1 -0
- package/lib/drivers/grok-build/index.js +291 -0
- package/lib/drivers/grok-build/index.js.map +1 -0
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.map +1 -1
- package/lib/orchestrator/config.d.ts.map +1 -1
- package/lib/orchestrator/config.js +2 -0
- package/lib/orchestrator/config.js.map +1 -1
- package/package.json +1 -1
- package/skills/lux-answerer/SKILL.md +1 -1
- package/src/answerers/persona/index.ts +3 -2
- package/src/cli/init-templates.ts +9 -1
- package/src/cli/init.ts +10 -2
- package/src/cli/program.ts +9 -1
- package/src/core/harness-catalogs/grok-build.ts +19 -0
- package/src/core/harness-catalogs/index.ts +10 -1
- package/src/drivers/claude-code/deferred-process.ts +118 -0
- package/src/drivers/claude-code/index.ts +53 -52
- package/src/drivers/grok-build/README.md +112 -0
- package/src/drivers/grok-build/config.ts +40 -0
- package/src/drivers/grok-build/index.ts +329 -0
- package/src/index.ts +7 -2
- package/src/orchestrator/config.ts +2 -0
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
type StarterDriver =
|
|
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([
|
|
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
|
})
|
package/src/cli/program.ts
CHANGED
|
@@ -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([
|
|
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 {
|
|
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,118 @@
|
|
|
1
|
+
import { spawnManagedProcess } from "../../core/exec.js";
|
|
2
|
+
import type { ParsedDriverEvent } from "../../core/index.js";
|
|
3
|
+
|
|
4
|
+
type DeferredProcessOptions = {
|
|
5
|
+
command: string;
|
|
6
|
+
args: string[];
|
|
7
|
+
cwd: string;
|
|
8
|
+
env: NodeJS.ProcessEnv;
|
|
9
|
+
input?: string;
|
|
10
|
+
signal: AbortSignal;
|
|
11
|
+
idleTimeoutMs: number;
|
|
12
|
+
turnTimeoutMs?: number;
|
|
13
|
+
parse: (line: string) => ParsedDriverEvent | ParsedDriverEvent[] | null;
|
|
14
|
+
onEvent: (event: ParsedDriverEvent) => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const MAX_LINE_BYTES = 10_000_000;
|
|
18
|
+
const MAX_STDERR_CHARS = 10_000;
|
|
19
|
+
|
|
20
|
+
/** Deliver progress live, but only resume or finish after the CLI has stopped writing. */
|
|
21
|
+
export const runDeferredProcess = async (options: DeferredProcessOptions) => {
|
|
22
|
+
const managed = spawnManagedProcess(options.command, options.args, {
|
|
23
|
+
cwd: options.cwd,
|
|
24
|
+
env: options.env,
|
|
25
|
+
signal: options.signal,
|
|
26
|
+
});
|
|
27
|
+
let failure: Error | undefined;
|
|
28
|
+
let idleTimer: NodeJS.Timeout | undefined;
|
|
29
|
+
let turnTimer: NodeJS.Timeout | undefined;
|
|
30
|
+
let terminal: ParsedDriverEvent | undefined;
|
|
31
|
+
let stderr = "";
|
|
32
|
+
const clearTimers = () => {
|
|
33
|
+
clearTimeout(idleTimer);
|
|
34
|
+
clearTimeout(turnTimer);
|
|
35
|
+
};
|
|
36
|
+
const fail = (error: Error) => {
|
|
37
|
+
failure ??= error;
|
|
38
|
+
clearTimers();
|
|
39
|
+
void managed.terminate();
|
|
40
|
+
};
|
|
41
|
+
const bumpIdle = () => {
|
|
42
|
+
clearTimeout(idleTimer);
|
|
43
|
+
idleTimer = setTimeout(
|
|
44
|
+
() => fail(new Error(`no event for ${options.idleTimeoutMs}ms`)),
|
|
45
|
+
options.idleTimeoutMs,
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
bumpIdle();
|
|
49
|
+
if (options.turnTimeoutMs !== undefined) {
|
|
50
|
+
turnTimer = setTimeout(
|
|
51
|
+
() => fail(new Error(`Claude turn exceeded ${options.turnTimeoutMs}ms elapsed time`)),
|
|
52
|
+
options.turnTimeoutMs,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
managed.child.stderr.setEncoding("utf8");
|
|
56
|
+
managed.child.stderr.on("data", (chunk: string) => {
|
|
57
|
+
stderr = (stderr + chunk).slice(-MAX_STDERR_CHARS);
|
|
58
|
+
});
|
|
59
|
+
const handleLine = async (line: string) => {
|
|
60
|
+
if (failure || terminal || !line.trim()) return;
|
|
61
|
+
if (Buffer.byteLength(line) > MAX_LINE_BYTES) {
|
|
62
|
+
throw new Error(`stdout line exceeded ${MAX_LINE_BYTES} bytes`);
|
|
63
|
+
}
|
|
64
|
+
const parsed = options.parse(line);
|
|
65
|
+
if (!parsed) return;
|
|
66
|
+
const events = Array.isArray(parsed) ? parsed : [parsed];
|
|
67
|
+
if (events.length === 0) return;
|
|
68
|
+
bumpIdle();
|
|
69
|
+
for (const event of events) {
|
|
70
|
+
if (event.tag === "ready" || event.tag === "progress") await options.onEvent(event);
|
|
71
|
+
else {
|
|
72
|
+
terminal = event;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
managed.child.stdout.setEncoding("utf8");
|
|
78
|
+
const readOutput = async () => {
|
|
79
|
+
let pending = "";
|
|
80
|
+
try {
|
|
81
|
+
for await (const chunk of managed.child.stdout) {
|
|
82
|
+
pending += chunk;
|
|
83
|
+
let newline = pending.indexOf("\n");
|
|
84
|
+
while (newline !== -1) {
|
|
85
|
+
const line = pending.slice(0, newline);
|
|
86
|
+
pending = pending.slice(newline + 1);
|
|
87
|
+
await handleLine(line);
|
|
88
|
+
newline = pending.indexOf("\n");
|
|
89
|
+
}
|
|
90
|
+
if (Buffer.byteLength(pending) > MAX_LINE_BYTES) {
|
|
91
|
+
throw new Error(`stdout line exceeded ${MAX_LINE_BYTES} bytes`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (pending) await handleLine(pending);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const output = readOutput();
|
|
100
|
+
managed.child.stdin.end(options.input);
|
|
101
|
+
try {
|
|
102
|
+
await managed.spawned;
|
|
103
|
+
const result = await managed.exited;
|
|
104
|
+
await output;
|
|
105
|
+
if (failure) throw failure;
|
|
106
|
+
if (options.signal.aborted) throw new Error("Claude turn aborted");
|
|
107
|
+
if (result.signal) throw new Error(`claude exited from signal ${result.signal}`);
|
|
108
|
+
if (result.exitCode !== 0 && terminal?.tag !== "error") {
|
|
109
|
+
throw new Error(`claude exited ${result.exitCode}`);
|
|
110
|
+
}
|
|
111
|
+
return terminal;
|
|
112
|
+
} finally {
|
|
113
|
+
clearTimers();
|
|
114
|
+
await managed.terminate();
|
|
115
|
+
await output;
|
|
116
|
+
if (stderr.trim()) process.stderr.write(`[claude-code] ${stderr.trimEnd()}\n`);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
type DriverEventMap,
|
|
14
14
|
defineDriver,
|
|
15
15
|
discoverCliVersion,
|
|
16
|
-
exec,
|
|
17
16
|
type ParsedDriverEvent,
|
|
18
17
|
type ReadyState,
|
|
19
18
|
type StartContext,
|
|
@@ -29,6 +28,7 @@ import {
|
|
|
29
28
|
import { prepareClaudeExperimentConfig } from "../shared/experiment-policy.js";
|
|
30
29
|
import { copyRegularFileNoFollow } from "../shared/secure-copy.js";
|
|
31
30
|
import { subprocessDriver } from "../subprocess/index.js";
|
|
31
|
+
import { runDeferredProcess } from "./deferred-process.js";
|
|
32
32
|
import { ClaudeCodeLineSchema, makeParseEvent } from "./parse-events.js";
|
|
33
33
|
|
|
34
34
|
const LUX_BOOKKEEPING_FILES = new Set([
|
|
@@ -107,13 +107,22 @@ export const ClaudeCodeDriverConfigSchema = z
|
|
|
107
107
|
* when a weaker model asks in text instead of calling `askToolName`.
|
|
108
108
|
*/
|
|
109
109
|
textQuestionFallback: z.boolean().default(false),
|
|
110
|
-
/**
|
|
110
|
+
/** Maximum silence between recognized protocol events; excludes answerer wait time. */
|
|
111
111
|
idleTimeoutMs: z.number().positive().default(600_000),
|
|
112
|
+
/** Optional elapsed-time cap per defer-resume CLI invocation, regardless of activity. */
|
|
113
|
+
turnTimeoutMs: z.number().positive().optional(),
|
|
112
114
|
/** Cap on outstanding interrupts before the driver aborts. */
|
|
113
115
|
maxInterrupts: z.number().positive().default(20),
|
|
114
116
|
})
|
|
115
117
|
.strict()
|
|
116
118
|
.superRefine((config, ctx) => {
|
|
119
|
+
if (config.turnTimeoutMs !== undefined && config.interactionMode !== "defer-resume") {
|
|
120
|
+
ctx.addIssue({
|
|
121
|
+
code: "custom",
|
|
122
|
+
path: ["turnTimeoutMs"],
|
|
123
|
+
message: "turnTimeoutMs requires defer-resume mode",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
117
126
|
if (config.interactionMode === "defer-resume") {
|
|
118
127
|
const reserved = config.extraArgs?.find((arg) =>
|
|
119
128
|
["--input-format", "--no-session-persistence", "--resume", "-r", "--settings"].some(
|
|
@@ -522,6 +531,7 @@ class ClaudeCodeDriverImpl implements AgentDriver {
|
|
|
522
531
|
private deferredInterrupts = 0;
|
|
523
532
|
private deferredTerminal = false;
|
|
524
533
|
private deferredTurn: Promise<void> | null = null;
|
|
534
|
+
private deferredAbort = new AbortController();
|
|
525
535
|
|
|
526
536
|
constructor(config: ClaudeCodeDriverConfig) {
|
|
527
537
|
this.config = config;
|
|
@@ -620,7 +630,7 @@ class ClaudeCodeDriverImpl implements AgentDriver {
|
|
|
620
630
|
// A terminal protocol event can arrive before the CLI and its descendants
|
|
621
631
|
// have exited. Quiesce the whole process tree before walking/copying files
|
|
622
632
|
// so it cannot swap a checked regular file into a symlink.
|
|
623
|
-
await this.
|
|
633
|
+
await this.quiesce();
|
|
624
634
|
const artifacts = await walkArtifacts(this.collectDir);
|
|
625
635
|
if (this.isolatedWorkspace && this.artifactsDir) {
|
|
626
636
|
await Promise.all(
|
|
@@ -652,6 +662,7 @@ class ClaudeCodeDriverImpl implements AgentDriver {
|
|
|
652
662
|
|
|
653
663
|
async quiesce(): Promise<void> {
|
|
654
664
|
if (this.config.interactionMode === "defer-resume") {
|
|
665
|
+
this.deferredAbort.abort();
|
|
655
666
|
await this.deferredTurn;
|
|
656
667
|
return;
|
|
657
668
|
}
|
|
@@ -661,6 +672,7 @@ class ClaudeCodeDriverImpl implements AgentDriver {
|
|
|
661
672
|
async close(): Promise<void> {
|
|
662
673
|
try {
|
|
663
674
|
if (this.config.interactionMode === "defer-resume") {
|
|
675
|
+
await this.quiesce();
|
|
664
676
|
this.emitter.removeAllListeners();
|
|
665
677
|
if (this.deferredHookPath) {
|
|
666
678
|
await rm(this.deferredHookPath, { force: true });
|
|
@@ -728,58 +740,47 @@ class ClaudeCodeDriverImpl implements AgentDriver {
|
|
|
728
740
|
} else if (resumeWithAnswer && answer?.kind === "ask") {
|
|
729
741
|
input = { input: answer.text };
|
|
730
742
|
}
|
|
731
|
-
const
|
|
732
|
-
this.config.command,
|
|
733
|
-
buildDeferredArgs(this.config, hookPath, this.deferredSessionId ?? undefined),
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
env
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
...(answer ? { LUX_CLAUDE_DEFERRED_ANSWER: JSON.stringify(answer) } : {}),
|
|
742
|
-
},
|
|
743
|
-
...input,
|
|
744
|
-
timeoutMs: this.config.idleTimeoutMs,
|
|
745
|
-
signal: ctx.abortSignal,
|
|
743
|
+
const terminal = await runDeferredProcess({
|
|
744
|
+
command: this.config.command,
|
|
745
|
+
args: buildDeferredArgs(this.config, hookPath, this.deferredSessionId ?? undefined),
|
|
746
|
+
cwd,
|
|
747
|
+
env: {
|
|
748
|
+
...process.env,
|
|
749
|
+
LUX_RUN_DIR: ctx.runDir,
|
|
750
|
+
LUX_ARTIFACTS_DIR: ctx.artifactsDir,
|
|
751
|
+
...this.config.env,
|
|
752
|
+
...(answer ? { LUX_CLAUDE_DEFERRED_ANSWER: JSON.stringify(answer) } : {}),
|
|
746
753
|
},
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
754
|
+
...input,
|
|
755
|
+
idleTimeoutMs: this.config.idleTimeoutMs,
|
|
756
|
+
...(this.config.turnTimeoutMs === undefined
|
|
757
|
+
? {}
|
|
758
|
+
: { turnTimeoutMs: this.config.turnTimeoutMs }),
|
|
759
|
+
signal: AbortSignal.any([ctx.abortSignal, this.deferredAbort.signal]),
|
|
760
|
+
parse: (line) => {
|
|
761
|
+
try {
|
|
762
|
+
const envelope = ClaudeCodeLineSchema.safeParse(JSON.parse(line));
|
|
763
|
+
if (
|
|
764
|
+
envelope.success &&
|
|
765
|
+
(envelope.data.type === "system" || envelope.data.type === "result") &&
|
|
766
|
+
envelope.data.session_id
|
|
767
|
+
) {
|
|
768
|
+
this.deferredSessionId = envelope.data.session_id;
|
|
769
|
+
}
|
|
770
|
+
} catch {
|
|
771
|
+
// The parser below deliberately ignores unmodeled output.
|
|
763
772
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
if (result.timedOut) {
|
|
774
|
-
throw new Error(`no event for ${this.config.idleTimeoutMs}ms`);
|
|
775
|
-
}
|
|
776
|
-
if (result.signal) throw new Error(`claude exited from signal ${result.signal}`);
|
|
777
|
-
if (result.exitCode !== 0 && !this.deferredTerminal) {
|
|
778
|
-
throw new Error(`claude exited ${result.exitCode}`);
|
|
779
|
-
}
|
|
780
|
-
if (!resuming && this.deferredReady) {
|
|
781
|
-
throw new Error("claude exited before ready");
|
|
773
|
+
return parse(line);
|
|
774
|
+
},
|
|
775
|
+
onEvent: (event) => this.dispatchDeferred(event),
|
|
776
|
+
});
|
|
777
|
+
if (this.deferredReady) {
|
|
778
|
+
throw new Error(
|
|
779
|
+
terminal?.tag === "error" ? terminal.error.reason : "claude exited before ready",
|
|
780
|
+
);
|
|
782
781
|
}
|
|
782
|
+
if (terminal) await this.dispatchDeferred(terminal);
|
|
783
|
+
if (!terminal) throw new Error("claude exited before a terminal result");
|
|
783
784
|
}
|
|
784
785
|
|
|
785
786
|
private async dispatchDeferred(event: ParsedDriverEvent): Promise<void> {
|
|
@@ -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
|
+
];
|