Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 37 additions & 28 deletions docs/arch/arch.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
# 05 — Cross-platform, agent-agnostic MCP & skills management
# 05 — Agent-agnostic MCP & skills management

Tracking: [#73](https://github.com/yicheng47/runner/issues/73)

> Rewritten 2026-07-15. The original spec at this number ("Skills + MCPs management per runner") attached skills/MCPs to runner templates and injected them through a per-spawn synthetic agent home built from symlink overlays. That direction is superseded: the per-runner-only surface buried the catalog inside edit forms, and the symlink-overlay home is a Unix-only mechanism. The data-model and materialization details below carry forward what still holds; see git history for the full original text.

## Motivation

Every coding agent ships its own way to configure MCP servers and skills — claude-code reads `~/.claude.json` + `~/.claude/skills/`, codex reads `~/.codex/config.toml`, and each agent's settings UI is specific to that agent and that machine. There's no single place to manage these, and nothing that works the same across agents or across platforms.
Every coding agent ships its own way to configure MCP servers and skills — claude-code reads `~/.claude.json` + `~/.claude/skills/`, codex reads `~/.codex/config.toml`, and each agent's settings UI is specific to that agent and that machine. There's no single place to manage these, and nothing that works the same across agents.

Runner already coordinates multiple agents from one app, so it's the natural home for one central, agent-agnostic place to define and manage MCP servers and skills: define a server or a skill once, and let Runner apply it to whichever agent a runner is backed by.

## Direction

- **Central catalog.** A dedicated management surface (settings-style, like Codex's "MCP servers" screen) to create / edit / delete reusable MCP servers and skills. One catalog, not buried inside per-runner edit forms.
- **Agent-agnostic.** Definitions are stored in Runner's own neutral shape and materialized into whatever the target agent expects (claude-code JSON, codex TOML, skill directories). The user defines an MCP/skill once; Runner handles the per-agent translation.
- **Cross-platform.** Both the management surface and the apply mechanism must work on macOS, Linux, and Windows — no Unix-only assumptions.
- **macOS-only, so Unix mechanisms are fine.** Symlinks always work; the apply mechanism needs no portability ladder. (This lifts the constraint that partly motivated the 2026-07-15 rewrite below — the old symlink-overlay agent home was rejected as "Unix-only", which is no longer disqualifying. It stays rejected on the other grounds: the per-runner-only surface buried the catalog.)

## Reference analysis: skills-manager

Expand All @@ -33,7 +33,7 @@ Runner already coordinates multiple agents from one app, so it's the natural hom
### What Runner borrows

1. **The adapter-registry shape.** A neutral per-agent record describing where skills/config live, how to detect the agent, and per-agent overrides — extended in Runner to also describe the MCP config file format (JSON at `~/.claude.json` vs TOML at `~/.codex/config.toml`) and merge strategy.
2. **Copy-capable apply with the Windows ladder.** Symlink where possible, junction on NTFS, copy as the universal fallback — plus their src/dst overlap guards. This directly resolves the cross-platform constraint that killed the old spec's symlink-overlay agent home.
2. **Copy-capable apply, plus their overlap guards.** Symlink by default with copy as a per-agent option, and their guards refusing syncs where source and destination overlap in either direction. Their Windows symlink→junction→copy ladder is moot here — on macOS symlinks always work — but copy stays worth offering for agents that follow symlinks badly.
3. **Scan-don't-trust.** The management surface should show what the agent actually sees (scan the real dirs/config), surface externally-added entries, and offer adoption — not maintain a parallel belief that drifts.
4. **Source metadata + content hash** on catalog entries, so "imported from git / local / hand-written" is recorded and update checks are possible later without redesign.
5. **`SKILL.md` + YAML frontmatter** as the on-disk skill format — it's the ecosystem convention; claude-code loads it natively.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ same workspace UI. The only new concept is the trigger.

- **Remote/headless execution.** The scheduler runs in-process in
Tauri; the app must be open for ticks to fire. A headless daemon
mode (launchd agent on macOS, systemd on Linux) is a follow-up.
mode (a launchd agent) is a follow-up.
- **Cron expression editor UI.** v1 ships a text input with presets
and a human-readable preview. A visual day/hour picker grid is a
follow-up.
Expand All @@ -141,9 +141,10 @@ same workspace UI. The only new concept is the trigger.
1. **In-process Tokio scheduler, not OS-level cron/launchd.** The
app must be running to spawn PTYs (they're child processes of the
Tauri backend). An OS-level trigger that launches the app on
schedule is attractive but adds platform-specific complexity
(launchd plist on macOS, Task Scheduler on Windows, systemd on
Linux) that doesn't justify itself in v1. The in-process
schedule is attractive but adds a whole install/uninstall
lifecycle (a launchd plist, its permissions, and the
app-not-running semantics) that doesn't justify itself in v1.
The in-process
scheduler is ~100 lines of Rust and covers the "app is open all
day" use case that cronjobs target.
2. **Skip on overlap, don't queue.** If a mission takes 2 hours and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Runtime-only direct chats have no configurable command, and the create/edit runn

### In scope

- **Agent runtimes settings pane.** Add an Agent runtimes pane under Settings → Integrations. Show one row/card for each first-class runtime returned by the backend registry, initially Claude Code and Codex.
- **Agents settings pane.** Add an Agents pane to the Integrations group in Settings, next to MCP. Show one row for each first-class runtime returned by the backend registry, initially Claude Code and Codex.
- **Detected executable.** Resolve each runtime's catalog command against Runner's composed user `$PATH` and show the resulting absolute executable path, or a clear Not found / Detection failed state.
- **Executable override.** Let the user enter or pick an absolute executable path per runtime. An empty override means automatic discovery. Validate that a non-empty path exists, is a regular file, and is executable before saving.
- **Resolution precedence.** Use the explicit runtime override first, then the automatically detected executable, then the catalog command only when it can be resolved through the effective child `$PATH`. Do not silently report a configured runtime as available when none of those paths resolves.
Expand All @@ -24,10 +24,11 @@ Runtime-only direct chats have no configurable command, and the create/edit runn
- **Slow shell initialization.** Replace the current all-or-nothing two-second startup probe with a non-blocking or otherwise startup-safe discovery flow that accommodates realistic zsh/Oh My Zsh initialization. Preserve the last known good result on timeout and expose the timeout as a diagnosable state instead of silently dropping to launchd's stripped environment.
- **Diagnostics.** Log the selected shell, discovery duration, success/failure reason, and resolved runtime executable paths without logging unrelated environment values. Surface enough status in Settings for a user to distinguish Not installed from Shell probe timed out.
- **Backend persistence.** Store overrides in backend-owned app settings so all windows and all Rust spawn paths share the same value; do not make localStorage the source of truth for executable selection.
- **Design first.** Add the settings pane and its detected/override/error/refresh states to `design/runner-mvp-design.pen` before implementation.
- **Design first.** The pane and its states are designed in `design/runner-setting.pen`: frame `Settings — Agents` (node `Zes2l`) for the pane layout, and `Spec — Agent runtime row states` (node `cXdkp`) for the six row states (detected, override, not-found, checking, probe-timed-out, invalid-override).

### Out of scope

- User-defined custom runtimes (the registry-as-data extension in #279). Deliberately cut for now: built-ins only, so the pane stays simple. The backend registry shape should not preclude adding custom rows later.
- Installing, upgrading, or authenticating Claude Code or Codex.
- Accepting aliases or shell functions as runtime executables; Runner spawns a real process and requires an executable file.
- General-purpose editing of the child process `$PATH`.
Expand All @@ -39,7 +40,7 @@ Runtime-only direct chats have no configurable command, and the create/edit runn

### Phase 1 — UX design and settings contract

- Design Settings → Integrations → Agent runtimes in `design/runner-mvp-design.pen`, including detected, overridden, not-found, probing, timeout, validation-error, and refresh states.
- ~~Design the Agents pane~~ — done in `design/runner-setting.pen` (`Settings — Agents` + `Spec — Agent runtime row states`), covering detected, overridden, not-found, checking, probe-timed-out, invalid-override, and refresh states.
- Define a backend runtime-settings shape keyed by stable runtime name with an optional executable override.
- Define the effective-command precedence and legacy `runner.command` compatibility rules in tests before changing spawn behavior.

Expand Down Expand Up @@ -74,21 +75,23 @@ Runtime-only direct chats have no configurable command, and the create/edit runn

## Verification

- [ ] Settings → Integrations → Agent runtimes shows Claude Code and Codex from the backend registry.
Unchecked items below require manual app verification.

- [ ] Settings → Agents (Integrations group) shows Claude Code and Codex from the backend registry.
- [ ] A standard executable on the captured login-shell `$PATH` is displayed as an absolute detected path.
- [ ] A slow zsh/Oh My Zsh startup does not silently discard a previously valid `$PATH` after two seconds.
- [ ] Bash, zsh, and other explicitly supported shells use documented, tested startup semantics.
- [x] Bash, zsh, and other explicitly supported shells use documented, tested startup semantics.
- [ ] Missing, invalid, or unsupported login shells produce a visible detection failure rather than a misleading Not installed state.
- [ ] A user can refresh discovery after installing Codex without restarting Runner.
- [ ] A valid absolute override is persisted and used by runtime-only direct chats.
- [ ] The override is used by runner and mission spawns whose stored command is the runtime's catalog default.
- [ ] A runner with a custom non-default command continues using that command.
- [ ] Clearing an override returns the runtime to automatic discovery.
- [x] A valid absolute override is persisted and used by runtime-only direct chats.
- [x] The override is used by runner and mission spawns whose stored command is the runtime's catalog default.
- [x] A runner with a custom non-default command continues using that command.
- [x] Clearing an override returns the runtime to automatic discovery.
- [ ] Nonexistent, non-file, and non-executable overrides are rejected with inline errors.
- [ ] Aliases and shell functions are not accepted as executable paths.
- [ ] A missing runtime fails before PTY spawn with actionable copy pointing to Agent runtimes settings.
- [x] Aliases and shell functions are not accepted as executable paths.
- [x] A missing runtime fails before PTY spawn with actionable copy pointing to Agents settings.
- [ ] Discovery logs include shell, duration, outcome, and resolved executable without dumping the user's full environment.
- [ ] Settings and effective command behavior remain consistent across multiple app windows.
- [ ] `pnpm exec tsc --noEmit` passes.
- [ ] `pnpm run lint` passes.
- [ ] Relevant Rust tests pass, followed by `cargo test --workspace` when implementation is complete.
- [x] `pnpm exec tsc --noEmit` passes.
- [x] `pnpm run lint` passes.
- [x] Relevant Rust tests pass, followed by `cargo test --workspace` when implementation is complete.
55 changes: 55 additions & 0 deletions docs/features/45-auto-resume-on-launch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# 45 — Auto-resume running chats and missions on launch

> Tracking issue: [#320](https://github.com/yicheng47/runner/issues/320)

## Motivation

Quitting the app kills every running agent (PTYs die with the process; `stop_running_sessions_on_quit` stops direct chats gracefully, startup demotes stale `running` rows). On next launch the user manually resumes each chat and mission they were working in — pure friction, since the app already knows how to resume everything: sessions persist `agent_session_key`, `session_resume` respawns into the prior conversation (impl 0024 keeps claude-code scrollback), and missions keep `status = running` across restarts (`mount_all_running_mission_routers` re-mounts their buses at startup — only their sessions are dead).

The gap is memory plus initiative: nothing records *which* sessions were live at quit, and nothing acts on it at launch.

## Scope

### In scope

- **Mark at quit.** `stop_running_sessions_on_quit` already enumerates running direct sessions; stamp them (`resume_on_launch` flag on the session row) before killing. The same pass marks running-slot sessions of running missions (they're demoted by startup cleanup today with no trace). A crash skips the stamp — see key decision 2.
- **Auto-resume at launch.** After the webview is ready, resume every marked session that is still resumable (`agent_session_key` present, not archived), clearing the flag as each is consumed. Missions need no extra start step: their status is still `running` and buses re-mount as today — resuming their marked slot sessions brings the workspace back to life.
- **Staggered spawns.** Resume sequentially with a short gap, not as one burst — N simultaneous PTY spawns + login-shell env snapshots is a stampede for no benefit.
- **Failure tolerance.** The existing resume-failure heuristic (fast death → `crashed` + warning toast, next launch starts fresh) already covers rejected `--resume` keys; auto-resume inherits it. A failed auto-resume must not block the rest of the queue.
- **Opt-in setting.** One toggle in Settings ("Resume running agents on launch", default off). Spawning agents unprompted at launch must be an explicit choice; there is no per-chat granularity in v1.

### Out of scope

- Restoring UI state beyond what already persists (sidebar tree, tab layouts, window geometry are all covered; the restored sessions simply light up their existing rows).
- Auto-resuming sessions the user stopped *manually* before quitting — stopped means stopped; only quit-time-running sessions are marked.
- Re-injecting prompts or auto-continuing agent work. Resume reopens the conversation; the agent stays idle until spoken to.
- Cross-device / sync anything.

### Key decisions

1. **Explicit flag, not timestamp inference.** Inferring "was running at quit" from `stopped_at` proximity to shutdown confuses deliberately-stopped chats with quit-killed ones. The quit hook knows exactly which rows it's killing; it should say so.
2. **Crash = no auto-resume.** The stamp lives in the graceful-quit path only. After a crash, sessions demote via startup cleanup as today and stay stopped — auto-respawning agents after a crash risks looping into whatever caused it. (If crash-restore is ever wanted, it's a separate, deliberate decision.)
3. **Resume, never fresh-spawn.** A marked session that lost its `agent_session_key` (or whose resume fails) stays stopped with the existing Resume affordance — auto-starting a *fresh* conversation the user didn't ask for is worse than doing nothing.
4. **Quit stamping is unconditional.** The backend does not read the frontend setting. Every graceful quit records the live set; the launch consumer decides whether to resume it.
5. **Toggle-off consumes without resuming.** A launch with auto-resume disabled clears every pending `resume_on_launch` stamp so turning the setting on later cannot resurrect work from an older quit.

### Resolved product decisions

- Stagger resume spawns by 300ms.
- Resume silently in v1; do not add a "Resuming N agents…" indicator.
- Put "Resume running agents on launch" in Settings → General under a new Startup section. It defaults off and persists in `localStorage` through the `src/lib/settings.ts` `STORAGE_*` pattern. The default flip also applies to existing users who never stored an explicit choice; there is no migration key.

## Implementation phases

1. **Schema + quit stamp** — `resume_on_launch` column (sessions), stamped in `stop_running_sessions_on_quit` for direct chats and running-mission slot sessions.
2. **Launch consumer** — post-ready sequential resume of marked resumable sessions via the existing `session_resume` path; flag cleared per session; Settings toggle gating the whole pass.
3. **Polish** — stagger tuning, resume indicator if decided in.

## Verification

- [ ] Turn the toggle on, quit with two running chats and a running mission → relaunch → all three come back live without interaction; scrollback intact for claude-code.
- [ ] A chat stopped manually before quit stays stopped after relaunch.
- [ ] Kill the app process (simulated crash) → nothing auto-resumes.
- [ ] A session with a rejected resume key surfaces the existing crash warning and doesn't block other resumes.
- [ ] Toggle absent or off → relaunch restores nothing and clears pending stamps; rows keep their normal Resume buttons; turning the toggle back on does not resume the old quit's set.
- [ ] `cargo fmt --check`, `cargo clippy --workspace`, `cargo test --workspace`, `pnpm exec tsc --noEmit`, `pnpm run lint` clean.
Loading