Skip to content
Closed
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
114 changes: 78 additions & 36 deletions plugins/orchestrator/skills/install-launchers/SKILL.md
Original file line number Diff line number Diff line change
@@ -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@<marketplace>`
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@<marketplace>` for
`--dangerously-load-development-channels`.

## When to use

Expand Down Expand Up @@ -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
Expand All @@ -103,28 +124,40 @@ 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 <PROJECT_ROOT>. Usage:

Windows / cmd.exe / PowerShell:
.\pa-start.bat Start a new PA (gold tab)
.\pa-start.bat -Resume <uuid-or-name> Resume an existing session as PA
.\sa-start.bat Start a new SA (default tab)
.\sa-start.bat -Name "SA-frontend" Start SA with an explicit name
.\sa-start.bat -Resume <uuid-or-name> 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 <uuid-or-name> 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 <uuid-or-name> Resume an existing session as SA
```

## Quick reference
Expand All @@ -134,7 +167,7 @@ Installed orchestrator launchers into <PROJECT_ROOT>. Usage:
| 1 | Confirm `$PWD` is project root | Terminal |
| 2 | Locate scripts dir | `<base-dir>/scripts/` |
| 3 | Extract marketplace slug | `<cache-path>` 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 |

Expand Down Expand Up @@ -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
Expand All @@ -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.
204 changes: 204 additions & 0 deletions plugins/orchestrator/skills/install-launchers/scripts/pa-start.sh
Original file line number Diff line number Diff line change
@@ -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 <uuid-or-display-name>
# 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 <uuid-or-name>] [--project-dir <path>]" >&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: <name>".
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[@]}"
Loading