Skip to content
Copilot CLI Adapter

Copilot CLI Adapter

The Copilot CLI adapter connects Sortie to the GitHub Copilot CLI via subprocess management. It launches the copilot binary with --output-format json, reads newline-delimited JSON from stdout, and normalizes events into domain types. Registered under kind "copilot-cli".

Each RunTurn call spawns an independent subprocess. The adapter is safe for concurrent use: one adapter instance serves all sessions, with per-session state held in an opaque internal handle. Node.js 22+ is required - a canary check runs copilot --version at session start to verify the binary is functional.

See also: WORKFLOW.md configuration for the full agent schema, environment variables for GitHub token variables, error reference for all agent error kinds, how to write a prompt template for template authoring.


Configuration

The adapter reads from two configuration sections in WORKFLOW.md front matter: the generic agent block (shared by all adapters) and the copilot-cli extension block (pass-through to the Copilot CLI).

agent section

These fields control the orchestrator’s scheduling behavior. They are not passed to the Copilot CLI.

FieldTypeDefaultDescription
kindstring-Must be "copilot-cli" to select this adapter.
commandstringcopilotPath or name of the Copilot CLI binary. Resolved via exec.LookPath at session start.
max_turnsinteger20Maximum Sortie turns per worker session. The orchestrator calls RunTurn up to this many times, re-checking tracker state after each turn.
max_sessionsinteger0 (unlimited)Maximum completed worker sessions per issue before the orchestrator stops retrying. 0 disables the budget.
max_concurrent_agentsinteger10Global concurrency limit across all issues.
turn_timeout_msinteger3600000 (1 hour)Total timeout for a single RunTurn call. The orchestrator cancels the turn context when exceeded.
read_timeout_msinteger5000 (5 seconds)Timeout for startup and synchronous operations.
stall_timeout_msinteger300000 (5 minutes)Maximum time between consecutive events before the orchestrator treats the session as stalled. 0 or negative disables stall detection.
max_retry_backoff_msinteger300000 (5 minutes)Maximum delay cap for exponential backoff between retry attempts.
agent:
  kind: copilot-cli
  command: copilot
  max_turns: 5
  max_sessions: 3
  max_concurrent_agents: 4
  stall_timeout_ms: 300000

copilot-cli extension section

These fields are adapter-specific. The orchestrator forwards them to the adapter without validation. Each field maps to a Copilot CLI flag.

FieldCLI flagTypeDefaultDescription
model--modelstring(CLI default)LLM model identifier (e.g., gpt-4.1).
max_autopilot_continues--max-autopilot-continuesinteger50Maximum autopilot continuation steps within a single RunTurn invocation.
agent--agentstring(none)Agent persona to use.
allowed_tools--allow-toolstring(none)Tool to allow explicitly.
denied_tools--deny-toolstring(none)Tool to deny explicitly.
available_tools--available-toolsstring(none)Set of available tools.
excluded_tools--excluded-toolsstring(none)Set of excluded tools.
mcp_config--additional-mcp-configstring(none)Path to an MCP server configuration file.
disable_builtin_mcps--disable-builtin-mcpsbooleanfalseDisable built-in MCP servers.
no_custom_instructions--no-custom-instructionsbooleanfalseSkip custom instruction files.
experimental--experimentalbooleanfalseEnable experimental features.
copilot-cli:
  model: gpt-4.1
  max_autopilot_continues: 100
  agent: coding-agent
  mcp_config: ./mcp-servers.json
  disable_builtin_mcps: true

agent.max_turns vs. copilot-cli.max_autopilot_continues

These two fields control different systems at different levels.

FieldControlsScope
agent.max_turnsSortie’s orchestrator turn loopHow many times the orchestrator invokes RunTurn per worker session.
copilot-cli.max_autopilot_continuesCopilot CLI’s internal autopilot loopHow many autopilot continuation steps Copilot takes within a single RunTurn invocation.

With agent.max_turns: 5 and max_autopilot_continues: 50, the orchestrator runs up to 5 turns. Within each turn, Copilot takes up to 50 autopilot steps. The total step budget per session is at most 250.

Setting max_autopilot_continues too low causes Copilot to exit mid-task. Setting agent.max_turns too low causes the orchestrator to stop re-invoking the agent before the issue is resolved.

Tool scoping behavior

When no explicit tool scoping flags are configured (allowed_tools, denied_tools, available_tools, and excluded_tools are all empty), the adapter passes --allow-all to auto-approve all tool calls. When any scoping flag is set, --allow-all is omitted and the explicit scoping flags take effect instead.

Every invocation also includes --autopilot and --no-ask-user, which are always present regardless of tool scoping configuration.


Session lifecycle

StartSession

Validates the workspace path, resolves the agent binary, runs a canary check, and verifies authentication. No subprocess is spawned.

  1. Validates that WorkspacePath is a non-empty absolute path pointing to an existing directory.
  2. Resolves the command via exec.LookPath. In SSH mode, resolves the local ssh binary instead; the agent command resolves on the remote host.
  3. Canary check (local mode only): runs copilot --version with a 5-second timeout to verify the binary is functional and Node.js 22+ is available.
  4. Authentication preflight (local mode only): checks for COPILOT_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN environment variables. Falls back to gh auth status (2-second timeout) if no env var is set.
  5. Adopts ResumeSessionID for continuation sessions. The session ID may remain empty until the first result event populates it.
  6. Returns an opaque Session handle containing workspace path, resolved binary, session ID, and SSH configuration.

Errors:

ConditionError kind
Empty or non-existent workspace pathinvalid_workspace_cwd
Workspace path is not a directoryinvalid_workspace_cwd
Agent binary not found in PATHagent_not_found
Binary found but not functional (Node.js missing)agent_not_found
No GitHub authentication source foundagent_not_found
SSH binary not found (SSH mode)agent_not_found

RunTurn

Spawns a Copilot CLI subprocess, reads JSONL events from stdout, and delivers normalized events via the OnEvent callback.

  1. Builds the CLI argument list from session state and pass-through configuration.
  2. Always includes: -p <prompt>, --output-format json, -s, --autopilot, --no-ask-user.
  3. Applies session management flags (see session resume mechanism).
  4. Spawns the subprocess with exec.Command (not exec.CommandContext - see process shutdown for rationale).
  5. Sets cmd.Dir to the workspace path and cmd.Env to the full parent process environment.
  6. Emits session_started event before the scan loop begins.
  7. Reads stdout line by line via a buffered scanner (64 KB initial buffer, 10 MB max line).
  8. Drains stderr in a separate goroutine (debug-level logging).
  9. Parses each line as JSON and dispatches to the appropriate event handler.
  10. After stdout closes, calls cmd.Wait to collect exit status.
  11. Captures session ID from the result event for subsequent turns.
  12. Returns a TurnResult with session ID, exit reason, and cumulative token usage.

StopSession

Terminates a running subprocess. Safe to call when no subprocess is active.

  1. Sends a graceful shutdown signal to the process group (POSIX: SIGTERM; Windows: CTRL_BREAK_EVENT).
  2. Waits up to 5 seconds for the process to exit.
  3. Force-terminates the process tree if still running (POSIX: SIGKILL to process group; Windows: TerminateJobObject).

EventStream

Returns nil. The adapter delivers all events synchronously through the OnEvent callback in RunTurn.


Process shutdown

The adapter uses exec.Command instead of exec.CommandContext. This is intentional.

exec.CommandContext sends an immediate kill signal on context cancellation by default. The agent process cannot flush output buffers, close network connections, or emit final token-usage events. Instead, the adapter sends a graceful shutdown signal first (POSIX: SIGTERM; Windows: CTRL_BREAK_EVENT via the process group) and escalates to a force kill after 5 seconds (POSIX: SIGKILL; Windows: TerminateJobObject), preserving the agent’s opportunity for a clean exit.

On all platforms, the subprocess is placed in its own process group at launch. On Windows, the subprocess is additionally assigned to a Job Object with KILL_ON_JOB_CLOSE, so the entire process tree (including MCP servers and other children) is terminated on shutdown or if Sortie crashes.

A dedicated goroutine monitors ctx.Done() and calls the graceful-kill sequence when the context is cancelled. This covers both orchestrator-initiated cancellation (reconciliation kill, stall detection) and shutdown signals.


JSONL event stream

Copilot CLI emits one JSON object per line on stdout when invoked with --output-format json. The adapter parses each line and maps it to a normalized domain event.

Event type mapping

Copilot CLI eventDomain event typeNotes
assistant.message_deltanotificationStall timer reset. Ephemeral streaming content.
assistant.messagetoken_usage + notificationExtracts outputTokens from data, accumulates cumulatively. Summarizes content or tool requests.
assistant.turn_startnotificationTurn boundary marker.
assistant.turn_endnotificationTurn boundary marker.
tool.execution_startnotificationRecords tool name and timestamp in in-flight map.
tool.execution_completetool_resultCorrelates with in-flight tool.execution_start for duration. Includes ToolError from success field.
session.warningnotificationLogs at warn level. Extracts message from data.
session.infonotificationInformational message. Extracts message from data.
session.task_completenotificationTask completion summary. Extracts summary from data.
session.mcp_server_status_changed(debug log only)Not emitted as domain event.
session.mcp_servers_loaded(debug log only)Not emitted as domain event.
session.tools_updated(debug log only)Not emitted as domain event.
user.message(debug log only)Not emitted as domain event.
resultturn_completed or turn_failedFinal event. Contains session ID, exit code, usage stats.
(parse failure)malformedUnparseable line, truncated to 500 characters.
(unknown type)other_messageUnrecognized event type.

Result event fields

The result event carries turn-level metadata at the top level (no data wrapper):

FieldTypeDescription
sessionIdstringCopilot CLI session ID. Captured for subsequent turns.
exitCodeintegerProcess-level exit code. 0 = success.
usage.premiumRequestsintegerNumber of premium API requests in this session.
usage.totalApiDurationMsintegerAggregate API response wait time in milliseconds.
usage.sessionDurationMsintegerWall-clock session duration in milliseconds.
usage.codeChanges.linesAddedintegerLines of code added.
usage.codeChanges.linesRemovedintegerLines of code removed.
usage.codeChanges.filesModifiedarray of stringsFiles modified in this session.

Token accounting

Key difference from Claude Code: Copilot CLI does not report per-request input token counts. The adapter accumulates outputTokens from assistant.message events. Input tokens are reported as 0.

Accumulation logic

  1. Each assistant.message event with outputTokens in its data increments the running total.
  2. totalTokens is computed as outputTokens (since inputTokens is always 0).
  3. Cumulative totals are emitted as token_usage events after each assistant message.
  4. The result event’s usage (if present) provides API duration and premium request counts but does not carry per-token breakdowns.

Model tracking

Copilot CLI does not report the model name in event payloads. The Model field on token_usage events is empty. Per-model cost attribution is not available for this adapter.

API timing

The result event carries usage.totalApiDurationMs, which the adapter attaches to the turn completion or failure event. Unlike the Claude Code adapter, there is no per-request API latency tracking between individual events.


Tool call tracking

The adapter observes tool execution by correlating tool.execution_start and tool.execution_complete events.

Correlation

  1. A tool.execution_start event records the tool name and a monotonic timestamp in an in-flight map, keyed by toolCallId.
  2. A tool.execution_complete event looks up the matching toolCallId in the in-flight map.
  3. When a match is found, the adapter emits a tool_result event with ToolName, ToolDurationMS (elapsed since the start timestamp), and ToolError (inverted from the success field: ToolError = !success).

Tool error detail

Key difference from Claude Code: the success boolean is the only error signal. There is no error text extraction or ANSI stripping. The Claude Code adapter extracts error text from tool_result content blocks and applies XML stripping, ANSI removal, and truncation - the Copilot CLI adapter reports only whether the tool succeeded or failed.


Error handling

Exit code mapping

Exit codeConditionError kindDescription
0No result event, output tokens > 0(none)Treated as success. Agent produced output but no result event (partial output).
0No result event, output tokens = 0turn_failedAgent exited without producing output. Retryable with exponential backoff. Check WARN-level logs for stderr content.
0Result event with exitCode: 0(none)Normal completion.
0Result event with exitCode != 0turn_failedNon-zero exit in result event despite clean process exit.
127-agent_not_foundBinary not found on local or remote host.
Non-zero (non-127)No result eventport_exitUnexpected subprocess exit.
Signal termination-turn_cancelledProcess killed by signal (graceful or forced).
Context cancelled-turn_cancelledOrchestrator cancelled the turn.

Stdout scanner failure

If the stdout scanner encounters an error (buffer overflow, broken pipe), the adapter:

  1. Sends a graceful-kill signal to the subprocess.
  2. Waits for exit.
  3. Returns a turn_failed result with error kind port_exit.

Session resume mechanism

Key difference from Claude Code: session ID discovery is deferred.

Claude Code generates a UUID session ID at session start and passes it immediately via --session-id. Copilot CLI reports its session ID only in the result event at the end of a turn. The adapter handles this with a fallback mechanism:

TurnSession ID known?CLI flag
First turn, new sessionNo(neither --resume nor --continue)
Subsequent turn, ID captured from resultYes--resume <sessionId>
Subsequent turn, no ID ever capturedNo--continue (resumes most recent conversation in workspace)

The --continue fallback is a safety net. Under normal operation, the first turn’s result event provides the session ID for all subsequent turns.


SSH remote execution

When the worker configuration includes ssh_hosts, the adapter launches Copilot CLI on a remote host via SSH instead of locally.

How it works

  1. StartSession resolves the local ssh binary via exec.LookPath. The agent command is stored for remote execution rather than resolved locally. The canary check and authentication preflight are skipped in SSH mode.
  2. RunTurn builds an SSH command that wraps the remote Copilot CLI invocation.
  3. The remote command is: cd '<workspace_path>' && '<agent_command>' <args...>

SSH options

The adapter uses these SSH options:

OptionValuePurpose
StrictHostKeyCheckingConfigurable (default: accept-new)Host key verification policy. Set via worker.ssh_strict_host_key_checking. Allowed values: accept-new, yes, no.
BatchModeyesDisables interactive prompts (password, passphrase).
ConnectTimeout30Connection timeout in seconds.
ServerAliveInterval15Keepalive interval in seconds.
ServerAliveCountMax3Number of missed keepalives before disconnect.

Shell quoting

All arguments in the remote command string are single-quoted with embedded single-quote escaping (the standard POSIX '\'' pattern). This prevents injection when SSH passes the remote command through the remote shell.

Exit codes

SSH exit code 255 indicates a connection failure (refused, timeout, unreachable) and maps to port_exit. Exit code 127 means the remote agent binary is not in PATH and maps to agent_not_found.


Authentication

Sortie does not manage Copilot CLI credentials. The adapter spawns the subprocess with the full parent process environment (cmd.Env = os.Environ()), and the Copilot CLI reads its authentication variables directly.

Authentication check order at StartSession (local mode only):

  1. COPILOT_GITHUB_TOKEN environment variable.
  2. GH_TOKEN environment variable.
  3. GITHUB_TOKEN environment variable.
  4. gh auth status (2-second timeout, fallback). If gh is authenticated, the adapter logs a warning and proceeds.

If none are found, StartSession returns agent_not_found with a descriptive message listing the expected variables.

At runtime, the Copilot CLI handles its own authentication using whichever token is available in the process environment.

Classic PATs do not work with Copilot CLI

Copilot CLI requires a fine-grained personal access token (prefix github_pat_) with the Copilot Requests permission enabled. Classic PATs (prefix ghp_) fail authentication silently - the CLI falls through all token variables and reports no valid credential. OAuth tokens (gho_ from copilot auth login) and GitHub App user-to-server tokens (ghu_) also work. If you see authentication failures despite having a token set, check the token prefix.


Concurrency safety

The adapter is safe for concurrent use. One CopilotAdapter instance serves all sessions. Per-session state (workspace path, session ID, process handle) is isolated in the opaque Session.Internal field. A mutex guards the subprocess handle for concurrent access from StopSession and the graceful-kill goroutine.

No adapter-level serialization is needed for RunTurn calls - each spawns an independent subprocess with its own stdout pipe and scanner.


Adapter registration

The adapter registers itself under kind "copilot-cli" via an init function in internal/agent/copilot. Registration metadata declares:

PropertyValue
RequiresCommandtrue

The orchestrator’s preflight validation uses this to produce a specific error message if the binary cannot be found before attempting session creation.


Key differences from Claude Code adapter

AspectClaude CodeCopilot CLI
Kindclaude-codecopilot-cli
Default commandclaudecopilot
Output format flag--output-format stream-json--output-format json
Session ID at startUUID generated by adapterDiscovered from first result event
Resume flag--resume <UUID>--resume <sessionId> or --continue fallback
Input token reportingPer-request cumulativeNot available (always 0)
Model reportingFrom assistant eventsNot available
Permission mode--permission-mode or --dangerously-skip-permissions--autopilot + --no-ask-user + --allow-all
Tool error detailError text with XML/ANSI strippingBoolean success flag only
AuthenticationANTHROPIC_API_KEY (+ Bedrock, Vertex)COPILOT_GITHUB_TOKEN / GH_TOKEN / GITHUB_TOKEN / gh auth
Canary checkNonecopilot --version (5-second timeout)
Auth preflightNoneChecks env vars + gh auth status

For Claude Code configuration, see Claude Code adapter reference.


External references


Related pages

Was this page helpful?