agent-orchestrator/docs/design-cli-redesign-analysi...

744 lines
28 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: CLI Redesign — Simplified Onboarding — Agent Orchestrator</title>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<script>mermaid.initialize({ theme: 'dark', startOnLoad: true });</script>
<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-new { background: rgba(63, 185, 80, 0.2); color: var(--green); }
.badge-changed { background: rgba(210, 153, 34, 0.2); color: var(--yellow); }
.badge-removed { background: rgba(248, 81, 73, 0.2); color: var(--red); }
.badge-deprecated { background: rgba(219, 109, 40, 0.2); color: var(--orange); }
.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); }
.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);
}
.files-changed li.removed::before {
content: "-";
color: var(--red);
}
.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-decision {
border-left-color: var(--yellow);
}
.callout-decision::before {
content: "Decision: ";
color: var(--yellow);
font-weight: 600;
}
.scenario-table td:first-child {
font-weight: 600;
white-space: nowrap;
width: 30px;
}
.mermaid {
margin: 16px 0 24px;
text-align: center;
}
.section-divider {
border: none;
border-top: 1px solid var(--border);
margin: 40px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>CLI Redesign: Simplified Onboarding</h1>
<div class="meta">
<span>PR #463</span>
<span>Status: Implemented</span>
<span>Updated: 2026-03-17</span>
</div>
<!-- ================================================================ -->
<h2>1. Overview</h2>
<p>
The CLI redesign eliminates multi-step onboarding by collapsing <code>ao init</code>,
<code>ao add-project</code>, and <code>ao start</code> into a single command.
The target experience:
</p>
<pre><code>npm install -g @composio/ao && ao start</code></pre>
<p>
Everything else &mdash; config generation, project detection, agent runtime discovery,
and environment validation &mdash; happens automatically. When AO is already running,
the CLI detects it and offers contextual options instead of crashing.
</p>
<hr class="section-divider">
<!-- ================================================================ -->
<h2>2. Before vs After</h2>
<table>
<thead>
<tr><th>Aspect</th><th>Before</th><th>After</th></tr>
</thead>
<tbody>
<tr>
<td>Setup steps</td>
<td>3&ndash;4 commands: <code>ao init</code>, edit YAML, <code>ao add-project</code>, <code>ao start</code></td>
<td>1 command: <code>ao start</code></td>
</tr>
<tr>
<td>Config creation</td>
<td>Manual via <code>ao init</code></td>
<td>Auto-generated on first <code>ao start</code> using environment detection</td>
</tr>
<tr>
<td>Agent selection</td>
<td>User edits YAML manually</td>
<td>Auto-detected from installed runtimes; interactive picker if multiple</td>
</tr>
<tr>
<td>Spawning a session</td>
<td><code>ao spawn &lt;project&gt; &lt;issue&gt;</code></td>
<td><code>ao spawn &lt;issue&gt;</code> (project auto-detected)</td>
</tr>
<tr>
<td>Already-running handling</td>
<td>Error / undefined behavior</td>
<td>Interactive menu (human) or structured info+exit (agent)</td>
</tr>
<tr>
<td>Config reference</td>
<td>Read source code or docs site</td>
<td><code>ao config-help</code> prints annotated schema</td>
</tr>
<tr>
<td><code>ao init</code></td>
<td>Primary setup command</td>
<td>Deprecated thin wrapper that calls <code>ao start</code> config path</td>
</tr>
<tr>
<td><code>ao add-project</code></td>
<td>Required for each repo</td>
<td>Fully removed</td>
</tr>
</tbody>
</table>
<div class="before-after">
<div class="before">
<h4>Before (4 steps)</h4>
<pre><code>ao init
# manually edit agent-orchestrator.yaml
ao add-project my-app ~/code/my-app
ao start</code></pre>
</div>
<div class="after">
<h4>After (1 step)</h4>
<pre><code>ao start
# config auto-generated, agent auto-detected
# dashboard opens at http://localhost:3000</code></pre>
</div>
</div>
<hr class="section-divider">
<!-- ================================================================ -->
<h2>3. Architecture Changes</h2>
<h3>File Map</h3>
<ul class="files-changed">
<li class="new">packages/cli/src/lib/running-state.ts</li>
<li class="new">packages/cli/src/lib/caller-context.ts</li>
<li class="new">packages/cli/src/lib/detect-agent.ts</li>
<li class="new">packages/cli/src/lib/detect-env.ts</li>
<li class="new">packages/cli/src/lib/config-instruction.ts</li>
<li>packages/cli/src/commands/start.ts</li>
<li>packages/cli/src/commands/spawn.ts</li>
<li>packages/cli/src/commands/init.ts (rewritten as deprecation wrapper)</li>
<li class="removed">packages/cli/src/commands/add-project.ts</li>
<li>packages/core/src/orchestrator-prompt.ts</li>
<li>packages/core/src/session-manager.ts (env var injection)</li>
<li>packages/plugin-agent-claude-code (detect + displayName)</li>
<li>packages/plugin-agent-aider (detect + displayName)</li>
<li>packages/plugin-agent-codex (detect + displayName)</li>
<li>packages/plugin-agent-opencode (detect + displayName)</li>
</ul>
<p>
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.
</p>
<hr class="section-divider">
<!-- ================================================================ -->
<h2>4. New Library Files</h2>
<h3><code>running-state.ts</code> &mdash; Single-Instance Tracking</h3>
<p>
Tracks whether an AO instance is already running. Writes a
<code>~/.agent-orchestrator/running.json</code> file containing the running state:
</p>
<pre><code>interface RunningState {
pid: number;
configPath: string;
port: number;
startedAt: string;
projects: string[];
}</code></pre>
<ul>
<li>Uses an advisory lockfile (<code>running.lock</code>) with <code>O_CREAT | O_EXCL</code> for atomic creation &mdash; prevents concurrent registration races.</li>
<li>Auto-prunes stale entries: if the recorded PID is dead (<code>process.kill(pid, 0)</code> fails), the entry is cleared on read.</li>
<li>Lock timeout of 5 seconds with force-remove handles crashed processes that left a stale lock.</li>
<li>Exports: <code>register()</code>, <code>unregister()</code>, <code>getRunning()</code>, <code>isAlreadyRunning()</code>, <code>waitForExit(pid, timeoutMs)</code>.</li>
</ul>
<h3><code>caller-context.ts</code> &mdash; Human/Orchestrator/Agent Detection</h3>
<p>
Detects who is invoking the CLI and provides typed helpers for propagating context.
</p>
<pre><code>type CallerType = "human" | "orchestrator" | "agent";</code></pre>
<ul>
<li><code>getCallerType()</code>: If <code>AO_CALLER_TYPE</code> env var is set, trusts it directly. Otherwise: TTY = human, non-TTY = agent.</li>
<li><code>isHumanCaller()</code>: Convenience boolean check.</li>
<li><code>setCallerContext(env, opts)</code>: Injects <code>AO_CALLER_TYPE</code>, <code>AO_SESSION_ID</code>, <code>AO_PROJECT_ID</code>, <code>AO_CONFIG_PATH</code>, and <code>AO_PORT</code> into a spawn environment record.</li>
</ul>
<h3><code>detect-agent.ts</code> &mdash; Plugin-Based Runtime Discovery</h3>
<p>
No hardcoded binary paths. Dynamically imports each agent plugin and calls its <code>detect()</code> method.
</p>
<p>Known plugins:</p>
<table>
<thead><tr><th>Name</th><th>Package</th></tr></thead>
<tbody>
<tr><td>claude-code</td><td><code>@composio/ao-plugin-agent-claude-code</code></td></tr>
<tr><td>aider</td><td><code>@composio/ao-plugin-agent-aider</code></td></tr>
<tr><td>codex</td><td><code>@composio/ao-plugin-agent-codex</code></td></tr>
<tr><td>opencode</td><td><code>@composio/ao-plugin-agent-opencode</code></td></tr>
</tbody>
</table>
<p>Selection logic in <code>detectAgentRuntime()</code>:</p>
<ul>
<li>0 agents detected &rarr; default to <code>claude-code</code>.</li>
<li>1 agent &rarr; auto-select.</li>
<li>Multiple + human caller &rarr; interactive picker via <code>node:readline/promises</code>.</li>
<li>Multiple + non-human &rarr; prefer <code>claude-code</code>, else first available.</li>
</ul>
<h3><code>detect-env.ts</code> &mdash; Environment Detection</h3>
<p>
Probes the local environment to auto-populate config fields. Returns an <code>EnvironmentInfo</code> object:
</p>
<ul>
<li><strong>Git</strong>: repo detection, remote URL parsing, <code>owner/repo</code> extraction, current branch, default branch (via <code>detectDefaultBranch()</code>).</li>
<li><strong>Tools</strong>: tmux availability, gh CLI availability, gh auth status.</li>
<li><strong>API keys</strong>: <code>LINEAR_API_KEY</code>, <code>SLACK_WEBHOOK_URL</code> presence.</li>
</ul>
<h3><code>config-instruction.ts</code> &mdash; Config Schema Reference</h3>
<p>
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 <code>ao config-help</code>.
</p>
<hr class="section-divider">
<!-- ================================================================ -->
<h2>5. Command Changes</h2>
<h3><code>ao start</code> <span class="badge badge-changed">changed</span></h3>
<p>Now the single entry point for all of AO. On first run:</p>
<ol>
<li>Calls <code>detectEnvironment(cwd)</code> to probe git, tools, and APIs.</li>
<li>Calls <code>detectAgentRuntime()</code> to find an installed agent.</li>
<li>Generates <code>agent-orchestrator.yaml</code> with all detected values.</li>
<li>Calls <code>register()</code> to write running state to <code>~/.agent-orchestrator/running.json</code>.</li>
<li>Starts the dist-server and orchestrator session.</li>
</ol>
<p>If already running, delegates to the <strong>Already-Running Detection</strong> flow (Section 8).</p>
<p>Also exports <code>createConfigOnly()</code> for the deprecated <code>ao init</code> wrapper.</p>
<h3><code>ao spawn</code> <span class="badge badge-changed">changed</span></h3>
<p>
Simplified from <code>ao spawn &lt;project&gt; &lt;issue&gt;</code> to <code>ao spawn [issue]</code>.
The project is always auto-detected &mdash; no project argument is accepted.
See Section 9 for the auto-detection logic.
</p>
<p>
If a user passes two args (old syntax), a friendly warning is shown:
</p>
<pre><code>⚠ 'ao spawn &lt;project&gt; &lt;issue&gt;' 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</code></pre>
<p>
The <code>autoDetectProject()</code> function resolves the project from: single project in config,
<code>AO_PROJECT_ID</code> env var, or cwd matching a project path.
</p>
<h3><code>ao batch-spawn</code> <span class="badge badge-changed">changed</span></h3>
<p>Same simplification: takes only issue IDs as arguments. The project is always auto-detected &mdash; no project prefix is accepted.</p>
<h3><code>ao init</code> <span class="badge badge-deprecated">deprecated</span></h3>
<p>Reduced to a thin wrapper that prints a deprecation warning then delegates to <code>createConfigOnly()</code> from <code>start.ts</code>:</p>
<pre><code>// 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();
});</code></pre>
<h3><code>ao add-project</code> <span class="badge badge-removed">removed</span></h3>
<p>Fully deleted. Projects are auto-detected from cwd or added manually to the YAML config file.</p>
<h3><code>ao config-help</code> <span class="badge badge-new">new</span></h3>
<p>Prints the annotated config schema from <code>config-instruction.ts</code> to stdout. Provides both humans and orchestrator agents a quick reference without needing external docs.</p>
<hr class="section-divider">
<!-- ================================================================ -->
<h2>6. Plugin System Enhancements</h2>
<p>All four agent plugins gained two new exports to support runtime discovery:</p>
<table>
<thead>
<tr><th>Export</th><th>Type</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr>
<td><code>detect()</code></td>
<td><code>() =&gt; boolean</code></td>
<td>Returns <code>true</code> if the agent runtime is available on the system (binary exists in PATH, required API key is set, etc.)</td>
</tr>
<tr>
<td><code>manifest.displayName</code></td>
<td><code>string</code></td>
<td>Human-readable name for the interactive picker (e.g. "Claude Code", "Aider", "OpenAI Codex", "OpenCode")</td>
</tr>
</tbody>
</table>
<p>
These are consumed by <code>detect-agent.ts</code> via dynamic <code>import()</code>.
Plugins that fail to import (not installed) are silently skipped &mdash; this is by design,
as missing plugins are the normal case.
</p>
<hr class="section-divider">
<!-- ================================================================ -->
<h2>7. Session Environment Variables</h2>
<p>
Every spawned session (orchestrator and worker) receives these environment variables,
set in <code>session-manager.ts</code> (core package) at the single spawn point:
</p>
<table>
<thead>
<tr><th>Variable</th><th>Value</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr>
<td><code>AO_CALLER_TYPE</code></td>
<td><code>"orchestrator"</code> or <code>"agent"</code></td>
<td>Tells the CLI who is calling &mdash; affects interactive prompts and error handling</td>
</tr>
<tr>
<td><code>AO_PROJECT_ID</code></td>
<td>Project key from config</td>
<td>Enables <code>ao spawn &lt;issue&gt;</code> auto-detection inside agent sessions</td>
</tr>
<tr>
<td><code>AO_CONFIG_PATH</code></td>
<td>Absolute path to YAML</td>
<td>Sessions find config without filesystem search</td>
</tr>
<tr>
<td><code>AO_PORT</code></td>
<td>Dashboard port number</td>
<td>Agents reach the API without parsing config</td>
</tr>
</tbody>
</table>
<p>
The <code>setCallerContext()</code> helper in <code>caller-context.ts</code> provides a typed interface
for populating these variables. All three spawn paths (orchestrator session, <code>ao spawn</code>, <code>ao batch-spawn</code>)
go through the session manager, ensuring consistency.
</p>
<hr class="section-divider">
<!-- ================================================================ -->
<h2>8. Already-Running Detection</h2>
<p>
When <code>ao start</code> is invoked and <code>isAlreadyRunning()</code> returns a live state,
behavior branches on caller type:
</p>
<div class="mermaid">
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"]
</div>
<p>
<strong>Human callers</strong> 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.
</p>
<p>
<strong>Agent/orchestrator callers</strong> receive a structured info dump (port, PID, projects list)
and a clean exit code 0, so they can connect to the already-running instance.
</p>
<hr class="section-divider">
<!-- ================================================================ -->
<h2>9. Spawn Auto-Detection Logic</h2>
<p>The <code>ao spawn</code> argument parser uses this decision tree:</p>
<div class="mermaid">
graph TD
A["ao spawn [issue]"] --> B{"Two args provided?"}
B -->|Yes| C["⚠ Warning: old syntax<br/>Show correct usage + exit"]
B -->|No| D{"One arg provided?"}
D -->|No| E["autoDetectProject<br/>no issue — bare session"]
D -->|Yes| F["arg = issueId<br/>autoDetectProject"]
</div>
<h3><code>autoDetectProject()</code> Resolution Order</h3>
<ol>
<li>If only one project in config &rarr; use it.</li>
<li>If <code>AO_PROJECT_ID</code> env var is set and matches a configured project &rarr; use it.</li>
<li>If <code>process.cwd()</code> matches a project's <code>path</code> field &rarr; use it.</li>
<li>Otherwise &rarr; throw error listing available projects.</li>
</ol>
<p>This means an orchestrator agent calling <code>ao spawn INT-1234</code> from within a session always resolves correctly via the injected <code>AO_PROJECT_ID</code> env var.</p>
<hr class="section-divider">
<!-- ================================================================ -->
<h2>10. Testing Scenarios</h2>
<table class="scenario-table">
<thead>
<tr><th>#</th><th>Scenario</th><th>Expected Outcome</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>Fresh install, <code>ao start</code> in a git repo</td><td>Config auto-generated with detected repo, agent, branch. Server starts.</td></tr>
<tr><td>2</td><td>Fresh install, <code>ao start</code> outside git repo</td><td>Error with clear message: "Run ao start inside a git repository."</td></tr>
<tr><td>3</td><td><code>ao start</code> when already running (human)</td><td>Interactive menu: open / restart / override / quit.</td></tr>
<tr><td>4</td><td><code>ao start</code> when already running (agent)</td><td>JSON state printed to stdout, exit 0.</td></tr>
<tr><td>5</td><td><code>ao start</code> with stale PID in running.json</td><td>Stale entry auto-pruned, fresh start proceeds normally.</td></tr>
<tr><td>6</td><td><code>ao init</code></td><td>Deprecation warning printed, config created via <code>createConfigOnly()</code>.</td></tr>
<tr><td>7</td><td><code>ao spawn #123</code> (single project in config)</td><td>Auto-detects the only project, spawns session for issue #123.</td></tr>
<tr><td>8</td><td><code>ao spawn my-app #123</code> (two args)</td><td>Warning: "'ao spawn &lt;project&gt; &lt;issue&gt;' is no longer supported." Shows correct usage and exits.</td></tr>
<tr><td>9</td><td><code>ao spawn #123</code> (multi-project, cwd matches one)</td><td>Auto-detects project from cwd path match.</td></tr>
<tr><td>10</td><td><code>ao spawn #123</code> (multi-project, no cwd match)</td><td>Error listing available projects.</td></tr>
<tr><td>11</td><td><code>ao spawn my-app</code> (arg matches a project ID)</td><td>Treated as issue "my-app" (project always auto-detected). May fail if no project matches cwd/env.</td></tr>
<tr><td>12</td><td><code>ao spawn</code> (no args, single project)</td><td>Auto-detect project, no issue &mdash; spawns bare session.</td></tr>
<tr><td>13</td><td><code>ao batch-spawn #1 #2 #3</code></td><td>Auto-detect project, spawn 3 sessions with duplicate detection.</td></tr>
<tr><td>14</td><td><code>ao batch-spawn #1 #2 #3</code> (multi-project, AO_PROJECT_ID set)</td><td>Env var resolves project. Spawn 3 sessions.</td></tr>
<tr><td>15</td><td><code>ao config-help</code></td><td>Full annotated YAML schema printed to stdout.</td></tr>
<tr><td>16</td><td>Agent session calls <code>ao spawn #123</code> with <code>AO_PROJECT_ID</code> set</td><td>Env var resolves project correctly without cwd match.</td></tr>
<tr><td>17</td><td>Multiple agents installed, human runs <code>ao start</code></td><td>Interactive picker with displayName labels shown.</td></tr>
<tr><td>18</td><td>Multiple agents installed, non-TTY <code>ao start</code></td><td>Claude Code auto-selected as preferred default.</td></tr>
</tbody>
</table>
<hr class="section-divider">
<!-- ================================================================ -->
<h2>11. Design Decisions</h2>
<div class="callout callout-decision">
<strong>priority field removed from spawn.</strong>
Spawning inherits priority from the project config. Per-spawn priority added API surface with no demonstrated use case &mdash; callers can override via config if needed.
</div>
<div class="callout callout-decision">
<strong>Config instruction NOT injected into orchestrator prompt.</strong>
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 <code>ao config-help</code> on demand when it needs to modify configuration.
</div>
<div class="callout callout-decision">
<strong>dist-server architecture kept unchanged.</strong>
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.
</div>
<div class="callout callout-decision">
<strong>add-project fully deleted, not deprecated.</strong>
Unlike <code>ao init</code> (which gets a deprecation wrapper for existing users), <code>add-project</code> was never on main and had no production users. Clean deletion avoids dead code and import graph bloat.
</div>
<div class="callout callout-decision">
<strong>Env vars set in session-manager.ts (core), not spawn.ts (cli).</strong>
All three spawn points (orchestrator session, <code>ao spawn</code>, <code>ao batch-spawn</code>) 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.
</div>
<hr class="section-divider">
<!-- ================================================================ -->
<h2>12. Review Findings &amp; Fixes</h2>
<table>
<thead>
<tr><th>Category</th><th>Finding</th><th>Resolution</th></tr>
</thead>
<tbody>
<tr>
<td>Lockfile spin</td>
<td>Busy-wait loop in <code>acquireLock()</code> blocks the event loop for up to 50ms per iteration</td>
<td>Acceptable for a single-writer scenario with sub-50ms contention windows. Async alternative adds complexity with no practical benefit.</td>
</tr>
<tr>
<td>Stale lock</td>
<td>If a process crashes between lock acquire and release, the lockfile persists indefinitely</td>
<td>5-second timeout triggers force-remove. Second attempt uses <code>O_EXCL</code> to ensure atomicity.</td>
</tr>
<tr>
<td>Race condition</td>
<td>Two concurrent <code>ao start</code> invocations could both see "not running"</td>
<td><code>O_EXCL</code> on lockfile makes <code>register()</code> atomic &mdash; the second writer wins and overwrites. First instance detects the conflict on next state check.</td>
</tr>
<tr>
<td>Plugin import errors</td>
<td><code>detect-agent.ts</code> silently swallows all import errors, including real bugs</td>
<td>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.</td>
</tr>
<tr>
<td>TTY detection edge</td>
<td><code>process.stdout.isTTY</code> is <code>undefined</code> (not <code>false</code>) when piped</td>
<td>The ternary <code>isTTY ? "human" : "agent"</code> correctly treats <code>undefined</code> as falsy.</td>
</tr>
<tr>
<td>ID collision</td>
<td>If a project ID and an issue ID are identical (e.g. project named "123"), <code>ao spawn 123</code> treats it as an issue</td>
<td>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.</td>
</tr>
<tr>
<td>Schema drift</td>
<td><code>config-instruction.ts</code> returns a static string that can drift from the actual config type definitions</td>
<td>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.</td>
</tr>
<tr>
<td>Env duplication</td>
<td>Env vars were initially set in three CLI commands independently, risking divergence</td>
<td>Moved to <code>session-manager.ts</code> in core. All spawn paths use a single <code>setCallerContext()</code> call site.</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>