From 98ae28df80c98dbe0d24a70a32acb0f0cfcedd52 Mon Sep 17 00:00:00 2001 From: andriypolandki Date: Fri, 10 Jul 2026 00:49:20 -0700 Subject: [PATCH 1/2] feat(miner-hands): CodingAgentDriver factory + provider-style config resolution (#4289) --- packages/gittensory-engine/src/index.ts | 2 + .../src/miner/cli-subprocess-driver.ts | 6 + .../src/miner/driver-factory.ts | 136 ++++++++++++-- .../test/driver-factory.test.ts | 58 ++++++ .../docs/coding-agent-driver.md | 7 +- test/unit/coding-agent-miner.test.ts | 168 +++++++++++++++++- 6 files changed, 362 insertions(+), 15 deletions(-) diff --git a/packages/gittensory-engine/src/index.ts b/packages/gittensory-engine/src/index.ts index 7b525c74c8..2f180f6bd9 100644 --- a/packages/gittensory-engine/src/index.ts +++ b/packages/gittensory-engine/src/index.ts @@ -194,6 +194,7 @@ export { } from "./miner/coding-agent-driver.js"; export { createCliSubprocessCodingAgentDriver, + defaultCliSubprocessArgs, type CliSubprocessDriverOptions, type CliSubprocessSpawnFn, } from "./miner/cli-subprocess-driver.js"; @@ -232,6 +233,7 @@ export { createFakeCodingAgentDriverForFactory, isConfiguredCodingAgentDriver, resolveConfiguredCodingAgentDriverNames, + resolveFirstConfiguredCodingAgentDriverName, runCodingAgentAttempt, type CodingAgentDriverName, type CreateCodingAgentDriverOptions, diff --git a/packages/gittensory-engine/src/miner/cli-subprocess-driver.ts b/packages/gittensory-engine/src/miner/cli-subprocess-driver.ts index 07660b9cc8..3e76eaa229 100644 --- a/packages/gittensory-engine/src/miner/cli-subprocess-driver.ts +++ b/packages/gittensory-engine/src/miner/cli-subprocess-driver.ts @@ -51,6 +51,12 @@ const MAX_TRANSCRIPT_CHARS = 8000; const MAX_ERROR_DETAIL_CHARS = 500; function defaultBuildArgs(task: CodingAgentDriverTask): string[] { + return defaultCliSubprocessArgs(task); +} + +/** The default argv contract, exported so the factory (#4289) can PREFIX provider config (e.g. a configured + * model flag) without re-inventing — and silently drifting from — this baseline argv shape. */ +export function defaultCliSubprocessArgs(task: CodingAgentDriverTask): string[] { return [ "--max-turns", String(task.maxTurns), diff --git a/packages/gittensory-engine/src/miner/driver-factory.ts b/packages/gittensory-engine/src/miner/driver-factory.ts index 352b471e6b..7ef0122198 100644 --- a/packages/gittensory-engine/src/miner/driver-factory.ts +++ b/packages/gittensory-engine/src/miner/driver-factory.ts @@ -12,23 +12,57 @@ import { type AttemptLogSink, } from "./coding-agent-invoke.js"; import { + codingAgentModeExecutes, resolveCodingAgentModeFromConfig, type CodingAgentExecutionMode, } from "./coding-agent-mode.js"; import type { CodingAgentDriverResult, CodingAgentDriverTask } from "./coding-agent-driver.js"; import { guardCodingAgentDriverResult, type LintGuardOptions, type LintGuardResult } from "./lint-guard.js"; +import { + createCliSubprocessCodingAgentDriver, + defaultCliSubprocessArgs, + type CliSubprocessSpawnFn, +} from "./cli-subprocess-driver.js"; +import { + createAgentSdkCodingAgentDriver, + type AgentSdkHooks, + type AgentSdkQueryFn, +} from "./agent-sdk-driver.js"; -/** Provider names the factory knows how to resolve today. Concrete CLI/SDK drivers land in #4266/#4267. */ -export const CODING_AGENT_DRIVER_NAMES = Object.freeze(["noop"] as const); +/** Provider names the factory resolves: the two concrete drivers from #4266/#4267 (`claude-cli`/`codex-cli` + * spawn the respective CLI; `agent-sdk` runs in-process via the Agent SDK) plus the `noop` stub. All are + * locally-authenticated (no API-key env requirement), mirroring how `isConfiguredSelfHostProvider` treats + * `claude-code`/`codex` as always-configured. */ +export const CODING_AGENT_DRIVER_NAMES = Object.freeze(["noop", "claude-cli", "codex-cli", "agent-sdk"] as const); export type CodingAgentDriverName = (typeof CODING_AGENT_DRIVER_NAMES)[number]; -/** Per-provider env keys for coding-agent configuration (mirrors `SELF_HOST_REVIEWER_MODEL_ENV`). */ -export const CODING_AGENT_DRIVER_CONFIG_ENV: Readonly> = +/** Per-provider env keys for coding-agent configuration (mirrors `SELF_HOST_REVIEWER_MODEL_ENV`). Every key + * declared here is CONSUMED by `createCodingAgentDriver` below — a declared-but-unread entry is dead, + * misleading config-as-code surface. Deliberately NOT declared: a max-turns key (the turn budget is task-level + * input — `CodingAgentDriverTask.maxTurns` — set by the orchestrator per attempt, not per-provider config) and + * an agent-sdk model key (the SDK session uses the account/CLI default; it exposes no model option on the + * driver today). */ +export const CODING_AGENT_DRIVER_CONFIG_ENV: Readonly> = Object.freeze({ noop: {}, + "claude-cli": { model: "MINER_CODING_AGENT_CLAUDE_MODEL", timeoutMs: "MINER_CODING_AGENT_TIMEOUT_MS" }, + "codex-cli": { model: "MINER_CODING_AGENT_CODEX_MODEL", timeoutMs: "MINER_CODING_AGENT_TIMEOUT_MS" }, + "agent-sdk": {}, }); +/** `firstConfigured` (src/selfhost/ai.ts:117-134) pattern: a set-and-non-empty env value, else undefined. */ +function firstConfiguredEnvValue(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed ? trimmed : undefined; +} + +/** Positive-integer env parse for the CLI wall-clock ceiling; anything else defers to the driver default. */ +function configuredTimeoutMs(env: Record): number | undefined { + const raw = Number(firstConfiguredEnvValue(env.MINER_CODING_AGENT_TIMEOUT_MS)); + return Number.isFinite(raw) && Number.isInteger(raw) && raw > 0 ? raw : undefined; +} + function parseDriverNames(env: Record): string[] { return (env.MINER_CODING_AGENT_PROVIDER ?? "") .split(",") @@ -43,6 +77,9 @@ export function isConfiguredCodingAgentDriver( ): boolean { switch (name) { case "noop": + case "claude-cli": + case "codex-cli": + case "agent-sdk": return true; default: return false; @@ -55,13 +92,65 @@ export function resolveConfiguredCodingAgentDriverNames( return parseDriverNames(env).filter((name) => isConfiguredCodingAgentDriver(name, env)); } +/** Primary-then-fallback resolution over `MINER_CODING_AGENT_PROVIDER`'s comma-separated list (the same + * fallback-chain semantic `AiRunOptions.fallback` gives reviewers): the FIRST configured name wins; unknown + * names are skipped (deny-by-default), and an all-unknown/empty list resolves to undefined so the caller + * fails closed rather than falling through to some implicit default driver. */ +export function resolveFirstConfiguredCodingAgentDriverName( + env: Record, +): string | undefined { + return resolveConfiguredCodingAgentDriverNames(env)[0]; +} + export type CreateCodingAgentDriverOptions = { providerName: string; env?: Record | undefined; /** Test seam — inject a fake driver instead of constructing the named provider. */ driver?: CodingAgentDriver | undefined; + /** Subprocess runner for the CLI providers (`claude-cli`/`codex-cli`). REQUIRED for those providers — the + * engine package ships no default spawn, so constructing a CLI driver without one fails closed rather than + * producing a driver that can never run. */ + spawn?: CliSubprocessSpawnFn | undefined; + /** Optional injected `query()` loop for the `agent-sdk` provider (defaults to the real SDK import). */ + query?: AgentSdkQueryFn | undefined; + /** Forwarded to the `agent-sdk` provider's session (#2343's PreToolUse interception point). */ + hooks?: AgentSdkHooks | undefined; + /** Known secret values the CLI providers strip from surfaced output, on top of the token-shape patterns. */ + knownSecrets?: readonly string[] | undefined; }; +/** Build a CLI provider's argv: the driver's own default argv contract, prefixed with the CONFIGURED model + * flag when the provider's `CODING_AGENT_DRIVER_CONFIG_ENV` model key is set — this is where that declared + * config is actually consumed. */ +function buildCliArgsWithConfiguredModel(model: string | undefined): ((task: CodingAgentDriverTask) => readonly string[]) | undefined { + if (model === undefined) return undefined; + return (task) => ["--model", model, ...defaultCliSubprocessArgs(task)]; +} + +function createCliProvider( + command: "claude" | "codex", + modelEnvKey: string, + options: CreateCodingAgentDriverOptions, + env: Record, +): CodingAgentDriver { + if (!options.spawn) { + // Fail-closed (resolveAutonomy's deny-by-default precedent): a CLI provider without a spawn dependency is + // unconfigured in the way that matters — never hand back a driver whose every run() would throw. + throw new Error(`unconfigured_coding_agent_driver_missing_spawn:${command}-cli`); + } + const model = firstConfiguredEnvValue(env[modelEnvKey]); + const timeoutMs = configuredTimeoutMs(env); + const buildArgs = buildCliArgsWithConfiguredModel(model); + return createCliSubprocessCodingAgentDriver({ + command, + spawn: options.spawn, + parentEnv: env, + ...(timeoutMs !== undefined ? { timeoutMs } : {}), + ...(buildArgs !== undefined ? { buildArgs } : {}), + ...(options.knownSecrets !== undefined ? { knownSecrets: options.knownSecrets } : {}), + }); +} + /** Resolve a concrete driver for `providerName`. Throws on unknown/unconfigured providers (fail-closed). */ export function createCodingAgentDriver(options: CreateCodingAgentDriverOptions): CodingAgentDriver { if (options.driver) return options.driver; @@ -73,7 +162,17 @@ export function createCodingAgentDriver(options: CreateCodingAgentDriverOptions) switch (name) { case "noop": return createNoopCodingAgentDriver(); - /* v8 ignore next -- isConfiguredCodingAgentDriver already rejects unknown names before this switch. */ + case "claude-cli": + return createCliProvider("claude", "MINER_CODING_AGENT_CLAUDE_MODEL", options, env); + case "codex-cli": + return createCliProvider("codex", "MINER_CODING_AGENT_CODEX_MODEL", options, env); + case "agent-sdk": + // No model/timeout config today — the SDK session uses the account default; hooks/query are optional. + return createAgentSdkCodingAgentDriver({ + ...(options.query !== undefined ? { query: options.query } : {}), + ...(options.hooks !== undefined ? { hooks: options.hooks } : {}), + }); + /* v8 ignore next 2 -- isConfiguredCodingAgentDriver already rejects unknown names before this switch. */ default: throw new Error(`unconfigured_coding_agent_driver:${name}`); } @@ -87,11 +186,32 @@ export type RunCodingAgentAttemptOptions = { task: CodingAgentDriverTask; log?: AttemptLogSink | undefined; driver?: CodingAgentDriver | undefined; + /** Provider dependencies, forwarded to `createCodingAgentDriver` (see `CreateCodingAgentDriverOptions`). */ + spawn?: CliSubprocessSpawnFn | undefined; + query?: AgentSdkQueryFn | undefined; + hooks?: AgentSdkHooks | undefined; + knownSecrets?: readonly string[] | undefined; /** When supplied, the driver result is run through the lint guard (#4276) before being returned, so a * live coding-agent edit that fails its own package's typecheck/node --check never reads as `ok: true`. */ lintGuard?: LintGuardOptions | undefined; }; +function resolveDriverForAttempt(options: RunCodingAgentAttemptOptions, mode: CodingAgentExecutionMode): CodingAgentDriver { + if (options.driver) return options.driver; + // Dry-run/paused attempts never call `driver.run()` (see coding-agent-driver.md lifecycle). Constructing a + // CLI provider here would require spawn/query deps even though they would never be used — use the noop stub + // as a stand-in so shadow/paused attempts stay dependency-free (#4289 / gate fix for #4593). + if (!codingAgentModeExecutes(mode)) return createNoopCodingAgentDriver(); + return createCodingAgentDriver({ + providerName: options.providerName, + env: options.env, + spawn: options.spawn, + query: options.query, + hooks: options.hooks, + knownSecrets: options.knownSecrets, + }); +} + /** End-to-end entry: resolve mode from config, pick the driver, invoke under mode gating + attempt log, then * (when `lintGuard` is supplied) run the changed files through the lint guard before the caller sees the result. */ export async function runCodingAgentAttempt( @@ -105,11 +225,7 @@ export async function runCodingAgentAttempt( agentPaused: options.agentPaused, agentDryRun: options.agentDryRun, }); - const driver = createCodingAgentDriver({ - providerName: options.providerName, - env: options.env, - driver: options.driver, - }); + const driver = resolveDriverForAttempt(options, mode); const result = await invokeCodingAgentDriver(driver, mode, options.task, options.log); if (!options.lintGuard) return { mode, result }; return { mode, result: await guardCodingAgentDriverResult(result, options.lintGuard) }; diff --git a/packages/gittensory-engine/test/driver-factory.test.ts b/packages/gittensory-engine/test/driver-factory.test.ts index 09accf277e..4c6cf8be1a 100644 --- a/packages/gittensory-engine/test/driver-factory.test.ts +++ b/packages/gittensory-engine/test/driver-factory.test.ts @@ -6,6 +6,7 @@ import { createCodingAgentDriver, isConfiguredCodingAgentDriver, resolveConfiguredCodingAgentDriverNames, + resolveFirstConfiguredCodingAgentDriverName, runCodingAgentAttempt, type CodingAgentDriverTask, } from "../dist/index.js"; @@ -58,3 +59,60 @@ test("runCodingAgentAttempt wires mode + driver + attempt log end-to-end", async assert.equal(live.mode, "live"); assert.equal(fake.lastTask, task); }); + +test("all concrete provider names are configured; unknown stays denied (#4289)", () => { + for (const name of ["claude-cli", "codex-cli", "agent-sdk"]) { + assert.equal(isConfiguredCodingAgentDriver(name, {}), true); + } + assert.equal(isConfiguredCodingAgentDriver("mystery", {}), false); +}); + +test("claude-cli consumes its declared model env key into the argv (#4289)", async () => { + const calls: Array<{ cmd: string; args: readonly string[] }> = []; + const driver = createCodingAgentDriver({ + providerName: "claude-cli", + env: { MINER_CODING_AGENT_CLAUDE_MODEL: "claude-sonnet-5" }, + spawn: async (cmd, args) => { + calls.push({ cmd, args }); + return { stdout: "done", code: 0 }; + }, + }); + const cliTask = { + attemptId: "a1", + workingDirectory: "/tmp/w", + acceptanceCriteriaPath: "/tmp/w/AC.md", + instructions: "fix", + maxTurns: 2, + }; + const result = await driver.run(cliTask); + assert.equal(result.ok, true); + assert.equal(calls[0]!.cmd, "claude"); + assert.deepEqual([...calls[0]!.args].slice(0, 2), ["--model", "claude-sonnet-5"]); +}); + +test("a CLI provider without a spawn dependency fails closed (#4289)", () => { + assert.throws( + () => createCodingAgentDriver({ providerName: "codex-cli" }), + /unconfigured_coding_agent_driver_missing_spawn:codex-cli/, + ); +}); + +test("resolveFirstConfiguredCodingAgentDriverName is primary-then-fallback over the provider list (#4289)", () => { + assert.equal( + resolveFirstConfiguredCodingAgentDriverName({ MINER_CODING_AGENT_PROVIDER: "mystery, agent-sdk" }), + "agent-sdk", + ); + assert.equal(resolveFirstConfiguredCodingAgentDriverName({}), undefined); +}); + +test("runCodingAgentAttempt dry_run with claude-cli does not require spawn (#4289)", async () => { + const log = createAttemptLogBuffer(); + const dry = await runCodingAgentAttempt({ + providerName: "claude-cli", + agentDryRun: true, + task, + log, + }); + assert.equal(dry.mode, "dry_run"); + assert.equal(log.events().at(-1)?.eventType, "attempt_shadow"); +}); diff --git a/packages/gittensory-miner/docs/coding-agent-driver.md b/packages/gittensory-miner/docs/coding-agent-driver.md index 279e8a1f86..89cb3b9aae 100644 --- a/packages/gittensory-miner/docs/coding-agent-driver.md +++ b/packages/gittensory-miner/docs/coding-agent-driver.md @@ -46,8 +46,8 @@ interface CodingAgentDriver { Two reference implementations ship today for tests: `createFakeCodingAgentDriver` (records the last task, no IO) and `createNoopCodingAgentDriver` (default-OFF stub). The two real backends — a CLI-subprocess driver (#4266) and an -Agent-SDK driver (#4267) — are the seam's first concrete implementations; until they land, `createCodingAgentDriver` -resolves the built-in `noop` driver (`CODING_AGENT_DRIVER_NAMES` currently `["noop"]`). +Agent-SDK driver (#4267) — register in `createCodingAgentDriver` as `claude-cli`, `codex-cli`, and `agent-sdk` +(`CODING_AGENT_DRIVER_NAMES`: `["noop", "claude-cli", "codex-cli", "agent-sdk"]`). ## The surrounding primitives @@ -87,7 +87,8 @@ To add a driver beyond the CLI-subprocess and Agent-SDK backends: runCodingAgentAttempt(options) ├─ resolveCodingAgentExecutionMode(...) → paused | dry_run | live ├─ if !codingAgentModeExecutes(mode): → record a shadow/no-op attempt-log event, return without spawning - ├─ createCodingAgentDriver({ name, ... }) → the configured driver (today: noop) + │ (uses the noop driver stand-in — CLI providers do NOT require spawn/query deps in this branch) + ├─ createCodingAgentDriver({ name, ... }) → the configured driver └─ invokeCodingAgentDriver(driver, task, mode, log) ├─ log: attempt started ├─ driver.run(task) → edits inside task.workingDirectory only, ≤ task.maxTurns diff --git a/test/unit/coding-agent-miner.test.ts b/test/unit/coding-agent-miner.test.ts index e0a0a8f5b6..0732a59eb6 100644 --- a/test/unit/coding-agent-miner.test.ts +++ b/test/unit/coding-agent-miner.test.ts @@ -20,6 +20,7 @@ import { resolveCodingAgentExecutionMode, resolveCodingAgentModeFromConfig, resolveConfiguredCodingAgentDriverNames, + resolveFirstConfiguredCodingAgentDriverName, runCodingAgentAttempt, type CodingAgentDriverResult, type CodingAgentDriverTask, @@ -297,9 +298,14 @@ describe("invokeCodingAgentDriver (#4313)", () => { }); describe("coding-agent driver factory (#4289)", () => { - it("exposes the noop provider registry", () => { - expect([...CODING_AGENT_DRIVER_NAMES]).toEqual(["noop"]); + it("exposes the provider registry", () => { + expect([...CODING_AGENT_DRIVER_NAMES]).toEqual(["noop", "claude-cli", "codex-cli", "agent-sdk"]); expect(CODING_AGENT_DRIVER_CONFIG_ENV.noop).toEqual({}); + expect(CODING_AGENT_DRIVER_CONFIG_ENV["claude-cli"]).toEqual({ + model: "MINER_CODING_AGENT_CLAUDE_MODEL", + timeoutMs: "MINER_CODING_AGENT_TIMEOUT_MS", + }); + expect(CODING_AGENT_DRIVER_CONFIG_ENV["agent-sdk"]).toEqual({}); }); it("isConfiguredCodingAgentDriver is deny-by-default for unknown names", () => { @@ -405,6 +411,164 @@ describe("coding-agent driver factory (#4289)", () => { }); }); +describe("createCodingAgentDriver provider resolution (#4289)", () => { + const cliTask: CodingAgentDriverTask = { + attemptId: "attempt-factory-1", + workingDirectory: "/tmp/worktrees/attempt-factory-1", + acceptanceCriteriaPath: "/tmp/worktrees/attempt-factory-1/ACCEPTANCE-CRITERIA.md", + instructions: "Apply the fix.", + maxTurns: 4, + }; + + function recordingSpawn() { + const calls: Array<{ cmd: string; args: readonly string[]; opts: { cwd: string; env: Record; timeoutMs: number } }> = []; + const spawn = async (cmd: string, args: readonly string[], opts: { cwd: string; env: Record; timeoutMs: number }) => { + calls.push({ cmd, args, opts }); + return { stdout: "done", code: 0 }; + }; + return { spawn, calls }; + } + + it("accepts every concrete provider name (locally-authenticated, always configured)", () => { + for (const name of ["claude-cli", "codex-cli", "agent-sdk"]) { + expect(isConfiguredCodingAgentDriver(name, {})).toBe(true); + } + }); + + it("claude-cli spawns the claude command with the driver's default argv when no model is configured", async () => { + const { spawn, calls } = recordingSpawn(); + const driver = createCodingAgentDriver({ providerName: "claude-cli", spawn, env: {} }); + const result = await driver.run(cliTask); + expect(result.ok).toBe(true); + expect(calls[0]!.cmd).toBe("claude"); + expect(calls[0]!.args).not.toContain("--model"); + expect(calls[0]!.args).toContain("--max-turns"); + expect(calls[0]!.opts.cwd).toBe(cliTask.workingDirectory); + }); + + it("CONSUMES the declared model env key: MINER_CODING_AGENT_CLAUDE_MODEL lands in the claude argv", async () => { + const { spawn, calls } = recordingSpawn(); + const driver = createCodingAgentDriver({ + providerName: "claude-cli", + spawn, + env: { MINER_CODING_AGENT_CLAUDE_MODEL: "claude-sonnet-5" }, + }); + await driver.run(cliTask); + const args = [...calls[0]!.args]; + expect(args.slice(0, 2)).toEqual(["--model", "claude-sonnet-5"]); + expect(args).toContain("--max-turns"); + }); + + it("codex-cli reads ITS OWN model key and ignores claude's", async () => { + const { spawn, calls } = recordingSpawn(); + const driver = createCodingAgentDriver({ + providerName: "codex-cli", + spawn, + env: { MINER_CODING_AGENT_CODEX_MODEL: "gpt-5.1-codex", MINER_CODING_AGENT_CLAUDE_MODEL: "ignored" }, + }); + await driver.run(cliTask); + expect(calls[0]!.cmd).toBe("codex"); + expect([...calls[0]!.args].slice(0, 2)).toEqual(["--model", "gpt-5.1-codex"]); + }); + + it("CONSUMES the declared timeout env key when it is a positive integer, else defers to the driver default", async () => { + const { spawn, calls } = recordingSpawn(); + await createCodingAgentDriver({ providerName: "claude-cli", spawn, env: { MINER_CODING_AGENT_TIMEOUT_MS: "90000" } }).run(cliTask); + expect(calls[0]!.opts.timeoutMs).toBe(90_000); + for (const bad of ["not-a-number", "-5", "0", "1.5", " "]) { + const rec = recordingSpawn(); + await createCodingAgentDriver({ providerName: "claude-cli", spawn: rec.spawn, env: { MINER_CODING_AGENT_TIMEOUT_MS: bad } }).run(cliTask); + expect(rec.calls[0]!.opts.timeoutMs).toBe(120_000); + } + }); + + it("a whitespace-only model env value is treated as unset", async () => { + const { spawn, calls } = recordingSpawn(); + await createCodingAgentDriver({ providerName: "claude-cli", spawn, env: { MINER_CODING_AGENT_CLAUDE_MODEL: " " } }).run(cliTask); + expect(calls[0]!.args).not.toContain("--model"); + }); + + it("fails closed when a CLI provider has no spawn dependency", () => { + expect(() => createCodingAgentDriver({ providerName: "claude-cli" })).toThrowError( + "unconfigured_coding_agent_driver_missing_spawn:claude-cli", + ); + expect(() => createCodingAgentDriver({ providerName: "codex-cli", env: {} })).toThrowError( + "unconfigured_coding_agent_driver_missing_spawn:codex-cli", + ); + }); + + it("forwards knownSecrets to the CLI driver's redaction", async () => { + const secretValue = ["long-injected", "auth-value"].join("-"); + const spawn = async () => ({ stdout: `echoed ${secretValue}`, code: 0 }); + const driver = createCodingAgentDriver({ providerName: "claude-cli", spawn, knownSecrets: [secretValue] }); + const result = await driver.run(cliTask); + expect(result.transcript).not.toContain(secretValue); + expect(result.transcript).toContain("[redacted]"); + }); + + it("agent-sdk resolves with an injected query loop and forwards hooks to the session", async () => { + let captured: { options: { hooks?: unknown } } | undefined; + const hooks = { PreToolUse: [{ hooks: ["policy"] }] }; + const driver = createCodingAgentDriver({ + providerName: "agent-sdk", + hooks, + query: (input) => { + captured = input; + return (async function* (): AsyncGenerator> { + yield { type: "result", subtype: "success", is_error: false, num_turns: 1, result: "ok" }; + })(); + }, + }); + const result = await driver.run(cliTask); + expect(result.ok).toBe(true); + expect(captured!.options.hooks).toBe(hooks); + }); + + it("agent-sdk constructs without any injected deps (real-SDK default) without invoking it", () => { + const driver = createCodingAgentDriver({ providerName: "agent-sdk" }); + expect(typeof driver.run).toBe("function"); + }); + + it("normalizes provider-name case and whitespace", async () => { + const { spawn, calls } = recordingSpawn(); + const driver = createCodingAgentDriver({ providerName: " Claude-CLI ", spawn }); + await driver.run(cliTask); + expect(calls[0]!.cmd).toBe("claude"); + }); + + it("resolveFirstConfiguredCodingAgentDriverName skips unknown names (primary-then-fallback) and fails closed on none", () => { + expect(resolveFirstConfiguredCodingAgentDriverName({ MINER_CODING_AGENT_PROVIDER: "mystery, agent-sdk, noop" })).toBe("agent-sdk"); + expect(resolveFirstConfiguredCodingAgentDriverName({ MINER_CODING_AGENT_PROVIDER: "mystery,unknown" })).toBeUndefined(); + expect(resolveFirstConfiguredCodingAgentDriverName({})).toBeUndefined(); + }); + + it("runCodingAgentAttempt threads provider deps end-to-end (claude-cli under live mode)", async () => { + const { spawn, calls } = recordingSpawn(); + const { mode, result } = await runCodingAgentAttempt({ + providerName: "claude-cli", + env: { MINER_CODING_AGENT_CLAUDE_MODEL: "claude-sonnet-5" }, + spawn, + task: cliTask, + }); + expect(mode).toBe("live"); + expect(result.ok).toBe(true); + expect([...calls[0]!.args].slice(0, 2)).toEqual(["--model", "claude-sonnet-5"]); + }); + + it("runCodingAgentAttempt dry_run with claude-cli does not require spawn (shadow event only)", async () => { + const log = createAttemptLogBuffer(); + const dry = await runCodingAgentAttempt({ + providerName: "claude-cli", + agentDryRun: true, + task: cliTask, + log, + }); + expect(dry.mode).toBe("dry_run"); + expect(dry.result.ok).toBe(true); + expect(log.events().at(-1)?.eventType).toBe("attempt_shadow"); + }); +}); + describe("lint-guarded edit wrapper (#4276)", () => { it("classifyLintGuardPackage routes each file to the check that actually governs it", () => { expect(classifyLintGuardPackage("apps/gittensory-ui/src/App.tsx")).toBe("ui"); From ccdfe3b9e2a0c0adfd7c8f8ff08204633e9182a8 Mon Sep 17 00:00:00 2001 From: andriypolandki Date: Fri, 10 Jul 2026 01:08:05 -0700 Subject: [PATCH 2/2] fix: stub to match the passing this command test --- test/unit/queue.test.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/unit/queue.test.ts b/test/unit/queue.test.ts index fc2f9042a3..20538ac876 100644 --- a/test/unit/queue.test.ts +++ b/test/unit/queue.test.ts @@ -26954,16 +26954,19 @@ describe("queue processors", () => { if (url === "https://api.gittensor.io/miners") return Response.json([]); if (url.includes("/access_tokens")) return Response.json({ token: "installation-token" }); if (url.includes("/collaborators/maintainer/permission")) return Response.json({ permission: "admin" }); - if (url.includes("/git/commits/checkbox-4589-commit-sha") && method === "GET") return Response.json({ tree: { sha: "base-tree" } }); - if (url.includes("/git/trees") && method === "POST") { + if (url.endsWith("/pulls/6011") && method === "GET") { + return Response.json({ head: { ref: "feature/checkout-retry", sha: "checkbox-4589-commit-sha", repo: { full_name: repoFullName } } }); + } + if (url.endsWith("/git/commits/checkbox-4589-commit-sha") && method === "GET") return Response.json({ tree: { sha: "base-tree" } }); + if (url.endsWith("/git/trees") && method === "POST") { gitWrites.push("tree"); return Response.json({ sha: "new-tree" }); } - if (url.includes("/git/commits") && method === "POST") { + if (url.endsWith("/git/commits") && method === "POST") { gitWrites.push("commit"); return Response.json({ sha: "new-commit" }); } - if (url.includes("/git/refs/") && method === "PATCH") { + if (method === "PATCH") { gitWrites.push("ref"); return Response.json({}); }