diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..25fb727e8 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "statusLine": { + "type": "command", + "command": "bash .claude/statusline-plugin.sh", + "padding": 0 + } +} diff --git a/.claude/statusline-plugin.sh b/.claude/statusline-plugin.sh new file mode 100755 index 000000000..892e36c0a --- /dev/null +++ b/.claude/statusline-plugin.sh @@ -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