* 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>