* docs: grounded spec for ao start bootstrapper + npm deprecation Real-codebase-verified implementation spec (Track A: launcher + app-state marker + release asset wiring). Replaces the somthing.md draft's aspirational assumptions with file:line ground truth: correct bundle name (Agent Orchestrator.app), publish repo (aoagents/agent-orchestrator), draft-release + asset-rename gaps, unsigned-build reality, and the already-wired update-electron-app updater. Includes a dependency-ordered task breakdown for AO execution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: reframe spec around Go ao start subcommand (not a JS launcher) The npm package ships the existing Go cobra CLI (backend/cmd/ao); this effort rewrites the `ao start` subcommand to fetch+open the desktop app. Corrections: - releases land on AgentWrapper/agent-orchestrator (aoagents was the temporary rewrite home; forge publisher must be repointed) - ao start stops starting the daemon; the frontend owns the daemon - adds the real Go CLI command surface (1.7), the npm-delivery gap for the Go binary (1.6), and the legacy first-boot import decision (6.4) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: separate prod vs test release targets in ao start spec Dev/test loop must never cut a production release. Download repo + forge publisher + npm scope are now build-time overridable: - prod: AgentWrapper/agent-orchestrator + real package name - test: harshitsinghbhandari/agent-orchestrator (fork) + @theharshitsingh/ao T3/T5 now release+test against the fork and the test scope, with prod cut as a separate gated step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(cli): rewrite ao start to fetch + open the desktop app (T1) ao start no longer starts the daemon (the desktop app owns it). It now resolves the installed app (~/.ao/app-state.json marker -> stat -> known- location scan), fetches the latest release zip and ditto-unpacks it on macOS when absent, opens it with --installed-via=npm-bootstrap, and prints an honest deprecation notice. releaseRepo is build-time overridable (-ldflags) so test builds fetch from the fork. Windows/Linux fetch/open are stubbed for T6/T7. Review fixes folded in: download() copies deps.HTTPClient and drops its 2s loopback-probe timeout (a real release asset is hundreds of MB), and fetchApp clears ~/.ao/staging before unpacking. Regression test covers the timeout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(desktop): write ~/.ao/app-state.json marker on launch (T4) The app is the sole writer of the marker ao start reads to locate the bundle. New frontend/src/main/app-state.ts does an atomic temp+rename write mirroring the daemon's runfile.Write, preserving installedAt/installSource across launches and refreshing appPath/version/lastReconciledAt. main.ts hooks it into app.whenReady ordered relocate (macOS) -> write marker -> createWindow, both non-fatal. Bundle path is derived from process.execPath (not app.getAppPath, which is the asar path); JSON keys match start.go's appState reader exactly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci(desktop-release): overridable repo, published release, stable asset aliases (T3) Make the bootstrapper's constant releases/latest/download URL resolve: - forge publisher repo reads AO_RELEASE_REPO (default AgentWrapper/agent- orchestrator); the workflow sets it to github.repository so a fork run publishes to the fork and never to prod - draft:false so the release is immediately live (constant URL needs it) - add ubuntu-latest to the matrix (issue #2191) - post-publish steps upload stable space-free aliases (agent-orchestrator-darwin-arm64.zip, agent-orchestrator-win32-x64.exe) matching exactly what start.go fetches Review fix: the alias upload targeted GITHUB_REF_NAME (the git tag), but publisher-github creates the release as v<package.json version>; retargeted to that and relaxed the guard so workflow_dispatch also produces aliases. Known gaps (documented inline): macOS x64 needs an Intel runner (macos-latest is arm64-only); the Linux stable asset name awaits the deb/rpm-vs-AppImage decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: format with prettier [skip ci] * feat(npm): deliver Go ao binary as @aoagents/ao via per-platform packages (T2) Ship the ao CLI to npm with ZERO install scripts using the esbuild model: - packages/ao: pure-JS @aoagents/ao with a bin/ao.js shim that resolves and execs the matching @aoagents/ao-<platform>-<arch> optionalDependency - four platform packages (darwin-arm64/x64, win32-x64, linux-x64), each os/cpu gated so npm installs only the host's; binary cross-compiled CGO-free (modernc.org/sqlite), gitignored, shipped via files - build-binaries.sh cross-compiles all four; releaseRepo keeps its prod default Not added as root workspaces on purpose: os/cpu-restricted members make root `npm ci` fail EBADPLATFORM (CI's api-drift job runs it). Packages publish standalone; the shim was verified in a published-like layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(npm): set @aoagents/ao packages to 0.10.0 (above existing 0.9.5) The existing @aoagents/ao on npm is 0.9.5; the launcher must publish a higher version so npm latest advances and existing users get the new fetch-and-open binary on update. All five packages plus the four optionalDeps pins set to 0.10.0. * chore: format with prettier [skip ci] * fix(start): capture install provenance before macOS relocation moveToApplicationsFolder() relaunches the app from /Applications without forwarding the --installed-via arg, and code past a successful move never runs in the staging instance. The post-move instance therefore wrote installSource="unknown", and writeAppStateMarker's sticky logic then locked it there, losing the npm-bootstrap provenance in the exact path it exists for. Write the marker before relocation when --installed-via is present so the source is persisted while the arg is still available; the post-move launch preserves it (sticky installSource) while refreshing appPath to /Applications. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: format with prettier [skip ci] --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> |
||
|---|---|---|
| .. | ||
| README.md | ||
README.md
Agent Adapter Contract
This document defines the contract between Agent Orchestrator and CLI-based coding agent adapters. Every agent must implement the contract defined in backend/internal/ports/agent.go to be usable by AO.
Table of Contents
- Overview
- Agent Contract
- Hook System
- Session Metadata
- Agent Lifecycle
- Implementation Guide
- Supported Agents
- Testing
Overview
Agent adapters let AO run and observe different CLI coding agents without hardcoding agent-specific behavior into the spawn engine. The adapter contract provides:
- Agent discovery — Find and validate the agent binary
- Launch configuration — Build the native agent command
- Session restoration — Resume existing sessions
- Hook integration — Install and manage agent hooks
- Metadata extraction — Surface session title, summary, and native session ID
graph TB
AO[Agent Orchestrator] -->|uses| Adapter[Agent Adapter]
Adapter -->|implements| Contract[ports.Agent Contract]
Contract --> Methods[Required Methods]
Methods --> GetConfig[GetConfigSpec]
Methods --> GetLaunch[GetLaunchCommand]
Methods --> GetStrategy[GetPromptDeliveryStrategy]
Methods --> GetHooks[GetAgentHooks]
Methods --> GetRestore[GetRestoreCommand]
Methods --> SessionInfo[SessionInfo]
Methods --> Uninstall[UninstallHooks]
Adapter --> Agent[Native CLI Agent]
Adapter --> Hooks[Agent Hook Config]
Agent Contract
Port Interface
The ports.Agent interface (defined in backend/internal/ports/agent.go) specifies the required adapter methods:
// Agent is the interface all coding agent adapters must implement.
type Agent interface {
// GetConfigSpec describes user-facing agent configuration.
GetConfigSpec() ConfigSpec
// GetLaunchCommand builds the native agent command for spawning a new session.
GetLaunchCommand(ctx context.Context, cfg LaunchConfig) ([]string, error)
// GetPromptDeliveryStrategy reports how the prompt is delivered.
GetPromptDeliveryStrategy() PromptDeliveryStrategy
// GetAgentHooks installs or merges AO hooks into the agent's workspace-local config.
GetAgentHooks(ctx context.Context, cfg WorkspaceHookConfig) error
// GetRestoreCommand builds a command to resume an existing session.
GetRestoreCommand(ctx context.Context, cfg RestoreConfig) ([]string, bool, error)
// SessionInfo returns normalized session metadata from the session record.
SessionInfo(ctx context.Context, session SessionRef) (SessionInfo, bool, error)
// UninstallHooks removes AO hooks from the workspace.
UninstallHooks(ctx context.Context, workspacePath string) error
}
Method Details
GetConfigSpec()
Returns a specification of user-facing configuration options:
type ConfigSpec struct {
// Permissions is the permission mode the agent supports.
Permissions PermissionMode
// Model is the optional model identifier (e.g., "claude-3-5-sonnet-20241022").
Model string
// SupportsSystemPrompt indicates the agent accepts a system prompt.
SupportsSystemPrompt bool
// SupportsRestore indicates the agent supports session restoration.
SupportsRestore bool
}
GetLaunchCommand()
Builds the native agent launch command:
flowchart LR
Input[LaunchConfig] --> Validate[Validate Config]
Validate --> Resolve[Resolve Binary]
Resolve --> Build[Build Command]
Build --> Apply[Apply Permissions]
Apply --> Model[Add Model Flag]
Model --> System[Add System Prompt]
System --> Prompt[Add Prompt]
Prompt --> Output[Launch Command]
Input (LaunchConfig):
SessionID— AO session identifierProjectID— AO project identifierPrompt— User prompt to sendConfig— Agent-specific configurationPermissions— Permission mode overrideWorkspacePath— Path to session worktree
Output: Executable command line as argv array
GetPromptDeliveryStrategy()
Reports how the prompt is delivered to the agent:
type PromptDeliveryStrategy string
const (
// StrategyArgv means the prompt is passed as a command-line argument.
StrategyArgv PromptDeliveryStrategy = "argv"
// StrategyStdin means the prompt is written to stdin after launch.
StrategyStdin PromptDeliveryStrategy = "stdin"
)
GetAgentHooks()
Installs AO hooks into the agent's workspace-local configuration:
flowchart TD
Input[WorkspaceHookConfig] --> Read[Read Existing Config]
Read --> Merge[Merge AO Hooks]
Merge --> Dedup[Deduplicate Entries]
Dedupe --> Preserve[Preserve User Hooks]
Preserve --> Write[Write Updated Config]
Write --> Gitignore[Update .gitignore]
Requirements:
- Preserve all user hooks
- Deduplicate AO hook entries
- Make AO hooks machine-portable (use
ao hooks ...command) - Ensure all written files are covered by
.gitignore
GetRestoreCommand()
Builds a command to resume an existing session:
Input (RestoreConfig):
Session— Complete session record with metadataPermissions— Permission mode to applySystemPrompt— System prompt to re-apply
Output:
cmd— Restore command argvok— True if restore is supportederr— Error if restore fails
SessionInfo()
Returns normalized session metadata from the stored session record:
type SessionInfo struct {
// AgentSessionID is the native agent's session identifier.
AgentSessionID string
// Title is the display title derived from the first user prompt.
Title string
// Summary is the display summary derived from the final assistant message.
Summary string
// Metadata contains adapter-specific extra fields.
Metadata map[string]any
}
Important: SessionInfo must read from the session.Metadata map (populated by hooks), not from scanning agent transcript/cache files.
UninstallHooks()
Removes AO hooks from the workspace while preserving user hooks.
Hook System
Hook Overview
AO uses agent hooks to receive activity signals and session metadata from running agents:
sequenceDiagram
participant AO as AO Daemon
participant Adapter as Agent Adapter
participant Agent as Native Agent
participant Hook as Hook Command
Note over AO: Spawning session
AO->>Adapter: GetAgentHooks(config)
Adapter->>Hook: Write hook config
Hook-->>Adapter: Config written
AO->>Agent: Launch agent
Agent->>Agent: Running in worktree
Note over Agent: Agent triggers event
Agent->>Hook: Execute hook callback
Hook->>Hook: Read stdin payload
Hook->>AO: POST /api/v1/sessions/{id}/activity
AO->>AO: Update activity_state
AO->>Hook: 200 OK
Hook-->>Agent: Exit 0
Agent->>Agent: Continue work
Hook Events
AO supports hooks for these agent events:
| Event | Purpose | Activity State |
|---|---|---|
SessionStart |
Session initialized | - |
UserPromptSubmit |
User submitted prompt | active |
AssistantMessage |
Assistant response | - |
ToolUse |
Agent used a tool | active |
Stop |
Session stopped | idle or exited |
Hook Contract
All hook callbacks follow this contract:
# Hook command format
ao hooks <agent-adapter> <event> <session-id>
# Input (stdin)
# JSON payload from agent (agent-specific format)
# Output (stdout)
# None (discarded)
# Exit code
# 0 = success (always, even on delivery failure)
Hook Behavior
flowchart TD
Trigger[Agent Event Triggered] --> Execute[Execute Hook Command]
Execute --> ReadEnv[Read AO_SESSION_ID<br/>from environment]
ReadEnv --> CheckSession{Is AO Session?}
CheckSession -->|No| Exit[Exit 0 - ignore]
CheckSession -->|Yes| ReadStdin[Read JSON Payload<br/>from stdin]
ReadStdin --> Derive[Derive Activity State<br/>from event]
Derive --> Post[POST to Daemon<br/>/api/v1/sessions/{id}/activity]
Post --> Success{Success?}
Success -->|Yes| Update[Daemon updates<br/>activity_state]
Success -->|No| Log[Log to hooks.log<br/>under AO_DATA_DIR]
Update --> Exit
Log --> Exit
Critical rules:
- Always exit 0 — Hook failure must never break the user's agent
- Log failures — Append to
hooks.logunderAO_DATA_DIR - Best-effort delivery — The daemon may be temporarily unavailable
- Idempotent — Duplicate hook deliveries are safe
Hook Installation Patterns
Different agents use different hook mechanisms:
Claude Code / Compatible Agents
// .claude/hooks.json
{
"SessionStart": ["ao hooks claude-code SessionStart"],
"UserPromptSubmit": ["ao hooks claude-code UserPromptSubmit"],
"Stop": ["ao hooks claude-code Stop"]
}
Factory Droid
// .factory/hooks.json
{
"hooks": [
{
"event": "agent:beforeThinking",
"command": "ao hooks droid beforeThinking"
},
{
"event": "agent:afterThinking",
"command": "ao hooks droid afterThinking"
}
]
}
Codex (Session Flags)
# Codex passes hooks as session flags
codex -c 'hooks.SessionStart=["ao hooks codex SessionStart"]' \
-c 'hooks.Stop=["ao hooks codex Stop"]' \
--dangerously-bypass-hook-trust
Session Metadata
Metadata Keys
Hook callbacks persist normalized keys in the session metadata JSON blob:
const (
// MetadataKeyAgentSessionID is the native agent's session identifier.
MetadataKeyAgentSessionID = "agentSessionId"
// MetadataKeyTitle is the display title from the first user prompt.
MetadataKeyTitle = "title"
// MetadataKeySummary is the display summary from the final assistant message.
MetadataKeySummary = "summary"
)
Metadata Flow
sequenceDiagram
participant Hook as Hook Callback
participant Daemon as AO Daemon
participant Store as SQLite Store
participant UI as Dashboard
Note over Hook: User submits first prompt
Hook->>Daemon: POST /activity<br/>{state: "active", title: "..."}
Daemon->>Store: Update session metadata<br/>metadata.title = "..."
Store->>Store: CDC: session.updated
Store->>UI: SSE: session.updated
UI->>UI: Update session title
Note over Hook: Agent finishes work
Hook->>Daemon: POST /activity<br/>{state: "idle", summary: "..."}
Daemon->>Store: Update session metadata<br/>metadata.summary = "..."
Store->>Store: CDC: session.updated
Store->>UI: SSE: session.updated
UI->>UI: Update session summary
Metadata Persistence
flowchart LR
Hook[Hook Callback] --> POST[POST Activity]
POST --> LCM[Lifecycle Manager]
LCM --> Update[Update Session Record]
Update --> Metadata[Set Metadata Fields]
Metadata --> CDC[CDC Broadcast]
CDC --> UI[Dashboard Updates]
Agent Lifecycle
Spawn Flow
sequenceDiagram
participant User as User
participant UI as Dashboard
participant API as HTTP API
participant Mgr as Session Manager
participant Adapter as Agent Adapter
participant Runtime as Runtime
participant Agent as Agent Process
User->>UI: Click "Spawn Session"
UI->>API: POST /sessions
API->>Mgr: Spawn(config)
Note over Mgr: 1. Create session row
Mgr->>Mgr: Insert session with empty metadata
Note over Mgr: 2. Prepare workspace
Mgr->>Adapter: GetAgentHooks(config)
Adapter->>Adapter: Write hook config
Adapter-->>Mgr: Hooks installed
Note over Mgr: 3. Build launch command
Mgr->>Adapter: GetLaunchCommand(config)
Adapter-->>Mgr: Launch command argv
Note over Mgr: 4. Execute in runtime
Mgr->>Runtime: Execute(agent command)
Runtime->>Agent: Spawn process
Note over Agent: Agent starts
Agent->>Agent: Execute SessionStart hook
Agent->>API: POST /activity (agentSessionId)
API->>API: Update metadata
Agent-->>Runtime: Running
Runtime-->>Mgr: Session active
Mgr-->>API: Session response
API-->>UI: Session created
Restore Flow
sequenceDiagram
participant User as User
participant UI as Dashboard
participant API as HTTP API
participant Mgr as Session Manager
participant Adapter as Agent Adapter
participant Runtime as Runtime
participant Agent as Agent Process
User->>UI: Click "Restore Session"
UI->>API: POST /sessions/{id}/restore
API->>Mgr: Restore(session)
Note over Mgr: Check adapter supports restore
Mgr->>Adapter: GetRestoreCommand(config)
alt Restore supported
Adapter-->>Mgr: Restore command
Mgr->>Runtime: Execute(restore command)
Runtime->>Agent: Spawn process
Agent-->>Mgr: Running
Mgr-->>API: Session restored
else Restore not supported
Adapter-->>Mgr: (cmd, false, nil)
Mgr->>Mgr: Fresh spawn instead
Mgr-->>API: Session created (new)
end
API-->>UI: Session response
Activity Flow
stateDiagram-v2
[*] --> Spawning: Spawn()
Spawning --> Idle: Agent starts
Idle --> Active: User prompt
Active --> Active: Agent working
Active --> Waiting: Needs input
Waiting --> Active: User responds
Active --> Idle: Agent completes
Idle --> Exited: Agent exits
Active --> Exited: Agent crashes
Exited --> [*]
note right of Active
Hook: UserPromptSubmit
Hook: ToolUse
activity_state = "active"
end note
note right of Waiting
Hook: WaitingForInput
activity_state = "waiting_input"
end note
note right of Idle
Hook: Stop
activity_state = "idle"
end note
note right of Exited
Hook: Stop
activity_state = "exited"
is_terminated = true
end note
Implementation Guide
Adapter Template
package myagent
import (
"context"
"fmt"
)
// Plugin is the MyAgent adapter.
type Plugin struct {
// Adapter state (e.g., resolved binary path)
}
// New returns a ready-to-register MyAgent adapter.
func New() *Plugin {
return &Plugin{}
}
// GetConfigSpec describes the agent configuration.
func (p *Plugin) GetConfigSpec() ports.ConfigSpec {
return ports.ConfigSpec{
Permissions: ports.PermissionReadWrite, // or ReadOnly, WriteOnly
SupportsSystemPrompt: true,
SupportsRestore: true,
}
}
// GetLaunchCommand builds the native agent command.
func (p *Plugin) GetLaunchCommand(ctx context.Context, cfg ports.LaunchConfig) ([]string, error) {
// 1. Validate config
if err := cfg.Config.Validate(); err != nil {
return nil, fmt.Errorf("myagent: %w", err)
}
// 2. Resolve binary
binary, err := p.resolveBinary(ctx)
if err != nil {
return nil, err
}
// 3. Build command
cmd := []string{binary}
// Add flags (model, permissions, etc.)
if cfg.Config.Model != "" {
cmd = append(cmd, "--model", cfg.Config.Model)
}
// Add system prompt if supported
if cfg.SystemPrompt != "" {
cmd = append(cmd, "--system-prompt", cfg.SystemPrompt)
}
// Add prompt
if cfg.Prompt != "" {
cmd = append(cmd, "--prompt", cfg.Prompt)
}
return cmd, nil
}
// GetPromptDeliveryStrategy reports how the prompt is delivered.
func (p *Plugin) GetPromptDeliveryStrategy() ports.PromptDeliveryStrategy {
return ports.StrategyArgv // or StrategyStdin
}
// GetAgentHooks installs AO hooks.
func (p *Plugin) GetAgentHooks(ctx context.Context, cfg ports.WorkspaceHookConfig) error {
// 1. Read existing config
config, err := p.readConfig(cfg.WorkspacePath)
if err != nil {
return err
}
// 2. Add AO hooks (preserving user hooks)
config = p.addHooks(config, cfg.SessionID)
// 3. Write updated config
if err := p.writeConfig(cfg.WorkspacePath, config); err != nil {
return err
}
// 4. Ensure .gitignore coverage
if err := p.ensureGitignore(cfg.WorkspacePath); err != nil {
return err
}
return nil
}
// GetRestoreCommand builds a restore command.
func (p *Plugin) GetRestoreCommand(ctx context.Context, cfg ports.RestoreConfig) ([]string, bool, error) {
// 1. Extract native session ID from metadata
sessionID := cfg.Session.Metadata[ports.MetadataKeyAgentSessionID]
if sessionID == "" {
return nil, false, nil // Restore not supported
}
// 2. Build restore command
binary, err := p.resolveBinary(ctx)
if err != nil {
return nil, false, err
}
cmd := []string{
binary,
"--resume", sessionID,
// Re-apply permissions and system prompt
}
return cmd, true, nil
}
// SessionInfo returns normalized metadata.
func (p *Plugin) SessionInfo(ctx context.Context, session ports.SessionRef) (ports.SessionInfo, bool, error) {
info := ports.SessionInfo{
AgentSessionID: session.Metadata[ports.MetadataKeyAgentSessionID],
Title: session.Metadata[ports.MetadataKeyTitle],
Summary: session.Metadata[ports.MetadataKeySummary],
}
// Return hasData=true if any field is populated
hasData := info.AgentSessionID != "" || info.Title != "" || info.Summary != ""
return info, hasData, nil
}
// UninstallHooks removes AO hooks.
func (p *Plugin) UninstallHooks(ctx context.Context, workspacePath string) error {
// Remove AO hook entries while preserving user hooks
config, err := p.readConfig(workspacePath)
if err != nil {
return err
}
config = p.removeHooks(config)
return p.writeConfig(workspacePath, config)
}
Hook Implementation
// Add hooks to the agent's config
func (p *Plugin) addHooks(config map[string]any, sessionID string) map[string]any {
// Define AO hook commands
aoHooks := map[string]string{
"SessionStart": "ao hooks myagent SessionStart",
"UserPromptSubmit": "ao hooks myagent UserPromptSubmit",
"Stop": "ao hooks myagent Stop",
}
// Merge with existing config, preserving user hooks
for event, command := range aoHooks {
if config[event] == nil {
config[event] = []string{command}
} else {
// Deduplicate: check if AO hook already exists
hooks := config[event].([]string)
found := false
for _, h := range hooks {
if strings.HasPrefix(h, "ao hooks myagent") {
found = true
break
}
}
if !found {
config[event] = append(hooks, command)
}
}
}
return config
}
Gitignore Enforcement
import "github.com/aoagents/agent-orchestrator/backend/internal/adapters/agent/hookutil"
func (p *Plugin) ensureGitignore(workspacePath string) error {
// Every file the adapter writes must be gitignored
filesToIgnore := []string{
".myagent/config.json",
".myagent/hooks.json",
}
return hookutil.EnsureWorkspaceGitignore(workspacePath, ".myagent/")
}
Supported Agents
Current Adapters
AO currently supports 23+ agent adapters:
mindmap
root((Agent Adapters))
Anthropic
Claude Code
OpenAI
Codex
Cursor
Cursor
OpenCode
OpenCode
Aider
Aider
Amp
Amp Code
Goose
Goose
GitHub
Copilot
xAI
Grok
Alibaba
Qwen Code
Moonshot
Kimi Code
Reverb
Crush
Cline
Cline
Factory
Droid
Devin
Augment
Auggie
Continue
Continue
Kiro
Kiro
Kilo
Kilo Code
Agy
Agy
Roo
Roo Code
Windsurf
Windsurf
Adapter Locations
backend/internal/adapters/agent/
├── agy/
├── aider/
├── amp/
├── augie/
├── claudecode/
├── clone/
├── codex/
├── continue/
├── cursor/
├── devin/
├── droid/
├── grok/
├── goose/
├── kilo/
├── kimi/
├── kiro/
├── opencode/
├── qwen/
├── roo/
├── crush/
├── windsurf/
└── ...
Adapter Capabilities
| Agent | Permissions | System Prompt | Restore | Hooks |
|---|---|---|---|---|
| Claude Code | ✓ | ✓ | ✓ | ✓ |
| Codex | ✓ | ✓ | ✓ | ✓ (session flags) |
| Cursor | ✓ | ✓ | ✗ | ✗ |
| OpenCode | ✓ | ✓ | ✗ | ✗ |
| Aider | ✓ | ✓ | ✓ | ✓ |
| Amp | ✓ | ✓ | ✓ | ✓ |
| Grok | ✓ | ✓ | ✓ | ✓ (Claude compat) |
| ... | ... | ... | ... | ... |
Testing
Unit Tests
func TestGetLaunchCommand(t *testing.T) {
p := New()
cfg := ports.LaunchConfig{
SessionID: "test-session",
Prompt: "Write a function",
Config: domain.AgentConfig{
Model: "gpt-4",
},
}
cmd, err := p.GetLaunchCommand(context.Background(), cfg)
assert.NoError(t, err)
assert.Contains(t, cmd, "myagent")
assert.Contains(t, cmd, "--model", "gpt-4")
assert.Contains(t, cmd, "Write a function")
}
Integration Tests
func TestAgentE2E(t *testing.T) {
// Skip if agent not installed
if !agentInstalled(t) {
t.Skip("myagent not installed")
}
// Create test project
ctx := context.Background()
project := createTestProject(t)
// Spawn session
session := spawnSession(t, ctx, project.ID, "myagent", "Hello")
// Wait for activity
waitForActivity(t, ctx, session.ID, "active")
// Verify metadata
info := getSessionInfo(t, ctx, session.ID)
assert.NotEmpty(t, info.AgentSessionID)
assert.NotEmpty(t, info.Title)
// Kill session
killSession(t, ctx, session.ID)
}
Conformance Tests
// TestGetAgentHooksFootprintIsGitignored verifies all adapter-written
// files are covered by .gitignore.
func TestGetAgentHooksFootprintIsGitignored(t *testing.T) {
registry := agentRegistry.New()
for _, adapter := range registry.List() {
t.Run(adapter, func(t *testing.T) {
tmpDir := t.TempDir()
cfg := ports.WorkspaceHookConfig{
WorkspacePath: tmpDir,
SessionID: "test-session",
}
agent := registry.Get(adapter)
err := agent.GetAgentHooks(context.Background(), cfg)
assert.NoError(t, err)
// Verify all written files are gitignored
verifyGitignore(t, tmpDir)
})
}
}
Hook Tests
func TestHookDelivery(t *testing.T) {
// Mock daemon endpoint
server := mockActivityServer(t)
defer server.Close()
// Simulate hook callback
payload := map[string]any{
"state": "active",
"title": "Test session",
}
cmd := exec.Command("ao", "hooks", "myagent", "UserPromptSubmit", "test-session")
cmd.Env = append(cmd.Env,
fmt.Sprintf("AO_SESSION_ID=%s", "test-session"),
fmt.Sprintf("AO_DAEMON_URL=%s", server.URL),
)
stdin, _ := cmd.StdinPipe()
json.NewEncoder(stdin).Encode(payload)
stdin.Close()
err := cmd.Run()
assert.NoError(t, err)
// Verify daemon received the activity
assert.ActivityReceived(t, server, "active")
}
Summary
Key points:
- Port contract — All agents implement
ports.Agent - Hooks are critical — Activity signals and metadata flow through hooks
- Always gitignore — Every adapter-written file must be covered
- Metadata from hooks —
SessionInforeads from metadata, not files - Preserve user hooks — Never delete user configuration
- Exit 0 always — Hook failure must never break the agent
Implementation checklist:
- Implement
ports.Agentinterface - Write hook installation/removal
- Ensure all files are gitignored
- Implement
SessionInfofrom metadata - Add unit tests
- Add integration tests
- Add conformance tests
- Register in daemon wiring