744 lines
28 KiB
HTML
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 — config generation, project detection, agent runtime discovery,
|
|
and environment validation — happens automatically. When AO is already running,
|
|
the CLI detects it and offers contextual options instead of crashing.
|
|
</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–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 <project> <issue></code></td>
|
|
<td><code>ao spawn <issue></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> — 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 — 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> — 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> — 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 → default to <code>claude-code</code>.</li>
|
|
<li>1 agent → auto-select.</li>
|
|
<li>Multiple + human caller → interactive picker via <code>node:readline/promises</code>.</li>
|
|
<li>Multiple + non-human → prefer <code>claude-code</code>, else first available.</li>
|
|
</ul>
|
|
|
|
<h3><code>detect-env.ts</code> — 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> — 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, decomposer),
|
|
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 <project> <issue></code> to <code>ao spawn [issue]</code>.
|
|
The project is always auto-detected — 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 <project> <issue>' is no longer supported.
|
|
The project is now auto-detected. Use:
|
|
|
|
ao spawn INT-100 # spawn with issue INT-100
|
|
ao spawn # spawn without an issue</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 — 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>() => 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 — 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 — 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 <issue></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 → use it.</li>
|
|
<li>If <code>AO_PROJECT_ID</code> env var is set and matches a configured project → use it.</li>
|
|
<li>If <code>process.cwd()</code> matches a project's <code>path</code> field → use it.</li>
|
|
<li>Otherwise → 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 <project> <issue>' 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 — 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 — 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 & 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 — 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>
|