1066 lines
37 KiB
HTML
1066 lines
37 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Design: True Two-Step Setup Fixes — Agent Orchestrator</title>
|
|
<style>
|
|
:root {
|
|
--bg: #0d1117;
|
|
--surface: #161b22;
|
|
--border: #30363d;
|
|
--text: #e6edf3;
|
|
--text-muted: #8b949e;
|
|
--accent: #58a6ff;
|
|
--green: #3fb950;
|
|
--red: #f85149;
|
|
--yellow: #d29922;
|
|
--orange: #db6d28;
|
|
--code-bg: #1c2128;
|
|
}
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
line-height: 1.6;
|
|
padding: 40px 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 2rem;
|
|
margin-bottom: 8px;
|
|
border-bottom: 1px solid var(--border);
|
|
padding-bottom: 16px;
|
|
}
|
|
|
|
.meta {
|
|
color: var(--text-muted);
|
|
font-size: 0.9rem;
|
|
margin-bottom: 32px;
|
|
}
|
|
|
|
.meta span {
|
|
margin-right: 24px;
|
|
}
|
|
|
|
h2 {
|
|
font-size: 1.4rem;
|
|
margin-top: 40px;
|
|
margin-bottom: 16px;
|
|
color: var(--accent);
|
|
}
|
|
|
|
h3 {
|
|
font-size: 1.1rem;
|
|
margin-top: 28px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
p {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
ul, ol {
|
|
margin: 8px 0 16px 24px;
|
|
}
|
|
|
|
li {
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin: 16px 0 24px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
th {
|
|
background: var(--surface);
|
|
text-align: left;
|
|
padding: 10px 14px;
|
|
border: 1px solid var(--border);
|
|
font-weight: 600;
|
|
}
|
|
|
|
td {
|
|
padding: 10px 14px;
|
|
border: 1px solid var(--border);
|
|
vertical-align: top;
|
|
}
|
|
|
|
tr:nth-child(even) td {
|
|
background: rgba(22, 27, 34, 0.5);
|
|
}
|
|
|
|
code {
|
|
background: var(--code-bg);
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-size: 0.85em;
|
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
|
}
|
|
|
|
pre {
|
|
background: var(--code-bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
overflow-x: auto;
|
|
margin: 12px 0 20px;
|
|
font-size: 0.85rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
pre code {
|
|
background: none;
|
|
padding: 0;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 12px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.badge-p0 { background: rgba(248, 81, 73, 0.2); color: var(--red); }
|
|
.badge-p1 { background: rgba(210, 153, 34, 0.2); color: var(--yellow); }
|
|
.badge-p2 { background: rgba(219, 109, 40, 0.2); color: var(--orange); }
|
|
.badge-fixed { background: rgba(63, 185, 80, 0.2); color: var(--green); }
|
|
|
|
.before-after {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 16px;
|
|
margin: 16px 0 24px;
|
|
}
|
|
|
|
.before-after > div {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.before-after h4 {
|
|
font-size: 0.8rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.before-after .before h4 { color: var(--red); }
|
|
.before-after .after h4 { color: var(--green); }
|
|
|
|
.callout {
|
|
background: var(--surface);
|
|
border-left: 3px solid var(--accent);
|
|
border-radius: 0 8px 8px 0;
|
|
padding: 14px 18px;
|
|
margin: 16px 0 20px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.callout-warning {
|
|
border-left-color: var(--yellow);
|
|
}
|
|
|
|
.callout-warning::before {
|
|
content: "Note: ";
|
|
color: var(--yellow);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.callout-decision {
|
|
border-left-color: var(--green);
|
|
}
|
|
|
|
.callout-decision::before {
|
|
content: "Decision: ";
|
|
color: var(--green);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.files-changed {
|
|
list-style: none;
|
|
padding: 0;
|
|
}
|
|
|
|
.files-changed li {
|
|
color: var(--text-muted);
|
|
font-size: 0.85rem;
|
|
padding: 2px 0;
|
|
}
|
|
|
|
.files-changed li::before {
|
|
content: "~";
|
|
color: var(--yellow);
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.files-changed li.new::before {
|
|
content: "+";
|
|
color: var(--green);
|
|
}
|
|
|
|
.section-divider {
|
|
border: none;
|
|
border-top: 1px solid var(--border);
|
|
margin: 40px 0;
|
|
}
|
|
|
|
.flow-diagram {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
margin: 16px 0 24px;
|
|
font-family: 'SFMono-Regular', Consolas, monospace;
|
|
font-size: 0.85rem;
|
|
line-height: 1.8;
|
|
white-space: pre;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
/* Visual flowchart styles */
|
|
.flowchart {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0;
|
|
margin: 24px 0;
|
|
padding: 24px 16px;
|
|
}
|
|
|
|
.flow-node {
|
|
padding: 10px 20px;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
font-size: 0.85rem;
|
|
font-family: 'SFMono-Regular', Consolas, monospace;
|
|
max-width: 420px;
|
|
width: 100%;
|
|
}
|
|
|
|
.flow-cmd {
|
|
background: rgba(88, 166, 255, 0.15);
|
|
border: 2px solid var(--accent);
|
|
color: var(--accent);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.flow-action {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
}
|
|
|
|
.flow-decision {
|
|
background: rgba(210, 153, 34, 0.1);
|
|
border: 2px solid var(--yellow);
|
|
color: var(--yellow);
|
|
transform: rotate(0deg);
|
|
padding: 8px 16px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.flow-success {
|
|
background: rgba(63, 185, 80, 0.15);
|
|
border: 2px solid var(--green);
|
|
color: var(--green);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.flow-fail {
|
|
background: rgba(248, 81, 73, 0.12);
|
|
border: 2px solid var(--red);
|
|
color: var(--red);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.flow-arrow {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
color: var(--text-muted);
|
|
font-size: 0.75rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.flow-arrow::before {
|
|
content: "│";
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.flow-arrow::after {
|
|
content: "▼";
|
|
font-size: 0.7rem;
|
|
margin-top: -2px;
|
|
}
|
|
|
|
.flow-arrow-label {
|
|
color: var(--text-muted);
|
|
font-size: 0.75rem;
|
|
font-style: italic;
|
|
margin: 2px 0;
|
|
}
|
|
|
|
.flow-branch {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 24px;
|
|
width: 100%;
|
|
max-width: 500px;
|
|
}
|
|
|
|
.flow-branch-col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0;
|
|
}
|
|
|
|
.flow-branch-label {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
padding: 4px 10px;
|
|
border-radius: 10px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.flow-branch-yes { background: rgba(63, 185, 80, 0.15); color: var(--green); }
|
|
.flow-branch-no { background: rgba(248, 81, 73, 0.12); color: var(--red); }
|
|
|
|
.flow-section-label {
|
|
color: var(--text-muted);
|
|
font-size: 0.7rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
margin-bottom: 4px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
|
|
<h1>True Two-Step Setup Fixes</h1>
|
|
<div class="meta">
|
|
<span>PR #638</span>
|
|
<span>Issues: #619, #624</span>
|
|
<span>Status: Open</span>
|
|
<span>Date: 2026-03-24</span>
|
|
</div>
|
|
|
|
<!-- ================================================================ -->
|
|
<h2>1. Problem Statement</h2>
|
|
|
|
<p>
|
|
The target onboarding experience for Agent Orchestrator is <strong>truly two commands
|
|
from a fresh machine</strong>:
|
|
</p>
|
|
|
|
<pre><code>npm install -g @composio/ao && ao start</code></pre>
|
|
|
|
<p>
|
|
<strong>This does not work.</strong> Multiple bugs cause <code>ao start</code> to fail after
|
|
a global npm install. Beyond the bugs, there is an architectural issue: <code>ao start</code>
|
|
hardcodes <code>runtime: "tmux"</code> in the generated config, making tmux a hidden
|
|
prerequisite that breaks the "two commands" promise.
|
|
</p>
|
|
|
|
<p>The failure chain on a fresh machine:</p>
|
|
|
|
<ol>
|
|
<li><strong>"Dependencies not installed"</strong> — <code>checkBuilt()</code> hardcoded path fails for npm hoisted layout</li>
|
|
<li><strong>"posix_spawnp: Operation not permitted"</strong> — node-pty spawn-helper missing execute bit</li>
|
|
<li><strong>tmux is a hidden prerequisite</strong> — config always sets <code>runtime: "tmux"</code>, orchestrator creation fails hard if tmux is missing, kills the dashboard</li>
|
|
<li><strong>Terminal server crash is permanent</strong> — no auto-restart after crash</li>
|
|
</ol>
|
|
|
|
<h3>Two-Command Setup Workflow</h3>
|
|
|
|
<!-- Command 1 -->
|
|
<div class="flowchart">
|
|
<div class="flow-section-label">COMMAND 1</div>
|
|
<div class="flow-node flow-cmd">npm install -g @composio/ao</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action">postinstall.js runs</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action">find node-pty via walk-up resolver</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action">chmod spawn-helper (+x)<br><small style="color:var(--text-muted)">non-fatal on failure</small></div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-success">Install complete</div>
|
|
</div>
|
|
|
|
<!-- Command 2 -->
|
|
<div class="flowchart">
|
|
<div class="flow-section-label">COMMAND 2</div>
|
|
<div class="flow-node flow-cmd">ao start<br><small style="color:var(--text-muted)">normal | URL | existing config</small></div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action">load/create config<br><small style="color:var(--text-muted)">defaults.runtime = "tmux"</small></div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action">runStartup()</div>
|
|
<div class="flow-arrow"></div>
|
|
|
|
<!-- tmux check -->
|
|
<div class="flow-node flow-decision">tmux -V — installed?</div>
|
|
<div class="flow-branch">
|
|
<div class="flow-branch-col">
|
|
<div class="flow-branch-label flow-branch-yes">YES</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-success" style="font-size:0.8rem">continue ↓</div>
|
|
</div>
|
|
<div class="flow-branch-col">
|
|
<div class="flow-branch-label flow-branch-no">NO</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-decision" style="font-size:0.8rem">askYesNo("Install tmux?")</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- askYesNo branch -->
|
|
<div style="width:100%;max-width:500px">
|
|
<div class="flow-branch" style="margin-left:50%">
|
|
<div class="flow-branch-col">
|
|
<div class="flow-branch-label flow-branch-yes">YES</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action" style="font-size:0.8rem">tryInstallWithAttempts()<br><small style="color:var(--text-muted)">logs each cmd before running</small></div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-decision" style="font-size:0.8rem">verify tmux -V</div>
|
|
<div class="flow-branch" style="max-width:100%">
|
|
<div class="flow-branch-col">
|
|
<div class="flow-branch-label flow-branch-yes" style="font-size:0.65rem">OK</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-success" style="font-size:0.75rem">continue ↓</div>
|
|
</div>
|
|
<div class="flow-branch-col">
|
|
<div class="flow-branch-label flow-branch-no" style="font-size:0.65rem">FAIL</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-fail" style="font-size:0.75rem">print hints<br>exit 1</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flow-branch-col">
|
|
<div class="flow-branch-label flow-branch-no">NO</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-fail" style="font-size:0.8rem">print platform install hints<br>exit 1</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top:16px"></div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action">preflight.checkBuilt()<br><small style="color:var(--text-muted)">findPackageUp(@composio/ao-core)</small></div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action">start dashboard<br><small style="color:var(--text-muted)">Next.js + terminal servers (auto-restart)</small></div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action">create orchestrator session</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action">open dashboard URL</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-success">SUCCESS</div>
|
|
</div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- ================================================================ -->
|
|
<h2>2. Bug Analysis</h2>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>#</th><th>Bug</th><th>Severity</th><th>Issue</th><th>Affects</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>1</td>
|
|
<td><code>checkBuilt()</code> hardcoded single-level path</td>
|
|
<td><span class="badge badge-p0">P0</span></td>
|
|
<td><a href="https://github.com/ComposioHQ/agent-orchestrator/issues/619">#619</a></td>
|
|
<td>All npm/yarn global installs</td>
|
|
</tr>
|
|
<tr>
|
|
<td>2</td>
|
|
<td>node-pty <code>spawn-helper</code> missing execute bit</td>
|
|
<td><span class="badge badge-p0">P0</span></td>
|
|
<td><a href="https://github.com/ComposioHQ/agent-orchestrator/issues/624">#624</a></td>
|
|
<td>macOS + Linux npm installs</td>
|
|
</tr>
|
|
<tr>
|
|
<td>3</td>
|
|
<td>tmux is a hidden prerequisite — hardcoded <code>runtime: "tmux"</code>, no auto-install, no fallback</td>
|
|
<td><span class="badge badge-p0">P0</span></td>
|
|
<td>—</td>
|
|
<td>Any fresh machine without tmux</td>
|
|
</tr>
|
|
<tr>
|
|
<td>4</td>
|
|
<td>Terminal server crash is permanent (no restart), bookkeeping bug on restart</td>
|
|
<td><span class="badge badge-p1">P1</span></td>
|
|
<td>Related to <a href="https://github.com/ComposioHQ/agent-orchestrator/issues/489">#489</a></td>
|
|
<td>linux-arm64 (AWS Graviton)</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- ================================================================ -->
|
|
<h2>3. Fix 1 — checkBuilt() Walk-Up Resolution</h2>
|
|
|
|
<h3>Root Cause</h3>
|
|
<p>
|
|
<code>preflight.ts</code> verifies <code>@composio/ao-core</code> exists using a single hardcoded path:
|
|
</p>
|
|
|
|
<pre><code>// BEFORE: only checks one level deep
|
|
const nodeModules = resolve(webDir, "node_modules", "@composio", "ao-core");</code></pre>
|
|
|
|
<p>
|
|
npm <strong>hoists</strong> dependencies to a parent <code>node_modules/</code>.
|
|
The check always fails for global installs because <code>ao-core</code> lives one or more
|
|
levels up from <code>ao-web</code>.
|
|
</p>
|
|
|
|
<div class="before-after">
|
|
<div class="before">
|
|
<h4>pnpm layout (works)</h4>
|
|
<pre><code>packages/web/
|
|
node_modules/
|
|
@composio/ao-core -> ../../core</code></pre>
|
|
</div>
|
|
<div class="after">
|
|
<h4>npm global layout (broken)</h4>
|
|
<pre><code>/usr/lib/node_modules/@composio/
|
|
ao-web/ # webDir
|
|
ao-core/ # sibling, not child</code></pre>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Fix</h3>
|
|
<p>
|
|
Replace with <code>findPackageUp()</code> — walks up directories checking
|
|
<code>node_modules/@composio/ao-core</code> at each level, mirroring Node's own resolution:
|
|
</p>
|
|
|
|
<pre><code>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;
|
|
}</code></pre>
|
|
|
|
<p>Error messages distinguish install method:</p>
|
|
<ul>
|
|
<li>Path contains <code>node_modules</code> → <code>"npm install -g @composio/ao@latest"</code></li>
|
|
<li>Otherwise → <code>"pnpm install && pnpm build"</code></li>
|
|
</ul>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- ================================================================ -->
|
|
<h2>4. Fix 2 — node-pty spawn-helper Postinstall</h2>
|
|
|
|
<h3>Root Cause</h3>
|
|
<p>
|
|
<code>node-pty@1.1.0</code> ships <code>spawn-helper</code> without the execute bit.
|
|
The monorepo's <code>scripts/rebuild-node-pty.js</code> fixes this via <code>node-gyp rebuild</code>,
|
|
but it hardcodes pnpm paths and never runs for npm global installs.
|
|
Upstream fix (<a href="https://github.com/microsoft/node-pty/pull/866">microsoft/node-pty#866</a>)
|
|
is only in <code>1.2.0-beta</code>.
|
|
</p>
|
|
|
|
<h3>Fix</h3>
|
|
<p>
|
|
New <code>packages/ao/bin/postinstall.js</code> runs after <code>npm install -g @composio/ao</code>.
|
|
Uses the same <code>findPackageUp</code> pattern to locate node-pty, then <code>chmod 0o755</code>
|
|
on <code>spawn-helper</code>. Skips on Windows, silent on failure.
|
|
</p>
|
|
|
|
<div class="callout callout-warning">
|
|
Temporary workaround. Remove when <code>node-pty</code> ships stable release with the upstream fix.
|
|
</div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- ================================================================ -->
|
|
<h2>5. Fix 3 — Startup Preflight: tmux + Interactive Prerequisites</h2>
|
|
|
|
<h3>Root Cause</h3>
|
|
<p>
|
|
This was the biggest gap in the "two-step" promise. Three compounding issues:
|
|
</p>
|
|
<ul>
|
|
<li>Config generation hardcodes <code>runtime: "tmux"</code></li>
|
|
<li>Orchestrator creation fails hard if tmux runtime fails, then kills the dashboard</li>
|
|
<li>tmux checks were not enforced consistently across all <code>ao start</code> entry paths (URL start and existing-config retries could bypass friendly remediation)</li>
|
|
</ul>
|
|
|
|
<p>On a fresh machine without tmux, the user saw:</p>
|
|
<pre><code>$ ao start
|
|
✓ Config created: agent-orchestrator.yaml
|
|
⚠ tmux not found — install with: brew install tmux
|
|
...
|
|
✗ Failed to setup orchestrator: tmux: command not found</code></pre>
|
|
|
|
<h3>Fix: Centralized Startup Gate with User Consent</h3>
|
|
|
|
<div class="flowchart">
|
|
<div class="flow-node flow-cmd">ao start<br><small style="color:var(--text-muted)">normal | URL | existing config</small></div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action">runStartup()</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action">ensureTmux()</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-decision">tmux -V — installed?</div>
|
|
<div class="flow-branch">
|
|
<div class="flow-branch-col">
|
|
<div class="flow-branch-label flow-branch-yes">YES</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-success" style="font-size:0.8rem">continue startup</div>
|
|
</div>
|
|
<div class="flow-branch-col">
|
|
<div class="flow-branch-label flow-branch-no">NO</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-decision" style="font-size:0.8rem">askYesNo("Install?")</div>
|
|
<div class="flow-branch" style="max-width:100%">
|
|
<div class="flow-branch-col">
|
|
<div class="flow-branch-label flow-branch-yes" style="font-size:0.65rem">YES</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-action" style="font-size:0.75rem">tryInstallWithAttempts()<br><small>logs each cmd</small></div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-decision" style="font-size:0.75rem">verify?</div>
|
|
<div class="flow-branch" style="max-width:100%">
|
|
<div class="flow-branch-col">
|
|
<div class="flow-branch-label flow-branch-yes" style="font-size:0.6rem">OK</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-success" style="font-size:0.7rem;padding:6px">continue</div>
|
|
</div>
|
|
<div class="flow-branch-col">
|
|
<div class="flow-branch-label flow-branch-no" style="font-size:0.6rem">FAIL</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-fail" style="font-size:0.7rem;padding:6px">exit 1</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flow-branch-col">
|
|
<div class="flow-branch-label flow-branch-no" style="font-size:0.65rem">NO</div>
|
|
<div class="flow-arrow"></div>
|
|
<div class="flow-node flow-fail" style="font-size:0.75rem">print hints<br>exit 1</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3>Two-Layer Install Architecture</h3>
|
|
<p>
|
|
Install logic lives in two layers, each with a clear responsibility:
|
|
</p>
|
|
<ul>
|
|
<li><strong><code>preflight.checkTmux()</code></strong> — low-level check-only guard used by
|
|
<code>ao spawn</code> preflight. It validates <code>tmux -V</code> and throws platform-specific
|
|
manual install instructions when missing (no install attempts).</li>
|
|
<li><strong><code>ensureTmux()</code> in start.ts</strong> — interactive wrapper. Uses <code>askYesNo()</code>
|
|
to get user consent before running <code>tryInstallWithAttempts()</code> with
|
|
<code>tmuxInstallAttempts()</code>. Matches the existing <code>ensureGit()</code> pattern.
|
|
Non-interactive callers default to skip install and print manual hints.</li>
|
|
</ul>
|
|
|
|
<pre><code>// 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);
|
|
}</code></pre>
|
|
|
|
<h3>All-Path Enforcement</h3>
|
|
<p>
|
|
<code>start.ts</code> centralizes tmux enforcement in <code>runStartup()</code>, so all
|
|
start entry paths are covered (normal start, URL start, and retries with existing config):
|
|
</p>
|
|
|
|
<pre><code>async function runStartup(...) {
|
|
const runtime = config.defaults?.runtime ?? "tmux";
|
|
if (runtime === "tmux") {
|
|
await ensureTmux();
|
|
}
|
|
// continue dashboard + orchestrator startup...
|
|
}</code></pre>
|
|
|
|
<h3>Interactive Prerequisite Prompts (TTY)</h3>
|
|
<p>
|
|
When <code>ao start</code> detects a missing prerequisite on a fresh machine, it prompts
|
|
the user before attempting any install. This ensures no surprises — no silent
|
|
<code>sudo</code>, no unexpected package manager invocations.
|
|
</p>
|
|
|
|
<h4>Prerequisite Matrix</h4>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Prerequisite</th>
|
|
<th>Required?</th>
|
|
<th>Prompt</th>
|
|
<th>Install methods (by platform)</th>
|
|
<th>On failure</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td><strong>tmux</strong></td>
|
|
<td>Yes <small>(for <code>runtime: "tmux"</code>)</small></td>
|
|
<td><code>Install tmux now? [Y/n]</code></td>
|
|
<td>
|
|
macOS: <code>brew install tmux</code><br>
|
|
Linux: <code>apt-get</code> → <code>dnf</code>
|
|
</td>
|
|
<td><span class="badge badge-p0" style="font-size:0.7rem">blocks</span> — prints platform hints + exit 1</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>git</strong></td>
|
|
<td>Yes <small>(for URL clone & add-project)</small></td>
|
|
<td><code>Install Git now? [Y/n]</code></td>
|
|
<td>
|
|
macOS: <code>brew install git</code><br>
|
|
Linux: <code>apt-get</code> → <code>dnf</code><br>
|
|
Windows: <code>winget install Git.Git</code>
|
|
</td>
|
|
<td><span class="badge badge-p0" style="font-size:0.7rem">blocks</span> — prints platform hints + exit 1</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Agent runtime</strong><br><small>(Claude Code, Codex, Aider, OpenCode)</small></td>
|
|
<td>Yes <small>(at least one)</small></td>
|
|
<td>Numbered menu:<br><code>Choose runtime to install [1-5]</code></td>
|
|
<td>
|
|
1. Claude Code (<code>npm i -g @anthropic-ai/claude-code</code>)<br>
|
|
2. Codex (<code>npm i -g @openai/codex</code>)<br>
|
|
3. Aider (<code>pipx install aider-chat</code>)<br>
|
|
4. OpenCode (<code>go install ...</code>)<br>
|
|
5. Skip
|
|
</td>
|
|
<td>Config generation continues — user can install later</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>GitHub CLI (gh)</strong></td>
|
|
<td>No <small>(optional)</small></td>
|
|
<td><code>Install GitHub CLI now? [y/N]</code><br><small>(defaults to <strong>no</strong>)</small></td>
|
|
<td>
|
|
macOS: <code>brew install gh</code><br>
|
|
Linux: <code>apt-get</code> → <code>dnf</code><br>
|
|
Windows: <code>winget install GitHub.cli</code>
|
|
</td>
|
|
<td>Skipped silently — optional dependency</td>
|
|
</tr>
|
|
<tr>
|
|
<td><strong>Docker</strong></td>
|
|
<td>No</td>
|
|
<td><em>Not prompted</em></td>
|
|
<td>—</td>
|
|
<td>— <small>(no <code>runtime-docker</code> plugin registered)</small></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<h4>Key Behaviors</h4>
|
|
<ul>
|
|
<li><strong>Always asks first</strong> — uses <code>askYesNo()</code> before running any install command. No silent <code>sudo</code> or background package manager invocations.</li>
|
|
<li><strong>Logs each command</strong> — shows <code>"Running: brew install tmux"</code> (via <code>tryInstallWithAttempts()</code>) so the user knows exactly what's executing.</li>
|
|
<li><strong>Blocks on required failures</strong> — for tmux and git, prints platform-specific manual install instructions and exits rather than silently degrading the experience.</li>
|
|
<li><strong>Non-interactive fallback</strong> — when not a TTY (CI, piped scripts, agent callers), <code>canPromptForInstall()</code> returns false and <code>askYesNo()</code> returns its non-interactive default without prompting. Required and optional tools both default to <code>false</code> (skip), then print manual instructions when required tools are missing.</li>
|
|
<li><strong>Consistent pattern</strong> — all prerequisite helpers (<code>ensureTmux()</code>, <code>ensureGit()</code>, <code>promptInstallAgentRuntime()</code>) follow the same structure: detect → prompt → install → verify → block or skip.</li>
|
|
</ul>
|
|
|
|
<h4>Example: Fresh Machine Flow</h4>
|
|
<pre><code>$ 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</code></pre>
|
|
|
|
<p>If the user declines or install fails:</p>
|
|
<pre><code> 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</code></pre>
|
|
|
|
<div class="callout callout-decision">
|
|
We chose to block and show clear instructions rather than fall back to <code>runtime: "process"</code>.
|
|
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.
|
|
</div>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- ================================================================ -->
|
|
<h2>6. Fix 4 — Terminal Server Crash Recovery</h2>
|
|
|
|
<h3>Root Cause</h3>
|
|
<p>
|
|
<code>start-all.ts</code> spawns three child processes. If a terminal server crashes,
|
|
it logs the exit and never restarts.
|
|
</p>
|
|
|
|
<h3>Fix</h3>
|
|
<p>
|
|
Auto-restart logic with <strong>correct bookkeeping</strong>:
|
|
</p>
|
|
|
|
<ul>
|
|
<li>Terminal servers restart up to <strong>3 times</strong> on non-zero exit</li>
|
|
<li>Next.js is <strong>not</strong> restarted</li>
|
|
<li>No restart during graceful shutdown</li>
|
|
</ul>
|
|
|
|
<h3>Bookkeeping: Slot-Based Child Tracking</h3>
|
|
<p>
|
|
The initial version had a bug: <code>launch()</code> always called <code>children.push(child)</code>,
|
|
creating duplicate entries on restart. The <code>cleanup()</code> function counts
|
|
<code>children.length</code> and waits for exits, so stale handles would skew the alive count
|
|
and trigger the 5-second force-shutdown timer.
|
|
</p>
|
|
|
|
<p>Fix: each process gets a <strong>slot index</strong> assigned on first launch. Restarts replace in-place:</p>
|
|
|
|
<pre><code>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;
|
|
}</code></pre>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- ================================================================ -->
|
|
<h2>7. Files Changed</h2>
|
|
|
|
<ul class="files-changed">
|
|
<li>packages/cli/src/lib/preflight.ts — walk-up resolver, check-only tmux preflight + platform messages</li>
|
|
<li>packages/cli/src/commands/start.ts — centralized <code>ensureTmux()</code> gate + interactive install prompts for git/tmux/agent runtimes (+ optional gh)</li>
|
|
<li>packages/cli/src/lib/detect-agent.ts — shared runtime selection with optional pre-detected list (avoids redundant plugin scan)</li>
|
|
<li class="new">packages/ao/bin/postinstall.js — node-pty spawn-helper chmod</li>
|
|
<li>packages/ao/package.json — postinstall script registration</li>
|
|
<li>packages/web/server/start-all.ts — restart logic with slot-based bookkeeping</li>
|
|
<li>eslint.config.js — packages/ao/bin globals override</li>
|
|
<li>packages/cli/__tests__/lib/preflight.test.ts — expanded tests</li>
|
|
</ul>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- ================================================================ -->
|
|
<h2>8. What Was Already Working</h2>
|
|
|
|
<p>Investigated but not broken:</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Suspected Blocker</th><th>Actual State</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>Port 3000 conflict</td>
|
|
<td><span class="badge badge-fixed">Already fixed</span> — <code>findFreePort(port + 1)</code> auto-increments up to 100 ports</td>
|
|
</tr>
|
|
<tr>
|
|
<td>node-pty crash kills dashboard</td>
|
|
<td><span class="badge badge-fixed">Already handled</span> — <code>direct-terminal-ws.ts</code> catches import failure, gracefully rejects connections</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>findWebDir()</code> in global install</td>
|
|
<td><span class="badge badge-fixed">Already handled</span> — uses <code>require.resolve()</code> with proper walk-up</td>
|
|
</tr>
|
|
<tr>
|
|
<td><code>concurrently</code> at runtime</td>
|
|
<td><span class="badge badge-fixed">Already fixed</span> — production uses <code>node dist-server/start-all.js</code></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- ================================================================ -->
|
|
<h2>9. Post-Merge: True Two-Step Matrix</h2>
|
|
|
|
<p>After this PR, <code>npm install -g @composio/ao && ao start</code>:</p>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Platform</th><th>tmux Available</th><th>tmux Not Available</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>macOS (Intel + Apple Silicon)</td>
|
|
<td><span class="badge badge-fixed">Works</span></td>
|
|
<td><span class="badge badge-fixed">Interactive prompt + brew install</span><br>Clear error if brew missing or declined</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Linux x64</td>
|
|
<td><span class="badge badge-fixed">Works</span></td>
|
|
<td><span class="badge badge-fixed">Interactive prompt + apt/dnf install</span><br>Clear error if sudo unavailable or declined</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Linux arm64 (Graviton)</td>
|
|
<td><span class="badge badge-fixed">Works</span><br>Direct terminal unavailable (no node-pty prebuild)</td>
|
|
<td><span class="badge badge-fixed">Interactive prompt + apt/dnf install</span><br>Clear error if sudo unavailable or declined</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Windows</td>
|
|
<td>N/A</td>
|
|
<td><span class="badge badge-p2">Clear error</span><br>Guides user to WSL</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<p>
|
|
<strong>Key improvement:</strong> Missing prerequisites are handled with explicit user consent in
|
|
<code>ao start</code>. If install is declined or fails, the CLI prints one-line commands to run
|
|
manually, then exits clearly.
|
|
</p>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- ================================================================ -->
|
|
<h2>10. Testing</h2>
|
|
|
|
<table>
|
|
<thead>
|
|
<tr><th>Check</th><th>Result</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td>TypeScript typecheck</td><td>Passes (CLI + web server)</td></tr>
|
|
<tr><td>ESLint</td><td>No new errors on changed files</td></tr>
|
|
<tr><td>Preflight tests</td><td>15/15 pass (expanded from 13)</td></tr>
|
|
<tr><td>CLI tests</td><td>222/223 pass (1 = pre-existing flaky doctor-script.test.ts)</td></tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<div class="callout callout-warning">
|
|
Local validation caveat: if <code>node_modules</code> contains root-owned artifacts
|
|
(notably <code>node-pty/build/*</code> from earlier sudo installs), <code>pnpm install</code>
|
|
can fail with <code>EACCES</code> on unlink/rebuild. This is an environment ownership issue,
|
|
not a behavior regression in startup logic.
|
|
</div>
|
|
|
|
<h3>New/Updated Test Cases</h3>
|
|
<table>
|
|
<thead>
|
|
<tr><th>Test</th><th>What It Verifies</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td>pnpm layout (direct match)</td>
|
|
<td><code>findPackageUp</code> finds ao-core in webDir/node_modules on first check</td>
|
|
</tr>
|
|
<tr>
|
|
<td>npm hoisted layout (walk-up)</td>
|
|
<td><code>findPackageUp</code> misses first level, finds ao-core one level up</td>
|
|
</tr>
|
|
<tr>
|
|
<td>npm hint on missing deps</td>
|
|
<td>Global install path shows <code>npm install -g</code> hint</td>
|
|
</tr>
|
|
<tr>
|
|
<td>pnpm hint on missing deps</td>
|
|
<td>Monorepo path shows <code>pnpm install && pnpm build</code> hint</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Missing dist</td>
|
|
<td>ao-core found but <code>dist/index.js</code> missing shows build hint</td>
|
|
</tr>
|
|
<tr>
|
|
<td>tmux interactive install success</td>
|
|
<td>When tmux -V fails initially and user-approved install succeeds, <code>ensureTmux()</code> passes</td>
|
|
</tr>
|
|
<tr>
|
|
<td>tmux interactive install declined/failure</td>
|
|
<td>When user declines or install attempts fail, prints platform-specific instructions and exits</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<hr class="section-divider">
|
|
|
|
<!-- ================================================================ -->
|
|
<h2>11. Design Decisions</h2>
|
|
|
|
<div class="callout callout-decision">
|
|
<strong>Prompted tmux install in <code>ao start</code>, then block with clear instructions on failure.</strong>
|
|
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 <code>runtime: "process"</code>.
|
|
The process runtime lacks session attach, terminal panel, and <code>ao send</code> —
|
|
a degraded first impression is worse than asking the user to run one command.
|
|
</div>
|
|
|
|
<div class="callout callout-decision">
|
|
<strong>Slot-based restart bookkeeping.</strong>
|
|
Each child process gets a fixed slot index in the <code>children</code> array on first launch.
|
|
Restarts replace in-place (<code>children[slotIndex] = replacement</code>) instead of pushing,
|
|
preventing duplicate entries that would break the cleanup countdown.
|
|
</div>
|
|
|
|
<div class="callout callout-decision">
|
|
<strong>Postinstall is temporary.</strong>
|
|
The <code>spawn-helper</code> chmod workaround will be removed once <code>node-pty</code> ships
|
|
a stable release with the upstream fix (microsoft/node-pty#866).
|
|
</div>
|
|
|
|
</div>
|
|
</body>
|
|
</html>
|