* feat(core): add prs[] array to Session for multi-PR support — Phase 1 metadata layer * feat(core): update lifecycle-manager for multi-PR per session — Phase 2 * feat(web): add prs[] to DashboardSession and wire up multi-PR display — Phase 3 * feat(plugins): append to prs metadata field on gh pr create — Phase 4 * fix: address all code review issues — PR number, enrichment pipeline, claimPR append, stale PRs, V1 compat, SCM calls, dead code, isDraft, session validation * fix: address Greptile review — state machine aggregation, URL dedup, React keys, review comments, typecheck * fix: remove useless assignment in lifecycle-manager — lint error * fix: add missing prs field to Session objects in cli and plugin test files * fix: add prs field to integration-tests helpers and fix V1 flat-file fallback in agent-claude-code * fix: use stack approach for claimPR prs field — claimed PR becomes primary * feat(core,web): multi-PR per session — enrichment, status dots, bug fixes (#1821) - Session metadata now stores a comma-separated prs list; all PR URLs parsed into session.prs[] on load, backwards-compatible with single-PR - Enrichment loop hydrates every PR in the list each poll cycle via the existing batch GraphQL call - Fixed closed-PR filter reading from session lifecycle state instead of per-PR enrichment cache, which misclassified secondary PRs - Fixed isDraft never propagating from enrichment cache to dashboard.pr and dashboard.prs[i] - Fixed prs[0] and dashboard.pr being separate objects — enrichment on one now syncs to the other - SessionCard PR badges now show a colored status dot per PR: green (CI passing/merged), yellow (CI pending), red (CI failing/changes requested), grey (unenriched/draft/closed) - Corrected color token mapping: passing uses --color-status-merge (green), pending uses --color-status-pending (yellow) * fix(core): add missing prs field to code-review-manager test fixture * feat(web): per-PR rows with colored chips, titles, and click-to-switch on session card - Replace single-PR badges with one row per PR when session has multiple PRs - Each row shows: status-colored chip, PR title (truncated), diff size - Chip colors: green (CI passing/merged), yellow (CI pending), red (CI failing/changes requested), grey (closed/draft/unenriched) - Repo initials badge (e.g. AO, CM) shown per row when PRs span multiple repos - Clicking a row selects it — description, alerts, and action buttons in the card body all switch to that PR's context - Clicking the chip opens GitHub; clicking title/diff area selects without navigating away - Single-PR sessions are completely unchanged * fix(web): address multi-PR card review — effectivePR for merge button, color-mix chips, left-border selection * fix(web): derive prs from pr in makeSession test helper * fix(core): fall back to raw[pr] when prs absent in claimPR * feat(web): PR switcher in session detail header for multi-PR sessions * fix(core): treat ciStatus/reviewDecision 'none' as passing in multi-PR aggregation * fix(core): stale-session cleanup checks all prs before killing multi-PR session * fix(core): all-none reviewDecision aggregates to none, not approved * fix(core): prs accumulation in Node hook, bump WRAPPER_VERSION, clear stale enrichment on claimPR * fix(core,web): webhook matching for secondary PRs, restore lifecycle PR number fallback * fix(web): merge endpoint resolves target PR from session.prs for secondary PRs * fix(core,web): clamp selectedPRIndex, guard multi-PR partial cache miss, clear prs on takeover * fix(core,web): scope webhook secondary PR match by owner/repo, handle JSON metadata in Node hook * fix(web): guard merge route PR lookup by owner/repo to prevent number collision * fix(web): forward owner/repo from merge button to activate PR disambiguation guard * fix(web): propagate owner/repo through BottomSheet and AttentionZone merge triggers * test(web): update onMerge assertion to expect owner/repo arguments * fix(core,web): resolve 5 multi-PR logical gaps — conflict detection, partial cache miss, isDraft aggregation, event context prs, AttentionZone labels * fix(core,cli,plugins): add prs field to NotificationEventContext fixtures across all packages * fix(web): add owner/repo guard to primary PR branch in webhook session match * fix: handle unknown update versions and flat interactive config * ci: pin GitHub Actions to SHAs * fix: support new orchestrator from flat config * ci: skip dependency review on unsupported forks * revert: run dependency review on all PRs * docs(design): add dashboard design-language exploration + mockups Capture the design-language direction explored for the AO dashboard as live HTML mockups plus rationale, under docs/design/. - dashboard-language.md: the language — blue=orchestrator / orange=agents identity, rationed semantic color, Schibsted Grotesk + JetBrains Mono, unified status system, layout patterns, and how it diverges from DESIGN.md. - mockups/kanban.html: canonical fleet board (frameless lifecycle columns). - mockups/session.html: canonical session detail (framed xterm + pluggable inspector rail: Summary / Changes / Browser; review-comment "Address"). - mockups/{concepts,refined,*-icons}.html: exploration / icon comparisons. - mockups/mascot.png: the blue conductor mascot. Reference mockups only (CDN fonts + inline styles); production must use the Tailwind tokens in globals.css per C-02/C-07. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(web): migrate dashboard to mission-control design language (supersedes DESIGN.md) Replace the "Warm Terminal" system with the "Mission Control" language from the merged design exploration (docs/design/dashboard-language.md + the kanban & session mockups). One coherent system, dark control-room aesthetic, rationed color (blue = orchestrator/you, orange = working agent, amber = needs-input, red = failing/stuck, green = mergeable, else grayscale). Tokens & fonts - globals.css @theme/.dark: add literal palette (--bg #0a0b0d, --bg-side, --card #15171b = only bordered surface, --term #0c0d10, --line/--line-2, text ramp --t1..--t4, --blue/--orange/--amber/--red/--green) and re-point the existing --color-* semantic tokens to it (no token renames — alias + migrate). Flat surfaces, no warm gradients/glows. Dark theme preserved. - Self-host Schibsted Grotesk (UI) + JetBrains Mono (machine) via next/font/local — no external font CDN. Geist Sans removed. Status system (one spectrum, used everywhere) - lib/status-spec.ts (getStatusSpec) + <StatusBadge> render the kanban card badge, sidebar dot, and session topbar pill from a single source; working dot breathes (CSS-only @keyframes breathe). Fleet board (kanban.html) - Frameless tinted columns Working -> Needs you -> In review -> Ready to merge with per-column semantic top-glow; the card is the only bordered surface (hairline ring, no status left-rail). Sidebar-shows-all-projects + SSE 5s unchanged. Session detail (session.html) - Framed xterm terminal: theme object tied to tokens (orange cursor, blue selection, token-mapped ANSI) — terminal content unstyled. - New pluggable inspector rail <SessionInspector>: Summary (PR -> review comments -> Activity -> Overview) / Changes / Browser. Review comments keep the soft-blue Address action (askAgentToFix passing comment + file:line). - Topbar: shared status pill, Kill, solid-blue Orchestrator primary; the PR popover is now mobile-only (desktop has the inspector). Blue mascot mark. Tests: add status-spec / StatusBadge / SessionInspector / AppMark tests; update column-label, theme, layout-metadata and PR-location assertions; mock next/font/local in vitest. typecheck / test / lint / build all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * wip(web): compact card footer + cleanup before mockup-fidelity pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(web): mockup-faithful redesign pass — sidebar brand, resizable panels, cleaner chrome Addresses review feedback to follow docs/design/mockups/{kanban,session}.html closely. Sidebar (ProjectSidebar + mc-sidebar.css) - Brand + blue mascot mark moved to the TOP of the sidebar (was in the topbar). - Cool sidebar background (var(--color-bg-sidebar) #08090b) — removed the warm tint. - Removed the yellow session-count chip (plain mono count) and the Review button. - Per-project action icons now reveal on hover only. - Consolidated the footer's four buttons into a single Settings gear + popover (show killed / show done / show session id / theme). - Right edge is drag-resizable. Board (Dashboard + AttentionZone + SessionCard + mc-board.css) - Topbar brand removed; "Board" header + mockup subtitle; blue primary CTA. - Frameless tinted columns with per-column glow; compact informational cards (status badge · id · hover terminal, 2-line title, branch w/ git icon, thin PR/CI footer). Removed inline quick-reply/alerts/CI-chips/merge+review buttons from cards (actions live on the session page); kept hover kill + terminal link. - Split Done card into SessionCard.parts.tsx (≤400 lines). Session detail (SessionDetail* + SessionInspector + TerminalControls + mc-session.css) - Topbar: ‹ Kanban back button, title with branch to its RIGHT (git icon, no odd truncation), shared status pill, Kill, blue Orchestrator. Brand removed from the session shell header too. - Terminal header: removed the "Connected" status text; matches the mockup (Terminal label · id · zoom · fullscreen). Terminal flush (no stray left margin). - Terminal head and inspector tab bar are both 47px so their rules align. - Inspector: PR → review comments → Activity timeline → Overview; left edge drag-resizable. Split header helpers into SessionDetailHeader.parts.tsx. Shared infra - useResizable hook (pointer drag → CSS var on :root, localStorage-persisted, no inline style); .resize-handle styles; per-screen mc-*.css loaded after globals.css in layout.tsx; sidebar/inspector widths read --ao-sidebar-w / --ao-inspector-w with sensible min/max + double-click reset. typecheck / web tests (1005) / lint (0 errors) / build all green; resize verified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): sidebar/topbar polish from review feedback - Notification bell icon 13→17px (was tiny inside its 34px button). - Sidebar project rows: count shows at rest in the right slot; per-project action icons are now an absolutely-positioned hover-reveal group (out of flow) so the project name keeps full width and the row stays single-height (fixes the inflated rows / vertically-stacked icons). - Session status dot nudged 1px to vertically center against the mono label. - Orchestrator session page no longer renders the inspector rail (full-width terminal — nothing to inspect). typecheck / web tests (1005) / lint (0 errors) green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): notification icon size + sidebar/topbar review fixes - Notification bell on the session topbar was squeezed to a 6px sliver: the text-button padding rule applied to the icon-only button inside its fixed 34px width. Give the bell its own 34px square / zero-padding rule → 17px icon. - Remove the redundant sidebar toggle from the session topbar on desktop (mobile keeps the drawer hamburger); the sidebar owns collapse/expand. - Collapsed sidebar now shows an expand button so it can be reopened. - Trim sidebar padding + session-list indent so project/agent rows get more width. typecheck / web tests (1005) / lint (0 errors) green; collapse↔expand + icon size verified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): sidebar session names truncate with ellipsis; rename pencil no longer overlays - The session label was an inline <span>, so text-overflow:ellipsis never applied — long names hard-clipped. Make it display:block so it ellipsizes. - The rename pencil now absolutely positions at the row's right edge; at rest the label gets the full row width, and on hover the link gains right-padding so the text re-truncates and the pencil sits in the reserved space instead of covering the name. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): project name reserves space for hover action icons (no overlap) On hover the per-project action icons reveal over the right slot; the toggle now gains right-padding so the project name re-truncates with ellipsis instead of running underneath the icons — same pattern as the session rows. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): remove kanban topbar toggle + redesign empty state to mission-control - The board (kanban) topbar still rendered a desktop sidebar toggle; make it mobile-only (the sidebar owns collapse/expand), matching the session topbar. - Empty state (Skeleton EmptyState): ghost columns now use the live board's labels (Working / Needs you / In review / Ready to merge), 4 columns, and the frameless tinted-trough look — removing the old dashed/warm-tinted columns and the stale 5-category set. Orchestrator glyph recolored to the blue accent via tokens (was hardcoded orange). typecheck / web tests (1005) / lint (0 errors) green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(web): show project name in titlebar, not the raw project id hash The board topbar special-cased projects named 'Agent Orchestrator' to display the project id (e.g. agent-orchestrator_649ba24578) — unreadable gibberish. Always show the human-readable project name instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(core,web): detectPR branch guard, multi-PR prLabel aggregation, reset selectedPRIndex on session change * fix(web): render terminal with xterm WebGL renderer (fixes broken box-drawing) The dashboard terminal used xterm's default DOM renderer, which draws each row as a separate DOM line and cannot tile box-drawing / block glyphs across cells. Agent TUIs (Claude Code's bordered panels, the "N shell command" expansion) rendered with broken/gappy frames and a grey blob — independent of font or lineHeight (verified: even a complete font at lineHeight 1.0 breaks in the DOM renderer). Load xterm's WebGL renderer (@xterm/addon-webgl), which GPU-draws box/block glyphs into each cell so frames connect and shaded regions stay solid. Loaded rAF-deferred after open() with a DOM fallback on context-loss / when WebGL is unavailable (headless, blocklisted GPU). This matches how superset renders the same agent TUIs. The WebGL addon only ships for the xterm 6.1.0-beta train, so bump @xterm/xterm 6.0.0 -> 6.1.0-beta.256 and the addons to the matched beta set (the same pair superset runs in production). Also drops the earlier no-op fontWeight/fontWeightBold/letterSpacing change — those values equalled xterm's defaults and had no visible effect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): enforce dark-mode contrast floor so agent blocks aren't grey blobs Claude Code's expanded "Ran N shell command" block is painted on an ANSI white background (ESC[47m). The terminal theme sets `white` ≈ `foreground` (#c5ccd3), and dark mode used `minimumContrastRatio: 1` (no floor), so the block's text was the same colour as its background — rendering as an unreadable grey blob (only independently-coloured links/emoji showed through). Raise the dark-mode floor to 4.5 (WCAG AA), matching the rationale already used for light mode (7). xterm then auto-adjusts only the foregrounds that fail the floor, leaving all other agent colours untouched. Verified in an isolated harness with the real theme: white-on-white block goes from invisible to legible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): load Unicode 11 addon so emoji/wide-char widths match tmux Agent TUIs and tmux lay out tables treating emoji like ✅/❌ as 2 cells wide (modern Unicode). xterm defaults to Unicode 6 width tables, where those are 1 cell — so the grid shifts a column after every emoji, breaking table borders and leaving stray text fragments (visible only in the web terminal; a direct tmux attach via a modern terminal renders it fine). Load @xterm/addon-unicode11 and set unicode.activeVersion = "11" so xterm's width tables agree with tmux/the agent. Same beta train as the other addons. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): rescale overlapping glyphs so out-of-font chars don't overlap text JetBrains Mono is subset to Latin glyphs, so characters it lacks (arrows → ← ↔, CJK, emoji, powerline) are drawn from a fallback font whose advance can exceed our monospace cell — the glyph then bleeds into the next cell and overlaps the following character. Set rescaleOverlappingGlyphs so xterm shrinks any glyph wider than its cell back to fit. Fixes the whole class of out-of-font overflow, not just arrows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): ship full-coverage JetBrains Mono (restore arrows/box/blocks) The self-hosted JetBrains Mono was subset to ~229 Latin glyphs, dropping arrows (→ ← ↔), box-drawing, block elements and other symbols agents emit. Those fell back to a system font at the wrong cell width — overlapping (pre-rescale) or shrunk/blurry (post-rescale). Replace it with the full JetBrains Mono variable face (OFL-1.1, wght 100–800, ~111 KB woff2, same metrics) so those glyphs render in-font, full-size and crisp at the correct monospace width. rescaleOverlappingGlyphs stays as a safety net for the few glyphs even the full face lacks (CJK, emoji, powerline PUA). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Laraib-1629 <laraibahmed73@gmail.com> Co-authored-by: suraj-markup <sk9261712674@gmail.com> Co-authored-by: itrytoohard <ayetrytoohard@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Priyanshu Choudhary <57816400+Priyanchew@users.noreply.github.com> |
||
|---|---|---|
| .. | ||
| __tests__ | ||
| src | ||
| CHANGELOG.md | ||
| README.md | ||
| package.json | ||
| rollup.config.ts | ||
| tsconfig.build.json | ||
| tsconfig.json | ||
| vitest.config.ts | ||
README.md
@aoagents/ao-core
Core services, types, and configuration for the Agent Orchestrator system.
What's Here
src/types.ts— All TypeScript interfaces (Runtime, Agent, Workspace, Tracker, SCM, Notifier, Terminal, Session, events)src/services/— Core services (SessionManager, LifecycleManager, PluginRegistry)src/config.ts— Configuration loading + Zod schemassrc/utils/— Shared utilities (shell escaping, metadata parsing, etc.)
Key Files
src/types.ts — The Source of Truth
Every interface the system uses is defined here. If you're working on any part of the orchestrator, start by reading this file.
Main interfaces:
Runtime— where sessions execute (tmux on Unix,process/ ConPTY via node-pty on Windows, docker, k8s)Agent— AI coding tool adapter (claude-code, codex, aider)Workspace— code isolation (worktree, clone)Tracker— issue tracking (GitHub Issues, Linear)SCM— PR/CI/reviews (GitHub, GitLab)Notifier— push notifications (desktop, Slack, webhook)Terminal— human interaction UI (iTerm2, web)Session— running agent instance (state, metadata, handles)OrchestratorEvent— events emitted by lifecycle managerPluginModule— what every plugin exports
src/services/session-manager.ts — Session CRUD
Handles session lifecycle:
spawn(config)— create new session (workspace + runtime + agent)list(projectId?)— list all sessionsget(sessionId)— get session detailskill(sessionId)— terminate sessioncleanup(projectId?)— kill completed/merged sessionssend(sessionId, message)— send message to agent
Data flow in spawn():
- Load project config
- Validate issue exists via
Tracker.getIssue()(if issueId provided, fails-fast if not found) - Reserve session ID
- Determine branch name
- Create workspace via
Workspace.create() - Generate prompt via
Tracker.generatePrompt() - Build layered worker prompt via
buildPrompt()intosystemPrompt+taskPrompt - Persist
systemPromptFilefor the session and, for OpenCode workers, writeOPENCODE_CONFIG - Build launch command via
Agent.getLaunchCommand() - Create runtime session via
Runtime.create() - Run
Agent.postLaunchSetup()(optional) - Write metadata file
- Return Session object
Note: If issue validation fails (not found, auth error), spawn fails before creating any resources (no workspace, no runtime, no session ID). This prevents spawning sessions with broken issue references.
Worker sessions keep persistent instructions in the prompt file. OpenCode workers consume that file through OPENCODE_CONFIG, while OpenCode orchestrators continue to project their system prompt into workspace AGENTS.md.
src/services/lifecycle-manager.ts — State Machine + Reactions
Polls sessions, detects state changes, triggers reactions:
State machine:
spawning → working → pr_open → ci_failed/review_pending/approved → mergeable → merged
Reactions:
ci-failed→ send fix prompt to agentchanges-requested→ send review comments to agentapproved-and-green→ notify human (or auto-merge)agent-stuck→ notify human
Polling loop:
- For each session: check agent activity state (
Agent.getActivityState()) - If PR exists: check CI status (
SCM.getCISummary()), review state (SCM.getReviewDecision()) - Update session status based on state
- Trigger reactions if state changed
- Emit events
src/services/plugin-registry.ts — Plugin Discovery + Loading
Loads plugins and provides access to them:
register(plugin, config?)— register a plugin instanceget<T>(slot, name)— get plugin by slot + namelist(slot)— list all plugins for a slotloadBuiltins(config?)— load built-in plugins (runtime-tmux, agent-claude-code, etc.)loadFromConfig(config)— load built-ins today; external plugin descriptors are the marketplace extension point
Built-in plugins (loaded by default):
- runtime-tmux, runtime-process
- agent-claude-code, agent-codex, agent-aider, agent-cursor, agent-kimicode, agent-opencode
- workspace-worktree, workspace-clone
- tracker-github, tracker-linear, tracker-gitlab
- scm-github, scm-gitlab
- notifier-desktop, notifier-discord, notifier-slack, notifier-composio, notifier-openclaw, notifier-webhook
- terminal-iterm2, terminal-web
src/config.ts — Configuration Loading
Loads and validates agent-orchestrator.yaml:
Main config sections:
- Runtime data paths are auto-derived from the config location under
~/.agent-orchestrator/{hash}-{projectId}/ port— web dashboard port (default 3000, set different values for multiple projects)terminalPort— terminal WebSocket port (auto-detected if not set)directTerminalPort— direct terminal WebSocket port (auto-detected if not set)defaults— default plugins (runtime, agent, workspace, notifiers)plugins— installer-managed external plugin descriptors (registry, npm, or local)projects— per-project config (repo, path, branch, symlinks, reactions, agentRules)notifiers— notification channel config (Slack webhooks, etc.)notificationRouting— which notifiers get which priority eventsreactions— auto-response config (ci-failed, changes-requested, approved-and-green, etc.)
Zod schemas validate all config at load time.
Common Tasks
Adding a Field to Session
- Edit
src/types.ts→Sessioninterface - Edit
src/services/session-manager.ts→ initialize field inspawn() - Rebuild:
pnpm --filter @aoagents/ao-core build
Adding an Event Type
- Edit
src/types.ts→EventTypeunion - Emit the event:
eventEmitter.emit()in relevant service - Add reaction handler (optional):
src/services/lifecycle-manager.ts
Adding a Reaction
- Edit
src/services/lifecycle-manager.ts→ add handler function - Wire it up in the polling loop
- Add config schema in
src/config.tsif new reaction type
Feedback Tools (v1)
@aoagents/ao-core exports two structured feedback tool contracts:
bug_reportimprovement_suggestion
Both share the same required input fields:
titlebodyevidence(array of strings)sessionsourceconfidence(0..1)
Example:
import { FEEDBACK_TOOL_NAMES, FeedbackReportStore, getFeedbackReportsDir } from "@aoagents/ao-core";
const reportsDir = getFeedbackReportsDir(configPath, projectPath);
const store = new FeedbackReportStore(reportsDir);
const saved = store.persist(FEEDBACK_TOOL_NAMES.BUG_REPORT, {
title: "SSO login loop",
body: "Google SSO redirects back to /login repeatedly.",
evidence: ["trace_id=abc123", "screenshot: login-loop.png"],
session: "ao-22",
source: "agent",
confidence: 0.84,
});
Storage format:
- Reports are persisted under
~/.agent-orchestrator/{hash}-{projectId}/feedback-reports - Each report is a typed key=value file (
report_<timestamp>_<id>.kv) for easy inspection - A deterministic dedupe key (
sha256, 16 hex chars) is generated from normalized tool+content
Migration notes:
- No migration needed for existing AO installs
- The
feedback-reportsdirectory is created lazily on first persisted report
Testing
# Run all core tests
pnpm --filter @aoagents/ao-core test
# Run in watch mode
pnpm --filter @aoagents/ao-core test -- --watch
# Run specific test
pnpm --filter @aoagents/ao-core test -- session-manager.test.ts
Tests are in src/__tests__/:
session-manager.test.ts— session CRUD, spawn, cleanuplifecycle-manager.test.ts— state machine, reactionsplugin-registry.test.ts— plugin loading, resolutiontmux.test.ts— tmux utility functions (not a plugin test)prompt-builder.test.ts— prompt generation utilities
Building
# Build core
pnpm --filter @aoagents/ao-core build
# Typecheck
pnpm --filter @aoagents/ao-core typecheck
This package is a dependency of all other packages. Build it first if working on the codebase.
Architecture Notes
Why flat metadata files?
- Debuggability:
cat ~/.agent-orchestrator/<hash>-my-app/sessions/app-3shows full state - No database dependency (survives crashes, easy to inspect)
- Backwards-compatible with bash script orchestrator
Why polling instead of webhooks?
- Simpler (no webhook setup, no ngrok for local dev)
- Works offline (CI/review state is fetched, not pushed)
- Survives orchestrator restarts (no missed events)
Why plugin slots?
- Swappability: use tmux on Linux/macOS,
process(ConPTY) on Windows, docker in CI, k8s in prod — same agent/workspace stack across all of them - Testability: mock plugins for tests
- Extensibility: users can add custom plugins (e.g., company-specific notifier)