diff --git a/plugins/orchestrator/skills/install-launchers/SKILL.md b/plugins/orchestrator/skills/install-launchers/SKILL.md index 2b898a4..47eb241 100644 --- a/plugins/orchestrator/skills/install-launchers/SKILL.md +++ b/plugins/orchestrator/skills/install-launchers/SKILL.md @@ -1,31 +1,46 @@ --- name: install-launchers -description: Use when setting up the orchestrator plugin in a new project (or refreshing after a plugin update). Installs the canonical pa-start / sa-start / discord-start launchers into the current project root so the user can spawn PA/SA/Discord-ops Claude Code sessions from their terminal. +description: Use when setting up the orchestrator plugin in a new project (or refreshing after a plugin update). Installs the canonical pa-start / sa-start / discord-start launchers (PowerShell + bash variants) into the current project root so the user can spawn PA/SA/Discord-ops Claude Code sessions from their terminal on Windows, WSL, Linux, or macOS. --- # Install orchestrator launchers into this project ## Overview -The orchestrator plugin ships canonical PowerShell launchers for spawning -Claude Code sessions wired into the agent-channel. These launchers must -live in the user's project root (not the plugin's `bin/`) because the -user invokes them from their OS terminal to spawn NEW Claude sessions - -and Claude Code's plugin `bin/` PATH only applies inside an -already-running Claude session. - -Three launcher kinds ship today: - -| Launcher | Role | Channels attached | Tab color | -|---|---|---|---| -| `pa-start` | PrimeAgent (prime) | orchestrator | gold (#F59E0B) | -| `sa-start` | Subordinate (subordinate) | orchestrator | default | -| `discord-start` | Discord-ops (subordinate) | orchestrator + Discord | red (#DC2626) | - -This skill copies SIX files into the user's CWD (three `.ps1` + -three `.bat` shims) and substitutes the marketplace slug into the -copies so they reference the right `plugin:orchestrator@` -for `--dangerously-load-development-channels`. +The orchestrator plugin ships canonical launchers for spawning Claude Code +sessions wired into the agent-channel. These launchers must live in the +user's project root (not the plugin's `bin/`) because the user invokes +them from their OS terminal to spawn NEW Claude sessions - and Claude +Code's plugin `bin/` PATH only applies inside an already-running Claude +session. + +Three launcher kinds ship today, each in three variants: + +| Launcher | Role | Channels attached | Tab color | Variants | +|---|---|---|---|---| +| `pa-start` | PrimeAgent (prime) | orchestrator | gold (#F59E0B) on `wt.exe` | `.ps1`, `.bat`, `.sh` | +| `sa-start` | Subordinate (subordinate) | orchestrator | default | `.ps1`, `.bat`, `.sh` | +| `discord-start` | Discord-ops (subordinate) | orchestrator + Discord | red (#DC2626) on `wt.exe` | `.ps1`, `.bat` | + +**Per-platform variant guide:** + +- `.ps1` — canonical PowerShell implementation. Real logic lives here. +- `.bat` — 4-line cmd.exe shim that dispatches to the `.ps1`. Lets users + double-click or invoke from `cmd.exe`. +- `.sh` — bash port of the `.ps1`. For users who run Claude Code from a + POSIX shell (WSL, Linux, macOS) and don't want PowerShell interop. + +Currently `discord-start` has no `.sh` variant because its dual-channel +attach (Discord + orchestrator) is tightly coupled to wt.exe-style tab +coloring and the Windows-side Discord plugin install path. WSL/Linux +users who need Discord-ops can run `discord-start.bat` via interop or +add a `.sh` port (PRs welcome). + +This skill copies EIGHT files into the user's CWD (three `.ps1` + three +`.bat` shims + two `.sh` bash launchers) and substitutes the marketplace +slug into the copies so they reference the right +`plugin:orchestrator@` for +`--dangerously-load-development-channels`. ## When to use @@ -82,17 +97,23 @@ echo "Marketplace: $MARKETPLACE" If `MARKETPLACE` is empty, ask the user for the correct marketplace slug (visible via `/plugin marketplace list`) before proceeding. -### 4. Copy + substitute the six files +### 4. Copy + substitute the eight files -The source `.ps1` files contain the literal token `__ORCH_MARKETPLACE__` -where the orchestrator marketplace slug needs to be. Copy each file and -replace the token in-place: +The source `.ps1` and `.sh` files contain the literal token +`__ORCH_MARKETPLACE__` where the orchestrator marketplace slug needs to +be. Copy each file and replace the token in-place. The `.sh` files also +need their executable bit preserved (`install -m 0755 ...`): ```bash for f in pa-start.ps1 pa-start.bat sa-start.ps1 sa-start.bat discord-start.ps1 discord-start.bat; do sed "s|__ORCH_MARKETPLACE__|$MARKETPLACE|g" "$SCRIPTS_DIR/$f" > "$PWD/$f" echo "Installed $f" done +for f in pa-start.sh sa-start.sh; do + sed "s|__ORCH_MARKETPLACE__|$MARKETPLACE|g" "$SCRIPTS_DIR/$f" > "$PWD/$f" + chmod 0755 "$PWD/$f" + echo "Installed $f" +done ``` If any target file already exists at `$PWD/$f`, **ask the user before @@ -103,21 +124,25 @@ been hand-tuned for the user's existing Discord workflow. ### 5. Verify the install ```bash -ls -la "$PWD"/{pa,sa,discord}-start.{ps1,bat} -grep -l "__ORCH_MARKETPLACE__" "$PWD"/{pa,sa,discord}-start.{ps1,bat} || echo "Substitution complete." -grep -h "plugin:orchestrator@" "$PWD"/pa-start.ps1 +ls -la "$PWD"/{pa,sa,discord}-start.{ps1,bat} "$PWD"/{pa,sa}-start.sh +grep -l "__ORCH_MARKETPLACE__" "$PWD"/{pa,sa,discord}-start.{ps1,bat} "$PWD"/{pa,sa}-start.sh 2>/dev/null \ + || echo "Substitution complete." +grep -h "plugin:orchestrator@" "$PWD"/pa-start.ps1 "$PWD"/pa-start.sh +test -x "$PWD/pa-start.sh" && test -x "$PWD/sa-start.sh" && echo "bash launchers executable." ``` The first grep should produce no output (the literal token is gone). -The second should print the substituted plugin reference matching the -marketplace slug. +The second should print the substituted plugin reference twice (matching +the marketplace slug). The `test -x` confirms `chmod` worked. ### 6. Output usage instructions -Print to terminal: +Print to terminal (adapt to user's platform - omit irrelevant rows): ``` Installed orchestrator launchers into . Usage: + +Windows / cmd.exe / PowerShell: .\pa-start.bat Start a new PA (gold tab) .\pa-start.bat -Resume Resume an existing session as PA .\sa-start.bat Start a new SA (default tab) @@ -125,6 +150,14 @@ Installed orchestrator launchers into . Usage: .\sa-start.bat -Resume Resume an existing session as SA .\discord-start.bat Start a Discord-ops session (red tab, both Discord + orchestrator channels) + +WSL / Linux / macOS bash: + ./pa-start.sh Start a new PA + ./pa-start.sh --resume Resume an existing session as PA + ./sa-start.sh Start a new SA + ./sa-start.sh --name SA-frontend Start SA with an explicit name + ./sa-start.sh --effort max Start SA at max reasoning effort + ./sa-start.sh --resume Resume an existing session as SA ``` ## Quick reference @@ -134,7 +167,7 @@ Installed orchestrator launchers into . Usage: | 1 | Confirm `$PWD` is project root | Terminal | | 2 | Locate scripts dir | `/scripts/` | | 3 | Extract marketplace slug | `` after `cache/` | -| 4 | Copy + substitute `__ORCH_MARKETPLACE__` (6 files) | `$PWD/*.{ps1,bat}` | +| 4 | Copy + substitute `__ORCH_MARKETPLACE__` (8 files; chmod +x the `.sh`) | `$PWD/*.{ps1,bat,sh}` | | 5 | Verify no unsubstituted tokens remain | grep check | | 6 | Print usage | Terminal | @@ -162,10 +195,14 @@ own `--channels` arg). ## Common mistakes -- **Forgetting the substitution step**: copying the raw `.ps1` files - with the literal `__ORCH_MARKETPLACE__` placeholder will produce +- **Forgetting the substitution step**: copying the raw `.ps1` or `.sh` + files with the literal `__ORCH_MARKETPLACE__` placeholder will produce launchers that fail with "plugin not found in marketplace '__ORCH_MARKETPLACE__'". Always run the `sed` step. +- **Forgetting `chmod +x` on the `.sh` files**: the source `.sh` files + have their executable bit set in the plugin repo, but `sed > $PWD/$f` + creates the destination as a regular file (mode 0644 by default). + Always `chmod 0755` after copying, or use `install -m 0755`. - **Picking the wrong cache version**: if the user has multiple installed versions, `sort | tail -1` may not be what they want. If uncertain, look at `/plugin` for the active version and use that @@ -182,9 +219,14 @@ own `--channels` arg). right way to pick up launcher improvements. The installed copies are static; they don't auto-update with the plugin. - The launchers themselves are project-agnostic: they use `$PWD` (or - an explicit `-ProjectDir` parameter) as the project root, set - `ORCHESTRATOR_PROJECT_ROOT` env for the spawned MCP, and work in - any project where the orchestrator plugin is installed. + an explicit `-ProjectDir` / `--project-dir` parameter) as the project + root, set `ORCHESTRATOR_PROJECT_ROOT` env for the spawned MCP, and + work in any project where the orchestrator plugin is installed. +- **Runtime deps for `.sh` launchers:** `pa-start.sh` needs `jq` and + GNU coreutils for the singleton check; `sa-start.sh` has no deps + beyond bash 4+. Standard on WSL/Ubuntu (`apt install jq` if missing). + macOS: `brew install jq coreutils` and the launcher must run under + bash 4+ (not the default 3.2). - The `__ORCH_MARKETPLACE__` placeholder makes the source scripts portable across marketplace slugs; only the COPIED versions in each project root are slug-specific. diff --git a/plugins/orchestrator/skills/install-launchers/scripts/pa-start.sh b/plugins/orchestrator/skills/install-launchers/scripts/pa-start.sh new file mode 100755 index 0000000..fbdcf1d --- /dev/null +++ b/plugins/orchestrator/skills/install-launchers/scripts/pa-start.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash +# pa-start.sh — bash port of pa-start.ps1 +# +# Launch the PrimeAgent (PA) Claude Code session for the current project. +# PA runs Opus at max effort with agent-channel attached. Singleton per project. +# +# Project-agnostic. Single source-of-truth lives in the orchestrator plugin +# at `plugins/orchestrator/skills/install-launchers/scripts/pa-start.sh`. +# Install per-project via `/orchestrator:install-launchers` from inside a +# Claude session. +# +# Usage: +# ./pa-start.sh +# Fresh PA session in the current directory, auto-named PA-YYYY-MM-DD-HH-MM-SS +# ./pa-start.sh --resume +# Resume an existing session as PA. Display names resolved via JSONL grep. +# ./pa-start.sh --project-dir /path/to/project +# Run against a different project root than $PWD +# +# Requirements: bash 4+, jq, GNU coreutils (date -d, mktemp). Standard on +# WSL/Ubuntu. macOS users need `brew install coreutils jq` and may need to +# use `gdate`-aware shells. + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Arg parsing +# --------------------------------------------------------------------------- + +resume="" +project_dir="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --resume) + resume="${2:?--resume requires a value}" + shift 2 + ;; + --project-dir) + project_dir="${2:?--project-dir requires a value}" + shift 2 + ;; + -h|--help) + sed -n '2,/^set -euo/p' "$0" | sed 's/^# \{0,1\}//; $d' + exit 0 + ;; + *) + echo "ERROR: Unknown argument: $1" >&2 + echo "Usage: $0 [--resume ] [--project-dir ]" >&2 + exit 2 + ;; + esac +done + +if [[ -z "$project_dir" ]]; then + project_dir="$PWD" +fi +project_dir="$(cd "$project_dir" && pwd)" + +# --------------------------------------------------------------------------- +# Resolve display name -> UUID if needed +# --------------------------------------------------------------------------- + +uuid_re='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' + +if [[ -n "$resume" && ! "$resume" =~ $uuid_re ]]; then + # Match Claude Code's project-dir -> hash transform. POSIX paths only contain + # `/` separators (no `\` or `:`), so the transform reduces to s|/|-|g plus a + # leading-dash trim. CC does NOT collapse consecutive dashes. + project_hash="${project_dir//\//-}" + project_hash="${project_hash#"${project_hash%%[!-]*}"}" # strip leading dashes + + jsonl_dir="$HOME/.claude/projects/$project_hash" + if [[ ! -d "$jsonl_dir" ]]; then + echo "ERROR: Projects dir not found: $jsonl_dir" >&2 + exit 1 + fi + + # Find newest JSONL containing "Session renamed to: ". + resolved_uuid="" + while IFS= read -r f; do + if grep -qF "Session renamed to: $resume" "$f"; then + resolved_uuid="$(basename "$f" .jsonl)" + break + fi + done < <(ls -t "$jsonl_dir"/*.jsonl 2>/dev/null) + + if [[ -z "$resolved_uuid" ]]; then + echo "ERROR: No session in $jsonl_dir has been renamed to: $resume" >&2 + exit 1 + fi + echo " Resolved display name to session: $resolved_uuid" + resume="$resolved_uuid" +fi + +# --------------------------------------------------------------------------- +# Singleton awareness - auto-supersede existing PA if any. +# +# Pre-emptively demote any role=prime entry to subordinate in sessions.json. +# In the normal "user closed old window and is relaunching" flow, the old +# MCP is already dead and the demotion sticks. If the old MCP is still +# alive, its heartbeat will overwrite back to role=prime briefly until the +# user runs /pa-takeover or closes the older window. +# --------------------------------------------------------------------------- + +state_file="$project_dir/.orchestrator-state/agent-channel/sessions.json" +if [[ -f "$state_file" ]]; then + now_epoch="$(date -u +%s)" + cutoff=$((now_epoch - 90)) + + # `fromdateiso8601` rejects fractional seconds — strip `.NNN` before `Z`. + fresh_pa_json="$(jq --argjson cutoff "$cutoff" ' + [ .sessions[]? + | select(.role == "prime") + | select(((.last_heartbeat_at | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) // 0) > $cutoff) + ] + ' "$state_file" 2>/dev/null || echo "[]")" + + fresh_pa_count="$(echo "$fresh_pa_json" | jq 'length')" + + if [[ "$fresh_pa_count" -gt 0 ]]; then + echo + echo " Existing PrimeAgent detected - auto-superseding:" + echo "$fresh_pa_json" | jq -r '.[] | " * \(.session_id) (\(.name // "unnamed"))"' + + tmp="$(mktemp)" + jq '(.sessions[]? | select(.role == "prime") | .role) = "subordinate"' \ + "$state_file" > "$tmp" + mv "$tmp" "$state_file" + + echo " (Existing PA(s) demoted. New PA will register as prime.)" + echo " (Press Ctrl+C in the next ~2s to cancel.)" + echo + sleep 2 + fi +fi + +# --------------------------------------------------------------------------- +# Naming policy +# --------------------------------------------------------------------------- + +session_name="" +if [[ -z "$resume" ]]; then + session_name="PA-$(date '+%Y-%m-%d-%H-%M-%S')" +fi + +# --------------------------------------------------------------------------- +# Env vars (inherited by child `claude` -> MCP server) +# --------------------------------------------------------------------------- + +# Bump MCP startup timeout from the 5s default to 30s. The orchestrator +# MCP server's `npx -y bun` cold-start can exceed 5s on first invocation. +export MCP_TIMEOUT=30000 + +# Tell the MCP which project root we're operating in. +export ORCHESTRATOR_PROJECT_ROOT="$project_dir" + +# Canonical role env. SPAWNBOX_ prefix kept for backwards compatibility. +export ORCHESTRATOR_AGENT_ROLE=prime +export SPAWNBOX_AGENT_ROLE=prime + +# Opt into the PA-gated permission relay (0.30.17+). When set, SA permission +# requests route through agent-channel to PA for authorization instead of +# falling back to in-terminal prompts. PA needs the `respond_to_permission` +# tool registered, which is gated on this env var. +export ORCHESTRATOR_PA_PERMISSION_RELAY=1 + +# Only set the NAME env when we have an explicit name. On --resume without an +# explicit name, leave NAME unset so the resumed session's existing +# /rename-set name is preserved. +if [[ -n "$session_name" ]]; then + export ORCHESTRATOR_AGENT_NAME="$session_name" + export SPAWNBOX_AGENT_NAME="$session_name" +fi + +# --------------------------------------------------------------------------- +# Build claude args +# --------------------------------------------------------------------------- + +# The marketplace slug below is substituted by the +# /orchestrator:install-launchers skill at copy-into-project time. If you see +# the literal `__ORCH_MARKETPLACE__` below, re-run /orchestrator:install-launchers. +# +# 0.30.28+: PA always launches at max effort. PA is the singleton +# orchestration session - judgment calls, cross-cutting coordination, holding +# the macro view. Token cost is the right tradeoff for the role. +claude_args=( + --dangerously-load-development-channels + "plugin:orchestrator@__ORCH_MARKETPLACE__" + --effort max +) +if [[ -n "$session_name" ]]; then + claude_args+=(--name "$session_name") +fi +if [[ -n "$resume" ]]; then + claude_args+=(--resume "$resume") +fi + +# --------------------------------------------------------------------------- +# Launch +# --------------------------------------------------------------------------- + +cd "$project_dir" +exec claude "${claude_args[@]}" diff --git a/plugins/orchestrator/skills/install-launchers/scripts/sa-start.sh b/plugins/orchestrator/skills/install-launchers/scripts/sa-start.sh new file mode 100755 index 0000000..922c1c1 --- /dev/null +++ b/plugins/orchestrator/skills/install-launchers/scripts/sa-start.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +# sa-start.sh — bash port of sa-start.ps1 +# +# Launch a Subordinate Agent (SA) Claude Code session participating in +# the orchestrator's agent-channel. +# +# Project-agnostic. Single source-of-truth lives in the orchestrator plugin +# at `plugins/orchestrator/skills/install-launchers/scripts/sa-start.sh`. +# Install per-project via `/orchestrator:install-launchers`. +# +# Usage: +# ./sa-start.sh +# Fresh session in current dir, auto-named SA-YYYY-MM-DD-HH-MM-SS +# ./sa-start.sh --name SA-frontend +# Fresh session with explicit name +# ./sa-start.sh --resume +# Resume an existing session +# ./sa-start.sh --name SA-architecture --effort max +# Fresh session at max effort for heavy reasoning +# +# Requirements: bash 4+. (jq + GNU coreutils only needed by pa-start.sh's +# singleton check; SA has no such logic, so SA's runtime deps are minimal.) + +set -euo pipefail + +# --------------------------------------------------------------------------- +# Arg parsing +# --------------------------------------------------------------------------- + +resume="" +name="" +project_dir="" +effort="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --resume) + resume="${2:?--resume requires a value}" + shift 2 + ;; + --name) + name="${2:?--name requires a value}" + shift 2 + ;; + --project-dir) + project_dir="${2:?--project-dir requires a value}" + shift 2 + ;; + --effort) + effort="${2:?--effort requires a value}" + case "$effort" in + low|medium|high|xhigh|max) ;; + *) + echo "ERROR: --effort must be one of: low, medium, high, xhigh, max" >&2 + exit 2 + ;; + esac + shift 2 + ;; + -h|--help) + sed -n '2,/^set -euo/p' "$0" | sed 's/^# \{0,1\}//; $d' + exit 0 + ;; + *) + echo "ERROR: Unknown argument: $1" >&2 + echo "Usage: $0 [--name ] [--resume ] [--effort ] [--project-dir ]" >&2 + exit 2 + ;; + esac +done + +if [[ -z "$project_dir" ]]; then + project_dir="$PWD" +fi +project_dir="$(cd "$project_dir" && pwd)" + +# --------------------------------------------------------------------------- +# Resolve display name -> UUID if needed +# --------------------------------------------------------------------------- + +uuid_re='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' + +if [[ -n "$resume" && ! "$resume" =~ $uuid_re ]]; then + project_hash="${project_dir//\//-}" + project_hash="${project_hash#"${project_hash%%[!-]*}"}" + + jsonl_dir="$HOME/.claude/projects/$project_hash" + if [[ ! -d "$jsonl_dir" ]]; then + echo "ERROR: Projects dir not found: $jsonl_dir" >&2 + exit 1 + fi + + resolved_uuid="" + while IFS= read -r f; do + if grep -qF "Session renamed to: $resume" "$f"; then + resolved_uuid="$(basename "$f" .jsonl)" + break + fi + done < <(ls -t "$jsonl_dir"/*.jsonl 2>/dev/null) + + if [[ -z "$resolved_uuid" ]]; then + echo "ERROR: No session in $jsonl_dir has been renamed to: $resume" >&2 + exit 1 + fi + echo " Resolved display name to session: $resolved_uuid" + resume="$resolved_uuid" +fi + +# --------------------------------------------------------------------------- +# Naming policy +# --resume given -> let claude use the resumed session's name +# --name given -> use that name +# neither -> auto-generate SA-YYYY-MM-DD-HH-MM-SS +# --------------------------------------------------------------------------- + +session_name="" +if [[ -n "$name" ]]; then + session_name="$name" +elif [[ -z "$resume" ]]; then + session_name="SA-$(date '+%Y-%m-%d-%H-%M-%S')" +fi + +# --------------------------------------------------------------------------- +# Env vars +# --------------------------------------------------------------------------- + +export MCP_TIMEOUT=30000 +export ORCHESTRATOR_PROJECT_ROOT="$project_dir" + +# Canonical role env. SPAWNBOX_ prefix kept for backwards compatibility. +export ORCHESTRATOR_AGENT_ROLE=subordinate +export SPAWNBOX_AGENT_ROLE=subordinate + +# Opt into the PA-gated permission relay (0.30.17+). When set, this SA's MCP +# declares the `claude/channel/permission` capability so tool permission +# requests route through agent-channel to PA for authorization instead of +# falling back to in-terminal prompts. +export ORCHESTRATOR_PA_PERMISSION_RELAY=1 + +if [[ -n "$session_name" ]]; then + export ORCHESTRATOR_AGENT_NAME="$session_name" + export SPAWNBOX_AGENT_NAME="$session_name" +fi + +# --------------------------------------------------------------------------- +# Build claude args +# --------------------------------------------------------------------------- + +# Marketplace slug substituted by /orchestrator:install-launchers at copy time. +# If you see the literal `__ORCH_MARKETPLACE__` below, re-run the install skill. +claude_args=( + --dangerously-load-development-channels + "plugin:orchestrator@__ORCH_MARKETPLACE__" +) +if [[ -n "$session_name" ]]; then + claude_args+=(--name "$session_name") +fi +# 0.30.28+: optional reasoning-effort override. Only emitted when --effort is +# explicitly set; otherwise Claude Code uses its session default. +if [[ -n "$effort" ]]; then + claude_args+=(--effort "$effort") +fi +if [[ -n "$resume" ]]; then + claude_args+=(--resume "$resume") +fi + +# --------------------------------------------------------------------------- +# Launch +# --------------------------------------------------------------------------- + +cd "$project_dir" +exec claude "${claude_args[@]}"