@marcomigozzi/trailhead 0.16.1 → 0.17.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 (76) hide show
  1. package/README.it.md +57 -10
  2. package/README.md +57 -10
  3. package/bin/capture-action.test.js +167 -0
  4. package/bin/capture-docs.test.js +32 -0
  5. package/bin/engine-route-docs.test.js +148 -0
  6. package/bin/grill-model-docs.test.js +155 -0
  7. package/bin/grilling-holistic-review-docs.test.js +7 -3
  8. package/bin/lib/codex-projection.js +22 -13
  9. package/bin/lib/codex-projection.test.js +58 -17
  10. package/bin/lib/host-descriptor.js +1 -1
  11. package/bin/lib/host-descriptor.test.js +6 -0
  12. package/bin/report-status-docs.test.js +10 -2
  13. package/bin/session-trust-docs.test.js +37 -0
  14. package/bin/surfaced-scope-docs.test.js +57 -0
  15. package/bin/trailhead.js +7 -5
  16. package/bin/trailhead.test.js +53 -11
  17. package/bin/unblock-docs.test.js +56 -0
  18. package/package.json +1 -1
  19. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  20. package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
  21. package/plugins/trailhead/agents/trailhead-grill.md +41 -0
  22. package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
  23. package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
  24. package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
  25. package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
  26. package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
  27. package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
  28. package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
  29. package/plugins/trailhead/hooks/lib/issue-body.test.js +84 -0
  30. package/plugins/trailhead/hooks/lib/model-defaults-review.js +22 -1
  31. package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +64 -1
  32. package/plugins/trailhead/hooks/lib/model-route.js +207 -0
  33. package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
  34. package/plugins/trailhead/hooks/lib/report-status.test.js +56 -1
  35. package/plugins/trailhead/hooks/mod/README.md +16 -7
  36. package/plugins/trailhead/hooks/mod/map-pane.tsx +72 -16
  37. package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
  38. package/plugins/trailhead/hooks/mod/repo-pin.ts +8 -5
  39. package/plugins/trailhead/hooks/trailhead-check-update.js +17 -1
  40. package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
  41. package/plugins/trailhead/skills/_shared/capture-core.md +118 -0
  42. package/plugins/trailhead/skills/_shared/choices.md +2 -2
  43. package/plugins/trailhead/skills/_shared/configuration-reference.md +18 -16
  44. package/plugins/trailhead/skills/_shared/configuration.md +1 -1
  45. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  46. package/plugins/trailhead/skills/_shared/substrate-commands.md +8 -4
  47. package/plugins/trailhead/skills/_shared/substrate.md +1 -1
  48. package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
  49. package/plugins/trailhead/skills/_shared/teamwork.md +6 -2
  50. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
  51. package/plugins/trailhead/skills/_shared/techniques/code-review.md +5 -2
  52. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +4 -1
  53. package/plugins/trailhead/skills/_shared/techniques/debug.md +4 -1
  54. package/plugins/trailhead/skills/_shared/techniques/grilling.md +26 -1
  55. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
  56. package/plugins/trailhead/skills/_shared/techniques/verify.md +3 -1
  57. package/plugins/trailhead/skills/_shared/techniques.md +6 -4
  58. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
  59. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
  60. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
  61. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +4 -4
  62. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +3 -3
  63. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
  64. package/plugins/trailhead/skills/trailhead-work/SKILL.md +11 -9
  65. package/plugins/trailhead/skills/trailhead-work/references/auto.md +1 -1
  66. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +10 -10
  67. package/plugins/trailhead/templates/trailhead-capture.js +418 -0
  68. package/plugins/trailhead/templates/trailhead-checkout-find.js +256 -0
  69. package/plugins/trailhead/templates/trailhead-checkout-in.js +76 -20
  70. package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
  71. package/plugins/trailhead/templates/trailhead-issue-body.js +69 -6
  72. package/plugins/trailhead/templates/trailhead-report-status.js +57 -9
  73. package/plugins/trailhead/tests/map-pane-ui.test.ts +122 -37
  74. package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
  75. package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
  76. package/plugins/trailhead/types/index.d.ts +7 -0
package/README.it.md CHANGED
@@ -26,7 +26,7 @@ Due approcci al lavoro di progetto guidato dagli agenti risolvono ciascuno una m
26
26
 
27
27
  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 insieme di skill a cluster coesi su un core condiviso, una sua interpretazione autonoma di entrambe le idee anziché un wrapper attorno all'una o all'altra.
28
28
 
29
- > **trailhead è self-contained** nel senso che ha **zero dipendenze da plugin esterni**: gli agent di trailhead e i built-in dell'host vanno bene, e nessun'altra skill o plugin è richiesta. 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; ogni tecnica del motore dispatcha al **proprio agent `trailhead-*`** (`trailhead-executor`, `trailhead-plan`, `trailhead-research`, `trailhead-code-review`, `trailhead-debug`, più `trailhead-fix` e `trailhead-codebase-map`), scritto una volta come file `agents/trailhead-*.md` versionato e **proiettato su ogni host** (su Codex diventa `~/.codex/agents/trailhead-*.toml` generato dalla stessa sorgente). Questi agent sono **thin** (identità + tool policy + un modello guidato dalla config, con il protocollo della tecnica iniettato allo spawn) e girano sul toolkit di subagent nativo 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: i mockup hosted. `design: artifacts` usa il tool `Artifact` integrato in Claude Code (niente da installare), e `design: stitch` usa l'MCP Google Stitch (offerto come installazione una tantum). Vedi [Mockup prima del codice](#-mockup-prima-del-codice).
29
+ > **trailhead è self-contained** nel senso che ha **zero dipendenze da plugin esterni**: gli agent di trailhead e i built-in dell'host vanno bene, e nessun'altra skill o plugin è richiesta. 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; ogni tecnica del motore dispatcha al **proprio agent `trailhead-*`** (`trailhead-executor`, `trailhead-plan`, `trailhead-research`, `trailhead-code-review`, `trailhead-debug`, `trailhead-verify`, `trailhead-grill`, più `trailhead-fix` e `trailhead-codebase-map`), scritto una volta come file `agents/trailhead-*.md` versionato e **proiettato su ogni host** (su Codex diventa `~/.codex/agents/trailhead-*.toml` generato dalla stessa sorgente). Questi agent sono **thin** (identità + tool policy + un modello guidato dalla config, con il protocollo della tecnica iniettato allo spawn) e girano sul toolkit di subagent nativo 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: i mockup hosted. `design: artifacts` usa il tool `Artifact` integrato in Claude Code (niente da installare), e `design: stitch` usa l'MCP Google Stitch (offerto come installazione una tantum). Vedi [Mockup prima del codice](#-mockup-prima-del-codice).
30
30
 
31
31
  Le scelte di design che ne derivano:
32
32
 
@@ -51,7 +51,7 @@ Le scelte di design che ne derivano:
51
51
 
52
52
  > **Gira su Claude Code e Codex CLI.** trailhead è scritto una volta sola; l'installer proietta artefatti nativi per l'host. Su **Claude Code**: una skill + comandi `/trailhead:*` + hook. Su **Codex CLI**: una skill nativa invocata `$trailhead`, una skill `$trailhead-<verb>` per ogni verbo per la discoverability, e le guardrail come hook nativi di Codex. Scegli l'host con `--claude` / `--codex`, o lascia che l'installer lo rilevi da solo. Altri host possono seguire (l'installer usa un descrittore per-host).
53
53
 
54
- **Prerequisiti:** una [`gh` CLI](https://cli.github.com) autenticata (il tracker è GitHub Issues) e un repo GitHub in cui lavorare. Il percorso npm richiede anche Node 18+.
54
+ **Prerequisiti:** una [`gh` CLI](https://cli.github.com) autenticata (il tracker è GitHub Issues) e un repo GitHub in cui lavorare. Il percorso npm richiede anche Node 18+. Se un `GH_TOKEN`/`GITHUB_TOKEN` non valido nell'ambiente oscura un login `gh` funzionante, un controllo all'avvio della sessione lo segnala una volta, con la correzione.
55
55
 
56
56
  ### Come plugin Claude Code (nativo, gestito da `/plugin`)
57
57
  ```
@@ -69,7 +69,7 @@ npx @marcomigozzi/trailhead --symlink # dev install (Claude; symlink al check
69
69
  npx @marcomigozzi/trailhead --uninstall # rimuove tutto ciò che ha aggiunto (abbinalo a --claude / --codex)
70
70
  npx @marcomigozzi/trailhead --dir=<path> # punta a una config dir specifica
71
71
  ```
72
- 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 (workflow label-guard, statusline, l'hook git `commit-msg` e gli script di supporto che il motore esegue), in modo idempotente.
72
+ 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 (che esegue anche il controllo del token `gh`), e i template (workflow label-guard, statusline, l'hook git `commit-msg` e gli script di supporto che il motore esegue), in modo idempotente.
73
73
 
74
74
  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 otto guardrail come **hook** nativi di Codex (`~/.codex/hooks.json`, abilitando `features.hooks`; i cinque 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.
75
75
 
@@ -230,7 +230,7 @@ La mod di trailhead (Claude Code 2.1.287+) è inclusa nel plugin e disegna il la
230
230
 
231
231
  ![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)
232
232
 
233
- Il pannello della mappa elenca le mappe aperte da consultare, i ticket che una run autonoma ha parcheggiato per te in un gruppo `Needs you` in cima, la frontier con un pulsante `work` per ogni ticket che pulisce il contesto e avvia il ticket, chi tiene cosa (il tuo ticket è segnato `this session`), i follow-up UAT in sospeso in un gruppo `UAT` (ognuno con il ticket che collauda), i ticket in pausa in un gruppo `Paused` e cosa è bloccato. Una riga dim sotto la riga Whiteboard somma i ticket aperti di tutte le mappe aperte più la whiteboard, lo stesso numero che dichiara `/trailhead:map` (`at least N` quando le mappe aperte sono più di quelle lette dal pannello). Il footer ha `refresh` e `close`. `/trailhead:panel` lo apre e lo chiude, e si apre da solo una volta in fullscreen.
233
+ Il pannello della mappa elenca le mappe aperte da consultare, subito sotto di esse i tuoi ticket in lavorazione (in un gruppo `In progress`, da ogni mappa e dalla whiteboard), i ticket che una run autonoma ha parcheggiato per te in un gruppo `Needs you`, la frontier con un pulsante `work` per ogni ticket che pulisce il contesto e avvia il ticket (nel checkout della mappa; se quel checkout è stato spostato, viene ritrovato tramite il suo remote `origin`, e scegli tu quando ne corrispondono diversi), chi tiene cosa (il tuo ticket è segnato `this session`), i follow-up UAT in sospeso in un gruppo `UAT` (ognuno con il ticket che collauda), i ticket in pausa in un gruppo `Paused` e cosa è bloccato. Una riga dim sotto la riga Whiteboard somma i ticket aperti di tutte le mappe aperte più la whiteboard, lo stesso numero che dichiara `/trailhead:map` (`at least N` quando le mappe aperte sono più di quelle lette dal pannello). Il footer ha `refresh` e `close`. `/trailhead:panel` lo apre e lo chiude (se richiesto all'avvio della sessione, prima che lo schermo sia pronto, si apre appena lo è), e si apre da solo una volta in fullscreen.
234
234
 
235
235
  ![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)
236
236
 
@@ -407,13 +407,59 @@ Molte persone (e le loro sessioni di agente) condividono una mappa e la lavorano
407
407
  - **Dai scope ai ticket per path su un monorepo.** Un ticket può portare una riga `Scope:` che nomina il/i package/dir che tocca: scope disgiunti parallelizzano in sicurezza, e dà scope a build/test e ai commit del worktree limitandoli al package interessato. `/trailhead:adopt` rileva un monorepo e propone sia `isolation: worktree` sia la convenzione `Scope:`. Sotto `isolation: none`, `Scope:` **serializza** anche un'area condivisa: al momento del claim una sessione si trattiene se lo scope di un altro ticket in corso si sovrappone, così due ticket con scope sullo stesso submodule/package non possono girare insieme (è così che tieni due sessioni fuori da un'app legata a un path).
408
408
  - **I git submodule sono gestiti come repo separati.** Il lavoro che atterra dentro un submodule viene committato *dentro il submodule* (su un branch) e pubblicato prima che un bump del gitlink nel parent lo referenzi. Poiché i worktree del superproject condividono un unico git dir del submodule, un **repo con submodule risolve a `clone`** (sotto il default `auto`, o scalando un `worktree` esplicito), così ogni ticket ottiene uno stato del submodule indipendente; il clone provvede i suoi submodule al setup (`--recurse-submodules`) e il Verify controlla `git status` dentro ogni submodule toccato. Un ticket che copre due submodule resta atomico tramite un singolo commit del parent che bumpa entrambi i gitlink insieme; il suo `Scope:` li nomina entrambi, e lo stesso submodule conta come scope sovrapposto, quindi due ticket su di esso serializzano. `/trailhead:adopt` rileva `.gitmodules` e scrive la regola nelle conventions.
409
409
  - **Lo stato vive sui ticket** (claim, label, commenti; tutti conflict-free); il corpo della mappa è un indice ri-leggibile, non la fonte di verità. Il record durevole di una risoluzione è il commento del ticket stesso.
410
- - **Le label sono protette dagli esterni.** Le label `trailhead:*` guidano la mappa, quindi trailhead si fida di una issue etichettata solo quando un collaboratore con accesso in scrittura ha applicato la label e (per un ticket) porta un `Parent:` valido. Qualsiasi altra cosa è messa in quarantena come `trailhead:unverified`, fuori dalla frontier. GitHub già blocca gli utenti senza accesso in scrittura dall'applicare label; contro gli utenti di triage e le automazioni, trailhead può **installare per te la GitHub Action di label guard** (da `templates/trailhead-label-guard.yml`) nella `.github/workflows/` del tuo repo, dove rimuove alla fonte le label `trailhead:*` non autorizzate. Installare un workflow richiede un token con lo scope `workflow`, quindi trailhead esegue `gh auth refresh -s workflow` se il push viene rifiutato.
411
- - **Le issue aperte da altri vengono triage-ate, non fidate alla cieca.** `/trailhead:inbox` elenca le issue in ingresso (bug report, richieste, domande che non sono ancora trailhead ticket) e integra quelle valide **sul posto**: riformulandole come ticket, mantenendo l'autorialità di chi ha segnalato, e applicando le label all'adozione. Il resto viene instradato a fog, out-of-scope, duplicato, o needs-info.
410
+ - **Le label sono protette dagli esterni.** Le label `trailhead:*` guidano la mappa, quindi trailhead si fida di una issue etichettata solo quando ogni sua label `trailhead:*` è stata applicata da un etichettatore fidato (un collaboratore con accesso in scrittura, `github-actions[bot]`, o un login nella variabile di repo `TRAILHEAD_LABEL_ALLOWLIST`: gli stessi etichettatori che la label guard non rimuove mai) e (per un ticket) porta un `Parent:` valido. Qualsiasi altra cosa è messa in quarantena come `trailhead:unverified`, fuori dalla frontier. GitHub già blocca gli utenti senza accesso in scrittura dall'applicare label; contro gli utenti di triage e le automazioni, trailhead può **installare per te la GitHub Action di label guard** (da `templates/trailhead-label-guard.yml`) nella `.github/workflows/` del tuo repo, dove rimuove alla fonte le label `trailhead:*` non autorizzate. Installare un workflow richiede un token con lo scope `workflow`, quindi trailhead esegue `gh auth refresh -s workflow` se il push viene rifiutato. Per le automazioni (CI, cron, bot) che aprono ticket senza una sessione di chat, `templates/trailhead-capture.js` cattura un bug, todo, seed o idea con tutte le label e si rifiuta di girare con un'identità che la guard strapperebbe, e su GitHub la composite Action [`actions/capture`](#catturare-da-ci) lo incapsula in poche righe di workflow.
411
+ - **Le issue aperte da altri vengono triage-ate, non fidate alla cieca.** `/trailhead:inbox` elenca le issue in ingresso (bug report, richieste, domande che non sono ancora trailhead ticket) e integra quelle valide **sul posto**: riformulandole come ticket, mantenendo l'autorialità di chi ha segnalato, e applicando le label all'adozione. Il resto viene instradato a fog, out-of-scope, duplicato, o needs-info. Lì sono elencate anche le issue in quarantena `trailhead:unverified`, da adottare o rifiutare.
412
+
413
+ ### Catturare da CI
414
+
415
+ `ToRvaLDz/trailhead/actions/capture` è una composite Action sottile sopra l'helper di cattura: ogni regola (trust, destinazione, dedupe, secret scan, label, edge, dashboard) resta nell'helper. Fissa un tag di release (l'Action è disponibile dalla 0.17.0).
416
+
417
+ ```yaml
418
+ permissions:
419
+ issues: write
420
+
421
+ jobs:
422
+ build:
423
+ runs-on: ubuntu-latest
424
+ steps:
425
+ # ... i tuoi step di build e test ...
426
+ - if: failure()
427
+ uses: ToRvaLDz/trailhead/actions/capture@v0.17.0
428
+ with:
429
+ type: bug
430
+ title: "CI fallita su ${{ github.ref_name }}"
431
+ body: "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
432
+ dedupe-key: ci-failure-${{ github.job }}
433
+ ```
434
+
435
+ Una variante manuale funziona uguale: avvia il job con `workflow_dispatch` e passa `type`, `title` e `body` da `github.event.inputs`.
436
+
437
+ | Input | Obbligatorio | Significato |
438
+ |---|---|---|
439
+ | `type` | sì | `bug`, `todo`, `idea` o `seed` |
440
+ | `title` | sì | titolo della issue |
441
+ | `body` / `body-file` | uno dei due | corpo della issue, o un percorso file (`-` per stdin); non entrambi |
442
+ | `map` / `of` | no | archivia sotto la mappa `<n>`, o eredita la mappa del ticket d'origine `<n>` (solo bug); esclusivi |
443
+ | `trigger` | solo seed | il testo di `Trigger:` |
444
+ | `unverified` | no | `true` aggiunge `trailhead:unverified` (non per `idea`) |
445
+ | `dedupe-key` | no | chiave di idempotenza, da 1 a 100 caratteri `A-Za-z0-9._:-` (niente spazi né `/`) |
446
+ | `no-dashboard` | no | `true` salta l'aggiornamento della dashboard della whiteboard |
447
+ | `repo` | no | `owner/name` di destinazione (default: questo repo) |
448
+ | `token` | no | token per l'helper (default: `github.token`) |
449
+
450
+ Output: `issue`, `url` e `status` (`created`, `existing`, `partial`, `error`, `usage`).
451
+
452
+ - Il token predefinito agisce come `github-actions[bot]`, di cui la label guard si fida; l'helper lo verifica dopo la creazione.
453
+ - Un nuovo run con lo stesso `dedupe-key` restituisce `existing` e non crea nulla.
454
+ - `partial` (exit 3, la issue esiste ma l'edge di sub-issue è fallito), `error` e `usage` (exit 2, input errati) fanno fallire lo step; `status` viene comunque impostato.
455
+ - Il runner richiede bash, node e gh, presenti sui runner ospitati da GitHub; i runner self-hosted devono fornirli.
412
456
 
413
457
  ## ✅ Tenere i ticket onesti
414
458
 
415
459
  Un ticket è una singola Question a cui si può rispondere, dimensionata a una sessione, così non lo fai mai crescere in corsa. Quando nuovo scope emerge mentre lavori (i test soprattutto scatenano idee), decidi caso per caso: piccolo e parte della stessa Question → fallo e basta; gonfia il ticket oltre una sessione → **splittalo**; lavoro separato o un follow-up → **catturalo** (`idea`/`todo`/`ticket`/`bug`) e continua. Gli extra non diventano mai righe silenziose dentro il ticket su cui sei.
416
460
 
461
+ Un ticket catturato finisce dove appartiene il suo argomento: sulla mappa corrente quando serve la sua destinazione, su un'altra mappa aperta quando serve chiaramente quella, e altrimenti trailhead chiede (ogni mappa aperta, poi la whiteboard). Con una mappa aperta non ripiega mai in silenzio sulla whiteboard, e ti dice in una riga cosa ha aperto e dove.
462
+
417
463
  I ticket che nascono da altro lavoro portano un puntatore di lignaggio nel loro corpo, così la mappa resta tracciabile:
418
464
 
419
465
  | Pointer | Significato |
@@ -445,8 +491,8 @@ Il passo **📊 statusline** offre di installare la status bar di trailhead per
445
491
  | Key | Values (default **bold**) | Effetto |
446
492
  |---|---|---|
447
493
  | `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 |
448
- | `models.{plan,execute,research,review,debug,verify,codebase-map}` | un id modello completo **versionato** (**default del profilo Balanced**; `inherit session` è una scelta esplicita) | quale modello esegue ogni attività; `plan` ed `execute` sono sempre impostati separatamente |
449
- | `models.codex.{plan,execute,research,review,debug,verify,codebase-map}` | un id modello OpenAI, o `{model, effort}` (**inherit session**) | la stessa divisione per attività su Codex, indipendente da `models.*` (id Claude); riesegui l'installer per proiettarla in `~/.codex/agents/` |
494
+ | `models.{plan,execute,research,review,debug,verify,codebase-map,grill}` | un id modello completo **versionato** (**default del profilo Balanced**; `inherit session` è una scelta esplicita) | quale modello esegue ogni attività; `plan` ed `execute` sono sempre impostati separatamente |
495
+ | `models.codex.{plan,execute,research,review,debug,verify,codebase-map,grill}` | un id modello OpenAI, o `{model, effort}` (**inherit session**) | la stessa divisione per attività su Codex, indipendente da `models.*` (id Claude); riesegui l'installer per proiettarla in `~/.codex/agents/` |
450
496
  | `design` | **`disk`** \| `artifacts` \| `stitch` | dove vanno i mockup UI: HTML usa-e-getta locale, un canvas Design **Artifact** su claude.ai (solo Claude Code), o Google Stitch (cross-host, via l'MCP Stitch). Il vecchio valore `claude.ai/design` viene letto come `artifacts` |
451
497
  | `design.project` | URL del canvas Artifact / id del progetto Stitch | solo `artifacts` e `stitch`: il canvas o il progetto Stitch su cui finiscono i mockup; impostato alla prima schermata UI (scegli, incolla o crea), non a mano. Un vecchio progetto claude.ai/design non viene riutilizzato |
452
498
  | `design.surface` | **`canvas`** \| `design-system` | solo `stitch`: un progetto Stitch di schermate (default) o un design system Stitch condiviso |
@@ -468,10 +514,11 @@ Il passo **📊 statusline** offre di installare la status bar di trailhead per
468
514
 
469
515
  - `plan` ed `execute` sono i passi del motore build/bug. Execute è anch'esso un subagent, e committa su `main`.
470
516
  - `research`, `review`, `debug`, `verify` (il controllo goal-backward in Verify), e il fan-out della codebase-map girano ciascuno sulla propria chiave.
517
+ - `grill` esegue i round di grilling sul subagent read-only `trailhead-grill`; se non impostata, usa lo stesso modello di `plan`, e una config esistente senza chiave `grill` fa grilling sul suo modello `plan`. La sessione ti fa comunque ogni domanda e tiene le risposte.
471
518
 
472
- Scegli `plan` ed `execute` separatamente, ciascuno per **id completo versionato** (mai un `opus` o `sonnet` nudo). Una chiave non impostata usa il default del profilo Balanced, e una uguale al modello della sessione gira semplicemente inline; imposta `inherit session` per usare il modello della sessione.
519
+ Scegli `plan` ed `execute` separatamente, ciascuno per **id completo versionato** (mai un `opus` o `sonnet` nudo). Una chiave non impostata usa il default del profilo Balanced, e una uguale al modello della sessione gira semplicemente inline; imposta `inherit session` per usare il modello della sessione. Su Codex, un `models.codex.grill` non impostato segue `models.codex.plan` quando è pinnato (riesegui l'installer per proiettarlo), altrimenti eredita il modello della sessione; un pin uguale al modello della sessione gira inline. Un profilo non scrive mai una voce `grill`, quindi continua a seguire `plan`.
473
520
 
474
- La **sessione principale resta l'orchestratore**. Tiene i momenti interattivi (il Discuss di un build, e l'acceptance/UAT del Verify) e fa charting, grilling e scrittura dei ticket sul proprio modello. Nessuna chiave governa la qualità dei ticket, quindi esegui le sessioni di charting sul tuo modello forte.
521
+ La **sessione principale resta l'orchestratore**. Tiene i momenti interattivi (il Discuss di un build, e l'acceptance/UAT del Verify) e fa charting e scrittura dei ticket sul proprio modello; per il grilling inoltra ogni domanda di `trailhead-grill` e te la pone lei stessa. Nessuna chiave governa la qualità dei ticket, quindi esegui le sessioni di charting sul tuo modello forte.
475
522
 
476
523
  Esempio di file di config (la stessa forma funziona per il `.trailhead/config.json` di progetto e il `trailhead/config.json` globale nella tua directory di config):
477
524
 
package/README.md CHANGED
@@ -26,7 +26,7 @@ Two approaches to agent-driven project work each nail one half of the problem:
26
26
 
27
27
  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.
28
28
 
29
- > **trailhead is self-contained** in the sense that it has **zero external-plugin dependencies**: trailhead-owned agents and the host's built-ins are fine, and no other skill or plugin is required. 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; each engine technique dispatches to **trailhead's own `trailhead-*` agent** (`trailhead-executor`, `trailhead-plan`, `trailhead-research`, `trailhead-code-review`, `trailhead-debug`, plus `trailhead-fix` and `trailhead-codebase-map`), authored once as a committed `agents/trailhead-*.md` file and **projected to each host** (Codex gets `~/.codex/agents/trailhead-*.toml` generated from that same source). These agents are **thin** (identity + tool policy + a config-driven model, with the technique protocol injected at spawn) and run on the host's native subagent toolkit (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 mockups. `design: artifacts` uses Claude Code's built-in `Artifact` tool (nothing to install), and `design: stitch` uses the Google Stitch MCP (offered as a one-time install). See [Mockups before code](#-mockups-before-code).
29
+ > **trailhead is self-contained** in the sense that it has **zero external-plugin dependencies**: trailhead-owned agents and the host's built-ins are fine, and no other skill or plugin is required. 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; each engine technique dispatches to **trailhead's own `trailhead-*` agent** (`trailhead-executor`, `trailhead-plan`, `trailhead-research`, `trailhead-code-review`, `trailhead-debug`, `trailhead-verify`, `trailhead-grill`, plus `trailhead-fix` and `trailhead-codebase-map`), authored once as a committed `agents/trailhead-*.md` file and **projected to each host** (Codex gets `~/.codex/agents/trailhead-*.toml` generated from that same source). These agents are **thin** (identity + tool policy + a config-driven model, with the technique protocol injected at spawn) and run on the host's native subagent toolkit (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 mockups. `design: artifacts` uses Claude Code's built-in `Artifact` tool (nothing to install), and `design: stitch` uses the Google Stitch MCP (offered as a one-time install). See [Mockups before code](#-mockups-before-code).
30
30
 
31
31
  The design choices that fall out of it:
32
32
 
@@ -51,7 +51,7 @@ The design choices that fall out of it:
51
51
 
52
52
  > **Runs on Claude Code and Codex CLI.** trailhead is authored once; the installer projects host-native artifacts. On **Claude Code**: a skill + `/trailhead:*` commands + hooks. On **Codex CLI**: a native skill invoked `$trailhead`, one `$trailhead-<verb>` skill per verb for discoverability, and the guardrails as native Codex hooks. Choose the host with `--claude` / `--codex`, or let the installer auto-detect. More hosts can follow (the installer uses a per-host descriptor).
53
53
 
54
- **Prerequisites:** an authenticated [`gh` CLI](https://cli.github.com) (the tracker is GitHub Issues) and a GitHub repo to work in. The npm path also needs Node 18+.
54
+ **Prerequisites:** an authenticated [`gh` CLI](https://cli.github.com) (the tracker is GitHub Issues) and a GitHub repo to work in. The npm path also needs Node 18+. If an invalid `GH_TOKEN`/`GITHUB_TOKEN` in your environment shadows a working `gh` login, a check at session start says so once, with the fix.
55
55
 
56
56
  ### As a Claude Code plugin (native, managed by `/plugin`)
57
57
  ```
@@ -69,7 +69,7 @@ npx @marcomigozzi/trailhead --symlink # dev install (Claude; symlink to the c
69
69
  npx @marcomigozzi/trailhead --uninstall # remove everything it added (pair with --claude / --codex)
70
70
  npx @marcomigozzi/trailhead --dir=<path> # target a specific config dir
71
71
  ```
72
- 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 templates (label-guard workflow, statusline, the git `commit-msg` hook and the helper scripts the engine runs), idempotently.
72
+ 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** (which also runs the `gh` token check), and the templates (label-guard workflow, statusline, the git `commit-msg` hook and the helper scripts the engine runs), idempotently.
73
73
 
74
74
  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 eight guardrails as native Codex **hooks** (`~/.codex/hooks.json`, enabling `features.hooks`; the five 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.
75
75
 
@@ -230,7 +230,7 @@ The trailhead mod (Claude Code 2.1.287+) ships inside the plugin and draws the w
230
230
 
231
231
  ![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)
232
232
 
233
- The map panel lists the open maps to view, the tickets an autonomous run parked for you in a `Needs you` group at the top, the frontier with a `work` button per ticket that clears the context and starts the ticket, who holds what (your own ticket is marked `this session`), the pending UAT follow-ups in a `UAT` group (each naming the ticket it tests), the paused ones in a `Paused` group, and what is blocked. A dim line under the Whiteboard row totals the open tickets across every open map plus the whiteboard, the same number `/trailhead:map` states (`at least N` when more open maps exist than the panel reads). The footer has `refresh` and `close`. `/trailhead:panel` toggles it, and it opens by itself once in fullscreen.
233
+ The map panel lists the open maps to view, your tickets in progress right under them (in an `In progress` group, from every map and the whiteboard), the tickets an autonomous run parked for you in a `Needs you` group, the frontier with a `work` button per ticket that clears the context and starts the ticket (in the map's checkout; if that checkout has moved, it is found again by its `origin` remote, and you pick one when several match), who holds what (your own ticket is marked `this session`), the pending UAT follow-ups in a `UAT` group (each naming the ticket it tests), the paused ones in a `Paused` group, and what is blocked. A dim line under the Whiteboard row totals the open tickets across every open map plus the whiteboard, the same number `/trailhead:map` states (`at least N` when more open maps exist than the panel reads). The footer has `refresh` and `close`. `/trailhead:panel` toggles it (asked at session start, before the screen is ready, it opens as soon as it is), and it opens by itself once in fullscreen.
234
234
 
235
235
  ![The panel viewing another map, with a viewing and active header and a back to active button](assets/screenshots/map-panel-viewing.png)
236
236
 
@@ -407,13 +407,59 @@ Many people (and their agent sessions) share one map and work it concurrently:
407
407
  - **Scope tickets by path on a monorepo.** A ticket can carry a `Scope:` line naming the package/dir(s) it touches: disjoint scopes parallelize safely, and it scopes build/test and the worktree's commits to the affected package. `/trailhead:adopt` detects a monorepo and proposes both `isolation: worktree` and the `Scope:` convention. Under `isolation: none`, `Scope:` also **serializes** a shared area: at claim time a session holds off if another in-progress ticket's scope overlaps, so two tickets scoped to the same submodule/package can't run at once (how you keep two sessions off one path-bound app).
408
408
  - **Git submodules are handled as separate repos.** Work that lands inside a submodule is committed *inside the submodule* (on a branch) and published before a gitlink bump in the parent points at it. Because superproject worktrees share one submodule git dir, a **submodule-bearing repo resolves to `clone`** (under the default `auto`, or by escalating an explicit `worktree`), so each ticket gets independent submodule state; the clone provisions its submodules at setup (`--recurse-submodules`) and Verify checks `git status` inside each touched submodule. A ticket spanning two submodules stays atomic via a single parent commit that bumps both gitlinks together; its `Scope:` names both, and the same submodule counts as overlapping scope, so two tickets on it serialise. `/trailhead:adopt` detects `.gitmodules` and writes the rule into the conventions.
409
409
  - **State lives on the tickets** (claim, labels, comments; all conflict-free); the map body is a re-readable index, not the source of truth. The durable record of a resolution is the ticket's own comment.
410
- - **Labels are protected from outsiders.** `trailhead:*` labels drive the map, so trailhead trusts a labelled issue only when a write-access collaborator applied the label and (for a ticket) it carries a valid `Parent:`. Anything else is quarantined as `trailhead:unverified`, off the frontier. GitHub already blocks non-write users from labelling; against triage users and automations, trailhead can **install the label guard GitHub Action for you** (from `templates/trailhead-label-guard.yml`) into your repo's `.github/workflows/`, where it strips unauthorized `trailhead:*` labels at the source. Installing a workflow needs a token with the `workflow` scope, so trailhead runs `gh auth refresh -s workflow` if the push is rejected.
411
- - **Issues opened by others are triaged, not trusted blindly.** `/trailhead:inbox` lists inbound issues (bug reports, requests, questions that aren't yet trailhead tickets) and integrates the worthwhile ones **in place**: reframing them as tickets, keeping the reporter's authorship, and applying the labels on adoption. The rest routes to fog, out-of-scope, duplicate, or needs-info.
410
+ - **Labels are protected from outsiders.** `trailhead:*` labels drive the map, so trailhead trusts a labelled issue only when every `trailhead:*` label on it was applied by a trusted labeller (a write-access collaborator, `github-actions[bot]`, or a login in the `TRAILHEAD_LABEL_ALLOWLIST` repo variable: the same labellers the label guard never strips) and (for a ticket) it carries a valid `Parent:`. Anything else is quarantined as `trailhead:unverified`, off the frontier. GitHub already blocks non-write users from labelling; against triage users and automations, trailhead can **install the label guard GitHub Action for you** (from `templates/trailhead-label-guard.yml`) into your repo's `.github/workflows/`, where it strips unauthorized `trailhead:*` labels at the source. Installing a workflow needs a token with the `workflow` scope, so trailhead runs `gh auth refresh -s workflow` if the push is rejected. For automations (CI, cron, bots) that open tickets without a chat session, `templates/trailhead-capture.js` captures a bug, todo, seed or idea with the full label wiring and refuses to run as an identity the guard would strip, and on GitHub the [`actions/capture`](#capture-from-ci) composite Action wraps it in a few lines of workflow.
411
+ - **Issues opened by others are triaged, not trusted blindly.** `/trailhead:inbox` lists inbound issues (bug reports, requests, questions that aren't yet trailhead tickets) and integrates the worthwhile ones **in place**: reframing them as tickets, keeping the reporter's authorship, and applying the labels on adoption. The rest routes to fog, out-of-scope, duplicate, or needs-info. Quarantined `trailhead:unverified` issues are listed there too, to adopt or reject.
412
+
413
+ ### Capture from CI
414
+
415
+ `ToRvaLDz/trailhead/actions/capture` is a thin composite Action over the capture helper: every rule (trust, destination, dedupe, secret scan, labels, edge, dashboard) stays in the helper. Pin a release tag (the Action ships from 0.17.0).
416
+
417
+ ```yaml
418
+ permissions:
419
+ issues: write
420
+
421
+ jobs:
422
+ build:
423
+ runs-on: ubuntu-latest
424
+ steps:
425
+ # ... your build and test steps ...
426
+ - if: failure()
427
+ uses: ToRvaLDz/trailhead/actions/capture@v0.17.0
428
+ with:
429
+ type: bug
430
+ title: "CI failed on ${{ github.ref_name }}"
431
+ body: "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
432
+ dedupe-key: ci-failure-${{ github.job }}
433
+ ```
434
+
435
+ A manual variant works the same way: trigger the job with `workflow_dispatch` and pass `type`, `title` and `body` from `github.event.inputs`.
436
+
437
+ | Input | Required | Meaning |
438
+ |---|---|---|
439
+ | `type` | yes | `bug`, `todo`, `idea` or `seed` |
440
+ | `title` | yes | issue title |
441
+ | `body` / `body-file` | one of | issue body, or a file path (`-` for stdin); not both |
442
+ | `map` / `of` | no | file under map `<n>`, or inherit the map of origin ticket `<n>` (bug only); exclusive |
443
+ | `trigger` | seed only | the `Trigger:` text |
444
+ | `unverified` | no | `true` adds `trailhead:unverified` (not for `idea`) |
445
+ | `dedupe-key` | no | idempotency key, 1 to 100 characters of `A-Za-z0-9._:-` (no spaces or `/`) |
446
+ | `no-dashboard` | no | `true` skips the whiteboard dashboard refresh |
447
+ | `repo` | no | target `owner/name` (default: this repo) |
448
+ | `token` | no | token for the helper (default: `github.token`) |
449
+
450
+ Outputs: `issue`, `url` and `status` (`created`, `existing`, `partial`, `error`, `usage`).
451
+
452
+ - The default token acts as `github-actions[bot]`, which the label guard trusts; the helper verifies it after the create.
453
+ - A re-run with the same `dedupe-key` returns `existing` and creates nothing.
454
+ - `partial` (exit 3, the issue exists but the sub-issue edge failed), `error` and `usage` (exit 2, bad inputs) fail the step; `status` is still set.
455
+ - The runner needs bash, node and gh, all present on GitHub-hosted runners; self-hosted runners must provide them.
412
456
 
413
457
  ## ✅ Keeping tickets honest
414
458
 
415
459
  A ticket is one answerable Question, sized to one session, so you never grow it in flight. When new scope surfaces while you're working (testing especially sparks ideas), you decide per idea: small and part of the same Question → just do it; balloons the ticket past one session → **split** it; separate work or a follow-up → **capture** it (`idea`/`todo`/`ticket`/`bug`) and keep going. Extras never become silent lines inside the ticket you're on.
416
460
 
461
+ A captured ticket lands where its topic belongs: on the current map when it serves that map's destination, on another open map when it plainly serves that one, and otherwise trailhead asks (every open map, then the whiteboard). While a map is open it never falls back to the whiteboard silently, and it tells you in one line what it opened and where.
462
+
417
463
  Tickets that spin off from other work carry a lineage pointer in their body, so the map stays traceable:
418
464
 
419
465
  | Pointer | Meaning |
@@ -445,8 +491,8 @@ The **📊 statusline** step offers to install trailhead's Claude Code status ba
445
491
  | Key | Values (default **bold**) | Effect |
446
492
  |---|---|---|
447
493
  | `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 |
448
- | `models.{plan,execute,research,review,debug,verify,codebase-map}` | a full **versioned** model id (**Balanced-profile default**; `inherit session` is an explicit choice) | which model runs each activity; `plan` and `execute` are always set separately |
449
- | `models.codex.{plan,execute,research,review,debug,verify,codebase-map}` | an OpenAI model id, or `{model, effort}` (**inherit session**) | the same per-activity split on Codex, independent of `models.*` (Claude ids); re-run the installer to project it into `~/.codex/agents/` |
494
+ | `models.{plan,execute,research,review,debug,verify,codebase-map,grill}` | a full **versioned** model id (**Balanced-profile default**; `inherit session` is an explicit choice) | which model runs each activity; `plan` and `execute` are always set separately |
495
+ | `models.codex.{plan,execute,research,review,debug,verify,codebase-map,grill}` | an OpenAI model id, or `{model, effort}` (**inherit session**) | the same per-activity split on Codex, independent of `models.*` (Claude ids); re-run the installer to project it into `~/.codex/agents/` |
450
496
  | `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` |
451
497
  | `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 |
452
498
  | `design.surface` | **`canvas`** \| `design-system` | `stitch` only: a Stitch project of screens (default) or a shared Stitch design system |
@@ -468,10 +514,11 @@ The **📊 statusline** step offers to install trailhead's Claude Code status ba
468
514
 
469
515
  - `plan` and `execute` are the build/bug engine's steps. Execute is a subagent too, and it commits to `main`.
470
516
  - `research`, `review`, `debug`, `verify` (the goal-backward Verify check), and the codebase-map fan-out each run on their own key.
517
+ - `grill` runs the Grilling rounds on the read-only `trailhead-grill` subagent; unset, it uses the same model as `plan`, and an existing config with no `grill` key simply grills on its `plan` model. The session still asks you each question and keeps the answers.
471
518
 
472
- Choose `plan` and `execute` separately, each by **full versioned id** (never a bare `opus` or `sonnet`). A key left unset uses its Balanced-profile default, and one equal to the session model simply runs inline; set `inherit session` to use the session model.
519
+ Choose `plan` and `execute` separately, each by **full versioned id** (never a bare `opus` or `sonnet`). A key left unset uses its Balanced-profile default, and one equal to the session model simply runs inline; set `inherit session` to use the session model. On Codex, an unset `models.codex.grill` follows `models.codex.plan` when that is pinned (re-run the installer to project it), else inherits the session model; a pin equal to the session model runs inline. A profile never writes a `grill` entry, so it keeps following `plan`.
473
520
 
474
- The **main session stays the orchestrator**. It holds the interactive moments (a build's Discuss, and Verify's acceptance/UAT) and it does charting, grilling, and ticket-writing on its own model. No key governs ticket quality, so run charting sessions on your strong model.
521
+ The **main session stays the orchestrator**. It holds the interactive moments (a build's Discuss, and Verify's acceptance/UAT) and it does charting and ticket-writing on its own model; for grilling it relays each question from `trailhead-grill` and asks you itself. No key governs ticket quality, so run charting sessions on your strong model.
475
522
 
476
523
  Example config file (the same shape works for the project `.trailhead/config.json` and the global `trailhead/config.json` in your config directory):
477
524
 
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env node
2
+ // Test for #334: the composite Action actions/capture is a thin wrapper over the capture
3
+ // helper. Checks the input/output mirror, injection safety, the run block against a stub
4
+ // helper, and the docs. Plain asserts, no framework, no YAML dependency.
5
+ // Run: node bin/capture-action.test.js
6
+ const assert = require('assert');
7
+ const fs = require('fs');
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const { spawnSync } = require('child_process');
11
+
12
+ const root = path.resolve(__dirname, '..');
13
+ const actionPath = path.join(root, 'actions', 'capture', 'action.yml');
14
+ let passed = 0;
15
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
16
+ const eq = (name, a, b) => { assert.deepStrictEqual(a, b, name); passed++; };
17
+ const EMDASH = String.fromCharCode(0x2014);
18
+
19
+ const yml = fs.readFileSync(actionPath, 'utf8');
20
+ const lines = yml.split('\n');
21
+
22
+ // Top-level keys under a section (2-space indented names) of the minimal YAML.
23
+ function sectionKeys(name) {
24
+ const i = lines.findIndex((l) => l === `${name}:`);
25
+ ok(`action.yml has a ${name}: section`, i >= 0);
26
+ const keys = [];
27
+ for (let j = i + 1; j < lines.length; j++) {
28
+ if (lines[j] !== '' && !lines[j].startsWith(' ')) break;
29
+ const m = /^ {2}([A-Za-z0-9_-]+):/.exec(lines[j]);
30
+ if (m) keys.push(m[1]);
31
+ }
32
+ return keys;
33
+ }
34
+
35
+ // Mirror
36
+ eq('inputs mirror the helper flags plus token',
37
+ sectionKeys('inputs').sort(),
38
+ ['body', 'body-file', 'dedupe-key', 'map', 'no-dashboard', 'of', 'repo', 'title', 'token', 'trigger', 'type', 'unverified']);
39
+ eq('outputs are issue, url, status', sectionKeys('outputs').sort(), ['issue', 'status', 'url']);
40
+ for (const name of ['issue', 'url', 'status']) {
41
+ ok(`output ${name} reads the capture step`, yml.includes(`value: \${{ steps.capture.outputs.${name} }}`));
42
+ }
43
+ ok('composite action', /^ {2}using: composite$/m.test(yml));
44
+ ok('the step has id: capture', /^ {4}- id: capture$/m.test(yml));
45
+ ok('description notes issues: write', /issues: write/.test(yml.split('\ninputs:')[0]));
46
+ ok('branding present', /^branding:/m.test(yml));
47
+
48
+ // Extract the literal run block (fixed indentation: `run: |` then 8-space lines).
49
+ const runIdx = lines.findIndex((l) => /^ {6}run: \|$/.test(l));
50
+ assert.ok(runIdx >= 0, 'run: | literal block not found in action.yml');
51
+ const runLines = [];
52
+ for (let j = runIdx + 1; j < lines.length; j++) {
53
+ if (lines[j] === '') { runLines.push(''); continue; }
54
+ if (!lines[j].startsWith(' ')) break;
55
+ runLines.push(lines[j].slice(8));
56
+ }
57
+ const script = runLines.join('\n');
58
+ ok('run block is non-empty', script.trim().length > 0);
59
+
60
+ // Injection safety
61
+ ok('no ${{ expression inside the run script', !script.includes('${{'));
62
+ ok('inputs reach the script through env:', /^ {6}env:$/m.test(yml) && /INPUT_BODY: \$\{\{ inputs\.body \}\}/.test(yml));
63
+
64
+ // Runtime against a stub helper
65
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'capture-action-'));
66
+ const actionDir = path.join(tmp, 'actions', 'capture');
67
+ const tplDir = path.join(tmp, 'plugins', 'trailhead', 'templates');
68
+ fs.mkdirSync(actionDir, { recursive: true });
69
+ fs.mkdirSync(tplDir, { recursive: true });
70
+ fs.writeFileSync(path.join(tmp, 'run.sh'), script);
71
+ fs.writeFileSync(path.join(tplDir, 'trailhead-capture.js'), `
72
+ const fs = require('fs');
73
+ fs.writeFileSync(process.env.STUB_ARGV_FILE, JSON.stringify({ argv: process.argv.slice(2), token: process.env.GH_TOKEN || null }));
74
+ if (process.env.STUB_RAW !== undefined) process.stdout.write(process.env.STUB_RAW);
75
+ else console.log(process.env.STUB_JSON || '{"status":"created","issue":7,"url":"https://github.com/o/r/issues/7"}');
76
+ process.exit(Number(process.env.STUB_EXIT || 0));
77
+ `);
78
+
79
+ const baseInputs = { INPUT_TYPE: 'bug', INPUT_TITLE: 'T', INPUT_REPO: 'o/r', INPUT_TOKEN: 'tok' };
80
+ function run(inputs, stub = {}) {
81
+ const out = path.join(tmp, 'output.txt');
82
+ const argvFile = path.join(tmp, 'argv.json');
83
+ fs.rmSync(out, { force: true });
84
+ fs.rmSync(argvFile, { force: true });
85
+ const env = {
86
+ PATH: process.env.PATH,
87
+ GITHUB_ACTION_PATH: actionDir,
88
+ GITHUB_OUTPUT: out,
89
+ STUB_ARGV_FILE: argvFile,
90
+ GH_TOKEN: inputs.INPUT_TOKEN || '',
91
+ ...inputs,
92
+ ...stub,
93
+ };
94
+ const r = spawnSync('bash', ['--noprofile', '--norc', '-eo', 'pipefail', path.join(tmp, 'run.sh')], { env, encoding: 'utf8' });
95
+ const outputs = {};
96
+ if (fs.existsSync(out)) {
97
+ for (const l of fs.readFileSync(out, 'utf8').split('\n')) {
98
+ const m = /^([a-z]+)=(.*)$/.exec(l);
99
+ if (m) outputs[m[1]] = m[2];
100
+ }
101
+ }
102
+ const seen = fs.existsSync(argvFile) ? JSON.parse(fs.readFileSync(argvFile, 'utf8')) : null;
103
+ return { status: r.status, outputs, seen, stderr: r.stderr };
104
+ }
105
+
106
+ let r = run(baseInputs);
107
+ eq('minimal argv: empty optionals omitted', r.seen.argv, ['--type', 'bug', '--title', 'T', '--repo', 'o/r']);
108
+ eq('exit 0 on created', r.status, 0);
109
+ eq('outputs from the JSON line', r.outputs, { issue: '7', url: 'https://github.com/o/r/issues/7', status: 'created' });
110
+ eq('GH_TOKEN reaches the helper', r.seen.token, 'tok');
111
+
112
+ r = run({ ...baseInputs, INPUT_BODY: 'a\nb "c" $HOME\n', INPUT_BODY_FILE: 'f.md', INPUT_MAP: '5', INPUT_OF: '9', INPUT_TRIGGER: 'x y',
113
+ INPUT_UNVERIFIED: 'true', INPUT_DEDUPE_KEY: 'k1', INPUT_NO_DASHBOARD: 'true' });
114
+ eq('full argv maps 1:1, multi-line body intact, booleans bare',
115
+ r.seen.argv,
116
+ ['--type', 'bug', '--title', 'T', '--body', 'a\nb "c" $HOME\n', '--body-file', 'f.md', '--repo', 'o/r', '--map', '5', '--of', '9',
117
+ '--trigger', 'x y', '--unverified', '--dedupe-key', 'k1', '--no-dashboard']);
118
+
119
+ r = run({ ...baseInputs, INPUT_UNVERIFIED: 'false', INPUT_NO_DASHBOARD: '' });
120
+ ok('false/empty booleans are absent', !r.seen.argv.includes('--unverified') && !r.seen.argv.includes('--no-dashboard'));
121
+
122
+ r = run(baseInputs, { STUB_JSON: '{"status":"existing"}' });
123
+ eq('status only when JSON lacks issue/url', r.outputs, { issue: '', url: '', status: 'existing' });
124
+ eq('existing exits 0', r.status, 0);
125
+
126
+ for (const code of [3, 1, 2]) {
127
+ r = run(baseInputs, { STUB_EXIT: String(code), STUB_JSON: `{"status":"x${code}","issue":4,"url":"u"}` });
128
+ eq(`helper exit ${code} propagates`, r.status, code);
129
+ eq(`status still written on exit ${code}`, r.outputs.status, `x${code}`);
130
+ }
131
+
132
+ for (const [label, raw] of [['malformed', 'not json\n'], ['empty', '']]) {
133
+ r = run(baseInputs, { STUB_RAW: raw, STUB_EXIT: '3' });
134
+ eq(`${label} stdout never masks exit 3`, r.status, 3);
135
+ eq(`${label} stdout leaves outputs empty`, { issue: r.outputs.issue || '', url: r.outputs.url || '', status: r.outputs.status || '' }, { issue: '', url: '', status: '' });
136
+ r = run(baseInputs, { STUB_RAW: raw, STUB_EXIT: '0' });
137
+ eq(`${label} stdout with exit 0 exits 1`, r.status, 1);
138
+ }
139
+
140
+ fs.rmSync(tmp, { recursive: true, force: true });
141
+
142
+ // Docs
143
+ const readme = fs.readFileSync(path.join(root, 'README.md'), 'utf8');
144
+ const readmeIt = fs.readFileSync(path.join(root, 'README.it.md'), 'utf8');
145
+ for (const [name, doc] of [['README.md', readme], ['README.it.md', readmeIt]]) {
146
+ ok(`${name} names the Action`, doc.includes('ToRvaLDz/trailhead/actions/capture@'));
147
+ ok(`${name} names issues: write`, doc.includes('issues: write'));
148
+ ok(`${name} names dedupe-key`, doc.includes('dedupe-key'));
149
+ }
150
+ const capDoc = fs.readFileSync(path.join(root, 'plugins', 'trailhead', 'skills', 'trailhead-capture', 'references', 'capture.md'), 'utf8');
151
+ ok('capture.md names the Action', capDoc.includes('actions/capture'));
152
+ for (const [name, text] of [['action.yml', yml], ['README.md', readme], ['README.it.md', readmeIt], ['capture.md', capDoc], ['test', fs.readFileSync(__filename, 'utf8').replace(/String\.fromCharCode\(0x2014\)/g, '')]]) {
153
+ ok(`no em-dash in ${name}`, !text.includes(EMDASH));
154
+ }
155
+ ok('real helper path used by the run block exists',
156
+ fs.existsSync(path.join(root, 'actions/capture', '../../plugins/trailhead/templates/trailhead-capture.js')));
157
+ for (const [name, doc] of [['README.md', readme], ['README.it.md', readmeIt]]) {
158
+ const keys = [...doc.matchAll(/^\s*dedupe-key: (.+)$/gm)].map((m) => m[1]);
159
+ ok(`${name} has a dedupe-key example`, keys.length > 0);
160
+ for (const k of keys) {
161
+ ok(`${name} dedupe-key example avoids unsafe contexts`, !/github\.(ref_name|workflow|head_ref|ref)\b/.test(k));
162
+ // job ids are [A-Za-z0-9_-]; run_id and run_number are digits
163
+ const resolved = k.replace(/\$\{\{\s*github\.(job|run_id|run_number)\s*\}\}/g, 'x1');
164
+ ok(`${name} dedupe-key example matches the helper regex`, /^[A-Za-z0-9._:-]{1,100}$/.test(resolved));
165
+ }
166
+ }
167
+ console.log(`capture-action.test.js: ${passed} checks passed`);
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #332: capture.md documents the scripted capture helper (CLI, statuses,
3
+ // exit codes, trust, idea as fog, dedupe, dashboard). Plain asserts, no framework.
4
+ // Run: node bin/capture-docs.test.js
5
+ const assert = require('assert');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const root = path.resolve(__dirname, '..');
10
+ const doc = fs.readFileSync(path.join(root, 'plugins', 'trailhead', 'skills', 'trailhead-capture', 'references', 'capture.md'), 'utf8');
11
+ const start = doc.indexOf('## Scripted capture for automations');
12
+ const section = start < 0 ? '' : doc.slice(start);
13
+ let passed = 0;
14
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
15
+
16
+ ok('capture.md has the scripted capture section', section !== '');
17
+ ok('names the helper', section.includes('trailhead-capture.js'));
18
+ for (const flag of ['--type', '--title', '--body-file', '--repo', '--map', '--of', '--trigger', '--unverified', '--dedupe-key', '--no-dashboard']) {
19
+ ok(`names ${flag}`, section.includes(flag));
20
+ }
21
+ for (const status of ['created', 'existing', 'partial', 'error', 'usage']) {
22
+ ok(`documents status ${status}`, section.includes(`"status":"${status}"`));
23
+ }
24
+ ok('documents exit codes 0, 1, 2 and 3', ['exit 0', 'exit 1', 'exit 2', 'exit 3'].every((c) => section.includes(c)));
25
+ ok('partial names prune', /partial[^]*\/trailhead:prune/.test(section));
26
+ ok('trust: allowlist is the repo variable, not the local env', section.includes('TRAILHEAD_LABEL_ALLOWLIST') && /never consults its local environment/.test(section));
27
+ ok('trust: installation tokens are verified post-create', section.includes('github-actions[bot]'));
28
+ ok('idea is parked fog', /`idea` is parked fog/.test(section) && section.includes('`trailhead:fog`'));
29
+ ok('dedupe: marker, cap fallback and the lag residual', section.includes('trailhead-dedupe') && section.includes('1000-result cap') && /search-index lag/.test(section));
30
+ ok('dashboard rule: whiteboard only, update-only', /whiteboard ticket refreshes the dashboard/.test(section) && section.includes('--update-only'));
31
+ ok('no em-dash', !doc.includes(String.fromCharCode(0x2014)));
32
+ console.log(`capture-docs.test.js: ${passed} checks passed`);