@marcomigozzi/trailhead 0.8.0 → 0.9.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 (41) hide show
  1. package/README.it.md +12 -9
  2. package/README.md +12 -9
  3. package/bin/lib/codex-projection.js +5 -4
  4. package/bin/lib/codex-projection.test.js +3 -1
  5. package/bin/trailhead.js +9 -5
  6. package/bin/trailhead.test.js +152 -0
  7. package/package.json +1 -1
  8. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  9. package/plugins/trailhead/commands/prune.md +6 -0
  10. package/plugins/trailhead/hooks/hooks.json +4 -0
  11. package/plugins/trailhead/hooks/lib/gh-subcommand.js +102 -0
  12. package/plugins/trailhead/hooks/lib/gh-subcommand.test.js +99 -0
  13. package/plugins/trailhead/hooks/trailhead-body-guard.js +221 -0
  14. package/plugins/trailhead/hooks/trailhead-body-guard.test.js +222 -0
  15. package/plugins/trailhead/hooks/trailhead-issue-injection-scanner.js +61 -35
  16. package/plugins/trailhead/hooks/trailhead-issue-injection-scanner.test.js +76 -0
  17. package/plugins/trailhead/hooks/trailhead-secret-guard.js +4 -15
  18. package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +31 -0
  19. package/plugins/trailhead/skills/_shared/choices.md +2 -1
  20. package/plugins/trailhead/skills/_shared/configuration-reference.md +1 -1
  21. package/plugins/trailhead/skills/_shared/configuration.md +1 -1
  22. package/plugins/trailhead/skills/_shared/load-first.md +16 -0
  23. package/plugins/trailhead/skills/_shared/multi-map.md +1 -1
  24. package/plugins/trailhead/skills/_shared/principles.md +1 -1
  25. package/plugins/trailhead/skills/_shared/session-handoff.md +3 -3
  26. package/plugins/trailhead/skills/_shared/substrate-commands.md +41 -5
  27. package/plugins/trailhead/skills/_shared/substrate.md +10 -6
  28. package/plugins/trailhead/skills/_shared/teamwork.md +19 -12
  29. package/plugins/trailhead/skills/_shared/techniques/grilling.md +2 -1
  30. package/plugins/trailhead/skills/trailhead/SKILL.md +4 -15
  31. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -11
  32. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
  33. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +3 -12
  34. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +14 -4
  35. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
  36. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +7 -15
  37. package/plugins/trailhead/skills/trailhead-manage/references/pruning.md +48 -0
  38. package/plugins/trailhead/skills/trailhead-view/SKILL.md +6 -13
  39. package/plugins/trailhead/skills/trailhead-work/SKILL.md +15 -22
  40. package/plugins/trailhead/skills/trailhead-work/references/auto.md +1 -1
  41. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +3 -3
package/README.it.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  🇬🇧 [English](README.md) · 🇮🇹 **Italiano**
8
8
 
9
- [![sito](https://img.shields.io/badge/sito-trailhead.marcomigozzi.it-3C7A5A?style=flat-square)](https://trailhead.marcomigozzi.it) [![CI sito](https://img.shields.io/github/actions/workflow/status/ToRvaLDz/trailhead/site-ci.yml?branch=main&style=flat-square&label=CI%20sito)](https://github.com/ToRvaLDz/trailhead/actions/workflows/site-ci.yml) [![npm](https://img.shields.io/npm/v/@marcomigozzi/trailhead?style=flat-square&label=npm&color=CB3837)](https://www.npmjs.com/package/@marcomigozzi/trailhead) [![license](https://img.shields.io/github/license/ToRvaLDz/trailhead?style=flat-square&color=3C7A5A)](LICENSE) ![host](https://img.shields.io/badge/host-Claude%20Code%20%C2%B7%20Codex-1E2A54?style=flat-square) ![comandi](https://img.shields.io/badge/comandi-21%20verbi-0e8a16?style=flat-square) ![guardrail](https://img.shields.io/badge/guardrail-6%20hook-e8710a?style=flat-square)
9
+ [![sito](https://img.shields.io/badge/sito-trailhead.marcomigozzi.it-3C7A5A?style=flat-square)](https://trailhead.marcomigozzi.it) [![CI sito](https://img.shields.io/github/actions/workflow/status/ToRvaLDz/trailhead/site-ci.yml?branch=main&style=flat-square&label=CI%20sito)](https://github.com/ToRvaLDz/trailhead/actions/workflows/site-ci.yml) [![npm](https://img.shields.io/npm/v/@marcomigozzi/trailhead?style=flat-square&label=npm&color=CB3837)](https://www.npmjs.com/package/@marcomigozzi/trailhead) [![license](https://img.shields.io/github/license/ToRvaLDz/trailhead?style=flat-square&color=3C7A5A)](LICENSE) ![host](https://img.shields.io/badge/host-Claude%20Code%20%C2%B7%20Codex-1E2A54?style=flat-square) ![comandi](https://img.shields.io/badge/comandi-22%20verbi-0e8a16?style=flat-square) ![guardrail](https://img.shields.io/badge/guardrail-7%20hook-e8710a?style=flat-square)
10
10
 
11
11
  </div>
12
12
 
@@ -37,7 +37,7 @@ Le scelte di design che ne derivano:
37
37
  - **Costruito per i team, perché il piano è condiviso.** Il tracker è l'unica fonte di verità, quindi la mappa è un **workspace condiviso**, non il file locale di una singola persona. Molte persone (e i loro agenti) ci lavorano contemporaneamente, e i meccanismi impediscono che si scontrino:
38
38
  - **Claiming.** Prendi un ticket assegnandolo a te stesso; l'assegnatario *è* il lock. Una collisione ti ferma e chiede, invece di sovrascrivere il lavoro di qualcun altro. Chi rivendica un ticket lo possiede dall'inizio alla fine e lo chiude, senza un approvatore separato.
39
39
  - **Una frontier condivisa, un ticket per sessione.** La **frontier** (ticket aperti, non assegnati, non bloccati) è la coda comune di ciò che è prendibile ora; ognuno ne sceglie uno diverso. Risolvere un ticket termina la sessione, così il lavoro resta in porzioni revisionabili, un ticket alla volta.
40
- - **Isolation per sessioni concorrenti su una macchina.** Due agenti che modificano un unico checkout si corrompono a vicenda, quindi la convenzione `isolation:` dà a ogni ticket in corso il proprio **`git worktree`** (o un intero **`clone`** per le app legate a un path che un worktree non riesce a buildare); righe `Scope:` disgiunte lasciano girare in parallelo lavori non sovrapposti e serializzano solo dove c'è vera sovrapposizione.
40
+ - **Isolation per sessioni concorrenti su una macchina.** Due agenti che modificano un unico checkout si corrompono a vicenda, quindi la convenzione `isolation:` (default **`auto`**, che chiede al work-start di ogni ticket se lavorerai più di un ticket alla volta e poi sceglie la modalità giusta in base alla forma del repo) dà a ogni ticket in corso il proprio **`git worktree`** (o un intero **`clone`** per le app legate a un path, o i repo con submodule, che un worktree non riesce a isolare in sicurezza) quando ne lavori più di uno alla volta; righe `Scope:` disgiunte lasciano girare in parallelo lavori non sovrapposti e serializzano solo dove c'è vera sovrapposizione.
41
41
  - **Pause, resume, split.** Qualsiasi ticket può essere messo in **pause** con un commento di checkpoint e **ripreso** più tardi da chiunque (una volta rilasciato), o **splittato** in figli quando supera la dimensione di una sessione, con i blocker ripuntati così la frontier resta onesta.
42
42
  - **Più mappe insieme.** Un repo può portare **più di una mappa attiva** (milestone o feature parallele), ciascuna con la propria frontier con scope, così due persone possono condurre due sforzi sullo stesso repo senza pestarsi i piedi, e puoi parcheggiare una mappa per lavorarne un'altra.
43
43
  - **Esterni e fiducia.** Le issue aperte da altre persone passano attraverso una **inbox** che riformula quelle valide in ticket *sul posto* (mantenendo l'autorialità di chi ha segnalato), mentre una guardia di **trust e provenance** mette in quarantena le issue etichettate `trailhead:*` da fonti non fidate finché un maintainer non le adotta, supportata da un workflow di label-guard lato repo.
@@ -70,7 +70,7 @@ npx @marcomigozzi/trailhead --dir=<path> # punta a una config dir specifica
70
70
  ```
71
71
  Su **Claude Code** copia la skill (+ le sue `references/`), i comandi `/trailhead:*`, gli hook (in `hooks/`, registrati in `settings.json`) incluso un **update check** al SessionStart, e i template di label-guard + statusline, in modo idempotente.
72
72
 
73
- Su **Codex CLI** (`--codex`) proietta una skill nativa Codex invocata come **`$trailhead`** (`$trailhead work`, `$trailhead new "idea"`, …), una skill sottile **`$trailhead-<verb>`** per ogni verbo per la discoverability, le sei guardrail come **hook** nativi di Codex (`~/.codex/hooks.json`, abilitando `features.hooks`), e i pin di modello per-tecnica da **`models.codex.*`** in `~/.codex/agents/`. Serve Codex **0.145.0+** (l'installer fa un gate su `codex --version`). La modalità mockup `claude.ai/design` non è disponibile su Codex; lì l'opzione hosted è la modalità cross-host `design: stitch` (MCP Google Stitch), con il disco locale come ripiego.
73
+ Su **Codex CLI** (`--codex`) proietta una skill nativa Codex invocata come **`$trailhead`** (`$trailhead work`, `$trailhead new "idea"`, …), una skill sottile **`$trailhead-<verb>`** per ogni verbo per la discoverability, le sette guardrail come **hook** nativi di Codex (`~/.codex/hooks.json`, abilitando `features.hooks`), e i pin di modello per-tecnica da **`models.codex.*`** in `~/.codex/agents/`. Serve Codex **0.145.0+** (l'installer fa un gate su `codex --version`). La modalità mockup `claude.ai/design` non è disponibile su Codex; lì l'opzione hosted è la modalità cross-host `design: stitch` (MCP Google Stitch), con il disco locale come ripiego.
74
74
 
75
75
  Ri-esegui l'install per aggiornare, oppure esegui **`/trailhead:update`** (Claude) / **`$trailhead update`** (Codex) da dentro l'agente: rileva come trailhead è stato installato e installa la versione più recente dove è sicuro farlo (un `git pull` per un dev-symlink, una ri-esecuzione `npx` per npm, o `/plugin update` per il plugin). Quando esiste una versione più recente, la statusline mostra un flag `⬆ trailhead <version>` (Claude).
76
76
 
@@ -94,7 +94,7 @@ Man mano che i ticket si risolvono, la fog si dirada: domande che erano troppo v
94
94
  Una mappa ha come scope **un** solo sforzo, quindi la conoscenza che appartiene al *repo* (non a una singola mappa) vive in **anchor issue con scope di repo**, create una volta e condivise da ogni mappa (le Notes di ciascuna mappa le collegano soltanto, così nulla resta orfano quando una mappa finisce):
95
95
 
96
96
  - **`trailhead:codebase`**: la codebase map distillata (architettura, stack, convenzioni, rischi, test/build), scritta una volta all'adopt e aggiornata solo su drift importanti.
97
- - **`trailhead:conventions`**: il **modo di lavorare** del progetto, leggibile da tutti: un piccolo header machine-read che il motore obbedisce (`git: main|pr`, `release: command|auto`, `isolation: none|worktree|clone`) sopra la prosa umana. `/trailhead:adopt` e `:new` lo chiedono all'inizio. `isolation: worktree` dà a ogni ticket in esecuzione il proprio `git worktree` + branch (le sessioni concorrenti su un clone non condividono mai un working tree); `isolation: clone` gli dà invece un clone dedicato, per le app legate a un path che un worktree non riesce a buildare.
97
+ - **`trailhead:conventions`**: il **modo di lavorare** del progetto, leggibile da tutti: un piccolo header machine-read che il motore obbedisce (`git: main|pr`, `release: command|auto`, `isolation: auto|none|worktree|clone`) sopra la prosa umana. `/trailhead:adopt` e `:new` lo chiedono all'inizio. `isolation: auto` (il default) decide per ogni ticket al work-start: chiede se lavorerai più di un ticket alla volta e, se sì, usa un `git worktree` (repo semplice) o un `clone` (superrepo con submodule); altrimenti lavora nel checkout corrente. `isolation: worktree` dà a ogni ticket in esecuzione il proprio `git worktree` + branch (le sessioni concorrenti su un clone non condividono mai un working tree); `isolation: clone` gli dà invece un clone dedicato, per le app legate a un path che un worktree non riesce a buildare e per i superrepo con submodule (dove `worktree` viene scalato a `clone`).
98
98
  - **`trailhead:dashboard`**: l'indice pinnato dell'intera superficie, un link a ogni mappa aperta (con le progress bar native di GitHub), la whiteboard, e conteggi live (inbox, frontier della whiteboard). Aggiornato quando una mappa viene tracciata o esaurita, quando un ticket della whiteboard nasce o si risolve (non ha una progress bar nativa, quindi la dashboard è l'unico posto in cui appare; un ticket nato da `quick`, lavorato e risolto nella stessa sessione, si aggiorna al suo handoff o alla pausa, non alla creazione), e su richiesta via `/trailhead:dashboard`.
99
99
 
100
100
  Questi tre riempiono i **3 slot di issue pinnate** di GitHub, così gli anchor di trailhead di un repo restano a un click di distanza; **le mappe stesse non vengono mai pinnate** (sono indicizzate dalla dashboard, dato che la mappa "attiva" è stato locale per-checkout, non un fatto globale del repo). La *config* del progetto (models, TDD, design…) è ancora una cosa separata, un semplice file `.trailhead/config.json` alla radice del repo, mai in una issue.
@@ -259,11 +259,11 @@ Molte persone (e le loro sessioni di agente) condividono una mappa e la lavorano
259
259
  - **Claim = assegnare a te stesso.** Chi rivendica possiede il ticket dall'inizio alla fine e lo chiude: non c'è approvatore e nessuno da attendere. Su una collisione di claim la sessione si ferma e chiede a te, invece di risolverla silenziosamente.
260
260
  - **Split** un ticket sovradimensionato in figli e supersede l'originale (`/trailhead:split`).
261
261
  - **Pause/resume** tramite un commento di checkpoint `PAUSED`, così qualsiasi sessione può riprendere un ticket.
262
- - **Isola le sessioni concorrenti con i worktree.** Il claiming tiene due sessioni fuori dallo *stesso* ticket, ma due sessioni su ticket *diversi* nello *stesso* clone condividono comunque un working tree. Imposta `isolation: worktree` nell'header delle conventions e ogni ticket in esecuzione gira nel proprio `git worktree` su un branch `trailhead/t<n>`, integrato di nuovo nel trunk al Resolve. Implica un branch per ticket anche sotto `git: main` (git non farà il checkout del trunk due volte), ed è la postura giusta per un **monorepo** (un worktree condivide l'object store, molto più economico di un secondo clone). Un'avvertenza: un worktree isola il *source* ma è un path nuovo senza dipendenze installate, quindi un **package legato a un path** (React Native/Expo, toolchain native, qualsiasi cosa legata a un `node_modules` locale) non buildera da lì. Per questi c'è **`isolation: clone`**: un clone dedicato per-ticket che installa le proprie dipendenze e builda/UATa come l'app vera (più pesante di un worktree, e lo strumento giusto per un submodule Expo/RN). Oppure tieni `isolation: none` e serializza. Altrimenti esegui le sessioni concorrenti in clone separati.
262
+ - **Isola le sessioni concorrenti con i worktree.** Di default (`isolation: auto`) trailhead lo fa per te: al work-start di ogni ticket chiede se ne lavorerai più di uno alla volta e sceglie la modalità in base alla forma del repo. Per forzarla in anticipo, imposta `isolation: worktree` nell'header delle conventions, e ogni ticket in esecuzione gira nel proprio `git worktree` su un branch `trailhead/t<n>`, integrato di nuovo nel trunk al Resolve. Implica un branch per ticket anche sotto `git: main` (git non farà il checkout del trunk due volte), ed è la postura giusta per un **monorepo** (un worktree condivide l'object store, molto più economico di un secondo clone). Un'avvertenza: un worktree isola il *source* ma è un path nuovo senza dipendenze installate, quindi un **package legato a un path** (React Native/Expo, toolchain native, qualsiasi cosa legata a un `node_modules` locale) non buildera da lì. Per questi c'è **`isolation: clone`**: un clone dedicato per-ticket che installa le proprie dipendenze e builda/UATa come l'app vera (più pesante di un worktree, e lo strumento giusto per un submodule Expo/RN). Oppure tieni `isolation: none` e serializza. Altrimenti esegui le sessioni concorrenti in clone separati.
263
263
  - **Un marker session-ticket per la tua statusline.** Quando inizia a lavorare un ticket, trailhead deposita una riga singola, gitignorata, `.trailhead/session-ticket` (`#<n> <title>`) alla radice di lavoro e la pulisce all'handoff. È puramente un hint economico e offline così una statusline (o qualsiasi tool) può mostrare *su quale ticket è questa sessione* senza colpire il tracker; trailhead stesso continua a trattare le Issue come fonte di verità.
264
- - **Ti dà una spinta quando l'isolation non è mai stata scelta.** Se un repo è stato tracciato o adottato prima che impostassi una modalità `isolation:` (quindi assume silenziosamente `none`), la prima volta che vai a lavorare un ticket con un vero rischio di collisione (un altro ticket in corso, o tooling legato a un path nel suo `Scope:`) trailhead fa emergere un suggerimento di una riga della modalità adatta (`worktree` o `clone`) invece di modificare silenziosamente il checkout condiviso. Qualunque cosa decidi, incluso tenere `none`, scrive la chiave `isolation:` nelle conventions così la scelta è registrata e non chiede mai più. Un header che ha già impostato `isolation:` viene lasciato in pace.
264
+ - **Sceglie l'isolation per te di default (`auto`).** Senza un `isolation:` esplicito impostato (il default è `auto`), la prima volta che lavori un ticket trailhead chiede se ne lavorerai più di uno alla volta su questa macchina in questa sessione; se sì usa un `git worktree` (repo semplice) o un `clone` (superrepo con submodule), altrimenti lavora semplicemente nel checkout corrente. Non viene scritto nulla nelle conventions, e puoi comunque forzare `none`/`worktree`/`clone` esplicitamente.
265
265
  - **Dai scope ai ticket per path su un monorepo.** Un ticket può portare una riga `Scope:` che nomina il/i package/dir che tocca: scope disgiunti parallelizzano in sicurezza, e dà scope a build/test e ai commit del worktree limitandoli al package interessato. `/trailhead:adopt` rileva un monorepo e propone sia `isolation: worktree` sia la convenzione `Scope:`. Sotto `isolation: none`, `Scope:` **serializza** anche un'area condivisa: al momento del claim una sessione si trattiene se lo scope di un altro ticket in corso si sovrappone, così due ticket con scope sullo stesso submodule/package non possono girare insieme (è così che tieni due sessioni fuori da un'app legata a un path).
266
- - **I git submodule sono gestiti come repo separati.** Il lavoro che atterra dentro un submodule viene committato *dentro il submodule* più un bump del gitlink nel parent, e (sotto `worktree`) isolato a livello di submodule, non del superproject. Un ticket che copre due submodule resta atomico tramite un singolo commit del parent che bumpa entrambi i gitlink insieme; il suo `Scope:` li nomina entrambi. `/trailhead:adopt` rileva `.gitmodules` e scrive la regola nelle conventions.
266
+ - **I git submodule sono gestiti come repo separati.** Il lavoro che atterra dentro un submodule viene committato *dentro il submodule* (su un branch) e pubblicato prima che un bump del gitlink nel parent lo referenzi. Poiché i worktree del superproject condividono un unico git dir del submodule, un **repo con submodule risolve a `clone`** (sotto il default `auto`, o scalando un `worktree` esplicito), così ogni ticket ottiene uno stato del submodule indipendente; il clone provvede i suoi submodule al setup (`--recurse-submodules`) e il Verify controlla `git status` dentro ogni submodule toccato. Un ticket che copre due submodule resta atomico tramite un singolo commit del parent che bumpa entrambi i gitlink insieme; il suo `Scope:` li nomina entrambi, e lo stesso submodule conta come scope sovrapposto, quindi due ticket su di esso serializzano. `/trailhead:adopt` rileva `.gitmodules` e scrive la regola nelle conventions.
267
267
  - **Lo stato vive sui ticket** (claim, label, commenti; tutti conflict-free); il corpo della mappa è un indice ri-leggibile, non la fonte di verità. Il record durevole di una risoluzione è il commento del ticket stesso.
268
268
  - **Le label sono protette dagli esterni.** Le label `trailhead:*` guidano la mappa, quindi trailhead si fida di una issue etichettata solo quando un collaboratore con accesso in scrittura ha applicato la label e (per un ticket) porta un `Parent:` valido. Qualsiasi altra cosa è messa in quarantena come `trailhead:unverified`, fuori dalla frontier. GitHub già blocca gli utenti senza accesso in scrittura dall'applicare label; contro gli utenti di triage e le automazioni, trailhead può **installare per te la GitHub Action di label guard** (da `templates/trailhead-label-guard.yml`) nella `.github/workflows/` del tuo repo, dove rimuove alla fonte le label `trailhead:*` non autorizzate. Installare un workflow richiede un token con lo scope `workflow`, quindi trailhead esegue `gh auth refresh -s workflow` se il push viene rifiutato.
269
269
  - **Le issue aperte da altri vengono triage-ate, non fidate alla cieca.** `/trailhead:inbox` elenca le issue in ingresso (bug report, richieste, domande che non sono ancora trailhead ticket) e integra quelle valide **sul posto**: riformulandole come ticket, mantenendo l'autorialità di chi ha segnalato, e applicando le label all'adozione. Il resto viene instradato a fog, out-of-scope, duplicato, o needs-info.
@@ -292,6 +292,8 @@ Tre layer: **il più vicino vince**, chiave per chiave (una chiave non impostata
292
292
 
293
293
  `/trailhead:config` esegue un **setup guidato, a menu**: scegli lo scope, poi percorri ogni impostazione (🌐 ticket language · 🧠 models · 🎨 design + approval · 🧪 TDD · 🖥️ acceptance testing · 🧑‍⚖️ plan review · 📊 statusline) come un menu con icone; niente editing manuale del JSON. Ogni passo è chiesto (nessuno saltato), e i **modelli plan ed execute sono sempre due scelte separate e version-pinned**. `config get` stampa la config effettiva unita; `config set <key> <value>` scrive una chiave.
294
294
 
295
+ `/trailhead:prune [map]` recupera gli slot di sub-issue di una mappa sotto il cap di 100 di GitHub (elimina gli edge chiusi/orfani, mantiene ogni riferimento). Offerto automaticamente quando una mappa è vicina o al cap, ed eseguibile anche su richiesta.
296
+
295
297
  Il passo **📊 statusline** offre di installare la status bar di trailhead per Claude Code: una riga con **model · project · branch · plan usage (`5h %` · reset · `7d %`) · una barra della context-window**, più una **seconda riga con il ticket attivo** (`▸ #N Title`) ogni volta che ne stai lavorando uno, e un flag `⬆ trailhead <version>` quando è disponibile un trailhead più recente (esegui `/trailhead:update`). Il project è sempre il nome del repo principale anche da un checkout isolato, e il branch porta un tag `(WT)` in un worktree o `(C)` in un clone per-ticket (niente sul checkout originale). È un'impostazione globale di Claude Code; se già usi una statusline (es. `ccstatusline`) il setup chiede prima di rimpiazzarla, e lo script espone anche i segmenti `--ticket-only` / `--context-only` / `--usage-only` da inserire in un tool esistente.
296
298
 
297
299
  **Offerta al primo uso (una volta per progetto).** Non devi andarla a cercare: la prima volta che configuri un progetto (alla fine di `/trailhead:new`/`:adopt`, o su un `/trailhead` nudo quando esiste una mappa ma non una config) trailhead offre di eseguire il setup guidato, o di continuare sui default. Chiede **solo una volta**: configurare scrive `.trailhead/config.json`, e "continua sui default" scrive un `{}` vuoto. Una volta che quel file esiste non viene più offerto, e non interrompe mai a metà ticket.
@@ -339,16 +341,17 @@ Se l'MCP della modalità hosted non è connesso, trailhead **offre di installarl
339
341
 
340
342
  ## 🪝 Hooks
341
343
 
342
- Oltre alle istruzioni della skill, trailhead include sei hook propri (in `hooks/`, self-contained via `${CLAUDE_PLUGIN_ROOT}`) che *impongono* le parti della disciplina che non ci si dovrebbe fidare che il modello ricordi:
344
+ Oltre alle istruzioni della skill, trailhead include sette hook propri (in `hooks/`, self-contained via `${CLAUDE_PLUGIN_ROOT}`) che *impongono* le parti della disciplina che non ci si dovrebbe fidare che il modello ricordi:
343
345
 
344
346
  - **Commit guard** (`PreToolUse` su `git commit`): blocca in modo netto un commit il cui subject non è [Conventional Commits](https://www.conventionalcommits.org), e blocca in modo netto qualsiasi trailer `Co-Authored-By`. Questo rispecchia la validazione dei commit di GSD.
345
347
  - **Secret guard** (`PreToolUse` sulle scritture `gh` di issue/PR): trailhead pubblica molto sul tracker (corpi dei ticket, commenti del motore, issue codebase/conventions, risoluzioni). Questo scansiona ciò che una scrittura `gh issue`/`gh pr`/`gh api` sta per pubblicare (l'inline `--body`, un heredoc, o un `--body-file`) e lo **blocca in modo netto** se corrisponde a un pattern di credenziale (chiavi private, token GitHub/AWS/OpenAI/Slack/Google/Stripe, JWT, o un `password`/`secret`/`token=…` hardcoded). Il blocco è un **segnale di pulisci-e-riprova, non un vicolo cieco**: trailhead redige il valore segnalato sul posto (`<REDACTED>` o un riferimento a env-var) e ripubblica automaticamente, così il contenuto pulito atterra comunque: il segreto semplicemente non raggiunge mai il tracker. Resta un *blocco* fail-safe anziché una silenziosa auto-riscrittura di proposito: un blocco non fa mai leak, mentre una redazione che silenziosamente non è stata applicata sì. Non scansiona mai le letture, solo le scritture in uscita.
346
348
  - **Install guard** (`PreToolUse` sulle installazioni di pacchetti): una barriera anti-slopsquatting. Blocca in modo netto un'installazione da registry pubblico (npm / PyPI / crates.io / Go / RubyGems) finché il pacchetto non è verificato (similarità typosquat a nomi popolari, età / prima apparizione, numero di download, metadati del repository presenti e coerenti); una volta verificato, ri-esegui il comando esatto con il prefisso `TRAILHEAD_VERIFIED_INSTALL=1` per procedere. Il motore Execute / Fix porta lo stesso block-and-ask.
347
349
  - **Search guard** (`PreToolUse` su `Bash`): un backstop strutturale per la regola di igiene dei comandi di ricerca (mai `cd` seguito da una lettura/ricerca su percorso relativo). Blocca in modo netto un singolo comando Bash che sia insieme un cambio di directory (`cd`/`pushd`/`env -C`) e, in un'istruzione unita o successiva, una lettura/ricerca su percorso relativo (`grep`/`rg`/`ag`/`cat`/`head`/`tail`/`wc`/`nl`/`sed`/`awk`/`less`/`more`, o `git grep`/`log`/`show`/`diff` senza `-C`), così la forma che fa scattare una regola di deny su `Read()` sotto bypass permissions (ripiegando su un prompt di approvazione manuale invece di girare headless) non viene mai eseguita, anziché essere solo scoraggiata nella prosa dell'agente.
350
+ - **Body guard** (`PreToolUse` sulle scritture `gh` di body issue/PR): blocca in modo netto una scrittura `gh issue edit` / `gh pr edit` / `gh api` che imposterebbe il body di un issue a vuoto o solo spazi, così un read-modify-write la cui lettura è tornata con un body vuoto/degradato (es. un HTTP 5xx transitorio su `gh issue view --json body`) non può mai cancellare il body esistente.
348
351
  - **Secret-read guard** (`PreToolUse` su `Read|Bash`): blocca in modo netto la lettura di `.env`/`.env.*`/`.secrets`, sia tramite il tool Read sia tramite un comando Bash (un operando file, il valore di un'opzione, o un target di redirect `<`). È una decisione `deny` a livello di hook anziché una regola di permesso su `Read()`, quindi i segreti restano illeggibili senza attivare il prompt di approvazione `cd-compound-read` di Claude Code 2.1.259.
349
352
  - **Issue injection scanner** (`PostToolUse` sulle letture `gh`): trailhead legge testo di issue/PR/commenti scritto da chiunque abbia accesso al repo, cioè input non fidato. Quando quel testo contiene frasi di prompt-injection, l'hook inietta un avviso che ricorda all'agente di trattarlo come dati, mai come comandi. Solo advisory: non blocca mai.
350
353
 
351
- Tutti e sei sono crash-safe (qualsiasi errore → allow) e attivi ogni volta che il plugin è installato. I guard di commit, secret, install, search e secret-read quindi si applicano in **ogni** repo, non solo nei progetti trailhead: intenzionale, dato che i conventional commit, niente `Co-Authored-By`, nessun segreto leakato, nessuna installazione non verificata, niente `cd`-poi-lettura-relativa, e niente `.env`/`.secrets` leggibili sono regole permanenti di trailhead. Per fare opt-out, disabilita gli hook del plugin nelle impostazioni di Claude Code.
354
+ Tutti e sette sono crash-safe (qualsiasi errore → allow) e attivi ogni volta che il plugin è installato. I guard di commit, secret, install, search, body e secret-read quindi si applicano in **ogni** repo, non solo nei progetti trailhead: intenzionale, dato che i conventional commit, niente `Co-Authored-By`, nessun segreto leakato, nessuna installazione non verificata, niente `cd`-poi-lettura-relativa, nessun body issue cancellato, e niente `.env`/`.secrets` leggibili sono regole permanenti di trailhead. Per fare opt-out, disabilita gli hook del plugin nelle impostazioni di Claude Code.
352
355
 
353
356
  ---
354
357
 
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  🇬🇧 **English** · 🇮🇹 [Italiano](README.it.md)
8
8
 
9
- [![website](https://img.shields.io/badge/website-trailhead.marcomigozzi.it-3C7A5A?style=flat-square)](https://trailhead.marcomigozzi.it) [![site CI](https://img.shields.io/github/actions/workflow/status/ToRvaLDz/trailhead/site-ci.yml?branch=main&style=flat-square&label=site%20CI)](https://github.com/ToRvaLDz/trailhead/actions/workflows/site-ci.yml) [![npm](https://img.shields.io/npm/v/@marcomigozzi/trailhead?style=flat-square&label=npm&color=CB3837)](https://www.npmjs.com/package/@marcomigozzi/trailhead) [![license](https://img.shields.io/github/license/ToRvaLDz/trailhead?style=flat-square&color=3C7A5A)](LICENSE) ![hosts](https://img.shields.io/badge/hosts-Claude%20Code%20%C2%B7%20Codex-1E2A54?style=flat-square) ![commands](https://img.shields.io/badge/commands-21%20verbs-0e8a16?style=flat-square) ![guardrails](https://img.shields.io/badge/guardrails-6%20hooks-e8710a?style=flat-square)
9
+ [![website](https://img.shields.io/badge/website-trailhead.marcomigozzi.it-3C7A5A?style=flat-square)](https://trailhead.marcomigozzi.it) [![site CI](https://img.shields.io/github/actions/workflow/status/ToRvaLDz/trailhead/site-ci.yml?branch=main&style=flat-square&label=site%20CI)](https://github.com/ToRvaLDz/trailhead/actions/workflows/site-ci.yml) [![npm](https://img.shields.io/npm/v/@marcomigozzi/trailhead?style=flat-square&label=npm&color=CB3837)](https://www.npmjs.com/package/@marcomigozzi/trailhead) [![license](https://img.shields.io/github/license/ToRvaLDz/trailhead?style=flat-square&color=3C7A5A)](LICENSE) ![hosts](https://img.shields.io/badge/hosts-Claude%20Code%20%C2%B7%20Codex-1E2A54?style=flat-square) ![commands](https://img.shields.io/badge/commands-22%20verbs-0e8a16?style=flat-square) ![guardrails](https://img.shields.io/badge/guardrails-7%20hooks-e8710a?style=flat-square)
10
10
 
11
11
  </div>
12
12
 
@@ -37,7 +37,7 @@ The design choices that fall out of it:
37
37
  - **Built for teams, because the plan is shared.** The tracker is the single source of truth, so the map is a **shared workspace**, not one person's local file. Many people (and their agents) work it at once, and the mechanics keep them from colliding:
38
38
  - **Claiming.** You take a ticket by assigning it to yourself; the assignee *is* the lock. A collision stops you and asks, rather than clobbering someone else's work. Whoever claims a ticket owns it end to end and closes it, no separate approver.
39
39
  - **A shared frontier, one ticket per session.** The **frontier** (open, unassigned, unblocked tickets) is the common queue of what's takeable now; each person picks a different one. Resolving a ticket ends the session, so work stays in reviewable, single-ticket chunks.
40
- - **Isolation for concurrent sessions on one machine.** Two agents editing one checkout corrupt each other, so the `isolation:` convention gives each in-flight ticket its own **`git worktree`** (or a full **`clone`** for path-bound apps a worktree can't build); disjoint `Scope:` lines let non-overlapping work run in parallel and serialise only where it truly overlaps.
40
+ - **Isolation for concurrent sessions on one machine.** Two agents editing one checkout corrupt each other, so the `isolation:` convention (default **`auto`**, which asks at each ticket's work-start whether you'll work more than one at once and then picks the right mode by repo shape) gives each in-flight ticket its own **`git worktree`** (or a full **`clone`** for path-bound apps, or repos with submodules, a worktree can't safely isolate) when you run more than one at once; disjoint `Scope:` lines let non-overlapping work run in parallel and serialise only where it truly overlaps.
41
41
  - **Pause, resume, split.** Any ticket can be **paused** with a checkpoint comment and **resumed** later by anyone (once released), or **split** into children when it outgrows a session, with blockers re-pointed so the frontier stays honest.
42
42
  - **Several maps at once.** A repo can carry **more than one live map** (parallel milestones or features), each with its own scoped frontier, so two people can drive two efforts on the same repo without stepping on each other, and you can park one map to work another.
43
43
  - **Outsiders and trust.** Issues opened by other people flow through an **inbox** that reframes the worthwhile ones into tickets *in place* (keeping the reporter's authorship), while a **trust and provenance** guard quarantines `trailhead:*`-labelled issues from untrusted sources until a maintainer adopts them, backed by a repo-side label-guard workflow.
@@ -70,7 +70,7 @@ npx @marcomigozzi/trailhead --dir=<path> # target a specific config dir
70
70
  ```
71
71
  On **Claude Code** it copies the skill (+ its `references/`), the `/trailhead:*` commands, the hooks (into `hooks/`, registered in `settings.json`) including a SessionStart **update check**, and the label-guard + statusline templates, idempotently.
72
72
 
73
- On **Codex CLI** (`--codex`) it projects a native Codex skill invoked as **`$trailhead`** (`$trailhead work`, `$trailhead new "idea"`, …), one thin **`$trailhead-<verb>`** skill per verb for discoverability, the six guardrails as native Codex **hooks** (`~/.codex/hooks.json`, enabling `features.hooks`), and per-technique subagent model pins from **`models.codex.*`** into `~/.codex/agents/`. Codex **0.145.0+** is required (the installer gates on `codex --version`). The `claude.ai/design` mockup mode isn't available on Codex; the cross-host `design: stitch` mode (Google Stitch MCP) is the hosted option there, with local disk as the fallback.
73
+ On **Codex CLI** (`--codex`) it projects a native Codex skill invoked as **`$trailhead`** (`$trailhead work`, `$trailhead new "idea"`, …), one thin **`$trailhead-<verb>`** skill per verb for discoverability, the seven guardrails as native Codex **hooks** (`~/.codex/hooks.json`, enabling `features.hooks`), and per-technique subagent model pins from **`models.codex.*`** into `~/.codex/agents/`. Codex **0.145.0+** is required (the installer gates on `codex --version`). The `claude.ai/design` mockup mode isn't available on Codex; the cross-host `design: stitch` mode (Google Stitch MCP) is the hosted option there, with local disk as the fallback.
74
74
 
75
75
  Re-run the install to update, or run **`/trailhead:update`** (Claude) / **`$trailhead update`** (Codex) from inside the agent: it detects how trailhead was installed and installs the newer version where that is safe (a `git pull` for a dev-symlink, an `npx` re-run for npm, or `/plugin update` for the plugin). When a newer version exists, the statusline shows a `⬆ trailhead <version>` flag (Claude).
76
76
 
@@ -94,7 +94,7 @@ As tickets resolve, the fog clears: questions that were too vague to phrase beco
94
94
  A map is scoped to **one** effort, so knowledge that belongs to the *repo* (not to any single map) lives in **repo-scoped anchor issues**, created once and shared by every map (each map's Notes just links them, so nothing is stranded when a map finishes):
95
95
 
96
96
  - **`trailhead:codebase`**: the distilled codebase map (architecture, stack, conventions, risks, test/build), written once at adopt and refreshed only on major drift.
97
- - **`trailhead:conventions`**: the project's **way of working**, readable by everyone: a small machine-read header the engine obeys (`git: main|pr`, `release: command|auto`, `isolation: none|worktree|clone`) over human prose. `/trailhead:adopt` and `:new` ask for it up front. `isolation: worktree` gives each executing ticket its own `git worktree` + branch (concurrent sessions on one clone never share a working tree); `isolation: clone` gives it a dedicated clone instead, for path-bound apps a worktree can't build.
97
+ - **`trailhead:conventions`**: the project's **way of working**, readable by everyone: a small machine-read header the engine obeys (`git: main|pr`, `release: command|auto`, `isolation: auto|none|worktree|clone`) over human prose. `/trailhead:adopt` and `:new` ask for it up front. `isolation: auto` (the default) decides per ticket at work-start: it asks whether you'll work more than one ticket at once and, if so, uses a `git worktree` (plain repo) or a `clone` (submodule superrepo); otherwise it works in the current checkout. `isolation: worktree` gives each executing ticket its own `git worktree` + branch (concurrent sessions on one clone never share a working tree); `isolation: clone` gives it a dedicated clone instead, for path-bound apps a worktree can't build and for submodule-bearing superrepos (where `worktree` is escalated to `clone`).
98
98
  - **`trailhead:dashboard`**: the pinned index of the whole surface, a link to every open map (with GitHub's native progress bars), the whiteboard, and live counts (inbox, whiteboard frontier). Refreshed when a map is charted or exhausted, when a whiteboard ticket is born or resolved (it has no native progress bar, so the dashboard is the only place it shows; a `quick`-born ticket, worked and resolved in one sitting, refreshes at its handoff or pause, not at creation), and on demand via `/trailhead:dashboard`.
99
99
 
100
100
  These three fill GitHub's **3 pinned-issue slots**, so a repo's trailhead anchors stay one click away; **maps themselves are never pinned** (they're indexed by the dashboard instead, since the "active" map is per-checkout local state, not a repo-global fact). Project *config* (models, TDD, design…) is separate again, a plain `.trailhead/config.json` file at the repo root, never in an issue.
@@ -259,11 +259,11 @@ Many people (and their agent sessions) share one map and work it concurrently:
259
259
  - **Claim = assign to yourself.** The claimer owns the ticket end to end and closes it: there is no approver and no one to wait on. On a claim collision the session stops and asks you, rather than resolving it silently.
260
260
  - **Split** an oversized ticket into children and supersede the original (`/trailhead:split`).
261
261
  - **Pause/resume** via a `PAUSED` checkpoint comment, so any session can pick a ticket back up.
262
- - **Isolate concurrent sessions with worktrees.** Claiming keeps two sessions off the *same* ticket, but two sessions on *different* tickets in the *same* clone still share one working tree. Set `isolation: worktree` in the conventions header and each executing ticket runs in its own `git worktree` on a `trailhead/t<n>` branch, integrated back to the trunk at Resolve. It implies a branch per ticket even under `git: main` (git won't check the trunk out twice), and it is the right posture for a **monorepo** (a worktree shares the object store, far cheaper than a second clone). One caveat: a worktree isolates *source* but is a fresh path with no installed deps, so a **path-bound package** (React Native/Expo, native toolchains, anything tied to a local `node_modules`) won't build from it. For those there's **`isolation: clone`**: a dedicated per-ticket clone that installs its own deps and builds/UATs like the real app (heavier than a worktree, and the right tool for an Expo/RN submodule). Or keep `isolation: none` and serialize. Otherwise run concurrent sessions in separate clones.
262
+ - **Isolate concurrent sessions with worktrees.** By default (`isolation: auto`) trailhead does this for you: at each ticket's work-start it asks whether you'll run more than one at once and picks the mode by repo shape. To force it eagerly, set `isolation: worktree` in the conventions header, and each executing ticket runs in its own `git worktree` on a `trailhead/t<n>` branch, integrated back to the trunk at Resolve. It implies a branch per ticket even under `git: main` (git won't check the trunk out twice), and it is the right posture for a **monorepo** (a worktree shares the object store, far cheaper than a second clone). One caveat: a worktree isolates *source* but is a fresh path with no installed deps, so a **path-bound package** (React Native/Expo, native toolchains, anything tied to a local `node_modules`) won't build from it. For those there's **`isolation: clone`**: a dedicated per-ticket clone that installs its own deps and builds/UATs like the real app (heavier than a worktree, and the right tool for an Expo/RN submodule). Or keep `isolation: none` and serialize. Otherwise run concurrent sessions in separate clones.
263
263
  - **A session-ticket marker for your statusline.** When it starts working a ticket, trailhead drops a one-line, gitignored `.trailhead/session-ticket` (`#<n> <title>`) at the working root and clears it at handoff. It's purely a cheap, offline hint so a statusline (or any tool) can show *which ticket this session is on* without hitting the tracker; trailhead itself still treats the Issues as the source of truth.
264
- - **It nudges you when isolation was never chosen.** If a repo was charted or adopted before you set an `isolation:` mode (so it silently defaults to `none`), the first time you go to work a ticket with a real collision risk (another ticket in progress, or path-bound tooling in its `Scope:`) trailhead surfaces a one-line suggestion of the fitting mode (`worktree` or `clone`) instead of quietly editing the shared checkout. Whatever you decide, including keeping `none`, it writes the `isolation:` key into the conventions so the choice is recorded and it never asks again. A header that already set `isolation:` is left alone.
264
+ - **It picks isolation for you by default (`auto`).** With no explicit `isolation:` set (the default is `auto`), the first time you work a ticket trailhead asks whether you'll work more than one at once on this machine this session; if so it uses a `git worktree` (plain repo) or a `clone` (submodule superrepo), otherwise it just works in the current checkout. Nothing is written into the conventions, and you can still force `none`/`worktree`/`clone` explicitly.
265
265
  - **Scope tickets by path on a monorepo.** A ticket can carry a `Scope:` line naming the package/dir(s) it touches: disjoint scopes parallelize safely, and it scopes build/test and the worktree's commits to the affected package. `/trailhead:adopt` detects a monorepo and proposes both `isolation: worktree` and the `Scope:` convention. Under `isolation: none`, `Scope:` also **serializes** a shared area: at claim time a session holds off if another in-progress ticket's scope overlaps, so two tickets scoped to the same submodule/package can't run at once (how you keep two sessions off one path-bound app).
266
- - **Git submodules are handled as separate repos.** Work that lands inside a submodule is committed *inside the submodule* plus a gitlink bump in the parent, and (under `worktree`) isolated at the submodule, not the superproject. A ticket spanning two submodules stays atomic via a single parent commit that bumps both gitlinks together; its `Scope:` names both. `/trailhead:adopt` detects `.gitmodules` and writes the rule into the conventions.
266
+ - **Git submodules are handled as separate repos.** Work that lands inside a submodule is committed *inside the submodule* (on a branch) and published before a gitlink bump in the parent points at it. Because superproject worktrees share one submodule git dir, a **submodule-bearing repo resolves to `clone`** (under the default `auto`, or by escalating an explicit `worktree`), so each ticket gets independent submodule state; the clone provisions its submodules at setup (`--recurse-submodules`) and Verify checks `git status` inside each touched submodule. A ticket spanning two submodules stays atomic via a single parent commit that bumps both gitlinks together; its `Scope:` names both, and the same submodule counts as overlapping scope, so two tickets on it serialise. `/trailhead:adopt` detects `.gitmodules` and writes the rule into the conventions.
267
267
  - **State lives on the tickets** (claim, labels, comments; all conflict-free); the map body is a re-readable index, not the source of truth. The durable record of a resolution is the ticket's own comment.
268
268
  - **Labels are protected from outsiders.** `trailhead:*` labels drive the map, so trailhead trusts a labelled issue only when a write-access collaborator applied the label and (for a ticket) it carries a valid `Parent:`. Anything else is quarantined as `trailhead:unverified`, off the frontier. GitHub already blocks non-write users from labelling; against triage users and automations, trailhead can **install the label guard GitHub Action for you** (from `templates/trailhead-label-guard.yml`) into your repo's `.github/workflows/`, where it strips unauthorized `trailhead:*` labels at the source. Installing a workflow needs a token with the `workflow` scope, so trailhead runs `gh auth refresh -s workflow` if the push is rejected.
269
269
  - **Issues opened by others are triaged, not trusted blindly.** `/trailhead:inbox` lists inbound issues (bug reports, requests, questions that aren't yet trailhead tickets) and integrates the worthwhile ones **in place**: reframing them as tickets, keeping the reporter's authorship, and applying the labels on adoption. The rest routes to fog, out-of-scope, duplicate, or needs-info.
@@ -292,6 +292,8 @@ Three layers: **nearest wins**, key by key (a key unset at one layer inherits th
292
292
 
293
293
  `/trailhead:config` runs a **guided, menu-driven setup**: pick the scope, then walk each setting (🌐 ticket language · 🧠 models · 🎨 design + approval · 🧪 TDD · 🖥️ acceptance testing · 🧑‍⚖️ plan review · 📊 statusline) as an icon-labelled menu; no hand-editing JSON. Every step is asked (none skipped), and **plan and execute models are always two separate, version-pinned choices**. `config get` prints the effective merged config; `config set <key> <value>` writes one key.
294
294
 
295
+ `/trailhead:prune [map]` reclaims a map's sub-issue slots under GitHub's 100-cap (drops closed/orphan edges, keeps every reference). Offered automatically when a map is near or at the cap, and runnable on demand.
296
+
295
297
  The **📊 statusline** step offers to install trailhead's Claude Code status bar: one line with **model · project · branch · plan usage (`5h %` · reset · `7d %`) · a context-window bar**, plus a **second line with the active ticket** (`▸ #N Title`) whenever you're working one, and a `⬆ trailhead <version>` flag when a newer trailhead is available (run `/trailhead:update`). The project is always the main repo's name even from an isolated checkout, and the branch carries a `(WT)` tag in a worktree or `(C)` in a per-ticket clone (nothing on the original checkout). It's a global Claude Code setting; if you already run a statusline (e.g. `ccstatusline`) the setup asks before replacing it, and the script also exposes `--ticket-only` / `--context-only` / `--usage-only` segments to slot into an existing tool.
296
298
 
297
299
  **First-use offer (once per project).** You don't have to seek this out: the first time you set a project up (at the end of `/trailhead:new`/`:adopt`, or on a bare `/trailhead` when a map exists but no config does) trailhead offers to run the guided setup, or to continue on defaults. It asks **only once**: configuring writes `.trailhead/config.json`, and "continue on defaults" writes an empty `{}`. Once that file exists it's never offered again, and it never interrupts mid-ticket.
@@ -339,16 +341,17 @@ If a hosted mode's MCP isn't connected, trailhead **offers to install it once**
339
341
 
340
342
  ## 🪝 Hooks
341
343
 
342
- Beyond the skill's instructions, trailhead ships six of its own hooks (in `hooks/`, self-contained via `${CLAUDE_PLUGIN_ROOT}`) that *enforce* the parts of the discipline the model shouldn't be trusted to remember:
344
+ Beyond the skill's instructions, trailhead ships seven of its own hooks (in `hooks/`, self-contained via `${CLAUDE_PLUGIN_ROOT}`) that *enforce* the parts of the discipline the model shouldn't be trusted to remember:
343
345
 
344
346
  - **Commit guard** (`PreToolUse` on `git commit`): hard-blocks a commit whose subject isn't [Conventional Commits](https://www.conventionalcommits.org), and hard-blocks any `Co-Authored-By` trailer. This mirrors GSD's commit validation.
345
347
  - **Secret guard** (`PreToolUse` on `gh` issue/PR writes): trailhead posts a lot to the tracker (ticket bodies, engine comments, codebase/conventions issues, resolutions). This scans what a `gh issue`/`gh pr`/`gh api` write is about to post (the inline `--body`, a heredoc, or a `--body-file`) and **hard-blocks** it if it matches a credential pattern (private keys, GitHub/AWS/OpenAI/Slack/Google/Stripe tokens, JWTs, or a hardcoded `password`/`secret`/`token=…`). The block is a **clean-and-retry cue, not a dead end**: trailhead redacts the flagged value in place (`<REDACTED>` or an env-var reference) and re-posts automatically, so the cleaned content still lands: the secret just never reaches the tracker. It stays a fail-safe *block* rather than a silent auto-rewrite on purpose: a block never leaks, whereas a redaction that quietly failed to apply would. It never scans reads, only outbound writes.
346
348
  - **Install guard** (`PreToolUse` on package installs): a slopsquatting barrier. It hard-blocks a named public-registry install (npm / PyPI / crates.io / Go / RubyGems) until the package is vetted (typosquat similarity to popular names, age / first-seen date, download counts, repository metadata present and matching); once vetted, re-run the exact command prefixed with `TRAILHEAD_VERIFIED_INSTALL=1` to proceed. The Execute / Fix engine carries the same block-and-ask.
347
349
  - **Search guard** (`PreToolUse` on `Bash`): structural backstop for the search-command-hygiene rule (never `cd` then read/search a relative path). It hard-blocks a single Bash command that both changes directory (`cd`/`pushd`/`env -C`) and, in a joined or later statement, reads/searches a relative path (`grep`/`rg`/`ag`/`cat`/`head`/`tail`/`wc`/`nl`/`sed`/`awk`/`less`/`more`, or `git grep`/`log`/`show`/`diff` without a `-C`), so the shape that trips a `Read()` deny rule under bypass permissions (falling back to a manual approval prompt instead of running headless) never runs at all, rather than merely being discouraged in the agent's own prose.
350
+ - **Body guard** (`PreToolUse` on `gh` issue/PR body writes): hard-blocks a `gh issue edit` / `gh pr edit` / `gh api` write that would set an issue body to empty or whitespace-only, so a read-modify-write whose read returned an empty/degraded body (e.g. a transient HTTP 5xx on `gh issue view --json body`) can never clobber the existing body.
348
351
  - **Secret-read guard** (`PreToolUse` on `Read|Bash`): denies reads of `.env`/`.env.*`/`.secrets`, whether via the Read tool or a Bash command (a file operand, an option value, or a `<` redirect target). This is a hook `deny` decision rather than a `Read()` permission rule, so secrets stay unreadable without arming Claude Code 2.1.259's `cd-compound-read` approval prompt.
349
352
  - **Issue injection scanner** (`PostToolUse` on `gh` reads): trailhead reads issue/PR/comment text written by anyone with repo access, i.e. untrusted input. When that text contains prompt-injection phrases, the hook injects an advisory reminding the agent to treat it as data, never as commands. Advisory only: it never blocks.
350
353
 
351
- All six are crash-safe (any error → allow) and active whenever the plugin is installed. The commit, secret, install, search, and secret-read guards therefore apply in **every** repo, not only trailhead projects: intentional, since conventional commits, no `Co-Authored-By`, no leaked secrets, no unvetted installs, no `cd`-then-relative-read, and no readable `.env`/`.secrets` are trailhead's standing rules. To opt out, disable the plugin's hooks in your Claude Code settings.
354
+ All seven are crash-safe (any error → allow) and active whenever the plugin is installed. The commit, secret, install, search, body, and secret-read guards therefore apply in **every** repo, not only trailhead projects: intentional, since conventional commits, no `Co-Authored-By`, no leaked secrets, no unvetted installs, no `cd`-then-relative-read, no clobbered issue bodies, and no readable `.env`/`.secrets` are trailhead's standing rules. To opt out, disable the plugin's hooks in your Claude Code settings.
352
355
 
353
356
  ---
354
357
 
@@ -109,7 +109,7 @@ Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_inp
109
109
  The engine's \`/clear\` is Codex's \`/new\` (start a fresh session). Command references in a handoff use the skill form (\`$trailhead work <n>\`).
110
110
 
111
111
  ## F. Lifecycle hooks (native Codex hooks)
112
- Codex has a hook bus, so trailhead's guardrails run as **real Codex hooks**, not degraded prose. The installer registers them in \`~/.codex/hooks.json\`: commit-guard, secret-guard, install-guard and search-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs, secret-read-guard as a \`PreToolUse\` (matcher \`Read|Bash\`) hook that denies reads of \`.env\`/\`.env.*\`/\`.secrets\`, the injection-scanner as a \`PostToolUse\` (matcher \`Bash\`) advisory, and check-update as a \`SessionStart\` hook. On Codex, file reads happen through shell commands rather than a native Read-tool event, so the secret-read guard's enforcement here runs via its **Bash** arm (protection is not lost); the \`Read\` half of its matcher is a Claude-Code-native path, registered on Codex too only so it fires without changes if a future Codex version adds an equivalent event. They use the same wire format they use on Claude Code (JSON on stdin, a \`decision\`/\`permissionDecision\` verdict on stdout), which Codex accepts. The host-independent git \`commit-msg\` hook is still installed at repo first-use as well (defence in depth), exactly as on Claude. Codex gates hooks behind \`features.hooks = true\` and reviews them for trust on first start: the installer enables the flag, and Codex will ask you to trust trailhead's hooks the next time it starts.
112
+ Codex has a hook bus, so trailhead's guardrails run as **real Codex hooks**, not degraded prose. The installer registers them in \`~/.codex/hooks.json\`: commit-guard, secret-guard, install-guard, search-guard and body-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs, secret-read-guard as a \`PreToolUse\` (matcher \`Read|Bash\`) hook that denies reads of \`.env\`/\`.env.*\`/\`.secrets\`, the injection-scanner as a \`PostToolUse\` (matcher \`Bash\`) advisory, and check-update as a \`SessionStart\` hook. On Codex, file reads happen through shell commands rather than a native Read-tool event, so the secret-read guard's enforcement here runs via its **Bash** arm (protection is not lost); the \`Read\` half of its matcher is a Claude-Code-native path, registered on Codex too only so it fires without changes if a future Codex version adds an equivalent event. They use the same wire format they use on Claude Code (JSON on stdin, a \`decision\`/\`permissionDecision\` verdict on stdout), which Codex accepts. The host-independent git \`commit-msg\` hook is still installed at repo first-use as well (defence in depth), exactly as on Claude. Codex gates hooks behind \`features.hooks = true\` and reviews them for trust on first start: the installer enables the flag, and Codex will ask you to trust trailhead's hooks the next time it starts.
113
113
 
114
114
  ## G. Design / UI mockups -> local disk
115
115
  The \`claude.ai/design\` mockup mode (DesignSync: the \`claude_design\` MCP + the \`/design-sync\` skill) does not exist on Codex, so it **collapses to local-disk mockups** here: whatever \`config.design\` says, the Prototype technique's \`disk\` path is the only one available. Produce a throwaway static HTML mockup next to the code, link it from the ticket, and never attempt DesignSync / claude.ai/design on Codex (there is no \`claude_design\` MCP or \`/design-sync\` skill to call). \`config.design.approval\` still applies unchanged: \`explicit\` waits for the user's go-ahead before any UI code, \`auto\` surfaces the mockup and proceeds.
@@ -354,11 +354,11 @@ function codexVerbSkillPlan(codexHome, verbs) {
354
354
  }
355
355
 
356
356
  // --- codexHookEntries -----------------------------------------------------
357
- // The six guardrail hooks trailhead registers on Codex, as {event, matcher,
357
+ // The seven guardrail hooks trailhead registers on Codex, as {event, matcher,
358
358
  // command} records. Commands run the copied guard scripts under hooksScriptsDir
359
359
  // via node, matching Claude's `node "<path>"` form. PreToolUse guards (commit,
360
- // secret, install, search, secret-read) can veto; the injection-scanner is a
361
- // PostToolUse advisory; check-update runs at SessionStart. Reused by the
360
+ // secret, install, search, body, secret-read) can veto; the injection-scanner
361
+ // is a PostToolUse advisory; check-update runs at SessionStart. Reused by the
362
362
  // installer, which merges them into ~/.codex/hooks.json (same shape as
363
363
  // Claude's settings.json hooks block).
364
364
  function codexHookEntries(hooksScriptsDir) {
@@ -368,6 +368,7 @@ function codexHookEntries(hooksScriptsDir) {
368
368
  { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-secret-guard.js') },
369
369
  { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-install-guard.js') },
370
370
  { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-search-guard.js') },
371
+ { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-body-guard.js') },
371
372
  { event: 'PreToolUse', matcher: 'Read|Bash', command: cmd('trailhead-secret-read-guard.js') },
372
373
  { event: 'PostToolUse', matcher: 'Bash', command: cmd('trailhead-issue-injection-scanner.js') },
373
374
  { event: 'SessionStart', matcher: '', command: cmd('trailhead-check-update.js') },
@@ -143,7 +143,7 @@ ok('codexVerbSkillPlan: null verbs yields nothing', codexVerbSkillPlan('/c', nul
143
143
 
144
144
  // --- codexHookEntries ---
145
145
  const entries = codexHookEntries('/h');
146
- ok('codexHookEntries returns 7 entries', Array.isArray(entries) && entries.length === 7);
146
+ ok('codexHookEntries returns 8 entries', Array.isArray(entries) && entries.length === 8);
147
147
  ok('codexHookEntries: commit-guard is PreToolUse/Bash', entries.some((e) =>
148
148
  e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-commit-guard.js') && e.command.includes('/h')));
149
149
  ok('codexHookEntries: secret-guard is PreToolUse/Bash', entries.some((e) =>
@@ -152,6 +152,8 @@ ok('codexHookEntries: install-guard is PreToolUse/Bash', entries.some((e) =>
152
152
  e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-install-guard.js')));
153
153
  ok('codexHookEntries: search-guard is PreToolUse/Bash', entries.some((e) =>
154
154
  e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-search-guard.js')));
155
+ ok('codexHookEntries: body-guard is PreToolUse/Bash', entries.some((e) =>
156
+ e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-body-guard.js')));
155
157
  ok('codexHookEntries: secret-read-guard is PreToolUse/Read|Bash', entries.some((e) =>
156
158
  e.event === 'PreToolUse' && e.matcher === 'Read|Bash' && e.command.includes('trailhead-secret-read-guard.js')));
157
159
  ok('codexHookEntries: injection-scanner is PostToolUse/Bash', entries.some((e) =>
package/bin/trailhead.js CHANGED
@@ -213,14 +213,15 @@ function claudePaths(configDir) {
213
213
  settings: path.join(configDir, 'settings.json'),
214
214
  };
215
215
  }
216
- const HOOK_FILES = ['trailhead-commit-guard.js', 'trailhead-issue-injection-scanner.js', 'trailhead-secret-guard.js', 'trailhead-install-guard.js', 'trailhead-search-guard.js', 'trailhead-secret-read-guard.js', 'trailhead-check-update.js'];
216
+ const HOOK_FILES = ['trailhead-commit-guard.js', 'trailhead-issue-injection-scanner.js', 'trailhead-secret-guard.js', 'trailhead-install-guard.js', 'trailhead-search-guard.js', 'trailhead-secret-read-guard.js', 'trailhead-body-guard.js', 'trailhead-check-update.js'];
217
217
  // Runtime libs the hook scripts require (trailhead-commit-guard.js does
218
218
  // require('./lib/commit-message-check.js'); trailhead-search-guard.js and
219
219
  // trailhead-secret-read-guard.js both do require('./lib/shell-scan.js'), #135
220
- // follow-up). Copied by name, never a recursive sweep: the Claude hooks/lib
221
- // dir is shared with other plugins, so we must not clobber a co-tenant's lib
222
- // nor ship our own *.test.js files.
223
- const HOOK_LIB_FILES = ['commit-message-check.js', 'shell-scan.js'];
220
+ // follow-up; trailhead-body-guard.js and trailhead-secret-guard.js both do
221
+ // require('./lib/gh-subcommand.js'), #153 follow-up). Copied by name, never a
222
+ // recursive sweep: the Claude hooks/lib dir is shared with other plugins, so
223
+ // we must not clobber a co-tenant's lib nor ship our own *.test.js files.
224
+ const HOOK_LIB_FILES = ['commit-message-check.js', 'shell-scan.js', 'gh-subcommand.js'];
224
225
 
225
226
  // Copy the hook scripts and their runtime libs into a destination hooks dir
226
227
  // (Claude's ~/.claude/hooks or Codex's skills/trailhead/hooks). Shared by both
@@ -329,6 +330,7 @@ function uninstallClaude(configDir) {
329
330
  stripHook(s, 'PreToolUse', 'trailhead-install-guard.js');
330
331
  stripHook(s, 'PreToolUse', 'trailhead-search-guard.js');
331
332
  stripHook(s, 'PreToolUse', 'trailhead-secret-read-guard.js');
333
+ stripHook(s, 'PreToolUse', 'trailhead-body-guard.js');
332
334
  stripHook(s, 'PostToolUse', 'trailhead-issue-injection-scanner.js');
333
335
  stripHook(s, 'SessionStart', 'trailhead-check-update.js');
334
336
  writeJSON(P.settings, s);
@@ -375,6 +377,7 @@ function installClaude(configDir, { useSymlink }) {
375
377
  addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-secret-guard.js')),
376
378
  addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-install-guard.js')),
377
379
  addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-search-guard.js')),
380
+ addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-body-guard.js')),
378
381
  addHook(s, 'PreToolUse', 'Read|Bash', hookCmd('trailhead-secret-read-guard.js')),
379
382
  addHook(s, 'PostToolUse', 'Bash', hookCmd('trailhead-issue-injection-scanner.js')),
380
383
  addHook(s, 'SessionStart', '', hookCmd('trailhead-check-update.js')),
@@ -681,6 +684,7 @@ function uninstallCodex(configDir) {
681
684
  stripHook(h, 'PreToolUse', 'trailhead-install-guard.js');
682
685
  stripHook(h, 'PreToolUse', 'trailhead-search-guard.js');
683
686
  stripHook(h, 'PreToolUse', 'trailhead-secret-read-guard.js');
687
+ stripHook(h, 'PreToolUse', 'trailhead-body-guard.js');
684
688
  stripHook(h, 'PostToolUse', 'trailhead-issue-injection-scanner.js');
685
689
  stripHook(h, 'SessionStart', 'trailhead-check-update.js');
686
690
  writeJSON(L.hooksJson, h);
@@ -118,6 +118,143 @@ for (const cl of ['trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailh
118
118
  // A cluster's ../_shared/ reference target actually resolves as a sibling.
119
119
  ok('codex: trailhead-work/../_shared/substrate.md resolves', fs.existsSync(path.join(codexDir, 'skills', 'trailhead-work', '..', '_shared', 'substrate.md')));
120
120
 
121
+ // --- #139: single-sourced "Load first, in order" shared section --------------
122
+ ok('codex: _shared/load-first.md projected', fs.existsSync(path.join(codexDir, 'skills', '_shared', 'load-first.md')));
123
+ for (const cl of ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage']) {
124
+ ok(`codex: ${cl}/../_shared/load-first.md resolves as a sibling`, fs.existsSync(path.join(codexDir, 'skills', cl, '..', '_shared', 'load-first.md')));
125
+ ok(`codex: ${cl} SKILL.md references ../_shared/load-first.md`, fs.readFileSync(path.join(codexDir, 'skills', cl, 'SKILL.md'), 'utf8').includes('../_shared/load-first.md'));
126
+ }
127
+ // Source-level anti-drift: the shared core-list signature line lives ONLY in
128
+ // _shared/load-first.md, never duplicated back into a cluster SKILL.md.
129
+ const sourceSkillsDir = path.join(__dirname, '..', 'plugins', 'trailhead', 'skills');
130
+ const loadFirstSignature = '`../_shared/principles.md`: refer by name';
131
+ const loadFirstSourcePath = path.join(sourceSkillsDir, '_shared', 'load-first.md');
132
+ ok('source: _shared/load-first.md carries the core-list signature line', fs.readFileSync(loadFirstSourcePath, 'utf8').includes(loadFirstSignature));
133
+ for (const cl of ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage']) {
134
+ ok(`source: ${cl}/SKILL.md does NOT duplicate the core-list signature line`, !fs.readFileSync(path.join(sourceSkillsDir, cl, 'SKILL.md'), 'utf8').includes(loadFirstSignature));
135
+ }
136
+
137
+ // --- #146: single-sourced ticket-language directive (anti-drift) -------------
138
+ // The standing ticket-language rule (write Issue prose + commit bodies in
139
+ // config.ticket.language, decoupled from the chat language) lives in ONE place,
140
+ // _shared/ticket-language.md; it is listed in the load contract's six-file core
141
+ // and REFERENCED (never restated) by the engine references. Mirrors the #139
142
+ // load-first invariant so the directive can't silently drift.
143
+ ok('codex: _shared/ticket-language.md projected', fs.existsSync(path.join(codexDir, 'skills', '_shared', 'ticket-language.md')));
144
+ for (const cl of ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage']) {
145
+ ok(`codex: ${cl}/../_shared/ticket-language.md resolves as a sibling`, fs.existsSync(path.join(codexDir, 'skills', cl, '..', '_shared', 'ticket-language.md')));
146
+ }
147
+ // The single source states the rule's substance: the canonical config key AND
148
+ // the decoupling clause (the heart of the rule), not merely a heading.
149
+ const ticketLangSourcePath = path.join(sourceSkillsDir, '_shared', 'ticket-language.md');
150
+ const ticketLangSource = fs.readFileSync(ticketLangSourcePath, 'utf8');
151
+ const ticketLangKey = '`config.ticket.language`';
152
+ const ticketLangDecoupling = '**independent of the language the agent converses in**';
153
+ ok('source: ticket-language.md states the rule (config.ticket.language + decoupling clause)',
154
+ ticketLangSource.includes(ticketLangKey) && ticketLangSource.includes(ticketLangDecoupling));
155
+ // The load contract lists it in the six-file core (item 2), by path reference.
156
+ const loadFirstSource = fs.readFileSync(loadFirstSourcePath, 'utf8');
157
+ ok('source: load-first.md references ../_shared/ticket-language.md', loadFirstSource.includes('../_shared/ticket-language.md'));
158
+ // The engine references point AT the single source (path reference), not restate it.
159
+ const ticketLangAutoSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-work', 'references', 'auto.md'), 'utf8');
160
+ const ticketLangCaptureSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-capture', 'references', 'capture.md'), 'utf8');
161
+ ok('source: auto.md references ticket-language.md', ticketLangAutoSource.includes('ticket-language.md'));
162
+ ok('source: capture.md references ticket-language.md', ticketLangCaptureSource.includes('ticket-language.md'));
163
+ // Source-level anti-drift: the substantive decoupling clause lives in EXACTLY
164
+ // one file across the whole skills tree, _shared/ticket-language.md. A global
165
+ // single-occurrence scan (not an enumerated per-file list) so any future doc
166
+ // that restates the verbatim directive is caught, including reachable
167
+ // shared-core references beyond the load contract and the two engine references
168
+ // (e.g. configuration-reference.md, choices.md). The legitimate paraphrases in
169
+ // configuration-reference.md/capture.md reword the rule and do not match this
170
+ // bolded verbatim clause, so they don't trip the scan.
171
+ const mdFilesUnder = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
172
+ const full = path.join(dir, e.name);
173
+ return e.isDirectory() ? mdFilesUnder(full) : (e.name.endsWith('.md') ? [full] : []);
174
+ });
175
+ const ticketLangClauseFiles = mdFilesUnder(sourceSkillsDir).filter((f) => fs.readFileSync(f, 'utf8').includes(ticketLangDecoupling));
176
+ ok('source: ticket-language clause appears in exactly one skills-tree file', ticketLangClauseFiles.length === 1);
177
+ ok('source: that one file is _shared/ticket-language.md', ticketLangClauseFiles.length === 1 && path.basename(ticketLangClauseFiles[0]) === 'ticket-language.md');
178
+
179
+ // --- #147: chart's close anchors the next step to /trailhead:work -----------
180
+ // Source-level invariant: charting.md renders a codified next-step block at the
181
+ // chart/adopt close, led by /clear, anchored to /trailhead:work (the map-frontier
182
+ // verb) with /trailhead:quick only as the off-map alternative, single-sourced
183
+ // once in Mode 1 step 6 with Mode 1-bis reaching it via "As Mode 1, steps 4-6".
184
+ const chartingSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-chart', 'references', 'charting.md'), 'utf8');
185
+ ok('source: charting.md close carries a codified next-step block', /Next-step block \(the close of chart/.test(chartingSource));
186
+ // Structural: the rendered blockquote must LEAD with a standalone /clear bullet
187
+ // (guards the "never demote /clear to a trailing parenthetical" regression #147 exists for).
188
+ ok('source: charting.md next-step block leads with a standalone /clear bullet', /> \*\*Prossimo passo:\*\*\n> - `\/clear`/.test(chartingSource));
189
+ // Structural: /trailhead:work is the primary bullet, carrying the number (name in prose).
190
+ ok('source: charting.md next-step block makes /trailhead:work the primary move with the number', /`\/trailhead:work <numero>` per lavorare \*\*<nome/.test(chartingSource));
191
+ // Structural: /trailhead:quick is demoted to the last "in alternativa ... fuori mappa" bullet.
192
+ ok('source: charting.md next-step block demotes /trailhead:quick to the off-map alternative bullet', /> - in alternativa `\/trailhead:quick[^\n]*fuori mappa/.test(chartingSource));
193
+ ok('source: charting.md next-step block presents several independent tickets as a set to choose from', chartingSource.includes('set to choose from'));
194
+ ok('source: charting.md next-step block frames quick as the off-map alternative', chartingSource.includes('only as the off-map alternative'));
195
+ ok('source: charting.md single-sources the block via the "As Mode 1, steps 4-6" reference', /As Mode 1, steps 4[–-]6/.test(chartingSource));
196
+ const chartSkillSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-chart', 'SKILL.md'), 'utf8');
197
+ ok('source: trailhead-chart SKILL.md config-offer points at the next-step block', chartSkillSource.includes('next-step block') && chartSkillSource.includes('/trailhead:work'));
198
+
199
+ // --- #149: capture confirmation anchors the next step to /trailhead:work -----
200
+ // Source-level invariant, mirroring #147 for chart: capture.md's confirmation-line
201
+ // rule names /trailhead:work as the anchor (full engine, keeps the map record) and
202
+ // demotes /trailhead:quick to the off-map / no-split alternative, offered alongside,
203
+ // never in its place. Localized + ordered assertions (not broad substrings): the anchor
204
+ // clause precedes the quick-demotion clause within the Confirmation line. Targets the
205
+ // capture confirmation line specifically, NOT a blanket /trailhead:quick ban
206
+ // (session-handoff.md legitimately offers /trailhead:quick <n> for the next loose ticket).
207
+ const captureSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-capture', 'references', 'capture.md'), 'utf8');
208
+ ok('source: capture.md confirmation anchors the next step to /trailhead:work', /[Aa]nchor the next step to `\/trailhead:work <n>`, always/.test(captureSource));
209
+ ok('source: capture.md demotes /trailhead:quick to only the off-map / no-split alternative, after the work anchor',
210
+ /[Aa]nchor the next step to `\/trailhead:work <n>`[\s\S]{0,400}`\/trailhead:quick <n>` is \*\*only\*\* the off-map \/ no-split alternative/.test(captureSource));
211
+ ok('source: capture.md says quick is offered alongside work, never in its place', captureSource.includes('never in its place'));
212
+ ok('source: capture.md keeps /trailhead:work the anchor even for a whiteboard capture', /whiteboard[\s\S]{0,200}the anchor stays `\/trailhead:work <n>`/.test(captureSource));
213
+
214
+ // --- #151: eager edge drop at close (superseded / out-of-scope only) ---------
215
+ // Source-level invariant: closing a ticket as trailhead:superseded or
216
+ // trailhead:out-of-scope drops its native sub-issue edge at close (reusing the
217
+ // 404-tolerant DELETE from #150), while a genuinely-resolved close leaves the
218
+ // edge in place (it feeds GitHub's native progress bar; /trailhead:prune is its
219
+ // only shedder). Asserts the canonical rule, every operative close site, and a
220
+ // NEGATIVE invariant on the resolved-close path.
221
+ const substrateCommandsSource = fs.readFileSync(path.join(sourceSkillsDir, '_shared', 'substrate-commands.md'), 'utf8');
222
+ ok('source: substrate-commands.md documents the eager edge drop at close (superseded / out-of-scope only)',
223
+ /[Ee]ager edge drop at close \(superseded \/ out-of-scope only\)/.test(substrateCommandsSource));
224
+ ok('source: substrate-commands.md fixes the label -> DELETE -> close ordering for the eager drop',
225
+ /apply the state label first, then DELETE the edge, then `gh issue close`/.test(substrateCommandsSource));
226
+ ok('source: substrate-commands.md eager drop excludes resolved closes (progress bar / prune)',
227
+ // Proximity-scoped so each conjunct is load-bearing on the NEW paragraph's own
228
+ // text, not on the pre-existing "progress bar" mentions elsewhere in the file.
229
+ /Only these two closes shed eagerly[\s\S]{0,300}progress bar/.test(substrateCommandsSource) &&
230
+ /Resolved edges stay[\s\S]{0,160}`\/trailhead:prune`'s job/.test(substrateCommandsSource));
231
+
232
+ const substrateSource = fs.readFileSync(path.join(sourceSkillsDir, '_shared', 'substrate.md'), 'utf8');
233
+ ok('source: substrate.md out-of-scope label notes the edge is dropped at close',
234
+ /`trailhead:out-of-scope`[\s\S]{0,260}native sub-issue edge is dropped at close/.test(substrateSource));
235
+ ok('source: substrate.md superseded label notes the edge is dropped at close',
236
+ /`trailhead:superseded`[\s\S]{0,260}native sub-issue edge is dropped at close/.test(substrateSource));
237
+
238
+ const teamworkSource = fs.readFileSync(path.join(sourceSkillsDir, '_shared', 'teamwork.md'), 'utf8');
239
+ ok('source: teamwork.md supersede close drops the native sub-issue edge',
240
+ /Supersede the original[\s\S]{0,400}drop its native sub-issue edge from the map/.test(teamworkSource));
241
+
242
+ const workSkillSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-work', 'SKILL.md'), 'utf8');
243
+ ok('source: trailhead-work SKILL.md out-of-scope close drops the native sub-issue edge',
244
+ /out of scope\*\* \(label, then \*\*drop its native sub-issue edge\*\*/.test(workSkillSource));
245
+
246
+ const inboxSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-chart', 'references', 'inbox.md'), 'utf8');
247
+ ok('source: inbox.md out-of-scope close drops the native sub-issue edge',
248
+ /`trailhead:out-of-scope`, \*\*drop its native sub-issue edge\*\*/.test(inboxSource));
249
+ ok('source: inbox.md dropped-fog close drops any native sub-issue edge',
250
+ /drop any native sub-issue edge to reclaim a slot/.test(inboxSource));
251
+
252
+ // NEGATIVE invariant: the resolved-close paths (build/bug Resolve) must NOT
253
+ // carry an eager edge-drop instruction; resolved edges stay for the progress bar.
254
+ const ticketEnginesSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-work', 'references', 'ticket-engines.md'), 'utf8');
255
+ ok('source: ticket-engines.md resolved-close path does NOT drop the native sub-issue edge',
256
+ !/drop (its|any) native sub-issue edge/.test(ticketEnginesSource));
257
+
121
258
  // --- codex hooks (#29) --------------------------------------------------------
122
259
  const codexHooksJsonPath = path.join(codexDir, 'hooks.json');
123
260
  ok('codex: hooks.json exists', fs.existsSync(codexHooksJsonPath));
@@ -132,6 +269,8 @@ ok('codex: hooks.json registers check-update under SessionStart', (codexHooksJso
132
269
  ok('codex: skills/trailhead/hooks/trailhead-secret-guard.js exists', fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'trailhead-secret-guard.js')));
133
270
  ok('codex: skills/trailhead/hooks/lib/commit-message-check.js exists (commit-guard require target)',
134
271
  fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'commit-message-check.js')));
272
+ ok('codex: skills/trailhead/hooks/lib/gh-subcommand.js exists (body-guard/secret-guard require target, #153)',
273
+ fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'gh-subcommand.js')));
135
274
 
136
275
  const codexConfigTomlPath = path.join(codexDir, 'config.toml');
137
276
  ok('codex: config.toml exists', fs.existsSync(codexConfigTomlPath));
@@ -249,11 +388,24 @@ for (const cl of ['trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailh
249
388
  ok(`claude: skills/${cl}/SKILL.md exists`, fs.existsSync(path.join(claudeDir, 'skills', cl, 'SKILL.md')));
250
389
  }
251
390
  ok('claude: cluster ../_shared/ reference resolves', fs.existsSync(path.join(claudeDir, 'skills', 'trailhead-work', '..', '_shared', 'substrate.md')));
391
+ // #139: single-sourced "Load first, in order" shared section, projected on Claude too.
392
+ ok('claude: skills/_shared/load-first.md exists', fs.existsSync(path.join(claudeDir, 'skills', '_shared', 'load-first.md')));
393
+ ok('claude: trailhead-work/../_shared/load-first.md resolves', fs.existsSync(path.join(claudeDir, 'skills', 'trailhead-work', '..', '_shared', 'load-first.md')));
394
+ for (const cl of ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage']) {
395
+ ok(`claude: ${cl} SKILL.md references ../_shared/load-first.md`, fs.readFileSync(path.join(claudeDir, 'skills', cl, 'SKILL.md'), 'utf8').includes('../_shared/load-first.md'));
396
+ }
397
+
398
+ // #146: single-sourced ticket-language directive, projected on Claude too.
399
+ ok('claude: skills/_shared/ticket-language.md exists', fs.existsSync(path.join(claudeDir, 'skills', '_shared', 'ticket-language.md')));
400
+ ok('claude: trailhead-work/../_shared/ticket-language.md resolves', fs.existsSync(path.join(claudeDir, 'skills', 'trailhead-work', '..', '_shared', 'ticket-language.md')));
401
+
252
402
  ok('claude: commands/trailhead/work.md exists', fs.existsSync(path.join(claudeDir, 'commands', 'trailhead', 'work.md')));
253
403
  ok('claude: commands/trailhead/auto.md exists', fs.existsSync(path.join(claudeDir, 'commands', 'trailhead', 'auto.md')));
254
404
  ok('claude: hooks/trailhead-commit-guard.js exists', fs.existsSync(path.join(claudeDir, 'hooks', 'trailhead-commit-guard.js')));
255
405
  ok('claude: hooks/lib/commit-message-check.js exists (commit-guard require target)',
256
406
  fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'commit-message-check.js')));
407
+ ok('claude: hooks/lib/gh-subcommand.js exists (body-guard/secret-guard require target, #153)',
408
+ fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'gh-subcommand.js')));
257
409
  // Regression: the commit-guard does require('./lib/commit-message-check.js'), so
258
410
  // it only loads if the lib was copied alongside it. Run it with a benign Bash
259
411
  // payload and assert it does not crash with a missing-module error.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcomigozzi/trailhead",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Start and drive large projects as a map of tickets on GitHub Issues. Map-based onboarding + a discuss→plan→execute→verify engine, all on the Issues. Installs the trailhead skill, commands, and hooks into your agent's config dir.",
5
5
  "bin": {
6
6
  "trailhead": "bin/trailhead.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trailhead",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Start and drive large projects as a map of tickets on GitHub Issues, resolving one at a time. Map-based onboarding + a discuss→plan→execute→verify engine with atomic commits, all on the Issues. Ships as a dispatcher plus cohesion-clustered skills over a shared core; depends on nothing beyond an authenticated gh CLI.",
5
5
  "author": {
6
6
  "name": "Marco Migozzi",
@@ -0,0 +1,6 @@
1
+ ---
2
+ description: Prune a map's closed and orphan sub-issue edges to reclaim slots under GitHub's 100-cap (references kept)
3
+ argument-hint: "[map]"
4
+ ---
5
+
6
+ Invoke the `trailhead` skill for the **prune** action: call the Skill tool with skill name `trailhead-manage` and arguments `prune $ARGUMENTS`, then **read the skill's `references/pruning.md` and follow it**: that file holds the prune protocol (resolve the target map, read the sub-issue count, reconcile the map's native edges bidirectionally, preview and confirm, then apply, keeping every reference). Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -19,6 +19,10 @@
19
19
  {
20
20
  "type": "command",
21
21
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/trailhead-search-guard.js\""
22
+ },
23
+ {
24
+ "type": "command",
25
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/trailhead-body-guard.js\""
22
26
  }
23
27
  ]
24
28
  },