From fe4336cbfb0e2b03186f106dbd859728024b4413 Mon Sep 17 00:00:00 2001 From: Evan Nadeau <1878498+evannadeau@users.noreply.github.com> Date: Wed, 13 May 2026 16:37:46 -0700 Subject: [PATCH 1/2] feat(orchestrator): add install-statusline skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New skill: installs a role-aware Claude Code statusline that reads $ORCHESTRATOR_SESSION_KIND + $ORCHESTRATOR_AGENT_NAME (set by the orchestrator launchers) and emits a one-line, ANSI-colored role indicator inside the Claude UI. Cross-platform complement to the Windows-only wt.exe tab color. Output (per role): 🟡 PA PA- prime (yellow/gold) ⚪ SA SA- subordinate (neutral grey) 🔴 DISCORD DISCORD-LIVE-... discord-bot (red) orchestrator no launcher env (graceful fallback) Files: - scripts/orchestrator_statusline.py — canonical script, stdlib only - scripts/orchestrator-statusline.sh — POSIX wrapper - scripts/orchestrator-statusline.ps1 — Windows PowerShell wrapper - SKILL.md — install instructions Non-clobber pattern for existing user statuslines: Claude Code's statusLine setting is a single command — there's no compose API. The install skill detects an existing statusLine and prompts the user with three options: 1. SKIP — keep your existing one 2. REPLACE — clobber (back up first) 3. COMPOSE MANUALLY — install the script + the user merges the fragment into their existing statusline by hand Default is SKIP when ambiguous. The skill never auto-composes arbitrary shell commands (quoting / IFS / side-effects = fragile). Depends on the env contract from the Python-canonical launcher PR (pa-start / sa-start / discord-start set ORCHESTRATOR_SESSION_KIND). If launched without those, the statusline degrades to a neutral 'orchestrator ' line rather than erroring. Stdlib only (Python). Performance: <1ms per render — single env lookup + string format. No subprocess / file IO / network. --- .../skills/install-statusline/SKILL.md | 258 ++++++++++++++++++ .../scripts/orchestrator-statusline.ps1 | 33 +++ .../scripts/orchestrator-statusline.sh | 24 ++ .../scripts/orchestrator_statusline.py | 84 ++++++ 4 files changed, 399 insertions(+) create mode 100644 plugins/orchestrator/skills/install-statusline/SKILL.md create mode 100644 plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline.ps1 create mode 100755 plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline.sh create mode 100644 plugins/orchestrator/skills/install-statusline/scripts/orchestrator_statusline.py diff --git a/plugins/orchestrator/skills/install-statusline/SKILL.md b/plugins/orchestrator/skills/install-statusline/SKILL.md new file mode 100644 index 0000000..37b201c --- /dev/null +++ b/plugins/orchestrator/skills/install-statusline/SKILL.md @@ -0,0 +1,258 @@ +--- +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 + +Claude Code's `statusLine` setting in `.claude/settings.json` is a +single object — there's no built-in way to compose two statuslines. If +the user already has one configured, we must not silently clobber it. + +```bash +SETTINGS="$PWD/.claude/settings.json" +EXISTING="" +if [ -f "$SETTINGS" ]; then + EXISTING=$(jq -r '.statusLine // empty | tostring' "$SETTINGS" 2>/dev/null || echo "") +fi +``` + +If `EXISTING` is non-empty (and not the literal `"null"` from `jq`): + +``` +⚠ Existing statusLine detected: + + +The orchestrator statusline cannot auto-compose with arbitrary +existing statuslines (would require parsing + wrapping a shell +command we don't own). Three options: + + (1) SKIP install — keep your existing statusLine. + + (2) REPLACE — clobber the existing statusLine with the + orchestrator one. Your current setting will be lost; back it + up first if you want to restore it later. + + (3) COMPOSE MANUALLY — install the orchestrator script and the + role-indicator fragment it produces, then merge it into your + existing statusline by hand. The fragment is one line of + text on stdout; you can wrap it in your existing script with + `$(/abs/path/to/orchestrator-statusline.sh)` or similar. + +Which option? +``` + +Wait for the user's answer before proceeding. Default to (1) skip if +unclear. + +If `EXISTING` is empty / absent: proceed to step 4 without prompting. + +### 4. Copy script files into the project root + +```bash +INSTALL_DIR="$PWD" +for f in orchestrator_statusline.py \ + orchestrator-statusline.sh \ + orchestrator-statusline.ps1; do + cp "$SCRIPTS_DIR/$f" "$INSTALL_DIR/$f" +done +chmod 755 "$INSTALL_DIR/orchestrator-statusline.sh" +``` + +On Windows the `chmod` step is a no-op. + +### 5. Configure `.claude/settings.json` + +Create or update `.claude/settings.json` to wire the statusline: + +```bash +mkdir -p "$PWD/.claude" +SETTINGS="$PWD/.claude/settings.json" + +# Build the OS-appropriate command. +case "$(uname -s 2>/dev/null || echo Windows)" in + MINGW*|MSYS*|CYGWIN*|*NT*|Windows*) + CMD="powershell -NoProfile -ExecutionPolicy Bypass -File $INSTALL_DIR\\orchestrator-statusline.ps1" + ;; + *) + CMD="$INSTALL_DIR/orchestrator-statusline.sh" + ;; +esac + +# Merge into existing settings.json (or create new). +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.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..be9cc3d --- /dev/null +++ b/plugins/orchestrator/skills/install-statusline/scripts/orchestrator_statusline.py @@ -0,0 +1,84 @@ +#!/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 + session name. Suitable for Claude Code's +`statusLine` setting. + +Output format (single line, ANSI-colored): + 🟡 PA-2026-05-13-12-00-00 (yellow, kind=prime) + ⚪ SA-frontend (default, kind=subordinate) + 🔴 DISCORD-LIVE-... (red, kind=discord-bot) + +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. +""" + +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() + name = os.environ.get("ORCHESTRATOR_AGENT_NAME", "").strip() + project = os.environ.get( + "ORCHESTRATOR_PROJECT_ROOT", + os.environ.get("CLAUDE_PROJECT_DIR", ""), + ).strip() + + project_label = "" + if project: + # Show just the project's basename to keep the line short. + project_label = f"{DIM}{os.path.basename(project.rstrip('/'))}{RESET}" + + if kind not in ROLE_STYLES: + # No launcher env present — emit a neutral indicator so the + # statusline still renders something useful. + return f"{DIM}orchestrator{RESET} {project_label}".rstrip() + + glyph, color, label = ROLE_STYLES[kind] + role_display = f"{color}{BOLD}{glyph} {label}{RESET}" + + name_display = "" + if name: + name_display = f" {color}{name}{RESET}" + + line = f"{role_display}{name_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()) From eea873e7a1e01fd7840e1e7cc15e6fabf843f510 Mon Sep 17 00:00:00 2001 From: Evan Nadeau <1878498+evannadeau@users.noreply.github.com> Date: Wed, 13 May 2026 17:11:24 -0700 Subject: [PATCH 2/2] =?UTF-8?q?fix(orchestrator):=20install-statusline=20?= =?UTF-8?q?=E2=80=94=20AUTO-COMPOSE=20+=20global=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes surfaced by 2026-05-13 sideload test: 1. **Detect existing statusLine at BOTH global and project level.** The original install only checked `.claude/settings.json` (project). But users with a global `~/.claude/settings.json` statusLine would silently lose that statusline when this skill wrote a project-level one — the project-level config shadows the global, so the user's global statusline stopped rendering in this project. Step 3 now checks both levels in precedence order and surfaces the existing value with its detected location ("project" or "global"). 2. **Add AUTO-COMPOSE as the recommended default** for users with an existing statusline. Ships a new `orchestrator-statusline-composed.sh` template that: - Captures Claude Code's stdin JSON once (since stdin can only be consumed once per process). - Runs the orchestrator role indicator (which ignores stdin). - Re-pipes the captured JSON into the user's `~/.claude/statusline.sh`. Result: two-line statusline where line 1 is the orchestrator role indicator and line 2+ is the user's existing renderer, untouched. Non-destructive — the user's existing script + global settings stay exactly as they were. 3. **Drop the agent-name from `orchestrator_statusline.py` output.** The session-name (e.g. `PA-2026-05-13-12-00-00`) added timestamp noise without informational value. Now renders just the role glyph + label + project basename: `🟡 PA quayline`. The previous 3-option menu (SKIP / REPLACE / COMPOSE MANUALLY) becomes a 4-option menu (AUTO-COMPOSE / SKIP / REPLACE / COMPOSE MANUALLY) with AUTO-COMPOSE as the recommended default for the existing-statusline case. Documents the stdin fan-out caveat that motivates AUTO-COMPOSE over a naive `cmd1; cmd2` composition. Test-validated locally: composed wrapper renders '🟡 PA quayline' + the user's personal model/usage line cleanly, both reading their respective inputs without conflict. --- .../skills/install-statusline/SKILL.md | 142 +++++++++++++----- .../orchestrator-statusline-composed.sh | 44 ++++++ .../scripts/orchestrator_statusline.py | 26 ++-- 3 files changed, 161 insertions(+), 51 deletions(-) create mode 100755 plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline-composed.sh diff --git a/plugins/orchestrator/skills/install-statusline/SKILL.md b/plugins/orchestrator/skills/install-statusline/SKILL.md index 37b201c..52680dc 100644 --- a/plugins/orchestrator/skills/install-statusline/SKILL.md +++ b/plugins/orchestrator/skills/install-statusline/SKILL.md @@ -72,83 +72,151 @@ 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 +### 3. Detect existing `statusLine` setting (global AND project) -Claude Code's `statusLine` setting in `.claude/settings.json` is a -single object — there's no built-in way to compose two statuslines. If -the user already has one configured, we must not silently clobber it. +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 -SETTINGS="$PWD/.claude/settings.json" -EXISTING="" -if [ -f "$SETTINGS" ]; then - EXISTING=$(jq -r '.statusLine // empty | tostring' "$SETTINGS" 2>/dev/null || echo "") +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 `EXISTING` is non-empty (and not the literal `"null"` from `jq`): +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 ``` -⚠ Existing statusLine detected: - - -The orchestrator statusline cannot auto-compose with arbitrary -existing statuslines (would require parsing + wrapping a shell -command we don't own). Three options: - - (1) SKIP install — keep your existing statusLine. - (2) REPLACE — clobber the existing statusLine with the - orchestrator one. Your current setting will be lost; back it - up first if you want to restore it later. +If `EXISTING_LEVEL` is non-empty, surface the existing config and the +four options: - (3) COMPOSE MANUALLY — install the orchestrator script and the - role-indicator fragment it produces, then merge it into your - existing statusline by hand. The fragment is one line of - text on stdout; you can wrap it in your existing script with - `$(/abs/path/to/orchestrator-statusline.sh)` or similar. +``` +⚠ 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) skip if -unclear. +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` is empty / absent: proceed to step 4 without prompting. +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; do + orchestrator-statusline.ps1 \ + orchestrator-statusline-composed.sh; do cp "$SCRIPTS_DIR/$f" "$INSTALL_DIR/$f" done -chmod 755 "$INSTALL_DIR/orchestrator-statusline.sh" +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` -Create or update `.claude/settings.json` to wire the statusline: +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. +# 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*) - CMD="powershell -NoProfile -ExecutionPolicy Bypass -File $INSTALL_DIR\\orchestrator-statusline.ps1" + 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 ;; *) - CMD="$INSTALL_DIR/orchestrator-statusline.sh" + 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). +# 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" 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.py b/plugins/orchestrator/skills/install-statusline/scripts/orchestrator_statusline.py index be9cc3d..b102056 100644 --- a/plugins/orchestrator/skills/install-statusline/scripts/orchestrator_statusline.py +++ b/plugins/orchestrator/skills/install-statusline/scripts/orchestrator_statusline.py @@ -3,19 +3,25 @@ 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 + session name. Suitable for Claude Code's +a colored role indicator + project basename. Suitable for Claude Code's `statusLine` setting. Output format (single line, ANSI-colored): - 🟡 PA-2026-05-13-12-00-00 (yellow, kind=prime) - ⚪ SA-frontend (default, kind=subordinate) - 🔴 DISCORD-LIVE-... (red, kind=discord-bot) + 🟡 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. +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 @@ -39,7 +45,6 @@ def render_statusline() -> str: kind = os.environ.get("ORCHESTRATOR_SESSION_KIND", "").strip() - name = os.environ.get("ORCHESTRATOR_AGENT_NAME", "").strip() project = os.environ.get( "ORCHESTRATOR_PROJECT_ROOT", os.environ.get("CLAUDE_PROJECT_DIR", ""), @@ -47,22 +52,15 @@ def render_statusline() -> str: project_label = "" if project: - # Show just the project's basename to keep the line short. project_label = f"{DIM}{os.path.basename(project.rstrip('/'))}{RESET}" if kind not in ROLE_STYLES: - # No launcher env present — emit a neutral indicator so the - # statusline still renders something useful. return f"{DIM}orchestrator{RESET} {project_label}".rstrip() glyph, color, label = ROLE_STYLES[kind] role_display = f"{color}{BOLD}{glyph} {label}{RESET}" - name_display = "" - if name: - name_display = f" {color}{name}{RESET}" - - line = f"{role_display}{name_display}" + line = role_display if project_label: line = f"{line} {project_label}" return line