Agent Runners
The orchestrator invokes AI coding agents through the AgentRunner interface. Runners are auto-discovered from environment variables via the RunnerRegistry.
AgentRunner Interface
Every runner implements:
interface AgentRunner {
run(ctx: AgentContext): Promise<AgentResult>;
}The orchestrator provides context (issue details, codebase profile, constraints) and the runner spawns the agent, collects output, and commits changes.
Available Runners
ClaudeCodeRunner
Invokes Claude Code CLI in --print mode.
| Property | Value |
|---|---|
| CLI command | claude -p --model <model> --allowedTools <tools> |
| stdin | Prompt sent via stdin |
| Auth | ANTHROPIC_API_KEY (inherited from environment) |
| Model override | AI_SDLC_MODEL env var (default: claude-sonnet-4-5-20250929) |
| Registration | Always available (built-in) |
import { ClaudeCodeRunner } from '@ai-sdlc/orchestrator';CopilotRunner
Invokes GitHub Copilot CLI in --yolo autonomous mode.
| Property | Value |
|---|---|
| CLI command | copilot -p <prompt> --yolo [--model <model>] |
| stdin | None (prompt is a CLI argument) |
| Auth | GH_TOKEN or GITHUB_TOKEN (passed through environment) |
| Model override | AI_SDLC_COPILOT_MODEL env var |
| Registration | Available when GH_TOKEN or GITHUB_TOKEN is set |
import { CopilotRunner } from '@ai-sdlc/orchestrator';CursorRunner
Invokes Cursor CLI agent with NDJSON stream output.
| Property | Value |
|---|---|
| CLI command | cursor-agent --print <prompt> --force --output-format=stream-json [--model <model>] |
| stdin | None (prompt is a CLI argument) |
| Auth | CURSOR_API_KEY |
| Model override | AI_SDLC_CURSOR_MODEL env var |
| Registration | Available when CURSOR_API_KEY is set |
The runner parses NDJSON output to extract the final assistant message for use as the PR summary.
import { CursorRunner, parseStreamJson } from '@ai-sdlc/orchestrator';CodexRunner
Invokes OpenAI Codex CLI in --full-auto --json mode.
| Property | Value |
|---|---|
| CLI command | codex exec - --full-auto --json [-m <model>] |
| stdin | Prompt written to stdin (Codex reads from -) |
| Auth | CODEX_API_KEY |
| Model override | AI_SDLC_CODEX_MODEL env var |
| Registration | Available when CODEX_API_KEY is set |
The runner includes an enhanced token usage parser that first tries NDJSON usage/token_usage events from stderr (accumulating across multiple events), then falls back to regex.
import { CodexRunner, parseTokenUsage } from '@ai-sdlc/orchestrator';GenericLLMRunner
Invokes any OpenAI-compatible chat completions API over HTTP.
| Property | Value |
|---|---|
| Transport | HTTP POST to /v1/chat/completions endpoint |
| Auth | API key via Authorization: Bearer header |
| Registration | Available when OPENAI_API_KEY or LLM_API_KEY + LLM_API_URL is set |
import { GenericLLMRunner } from '@ai-sdlc/orchestrator';Ollama (local models)
There is no dedicated Ollama runner class — Ollama exposes an
OpenAI-compatible chat-completions endpoint
that GenericLLMRunner already speaks. This is also how Gemma (and any other
model you ollama pull) runs — via the OpenAI-compatible HTTP path, not
via a Claude-Code model alias.
Generic form (works today with any three env vars):
export LLM_API_URL=http://localhost:11434/v1/chat/completions
export LLM_API_KEY=ollama # any non-empty value; Ollama ignores it
export LLM_MODEL=gemma4:31b
ai-sdlc run --issue 42 --runner generic-llmollama preset form (first-class, named preset — mirrors openai/anthropic):
export OLLAMA_MODEL=gemma4:31b # defaults apiUrl to http://localhost:11434/v1/chat/completions
ai-sdlc run --issue 42 --runner ollamaOverride the endpoint with OLLAMA_API_URL (exact URL, takes precedence) or
OLLAMA_HOST (host[:port], normalized into the /v1/chat/completions URL —
matches the env var Ollama itself reads):
export OLLAMA_MODEL=gemma4:31b
export OLLAMA_HOST=127.0.0.1:11500 # -> http://127.0.0.1:11500/v1/chat/completionsSelecting a Runner
--runner <name> flag
The ai-sdlc run command accepts a --runner <name> flag to select any registered runner by name:
ai-sdlc run --issue 42 --runner copilot
ai-sdlc run --issue 42 --runner cursor
ai-sdlc run --issue 42 --runner claude-code # explicit built-in defaultIf the specified name is not registered, the command fails immediately with an actionable error listing the available runners — no silent fallback.
AI_SDLC_RUNNER_PLUGIN environment variable
To plug in a custom runner (e.g. Kiro, a proprietary agent CLI, or a test double) without forking the package, set AI_SDLC_RUNNER_PLUGIN to the path of a module that exports an AgentRunner:
export AI_SDLC_RUNNER_PLUGIN=/path/to/my-runner.mjs
ai-sdlc run --issue 42The module must export a default export or a named runner export satisfying the AgentRunner interface:
// my-runner.mjs (ESM)
export default {
async run(ctx) {
// Invoke your agent, commit changes, return AgentResult
return { success: true, filesChanged: ['src/fix.ts'], summary: 'Done.' };
},
};Or with a named export:
// my-runner.mjs
export const runner = {
async run(ctx) { /* ... */ }
};If the module cannot be imported or does not export a valid AgentRunner, the pipeline fails immediately with an actionable error — no silent fallback.
Runner precedence
The full precedence chain (first match wins):
| Priority | Source | How to set |
|---|---|---|
| 1 (highest) | Programmatic injection | new Orchestrator({ runner: myRunner }) |
| 2 | --runner <name> flag | ai-sdlc run --runner copilot |
| 3 | AI_SDLC_RUNNER_PLUGIN | export AI_SDLC_RUNNER_PLUGIN=/path/to/runner.mjs |
| 4 (default) | ClaudeCodeRunner | (always available) |
Env-discovered runners do not auto-select. Setting
GH_TOKEN,OPENAI_API_KEY,ANTHROPIC_API_KEY, etc. registers the corresponding runner so it is selectable by name (--runner copilot), but the mere presence of an ambient env var does not silently switch the default runner — these vars are commonly set for unrelated tools. Absent an explicit--runner/AI_SDLC_RUNNER_PLUGIN/programmatic selection, the default is alwaysClaudeCodeRunner. Select an env-discovered runner explicitly with--runner <name>.
Runner Registry
The RunnerRegistry manages discovery and selection of runners:
import { createRunnerRegistry } from '@ai-sdlc/orchestrator';
const registry = createRunnerRegistry();
// List all available runners
const available = registry.listAvailable();
console.log(available.map(r => r.name));
// ['claude-code', 'copilot', 'cursor', ...]
// Get a specific runner
const runner = registry.get('copilot');
// Get the default runner (first available)
const defaultRunner = registry.getDefault();Auto-Discovery
discoverFromEnv() registers runners based on environment variables:
| Runner | Required Env Var(s) | Source |
|---|---|---|
claude-code | (always available) | built-in |
copilot | GH_TOKEN or GITHUB_TOKEN | env |
cursor | CURSOR_API_KEY | env |
codex | CODEX_API_KEY | env |
openai | OPENAI_API_KEY | env |
anthropic | ANTHROPIC_API_KEY | env |
generic-llm | LLM_API_URL + LLM_API_KEY | env |
ollama | OLLAMA_MODEL (overridable via OLLAMA_API_URL/OLLAMA_HOST) | env |
Manual Registration
You can register custom runners:
import { RunnerRegistry } from '@ai-sdlc/orchestrator';
class MyCustomRunner implements AgentRunner {
async run(ctx: AgentContext): Promise<AgentResult> {
// Custom agent invocation logic
}
}
const registry = new RunnerRegistry();
registry.register('my-agent', new MyCustomRunner());OpenShell Sandbox Integration
When the sandboxId field is set on AgentContext, CLI-based runners execute the agent inside an NVIDIA OpenShell sandbox. The runner prefixes the spawn command with openshell sandbox connect <id> --, so instead of:
claude -p --model claude-sonnet-4-5-20250929 --allowedTools Edit,Write,...it becomes:
openshell sandbox connect aisdlc-issue-42-1711316400 -- claude -p --model claude-sonnet-4-5-20250929 --allowedTools Edit,Write,...This provides kernel-level isolation (Landlock filesystem policies, seccomp syscall filtering, network policy enforcement) without any changes to the agent itself. The orchestrator's executePipeline() automatically passes sandboxId when a SecurityContext with an OpenShell sandbox is configured.
See Security > OpenShell for setup details.
Common Pattern
All CLI-based runners (Claude Code, Copilot, Cursor, Codex) follow the same subprocess pattern:
- Build prompt —
buildPrompt(ctx)constructs a prompt from issue details, constraints, codebase context, and episodic memory - Spawn CLI — Run the agent CLI as a child process with appropriate flags
- Collect output — Buffer stdout and stderr
- Parse token usage — Extract input/output token counts from stderr for cost tracking
- Git diff — Run
git diff --name-onlyandgit ls-files --othersto find changed files - Commit —
git add -Aandgit commitwith the configured message template and co-author
Environment Variables
| Variable | Default | Description |
|---|---|---|
AI_SDLC_RUNNER_PLUGIN | (none) | Path to a custom runner plugin module (ESM/CJS, must export default or named runner). Fails fast on invalid module. |
AI_SDLC_MODEL | claude-sonnet-4-5-20250929 | Model for ClaudeCodeRunner |
AI_SDLC_COPILOT_MODEL | (CLI default) | Model override for CopilotRunner |
AI_SDLC_CURSOR_MODEL | (CLI default) | Model override for CursorRunner |
AI_SDLC_CODEX_MODEL | (CLI default) | Model override for CodexRunner |
AI_SDLC_RUNNER_TIMEOUT | 900000 (15 min) | Runner timeout in ms (supports duration strings) |
AI_SDLC_LINT_COMMAND | (none) | Lint command injected into agent prompts |
AI_SDLC_FORMAT_COMMAND | (none) | Format command injected into agent prompts |
AI_SDLC_COMMIT_MESSAGE_TEMPLATE | fix: resolve issue #{issueNumber}\n\n{issueTitle} | Commit message template |
AI_SDLC_COMMIT_CO_AUTHOR | Claude <noreply@anthropic.com> | Co-author for commits |
OLLAMA_MODEL | (none) | Selects the ollama runner preset; model name (e.g. gemma4:31b) |
OLLAMA_API_URL | http://localhost:11434/v1/chat/completions | Exact chat-completions URL override for the ollama preset |
OLLAMA_HOST | (none) | Host[:port] override for the ollama preset (normalized to the /v1/chat/completions URL); ignored if OLLAMA_API_URL is set |