@marcomigozzi/trailhead 0.13.0 → 0.14.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 (61) hide show
  1. package/README.it.md +28 -3
  2. package/README.md +38 -3
  3. package/bin/lib/codex-projection.js +16 -9
  4. package/bin/lib/codex-projection.test.js +39 -11
  5. package/bin/lib/install-verify.js +10 -5
  6. package/bin/lib/install-verify.test.js +51 -0
  7. package/bin/trailhead.js +37 -25
  8. package/bin/trailhead.test.js +162 -5
  9. package/package.json +4 -1
  10. package/plugins/trailhead/.claude-plugin/plugin.json +3 -2
  11. package/plugins/trailhead/commands/panel.md +10 -0
  12. package/plugins/trailhead/hooks/hooks.json +3 -18
  13. package/plugins/trailhead/hooks/lib/body-sources.js +795 -0
  14. package/plugins/trailhead/hooks/lib/gh-subcommand.js +92 -10
  15. package/plugins/trailhead/hooks/lib/gh-subcommand.test.js +55 -0
  16. package/plugins/trailhead/hooks/lib/mockup-link.js +21 -5
  17. package/plugins/trailhead/hooks/lib/shell-scan.js +496 -64
  18. package/plugins/trailhead/hooks/lib/shell-scan.test.js +420 -0
  19. package/plugins/trailhead/hooks/lib/unmodelled.js +345 -0
  20. package/plugins/trailhead/hooks/lib/unmodelled.test.js +173 -0
  21. package/plugins/trailhead/hooks/lib/work-state.test.js +252 -0
  22. package/plugins/trailhead/hooks/mod/README.md +187 -0
  23. package/plugins/trailhead/hooks/mod/map-pane-view.ts +453 -0
  24. package/plugins/trailhead/hooks/mod/map-pane.tsx +663 -0
  25. package/plugins/trailhead/hooks/mod/register.tsx +280 -0
  26. package/plugins/trailhead/hooks/mod/work-state-prompt.ts +69 -0
  27. package/plugins/trailhead/hooks/mod/work-state-view.ts +93 -0
  28. package/plugins/trailhead/hooks/trailhead-bash-guards.js +140 -0
  29. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +233 -0
  30. package/plugins/trailhead/hooks/trailhead-body-guard.js +145 -61
  31. package/plugins/trailhead/hooks/trailhead-body-guard.test.js +224 -3
  32. package/plugins/trailhead/hooks/trailhead-commit-guard.js +41 -14
  33. package/plugins/trailhead/hooks/trailhead-commit-guard.test.js +2 -1
  34. package/plugins/trailhead/hooks/trailhead-install-guard.js +22 -12
  35. package/plugins/trailhead/hooks/trailhead-install-guard.test.js +2 -1
  36. package/plugins/trailhead/hooks/trailhead-mockup-link-guard.js +48 -11
  37. package/plugins/trailhead/hooks/trailhead-mockup-link-guard.test.js +75 -3
  38. package/plugins/trailhead/hooks/trailhead-search-guard.js +19 -9
  39. package/plugins/trailhead/hooks/trailhead-search-guard.test.js +2 -1
  40. package/plugins/trailhead/hooks/trailhead-secret-guard.js +541 -57
  41. package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +1000 -4
  42. package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +171 -6
  43. package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +170 -0
  44. package/plugins/trailhead/skills/_shared/choices.md +3 -1
  45. package/plugins/trailhead/skills/_shared/multi-map.md +1 -1
  46. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  47. package/plugins/trailhead/skills/_shared/teamwork.md +4 -2
  48. package/plugins/trailhead/skills/_shared/techniques/prototype.md +1 -1
  49. package/plugins/trailhead/skills/trailhead/SKILL.md +2 -2
  50. package/plugins/trailhead/skills/trailhead-work/SKILL.md +3 -2
  51. package/plugins/trailhead/skills/trailhead-work/references/auto.md +2 -0
  52. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +3 -1
  53. package/plugins/trailhead/templates/trailhead-work-state.js +199 -0
  54. package/plugins/trailhead/tests/map-pane-ui.test.ts +1374 -0
  55. package/plugins/trailhead/tests/map-pane-view.test.ts +565 -0
  56. package/plugins/trailhead/tests/mod.test.ts +14 -0
  57. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +207 -0
  58. package/plugins/trailhead/tests/work-state-prompt.test.ts +109 -0
  59. package/plugins/trailhead/tests/work-state-ui.test.ts +365 -0
  60. package/plugins/trailhead/tests/work-state-view.test.ts +86 -0
  61. package/plugins/trailhead/types/index.d.ts +69 -0
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-23%20verbi-0e8a16?style=flat-square) ![guardrail](https://img.shields.io/badge/guardrail-7%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-24%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
 
@@ -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`; la search guard è solo per Claude Code e non è proiettata su Codex), 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 `artifacts` (il tool `Artifact` di Claude Code) 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 sei guardrail come **hook** nativi di Codex (`~/.codex/hooks.json`, abilitando `features.hooks`; i quattro guard Bash condividono un solo processo dietro una singola registrazione; la search guard è solo per Claude Code e non è proiettata su Codex), 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 `artifacts` (il tool `Artifact` di Claude Code) 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
 
@@ -185,6 +185,27 @@ Ogni verbo è anche un comando con namespace (`/trailhead:new`, `/trailhead:work
185
185
  | `/trailhead:grill [topic]` | esegue una sessione di grilling autonoma su una decision/topic |
186
186
  | `/trailhead:map` | mostra la mappa a bassa risoluzione (destination, decisions, frontier, fog) |
187
187
  | `/trailhead:dashboard` | mostra la dashboard del repo: l'indice pinnato di ogni mappa aperta, la whiteboard, e i conteggi live |
188
+ | `/trailhead:panel` | apre o chiude il pannello live della mappa: i ticket aperti della mappa attiva per stato, con un pulsante `work` per ogni ticket della frontier (**solo Claude Code**: richiede la mod di trailhead, 2.1.287+) |
189
+
190
+ ### 🖥️ A schermo in Claude Code
191
+
192
+ La mod di trailhead (Claude Code 2.1.287+) è inclusa nel plugin e disegna il lavoro accanto alla conversazione. Codex mantiene gli hook dei comandi e non ha un pannello.
193
+
194
+ ![Il pannello della mappa affiancato al transcript: mappe aperte, frontier con un pulsante work per ticket, in corso, bloccati e un footer con refresh e close](assets/screenshots/map-panel.png)
195
+
196
+ Il pannello della mappa elenca le mappe aperte da consultare, la frontier con un pulsante `work` per ogni ticket che pulisce il contesto e avvia il ticket, chi tiene cosa e cosa è bloccato. Il footer ha `refresh` e `close`. `/trailhead:panel` lo apre e lo chiude, e si apre da solo una volta in fullscreen.
197
+
198
+ ![Il pannello che mostra un'altra mappa, con l'intestazione viewing e active e il pulsante back to active](assets/screenshots/map-panel-viewing.png)
199
+
200
+ Un click mostra soltanto una mappa, e `back to active` torna a quella attiva. La mappa attiva cambia quando lavori uno dei suoi ticket.
201
+
202
+ ![La sezione Burn-down del pannello: riga dei conteggi e grafico dei ticket aperti nel tempo](assets/screenshots/burndown.png)
203
+
204
+ Il burn-down mostra i ticket aperti nel tempo, dal primo ticket della mappa a oggi, con chiusi e rimossi contati a parte.
205
+
206
+ ![La pipeline band sopra il prompt e la voce di stato trailhead sotto](assets/screenshots/work-state-band.png)
207
+
208
+ La pipeline band sopra il prompt e la voce di stato sotto mostrano il ticket in gioco e il suo step.
188
209
 
189
210
  ### Capture: zero attrito, una riga di conferma, non risolve nulla
190
211
 
@@ -351,9 +372,13 @@ Oltre alle istruzioni della skill, trailhead include otto hook propri (in `hooks
351
372
  - **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. Solo Claude Code; non è proiettata su Codex, che non ha una regola di deny su `Read()`.
352
373
  - **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.
353
374
  - **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.
354
- - **Mockup-link guard** (`PreToolUse` su `AskUserQuestion`): rete di sicurezza strutturale per la regola della tecnica Prototype secondo cui la richiesta di approvazione di un mockup porta con sé il link al mockup. Blocca un `AskUserQuestion` che chiede di approvare un mockup (o di scegliere tra varianti) quando il testo della risposta subito prima non contiene un URL o un path, così l'agente mette il link cliccabile nel messaggio prima di richiedere. Su Codex, che non ha il tool `AskUserQuestion` e pone le domande come elenco numerato in testo, il gemello `mockup-link-stop` gira su `Stop`: se la risposta finale chiede quell'approvazione senza link, Codex fa proseguire il turno e l'agente riscrive la domanda col link.
375
+ - **Mockup-link guard** (`PreToolUse` su `AskUserQuestion`): rete di sicurezza strutturale per la regola della tecnica Prototype secondo cui la richiesta di approvazione di un mockup porta con sé il link al mockup. Blocca un `AskUserQuestion` che chiede di approvare un mockup (o di scegliere tra varianti) quando né il testo della risposta subito prima né la domanda di approvazione stessa contengono un URL o un path (se il messaggio corrente non è ancora nel transcript, la guardia non può giudicare e lascia passare), così l'agente mette il link cliccabile nel messaggio prima di richiedere. Su Codex, che non ha il tool `AskUserQuestion` e pone le domande come elenco numerato in testo, il gemello `mockup-link-stop` gira su `Stop`: se la risposta finale chiede quell'approvazione senza link, Codex fa proseguire il turno e l'agente riscrive la domanda col link.
355
376
  - **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.
356
377
 
378
+ I guard di commit, secret, install, search e body girano in **un solo processo**: una singola registrazione `trailhead-bash-guards.js` li valuta in quell'ordine, così una chiamata Bash paga un solo avvio di node invece di cinque (tre processi hook per chiamata Bash in tutto: l'hook unificato, il secret-read guard e l'injection scanner). Restano cinque guard distinti con verdetti invariati; vince il primo blocco, e un guard che va in errore consente da solo senza fermare gli altri. Anche un guard il cui modulo manca o è rotto viene saltato, ma mai in silenzio: l'hook segnala alla sessione quale guard non ha controllato il comando, così un'installazione parziale non passa inosservata. Su Codex lo stesso hook è registrato con `--guards=commit,secret,install,body`, perché lì non c'è la search guard.
379
+
380
+ I guard secret, body e secret-read **falliscono in modo chiuso sui costrutti shell che non sanno interpretare con certezza** (`eval`, `source` / `.`, un `${ cmd; }` di bash 5.3, `printf -v`, la definizione di una funzione, un alias, una chiave body di `gh api` quotata, un backslash-a-capo dentro una parola): quando uno di questi può nascondere o precedere una scrittura (o sta accanto a un path segreto) il comando viene bloccato con un messaggio "riscrivilo in forma semplice" invece di essere emulato; gli stessi costrutti senza scritture o segreti vicini passano.
381
+
357
382
  Tutti e otto 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.
358
383
 
359
384
  ---
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-23%20verbs-0e8a16?style=flat-square) ![guardrails](https://img.shields.io/badge/guardrails-7%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-24%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
 
@@ -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`; the search guard is Claude-Code-only and isn't projected to Codex), 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 `artifacts` mockup mode (Claude Code's `Artifact` tool) 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 six guardrails as native Codex **hooks** (`~/.codex/hooks.json`, enabling `features.hooks`; the four Bash guards share one process behind a single registration; the search guard is Claude-Code-only and isn't projected to Codex), 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 `artifacts` mockup mode (Claude Code's `Artifact` tool) 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
 
@@ -185,6 +185,27 @@ Every verb is also a namespaced command (`/trailhead:new`, `/trailhead:work`, `/
185
185
  | `/trailhead:grill [topic]` | run a standalone grilling session on a decision/topic |
186
186
  | `/trailhead:map` | show the low-res map (destination, decisions, frontier, fog) |
187
187
  | `/trailhead:dashboard` | show the repo dashboard: the pinned index of every open map, the whiteboard, and live counts |
188
+ | `/trailhead:panel` | open or close the live map panel: the active map's open tickets by state, with a `work` button per frontier ticket (**Claude Code only**: needs the trailhead mod, 2.1.287+) |
189
+
190
+ ### 🖥️ On screen in Claude Code
191
+
192
+ The trailhead mod (Claude Code 2.1.287+) ships inside the plugin and draws the work beside the conversation. Codex keeps the command hooks and has no panel.
193
+
194
+ ![The map panel docked beside the transcript: open maps, frontier with a work button per ticket, in progress, blocked, and a footer with refresh and close](assets/screenshots/map-panel.png)
195
+
196
+ The map panel lists the open maps to view, the frontier with a `work` button per ticket that clears the context and starts the ticket, who holds what, and what is blocked. The footer has `refresh` and `close`. `/trailhead:panel` toggles it, and it opens by itself once in fullscreen.
197
+
198
+ ![The panel viewing another map, with a viewing and active header and a back to active button](assets/screenshots/map-panel-viewing.png)
199
+
200
+ A click only views a map, and `back to active` returns to the active one. The active map changes when you work one of its tickets.
201
+
202
+ ![The Burn-down section of the panel: counts line and a chart of open tickets over time](assets/screenshots/burndown.png)
203
+
204
+ The burn-down shows the open tickets over time, from the map's first ticket to now, with done and removed counted apart.
205
+
206
+ ![The pipeline band above the prompt and the trailhead status entry under it](assets/screenshots/work-state-band.png)
207
+
208
+ The pipeline band above the prompt and the status entry under it show the ticket in play and its step.
188
209
 
189
210
  ### Capture: zero friction, one confirmation line, resolves nothing
190
211
 
@@ -217,6 +238,15 @@ The three that trip people up are **idea, seed, todo**, so here they are spelled
217
238
 
218
239
  The two cuts that matter: **idea vs seed** is waiting on *clarity* vs waiting on a *condition* (both land "later", for different reasons); **seed vs todo** is committed *later* vs committed *now*. Below all three sits **`note`** (raw text to remember, maybe never work), and off to the side is **`bug`** (a defect, not a commitment tier). The test for idea vs ticket is always: **can you phrase the question precisely now?** Yes → a ticket (`todo` if takeable, `seed` if gated); no → the fog (`idea`).
219
240
 
241
+ ### Manage: keep the install and the maps healthy
242
+
243
+ | Command | What it does |
244
+ |---|---|
245
+ | `/trailhead:config [get\|set …]` | guided menu setup; `get` prints the effective config, `set <key> <value>` writes one key (see Configuration below) |
246
+ | `/trailhead:update` | check for a newer trailhead and install it where that is safe |
247
+ | `/trailhead:prune [map]` | reclaim a map's sub-issue slots under GitHub's 100-cap (drops closed/orphan edges, keeps references) |
248
+ | `/trailhead:audit [map] [--all]` | audit closed tickets for regressions against today's code, plus duplicate and groupable open ones; read-only until you confirm |
249
+
220
250
  ---
221
251
 
222
252
  ## 🎫 Ticket types and their engines
@@ -305,6 +335,7 @@ The **📊 statusline** step offers to install trailhead's Claude Code status ba
305
335
  | `ticket.language` | an ISO 639-1 code (**`en`**) | the language trailhead **writes** its GitHub prose & commit descriptions in, decoupled from the language it converses in |
306
336
  | `models.{plan,execute,research,review,debug,verify,codebase-map}` | a full **versioned** model id (**inherit session**) | which model runs each activity; `plan` and `execute` are always set separately |
307
337
  | `design` | **`disk`** \| `artifacts` \| `stitch` | where UI mockups go: local throwaway HTML, a Design canvas **Artifact** on claude.ai (Claude Code only), or Google Stitch (cross-host, via the Stitch MCP). The legacy `claude.ai/design` value is read as `artifacts` |
338
+ | `design.project` | canvas Artifact URL / Stitch project id | `artifacts` and `stitch` only: the canvas or Stitch project the mockups land on; set at the first UI screen (pick, paste or create), not by hand. A leftover claude.ai/design project is not reused |
308
339
  | `design.surface` | **`canvas`** \| `design-system` | `stitch` only: a Stitch project of screens (default) or a shared Stitch design system |
309
340
  | `design.system` | Design System Artifact URL | `artifacts` only: the design system installed on the mockup canvas (asked at the first screen if unset) |
310
341
  | `design.approval` | **`explicit`** \| `auto` | wait for mockup approval before UI code, or proceed without blocking |
@@ -351,9 +382,13 @@ Beyond the skill's instructions, trailhead ships eight of its own hooks (in `hoo
351
382
  - **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. Claude Code only; not projected to Codex, which has no such `Read()` deny rule.
352
383
  - **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.
353
384
  - **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.
354
- - **Mockup-link guard** (`PreToolUse` on `AskUserQuestion`): structural backstop for the Prototype rule that a mockup approval ask carries the mockup's link. It hard-blocks an `AskUserQuestion` that asks to approve a mockup (or pick between mockup variants) when the reply text right before it holds no URL or file path, so the agent puts the clickable link in the message before asking again. On Codex, which has no `AskUserQuestion` tool and asks as a plain-text numbered list, its twin `mockup-link-stop` runs on `Stop`: when the final reply asks for that approval without a link, Codex continues the turn so the agent rewrites the ask with it.
385
+ - **Mockup-link guard** (`PreToolUse` on `AskUserQuestion`): structural backstop for the Prototype rule that a mockup approval ask carries the mockup's link. It hard-blocks an `AskUserQuestion` that asks to approve a mockup (or pick between mockup variants) when neither the reply text right before it nor the approval question itself holds a URL or file path (when the current message is not yet in the transcript, the guard cannot judge and allows), so the agent puts the clickable link in the message before asking again. On Codex, which has no `AskUserQuestion` tool and asks as a plain-text numbered list, its twin `mockup-link-stop` runs on `Stop`: when the final reply asks for that approval without a link, Codex continues the turn so the agent rewrites the ask with it.
355
386
  - **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.
356
387
 
388
+ The commit, secret, install, search, and body guards run in **one process**: a single `trailhead-bash-guards.js` registration evaluates them in that order, so a Bash call pays one node start-up instead of five (three hook processes per Bash call in all: the merged hook, the secret-read guard, and the injection scanner). They remain five distinct guards with unchanged verdicts; the first block wins, and a guard that errors allows on its own without stopping the others. A guard whose module is missing or broken is skipped too, but never silently: the hook tells the session which guard did not check the command, so a partial install shows up. On Codex the same hook is registered with `--guards=commit,secret,install,body`, since there is no search guard there.
389
+
390
+ The secret, body, and secret-read guards also **fail closed on shell constructs they cannot interpret with certainty** (`eval`, `source` / `.`, a bash 5.3 `${ cmd; }`, `printf -v`, a function definition, an alias, a quoted `gh api` body key, a backslash-newline inside a word): when one can hide or precede a write (or sits next to a secret path), the command is blocked with a "rewrite it in a simple form" message instead of being emulated; the same constructs with no write or secret nearby pass.
391
+
357
392
  All eight 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.
358
393
 
359
394
  ---
@@ -113,7 +113,7 @@ Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_inp
113
113
  The engine's \`/clear\` is Codex's \`/new\` (start a fresh session). Command references in a handoff use the skill form (\`$trailhead work <n>\`).
114
114
 
115
115
  ## F. Lifecycle hooks (native Codex hooks)
116
- 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 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, mockup-link-stop as a \`Stop\` hook (when your final reply asks to approve a mockup without carrying its link, it continues the turn so you rewrite the ask with the URL or file path in the prose), 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. search-guard is Claude-Code-specific (it only prevents arming Claude's \`Read()\` deny rule under bypass permissions, a mechanism Codex lacks), so it is not projected to Codex at all. 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.
116
+ 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 body-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs (the four run in one process, a single \`trailhead-bash-guards.js --guards=commit,secret,install,body\` registration, so a Bash call pays one start-up instead of four), 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, mockup-link-stop as a \`Stop\` hook (when your final reply asks to approve a mockup without carrying its link, it continues the turn so you rewrite the ask with the URL or file path in the prose), 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. search-guard is Claude-Code-specific (it only prevents arming Claude's \`Read()\` deny rule under bypass permissions, a mechanism Codex lacks), so it is not projected to Codex at all. 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.
117
117
 
118
118
  ## G. Design / UI mockups: artifacts -> stitch offer, else local disk
119
119
  The \`artifacts\` mockup mode (a Design canvas Artifact built with Claude Code's \`Artifact\` tool; the legacy \`claude.ai/design\` value means the same) does not exist on Codex: there is no \`Artifact\` tool here. When \`config.design\` names it, follow the Prototype technique's fallback: **offer once to switch this project to \`design: stitch\`** (the Google Stitch MCP runs on Codex too), and only on decline produce a throwaway static HTML mockup next to the code (\`disk\`), link it from the ticket, and say why. Never try to create or edit an Artifact on Codex. \`config.design.approval\` still applies unchanged: \`explicit\` waits for the user's go-ahead before any UI code, \`auto\` surfaces the mockup and proceeds.
@@ -323,6 +323,11 @@ policy:
323
323
  `;
324
324
  }
325
325
 
326
+ // --- CODEX_MOD_ONLY_VERBS --------------------------------------------------
327
+ // Verbs only the Claude Code mod answers (commands/<verb>.md exists for the
328
+ // namespaced command, but nothing on Codex can draw it): never projected.
329
+ const CODEX_MOD_ONLY_VERBS = ['panel'];
330
+
326
331
  // --- codexVerbSkillPlan ----------------------------------------------------
327
332
  // Given the resolved verb list (each { verb, description? }, or a bare string
328
333
  // verb; normally read from plugins/trailhead/commands/*.md by the installer),
@@ -340,6 +345,7 @@ function codexVerbSkillPlan(codexHome, verbs) {
340
345
  const v = entry && typeof entry === 'object' ? entry.verb : entry;
341
346
  if (v == null || String(v).trim() === '') continue;
342
347
  const verb = String(v).trim();
348
+ if (CODEX_MOD_ONLY_VERBS.includes(verb)) continue;
343
349
  const description = entry && typeof entry === 'object' ? entry.description : undefined;
344
350
  const skillDir = path.join(skillsRoot, `trailhead-${verb}`);
345
351
  dirs.push(skillDir);
@@ -358,10 +364,13 @@ function codexVerbSkillPlan(codexHome, verbs) {
358
364
  }
359
365
 
360
366
  // --- codexHookEntries -----------------------------------------------------
361
- // The six guardrail hooks trailhead registers on Codex, as {event, matcher,
362
- // command} records. Commands run the copied guard scripts under hooksScriptsDir
363
- // via node, matching Claude's `node "<path>"` form. PreToolUse guards (commit,
364
- // secret, install, body, secret-read) can veto; the injection-scanner
367
+ // The guardrail hooks trailhead registers on Codex, as {event, matcher,
368
+ // command} records. Commands run the copied hook scripts under hooksScriptsDir
369
+ // via node, matching Claude's `node "<path>"` form. The four Bash guards
370
+ // (commit, secret, install, body) are ONE registration, the merged
371
+ // trailhead-bash-guards.js with `--guards=commit,secret,install,body` (a single
372
+ // node process per Bash call); secret-read is a separate PreToolUse hook. All
373
+ // PreToolUse guards can veto; the injection-scanner
365
374
  // is a PostToolUse advisory; check-update runs at SessionStart. Reused by the
366
375
  // installer, which merges them into ~/.codex/hooks.json (same shape as
367
376
  // Claude's settings.json hooks block).
@@ -372,10 +381,7 @@ function codexVerbSkillPlan(codexHome, verbs) {
372
381
  function codexHookEntries(hooksScriptsDir) {
373
382
  const cmd = (name) => `node "${path.join(hooksScriptsDir, name)}"`;
374
383
  return [
375
- { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-commit-guard.js') },
376
- { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-secret-guard.js') },
377
- { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-install-guard.js') },
378
- { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-body-guard.js') },
384
+ { event: 'PreToolUse', matcher: 'Bash', command: `${cmd('trailhead-bash-guards.js')} --guards=commit,secret,install,body` },
379
385
  { event: 'PreToolUse', matcher: 'Read|Bash', command: cmd('trailhead-secret-read-guard.js') },
380
386
  { event: 'PostToolUse', matcher: 'Bash', command: cmd('trailhead-issue-injection-scanner.js') },
381
387
  { event: 'Stop', matcher: '', command: cmd('trailhead-mockup-link-stop.js') },
@@ -447,6 +453,7 @@ module.exports = {
447
453
  codexVerbSkillContent,
448
454
  codexVerbSkillAgentsYaml,
449
455
  codexVerbSkillPlan,
456
+ CODEX_MOD_ONLY_VERBS,
450
457
  codexHookEntries,
451
458
  enableCodexFeatureFlag,
452
459
  enableCodexHooksFeature,
@@ -15,6 +15,7 @@ const {
15
15
  codexVerbSkillContent,
16
16
  codexVerbSkillAgentsYaml,
17
17
  codexVerbSkillPlan,
18
+ CODEX_MOD_ONLY_VERBS,
18
19
  codexHookEntries,
19
20
  enableCodexHooksFeature,
20
21
  enableCodexMultiAgentV2Feature,
@@ -160,19 +161,22 @@ ok('codexVerbSkillPlan: accepts a bare-string verb and skips empty/invalid', (()
160
161
  })());
161
162
  ok('codexVerbSkillPlan: null verbs yields nothing', codexVerbSkillPlan('/c', null).dirs.length === 0 && codexVerbSkillPlan('/c').dirs.length === 0);
162
163
 
164
+ // #208: a verb only the Claude Code mod answers (the map panel) has no Codex projection.
165
+ ok('CODEX_MOD_ONLY_VERBS names exactly the panel verb', Array.isArray(CODEX_MOD_ONLY_VERBS) && CODEX_MOD_ONLY_VERBS.length === 1 && CODEX_MOD_ONLY_VERBS[0] === 'panel');
166
+ ok('codexVerbSkillPlan: never projects a mod-only verb, object or bare string', (() => {
167
+ const plan = codexVerbSkillPlan('/c', [{ verb: 'panel', description: 'x' }, 'panel', { verb: 'map' }]);
168
+ return plan.dirs.length === 1 && plan.dirs[0] === '/c/skills/trailhead-map' && !plan.writes.some((w) => /panel/.test(w.path));
169
+ })());
170
+
163
171
  // --- codexHookEntries ---
164
172
  const entries = codexHookEntries('/h');
165
- ok('codexHookEntries returns 8 entries', Array.isArray(entries) && entries.length === 8);
166
- ok('codexHookEntries: commit-guard is PreToolUse/Bash', entries.some((e) =>
167
- e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-commit-guard.js') && e.command.includes('/h')));
168
- ok('codexHookEntries: secret-guard is PreToolUse/Bash', entries.some((e) =>
169
- e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-secret-guard.js')));
170
- ok('codexHookEntries: install-guard is PreToolUse/Bash', entries.some((e) =>
171
- e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-install-guard.js')));
172
- ok('codexHookEntries: search-guard is absent (Claude-Code-only, not projected to Codex)',
173
- entries.every((e) => !e.command.includes('trailhead-search-guard.js')));
174
- ok('codexHookEntries: body-guard is PreToolUse/Bash', entries.some((e) =>
175
- e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-body-guard.js')));
173
+ ok('codexHookEntries returns 5 entries', Array.isArray(entries) && entries.length === 5);
174
+ const bashEntries = entries.filter((e) => e.event === 'PreToolUse' && e.matcher === 'Bash');
175
+ ok('codexHookEntries: exactly one PreToolUse/Bash registration', bashEntries.length === 1);
176
+ ok('codexHookEntries: the Bash entry is the merged hook with exactly --guards=commit,secret,install,body',
177
+ bashEntries[0].command === 'node "/h/trailhead-bash-guards.js" --guards=commit,secret,install,body');
178
+ ok('codexHookEntries: no per-guard script is registered on its own', entries.every((e) =>
179
+ !/trailhead-(commit|secret|install|search|body)-guard\.js/.test(e.command)));
176
180
  ok('codexHookEntries: secret-read-guard is PreToolUse/Read|Bash', entries.some((e) =>
177
181
  e.event === 'PreToolUse' && e.matcher === 'Read|Bash' && e.command.includes('trailhead-secret-read-guard.js')));
178
182
  ok('codexHookEntries: injection-scanner is PostToolUse/Bash', entries.some((e) =>
@@ -402,4 +406,28 @@ ok('convertToCodex projects the anchor positives to Codex forms', (() => {
402
406
  !out.includes('/trailhead:work') && !out.includes('/clear');
403
407
  })());
404
408
 
409
+ // #205: the work state helper and its session env var survive the projection;
410
+ // the projected work skill and handoff carry the set/clear calls.
411
+ ok('convertToCodex keeps the trailhead-work-state.js filename',
412
+ convertToCodex('node <templates>/trailhead-work-state.js set').includes('<templates>/trailhead-work-state.js set'));
413
+ ok('convertToCodex keeps CLAUDE_CODE_SESSION_ID intact',
414
+ convertToCodex('session from CLAUDE_CODE_SESSION_ID when set').includes('CLAUDE_CODE_SESSION_ID'));
415
+ {
416
+ const skillsDir = path.join(__dirname, '../../plugins/trailhead/skills');
417
+ const workSkill = convertToCodex(fs.readFileSync(path.join(skillsDir, 'trailhead-work', 'SKILL.md'), 'utf8'));
418
+ ok('projected work skill carries the work-start set call',
419
+ workSkill.includes('trailhead-work-state.js set --repo <working-root> --map <n> --ticket <n> --title "<title>" --step discuss'));
420
+ const handoff = convertToCodex(fs.readFileSync(path.join(skillsDir, '_shared', 'session-handoff.md'), 'utf8'));
421
+ ok('projected session handoff carries the work state clear call',
422
+ handoff.includes('trailhead-work-state.js clear --repo <working-root>'));
423
+ const teamwork = convertToCodex(fs.readFileSync(path.join(skillsDir, '_shared', 'teamwork.md'), 'utf8'));
424
+ ok('projected teamwork carries the step change call',
425
+ teamwork.includes('trailhead-work-state.js set --repo <working-root> --step <step>'));
426
+ ok('projected teamwork carries the autoDone counter flag',
427
+ teamwork.includes('--auto true --auto-done <n>'));
428
+ const autoRef = convertToCodex(fs.readFileSync(path.join(skillsDir, 'trailhead-work', 'references', 'auto.md'), 'utf8'));
429
+ ok('projected auto reference carries the autoDone counter flag',
430
+ autoRef.includes('--auto true --auto-done <n>'));
431
+ }
432
+
405
433
  console.log(`✓ codex-projection: ${passed} assertions passed`);
@@ -135,6 +135,9 @@ function readRegistry(registryFile) {
135
135
  // token expands to exactly hooksDir/<f>, OR an unresolved token's basename is
136
136
  // <f> (stays warning-only, never also missing).
137
137
  function checkRegisteredTargets(spec, missing, unverified) {
138
+ // Shipped files (hookFiles) may include modules that are only require()d;
139
+ // the files that must be registered are the entrypoints (default: all shipped).
140
+ const entrypoints = spec.entrypointFiles || spec.hookFiles;
138
141
  const registry = readRegistry(spec.registryFile);
139
142
  const registryName = path.basename(spec.registryFile);
140
143
  const events = (registry && typeof registry.hooks === 'object' && registry.hooks) ? registry.hooks : {};
@@ -168,7 +171,7 @@ function checkRegisteredTargets(spec, missing, unverified) {
168
171
  }
169
172
  }
170
173
 
171
- for (const f of spec.hookFiles) {
174
+ for (const f of entrypoints) {
172
175
  if (!registeredExact.has(f) && !registeredViaUnresolved.has(f)) {
173
176
  missing.push(`hook registration ${f} (${registryName})`);
174
177
  }
@@ -185,8 +188,8 @@ function checkAgents(spec, missing) {
185
188
  }
186
189
 
187
190
  // --- verifyInstall --------------------------------------------------------------
188
- // spec = { skillsRoot, skillDirs, sharedFiles, hooksDir, hookFiles,
189
- // hookLibFiles, registryFile, agentsDir, agentPattern, env, home }
191
+ // spec = { skillsRoot, skillDirs, sharedFiles, hooksDir, hookFiles (shipped),
192
+ // entrypointFiles (registered; defaults to hookFiles), hookLibFiles, registryFile, agentsDir, agentPattern, env, home }
190
193
  function verifyInstall(spec) {
191
194
  const missing = [];
192
195
  const unverified = [];
@@ -199,13 +202,14 @@ function verifyInstall(spec) {
199
202
  }
200
203
 
201
204
  // --- spec builders ---------------------------------------------------------
202
- function claudeVerifySpec(configDir, { skillDirs, sharedFiles, hookFiles, hookLibFiles, env, home }) {
205
+ function claudeVerifySpec(configDir, { skillDirs, sharedFiles, hookFiles, entrypointFiles, hookLibFiles, env, home }) {
203
206
  return {
204
207
  skillsRoot: path.join(configDir, 'skills'),
205
208
  skillDirs,
206
209
  sharedFiles,
207
210
  hooksDir: path.join(configDir, 'hooks'),
208
211
  hookFiles,
212
+ entrypointFiles,
209
213
  hookLibFiles,
210
214
  registryFile: path.join(configDir, 'settings.json'),
211
215
  agentsDir: path.join(configDir, 'agents'),
@@ -215,7 +219,7 @@ function claudeVerifySpec(configDir, { skillDirs, sharedFiles, hookFiles, hookLi
215
219
  };
216
220
  }
217
221
 
218
- function codexVerifySpec(configDir, { skillDirs, sharedFiles, hookFiles, hookLibFiles, env, home }) {
222
+ function codexVerifySpec(configDir, { skillDirs, sharedFiles, hookFiles, entrypointFiles, hookLibFiles, env, home }) {
219
223
  const L = codexLayout(configDir);
220
224
  return {
221
225
  skillsRoot: L.skillsRoot,
@@ -223,6 +227,7 @@ function codexVerifySpec(configDir, { skillDirs, sharedFiles, hookFiles, hookLib
223
227
  sharedFiles,
224
228
  hooksDir: L.hooksScriptsDir,
225
229
  hookFiles,
230
+ entrypointFiles,
226
231
  hookLibFiles,
227
232
  registryFile: L.hooksJson,
228
233
  agentsDir: L.codexAgentsDir,
@@ -359,6 +359,57 @@ function buildCodexFixture() {
359
359
  r.unverified.some((u) => u.includes('CLAUDE_PLUGIN_ROOT undefined')));
360
360
  }
361
361
 
362
+ // --- shipped files vs registered entrypoints (#207) ---------------------------
363
+ // The guard modules are shipped (they must exist) but only require()d by the
364
+ // merged hook, so they are never required to be registered.
365
+ function buildMergedFixture(host) {
366
+ const configDir = mktmp();
367
+ const skillDirs = ['trailhead', '_shared'];
368
+ const sharedFiles = ['load-first.md'];
369
+ const modules = ['trailhead-commit-guard.js', 'trailhead-body-guard.js'];
370
+ const entrypointFiles = ['trailhead-bash-guards.js'];
371
+ const hookFiles = [...modules, ...entrypointFiles];
372
+ const hooksDir = host === 'codex' ? path.join(configDir, 'skills', 'trailhead', 'hooks') : path.join(configDir, 'hooks');
373
+ write(path.join(configDir, 'skills', 'trailhead', 'SKILL.md'), 'x');
374
+ write(path.join(configDir, 'skills', '_shared', 'load-first.md'), 'x');
375
+ for (const f of hookFiles) write(path.join(hooksDir, f), 'x');
376
+ write(path.join(configDir, 'agents', host === 'codex' ? 'trailhead-execute.toml' : 'trailhead-execute.md'), 'x');
377
+ const registry = { hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [
378
+ { type: 'command', command: `node "${path.join(hooksDir, 'trailhead-bash-guards.js')}"${host === 'codex' ? ' --guards=commit,body' : ''}` },
379
+ ] }] } };
380
+ write(path.join(configDir, host === 'codex' ? 'hooks.json' : 'settings.json'), JSON.stringify(registry, null, 2));
381
+ const build = host === 'codex' ? codexVerifySpec : claudeVerifySpec;
382
+ const spec = build(configDir, { skillDirs, sharedFiles, hookFiles, entrypointFiles, hookLibFiles: [], env: {}, home: H });
383
+ return { configDir, spec, hooksDir };
384
+ }
385
+ for (const host of ['claude', 'codex']) {
386
+ {
387
+ const { spec } = buildMergedFixture(host);
388
+ const r = verifyInstall(spec);
389
+ ok(`${host}: passes with the guard modules shipped but unregistered`, r.missing.length === 0 && r.unverified.length === 0);
390
+ }
391
+ {
392
+ const { spec, hooksDir } = buildMergedFixture(host);
393
+ fs.rmSync(path.join(hooksDir, 'trailhead-commit-guard.js'));
394
+ const r = verifyInstall(spec);
395
+ ok(`${host}: a shipped guard module that is missing is still reported`, r.missing.includes('hook script trailhead-commit-guard.js'));
396
+ }
397
+ {
398
+ const { configDir, spec } = buildMergedFixture(host);
399
+ write(path.join(configDir, host === 'codex' ? 'hooks.json' : 'settings.json'), JSON.stringify({ hooks: {} }));
400
+ const r = verifyInstall(spec);
401
+ ok(`${host}: fails when the registered entrypoint is not registered`,
402
+ r.missing.some((m) => m.startsWith('hook registration trailhead-bash-guards.js')));
403
+ }
404
+ {
405
+ const { spec, hooksDir } = buildMergedFixture(host);
406
+ fs.rmSync(path.join(hooksDir, 'trailhead-bash-guards.js'));
407
+ const r = verifyInstall(spec);
408
+ ok(`${host}: fails when the registered entrypoint file is missing`,
409
+ r.missing.includes('hook script trailhead-bash-guards.js') && r.missing.some((m) => m.startsWith('registered hook target')));
410
+ }
411
+ }
412
+
362
413
  // --- formatters --------------------------------------------------------------
363
414
  {
364
415
  const out = formatVerifyFailure('Claude Code', '/c/dir', { missing: ['skill trailhead-work/SKILL.md'], unverified: ['/x/trailhead-y.js (PreToolUse, settings.json: $Y undefined)'] });
package/bin/trailhead.js CHANGED
@@ -243,18 +243,27 @@ function claudePaths(configDir) {
243
243
  settings: path.join(configDir, 'settings.json'),
244
244
  };
245
245
  }
246
- 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-mockup-link-guard.js', 'trailhead-mockup-link-stop.js', 'trailhead-check-update.js'];
246
+ // The five Bash guard modules. They are shipped, but only require()d by the
247
+ // merged trailhead-bash-guards.js (one process per Bash call), never registered
248
+ // on their own. Legacy installs registered each one; the installers strip those.
249
+ const BASH_GUARD_MODULES = ['trailhead-commit-guard.js', 'trailhead-secret-guard.js', 'trailhead-install-guard.js', 'trailhead-search-guard.js', 'trailhead-body-guard.js'];
250
+ // Every hook script copied into the hooks dir (shipped runtime files).
251
+ const HOOK_FILES = [...BASH_GUARD_MODULES, 'trailhead-bash-guards.js', 'trailhead-issue-injection-scanner.js', 'trailhead-secret-read-guard.js', 'trailhead-mockup-link-guard.js', 'trailhead-mockup-link-stop.js', 'trailhead-check-update.js'];
252
+ // The registered entrypoints: the shipped files that must appear in
253
+ // settings.json / hooks.json (everything but the require()d guard modules).
254
+ const ENTRYPOINT_FILES = HOOK_FILES.filter((f) => !BASH_GUARD_MODULES.includes(f));
247
255
  // Runtime libs the hook scripts require (trailhead-commit-guard.js does
248
256
  // require('./lib/commit-message-check.js'); trailhead-search-guard.js and
249
257
  // trailhead-secret-read-guard.js both do require('./lib/shell-scan.js'), #135
250
258
  // follow-up; trailhead-body-guard.js and trailhead-secret-guard.js both do
251
- // require('./lib/gh-subcommand.js'), #153 follow-up; trailhead-check-update.js
259
+ // require('./lib/gh-subcommand.js'), #153 follow-up; trailhead-secret-guard.js
260
+ // also does require('./lib/body-sources.js'), #210; trailhead-check-update.js
252
261
  // does require('./lib/model-defaults-review.js'), which in turn reads the
253
262
  // sibling model-defaults.json, #186; both mockup-link hooks do
254
263
  // require('./lib/mockup-link.js')). Copied by name, never a recursive
255
264
  // sweep: the Claude hooks/lib dir is shared with other plugins, so we must
256
265
  // not clobber a co-tenant's lib nor ship our own *.test.js files.
257
- const HOOK_LIB_FILES = ['commit-message-check.js', 'shell-scan.js', 'gh-subcommand.js', 'model-defaults-review.js', 'model-defaults.json', 'mockup-link.js'];
266
+ const HOOK_LIB_FILES = ['commit-message-check.js', 'shell-scan.js', 'gh-subcommand.js', 'body-sources.js', 'unmodelled.js', 'model-defaults-review.js', 'model-defaults.json', 'mockup-link.js'];
258
267
  // search-guard is Claude-Code-specific (#169), and so is mockup-link-guard
259
268
  // (it gates the AskUserQuestion tool, which Codex lacks): excluded from both
260
269
  // the Codex hook copy and the Codex verify spec's expected hookFiles, so a
@@ -264,6 +273,10 @@ const CODEX_HOOK_EXCLUDE = ['trailhead-search-guard.js', 'trailhead-mockup-link-
264
273
  // Claude Code gates the AskUserQuestion call instead, so it is neither copied
265
274
  // nor expected on a Claude install.
266
275
  const CLAUDE_HOOK_EXCLUDE = ['trailhead-mockup-link-stop.js'];
276
+ const claudeShippedHooks = () => HOOK_FILES.filter((f) => !CLAUDE_HOOK_EXCLUDE.includes(f));
277
+ const claudeEntrypoints = () => ENTRYPOINT_FILES.filter((f) => !CLAUDE_HOOK_EXCLUDE.includes(f));
278
+ const codexShippedHooks = () => HOOK_FILES.filter((f) => !CODEX_HOOK_EXCLUDE.includes(f));
279
+ const codexEntrypoints = () => ENTRYPOINT_FILES.filter((f) => !CODEX_HOOK_EXCLUDE.includes(f));
267
280
 
268
281
  // Copy the hook scripts and their runtime libs into a destination hooks dir
269
282
  // (Claude's ~/.claude/hooks or Codex's skills/trailhead/hooks). Shared by both
@@ -357,6 +370,12 @@ function stripHook(s, event, substr) {
357
370
  s.hooks[event] = s.hooks[event].filter((g) => (g.hooks || []).length);
358
371
  }
359
372
 
373
+ // Drop the per-guard PreToolUse registrations an earlier install made (and the
374
+ // pre-#169 Codex search-guard one): the merged entrypoint replaces all of them.
375
+ function stripLegacyBashGuards(s) {
376
+ for (const f of BASH_GUARD_MODULES) stripHook(s, 'PreToolUse', f);
377
+ }
378
+
360
379
  function uninstallClaude(configDir) {
361
380
  const P = claudePaths(configDir);
362
381
  // Sweep every trailhead-owned skill dir (current set + any stale dir a prior
@@ -371,12 +390,9 @@ function uninstallClaude(configDir) {
371
390
  // with other plugins, so never rmrf the whole dir.
372
391
  HOOK_LIB_FILES.forEach((f) => rmrf(path.join(P.hooksDir, 'lib', f)));
373
392
  const s = readJSON(P.settings);
374
- stripHook(s, 'PreToolUse', 'trailhead-commit-guard.js');
375
- stripHook(s, 'PreToolUse', 'trailhead-secret-guard.js');
376
- stripHook(s, 'PreToolUse', 'trailhead-install-guard.js');
377
- stripHook(s, 'PreToolUse', 'trailhead-search-guard.js');
393
+ stripLegacyBashGuards(s);
394
+ stripHook(s, 'PreToolUse', 'trailhead-bash-guards.js');
378
395
  stripHook(s, 'PreToolUse', 'trailhead-secret-read-guard.js');
379
- stripHook(s, 'PreToolUse', 'trailhead-body-guard.js');
380
396
  stripHook(s, 'PreToolUse', 'trailhead-mockup-link-guard.js');
381
397
  stripHook(s, 'PostToolUse', 'trailhead-issue-injection-scanner.js');
382
398
  stripHook(s, 'SessionStart', 'trailhead-check-update.js');
@@ -419,12 +435,11 @@ function installClaude(configDir, { useSymlink }) {
419
435
  }
420
436
 
421
437
  const s = readJSON(P.settings);
438
+ // Migration: an earlier install registered each Bash guard on its own; the
439
+ // merged entrypoint below replaces all five.
440
+ stripLegacyBashGuards(s);
422
441
  const added = [
423
- addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-commit-guard.js')),
424
- addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-secret-guard.js')),
425
- addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-install-guard.js')),
426
- addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-search-guard.js')),
427
- addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-body-guard.js')),
442
+ addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-bash-guards.js')),
428
443
  addHook(s, 'PreToolUse', 'Read|Bash', hookCmd('trailhead-secret-read-guard.js')),
429
444
  addHook(s, 'PreToolUse', 'AskUserQuestion', hookCmd('trailhead-mockup-link-guard.js')),
430
445
  addHook(s, 'PostToolUse', 'Bash', hookCmd('trailhead-issue-injection-scanner.js')),
@@ -438,7 +453,8 @@ function installClaude(configDir, { useSymlink }) {
438
453
  const spec = claudeVerifySpec(configDir, {
439
454
  skillDirs: shippedSkillDirs(),
440
455
  sharedFiles: readSharedCoreFiles(),
441
- hookFiles: HOOK_FILES.filter((f) => !CLAUDE_HOOK_EXCLUDE.includes(f)),
456
+ hookFiles: claudeShippedHooks(),
457
+ entrypointFiles: claudeEntrypoints(),
442
458
  hookLibFiles: HOOK_LIB_FILES,
443
459
  env: process.env,
444
460
  home: os.homedir(),
@@ -653,7 +669,7 @@ function installCodex(configDir, { useSymlink }) {
653
669
  // Migration: strip a pre-#169 search-guard registration. Its target is
654
670
  // always gone now (search-guard is never copied to Codex), so a prior
655
671
  // install's stale entry would otherwise fail the post-install check below.
656
- stripHook(h, 'PreToolUse', 'trailhead-search-guard.js');
672
+ stripLegacyBashGuards(h);
657
673
  for (const e of codexHookEntries(L.hooksScriptsDir)) addHook(h, e.event, e.matcher, e.command);
658
674
  writeJSON(L.hooksJson, h);
659
675
 
@@ -709,7 +725,8 @@ function installCodex(configDir, { useSymlink }) {
709
725
  const spec = codexVerifySpec(configDir, {
710
726
  skillDirs: shippedSkillDirs(),
711
727
  sharedFiles: readSharedCoreFiles(),
712
- hookFiles: HOOK_FILES.filter((f) => !CODEX_HOOK_EXCLUDE.includes(f)),
728
+ hookFiles: codexShippedHooks(),
729
+ entrypointFiles: codexEntrypoints(),
713
730
  hookLibFiles: HOOK_LIB_FILES,
714
731
  env: process.env,
715
732
  home: os.homedir(),
@@ -772,16 +789,11 @@ function uninstallCodex(configDir) {
772
789
  // settings.json flags alone.
773
790
  if (fs.existsSync(L.hooksJson)) {
774
791
  const h = readJSON(L.hooksJson);
775
- stripHook(h, 'PreToolUse', 'trailhead-commit-guard.js');
776
- stripHook(h, 'PreToolUse', 'trailhead-secret-guard.js');
777
- stripHook(h, 'PreToolUse', 'trailhead-install-guard.js');
778
- // search-guard is no longer projected to Codex (#169: it is
779
- // Claude-Code-specific), so it is never registered by a current install;
780
- // this strip is retained only to clean up a stale entry left by a
781
- // pre-#169 install that did register it.
782
- stripHook(h, 'PreToolUse', 'trailhead-search-guard.js');
792
+ // The merged Bash guard entry, plus every legacy per-guard registration
793
+ // (commit, secret, install, body, and the pre-#169 search-guard one).
794
+ stripHook(h, 'PreToolUse', 'trailhead-bash-guards.js');
795
+ stripLegacyBashGuards(h);
783
796
  stripHook(h, 'PreToolUse', 'trailhead-secret-read-guard.js');
784
- stripHook(h, 'PreToolUse', 'trailhead-body-guard.js');
785
797
  stripHook(h, 'PostToolUse', 'trailhead-issue-injection-scanner.js');
786
798
  stripHook(h, 'Stop', 'trailhead-mockup-link-stop.js');
787
799
  stripHook(h, 'SessionStart', 'trailhead-check-update.js');