docs: clarify one-orchestrator-per-project model + prune superseded docs (closes #2005) (#2012)

* docs: clarify one-orchestrator-per-project model (closes #2005)

The architecture design doc defined "Orchestrator" only as "the central
server that manages everything," conflating the central daemon with the
per-project orchestrator agent. This implied a single orchestrator agent
spanning all projects.

The actual (and intended) model: one central daemon (`ao start`) manages
all registered projects, but each project gets its own orchestrator agent
session — one per project, enforced by the canonical orchestrator in
session-manager (ensureOrchestrator). Disambiguate the term in the
nomenclature table and the server-centric design principle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: remove stale design/architecture artifacts (closes #2005)

Delete historical design research, implementation specs, and PR-handoff
notes that are superseded by the shipped code and the living docs
(DESIGN.md, CLAUDE.md, docs/ARCHITECTURE.md, docs/DEVELOPMENT.md). None
were linked from any current doc.

Removed:
- artifacts/ — original design research (incl. architecture-design.md,
  the doc #2005 flagged for the misleading "one central orchestrator"
  framing; removing it resolves the ambiguity at the source)
- docs/design/ — Feb 2026 dashboard design-research artifacts
- docs/specs/ + design-*.html — implemented feature specs/analyses
- docs/state-machine-stage2-evidence-recovery-plan.md — shipped plan
- docs/SECURITY-AUDIT-SUMMARY.md — point-in-time audit snapshot
- handoff/pr-1466/ — handoff notes for a long-merged PR

Drop the now-dangling artifacts/ entry from .cursor/BUGBOT.md ignore list.

Confirms the one-orchestrator-per-project model: code enforces a single
canonical orchestrator per project (ensureOrchestrator); no multiple
orchestrators within one project.

* docs: restore docs flagged as still-relevant by review (#2005)

@yyovil flagged several docs as still needed. Restore the full
artifacts/ and docs/design/ trees, docs/specs/project-based-dashboard-
architecture.md, and handoff/pr-1466/ (kept whole for link cohesion).

artifacts/architecture-design.md is kept WITH the one-orchestrator-per-
project clarification (the #2005 fix) rather than deleted — resolving the
ambiguity in place. Restores the artifacts/ entry in .cursor/BUGBOT.md.

Remaining removals are limited to clearly point-in-time/superseded docs:
docs/state-machine-stage2-evidence-recovery-plan.md,
docs/SECURITY-AUDIT-SUMMARY.md, and four design/spec HTML analyses.
This commit is contained in:
Harshit Singh Bhandari 2026-05-22 16:36:31 +05:30 committed by GitHub
parent c633a7ac49
commit 5d0b624fbe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 3 additions and 4579 deletions

View File

@ -32,7 +32,7 @@ Human only intervenes when notified. Everything else is handled.
### Design Principles
1. **Push, not pull**: Notifications are the primary interface. Dashboard is secondary drill-down.
2. **Server-centric**: All agents report to a central server. The server coordinates everything.
2. **Server-centric**: One central daemon (`ao start`) manages every registered project, and all agents report to it. Each project gets its own orchestrator agent — one orchestrator per project, never a single orchestrator spanning all projects.
3. **Plugin everything**: 8 pluggable abstraction slots. Swap any component.
4. **Works out of the box**: Default config (tmux + claude-code + worktree + github) requires zero setup beyond `npx agent-orchestrator init`.
5. **Silence by default, loud when needed**: Auto-handle routine issues (CI failures, review comments). Only notify the human when their judgment or action is truly required.
@ -44,7 +44,8 @@ Human only intervenes when notified. Everything else is handled.
| Term | Definition | Examples |
| ---------------- | ------------------------------------------ | -------------------------------- |
| **Orchestrator** | The central server that manages everything | The Next.js app |
| **Orchestrator (daemon)** | The central server process that manages **all** registered projects | `ao start` + the Next.js app |
| **Orchestrator agent** | A per-project agent session that spawns and supervises workers — **one per project** | `my-app-orchestrator`, `backend-api-orchestrator` |
| **Project** | A configured repository to work on | `my-app`, `backend-api` |
| **Session** | A running agent instance working on a task | `my-app-1`, `my-app-2` |
| **Runtime** | Where/how the session executes | tmux, docker, k8s, process |

View File

@ -1,362 +0,0 @@
# Security Audit Summary — Agent Orchestrator
**Date**: 2026-02-16
**Auditor**: Claude Sonnet 4.5
**Scope**: Full codebase + git history secret scanning, automated prevention measures
## Executive Summary
✅ **Security audit completed successfully**
- ⚠️ **1 historical secret found** (OpenClaw token, already removed from current code)
- ✅ **0 secrets in current codebase**
- ✅ **Automated prevention measures implemented**
- ✅ **CI/CD security pipeline added**
- ✅ **Documentation updated with security best practices**
---
## Findings
### 1. Historical Secret Leak (RESOLVED)
**Issue**: OpenClaw notifier token found in git history
- **Token**: `1af5c4f...872` (redacted - visible in commit history)
- **File**: `agent-orchestrator.yaml`
- **Commit**: `0393ab70a83e090883895d2168aa39a76f997ec8`
- **Date**: 2026-02-15
- **Status**: Token already removed from current code, still in git history
**Impact**: Medium
**Likelihood**: Low (local development token, not production)
**Action Required**:
- ⚠️ If this token is still in use, **rotate it immediately**
- Token is documented in [SECURITY.md](../SECURITY.md)
### 2. Current Codebase
**Status**: ✅ **CLEAN**
Scanned 1.46 MB of code:
- No hardcoded API keys
- No authentication tokens
- No passwords or private keys
- Test files use dummy values (`test_key`, `https://hooks.slack.com/test`)
- Example configs use environment variable references (`${SLACK_WEBHOOK_URL}`)
---
## Security Measures Implemented
### 1. Gitleaks Configuration (`.gitleaks.toml`)
**Purpose**: Prevent accidental commits of secrets
**Features**:
- Uses all default gitleaks rules (covers 100+ secret patterns)
- Custom allowlist for false positives
- Ignores build artifacts (`node_modules/`, `dist/`, `.next/`)
- Allowlists test files (dummy secrets are OK)
- Allowlists environment variable references (`${VAR_NAME}`)
**Patterns Detected**:
- GitHub tokens (`ghp_*`, `gho_*`, `ghs_*`, `ghu_*`)
- Linear API keys (`lin_api_*`)
- Slack webhooks & tokens (`xoxb-*`, `xoxa-*`, etc.)
- Anthropic API keys (`sk-ant-api03-*`)
- OpenAI API keys (`sk-*`)
- AWS keys (`AKIA*`)
- JWT tokens (`eyJ*`)
- Private keys (`-----BEGIN PRIVATE KEY-----`)
- Database connection strings (`postgres://user:pass@host`)
- Generic API keys (`api_key=...`, `token=...`, `password=...`)
**Test**:
```bash
# Scan current files
gitleaks detect --no-git
# Scan staged files (pre-commit)
gitleaks protect --staged
# Scan full git history
gitleaks detect
```
### 2. Pre-commit Hook (`.husky/pre-commit`)
**Purpose**: Block commits containing secrets
**Behavior**:
- Runs automatically before every `git commit`
- Scans only staged files (fast)
- Provides helpful error messages if secrets detected
- Gracefully skips if gitleaks not installed (with warning)
**Example Output**:
```bash
🔒 Scanning staged files for secrets...
✅ No secrets detected
```
Or if secret detected:
```bash
❌ Secret(s) detected in staged files!
To fix:
1. Remove the secret from the file
2. Use environment variables instead: ${SECRET_NAME}
3. Add to .env.local (which is in .gitignore)
4. Update agent-orchestrator.yaml.example with placeholder values
If this is a false positive, update .gitleaks.toml allowlist
```
**Setup**:
- Husky installed as dev dependency
- Hook is executable and version-controlled
- `prepare` script ensures hook is installed on `pnpm install`
### 3. GitHub Actions Security Workflow (`.github/workflows/security.yml`)
**Purpose**: Automated security scanning in CI/CD
**Jobs**:
1. **Gitleaks** — Scans full git history on every push/PR
2. **Dependency Review** — Scans PRs for vulnerable dependencies
3. **NPM Audit** — Detects known vulnerabilities in dependencies
**Triggers**:
- Every push to `main`
- Every pull request to `main`
- Weekly scheduled scan (Monday 8am UTC)
**Benefits**:
- Catches secrets missed by pre-commit hook
- Prevents secrets from reaching main branch
- Alerts on dependency vulnerabilities
- Provides security badge for repo
### 4. Updated `.gitignore`
**Purpose**: Prevent accidental commits of secret files
**Added Patterns**:
```gitignore
# Environment files
.env
.env.local
.env.*.local
.env.production.local
.env.development.local
.env.test.local
# Credentials and secrets
*.key
*.pem
*.p12
*.pfx
*.cer
*.crt
*.der
*.csr
secrets.yaml
secrets.yml
credentials.json
credentials.yaml
*-credentials.*
.secrets/
.credentials/
# API keys and tokens
.token
.api-key
*-token.txt
*-api-key.txt
# Cloud provider credentials
.aws/
.gcloud/
.azure/
# SSH keys
id_rsa
id_dsa
id_ecdsa
id_ed25519
*.ppk
# Local config (may contain secrets)
agent-orchestrator.yaml
```
**Critical**: `agent-orchestrator.yaml` is now ignored because it contains user secrets
### 5. Documentation
**Created/Updated**:
1. **[SECURITY.md](../SECURITY.md)** — Security policy & best practices
- Responsible disclosure process
- Historical audit findings
- Developer best practices
- User best practices
- Required secrets table
- Security tools reference
2. **[README.md](../README.md)** — Added security section
- How secret scanning works
- Link to SECURITY.md
- Environment variable usage examples
- Required secrets table
3. **[docs/DEVELOPMENT.md](./DEVELOPMENT.md)** — Developer security guide
- Secret scanning during development
- What triggers the scanner
- How to handle false positives
- Environment variable conventions
- Testing locally
**Key Messages**:
- ⚠️ **NEVER commit real secrets to git**
- ✅ **Always use environment variables**
- ✅ **Pre-commit hook will block secrets**
- ✅ **CI will catch anything that slips through**
---
## Verification
### Automated Scans
```bash
# ✅ Current codebase scan
$ gitleaks detect --no-git
INFO: scanned ~1.46 MB in 79.9ms
INFO: no leaks found
# ⚠️ Full git history scan
$ gitleaks detect
WARN: leaks found: 1
Finding: OpenClaw token in commit 0393ab70 (documented)
```
### Security Checklist
- [x] Gitleaks configuration created and tested
- [x] Pre-commit hook installed and working
- [x] GitHub Actions security workflow added
- [x] `.gitignore` updated with secret patterns
- [x] SECURITY.md created with disclosure process
- [x] README.md updated with security section
- [x] Development docs updated with security practices
- [x] All example configs use placeholders (not real secrets)
- [x] Test files use dummy values (not real secrets)
- [x] Documentation clarifies which env vars are required
---
## Recommendations
### Immediate Actions
1. **Rotate OpenClaw Token** (if still in use)
- Generate new token
- Update deployment configs
- Revoke old token
2. **Set Up Required Environment Variables**
```bash
# Add to ~/.zshrc or ~/.bashrc
export GITHUB_TOKEN="ghp_..."
export LINEAR_API_KEY="lin_api_..."
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
```
3. **Verify Pre-commit Hook Works**
```bash
# Try committing a fake secret (should be blocked)
echo "token=ghp_XXXX_fake_token_for_testing_XXXX" > test.txt
git add test.txt
git commit -m "test" # Should fail with error message
rm test.txt
```
### Ongoing Practices
1. **Code Review**: Check PRs for hardcoded credentials
2. **Token Rotation**: Rotate tokens every 90 days
3. **Minimal Permissions**: Use read-only tokens when possible
4. **Secret Management**: Consider 1Password, AWS Secrets Manager, etc.
5. **Monitor CI**: Watch for security workflow failures
6. **Update Dependencies**: Keep gitleaks and dependencies up-to-date
### Before Open-Sourcing
- [ ] Verify all historical secrets have been rotated
- [ ] Confirm no production secrets in git history
- [ ] Add security badge to README
- [ ] Set up security@composio.dev email alias
- [ ] Enable GitHub security features:
- [ ] Dependabot alerts
- [ ] Code scanning
- [ ] Secret scanning (if available for public repos)
---
## Tools Used
| Tool | Purpose | Version |
| ----------------------------------------------------- | --------------- | ------- |
| [Gitleaks](https://github.com/gitleaks/gitleaks) | Secret scanning | 8.x |
| [Husky](https://typicode.github.io/husky/) | Git hooks | 9.1.7 |
| [GitHub Actions](https://github.com/features/actions) | CI/CD security | — |
---
## Summary Statistics
- **Files Scanned**: 1.46 MB
- **Git Commits Scanned**: 404
- **Historical Secrets Found**: 1 (documented, requires rotation)
- **Current Secrets Found**: 0
- **False Positives**: 0 (test files allowlisted)
- **Time to Scan**: ~80ms (current), ~960ms (full history)
---
## Conclusion
✅ **Agent Orchestrator is now protected against secret leaks**
The codebase is currently clean, with one historical secret that needs rotation. Comprehensive automated scanning prevents future accidents. All developers are protected by pre-commit hooks, and CI/CD ensures nothing reaches the main branch.
**Next Steps**:
1. Rotate the OpenClaw token if still in use
2. Test the pre-commit hook locally
3. Monitor CI for security workflow runs
4. Review SECURITY.md before first public release
---
**Audit completed**: 2026-02-16
**Approved for**: Local development, internal testing
**Before open-sourcing**: Rotate historical secrets, verify no production credentials

View File

@ -1,743 +0,0 @@
<!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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,573 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Runtime Terminal Port and Project-ID Hardening</title>
<style>
:root {
--bg: #0b1020;
--panel: #111a33;
--text: #e5ecff;
--muted: #9fb0df;
--accent: #71a2ff;
--border: #2a3d74;
--code: #0e1530;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: radial-gradient(1200px 700px at 10% -20%, #223562 0%, var(--bg) 50%);
color: var(--text);
line-height: 1.55;
}
main {
max-width: 980px;
margin: 32px auto;
padding: 24px;
background: color-mix(in srgb, var(--panel) 92%, black 8%);
border: 1px solid var(--border);
border-radius: 14px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
}
h1, h2, h3 { margin-top: 1.25em; line-height: 1.25; }
h1 { margin-top: 0; font-size: 1.9rem; }
h2 {
font-size: 1.3rem;
border-bottom: 1px solid var(--border);
padding-bottom: 0.35rem;
}
h3 { font-size: 1.05rem; color: #c8d8ff; }
p, li { color: var(--text); }
.meta {
display: grid;
grid-template-columns: repeat(2, minmax(180px, 1fr));
gap: 10px 20px;
margin: 12px 0 22px;
color: var(--muted);
}
.meta strong { color: var(--text); }
ul, ol { padding-left: 1.2rem; }
code {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.95em;
background: var(--code);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.06rem 0.35rem;
color: #c6f0ff;
}
hr {
border: none;
border-top: 1px solid var(--border);
margin: 1.4rem 0;
}
.muted { color: var(--muted); }
</style>
</head>
<body>
<main>
<h1>Runtime Terminal Port and Project-ID Hardening</h1>
<div class="meta">
<div><strong>Status:</strong> Draft</div>
<div><strong>Author:</strong> Agent Orchestrator</div>
<div><strong>Date:</strong> 2026-03-28</div>
<div><strong>Target Merge:</strong> <code>main</code></div>
</div>
<hr />
<h2>Overview</h2>
<p>
This design documents two production issues that surfaced in first-run and npm-installed flows:
</p>
<ol>
<li>Direct terminal stuck on <code>CONNECTING...</code> when runtime ports differ from client bundle fallback.</li>
<li><code>/api/spawn</code> receiving a session ID as <code>projectId</code>, leading to deep-core <code>Unknown project</code> failures.</li>
</ol>
<p>
The implementation introduces runtime configuration discovery for terminal WebSocket connection and stricter semantic validation for project identifiers at API and page-data boundaries.
</p>
<h2>Problem Statement</h2>
<h3>Problem A: Terminal WebSocket Port Drift</h3>
<ul>
<li><code>ao start</code> can auto-select terminal ports at runtime (for example <code>14802/14803</code>) when defaults are occupied.</li>
<li>Direct terminal server listens on <code>DIRECT_TERMINAL_PORT</code> at runtime.</li>
<li>Browser client previously relied on build-time <code>NEXT_PUBLIC_DIRECT_TERMINAL_PORT</code>, with fallback <code>14801</code>.</li>
<li>In prebuilt Next.js client bundles, <code>NEXT_PUBLIC_*</code> values are embedded at build time.</li>
<li>Result: client can attempt <code>ws://...:14801</code> while server is on <code>14803</code>, leaving UI in permanent <code>CONNECTING</code>.</li>
</ul>
<h3>Problem B: Project ID / Session ID Domain Confusion</h3>
<ul>
<li>Session IDs and project IDs share syntax (<code>[a-zA-Z0-9_-]+</code>).</li>
<li>Orchestrator session IDs are generated as <code>${project.sessionPrefix}-orchestrator</code>, which can visually resemble project keys.</li>
<li><code>/api/spawn</code> previously validated only identifier shape, not membership in <code>config.projects</code>.</li>
<li>Invalid but syntactically valid IDs reached core spawn logic and failed late with <code>500</code>.</li>
</ul>
<h2>Root Cause Analysis</h2>
<h3>A. Build-Time vs Runtime Config Boundary Mismatch</h3>
<ul>
<li>The server process controls actual runtime port assignment.</li>
<li>The browser bundle cannot safely depend on build-time env values for runtime-selected ports.</li>
<li>There was no first-party runtime endpoint for client port discovery.</li>
</ul>
<h3>B. Namespace Collision and Inconsistent Validation</h3>
<ul>
<li>Project IDs and session IDs were treated as plain strings at API boundaries.</li>
<li>Some routes sanitize project filters against config; others previously did not.</li>
<li>Validation was format-only in <code>/api/spawn</code> instead of semantic (<code>is configured project</code>).</li>
</ul>
<h2>Goals</h2>
<ol>
<li>Make direct terminal connection deterministic across runtime-selected ports in prebuilt deployments.</li>
<li>Ensure <code>/api/spawn</code> rejects non-configured project IDs early and predictably.</li>
<li>Normalize dashboard project filter values so invalid query state cannot poison project context.</li>
<li>Preserve backwards compatibility for existing default-port setups.</li>
</ol>
<h2>Non-Goals</h2>
<ol>
<li>Redesign session ID format.</li>
<li>Introduce full typed ID wrappers across all packages in this change.</li>
<li>Remove existing reverse-proxy path-based WS support.</li>
</ol>
<h2>Proposed Design</h2>
<h3>1. Runtime Terminal Config Endpoint</h3>
<p>Add <code>GET /api/runtime/terminal</code> (dynamic, no-store):</p>
<ul>
<li><code>terminalPort</code> from <code>TERMINAL_PORT</code> (normalized, fallback <code>14800</code>)</li>
<li><code>directTerminalPort</code> from <code>DIRECT_TERMINAL_PORT</code> (normalized, fallback <code>14801</code>)</li>
<li><code>proxyWsPath</code> from <code>TERMINAL_WS_PATH</code>/<code>NEXT_PUBLIC_TERMINAL_WS_PATH</code> (normalized path or <code>null</code>)</li>
</ul>
<h3>2. Runtime-Aware DirectTerminal Connection</h3>
<p>Update client connection flow:</p>
<ol>
<li>Resolve build-time values if available.</li>
<li>Fetch <code>/api/runtime/terminal</code> before socket connect when needed.</li>
<li>Parse and normalize returned values.</li>
<li>Build WS URL from runtime values.</li>
<li>Reuse the same runtime-aware logic on reconnect attempts.</li>
</ol>
<p class="muted">Default ports remain unchanged; runtime-shifted ports become deterministic.</p>
<h3>3. Semantic Project Validation in <code>/api/spawn</code></h3>
<p>Before calling spawn, verify <code>config.projects[projectId]</code> exists. If missing:</p>
<ul>
<li>Return <code>404</code> with <code>Unknown project: &lt;id&gt;</code></li>
<li>Record structured observability failure reason</li>
<li>Do not invoke core spawn path</li>
</ul>
<h3>4. Dashboard Project Filter Normalization</h3>
<ul>
<li>keep <code>"all"</code></li>
<li>keep only configured project IDs</li>
<li>otherwise fallback to first valid configured project (or primary fallback)</li>
</ul>
<h2>Flow Chart</h2>
<style>
.flow-section {
margin: 1.5rem 0 2.5rem;
padding: 1.5rem;
background: rgba(0,0,0,0.2);
border: 1px solid var(--border);
border-radius: 12px;
}
.flow-section-title {
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--accent);
margin: 0 0 1.2rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
.flow {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
}
.flow-node {
position: relative;
padding: 12px 20px;
border-radius: 10px;
text-align: center;
max-width: 520px;
width: 100%;
font-size: 0.88rem;
line-height: 1.45;
}
.flow-node strong { color: #fff; }
.flow-node code {
font-size: 0.82rem;
padding: 0.04rem 0.3rem;
}
.node-action {
background: linear-gradient(135deg, #1a2a55, #1e3468);
border: 1px solid #3456a0;
color: #cdd9f5;
}
.node-decision {
background: linear-gradient(135deg, #2a1a44, #3a2060);
border: 1px solid #6a4ca0;
color: #ddd0f5;
border-radius: 10px;
padding: 14px 24px;
}
.node-success {
background: linear-gradient(135deg, #0d2a1a, #143a24);
border: 1px solid #2a8050;
color: #b0e8c8;
}
.node-error {
background: linear-gradient(135deg, #2a0d0d, #3a1414);
border: 1px solid #803030;
color: #e8b0b0;
}
.node-endpoint {
background: linear-gradient(135deg, #1a2040, #1e2855);
border: 1px solid #4070b0;
color: #a8c8f0;
}
.flow-arrow {
display: flex;
flex-direction: column;
align-items: center;
color: var(--muted);
font-size: 0.75rem;
padding: 2px 0;
line-height: 1;
}
.flow-arrow .arrow-line {
width: 2px;
height: 14px;
background: var(--border);
}
.flow-arrow .arrow-head {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid var(--border);
}
.flow-arrow .arrow-label {
margin: 2px 0;
color: var(--accent);
font-weight: 600;
}
.flow-branch {
display: flex;
gap: 16px;
width: 100%;
max-width: 560px;
align-items: flex-start;
justify-content: center;
}
.flow-branch-arm {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
gap: 0;
}
.branch-label {
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 3px 10px;
border-radius: 20px;
margin-bottom: 2px;
}
.branch-yes { background: #1a3a24; color: #70d898; border: 1px solid #2a6040; }
.branch-no { background: #3a1a1a; color: #e88080; border: 1px solid #603030; }
.branch-match { background: #1a2a44; color: #70a8e8; border: 1px solid #2a5080; }
.flow-node .sub {
display: block;
font-size: 0.78rem;
color: var(--muted);
margin-top: 4px;
}
/* Filter table */
.filter-table {
width: 100%;
max-width: 540px;
border-collapse: separate;
border-spacing: 0;
margin: 0 auto;
border-radius: 10px;
overflow: hidden;
border: 1px solid var(--border);
font-size: 0.85rem;
}
.filter-table th {
background: #1a2550;
color: var(--accent);
text-align: left;
padding: 10px 16px;
font-weight: 700;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.filter-table td {
padding: 9px 16px;
border-top: 1px solid var(--border);
}
.filter-table tr:nth-child(even) td { background: rgba(0,0,0,0.15); }
.filter-table tr:nth-child(odd) td { background: rgba(0,0,0,0.08); }
.filter-table .result-keep { color: #70d898; }
.filter-table .result-fallback { color: #e8c870; }
</style>
<!-- TERMINAL CONNECTION FLOW -->
<div class="flow-section">
<div class="flow-section-title">Terminal Connection Flow</div>
<div class="flow">
<div class="flow-node node-action">
<strong>1.</strong> User runs <code>ao start &lt;project-path&gt;</code>
</div>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-action">
<strong>2.</strong> CLI runtime setup — <code>buildDashboardEnv()</code>
<span class="sub">Picks <code>TERMINAL_PORT</code> / <code>DIRECT_TERMINAL_PORT</code> at runtime</span>
</div>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-action">
<strong>3.</strong> <code>start-all</code> launches servers
<span class="sub">Next.js server + direct-terminal-ws on <code>DIRECT_TERMINAL_PORT</code></span>
</div>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-action">
<strong>4.</strong> Browser opens session page
</div>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-decision">
<strong>5.</strong> <code>resolveConnectionConfig()</code>
<span class="sub">Build-time <code>NEXT_PUBLIC_*</code> values available and valid?</span>
</div>
<div class="flow-arrow"><div class="arrow-line" style="height:6px"></div></div>
<div class="flow-branch">
<div class="flow-branch-arm">
<span class="branch-label branch-yes">Yes</span>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-success" style="font-size:0.82rem;">
Use build-time values directly
</div>
</div>
<div class="flow-branch-arm">
<span class="branch-label branch-no">No</span>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-endpoint" style="font-size:0.82rem;">
<code>GET /api/runtime/terminal</code>
<span class="sub">Read runtime env ports, normalize, return config</span>
</div>
</div>
</div>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-label">merge</div><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-action">
<strong>6.</strong> Client builds WS URL from resolved runtime config
</div>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-action">
<strong>7.</strong> WebSocket connect to direct-terminal-ws
</div>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-action">
<strong>8.</strong> Resolve tmux session
<span class="sub">Exact match first → hash-prefixed suffix fallback</span>
</div>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-success">
<strong>9.</strong> PTY attach succeeds — terminal interactive (<code>CONNECTED</code>)
</div>
</div>
</div>
<!-- SPAWN PATH FLOW -->
<div class="flow-section">
<div class="flow-section-title">Spawn Path</div>
<div class="flow">
<div class="flow-node node-action">
<strong>A.</strong> User triggers spawn
<span class="sub">Dashboard / mobile / API caller</span>
</div>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-endpoint">
<strong>B.</strong> <code>POST /api/spawn</code> with <code>body.projectId</code>
</div>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-action">
<strong>C.</strong> <code>validateIdentifier(projectId)</code>
<span class="sub">Syntax check — format only</span>
</div>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-decision">
<strong>D.</strong> Semantic guard: <code>config.projects[projectId]</code> exists?
</div>
<div class="flow-arrow"><div class="arrow-line" style="height:6px"></div></div>
<div class="flow-branch">
<div class="flow-branch-arm">
<span class="branch-label branch-yes">Yes</span>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-action" style="font-size:0.82rem;">
<code>sessionManager.spawn()</code>
</div>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-success" style="font-size:0.82rem;">
<code>201 Created</code> + session payload
<span class="sub">Dashboard/SSE shows active state</span>
</div>
</div>
<div class="flow-branch-arm">
<span class="branch-label branch-no">No</span>
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
<div class="flow-node node-error" style="font-size:0.82rem;">
<code>404</code><code>Unknown project: &lt;id&gt;</code>
<span class="sub">Stop early — core spawn not invoked</span>
</div>
</div>
</div>
</div>
</div>
<!-- PROJECT FILTER NORMALIZATION -->
<div class="flow-section">
<div class="flow-section-title">Project Filter Normalization</div>
<div class="flow" style="gap: 12px;">
<div class="flow-node node-endpoint" style="margin-bottom:8px;">
Incoming <code>?project=&lt;value&gt;</code> from dashboard query
</div>
<table class="filter-table">
<thead>
<tr>
<th>Condition</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>value == <code>"all"</code></td>
<td class="result-keep">Keep <code>"all"</code></td>
</tr>
<tr>
<td>value ∈ configured projects</td>
<td class="result-keep">Keep value</td>
</tr>
<tr>
<td>otherwise (e.g. session ID like <code>mono-orchestrator</code>)</td>
<td class="result-fallback">Fallback to primary valid project</td>
</tr>
</tbody>
</table>
<p style="font-size:0.82rem; color:var(--muted); text-align:center; margin-top:4px;">
Invalid values cannot become active project context.
</p>
</div>
</div>
<h2>Alternatives Considered</h2>
<ol>
<li>Keep fixed ports only (<code>14800/14801</code>) and disable auto-shift. Rejected: blocks multi-instance startup and fails on legitimate port conflicts.</li>
<li>Continue relying on <code>NEXT_PUBLIC_*</code> runtime injection. Rejected: production client bundles are build-time materialized.</li>
<li>Accept any <code>projectId</code> in <code>/api/spawn</code> and let core throw. Rejected: late 500 errors and poor API ergonomics.</li>
</ol>
<h2>Risks and Mitigations</h2>
<ol>
<li>Runtime endpoint unavailable. Mitigation: client retains safe fallback and reconnect logic.</li>
<li>Reverse-proxy deployments with custom WS path. Mitigation: preserve proxy-path precedence and include runtime proxy field.</li>
<li>Behavior change for invalid project query. Mitigation: normalization affects only unknown IDs; valid IDs and <code>all</code> remain unchanged.</li>
</ol>
<h2>Validation Plan</h2>
<ul>
<li><code>GET /api/runtime/terminal</code> returns runtime env ports.</li>
<li><code>POST /api/spawn</code> returns <code>404</code> for unknown project and does not call spawn.</li>
<li>Project filter normalization keeps valid IDs, keeps <code>all</code>, and falls back on unknown IDs.</li>
<li>Existing <code>DirectTerminal</code> URL construction tests remain green.</li>
</ul>
<h2>Rollout Plan</h2>
<ol>
<li>Merge patch to main.</li>
<li>Release new npm package version containing web/client and API updates.</li>
<li>Announce behavior note:
<ul>
<li>default-port users unaffected</li>
<li>runtime-shifted port users no longer hit <code>CONNECTING</code> deadlock</li>
</ul>
</li>
</ol>
<h2>Release Checklist (Commands)</h2>
<pre><code># 1) Push branch and open PR
git push origin fix-runtime-terminal-projectid-hardening
# 2) Ensure patch changeset exists (this PR includes one)
ls .changeset/five-lamps-heal.md
# 3) CI validation (already required by repo workflows)
pnpm --filter @composio/ao-web test -- \
src/__tests__/api-routes.test.ts \
src/lib/__tests__/dashboard-page-data.test.ts \
src/components/__tests__/DirectTerminal.test.ts
# 4) After PR merge: version packages from changesets
pnpm changeset version
# 5) Commit version bumps and changelogs
git add .
git commit -m "chore(release): version packages for runtime terminal + spawn hardening"
# 6) Publish
pnpm release
# 7) Post-release smoke check
npm i -g @composio/ao@latest
ao start &lt;project-path&gt;
# verify terminal works when direct terminal port is non-default</code></pre>
<h2>Acceptance Criteria</h2>
<ol>
<li>Direct terminal connects in npm prebuilt flow even when runtime direct port is not <code>14801</code>.</li>
<li><code>/api/spawn</code> never returns <code>500</code> for unknown-but-valid-format project IDs.</li>
<li>Invalid <code>project</code> query values do not become active project state.</li>
<li>Existing default-port flows remain functional without configuration changes.</li>
</ol>
</main>
</body>
</html>

View File

@ -1,347 +0,0 @@
# State Machine Stage 2 Plan: Evidence, Detecting, And Recovery
Status: Draft
Primary issue: #95
Target branch: `sessions-redone`
Inputs:
- `~/.ao/ao-ahead/human-work/redesign.md`
- `~/.ao/ao-ahead/aa-2/state-machine-redesign-rollout-plan.md`
- `docs/state-machine-redesign.md`
- `CLAUDE.md`
## Purpose
Stage 2 is where lifecycle classification stops being a pile of loosely competing probes and becomes an explicit evidence assessment system.
The goal is not to add more statuses. The goal is to make AO answer six questions cleanly and in order:
1. what evidence exists right now
2. how fresh and trustworthy that evidence is
3. whether the evidence agrees
4. whether the system should enter `detecting`
5. whether retries are still allowed
6. whether AO should recover automatically, wait for a human, or declare a terminal outcome
This document is planning only. It defines policy, sequencing, and acceptance criteria for Stage 2. It does not prescribe code-level implementation details yet.
## Scope
Stage 2 covers:
- evidence assessment for runtime, process, activity, and PR-adjacent signals
- `detecting` state semantics and exit rules
- signal disagreement classification
- stale evidence policy by source type
- bounded retry policy for uncertain or failed probes
- recovery rules for worker and orchestrator sessions
- decision logging needed to explain why AO chose a state
Stage 2 does not cover:
- agent-authored lifecycle reporting commands
- prompt changes to force explicit acknowledgment or waiting signals
- UI redesign beyond the minimum data contracts Stage 2 requires
- full PR workflow automation redesign
## Design Position
Stage 2 should treat runtime truth, process truth, activity truth, and PR truth as separate inputs to an assessment pass. No single weak signal should be allowed to kill a session. `detecting` is the controlled buffer state used when the system cannot yet defend a confident answer.
The practical policy is:
- prefer explicit facts over heuristics
- prefer `detecting` over a false terminal label
- prefer bounded waiting over infinite ambiguity
- prefer recovery when the session is plausibly resumable
- prefer human escalation when the evidence is conflicting after retries
## Evidence Inventory
Each assessment pass should normalize evidence into a common shape with `source`, `observedAt`, `freshness`, `reliability`, and `value`.
### Source classes
- runtime evidence
- tmux/session handle exists
- runtime plugin reports reachable or unreachable
- process evidence
- agent process scan reports running or not running
- pid metadata exists or is missing
- activity evidence
- structured agent activity
- AO-generated activity markers
- terminal output heartbeat only as weak fallback
- recovery evidence
- provider-specific session resumability
- known recovery action available
- PR evidence
- PR open, merged, closed, CI pending, CI failing, review comments
- operator evidence
- explicit user kill
- explicit user continue/resume request
### Reliability tiers
- `authoritative`
- explicit user kill
- successful runtime/plugin confirmation
- successful provider resumability check
- `strong`
- process running or not running from a validated probe
- fresh structured activity from AO or provider logs
- `supporting`
- recent terminal output without structured semantics
- existing metadata from prior successful scans
- `historical`
- stale activity
- previous assessment results
## Assessment Model
Every lifecycle pass should produce an evidence report before it produces a session state.
### Pass order
1. collect the latest evidence for runtime, process, activity, recovery, and PR
2. grade each evidence item for freshness and reliability
3. detect contradictions
4. decide whether the contradiction is resolvable within retry bounds
5. emit one of:
- confident classification
- `detecting`
- terminal outcome with explicit reason
- recovery-needed classification
### Required outputs
Each pass should produce:
- session assessment outcome
- primary reason
- evidence summary
- disagreement summary if any
- retry counter state
- recovery recommendation if any
## `detecting` State Semantics
`detecting` is not a synonym for `unknown`. It is an active assessment window where AO is intentionally trying to resolve conflicting, missing, or failed evidence.
### Enter `detecting` when
- runtime and process signals disagree
- runtime is unreachable but fresh activity still exists
- process probe fails in a way that could be transient
- activity is recent enough to block a dead classification but not enough to confirm health
- recovery appears possible but the recovery preconditions are not yet confirmed
- required probes time out or return incomplete data
### Do not enter `detecting` when
- user kill is explicit and confirmed
- runtime and process are both confirmed dead and no recovery path exists
- session is intentionally terminal for a reason independent of liveness
- the same disagreement has already exhausted the retry budget
### Exit `detecting` when
- evidence converges on a confident non-terminal state
- evidence converges on a terminal outcome
- bounded retries are exhausted and the result must be escalated
- recovery succeeds and a new healthy state is established
- recovery is impossible and the terminal reason is clear
## Signal Disagreement Handling
Disagreement handling must be explicit. Stage 2 should classify disagreements instead of burying them inside ad hoc fallthrough logic.
### Core disagreement classes
- `runtime_alive_process_dead`
- `runtime_dead_process_alive`
- `runtime_dead_recent_activity`
- `process_alive_no_recent_activity`
- `probe_failed_runtime_unknown`
- `probe_failed_process_unknown`
- `pr_terminal_runtime_alive`
- `recovery_possible_runtime_uncertain`
### Resolution policy
- `runtime_alive_process_dead`
- treat as likely recoverable worker failure
- enter `detecting`
- retry process probe
- evaluate resumability before declaring terminal
- `runtime_dead_process_alive`
- treat process evidence as suspicious
- prefer `detecting`
- retry runtime probe
- downgrade process evidence if the runtime container/session cannot be confirmed
- `runtime_dead_recent_activity`
- treat stale-vs-live timing carefully
- recent structured activity blocks immediate death
- historical activity does not
- `process_alive_no_recent_activity`
- do not classify as dead
- remain in current workflow state or `idle`/`stuck` depending on broader context
- `pr_terminal_runtime_alive`
- do not kill solely because the PR state is terminal
- classify workflow and runtime separately
## Stale Evidence Policy
Stale evidence should explain history, not overrule current hard facts.
### Freshness windows
- structured activity
- `0s-60s`: strong support for liveness
- `61s-5m`: weak support for liveness, enough to block immediate death on its own
- `>5m`: historical only
- terminal heartbeat output
- `0s-30s`: supporting only
- `31s-2m`: weak historical context
- `>2m`: ignore for liveness decisions
- runtime/process probe results
- `0s-30s`: current
- `31s-90s`: usable but should be refreshed before a terminal classification
- `>90s`: stale for decisive outcomes
- PR state
- remains authoritative for PR truth
- does not become authoritative for runtime death
### Rules
- stale activity cannot by itself prove the session is alive
- stale activity can explain why AO avoided a dead classification earlier
- a terminal outcome requires current hard evidence or an explicit operator action
- recovery eligibility may use stale evidence only to rank options, not to assert that recovery is safe
## Bounded Retry Policy
`detecting` must be temporary. Every disagreement path needs a fixed retry budget and an explicit escalation outcome.
### Retry budgets
- transient probe failures
- retry up to 3 times
- exponential spacing such as immediate, 10s, 30s
- hard disagreement with partial evidence
- retry up to 2 full assessment passes after the initial pass
- recovery attempt verification
- allow 1 recovery attempt per assessment cycle unless the operator explicitly asks again
### Retry stop conditions
- decisive evidence arrives
- the retry budget is exhausted
- an operator action overrides automated handling
- the recovery subsystem returns a definitive non-recoverable result
### Escalation after retries
When retries are exhausted, AO should not loop forever. It should produce one of:
- `stuck` with a clear reason if human action is needed
- terminal with explicit reason if death is defensible
- recovery-pending-human if recovery exists but is unsafe to auto-run
## Recovery Rules
Recovery must be policy-driven, not implicit.
### Worker sessions
Auto-recovery is allowed when:
- the runtime is reachable or can be recreated safely
- the provider session is resumable or a new worker can be created with preserved context
- the failure reason is operational rather than semantic
- there is no explicit user kill or explicit human stop condition
Human-gated recovery is required when:
- the evidence is still contradictory after retries
- PR state suggests the worker should be preserved rather than replaced
- the replacement action could duplicate work or create PR confusion
- permissions or credentials are required
Do not auto-recover when:
- the user explicitly killed the session
- the provider reports the session as non-resumable and replacement would violate workflow policy
- the session is already intentionally terminal
### Orchestrator sessions
Orchestrators should have a higher bar for terminal classification and auto-recovery attempts because they coordinate other sessions.
Policy:
- prefer recovery over terminal classification
- require stronger evidence before declaring them dead
- never tie orchestrator death to PR state
- surface unresolved orchestrator disagreements prominently for human review
## Recovery Outcome Matrix
### Recover automatically
- worker runtime reachable, process dead, resumable session available
- worker runtime reachable, process dead, replacement worker policy explicitly allows restart
### Wait for human
- retries exhausted with unresolved disagreement
- PR merged or closed but runtime is still alive and policy choice is needed
- recovery would create a second worker or reopen a completed workflow
### Declare terminal
- explicit user kill confirmed
- runtime and process both confirmed dead after fresh probes
- recovery path explicitly unavailable and workflow policy allows termination
## Decision Logging Requirements
Stage 2 needs durable reasoning trails so the dashboard and operators can understand why a status changed.
Each assessment result should record:
- prior session state and reason
- new session state and reason
- major evidence items considered
- freshness classification for each decisive signal
- disagreement code if one existed
- retry count and remaining budget
- recovery action attempted, skipped, or blocked
## Acceptance Criteria
Stage 2 is ready when the plan can support these guarantees:
- AO no longer declares a session dead from one weak signal
- `detecting` has defined entry and exit conditions
- every disagreement class has a retry policy and an escalation outcome
- stale evidence has explicit freshness windows by source type
- worker and orchestrator recovery policies differ where operationally necessary
- every terminal outcome has a defendable reason
- operators can inspect why AO chose the current state
## Suggested Implementation Sequence
1. define evidence data shapes and disagreement codes
2. define freshness grading and stale evidence rules
3. extract lifecycle assessment into a dedicated evaluation step
4. wire `detecting` entry, exit, and retry counters
5. define recovery policy gates for worker versus orchestrator sessions
6. add decision logging and test scenarios for each disagreement class
## Open Decisions To Confirm Before Coding
- whether the activity freshness windows should differ by agent provider
- whether terminal output should be considered at all once structured activity exists
- whether worker replacement and session resume are separate recovery classes in Stage 2 or deferred to Stage 3
- whether retry counters live in session metadata or are recomputed from assessment history
- whether `stuck` is emitted directly by Stage 2 or remains a later projection over assessment outcomes