@marcomigozzi/trailhead 0.4.0 → 0.5.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 (65) hide show
  1. package/README.it.md +19 -14
  2. package/README.md +20 -15
  3. package/bin/lib/codex-projection.js +23 -2
  4. package/bin/lib/codex-projection.test.js +14 -0
  5. package/bin/trailhead.js +99 -36
  6. package/bin/trailhead.test.js +130 -5
  7. package/package.json +1 -1
  8. package/plugins/trailhead/.claude-plugin/plugin.json +7 -2
  9. package/plugins/trailhead/commands/adopt.md +1 -1
  10. package/plugins/trailhead/commands/bug.md +1 -1
  11. package/plugins/trailhead/commands/config.md +1 -1
  12. package/plugins/trailhead/commands/dashboard.md +1 -1
  13. package/plugins/trailhead/commands/grill.md +1 -1
  14. package/plugins/trailhead/commands/idea.md +1 -1
  15. package/plugins/trailhead/commands/inbox.md +1 -1
  16. package/plugins/trailhead/commands/map.md +1 -1
  17. package/plugins/trailhead/commands/new.md +1 -1
  18. package/plugins/trailhead/commands/note.md +1 -1
  19. package/plugins/trailhead/commands/pause.md +1 -1
  20. package/plugins/trailhead/commands/quick.md +3 -1
  21. package/plugins/trailhead/commands/resume.md +1 -1
  22. package/plugins/trailhead/commands/seed.md +1 -1
  23. package/plugins/trailhead/commands/split.md +1 -1
  24. package/plugins/trailhead/commands/ticket.md +1 -1
  25. package/plugins/trailhead/commands/todo.md +1 -1
  26. package/plugins/trailhead/commands/update.md +1 -1
  27. package/plugins/trailhead/commands/whiteboard.md +1 -1
  28. package/plugins/trailhead/commands/work.md +1 -1
  29. package/plugins/trailhead/hooks/trailhead-commit-guard.js +1 -1
  30. package/plugins/trailhead/hooks/trailhead-secret-guard.js +19 -4
  31. package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +41 -1
  32. package/plugins/trailhead/skills/{trailhead/references/configuration.md → _shared/configuration-reference.md} +9 -3
  33. package/plugins/trailhead/skills/_shared/configuration.md +8 -0
  34. package/plugins/trailhead/skills/_shared/multi-map.md +27 -0
  35. package/plugins/trailhead/skills/_shared/out-of-scope.md +11 -0
  36. package/plugins/trailhead/skills/_shared/principles.md +8 -0
  37. package/plugins/trailhead/skills/_shared/session-handoff.md +22 -0
  38. package/plugins/trailhead/skills/_shared/substrate-commands.md +159 -0
  39. package/plugins/trailhead/skills/_shared/substrate.md +35 -0
  40. package/plugins/trailhead/skills/{trailhead/references → _shared}/teamwork.md +5 -4
  41. package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/acceptance-testing.md +3 -2
  42. package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/code-review.md +5 -1
  43. package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/plan-review.md +4 -7
  44. package/plugins/trailhead/skills/_shared/techniques/prototype.md +36 -0
  45. package/plugins/trailhead/skills/_shared/techniques.md +20 -0
  46. package/plugins/trailhead/skills/_shared/ticket-anatomy.md +32 -0
  47. package/plugins/trailhead/skills/_shared/ticket-language.md +3 -0
  48. package/plugins/trailhead/skills/trailhead/SKILL.md +25 -452
  49. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +28 -0
  50. package/plugins/trailhead/skills/{trailhead → trailhead-capture}/references/capture.md +4 -4
  51. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +51 -0
  52. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +24 -0
  53. package/plugins/trailhead/skills/{trailhead → trailhead-chart}/references/inbox.md +1 -1
  54. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +29 -0
  55. package/plugins/trailhead/skills/trailhead-view/SKILL.md +112 -0
  56. package/plugins/trailhead/skills/trailhead-work/SKILL.md +65 -0
  57. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +52 -0
  58. package/plugins/trailhead/skills/trailhead/references/techniques/prototype.md +0 -18
  59. /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/codebase-map.md +0 -0
  60. /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/debug.md +0 -0
  61. /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/domain-vocabulary.md +0 -0
  62. /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/grilling.md +0 -0
  63. /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/research.md +0 -0
  64. /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/tdd.md +0 -0
  65. /package/plugins/trailhead/skills/{trailhead → trailhead-manage}/references/updating.md +0 -0
package/README.it.md CHANGED
@@ -1,10 +1,16 @@
1
- # trailhead
1
+ <div align="center">
2
+
3
+ # Trailhead
2
4
 
3
5
  🇬🇧 [English](README.md) · 🇮🇹 **Italiano**
4
6
 
7
+ [![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-20%20verbi-0e8a16?style=flat-square) ![guardrail](https://img.shields.io/badge/guardrail-4%20hook-e8710a?style=flat-square)
8
+
9
+ </div>
10
+
5
11
  **Avvia e conduci grandi progetti come una mappa di ticket su GitHub Issues, risolvendone uno alla volta finché la via verso la destination non è chiara.**
6
12
 
7
- `trailhead` è una skill orchestratrice per [Claude Code](https://docs.claude.com/en/docs/claude-code). Dà a un'idea grande e nebbiosa un punto da cui partire (un *trailhead*) e un modo disciplinato per portarla fino a un risultato funzionante, con l'intero piano che vive sul tuo issue tracker invece che in file locali sparsi.
13
+ `trailhead` è una skill orchestratrice per coding agent, che gira su [Claude Code](https://docs.claude.com/en/docs/claude-code) e [Codex CLI](https://developers.openai.com/codex/cli) da un'unica sorgente. Dà a un'idea grande e nebbiosa un punto da cui partire (un *trailhead*) e un modo disciplinato per portarla fino a un risultato funzionante, con l'intero piano che vive sul tuo issue tracker invece che in file locali sparsi.
8
14
 
9
15
  ---
10
16
 
@@ -18,7 +24,7 @@ Due approcci al lavoro di progetto guidato dagli agenti risolvono ciascuno una m
18
24
 
19
25
  Lavorando su entrambi, uno schema continuava a ripresentarsi: **usa una mappa e un onboarding in stile Wayfinder per decidere la forma del lavoro, poi un motore in stile GSD per costruire ogni pezzo, senza mai lasciare l'issue tracker.** `trailhead` è quello schema trasformato in un'unica skill, una sua interpretazione autonoma di entrambe le idee anziché un wrapper attorno all'una o all'altra.
20
26
 
21
- > **trailhead è self-contained.** Wayfinder e GSD sono *ispirazione*, non dipendenze. Ogni tecnica che quei sistemi impacchettano come skill separata (grilling, TDD, debugging sistematico, codebase mapping, code review) è **integrata qui come protocollo inline**. Il core di trailhead non invoca nessun'altra skill; i suoi subagent (research, codebase-map, review) usano i subagent nativi dell'host (il tool `Agent` integrato di Claude Code, oppure il toolkit `multi_agent` di Codex). L'unica cosa da installare è trailhead stesso. C'è un'unica eccezione opt-in: quando abilitata, la modalità mockup di `claude.ai/design` invia a un progetto design-system tramite DesignSync (l'MCP `claude_design` più la skill `/design-sync`). Se questa non è disponibile, ripiega silenziosamente sui mockup su disco locale.
27
+ > **trailhead è self-contained.** Wayfinder e GSD sono *ispirazione*, non dipendenze. Ogni tecnica che quei sistemi impacchettano come skill separata (grilling, TDD, debugging sistematico, codebase mapping, code review) è **integrata qui come protocollo inline**. Il core di trailhead non invoca nessun'altra skill; i suoi subagent (research, codebase-map, review) usano i subagent nativi dell'host (il tool `Agent` integrato di Claude Code, oppure il toolkit `multi_agent` di Codex). L'unica cosa da installare è trailhead stesso. C'è un'unica eccezione opt-in: la modalità mockup hosted. Quando abilitata, `claude.ai/design` costruisce il mockup su claude.ai/design (solo Claude): per default una canvas (la skill integrata `/design` pubblicata come Artifact, o un progetto creato a mano nella web app, non guidato da MCP) o, per una libreria di componenti condivisa, un progetto design-system tramite DesignSync (l'MCP `claude-design` più la skill `/design-sync`, l'unica superficie guidata da MCP). Una terza modalità, `stitch`, usa l'MCP Google Stitch e funziona sia su **Claude Code sia su Codex**, quindi è l'opzione hosted cross-host. Se l'MCP della modalità configurata non è connesso, offre un'installazione una tantum e ripiega sui mockup su disco locale se rifiuti; su Codex, dove `claude.ai/design` non è disponibile, offre di passare a `stitch` prima di ripiegare sul disco locale.
22
28
 
23
29
  Le scelte di design che ne derivano:
24
30
 
@@ -62,7 +68,7 @@ npx @marcomigozzi/trailhead --dir=<path> # punta a una config dir specifica
62
68
  ```
63
69
  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.
64
70
 
65
- 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 quattro 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, quindi lì i mockup UI ripiegano sul disco locale.
71
+ 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 quattro 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.
66
72
 
67
73
  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).
68
74
 
@@ -234,7 +240,7 @@ Due regole pratiche: i build ticket **non fanno mai auto-grill**: su un'ambiguit
234
240
  Tutto ciò di cui la mappa ha bisogno è espresso come label GitHub, così lo stato è interrogabile nella UI del tracker:
235
241
 
236
242
  - **Strutturali:** `trailhead:map`, `trailhead:ticket`
237
- - **Anchor con scope di repo (uno ciascuno per repo, pinnati):** `trailhead:codebase` 🧱 (la codebase map distillata), `trailhead:conventions` 📜 (il modo di lavorare), `trailhead:dashboard` 📊 (l'indice pinnato di mappe + whiteboard + conteggi) — ogni titolo porta la sua icona (come il 🗺 della mappa) così gli anchor pinnati si distinguono a colpo d'occhio
243
+ - **Anchor con scope di repo (uno ciascuno per repo, pinnati):** `trailhead:codebase` 🧱 (la codebase map distillata), `trailhead:conventions` 📜 (il modo di lavorare), `trailhead:dashboard` 📊 (l'indice pinnato di mappe + whiteboard + conteggi); ogni titolo porta la sua icona (come il 🗺 della mappa) così gli anchor pinnati si distinguono a colpo d'occhio
238
244
  - **Type (una per ticket):** 🧭 `trailhead:decision` · 🔬 `research` · 🎨 `prototype` · 🔨 `build` · 🐛 `bug` · 🔧 `task`
239
245
  - **State:** `trailhead:blocked` (ha un blocker aperto) · `seed` (parcheggiato su un trigger) · `out-of-scope` (chiuso, oltre la destination) · `superseded` (chiuso, splittato in figli)
240
246
  - **Container:** `trailhead:whiteboard` (un ticket sciolto, senza mappa, fuori dalla frontier di ogni mappa, sulla frontier propria della whiteboard)
@@ -289,13 +295,15 @@ Il passo **📊 statusline** offre di installare la status bar di trailhead per
289
295
  |---|---|---|
290
296
  | `ticket.language` | un codice ISO 639-1 (**`en`**) | la lingua in cui trailhead **scrive** la sua prosa GitHub e le descrizioni dei commit, disaccoppiata dalla lingua in cui conversa |
291
297
  | `models.{plan,execute,research,review,debug}` | un id modello completo **versionato** (**inherit session**) | quale modello esegue ogni attività; `plan` ed `execute` sono sempre impostati separatamente |
292
- | `design` | **`disk`** \| `claude.ai/design` | dove vanno i mockup UI: HTML usa-e-getta locale, o un progetto design-system su claude.ai/design via DesignSync |
298
+ | `design` | **`disk`** \| `claude.ai/design` \| `stitch` | dove vanno i mockup UI: HTML usa-e-getta locale, claude.ai/design via l'MCP ufficiale Claude Design di Anthropic, o Google Stitch (cross-host, via l'MCP Stitch) |
299
+ | `design.surface` | **`canvas`** \| `design-system` | sotto `claude.ai/design` o `stitch`, quale superficie: un progetto canvas (default) o un progetto design-system via `/design-sync` |
293
300
  | `design.approval` | **`explicit`** \| `auto` | attendi l'approvazione del mockup prima del codice UI, o procedi senza bloccare |
294
301
  | `tdd` | **`seams`** \| `on` \| `off` | come il motore `build` testa |
295
302
  | `acceptance.browser` | **`auto`** \| `on` \| `off` | guida il browser in Verify, o ti accompagna in una UAT conversazionale (passo-passo in chat, non una checklist da fare da solo) |
296
303
  | `testing.webapp` / `testing.url` | bool / URL | è browser-drivabile, e dove |
297
304
  | `plan_review` | **`off`** \| `on` \| CLI list | invia i PLAN di `build` ad AI CLI esterne (Gemini, Codex, …) per un secondo parere e converge sulle loro obiezioni |
298
305
  | `plan_review.rounds` | integer (**`2`**) | numero massimo di round converge-and-re-review |
306
+ | `code_review.rounds` | integer (**`2`**) | numero massimo di round fix-and-re-review in Code review prima del checkpoint HITL di confine |
299
307
 
300
308
  **Models.** Ogni chiave esegue la sua attività come **subagent** sul modello che nomini, così l'intera suddivisione per-attività si applica dentro una singola sessione di lavoro, qualunque modello quella sessione esegua:
301
309
 
@@ -312,17 +320,14 @@ Esempio di file di config (la stessa forma funziona per il `.trailhead/config.js
312
320
  { "ticket": { "language": "en" }, "models": { "plan": "claude-opus-4-8", "execute": "claude-sonnet-5" }, "tdd": "seams", "acceptance": { "browser": "auto" } }
313
321
  ```
314
322
 
315
- **Design mockups.** `design: disk` (il default) deposita un mockup HTML statico usa-e-getta accanto al codice e lo collega dal ticket. `design: claude.ai/design` invece invia il mockup a un **progetto design-system** su claude.ai/design via **DesignSync** (la skill `/design-sync` più l'MCP `claude_design`), dove lo affini visivamente.
316
-
317
- Alla prima schermata UI, trailhead chiede quale design system usare. Puoi:
323
+ **Design mockups.** `design: disk` (il default) deposita un mockup HTML statico usa-e-getta accanto al codice e lo collega dal ticket. `design: claude.ai/design` invece costruisce il mockup su claude.ai/design (canvas tramite la skill integrata `/design` o un progetto creato a mano nella web app; design-system tramite l'**MCP ufficiale Claude Design** di Anthropic, `claude mcp add --scope user --transport http claude-design https://api.anthropic.com/v1/design/mcp`, accedi con `/design-login`), dove lo affini visivamente, e `design: stitch` usa l'**MCP Google Stitch** (`claude mcp add --scope user --transport http stitch https://stitch.googleapis.com/mcp`), il generatore UI di Google basato su Gemini che gira sia su **Claude Code sia su Codex** (l'opzione hosted cross-host). Entrambe onorano `design.surface`:
318
324
 
319
- - **scegliere tra i tuoi 10 più recenti**,
320
- - **incollare un URL `claude.ai/design/p/<id>`**, oppure
321
- - **crearne uno nuovo** (ti chiede il nome).
325
+ - **`canvas`** (default): una schermata singola a cui reagire, modificabile ed esportabile come prototipo live. Sotto `claude.ai/design` è la skill integrata `/design` (pubblicata come Artifact) o un progetto claude.ai/design creato a mano nella web app; sotto `stitch` è un progetto Stitch.
326
+ - **`design-system`**: un progetto design-system guidato da **DesignSync** (la skill `/design-sync` più l'MCP `claude-design`), per una libreria di componenti condivisa tenuta in sync.
322
327
 
323
- Fa il caching dell'id scelto in `design.project`. Ogni schermata viene inviata lì, e la sua URL è collegata dal ticket. Una volta che approvi, trailhead **ri-fetcha** il design corrente (nel caso tu l'abbia modificato live) prima di scrivere qualsiasi codice UI.
328
+ Alla prima schermata UI, trailhead si aggancia a una superficie: per un progetto **Stitch** o un **design-system claude.ai/design** può **sceglierne uno esistente**, **incollare un URL**, oppure **crearne uno nuovo** (ti chiede il nome); per una **canvas claude.ai/design** non esiste la creazione via MCP, quindi usa la skill `/design` (un **Artifact**) o un progetto web-app di cui incolli l'URL `/p/<id>`. Fa il caching dell'id/URL scelto in `design.project`; un progetto già in cache da prima mantiene la superficie `design-system` a meno che tu non imposti `design.surface`, così i setup esistenti non cambiano. Ogni schermata viene aggiunta lì, e la sua URL è collegata dal ticket. Una volta che approvi, trailhead **ri-fetcha** il design corrente (nel caso tu l'abbia modificato live) prima di scrivere qualsiasi codice UI.
324
329
 
325
- DesignSync guida solo i progetti design-system, non quelli normali; per un progetto normale posizioni il mockup a mano. `design.approval` decide se il build attende il tuo esplicito via libera (`explicit`), o procede subito dopo aver mostrato il mockup (`auto`).
330
+ Se l'MCP della modalità hosted non è connesso, trailhead **offre di installarlo una volta** (`claude mcp add ...`); se rifiuti, ripiega sul disco locale. (La superficie **canvas** di `claude.ai/design` non richiede MCP, quindi per lei non viene offerto nulla; solo la superficie **design-system** ha l'offerta di installazione.) Su Codex `claude.ai/design` non è disponibile, quindi trailhead offre di passare a `stitch` (cross-host) prima di ripiegare sul disco locale. `design.approval` decide se il build attende il tuo esplicito via libera (`explicit`), o procede subito dopo aver mostrato il mockup (`auto`); non tocca l'offerta di installazione una tantum, che compare in entrambi i casi.
326
331
 
327
332
  ---
328
333
 
package/README.md CHANGED
@@ -1,10 +1,16 @@
1
- # trailhead
1
+ <div align="center">
2
+
3
+ # Trailhead
2
4
 
3
5
  🇬🇧 **English** · 🇮🇹 [Italiano](README.it.md)
4
6
 
7
+ [![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-20%20verbs-0e8a16?style=flat-square) ![guardrails](https://img.shields.io/badge/guardrails-4%20hooks-e8710a?style=flat-square)
8
+
9
+ </div>
10
+
5
11
  **Start and drive large projects as a map of tickets on GitHub Issues, resolving one at a time until the way to the destination is clear.**
6
12
 
7
- `trailhead` is an orchestrator skill for [Claude Code](https://docs.claude.com/en/docs/claude-code). It gives a big, foggy idea a place to begin (a *trailhead*) and a disciplined way to walk it to a working result, with the whole plan living on your issue tracker instead of in scattered local files.
13
+ `trailhead` is an orchestrator skill for coding agents, running on [Claude Code](https://docs.claude.com/en/docs/claude-code) and [Codex CLI](https://developers.openai.com/codex/cli) from a single source. It gives a big, foggy idea a place to begin (a *trailhead*) and a disciplined way to walk it to a working result, with the whole plan living on your issue tracker instead of in scattered local files.
8
14
 
9
15
  ---
10
16
 
@@ -16,9 +22,9 @@ Two approaches to agent-driven project work each nail one half of the problem:
16
22
 
17
23
  - **GSD** (the [open-gsd](https://github.com/open-gsd/gsd-core) workflow system) is brilliant at *doing*. Its `discuss → plan → execute → verify` cycle, atomic commits, and zero-friction capture make the actual construction reliable and reviewable. But its state lives in a local `.planning/` tree, separate from where the team can see it.
18
24
 
19
- Working across both, one pattern kept recurring: **use a Wayfinder-style map and onboarding to decide the shape of the work, then a GSD-style engine to build each piece, without ever leaving the issue tracker.** `trailhead` is that pattern made into a single skill, its own self-contained take on both ideas rather than a wrapper around either.
25
+ Working across both, one pattern kept recurring: **use a Wayfinder-style map and onboarding to decide the shape of the work, then a GSD-style engine to build each piece, without ever leaving the issue tracker.** `trailhead` is that pattern made into a set of cohesion-clustered skills over a shared core, its own self-contained take on both ideas rather than a wrapper around either.
20
26
 
21
- > **trailhead is self-contained.** Wayfinder and GSD are *inspiration*, not dependencies. Every technique those systems package as a separate skill (grilling, TDD, systematic debugging, codebase mapping, code review) is **built in here as an inline protocol**. trailhead's core invokes no other skill; its subagents (research, codebase-map, review) use the host's native subagents (Claude Code's built-in `Agent` tool, or Codex's `multi_agent` toolkit). The only thing to install is trailhead itself. There is one opt-in exception: when enabled, `claude.ai/design` mockup mode pushes to a design-system project via DesignSync (the `claude_design` MCP plus the `/design-sync` skill). If that isn't available, it silently falls back to local-disk mockups.
27
+ > **trailhead is self-contained.** Wayfinder and GSD are *inspiration*, not dependencies. Every technique those systems package as a separate skill (grilling, TDD, systematic debugging, codebase mapping, code review) is **built in here as an inline protocol**. trailhead's core invokes no other skill; its subagents (research, codebase-map, review) use the host's native subagents (Claude Code's built-in `Agent` tool, or Codex's `multi_agent` toolkit). The only thing to install is trailhead itself. There is one opt-in exception: hosted mockup mode. When enabled, `claude.ai/design` builds the mockup on claude.ai/design (Claude only): by default a canvas (the built-in `/design` skill published as an Artifact, or a hand-made web-app project, not MCP-driven) or, for a shared component library, a design-system project via DesignSync (the `claude-design` MCP plus the `/design-sync` skill, the only MCP-driven surface). A third mode, `stitch`, uses the Google Stitch MCP and works on **both Claude Code and Codex**, so it's the cross-host hosted option. If a hosted mode's MCP isn't connected, it offers a one-time install and falls back to local-disk mockups if you decline; on Codex, where `claude.ai/design` isn't available, it offers to switch to `stitch` before falling to local disk.
22
28
 
23
29
  The design choices that fall out of it:
24
30
 
@@ -62,7 +68,7 @@ npx @marcomigozzi/trailhead --dir=<path> # target a specific config dir
62
68
  ```
63
69
  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.
64
70
 
65
- 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 four 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, so UI mockups fall back to local disk there.
71
+ 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 four 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.
66
72
 
67
73
  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).
68
74
 
@@ -234,7 +240,7 @@ Two rules of thumb: build tickets **never auto-grill**: on blocking ambiguity th
234
240
  Everything the map needs is expressed as GitHub labels, so state is queryable in the tracker UI:
235
241
 
236
242
  - **Structural:** `trailhead:map`, `trailhead:ticket`
237
- - **Repo-scoped anchors (one each per repo, pinned):** `trailhead:codebase` 🧱 (the distilled codebase map), `trailhead:conventions` 📜 (the way of working), `trailhead:dashboard` 📊 (the pinned index of maps + whiteboard + counts) — each title carries its icon (like the map's 🗺) so the pinned anchors stand apart at a glance
243
+ - **Repo-scoped anchors (one each per repo, pinned):** `trailhead:codebase` 🧱 (the distilled codebase map), `trailhead:conventions` 📜 (the way of working), `trailhead:dashboard` 📊 (the pinned index of maps + whiteboard + counts); each title carries its icon (like the map's 🗺) so the pinned anchors stand apart at a glance
238
244
  - **Type (one per ticket):** 🧭 `trailhead:decision` · 🔬 `research` · 🎨 `prototype` · 🔨 `build` · 🐛 `bug` · 🔧 `task`
239
245
  - **State:** `trailhead:blocked` (has an open blocker) · `seed` (parked on a trigger) · `out-of-scope` (closed, beyond the destination) · `superseded` (closed, split into children)
240
246
  - **Container:** `trailhead:whiteboard` (a loose, map-less ticket, off every map's frontier, on the whiteboard's own)
@@ -289,13 +295,15 @@ The **📊 statusline** step offers to install trailhead's Claude Code status ba
289
295
  |---|---|---|
290
296
  | `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 |
291
297
  | `models.{plan,execute,research,review,debug}` | a full **versioned** model id (**inherit session**) | which model runs each activity; `plan` and `execute` are always set separately |
292
- | `design` | **`disk`** \| `claude.ai/design` | where UI mockups go: local throwaway HTML, or a design-system project on claude.ai/design via DesignSync |
298
+ | `design` | **`disk`** \| `claude.ai/design` \| `stitch` | where UI mockups go: local throwaway HTML, claude.ai/design via Anthropic's official Claude Design MCP, or Google Stitch (cross-host, via the Stitch MCP) |
299
+ | `design.surface` | **`canvas`** \| `design-system` | under `claude.ai/design` or `stitch`, which surface: a canvas project (default) or a design-system project via `/design-sync` |
293
300
  | `design.approval` | **`explicit`** \| `auto` | wait for mockup approval before UI code, or proceed without blocking |
294
301
  | `tdd` | **`seams`** \| `on` \| `off` | how the `build` engine tests |
295
302
  | `acceptance.browser` | **`auto`** \| `on` \| `off` | drive the browser in Verify, or walk you through a conversational UAT (step by step in chat, not a checklist to self-serve) |
296
303
  | `testing.webapp` / `testing.url` | bool / URL | is it browser-drivable, and where |
297
304
  | `plan_review` | **`off`** \| `on` \| CLI list | send `build` PLANs to external AI CLIs (Gemini, Codex, …) for a second opinion and converge on their concerns |
298
305
  | `plan_review.rounds` | integer (**`2`**) | max converge-and-re-review rounds |
306
+ | `code_review.rounds` | integer (**`2`**) | max fix-and-re-review rounds in Code review before its boundary HITL checkpoint |
299
307
 
300
308
  **Models.** Each key runs its activity as a **subagent** on the model you name, so the whole per-activity split applies within a single work session, whatever model that session runs on:
301
309
 
@@ -312,17 +320,14 @@ Example config file (the same shape works for the project `.trailhead/config.jso
312
320
  { "ticket": { "language": "en" }, "models": { "plan": "claude-opus-4-8", "execute": "claude-sonnet-5" }, "tdd": "seams", "acceptance": { "browser": "auto" } }
313
321
  ```
314
322
 
315
- **Design mockups.** `design: disk` (the default) drops a throwaway static HTML mockup next to the code and links it from the ticket. `design: claude.ai/design` instead pushes the mockup to a **design-system project** on claude.ai/design via **DesignSync** (the `/design-sync` skill plus the `claude_design` MCP), where you refine it visually.
316
-
317
- On the first UI screen, trailhead asks which design system to use. You can:
323
+ **Design mockups.** `design: disk` (the default) drops a throwaway static HTML mockup next to the code and links it from the ticket. `design: claude.ai/design` instead builds the mockup on claude.ai/design (canvas via the built-in `/design` skill or a hand-made web-app project; design-system via Anthropic's **official Claude Design MCP**, `claude mcp add --scope user --transport http claude-design https://api.anthropic.com/v1/design/mcp`, sign in with `/design-login`), where you refine it visually, and `design: stitch` uses the **Google Stitch MCP** (`claude mcp add --scope user --transport http stitch https://stitch.googleapis.com/mcp`), Google's Gemini-powered UI generator that runs on **both Claude Code and Codex** (the cross-host hosted option). Both honour `design.surface`:
318
324
 
319
- - **pick from your 10 most-recent**,
320
- - **paste a `claude.ai/design/p/<id>` URL**, or
321
- - **create a new one** (it asks you for the name).
325
+ - **`canvas`** (default): a one-off screen to react to, editable and exportable as a live prototype. Under `claude.ai/design` it's the built-in `/design` skill (published as an Artifact) or a claude.ai/design project made by hand in the web app; under `stitch` it's a Stitch project.
326
+ - **`design-system`**: a design-system project driven by **DesignSync** (the `/design-sync` skill plus the `claude-design` MCP), for a shared component library kept in sync.
322
327
 
323
- It caches the chosen id in `design.project`. Each screen is pushed there, and its URL is linked from the ticket. Once you approve, trailhead **re-fetches** the current design (in case you edited it live) before writing any UI code.
328
+ On the first UI screen, trailhead attaches to a surface: for a **Stitch** project or a **claude.ai/design design-system**, it can **pick an existing one**, **paste a URL**, or **create a new one** (it asks you for the name); for a **claude.ai/design canvas** there is no create-via-MCP, so it uses the `/design` skill (an **Artifact**) or a web-app project whose `/p/<id>` URL you **paste**. It caches the chosen id/URL in `design.project`; a project already cached from before keeps the `design-system` surface unless you set `design.surface`, so existing setups don't change. Each screen is added there, and its URL is linked from the ticket. Once you approve, trailhead **re-fetches** the current design (in case you edited it live) before writing any UI code.
324
329
 
325
- DesignSync drives only design-system projects, not regular ones; for a regular project you place the mockup by hand. `design.approval` decides whether the build waits for your explicit go-ahead (`explicit`), or proceeds right after surfacing the mockup (`auto`).
330
+ If a hosted mode's MCP isn't connected, trailhead **offers to install it once** (`claude mcp add ...`); decline and it falls back to local disk. (The `claude.ai/design` **canvas** surface needs no MCP, so nothing is offered for it; only the **design-system** surface has an install offer.) On Codex, `claude.ai/design` isn't available, so trailhead offers to switch to `stitch` (cross-host) before falling to local disk. `design.approval` decides whether the build waits for your explicit go-ahead (`explicit`), or proceeds right after surfacing the mockup (`auto`); it doesn't affect the one-time install offer, which prompts either way.
326
331
 
327
332
  ---
328
333
 
@@ -90,8 +90,8 @@ This trailhead engine is running on **Codex CLI**, not Claude Code. The installe
90
90
  ## A. Commands
91
91
  trailhead is a native Codex skill. Invoke it as \`$trailhead <verb>\` (e.g. \`$trailhead work\`, \`$trailhead new "idea"\`); bare \`$trailhead\` is smart entry. Never \`/trailhead:<verb>\` (that is the Claude Code surface). Whatever you type after the verb is the arguments. For discoverability the installer also projects one thin skill per verb (\`$trailhead-work\`, \`$trailhead-bug\`, …) that just delegates to \`$trailhead <verb>\`, so the verbs surface in the \`$\`-menu; \`$trailhead <verb>\` stays the canonical form and this SKILL.md the single source of truth. (Codex custom prompts under \`~/.codex/prompts/\` are deliberately NOT used: they do not surface as slash commands.)
92
92
 
93
- ## B. This IS the skill
94
- Codex loaded this SKILL.md because you invoked \`$trailhead\`; there is no separate Skill tool to call. When the engine says to load a \`references/*.md\` file, Read it from this skill directory (\`~/.codex/skills/trailhead/\`).
93
+ ## B. This IS the skill; the engine is split across sibling skill dirs
94
+ Codex loaded this SKILL.md because you invoked \`$trailhead\` (or a \`$trailhead-<verb>\` / \`$trailhead-<cluster>\` entry); there is no separate Skill tool to call. The engine is split across sibling skill dirs under \`~/.codex/skills/\`: the dispatcher (\`trailhead/\`), five cluster skills (\`trailhead-chart/\`, \`trailhead-work/\`, \`trailhead-view/\`, \`trailhead-capture/\`, \`trailhead-manage/\`), and the shared core (\`_shared/\`, no SKILL.md). Read every referenced file **relative to the SKILL.md you are currently in**: a \`references/*.md\` from this skill's own dir, and the shared core (\`../_shared/*.md\`, or \`../../_shared/*.md\` from a \`references/\` file) from the sibling \`_shared/\` dir. Where the dispatcher's routing says "call the Skill tool with skill name \`trailhead-<cluster>\` and arguments \`<verb> <rest>\`", there is no Skill tool on Codex: **Read \`../trailhead-<cluster>/SKILL.md\` (the sibling dir) and follow it**, treating \`<verb> <rest>\` as its arguments.
95
95
 
96
96
  ## C. AskUserQuestion -> request_user_input
97
97
  Where the engine uses \`AskUserQuestion\`, use Codex's \`request_user_input\`: map \`header\`->\`header\`, \`question\`->\`question\`, each option \`{label, description}\`; generate an \`id\` from the header (lowercase, spaces->underscores). Codex has no \`multiSelect\`: present sequential single-selects, or a numbered freeform list and ask for comma-separated numbers. If \`request_user_input\` is unavailable, present the choices as a plain-text numbered list and STOP for the user's reply; do not silently pick a default and proceed.
@@ -99,6 +99,8 @@ Where the engine uses \`AskUserQuestion\`, use Codex's \`request_user_input\`: m
99
99
  ## D. Subagents (native multi_agent)
100
100
  Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_input\`, \`wait_agent\`, \`resume_agent\`, \`close_agent\`), stable and on by default on supported Codex (the installer gates on the floor that guarantees them). Where the engine delegates to a subagent (research, codebase-map, review, plan, execute, debug), spawn a real Codex subagent with these tools and the technique's own protocol (each technique carries its full protocol in-prompt, so no specialised agent is needed; pass the ticket's \`Scope:\` when relevant). The Claude-only Explore-vs-general-purpose split does not apply: Codex has one subagent kind. A subagent runs as a FRESH session that receives a task prompt, not this conversation; run independent ones in parallel (e.g. the codebase-map readers) and \`wait_agent\` on them.
101
101
 
102
+ **Never surrender the session to one open-ended \`wait_agent\`; poll instead.** \`wait_agent\` blocks until the subagent returns, so a single unconditional \`wait_agent\` on a long or stalling subagent freezes the whole session with nothing to check: this is the review / plan-review hang. Collect every delegated subagent through a **bounded wait in a poll loop**, not one open-ended blocking call. Give \`wait_agent\` a short timeout (30 to 60s); on each timeout, report a one-line "still running" progress note and loop again, so the session stays responsive and can be checked or interrupted between polls. The two **review** steps, **Code review** and **Cross-AI plan review**, always run this way: backgrounding and polling them (never a blind \`wait_agent\`) is the engine's hard cross-host default, not merely the case where it matters most. They are also the longest quiet activities in the cycle (a full-diff adversarial review, or an external CLI) and the ones most often seen to hang. If your Codex build's \`wait_agent\` exposes no timeout, poll the agent's status/output at intervals instead of blocking, or run the review as a **background job** and check back on it at intervals; the rule is the same either way. When several subagents run in parallel, poll them as a set, a bounded wait cycling across the outstanding handles, reporting which are still running.
103
+
102
104
  **Per-technique model pins (\`agent_type\` dispatch).** When \`models.codex.*\` is set, the installer projects one \`~/.codex/agents/trailhead-<technique>.toml\` per key (that technique's OpenAI model, plus its reasoning effort) and enables \`features.multi_agent_v2\`, so Codex exposes a per-technique agent registry. Detect that registry at runtime by introspecting the visible \`spawn_agent\` tool schema (GSD-style, never a config read): if its parameters expose an \`agent_type\` field, \`multi_agent_v2\` is live, so spawn each delegated activity as \`spawn_agent(agent_type="trailhead-<technique>", reasoning_effort=…)\` (\`trailhead-plan\` for the Plan step, \`trailhead-execute\` for Execute, likewise research/review/debug), which routes it onto its pinned model. If the \`spawn_agent\` schema exposes NO \`agent_type\` field (only base multi_agent v1 is present, or no \`models.codex.*\` pin was projected), spawn with base \`spawn_agent\` and the subagents inherit the one session model. Trust the tool surface you actually see and degrade gracefully: \`multi_agent_v2\` is still under development upstream, so v1 stays a supported runtime.
103
105
 
104
106
  \`config.models.*\` (Claude model ids) still cannot take effect here regardless: Codex's subagent model registry is OpenAI-only, so a Claude id cannot run and those keys collapse to the session model. The honoured namespace on Codex is \`models.codex.*\` (above); the one-time models-collapse notice still applies while a stray \`models.*\` is set and \`models.codex.*\` is empty.
@@ -111,6 +113,9 @@ Codex has a hook bus, so trailhead's guardrails run as **real Codex hooks**, not
111
113
 
112
114
  ## G. Design / UI mockups -> local disk
113
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.
116
+
117
+ ## H. Shell timeouts & background jobs (plan-review's external CLIs)
118
+ The **Cross-AI plan review** technique invokes each external reviewer CLI (\`codex exec\`, \`gemini -p\`, \`qwen -p\`, …) through Claude Code's **Bash tool**, and tells the engine to beat that tool's ~2-minute default cut by setting its \`timeout\` parameter to the 10-minute max, or launching the call with \`run_in_background: true\` and collecting it later. Those are Claude Bash-tool knobs with no Codex equivalent, so map the *intent* onto Codex's own shell/exec tool. Reviews never block the session (the engine's hard cross-host default), so **the default is to background the reviewers** and collect them by polling, never block on them: launch each with a trailing \`&\` (or your shell tool's background form), **redirect its stdin from \`/dev/null\` and its output to a file** (\`codex exec … "<prompt>" < /dev/null > out.txt 2>&1 &\`), and poll/collect when it finishes, so several reviewers run in parallel instead of serially eating the wall-clock; or wrap each reviewer invocation in a §D subagent so the long-running call sits behind the multi_agent poll loop. Where a reviewer must run synchronously instead, give that call its **own long timeout** (the \`timeout_ms\` / equivalent on Codex's shell/exec tool, set to several minutes, never the short default), so a reviewer that legitimately runs for minutes is not cut mid-review. Either way plan-review's own rule is unchanged: never rely on a short default timeout, retry once at the wide margin, then drop a still-slow reviewer rather than let it wedge the build. **Close the reviewer's stdin when backgrounding it** (\`< /dev/null\`): \`codex exec\` reads additional prompt input from stdin and blocks on EOF, so a backgrounded reviewer that inherits an open or attached stdin sits idle at "Reading additional input from stdin..." (0% CPU) forever instead of running (passing the prompt as an argument does not exempt it). Guard the poll loop too: a reviewer pinned at 0% CPU with no output past a threshold is stuck, so surface it and drop it after the one retry rather than reporting "still running" indefinitely. This is the external-CLI **shell** path; §D covers the separate **subagent** (\`wait_agent\`) path, and the two are distinct (an external reviewer CLI is a plain shell call, not a spawned Codex subagent).
114
119
  </codex_skill_adapter>`;
115
120
  }
116
121
 
@@ -145,6 +150,21 @@ policy:
145
150
  `;
146
151
  }
147
152
 
153
+ // --- codexClusterAgentsYaml ------------------------------------------------
154
+ // UI metadata for a projected cluster skill (trailhead-chart, trailhead-work,
155
+ // ...): a display name + explicit-only invocation, so a cluster never
156
+ // auto-loads into an unrelated turn. Mirrors codexAgentsYaml's shape.
157
+ function codexClusterAgentsYaml(cluster) {
158
+ const short = String(cluster).replace(/^trailhead-/, '');
159
+ return `interface:
160
+ display_name: ${yamlQuote(`Trailhead: ${short}`)}
161
+ short_description: ${yamlQuote(`trailhead ${short} cluster`)}
162
+ default_prompt: ${yamlQuote(`Run $${cluster}`)}
163
+ policy:
164
+ allow_implicit_invocation: false
165
+ `;
166
+ }
167
+
148
168
  // --- CODEX_AGENT_TECHNIQUES ------------------------------------------------
149
169
  // Metadata for the 5 trailhead activities that can get a per-technique Codex
150
170
  // custom-agent TOML: a short UI description and the developer_instructions
@@ -388,6 +408,7 @@ module.exports = {
388
408
  codexSkillAdapterHeader,
389
409
  injectCodexAdapterHeader,
390
410
  codexAgentsYaml,
411
+ codexClusterAgentsYaml,
391
412
  codexAgentToml,
392
413
  codexAgentTomlPlan,
393
414
  codexVerbSkillContent,
@@ -81,8 +81,22 @@ ok('codexSkillAdapterHeader §D pins the exact agent_type dispatch call shape',
81
81
  header.includes('spawn_agent(agent_type="trailhead-<technique>"'));
82
82
  ok('codexSkillAdapterHeader §D keeps the base spawn_agent v1 fallback path',
83
83
  header.includes('base multi_agent v1') && header.includes('inherit the one session model'));
84
+ ok('codexSkillAdapterHeader §D warns against a single blocking wait_agent with a poll loop',
85
+ header.includes('bounded wait') && header.includes('poll'));
86
+ ok('codexSkillAdapterHeader §D offers a background-job fallback for a stalling review',
87
+ header.includes('background job'));
88
+ ok('codexSkillAdapterHeader §D makes background+poll the hard cross-host default for reviews',
89
+ header.includes('always run this way') && header.includes('hard cross-host default'));
84
90
  ok('codexSkillAdapterHeader §F mentions real Codex hooks', header.includes('real Codex hooks'));
85
91
  ok('codexSkillAdapterHeader §F no longer says Codex has no hook bus', !header.includes('Codex has no hook bus'));
92
+ ok('codexSkillAdapterHeader §H maps plan-review external-CLI Bash timeout/background onto Codex shell',
93
+ header.includes('## H.') && header.includes('Cross-AI plan review') && header.includes('run_in_background') && header.includes('shell/exec'));
94
+ ok('codexSkillAdapterHeader §H distinguishes the external-CLI shell path from the §D subagent path',
95
+ header.includes('external-CLI') && header.includes('not a spawned Codex subagent'));
96
+ ok('codexSkillAdapterHeader §H makes backgrounding reviewers the default',
97
+ header.includes('the default is to background the reviewers'));
98
+ ok('codexSkillAdapterHeader §H closes reviewer stdin to avoid the EOF hang',
99
+ header.includes('/dev/null') && header.toLowerCase().includes('stdin'));
86
100
 
87
101
  // --- codexVerbSkillContent (#46) ---
88
102
  ok('codexVerbSkillContent: SKILL.md frontmatter is at byte 0 with name+description', (() => {
package/bin/trailhead.js CHANGED
@@ -23,10 +23,41 @@ const readline = require('readline');
23
23
  const { spawnSync } = require('child_process');
24
24
 
25
25
  const { getHost, configDirFor, codexVersionGate } = require('./lib/host-descriptor.js');
26
- const { codexLayout, convertToCodex, injectCodexAdapterHeader, codexAgentsYaml, codexHookEntries, enableCodexHooksFeature, codexAgentTomlPlan, codexVerbSkillPlan, enableCodexMultiAgentV2Feature } = require('./lib/codex-projection.js');
26
+ const { codexLayout, convertToCodex, injectCodexAdapterHeader, codexAgentsYaml, codexClusterAgentsYaml, codexHookEntries, enableCodexHooksFeature, codexAgentTomlPlan, codexVerbSkillPlan, enableCodexMultiAgentV2Feature } = require('./lib/codex-projection.js');
27
27
 
28
28
  const PKG = path.resolve(__dirname, '..');
29
29
  const SRC = path.join(PKG, 'plugins', 'trailhead');
30
+
31
+ // Enumerate the engine skill dirs shipped under plugins/trailhead/skills: the
32
+ // dispatcher, the cluster skills, and the shared core. A subdir qualifies when
33
+ // it is `_shared` (the shared core, no SKILL.md) or contains a SKILL.md, so a
34
+ // stray empty dir is skipped and a new cluster is picked up automatically.
35
+ function engineSkillDirs() {
36
+ const skillsRoot = path.join(SRC, 'skills');
37
+ if (!fs.existsSync(skillsRoot)) return [];
38
+ return fs.readdirSync(skillsRoot, { withFileTypes: true })
39
+ .filter((e) => e.isDirectory())
40
+ .map((e) => e.name)
41
+ .filter((name) => name === '_shared' || fs.existsSync(path.join(skillsRoot, name, 'SKILL.md')))
42
+ .sort();
43
+ }
44
+
45
+ // Sweep every trailhead-owned skill dir under a skills root, by name: the
46
+ // dispatcher (`trailhead`), the shared core (`_shared`), and every
47
+ // `trailhead-<cluster|verb>` sibling. Clears a prior layout's stale dirs (an
48
+ // old monolith fallback, a removed cluster, a renamed verb skill) the current
49
+ // install no longer ships, so a re-install or /trailhead:update never leaves a
50
+ // dangling skill. The skills root is shared with other plugins, so only
51
+ // trailhead's own names are touched, never the whole dir.
52
+ function sweepTrailheadSkills(skillsRoot) {
53
+ rmrf(path.join(skillsRoot, 'trailhead'));
54
+ rmrf(path.join(skillsRoot, '_shared'));
55
+ if (!fs.existsSync(skillsRoot)) return;
56
+ for (const f of fs.readdirSync(skillsRoot)) {
57
+ if (f.startsWith('trailhead-')) rmrf(path.join(skillsRoot, f));
58
+ }
59
+ }
60
+
30
61
  const args = process.argv.slice(2);
31
62
  const has = (f) => args.includes(f);
32
63
  const useSymlink = has('--symlink');
@@ -138,7 +169,6 @@ async function resolveHostName() {
138
169
  // --- Claude adapter (unchanged behaviour) -----------------------------------
139
170
  function claudePaths(configDir) {
140
171
  return {
141
- skill: path.join(configDir, 'skills', 'trailhead'),
142
172
  commands: path.join(configDir, 'commands', 'trailhead'),
143
173
  hooksDir: path.join(configDir, 'hooks'),
144
174
  templates: path.join(configDir, 'trailhead', 'templates'),
@@ -155,18 +185,28 @@ const HOOK_LIB_FILES = ['commit-message-check.js'];
155
185
  // Copy the hook scripts and their runtime libs into a destination hooks dir
156
186
  // (Claude's ~/.claude/hooks or Codex's skills/trailhead/hooks). Shared by both
157
187
  // install paths so the lib copy can never drift from the script copy again.
158
- function copyHookScripts(destHooksDir) {
188
+ // Place the allow-listed hook scripts into destHooksDir. Normal install copies
189
+ // (+chmod so the exec bit is set regardless of the source mode); a --symlink dev
190
+ // install links each file per-file into the package source so hook edits are
191
+ // live too, matching skills/commands. Never symlink the whole hooks/ dir: it is
192
+ // shared with other plugins, so only the curated allowlist is touched. rmrf the
193
+ // dest first so a reinstall over an existing file or symlink never hits EEXIST.
194
+ function copyHookScripts(destHooksDir, { useSymlink = false } = {}) {
159
195
  ensure(destHooksDir);
160
- for (const f of HOOK_FILES) {
161
- fs.copyFileSync(path.join(SRC, 'hooks', f), path.join(destHooksDir, f));
162
- fs.chmodSync(path.join(destHooksDir, f), 0o755);
163
- }
196
+ const put = (src, dest) => {
197
+ rmrf(dest);
198
+ if (useSymlink) {
199
+ fs.symlinkSync(src, dest, 'file');
200
+ } else {
201
+ fs.copyFileSync(src, dest);
202
+ fs.chmodSync(dest, 0o755);
203
+ }
204
+ };
205
+ for (const f of HOOK_FILES) put(path.join(SRC, 'hooks', f), path.join(destHooksDir, f));
164
206
  if (HOOK_LIB_FILES.length) {
165
207
  const libDest = path.join(destHooksDir, 'lib');
166
208
  ensure(libDest);
167
- for (const f of HOOK_LIB_FILES) {
168
- fs.copyFileSync(path.join(SRC, 'hooks', 'lib', f), path.join(libDest, f));
169
- }
209
+ for (const f of HOOK_LIB_FILES) put(path.join(SRC, 'hooks', 'lib', f), path.join(libDest, f));
170
210
  }
171
211
  }
172
212
 
@@ -195,7 +235,10 @@ function stripHook(s, event, substr) {
195
235
 
196
236
  function uninstallClaude(configDir) {
197
237
  const P = claudePaths(configDir);
198
- [P.skill, P.commands, path.join(configDir, 'trailhead')].forEach(rmrf);
238
+ // Sweep every trailhead-owned skill dir (current set + any stale dir a prior
239
+ // layout left that this package no longer ships), never the shared dir itself.
240
+ sweepTrailheadSkills(path.join(configDir, 'skills'));
241
+ [P.commands, path.join(configDir, 'trailhead')].forEach(rmrf);
199
242
  HOOK_FILES.forEach((f) => rmrf(path.join(P.hooksDir, f)));
200
243
  // Remove only trailhead's own lib files by name: the hooks/lib dir is shared
201
244
  // with other plugins, so never rmrf the whole dir.
@@ -213,10 +256,21 @@ function installClaude(configDir, { useSymlink }) {
213
256
  const P = claudePaths(configDir);
214
257
  const hookCmd = (name) => `node "${path.join(P.hooksDir, name)}"`;
215
258
 
216
- place(path.join(SRC, 'skills', 'trailhead'), P.skill, useSymlink);
259
+ // Place each engine skill dir as a sibling under configDir/skills so the
260
+ // ../_shared/ and ../trailhead-<cluster>/ relative refs resolve. The skills
261
+ // dir is shared with other plugins: place by name, never touch the whole dir.
262
+ // Migration: sweep any stale trailhead-owned skill dirs from a prior layout
263
+ // (an old monolith, a removed cluster) before laying the current set down, so
264
+ // an update over an old install leaves no dangling skill. The skills dir is
265
+ // shared with other plugins: place by name, never touch the whole dir.
266
+ const skillDirs = engineSkillDirs();
267
+ sweepTrailheadSkills(path.join(configDir, 'skills'));
268
+ for (const name of skillDirs) {
269
+ place(path.join(SRC, 'skills', name), path.join(configDir, 'skills', name), useSymlink);
270
+ }
217
271
  place(path.join(SRC, 'commands'), P.commands, useSymlink);
218
272
  place(path.join(SRC, 'templates'), P.templates, useSymlink);
219
- copyHookScripts(P.hooksDir);
273
+ copyHookScripts(P.hooksDir, { useSymlink });
220
274
  // version marker: serve al check-update hook per la versione installata (canale npm)
221
275
  const pkgVersion = (readJSON(path.join(SRC, '.claude-plugin', 'plugin.json')).version || '').trim();
222
276
  if (pkgVersion) {
@@ -234,7 +288,7 @@ function installClaude(configDir, { useSymlink }) {
234
288
  writeJSON(P.settings, s);
235
289
 
236
290
  console.log(`✓ trailhead installed for Claude Code → ${configDir}`);
237
- console.log(` skill → ${P.skill}${useSymlink ? ' (symlink)' : ''}`);
291
+ console.log(` skills → ${path.join(configDir, 'skills')}/ (${skillDirs.join(', ')})${useSymlink ? ' (symlink)' : ''}`);
238
292
  console.log(` commands → ${P.commands} (/trailhead:*)`);
239
293
  console.log(` hooks → ${P.hooksDir}/ (${added ? 'registered' : 'already present'} in settings.json)`);
240
294
  console.log(` templates → ${P.templates}`);
@@ -333,7 +387,7 @@ function installCodex(configDir, { useSymlink }) {
333
387
  const L = codexLayout(configDir);
334
388
 
335
389
  if (useSymlink) {
336
- console.log('Note: symlink install is unavailable for Codex (artifacts are generated, not copied verbatim); installing copies instead.');
390
+ console.log('Note: on Codex, --symlink links the verbatim artifacts (hooks, templates) into the package source for live edits; the skill files are text-converted at install, so they are written (projected), not linked.');
337
391
  }
338
392
 
339
393
  // Migration: sweep the pre-#25 layout AND the #42 prompt shims that never
@@ -346,31 +400,44 @@ function installCodex(configDir, { useSymlink }) {
346
400
  }
347
401
  rmrf(L.legacyEngineDir);
348
402
 
349
- // Engine: clean skill dir, then project with conversion.
350
- rmrf(L.skillDir);
351
- ensure(L.skillDir);
352
- copySkillConverted(path.join(SRC, 'skills', 'trailhead'), L.skillDir, true);
403
+ // Sweep the whole engine + any prior per-verb skills, then project fresh.
404
+ sweepTrailheadSkills(L.skillsRoot);
405
+
406
+ // Project each engine skill dir (dispatcher + 5 clusters + _shared) as a
407
+ // sibling under skills/, converting text and injecting the adapter header into
408
+ // each top-level SKILL.md. The ../_shared/ relative refs survive conversion and
409
+ // resolve as siblings (decision #54). _shared has no SKILL.md, so no header.
410
+ const engineDirs = engineSkillDirs();
411
+ for (const name of engineDirs) {
412
+ copySkillConverted(path.join(SRC, 'skills', name), path.join(L.skillsRoot, name), true);
413
+ }
353
414
 
354
415
  // Templates (verbatim, never converted), bundled inside the skill dir so
355
416
  // ${CLAUDE_PLUGIN_ROOT}/templates resolves to ~/.codex/skills/trailhead/templates.
356
- ensure(L.templatesDir);
357
- fs.cpSync(path.join(SRC, 'templates'), L.templatesDir, { recursive: true });
417
+ place(path.join(SRC, 'templates'), L.templatesDir, useSymlink);
358
418
 
359
419
  // agents/openai.yaml: UI metadata + explicit-only invocation (no auto-trigger).
360
420
  ensure(L.agentsDir);
361
421
  fs.writeFileSync(L.agentsYaml, codexAgentsYaml());
422
+ // Each cluster skill also gets explicit-only UI metadata (never auto-invoked).
423
+ for (const name of engineDirs) {
424
+ if (name === 'trailhead' || name === '_shared') continue;
425
+ const dir = path.join(L.skillsRoot, name, 'agents');
426
+ ensure(dir);
427
+ fs.writeFileSync(path.join(dir, 'openai.yaml'), codexClusterAgentsYaml(name));
428
+ }
362
429
 
363
430
  // Per-verb discoverability (#46): one thin Codex skill per verb
364
431
  // (skills/trailhead-<verb>/, invocable as $trailhead-<verb>), each delegating
365
432
  // to $trailhead <verb>. Codex custom prompts (~/.codex/prompts/) never surface
366
433
  // as slash commands, so per-verb skills carry the surface, exactly as GSD does.
367
- // Sweep stale trailhead-* verb skills first (a removed verb, or an upgrade from
368
- // the old prompt-shim layout), never the main skills/trailhead. Verbs come from
369
- // the canonical commands/*.md so the surfaces never drift.
370
- for (const f of (fs.existsSync(L.skillsRoot) ? fs.readdirSync(L.skillsRoot) : [])) {
371
- if (f.startsWith('trailhead-')) rmrf(path.join(L.skillsRoot, f));
372
- }
373
- const verbSkillPlan = codexVerbSkillPlan(configDir, readCommandVerbs());
434
+ // Verbs come from the canonical commands/*.md so the surfaces never drift.
435
+ // Skip any verb whose skill dir would collide with a projected cluster skill
436
+ // (e.g. verb `work` vs the trailhead-work cluster): the cluster skill is its
437
+ // own $trailhead-<name> entry. Compute the skip-set from the projected dirs.
438
+ const clusterDirNames = new Set(engineDirs);
439
+ const verbList = readCommandVerbs().filter((c) => !clusterDirNames.has(`trailhead-${c.verb}`));
440
+ const verbSkillPlan = codexVerbSkillPlan(configDir, verbList);
374
441
  for (const d of verbSkillPlan.dirs) ensure(path.join(d, 'agents'));
375
442
  for (const w of verbSkillPlan.writes) fs.writeFileSync(w.path, w.content);
376
443
 
@@ -379,7 +446,7 @@ function installCodex(configDir, { useSymlink }) {
379
446
 
380
447
  // Hooks: copy the 4 guard scripts into the skill dir and register them in
381
448
  // ~/.codex/hooks.json (same shape as Claude's settings.json hooks block).
382
- copyHookScripts(L.hooksScriptsDir);
449
+ copyHookScripts(L.hooksScriptsDir, { useSymlink });
383
450
  const h = readJSON(L.hooksJson);
384
451
  for (const e of codexHookEntries(L.hooksScriptsDir)) addHook(h, e.event, e.matcher, e.command);
385
452
  writeJSON(L.hooksJson, h);
@@ -443,7 +510,7 @@ function installCodex(configDir, { useSymlink }) {
443
510
  // edit config.toml, the hooks stay inert until the user sets it by hand, so
444
511
  // don't reassure them about a trust review that won't happen yet.
445
512
  if (featureManual) {
446
- console.log(` ⚠ could not edit ${L.configToml} automatically — add \`features.hooks = true\` under [features] by hand, then Codex will ask you to trust trailhead's hooks on next start.`);
513
+ console.log(` ⚠ could not edit ${L.configToml} automatically; add \`features.hooks = true\` under [features] by hand, then Codex will ask you to trust trailhead's hooks on next start.`);
447
514
  } else {
448
515
  console.log('Note: Codex will ask you to trust trailhead\'s hooks on next start (feature `features.hooks`).');
449
516
  }
@@ -454,12 +521,8 @@ function installCodex(configDir, { useSymlink }) {
454
521
 
455
522
  function uninstallCodex(configDir) {
456
523
  const L = codexLayout(configDir);
457
- rmrf(L.skillDir);
458
- // sweep the per-verb discoverability skills (skills/trailhead-<verb>/, #46),
459
- // never the main skills/trailhead (already removed above; does not match).
460
- for (const f of (fs.existsSync(L.skillsRoot) ? fs.readdirSync(L.skillsRoot) : [])) {
461
- if (f.startsWith('trailhead-')) rmrf(path.join(L.skillsRoot, f));
462
- }
524
+ // Sweep the dispatcher, _shared, the clusters, and the per-verb skills.
525
+ sweepTrailheadSkills(L.skillsRoot);
463
526
  // sweep any pre-#25 layout AND the #42 prompt shims from prompts/
464
527
  rmrf(path.join(L.promptsDir, 'trailhead.md'));
465
528
  for (const f of (fs.existsSync(L.promptsDir) ? fs.readdirSync(L.promptsDir) : [])) {