Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/gittensory-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export {
} from "./miner/coding-agent-driver.js";
export {
createCliSubprocessCodingAgentDriver,
defaultCliSubprocessArgs,
type CliSubprocessDriverOptions,
type CliSubprocessSpawnFn,
} from "./miner/cli-subprocess-driver.js";
Expand Down Expand Up @@ -232,6 +233,7 @@ export {
createFakeCodingAgentDriverForFactory,
isConfiguredCodingAgentDriver,
resolveConfiguredCodingAgentDriverNames,
resolveFirstConfiguredCodingAgentDriverName,
runCodingAgentAttempt,
type CodingAgentDriverName,
type CreateCodingAgentDriverOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ const DEFAULT_TIMEOUT_MS = 120_000;
const MAX_TRANSCRIPT_CHARS = 8000;
const MAX_ERROR_DETAIL_CHARS = 500;

function defaultBuildArgs(task: CodingAgentDriverTask): string[] {
/** 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),
Expand All @@ -67,7 +69,7 @@ function defaultBuildArgs(task: CodingAgentDriverTask): string[] {
*/
export function createCliSubprocessCodingAgentDriver(options: CliSubprocessDriverOptions): CodingAgentDriver {
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const buildArgs = options.buildArgs ?? defaultBuildArgs;
const buildArgs = options.buildArgs ?? defaultCliSubprocessArgs;
const knownSecrets = options.knownSecrets ?? [];
return {
async run(task: CodingAgentDriverTask): Promise<CodingAgentDriverResult> {
Expand Down
116 changes: 111 additions & 5 deletions packages/gittensory-engine/src/miner/driver-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,51 @@ import {
} 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<Record<CodingAgentDriverName, { model?: string; maxTurns?: string }>> =
/** 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<Record<CodingAgentDriverName, { model?: string; timeoutMs?: string }>> =
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<string, string | undefined>): 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, string | undefined>): string[] {
return (env.MINER_CODING_AGENT_PROVIDER ?? "")
.split(",")
Expand All @@ -43,6 +76,9 @@ export function isConfiguredCodingAgentDriver(
): boolean {
switch (name) {
case "noop":
case "claude-cli":
case "codex-cli":
case "agent-sdk":
return true;
default:
return false;
Expand All @@ -55,13 +91,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, string | undefined>,
): string | undefined {
return resolveConfiguredCodingAgentDriverNames(env)[0];
}

export type CreateCodingAgentDriverOptions = {
providerName: string;
env?: Record<string, string | undefined> | 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<string, string | undefined>,
): 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;
Expand All @@ -73,7 +161,16 @@ 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":
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}`);
}
Expand All @@ -87,6 +184,11 @@ 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;
Expand All @@ -109,6 +211,10 @@ export async function runCodingAgentAttempt(
providerName: options.providerName,
env: options.env,
driver: options.driver,
spawn: options.spawn,
query: options.query,
hooks: options.hooks,
knownSecrets: options.knownSecrets,
});
const result = await invokeCodingAgentDriver(driver, mode, options.task, options.log);
if (!options.lintGuard) return { mode, result };
Expand Down
48 changes: 48 additions & 0 deletions packages/gittensory-engine/test/driver-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createCodingAgentDriver,
isConfiguredCodingAgentDriver,
resolveConfiguredCodingAgentDriverNames,
resolveFirstConfiguredCodingAgentDriverName,
runCodingAgentAttempt,
type CodingAgentDriverTask,
} from "../dist/index.js";
Expand Down Expand Up @@ -58,3 +59,50 @@ test("runCodingAgentAttempt wires mode + driver + attempt log end-to-end", async
assert.equal(live.mode, "live");
assert.equal(fake.lastTask, task);
});

// ── #4289: concrete provider resolution (mirrors the root vitest suite's key cases) ────────────────────────

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 task = {
attemptId: "a1",
workingDirectory: "/tmp/w",
acceptanceCriteriaPath: "/tmp/w/AC.md",
instructions: "fix",
maxTurns: 2,
};
const result = await driver.run(task);
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);
});
Loading