Status line: which plugin this session is working on

Several builder sessions run at once, each in its own worktree, and from
the outside every one looks like a terminal printing tool calls. Asking
"which plugin are you on?" cost a round trip every time.

  doxa-maps · bundler-2/work · ▶ research-map-plus-grid (cap-4)

What it reports, in order: the bundle with the most recently modified
UNCOMMITTED file (that is what "working on" means); else library/ or
internal/ named as a scope, because an edit there is not one plugin's
business and naming whichever bundle happens to be dirty would be a lie;
else the last plugin touched in the recent commits, so the line still says
something true at the moment work lands instead of going blank.

The capability tier is read from manifest.json with the studio's own
formula (5 - PT[type], plugin-studio.vue:293), not a table retyped here.
Two copies of that mapping with nothing forcing agreement is the shape
check:capability-citations exists to catch — the README once captioned a
bundle cap-1 while the manifest said cap-2.

Details that were wrong on the first cut and are now measured: the repo
name comes from the remote, not the checkout directory, or every session
gets labelled with a worktree name nobody recognises (wt-bundler-2); and
the commit fallback uses `git log -m`, without which a MERGE commit lists
no files and the line read "idle" immediately after landing work.

Fail-soft throughout and never exits non-zero: no git, no branch; no
manifest, no tier; nothing edited, "idle". A status line runs on every
render, so one that errors or prints a stack trace is worse than none.
~172ms per render, no absolute paths (check:no-abs-paths).

Verified across all five tiers plus the library/ and internal/ scopes;
7/7 pre, 18/18 post.
This commit is contained in:
doxa-maps-88655c70 2026-09-24 00:26:24 +00:00
parent 8653147cfc
commit dd81a8818c
2 changed files with 107 additions and 0 deletions

7
.claude/settings.json Normal file
View file

@ -0,0 +1,7 @@
{
"statusLine": {
"type": "command",
"command": "bash .claude/statusline-plugin.sh",
"padding": 0
}
}

100
.claude/statusline-plugin.sh Executable file
View file

@ -0,0 +1,100 @@
#!/usr/bin/env bash
# statusline-plugin.sh — "which plugin is this agent working on?", in one terminal line.
#
# WHY: several builder agents run at once against this repo, each in its own worktree, and from the
# outside every one of them looks like a Claude terminal printing tool calls. Asking "which plugin
# are you on?" cost a round trip every time. This answers it continuously, without being asked.
#
# Renders: doxa-maps · bundler-2/work · ▶ research-map-plus-grid (cap-4)
#
# ★ THE TIER COMES FROM THE MANIFEST, NOT FROM A TABLE RETYPED HERE.
# plugin-studio.vue derives every badge with PT/capFor off the manifest's profile `type`, so
# retyping a profile there moves its badge with zero edits. A second hardcoded copy of that
# mapping in this script would be a fact stored twice with nothing forcing agreement — the exact
# shape check:capability-citations exists to catch (the README once captioned a bundle as cap-1
# while the manifest said cap-2). So this reads manifest.json and applies 5 - PT[type], the one
# formula, to the one source.
#
# ★ FAIL-SOFT EVERYWHERE, ON PURPOSE. A status line runs on every render. If it errors, is slow, or
# prints a stack trace, it makes the terminal worse than having none — so every step degrades to a
# quieter line instead of failing: no git → no branch, no manifest → no tier, nothing edited → "idle".
# It never exits non-zero.
#
# Wired up by .claude/settings.json (statusLine). No absolute paths: check:no-abs-paths requires a
# clone to work on any machine, and a status line is exactly the sort of dotfile that quietly
# hardcodes someone's home directory.
set -uo pipefail
IN=$(cat 2>/dev/null || true)
# Claude Code hands the hook a JSON blob; workspace.current_dir is the directory the session is in.
# Parsed with sed rather than a JSON tool because this runs on every render and must stay cheap.
DIR=$(printf '%s' "$IN" | sed -n 's/.*"current_dir"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
[ -z "$DIR" ] && DIR=$(printf '%s' "$IN" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)
[ -z "$DIR" ] && DIR="$PWD"
cd "$DIR" 2>/dev/null || true
ROOT=$(git rev-parse --show-toplevel 2>/dev/null) || ROOT=""
if [ -n "$ROOT" ]; then cd "$ROOT" 2>/dev/null || true; fi
# The repo NAME, not the checkout's directory name: every builder works in its own worktree
# (wt-bundler-2, wt-bundler-1, ...), and printing the folder would label each agent with a name
# nobody recognises instead of the project they are all in. The remote is the stable identity.
REPO=$(basename -s .git "$(git config --get remote.origin.url 2>/dev/null)" 2>/dev/null)
[ -z "$REPO" ] && REPO=$(basename "${ROOT:-$PWD}")
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
# WHAT IS BEING WORKED ON — uncommitted work first, because that is what "working on" means.
# Ordered by mtime, not by git's alphabetical listing: with several bundles dirty, the one touched
# most recently is the honest answer and the alphabetical first is an arbitrary one.
DIRTY=$(git status --porcelain 2>/dev/null | sed 's/^...//' | sed 's/^.* -> //')
BUNDLE=$(printf '%s\n' "$DIRTY" | grep '^plugin-designs/' 2>/dev/null | xargs -r ls -td 2>/dev/null | head -1 | cut -d/ -f2)
SCOPE=""
if [ -z "$BUNDLE" ]; then
# A library/ edit is not one plugin's business — it compiles into every bundle that imports it,
# which is the blast radius AGENT-BRIEF makes you name before changing anything in there. Say so
# rather than naming whichever bundle happens to be dirty for an unrelated reason.
if printf '%s\n' "$DIRTY" | grep -q '^library/'; then
SCOPE="library (reaches every bundle)"
elif printf '%s\n' "$DIRTY" | grep -q '^internal/'; then
SCOPE="internal (build + gates)"
else
# Nothing uncommitted: fall back to the last commit that touched a plugin, so a line still says
# something true right after a commit instead of going blank at the moment work lands.
# -m makes a MERGE commit list its files: without it a merge reports nothing changed, so the
# line read "idle" immediately after landing work — the least accurate moment to say so. Look
# back a few commits too, since the tip is often a merge or a build-output commit.
BUNDLE=$(git log -6 --name-only --format= -m 2>/dev/null | grep -m1 '^plugin-designs/' | cut -d/ -f2)
[ -n "$BUNDLE" ] && SCOPE="" || SCOPE="idle"
fi
fi
TIER=""
if [ -n "$BUNDLE" ] && [ -f public/doxa-maps/manifest.json ]; then
TIER=$(python3 - "$BUNDLE" <<'PY' 2>/dev/null || true
import json, sys
# The studio's own table (plugin-studio.vue:293) — tier = 5 - PT[type]; unknown types are cap-1.
PT = {'nested-mfe-spa': 0, 'nested-elements-mfe': 1, 'monolithic': 2, 'parameterized': 3, 'single': 4}
try:
m = json.load(open('public/doxa-maps/manifest.json'))
want = sys.argv[1]
caps = [5 - PT[p.get('type')] if p.get('type') in PT else 1
for b in m.get('bundles', []) if b.get('bundle') == want
for p in b.get('profiles', [])]
# A bundle may ship several profiles at different tiers; the HEADLINE capability is the highest
# one it demonstrates, which is how the studio's left rail groups it.
if caps: print(max(caps))
except Exception:
pass
PY
)
fi
OUT="$REPO"
[ -n "$BRANCH" ] && OUT="$OUT · $BRANCH"
if [ -n "$BUNDLE" ]; then
if [ -n "$TIER" ]; then OUT="$OUT · ▶ $BUNDLE (cap-$TIER)"; else OUT="$OUT · ▶ $BUNDLE"; fi
elif [ -n "$SCOPE" ]; then
OUT="$OUT · ▶ $SCOPE"
fi
printf '%s\n' "$OUT"
exit 0