feat(orchestrator): Python-canonical launchers (replaces PR#3 bash port)#5
Open
evannadeau wants to merge 18 commits into
Open
Conversation
Design document for the pa_start / sa_start / discord_start Python-canonical launcher refactor, replacing the existing PowerShell-canonical trio with thin .sh / .ps1 / .bat wrappers. Implements upstream decision to retire PR#3 (the closed bash port) in favor of a single canonical Python implementation. Python is already a baseline dep of this plugin via sidecar/embed_server.py, so the rewrite adds no new runtime dependency. Captures: - File layout (13 files: 1 shared module, 3 entry points, 9 wrappers) - Shared-module interface (_launcher_common.py) - Per-entry-point data flow + distinctness table - --dry-run mode (debug + smoke-test surface) - Wrapper templates (POSIX + Windows with MS Store stub detection) - Error-handling table with exit codes - Test layout (pytest under tests/launchers/, ~8 test files) - install-launchers/SKILL.md changes required - Migration notes for existing local bash-port users - Out-of-scope items (macOS osascript, Python-driven installer, sidecar discovery convergence) No code changes in this commit — spec only. Implementation follows in a separate commit on the same branch once the plan is written.
… rewrite Task-by-task TDD plan for executing the design committed in 2f1da2d. 17 tasks across the shared module, three entry points, six platform wrappers, install-skill update, and final integration check. Each task has 5-7 steps following red-green-refactor-commit: 1. Write the failing test (concrete code). 2. Run the test, expected: fail. 3. Implement (concrete code). 4. Run the test, expected: pass. 5. Commit. All file paths absolute, all commands runnable, no placeholders. Plan self-reviewed against the design; type signatures cross-checked across referencing tasks.
Adds pyproject.toml (Python 3.10+ floor, pytest dev-only), a minimal conftest.py with project-dir / sessions-file / env-snapshot / fake- projects-dir fixtures, and a 'test:py' package.json script that runs pytest via uvx (matching the sidecar's existing uvx fallback pattern, so contributors don't need a separate pip install). No production code yet — empty harness baseline for the launcher rewrite (see docs/plans/2026-05-13-launcher-python-canonical-design.md).
… placeholder Adds the shared launcher module skeleton with two foundational pieces: 1. sys.version_info check at import time. Floor is Python 3.10 (justified by PEP 604 type-union syntax used elsewhere in the module and broad ecosystem availability). Wrappers also catch missing-Python at the shell level; this catches the case where the wrapper finds a too-old interpreter. 2. MARKETPLACE_PLACEHOLDER constant + check_marketplace_substituted() guard. The /orchestrator:install-launchers skill substitutes the placeholder at copy-into-project time. If the guard runs against an unsubstituted module, it exits with an actionable error pointing back at the install skill. Split-string comparison in the guard avoids self-detection by the substitution tool.
Adds project_hash_for(project_dir: PurePath) -> str, mirroring the literal character-substitution transform that Claude Code applies to project paths when naming ~/.claude/projects/<hash>/ directories: - backslash, forward slash, colon → single dash - consecutive dashes NOT collapsed (C:\ yields C--) - leading and trailing dashes stripped Test coverage: POSIX simple + deep, Windows with drive letter (verifies the C-- prefix is preserved), leading-dash strip, no trailing dash. Five cases, all pass.
Adds resolve_project_dir(arg: str | None) -> Path. Handles: - None / empty arg → CWD - Relative path → resolved against CWD - Absolute path → passes through Always returns absolute. Exits 1 if the resolved path doesn't exist (matches the .ps1 launchers' Resolve-Path + Test-Path behavior). Test coverage: default-to-CWD, explicit absolute arg, relative arg resolved against CWD, missing-dir exits 1.
Adds resolve_resume_target(resume: str, project_dir: Path) -> str.
Mirrors the .ps1 launchers' display-name → UUID lookup:
- canonical UUID shape passes through unchanged
- otherwise, grep ~/.claude/projects/<hash>/*.jsonl for the literal
'Session renamed to: <name>' marker
- newest match wins (by mtime)
- missing projects-dir or no-match exits 1
Tests cover: UUID passthrough, single match, newest-wins on duplicates,
no-match exit, missing-projects-dir exit. Five cases.
Adds the PA singleton-supersede helper. Mirrors the .ps1 launchers'
pre-launch demote block:
- no sessions.json → no-op
- corrupt JSON → WARNING (treated as no-PA, self-heals)
- any role=prime entries with last_heartbeat_at > now-90s → demoted
to role=subordinate, file rewritten
- stale primes (heartbeat older than 90s) left alone
- 2-second pause after demote (matches .ps1 'press Ctrl+C to cancel')
Tightens one .ps1 behavior: write errors are FATAL (exit 1) instead
of warn-only. Silent write failure would leave two role=prime entries,
breaking the singleton invariant.
Tests cover all 6 paths: no-state-file, no-primes, stale-prime,
fresh-prime, parse-failure (warning), write-failure (fatal).
Adds make_session_name(prefix: str) -> str. Returns '<PREFIX>-YYYY-MM-DD-HH-MM-SS' using local time (matches the .ps1 launchers' Get-Date format). Tests verify regex shape for PA, SA, and DISCORD-LIVE prefixes.
Adds setup_env(*, role, session_kind, project_dir, session_name). Sets the same env vars the .ps1 launchers set on the spawned claude.exe: MCP_TIMEOUT, ORCHESTRATOR_PROJECT_ROOT, role + kind + name (each with the SPAWNBOX_ alias for back-compat), and the PA permission-relay flag. When session_name is None (--resume without --name), the NAME envs are left unset so the resumed session's existing /rename name is preserved. Tests cover PA, SA, Discord-bot, and no-session-name paths.
Adds build_claude_args(*, marketplace, session_name, resume, effort,
extra_channels) -> list[str]. Constructs the argv passed to the spawned
'claude' process. Mirrors the .ps1 launchers' claudeArgs assembly:
- extra_channels prepended (Discord-only)
- --dangerously-load-development-channels plugin:orchestrator@<mkt>
- --effort <level> if set (PA: hardcoded 'max'; SA: optional CLI;
Discord: never)
- --name <session_name> if set (omitted on --resume without --name)
- --resume <uuid> if set
Tests cover: PA minimal shape, SA --resume, Discord with both channel
flags in correct order, marketplace-slug verbatim interpolation.
…tion)
Adds launch(claude_args, *, project_dir, tab_color, no_wt) -> int.
The only place in _launcher_common.py that branches on platform.
- shutil.which('claude') gate: missing → exit 127 with install hint
- POSIX: os.execvp('claude', ...) — replaces Python process
- Windows + wt.exe + not no_wt: wt.exe -w new new-tab [--tabColor X]
-d <dir> claude <argv>
- Windows otherwise: subprocess.run([claude, ...])
Tests use shutil.which / platform.system / os.execvp / subprocess.run
monkeypatching to assert on the resolved spawn command without
actually exec'ing claude or wt.exe.
This completes the _launcher_common.py shared module. Full suite:
38/38 passing.
PA launcher entry point. Composes _launcher_common helpers:
- check_marketplace_substituted() (fail-fast guard)
- resolve_project_dir → resolve_resume_target (if --resume)
- supersede_existing_pa (singleton enforcement; skipped on --dry-run)
- make_session_name('PA') unless resuming
- setup_env(role='prime', session_kind='prime', ...)
- build_claude_args(effort='max', ...)
- launch(tab_color='#F59E0B', ...) OR --dry-run JSON output
--dry-run mode performs all state mutations EXCEPT the sessions.json
write and launch() call, then prints a JSON envelope describing the
resolved argv + env_overrides + tab_color + use_wt. Used by the smoke
tests and as a user-facing debug tool.
Two smoke tests: dry-run output shape + PA-prefixed session name.
SA launcher entry point. Same composition as pa_start.py with the
SA-specific deltas:
- role='subordinate', session_kind='subordinate'
- no singleton-supersede (subordinates are not singleton)
- --effort is an optional CLI flag (validated against
low/medium/high/xhigh/max); omitted from claude argv when not set
- --name is an optional CLI flag (overrides auto-generated SA- name)
- tab_color=None (no special tab color for SAs)
Tests cover: no --effort, --effort max, --name override.
Discord-ops launcher entry point. SA-shaped composition with the
Discord-specific deltas:
- role='subordinate', session_kind='discord-bot' (gates classifier
policies + discord-bootstrap skill identity check)
- session name always auto-generated DISCORD-LIVE-<timestamp>
(no --name flag, no --resume flag)
- extra --channels plugin:discord@claude-plugins-official IN
ADDITION to the orchestrator dev-channels flag
- tab_color='#DC2626' (red)
This completes the three Python entry points. Full suite: 45/45.
Three thin Bash wrappers (~17 LOC each) that:
- locate a Python interpreter (\$ORCH_PYTHON > python3)
- emit an actionable 'Python not found' error with install hints if
none available
- exec the corresponding pa_start / sa_start / discord_start .py
with passthrough args
Mode 755 — git tracks the executable bit.
Smoke-tested locally: each wrapper invokes its .py and emits a valid
--dry-run JSON envelope when the marketplace slug is substituted.
PA emits gold tab + --effort max + role=prime; SA emits no tab color
and respects --effort; Discord emits red tab + dual --channels +
session_kind=discord-bot.
Replaces the three canonical PowerShell launcher scripts (208 + 175 +
113 = 496 LOC of business logic) with thin wrappers (~30-33 LOC each
identical-pattern) that:
- try \$env:ORCH_PYTHON, then python.exe, then py.exe
- detect the Microsoft Store App-Execution-Alias stub by output
match ('Python was not found') per anti-pattern adf2b104
- check \$LASTEXITCODE after every native-exe call (PowerShell
try/catch does NOT catch native-exe failure)
- emit actionable install hints (winget / python.org / MS Store
Python real / \$ORCH_PYTHON) if no interpreter resolves
- exec the corresponding pa_start / sa_start / discord_start .py
with PowerShell splat (@Args) for full argument passthrough
.bat trampolines unchanged — they dispatch to the .ps1 by filename,
which is unchanged in name (only the .ps1 body differs).
Also adds __pycache__/ and .pytest_cache/ to plugins/orchestrator/
.gitignore so Python test runs don't dirty the worktree.
…chers
Updates the install skill to reflect the 13-file layout (1 shared
Python module + 3 entry points + 3 .sh + 3 .ps1 + 3 .bat). Substantive
edits:
- Overview: 'ships canonical PowerShell launchers' → 'ships canonical
Python launchers (with thin platform wrappers)'.
- File count: 'SIX files' → 'THIRTEEN files' with breakdown.
- Launcher inventory table: gains a 'Files installed' column
enumerating the 4 files per launcher kind.
- New 'Prerequisites' section calling out Python 3.10+ as a hard
requirement (with install hints per platform) and noting that
Python is already a baseline plugin dep via the sidecar.
- Step 4 (copy): enumerates all 13 filenames + chmod 755 on the
three .sh wrappers (no-op on Windows).
- Step 5 (substitute): single sed/PowerShell replace against
_launcher_common.py instead of the previous loop over .ps1 files.
New marketplace guard verification (python3 -c ... import
_launcher_common; check_marketplace_substituted()).
- Step 6 (verify): updated grep targets to _launcher_common.py.
- Step 7 (usage): split into POSIX + Windows sections, documents
--dry-run and $ORCH_PYTHON overrides.
- Quick reference table updated for 7 steps + correct file counts.
- Common mistakes: added Python interpreter mismatch (MS Store stub)
and chmod-the-.sh entries.
- Notes: --dry-run debug tip added.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Per upstream operator decision (sequenced after PR#4 — backup-plugin-db
skill — though that PR remains unmerged at time of opening), this PR
retires the cross-language drift between the canonical PowerShell
launchers and the closed-PR#3 bash port by consolidating both
implementations into a single Python-canonical surface.
Python is already a baseline dependency of the orchestrator plugin via
sidecar/embed_server.pyandsidecar/requirements.txt, so this PRadds no new runtime dependency.
What changes
Removed (replaced in place):
.ps1launchers.
Added:
_launcher_common.py— shared module (project-dir resolution,CC project-hash transform, sessions.json singleton-supersede,
env-var assembly, claude argv builder, terminal-spawn abstraction).
pa_start.py/sa_start.py/discord_start.py— three entrypoints (one each per launcher kind).
pa-start.sh/sa-start.sh/discord-start.sh— thin POSIXwrappers that locate `python3` (honoring `$ORCH_PYTHON`) and exec
the entry .py.
pa-start.ps1/sa-start.ps1/discord-start.ps1— thinWindows PS wrappers that locate `python.exe` / `py.exe` with
Microsoft Store stub detection (per the published preflight rule).
tests/launchers/— pytest suite for the shared module(project-hash, resume-resolve, supersede, session-name, setup-env,
build-args, launch) + smoke tests for each entry point via the new
`--dry-run` mode.
pyproject.toml— Python 3.10+ floor, pytest as dev-onlydependency.
package.json:`"test:py": "uvx --from pytest pytest tests/launchers/"`.
Unchanged:
.batdouble-click trampolines retain their shape (theydispatch to the same-name
.ps1, which is unchanged in name only).Behavior
User-observable behavior is preserved: same CLI flags, same env vars
set on the spawned `claude` process, same sessions.json mutations,
same wt.exe tab colors. One tightening: `sessions.json` write failure
during the supersede block is now FATAL (the .ps1 treated it as
warn-only, which could silently leave two `role=prime` entries and
break the singleton invariant).
Discord-start was omitted from the closed bash port (PR#3) — this PR
ships it.
Test plan
Locally (CI surface):
```bash
cd plugins/orchestrator
bun install
bun run typecheck # unchanged — no .ts touched
bun test # unchanged — no .ts touched
uvx --from pytest pytest tests/launchers/ # new — 45 tests, all green
```
Operator-driven E2E on real systems (per the bash-port verification
protocol from the closed PR#3):
distribution, distinct from the App Execution Alias stub).
"install Python" error message is actionable).
Stack independence
This PR is independent of:
files in `mcp/server.ts`.
Cleanly mergeable in any order.
Design + Plan
Both committed alongside the code: