Contributing

Model gateway validation

Gateway assertions protect each orchestrator's lifecycle: configured, started, routed and restarted. They enrich the nine existing scenarios and helpers in internal/cli/orchestrator_e2e_test.go. Do not add a standalone protocol runner, another E2E suite/file, entry point, job or Makefile runner. Agents validate real account/model behavior as temporary manual checks using the shared tooling in Live validation. Real-provider requests are explicitly opt-in; choose only the checks relevant to the feature, with short prompts, command bounds and client budgets where supported. Keep scripts and reports outside Git.

Automated checks without provider calls

go test ./...
make lint test-scripts build
make test-e2e-claude-code-up
make test-e2e-codex-up

Existing lifecycle scenarios exercise their actual lab LiteLLM gateway with a local controlled provider and fake credentials. They cover Anthropic subscription/API-key routing, signed thinking replay, Responses streaming, encrypted reasoning, tool replay, parallel_tool_calls and provider errors. Orchestrator E2Es cover shipped model defaults/selections, configuration, interactive completion and restart/handoff propagation. These checks cannot establish real account access or model-driven delegation.

Choose a real-account check

Use existing labs test-claude, test-codex and test-gastown in the isolated worktree context, or their actual existing names. Follow the live guide for reference preflight, authentication propagation and owned temporary labs. Codex uses the existing host login through its gateway; do not copy auth into clients.

For Claude model selection, try the configured principal model, an alias such as sonnet, or a full catalog ID relevant to the change. For delegation, define a small custom agent with an explicit model or inherited model and ask the principal to invoke it once and wait for its result. Capture bounded stream-json output with --verbose --forward-subagent-text; verify client success, the actual Agent tool invocation and child response model IDs. Use run_in_lab to load the target's own gateway environment. After a restart, repeat a bounded real request to establish auth and routing still work; credential file presence alone is insufficient.

For Codex, ask for one fresh-context subagent with the model needed for the feature, then wait for completion. Inspect guest session journals for actual parent/child relationships, exact expected child count, requested child models, the taxiway-litellm provider and inherited approval/sandbox settings. Inspect matching gateway requests and positive output tokens. A shared telemetry session ID or session count alone does not prove delegation. Gateway/Langfuse evidence must identify observed models, not just the model requested in a prompt.

For permission/tool features, ask for a harmless proof file and a tiny build such as python3 -m py_compile; inspect the file, compiled artifact, tool events and absence of permission denials. If outbound networking is affected, use one bounded HTTPS request and inspect its result. Test delegated actions when the change affects children, without supplying extra permission bypass flags that could conceal a broken shipped default.

For Gas Town startup/patrol changes, inspect gt status --json, actual role processes, onboarding readiness and the selected model/gateway environment. Observe Deacon tool results for at least three distinct patrol checks, a successful gt patrol report and advancing heartbeat cycle. An alive daemon or heartbeat alone is insufficient. If handoff is affected, check a replaced Deacon/Mayor process and fresh patrol progress after gt handoff; a self-sling interruption is a failure even if the daemon recovers. A separate fresh lab can expose startup failures hidden by populated hooks. Stop background patrols once the selected observations are complete to bound account usage. This does not qualify a full polecat/refinery/merge-queue workload.

For Gas Town inference, execute a bounded request through its shipped launch-agent.sh, then inspect principal/child model responses and actual file, build or network effects relevant to the feature. Do not inject replacement gateway settings that bypass the launcher being validated.

For interactive changes, attach with taxiway shell <lab> and verify real completion without onboarding/authentication/permission loops. Headless success alone does not establish interactive readiness.

Record the tested commit, driver, client versions, selected checks, actual effects and sanitized evidence. Mark blocked or unexecuted paths explicitly. Preserve reference labs and clean only owned temporary resources. See Claude subagents and Codex subagents for client usage.

Evidence collection examples

Use these fragments inside a temporary feature check, choosing driver="docker" or driver="lima" in temporary_lab according to the feature. The shared helper resolves guest execution from the persisted driver; gateway containers still use Docker for both. Capture output locally and return only relevant evidence.

For a bounded Claude observation, choose the model/agents relevant to the task:

output = run_in_lab(lab, ["claude", "-p", prompt, "--model", selected_model,
    "--output-format", "stream-json", "--verbose", "--forward-subagent-text",
    "--no-session-persistence", "--effort", "low", "--tools", "Agent,Bash",
    "--max-budget-usd", "1", "--agents", json.dumps(selected_agents)],
    agent="claude-code", timeout=180)
events = [json.loads(line) for line in output.splitlines() if line.startswith(b"{")]
results = [event for event in events if event.get("type") == "result"]
assert results and results[-1].get("subtype") == "success"
assert not results[-1].get("is_error") and not results[-1].get("permission_denials")
children = {event.get("message", {}).get("model") for event in events
            if event.get("type") == "assistant" and event.get("parent_tool_use_id")}
agent_calls = [block.get("input", {}).get("subagent_type") for event in events
    if event.get("type") == "assistant"
    for block in event.get("message", {}).get("content", [])
    if block.get("type") == "tool_use" and block.get("name") == "Agent"]
# Assert the requested child identities/models, then inspect their actual effects.

For Codex, use run_in_lab(..., agent="codex", timeout=180) with codex exec --skip-git-repo-check --json -m <model> -c 'features.multi_agent_v2=true' <short-prompt>. Require turn.completed, no error/turn.failed, and extract thread_id from thread.started. A bounded guest Python query over ~/.codex/sessions/**/*.jsonl reads only these fields:

# Inside the guest; parent is the thread ID from the current client observation.
children = []
for path in (Path.home() / ".codex/sessions").rglob("*.jsonl"):
    meta, context = {}, {}
    for line in path.read_text().splitlines():
        try: event = json.loads(line)
        except ValueError: continue
        if event.get("type") == "session_meta": meta = event.get("payload", {})
        if event.get("type") == "turn_context": context = event.get("payload", {})
    if meta.get("parent_thread_id") == parent:
        children.append({"model": context.get("model"),
                         "provider": meta.get("model_provider"),
                         "approval": context.get("approval_policy"),
                         "sandbox": context.get("sandbox_policy", {}).get("type")})
# Return only this metadata, never full journals or arbitrary message content.

Execute the guest query with guest(..., timeout=30) and assert the expected count/models, provider taxiway-litellm, approval never, sandbox danger-full-access. For tools, check exact harmless file contents and a compiled artifact with guest, plus the bounded network-proof file where relevant.

For gateway evidence, identify the lab's gateway Postgres container using its owned runtime ID. Query only aggregates after the observation's UTC start time, not full request logs; poll for at most 20 seconds because writes are asynchronous:

SELECT model, count(DISTINCT session_id), sum(completion_tokens)
FROM "LiteLLM_SpendLogs"
WHERE "startTime" >= TIMESTAMP '<observation-start-UTC>' AND completion_tokens > 0
GROUP BY model LIMIT 20;

Run bounded docker exec <owned-gateway-postgres> psql -U litellm -d litellm -At -c <query> through command(..., timeout=30). Assert observed models and positive output tokens. Langfuse generations can establish the same evidence; a requested model, response marker or session count cannot replace it.

For Gas Town, read gt status --json and its tmux.socket. Use that socket for tmux -L <socket> display-message -p -t hq-deacon '#{pane_pid}|#{pane_dead}|#{pane_current_command}' before/after handoff; require a new live Claude process. Inspect process argv/environment inside the guest, returning booleans for model, gateway/alias and permission settings rather than values containing secrets. Apply the same technique to Mayor when relevant.

For actual patrols, inspect /lab/work/gt/deacon/heartbeat.json and Claude journals under ~/.claude/projects/**/*.jsonl, filtering cwd to /lab/work/gt/deacon. Match tool_use IDs for Bash to tool_result.tool_use_id; only successful results count. Useful checks include gt mail inbox, gt convoy, gt witness status, gt refinery status, gt deacon cleanup-orphans, gt dog, gt dispatch. A successful gt patrol report must show a closed patrol and a new patrol; heartbeat cycle and successful check/report counts must advance after the observation's baseline. Flag any errored/interrupted result for gt sling mol-deacon-patrol deacon rather than treating later recovery as success. Return only distinct checks/counts/report count/cycle/interruption booleans. Bound polling (for example 180 seconds for fresh startup, 600 for a complete patrol) and guest inspections (30–90 seconds); stop gt down once selected observations finish. Do not print full transcripts or unlimited role logs.