Skip to content
Merged
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
13 changes: 12 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,18 @@ CODEX_ACCOUNT_ID=
# OPENROUTER_API_KEY= # Pi provider id: openrouter
# HUGGINGFACE_API_KEY= # Pi provider id: huggingface

# Default AI Assistant (must match a registered provider, e.g. claude, codex, pi)
# GitHub Copilot (community provider — @github/copilot-sdk, public preview)
# Spawns the GitHub `copilot` CLI binary. Install once, then either:
# 1. Run `gh extension install github/gh-copilot && gh auth login`, OR
# 2. Set COPILOT_GITHUB_TOKEN below to a PAT with the `copilot` scope.
# Optionally pin the binary path (otherwise resolved from PATH):
# COPILOT_CLI_PATH=/usr/local/bin/copilot
# Use by setting `provider: copilot` in workflow YAML or
# `assistants.copilot.model` in `.archon/config.yaml` (e.g. claude-sonnet-4.5, gpt-5).
# COPILOT_GITHUB_TOKEN=
# COPILOT_CLI_PATH=

# Default AI Assistant (must match a registered provider, e.g. claude, codex, pi, copilot)
# Used for new conversations when no codebase specified — errors on unknown values
DEFAULT_AI_ASSISTANT=claude

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ yarn-error.log*
# ESLint cache
.eslintcache

# Devbox / Nix (devbox.json + devbox.lock are committed; the local profile is not)
.devbox/

# Environment files
.env
.env.local
Expand Down
351 changes: 351 additions & 0 deletions BUG.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,20 @@ Thank you for your interest in contributing to Archon!
4. Copy `.env.example` to `.env` and configure
5. Start development: `bun run dev`

### Reproducible toolchain via Devbox (optional)

Don't want to install bun, gh, jq, postgres-client, python, and uv yourself? The repo ships a `devbox.json` that pins all of them via Nix.

```bash
# 1. Install devbox once: https://www.jetify.com/devbox/docs/installing_devbox/
# 2. From the repo root:
devbox shell # drops you into a shell with the pinned toolchain
bun install # then proceed normally
bun run validate
```

`devbox.json` pins `bun` to the same version (`1.3.11`) the production `Dockerfile` uses, so local validation matches CI.

## Development Workflow

### Code Quality
Expand Down
21 changes: 21 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions devbox.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.14.2/.schema/devbox.schema.json",
"packages": {
"bun": "1.3.11",
"nodejs": "22",
"git": "latest",
"gh": "latest",
"jq": "latest",
"postgresql": "16",
"python3": "latest",
"uv": "latest"
},
"shell": {
"init_hook": [
"echo \"Archon dev shell — bun $(bun --version), node $(node --version)\"",
"echo \"Run 'bun install' once, then 'bun run validate' for the full suite.\""
],
"scripts": {
"setup": "bun install --frozen-lockfile",
"validate": "bun run validate",
"dev": "bun run dev",
"test": "bun run test"
}
}
}
2 changes: 2 additions & 0 deletions packages/core/src/config/config-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
import type {
ClaudeProviderDefaults,
CodexProviderDefaults,
CopilotProviderDefaults,
PiProviderDefaults,
ProviderDefaultsMap,
} from '@archon/providers/types';

export type {
ClaudeProviderDefaults,
CodexProviderDefaults,
CopilotProviderDefaults,
PiProviderDefaults,
ProviderDefaultsMap,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,99 @@ Unsupported YAML fields trigger a visible warning from the dag-executor when the
- [Adding a Community Provider](../contributing/adding-a-community-provider/) — the contributor-facing guide for extending Archon with your own provider.
- [Pi on GitHub](https://github.com/badlogic/pi-mono) — upstream project.

## GitHub Copilot (Community Provider)

**GitHub's official agentic SDK.** Copilot (`@github/copilot-sdk`, public preview) is a community-maintained provider that wraps GitHub's Copilot CLI over JSON-RPC. It runs the same agent engine that powers `gh copilot` and the Copilot mobile app — file edits, shell execution, tool use, and durable sessions.

Copilot is registered as `builtIn: false` because the SDK is in public preview (`0.2.x`) with explicit breaking-change warnings. Promotion to `builtIn: true` will follow once the SDK reaches GA and the integration proves stable.

### Install

The SDK ships as a Bun/npm dependency of `@archon/providers`. The `copilot` CLI binary itself is **not** bundled — install it separately:

```bash
gh extension install github/gh-copilot
gh auth login # or set COPILOT_GITHUB_TOKEN
```

If `copilot` isn't on `PATH`, point Archon at it explicitly via `assistants.copilot.cliPath` in `.archon/config.yaml` or the `COPILOT_CLI_PATH` env var.

### Authenticate

Three credential paths (priority high → low):

1. `assistants.copilot.githubToken` in `.archon/config.yaml` (PAT with `copilot` scope).
2. `COPILOT_GITHUB_TOKEN` env var (or `GH_TOKEN` / `GITHUB_TOKEN` — the CLI reads them).
3. Whatever `gh auth login` / `copilot auth login` wrote to your shell's GitHub credential store.

A Copilot subscription (Pro / Business / Enterprise / Free tier) is required unless you use BYOK with a custom provider configured in the Copilot CLI itself.

### Models

Copilot accepts named model strings:

```yaml
assistants:
copilot:
model: claude-sonnet-4.5 # Anthropic via Copilot
# model: gpt-5 # OpenAI
# model: gpt-4.1 # OpenAI
reasoningEffort: medium # low | medium | high | xhigh
```

Archon does not validate model names client-side — the SDK rejects unknown models at runtime with a clear error. BYOK provider strings (custom OpenAI-compatible endpoints configured in the Copilot CLI) are also accepted.

### Usage in workflows

```yaml
name: copilot-flow
provider: copilot
model: claude-sonnet-4.5

nodes:
- id: think
provider: copilot
model: gpt-5
effort: high
prompt: "Plan the migration to async/await."

- id: write
provider: copilot
model: claude-sonnet-4.5
effort: medium
depends_on: [think]
prompt: "Implement the plan from $think.output."
```

### Copilot capabilities (v1)

| Feature | Support | YAML field |
|---|---|---|
| Session resume | ✅ | automatic (Archon persists `sessionId`) |
| Reasoning effort | ✅ | `effort: low\|medium\|high\|max` (max → xhigh) |
| Codebase env vars (`envInjection`) | ✅ | `.archon/config.yaml` `env:` section |
| MCP servers | ❌ | follow-up — SDK exposes `mcp` permission type but config path TBD |
| Tool restrictions | ❌ | follow-up — `allowed_tools`/`denied_tools` not yet mapped |
| Skills | ❌ | no SDK system-prompt slot in current preview |
| Structured output | ❌ | not in current SDK |
| Thinking control (separate from effort) | ❌ | not in current SDK |
| Cost limits (`maxBudgetUsd`) | ❌ | SDK doesn't surface per-request cost |
| Fallback model | ❌ | not in current SDK |
| Sandbox / hooks | ❌ | not in current SDK |

Unsupported YAML fields trigger a visible warning from the dag-executor when the workflow runs, so you always know what was ignored. Capabilities will be flipped on as their plumbing lands in follow-up PRs.

### Caveats

- **Public preview.** SDK pinned at exact `0.2.2` because the project warns about breaking changes between minor versions.
- **CLI binary required.** Unlike Claude/Codex/Pi, the Copilot SDK spawns the `copilot` CLI process per call. Make sure the binary is reachable in every environment Archon runs in (local, Docker, CI).
- **`approveAll` permission policy.** Copilot's permission callback is hard-wired to `approveAll` in v1, matching the trust model already provided by Archon's worktree isolation. A `copilotPermissionMode` option for stricter modes is a follow-up.

### See also

- [Adding a Community Provider](../contributing/adding-a-community-provider/) — the contributor-facing guide.
- [@github/copilot-sdk on GitHub](https://github.com/github/copilot-sdk) — upstream project.

## How Assistant Selection Works

- Assistant type is set per codebase via the `assistant` field in `.archon/config.yaml` or the `DEFAULT_AI_ASSISTANT` env var
Expand Down
4 changes: 3 additions & 1 deletion packages/providers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@
"./codex/config": "./src/codex/config.ts",
"./codex/binary-resolver": "./src/codex/binary-resolver.ts",
"./community/pi": "./src/community/pi/index.ts",
"./community/copilot": "./src/community/copilot/index.ts",
"./errors": "./src/errors.ts",
"./registry": "./src/registry.ts"
},
"scripts": {
"test": "bun test src/claude/provider.test.ts && bun test src/codex/provider.test.ts && bun test src/registry.test.ts && bun test src/codex/binary-guard.test.ts && bun test src/codex/binary-resolver.test.ts && bun test src/codex/binary-resolver-dev.test.ts && bun test src/claude/binary-resolver.test.ts && bun test src/claude/binary-resolver-dev.test.ts && bun test src/community/pi/model-ref.test.ts && bun test src/community/pi/config.test.ts && bun test src/community/pi/event-bridge.test.ts && bun test src/community/pi/options-translator.test.ts && bun test src/community/pi/session-resolver.test.ts && bun test src/community/pi/provider.test.ts",
"test": "bun test src/claude/provider.test.ts && bun test src/codex/provider.test.ts && bun test src/registry.test.ts && bun test src/codex/binary-guard.test.ts && bun test src/codex/binary-resolver.test.ts && bun test src/codex/binary-resolver-dev.test.ts && bun test src/claude/binary-resolver.test.ts && bun test src/claude/binary-resolver-dev.test.ts && bun test src/community/pi/model-ref.test.ts && bun test src/community/pi/config.test.ts && bun test src/community/pi/event-bridge.test.ts && bun test src/community/pi/options-translator.test.ts && bun test src/community/pi/session-resolver.test.ts && bun test src/community/pi/provider.test.ts && bun test src/community/copilot/config.test.ts && bun test src/community/copilot/model-ref.test.ts && bun test src/community/copilot/options-translator.test.ts && bun test src/community/copilot/event-bridge.test.ts && bun test src/community/copilot/session-resolver.test.ts && bun test src/community/copilot/provider.test.ts",
"type-check": "bun x tsc --noEmit"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.89",
"@archon/paths": "workspace:*",
"@github/copilot-sdk": "0.2.2",
"@mariozechner/pi-ai": "^0.67.5",
"@mariozechner/pi-coding-agent": "^0.67.5",
"@openai/codex-sdk": "^0.116.0",
Expand Down
36 changes: 36 additions & 0 deletions packages/providers/src/community/copilot/capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { ProviderCapabilities } from '../../types';

/**
* Copilot v1 capabilities — intentionally conservative. Each `true` flag
* MUST correspond to wired-up behavior; the dag-executor surfaces warnings
* for any nodeConfig field a provider declares as unsupported. Honest
* under-declaration beats silent ignoring.
*
* v1 wires:
* - sessionResume: SDK exposes listSessions / resumeSession / getLastSessionId
* - effortControl: SDK supports `reasoningEffort: low|medium|high|xhigh`
* - envInjection: per-codebase env vars are passed into the spawned CLI
*
* Roadmap (flip per follow-up PR as plumbing lands):
* - mcp: permission type 'mcp' exists; configuration path TBD
* - toolRestrictions: map allowed_tools / denied_tools to Copilot's gate model
* - skills: if Copilot exposes a system-prompt or skill slot
* - structuredOutput: if SDK adds JSON-schema response support
* - thinkingControl: distinct from effort; not exposed in current SDK
* - costControl: SDK doesn't surface per-request cost in current docs
* - hooks / sandbox: no SDK equivalents in current docs
*/
export const COPILOT_CAPABILITIES: ProviderCapabilities = {
sessionResume: true,
mcp: false,
hooks: false,
skills: false,
toolRestrictions: false,
structuredOutput: false,
envInjection: true,
costControl: false,
effortControl: true,
thinkingControl: false,
fallbackModel: false,
sandbox: false,
};
88 changes: 88 additions & 0 deletions packages/providers/src/community/copilot/cli-resolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { afterEach, describe, expect, spyOn, test } from 'bun:test';

import * as resolver from './cli-resolver';
import { resolveCopilotCliPath, resolveNativeCopilotBinary } from './cli-resolver';

describe('resolveCopilotCliPath', () => {
const origEnv = process.env.COPILOT_CLI_PATH;

afterEach(() => {
if (origEnv === undefined) delete process.env.COPILOT_CLI_PATH;
else process.env.COPILOT_CLI_PATH = origEnv;
});

test('config path wins over everything else', () => {
process.env.COPILOT_CLI_PATH = '/env/path';
const spy = spyOn(resolver, 'resolveNativeCopilotBinary').mockReturnValue('/native/path');
try {
expect(resolveCopilotCliPath('/config/path')).toBe('/config/path');
expect(spy).not.toHaveBeenCalled();
} finally {
spy.mockRestore();
}
});

test('env var wins when config path is absent', () => {
process.env.COPILOT_CLI_PATH = '/env/path';
const spy = spyOn(resolver, 'resolveNativeCopilotBinary').mockReturnValue('/native/path');
try {
expect(resolveCopilotCliPath(undefined)).toBe('/env/path');
expect(spy).not.toHaveBeenCalled();
} finally {
spy.mockRestore();
}
});

test('falls back to native binary resolution when no config or env', () => {
delete process.env.COPILOT_CLI_PATH;
const spy = spyOn(resolver, 'resolveNativeCopilotBinary').mockReturnValue('/native/bin');
try {
expect(resolveCopilotCliPath(undefined)).toBe('/native/bin');
expect(spy).toHaveBeenCalledTimes(1);
} finally {
spy.mockRestore();
}
});

test('returns undefined when nothing resolves (SDK fallback will run)', () => {
delete process.env.COPILOT_CLI_PATH;
const spy = spyOn(resolver, 'resolveNativeCopilotBinary').mockReturnValue(undefined);
try {
expect(resolveCopilotCliPath(undefined)).toBeUndefined();
} finally {
spy.mockRestore();
}
});
});

describe('resolveNativeCopilotBinary', () => {
test('returns the bundled platform binary when the package is installed', () => {
// The test environment has @github/copilot installed (it's a real
// dependency), and the matching @github/copilot-linux-x64 / darwin-arm64
// package gets pulled in as a peer install. We don't assert the exact
// path (it varies by platform) — just that resolution produces an
// absolute path pointing at a file that exists.
const result = resolveNativeCopilotBinary();

if (result === undefined) {
// If the platform-specific package isn't installed on this machine
// (e.g., CI on an unsupported arch), the function correctly returns
// undefined rather than throwing. That's the documented fallback.
expect(result).toBeUndefined();
return;
}

expect(result).toMatch(/copilot$/);
expect(result.startsWith('/')).toBe(true);
});

test('returns undefined when fileExists is false', () => {
// Simulate the resolved path not existing on disk — e.g., partial install.
const spy = spyOn(resolver, 'fileExists').mockReturnValue(false);
try {
expect(resolveNativeCopilotBinary()).toBeUndefined();
} finally {
spy.mockRestore();
}
});
});
Loading
Loading