* feat(core): add PreflightContext + optional preflight() to plugin interfaces Foundation for PR 2 of the ao spawn refactor: lets plugins own their own prerequisites instead of the CLI hardcoding 'if runtime === tmux check tmux' / 'if tracker === github check gh auth' switches. PreflightContext describes intent (willClaimExistingPR, role) rather than CLI flag names, so plugins never learn about flags. New flags map to new intent fields only when a plugin actually needs them. Adds preflight?(ctx) as an optional method on Runtime, Agent, Workspace, Tracker, SCM. Backwards-compatible: existing plugins keep working unchanged. Subsequent commits move checkTmux into runtime-tmux and checkGhAuth into the github plugins, then update spawn.ts to iterate selected plugins instead of switching on plugin names. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(plugins): implement preflight() in runtime-tmux + tracker-github + scm-github Each plugin now owns its own prerequisite checks (tmux binary, gh auth) behind the optional PluginModule preflight() contract added in the previous commit. The CLI no longer needs to know which plugin needs which tool — it just iterates the selected plugins. - runtime-tmux: checks 'tmux -V' and throws with platform-appropriate install hint (brew / apt / dnf / WSL) - tracker-github: checks 'gh --version' and 'gh auth status' unconditionally (tracker is exercised on every spawn that has an issueId AND on lifecycle polling for issue closure) - scm-github: same gh auth checks but only when the spawn will exercise PR-write paths — gates on context.intent.willClaimExistingPR Subsequent commit refactors the CLI to iterate plugins instead of hardcoded 'if runtime === tmux' switches. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(cli): make ao spawn iterate plugin preflight, collapse project resolution Three small changes bundled because they all touch spawn.ts: 1. Plugin-iterating preflight: replaces the hardcoded 'if runtime === tmux check tmux' / 'if tracker === github check gh auth' switches in runSpawnPreflight with a 4-line loop that walks the selected plugins and calls each one's optional preflight(). Plugin internals are no longer leaked into the CLI; new plugins only need to declare their own preflight. 2. Project-resolution collapse: the prefix/no-prefix and issue/no-issue paths previously had three near-duplicate code blocks each with its own try/catch around autoDetectProject. Replaced by one resolveProjectAndIssue() helper that uses resolveSpawnTarget's fallback parameter — caller wraps in a single try/catch. 3. Micro-deletes: drop the unused 'return session.id' in spawnSession (callers already ignore it; the SESSION=<id> stdout line is the scriptable contract). Drop checkTmux/checkGhAuth from lib/preflight.ts (now in their respective plugins) along with their orphaned tests. LOC: roughly net-zero. Wins are structural — adding runtime-podman / tracker-jira / scm-bitbucket no longer requires editing spawn.ts. Pre-existing start.test.ts 'stop command' failures are unrelated (verified on upstream/main bare). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * perf(plugins): dedupe gh-auth check across tracker-github + scm-github Address greptile P2 on PR #1622: when a project has both tracker: github and scm: github with --claim-pr, both plugin preflights ran 'gh --version' + 'gh auth status' independently — 4 execs where 2 suffice, and two identical error messages on failure. Add memoizeAsync(key, fn) to core (process-scoped Promise cache) and have both github plugins share the key 'gh-cli-auth'. Second caller hits the in-flight (or resolved) promise — zero extra subprocess overhead, one error on failure. Caches both successes and rejections: failed checks should never re-run within a process (cache dies with the CLI, user fixes the underlying issue and re-invokes). 5 unit tests for memoizeAsync covering: single-fire dedup, value identity, distinct keys, rejection caching, concurrent in-flight dedup. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(spawn): collect-all preflight + per-plugin tests + key-namespacing docs Address self-review feedback on PR #1622: 1. **Collect-all preflight** (spawn.ts): runSpawnPreflight previously aborted at the first plugin's failure, so a user with multiple broken prereqs (tmux missing AND gh logged out) had to fix-and-retry to discover the second one. Now collects every plugin's error and reports them together ("2 preflight checks failed:\n 1. ...\n 2. ..."). Single-failure path is unchanged — that error throws as-is without the wrapper. Test added: 'collects every plugin's preflight failure into one combined error'. 2. **Drop redundant workspace literal fallback** (spawn.ts): DefaultPluginsSchema in core/config.ts applies .default("worktree") to workspace, same as runtime/agent. The literal '?? "worktree"' was asymmetric defensive theater — dropped to match the runtime/agent form. 3. **memoizeAsync key-namespacing convention** (process-cache.ts): Added a JSDoc section documenting that two callers using the same key get shared state (intentional for cross-cutting checks like gh-cli-auth, dangerous for plugin-internal caching). Recommends namespacing plugin-internal keys as 'plugin-name:thing'. 4. **Per-plugin preflight unit tests**: - runtime-tmux: tmux-present resolves; tmux-missing throws with platform-specific install hint (verified per-platform branch) - tracker-github: happy path, gh-not-installed, gh-not-authenticated - scm-github: no-op when willClaimExistingPR=false (zero gh calls), full check when true, plus install/auth failure branches Process cache cleared in beforeEach so each test starts fresh. Required exporting _clearProcessCacheForTests from core/index.ts (matches existing _testUtils pattern in gh-trace.ts). Pre-existing start.test.ts 'stop command' failures unchanged (verified on bare upstream/main). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(test): collapse duplicate @aoagents/ao-core import in tracker-github test eslint no-duplicate-imports caught it on CI — combined the value and type-only imports into one statement. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| src | ||
| CHANGELOG.md | ||
| README.md | ||
| package.json | ||
| tsconfig.json | ||
README.md
@agent-orchestrator/plugin-runtime-tmux
Runtime plugin for executing agent sessions in tmux.
What This Does
Creates isolated tmux sessions for each agent. Each session runs in a separate tmux session with:
- Working directory set to workspace path
- Environment variables from config
- Agent launch command executed automatically
How It Works
Creating a Session
const handle = await runtime.create({
sessionId: "my-app-3",
workspacePath: "/Users/dev/.worktrees/my-app/my-app-3",
launchCommand: "claude -p 'Fix bug in auth module'",
environment: {
AO_SESSION_ID: "my-app-3",
AO_PROJECT_ID: "my-app",
},
});
What happens:
- Validates
sessionId(only alphanumeric, dash, underscore allowed) - Creates detached tmux session:
tmux new-session -d -s my-app-3 -c /path/to/workspace - Sets environment variables:
tmux ... -e KEY=VALUE - Sends launch command:
tmux send-keys -t my-app-3 "claude -p '...'" Enter - Returns RuntimeHandle with tmux session name
Sending Messages
await runtime.sendMessage(handle, "Fix the test failure in auth.test.ts");
What happens:
- Clears partial input:
tmux send-keys -t my-app-3 C-u - For short messages (<200 chars, no newlines): sends directly with
-lflag (literal mode) - For long/multiline messages: writes to temp file →
tmux load-buffer→tmux paste-buffer - Waits 300ms (let tmux process the text)
- Sends Enter:
tmux send-keys -t my-app-3 Enter
Why the complexity?
send-keyswithout-linterprets special strings ("Enter", "Space") as key names- Long strings can overflow tmux's command buffer
- Multiline strings need special handling
Getting Output
const output = await runtime.getOutput(handle, 50); // last 50 lines
Uses tmux capture-pane -t my-app-3 -p -S -50 to capture terminal buffer.
Checking if Alive
const alive = await runtime.isAlive(handle);
Uses tmux has-session -t my-app-3 (exit code 0 = exists, 1 = doesn't exist).
Destroying
await runtime.destroy(handle);
Kills tmux session: tmux kill-session -t my-app-3 (ignores errors if already dead).
Attaching to Sessions
For Terminal plugins (iTerm2, web):
const attachInfo = await runtime.getAttachInfo(handle);
// Returns: { type: "tmux", target: "my-app-3", command: "tmux attach -t my-app-3" }
Security
Session ID validation:
const SAFE_SESSION_ID = /^[a-zA-Z0-9_-]+$/;
Only allows safe characters. Prevents shell injection via session name (used in tmux commands).
Error Handling
- Session creation fails → cleans up (kills session) before throwing
- Message send fails → throws (caller should handle)
- Session already dead →
destroy()silently succeeds (idempotent)
Metrics
const metrics = await runtime.getMetrics(handle);
// Returns: { uptimeMs: 123456 }
Tracks uptime (stored in RuntimeHandle.data.createdAt).
Testing
This plugin is tested indirectly via packages/core/src/__tests__/tmux.test.ts (utility functions) and integration tests.
To test manually:
# Start a test session
tmux new-session -d -s test-session -c /tmp
tmux send-keys -t test-session "echo hello" Enter
# Capture output
tmux capture-pane -t test-session -p
# Kill session
tmux kill-session -t test-session
Common Issues
tmux not installed
If tmux is not in PATH, all operations fail. Install via:
- macOS:
brew install tmux - Linux:
apt-get install tmuxoryum install tmux
Session name conflicts
If a session with the same ID already exists, create() fails. The orchestrator should ensure unique session IDs.
Detached sessions persist after orchestrator crashes
tmux sessions keep running even if the orchestrator dies. Use tmux list-sessions to find orphans, tmux kill-session -t <name> to clean up.
Limitations
- macOS/Linux only — tmux is not available on Windows (use WSL)
- No Windows native support — use runtime-process instead on Windows
- Terminal buffer size —
getOutput()limited by tmux buffer size (default 2000 lines) - No resource limits — agents can consume unlimited CPU/memory (use docker/k8s runtimes for isolation)
Architecture Notes
Why tmux over raw processes?
- Sessions persist across orchestrator restarts
- Easy to attach for debugging:
tmux attach -t session-name - Terminal emulation (colors, ANSI codes work)
- Works well with interactive AI tools (Claude Code, Aider)
Why detached mode?
- Orchestrator doesn't block waiting for agent
- Multiple agents can run in parallel
- Humans can attach later without interrupting agent