@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.
Files changed (48) hide show
  1. package/README.md +175 -98
  2. package/bin/trailhead.js +14 -3
  3. package/package.json +2 -4
  4. package/plugins/trailhead/.claude-plugin/plugin.json +2 -4
  5. package/plugins/trailhead/commands/adopt.md +2 -2
  6. package/plugins/trailhead/commands/bug.md +1 -1
  7. package/plugins/trailhead/commands/config.md +1 -1
  8. package/plugins/trailhead/commands/dashboard.md +6 -0
  9. package/plugins/trailhead/commands/grill.md +1 -1
  10. package/plugins/trailhead/commands/idea.md +1 -1
  11. package/plugins/trailhead/commands/inbox.md +1 -1
  12. package/plugins/trailhead/commands/map.md +1 -1
  13. package/plugins/trailhead/commands/new.md +1 -1
  14. package/plugins/trailhead/commands/note.md +1 -1
  15. package/plugins/trailhead/commands/pause.md +1 -1
  16. package/plugins/trailhead/commands/quick.md +6 -0
  17. package/plugins/trailhead/commands/resume.md +1 -1
  18. package/plugins/trailhead/commands/seed.md +2 -2
  19. package/plugins/trailhead/commands/split.md +1 -1
  20. package/plugins/trailhead/commands/ticket.md +1 -1
  21. package/plugins/trailhead/commands/todo.md +2 -2
  22. package/plugins/trailhead/commands/update.md +5 -0
  23. package/plugins/trailhead/commands/whiteboard.md +6 -0
  24. package/plugins/trailhead/commands/work.md +1 -1
  25. package/plugins/trailhead/hooks/hooks.json +14 -0
  26. package/plugins/trailhead/hooks/trailhead-check-update.js +125 -0
  27. package/plugins/trailhead/hooks/trailhead-commit-guard.js +3 -3
  28. package/plugins/trailhead/hooks/trailhead-issue-injection-scanner.js +5 -5
  29. package/plugins/trailhead/hooks/trailhead-secret-guard.js +131 -0
  30. package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +71 -0
  31. package/plugins/trailhead/skills/trailhead/SKILL.md +243 -203
  32. package/plugins/trailhead/skills/trailhead/references/capture.md +24 -0
  33. package/plugins/trailhead/skills/trailhead/references/configuration.md +44 -16
  34. package/plugins/trailhead/skills/trailhead/references/inbox.md +29 -0
  35. package/plugins/trailhead/skills/trailhead/references/teamwork.md +87 -0
  36. package/plugins/trailhead/skills/trailhead/references/techniques/acceptance-testing.md +6 -2
  37. package/plugins/trailhead/skills/trailhead/references/techniques/code-review.md +10 -6
  38. package/plugins/trailhead/skills/trailhead/references/techniques/codebase-map.md +9 -7
  39. package/plugins/trailhead/skills/trailhead/references/techniques/debug.md +13 -9
  40. package/plugins/trailhead/skills/trailhead/references/techniques/domain-vocabulary.md +6 -5
  41. package/plugins/trailhead/skills/trailhead/references/techniques/grilling.md +8 -8
  42. package/plugins/trailhead/skills/trailhead/references/techniques/plan-review.md +14 -5
  43. package/plugins/trailhead/skills/trailhead/references/techniques/prototype.md +10 -8
  44. package/plugins/trailhead/skills/trailhead/references/techniques/research.md +7 -7
  45. package/plugins/trailhead/skills/trailhead/references/techniques/tdd.md +6 -6
  46. package/plugins/trailhead/skills/trailhead/references/updating.md +42 -0
  47. package/plugins/trailhead/templates/statusline-trailhead.sh +344 -0
  48. 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 — sharp enough that the answer is a decision, not a survey. Vague scope breeds rabbit holes; a narrow question is its own leash.
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** — 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.
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 — "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.
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 — add tests after if they earn their keep.
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** — 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).
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** — 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`.
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 — 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. The discipline is absolute: **no implementation code before a failing test exists.**
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 — repo-side enforcement.
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 — 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.`,
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(() => {});