跳转至

launcher-select

A session picks a NAMED launcher profile at create time — a { harness, cmd } pair from config — and that choice is PERSISTED on its record, so resume/relaunch reuses the same command (and the same auth) instead of re-resolving the global default.

How a worker is brought up has TWO free variables: WHICH harness (harness-adapter — claude vs codex) and WHICH command actually launches it (a login reclaude, an API-key claude-glm, a bespoke wrapper). A launcher fuses those two into ONE named profile, so the human picks a single thing per session and the harness rides along for free. A launcher is a { harness?, cmd } pair in spexcode.json's sessions.launchers map, keyed by a name the human chooses (claude-glm, reclaude, …); harness defaults to claude. Because a launcher NAMES a harness, picking a launcher SUBSUMES the harness axis — the old free-standing harness pick is the special case of "no launchers configured".

sessions.defaultLauncher names the profile a session with no explicit choice uses; sessions.claudeCmd / sessions.codexCmd remain the UNNAMED backward-compatible default, resolved (with the SPEXCODE_CLAUDE_CMD / SPEXCODE_CODEX_CMD env override) only when a session carries no named launcher. Host-specific absolute commands belong in the gitignored spexcode.local.json, never in the committed file — a launcher name is portable, its cmd is a machine fact.

Selection at create time. spex new "…" --launcher <name> picks it on the CLI (threaded through createSession/newSession and the POST /api/sessions body); the dashboard New-Session form shows a launcher dropdown sourced from GET /api/launchers, which REPLACES the harness picker. That endpoint reports BOTH the { name, harness } list AND the configured default name ({ launchers, default }) — because the dropdown must AGREE with the CLI on which launcher a no-choice create uses. So the dropdown's INITIAL selection honors defaultLauncher: a still-valid remembered (per-browser) pick wins, else the configured default, else the first — never the bare alphabetically-first, which would silently disagree with the config default (the confusion this closes: a human "testing claude-glm" who is quietly handed another launcher). When no launchers are configured the list is empty (and default blank) and the form falls back to the plain harness picker, so a zero-config project is unchanged. A resolved launcher fixes the session's harness; an unknown launcher name is rejected fail-loud (a 400 from the create path), never silently defaulted.

Visible per session. A launcher choice that vanishes the moment the worker boots is unanswerable — "did this session actually launch under claude-glm?" A session's persisted launcher NAME therefore reaches the board: it rides on the session payload (/api/sessions + /api/board) alongside its harness, and every session-listing row BADGES it — the launcher's harness vendor glyph (the SAME mark the New-Session picker draws, shared from harness.jsx) plus the launcher name — so the launch identity is legible at a glance without opening the terminal. A session with no named launcher (an old record, or a zero-config default launch) shows no badge; only a deliberately-named launch earns one.

Correctness — the choice is persisted, not re-resolved. The launch command used to be re-resolved globally at every launch (env → config → default), so a session created under an API-key launcher would silently become a login session on resume the moment the backend's env or default differed. The fix: the chosen launcher NAME is stored on the session record (sessions-core's launcher field) and consulted at EVERY launch — first launch, drain, and reopen/relaunch alike — resolving that named profile's cmd (bypassing the env default) so the same auth path is reused for the life of the session. A record with no launcher (an old session, or a zero-config default launch) falls back to the current global resolution, so nothing pre-dating this node changes behavior. The per-session command reaches the agent through the harness-adapter's launchCmd, which now accepts the resolved command as an override rather than always reading the global default — the ONE seam where a launcher's identity overrides the ambient default.