@marcomigozzi/trailhead 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -98
- package/bin/trailhead.js +14 -3
- package/package.json +2 -4
- package/plugins/trailhead/.claude-plugin/plugin.json +2 -4
- package/plugins/trailhead/commands/adopt.md +2 -2
- package/plugins/trailhead/commands/bug.md +1 -1
- package/plugins/trailhead/commands/config.md +1 -1
- package/plugins/trailhead/commands/dashboard.md +6 -0
- package/plugins/trailhead/commands/grill.md +1 -1
- package/plugins/trailhead/commands/idea.md +1 -1
- package/plugins/trailhead/commands/inbox.md +1 -1
- package/plugins/trailhead/commands/map.md +1 -1
- package/plugins/trailhead/commands/new.md +1 -1
- package/plugins/trailhead/commands/note.md +1 -1
- package/plugins/trailhead/commands/pause.md +1 -1
- package/plugins/trailhead/commands/quick.md +6 -0
- package/plugins/trailhead/commands/resume.md +1 -1
- package/plugins/trailhead/commands/seed.md +2 -2
- package/plugins/trailhead/commands/split.md +1 -1
- package/plugins/trailhead/commands/ticket.md +1 -1
- package/plugins/trailhead/commands/todo.md +2 -2
- package/plugins/trailhead/commands/update.md +5 -0
- package/plugins/trailhead/commands/whiteboard.md +6 -0
- package/plugins/trailhead/commands/work.md +1 -1
- package/plugins/trailhead/hooks/hooks.json +14 -0
- package/plugins/trailhead/hooks/trailhead-check-update.js +125 -0
- package/plugins/trailhead/hooks/trailhead-commit-guard.js +3 -3
- package/plugins/trailhead/hooks/trailhead-issue-injection-scanner.js +5 -5
- package/plugins/trailhead/hooks/trailhead-secret-guard.js +131 -0
- package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +71 -0
- package/plugins/trailhead/skills/trailhead/SKILL.md +243 -203
- package/plugins/trailhead/skills/trailhead/references/capture.md +24 -0
- package/plugins/trailhead/skills/trailhead/references/configuration.md +44 -16
- package/plugins/trailhead/skills/trailhead/references/inbox.md +29 -0
- package/plugins/trailhead/skills/trailhead/references/teamwork.md +87 -0
- package/plugins/trailhead/skills/trailhead/references/techniques/acceptance-testing.md +6 -2
- package/plugins/trailhead/skills/trailhead/references/techniques/code-review.md +10 -6
- package/plugins/trailhead/skills/trailhead/references/techniques/codebase-map.md +9 -7
- package/plugins/trailhead/skills/trailhead/references/techniques/debug.md +13 -9
- package/plugins/trailhead/skills/trailhead/references/techniques/domain-vocabulary.md +6 -5
- package/plugins/trailhead/skills/trailhead/references/techniques/grilling.md +8 -8
- package/plugins/trailhead/skills/trailhead/references/techniques/plan-review.md +14 -5
- package/plugins/trailhead/skills/trailhead/references/techniques/prototype.md +10 -8
- package/plugins/trailhead/skills/trailhead/references/techniques/research.md +7 -7
- package/plugins/trailhead/skills/trailhead/references/techniques/tdd.md +6 -6
- package/plugins/trailhead/skills/trailhead/references/updating.md +42 -0
- package/plugins/trailhead/templates/statusline-trailhead.sh +344 -0
- package/plugins/trailhead/templates/trailhead-label-guard.yml +2 -2
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Research
|
|
2
|
-
Spawn a focused subagent (AFK, parallelisable) with exactly ONE clear question
|
|
2
|
+
Spawn a focused subagent (AFK, parallelisable) with exactly ONE clear question: sharp enough that the answer is a decision, not a survey. Vague scope breeds rabbit holes; a narrow question is its own leash. Spawn it as the built-in **`general-purpose`** agent (it writes findings to a throwaway `research/<name>` branch, so it needs Write); **never** a specialised researcher agent that another installed tool registered.
|
|
3
3
|
|
|
4
|
-
- **Treat your own prior knowledge as hypothesis, not fact
|
|
5
|
-
- **Chase primary, high-trust sources**: official docs, source code, specs, first-party APIs
|
|
4
|
+
- **Treat your own prior knowledge as hypothesis, not fact**: training is stale. "As of my training" is a warning flag, not a citation.
|
|
5
|
+
- **Chase primary, high-trust sources**: official docs, source code, specs, first-party APIs; never a secondary write-up. Follow every claim back to the source that owns it; resolve *current* docs rather than reciting from memory.
|
|
6
6
|
- **Verify before asserting.** Cross-reference critical claims across sources: official docs (primary), release notes (currency), one more (confirmation). Check version numbers and dates.
|
|
7
|
-
- **Guard the classic traps:** a negative claim ("X isn't possible") needs official verification, not a failed search
|
|
8
|
-
- **Distinguish verified fact from inference explicitly.** Assign honest confidence (LOW when only training data backs a claim). "I couldn't find X", "sources contradict", "LOW confidence" are valuable findings, not failures
|
|
9
|
-
- **Investigation, not confirmation:** gather evidence, then conclude
|
|
10
|
-
- Capture findings as a durable Markdown artifact on a throwaway `research/<name>` branch (out of main), each claim carrying its source URL; leave a pointer on the ticket. Return a **decision-ready answer**
|
|
7
|
+
- **Guard the classic traps:** a negative claim ("X isn't possible") needs official verification, not a failed search; "didn't find it" ≠ "doesn't exist"; old docs may describe a deprecated path (check the changelog); verify ALL config scopes before ruling one out.
|
|
8
|
+
- **Distinguish verified fact from inference explicitly.** Assign honest confidence (LOW when only training data backs a claim). "I couldn't find X", "sources contradict", "LOW confidence" are valuable findings, not failures; never hide uncertainty behind confident prose.
|
|
9
|
+
- **Investigation, not confirmation:** gather evidence, then conclude; don't start from the answer you want and collect support.
|
|
10
|
+
- Capture findings as a durable Markdown artifact on a throwaway `research/<name>` branch (out of main), each claim carrying its source URL; leave a pointer on the ticket. Return a **decision-ready answer** (verdict, tradeoffs, confidence, citations) not a raw data dump.
|
|
11
11
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# TDD
|
|
2
|
-
Use TDD wherever behaviour has a definable input→output: business logic, parsers, validators, transforms, algorithms, state machines, API contracts. The gate: can you write `expect(fn(input)).toBe(output)` before `fn` exists? If yes, go test-first. Skip it for UI layout/styling, config, glue, one-off scripts, trivial CRUD, and exploratory spikes
|
|
2
|
+
Use TDD wherever behaviour has a definable input→output: business logic, parsers, validators, transforms, algorithms, state machines, API contracts. The gate: can you write `expect(fn(input)).toBe(output)` before `fn` exists? If yes, go test-first. Skip it for UI layout/styling, config, glue, one-off scripts, trivial CRUD, and exploratory spikes; add tests after if they earn their keep.
|
|
3
3
|
|
|
4
|
-
Before writing any test, name the **seams**
|
|
4
|
+
Before writing any test, name the **seams** (the public boundaries where behaviour is observable without reaching inside) and confirm them. Tests live at seams, never against internals. Work in vertical slices, one behaviour at a time: one test → one implementation → repeat. Never write all tests then all code (that tests *imagined* behaviour and locks in structure you don't yet understand).
|
|
5
5
|
|
|
6
|
-
- **RED
|
|
7
|
-
- **GREEN
|
|
8
|
-
- **REFACTOR
|
|
6
|
+
- **RED**: one failing test that pins the intended behaviour as a specification (`should reject empty email`, not `test1`). Run it; SEE it fail for the *right reason*: a real assertion mismatch, not an import/syntax error. If it passes before you write code, STOP: the feature already exists or the test is wrong. Commit `test(...): add failing test for X`.
|
|
7
|
+
- **GREEN**: the minimal code to pass. No cleverness, no speculative features, no anticipating future tests. If unrelated tests break, stop and investigate coupling. Commit `feat(...): implement X`.
|
|
8
|
+
- **REFACTOR**: only with tests green, clean up; rerun. Tests MUST still pass or you undo and go smaller. Commit if changed: `refactor(...): clean up X`.
|
|
9
9
|
|
|
10
|
-
A good test verifies behaviour through the public interface and survives refactors. Reject **implementation-coupled** tests (mock internal collaborators, assert private state, hit the DB not the interface
|
|
10
|
+
A good test verifies behaviour through the public interface and survives refactors. Reject **implementation-coupled** tests (mock internal collaborators, assert private state, hit the DB not the interface; break on refactor though behaviour didn't) and **tautological** tests (recompute the expected value the way the code does; expected values come from an independent source: a known-good literal, a worked example, the spec). Prefer integration tests exercising real seams over isolated unit mocks. **Mock only at system boundaries** (external APIs, the DB, the clock, randomness, the filesystem) and never your own code: needing to mock an internal collaborator is a design smell, so inject the dependency instead. Design for testability: accept dependencies rather than construct them, return results rather than fire side effects, keep the seam's surface small. The discipline is absolute: **no implementation code before a failing test exists.**
|
|
11
11
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Updating trailhead
|
|
2
|
+
|
|
3
|
+
`/trailhead:update` checks whether a newer trailhead exists and installs it **where that is safe**. The version check itself is deterministic (a shipped script, not a guess): the **`trailhead-check-update.js`** SessionStart hook writes a cache the statusline and this command read.
|
|
4
|
+
|
|
5
|
+
## 1. Read the check cache (refresh it if stale)
|
|
6
|
+
|
|
7
|
+
Read `${XDG_CACHE_HOME:-~/.cache}/trailhead/update-check.json`. It carries `{channel, installed, latest, updateAvailable, where, checkedAt}`. If it is **missing or older than a few hours**, refresh it first by running the check hook and re-reading:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# prefer the installed hook; on a dev-symlink install it may only live in the repo
|
|
11
|
+
HOOK="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hooks/trailhead-check-update.js"
|
|
12
|
+
[ -f "$HOOK" ] || HOOK="<repoRoot>/plugins/trailhead/hooks/trailhead-check-update.js" # `where` from the cache is the repo root on dev
|
|
13
|
+
rm -f "${XDG_CACHE_HOME:-$HOME/.cache}/trailhead/update-check.json" # force a fresh check (bypass the 6h throttle)
|
|
14
|
+
node "$HOOK"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If after this the cache still can't be read (no network, npm/gh unavailable), say so and stop: don't guess a version.
|
|
18
|
+
|
|
19
|
+
## 2. If already current, stop
|
|
20
|
+
|
|
21
|
+
If `updateAvailable` is `false`, tell the user they're on the latest (`installed` == `latest`) and stop. Nothing to do.
|
|
22
|
+
|
|
23
|
+
## 3. If newer, confirm, then install per channel
|
|
24
|
+
|
|
25
|
+
Show `installed → latest` and **confirm before touching anything** (auto-install, but with a go-ahead). Then act on `channel`:
|
|
26
|
+
|
|
27
|
+
- **`dev`** (skill symlinked to a checkout): the code is a git repo at `where`. Update in place:
|
|
28
|
+
```bash
|
|
29
|
+
git -C "<where>" pull --ff-only
|
|
30
|
+
```
|
|
31
|
+
Edits go live through the symlink, no reinstall needed. If the pull isn't fast-forwardable (local commits/changes), **stop and tell the user** rather than forcing it; let them reconcile.
|
|
32
|
+
- **`npm`** (installed via `npx @marcomigozzi/trailhead`): re-run the installer at the latest version, which copies the new files idempotently:
|
|
33
|
+
```bash
|
|
34
|
+
npx -y @marcomigozzi/trailhead@latest
|
|
35
|
+
```
|
|
36
|
+
- **`plugin`** (installed via the Claude Code marketplace): the update is a **Claude Code command**, which this session cannot run for the user. Tell them to run **`/plugin update trailhead`** themselves (and `/plugin marketplace update trailhead` first if the marketplace is stale).
|
|
37
|
+
|
|
38
|
+
## 4. After a successful auto-update
|
|
39
|
+
|
|
40
|
+
For `dev`/`npm`, once the update lands: **refresh the check cache** so the statusline's `⬆` flag clears (`rm` the cache and re-run the hook, as in step 1), then tell the user to **restart or reload the agent** so the new skill, commands, and hooks are picked up. Report the new version plainly (`now on <latest>`).
|
|
41
|
+
|
|
42
|
+
Never edit `package.json`/`plugin.json` versions here: this command **consumes** releases, it does not cut them.
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Statusline trailhead-aware: "modello | progetto | branch | ticket | contesto | usage"
|
|
3
|
+
#
|
|
4
|
+
# Legge su stdin il JSON che Claude Code passa alle statusline
|
|
5
|
+
# (.model.display_name, .model.id, .workspace.current_dir, .cwd, .transcript_path).
|
|
6
|
+
#
|
|
7
|
+
# Ticket in lavorazione: marker locale `.trailhead/session-ticket` (scritto da
|
|
8
|
+
# trailhead al work-start, porta anche il titolo) -> numero dal branch
|
|
9
|
+
# `trailhead/t<N>` -> titolo via `gh` in cache/background (solo se origin GitHub).
|
|
10
|
+
#
|
|
11
|
+
# Contesto: barra `[BARRA] Nk/Mk (P%)`, dall'ultimo usage del transcript. Tetto:
|
|
12
|
+
# $CONTEXT_LIMIT -> .model.context_window -> tabella modello -> auto-bump 1M.
|
|
13
|
+
#
|
|
14
|
+
# Usage: % REALE del piano via l'API OAuth di Anthropic (come ccstatusline):
|
|
15
|
+
# GET https://api.anthropic.com/api/oauth/usage con il Bearer preso da
|
|
16
|
+
# ~/.claude/.credentials.json (claudeAiOauth.accessToken). Mostra la finestra
|
|
17
|
+
# 5h e la settimana (7 giorni) piu' il reset. Cache ~60s, chiamata in background
|
|
18
|
+
# (il render non aspetta la rete). Disabilita con TRAILHEAD_STATUSLINE_USAGE=0.
|
|
19
|
+
#
|
|
20
|
+
# Uso:
|
|
21
|
+
# - standalone: settings.json -> statusLine.command = "bash ~/.claude/statusline-trailhead.sh"
|
|
22
|
+
# - widget singolo (ccstatusline custom command): "--ticket-only" | "--context-only" | "--usage-only"
|
|
23
|
+
|
|
24
|
+
set -uo pipefail
|
|
25
|
+
|
|
26
|
+
MODE="${1:-full}"
|
|
27
|
+
input="$(cat)"
|
|
28
|
+
|
|
29
|
+
j() { printf '%s' "$input" | jq -r "$1 // empty" 2>/dev/null; }
|
|
30
|
+
|
|
31
|
+
# --- colori ANSI (disattivabili con NO_COLOR) ---
|
|
32
|
+
if [ -n "${NO_COLOR:-}" ]; then
|
|
33
|
+
C_RST= ; C_SEP= ; C_MODEL= ; C_PROJ= ; C_BRANCH= ; C_TICKET= ; C_CTX= ; C_5H= ; C_RESET= ; C_7D= ; C_UPDATE=
|
|
34
|
+
else
|
|
35
|
+
e=$'\033['
|
|
36
|
+
C_RST="${e}0m" # reset
|
|
37
|
+
C_SEP="${e}90m" # separatori: grigio
|
|
38
|
+
C_MODEL="${e}36m" # modello: cyan
|
|
39
|
+
C_PROJ="${e}94m" # progetto: blu chiaro
|
|
40
|
+
C_BRANCH="${e}92m" # branch: verde chiaro
|
|
41
|
+
C_TICKET="${e}93m" # ticket: giallo chiaro
|
|
42
|
+
C_CTX="${e}95m" # contesto: magenta chiaro
|
|
43
|
+
C_5H="${e}33m" # finestra 5h: giallo
|
|
44
|
+
C_RESET="${e}32m" # reset: verde
|
|
45
|
+
C_7D="${e}96m" # settimana: cyan chiaro
|
|
46
|
+
C_UPDATE="${e}91m" # aggiornamento disponibile: rosso chiaro
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# --- modello ---
|
|
50
|
+
MODEL="$(j '.model.display_name')"
|
|
51
|
+
TRANSCRIPT="$(j '.transcript_path')"
|
|
52
|
+
|
|
53
|
+
# --- directory di lavoro ---
|
|
54
|
+
DIR="$(j '.workspace.current_dir')"
|
|
55
|
+
[ -z "$DIR" ] && DIR="$(j '.cwd')"
|
|
56
|
+
[ -z "$DIR" ] && DIR="$PWD"
|
|
57
|
+
|
|
58
|
+
# --- repo root + nome progetto ---
|
|
59
|
+
ROOT="$(git -C "$DIR" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$DIR")"
|
|
60
|
+
PROJ="$(basename "$ROOT")"
|
|
61
|
+
|
|
62
|
+
# --- branch (con gestione detached HEAD) ---
|
|
63
|
+
BRANCH="$(git -C "$DIR" rev-parse --abbrev-ref HEAD 2>/dev/null)"
|
|
64
|
+
[ "$BRANCH" = "HEAD" ] && BRANCH="@$(git -C "$DIR" rev-parse --short HEAD 2>/dev/null)"
|
|
65
|
+
|
|
66
|
+
# --- ticket in lavorazione ---
|
|
67
|
+
TICKET_N=""
|
|
68
|
+
TITLE=""
|
|
69
|
+
MARKER="$ROOT/.trailhead/session-ticket"
|
|
70
|
+
if [ -f "$MARKER" ]; then
|
|
71
|
+
line="$(head -1 "$MARKER" 2>/dev/null)"
|
|
72
|
+
TICKET_N="$(printf '%s' "$line" | grep -oE '[0-9]+' | head -1)"
|
|
73
|
+
TITLE="$(printf '%s' "$line" | sed -E 's/^[^0-9]*[0-9]+[[:space:]]*//')"
|
|
74
|
+
elif [[ "$BRANCH" =~ trailhead/t([0-9]+) ]]; then
|
|
75
|
+
TICKET_N="${BASH_REMATCH[1]}"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# --- titolo via gh, solo se manca e se l'origin e' GitHub (cache + background) ---
|
|
79
|
+
if [ -n "$TICKET_N" ] && [ -z "$TITLE" ] && command -v gh >/dev/null 2>&1; then
|
|
80
|
+
ORIGIN="$(git -C "$ROOT" remote get-url origin 2>/dev/null)"
|
|
81
|
+
REPO=""
|
|
82
|
+
case "$ORIGIN" in
|
|
83
|
+
*github.com[:/]*) REPO="$(printf '%s' "$ORIGIN" | sed -E 's#.*github\.com[:/]([^/]+/[^/.]+)(\.git)?/?$#\1#')" ;;
|
|
84
|
+
esac
|
|
85
|
+
if [ -n "$REPO" ]; then
|
|
86
|
+
CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/trailhead-statusline"
|
|
87
|
+
mkdir -p "$CACHE" 2>/dev/null
|
|
88
|
+
CF="$CACHE/$(printf '%s' "$REPO" | tr -c 'a-zA-Z0-9' '_')_${TICKET_N}.title"
|
|
89
|
+
[ -f "$CF" ] && TITLE="$(cat "$CF" 2>/dev/null)"
|
|
90
|
+
if [ ! -f "$CF" ] || [ -n "$(find "$CF" -mmin +10 2>/dev/null)" ]; then
|
|
91
|
+
( t="$(gh issue view "$TICKET_N" -R "$REPO" --json title -q .title 2>/dev/null)" \
|
|
92
|
+
&& [ -n "$t" ] && printf '%s' "$t" > "$CF" ) >/dev/null 2>&1 &
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# --- composizione ticket ---
|
|
98
|
+
TICKET=""
|
|
99
|
+
if [ -n "$TICKET_N" ]; then
|
|
100
|
+
if [ -n "$TITLE" ]; then
|
|
101
|
+
# titolo intero di default (riga 2 dedicata); tetto opzionale con TICKET_TITLE_MAX
|
|
102
|
+
TMAX="${TICKET_TITLE_MAX:-}"
|
|
103
|
+
[ -n "$TMAX" ] && [ "${#TITLE}" -gt "$TMAX" ] && TITLE="${TITLE:0:$((TMAX-1))}…"
|
|
104
|
+
TICKET="#${TICKET_N} ${TITLE}"
|
|
105
|
+
else
|
|
106
|
+
TICKET="#${TICKET_N}"
|
|
107
|
+
fi
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
# --- barra dimensione contesto: "[BARRA] 364k/1000k (36%)" ---
|
|
111
|
+
# Fonte primaria (come ccstatusline): .context_window del JSON di Claude Code
|
|
112
|
+
# (context_window_size = tetto reale, current_usage = token). Fallback: ultimo
|
|
113
|
+
# usage del transcript + tabella modello. $CONTEXT_LIMIT forza il tetto.
|
|
114
|
+
# Denominatore = finestra * 0.8 (soglia auto-compact, come ccstatusline); il
|
|
115
|
+
# rapporto e' regolabile con CONTEXT_USABLE_RATIO (1 = tetto pieno).
|
|
116
|
+
CTX="$(
|
|
117
|
+
{ [ -n "$TRANSCRIPT" ] && [ -f "$TRANSCRIPT" ] && tail -n 300 "$TRANSCRIPT" 2>/dev/null | tac | grep -m1 'cache_read_input_tokens'; } | \
|
|
118
|
+
CTX_MODEL_ID="$(j '.model.id')" \
|
|
119
|
+
CTX_CW_SIZE="$(j '.context_window.context_window_size')" \
|
|
120
|
+
CTX_USAGE="$(printf '%s' "$input" | jq -c '.context_window.current_usage // empty' 2>/dev/null)" \
|
|
121
|
+
python3 -c '
|
|
122
|
+
import sys, os, json, re
|
|
123
|
+
|
|
124
|
+
def as_int(x):
|
|
125
|
+
try: return int(float(x))
|
|
126
|
+
except Exception: return 0
|
|
127
|
+
|
|
128
|
+
def sum_usage(u):
|
|
129
|
+
# "context length" = input + cache_creation + cache_read (esclude output), come ccstatusline
|
|
130
|
+
if isinstance(u, (int, float)):
|
|
131
|
+
return int(u)
|
|
132
|
+
if isinstance(u, dict):
|
|
133
|
+
return (as_int(u.get("input_tokens")) + as_int(u.get("cache_creation_input_tokens"))
|
|
134
|
+
+ as_int(u.get("cache_read_input_tokens")))
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
tok = 0
|
|
138
|
+
# 1) fonte primaria: .context_window.current_usage dal JSON di Claude Code
|
|
139
|
+
cw = os.environ.get("CTX_USAGE","")
|
|
140
|
+
if cw:
|
|
141
|
+
try: tok = sum_usage(json.loads(cw))
|
|
142
|
+
except Exception: tok = 0
|
|
143
|
+
# 2) fallback: ultimo usage del transcript
|
|
144
|
+
if tok <= 0:
|
|
145
|
+
line = sys.stdin.readline()
|
|
146
|
+
if line:
|
|
147
|
+
try:
|
|
148
|
+
u = json.loads(line).get("message", {}).get("usage", {})
|
|
149
|
+
except Exception:
|
|
150
|
+
m = re.search(r"\"usage\":\{.*?\}\}", line) or re.search(r"\"usage\":\{[^}]*\}", line)
|
|
151
|
+
u = json.loads("{"+m.group(0)+"}")["usage"] if m else {}
|
|
152
|
+
tok = sum_usage(u)
|
|
153
|
+
if tok <= 0:
|
|
154
|
+
raise SystemExit
|
|
155
|
+
|
|
156
|
+
# tetto: CONTEXT_LIMIT (override) > context_window_size (autorevole) > tabella modello
|
|
157
|
+
limit = as_int(os.environ.get("CONTEXT_LIMIT"))
|
|
158
|
+
if not limit:
|
|
159
|
+
limit = as_int(os.environ.get("CTX_CW_SIZE"))
|
|
160
|
+
if not limit:
|
|
161
|
+
mid = (os.environ.get("CTX_MODEL_ID") or "").lower()
|
|
162
|
+
table = [("haiku",200000),("sonnet",200000),("opus",200000)]
|
|
163
|
+
limit = next((v for k,v in table if k in mid), 200000)
|
|
164
|
+
if tok > limit and limit <= 200000: # auto-bump 1M solo nel fallback
|
|
165
|
+
limit = 1000000
|
|
166
|
+
|
|
167
|
+
# soglia usabile (auto-compact): denominatore = finestra * ratio (0.8 come ccstatusline)
|
|
168
|
+
try:
|
|
169
|
+
ratio = float(os.environ.get("CONTEXT_USABLE_RATIO") or 0.8)
|
|
170
|
+
except Exception:
|
|
171
|
+
ratio = 0.8
|
|
172
|
+
denom = round(limit * ratio) if limit else 0
|
|
173
|
+
pct = min(100, round(tok*100/denom)) if denom else 0 # % contro la soglia 0.8
|
|
174
|
+
n = 10; filled = round(pct*n/100)
|
|
175
|
+
bar = "█"*filled + "░"*(n-filled)
|
|
176
|
+
# numeri REALI: token effettivi / tetto pieno; la percentuale riflette auto-compact 0.8
|
|
177
|
+
print(f"[{bar}] {round(tok/1000)}k/{round(limit/1000)}k ({pct}%)")
|
|
178
|
+
' 2>/dev/null
|
|
179
|
+
)"
|
|
180
|
+
|
|
181
|
+
# --- usage: % reale del piano via l'API OAuth di Anthropic (come ccstatusline) ---
|
|
182
|
+
# five_hour.utilization (finestra 5h) e seven_day.utilization (settimana), piu' i reset.
|
|
183
|
+
# Backoff variabile (come ccstatusline): cache success 180s; su 429 rispetta l'header
|
|
184
|
+
# Retry-After (default 300s); altri errori 30s. Timeout 5s (USAGE_API_TIMEOUT lo cambia).
|
|
185
|
+
# Chiamata in background: il render non attende mai. Off con TRAILHEAD_STATUSLINE_USAGE=0.
|
|
186
|
+
USAGE_RAW=""
|
|
187
|
+
CREDS="$HOME/.claude/.credentials.json"
|
|
188
|
+
if [ "${TRAILHEAD_STATUSLINE_USAGE:-1}" != "0" ] && [ -f "$CREDS" ]; then
|
|
189
|
+
CACHE="${XDG_CACHE_HOME:-$HOME/.cache}/trailhead-statusline"
|
|
190
|
+
mkdir -p "$CACHE" 2>/dev/null
|
|
191
|
+
UF="$CACHE/usage.txt"
|
|
192
|
+
# cache RAW (una parte per riga, senza colori): i colori si applicano al render
|
|
193
|
+
[ -f "$UF" ] && USAGE_RAW="$(cat "$UF" 2>/dev/null)"
|
|
194
|
+
# refresh solo dopo il "next" (epoch): backoff diverso per success / 429 / errore
|
|
195
|
+
NOW="$(date +%s)"; NEXT=0
|
|
196
|
+
[ -f "$UF.next" ] && NEXT="$(tr -dc 0-9 < "$UF.next" 2>/dev/null)"
|
|
197
|
+
[ -z "$NEXT" ] && NEXT=0
|
|
198
|
+
if [ "$NOW" -ge "$NEXT" ]; then
|
|
199
|
+
# lock atomico (mkdir): con piu' terminali che renderizzano insieme, un solo
|
|
200
|
+
# processo acquisisce e chiama; gli altri saltano. Il background rilascia il
|
|
201
|
+
# lock a fine chiamata. Rimuovo prima un lock stantio (processo morto, >60s).
|
|
202
|
+
LOCK="$CACHE/.lock"
|
|
203
|
+
[ -d "$LOCK" ] && [ -n "$(find "$LOCK" -maxdepth 0 -mmin +1 2>/dev/null)" ] && rmdir "$LOCK" 2>/dev/null
|
|
204
|
+
if command mkdir "$LOCK" 2>/dev/null; then
|
|
205
|
+
printf '%s' "$((NOW + 15))" > "$UF.next" 2>/dev/null
|
|
206
|
+
( UF="$UF" CREDS="$CREDS" USAGE_TIMEOUT="${USAGE_API_TIMEOUT:-5}" python3 - >/dev/null 2>&1 <<'PY'
|
|
207
|
+
import os, json, time, datetime, urllib.request, urllib.error
|
|
208
|
+
uf = os.environ["UF"]; creds = os.environ["CREDS"]
|
|
209
|
+
now = int(time.time())
|
|
210
|
+
try: timeout = float(os.environ.get("USAGE_TIMEOUT") or 5)
|
|
211
|
+
except Exception: timeout = 5
|
|
212
|
+
|
|
213
|
+
def set_next(ttl):
|
|
214
|
+
try:
|
|
215
|
+
with open(uf + ".next", "w") as f:
|
|
216
|
+
f.write(str(now + int(ttl)))
|
|
217
|
+
except Exception:
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
o = json.load(open(creds)).get("claudeAiOauth", {})
|
|
222
|
+
tok = o.get("accessToken")
|
|
223
|
+
if not tok:
|
|
224
|
+
set_next(60); raise SystemExit
|
|
225
|
+
req = urllib.request.Request(
|
|
226
|
+
"https://api.anthropic.com/api/oauth/usage",
|
|
227
|
+
headers={"Authorization": f"Bearer {tok}", "anthropic-beta": "oauth-2025-04-20"})
|
|
228
|
+
r = json.load(urllib.request.urlopen(req, timeout=timeout))
|
|
229
|
+
parts = [] # RAW, una per riga; i colori li mette bash al render
|
|
230
|
+
fh = r.get("five_hour") or {}
|
|
231
|
+
if fh.get("utilization") is not None:
|
|
232
|
+
parts.append(f"5h ({round(fh['utilization'])}%)")
|
|
233
|
+
ra = fh.get("resets_at")
|
|
234
|
+
if ra:
|
|
235
|
+
try:
|
|
236
|
+
t = datetime.datetime.fromisoformat(str(ra).replace("Z", "+00:00"))
|
|
237
|
+
mins = int((t - datetime.datetime.now(datetime.timezone.utc)).total_seconds() // 60)
|
|
238
|
+
if mins > 0:
|
|
239
|
+
parts.append(f"⏳ {mins//60}h{mins%60:02d}m")
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
sd = r.get("seven_day") or {}
|
|
243
|
+
if sd.get("utilization") is not None:
|
|
244
|
+
parts.append(f"7d ({round(sd['utilization'])}%)")
|
|
245
|
+
out = "\n".join(parts)
|
|
246
|
+
if out:
|
|
247
|
+
with open(uf + ".tmp", "w") as f:
|
|
248
|
+
f.write(out)
|
|
249
|
+
os.replace(uf + ".tmp", uf)
|
|
250
|
+
set_next(180) # success: 3 min (come ccstatusline)
|
|
251
|
+
except urllib.error.HTTPError as e:
|
|
252
|
+
if e.code == 429: # rate limit: Retry-After (>0) o default 300s
|
|
253
|
+
ra = e.headers.get("Retry-After") if e.headers else None
|
|
254
|
+
ttl = 300
|
|
255
|
+
try:
|
|
256
|
+
v = int(str(ra).strip())
|
|
257
|
+
if v > 0: ttl = v
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
set_next(ttl)
|
|
261
|
+
else:
|
|
262
|
+
set_next(30) # altri errori HTTP
|
|
263
|
+
except SystemExit:
|
|
264
|
+
pass
|
|
265
|
+
except Exception:
|
|
266
|
+
set_next(30) # timeout / rete / parse
|
|
267
|
+
PY
|
|
268
|
+
rmdir "$LOCK" 2>/dev/null
|
|
269
|
+
) >/dev/null 2>&1 &
|
|
270
|
+
fi
|
|
271
|
+
fi
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# --- colora le parti dell'usage al render (cache RAW -> segmenti colorati) ---
|
|
275
|
+
USAGE=""
|
|
276
|
+
if [ -n "$USAGE_RAW" ]; then
|
|
277
|
+
while IFS= read -r part; do
|
|
278
|
+
[ -z "$part" ] && continue
|
|
279
|
+
case "$part" in
|
|
280
|
+
"5h "*) c="$C_5H" ;;
|
|
281
|
+
"⏳"*) c="$C_RESET" ;;
|
|
282
|
+
"7d "*) c="$C_7D" ;;
|
|
283
|
+
*) c="" ;;
|
|
284
|
+
esac
|
|
285
|
+
if [ -z "$USAGE" ]; then
|
|
286
|
+
USAGE="${c}${part}${C_RST}"
|
|
287
|
+
else
|
|
288
|
+
USAGE="${USAGE}${C_SEP} | ${C_RST}${c}${part}${C_RST}"
|
|
289
|
+
fi
|
|
290
|
+
done <<< "$USAGE_RAW"
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
# --- output ---
|
|
294
|
+
if [ "$MODE" = "--ticket-only" ]; then
|
|
295
|
+
printf '%s\n' "$TICKET"; exit 0
|
|
296
|
+
fi
|
|
297
|
+
if [ "$MODE" = "--context-only" ]; then
|
|
298
|
+
printf '%s\n' "$CTX"; exit 0
|
|
299
|
+
fi
|
|
300
|
+
if [ "$MODE" = "--usage-only" ]; then
|
|
301
|
+
printf '%s\n' "$USAGE"; exit 0
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
# --- aggiornamento trailhead disponibile (dalla cache scritta dall'hook SessionStart) ---
|
|
305
|
+
UPDATE=""
|
|
306
|
+
UPD_FILE="${XDG_CACHE_HOME:-$HOME/.cache}/trailhead/update-check.json"
|
|
307
|
+
if [ -f "$UPD_FILE" ]; then
|
|
308
|
+
UPD_AV="$(jq -r '.updateAvailable // false' "$UPD_FILE" 2>/dev/null)"
|
|
309
|
+
if [ "$UPD_AV" = "true" ]; then
|
|
310
|
+
UPD_LATEST="$(jq -r '.latest // empty' "$UPD_FILE" 2>/dev/null)"
|
|
311
|
+
[ -n "$UPD_LATEST" ] && UPDATE="⬆ trailhead ${UPD_LATEST}"
|
|
312
|
+
fi
|
|
313
|
+
fi
|
|
314
|
+
|
|
315
|
+
SEP="${C_SEP} | ${C_RST}"
|
|
316
|
+
|
|
317
|
+
# riga 1: modello | progetto | branch | usage | contesto | (update disponibile)
|
|
318
|
+
OUT="${C_MODEL}${MODEL}${C_RST}"
|
|
319
|
+
[ -n "$PROJ" ] && OUT="${OUT}${SEP}${C_PROJ}${PROJ}${C_RST}"
|
|
320
|
+
[ -n "$BRANCH" ] && OUT="${OUT}${SEP}${C_BRANCH}${BRANCH}${C_RST}"
|
|
321
|
+
[ -n "$USAGE" ] && OUT="${OUT}${SEP}${USAGE}"
|
|
322
|
+
[ -n "$CTX" ] && OUT="${OUT}${SEP}${C_CTX}${CTX}${C_RST}"
|
|
323
|
+
[ -n "$UPDATE" ] && OUT="${OUT}${SEP}${C_UPDATE}${UPDATE}${C_RST}"
|
|
324
|
+
printf '%s\n' "$OUT"
|
|
325
|
+
|
|
326
|
+
# riga 2: ticket / operazione trailhead in corso (solo se presente)
|
|
327
|
+
if [ -n "$TICKET" ]; then
|
|
328
|
+
# larghezza terminale, per non far wrappare la riga 2 (STATUSLINE_COLS la forza).
|
|
329
|
+
# Un valore assente/0/troppo piccolo e' considerato "sconosciuto" -> provo il metodo dopo.
|
|
330
|
+
valid_cols() { case "$1" in ''|*[!0-9]*) return 1 ;; esac; [ "$1" -ge 10 ]; }
|
|
331
|
+
COLS="${STATUSLINE_COLS:-${COLUMNS:-}}"
|
|
332
|
+
valid_cols "$COLS" || COLS="$(tput cols 2>/dev/null)"
|
|
333
|
+
valid_cols "$COLS" || COLS="$(stty size 2>/dev/null </dev/tty | awk '{print $2}')"
|
|
334
|
+
valid_cols "$COLS" || COLS=0
|
|
335
|
+
L2="▸ ${TICKET}"
|
|
336
|
+
# taglio a COLS-1 (margine per emoji a doppia cella), aggiungendo l'ellissi
|
|
337
|
+
if [ "$COLS" -gt 3 ] 2>/dev/null && [ "${#L2}" -gt "$((COLS-1))" ]; then
|
|
338
|
+
L2="${L2:0:$((COLS-2))}…"
|
|
339
|
+
fi
|
|
340
|
+
printf '%s\n' "${C_TICKET}${L2}${C_RST}"
|
|
341
|
+
fi
|
|
342
|
+
|
|
343
|
+
# exit 0 sempre: Claude Code scarta l'output della statusline se l'exit non e' 0
|
|
344
|
+
exit 0
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# trailhead label guard
|
|
1
|
+
# trailhead label guard: repo-side enforcement.
|
|
2
2
|
# Copy this file to your repository's .github/workflows/ directory.
|
|
3
3
|
# It strips any `trailhead:*` label applied by an actor without write access,
|
|
4
4
|
# so external / triage users (and stray automations) can't inject tickets into
|
|
@@ -51,5 +51,5 @@ jobs:
|
|
|
51
51
|
await github.rest.issues.createComment({
|
|
52
52
|
owner: context.repo.owner, repo: context.repo.repo,
|
|
53
53
|
issue_number: context.payload.issue.number,
|
|
54
|
-
body: `⚠️ \`${label}\` is a **trailhead-reserved** label and was removed automatically
|
|
54
|
+
body: `⚠️ \`${label}\` is a **trailhead-reserved** label and was removed automatically: only repo maintainers (write access) apply \`trailhead:*\` labels. It was applied by @${actor}, who lacks write access. If this issue should join the trailhead map, a maintainer will adopt it.`,
|
|
55
55
|
}).catch(() => {});
|