Skip to content
Open
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
68 changes: 68 additions & 0 deletions claude/skills/session-status/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
name: session-status
description: Report the status of all active `claude agents` background sessions — what each is working on and what input it needs to unblock — and optionally stop stale ones. Use when the user asks "what are my sessions/agents doing", "what's blocked", "which sessions need input", "status of my claude agents", or wants to triage the FleetView backlog.
---

# session-status

Give the user a consolidated readout of every active `claude agents` (FleetView)
background session: what each one was doing and, crucially, **what input it needs
to move forward**. FleetView sessions are peers with no cross-session control, so
this is a reporting/triage tool, not an orchestrator.

## What to do

1. Run the helper script (it reads each session's transcript, so it may take a few
seconds when there are many sessions):

```bash
~/.claude/skills/session-status/session-status.sh
```

Prefer `--json` if you want to sort/filter/group the data yourself before
presenting:

```bash
~/.claude/skills/session-status/session-status.sh --json
```

The script derives its data from `claude agents --json` (active sessions) plus
each session's persisted transcript under `~/.claude/projects/`. `claude logs`
is deliberately **not** used — it only works while a session's process is still
alive and returns raw ANSI, so it's unreliable here.

2. Present a scannable report, **oldest first**:
- Lead with the summary line: total, and counts of working / blocked / done /
failed, plus the blocked share as a one-decimal percentage.
- One block per active session: **name · id · repo · age (start date)**, then a
one-line **task** and a one-line **waiting on**.
- Call out the stalest sessions (largest age) and any that look truly stuck.
- Note when a "blocked" session's last message is actually a completed result
(contains `result:`) or an error (e.g. "API Error", "Connection closed") —
these are parked/finished, not waiting on a real decision, and are safe
candidates to stop.

3. **Formatting** (org rules): dates as `MM/DD/YY`, thousands separators in
numbers, percentages to one decimal place.

## Offer to act

After the report, offer to help clear the backlog — but **never run a mutating
command without explicit confirmation**:

- **Stop** sessions the user no longer wants: `claude stop <id>` (the conversation
is kept and can be resumed later). Confirm which ids first; only then run.
- **Resume** a session: print `claude attach <id>` for the user to run in their
own terminal. `attach` is interactive and cannot run inside this session.

Suggest concrete cleanup (e.g. "these 6 finished sessions are still marked
blocked — stop them?") rather than asking open-endedly.

## Notes

- `session-status.sh --json` emits one object per active session
(`id, sessionId, cwd, repo, name, state, startDate, ageDays, task, waitingOn`)
for programmatic use.
- The "waiting on" text is the session's **last assistant message**; "task" is its
**first user message**. Both are truncated — attach to the session for full
context.
123 changes: 123 additions & 0 deletions claude/skills/session-status/session-status.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
# session-status.sh — report what every active `claude agents` session is doing
# and what input it needs to unblock.
#
# Data sources:
# - `claude agents --json` → active sessions (blocked + working)
# - `claude agents --json --all` → all sessions, for summary counts
# - ~/.claude/projects/<escaped-cwd>/<sessionId>.jsonl → persisted transcript,
# from which we pull the first user prompt (the task) and the last assistant
# text (what the session is waiting on).
#
# Usage: session-status.sh [--json]
# --json emit one JSON object per active session (for programmatic use)
set -euo pipefail

MODE="text"
[ "${1:-}" = "--json" ] && MODE="json"

command -v claude >/dev/null || { echo "claude CLI not found on PATH" >&2; exit 1; }
command -v jq >/dev/null || { echo "jq not found on PATH" >&2; exit 1; }

PROJECTS="$HOME/.claude/projects"
NOW=$(date +%s)

active=$(claude agents --json 2>/dev/null || echo '[]')
all=$(claude agents --json --all 2>/dev/null || echo '[]')

# ---- transcript helpers -------------------------------------------------------

# Resolve a session's transcript file from its cwd + sessionId.
transcript_path() {
local cwd="$1" sid="$2" esc tf
esc=$(printf '%s' "$cwd" | sed 's#[/.]#-#g')
tf="$PROJECTS/$esc/$sid.jsonl"
if [ -f "$tf" ]; then printf '%s' "$tf"; return 0; fi
tf=$(find "$PROJECTS" -name "$sid.jsonl" 2>/dev/null | head -1)
printf '%s' "$tf"
}

# Collapse whitespace and truncate to $2 chars.
clean() { tr '\n\t' ' ' | sed 's/ */ /g; s/^ *//; s/ *$//' | cut -c "1-${1:-220}"; }

# First user text message in a transcript = the original task.
first_user_text() {
local tf="$1"
[ -n "$tf" ] && [ -f "$tf" ] || { printf '(transcript not found)'; return; }
head -n 120 "$tf" | jq -rs '
[ .[] | select(.message.role=="user") | .message.content
| if type=="array" then ([.[] | select(.type=="text") | .text] | join(" "))
elif type=="string" then . else "" end ]
| map(select(. != "" and (startswith("<") | not)))
| first // "(no user text)"' 2>/dev/null || printf '(unreadable)'
}

# Last assistant text message = what the session is waiting on / last said.
last_assistant_text() {
local tf="$1"
[ -n "$tf" ] && [ -f "$tf" ] || { printf '(transcript not found)'; return; }
tail -n 400 "$tf" | jq -rs '
[ .[] | select(.message.role=="assistant") | .message.content
| if type=="array" then ([.[] | select(.type=="text") | .text] | join(" "))
elif type=="string" then . else "" end ]
| map(select(. != ""))
| last // "(no assistant text)"' 2>/dev/null || printf '(unreadable)'
}

# ---- summary (all sessions) ---------------------------------------------------

total=$(printf '%s' "$all" | jq 'length')
done_n=$(printf '%s' "$all" | jq '[.[]|select(.state=="done")]|length')
failed_n=$(printf '%s' "$all" | jq '[.[]|select(.state=="failed")]|length')
blocked_n=$(printf '%s' "$all"| jq '[.[]|select(.state=="blocked")]|length')
working_n=$(printf '%s' "$all"| jq '[.[]|select(.state=="working")]|length')

comma() { printf "%'d" "$1"; } # locale grouping, e.g. 1,234
pct() { awk -v b="$1" -v t="$2" 'BEGIN{ if(t==0){print "0.0"} else {printf "%.1f", 100*b/t} }'; }

if [ "$MODE" = "text" ]; then
echo "=============================================================="
echo " Claude session status — $(date '+%m/%d/%y %H:%M')"
echo "=============================================================="
printf ' Total: %s | working: %s blocked: %s done: %s failed: %s\n' \
"$(comma "$total")" "$(comma "$working_n")" "$(comma "$blocked_n")" \
"$(comma "$done_n")" "$(comma "$failed_n")"
printf ' Blocked (needs input): %s of %s active (%s%%)\n' \
"$(comma "$blocked_n")" "$(comma "$((blocked_n + working_n))")" \
"$(pct "$blocked_n" "$((blocked_n + working_n))")"
echo
fi

# ---- per-session detail (active only, oldest first) ---------------------------

json_out="[]"

while IFS=$'\t' read -r id sid cwd name state started; do
[ -n "${id:-}" ] || continue
started_s=$(( started / 1000 ))
date_fmt=$(date -r "$started_s" '+%m/%d/%y' 2>/dev/null || echo '??/??/??')
age_days=$(( (NOW - started_s) / 86400 ))
repo=$(basename "$cwd")

tf=$(transcript_path "$cwd" "$sid")
task=$(first_user_text "$tf" | clean 200)
waiting=$(last_assistant_text "$tf" | clean 400)

if [ "$MODE" = "text" ]; then
printf '● %s [%s] %s · %s · %sd old (%s)\n' \
"$name" "$state" "$id" "$repo" "$age_days" "$date_fmt"
printf ' task : %s\n' "$task"
printf ' waiting : %s\n' "$waiting"
printf ' resume : claude attach %s stop: claude stop %s\n\n' "$id" "$id"
else
obj=$(jq -n \
--arg id "$id" --arg sid "$sid" --arg cwd "$cwd" --arg repo "$repo" \
--arg name "$name" --arg state "$state" --arg date "$date_fmt" \
--argjson age "$age_days" --arg task "$task" --arg waiting "$waiting" \
'{id:$id,sessionId:$sid,cwd:$cwd,repo:$repo,name:$name,state:$state,startDate:$date,ageDays:$age,task:$task,waitingOn:$waiting}')
json_out=$(printf '%s' "$json_out" | jq --argjson o "$obj" '. + [$o]')
fi
done < <(printf '%s' "$active" | jq -r 'sort_by(.startedAt)[] | [.id,.sessionId,.cwd,.name,.state,.startedAt] | @tsv')

[ "$MODE" = "json" ] && printf '%s\n' "$json_out"
exit 0