diff --git a/plugins/orchestrator/skills/install-statusline/SKILL.md b/plugins/orchestrator/skills/install-statusline/SKILL.md new file mode 100644 index 0000000..52680dc --- /dev/null +++ b/plugins/orchestrator/skills/install-statusline/SKILL.md @@ -0,0 +1,326 @@ +--- +name: install-statusline +description: Use when the user wants a role-aware Claude Code statusline that shows whether the current session is PA, SA, or Discord-ops (matching the launcher's wt.exe tab-color scheme on Windows). Installs the orchestrator statusline script into the project root and configures Claude Code's `statusLine` setting to point at it. Idempotent — refuses to clobber an existing custom statusLine without explicit override. +--- + +# Install orchestrator role-aware statusline + +## Overview + +The orchestrator launchers (`pa-start` / `sa-start` / `discord-start`) +set role-specific env vars on the spawned Claude Code session: + +| Launcher | `ORCHESTRATOR_SESSION_KIND` | `ORCHESTRATOR_AGENT_NAME` | +|---|---|---| +| `pa-start` | `prime` | `PA-` | +| `sa-start` | `subordinate` | `SA-` | +| `discord-start` | `discord-bot` | `DISCORD-LIVE-` | + +This skill installs a small Python script + thin wrapper that reads +those env vars and emits a one-line, ANSI-colored statusline like: + +``` +🟡 PA PA-2026-05-13-12-00-00 quayline +⚪ SA SA-frontend quayline +🔴 DISCORD DISCORD-LIVE-... quayline +``` + +This is the in-Claude complement to the Windows-only `wt.exe --tabColor` +flag used by the launchers: on POSIX you get the colored statusline +without needing a terminal-emulator tab-color API; on Windows you get +both (tab color from `wt.exe`, statusline indicator from this skill). + +This skill is **separate from `install-launchers`**: installing the +launchers does NOT auto-install the statusline (the user may have a +custom statusline they don't want to clobber). + +## Prerequisites + +- The orchestrator plugin is installed. +- Python 3.10+ is on PATH (same prerequisite as the launchers — see + `install-launchers` SKILL.md). +- The user has run `/orchestrator:install-launchers` so the env-var + contract is in place. Without that, the statusline degrades gracefully + to a generic `orchestrator` label, but won't show role info. + +## When to use + +- After installing the launchers, when the user asks for a visual + role indicator inside the Claude UI. +- When the user surfaces frustration about "which session am I in?" + during multi-session orchestration work. +- After a `/plugin update orchestrator` that bumps the statusline + source (re-run picks up improvements). + +## Steps + +### 1. Confirm target directory + +```bash +echo "Will install into: $PWD" +``` + +Same anchor convention as `install-launchers`: the user's project root. + +### 2. Locate the source scripts directory + +```bash +SCRIPTS_DIR=$(find ~/.claude/plugins/cache -path "*/orchestrator/*/skills/install-statusline/scripts" -type d 2>/dev/null | sort | tail -1) +echo "$SCRIPTS_DIR" +``` + +If the base directory was surfaced via the skill-load header, use that +path's `scripts/` subdirectory directly. + +### 3. Detect existing `statusLine` setting (global AND project) + +Claude Code resolves `statusLine` from a precedence chain — the +project-level `.claude/settings.json` shadows the global +`~/.claude/settings.json`. If we write an unconditional statusLine into +the project settings, we silently clobber a global statusline the user +relies on (the global one stops rendering in this project). Check BOTH +levels: + +```bash +PROJECT_SETTINGS="$PWD/.claude/settings.json" +GLOBAL_SETTINGS="$HOME/.claude/settings.json" + +EXISTING_LEVEL="" # "project" | "global" | "" +EXISTING_VALUE="" + +if [ -f "$PROJECT_SETTINGS" ]; then + v=$(jq -r '.statusLine // empty | tojson' "$PROJECT_SETTINGS" 2>/dev/null) + if [ -n "$v" ] && [ "$v" != "null" ]; then + EXISTING_LEVEL="project" + EXISTING_VALUE="$v" + fi +fi + +if [ -z "$EXISTING_LEVEL" ] && [ -f "$GLOBAL_SETTINGS" ]; then + v=$(jq -r '.statusLine // empty | tojson' "$GLOBAL_SETTINGS" 2>/dev/null) + if [ -n "$v" ] && [ "$v" != "null" ]; then + EXISTING_LEVEL="global" + EXISTING_VALUE="$v" + fi +fi +``` + +If `EXISTING_LEVEL` is non-empty, surface the existing config and the +four options: + +``` +⚠ Existing statusLine detected at level: + + + (1) AUTO-COMPOSE (recommended) — install a composed-wrapper script + that runs your existing statusline AND the orchestrator role + indicator together. Your existing statusline keeps rendering; + the orchestrator role indicator appears as the first line. + Non-destructive of your existing setup. + + (2) SKIP — keep your existing statusLine, do not install the + orchestrator statusline. + + (3) REPLACE — set the project-level statusLine to the orchestrator + one only. If your existing config was at the GLOBAL level, this + shadows it for this project only — your global statusline stays + intact and renders in other projects. If your existing was at + the PROJECT level, it's lost; back it up first. + + (4) COMPOSE MANUALLY — install the orchestrator scripts but do not + touch settings.json. You merge the fragment into your existing + statusline by hand. Use this when AUTO-COMPOSE's defaults + (running ~/.claude/statusline.sh as the user-side script) don't + match your setup. + +Which option? +``` + +Wait for the user's answer before proceeding. Default to (1) +AUTO-COMPOSE if unclear — it's non-destructive. + +**Stdin fan-out caveat (informs option 1):** Claude Code pipes session +JSON to the statusLine command via stdin on every refresh. A naive +"run cmd1; run cmd2" composition would have both commands fight over +stdin (cmd1 consumes it, cmd2 sees EOF). The shipped +`orchestrator-statusline-composed.sh` template handles this correctly: +it captures stdin once, runs the orchestrator script (which ignores +stdin), then re-pipes the captured JSON to the user's existing +statusline. Do NOT hand-roll a composition that skips the stdin +capture — the user's statusline will silently render blank. + +If `EXISTING_LEVEL` is empty: proceed to step 4 without prompting. + +### 4. Copy script files into the project root + +The script set installed depends on the option chosen in step 3: + +- For options (1) AUTO-COMPOSE, (3) REPLACE, (4) COMPOSE MANUALLY: + install all four files. +- For option (2) SKIP: install nothing, exit. + +```bash +INSTALL_DIR="$PWD" +for f in orchestrator_statusline.py \ + orchestrator-statusline.sh \ + orchestrator-statusline.ps1 \ + orchestrator-statusline-composed.sh; do + cp "$SCRIPTS_DIR/$f" "$INSTALL_DIR/$f" +done +chmod 755 "$INSTALL_DIR/orchestrator-statusline.sh" \ + "$INSTALL_DIR/orchestrator-statusline-composed.sh" +``` + +On Windows the `chmod` step is a no-op. + +The fourth file (`orchestrator-statusline-composed.sh`) is the +composed-wrapper template. It runs the orchestrator role indicator +THEN the user's `~/.claude/statusline.sh` (re-piping captured stdin). +For setups where the user's statusline is at a different path, the +template's `USER_STATUSLINE` variable can be edited in-place — it's a +single assignment at the top of the file. + +### 5. Configure `.claude/settings.json` + +For option (4) COMPOSE MANUALLY: skip this step. The scripts are in +place; the user merges into their existing settings.json on their own. + +For options (1) AUTO-COMPOSE and (3) REPLACE: write the statusLine +command into the **project-level** `.claude/settings.json`. Project- +level always takes precedence over global, so this is the right +surface either way. + +```bash +mkdir -p "$PWD/.claude" +SETTINGS="$PWD/.claude/settings.json" + +# Build the OS-appropriate command — pointing at the composed wrapper +# for option (1), or the orchestrator-only wrapper for option (3). +case "$(uname -s 2>/dev/null || echo Windows)" in + MINGW*|MSYS*|CYGWIN*|*NT*|Windows*) + WRAPPER_BASE="orchestrator-statusline.ps1" # option (3) + COMPOSED_BASE="orchestrator-statusline-composed.sh" # option (1) — bash via WSL + if [ "$CHOICE" = "auto-compose" ]; then + CMD="bash $INSTALL_DIR/$COMPOSED_BASE" + else + CMD="powershell -NoProfile -ExecutionPolicy Bypass -File $INSTALL_DIR\\$WRAPPER_BASE" + fi + ;; + *) + if [ "$CHOICE" = "auto-compose" ]; then + CMD="$INSTALL_DIR/orchestrator-statusline-composed.sh" + else + CMD="$INSTALL_DIR/orchestrator-statusline.sh" + fi + ;; +esac + +# Merge into existing settings.json (or create new). Preserves +# everything else in the file; only replaces `.statusLine`. +if [ -f "$SETTINGS" ]; then + jq --arg cmd "$CMD" '.statusLine = {"type": "command", "command": $cmd}' "$SETTINGS" > "$SETTINGS.new" + mv "$SETTINGS.new" "$SETTINGS" +else + cat > "$SETTINGS" < +``` + +When run inside a session spawned by `pa-start` / `sa-start` / +`discord-start`, the output includes the role glyph + session name. + +### 7. Restart the Claude session + +The statusline takes effect on session restart. Print: + +``` +Statusline installed. Close this Claude session and re-launch via +./pa-start.sh / sa-start.sh / discord-start.sh to see the role +indicator. Resumed sessions also pick it up. +``` + +## Composition guide for users with custom statuslines + +If the user picked option (3) MANUAL COMPOSE in step 3, document the +fragment they can merge into their existing statusline: + +The orchestrator statusline emits a single line of ANSI-colored +text on stdout. To include it inside an existing statusline script, +add this to your existing script's output: + +```bash +ORCH_FRAGMENT=$(/abs/path/to/orchestrator-statusline.sh) +echo "$ORCH_FRAGMENT | " +``` + +The fragment is purely role/name/project — it doesn't read other state, +make network calls, or modify the environment. Safe to compose with +git-status / model-cost / battery-level / etc. statuslines. + +## Uninstall + +```bash +# Remove the orchestrator statusline configuration. +jq 'del(.statusLine)' "$PWD/.claude/settings.json" > "$PWD/.claude/settings.json.new" +mv "$PWD/.claude/settings.json.new" "$PWD/.claude/settings.json" + +# Remove the script files. +rm "$PWD/orchestrator_statusline.py" \ + "$PWD/orchestrator-statusline.sh" \ + "$PWD/orchestrator-statusline.ps1" +``` + +If the user had a previous statusLine and we replaced it in option (2), +they'll need to restore from their backup — we don't track the +displaced value. + +## Common mistakes + +- **Replacing without backup**: option (2) replace is destructive. If + the user has a custom statusline they care about, show them the + current value before replacing so they can copy it elsewhere. +- **Auto-composing arbitrary commands**: do NOT attempt to wrap an + existing statusLine command into our script. Shell command + composition is fragile — quoting, $IFS, exit codes, side effects. + Option (3) puts the composition burden on the user, which is the + right place for it. +- **Setting the statusLine command to a relative path**: Claude Code + invokes the statusline from its own CWD, which may not be the + project root. Always use absolute paths in the `command` field. +- **Forgetting `chmod 755`**: on POSIX the `.sh` wrapper must be + executable, or Claude Code will fail to invoke it (errors are + swallowed silently and the statusline just goes blank). + +## Notes + +- The statusline depends on `$ORCHESTRATOR_SESSION_KIND` being set + by the launchers. If the user launches Claude Code directly (e.g., + `claude` from the CLI with no launcher), the statusline degrades + to a neutral `orchestrator ` line. +- Re-running this skill after a `/plugin update orchestrator` is the + right way to pick up statusline-script improvements. The installed + copies are static. +- The script is stdlib-only Python — no third-party deps. Cross-platform + by virtue of `os.environ` access being identical on POSIX and Windows. +- Performance: the script runs on every Claude UI refresh. Keep it + fast (single env-var lookup + string format ≈ <1ms). Do not add + subprocess calls, file I/O, or network access. diff --git a/plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline-composed.sh b/plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline-composed.sh new file mode 100755 index 0000000..0b00ac7 --- /dev/null +++ b/plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline-composed.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Composed statusline: orchestrator role indicator + user's personal statusline. +# +# Wraps the user's existing statusline (typically at ~/.claude/statusline.sh) +# so the orchestrator role indicator appears ABOVE the user's content, without +# replacing or modifying the user's script. +# +# Output lines: +# line 1: orchestrator role indicator (env-var driven via orchestrator-statusline.sh) +# line 2+: user's personal statusline (reads JSON from stdin) +# +# Wired into Claude Code's `statusLine` setting in .claude/settings.json by +# the /orchestrator:install-statusline skill (AUTO-COMPOSE option). Removing +# this file and pointing statusLine back at the user's script reverts to the +# original personal statusline; removing the statusLine block entirely reverts +# to user-level default. Either operation is non-destructive of the user's +# ~/.claude/statusline.sh. +# +# To point at a user statusline located somewhere other than the default +# ~/.claude/statusline.sh, edit the USER_STATUSLINE assignment below. + +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +USER_STATUSLINE="${HOME}/.claude/statusline.sh" + +# Claude Code pipes session JSON to stdin on each refresh. Capture it once so we +# can fan out to both downstream renderers without consuming stdin twice (the +# orchestrator renderer ignores stdin, but the user's renderer typically reads +# it). +JSON="" +if [ ! -t 0 ]; then + JSON="$(cat)" +fi + +# Line 1: orchestrator role indicator. Pure env-var driven, ignores stdin. +"${SCRIPT_DIR}/orchestrator-statusline.sh" || true + +# Lines 2+: user's personal statusline. Re-feeds the captured JSON via stdin. +if [ -f "${USER_STATUSLINE}" ] || [ -L "${USER_STATUSLINE}" ]; then + printf '%s' "${JSON}" | bash "${USER_STATUSLINE}" || true +fi + +exit 0 diff --git a/plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline.ps1 b/plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline.ps1 new file mode 100644 index 0000000..a3a7728 --- /dev/null +++ b/plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline.ps1 @@ -0,0 +1,33 @@ +# Thin wrapper for orchestrator_statusline.py on Windows. +# Locates a Python 3.10+ interpreter and execs the canonical script. +# Wire into Claude Code's `statusLine` setting (on Windows): +# +# "statusLine": { +# "type": "command", +# "command": "powershell -NoProfile -ExecutionPolicy Bypass -File C:\\abs\\path\\to\\orchestrator-statusline.ps1" +# } + +$ErrorActionPreference = 'Continue' + +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$candidates = @($env:ORCH_PYTHON, 'python.exe', 'py.exe') | Where-Object { $_ } +$python = $null + +foreach ($c in $candidates) { + $cmd = Get-Command $c -ErrorAction SilentlyContinue + if (-not $cmd) { continue } + $vout = & $cmd.Source --version 2>&1 + if ($LASTEXITCODE -eq 0 -and $vout -notmatch 'Python was not found') { + $python = $cmd.Source + break + } +} + +if (-not $python) { + # Don't break the Claude UI; emit a fallback line. + Write-Output "orchestrator (python missing)" + exit 0 +} + +& $python "$here\orchestrator_statusline.py" +exit 0 diff --git a/plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline.sh b/plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline.sh new file mode 100755 index 0000000..4e03d40 --- /dev/null +++ b/plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Thin wrapper for orchestrator_statusline.py. +# Locates a Python 3.10+ interpreter (honoring $ORCH_PYTHON override) +# and execs the canonical Python statusline renderer. +# +# Designed to be wired into Claude Code's `statusLine` setting: +# +# "statusLine": { +# "type": "command", +# "command": "/abs/path/to/orchestrator-statusline.sh" +# } + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PYTHON="${ORCH_PYTHON:-python3}" + +if ! command -v "$PYTHON" >/dev/null 2>&1; then + # Don't break the Claude UI if Python is missing; emit a fallback line. + echo "orchestrator (python missing)" + exit 0 +fi + +exec "$PYTHON" "$SCRIPT_DIR/orchestrator_statusline.py" diff --git a/plugins/orchestrator/skills/install-statusline/scripts/orchestrator_statusline.py b/plugins/orchestrator/skills/install-statusline/scripts/orchestrator_statusline.py new file mode 100644 index 0000000..b102056 --- /dev/null +++ b/plugins/orchestrator/skills/install-statusline/scripts/orchestrator_statusline.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Orchestrator role-aware statusline. + +Reads $ORCHESTRATOR_SESSION_KIND (set by pa-start / sa-start / +discord-start launchers) and emits a one-line statusline to stdout with +a colored role indicator + project basename. Suitable for Claude Code's +`statusLine` setting. + +Output format (single line, ANSI-colored): + 🟡 PA quayline (yellow, kind=prime) + ⚪ SA quayline (default, kind=subordinate) + 🔴 DISCORD quayline (red, kind=discord-bot) + +Session names (e.g. PA-2026-05-13-12-00-00) are intentionally not +included — they add timestamp noise without informational value. The +role indicator + project basename is enough for at-a-glance distinction. + +If the env var is unset, emits a neutral line that just identifies the +project root, so the statusline is never empty / never errors out +the Claude UI. + +Stdlib only. No third-party deps. Stdin is ignored (this renderer is +purely env-var driven); a composed wrapper script handles stdin fan-out +when this is chained with a user-provided statusline. +""" + +from __future__ import annotations + +import os +import sys + + +# ANSI color codes (256-color palette where helpful). +RESET = "\033[0m" +BOLD = "\033[1m" +DIM = "\033[2m" + +# Kind → (glyph, ANSI color sequence, human-readable label). +ROLE_STYLES: dict[str, tuple[str, str, str]] = { + "prime": ("🟡", "\033[38;5;220m", "PA"), # gold-ish yellow + "subordinate": ("⚪", "\033[38;5;245m", "SA"), # neutral grey + "discord-bot": ("🔴", "\033[38;5;160m", "DISCORD"), # red +} + + +def render_statusline() -> str: + kind = os.environ.get("ORCHESTRATOR_SESSION_KIND", "").strip() + project = os.environ.get( + "ORCHESTRATOR_PROJECT_ROOT", + os.environ.get("CLAUDE_PROJECT_DIR", ""), + ).strip() + + project_label = "" + if project: + project_label = f"{DIM}{os.path.basename(project.rstrip('/'))}{RESET}" + + if kind not in ROLE_STYLES: + return f"{DIM}orchestrator{RESET} {project_label}".rstrip() + + glyph, color, label = ROLE_STYLES[kind] + role_display = f"{color}{BOLD}{glyph} {label}{RESET}" + + line = role_display + if project_label: + line = f"{line} {project_label}" + return line + + +def main() -> int: + try: + line = render_statusline() + except Exception as err: # noqa: BLE001 + # Never crash the Claude UI; fall back to a minimal hint. + print(f"orchestrator-statusline error: {err}", file=sys.stderr) + print("orchestrator") + return 0 + print(line) + return 0 + + +if __name__ == "__main__": + sys.exit(main())