chore: remove tracked repo artifacts (#1931)

This commit is contained in:
whoisasx 2026-05-20 20:55:48 +05:30
parent 5d0b624fbe
commit 2a310c679e
24 changed files with 8 additions and 3430 deletions

4
.gitignore vendored
View File

@ -7,6 +7,10 @@ dist/
coverage/
*.patch
*-context.md
# Local/generated investigation artifacts
.issue-assets/
handoff/
artifacts/
packages/web/screenshots/
.playwright-cli/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -1,784 +0,0 @@
# Architecture Design — Agent Orchestrator
_Compiled: 2026-02-13_
## Core Philosophy
**Push, not pull.** The human never polls. The human never checks a dashboard wondering "what's happening?" The system pushes notifications to the human exactly when their attention is needed — and stays silent otherwise.
The dashboard is a **drill-down tool** you open after receiving a notification, not something you sit and watch. The **Notifier is the primary interface.**
### Interaction Model
```
Human spawns 20 agents → walks away → lives their life
┌───────────────────────────────┘
Orchestrator runs autonomously:
├── Agents work on issues
├── CI fails? → auto-send fix to agent → resolved silently
├── Review comments? → auto-send to agent → resolved silently
├── Agent stuck? → NOTIFY HUMAN
├── Agent needs input? → NOTIFY HUMAN
├── PR ready to merge? → NOTIFY HUMAN (or auto-merge if configured)
├── Agent errored? → NOTIFY HUMAN
└── All done? → NOTIFY HUMAN with summary
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**: 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.
6. **Runtime agnostic**: tmux is just one way to run agents. Docker, K8s, cloud, SSH, child processes — all through the same interface.
---
## Nomenclature
| Term | Definition | Examples |
| ---------------- | ------------------------------------------ | -------------------------------- |
| **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 |
| **Agent** | The AI coding tool being used | claude-code, codex, aider |
| **Workspace** | Isolated code copy for a session | git worktree, clone, volume |
| **Tracker** | Issue/task tracking system | github, linear, jira |
| **SCM** | Source code management platform | github, gitlab, bitbucket |
| **Notifier** | Communication/alert channel | slack, discord, desktop, webhook |
| **Terminal** | Human interaction interface | iterm2, web terminal, none |
---
## System Architecture
```
┌──────────────────────────────────────┐
CLI ───REST───► │ Orchestrator Server │
│ (Next.js) │
Web ───REST/───► │ │
SSE │ ┌────────────┐ ┌────────────────┐ │
│ │ Session │ │ Plugin │ │
Agents ────────► │ │ Manager │ │ Registry │ │
(heartbeat/ │ └──────┬─────┘ └───────┬────────┘ │
webhook) │ │ │ │
│ ┌──────┴─────┐ ┌───────┴────────┐ │
│ │ Lifecycle │ │ Config │ │
│ │ Manager │ │ Manager │ │
│ └──────┬─────┘ └────────────────┘ │
│ │ │
│ ┌──────┴──────────────────────────┐ │
│ │ Event Bus │ │
│ │ (pub/sub + persistence) │ │
│ └──┬──────┬──────┬──────┬────────┘ │
└─────┼──────┼──────┼──────┼──────────┘
│ │ │ │
┌───────┘ │ │ └───────┐
▼ ▼ ▼ ▼
┌─────────┐ ┌────────┐ ┌────────┐ ┌─────────┐
│ SSE → │ │Notifier│ │Reaction│ │ Event │
│ Web UI │ │Plugins │ │ Engine │ │ Log │
└─────────┘ └────────┘ └────────┘ └─────────┘
```
### Data Flow
1. **Agent → Server**: Heartbeats, status updates, "need input" signals
2. **Server → Dashboard**: SSE stream of session state changes
3. **Server → Notifiers**: Alerts when human attention is needed
4. **Server → Agents**: Commands via runtime-specific channels (tmux send-keys, docker exec, HTTP POST, etc.)
5. **CLI → Server**: REST API calls for spawn, kill, send, status
6. **SCM → Server**: PR state, CI checks, review comments (polled or webhooks)
---
## The 8 Plugin Slots
### 1. Runtime — Where sessions execute
```typescript
interface Runtime {
readonly name: string;
// Lifecycle
create(session: SessionConfig): Promise<RuntimeHandle>;
destroy(handle: RuntimeHandle): Promise<void>;
// Communication
sendMessage(handle: RuntimeHandle, message: string): Promise<void>;
getOutput(handle: RuntimeHandle, lines?: number): Promise<string>;
// Health
isAlive(handle: RuntimeHandle): Promise<boolean>;
getMetrics(handle: RuntimeHandle): Promise<RuntimeMetrics>;
// Optional: interactive access
attach?(handle: RuntimeHandle): Promise<AttachInfo>;
}
```
| Implementation | How it works | Best for |
| ---------------- | ------------------------------ | ------------------------------ |
| `tmux` (default) | tmux sessions + send-keys | Local development, interactive |
| `process` | Child processes + stdin/stdout | Headless, CI/CD, scripting |
| `docker` | Docker containers + exec | Isolation, reproducibility |
| `kubernetes` | K8s pods/jobs | Scale, enterprise |
| `ssh` | SSH to remote + tmux/process | Remote machines |
| `e2b` | E2B SDK (Firecracker microVMs) | Cloud sandboxes |
| `fly` | Fly.io Machines API | Cost-effective cloud |
| `modal` | Modal Sandboxes | GPU, autoscaling |
### 2. Agent — AI coding tool
```typescript
interface Agent {
readonly name: string;
readonly processName: string; // for detection
// Launch
getLaunchCommand(session: SessionConfig, project: ProjectConfig): string;
getEnvironment(session: SessionConfig): Record<string, string>;
// Activity detection
detectActivity(session: Session): Promise<ActivityState>;
isProcessRunning(runtimeHandle: RuntimeHandle): Promise<boolean>;
// Introspection
introspect(session: Session): Promise<AgentIntrospection | null>;
// Optional
postLaunchSetup?(session: Session): Promise<void>;
estimateCost?(session: Session): Promise<CostEstimate>;
}
```
| Implementation | Launch command | Activity detection |
| ----------------------- | --------------------------------------- | -------------------------- |
| `claude-code` (default) | `claude --dangerously-skip-permissions` | JSONL mtime + process tree |
| `claude-headless` | `claude -p --output-format stream-json` | stdout parsing |
| `codex` | `codex` | Process detection |
| `aider` | `aider --no-auto-commits` | Process detection |
| `goose` | `goose session` | Process detection |
| `custom` | User-defined command | Configurable |
### 3. Workspace — Code isolation
```typescript
interface Workspace {
readonly name: string;
create(project: ProjectConfig, session: SessionConfig): Promise<WorkspacePath>;
destroy(path: WorkspacePath): Promise<void>;
list(project: ProjectConfig): Promise<WorkspaceInfo[]>;
// Optional hooks
postCreate?(path: WorkspacePath, project: ProjectConfig): Promise<void>;
}
```
| Implementation | How | Tradeoff |
| -------------------- | ------------------------ | ---------------------------------------- |
| `worktree` (default) | `git worktree add` | Fast, shared objects, requires same repo |
| `clone` | `git clone` | Full isolation, slower, more disk |
| `copy` | `cp -r` | No git dependency, heaviest |
| `volume` | Docker/K8s volume mounts | For container runtimes |
### 4. Tracker — Issue/task tracking
```typescript
interface Tracker {
readonly name: string;
getIssue(identifier: string): Promise<Issue>;
isCompleted(identifier: string): Promise<boolean>;
issueUrl(identifier: string): string;
branchName(identifier: string): string;
generatePrompt(identifier: string, project: ProjectConfig): string;
// Optional
listIssues?(filters?: IssueFilters): Promise<Issue[]>;
updateIssue?(identifier: string, update: IssueUpdate): Promise<void>;
createIssue?(input: CreateIssueInput): Promise<Issue>;
}
```
| Implementation | API | Auth |
| ------------------ | ----------- | -------------- |
| `github` (default) | `gh` CLI | GitHub token |
| `linear` | GraphQL API | Linear API key |
| `jira` | REST API | Jira token |
| `plain` | Local files | None |
### 5. SCM — Source code platform (PR, CI, Reviews)
```typescript
interface SCM {
readonly name: string;
// PR lifecycle
detectPR(session: Session): Promise<PRInfo | null>;
getPRState(pr: PRInfo): Promise<PRState>;
createPR(session: Session, title: string, body: string): Promise<PRInfo>;
mergePR(pr: PRInfo, method?: MergeMethod): Promise<void>;
closePR(pr: PRInfo): Promise<void>;
// CI tracking
getCIChecks(pr: PRInfo): Promise<CICheck[]>;
getCISummary(pr: PRInfo): Promise<CIStatus>;
// Review tracking
getReviews(pr: PRInfo): Promise<Review[]>;
getReviewDecision(pr: PRInfo): Promise<ReviewDecision>;
getPendingComments(pr: PRInfo): Promise<ReviewComment[]>;
getAutomatedComments(pr: PRInfo): Promise<AutomatedComment[]>;
// Merge readiness
getMergeability(pr: PRInfo): Promise<MergeReadiness>;
}
```
| Implementation | API | Features |
| ------------------ | ------------------- | -------------------------- |
| `github` (default) | `gh` CLI + REST API | Full PR/CI/review support |
| `gitlab` | REST API | MR/pipeline/review support |
| `bitbucket` | REST API | PR/pipeline support |
### 6. Notifier — THE PRIMARY INTERFACE
The notifier is not a nice-to-have — it is the primary way the system communicates with humans. The human walks away after spawning agents. Notifications bring them back only when needed.
```typescript
interface Notifier {
readonly name: string;
// Core: push a notification to the human
notify(event: OrchestratorEvent): Promise<void>;
// Optional: actionable notifications (buttons/links)
notifyWithActions?(event: OrchestratorEvent, actions: NotifyAction[]): Promise<void>;
// Optional: richer communication (post to channel)
post?(message: string, context?: NotifyContext): Promise<string | null>;
}
// Notifications can include actions the human can take directly
interface NotifyAction {
label: string; // "Merge PR", "Open Dashboard", "Kill Session"
url?: string; // Deep link to dashboard action
callback?: string; // API endpoint to call
}
```
| Implementation | Channel | Best for | Actionable? |
| ------------------- | ---------------------------- | ------------------- | ----------------------------- |
| `desktop` (default) | OS notifications (clickable) | Solo developer | Click → opens dashboard |
| `slack` | Slack messages with buttons | Teams | Buttons → merge, review, kill |
| `discord` | Discord messages | Communities | Links |
| `webhook` | HTTP POST | Custom integrations | Custom |
| `email` | Email digest | Async | Links |
**Multiple notifiers can be active simultaneously.** E.g., desktop for immediate alerts + Slack for team visibility + email for daily digest.
### 7. Terminal — Human interaction interface
```typescript
interface Terminal {
readonly name: string;
openSession(session: Session): Promise<void>;
openAll(sessions: Session[]): Promise<void>;
// Optional
isSessionOpen?(session: Session): Promise<boolean>;
}
```
| Implementation | How | Platform |
| ---------------- | ------------------------- | ------------- |
| `auto` (default) | Detect best available | Any |
| `iterm2` | AppleScript API | macOS |
| `web` | xterm.js in browser | Any |
| `tmux-attach` | `tmux attach` in terminal | Any with tmux |
| `none` | Headless | CI/CD |
### 8. Lifecycle Manager (Core — not pluggable)
The Lifecycle Manager is the orchestrator's brain. It:
- Polls SCM + Agent plugins on configurable intervals
- Maintains state machine per session
- Emits events on state transitions
- Runs configured reactions
- Feeds real-time data to dashboard via SSE
---
## Session Lifecycle State Machine
```
┌──────────┐
│ SPAWNING │
└────┬─────┘
│ runtime.create() + agent launched
┌──────────┐
┌─────│ WORKING │◄─────────────────────────┐
│ └────┬─────┘ │
│ │ PR detected │
│ ▼ │
│ ┌──────────────┐ │
│ │ PR_OPEN │ │
│ └────┬─────────┘ │
│ │ │
│ ┌────┴────────────┐ │
│ ▼ ▼ │
│ ┌──────────┐ ┌─────────────────┐ │
│ │ CI_FAILED│ │ REVIEW_PENDING │ │
│ └────┬─────┘ └────┬────────────┘ │
│ │ │ │
│ │ ┌──────────┴──────┐ │
│ │ ▼ ▼ │
│ │ ┌──────────────┐ ┌──────────┐ │
│ │ │CHANGES_REQ'D │ │ APPROVED │ │
│ │ └──────┬───────┘ └────┬─────┘ │
│ │ │ │ │
│ └────────┼───────────────┘ │
│ │ agent fixes │
│ └──────────────────────────┘
│ When approved + CI green + no conflicts:
│ ▼
│ ┌──────────┐
│ │MERGEABLE │──► auto-merge or notify human
│ └────┬─────┘
│ │
│ ▼
│ ┌──────────┐
│ │ MERGED │
│ └────┬─────┘
│ │
│ ▼
│ ┌──────────┐
│ │ CLEANUP │──► destroy workspace + archive metadata
│ └──────────┘
│ At any point:
│ ┌───────────────┐
├────►│ NEEDS_INPUT │──► notify human
│ └───────────────┘
│ ┌───────────────┐
├────►│ STUCK/IDLE │──► notify human after threshold
│ └───────────────┘
│ ┌───────────────┐
├────►│ ERRORED │──► notify human
│ └───────────────┘
│ ┌───────────────┐
└────►│ KILLED │──► cleanup
└───────────────┘
```
---
## Human Attention Optimization
**The system notifies the human. The human never polls.**
The orchestrator operates on a simple principle: handle everything you can automatically, and push a notification to the human only when their judgment or action is truly required. The human spawns agents, walks away, and gets notified.
### Two-Tier Event Handling
**Tier 1: Auto-handled (human never sees these)**
The orchestrator resolves these silently. The human is only notified if auto-resolution fails.
| Event | Auto-Response | Escalation |
| ---------------------- | -------------------------------- | -------------------------------- |
| CI failed | Send fix prompt to agent | Notify after 2 failed attempts |
| Review comments | Send "address comments" to agent | Notify if unresolved after 30min |
| Bugbot/linter comments | Send fix prompt to agent | Notify if unresolved after 30min |
| Merge conflicts | Send "rebase" to agent | Notify if unresolved after 15min |
**Tier 2: Notify human (requires human judgment)**
These always push a notification. The human's phone buzzes, Slack pings, etc.
| Event | Priority | Notification |
| -------------------------------------------------------------- | -------- | ----------------------------------------------------- |
| **Agent needs input** (permission, question, stuck) | URGENT | "Session X needs your input" + deep link |
| **Agent errored** (crashed, unrecoverable) | URGENT | "Session X crashed" + error context |
| **PR ready to merge** (approved + CI green) | ACTION | "PR #42 ready to merge" + merge button |
| **Agent idle too long** (no PR, no progress) | WARNING | "Session X idle for 15min, may need help" |
| **Auto-fix failed** (CI fix failed 2x, comments not addressed) | WARNING | "Session X couldn't resolve CI/review — needs you" |
| **All work complete** | INFO | "All 20 sessions done. 18 PRs merged, 2 need review." |
### Escalation Chains
Events start at auto-handle and escalate through notification tiers:
```
Event detected
Can auto-handle? ──yes──► Auto-respond (send to agent)
│ │
no Resolved? ──yes──► Done (silent)
│ │
▼ no (retry N times)
NOTIFY HUMAN │
│ ▼
│ NOTIFY HUMAN
│ "Tried to auto-fix, couldn't resolve"
Human acts via:
├── Notification action button (merge, kill, open)
├── Dashboard deep link
├── CLI command
└── Direct tmux attach
```
### Notification Channels (Priority-Based Routing)
Different priorities route to different channels:
```yaml
notifications:
routing:
urgent: [desktop, slack, sms] # Agent stuck, errored, needs input
action: [desktop, slack] # PR ready to merge
warning: [slack] # Auto-fix failed, idle too long
info: [slack] # Summary, all done
```
### Reactions (configurable auto-responses)
```yaml
# agent-orchestrator.yaml
reactions:
ci-failed:
auto: true
action: send-to-agent
message: "CI is failing. Run `gh pr checks` to see failures, fix them, and push."
retries: 2
escalate-after: 2 # notify human after 2 failed auto-fix attempts
changes-requested:
auto: true
action: send-to-agent
message: "Review comments on your PR. Check with `gh pr view --comments` and address each one."
escalate-after: 30m
bugbot-comments:
auto: true
action: send-to-agent
message: "Automated review comments found. Fix the issues flagged by the bot."
escalate-after: 30m
merge-conflicts:
auto: true
action: send-to-agent
message: "Your branch has merge conflicts. Rebase on the default branch and resolve them."
escalate-after: 15m
approved-and-green:
auto: false # require human confirmation by default
action: notify
priority: action
message: "PR is ready to merge"
# Set auto: true + action: auto-merge for full automation
agent-stuck:
threshold: 10m
action: notify
priority: urgent
agent-needs-input:
action: notify
priority: urgent
agent-exited:
action: notify
priority: urgent
all-complete:
action: notify
priority: info
message: "All sessions complete"
include-summary: true # PRs merged, pending, failed
agent-idle-no-pr:
threshold: 30m # working for 30min with no PR
action: notify
priority: warning
message: "Agent has been working for 30min without creating a PR"
```
### Dashboard (Secondary — Drill-Down Tool)
The dashboard exists for when you get a notification and need to drill down. It's organized by attention priority:
- **Red zone** (top): URGENT — sessions needing human input RIGHT NOW
- **Orange zone**: ACTION — PRs ready to merge, decisions needed
- **Yellow zone**: WARNING — auto-fix failed, agents idle too long
- **Green zone**: Sessions working normally (collapsed by default)
- **Grey zone**: Completed/merged (collapsed by default)
Clicking a notification deep-links directly to the relevant session/PR in the dashboard.
---
## Configuration
### Minimal Config (works out of the box)
```yaml
# agent-orchestrator.yaml
projects:
my-app:
repo: org/repo
path: ~/my-app
```
Everything else uses sensible defaults:
- Runtime: tmux
- Agent: claude-code
- Workspace: worktree
- Tracker: github (inferred from repo)
- SCM: github (inferred from repo)
- Notifier: desktop
- Terminal: auto-detect
### Full Config
```yaml
# agent-orchestrator.yaml
dataDir: ~/.agent-orchestrator # metadata storage
worktreeDir: ~/.worktrees # workspace root
port: 3000 # web dashboard port
defaults:
runtime: tmux
agent: claude-code
workspace: worktree
notifiers: [desktop]
projects:
my-app:
name: My App
repo: org/repo
path: ~/my-app
defaultBranch: main
sessionPrefix: app
# Override defaults per project
agent: claude-code
runtime: tmux
# Issue tracker
tracker:
plugin: linear
teamId: "abc-123"
# SCM (usually inferred from repo)
scm:
plugin: github
# Symlinks to copy into workspaces
symlinks: [.env, .claude]
# Commands to run after workspace creation
postCreate:
- "pnpm install"
- "claude mcp add rube --transport http https://rube.app/mcp"
# Agent-specific config
agentConfig:
permissions: skip # --dangerously-skip-permissions
model: opus
# Reaction overrides
reactions:
approved-and-green:
auto: true # enable auto-merge for this project
# Notification channels
notifiers:
slack:
plugin: slack
webhook: ${SLACK_WEBHOOK_URL}
channel: "#agent-updates"
desktop:
plugin: desktop
# Reaction defaults (can be overridden per project)
reactions:
ci-failed:
auto: true
retries: 2
escalate-after: 2
changes-requested:
auto: true
escalate-after: 30m
approved-and-green:
auto: false
agent-stuck:
threshold: 10m
agent-needs-input:
priority: high
```
---
## Tech Stack
| Segment | Choice | Why |
| ------------------- | --------------------------------------- | ---------------------------------------------------- |
| **Core library** | TypeScript | Shared types across all packages |
| **Web + API** | Next.js 15 (App Router) | SSR + API routes in one process |
| **Styling** | Tailwind CSS | Dark theme, responsive |
| **Real-time** | Server-Sent Events | One-way push, auto-reconnect, simpler than WebSocket |
| **CLI** | TypeScript + Commander.js | Shares types with core |
| **Config** | YAML + Zod validation | Human-readable, type-safe |
| **State** | Flat metadata files + Event log (JSONL) | Stateless orchestrator, crash recovery |
| **Package manager** | pnpm workspaces | Fast, monorepo-native |
| **Distribution** | npm (`npx agent-orchestrator`) | Zero install |
### Why TypeScript Throughout
1. **One language** — Plugin authors only need TypeScript/JavaScript
2. **Shared types** — No serialization boundaries between core, web, CLI, plugins
3. **npm distribution**`npx agent-orchestrator` works everywhere
4. **Next.js** — Web + API server in one process, great DX
5. **Largest ecosystem** — More packages on npm than any other registry
6. **Performance is fine** — Bottleneck is AI agents, not orchestrator. We shell out to tmux/git/docker anyway.
---
## Directory Structure
```
agent-orchestrator/
├── package.json
├── pnpm-workspace.yaml
├── tsconfig.base.json
├── agent-orchestrator.yaml.example
├── packages/
│ ├── core/ # @aoagents/ao-core
│ │ └── src/
│ │ ├── types.ts # All interfaces + types
│ │ ├── config.ts # YAML config loader + Zod validation
│ │ ├── session-manager.ts # Session CRUD
│ │ ├── lifecycle-manager.ts # State machine + reactions
│ │ ├── event-bus.ts # Pub/sub + JSONL persistence
│ │ ├── plugin-registry.ts # Plugin discovery + loading
│ │ ├── metadata.ts # Flat-file read/write
│ │ └── index.ts
│ │
│ ├── cli/ # @aoagents/ao-cli → `ao` binary
│ │ └── src/
│ │ ├── index.ts # Commander.js setup
│ │ └── commands/
│ │ ├── init.ts # ao init
│ │ ├── status.ts # ao status
│ │ ├── spawn.ts # ao spawn <project> [issue]
│ │ ├── batch-spawn.ts # ao batch-spawn <project> <issues...>
│ │ ├── session.ts # ao session [ls|kill|cleanup]
│ │ ├── send.ts # ao send <session> <message>
│ │ ├── review-check.ts # ao review-check [project]
│ │ ├── dashboard.ts # ao dashboard (starts web)
│ │ └── open.ts # ao open [session|all]
│ │
│ ├── web/ # @aoagents/ao-web
│ │ ├── next.config.ts
│ │ └── src/
│ │ ├── app/
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx # Dashboard (attention-prioritized)
│ │ │ └── sessions/[id]/
│ │ │ └── page.tsx # Session detail
│ │ ├── api/
│ │ │ ├── sessions/ # CRUD + actions
│ │ │ ├── spawn/ # POST spawn
│ │ │ ├── events/ # SSE stream
│ │ │ └── health/ # Server health
│ │ └── components/
│ │ ├── SessionCard.tsx
│ │ ├── AttentionZone.tsx
│ │ ├── PRStatus.tsx
│ │ ├── CIBadge.tsx
│ │ └── Terminal.tsx # xterm.js
│ │
│ └── plugins/ # Built-in plugins
│ ├── runtime-tmux/
│ ├── runtime-process/
│ ├── runtime-docker/
│ ├── agent-claude-code/
│ ├── agent-codex/
│ ├── agent-aider/
│ ├── workspace-worktree/
│ ├── workspace-clone/
│ ├── tracker-github/
│ ├── tracker-linear/
│ ├── scm-github/
│ ├── notifier-desktop/
│ ├── notifier-slack/
│ ├── terminal-iterm2/
│ └── terminal-web/
├── artifacts/ # Research + design docs
│ ├── competitive-research.md
│ └── architecture-design.md
├── scripts/ # Original bash scripts (reference)
└── CLAUDE.md
```
---
## Implementation Phases
### Phase 1: Foundation (Dog-food ready)
- Monorepo scaffolding
- Core types + interfaces
- Config loader
- Session manager + lifecycle manager + event bus
- tmux runtime, claude-code agent, worktree workspace
- GitHub SCM (PR/CI/review tracking)
- GitHub tracker
- Desktop notifier
- CLI (init, status, spawn, session, send, dashboard)
- Web dashboard with attention-prioritized view
- SSE real-time updates
- Reaction engine (CI failed, changes requested, agent stuck)
### Phase 2: Multi-Runtime + More Plugins
- Process runtime (headless claude -p)
- Docker runtime
- Codex + Aider agent adapters
- Linear + Jira trackers
- Slack notifier
- Web terminal (xterm.js)
### Phase 3: Cloud + Scale
- Kubernetes runtime
- E2B / Fly.io runtimes
- Cost tracking
- Webhook-triggered spawning
### Phase 4: Team + Enterprise
- Dashboard auth
- Role-based access
- Remote session support
- Audit log

View File

@ -1,432 +0,0 @@
# Competitive Research — Agent Orchestration Tools
_Compiled: 2026-02-13_
## Overview
Research into 16+ projects that orchestrate AI coding agents. The goal: understand abstractions, architectures, and gaps to build the best, most extensible agent orchestrator.
---
## Tier 1: Direct Competitors (Multi-Agent Orchestrators)
### Gas Town (Steve Yegge)
- **GitHub**: https://github.com/steveyegge/gastown
- **Stack**: Go 1.23+ (~189K LOC), SQLite3, Git 2.25+, tmux 3.0+
- **Stars**: Growing rapidly (released Jan 2026)
**Architecture — MEOW Stack (Molecular Expression of Work):**
| Layer | What | How |
| ----------------------------- | ------------------------ | ------------------------------------------------------------------------------ |
| **Beads** | Atomic work units | JSONL files tracked in Git. IDs like `gt-abc12`. Universal data/control plane. |
| **Epics** | Hierarchical collections | Organize beads into tree structures for parallel/sequential execution |
| **Molecules** | Workflow graphs | Sequenced beads with dependencies, gates, loops |
| **Protomolecules & Formulas** | Reusable templates | TOML format workflow definitions |
**Agent Roles (7 roles, 2 scopes):**
| Role | Scope | Purpose |
| ------------ | ----- | --------------------------------------------------------- |
| **Mayor** | Town | Chief AI coordinator with full workspace context |
| **Deacon** | Town | Health daemon running patrol loops |
| **Dogs** | Town | Maintenance helpers |
| **Crew** | Rig | Named, persistent agents for sustained design/review work |
| **Polecats** | Rig | Ephemeral "cattle" workers spawned for specific tasks |
| **Refinery** | Rig | Merge queue manager handling conflicts |
| **Witness** | Rig | Supervises polecats, unblocks stuck work |
**Other Abstractions:**
- **Town** — Workspace directory (`~/gt/`) housing all projects
- **Rigs** — Project containers wrapping git repositories
- **Hooks** — Git worktree-based persistent storage surviving crashes
- **Convoys** — Work-tracking bundles grouping multiple beads for an agent
- **GUPP** — Agents must execute work on their hooks; scheduling persists across restarts
**Runtime Backends:** claude, gemini, codex, cursor, auggie, amp (per-rig config)
**Communication/Isolation:**
- Git worktrees for filesystem isolation per agent
- Beads/Hooks for coordination (external state, not shared context windows)
- GUPP: deterministic handoffs through version control, not LLM-judged phase gates
**Strengths:** Most architecturally ambitious. Crash recovery via git-backed Beads. Role-based agent hierarchy. Multi-agent support.
**Weaknesses:** ~$100/hr token burn, auto-merged failing tests, agents causing unexpected deletions. Go-only ecosystem. No web dashboard. Optimized for autonomous, not human-in-the-loop.
---
### Par (Coplane)
- **GitHub**: https://github.com/coplane/par
- **Stack**: Python 3.12+
- **Closest to our current approach**
**Key Abstractions:**
- **Sessions**: Single-repo isolated branches via git worktrees + tmux sessions
- **Workspaces**: Multi-repo synchronized development contexts
- **Control Center**: Unified tmux session with windows for each context
- **Labels**: Globally unique, human-readable names
**Features:**
- `par start my-feature` — creates worktree + branch + tmux session
- `par send <label> "<command>"` — execute commands in specific sessions remotely
- `par send all "<command>"` — broadcast to all sessions
- `par control-center` — unified navigation
- `.par.yaml` — automatic worktree initialization (copy .env, install deps, etc.)
- IDE integration via auto-generated `.code-workspace` files
**Strengths:** Simple, clean CLI. Very similar spirit to our system. Global-first access.
**Weaknesses:** Single runtime (tmux only). No web dashboard. No plugin system. No PR/CI tracking. No agent abstraction.
---
### CAO — CLI Agent Orchestrator (AWS Labs)
- **GitHub**: https://github.com/awslabs/cli-agent-orchestrator
- **Stack**: Python, tmux, HTTP server (localhost:9889)
**Key Abstractions:**
- **Supervisor + Workers**: Hierarchical model with three coordination patterns:
- **Handoff**: Synchronous task transfer
- **Assign**: Asynchronous spawning with callback
- **Send Message**: Direct communication to agent inboxes
- **Session Isolation**: Agents in separate tmux windows with unique `CAO_TERMINAL_ID`
- **Flows**: Cron-based scheduled agent execution
**Supported Agents:** Amazon Q CLI (default), Kiro CLI, Codex CLI, Claude Code
**Strengths:** Clean supervisor/worker hierarchy. AWS backing.
**Weaknesses:** AWS-centric. Limited ecosystem.
---
### ccswarm (nwiizo)
- **GitHub**: https://github.com/nwiizo/ccswarm
- **Stack**: Rust (2024 edition), ratatui TUI, Tokio async, OpenTelemetry
**Key Abstractions:**
- **ProactiveMaster**: Orchestration core with zero shared state (message-passing channels)
- **Specialized Agent Pools**: Frontend, Backend, DevOps, QA
- **Multi-Provider Layer**: Claude Code, Aider, OpenAI Codex, custom tools
- **Session-Persistent Manager**: Claims 93% token reduction
**Isolation:** Git worktrees per agent. Native PTY sessions (no tmux dependency).
**Strengths:** Rust performance. No tmux dependency. Good provider abstraction.
**Weaknesses:** Partially implemented (orchestrator loop WIP as of v0.4.0).
---
### agent-team (nekocode)
- **GitHub**: https://github.com/nekocode/agent-team
- **Stack**: Rust 92.8%, npm distribution
**Key Abstractions:**
- **Agent Client Protocol (ACP)**: Standardized interface across all agents
- **Process Isolation**: Each agent in its own process with UDS socket
- **Remote Access**: Interact with any agent from any terminal
**Supported Agents:** 20+: Gemini, Copilot, Claude, Goose, Cline, Blackbox, OpenHands, Qwen, Kimi, and more.
**Strengths:** Broadest agent support. Clean protocol.
**Weaknesses:** Thin orchestration. No lifecycle management. No PR/CI tracking.
---
### claude-flow (ruvnet)
- **GitHub**: https://github.com/ruvnet/claude-flow
- **Stack**: TypeScript, Node.js 20+, WebAssembly, SQLite, PostgreSQL
- **Claims**: 100K+ monthly active users, 84.8% SWE-Bench solve rate
**Key Abstractions:**
- **Swarm Topologies**: mesh, hierarchical, ring, star configurations
- **Queen-Led Hierarchies**: Strategic Queens (planning), Tactical Queens (execution), Adaptive Queens (optimization)
- **8 Worker Types**: researcher, coder, analyst, tester, architect, reviewer, optimizer, documenter
- **60+ Specialized Agents** across 8 categories
- **31+ MCP Tools** across 7 categories
- **Shared Memory**: LRU cache with SQLite persistence (WAL mode)
- **ReasoningBank**: Pattern storage with trajectory learning
- **Consensus Mechanisms**: Raft, Byzantine, Gossip, Weighted, Majority
**Extension System:**
- 17 integration hooks (pre-task, post-task, etc.)
- Custom workers (12 context-triggered background services)
- Plugin SDK with IPFS marketplace distribution
- Native MCP integration
**Strengths:** TypeScript. Feature-rich. MCP native.
**Weaknesses:** Claude-only. Overcomplicated. Questionable claims.
---
## Tier 2: Adjacent Tools (Single-Agent or Cloud-First)
### OpenHands (formerly OpenDevin)
- **GitHub**: https://github.com/OpenHands/OpenHands (67.8K stars)
- **Stack**: Python 75.5%, TypeScript/React 22.3%, Docker, Kubernetes
**Key Abstractions:**
- **Software Agent SDK**: Composable Python library
- **Runtime/Sandbox**: Docker-based sandboxed execution environments
- **Event Stream Architecture**: Event-driven communication between backend and frontend
- **ACI (Agent Computer Interface)**: Standardized tools for agent-computer interaction
**Runtime Backends:** Docker (default), Kubernetes, E2B (cloud)
**Deployment Options:** Local CLI, Desktop GUI, Cloud hosting, Enterprise K8s
**Strengths:** Most mature cloud story. Event-sourced architecture (enables replay/audit). 67K stars.
**Weaknesses:** Heavy (Docker required). Not optimized for human-in-the-loop. Single-task runs, not parallel session management.
---
### SWE-agent + SWE-ReX (Princeton NLP)
- **GitHub**: https://github.com/SWE-agent/SWE-agent + https://github.com/SWE-agent/SWE-ReX
- **Stack**: Python 94.6%
**Key Abstractions:**
- **SWEEnv**: Environment manager (thin wrapper around SWE-ReX)
- **Agent**: Configured via single YAML file
- **ACI (Agent-Computer Interface)**: Custom tools installed in container
- **Deployment**: Abstraction over execution targets
**Runtime Backends (SWE-ReX):**
- Local Docker containers
- Modal (serverless compute)
- AWS Fargate (container orchestration)
- AWS EC2 (remote machines)
- Daytona (WIP)
Agent code remains the same regardless of deployment target.
**Strengths:** Cleanest deployment abstraction. Research-backed. Massively parallel (30+ instances).
**Weaknesses:** Research-focused, not production orchestrator.
---
### Goose (Block/Square)
- **GitHub**: https://github.com/block/goose
- **Stack**: Rust 58.9%, TypeScript 33.0%, Go (temporal scheduler)
**Key Abstractions:**
- **Crate architecture**: goose (core), goose-cli, goose-server, goose-mcp, mcp-client, mcp-core
- **Sessions**: Stateful autonomous execution environments
- **Recipes**: Task automation workflows
- **Extensions**: MCP-based capability providers (1,700+ available)
- **Custom Distributions**: Preconfigured providers, extensions, and branding
**Strengths:** Rust core. MCP-native. 1,700+ extensions. Professional engineering.
**Weaknesses:** Single-agent tool. No multi-agent orchestration.
---
### Cline
- **GitHub**: https://github.com/cline/cline
- **Stack**: TypeScript, Node.js, esbuild
**Key Abstractions:**
- **Sequential Decision Loop**: Analysis → Planning → Execution → Monitoring → Iteration
- **Checkpoint System**: Workspace snapshots at each step for compare/restore
- **Context Attachments**: @file, @folder, @url, @problems
**Strengths:** Great human-in-the-loop UX. Checkpoint/restore. Multi-provider.
**Weaknesses:** VS Code only. Single-agent.
---
### Multi-Agent Coding System (Danau5tin)
- **GitHub**: https://github.com/Danau5tin/multi-agent-coding-system
- **Stack**: Python, LiteLLM/OpenRouter, Docker
- **Reached #13 on Stanford's TerminalBench**
**Key Abstractions:**
- **Orchestrator Agent**: Strategic coordinator; never touches code
- **Explorer Agent**: Read-only investigation specialist
- **Coder Agent**: Implementation specialist with write access
- **Context Store**: Persistent knowledge layer across interactions
- **Knowledge Artifacts**: Discrete, reusable context items
**Communication:** XML tags with YAML parameters for task creation/delegation.
**Key Innovation:** "Front-loading precision" — over-providing context vs. rapid iteration.
**Strengths:** Clean role separation. Context Store innovation.
**Weaknesses:** Small project. Not production-ready.
---
### CCPM (Automaze)
- **GitHub**: https://github.com/automazeio/ccpm
- **Stack**: Python, GitHub REST API, Claude Code
**Key Abstractions:**
- **5-Phase Workflow**: Brainstorm → Document → Plan → Decompose → Execute
- **GitHub Issues as Database**: Issues store specs, comments provide audit trail
- **Epic Worktrees**: Each epic spawns a dedicated worktree
- **Parallel Agent Execution**: Tasks marked `parallel: true` run concurrently
---
### AI-Agents-Orchestrator (hoangsonww)
- **GitHub**: https://github.com/hoangsonww/AI-Agents-Orchestrator
- **Stack**: Python (Flask + Socket.IO), Vue 3 + Vite, Docker/Kubernetes
**Key Abstractions:**
- **Workflow Presets**: Default (Codex→Gemini→Claude), Quick, Thorough, Review-Only, Document
- **AI Adapters**: Standardized interfaces per agent tool
- **Session Manager**: Context across workflow steps
- **Vue Dashboard**: Real-time Socket.IO with Monaco editor
---
### wshobson/agents
- **GitHub**: https://github.com/wshobson/agents
- **Stack**: Claude Code plugin ecosystem
**Key Abstractions:**
- **Plugins**: 73 plugins, 112 agents, 146 skills, 79 tools
- **Progressive Disclosure Skills**: 3-tier knowledge
- **16 Workflow Orchestrators**: review, debug, feature, fullstack, research, security, migration
- **4-Tier Model Strategy**: Opus (critical) → Inherit → Sonnet → Haiku
- **Conductor Plugin**: Context → Spec & Plan → Implement
---
## Runtime Backend Research
### Cloud Sandbox Platforms
| Platform | Startup Time | Isolation | API Style | Cost |
| ------------------- | ------------ | -------------------- | -------------------- | ----------- |
| **Docker (local)** | ~1-5s | Container namespace | Docker CLI/API | Free |
| **E2B** | ~200-400ms | Firecracker microVMs | Python/JS SDK | Pay-per-use |
| **Daytona** | ~27-90ms | OCI containers | Python/TS SDK + REST | Open source |
| **Modal Sandboxes** | Sub-second | gVisor containers | Python SDK | $0.03/hr |
| **Fly.io Machines** | ~200ms-1s | Firecracker microVMs | REST API | $0.02/hr |
### Agent-Sandbox Connection Patterns (per LangChain)
**Pattern 1: Agent IN Sandbox**
- Agent runs inside the container/VM
- Communicates outward via HTTP/WebSocket
- Pro: Direct filesystem access, mirrors local dev
- Con: API keys inside sandbox
**Pattern 2: Sandbox AS Tool**
- Agent runs on orchestrator/server
- Calls sandbox via SDK/API for code execution
- Pro: API keys secure, parallel execution
- Con: Network latency per call
### Communication Protocols
| Protocol | Use Case | Used By |
| -------------------- | ----------------------- | -------------------------------- |
| **REST API** | Request/response | OpenHands, Fly.io, Daytona |
| **WebSocket** | Bidirectional streaming | OpenHands, Claude Agent SDK |
| **stdio/subprocess** | Child process | Claude Agent SDK, Codex CLI, MCP |
| **tmux send-keys** | Terminal injection | Our orchestrator, Par, CAO |
| **SSE** | Server → client push | MCP remote transport |
### Heartbeat / Health Detection
| Pattern | Description | Used By |
| ------------------------ | ------------------------------------ | --------------------------- |
| **WebSocket ping/pong** | Periodic heartbeats | OpenHands |
| **Process polling** | Check PID alive | Claude Agent SDK |
| **tmux capture-pane** | Scrape terminal output | Our `claude-session-status` |
| **File-based signaling** | Status to shared filesystem | Our metadata files |
| **HTTP health endpoint** | `/health` or `/status` | OpenHands server |
| **JSONL mtime** | Check session file modification time | Our `claude-status` |
---
## Key Findings & Gaps
### What Everyone Does
1. **Git worktrees** = standard isolation primitive
2. **tmux** = dominant session manager for local
3. **External state > context windows** (Beads, Context Store, GitHub Issues)
4. **MCP** = emerging extension protocol
### What Nobody Does Well (Our Opportunity)
1. **Multiple runtime backends** (tmux + Docker + cloud) with same interface
2. **Multiple agent support** with proper abstraction
3. **Human-in-the-loop optimization** (our core differentiator — everyone else optimizes for autonomous)
4. **Works out of the box** with zero setup
5. **Truly extensible plugin architecture** for all concerns
6. **Beautiful web dashboard** with real-time PR/CI/review tracking
7. **Full PR lifecycle management** (CI checks, review comments, merge readiness, auto-reactions)
### Best Ideas to Steal
- **Gas Town**: Git-backed state (Beads), role-based agents, crash recovery
- **OpenHands**: Event-sourced architecture, Docker/K8s runtime abstraction
- **SWE-ReX**: Clean deployment backend interface (`swe-rex[modal]`, `swe-rex[fargate]`)
- **Par**: Simple `.par.yaml` config, global labels, broadcast to all
- **Goose**: MCP-based extensions, Rust crate architecture
- **Cline**: Checkpoint/restore system
- **Multi-Agent Coder**: Context Store, front-loading precision
- **agent-team**: Agent Client Protocol for 20+ agents
---
## Sources
- [Gas Town](https://github.com/steveyegge/gastown)
- [Gas Town Architecture Analysis](https://reading.torqsoftware.com/notes/software/ai-ml/agentic-coding/2026-01-15-gas-town-multi-agent-orchestration-framework/)
- [Gas Town: Two Kinds of Multi-Agent](https://paddo.dev/blog/gastown-two-kinds-of-multi-agent/)
- [Par](https://github.com/coplane/par)
- [CAO](https://github.com/awslabs/cli-agent-orchestrator)
- [ccswarm](https://github.com/nwiizo/ccswarm)
- [agent-team](https://github.com/nekocode/agent-team)
- [claude-flow](https://github.com/ruvnet/claude-flow)
- [OpenHands](https://github.com/OpenHands/OpenHands)
- [SWE-agent](https://github.com/SWE-agent/SWE-agent)
- [SWE-ReX](https://github.com/SWE-agent/SWE-ReX)
- [Goose](https://github.com/block/goose)
- [Cline](https://github.com/cline/cline)
- [Multi-Agent Coding System](https://github.com/Danau5tin/multi-agent-coding-system)
- [CCPM](https://github.com/automazeio/ccpm)
- [AI-Agents-Orchestrator](https://github.com/hoangsonww/AI-Agents-Orchestrator)
- [wshobson/agents](https://github.com/wshobson/agents)
- [LangChain: Two Agent-Sandbox Patterns](https://blog.langchain.com/the-two-patterns-by-which-agents-connect-sandboxes/)
- [Modal: Top Code Sandbox Products](https://modal.com/blog/top-code-agent-sandbox-products)
- [Rise of Coding Agent Orchestrators](https://www.aviator.co/blog/the-rise-of-coding-agent-orchestrators/)
- [E2B](https://e2b.dev/)
- [Daytona](https://www.daytona.io/)
- [Fly.io AI](https://fly.io/ai)

View File

@ -1,272 +0,0 @@
# Implementation Plan — Parallel Agent Work Breakdown
## Dependency Graph
```
┌─────────────────────┐
│ Phase 0: Foundation │ (sequential, orchestrator does this)
│ │
│ 1. Monorepo scaffold │
│ 2. types.ts │
│ 3. config.ts + Zod │
│ 4. plugin-registry │
│ 5. All package.json │
└──────────┬────────────┘
All Phase 1 agents work against the interfaces defined in types.ts
┌──────────┬──────────┬───┴────┬──────────┬──────────┬──────────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼
Agent 1 Agent 2 Agent 3 Agent 4 Agent 5 Agent 6 Agent 7
Core Runtime Agent SCM + CLI Web Notifier
Services Plugins Plugins Tracker Dashboard + Terminal
│ │
│ All plugins are independent of │
│ each other — pure interface impls │
│ │
└──────── CLI + Web depend on core services ─────────┘
(can code against interfaces, wire up later)
```
## Phase 0: Foundation (Sequential — Orchestrator Does This)
**Must be done first. Everything else depends on it.**
Creates the monorepo scaffold and ALL type definitions. After this, every agent has:
- A package to work in (with package.json, tsconfig)
- All interfaces defined (they just implement them)
- No ambiguity about what to build
### Deliverables
1. `pnpm-workspace.yaml` + root `package.json` + `tsconfig.base.json`
2. `packages/core/src/types.ts` — ALL interfaces (Runtime, Agent, Workspace, Tracker, SCM, Notifier, Terminal, Session, Event, Config, etc.)
3. `packages/core/src/config.ts` — Zod schemas for YAML config validation
4. `packages/core/src/plugin-registry.ts` — Plugin discovery + loading skeleton
5. `packages/core/package.json` + `tsconfig.json`
6. All plugin package scaffolds (package.json + tsconfig + src/index.ts stub)
7. `packages/cli/package.json` + `packages/web/package.json` scaffolds
8. `agent-orchestrator.yaml.example`
**Estimated effort**: Medium. ~500-800 lines of types + config.
---
## Phase 1: Parallel Implementation (7 Agents)
### Agent 1: Core Services
**Package**: `packages/core/src/`
**Branch**: `feat/core-services`
**Depends on**: Phase 0 types
**Blocked by**: Nothing after Phase 0
| File | What | Reference Script |
| ---------------------- | ---------------------------------------------------------------- | ----------------------------------------------- |
| `metadata.ts` | Flat-file metadata read/write (key=value) | Metadata parsing in all session managers |
| `event-bus.ts` | In-process pub/sub + JSONL persistence | New (inspired by OpenHands event stream) |
| `tmux.ts` | tmux command wrappers (list, new, send-keys, capture-pane, kill) | All scripts that call tmux |
| `session-manager.ts` | Session CRUD: spawn, list, kill, cleanup, send message | `claude-ao-session` |
| `lifecycle-manager.ts` | State machine per session + reaction engine | `claude-review-check` + `claude-session-status` |
**Key complexity**: session-manager.ts orchestrates Runtime + Agent + Workspace plugins together. lifecycle-manager.ts runs the polling loop and triggers reactions.
**Estimated effort**: Large (~1000-1500 lines)
---
### Agent 2: Runtime + Workspace Plugins
**Packages**: `packages/plugins/runtime-tmux/`, `runtime-process/`, `workspace-worktree/`, `workspace-clone/`
**Branch**: `feat/runtime-workspace-plugins`
**Depends on**: Phase 0 types only
**Blocked by**: Nothing after Phase 0
| Plugin | What | Reference |
| -------------------- | ------------------------------------------------------------------ | ---------------------------------- |
| `runtime-tmux` | Create/destroy tmux sessions, send-keys, capture-pane, alive check | `claude-ao-session` new/kill |
| `runtime-process` | Spawn child processes, stdin/stdout, signal handling | New (for headless `claude -p`) |
| `workspace-worktree` | `git worktree add/remove/list`, branch naming, symlinks | `claude-ao-session` worktree logic |
| `workspace-clone` | `git clone`, cleanup | New (for Docker/cloud runtimes) |
**Key complexity**: runtime-tmux must handle send-keys with proper escaping, busy detection, and the wait-for-idle pattern from `send-to-session`.
**Estimated effort**: Medium (~600-800 lines)
---
### Agent 3: Agent Plugins
**Packages**: `packages/plugins/agent-claude-code/`, `agent-codex/`, `agent-aider/`
**Branch**: `feat/agent-plugins`
**Depends on**: Phase 0 types only
**Blocked by**: Nothing after Phase 0
| Plugin | What | Reference |
| ------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `agent-claude-code` | Launch cmd, JSONL activity detection, process tree walk, introspection | `claude-status`, `get-claude-session-info`, `claude-session-status` |
| `agent-codex` | Launch cmd, process detection | New |
| `agent-aider` | Launch cmd, process detection | New |
**Key complexity**: `agent-claude-code` has the richest activity detection — reading JSONL session files, extracting summaries, walking process trees from tmux pane PID to find `claude` process, detecting working/idle/stuck/blocked states.
**Estimated effort**: Medium (~500-700 lines)
---
### Agent 4: SCM + Tracker Plugins
**Packages**: `packages/plugins/scm-github/`, `tracker-github/`, `tracker-linear/`
**Branch**: `feat/scm-tracker-plugins`
**Depends on**: Phase 0 types only
**Blocked by**: Nothing after Phase 0
| Plugin | What | Reference |
| ---------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
| `scm-github` | PR detection, CI checks, review comments, automated comments, merge readiness, merge | `claude-review-check`, `claude-bugbot-fix`, dashboard PR fetching |
| `tracker-github` | Issue fetch, completion check, branch naming, prompt generation | `claude-splitly-session` (GitHub Issues) |
| `tracker-linear` | Issue fetch via GraphQL, completion check, branch naming | `claude-ao-session` + `claude-integrator-session` Linear checks |
**Key complexity**: `scm-github` is the largest — it covers PR state, CI checks (gh pr checks), review decision (gh pr view), inline review comments (gh api), automated bot comments (cursor[bot], bugbot), and merge readiness.
**Estimated effort**: Large (~800-1000 lines)
---
### Agent 5: CLI
**Package**: `packages/cli/`
**Branch**: `feat/cli`
**Depends on**: Phase 0 types + core interfaces (codes against interfaces, wires up when core is ready)
**Partially blocked by**: Agent 1 (core services) for runtime testing
| Command | What | Reference Script |
| -------------------------------------- | ---------------------------------------------------- | ----------------------------------- |
| `ao init` | Interactive setup wizard → `agent-orchestrator.yaml` | New |
| `ao status` | Colored terminal table of all sessions | `claude-status` |
| `ao spawn <project> [issue]` | Spawn single session | `claude-spawn` |
| `ao batch-spawn <project> <issues...>` | Batch spawn with dedup | `claude-batch-spawn` |
| `ao session ls\|kill\|cleanup` | Session management | `claude-ao-session` ls/kill/cleanup |
| `ao send <session> <message>` | Smart message delivery | `send-to-session` |
| `ao review-check [project]` | Trigger PR review fixes | `claude-review-check` |
| `ao dashboard` | Start web server | `claude-dashboard` |
| `ao open [session\|all]` | Open terminal tabs | `claude-open-all`, `open-iterm-tab` |
**Key complexity**: `ao status` needs rich terminal output (colors, columns, live data). `ao batch-spawn` needs duplicate detection.
**Can start immediately** by coding against core interfaces. Wire up real implementations when Agent 1 finishes.
**Estimated effort**: Large (~800-1200 lines)
---
### Agent 6: Web Dashboard
**Package**: `packages/web/`
**Branch**: `feat/web-dashboard`
**Depends on**: Phase 0 types + core interfaces
**Partially blocked by**: Agent 1 (core services) for API routes
| Component | What | Reference |
| ----------------------------- | --------------------------------------------- | -------------------------------- |
| Next.js setup | App Router, Tailwind, dark theme | New |
| `GET /api/sessions` | List all sessions with full state | `claude-dashboard` /api/sessions |
| `POST /api/spawn` | Spawn new session | New |
| `POST /api/sessions/:id/send` | Send message to session | New |
| `POST /api/sessions/:id/kill` | Kill session | New |
| `POST /api/prs/:id/merge` | Merge PR | New |
| `GET /api/events` | SSE stream for real-time updates | New (replaces polling) |
| Dashboard page | Attention-prioritized session cards | `claude-dashboard` HTML |
| Session detail page | Full session info + terminal embed | New |
| Components | SessionCard, PRStatus, CIBadge, AttentionZone | `claude-dashboard` HTML |
**Key complexity**: SSE endpoint that streams lifecycle events in real-time. Attention-zone layout. xterm.js terminal embed.
**Can start immediately** with mock data, wire up real API when Agent 1 finishes.
**Estimated effort**: Large (~1500-2000 lines)
---
### Agent 7: Notifier + Terminal Plugins
**Packages**: `packages/plugins/notifier-desktop/`, `notifier-slack/`, `notifier-webhook/`, `terminal-iterm2/`, `terminal-web/`
**Branch**: `feat/notifier-terminal-plugins`
**Depends on**: Phase 0 types only
**Blocked by**: Nothing after Phase 0
| Plugin | What | Reference |
| ------------------ | ---------------------------------------------------------------- | ----------------------------------- |
| `notifier-desktop` | OS notifications (node-notifier), click → deep link to dashboard | `notify-session` |
| `notifier-slack` | Slack webhook messages with action buttons | New |
| `notifier-webhook` | Generic HTTP POST | New |
| `terminal-iterm2` | AppleScript tab management, reuse existing tabs | `open-iterm-tab`, `claude-open-all` |
| `terminal-web` | xterm.js config for web-based terminal | New |
**Key complexity**: `notifier-desktop` needs to be cross-platform (macOS/Linux/Windows). `terminal-iterm2` has AppleScript quirks (string length limits, tab detection).
**Estimated effort**: Medium (~500-700 lines)
---
## Parallelism Summary
```
Time ──────────────────────────────────────────────────►
Phase 0 (orchestrator):
████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
Phase 1 (7 parallel agents):
Agent 1 (core): ████████████████████████░░
Agent 2 (runtime): ██████████████░░░░░░░░░░░░
Agent 3 (agent): ██████████████░░░░░░░░░░░░
Agent 4 (scm): ████████████████████░░░░░░
Agent 5 (cli): ████████████████████████░░ ← can start on interfaces, wire later
Agent 6 (web): ██████████████████████████ ← can start on UI, wire later
Agent 7 (notifier): ██████████░░░░░░░░░░░░░░░░
Phase 2 (integration): after all agents done
████ ← wire everything together, test
```
### True Independence
These agents are **truly independent** after Phase 0:
- Agents 2, 3, 4, 7 implement plugin interfaces → zero inter-dependency
- Agent 1 (core) is the critical path
- Agents 5, 6 can start with mock/interface-only imports, wire later
### Risk: Agent 1 (Core) Is the Bottleneck
If core services are delayed, CLI and Web can't fully test. Mitigations:
- Agent 1 gets the most experienced agent
- Phase 0 writes enough core scaffolding (types, config, plugin-registry) that other agents aren't waiting
- CLI and Web start with mock implementations
---
## Linear Tickets (for spawning)
| Ticket | Title | Agent |
| ------ | ------------------------------------------------------------------------------------- | ----- |
| AO-10 | Implement core services (metadata, event-bus, session-manager, lifecycle-manager) | 1 |
| AO-11 | Implement runtime + workspace plugins (tmux, process, worktree, clone) | 2 |
| AO-12 | Implement agent plugins (claude-code, codex, aider) | 3 |
| AO-13 | Implement SCM + tracker plugins (github SCM, github tracker, linear tracker) | 4 |
| AO-14 | Implement CLI (ao init, status, spawn, session, send, review-check, dashboard, open) | 5 |
| AO-15 | Implement web dashboard (Next.js, API routes, SSE, attention-zone UI, session detail) | 6 |
| AO-16 | Implement notifier + terminal plugins (desktop, slack, webhook, iterm2, web) | 7 |
## Spawning Command
After Phase 0 is committed to `main`:
```bash
~/claude-batch-spawn ao AO-10 AO-11 AO-12 AO-13 AO-14 AO-15 AO-16
```
Each agent gets its own worktree branched from main (which contains the scaffold + types).

View File

@ -1,468 +0,0 @@
# Migration Guide: Hash-Based Architecture
**Date**: 2026-02-17
**Breaking Change**: Yes
**Affects**: All users with existing sessions
## What Changed
Agent Orchestrator has migrated from flat, configurable directories (`dataDir` and `worktreeDir`) to a **hash-based project isolation** architecture. This change eliminates configuration overhead and prevents collisions when running multiple orchestrator instances from different directories.
### Before (Flat Architecture)
```yaml
# agent-orchestrator.yaml
dataDir: ~/.ao-sessions
worktreeDir: ~/.ao-worktrees
projects:
my-app:
path: ~/repos/my-app
```
**Problems**:
- Multiple configs sharing same `dataDir` caused session collisions
- Manual path configuration required
- No isolation between orchestrator instances
- Backwards compatibility code added complexity
### After (Hash-Based Architecture)
```yaml
# agent-orchestrator.yaml
# No dataDir or worktreeDir needed!
projects:
my-app:
path: ~/repos/my-app
```
**Benefits**:
- Zero configuration — paths auto-derived from config location
- Complete isolation — each config gets unique namespace
- Collision-free — SHA256 hash prevents conflicts
- Clean codebase — no backwards compatibility code
## Architecture Details
### Directory Structure
All orchestrator data now lives under `~/.agent-orchestrator/`:
```
~/.agent-orchestrator/
├── {hash}-{projectId}/ # Unique per config+project
│ ├── .origin # Stores config path (collision detection)
│ ├── sessions/ # Session metadata
│ │ ├── int-1 # Metadata for session int-1
│ │ ├── int-2
│ │ └── archive/ # Archived metadata
│ └── worktrees/ # Git worktrees
│ ├── int-1/ # Worktree for session int-1
│ └── int-2/
```
### Hash Generation
The hash is the first 12 characters of `SHA256(realpath(dirname(configPath)))`:
```typescript
// Config at: ~/projects/acme/agent-orchestrator.yaml
// Hash of: /Users/you/projects/acme
// Result: a3b4c5d6e7f8
// Final path: ~/.agent-orchestrator/a3b4c5d6e7f8-my-app/
```
**Why 12 chars?** Balance between uniqueness (collision probability ~1 in 16 billion) and path length.
### Session Naming Layers
Three levels of naming for compatibility:
1. **User-facing**: `int-1`, `ao-42` (short, clean)
2. **Tmux**: `a3b4c5d6e7f8-int-1` (globally unique across all configs)
3. **Metadata file**: `int-1` (within project-specific `sessions/` directory)
## Breaking Changes
### 1. Config File Changes
**REMOVED fields** (will cause validation errors if present):
- `dataDir`
- `worktreeDir`
**REQUIRED field**:
- `configPath` (automatically set by `loadConfig()`)
**Migration**:
```diff
# agent-orchestrator.yaml
- dataDir: ~/.ao-sessions
- worktreeDir: ~/.ao-worktrees
projects:
my-app:
path: ~/repos/my-app
```
### 2. Metadata File Locations
**Before**:
```
~/.ao-sessions/
├── int-1 # Flat directory, all projects mixed
├── int-2
├── ao-1
└── ao-2
```
**After**:
```
~/.agent-orchestrator/
├── a3b4c5d6e7f8-integrator/sessions/
│ ├── int-1 # Integrator project sessions
│ └── int-2
└── f9e8d7c6b5a4-my-app/sessions/
├── ao-1 # My-app project sessions
└── ao-2
```
**Impact**: Existing metadata files **will not be automatically migrated**. See migration steps below.
### 3. Worktree Locations
**Before**:
```
~/.ao-worktrees/
├── integrator/
│ ├── int-1
│ └── int-2
└── my-app/
├── ao-1
└── ao-2
```
**After**:
```
~/.agent-orchestrator/
├── a3b4c5d6e7f8-integrator/worktrees/
│ ├── int-1
│ └── int-2
└── f9e8d7c6b5a4-my-app/worktrees/
├── ao-1
└── ao-2
```
**Impact**: Existing worktrees **will not be automatically migrated**. Git will report them as "missing" and they must be manually removed.
### 4. Environment Variables
**Before**:
```bash
AO_DATA_DIR=~/.ao-sessions # Flat path
```
**After**:
```bash
AO_DATA_DIR=~/.agent-orchestrator/a3b4c5d6e7f8-integrator/sessions/
```
**Impact**: Scripts or hooks relying on `AO_DATA_DIR` pointing to a flat directory will break.
### 5. API Changes (For Plugin Developers)
**Removed from `OrchestratorConfig`**:
```typescript
interface OrchestratorConfig {
dataDir: string; // REMOVED
worktreeDir: string; // REMOVED
configPath: string; // NOW REQUIRED
}
```
**New Path Utilities** (use these instead):
```typescript
import {
getSessionsDir,
getWorktreesDir,
getProjectBaseDir,
generateConfigHash,
generateInstanceId,
validateAndStoreOrigin,
} from "@aoagents/ao-core";
// Calculate paths dynamically
const sessionsDir = getSessionsDir(configPath, projectPath);
const worktreesDir = getWorktreesDir(configPath, projectPath);
```
## Migration Steps
### Step 1: Clean Up Config File
Remove `dataDir` and `worktreeDir` from your config:
```bash
# Edit your agent-orchestrator.yaml
vim ~/path/to/agent-orchestrator.yaml
```
Remove these lines:
```yaml
dataDir: ~/.ao-sessions
worktreeDir: ~/.ao-worktrees
```
### Step 2: Kill Existing Sessions
**IMPORTANT**: Existing sessions will **not** automatically migrate. You must kill them first.
```bash
# List all tmux sessions
tmux ls
# Kill all orchestrator sessions (adjust prefix as needed)
tmux ls | grep -E '^(int|ao|app)-[0-9]+:' | cut -d: -f1 | xargs -I{} tmux kill-session -t {}
# Or kill all tmux sessions (nuclear option)
tmux kill-server
```
### Step 3: Clean Up Old Directories
**Worktrees** must be removed from git:
```bash
# For each project, remove old worktrees
cd ~/repos/integrator
# List worktrees
git worktree list
# Remove each old worktree
git worktree remove ~/.ao-worktrees/integrator/int-1 --force
git worktree remove ~/.ao-worktrees/integrator/int-2 --force
# ... repeat for all
# Prune stale references
git worktree prune
```
**Metadata** can be archived for reference:
```bash
# Archive old metadata (optional)
mv ~/.ao-sessions ~/.ao-sessions-backup-$(date +%Y%m%d)
# Or delete if you don't need it
rm -rf ~/.ao-sessions
```
**Worktree directory** can be removed after cleaning up git references:
```bash
# Remove old worktree directory (after git worktree remove)
rm -rf ~/.ao-worktrees
```
### Step 4: Update to Latest Version
```bash
# Pull latest code
git pull origin main
# Reinstall dependencies
pnpm install
# Rebuild packages
pnpm build
```
### Step 5: Start Fresh
```bash
# Start orchestrator (creates new directory structure)
ao start
# Spawn new sessions (uses hash-based paths)
ao spawn my-app INT-1234
```
### Step 6: Verify New Structure
```bash
# Check that new directories were created
ls -la ~/.agent-orchestrator/
# You should see directories like:
# a3b4c5d6e7f8-my-app/
# .origin
# sessions/
# worktrees/
# Check tmux sessions have hash prefix
tmux ls
# Should show: a3b4c5d6e7f8-int-1, a3b4c5d6e7f8-int-2, etc.
```
## Rollback (If Needed)
If you need to rollback to the old architecture:
1. **Checkout previous commit** (before hash-based migration):
```bash
git checkout <commit-before-migration>
pnpm install
pnpm build
```
2. **Restore old config**:
```bash
# Add back to agent-orchestrator.yaml
dataDir: ~/.ao-sessions
worktreeDir: ~/.ao-worktrees
```
3. **Restore old metadata** (if archived):
```bash
mv ~/.ao-sessions-backup-20260217 ~/.ao-sessions
```
4. **Kill new hash-based sessions**:
```bash
tmux ls | grep -E '^[a-f0-9]{12}-' | cut -d: -f1 | xargs -I{} tmux kill-session -t {}
```
## FAQ
### Q: Why can't I use my old sessions?
**A**: The metadata file structure changed. Old sessions point to flat directories (`~/.ao-sessions/int-1`) but the new code expects hash-based paths (`~/.agent-orchestrator/a3b4c5d6e7f8-integrator/sessions/int-1`). You must kill old sessions and spawn new ones.
### Q: Will my PRs be lost?
**A**: No! PRs are on GitHub, not in local sessions. You can:
1. Check `gh pr list` to see all open PRs
2. Spawn new sessions for PRs that need work: `ao spawn integrator --branch feat/existing-branch`
### Q: What about in-progress work?
**A**: Git worktrees contain your code changes. Before killing sessions:
1. Commit or stash changes in each worktree
2. Note which issues/branches were being worked on
3. After migration, spawn new sessions and continue work
### Q: Can I migrate metadata files manually?
**A**: Yes, but **not recommended**. The manual process:
```bash
# For each project
OLD_DIR=~/.ao-sessions
NEW_DIR=~/.agent-orchestrator/$(python3 -c "import hashlib; print(hashlib.sha256(b'/Users/you/path/to/config/dir').hexdigest()[:12])")-integrator/sessions
mkdir -p "$NEW_DIR"
# Copy metadata files
cp "$OLD_DIR"/int-* "$NEW_DIR/"
# Update worktree paths in each file (required!)
for file in "$NEW_DIR"/*; do
sed -i '' 's|worktree=~/.ao-worktrees/integrator/|worktree=~/.agent-orchestrator/HASH-integrator/worktrees/|g' "$file"
done
```
This is error-prone. **Recommended**: Kill old sessions and spawn fresh ones.
### Q: What happens if I have multiple config files?
**A**: Each config gets a unique hash! This is the **main benefit** of the new architecture:
```bash
# Config 1: ~/projects/acme/agent-orchestrator.yaml
# Hash: a3b4c5d6e7f8
# Sessions: ~/.agent-orchestrator/a3b4c5d6e7f8-my-app/
# Config 2: ~/experiments/test/agent-orchestrator.yaml
# Hash: 1f2e3d4c5b6a
# Sessions: ~/.agent-orchestrator/1f2e3d4c5b6a-my-app/
```
No conflicts, complete isolation!
### Q: How do I find the hash for my config?
**A**:
```bash
# Calculate hash for your config directory
echo -n "/path/to/your/config/dir" | sha256sum | cut -c1-12
# Or let ao print it
ao status
# Output shows: Config: /path/to/config.yaml
# Hash will be in directory names: ~/.agent-orchestrator/{hash}-{project}/
```
### Q: What if two configs have the same hash (collision)?
**A**: The `.origin` file detects this and throws an error with instructions:
```
Hash collision detected!
Directory: ~/.agent-orchestrator/a3b4c5d6e7f8-my-app
Expected config: /Users/you/config1/agent-orchestrator.yaml
Actual config: /Users/you/config2/agent-orchestrator.yaml
This is a rare hash collision. Please move one of the configs to a different directory.
```
**Solution**: Move one config to a different directory. Collision probability is ~1 in 16 billion with 12-character hashes.
## Support
If you encounter issues during migration:
1. Check existing sessions: `tmux ls`
2. Check new directory structure: `ls -la ~/.agent-orchestrator/`
3. Check config validation: `ao status`
4. Review git worktrees: `git worktree list` (from project directory)
5. Check logs: `journalctl -u ao-orchestrator` or tmux session output
For bugs or questions, file an issue: https://github.com/composiohq/agent-orchestrator/issues
## Summary
**Action Required**:
1. ✅ Remove `dataDir` and `worktreeDir` from config
2. ✅ Kill all existing sessions (`tmux kill-server`)
3. ✅ Clean up old git worktrees (`git worktree remove --force`)
4. ✅ Remove old directories (`~/.ao-sessions`, `~/.ao-worktrees`)
5. ✅ Update to latest version (`pnpm install && pnpm build`)
6. ✅ Start fresh (`ao start`, `ao spawn`)
**Expected Downtime**: ~10 minutes (time to kill sessions, clean worktrees, respawn)
**Risk Level**: Low (PRs are safe, code is in git, only local sessions affected)
**Benefit**: Cleaner architecture, zero config, collision-free multi-instance support

View File

@ -1,24 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="296" height="48" viewBox="0 0 296 48">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#E8503A"/>
<stop offset="50%" stop-color="#D94530"/>
<stop offset="100%" stop-color="#C73A26"/>
</linearGradient>
<linearGradient id="shine" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FFFFFF" stop-opacity="0.25"/>
<stop offset="50%" stop-color="#FFFFFF" stop-opacity="0.05"/>
<stop offset="100%" stop-color="#000000" stop-opacity="0.1"/>
</linearGradient>
</defs>
<rect width="296" height="48" rx="10" fill="url(#bg)"/>
<rect width="296" height="48" rx="10" fill="url(#shine)"/>
<rect x="0.5" y="0.5" width="295" height="47" rx="9.5" fill="none" stroke="none"/>
<!-- Book icon -->
<g transform="translate(22, 14)" fill="#FFFFFF">
<path d="M0,2 C0,0.9 0.9,0 2,0 L8,0 C9.1,0 10,0.5 10,1.5 L10,18 C10,17 9.1,16.5 8,16.5 L2,16.5 C0.9,16.5 0,17.4 0,18.5 Z"/>
<path d="M20,2 C20,0.9 19.1,0 18,0 L12,0 C10.9,0 10,0.5 10,1.5 L10,18 C10,17 10.9,16.5 12,16.5 L18,16.5 C19.1,16.5 20,17.4 20,18.5 Z"/>
</g>
<!-- Text -->
<text x="52" y="29" font-family="system-ui,-apple-system,Segoe UI,Helvetica,Arial,sans-serif" font-size="16" font-weight="700" fill="#FFFFFF" letter-spacing="0.5">Read the Full Article on X</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,21 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="280" height="48" viewBox="0 0 280 48">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#6C5CE7"/>
<stop offset="50%" stop-color="#5B4BD5"/>
<stop offset="100%" stop-color="#4A3AC3"/>
</linearGradient>
<linearGradient id="shine" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#FFFFFF" stop-opacity="0.25"/>
<stop offset="50%" stop-color="#FFFFFF" stop-opacity="0.05"/>
<stop offset="100%" stop-color="#000000" stop-opacity="0.1"/>
</linearGradient>
</defs>
<rect width="280" height="48" rx="10" fill="url(#bg)"/>
<rect width="280" height="48" rx="10" fill="url(#shine)"/>
<rect x="0.5" y="0.5" width="279" height="47" rx="9.5" fill="none" stroke="none"/>
<!-- Play icon -->
<polygon points="28,16 28,32 40,24" fill="#FFFFFF"/>
<!-- Text -->
<text x="52" y="29" font-family="system-ui,-apple-system,Segoe UI,Helvetica,Arial,sans-serif" font-size="16" font-weight="700" fill="#FFFFFF" letter-spacing="0.5">Watch the Demo on X</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -16,22 +16,13 @@
| [`competitive-analysis-raw.md`](./competitive-analysis-raw.md) | Raw research notes from all 14 competitor sites (Linear, Vercel, Railway, Fly.io, Inngest, Temporal, Grafana, WandB, LangSmith, Retool, Render, PlanetScale, Supabase, GitHub Copilot) |
| [`design-brief-v1.md`](./design-brief-v1.md) | Original v1 brief (text-only research, pre-Playwright CSS extraction) — kept for reference |
## Screenshots
| File | Description |
|------|-------------|
| [`screenshots/linear-homepage.png`](./screenshots/linear-homepage.png) | Linear.app captured via Playwright (311KB) — source for verified CSS token extraction |
| [`screenshots/railway-homepage.png`](./screenshots/railway-homepage.png) | Railway.app captured via Playwright (444KB) — visual palette reference |
---
## Research Methods
**Phase 1 — Text analysis**: Two parallel research agents analyzed 14 competitor product sites via WebFetch, extracting visual patterns, color systems, and design philosophy from HTML/CSS content.
**Phase 2 — Playwright CSS extraction**: Installed `@playwright/mcp` and extracted live CSS custom properties from [linear.app](https://linear.app) using `document.styleSheets` enumeration. This yielded ground-truth values for Linear's token system — the most rigorous competitive design data available without access to their Figma files.
**Phase 3 — Playwright screenshots**: Captured screenshots of Linear and Railway via headless Chromium for visual reference.
**Phase 3 — Playwright screenshots**: Captured screenshots of Linear and Railway via headless Chromium for visual reference. The binary captures are intentionally not tracked; regenerate from the live sites if a fresh comparison is needed.
**Phase 4 — Codebase audit**: Read the entire `packages/web/` source (components, types, CSS tokens) to map research recommendations against the actual implementation. Produced implementation audit sections in each brief with prioritized delta tables.

View File

@ -21,7 +21,7 @@ Primary interaction model: **scan → identify → act**. Not explore, not brows
### Linear (linear.app) — **Ground truth via CSS extraction**
*Playwright was used to extract exact token values from the live site. See `screenshots/linear-homepage.png`.*
*Playwright was used to extract exact token values from the live site. Binary screenshot captures are intentionally not tracked; regenerate from the live site if a fresh comparison is needed.*
**Verified color palette:**
- Body background: `rgb(8, 9, 10)``#08090A` — near-pure black with imperceptible warm cast
@ -84,7 +84,7 @@ Primary interaction model: **scan → identify → act**. Not explore, not brows
### Railway (railway.app) — **Visually analyzed via screenshot**
*See `screenshots/railway-homepage.png`.*
*Visual notes were taken from a Playwright capture. Binary screenshot captures are intentionally not tracked; regenerate from the live site if a fresh comparison is needed.*
**Visual palette observations:**
- Background (hero): Dark desaturated blue-purple, approximately `hsl(250, 24%, 9%)``#13111C`
@ -523,7 +523,7 @@ scrollbar-color: rgba(255,255,255,0.1) transparent;
## 4. Inspiration References
### Linear issue list — *the* density benchmark
**URL**: https://linear.app (see `screenshots/linear-homepage.png`)
**URL**: https://linear.app
**Why**: The product UI visible in Linear's homepage hero screenshot shows exactly the information density and row compactness that ao session cards should match. Issue ID in muted monospace, title truncated, labels as small pill badges, state dot left-aligned. Zero wasted pixels.
### Vercel deployment list

View File

@ -1,214 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Feedback Pipeline Architecture Explainer</title>
<style>
:root {
--bg: #f2efe7;
--panel: #fffdf8;
--ink: #1f1a16;
--muted: #6f6357;
--brand: #165a72;
--line: #dfd3c2;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--ink);
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
line-height: 1.5;
background:
radial-gradient(circle at 12% 10%, #ebdfcb 0%, transparent 35%),
radial-gradient(circle at 88% 85%, #e5d4ba 0%, transparent 30%), var(--bg);
}
.wrap {
max-width: 1100px;
margin: 2rem auto;
padding: 0 1rem 3rem;
}
.hero,
.card {
border: 1px solid var(--line);
border-radius: 14px;
background: var(--panel);
}
.hero {
padding: 1.2rem;
}
.card {
padding: 1rem;
}
h1,
h2,
h3 {
font-family: "IBM Plex Serif", Georgia, serif;
margin: 0.6rem 0;
}
.muted {
color: var(--muted);
}
.grid {
margin-top: 1rem;
display: grid;
gap: 1rem;
}
@media (min-width: 900px) {
.grid.two {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grid.three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
ul,
ol {
margin: 0.4rem 0 0;
padding-left: 1rem;
}
code {
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
background: #f3ebe0;
border-radius: 4px;
padding: 0.1rem 0.28rem;
}
pre {
margin-top: 0.7rem;
padding: 0.7rem;
border: 1px solid var(--line);
border-radius: 10px;
background: #faf4ec;
overflow-x: auto;
}
</style>
</head>
<body>
<main class="wrap">
<section class="hero">
<h1>Feedback Pipeline Explainer</h1>
<p class="muted">
Durable architecture reference for the report -> issue -> agent-session -> PR pipeline,
including fork-aware execution and governance controls.
</p>
</section>
<section class="grid two">
<article class="card">
<h2>Formal Pipeline</h2>
<ol>
<li>Capture and validate report payload.</li>
<li>Resolve issue via dedupe markers (create or update/comment).</li>
<li>Plan follow-up: issue-only, issue+PR, or issue+fork.</li>
<li>Execute direct SCM operations or spawn agent session for code changes.</li>
<li>Link issue/fork/PR and update operation journal.</li>
</ol>
</article>
<article class="card">
<h2>Execution Boundaries</h2>
<ul>
<li>Orchestrator is the only component allowed to perform SCM side effects.</li>
<li>Optional subagent skill can provide recommendation signals only.</li>
<li>Policy fallback remains deterministic when subagent output is invalid.</li>
</ul>
</article>
</section>
<section class="grid three">
<article class="card">
<h3>Trigger Conditions</h3>
<ul>
<li>Valid report in <code>mode=scm</code>.</li>
<li>Confidence thresholds met.</li>
<li>Governance allows attempted mutation path.</li>
</ul>
</article>
<article class="card">
<h3>Target Selection</h3>
<ul>
<li>Prefer upstream when writable and policy allows.</li>
<li>Use fork path when upstream writes are blocked.</li>
<li>Downgrade to issue-only when neither path is permitted.</li>
</ul>
</article>
<article class="card">
<h3>Idempotency</h3>
<ul>
<li><code>dedupeKey</code> for issue identity.</li>
<li><code>operationKey</code> per mutation stage.</li>
<li>Find-or-create semantics before every mutation.</li>
</ul>
</article>
</section>
<section class="grid two">
<article class="card">
<h2>Consent Gates (Default Policy)</h2>
<ul>
<li>Outside AO dogfooding, human consent is required for fork creation.</li>
<li>Outside AO dogfooding, human consent is required for PR creation.</li>
<li>Outside AO dogfooding, human consent is required before upstream/fork target switch.</li>
<li>Project-level bypass is allowed only when owner explicitly enables it.</li>
</ul>
</article>
<article class="card">
<h2>Journal (Plain Language)</h2>
<ul>
<li>The journal is a progress log for every report.</li>
<li>Each mutation updates one tracked record, not separate duplicates.</li>
<li>Retries keep the same keys and increment attempt metadata.</li>
<li>Consent outcomes are stored for auditability.</li>
</ul>
<pre><code>{
"dedupeKey": "f4d7dbe5b0f8...",
"stage": "create_pr",
"status": "failed",
"attempt": 2,
"issueUrl": ".../issues/399",
"prUrl": null
}</code></pre>
</article>
</section>
<section class="grid two">
<article class="card">
<h2>PR Requirements</h2>
<ul>
<li>Issue reference must exist in PR metadata/body.</li>
<li>Stable lineage markers link issue, session, and PR.</li>
<li>Existing PR for same dedupe context must be linked, not duplicated.</li>
</ul>
</article>
<article class="card">
<h2>Governance Hooks</h2>
<ul>
<li>canCreateIssue / canCreateFork / canCreatePR / canSpawnSession checks.</li>
<li>Per-fork-owner allow/deny and optional approval gates.</li>
<li>Denied operations are journaled with explicit reason codes.</li>
</ul>
</article>
</section>
</main>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 KiB

View File

@ -1,209 +0,0 @@
# PR #1466 — Architecture Changes
What this PR changes, in the order you should learn it. Companion to [`main.md`](./main.md) and [`review-and-risks.md`](./review-and-risks.md).
---
## 1. Storage layout: V1 → V2
### V1 (before — `upstream/main`)
```
~/.agent-orchestrator/
{hash}-{projectId}/ # hash = SHA-256 of config dir
sessions/ # active session metadata (key=value flat files)
worktrees/{sessionId}/
archive/{sessionId}_{ts}/ # terminated sessions copied here
```
- `storageKey` = `{hash}-{projectId}` was the primary key threaded through every session.
- Archive was a *second place* to look for terminated sessions — restore/status had dual lookup paths.
### V2 (after — `storage-redesign`)
```
~/.agent-orchestrator/
config.yaml # global registry of all projects
running.json # current ao start PID/port
last-stop.json # NEW — sessions killed by ao stop / Ctrl+C
projects/
{projectId}/ # projectId = {basename}_{hash}
sessions/{sessionId}.json # JSON metadata, terminated sessions stay here
worktrees/{sessionId}/
```
**Key shifts:**
- One canonical directory per project — no more `{hash}-{projectId}` wrapper.
- `projectId` is now a *deterministic, human-readable, collision-safe* identifier: `{basename}_{8-char-hash}`. Duplicate basenames get suffixed automatically.
- Session metadata is JSON files (`.json` extension), not flat key-value blobs.
- `archive/` is gone. Terminated sessions stay in `sessions/` with a terminated lifecycle state.
- `storageKey` is fully removed from the type system. Any reference is dead code.
### Path helpers (where to read/write)
`packages/core/src/paths.ts`:
- `getProjectSessionsDir(projectId)` — replaces `getActiveMetadataDir(storageKey)`.
- `getProjectWorktreesDir(projectId)` — replaces worktree path under `{hash}-{projectId}`.
- Archive helpers (`getArchiveDir`, etc.) are **removed**. If you see one, it's dead.
---
## 2. Project identity (hashed)
`projectId` shape: `{sanitized-basename}_{8-char-sha256-prefix}`
Example: a config at `/Users/harshit/work/agent-orchestrator/agent-orchestrator.yaml``projectId = "agent-orchestrator_a1b2c3d4"`.
Properties:
- **Deterministic** — same path always yields the same id.
- **Collision-safe** — different paths with the same basename get distinct hashes.
- **Suffix allocation on duplicates** — if you have two checkouts with the same basename in the global config, the loader allocates `name_2`, `name_3`, etc. (see commit `f7118ef1`).
- **Sanitization** — basenames are stripped of dots and unsafe chars before hashing (commit `27666c6e`).
Where it's used:
- Storage paths (above)
- `agent-orchestrator.yaml` → global config registration
- Web tmux session resolution (commit `eca3001c` handles legacy wrapped keys for backward compat)
- `ao status`, `ao stop <project>`, `ao spawn` — all key off `projectId`
**Invariant:** one `projectId` ↔ one canonical orchestrator session per project. Enforced in `f674422a`.
---
## 3. Session lifecycle: dual-truth elimination
### Before
Sessions persisted **both** a `SessionStatus` enum (`spawning | working | pr_open | merged | killed | …`) **and** a lifecycle `state` + `reason`. They drifted. Restore code had to reconcile them. Tests asserted on whichever was easier.
### After
Source of truth is **`lifecycle-state.ts`**:
- `state`: `not_started | working | idle | needs_input | stuck | detecting | done | terminated`
- `reason`: `manually_killed | runtime_lost | agent_process_exited | probe_failure | error_in_process | auto_cleanup | pr_merged`
Legacy `SessionStatus` is **derived** at read time via `deriveLegacyStatus(state, reason, prState)` — used only for display backward-compat. Never persisted.
Practical consequences:
- `previousStatus` arguments throughout the codebase are gone (commit `e9d9c762`).
- Restore now resets lifecycle cleanly, including for previously-merged PRs (commits `d22f0c6f`, `b178eb66`, `fc6fd88b`).
- Stale runtime detection: `sm.list()` checks if the tmux/process backing each session is still alive during enrichment. Dead ones get persisted as `runtime_lost` → legacy status `killed`. Without this, sessions whose runtime died silently would show "active" forever (commit `9e2df894`).
---
## 4. Migration: V1 → V2 with rollback
The single most-reviewed code in the PR. Lives in `packages/core/src/migrate-storage/`.
### Command
```bash
ao migrate-storage --dry-run # show planned actions
ao migrate-storage # execute (atomic per-project, with rollback on failure)
```
### What it does
1. **Detect V1 layout** — looks for `{hash}-{projectId}/` directories at the AO root.
2. **Inventory** — enumerates sessions, worktrees, archives, validates each against Zod schemas.
3. **Plan** — computes target paths under `projects/{newProjectId}/`. Detects macOS case-insensitive collisions before writing anything.
4. **Execute, atomically per project:**
- Convert flat-file metadata → JSON.
- Move worktrees, rewriting any embedded paths in git config.
- Flatten `archive/` contents back into `sessions/` with terminated lifecycle.
- Mark the source dir as `.migrated` so re-runs don't re-process it (commit `880930a2` prevents `.migrated.migrated`).
5. **Rollback** if any step fails: restore originals, repair git worktrees (the trickiest part — `worktree` files contain absolute paths that need rewriting).
### Safety guarantees (commits handling each)
- File-locked global config updates so two `ao migrate-storage` runs can't race.
- Atomic writes: temp file + rename, never partial overwrites (`bb4c68fc`).
- macOS case-insensitive collision detection (`64caef04`).
- Worktree path rewriting handles recursion and stray paths (`ff61ee97`).
- Active session check is **skipped** during `--dry-run` so users can plan without killing sessions (`c800c89c`).
- Rollback preserves worktrees that were already migrated successfully (`fd4f969f`).
- Graceful errors: `b18cbe22` — failed migration shows actionable message instead of stack trace.
- 21+ named edge cases handled: see `handoff/pr-1466/review-and-risks.md`.
### Things to NOT touch in `migrate-storage/`
- The order of operations in the per-project transaction. Each step is checkpointed for rollback.
- The `.migrated` marker scheme.
- Git worktree path rewriting — the regex is intentionally narrow.
---
## 5. Cross-project CLI rework
### `ao stop`
**Note:** There is no `stop.ts` — the stop command is defined inside `packages/cli/src/commands/start.ts`.
| Invocation | Before | After |
|------------|--------|-------|
| `ao stop` | Kills only the most-recently-active orchestrator. Saw only local config (1 project). | Loads global config, kills **all** sessions across **all** registered projects. Stops parent process + dashboard. Writes `last-stop.json` with `{ projectId, sessionIds[], otherProjects: [...] }` for restore. |
| `ao stop <project>` | Same as above (no scoping). | Surgical: kills only `<project>`'s sessions. **Does not** kill parent process or dashboard (commit `95cf979d` — this was a real bug). Falls back to global config if `<project>` isn't in the local config (`2db2951a`). Calls `removeProjectFromRunning(projectId)` to remove the project from `running.json` so that a subsequent `ao start <project>` can restart without hitting the "already running" gate. |
### `ao start`
- Reads `last-stop.json` on startup. If it has sessions, prompts: **"Restore N sessions from your last shutdown?"** — including cross-project ones (`8b130964`).
- Loads global config for cross-project session-manager access during restore.
- Skips orchestrator restore if `ensureOrchestrator()` already restored it (avoids double-spawn).
- **`projectNeedsRestart` gate:** If `ao start <project>` is called and the project was removed from `running.json` by a prior `ao stop <project>`, the "already running" menu is bypassed and the orchestrator is re-created for that project. An `isProjectId` guard ensures this only triggers for project ID arguments (not filesystem paths or repo URLs).
- Project picker now offers "Add this project" when launched in an unregistered cwd (`d6a56a8f`, `e1ecc091`).
- Auto-registers a flat local config on first `ao start` if missing (`1972fa30`).
### `Ctrl+C` (signal handler in `ao start`)
Mirrors `ao stop` exactly: kills all sessions, writes `last-stop.json`, unregisters `running.json`. Implementation uses an async IIFE inside the sync signal handler (Node.js signal handlers are sync — `process.exit()` must be called explicitly since registering a handler removes the default exit behavior). 10s hard timeout via `setTimeout().unref()` in case cleanup hangs (`b4feda79`). Before this PR, Ctrl+C left tmux orphans.
### Tab completions
`packages/cli/completions/` — merge global + local config so tab completion shows every registered project, not just those in the current `agent-orchestrator.yaml` (`39ebb07f`).
---
## 6. Web dashboard changes
Minimal but important:
- **Sidebar:** always shows all sessions across all projects, regardless of which project is active. Per-project filtering is applied at the kanban level via `projectSessions = sessions.filter(s => s.projectId === projectId)`. (`53e8476f`)
- **Tmux session resolver:** handles the legacy wrapped storage key (`{hash}-{projectName}`) so dashboards open mid-migration don't break (`eca3001c`).
- No new UI component libraries, no inline styles — design system unchanged.
---
## 7. Archive removal — what to grep for
If you see any of these in the codebase, they're dead and should be deleted (or you're on the wrong branch):
```
getArchiveDir
ArchivedSessionMetadata
listArchive
moveToArchive
archive: true
```
The cleanup spans 11 commits (group C in `pr-1466.html` → Commit Story tab). Tests and docs were updated in lockstep.
---
## 8. New runtime artifacts
| File | Lifetime | Written by | Read by |
|------|----------|------------|---------|
| `~/.agent-orchestrator/config.yaml` | Persistent | `ao start` (auto-register), `ao spawn`, manual edits | All CLI commands needing cross-project visibility, tab completions |
| `~/.agent-orchestrator/running.json` | Lives while `ao start` is running | `ao start` (register), `ao stop`/Ctrl+C (unregister), `ao stop <project>` (removes project via `removeProjectFromRunning`) | `ao status`, `ao spawn`, dashboard, `ao start` (checks for already-running + `projectNeedsRestart` gate) |
| `~/.agent-orchestrator/last-stop.json` | Cleared after restore prompt | `ao stop`, Ctrl+C | `ao start` |
---
## 9. Things that intentionally did NOT change
- The 8-slot plugin system. No new plugins, no interface breakage.
- SSE 5s polling interval (CLAUDE.md C-14).
- The dashboard component library (no new deps; CLAUDE.md C-01).
- The lifecycle polling loop in `lifecycle-manager.ts` — only its inputs (state model) changed.
- The agent plugin contract (Claude Code, Codex, Aider, OpenCode all unchanged at the interface level).
If you find yourself touching any of the above to "fix" something, stop and re-read the original review thread on the PR — the answer is almost always "no, work around it."

View File

@ -1,196 +0,0 @@
# PR #1466 — Storage Redesign Handoff
**You are picking up an in-flight PR. Read this file first, then the two siblings.**
- [`architecture.md`](./architecture.md) — what the PR actually changes (storage layout, identity, lifecycle, CLI semantics)
- [`review-and-risks.md`](./review-and-risks.md) — review state, hot zones, edge cases, gotchas
---
## TL;DR
PR #1466 ("Storage V2") is the big refactor: replaces `storageKey`-based flat metadata with `projects/{projectId}/` JSON storage, introduces deterministic hashed project IDs (`{basename}_{hash}`), removes the `archive/` directory entirely, eliminates the `SessionStatus` dual-truth, ships a crash-safe `migrate-storage` command with rollback, and reworks `ao stop` / `ao start` / `Ctrl+C` for cross-project awareness with session restore.
| Metric | Value |
|--------|-------|
| Files changed | 90 |
| Insertions | +6,481 |
| Deletions | -2,421 |
| Net LOC | +4,060 |
| PR-owned commits | ~85+ (between `upstream/main..HEAD`) |
| Upstream commits brought in via merge | ~25 |
---
## PR & branch map
| Role | Branch | Where it lives | What it represents |
|------|--------|----------------|--------------------|
| **Base** | `main` | `ComposioHQ/agent-orchestrator` | The merge target. PR diffs against this. |
| **Head (PR)** | `storage-redesign` | `harshitsinghbhandari/agent-orchestrator` (fork) | The actual PR head — what GitHub shows on PR #1466. All authored commits live here. |
| **Simulation** | `simulate-pr-1466-merged` | `harshitsinghbhandari/agent-orchestrator` (fork) | What `main` will look like AFTER PR #1466 lands. Used for end-to-end testing of the merged state and for post-merge fixes. Tracks `storage-redesign` via repeated merges + a small number of additional fixes (e.g. `89a51107 fix(cli): add removeProjectFromRunning and targeted stop tests`). |
All three branches live in **Harshit's fork** (`harshitsinghbhandari/agent-orchestrator`). The upstream repo (`ComposioHQ/agent-orchestrator`) only has `main`.
---
## Checkout recipes
### If you're Harshit (PR author)
Your remotes are already set up:
- `origin` / `harshit``harshitsinghbhandari/agent-orchestrator`
- `upstream``ComposioHQ/agent-orchestrator`
```bash
git fetch upstream && git fetch origin
git checkout storage-redesign # PR branch
git pull origin storage-redesign
git checkout simulate-pr-1466-merged # post-merge simulation
git pull origin simulate-pr-1466-merged
```
### If you're anyone else (reviewer / new contributor)
Clone the upstream repo, then add Harshit's fork as a remote to access the PR branches:
```bash
# Clone upstream
git clone https://github.com/ComposioHQ/agent-orchestrator.git
cd agent-orchestrator
# Add the fork that holds the PR branches
git remote add harshit https://github.com/harshitsinghbhandari/agent-orchestrator.git
git fetch harshit
# Checkout the PR branch
git checkout -b storage-redesign harshit/storage-redesign
# Checkout the simulation branch (optional — for post-merge testing)
git checkout -b simulate-pr-1466-merged harshit/simulate-pr-1466-merged
```
Alternatively, use the GitHub CLI:
```bash
gh repo clone ComposioHQ/agent-orchestrator
cd agent-orchestrator
gh pr checkout 1466 # checks out storage-redesign from the fork automatically
```
### Which branch should I work on?
| Goal | Branch | Why |
|------|--------|-----|
| Fix a review comment / address feedback on PR #1466 | `storage-redesign` | Commits here show up on the PR. Push to the fork. |
| Test "what happens after this lands on main" | `simulate-pr-1466-merged` | Post-merge state with fixes already cherry-picked in. |
| Add a fix that should ride along with the merge but you're unsure about PR scope | `simulate-pr-1466-merged` first, then cherry-pick / merge into `storage-redesign` once validated | The simulate branch is the safe playground. |
| Compare the PR's diff against base | `git diff origin/main...storage-redesign` | Three-dot diff = PR's contribution only. |
**Do not push directly to `ComposioHQ/agent-orchestrator` main.** PR #1466 will land via the GitHub merge button.
---
## Workflow on `storage-redesign` (the PR head)
```bash
git checkout storage-redesign
git pull --rebase # avoid stacking review-fixup commits
# Make changes
pnpm install
pnpm build
pnpm typecheck
pnpm test
pnpm --filter @aoagents/ao-web test
# Commit conventionally — fix:/refactor:/docs:/test:/feat:
git commit -m "fix(core): address review on X"
git push # pushes to harshitsinghbhandari/agent-orchestrator (the fork)
```
**Before pushing, run the full pre-flight:**
```bash
pnpm lint && pnpm format:check && pnpm typecheck && pnpm test
```
CI on this branch runs lint, typecheck, tests, and a release dry-run.
---
## Workflow on `simulate-pr-1466-merged`
This branch exists to validate the *merged* state — useful when:
- A fix only manifests after PR #1466 conflicts have been resolved with `main`.
- You're testing CLI behavior end-to-end (e.g. `ao stop` cross-project flows) with a representative repo state.
- A reviewer asks "but what happens after this merges with PR #X?"
```bash
git checkout simulate-pr-1466-merged
# Keep it current with both sides (adjust remote names to your setup):
git fetch origin # upstream: ComposioHQ/agent-orchestrator
git merge origin/main # absorb new main commits (resolve conflicts)
# If you have the fork as a remote named 'harshit':
git fetch harshit
git merge harshit/storage-redesign # absorb new PR commits
```
Fixes that should also ship with the PR get cherry-picked back to `storage-redesign`:
```bash
git checkout storage-redesign
git cherry-pick <commit-from-simulate>
git push
```
---
## Project context (orient quickly)
- **Repo:** `ComposioHQ/agent-orchestrator` — pnpm workspace (~30 packages).
- **Stack:** TypeScript strict, Node 20+, Next.js 15 (App Router), React 19, Tailwind v4, xterm.js, Zod, Vitest.
- **Read this in the repo root:** `CLAUDE.md` — codebase conventions and working principles.
- **Read this for design rules:** `DESIGN.md` — design system, anti-patterns.
- **Read this for what PR #1466 *behaves like*:** `pr-1466.html` — open in a browser. It has two tabs: "Behavior" (per-feature before/after) and "Commit Story" (themed commit groups). Use this as your visual spec.
---
## Critical files this PR touches
| File | Why it matters |
|------|----------------|
| `packages/core/src/types.ts` | Plugin interfaces. Minimize changes. |
| `packages/core/src/session-manager.ts` | Session CRUD. Now lifecycle-centric, no archive lookup. |
| `packages/core/src/lifecycle-manager.ts` | State machine. Status is *derived*, not stored. |
| `packages/core/src/lifecycle-state.ts` | Canonical state + reason model — new in this PR. |
| `packages/core/src/paths.ts` | V2 path helpers (`getProjectSessionsDir`, etc.). Archive helpers removed. |
| `packages/core/src/migrate-storage/` | The migration command + rollback. Most-reviewed code in the PR. |
| `packages/cli/src/commands/start.ts` | Cross-project restore prompt, Ctrl+C graceful shutdown, `ao stop` logic (stop is handled inside start.ts, there is no separate stop.ts). |
| `packages/cli/src/lib/running-state.ts` | `running.json` / `last-stop.json` read/write, `removeProjectFromRunning()`, advisory locking. |
| `packages/web/src/components/Dashboard.tsx` | Sidebar always shows all projects' sessions. |
| `~/.agent-orchestrator/last-stop.json` | New runtime artifact. Read by `ao start` to offer restore. |
| `~/.agent-orchestrator/config.yaml` | Global config. Cross-project commands fall back here. |
---
## Quick context dump for an AI agent
If you're an AI agent picking this up, read in this order and you'll be productive:
1. This file — branch map + workflow.
2. `handoff/pr-1466/architecture.md` — what changed and why.
3. `handoff/pr-1466/review-and-risks.md` — what reviewers cared about + known edge cases.
4. `pr-1466.html` (open in browser) — visual spec, both tabs.
5. `CLAUDE.md` (repo root) — house rules.
6. `git log --oneline upstream/main..storage-redesign` — the actual commit list.
After that, `git diff main...storage-redesign -- packages/core/src/migrate-storage/` is where the depth lives (use whatever remote/branch ref matches your setup — e.g. `origin/main` or `upstream/main`).
---
## Contact
PR author: **Harshit Singh** (`@harshitsinghbhandari`)
PR URL: https://github.com/ComposioHQ/agent-orchestrator/pull/1466

View File

@ -1,148 +0,0 @@
# PR #1466 — Review State, Risks, Edge Cases
What reviewers cared about, where the bodies are buried, and what to verify before claiming done. Companion to [`main.md`](./main.md) and [`architecture.md`](./architecture.md).
---
## Review state at a glance
This PR has been through **11+ review rounds** across multiple reviewers (humans + Copilot bot). The commit log reflects this — see group L "Review fixes" in `pr-1466.html` (Commit Story tab). Treat any new feedback as the 12th round, not the 1st.
**Reviewer focus areas, in descending order of attention:**
1. `migrate-storage/` — the migration + rollback machinery. Most reviewed; most edge cases filed.
2. Cross-project CLI semantics (`ao stop` / `ao start` / Ctrl+C) — behavioral correctness around `last-stop.json`.
3. Status / lifecycle dual-truth elimination — making sure no consumer still reads `previousStatus`.
4. Worktree path safety during migration — git config rewriting.
5. Dashboard sidebar scoping regression.
---
## Edge cases handled (do not regress)
The migration machinery survived a brutal review pass. The PR has explicit handlers for **at least 21 named edge cases**, codified mostly in commit `64caef04` ("EC-1..EC-8, EC-14, EC-27") and follow-ups. The ones to keep in mind when changing this code:
| ID / theme | Scenario | Where handled |
|------------|----------|---------------|
| EC-1 | V1 root has both new and legacy directories simultaneously | `migrate-storage/inventory.ts` |
| EC-2 | macOS case-insensitive filesystem collision (`Foo` vs `foo`) | `64caef04` |
| EC-3 | Git worktree files contain absolute paths to old location | `ff61ee97` |
| EC-7 | Re-running migration on a partially-migrated tree | `880930a2` (`.migrated` markers, prevent `.migrated.migrated`) |
| EC-8 | Active sessions exist during migration | Pre-flight check, **bypassed only for `--dry-run`** (`c800c89c`) |
| EC-14 | Corrupt or unparseable session JSON | Whitelisted JSON parse with rejection (`fd4f969f`) |
| EC-27 | Rollback after partial success — preserve already-migrated worktrees | `fd4f969f` |
| — | Stray worktree recursion (worktree containing worktree path) | `ff61ee97` |
| — | Empty / orphaned archive directories | Archive removal commits (group C) |
| — | Stale `running.json` from previous crashed `ao start` | Auto-pruned on next read (pre-existing, kept) |
| — | High-entropy test placeholders triggering gitleaks | `31b20ed2`, `3e23c8db` (targeted regex) |
| — | Two `ao migrate-storage` runs racing | File-locked global config writes (`bb4c68fc`) |
| — | Partial failure in one project — others succeed | Per-project transaction isolation |
| — | `ao start <project>` after `ao stop <project>` hits "already running" | `removeProjectFromRunning()` + `projectNeedsRestart` gate |
| — | `projectNeedsRestart` false-triggers on path/URL args | `isProjectId` guard: `!isRepoUrl(arg) && !isLocalPath(arg)` |
| — | Ctrl+C leaves tmux orphans | Signal handler mirrors full `ao stop` cleanup (`b4feda79`) |
Each of these has at least one test. **If you change `migrate-storage/`, run** `pnpm --filter @aoagents/ao-core test` **and read the failures carefully** — they're load-bearing.
---
## Hot zones — touch with care
These files have subtle invariants and high blast radius. State which invariants you preserve when modifying them (per `CLAUDE.md`).
### `packages/core/src/migrate-storage/`
- The order of operations in the per-project transaction is checkpointed for rollback. Reordering = corruption on partial failure.
- The `.migrated` marker scheme — re-runs depend on it.
- Git worktree path rewriting regex — intentionally narrow. Broadening it has caused data loss before (`ff61ee97`).
- Atomic write helpers (temp + rename). Don't substitute direct `fs.writeFile`.
### `packages/core/src/lifecycle-state.ts` + `lifecycle-manager.ts`
- `state` + `reason` are the source of truth. **Never persist legacy `SessionStatus`.**
- `deriveLegacyStatus` is a *display-only* read function. Don't introduce write paths through it.
- State transitions have implicit dependencies — see CLAUDE.md "Working Principles → Think Before Coding."
### `packages/core/src/session-manager.ts`
- `sm.list()` reconciles stale runtimes (`runtime_lost`) — this is what keeps the dashboard honest. Don't short-circuit it.
- No more archive lookup paths. If you find yourself wanting one, you're solving the wrong problem.
### `packages/cli/src/commands/start.ts` (includes stop logic — there is no `stop.ts`)
- `last-stop.json` schema includes `otherProjects` for cross-project restore. Adding fields → bump schema, write a migration test.
- `ao stop <project>` (with arg) **must not** kill the parent process or the dashboard. There's a regression test for this; it caught a real bug (`95cf979d`).
- `ao stop <project>` calls `removeProjectFromRunning(projectId)` — removing the project from `running.json` so that `ao start <project>` can restart. The `projectNeedsRestart` gate in `ao start` depends on this.
- Ctrl+C handler has a **10s hard timeout**. Don't remove — cleanup hangs are real.
- The `projectNeedsRestart` gate uses an `isProjectId` guard (`!isRepoUrl && !isLocalPath`) to avoid false triggers when `projectArg` is a filesystem path or URL.
### `packages/cli/src/lib/running-state.ts`
- Advisory lockfile system (`O_EXCL` atomic creation) with jittered backoff and dead-owner cleanup.
- `removeProjectFromRunning()` — surgically removes a project from `running.json.projects[]`. 6 dedicated tests cover targeted vs full stop semantics.
### `packages/web/src/components/Dashboard.tsx`
- Sidebar must show all sessions across all projects, regardless of `projectId`. Per-project filtering happens client-side via `projectSessions` (see commit `53e8476f`).
- SSE 5s interval is hard-coded by constraint C-14. Don't change.
---
## Known risks at handoff
Things to verify still hold when you pick this up:
1. **Upstream drift.** This PR has been merged with `upstream/main` multiple times. Run `git fetch upstream && git log --oneline upstream/main ^storage-redesign` — if the result is non-empty, plan a merge before pushing.
2. **Migration on real user data.** All migration tests use synthetic fixtures. Before merge, the author validated against personal `~/.agent-orchestrator/` once. Worth re-verifying on any reviewer's machine.
3. **The `last-stop.json` ↔ `running.json` interaction.** If `ao start` crashes between writing `running.json` and the restore prompt, the next `ao start` will see both files. Current behavior: prompt restore, then proceed. Verify this is still the case.
4. **Dashboard during migration.** If the user has the dashboard open while running `ao migrate-storage`, the SSE stream may briefly 404 on a session whose path moved. Pre-existing tolerance handles it; don't tighten the error UI.
5. **Plugin authors with hardcoded `storageKey`.** External plugins that built against the old type may break. Search consumer plugins for `storageKey` references. Within this monorepo, all plugins are clean.
---
## Verification checklist before claiming "done"
Before pushing to `storage-redesign` or merging the simulation branch:
```bash
# Full pre-flight
pnpm install
pnpm build
pnpm lint
pnpm format:check
pnpm typecheck
pnpm test
pnpm --filter @aoagents/ao-web test
pnpm test:integration # only if your change touches CLI / lifecycle / migration
```
**Manual smoke tests (do not skip if you touched CLI or migration):**
1. Fresh `~/.agent-orchestrator/`:
- `ao start` → spawn a session → `ao stop``ao start` → confirm restore prompt → accept → session resumes.
2. Cross-project:
- Register two projects in global config.
- Spawn a session in each.
- `ao stop` (no args) — both die. `last-stop.json` has both.
- `ao start` from project A — restore prompt offers both, including project B's.
3. `ao stop <projectB>` from inside project A — only project B's sessions die. Dashboard + parent process untouched.
4. Ctrl+C in `ao start` — sessions die, `last-stop.json` written, no tmux orphans (`tmux ls` is empty).
5. Migration on a V1 layout snapshot:
- `ao migrate-storage --dry-run` → review plan.
- `ao migrate-storage` → V2 layout exists, archive content folded into `sessions/`, no orphan files.
6. Dashboard:
- Multi-project sidebar shows all projects' sessions when filter changes.
- Restore button works on a terminated session.
---
## Things reviewers explicitly rejected
If you're tempted to do any of these, *don't* — they were proposed in earlier rounds and shot down:
- Adding a configurable archive directory ("for users who want to keep an archive"). Plugin slot, not config.
- Keeping `SessionStatus` as a "compatibility field." It was the source of bugs; it stays derived-only.
- Using a database. AO is flat-file by design.
- Rebasing the PR. The merge commits from upstream are intentional — they preserve review context.
- Splitting the PR. Tried earlier; storage + migration + status + CLI are too entangled.
---
## When in doubt
1. Re-read `CLAUDE.md` "Working Principles."
2. Read the relevant commit message — they're detailed for this PR.
3. Search the PR conversation on GitHub: https://github.com/ComposioHQ/agent-orchestrator/pull/1466
4. Bias toward simplification (per `feedback_simplify_backend` memory). The backend is already lean after this PR; keep it that way.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

@ -1,649 +0,0 @@
#!/usr/bin/env bash
#
# scripts/demo-pr-1466.sh
#
# End-to-end demo for PR #1466 (storage redesign + cross-project CLI rework).
# Designed to be run live for a screencast — silent narration via section
# banners, no live typing, deterministic output.
#
# Strict sandbox: redirects $HOME to /tmp/ao-demo-1466 so getAoBaseDir()
# resolves there instead of touching the operator's real ~/.agent-orchestrator.
# After the script exits the original $HOME of the parent shell is unaffected.
#
# Usage:
# scripts/demo-pr-1466.sh
#
# Re-run is idempotent — wipes and recreates the sandbox each time.
#
set -euo pipefail
# ─── config ────────────────────────────────────────────────────────────────
DEMO_HOME="/tmp/ao-demo-1466"
DEMO_PORT="3947"
AO_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
AO_CLI="$AO_REPO/packages/cli/dist/index.js"
# Save the operator's real HOME so we can restore it for sub-commands
# that legitimately need it (running the full test suites — tests read
# getGlobalConfigPath() which honors AO_GLOBAL_CONFIG, and a sandboxed
# global config would break tests that touch the global registry).
REAL_HOME="$HOME"
# Sandbox the entire script under a fake HOME so AO's hardcoded
# ~/.agent-orchestrator path is redirected. Belt-and-suspenders: also
# pin AO_GLOBAL_CONFIG explicitly.
export HOME="$DEMO_HOME"
export AO_GLOBAL_CONFIG="$DEMO_HOME/.agent-orchestrator/config.yaml"
# Local CLI invoker — never use the system `ao`.
ao() {
node "$AO_CLI" "$@"
}
banner() {
printf '\n'
printf '═══════════════════════════════════════════════════════════════════════\n'
printf ' %s\n' "$1"
printf '═══════════════════════════════════════════════════════════════════════\n'
sleep 2
}
step() {
printf '\n→ %s\n' "$1"
sleep 1
}
note() {
printf ' %s\n' "$1"
}
# ─── pre-flight ────────────────────────────────────────────────────────────
banner "Pre-flight: build CLI + reset sandbox"
if [[ ! -f "$AO_CLI" ]]; then
note "CLI bundle not found, building..."
(cd "$AO_REPO" && pnpm --filter @aoagents/ao-cli build >/dev/null)
fi
rm -rf "$DEMO_HOME"
mkdir -p "$DEMO_HOME/.agent-orchestrator"
# Minimal git config so worktree operations during the demo don't fail
# from the redirected HOME.
cat >"$DEMO_HOME/.gitconfig" <<'GITCONFIG'
[user]
name = AO Demo
email = demo@example.com
[init]
defaultBranch = main
[advice]
detachedHead = false
GITCONFIG
note "Repo: $AO_REPO"
note "CLI: $AO_CLI"
note "Sandbox: $DEMO_HOME"
note "Port: $DEMO_PORT (no real ao daemon spawned in this demo)"
sleep 2
# ─── helpers: realistic project + session seeding ─────────────────────────
# seed_project DIR PKG_NAME
# Creates a real-looking Node/TypeScript project at DIR with multiple
# commits so reviewers see a credible source tree, not an empty README.
seed_project() {
local dir="$1"
local pkg="$2"
mkdir -p "$dir/src/lib" "$dir/tests"
cat >"$dir/package.json" <<EOF
{
"name": "${pkg}",
"version": "0.1.0",
"description": "Demo project for the PR #1466 migration screencast",
"type": "module",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"test": "vitest run",
"lint": "eslint src/"
},
"dependencies": { "zod": "^3.22.0" },
"devDependencies": { "typescript": "^5.4.0", "vitest": "^1.0.0" }
}
EOF
cat >"$dir/src/index.ts" <<'EOF'
import { processInput, validate } from "./lib/util.js";
import type { Result } from "./lib/types.js";
export async function main(input: string): Promise<Result> {
const validated = validate(input);
return processInput(validated);
}
if (import.meta.url === `file://${process.argv[1]}`) {
main(process.argv[2] ?? "")
.then((r) => console.log(r))
.catch((e) => {
console.error("error:", e.message);
process.exit(1);
});
}
EOF
cat >"$dir/src/lib/util.ts" <<'EOF'
import type { Result, Validated } from "./types.js";
export function validate(input: string): Validated {
if (!input || input.length < 2) throw new Error("input must be at least 2 chars");
return { value: input.trim(), receivedAt: new Date().toISOString() };
}
export async function processInput(v: Validated): Promise<Result> {
return { input: v.value, output: v.value.toUpperCase(), processedAt: new Date().toISOString() };
}
EOF
cat >"$dir/src/lib/types.ts" <<'EOF'
export interface Validated { value: string; receivedAt: string }
export interface Result { input: string; output: string; processedAt: string }
EOF
cat >"$dir/tests/index.test.ts" <<'EOF'
import { describe, it, expect } from "vitest";
import { main } from "../src/index.js";
describe("main", () => {
it("uppercases input", async () => {
expect((await main("hello")).output).toBe("HELLO");
});
it("rejects too-short input", async () => {
await expect(main("")).rejects.toThrow();
});
});
EOF
cat >"$dir/.gitignore" <<'EOF'
node_modules/
dist/
*.log
.env
.DS_Store
EOF
cat >"$dir/tsconfig.json" <<'EOF'
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"outDir": "dist",
"declaration": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
EOF
cat >"$dir/README.md" <<EOF
# ${pkg}
Demo project for the AO PR #1466 migration screencast. Standard TypeScript
package layout — sources in \`src/\`, tests in \`tests/\`, vitest harness.
## Develop
\`\`\`bash
pnpm install
pnpm test
pnpm build
\`\`\`
EOF
# Commit history so the project looks alive, not stamped-out
git -C "$dir" init --quiet -b main
git -C "$dir" add README.md .gitignore
git -C "$dir" commit -q -m "init: scaffold ${pkg}"
git -C "$dir" add tsconfig.json package.json
git -C "$dir" commit -q -m "chore: typescript + package.json"
git -C "$dir" add src/lib/types.ts
git -C "$dir" commit -q -m "feat: add Validated and Result types"
git -C "$dir" add src/lib/util.ts
git -C "$dir" commit -q -m "feat: validate and processInput helpers"
git -C "$dir" add src/index.ts
git -C "$dir" commit -q -m "feat: main entry point"
git -C "$dir" add tests/index.test.ts
git -C "$dir" commit -q -m "test: cover main happy path and validation"
}
# seed_session HASH_DIR SESSION_ID PROJECT_KEY METADATA_BODY
seed_session() {
local hash_dir="$1"
local sid="$2"
shift 2
local body="$*"
printf '%s\n' "$body" >"$hash_dir/sessions/$sid"
}
# ───────────────────────────────────────────────────────────────────────────
banner "Act 1 — Migration: V1 hash dirs → V2 projects/ (most-reviewed code)"
# ───────────────────────────────────────────────────────────────────────────
step "Seed a realistic environment: 2 projects, 6 sessions, real worktree content"
# Project 1: myproject (TS package, full source tree, 6-commit history)
DEMO_REPO_A="$DEMO_HOME/myproject"
seed_project "$DEMO_REPO_A" "myproject"
# Project 2: frontend (also TS package — different code so two distinct projects)
DEMO_REPO_B="$DEMO_HOME/frontend"
seed_project "$DEMO_REPO_B" "frontend"
# ── V1 hash dir 1: myproject ──────────────────────────────────────────────
# In V1: terminated sessions were MOVED from sessions/<sid> into
# sessions/archive/<sid>_<timestamp> as separate files. Active sessions
# stayed in sessions/<sid>.
# In V2 (this PR): the archive directory is removed entirely. Terminated
# sessions just stay in sessions/<sid>.json with lifecycle.session.state =
# "terminated". The migrator flattens archive entries into sessions/.
HASH_DIR_A="$DEMO_HOME/.agent-orchestrator/aaaaaa000000-myproject"
mkdir -p "$HASH_DIR_A/sessions/archive" "$HASH_DIR_A/worktrees"
LIFECYCLE_WORKING='{"version":2,"session":{"kind":"worker","state":"working"},"runtime":{"state":"alive"},"pr":{"state":"unknown"}}'
LIFECYCLE_STUCK='{"version":2,"session":{"kind":"worker","state":"stuck","reason":"agent_idle_too_long"},"runtime":{"state":"alive"},"pr":{"state":"unknown"}}'
LIFECYCLE_ORCH='{"version":2,"session":{"kind":"orchestrator","state":"working"},"runtime":{"state":"alive"},"pr":{"state":"none"}}'
# ao-1: the headline session — has BOTH agent-report state AND report-watcher
# counters set, plus PR fields. Exercises the entire @ashish921998 flat-key
# contract in a single record.
seed_session "$HASH_DIR_A" "ao-1" \
"project=myproject
agent=claude-code
status=working
createdAt=2026-04-21T12:00:00.000Z
agentReportedState=needs_input
agentReportedAt=2026-04-21T12:35:00.000Z
agentReportedNote=please clarify the spec
agentReportedPrUrl=https://github.com/demo/myproject/pull/41
agentReportedPrNumber=41
agentReportedPrIsDraft=true
reportWatcherTriggerCount=2
reportWatcherActiveTrigger=stale_report
reportWatcherTriggerActivatedAt=2026-04-21T12:30:00.000Z
reportWatcherLastAuditedAt=2026-04-21T12:36:00.000Z
prAutoDetect=on
dashboardPort=3000
terminalWsPort=3001
branch=session/ao-1
worktree=$DEMO_REPO_A/worktrees/ao-1
statePayload=$LIFECYCLE_WORKING
stateVersion=2
issue=demo/myproject#101
pr=demo/myproject#41
runtimeHandle={\"id\":\"ao-1\",\"runtimeName\":\"tmux\",\"data\":{\"name\":\"my-1\"}}"
# ao-3: stuck session with report-watcher counter (also exercises @ashish921998 fix)
seed_session "$HASH_DIR_A" "ao-3" \
"project=myproject
agent=claude-code
status=stuck
createdAt=2026-04-21T13:00:00.000Z
agentReportedState=working
agentReportedAt=2026-04-21T14:50:00.000Z
reportWatcherTriggerCount=3
reportWatcherActiveTrigger=stale_report
reportWatcherTriggerActivatedAt=2026-04-21T14:00:00.000Z
reportWatcherLastAuditedAt=2026-04-21T14:55:00.000Z
prAutoDetect=on
dashboardPort=3000
branch=session/ao-3
worktree=$DEMO_REPO_A/worktrees/ao-3
statePayload=$LIFECYCLE_STUCK
stateVersion=2"
# Orchestrator session — different `kind`, exercises lifecycle.session.kind=orchestrator
seed_session "$HASH_DIR_A" "my-orchestrator-1" \
"project=myproject
agent=claude-code
status=working
createdAt=2026-04-21T11:00:00.000Z
prAutoDetect=on
dashboardPort=3000
branch=orchestrator/my-orchestrator-1
worktree=$DEMO_REPO_A/worktrees/my-orchestrator-1
statePayload=$LIFECYCLE_ORCH
stateVersion=2
role=orchestrator"
# ao-2 in archive — V1 archive file at sessions/archive/<sid>_<ts>.
# Migration's job: flatten this into sessions/ao-2.json with terminated lifecycle.
cat >"$HASH_DIR_A/sessions/archive/ao-2_20260420T100000Z" <<'V1META'
project=myproject
agent=claude-code
status=killed
createdAt=2026-04-20T08:00:00.000Z
branch=session/ao-2
statePayload={"version":2,"session":{"kind":"worker","state":"terminated","reason":"manually_killed"},"runtime":{"state":"missing","reason":"manual_kill_requested"},"pr":{"state":"unknown"}}
stateVersion=2
V1META
# Real worktree content for ao-1 — proves worktree migration moves files,
# not just empty directories. Uses git worktree add so it's a registered
# worktree, exactly what the migrator handles in production.
git -C "$DEMO_REPO_A" worktree add -b session/ao-1 "$HASH_DIR_A/worktrees/ao-1" main >/dev/null 2>&1 || true
echo "// pending edits for issue #101" >"$HASH_DIR_A/worktrees/ao-1/src/lib/util.ts.draft"
# ── V1 hash dir 2: frontend ───────────────────────────────────────────────
HASH_DIR_B="$DEMO_HOME/.agent-orchestrator/bbbbbb111111-frontend"
mkdir -p "$HASH_DIR_B/sessions/archive" "$HASH_DIR_B/worktrees"
# fe-1: working session with PR fields populated
seed_session "$HASH_DIR_B" "fe-1" \
"project=frontend
agent=claude-code
status=pr_open
createdAt=2026-04-21T09:00:00.000Z
prAutoDetect=on
dashboardPort=3000
branch=session/fe-1
worktree=$DEMO_REPO_B/worktrees/fe-1
issue=demo/frontend#7
pr=demo/frontend#12
statePayload={\"version\":2,\"session\":{\"kind\":\"worker\",\"state\":\"working\"},\"runtime\":{\"state\":\"alive\"},\"pr\":{\"state\":\"open\",\"number\":12}}
stateVersion=2"
# fe-2 in archive — terminated by runtime_lost
cat >"$HASH_DIR_B/sessions/archive/fe-2_20260419T160000Z" <<'V1META'
project=frontend
agent=claude-code
status=killed
createdAt=2026-04-19T14:00:00.000Z
branch=session/fe-2
statePayload={"version":2,"session":{"kind":"worker","state":"terminated","reason":"runtime_lost"},"runtime":{"state":"missing","reason":"agent_process_exited"},"pr":{"state":"unknown"}}
stateVersion=2
V1META
# ── Pre-seed the global config the migrator reads ────────────────────────
cat >"$DEMO_HOME/.agent-orchestrator/config.yaml" <<CFG
port: $DEMO_PORT
defaults:
runtime: tmux
agent: claude-code
workspace: worktree
notifiers: []
projects:
myproject:
projectId: myproject
path: $DEMO_REPO_A
repo:
owner: demo
name: myproject
platform: github
originUrl: https://github.com/demo/myproject
defaultBranch: main
source: ao-project-add
registeredAt: 1776000000
displayName: myproject
sessionPrefix: my
storageKey: aaaaaa000000
frontend:
projectId: frontend
path: $DEMO_REPO_B
repo:
owner: demo
name: frontend
platform: github
originUrl: https://github.com/demo/frontend
defaultBranch: main
source: ao-project-add
registeredAt: 1776000100
displayName: frontend
sessionPrefix: fe
storageKey: bbbbbb111111
CFG
step "Before — V1 layout on disk (real repo + multi-session inventory)"
echo " Source repos (realistic TS package layout, multi-commit history):"
echo " myproject/ ($(git -C "$DEMO_REPO_A" log --oneline | wc -l | tr -d ' ') commits, $(find "$DEMO_REPO_A" -type f -not -path '*/.git/*' -not -path '*/worktrees/*' | wc -l | tr -d ' ') files)"
echo " frontend/ ($(git -C "$DEMO_REPO_B" log --oneline | wc -l | tr -d ' ') commits, $(find "$DEMO_REPO_B" -type f -not -path '*/.git/*' -not -path '*/worktrees/*' | wc -l | tr -d ' ') files)"
echo
echo " ~/.agent-orchestrator/ (V1: hash-prefixed dirs, key=value session files):"
ls -1 "$DEMO_HOME/.agent-orchestrator/" | sed 's/^/ /'
echo
echo " V1 hash dir 1 (myproject):"
echo " sessions/ : $(ls "$HASH_DIR_A/sessions" | grep -v '^archive$' | tr '\n' ' ')"
echo " sessions/archive/ : $(ls "$HASH_DIR_A/sessions/archive" 2>/dev/null | tr '\n' ' ')"
echo " worktrees/ : $(ls "$HASH_DIR_A/worktrees" 2>/dev/null | tr '\n' ' ')"
echo
echo " V1 hash dir 2 (frontend):"
echo " sessions/ : $(ls "$HASH_DIR_B/sessions" | grep -v '^archive$' | tr '\n' ' ')"
echo " sessions/archive/ : $(ls "$HASH_DIR_B/sessions/archive" 2>/dev/null | tr '\n' ' ')"
echo
echo " Session metadata format (key=value, flat strings) — ao-1:"
sed 's/^/ /' "$HASH_DIR_A/sessions/ao-1"
sleep 6
step "ao migrate-storage --dry-run (shows the plan, mutates nothing)"
ao migrate-storage --dry-run --force || true
sleep 3
step "ao migrate-storage (atomic per-project, with rollback on failure)"
ao migrate-storage --force
sleep 2
step "After — V2 layout (projects/{projectId}/sessions/{sid}.json)"
echo " Top level:"
ls -1 "$DEMO_HOME/.agent-orchestrator/" | sed 's/^/ /'
echo
echo " projects/ (one dir per project, archives flattened into sessions/):"
for p in "$DEMO_HOME"/.agent-orchestrator/projects/*; do
pname=$(basename "$p")
[[ "$pname" == *.migrated ]] && continue
echo " $pname/"
echo " sessions/ : $(ls "$p/sessions" 2>/dev/null | tr '\n' ' ')"
if [[ -d "$p/worktrees" ]]; then
echo " worktrees/ : $(ls "$p/worktrees" 2>/dev/null | tr '\n' ' ')"
fi
done
echo
# Inspect the JSON for ao-1 (the headline session: agent-report state set,
# PR fields, runtimeHandle as embedded JSON — exercises the most fields).
MIGRATED_PROJECT="myproject"
SESSION_JSON="$DEMO_HOME/.agent-orchestrator/projects/$MIGRATED_PROJECT/sessions/ao-1.json"
sleep 4
step "Migrated session JSON (note: typed fields, no key=value soup)"
node -e "
const fs = require('fs');
const d = JSON.parse(fs.readFileSync('$SESSION_JSON', 'utf-8'));
const out = {
branch: d.branch, status: d.status, agent: d.agent, prAutoDetect: d.prAutoDetect,
dashboard: d.dashboard, lifecycle: d.lifecycle ? '(...)' : undefined,
agentReportedState: d.agentReportedState,
agentReportedAt: d.agentReportedAt,
agentReportedNote: d.agentReportedNote,
reportWatcherTriggerCount: d.reportWatcherTriggerCount,
reportWatcherActiveTrigger: d.reportWatcherActiveTrigger,
agentReport_nested_wrapper: d.agentReport ?? '(undefined — correct)',
reportWatcher_nested_wrapper: d.reportWatcher ?? '(undefined — correct)',
};
console.log(JSON.stringify(out, null, 2).split('\n').map(l => ' ' + l).join('\n'));
"
sleep 4
step "Verify @ashish921998 fix: agent-report keys stayed FLAT after migration"
note "Live runtime readers (parseExistingAgentReport, lifecycle-manager)"
note "look up flat keys on session.metadata. readMetadataRaw → flattenToStringRecord"
note "does NOT unfold nested objects, so a nested agentReport.* would silently"
note "drop this state. Migration keeps these flat — proven below:"
echo
node -e "
const fs = require('fs');
const d = JSON.parse(fs.readFileSync('$SESSION_JSON', 'utf-8'));
const required = [
'agentReportedState', 'agentReportedAt', 'agentReportedNote',
'reportWatcherTriggerCount', 'reportWatcherActiveTrigger',
];
let ok = true;
for (const k of required) {
const present = d[k] !== undefined;
console.log(' ' + (present ? '✓' : '✗') + ' ' + k + ' = ' + (d[k] ?? 'MISSING'));
if (!present) ok = false;
}
if (d.agentReport !== undefined || d.reportWatcher !== undefined) {
console.log(' ✗ nested wrapper present — would shadow flat keys via flattenToStringRecord');
ok = false;
}
console.log();
console.log(' ' + (ok ? 'PASS' : 'FAIL') + ' — agent-report flat-key contract preserved');
"
sleep 5
step "Rollback safety: re-running migration is a no-op (markers prevent re-process)"
ao migrate-storage --force 2>&1 | tail -5
sleep 3
# ───────────────────────────────────────────────────────────────────────────
banner "Act 2 — Cross-project CLI (the P1 review fix)"
# ───────────────────────────────────────────────────────────────────────────
note "Behavior under test:"
note " 1. ao start (project=A) → running.json {pid, projects:[A]}"
note " 2. ao stop A → projects:[] (parent alive)"
note " 3. ao start A → projects:[A] same pid (ATTACH, no 2nd daemon)"
note ""
note "Pre-fix: step 3 fell through to runStartup() → spawned a SECOND dashboard"
note "on a new port, clobbered running.json. Reproduced and fixed in commit bfc7f48f."
sleep 3
step "The regression test that asserts no second daemon is registered"
echo
sed -n '/attaches to existing daemon (no second dashboard)/,/^ });$/p' \
"$AO_REPO/packages/cli/__tests__/commands/start.test.ts" \
| head -50 | sed 's/^/ /'
sleep 5
step "Run the test live (filtered by test name via vitest -t)"
(cd "$AO_REPO" && pnpm --filter @aoagents/ao-cli test -- start.test.ts -t "attaches to existing daemon" 2>&1 \
| grep -E "✓|✗|FAIL|Test Files|^\s*Tests " | head -10 | sed 's/^/ /') || true
sleep 3
step "removeProjectFromRunning + addProjectToRunning are the round-trip primitives"
echo
grep -n "export async function \(removeProjectFromRunning\|addProjectToRunning\)" \
"$AO_REPO/packages/cli/src/lib/running-state.ts" | sed 's/^/ /'
sleep 3
# ───────────────────────────────────────────────────────────────────────────
banner "Act 3 — Dashboard sidebar shows ALL projects regardless of route"
# ───────────────────────────────────────────────────────────────────────────
note "useSessionEvents on the dashboard is now called WITHOUT a project filter."
note "Per-project filtering happens client-side via the projectSessions memo."
sleep 2
step "The fix in Dashboard.tsx"
grep -B 1 -A 7 "No project filter — sidebar needs all sessions" \
"$AO_REPO/packages/web/src/components/Dashboard.tsx" 2>/dev/null \
| sed 's/^/ /' || note "(see commit 53e8476f)"
sleep 4
# ───────────────────────────────────────────────────────────────────────────
banner "Act 4 — Restore from ao stop (last-stop.json round-trip)"
# ───────────────────────────────────────────────────────────────────────────
step "Simulate what ao stop writes to last-stop.json"
cat >"$DEMO_HOME/.agent-orchestrator/last-stop.json" <<JSON
{
"stoppedAt": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"projectId": "$MIGRATED_PROJECT",
"sessionIds": ["ao-1"],
"otherProjects": []
}
JSON
echo
sed 's/^/ /' "$DEMO_HOME/.agent-orchestrator/last-stop.json"
sleep 3
step "What ao start does with it (start.ts)"
echo
grep -n "readLastStop\|Restore .* sessions" \
"$AO_REPO/packages/cli/src/commands/start.ts" | head -6 | sed 's/^/ /'
sleep 3
step "Cross-project sessions in the prompt — otherProjects field"
echo
grep -n "otherProjects" \
"$AO_REPO/packages/cli/src/lib/running-state.ts" \
"$AO_REPO/packages/cli/src/commands/start.ts" | head -6 | sed 's/^/ /'
sleep 3
# ───────────────────────────────────────────────────────────────────────────
banner "Act 5 — Ctrl+C performs a full graceful shutdown (no tmux orphans)"
# ───────────────────────────────────────────────────────────────────────────
note "SIGINT/SIGTERM handler in start.ts mirrors ao stop:"
note " kill all sessions → write last-stop.json → unregister → process.exit"
note " 10s hard timeout via setTimeout().unref() in case cleanup hangs."
sleep 2
step "The shutdown handler"
echo
grep -n "shutdown.*signal: NodeJS.Signals\|10s hard timeout\|SHUTDOWN_TIMEOUT_MS" \
"$AO_REPO/packages/cli/src/commands/start.ts" | head -10 | sed 's/^/ /'
sleep 3
# ───────────────────────────────────────────────────────────────────────────
banner "Act 6 — Empty-repo guard for ao start <URL>"
# ───────────────────────────────────────────────────────────────────────────
note "Before fix: empty repos caused 'Unable to resolve base ref' deep inside"
note "the worktree plugin. Now we detect via origin/HEAD and fail early with"
note "a useful message before ensureOrchestrator runs."
sleep 2
step "The detection helper + the early-exit message"
echo
sed -n '/detectClonedRepoDefaultBranch/,/^}/p' \
"$AO_REPO/packages/cli/src/commands/start.ts" \
| head -25 | sed 's/^/ /'
echo
grep -B 1 -A 4 "appears to be empty (no commits or refs)" \
"$AO_REPO/packages/cli/src/commands/start.ts" | head -10 | sed 's/^/ /'
sleep 5
# ───────────────────────────────────────────────────────────────────────────
banner "Test summary: 560 CLI + 981 core (last full run)"
# ───────────────────────────────────────────────────────────────────────────
# Restore the real HOME and unset sandbox env vars when running the full
# test suites — otherwise tests that read getGlobalConfigPath() see the
# demo's sparse config and fail spuriously.
step "pnpm --filter @aoagents/ao-cli test"
(cd "$AO_REPO" && env -u AO_GLOBAL_CONFIG HOME="$REAL_HOME" pnpm --filter @aoagents/ao-cli test 2>&1 \
| grep -E "^\s*(Tests|Test Files|Duration)" | sed 's/^/ /') || true
step "pnpm --filter @aoagents/ao-core test"
(cd "$AO_REPO" && env -u AO_GLOBAL_CONFIG HOME="$REAL_HOME" pnpm --filter @aoagents/ao-core test 2>&1 \
| grep -E "^\s*(Tests|Test Files|Duration)" | sed 's/^/ /') || true
# ───────────────────────────────────────────────────────────────────────────
banner "Demo complete — sandbox left in $DEMO_HOME for inspection"
# ───────────────────────────────────────────────────────────────────────────
note "To re-run: scripts/demo-pr-1466.sh"
note "To clean: rm -rf $DEMO_HOME"
note ""
note "Reviewer next steps:"
note " • Inspect $DEMO_HOME/.agent-orchestrator/ to verify V2 shape"
note " • Diff against base: git diff origin/main...storage-redesign"
note " • Visual spec: pr-1466.html"
note " • Behavior dashboard: https://theharshitsingh.com/static/pr-1466.html"
echo