Skip to content

feat(orchestrator): install-statusline skill — cross-platform role indicator#6

Open
evannadeau wants to merge 2 commits into
SpawnBox-dev:mainfrom
evannadeau:feat/orchestrator-install-statusline
Open

feat(orchestrator): install-statusline skill — cross-platform role indicator#6
evannadeau wants to merge 2 commits into
SpawnBox-dev:mainfrom
evannadeau:feat/orchestrator-install-statusline

Conversation

@evannadeau
Copy link
Copy Markdown

Why

The orchestrator launchers (`pa-start` / `sa-start` / `discord-start`)
already provide visual role distinction on Windows via `wt.exe --tabColor`
(gold for PA, red for Discord, default for SA). There's no portable POSIX
equivalent — Linux/macOS/WSL terminal emulators don't have a unified
tab-color API.

This PR adds a Claude Code statusline-based role indicator that:

  • Works inside the Claude UI itself, regardless of terminal emulator.
  • Renders `🟡 PA` / `⚪ SA` / `🔴 DISCORD` + project basename at the
    top of the statusline.
  • Reads only the env vars the launchers already set
    (`$ORCHESTRATOR_SESSION_KIND`, `$ORCHESTRATOR_PROJECT_ROOT`); no
    new env contract.
  • Complements (does not replace) the Windows tab color — Windows users
    get both.

The original design also intended to auto-set the in-Claude `/color`
prompt-bar color. That path was abandoned after empirical
verification: built-in Claude Code slash commands (`/color`, `/clear`,
`/compact`, etc.) cannot be invoked programmatically from any
agent-side mechanism (Skill tool, Bash tool, hook output, settings.json,
launcher CLI flags). Confirmed against
official commands docs
and hooks docs. The
statusline approach is the only path that actually works for
programmatic per-session role indication.

What changes

New skill: `plugins/orchestrator/skills/install-statusline/`

```
install-statusline/
├── SKILL.md
└── scripts/
├── orchestrator_statusline.py # canonical renderer (stdlib only)
├── orchestrator-statusline.sh # POSIX wrapper
├── orchestrator-statusline.ps1 # Windows PowerShell wrapper
└── orchestrator-statusline-composed.sh # AUTO-COMPOSE wrapper template
```

Renderer (`orchestrator_statusline.py`):

  • Stdlib only — no third-party deps.
  • Reads `$ORCHESTRATOR_SESSION_KIND` + `$ORCHESTRATOR_PROJECT_ROOT`
    from env.
  • Emits one ANSI-colored line: `🟡 PA ` (or SA / DISCORD).
  • Performance: <1ms per render — single env lookup + string format.
    No subprocess / file IO / network.
  • Graceful fallback: if env unset (manually-launched session), emits
    neutral `orchestrator ` line. Never errors the Claude UI.

Non-clobber pattern for users with existing statuslines:

Claude Code's `statusLine` setting is a single command — no built-in
composition API. The install skill detects existing config at BOTH
global (`~/.claude/settings.json`) AND project level
(`.claude/settings.json`) and surfaces a 4-option menu:

  1. AUTO-COMPOSE (recommended default) — install a composed wrapper
    that runs both the orchestrator renderer AND the user's existing
    statusline. Captures stdin JSON once, fans out to both. The user's
    existing script + global settings stay untouched. Non-destructive.
  2. SKIP — keep existing, install nothing.
  3. REPLACE — set project-level `statusLine` to orchestrator only.
    If existing was global, this only shadows in this project; global
    stays intact for other projects. If existing was project-level,
    it's lost.
  4. COMPOSE MANUALLY — install scripts but don't touch settings.json.
    User merges by hand. For non-standard setups where AUTO-COMPOSE's
    default of `~/.claude/statusline.sh` doesn't match.

Stdin fan-out (the non-obvious bit):

Claude Code pipes session JSON to the statusline command via stdin. A
naive `cmd1 ; cmd2` composition has both fight over stdin — cmd1
consumes it, cmd2 sees EOF and renders blank. The composed wrapper
captures stdin once at the top, runs the orchestrator renderer (which
ignores stdin), then re-pipes the captured JSON to the user's
existing renderer. Tested locally against a real Claude Code session;
both renderers produce their expected output.

Behavior

User-facing statusline before this PR (when launched via `pa-start`):
```
(no role indicator inside Claude UI — only the Windows tab color)
```

After:
```
🟡 PA quayline
Opus · ░░░░░░░░░░░░░░░░░░░░ 0% · 5h 0% · 7d 0% ← user's existing line, preserved
```

Or if the user has no existing statusline:
```
🟡 PA quayline
```

Test plan

Locally tested 2026-05-13 by sideloading into the marketplace skills
directory and running through the install flow against a real Claude
Code session with an existing global statusline. Verified:

  • Skill auto-discovered from `marketplaces//plugins/orchestrator/skills/`
    on fresh session.
  • Existing-statusline detection correctly identified the global config.
  • AUTO-COMPOSE option installed the composed wrapper, wrote
    project-level `statusLine` pointing at it, left the global setting
    untouched.
  • Composed wrapper rendered both lines correctly — orchestrator
    role indicator (yellow PA + project basename) followed by user's
    personal model/usage statusline.
  • Stdin fan-out worked — user's statusline received the JSON it
    expected from Claude Code.

Smoke test reproducible standalone:
```bash
echo '{"model":{"id":"claude-opus-4-7"}}' | \
ORCHESTRATOR_SESSION_KIND=prime \
ORCHESTRATOR_PROJECT_ROOT=/tmp/some-project \
./orchestrator-statusline-composed.sh
```

Operator E2E pending against fresh Windows + WSL hosts post-merge
(per the bash-port verification protocol).

Stack independence

Depends conceptually on `$ORCHESTRATOR_SESSION_KIND` being set by
the Python-canonical launcher PR (#5), but does not depend on PR #5
landing first
— the env var has been set by the prior PowerShell
launchers since 0.30.31 (WI `c03c9d6a`). This PR cleanly stands alone.

Unrelated to PR #2 (sidecar boot timeout) and PR #4 (backup-plugin-db).
Mergeable in any order.

Files changed

```
plugins/orchestrator/skills/install-statusline/SKILL.md (new, 326 lines)
plugins/orchestrator/skills/install-statusline/scripts/orchestrator_statusline.py (new, 82 lines)
plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline.sh (new, 24 lines)
plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline.ps1 (new, 33 lines)
plugins/orchestrator/skills/install-statusline/scripts/orchestrator-statusline-composed.sh (new, 44 lines)
```

No changes to existing files. No new third-party deps. Python 3.10+ is
already a baseline plugin dependency via `sidecar/embed_server.py`.

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-<timestamp>           prime (yellow/gold)
  ⚪ SA  SA-<name-or-timestamp>   subordinate (neutral grey)
  🔴 DISCORD  DISCORD-LIVE-...     discord-bot (red)
  orchestrator  <project>          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 <project>' line rather than erroring.

Stdlib only (Python). Performance: <1ms per render — single env
lookup + string format. No subprocess / file IO / network.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant