Skip to content

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

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

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.
SpawnBox-dev pushed a commit that referenced this pull request Jul 28, 2026
…e fleet is asleep

Firing #6 arrived at 13:09Z after the fleet had been idle overnight, taking the
detector to 0-for-6. SA-b14fafa3 named the shape and it is a class the previous
work did not touch: I fixed the HARM in 0.30.77 (the alert no longer asserts a
diagnosis or sends anyone to a terminal) and explicitly said it did not touch
PRECISION. This is precision, for one systematic slice.

"A channel delivery has sat unprocessed for >3 minutes" is the EXPECTED state
when nobody was awake. The terminal is not stuck; it is night. No triage step
can ever resolve that as anything but a false alarm, which makes it noise BY
CONSTRUCTION rather than a judgement call - and unlike the ambiguous cases, it
recurs every single morning.

DISCRIMINATOR: selective quiet vs fleet-wide quiet.
  nobody has taken a turn in a long while -> the fleet is dormant, an
                                             unprocessed delivery carries no
                                             information -> SUPPRESS
  others working, one silent past window  -> the signal this detector exists
                                             for -> FIRE, unchanged

MEASURED FROM TRANSCRIPT MTIME, NOT HEARTBEAT, and this is the load-bearing
detail. PA warned the suppressor must key on something that actually WRITES or
it inherits the blindness it exists to fix. Heartbeat is exactly the wrong
input: it stays fresh while a session sits idle, which is the very condition
the alert already reports ("heartbeat is fresh but..."). Keying on it would
produce a suppressor structurally incapable of suppressing, bolted to a
detector that cannot stop - two checks that cannot fail, facing each other.
A transcript write means a TURN RAN.

MEASUREMENT CAVEAT worth recording, because I nearly drew the wrong conclusion:
when I checked at 13:10 every transcript showed 13:09-13:10 and every heartbeat
was 25-50s old, which looks like an ACTIVE fleet contradicting the hypothesis.
It is not - the alert itself woke everyone. That reading is post-intervention
and says nothing about the state at 13:09:12 when the detector fired. Measuring
after the thing you are measuring has been disturbed is its own error, and the
overnight gap in the transcripts is the evidence that actually bears on it.

Guards: a lone session (no peers) does NOT suppress - it behaves exactly as
before rather than going silent forever. A single active peer is enough to keep
the detector armed. The 15-minute threshold sits well above the 3-minute
delivery window so an ordinary pause between turns cannot mask a genuine park.

Tests: +6 (suppresses on fleet-wide overnight quiet; fires when peers are
active; ONE active peer re-arms it; lone session never self-suppresses;
threshold boundary; and the threshold-vs-window relationship). Suite 835 pass /
0 fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MLELREep1GfMbuPSpmJRcu
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