True Two-Step Setup Fixes

PR #638 Issues: #619, #624 Status: Open Date: 2026-03-24

1. Problem Statement

The target onboarding experience for Agent Orchestrator is truly two commands from a fresh machine:

npm install -g @composio/ao && ao start

This does not work. Multiple bugs cause ao start to fail after a global npm install. Beyond the bugs, there is an architectural issue: ao start hardcodes runtime: "tmux" in the generated config, making tmux a hidden prerequisite that breaks the "two commands" promise.

The failure chain on a fresh machine:

  1. "Dependencies not installed"checkBuilt() hardcoded path fails for npm hoisted layout
  2. "posix_spawnp: Operation not permitted" — node-pty spawn-helper missing execute bit
  3. tmux is a hidden prerequisite — config always sets runtime: "tmux", orchestrator creation fails hard if tmux is missing, kills the dashboard
  4. Terminal server crash is permanent — no auto-restart after crash

Two-Command Setup Workflow

npm install -g @composio/ao
postinstall.js runs
find node-pty via walk-up resolver
chmod spawn-helper (+x)
non-fatal on failure
Install complete
ao start
normal | URL | existing config
load/create config
defaults.runtime = "tmux"
runStartup()
tmux -V — installed?
YES
continue ↓
NO
askYesNo("Install tmux?")
YES
tryInstallWithAttempts()
logs each cmd before running
verify tmux -V
OK
continue ↓
FAIL
print hints
exit 1
NO
print platform install hints
exit 1
preflight.checkBuilt()
findPackageUp(@composio/ao-core)
start dashboard
Next.js + terminal servers (auto-restart)
create orchestrator session
open dashboard URL
SUCCESS

2. Bug Analysis

#BugSeverityIssueAffects
1 checkBuilt() hardcoded single-level path P0 #619 All npm/yarn global installs
2 node-pty spawn-helper missing execute bit P0 #624 macOS + Linux npm installs
3 tmux is a hidden prerequisite — hardcoded runtime: "tmux", no auto-install, no fallback P0 Any fresh machine without tmux
4 Terminal server crash is permanent (no restart), bookkeeping bug on restart P1 Related to #489 linux-arm64 (AWS Graviton)

3. Fix 1 — checkBuilt() Walk-Up Resolution

Root Cause

preflight.ts verifies @composio/ao-core exists using a single hardcoded path:

// BEFORE: only checks one level deep
const nodeModules = resolve(webDir, "node_modules", "@composio", "ao-core");

npm hoists dependencies to a parent node_modules/. The check always fails for global installs because ao-core lives one or more levels up from ao-web.

pnpm layout (works)

packages/web/
  node_modules/
    @composio/ao-core -> ../../core

npm global layout (broken)

/usr/lib/node_modules/@composio/
  ao-web/          # webDir
  ao-core/         # sibling, not child

Fix

Replace with findPackageUp() — walks up directories checking node_modules/@composio/ao-core at each level, mirroring Node's own resolution:

function findPackageUp(startDir: string, ...segments: string[]): string | null {
  let dir = resolve(startDir);
  while (true) {
    const candidate = resolve(dir, "node_modules", ...segments);
    if (existsSync(candidate)) return candidate;
    const parent = dirname(dir);
    if (parent === dir) break;
    dir = parent;
  }
  return null;
}

Error messages distinguish install method:


4. Fix 2 — node-pty spawn-helper Postinstall

Root Cause

node-pty@1.1.0 ships spawn-helper without the execute bit. The monorepo's scripts/rebuild-node-pty.js fixes this via node-gyp rebuild, but it hardcodes pnpm paths and never runs for npm global installs. Upstream fix (microsoft/node-pty#866) is only in 1.2.0-beta.

Fix

New packages/ao/bin/postinstall.js runs after npm install -g @composio/ao. Uses the same findPackageUp pattern to locate node-pty, then chmod 0o755 on spawn-helper. Skips on Windows, silent on failure.

Temporary workaround. Remove when node-pty ships stable release with the upstream fix.

5. Fix 3 — Startup Preflight: tmux + Interactive Prerequisites

Root Cause

This was the biggest gap in the "two-step" promise. Three compounding issues:

On a fresh machine without tmux, the user saw:

$ ao start
✓ Config created: agent-orchestrator.yaml
⚠ tmux not found — install with: brew install tmux
...
✗ Failed to setup orchestrator: tmux: command not found

Fix: Centralized Startup Gate with User Consent

ao start
normal | URL | existing config
runStartup()
ensureTmux()
tmux -V — installed?
YES
continue startup
NO
askYesNo("Install?")
YES
tryInstallWithAttempts()
logs each cmd
verify?
OK
continue
FAIL
exit 1
NO
print hints
exit 1

Two-Layer Install Architecture

Install logic lives in two layers, each with a clear responsibility:

// start.ts — interactive install with user consent
async function ensureTmux(): Promise<void> {
  const hasTmux = (await execSilent("tmux", ["-V"])) !== null;
  if (hasTmux) return;

  console.log("⚠ tmux is required for runtime \"tmux\".");
  const shouldInstall = await askYesNo("Install tmux now?", true, false);
  if (shouldInstall) {
    const installed = await tryInstallWithAttempts(
      tmuxInstallAttempts(),
      async () => (await execSilent("tmux", ["-V"])) !== null,
    );
    if (installed) return; // ✓ tmux installed successfully
  }

  // Print platform-specific hints and exit
  for (const hint of tmuxInstallHints()) console.log(hint);
  process.exit(1);
}

All-Path Enforcement

start.ts centralizes tmux enforcement in runStartup(), so all start entry paths are covered (normal start, URL start, and retries with existing config):

async function runStartup(...) {
  const runtime = config.defaults?.runtime ?? "tmux";
  if (runtime === "tmux") {
    await ensureTmux();
  }
  // continue dashboard + orchestrator startup...
}

Interactive Prerequisite Prompts (TTY)

When ao start detects a missing prerequisite on a fresh machine, it prompts the user before attempting any install. This ensures no surprises — no silent sudo, no unexpected package manager invocations.

Prerequisite Matrix

Prerequisite Required? Prompt Install methods (by platform) On failure
tmux Yes (for runtime: "tmux") Install tmux now? [Y/n] macOS: brew install tmux
Linux: apt-getdnf
blocks — prints platform hints + exit 1
git Yes (for URL clone & add-project) Install Git now? [Y/n] macOS: brew install git
Linux: apt-getdnf
Windows: winget install Git.Git
blocks — prints platform hints + exit 1
Agent runtime
(Claude Code, Codex, Aider, OpenCode)
Yes (at least one) Numbered menu:
Choose runtime to install [1-5]
1. Claude Code (npm i -g @anthropic-ai/claude-code)
2. Codex (npm i -g @openai/codex)
3. Aider (pipx install aider-chat)
4. OpenCode (go install ...)
5. Skip
Config generation continues — user can install later
GitHub CLI (gh) No (optional) Install GitHub CLI now? [y/N]
(defaults to no)
macOS: brew install gh
Linux: apt-getdnf
Windows: winget install GitHub.cli
Skipped silently — optional dependency
Docker No Not prompted (no runtime-docker plugin registered)

Key Behaviors

Example: Fresh Machine Flow

$ ao start
  Detecting environment...
  ✓ Agent runtime: claude-code
  ✓ Config created: agent-orchestrator.yaml
  ⚠ tmux not found — will prompt to install at startup

  ⚠ tmux is required for runtime "tmux".
  Install tmux now? [Y/n]: y
    Running: brew install tmux
  ✓ tmux installed successfully

  Starting dashboard on port 3000...
  ✓ Dashboard: http://localhost:3000

If the user declines or install fails:

  Install tmux now? [Y/n]: n

  ✗ tmux is required but is not installed.

  Install tmux manually, then re-run ao start:

    brew install tmux          # macOS
    sudo apt install tmux      # Debian/Ubuntu
    sudo dnf install tmux      # Fedora/RHEL
We chose to block and show clear instructions rather than fall back to runtime: "process". The process runtime lacks session attach, terminal panel, and message sending — core AO features. A degraded first impression is worse than a one-line install command that gives the full experience.

6. Fix 4 — Terminal Server Crash Recovery

Root Cause

start-all.ts spawns three child processes. If a terminal server crashes, it logs the exit and never restarts.

Fix

Auto-restart logic with correct bookkeeping:

Bookkeeping: Slot-Based Child Tracking

The initial version had a bug: launch() always called children.push(child), creating duplicate entries on restart. The cleanup() function counts children.length and waits for exits, so stale handles would skew the alive count and trigger the 5-second force-shutdown timer.

Fix: each process gets a slot index assigned on first launch. Restarts replace in-place:

let slotIndex = -1;

function launch(): ChildProcess {
  const child = spawn(command, args, { ... });

  child.on("exit", (code) => {
    if (!shuttingDown && opts?.restart && code !== 0 && restarts < maxRestarts) {
      restarts++;
      const replacement = launch();
      children[slotIndex] = replacement; // replace in-place
    }
  });

  // Only push on first launch; restarts replace the existing slot
  if (slotIndex === -1) {
    slotIndex = children.length;
    children.push(child);
  }

  return child;
}

7. Files Changed


8. What Was Already Working

Investigated but not broken:

Suspected BlockerActual State
Port 3000 conflict Already fixedfindFreePort(port + 1) auto-increments up to 100 ports
node-pty crash kills dashboard Already handleddirect-terminal-ws.ts catches import failure, gracefully rejects connections
findWebDir() in global install Already handled — uses require.resolve() with proper walk-up
concurrently at runtime Already fixed — production uses node dist-server/start-all.js

9. Post-Merge: True Two-Step Matrix

After this PR, npm install -g @composio/ao && ao start:

Platformtmux Availabletmux Not Available
macOS (Intel + Apple Silicon) Works Interactive prompt + brew install
Clear error if brew missing or declined
Linux x64 Works Interactive prompt + apt/dnf install
Clear error if sudo unavailable or declined
Linux arm64 (Graviton) Works
Direct terminal unavailable (no node-pty prebuild)
Interactive prompt + apt/dnf install
Clear error if sudo unavailable or declined
Windows N/A Clear error
Guides user to WSL

Key improvement: Missing prerequisites are handled with explicit user consent in ao start. If install is declined or fails, the CLI prints one-line commands to run manually, then exits clearly.


10. Testing

CheckResult
TypeScript typecheckPasses (CLI + web server)
ESLintNo new errors on changed files
Preflight tests15/15 pass (expanded from 13)
CLI tests222/223 pass (1 = pre-existing flaky doctor-script.test.ts)
Local validation caveat: if node_modules contains root-owned artifacts (notably node-pty/build/* from earlier sudo installs), pnpm install can fail with EACCES on unlink/rebuild. This is an environment ownership issue, not a behavior regression in startup logic.

New/Updated Test Cases

TestWhat It Verifies
pnpm layout (direct match) findPackageUp finds ao-core in webDir/node_modules on first check
npm hoisted layout (walk-up) findPackageUp misses first level, finds ao-core one level up
npm hint on missing deps Global install path shows npm install -g hint
pnpm hint on missing deps Monorepo path shows pnpm install && pnpm build hint
Missing dist ao-core found but dist/index.js missing shows build hint
tmux interactive install success When tmux -V fails initially and user-approved install succeeds, ensureTmux() passes
tmux interactive install declined/failure When user declines or install attempts fail, prints platform-specific instructions and exits

11. Design Decisions

Prompted tmux install in ao start, then block with clear instructions on failure. We ask first, then try brew (macOS) and apt/dnf (Linux) only after consent. If install is declined or fails, we exit with a clear one-line install command rather than falling back to runtime: "process". The process runtime lacks session attach, terminal panel, and ao send — a degraded first impression is worse than asking the user to run one command.
Slot-based restart bookkeeping. Each child process gets a fixed slot index in the children array on first launch. Restarts replace in-place (children[slotIndex] = replacement) instead of pushing, preventing duplicate entries that would break the cleanup countdown.
Postinstall is temporary. The spawn-helper chmod workaround will be removed once node-pty ships a stable release with the upstream fix (microsoft/node-pty#866).