agent-orchestrator/docs/agent
Harshit Singh Bhandari 514946fd8b
feat(cli): ao start fetches + opens the desktop app; deprecate npm as an app channel (Track A) (#2201)
* 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>
2026-06-26 19:31:56 +05:30
..
README.md feat(cli): ao start fetches + opens the desktop app; deprecate npm as an app channel (Track A) (#2201) 2026-06-26 19:31:56 +05:30

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 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 identifier
  • ProjectID — AO project identifier
  • Prompt — User prompt to send
  • Config — Agent-specific configuration
  • Permissions — Permission mode override
  • WorkspacePath — 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 metadata
  • Permissions — Permission mode to apply
  • SystemPrompt — System prompt to re-apply

Output:

  • cmd — Restore command argv
  • ok — True if restore is supported
  • err — 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:

  1. Always exit 0 — Hook failure must never break the user's agent
  2. Log failures — Append to hooks.log under AO_DATA_DIR
  3. Best-effort delivery — The daemon may be temporarily unavailable
  4. 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:

  1. Port contract — All agents implement ports.Agent
  2. Hooks are critical — Activity signals and metadata flow through hooks
  3. Always gitignore — Every adapter-written file must be covered
  4. Metadata from hooksSessionInfo reads from metadata, not files
  5. Preserve user hooks — Never delete user configuration
  6. Exit 0 always — Hook failure must never break the agent

Implementation checklist:

  • Implement ports.Agent interface
  • Write hook installation/removal
  • Ensure all files are gitignored
  • Implement SessionInfo from metadata
  • Add unit tests
  • Add integration tests
  • Add conformance tests
  • Register in daemon wiring