The CLI redesign eliminates multi-step onboarding by collapsing ao init,
ao add-project, and ao start into a single command.
The target experience:
npm install -g @composio/ao && ao start
Everything else — config generation, project detection, agent runtime discovery, and environment validation — happens automatically. When AO is already running, the CLI detects it and offers contextual options instead of crashing.
| Aspect | Before | After |
|---|---|---|
| Setup steps | 3–4 commands: ao init, edit YAML, ao add-project, ao start |
1 command: ao start |
| Config creation | Manual via ao init |
Auto-generated on first ao start using environment detection |
| Agent selection | User edits YAML manually | Auto-detected from installed runtimes; interactive picker if multiple |
| Spawning a session | ao spawn <project> <issue> |
ao spawn <issue> (project auto-detected) |
| Already-running handling | Error / undefined behavior | Interactive menu (human) or structured info+exit (agent) |
| Config reference | Read source code or docs site | ao config-help prints annotated schema |
ao init |
Primary setup command | Deprecated thin wrapper that calls ao start config path |
ao add-project |
Required for each repo | Fully removed |
ao init
# manually edit agent-orchestrator.yaml
ao add-project my-app ~/code/my-app
ao start
ao start
# config auto-generated, agent auto-detected
# dashboard opens at http://localhost:3000
The core architecture (dist-server, session manager, workspace plugins) remains unchanged. Changes are concentrated in the CLI entry layer and the thin coordination between CLI and core.
running-state.ts — Single-Instance Tracking
Tracks whether an AO instance is already running. Writes a
~/.agent-orchestrator/running.json file containing the running state:
interface RunningState {
pid: number;
configPath: string;
port: number;
startedAt: string;
projects: string[];
}
running.lock) with O_CREAT | O_EXCL for atomic creation — prevents concurrent registration races.process.kill(pid, 0) fails), the entry is cleared on read.register(), unregister(), getRunning(), isAlreadyRunning(), waitForExit(pid, timeoutMs).caller-context.ts — Human/Orchestrator/Agent DetectionDetects who is invoking the CLI and provides typed helpers for propagating context.
type CallerType = "human" | "orchestrator" | "agent";
getCallerType(): If AO_CALLER_TYPE env var is set, trusts it directly. Otherwise: TTY = human, non-TTY = agent.isHumanCaller(): Convenience boolean check.setCallerContext(env, opts): Injects AO_CALLER_TYPE, AO_SESSION_ID, AO_PROJECT_ID, AO_CONFIG_PATH, and AO_PORT into a spawn environment record.detect-agent.ts — Plugin-Based Runtime Discovery
No hardcoded binary paths. Dynamically imports each agent plugin and calls its detect() method.
Known plugins:
| Name | Package |
|---|---|
| claude-code | @composio/ao-plugin-agent-claude-code |
| aider | @composio/ao-plugin-agent-aider |
| codex | @composio/ao-plugin-agent-codex |
| opencode | @composio/ao-plugin-agent-opencode |
Selection logic in detectAgentRuntime():
claude-code.node:readline/promises.claude-code, else first available.detect-env.ts — Environment Detection
Probes the local environment to auto-populate config fields. Returns an EnvironmentInfo object:
owner/repo extraction, current branch, default branch (via detectDefaultBranch()).LINEAR_API_KEY, SLACK_WEBHOOK_URL presence.config-instruction.ts — Config Schema Reference
Returns a comprehensive annotated YAML schema covering every config field:
ports, defaults (runtime, agent, workspace, notifiers), project settings (repo, path, branch,
agentConfig, agentRules, workspace symlinks/postCreate, tracker, SCM),
notification channels, and notification routing. Used by ao config-help.
ao start changedNow the single entry point for all of AO. On first run:
detectEnvironment(cwd) to probe git, tools, and APIs.detectAgentRuntime() to find an installed agent.agent-orchestrator.yaml with all detected values.register() to write running state to ~/.agent-orchestrator/running.json.If already running, delegates to the Already-Running Detection flow (Section 8).
Also exports createConfigOnly() for the deprecated ao init wrapper.
ao spawn changed
Simplified from ao spawn <project> <issue> to ao spawn [issue].
The project is always auto-detected — no project argument is accepted.
See Section 9 for the auto-detection logic.
If a user passes two args (old syntax), a friendly warning is shown:
⚠ 'ao spawn <project> <issue>' is no longer supported.
The project is now auto-detected. Use:
ao spawn INT-100 # spawn with issue INT-100
ao spawn # spawn without an issue
The autoDetectProject() function resolves the project from: single project in config,
AO_PROJECT_ID env var, or cwd matching a project path.
ao batch-spawn changedSame simplification: takes only issue IDs as arguments. The project is always auto-detected — no project prefix is accepted.
ao init deprecatedReduced to a thin wrapper that prints a deprecation warning then delegates to createConfigOnly() from start.ts:
// init.ts — full implementation
program.command("init")
.description("[deprecated] Use 'ao start' instead")
.action(async () => {
console.log("'ao init' is deprecated. Use 'ao start' instead.");
const { createConfigOnly } = await import("./start.js");
await createConfigOnly();
});
ao add-project removedFully deleted. Projects are auto-detected from cwd or added manually to the YAML config file.
ao config-help newPrints the annotated config schema from config-instruction.ts to stdout. Provides both humans and orchestrator agents a quick reference without needing external docs.
All four agent plugins gained two new exports to support runtime discovery:
| Export | Type | Purpose |
|---|---|---|
detect() |
() => boolean |
Returns true if the agent runtime is available on the system (binary exists in PATH, required API key is set, etc.) |
manifest.displayName |
string |
Human-readable name for the interactive picker (e.g. "Claude Code", "Aider", "OpenAI Codex", "OpenCode") |
These are consumed by detect-agent.ts via dynamic import().
Plugins that fail to import (not installed) are silently skipped — this is by design,
as missing plugins are the normal case.
Every spawned session (orchestrator and worker) receives these environment variables,
set in session-manager.ts (core package) at the single spawn point:
| Variable | Value | Purpose |
|---|---|---|
AO_CALLER_TYPE |
"orchestrator" or "agent" |
Tells the CLI who is calling — affects interactive prompts and error handling |
AO_PROJECT_ID |
Project key from config | Enables ao spawn <issue> auto-detection inside agent sessions |
AO_CONFIG_PATH |
Absolute path to YAML | Sessions find config without filesystem search |
AO_PORT |
Dashboard port number | Agents reach the API without parsing config |
The setCallerContext() helper in caller-context.ts provides a typed interface
for populating these variables. All three spawn paths (orchestrator session, ao spawn, ao batch-spawn)
go through the session manager, ensuring consistency.
When ao start is invoked and isAlreadyRunning() returns a live state,
behavior branches on caller type:
Human callers get an interactive menu with four options: open the existing dashboard, start a new instance (killing the old), override with a new config, or quit.
Agent/orchestrator callers receive a structured info dump (port, PID, projects list) and a clean exit code 0, so they can connect to the already-running instance.
The ao spawn argument parser uses this decision tree:
autoDetectProject() Resolution OrderAO_PROJECT_ID env var is set and matches a configured project → use it.process.cwd() matches a project's path field → use it.This means an orchestrator agent calling ao spawn INT-1234 from within a session always resolves correctly via the injected AO_PROJECT_ID env var.
| # | Scenario | Expected Outcome |
|---|---|---|
| 1 | Fresh install, ao start in a git repo | Config auto-generated with detected repo, agent, branch. Server starts. |
| 2 | Fresh install, ao start outside git repo | Error with clear message: "Run ao start inside a git repository." |
| 3 | ao start when already running (human) | Interactive menu: open / restart / override / quit. |
| 4 | ao start when already running (agent) | JSON state printed to stdout, exit 0. |
| 5 | ao start with stale PID in running.json | Stale entry auto-pruned, fresh start proceeds normally. |
| 6 | ao init | Deprecation warning printed, config created via createConfigOnly(). |
| 7 | ao spawn #123 (single project in config) | Auto-detects the only project, spawns session for issue #123. |
| 8 | ao spawn my-app #123 (two args) | Warning: "'ao spawn <project> <issue>' is no longer supported." Shows correct usage and exits. |
| 9 | ao spawn #123 (multi-project, cwd matches one) | Auto-detects project from cwd path match. |
| 10 | ao spawn #123 (multi-project, no cwd match) | Error listing available projects. |
| 11 | ao spawn my-app (arg matches a project ID) | Treated as issue "my-app" (project always auto-detected). May fail if no project matches cwd/env. |
| 12 | ao spawn (no args, single project) | Auto-detect project, no issue — spawns bare session. |
| 13 | ao batch-spawn #1 #2 #3 | Auto-detect project, spawn 3 sessions with duplicate detection. |
| 14 | ao batch-spawn #1 #2 #3 (multi-project, AO_PROJECT_ID set) | Env var resolves project. Spawn 3 sessions. |
| 15 | ao config-help | Full annotated YAML schema printed to stdout. |
| 16 | Agent session calls ao spawn #123 with AO_PROJECT_ID set | Env var resolves project correctly without cwd match. |
| 17 | Multiple agents installed, human runs ao start | Interactive picker with displayName labels shown. |
| 18 | Multiple agents installed, non-TTY ao start | Claude Code auto-selected as preferred default. |
ao config-help on demand when it needs to modify configuration.
ao init (which gets a deprecation wrapper for existing users), add-project was never on main and had no production users. Clean deletion avoids dead code and import graph bloat.
ao spawn, ao batch-spawn) funnel through the session manager. Setting env vars there provides a single source of truth, eliminating the risk of one spawn path forgetting to inject context.
| Category | Finding | Resolution |
|---|---|---|
| Lockfile spin | Busy-wait loop in acquireLock() blocks the event loop for up to 50ms per iteration |
Acceptable for a single-writer scenario with sub-50ms contention windows. Async alternative adds complexity with no practical benefit. |
| Stale lock | If a process crashes between lock acquire and release, the lockfile persists indefinitely | 5-second timeout triggers force-remove. Second attempt uses O_EXCL to ensure atomicity. |
| Race condition | Two concurrent ao start invocations could both see "not running" |
O_EXCL on lockfile makes register() atomic — the second writer wins and overwrites. First instance detects the conflict on next state check. |
| Plugin import errors | detect-agent.ts silently swallows all import errors, including real bugs |
By design: missing plugins are the expected case for most users. Import failures (syntax errors in installed plugins) are rare and would surface when that plugin is actually used. |
| TTY detection edge | process.stdout.isTTY is undefined (not false) when piped |
The ternary isTTY ? "human" : "agent" correctly treats undefined as falsy. |
| ID collision | If a project ID and an issue ID are identical (e.g. project named "123"), ao spawn 123 treats it as an issue |
All single args are now treated as issue IDs. Project is always auto-detected. No ambiguity possible since project ID is never accepted as a positional arg. |
| Schema drift | config-instruction.ts returns a static string that can drift from the actual config type definitions |
Accepted tradeoff: hand-written annotated comments are significantly more useful than auto-generated JSON Schema output. Drift risk is low given the schema changes infrequently. |
| Env duplication | Env vars were initially set in three CLI commands independently, risking divergence | Moved to session-manager.ts in core. All spawn paths use a single setCallerContext() call site. |