Reducing AO Onboarding Friction — CLI, npm & UX

Companion to: design-cli-redesign.html Author: Suraj + Claude Date: 2026-03-15 Last updated: 2026-03-15
Status: Draft — awaiting owner review. This doc covers the complete onboarding friction reduction: CLI consolidation, npm publishing & distribution, error hardening, auto-detection, and agent-friendly UX.

📋 Implementation Plan: Step-by-step implementation guide with exact code →

This document covers everything needed to reduce onboarding friction for Agent Orchestrator — from npm install to first agent spawn. It includes CLI consolidation, npm publishing, error handling, auto-detection, and UX design.

The goal — 2 steps max:
npm install -g @composio/ao
ao start <path-or-url>
That's it. No config files, no init commands, no port conflicts, no permission errors. Just works.

0. The Onboarding Journey — End to End

Every improvement in this doc serves one flow:

Target onboarding flow
npm install -g @composio/ao
npm fixes: package published, 67MB → 1MB, node-pty optional
ao start ~/my-project
CLI: auto-creates config, detects project type & agent runtime, finds free port
Dashboard running. Agent ready to spawn.
Adding a second project later:
ao start ~/other-repo
CLI: auto-adds to config, sidebar appears in same dashboard
Both projects managed. One dashboard.
Friction Point Before After Which section fixes it
Install friction npm install -g @composio/agent-orchestrator (40 chars) npm install -g @composio/ao (24 chars) + dashboard published, 1MB §15 npm Publishing
Must run ao init before ao start 13-prompt wizard, separate step ao start auto-creates config §4 Commands Absorbed
Adding a second project requires manual YAML editing Edit config by hand ao start ~/other-repo §4 Commands Absorbed
Port conflicts Fatal crash Auto-find next free port §15 npm Publishing
Permission errors (EACCES) Fatal crash Auto-retry with sudo §15 npm Publishing
Too many CLI commands to learn 16 commands, unclear which to use first ao start + ao stop for 99% of cases §4, §14 Interactive UX
Running ao start twice Starts duplicate dashboard Interactive: "Already running — restart or open?" §8 Already-Running
Agent doesn't know which runtime to use Must manually configure plugin Auto-detect installed runtimes §12 Agent Runtime Detection
Agent can't figure out config No documentation accessible at runtime ao config-help + auto-injected system prompt §13 System Prompt Injection
Unclear error messages Generic errors Install-specific guidance (npm vs source) §15 npm Publishing

1. Current CLI Structure (16 Commands)

All commands are registered in packages/cli/src/index.ts via individual register*(program) calls using Commander.js.

# Command File Lines Purpose
1 ao init commands/init.ts 481 Create agent-orchestrator.yaml from scratch (auto or interactive)
2 ao add-project commands/add-project.ts 148 Append a project to existing config
3 ao start [project] commands/start.ts 557 Start orchestrator + dashboard
4 ao stop commands/start.ts (shared) Stop orchestrator + dashboard
5 ao status commands/status.ts Show session statuses with PR/CI info
6 ao spawn commands/spawn.ts Create agent session for an issue
7 ao batch-spawn commands/spawn.ts Spawn multiple agents at once
8 ao session commands/session.ts ls / kill / restore / claim-pr subcommands
9 ao send commands/send.ts Send message to agent session
10 ao review-check commands/review-check.ts Check PRs for review comments
11 ao dashboard commands/dashboard.ts Start dashboard standalone
12 ao open commands/open.ts Open session in terminal tab
13 ao lifecycle-worker commands/lifecycle-worker.ts Internal background polling worker
14 ao verify commands/verify.ts Mark issues as verified post-merge
15 ao doctor commands/doctor.ts Health checks + optional fixes
16 ao update commands/update.ts Update installation + rebuild + smoke test

2. Current File Map & Line Counts

Command Files

commands/init.ts
Config creation — auto-detect environment, detect project type, generate rules, YAML output
481 lines · Absorb into start
commands/add-project.ts
Append project to existing config — detect git info, unique prefix, rules, YAML rewrite
148 lines · Absorb into start
commands/start.ts
Orchestrator lifecycle — URL clone, config load, project resolution, dashboard, lifecycle worker
557 lines · Core — heavy edit
commands/status.ts
Session status display with PR, CI, review info
Keep
commands/spawn.ts
Create agent sessions for issues
Keep
commands/session.ts
Session subcommands — ls, kill, restore, claim-pr
Keep
commands/send.ts
Send messages to agent sessions
Keep
commands/review-check.ts
PR review comment detection
Keep
commands/dashboard.ts
Standalone dashboard launch
Minor edit
commands/open.ts
Open session in terminal tab (iTerm)
Keep
commands/lifecycle-worker.ts
Internal background polling
Internal — no change
commands/verify.ts
Post-merge issue verification
Keep
commands/doctor.ts
Health checks (delegates to shell script)
Keep
commands/update.ts
Update installation (delegates to shell script)
Keep

Shared Library Files

lib/shell.ts
exec(), git(), gh(), tmux() wrappers
Keep
lib/web-dir.ts
findWebDir(), port utils, dashboard env
Keep
lib/preflight.ts
Pre-flight checks: port, build, tmux, gh auth
Keep
lib/project-detection.ts
Detect languages, frameworks, generate agent rules
Keep
lib/git-utils.ts
detectDefaultBranch() — 3-method fallback
Keep
lib/create-session-manager.ts
Factory for SessionManager & LifecycleManager
Keep
lib/format.ts
Console output helpers, banners, status icons
Keep
lib/session-utils.ts
Session name matching, prefix resolution
Keep
lib/plugins.ts
Agent and SCM plugin resolution
Keep
lib/lifecycle-service.ts
Lifecycle worker PID management
Keep
lib/dashboard-rebuild.ts
Clean .next cache, find running dashboard PID
Keep
lib/script-runner.ts
Delegate to shell scripts (doctor, update)
Keep

3. Overlap Analysis: init vs add-project vs start

These three commands share significant logic. The table below shows which capabilities exist in each:

Capability ao init ao add-project ao start (current)
Detect git repo & remote (URL mode)
Detect default branch (URL mode)
Detect project type (langs, frameworks)
Generate agent rules from templates
Generate session prefix
Create config YAML from scratch (URL mode)
Append project to existing config
Load existing config
Resolve which project to use
Start dashboard process
Start orchestrator session
Start lifecycle worker
Clone repo from URL
Interactive wizard prompts
Key insight: 6 of 14 capabilities are duplicated across 2+ commands. After absorbing init and add-project into start, every capability lives in exactly one place — ao start.

4. Commands to ABSORB into ao start

4.1   ao init → absorbed into ao start

Why: A user should never need to think "do I init or start?" Running ao start in a directory without a config should just create one and start. Init is only useful as a precondition to starting.
What moves into ao start
Logic BlockSource (init.ts)Destination (start.ts)How
detectEnvironment() Lines 45–102 Extracted to lib/detect-env.ts Shared helper — used by start's first-run path
handleAutoMode() Lines 353–475 Inlined into start's "no config found" branch Becomes the default first-run behavior when no agent-orchestrator.yaml exists
handleInteractiveMode() Lines 125–350 Removed — auto-detection handles first run without wizard Only on first run when user explicitly wants wizard. Optional — auto mode is default.
detectProjectType() Import from project-detection.ts No change Already a shared import — just add it to start.ts imports
generateRulesFromTemplates() Import from project-detection.ts No change Already a shared import
No flags transfer. Init's --output and --interactive are unnecessary — ao start auto-creates config and auto-detects everything.

What's dropped entirely

Note: add-project.ts does not currently exist on main branch. The logic described below may exist in a feature branch or PR. Verify which branch contains this code before implementation.

4.2   ao add-project <path> → absorbed into ao start <path>

Why: ao start ~/other-repo should detect that a config already exists, add the project if it's new, and start. There's no reason for a separate "add" step that doesn't start anything.
What moves into ao start
Logic BlockSource (add-project.ts)Destination (start.ts)How
Project-already-exists check Line 48 Start's path-resolution branch If project already in config → just start it (don't error)
Session prefix uniqueness Lines 74–87 Shared with init logic in start Reuse existing generateSessionPrefix() + collision check
Config append + YAML rewrite Lines 128–130 Start's "add new project" branch Load config, add project entry, write back, then start
Git info detection Lines 56–71 Already exists in start (URL mode) Reuse same detection — just apply it to path arg too
Project type detection Lines 90–108 Shared with init logic Same detectProjectType() + generateRulesFromTemplates()
No flags transfer. Add-project's --no-rules and --config are unnecessary — ao start auto-detects and auto-creates everything.

Key behavior change

Currently ao add-project errors if the project already exists. In the new ao start <path>, if the project is already in the config, it simply starts it — no error. This is the correct UX: "start this project" should always work.

4.3   ao start <url> — already exists, no change

URL handling (handleUrlStart(), resolveProjectByRepo()) stays exactly as-is. It already clones, auto-generates config if missing, and starts. The only addition: it will now also register in running.json.

5. New Functionality to ADD

5.1   running.json State File

New file lib/running-state.ts — ~80 lines

Why: Currently ao stop uses lsof to find PIDs by port — fragile and OS-dependent. There's no way to detect "already running" or show other active orchestrators. A state file solves both.
State file schema
// ~/.agent-orchestrator/running.json
[
  {
    "pid": 12345,
    "projectId": "my-app",
    "configPath": "/Users/me/my-app/agent-orchestrator.yaml",
    "port": 3000,
    "startedAt": "2026-03-15T10:30:00Z",
    // No identifier field needed — one instance per machine
  }
]
Exported functions
FunctionPurposeUsed by
register(entry) Add entry on start, auto-prunes dead PIDs first ao start
unregister(pid) Remove entry on stop ao stop
getRunning() List all entries, auto-prune dead PIDs ao stop, ao start, ao dashboard
isAlreadyRunning(configPath, projectId) Check before starting — returns entry or null ao start
pruneStale() Remove entries whose PID no longer exists (kill(pid, 0)) Internal — called by register/getRunning

5.2   Already-Running Detection in ao start

See §8 Already-Running Behavior — Deep Dive for full details, interactive menus, running.json mechanics, and edge cases.

5.3   ao stop Improvements

Edit commands/start.ts — ~30 changed lines

Why: Current stop is bare — no awareness of other running orchestrators, uses fragile lsof for PID discovery. Users don't know what's still running after stopping one.
Changes
CurrentNew
Find PID via lsof -t -i :PORT Look up PID from running.json, fallback to lsof
No awareness of other orchestrators After stopping: print list of other still-running orchestrators
No awareness of other projects Print which projects were being managed before stopping
Silent after stop Print confirmation: stopped project X on port Y (ran for Z)

5.4   ao --get-config-generation-instruction

New file lib/config-instruction.ts — ~60 lines

Why: The orchestrator agent needs to understand the config schema to help users modify their setup. Currently this knowledge is scattered and not machine-readable. Outputting it as a flag keeps the CLI self-documenting.
What it outputs
Where it's used
Implementation

Single function getConfigInstruction(): string that returns a formatted text block. Registered as a root-level flag on the Commander program in index.ts (not a subcommand).

5.5   CLI Context-Awareness via Environment Variables

New file lib/caller-context.ts — ~40 lines

Why: Commands behave differently depending on who calls them. Example: ao start should auto-open browser when called by a human, but NOT when called by the orchestrator agent. Currently there's no way to distinguish callers.
Environment variables
VariableValuesSet by
AO_CALLER_TYPE human | orchestrator | agent Inferred at start, explicit at spawn
AO_SESSION_ID e.g. myproj-42 Set when spawning agent sessions
AO_PROJECT_ID e.g. my-app Set at start and spawn
AO_CONFIG_PATH Absolute path to YAML Set at start
AO_PORT e.g. 3000 Set at start
AO_DATA_DIR e.g. ~/.agent-orchestrator Set at start
Behavior per caller type
Actionhumanorchestratoragent
Auto-open browser Yes No No
Print banners & color Yes Minimal No
Interactive prompts Yes (if TTY) Never Never
JSON output preference No Yes Yes
Exported functions
export function getCallerType(): "human" | "orchestrator" | "agent"
export function isHumanCaller(): boolean
export function setCallerContext(env: Record<string, string>, opts: CallerContextOpts): void

6. Commands that STAY Unchanged (10 Commands)

CommandWhy It StaysAny Minor Changes?
ao status Unique purpose — session monitoring with PR/CI/review data. No overlap with start/stop. None
ao spawn Core workflow — creates agent sessions for specific issues. Distinct from starting the orchestrator. Project arg becomes optional. Auto-resolves via AO_PROJECT_ID env var (set by ao start). Also sets AO_CALLER_TYPE on spawned sessions.
ao batch-spawn Bulk variant of spawn for multiple issues at once. Same env var injection as spawn
ao session Session management subcommands (ls, kill, restore, claim-pr) — operational tooling. None
ao send Message delivery to agent sessions — no overlap with lifecycle commands. None
ao review-check PR review automation — specialized, no overlap. None
ao dashboard Standalone dashboard launch (useful when orchestrator runs separately). Can read port from running.json if available
ao open Terminal integration (iTerm tabs) — UI shortcut, no overlap. None
ao verify Post-merge issue verification — distinct workflow step. None
ao doctor Diagnostics — essential for troubleshooting, delegates to shell script. None
ao update Self-update mechanism — essential for source installs, delegates to shell script. None
ao lifecycle-worker Internal — spawned by start, not user-facing. None

7. Deprecation Strategy

Principle: No breaking changes

Old commands continue to work but print a deprecation warning and delegate to the new code. This gives users and scripts time to migrate.

Old CommandNew EquivalentWrapper Behavior
ao init ao start (first-run auto-creates config) Print: ⚠ 'ao init' is deprecated. Use 'ao start' instead — it auto-creates config on first run.
Then call ao start --no-dashboard --no-orchestrator to create config without starting.
ao add-project <path> ao start <path> Print: ⚠ 'ao add-project' is deprecated. Use 'ao start <path>' instead.
Then call ao start <path> --no-dashboard --no-orchestrator to add without starting.

Implementation: Thin Wrappers (~15 lines each)

// commands/init.ts (after rewrite)
export function registerInit(program: Command): void {
  program
    .command("init")
    .description("[deprecated] Use 'ao start' instead")
    .action(async (opts) => {
      console.log(chalk.yellow("⚠ 'ao init' is deprecated. Use 'ao start' instead."));
      // Delegate to start's config-creation logic
      await createConfigOnly({ interactive: opts.interactive, output: opts.output });
    });
}

8. Already-Running Behavior — Deep Dive

When a user runs ao start for a project that is already running, the CLI must handle it gracefully instead of blindly starting a second instance.

8.1   How detection works

running.json lookup

Before starting anything, ao start reads ~/.agent-orchestrator/running.json and checks for a matching entry by configPath + projectId (and optionally identifier from --id).

// Pseudocode inside ao start, before any startup logic
const existing = findRunningEntry({
  configPath: resolvedConfigPath,
  projectId:  resolvedProjectId,
});

if (existing) {
  // Verify PID is actually alive (kill(pid, 0))
  if (isProcessAlive(existing.pid)) {
    printAlreadyRunning(existing);
    process.exit(0);
  } else {
    // Stale entry — clean it up and proceed
    unregister(existing.pid);
  }
}

8.2   Default behavior: interactive prompt (human) or inform-and-exit (agent)

If AO is already running for the same project, behavior depends on who is calling:

Human caller (TTY detected)

Instead of just printing info and exiting, the CLI presents an interactive prompt:

$ ao start

 AO is already running for this project.

  Project:    my-app
  URL:        http://localhost:3000
  PID:        12345
  Started:    2h 15m ago

? What would you like to do?
  ❯ Continue with the current one (open dashboard)
    Start a new orchestrator on the same project
    Override current config and restart
    Quit

No flags needed. The human just picks an option. This is the primary UX.

Agent caller (non-TTY / AO_CALLER_TYPE set)

Agents can't interact with prompts. They get a structured response and exit:

$ ao start

 AO is already running for this project.

  Project:    my-app
  URL:        http://localhost:3000
  PID:        12345
  Started:    2h 15m ago
  Config:     /Users/me/my-app/agent-orchestrator.yaml

  To restart: stop with 'ao stop', then 'ao start' again.

For agents, the structured output provides all the info needed to decide what to do next programmatically.

8.3   Restart — handled interactively

No --override flag. When the user selects "Restart" from the interactive menu, the CLI handles it automatically:
What happens when user picks "Restart"
  1. Find the existing entry in running.json
  2. Kill the existing process tree (dashboard + orchestrator + lifecycle worker)
  3. Unregister the old entry from running.json
  4. Proceed with normal startup (register new entry, start dashboard, etc.)
$ ao start

 Stopping existing instance (PID 12345, port 3000)...
 Stopped.

  Starting fresh...
  Dashboard:    http://localhost:3000
  Orchestrator: running

8.4   Multiple orchestrators on the same project — same instance

Design decision: Users can run multiple orchestrators on the same project, but everything stays within one AO instance, one port, one dashboard. No new ports, no separate dashboards. Internally, a new orchestrator is just another config entry with a unique session prefix pointing to the same repo path.

When user picks "Start a new orchestrator on the same project":

  1. CLI auto-generates a unique name (e.g. my-app-x7k2)
  2. Duplicates the project entry in the config with the new session prefix
  3. Spawns a new orchestrator session under the existing AO instance
  4. Dashboard sidebar shows both: my-app and my-app-x7k2
  5. Same port, same dashboard, same everything
$ ao start ~/my-app

 An orchestrator is already running on this project.
  Dashboard: http://localhost:3000
  Sessions:  3 active | Up: 2h

? What would you like to do?
  > Start a new orchestrator on the same project

 New orchestrator started on my-app
  ID: my-app-x7k2
  Dashboard: http://localhost:3000 (same dashboard)

  2 orchestrators now running on my-app:
    1. my-app      — 3 sessions
    2. my-app-x7k2 — 0 sessions (new)
Why this works without architecture changes: From AO’s perspective, a second orchestrator on the same project is just another config entry that happens to point to the same repo path. The dashboard already handles multiple entries. The sidebar already shows them. The only new code is generating a unique prefix and appending to the config — which the add-project logic already does.

If a user runs ao start ~/other-repo (a different project) while AO is already running:

  1. The new project is added to the existing config
  2. The dashboard sidebar shows both projects
  3. Same port, same instance, same dashboard

8.5   Config file resolution (auto-detected, no flag needed)

No --config flag. The CLI auto-detects the config file. If it doesn't exist, it creates one. If it exists, it uses it.
How config resolution works (priority order)
PrioritySourceExample
1 (highest) AO_CONFIG_PATH env var AO_CONFIG_PATH=~/prod.yaml ao start
2 (default) findConfigFile() auto-detection Searches cwd for agent-orchestrator.yaml or .yml
Impact on already-running detection

The configPath stored in running.json is always the resolved absolute path. This means:

8.6   running.json state

Single instance — one port, multiple projects
{
  "pid": 12345,
  "configPath": "/Users/me/my-app/agent-orchestrator.yaml",
  "port": 3000,
  "startedAt": "2026-03-15T10:30:00Z",
  "projects": ["my-app", "my-app-x7k2", "backend"]
}

One AO instance. One port. Multiple projects managed via the config file and shown in the dashboard sidebar. No separate instances, no separate ports.

8.7   Complete scenario matrix

User runsStateResult
ao start Nothing running, no config Auto-create config, start dashboard + orchestrator
ao start Nothing running, config exists Start normally, register in running.json
ao start Already running (human/TTY) Interactive menu: Open dashboard / Restart / Quit
ao start Already running (agent/non-TTY) Print URL + info, exit 0
ao start PID in running.json but process is dead Prune stale entry, start normally
ao start Already running, user picks "New orchestrator" Generate unique prefix, add as new config entry, spawn orchestrator session. Same port, same dashboard.
ao start Already running, user picks "Override & restart" Kill existing, remove config entry, start fresh
ao start ~/other-repo AO running with different project Add project to config, refresh dashboard sidebar
ao start ~/other-repo Project already in config Already managed — show dashboard URL
ao start <github-url> Nothing running Clone repo, auto-create config, start
ao stop AO running Stop everything — dashboard, orchestrator, lifecycle worker
ao stop Nothing running Print "nothing to stop"

8.8   Edge cases

Edge CaseHandling
running.json doesn't exist yet Create it on first register() call. getRunning() returns [].
running.json is corrupted / invalid JSON Log warning, treat as empty, overwrite on next register.
PID exists but is a different process (PID reuse) Check process name/command if possible. Fallback: trust the entry if PID is alive. User can select "Restart" from the interactive menu to force a fresh start.
Machine rebooted (all PIDs stale) pruneStale() runs on every getRunning() call — auto-cleans all dead entries.
User kills process with kill -9 manually Entry stays in running.json but next ao start prunes it (PID dead) and starts normally.
Two ao start commands run simultaneously (race) File-level write lock via lockfile or atomic write. Second caller retries once after 500ms.
~/.agent-orchestrator/ directory doesn't exist Create it with mkdirSync(recursive: true) on first write.

9. New ao start Decision Tree

After absorbing init and add-project, ao start handles every entry scenario:

ao start [path or URL]
ao start [arg]
Is arg a URL?
YES
Clone repo → detect/create config
(existing handleUrlStart logic)
continue to "Check running.json"
NO
Is arg a path?
YES (path)
Config exists?
(findConfigFile auto-detection)
YES
Project in config?
YES
Use existing project
NO
Add project to config
(absorbed add-project logic)
NO
Auto-create config
(absorbed init logic)
NO (project ID or empty)
Config exists in cwd?
YES
Resolve project
(existing logic)
NO
Auto-create config for cwd
(absorbed init logic)
Check running.json: already running?
YES (human: show interactive menu)
Print info & exit
"Already running on port 3000 (PID 12345)"
User picks "Restart"
Kill existing → unregister
continue
NO
continue
Register in running.json
Start dashboard + lifecycle worker + orchestrator
(existing runStartup logic)
Running!   Open browser (if human caller)

10. File Changes Summary

FileActionLines (est.)Details
lib/running-state.ts Create ~80 running.json state management — register, unregister, query, prune
lib/caller-context.ts Create ~40 Caller type detection, env var setters/getters
lib/config-instruction.ts Create ~60 Config schema documentation output for --get-config-generation-instruction
commands/start.ts Heavy edit 557 → ~700 Add: first-run config creation (from init), path-based project addition (from add-project), already-running check with interactive menu, running.json registration, caller context env injection
Refactor: Extract detectEnvironment() if inlining makes start.ts too large
commands/init.ts Rewrite → wrapper 481 → ~15 Deprecation warning + delegate to start's config-creation logic
commands/add-project.ts Rewrite → wrapper 148 → ~15 Deprecation warning + delegate to start's add-project logic
index.ts Minor edit 43 → ~55 Add --get-config-generation-instruction flag on root program. Registrations stay (wrappers still registered).
stop (in start.ts) Edit +30 Use running.json for PID lookup, print confirmation with project details after stop

Net Impact

Before

  • 3 command files with overlapping logic
  • init.ts — 481 lines
  • add-project.ts — 148 lines
  • start.ts — 557 lines
  • Total: 1,186 lines
  • No running state tracking
  • No caller awareness
  • User must know 3 commands for first use

After

  • 1 core file + 2 thin wrappers + 3 new lib files
  • start.ts — ~700 lines (consolidated)
  • init.ts — ~15 lines (wrapper)
  • add-project.ts — ~15 lines (wrapper)
  • running-state.ts — ~80 lines
  • caller-context.ts — ~40 lines
  • config-instruction.ts — ~60 lines
  • Total: ~910 lines (23% fewer, zero duplication)
  • Full running state tracking
  • Caller-aware behavior
  • User only needs ao start

11. Before / After Comparison

First-time user journey

Before (3 steps)

$ cd ~/my-project
$ ao init          # step 1: create config
$ ao start         # step 2: start orchestrator

# Later, adding another project:
$ ao add-project ~/other-repo   # step 3
$ ao start other-repo           # step 4

User must know about init, start, AND add-project. Running ao start first gives a confusing "no config found" error.

After (1 step)

$ cd ~/my-project
$ ao start         # auto-creates config + starts

# Later, adding another project:
$ ao start ~/other-repo   # auto-adds + starts

One command handles everything. Zero prerequisite knowledge needed.

Already-running scenario

Before

$ ao start
# Starts a SECOND dashboard — two processes competing, confusion

After

$ ao start
# Already running on port 3000 (PID 12345)
# Started 2h ago. Dashboard: http://localhost:3000
# Select "Restart" from the menu to restart.
# Or: ao stop && ao start

Stopping with awareness

Before

$ ao stop
# Stopped. (silence — no idea if other
# orchestrators are still running)

After

$ ao stop
# ✓ Stopped AO on port 3000
#   Projects managed: my-app, backend
#   Uptime: 4h 12m
#
# Dashboard and all sessions terminated.

Full new ao start flag inventory

FlagSourcePurpose
--no-dashboard Existing Skip dashboard server
--no-orchestrator Existing Skip orchestrator agent
--rebuild Existing Clean and rebuild dashboard

Full new ao stop flag inventory

FlagSourcePurpose
--keep-session Existing Keep session after stopping
--purge-session Existing Delete mapped session when stopping

12. Smart Agent Runtime Detection

From owner: "If it's claude code it should decide to use claude code for the agent runtime. If it's open code, set open code." — The config auto-creation must intelligently detect which agent runtimes are available and select the right plugin.

12.1   Why not hardcode?

No hardcoded runtime lists. AO is a growing project with a plugin ecosystem. Today there are 6 agent runtimes (claude-code, codex, aider, opencode, cursor, gemini). Tomorrow there will be more. The detection system must discover runtimes dynamically — adding a new agent plugin should never require changing the CLI detection code.

12.2   The plugin registry approach

PR #474 introduces packages/core/src/plugin-registry.ts — a proper plugin registry that discovers plugins dynamically. We build on this, not duplicate it.

How plugins are discovered (3 sources):

  1. Built-inpackages/plugins/agent-* (shipped with AO)
  2. npm packages — any package named @composio/ao-plugin-agent-*
  3. Local paths — specified in config (for custom/private plugins)

12.3   Plugin interface: each agent declares its own detection

Every agent plugin exports a detect() method. The CLI never needs to know how to detect each runtime — the plugin handles it.

// Plugin interface (in @composio/ao-core types)
interface AgentPluginManifest {
  name: string;           // e.g. "claude-code"
  displayName: string;    // e.g. "Claude Code"
  priority: number;       // Higher = preferred. e.g. 100 for claude, 90 for codex
  detect(): boolean;      // Can this runtime be found on the system?
}

// Example: claude-code plugin exports
export default {
  name: "claude-code",
  displayName: "Claude Code",
  priority: 100,
  detect() {
    try { execSync("which claude", { stdio: "ignore" }); return true; }
    catch { return false; }
  },
  create() { /* ... existing plugin factory ... */ }
};

12.4   Detection flow in ao start

// In ao start, when auto-creating config:
async function detectAgentRuntime(registry: PluginRegistry): Promise<string> {
  // 1. Get ALL registered agent plugins from the registry
  const agentPlugins = registry.getAll("agent");

  // 2. Filter to those that are detected on this system
  const available = agentPlugins.filter(p => p.detect());

  // 3. Selection logic
  if (available.length === 0) {
    console.warn("No agent runtimes detected. Using claude-code as default.");
    return "claude-code";
  }

  if (available.length === 1) {
    return available[0].name;
  }

  // Multiple available
  if (isHumanCaller()) {
    // Prompt: "Multiple runtimes detected. Which one?"
    return await promptChoice(
      available.sort((a, b) => b.priority - a.priority)
               .map(p => ({ name: p.displayName, value: p.name }))
    );
  }

  // Agent caller: pick highest priority
  const selected = available.sort((a, b) => b.priority - a.priority)[0];
  console.log(`Auto-selected: ${selected.displayName} (highest priority)`);
  return selected.name;
}

12.5   What this enables

Scenario What happens CLI code change needed?
New agent plugin added (e.g. agent-gemini) Plugin exports detect() + priority. Registry discovers it. ao start picks it up automatically. None
Third-party plugin via npm User installs @composio/ao-plugin-agent-foo. Registry discovers it by naming convention. None
Custom local plugin User specifies path in config. Registry loads it. None
Plugin removed from system detect() returns false. Skipped in auto-detection. None
Priority changes Plugin updates its priority value. No CLI involvement. None
Zero hardcoding. Zero maintenance. The CLI asks the registry “what’s available?” and the plugins answer for themselves. New plugins are auto-discovered. No lists to update, no binaries to check, no code changes needed.

13. System Prompt Auto-Injection

From owner: "Auto injection should be the CLI's output directly. So this is like there is one place to look for everything." — The ao --get-config-generation-instruction output must be the single source of truth, used both on-demand and auto-injected.

13.1   Dual-use architecture

Use Case How It's Triggered Who Uses It
On-demand ao config-help or ao --get-config-generation-instruction Human or agent that wants to understand / modify the config
Auto-injected generateOrchestratorPrompt() in ao-core Orchestrator agent session — knows how to create/modify configs for sub-agents

13.2   Implementation details

Why this matters: The orchestrator agent can then help users configure AO properly — it knows the full schema, valid plugins, and best practices. And agents calling ao config-help at runtime can self-correct bad configs.

14. Interactive-First UX Design Principles

From owner: "I want as less commands as possible. By default keep the user experience very simple." and "Even the dumbest of agents should be able to use it effectively."

14.1   Core principle: Guide, don't require flags

For human callers (TTY detected), the CLI should always guide through interactive prompts rather than requiring flag knowledge. Flags exist as shortcuts for power users and as the API surface for agents.

Scenario Human (TTY) Agent (non-TTY)
First run, no config Auto-create config, show what was created, offer to customize Auto-create config, print JSON summary, continue
Already running Interactive menu: Continue / New orchestrator / Override & restart / Quit Print structured info + flag hints, exit 0
Multiple runtimes detected Prompt: "Which agent runtime?" Use heuristic, note choice in output
Stopping with others running Print list, ask "Stop all?" Stop default only, print remaining list
Config looks wrong Suggest ao config-help for guidance Output config-help text inline

14.2   Output format per caller type

14.3   The "dumbest agent" test

Every CLI command should pass this test: Can an agent with zero prior knowledge of AO figure out what to do next from the CLI output alone?

15. npm Publishing & Distribution

This is not separate from CLI work — it IS the onboarding. npm publishing is the front door. The CLI improvements only matter because they make the post-install experience seamless. Together they form one continuous flow: install → start → running.

15.1   Current state (from PR #463)

PR #463 already fixes the major npm publishing blockers:

Problem Before After (PR #463)
@composio/ao-web dashboard private: true — not published, npm users get fatal crash Published on npm with production entry point (start-all.js)
Package size 67 MB (includes .next/cache bloat) ~1 MB (optimized files field)
Dashboard startup npx next start (slow, downloads next) node_modules/.bin/next (instant, local binary)
node-pty dependency Required — crashes on install failure Optional — direct terminal gracefully degrades
Dev vs production detection Hardcoded to dev mode Auto-detects: source users get pnpm run dev, npm users get node dist-server/start-all.js
findWebDir() errors Generic "Run: pnpm install" Install-specific: npm users see npm install -g, source users see pnpm install && pnpm build
Permission errors (EACCES) Fatal crash Auto-retries with sudo when interactive

15.2   The install experience target

Before (6+ steps)

git clone ...
cd agent-orchestrator
pnpm install
pnpm build
npm link
ao init          # 13 prompts
ao start         # port crash
# manually edit YAML...

After (3 steps via npm)

npm install -g @composio/ao
ao start ~/my-project   # or ao start https://github.com/org/repo

2 steps. That's it.

Config auto-created. Dashboard starts. Zero prompts.

15.3   How this connects to CLI redesign

The CLI consolidation (absorbing init and add-project into start) is what makes the 3-step npm experience possible:

15.4   Package structure

Package npm Name Purpose Install
CLI @composio/ao renamed Main entry point — ao binary. Short name for frictionless install. npm install -g @composio/ao
Core @composio/ao-core Config loading, session management, lifecycle Dependency of CLI
Web/Dashboard @composio/ao-web Next.js dashboard + direct terminal Dependency of CLI
Plugins @composio/ao-plugin-* Agent runtime plugins (claude-code, codex, opencode, gemini) Dependencies of CLI

15.5   Remaining considerations

Bottom line: The npm publish from PR #463 is what turns AO from a "clone and build" project into a "just install and run" product. Every CLI design decision should optimize for this install experience.

16. Simplifying ao spawn

Current problem: ao spawn <project> <issue> requires the project name as the first argument. But if you’re running this from an orchestrator terminal, the project context is already known. Requiring it is unnecessary friction.

16.1   Current vs new

Current

ao spawn my-app #42
ao spawn my-app INT-1234

Project name always required. User must remember the exact config key.

After

ao spawn #42
ao spawn INT-1234

Project auto-detected from AO_PROJECT_ID. Just pass the issue.

16.2   How auto-detection works

Simple — no fancy heuristics needed:

  1. AO_PROJECT_ID env var is set → use it. This covers 99% of cases because ao start sets this variable and every child process (orchestrator, agent sessions) inherits it.
  2. Not set, but config has only one project → use it. No ambiguity.
  3. Not set, multiple projects → error: “Multiple projects in config. Run from an orchestrator session, or specify: ao spawn <project> <issue>

16.3   Command signature change

// CURRENT in spawn.ts
.argument("<project>", "Project ID from config")
.argument("[issue]", "Issue identifier")

// NEW — project becomes optional
.argument("<issue>", "Issue identifier (e.g. #42, INT-1234)")
.argument("[project]", "Project ID — auto-detected if omitted")
Backward compatibility: If someone passes two arguments, treat the first as project and second as issue (old behavior). If one argument, treat it as issue and auto-detect project. This ensures existing scripts don’t break.

16.4   Implementation

// In registerSpawn action handler:
function resolveProjectForSpawn(
  config: OrchestratorConfig,
  arg1: string,
  arg2?: string
): { projectId: string; issueId: string } {

  // Two args: explicit project + issue (backward compat)
  if (arg2) {
    return { projectId: arg1, issueId: arg2 };
  }

  // One arg: auto-detect project, arg1 is the issue
  const issueId = arg1;

  // 1. Check AO_PROJECT_ID env var
  if (process.env.AO_PROJECT_ID) {
    return { projectId: process.env.AO_PROJECT_ID, issueId };
  }

  // 2. Single project in config
  const projectIds = Object.keys(config.projects);
  if (projectIds.length === 1) {
    return { projectId: projectIds[0], issueId };
  }

  // 3. Ambiguous — error
  throw new Error(
    `Multiple projects in config: ${projectIds.join(", ")}
` +
    `Run from an orchestrator session, or specify:
` +
    `  ao spawn <project> <issue>`
  );
}

17. Open Questions for Review

Items that need owner input before implementation:

# Question Options Recommendation
0 Rename npm package from @composio/agent-orchestrator to @composio/ao? Rename (shorter) vs keep (established) Rename — @composio/ao is available. Old package becomes a deprecated redirect. Every character counts in install friction.
1 Should ao config-help be a subcommand or keep the long flag --get-config-generation-instruction? Subcommand (shorter) vs flag (as discussed) Both — ao config-help as alias, long flag kept for backward compat
2 What default priority values should each agent plugin ship with? Each plugin sets its own priority number. Higher = preferred when multiple are detected. claude-code: 100, codex: 90, aider: 80, opencode: 70, gemini: 60, cursor: 50 (can be overridden per-plugin)
3 Should the --smart flag be removed immediately or deprecated first? Hard remove vs deprecation wrapper Hard remove — owner confirmed nobody uses it
4 Should running.json live in ~/.agent-orchestrator/ or alongside the project config? Global state dir vs project-local Global (~/.agent-orchestrator/running.json) — enables cross-project awareness
5 For the interactive "already running" menu, should "Open dashboard" auto-open browser or just print the URL? Auto-open vs print Auto-open (human chose the action explicitly)