Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"name": "phobos",
"source": "./",
"description": "Always-on efficiency discipline — triage token spend to task size, write minimal correct code, take fewer round-trips, keep context lean, use memory, route to the right tool. Three skills (phobos, phobos-code, phobos-plan) plus guard/state/status hooks.",
"version": "1.2.5",
"version": "1.3.0",
"category": "productivity",
"keywords": ["efficiency", "tokens", "cost", "context", "productivity", "workflow"],
"license": "MIT"
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin.json",
"name": "phobos",
"displayName": "phobos",
"version": "1.2.5",
"version": "1.3.0",
"description": "Always-on efficiency discipline for Claude Code — triage token spend to task size, write minimal correct code, take fewer round-trips, keep context lean. Ships three skills plus guard/state/status hooks.",
"author": {
"name": "broisnischal",
Expand Down
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
# Changelog

## 1.3.0 — 2026-07-28

Tab-title working spinner, an in-session `/phobos-update` command, status line shows effort instead of cost, a 1M context-meter fix, and auto-ignored scratch files.

### Added
- **Terminal tab-title spinner** (`spinner.sh`). While Claude works, the terminal
tab/window title animates with a braille spinner (`⠋ myrepo · working…`) and
resets to the plain project name when the turn ends — so a backgrounded tab
shows activity at a glance. Pure bash + OSC title escapes to the controlling
terminal (a detached, nohup'd animator started on `UserPromptSubmit`, killed on
`Stop`/`SessionEnd`); a quiet no-op where there's no tty. Opt out with
`PHOBOS_SPINNER=off`.
- **`/phobos-update` command** — update phobos from inside Claude Code. Installed
by `install.sh` into `~/.claude/commands/`; ships in the plugin as
`/phobos:update`. Runs `update.sh` for git checkouts, or points plugin installs
at `/plugin marketplace update phobos`.
- `update.sh` — pull the latest and re-apply in one command (`bash
phobos/update.sh`), passing install flags through so the install style is
preserved. `install.sh` referenced this script but it never existed.

### Changed
- The status line now shows the current **reasoning effort** (`effort high`,
`effort max`, …) where it used to show session dollar cost. Reads
`.effort.level` from the statusLine JSON; the badge is hidden when the model
has no effort knob, and the pricey tiers (`high`, `xhigh`, `max`) are
colour-escalated so a stray high setting is obvious. Cost is still available
via `benchmark.sh` / `savings.sh`.

### Fixed
- `context-warn.sh` no longer reports impossible readings like "context ~115%
full" on extended-context (1M) sessions. The `UserPromptSubmit` payload
doesn't carry the native `context_window` %, so the hook fell back to dividing
real tokens by a hardcoded 200k window — ~230k tokens then read as 115%. The
fallback now sizes the window from a `1m` marker on the model id/name, or
infers 1M when the token count already exceeds the 200k standard tier (the
only larger window), and caps the result at 100%. `statusline.sh`'s gauge got
the same guard.

### Added
- Phobos now registers its per-repo scratch files (`.claude/phobos-activity.log`,
`phobos-state.md`, `phobos-benchmark.jsonl`) in the repo's `.git/info/exclude`
at session start, so they're kept out of git automatically — without touching
the user's tracked `.gitignore` (which would surface in their commits/PRs).
Idempotent, and a no-op outside a git repo. `phobos-guard-allow` is left
visible since it's user-authored config a team may want to share.

## 1.2.5 — 2026-07-10

Drop the `⚠` caveat habit.
Expand Down
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ phobos is also a [Claude Code plugin marketplace](https://code.claude.com/docs/e

This installs all three skills (`phobos`, `phobos-code`, `phobos-plan`) and wires every hook automatically — no `settings.json` edits, updated with `/plugin marketplace update`.

One difference from `install.sh`: a plugin can't set the main status line — Claude Code only lets plugins define `agent`/`subagentStatusLine`, and the plugin's cache path is versioned and ephemeral. To get the cost/context gauge shown above, run `install.sh` (which wires it for you), or point your own `settings.json` `statusLine` at a stable clone: `bash "$HOME/.claude/skills/phobos/hooks/statusline.sh"`.
One difference from `install.sh`: a plugin can't set the main status line — Claude Code only lets plugins define `agent`/`subagentStatusLine`, and the plugin's cache path is versioned and ephemeral. To get the effort/context gauge shown above, run `install.sh` (which wires it for you), or point your own `settings.json` `statusLine` at a stable clone: `bash "$HOME/.claude/skills/phobos/hooks/statusline.sh"`.

## What you get

Expand All @@ -47,7 +47,9 @@ One difference from `install.sh`: a plugin can't set the main status line — Cl
- Session usage
- Activity
- State handoff
- Tab spinner
- Skills for plan and code
- `/phobos-update` command

## The read guard

Expand Down Expand Up @@ -97,6 +99,10 @@ _generated 2026-07-09T09:20Z · git a1b2c3d (main), 3 uncommitted — a snapshot

Written to `.claude/phobos-state.md` on edit turns and before every compaction, and injected at session start — so a new session (or a post-`/compact`/`/clear` turn) re-orients from a small stamped snapshot instead of the transcript. The `git` stamp is the staleness flag: trust it as of that SHA, then verify.

## The tab spinner

While Claude is working, the terminal tab/window title animates with a braille spinner (`⠋ myrepo · working…`) and resets to the plain project name when the turn ends — so a backgrounded tab shows activity at a glance. Pure bash + OSC title escapes written to the controlling terminal; a silent no-op where there's no tty. Turn it off with `PHOBOS_SPINNER=off`.

## Activity ledger

```
Expand Down Expand Up @@ -128,8 +134,22 @@ Dollar figures are estimates, not an invoice.

## Update

From inside Claude Code, run the command (installed for you):

```
/phobos-update
```

It pulls the latest and re-applies. Or do it by hand — installed via `install.sh`:

```sh
bash ~/.claude/skills/phobos/hooks/update.sh
bash ~/src/phobos/update.sh # same flags as install are honoured
```

Installed as a **plugin** (command is namespaced `/phobos:update`) — or update the marketplace directly:

```
/plugin marketplace update phobos
```

## Tuning knobs
Expand All @@ -139,8 +159,10 @@ bash ~/.claude/skills/phobos/hooks/update.sh
- `PHOBOS_MAX_READ_LINES` — default `2000`
- `PHOBOS_CMD_GUARD` — default `on`
- `PHOBOS_REPEAT_MAX` — default `2`
- `PHOBOS_WARN_PCT` — default `75`
- `PHOBOS_WARN_PCT` — default `80`
- `PHOBOS_CRIT_PCT` — default `92`
- `PHOBOS_CTX_LIMIT` — default `200000`
- `PHOBOS_SPINNER` — default `on`
- `BASELINE_MULT` — default `1.5`

Chat controls: `phobos:max`, "stop phobos", "normal mode".
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.2.5
1.3.0
16 changes: 16 additions & 0 deletions commands/update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
description: Update phobos to the latest version (pull + re-apply, or plugin marketplace)
allowed-tools: Bash
---

Update the phobos installation to the latest version. Be concise — do it, then report the result in one or two lines.

1. Locate the install and pick the right path:
- **Manual install (`install.sh`)** — `~/.claude/skills/phobos` resolves to a git checkout. The repo root is two levels above the real path of that skill. Run the updater there:
```sh
root=$(cd "$(dirname "$(readlink -f ~/.claude/skills/phobos)")/.." && pwd) && bash "$root/update.sh"
```
`update.sh` fast-forwards the checkout and re-applies the (idempotent) install, so any new hooks/skills land.
- **Plugin install** — the files live in a managed plugin cache, not a git checkout (the command above will fail or `update.sh` will say so). Do **not** git-pull it. Tell the user to run `/plugin marketplace update phobos` themselves — one slash command can't launch another.

2. Report: old → new version (from the repo's `VERSION`), or "already up to date". Remind the user to restart the Claude Code session to load the changes.
9 changes: 6 additions & 3 deletions hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
"SessionEnd": [
{
"hooks": [
{ "type": "command", "command": "bash \"${CLAUDE_PLUGIN_ROOT}/skills/phobos/hooks/session-end.sh\"" }
{ "type": "command", "command": "bash \"${CLAUDE_PLUGIN_ROOT}/skills/phobos/hooks/session-end.sh\"" },
{ "type": "command", "command": "bash \"${CLAUDE_PLUGIN_ROOT}/skills/phobos/hooks/spinner.sh\" stop" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "bash \"${CLAUDE_PLUGIN_ROOT}/skills/phobos/hooks/stop.sh\"" }
{ "type": "command", "command": "bash \"${CLAUDE_PLUGIN_ROOT}/skills/phobos/hooks/stop.sh\"" },
{ "type": "command", "command": "bash \"${CLAUDE_PLUGIN_ROOT}/skills/phobos/hooks/spinner.sh\" stop" }
]
}
],
Expand All @@ -31,7 +33,8 @@
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "bash \"${CLAUDE_PLUGIN_ROOT}/skills/phobos/hooks/context-warn.sh\"" }
{ "type": "command", "command": "bash \"${CLAUDE_PLUGIN_ROOT}/skills/phobos/hooks/context-warn.sh\"" },
{ "type": "command", "command": "bash \"${CLAUDE_PLUGIN_ROOT}/skills/phobos/hooks/spinner.sh\" start" }
]
}
],
Expand Down
27 changes: 22 additions & 5 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# --no-guard skip the PreToolUse read-guard hook
# --no-cmd-guard skip the PreToolUse Bash/Grep guard
# --no-statusline skip the status line
# --no-spinner skip the terminal tab-title "working" spinner
# --quiet print only warnings and errors
#
# Respects CLAUDE_CONFIG_DIR (default ~/.claude). Undo: bash uninstall.sh
Expand All @@ -24,16 +25,17 @@ root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cfg="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
settings="$cfg/settings.json"

skills=1 do_settings=1 guard=1 cmdguard=1 statusline=1 quiet=0
skills=1 do_settings=1 guard=1 cmdguard=1 statusline=1 spinner=1 quiet=0
for a in "$@"; do
case "$a" in
--skills-only) do_settings=0 ;;
--settings-only) skills=0 ;;
--no-guard) guard=0 ;;
--no-cmd-guard) cmdguard=0 ;;
--no-statusline) statusline=0 ;;
--no-spinner) spinner=0 ;;
--quiet) quiet=1 ;;
-h|--help) sed -n '2,18p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
-h|--help) sed -n '2,19p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "install.sh: unknown flag $a (see --help)" >&2; exit 1 ;;
esac
done
Expand All @@ -59,6 +61,16 @@ if [ "$skills" = 1 ]; then
fi
done
[ "$copied" = 1 ] && say "⚠ no symlink support here (Windows without Developer Mode?) — skills were copied, not linked. Re-run update.sh after a 'git pull' to refresh them."

# /phobos-update slash command — a single file linked (or copied) into commands/.
mkdir -p "$cfg/commands"
clink="$cfg/commands/phobos-update.md"; ctarget="$root/commands/update.md"
[ -e "$clink" ] && [ ! -L "$clink" ] && rm -f "$clink" # replace a stale copy
if ln -sfn "$ctarget" "$clink" 2>/dev/null && [ -L "$clink" ]; then
say "✓ command: /phobos-update → $ctarget"
else
rm -f "$clink"; cp "$ctarget" "$clink"; say "✓ command: /phobos-update (copied)"
fi
fi

[ "$do_settings" = 1 ] || { say "Done (skills only). Restart your Claude Code session."; exit 0; }
Expand All @@ -82,7 +94,7 @@ fi
# ensure(event; matcher; command; marker): append the hook entry unless some
# entry for that event already runs a command containing the marker.
merged=$(printf '%s' "$cur" | jq \
--arg hp "$hp" --argjson guard "$guard" --argjson cmdguard "$cmdguard" --argjson statusline "$statusline" '
--arg hp "$hp" --argjson guard "$guard" --argjson cmdguard "$cmdguard" --argjson statusline "$statusline" --argjson spinner "$spinner" '
def ensure(ev; m; cmd; marker):
(.hooks //= {})
| .hooks[ev] = ((.hooks[ev] // []) as $arr
Expand All @@ -101,6 +113,11 @@ merged=$(printf '%s' "$cur" | jq \
| (if $cmdguard == 1
then ensure("PreToolUse"; "Bash|Grep"; "bash \"\($hp)/guard-cmd.sh\""; "phobos/hooks/guard-cmd.sh")
else . end)
| (if $spinner == 1
then ensure("UserPromptSubmit"; ""; "bash \"\($hp)/spinner.sh\" start"; "phobos/hooks/spinner.sh")
| ensure("Stop"; ""; "bash \"\($hp)/spinner.sh\" stop"; "phobos/hooks/spinner.sh")
| ensure("SessionEnd"; ""; "bash \"\($hp)/spinner.sh\" stop"; "phobos/hooks/spinner.sh")
else . end)
| (if $statusline == 1 and ((.statusLine // {} | .command // "") | contains("phobos") or . == "")
then .statusLine = {type:"command", command:"bash \"\($hp)/statusline.sh\""}
else . end)
Expand All @@ -109,12 +126,12 @@ merged=$(printf '%s' "$cur" | jq \
if [ "$statusline" = 1 ]; then
existing=$(printf '%s' "$cur" | jq -r '.statusLine.command // ""')
if [ -n "$existing" ] && ! printf '%s' "$existing" | grep -q phobos; then
echo "⚠ you already have a custom statusLine — left untouched. phobos's renderer (cost/time/context gauge) is at: $hp/statusline.sh" >&2
echo "⚠ you already have a custom statusLine — left untouched. phobos's renderer (effort/context gauge) is at: $hp/statusline.sh" >&2
fi
fi

printf '%s\n' "$merged" | jq . > "$settings.tmp" && mv "$settings.tmp" "$settings"
say "✓ hooks: SessionStart · SessionEnd · Stop · PreCompact · UserPromptSubmit$( [ "$guard" = 1 ] && echo ' · PreToolUse(read-guard)')$( [ "$cmdguard" = 1 ] && echo ' · PreToolUse(cmd-guard)')"
say "✓ hooks: SessionStart · SessionEnd · Stop · PreCompact · UserPromptSubmit$( [ "$guard" = 1 ] && echo ' · PreToolUse(read-guard)')$( [ "$cmdguard" = 1 ] && echo ' · PreToolUse(cmd-guard)')$( [ "$spinner" = 1 ] && echo ' · tab-spinner')"
[ "$statusline" = 1 ] && say "✓ statusline wired"

say ""
Expand Down
20 changes: 19 additions & 1 deletion skills/phobos/hooks/context-warn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ IFS=$'\x1f' read -r tp sid < <(
# PHOBOS_CTX_LIMIT fallback below assumes 200k, so on a 1M session it reads ~100%
# when the window is only a fifth full — that was the source of false alarms.
npct=$(printf '%s' "$in" | jq -r '(.context_window.used_percentage // empty) | floor' 2>/dev/null)
# Model id/name, if this hook is handed one — only used to size the window in the
# transcript fallback. Empty on versions that don't pass it (then we infer below).
mdl=$(printf '%s' "$in" | jq -r '(.model.id // .model.display_name // "") | ascii_downcase' 2>/dev/null)
warn_at="${PHOBOS_WARN_PCT:-80}"
crit_at="${PHOBOS_CRIT_PCT:-92}"

Expand All @@ -36,8 +39,23 @@ else
| (.input_tokens // 0) + (.cache_read_input_tokens // 0) + (.cache_creation_input_tokens // 0)
' 2>/dev/null | tail -n 1)
[ -n "${used:-}" ] && [ "$used" -gt 0 ] 2>/dev/null || exit 0
limit="${PHOBOS_CTX_LIMIT:-200000}"
# Denominator = the model's real window. This hook isn't handed the native
# context_window object (the status line is), so size the window from whatever
# signal we have, in order: an explicit override; a 1M marker on the model
# id/name; a used count already past the 200k standard window — which can only
# be a 1M session (the sole larger tier), the exact case that used to overflow
# to an impossible ">100%".
if [ -n "${PHOBOS_CTX_LIMIT:-}" ]; then
limit="$PHOBOS_CTX_LIMIT"
elif case "$mdl" in *1m*) true ;; *) false ;; esac; then
limit=1000000
elif [ "$used" -gt 200000 ]; then
limit=1000000
else
limit=200000
fi
pct=$(( used * 100 / limit ))
[ "$pct" -gt 100 ] && pct=100
detail=" (~$(( used / 1000 ))k/$(( limit / 1000 ))k tokens)"
fi
[ "$pct" -ge "$warn_at" ] || exit 0
Expand Down
20 changes: 20 additions & 0 deletions skills/phobos/hooks/session-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,23 @@ elif [ -s "$log" ]; then
echo "## Recent activity (this repo)"
tail -n 8 "$log"
fi

# Keep phobos's own per-repo scratch files out of the user's git history without
# touching their tracked .gitignore (which would show up in their commits/PRs):
# register them in .git/info/exclude — a personal ignore list that is never
# pushed. Idempotent; a silent no-op outside a git repo. --git-common-dir points
# at the shared .git even from a worktree, so the rule lands in one place.
gitdir=$(git rev-parse --git-common-dir 2>/dev/null || true)
if [ -n "$gitdir" ] && [ -d "$gitdir" ]; then
excl="$gitdir/info/exclude"
if ! grep -qxF '.claude/phobos-activity.log' "$excl" 2>/dev/null; then
mkdir -p "$gitdir/info" 2>/dev/null || true
{
printf '%s\n' \
'# phobos per-repo runtime files — personal breadcrumbs, not project data' \
'.claude/phobos-activity.log' \
'.claude/phobos-benchmark.jsonl' \
'.claude/phobos-state.md'
} >> "$excl" 2>/dev/null || true
fi
fi
56 changes: 56 additions & 0 deletions skills/phobos/hooks/spinner.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/usr/bin/env bash
# phobos tab-title spinner — animate the terminal tab/window title while Claude
# works, so a backgrounded tab shows activity at a glance.
#
# UserPromptSubmit -> spinner.sh start spawn the animator
# Stop / SessionEnd -> spinner.sh stop kill it, reset the title
#
# Pure bash + OSC title escape codes written to the controlling terminal — a
# quiet no-op wherever there is no tty (piped / non-interactive runs). Opt out
# with PHOBOS_SPINNER=off. PHOBOS_SPINNER_TTY overrides the target terminal
# (used by the test suite); PHOBOS_SPINNER_LABEL overrides the "working…" text.
set -uo pipefail

[ "${PHOBOS_SPINNER:-on}" = "off" ] && exit 0
mode="${1:-start}"
tty_dev="${PHOBOS_SPINNER_TTY:-/dev/tty}"

in=$(cat 2>/dev/null || true)
read -r sid dir < <(
printf '%s' "$in" | jq -r '[(.session_id // "nosession"),
((.cwd // .workspace.current_dir // ".") | split("/") | last)] | join(" ")' 2>/dev/null
) || true
sid="${sid:-nosession}"; dir="${dir:-}"
state="${TMPDIR:-/tmp}/phobos-spinner-${sid}"

# Always clear any running animator first — makes both start and stop idempotent
# and stops overlapping daemons piling up across turns.
if [ -s "$state" ]; then
kill "$(cat "$state" 2>/dev/null)" 2>/dev/null || true
rm -f "$state"
fi

# No controlling terminal to draw on? Nothing to do (append-probe never truncates
# a real file, and opening /dev/tty fails cleanly when there's no tty).
{ : >>"$tty_dev"; } 2>/dev/null || exit 0

if [ "$mode" = "stop" ]; then
printf '\033]0;%s\007' "$dir" >"$tty_dev" 2>/dev/null || true
exit 0
fi

# start: spawn a fully-detached animator. All std fds are redirected and nohup'd
# so the hook returns at once and Claude Code never waits on our stdout.
label="${PHOBOS_SPINNER_LABEL:-${dir:+$dir · }working…}"
nohup bash -c '
tty=$1; label=$2
frames="⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"; i=0
ticks=$(( 30 * 60 * 10 )) # 30-min safety cap if Stop never fires
while [ "$ticks" -gt 0 ]; do
ch=${frames:i++%${#frames}:1}
printf "\033]0;%s %s\007" "$ch" "$label" >"$tty" 2>/dev/null || exit 0
ticks=$((ticks-1)); sleep 0.1
done
' _ "$tty_dev" "$label" >/dev/null 2>&1 </dev/null &
echo "$!" >"$state"
exit 0
Loading
Loading