CLI Redesign: Simplified Onboarding

PR #463 Status: Implemented Updated: 2026-03-17

1. Overview

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.


2. Before vs After

AspectBeforeAfter
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

Before (4 steps)

ao init
# manually edit agent-orchestrator.yaml
ao add-project my-app ~/code/my-app
ao start

After (1 step)

ao start
# config auto-generated, agent auto-detected
# dashboard opens at http://localhost:3000

3. Architecture Changes

File Map

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.


4. New Library Files

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[];
}

caller-context.ts — Human/Orchestrator/Agent Detection

Detects who is invoking the CLI and provides typed helpers for propagating context.

type CallerType = "human" | "orchestrator" | "agent";

detect-agent.ts — Plugin-Based Runtime Discovery

No hardcoded binary paths. Dynamically imports each agent plugin and calls its detect() method.

Known plugins:

NamePackage
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():

detect-env.ts — Environment Detection

Probes the local environment to auto-populate config fields. Returns an EnvironmentInfo object:

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.


5. Command Changes

ao start changed

Now the single entry point for all of AO. On first run:

  1. Calls detectEnvironment(cwd) to probe git, tools, and APIs.
  2. Calls detectAgentRuntime() to find an installed agent.
  3. Generates agent-orchestrator.yaml with all detected values.
  4. Calls register() to write running state to ~/.agent-orchestrator/running.json.
  5. Starts the dist-server and orchestrator session.

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 changed

Same simplification: takes only issue IDs as arguments. The project is always auto-detected — no project prefix is accepted.

ao init deprecated

Reduced 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 removed

Fully deleted. Projects are auto-detected from cwd or added manually to the YAML config file.

ao config-help new

Prints the annotated config schema from config-instruction.ts to stdout. Provides both humans and orchestrator agents a quick reference without needing external docs.


6. Plugin System Enhancements

All four agent plugins gained two new exports to support runtime discovery:

ExportTypePurpose
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.


7. Session Environment Variables

Every spawned session (orchestrator and worker) receives these environment variables, set in session-manager.ts (core package) at the single spawn point:

VariableValuePurpose
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.


8. Already-Running Detection

When ao start is invoked and isAlreadyRunning() returns a live state, behavior branches on caller type:

graph TD A["ao start"] --> B{"isAlreadyRunning()?"} B -->|No| C["Normal startup"] B -->|Yes| D{"getCallerType()"} D -->|human| E["Interactive menu"] D -->|agent / orchestrator| F["Print JSON state + exit 0"] E --> G["Open dashboard in browser"] E --> H["Start new instance — kill old"] E --> I["Override config + restart"] E --> J["Quit"]

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.


9. Spawn Auto-Detection Logic

The ao spawn argument parser uses this decision tree:

graph TD A["ao spawn [issue]"] --> B{"Two args provided?"} B -->|Yes| C["⚠ Warning: old syntax
Show correct usage + exit"] B -->|No| D{"One arg provided?"} D -->|No| E["autoDetectProject
no issue — bare session"] D -->|Yes| F["arg = issueId
autoDetectProject"]

autoDetectProject() Resolution Order

  1. If only one project in config → use it.
  2. If AO_PROJECT_ID env var is set and matches a configured project → use it.
  3. If process.cwd() matches a project's path field → use it.
  4. Otherwise → throw error listing available projects.

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.


10. Testing Scenarios

#ScenarioExpected Outcome
1Fresh install, ao start in a git repoConfig auto-generated with detected repo, agent, branch. Server starts.
2Fresh install, ao start outside git repoError with clear message: "Run ao start inside a git repository."
3ao start when already running (human)Interactive menu: open / restart / override / quit.
4ao start when already running (agent)JSON state printed to stdout, exit 0.
5ao start with stale PID in running.jsonStale entry auto-pruned, fresh start proceeds normally.
6ao initDeprecation warning printed, config created via createConfigOnly().
7ao spawn #123 (single project in config)Auto-detects the only project, spawns session for issue #123.
8ao spawn my-app #123 (two args)Warning: "'ao spawn <project> <issue>' is no longer supported." Shows correct usage and exits.
9ao spawn #123 (multi-project, cwd matches one)Auto-detects project from cwd path match.
10ao spawn #123 (multi-project, no cwd match)Error listing available projects.
11ao 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.
12ao spawn (no args, single project)Auto-detect project, no issue — spawns bare session.
13ao batch-spawn #1 #2 #3Auto-detect project, spawn 3 sessions with duplicate detection.
14ao batch-spawn #1 #2 #3 (multi-project, AO_PROJECT_ID set)Env var resolves project. Spawn 3 sessions.
15ao config-helpFull annotated YAML schema printed to stdout.
16Agent session calls ao spawn #123 with AO_PROJECT_ID setEnv var resolves project correctly without cwd match.
17Multiple agents installed, human runs ao startInteractive picker with displayName labels shown.
18Multiple agents installed, non-TTY ao startClaude Code auto-selected as preferred default.

11. Design Decisions

priority field removed from spawn. Spawning inherits priority from the project config. Per-spawn priority added API surface with no demonstrated use case — callers can override via config if needed.
Config instruction NOT injected into orchestrator prompt. The orchestrator system prompt focuses on session management and coordination. The full config schema (100+ lines of annotated YAML) would bloat the prompt. Instead, the orchestrator can run ao config-help on demand when it needs to modify configuration.
dist-server architecture kept unchanged. The server, session manager, and workspace plugins are out of scope for this PR. The redesign only touches CLI entry points and the thin coordination layer between CLI and core.
add-project fully deleted, not deprecated. Unlike 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.
Env vars set in session-manager.ts (core), not spawn.ts (cli). All three spawn points (orchestrator session, 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.

12. Review Findings & Fixes

CategoryFindingResolution
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.