@marcomigozzi/trailhead 0.12.0 → 0.13.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.
- package/README.it.md +11 -11
- package/README.md +11 -11
- package/bin/lib/codex-projection.js +2 -2
- package/bin/trailhead.test.js +27 -6
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/hooks/lib/fixtures/label-guard-0.12.0.yml +55 -0
- package/plugins/trailhead/hooks/lib/label-guard-sync.test.js +156 -0
- package/plugins/trailhead/hooks/trailhead-check-update.js +25 -1
- package/plugins/trailhead/hooks/trailhead-mockup-link-guard.test.js +4 -4
- package/plugins/trailhead/hooks/trailhead-mockup-link-stop.test.js +1 -1
- package/plugins/trailhead/skills/_shared/configuration-reference.md +5 -4
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/substrate-commands.md +1 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +2 -0
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +15 -16
- package/plugins/trailhead/skills/_shared/techniques.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/references/updating.md +4 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +1 -1
- package/plugins/trailhead/templates/trailhead-label-guard-sync.js +212 -0
- package/plugins/trailhead/templates/trailhead-label-guard.yml +5 -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'unica skill, 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: la modalità mockup hosted. Quando abilitata, `
|
|
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: la modalità mockup hosted. Quando abilitata, `artifacts` costruisce il mockup come canvas Design **Artifact** su claude.ai tramite il tool `Artifact` integrato in Claude Code (solo Claude Code, niente da installare), con il tuo design system installato sul canvas. Una seconda modalità hosted, `stitch`, usa l'MCP Google Stitch e funziona sia su **Claude Code sia su Codex**, quindi è l'opzione cross-host; se il suo MCP non è connesso, offre un'installazione una tantum e ripiega sui mockup su disco locale se rifiuti. Dove il tool `Artifact` non c'è (Codex, sessioni con API key), offre di passare a `stitch` prima di ripiegare sul disco locale.
|
|
30
30
|
|
|
31
31
|
Le scelte di design che ne derivano:
|
|
32
32
|
|
|
@@ -70,7 +70,7 @@ npx @marcomigozzi/trailhead --dir=<path> # punta a una config dir specifica
|
|
|
70
70
|
```
|
|
71
71
|
Su **Claude Code** copia la skill (+ le sue `references/`), i comandi `/trailhead:*`, gli hook (in `hooks/`, registrati in `settings.json`) incluso un **update check** al SessionStart, e i template di label-guard + statusline, in modo idempotente.
|
|
72
72
|
|
|
73
|
-
Su **Codex CLI** (`--codex`) proietta una skill nativa Codex invocata come **`$trailhead`** (`$trailhead work`, `$trailhead new "idea"`, …), una skill sottile **`$trailhead-<verb>`** per ogni verbo per la discoverability, le sei guardrail come **hook** nativi di Codex (`~/.codex/hooks.json`, abilitando `features.hooks`; la search guard è solo per Claude Code e non è proiettata su Codex), e i pin di modello per-tecnica da **`models.codex.*`** in `~/.codex/agents/`. Serve Codex **0.145.0+** (l'installer fa un gate su `codex --version`). La modalità mockup `
|
|
73
|
+
Su **Codex CLI** (`--codex`) proietta una skill nativa Codex invocata come **`$trailhead`** (`$trailhead work`, `$trailhead new "idea"`, …), una skill sottile **`$trailhead-<verb>`** per ogni verbo per la discoverability, le sei guardrail come **hook** nativi di Codex (`~/.codex/hooks.json`, abilitando `features.hooks`; la search guard è solo per Claude Code e non è proiettata su Codex), e i pin di modello per-tecnica da **`models.codex.*`** in `~/.codex/agents/`. Serve Codex **0.145.0+** (l'installer fa un gate su `codex --version`). La modalità mockup `artifacts` (il tool `Artifact` di Claude Code) non è disponibile su Codex; lì l'opzione hosted è la modalità cross-host `design: stitch` (MCP Google Stitch), con il disco locale come ripiego.
|
|
74
74
|
|
|
75
75
|
Ri-esegui l'install per aggiornare, oppure esegui **`/trailhead:update`** (Claude) / **`$trailhead update`** (Codex) da dentro l'agente: rileva come trailhead è stato installato e installa la versione più recente dove è sicuro farlo (un `git pull` per un dev-symlink, una ri-esecuzione `npx` per npm, o `/plugin update` per il plugin). Quando esiste una versione più recente, la statusline mostra un flag `⬆ trailhead <version>` (Claude).
|
|
76
76
|
|
|
@@ -130,7 +130,7 @@ Frontier iniziale = ①③④, tre persone possono partire in parallelo. Poi:
|
|
|
130
130
|
- **① research** → un subagent legge la documentazione, restituisce un finding pronto per la decisione, chiude → sblocca ②.
|
|
131
131
|
- **② decision** → dirama le opzioni, grill per scegliere native vs custom, registra il perché.
|
|
132
132
|
- **③ task** → l'agente ti consegna una checklist (servono i *tuoi* account Google/GitHub); tu registri le app, lui registra dove vivono le credenziali.
|
|
133
|
-
- **④ prototype** → una schermata di login grezza (sul
|
|
133
|
+
- **④ prototype** → una schermata di login grezza (sul canvas Artifacts configurato), approvata prima del codice UI.
|
|
134
134
|
- Con ②③④ chiusi, **⑤ build** si diploma → discuss → plan → execute (commit atomici, TDD ai seam dell'auth) → verify (test + code review + acceptance: chiesto chi guida, l'agente va AI-driven e guida il browser attraverso il vero flusso di login).
|
|
135
135
|
- Dopo il rilascio noti un redirect sbagliato in prod → `/trailhead:bug --of ⑤ "GitHub redirect goes to localhost"` → un nuovo ticket che porta `Regression of: ⑤`, lavorato repro → diagnose → fix → verify. ⑤ resta chiuso.
|
|
136
136
|
|
|
@@ -229,7 +229,7 @@ Ogni tipo ha il proprio motore inline: nessuna skill esterna viene invocata.
|
|
|
229
229
|
|---|---|---|---|
|
|
230
230
|
| 🧭 `decision` | una scelta | HITL | diverge le opzioni se non chiare, poi grill per convergere su una |
|
|
231
231
|
| 🔬 `research` | un fatto | AFK | un subagent su un branch usa-e-getta (l'unico tipo eseguito in parallelo) |
|
|
232
|
-
| 🎨 `prototype` | una direzione approvata | HITL | un artefatto grezzo usa-e-getta a cui reagire; le schermate UI passano da qui (disco,
|
|
232
|
+
| 🎨 `prototype` | una direzione approvata | HITL | un artefatto grezzo usa-e-getta a cui reagire; le schermate UI passano da qui (disco, un canvas Artifacts, o Stitch) prima del codice UI |
|
|
233
233
|
| 🔨 `build` | codice funzionante | HITL/AFK | `discuss → plan → execute → verify`: commit atomici, TDD ai seam, **prima un mockup per la UI rivolta all'utente** (tecnica Prototype, gated da `design.approval`), code review + acceptance testing (chiede chi guida, AI o umano, poi browser-drive o UAT conversazionale passo-passo) |
|
|
234
234
|
| 🐛 `bug` | codice corretto | HITL/AFK | `repro → diagnose → fix → verify`; un difetto in lavoro chiuso è un ticket *nuovo* (`Regression of:`), non una riapertura |
|
|
235
235
|
| 🔧 `task` | un cambiamento di stato esterno | HITL/AFK | lavoro manuale che sblocca una decision (provisioning accessi, spostamento dati, iscrizioni) |
|
|
@@ -304,8 +304,9 @@ Il passo **📊 statusline** offre di installare la status bar di trailhead per
|
|
|
304
304
|
|---|---|---|
|
|
305
305
|
| `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 |
|
|
306
306
|
| `models.{plan,execute,research,review,debug,verify,codebase-map}` | un id modello completo **versionato** (**inherit session**) | quale modello esegue ogni attività; `plan` ed `execute` sono sempre impostati separatamente |
|
|
307
|
-
| `design` | **`disk`** \| `
|
|
308
|
-
| `design.surface` | **`canvas`** \| `design-system` |
|
|
307
|
+
| `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` |
|
|
308
|
+
| `design.surface` | **`canvas`** \| `design-system` | solo `stitch`: un progetto Stitch di schermate (default) o un design system Stitch condiviso |
|
|
309
|
+
| `design.system` | URL di un Design System Artifact | solo `artifacts`: il design system installato sul canvas dei mockup (chiesto alla prima schermata se non impostato) |
|
|
309
310
|
| `design.approval` | **`explicit`** \| `auto` | attendi l'approvazione del mockup prima del codice UI, o procedi senza bloccare |
|
|
310
311
|
| `tdd` | **`seams`** \| `on` \| `off` | come il motore `build` testa |
|
|
311
312
|
| `acceptance.browser` | **`auto`** \| `on` \| `off` | guida il browser in Verify (dopo aver chiesto chi guida, AI o umano); `off` esegue comunque controlli AI-driven via comandi, e ti accompagna in una UAT conversazionale per ciò che solo tu puoi fare (passo-passo in chat, non una checklist da fare da solo) |
|
|
@@ -330,14 +331,13 @@ Esempio di file di config (la stessa forma funziona per il `.trailhead/config.js
|
|
|
330
331
|
{ "ticket": { "language": "en" }, "models": { "plan": "claude-opus-5-5", "execute": "claude-sonnet-5-5" }, "tdd": "seams", "acceptance": { "browser": "auto" } }
|
|
331
332
|
```
|
|
332
333
|
|
|
333
|
-
**Design mockups.** `design: disk` (il default) deposita un mockup HTML statico usa-e-getta accanto al codice e lo collega dal ticket. `design:
|
|
334
|
+
**Design mockups.** `design: disk` (il default) deposita un mockup HTML statico usa-e-getta accanto al codice e lo collega dal ticket. `design: artifacts` costruisce il mockup come **canvas Design Artifact** su claude.ai, tramite il tool `Artifact` integrato in Claude Code (niente da installare), dove lo affini visivamente, lo commenti e lo esporti; `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).
|
|
334
335
|
|
|
335
|
-
|
|
336
|
-
- **`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.
|
|
336
|
+
Con **`artifacts`**, alla prima schermata UI trailhead aggancia un canvas: **sceglierne uno esistente**, **incollare un URL**, oppure **crearne uno nuovo** (ti chiede il nome), e ne mette in cache l'URL in `design.project`. Ogni schermata (e ogni variante) è una propria tavola, raggruppata in una pagina del canvas per ticket; il design system in `design.system` (quello predefinito del tuo account, o quello che scegli) viene installato sul canvas, così colori, tipografia e componenti sono a portata di mano. I messaggi di approvazione riportano l'URL del canvas più il nome di ogni tavola. Una volta che approvi, trailhead **rilegge** le tavole (nel caso tu le abbia modificate live) prima di scrivere qualsiasi codice UI.
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
Con **`stitch`**, `design.surface` sceglie un progetto Stitch di schermate (`canvas`, default) o un design system Stitch condiviso (`design-system`); trailhead lo sceglie, incolla o crea allo stesso modo, e offre una volta di installare l'MCP Stitch se non è connesso (se rifiuti, ripiega sul disco locale).
|
|
339
339
|
|
|
340
|
-
|
|
340
|
+
**Passare da claude.ai/design.** Il sito standalone di Claude Design chiude il 14/12/2026 e i suoi design ora vivono in Claude come Artifacts. Una config che dice ancora `design: claude.ai/design` continua a funzionare come `artifacts` (trailhead te lo dice una volta e offre di riscrivere la chiave), ma un progetto claude.ai/design in cache non è riutilizzabile: migra i design system dal banner della pagina Artifacts e lascia che trailhead agganci un nuovo canvas. Dove il tool `Artifact` non c'è (Codex, o una sessione con API key), trailhead offre di passare a `stitch` 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`).
|
|
341
341
|
|
|
342
342
|
---
|
|
343
343
|
|
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 mockup mode. When enabled, `
|
|
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 mockup mode. When enabled, `artifacts` builds the mockup as a Design canvas **Artifact** on claude.ai through Claude Code's built-in `Artifact` tool (Claude Code only, nothing to install), with your design system installed on the canvas. A second hosted mode, `stitch`, uses the Google Stitch MCP and works on **both Claude Code and Codex**, so it's the cross-host option; if its MCP isn't connected, it offers a one-time install and falls back to local-disk mockups if you decline. Where the `Artifact` tool isn't available (Codex, API-key sessions), it offers to switch to `stitch` before falling to local disk.
|
|
30
30
|
|
|
31
31
|
The design choices that fall out of it:
|
|
32
32
|
|
|
@@ -70,7 +70,7 @@ npx @marcomigozzi/trailhead --dir=<path> # target a specific config dir
|
|
|
70
70
|
```
|
|
71
71
|
On **Claude Code** it copies the skill (+ its `references/`), the `/trailhead:*` commands, the hooks (into `hooks/`, registered in `settings.json`) including a SessionStart **update check**, and the label-guard + statusline templates, idempotently.
|
|
72
72
|
|
|
73
|
-
On **Codex CLI** (`--codex`) it projects a native Codex skill invoked as **`$trailhead`** (`$trailhead work`, `$trailhead new "idea"`, …), one thin **`$trailhead-<verb>`** skill per verb for discoverability, the six guardrails as native Codex **hooks** (`~/.codex/hooks.json`, enabling `features.hooks`; the search guard is Claude-Code-only and isn't projected to Codex), and per-technique subagent model pins from **`models.codex.*`** into `~/.codex/agents/`. Codex **0.145.0+** is required (the installer gates on `codex --version`). The `
|
|
73
|
+
On **Codex CLI** (`--codex`) it projects a native Codex skill invoked as **`$trailhead`** (`$trailhead work`, `$trailhead new "idea"`, …), one thin **`$trailhead-<verb>`** skill per verb for discoverability, the six guardrails as native Codex **hooks** (`~/.codex/hooks.json`, enabling `features.hooks`; the search guard is Claude-Code-only and isn't projected to Codex), and per-technique subagent model pins from **`models.codex.*`** into `~/.codex/agents/`. Codex **0.145.0+** is required (the installer gates on `codex --version`). The `artifacts` mockup mode (Claude Code's `Artifact` tool) isn't available on Codex; the cross-host `design: stitch` mode (Google Stitch MCP) is the hosted option there, with local disk as the fallback.
|
|
74
74
|
|
|
75
75
|
Re-run the install to update, or run **`/trailhead:update`** (Claude) / **`$trailhead update`** (Codex) from inside the agent: it detects how trailhead was installed and installs the newer version where that is safe (a `git pull` for a dev-symlink, an `npx` re-run for npm, or `/plugin update` for the plugin). When a newer version exists, the statusline shows a `⬆ trailhead <version>` flag (Claude).
|
|
76
76
|
|
|
@@ -130,7 +130,7 @@ Initial frontier = ①③④, three people can start in parallel. Then:
|
|
|
130
130
|
- **① research** → a subagent reads the docs, returns a decision-ready finding, closes → unblocks ②.
|
|
131
131
|
- **② decision** → diverge the options, grill to choose native vs custom, record the why.
|
|
132
132
|
- **③ task** → the agent hands you a checklist (it needs *your* Google/GitHub accounts); you register the apps, it records where the credentials live.
|
|
133
|
-
- **④ prototype** → a rough login screen (on the configured
|
|
133
|
+
- **④ prototype** → a rough login screen (on the configured Artifacts canvas), approved before UI code.
|
|
134
134
|
- With ②③④ closed, **⑤ build** graduates → discuss → plan → execute (atomic commits, TDD at the auth seams) → verify (tests + code review + acceptance: asked who drives, the agent goes AI-driven and drives the browser through the real login flow).
|
|
135
135
|
- After it ships you spot a wrong redirect in prod → `/trailhead:bug --of ⑤ "GitHub redirect goes to localhost"` → a new ticket carrying `Regression of: ⑤`, worked repro → diagnose → fix → verify. ⑤ stays closed.
|
|
136
136
|
|
|
@@ -229,7 +229,7 @@ Each type has its own inline engine: no external skill is invoked.
|
|
|
229
229
|
|---|---|---|---|
|
|
230
230
|
| 🧭 `decision` | a choice | HITL | diverge the options if unclear, then grill to converge on one |
|
|
231
231
|
| 🔬 `research` | a fact | AFK | a subagent on a throwaway branch (the only type run in parallel) |
|
|
232
|
-
| 🎨 `prototype` | an approved direction | HITL | a rough throwaway artifact to react to; UI screens go through this (disk,
|
|
232
|
+
| 🎨 `prototype` | an approved direction | HITL | a rough throwaway artifact to react to; UI screens go through this (disk, an Artifacts canvas, or Stitch) before UI code |
|
|
233
233
|
| 🔨 `build` | working code | HITL/AFK | `discuss → plan → execute → verify`: atomic commits, TDD at seams, **a mockup first for user-facing UI** (Prototype technique, gated by `design.approval`), code review + acceptance testing (asks who drives, AI or human, then browser-drive or conversational step-by-step UAT) |
|
|
234
234
|
| 🐛 `bug` | corrected code | HITL/AFK | `repro → diagnose → fix → verify`; a defect in closed work is a *new* ticket (`Regression of:`), not a reopen |
|
|
235
235
|
| 🔧 `task` | an external state change | HITL/AFK | manual work that unblocks a decision (provision access, move data, sign up) |
|
|
@@ -304,8 +304,9 @@ The **📊 statusline** step offers to install trailhead's Claude Code status ba
|
|
|
304
304
|
|---|---|---|
|
|
305
305
|
| `ticket.language` | an ISO 639-1 code (**`en`**) | the language trailhead **writes** its GitHub prose & commit descriptions in, decoupled from the language it converses in |
|
|
306
306
|
| `models.{plan,execute,research,review,debug,verify,codebase-map}` | a full **versioned** model id (**inherit session**) | which model runs each activity; `plan` and `execute` are always set separately |
|
|
307
|
-
| `design` | **`disk`** \| `
|
|
308
|
-
| `design.surface` | **`canvas`** \| `design-system` |
|
|
307
|
+
| `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` |
|
|
308
|
+
| `design.surface` | **`canvas`** \| `design-system` | `stitch` only: a Stitch project of screens (default) or a shared Stitch design system |
|
|
309
|
+
| `design.system` | Design System Artifact URL | `artifacts` only: the design system installed on the mockup canvas (asked at the first screen if unset) |
|
|
309
310
|
| `design.approval` | **`explicit`** \| `auto` | wait for mockup approval before UI code, or proceed without blocking |
|
|
310
311
|
| `tdd` | **`seams`** \| `on` \| `off` | how the `build` engine tests |
|
|
311
312
|
| `acceptance.browser` | **`auto`** \| `on` \| `off` | drive the browser in Verify (after asking who drives, AI or human); `off` still runs AI-driven checks via commands, and walks you through a conversational UAT for anything only you can do (step by step in chat, not a checklist to self-serve) |
|
|
@@ -330,14 +331,13 @@ Example config file (the same shape works for the project `.trailhead/config.jso
|
|
|
330
331
|
{ "ticket": { "language": "en" }, "models": { "plan": "claude-opus-5-5", "execute": "claude-sonnet-5-5" }, "tdd": "seams", "acceptance": { "browser": "auto" } }
|
|
331
332
|
```
|
|
332
333
|
|
|
333
|
-
**Design mockups.** `design: disk` (the default) drops a throwaway static HTML mockup next to the code and links it from the ticket. `design:
|
|
334
|
+
**Design mockups.** `design: disk` (the default) drops a throwaway static HTML mockup next to the code and links it from the ticket. `design: artifacts` builds the mockup as a **Design canvas Artifact** on claude.ai, through Claude Code's built-in `Artifact` tool (nothing to install), where you refine it visually, comment on it and export it; `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).
|
|
334
335
|
|
|
335
|
-
|
|
336
|
-
- **`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.
|
|
336
|
+
With **`artifacts`**, on the first UI screen trailhead attaches a canvas: **pick an existing one**, **paste a URL**, or **create a new one** (it asks you for the name), and caches its URL in `design.project`. Each screen (and each variant) is its own artboard, grouped in a canvas page per ticket; the design system in `design.system` (your account's default one, or the one you pick) is installed on the canvas so its colours, type and components are at hand. Approval messages carry the canvas URL plus the name of each artboard. Once you approve, trailhead **re-fetches** the artboards (in case you edited them live) before writing any UI code.
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
With **`stitch`**, `design.surface` picks a Stitch project of screens (`canvas`, default) or a shared Stitch design system (`design-system`); trailhead picks, pastes or creates it the same way, and offers once to install the Stitch MCP if it isn't connected (decline and it falls back to local disk).
|
|
339
339
|
|
|
340
|
-
|
|
340
|
+
**Moving from claude.ai/design.** The standalone Claude Design site closes on 2026-12-14 and its designs now live in Claude as Artifacts. A config that still says `design: claude.ai/design` keeps working as `artifacts` (trailhead tells you once and offers to rewrite the key), but a cached claude.ai/design project can't be reused: migrate your design systems from the Artifacts page banner and let trailhead attach a new canvas. Where the `Artifact` tool isn't available (Codex, or an API-key session), trailhead offers to switch to `stitch` 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`).
|
|
341
341
|
|
|
342
342
|
---
|
|
343
343
|
|
|
@@ -115,8 +115,8 @@ The engine's \`/clear\` is Codex's \`/new\` (start a fresh session). Command ref
|
|
|
115
115
|
## F. Lifecycle hooks (native Codex hooks)
|
|
116
116
|
Codex has a hook bus, so trailhead's guardrails run as **real Codex hooks**, not degraded prose. The installer registers them in \`~/.codex/hooks.json\`: commit-guard, secret-guard, install-guard and body-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs, secret-read-guard as a \`PreToolUse\` (matcher \`Read|Bash\`) hook that denies reads of \`.env\`/\`.env.*\`/\`.secrets\`, the injection-scanner as a \`PostToolUse\` (matcher \`Bash\`) advisory, mockup-link-stop as a \`Stop\` hook (when your final reply asks to approve a mockup without carrying its link, it continues the turn so you rewrite the ask with the URL or file path in the prose), and check-update as a \`SessionStart\` hook. On Codex, file reads happen through shell commands rather than a native Read-tool event, so the secret-read guard's enforcement here runs via its **Bash** arm (protection is not lost); the \`Read\` half of its matcher is a Claude-Code-native path, registered on Codex too only so it fires without changes if a future Codex version adds an equivalent event. search-guard is Claude-Code-specific (it only prevents arming Claude's \`Read()\` deny rule under bypass permissions, a mechanism Codex lacks), so it is not projected to Codex at all. They use the same wire format they use on Claude Code (JSON on stdin, a \`decision\`/\`permissionDecision\` verdict on stdout), which Codex accepts. The host-independent git \`commit-msg\` hook is still installed at repo first-use as well (defence in depth), exactly as on Claude. Codex gates hooks behind \`features.hooks = true\` and reviews them for trust on first start: the installer enables the flag, and Codex will ask you to trust trailhead's hooks the next time it starts.
|
|
117
117
|
|
|
118
|
-
## G. Design / UI mockups -> local disk
|
|
119
|
-
The \`
|
|
118
|
+
## G. Design / UI mockups: artifacts -> stitch offer, else local disk
|
|
119
|
+
The \`artifacts\` mockup mode (a Design canvas Artifact built with Claude Code's \`Artifact\` tool; the legacy \`claude.ai/design\` value means the same) does not exist on Codex: there is no \`Artifact\` tool here. When \`config.design\` names it, follow the Prototype technique's fallback: **offer once to switch this project to \`design: stitch\`** (the Google Stitch MCP runs on Codex too), and only on decline produce a throwaway static HTML mockup next to the code (\`disk\`), link it from the ticket, and say why. Never try to create or edit an Artifact on Codex. \`config.design.approval\` still applies unchanged: \`explicit\` waits for the user's go-ahead before any UI code, \`auto\` surfaces the mockup and proceeds.
|
|
120
120
|
|
|
121
121
|
## H. Shell timeouts & background jobs (plan-review's external CLIs)
|
|
122
122
|
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).
|
package/bin/trailhead.test.js
CHANGED
|
@@ -123,6 +123,10 @@ const codexCommitMsgSync = path.join(codexDir, 'skills', 'trailhead', 'templates
|
|
|
123
123
|
ok('codex: commit-msg-sync script projected', fs.existsSync(codexCommitMsgSync));
|
|
124
124
|
ok('codex: commit-msg-sync script is byte-identical to source',
|
|
125
125
|
fs.readFileSync(codexCommitMsgSync, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg-sync.js'), 'utf8'));
|
|
126
|
+
// Il sync del label guard (migrazione all'if a livello di job) viaggia con templates/.
|
|
127
|
+
const codexLabelGuardSync = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-label-guard-sync.js');
|
|
128
|
+
ok('codex: label-guard-sync script is byte-identical to source', fs.existsSync(codexLabelGuardSync) &&
|
|
129
|
+
fs.readFileSync(codexLabelGuardSync, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-label-guard-sync.js'), 'utf8'));
|
|
126
130
|
// #193: the audit fast path / attribution walk script rides in templates/ on Codex too.
|
|
127
131
|
const codexAuditWalk = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-audit-walk.js');
|
|
128
132
|
ok('codex: audit-walk script projected', fs.existsSync(codexAuditWalk));
|
|
@@ -870,6 +874,9 @@ const claudeCommitMsgSync = path.join(claudeDir, 'trailhead', 'templates', 'trai
|
|
|
870
874
|
ok('claude: commit-msg-sync script projected', fs.existsSync(claudeCommitMsgSync));
|
|
871
875
|
ok('claude: commit-msg-sync script is byte-identical to source',
|
|
872
876
|
fs.readFileSync(claudeCommitMsgSync, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg-sync.js'), 'utf8'));
|
|
877
|
+
const claudeLabelGuardSync = path.join(claudeDir, 'trailhead', 'templates', 'trailhead-label-guard-sync.js');
|
|
878
|
+
ok('claude: label-guard-sync script is byte-identical to source', fs.existsSync(claudeLabelGuardSync) &&
|
|
879
|
+
fs.readFileSync(claudeLabelGuardSync, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-label-guard-sync.js'), 'utf8'));
|
|
873
880
|
const claudeAuditWalk = path.join(claudeDir, 'trailhead', 'templates', 'trailhead-audit-walk.js');
|
|
874
881
|
ok('claude: audit-walk script projected', fs.existsSync(claudeAuditWalk));
|
|
875
882
|
ok('claude: audit-walk script is byte-identical to source',
|
|
@@ -1159,6 +1166,14 @@ for (const [label, src] of [
|
|
|
1159
1166
|
ok(`source: ${label} has no em-dash`, !src.includes('—'));
|
|
1160
1167
|
}
|
|
1161
1168
|
|
|
1169
|
+
// --- label guard: setup e update rilevano/migrano un guard senza if a livello di job
|
|
1170
|
+
ok('source: teamwork.md references trailhead-label-guard-sync.js', teamworkSource.includes('trailhead-label-guard-sync.js'));
|
|
1171
|
+
ok('source: substrate-commands.md references trailhead-label-guard-sync.js', substrateCommandsSource.includes('trailhead-label-guard-sync.js'));
|
|
1172
|
+
ok('source: load-first.md runs the label guard check with --notice',
|
|
1173
|
+
fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'skills', '_shared', 'load-first.md'), 'utf8').includes('trailhead-label-guard-sync.js --repo <project root> --notice'));
|
|
1174
|
+
ok('source: updating.md realigns the repo label guard',
|
|
1175
|
+
fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'skills', 'trailhead-manage', 'references', 'updating.md'), 'utf8').includes('label guard sync script'));
|
|
1176
|
+
|
|
1162
1177
|
// --- #189: script mancante / errore node -> fallback che ispeziona la hooks dir
|
|
1163
1178
|
// invece di dichiarare alla cieca "git non applica le regole" ------------------
|
|
1164
1179
|
ok('source: teamwork.md fallback resolves the hooks dir via rev-parse --git-path hooks',
|
|
@@ -1583,9 +1598,16 @@ const auditDescLine192 = (auditCmdSrc.match(/^description: (.+)$/m) || [])[1] ||
|
|
|
1583
1598
|
ok('source: commands/audit.md description mentions regressions, starts "Audit and stays under 180 chars',
|
|
1584
1599
|
/^"Audit /.test(auditDescLine192) && /regression/i.test(auditDescLine192) && auditDescLine192.length < 180);
|
|
1585
1600
|
ok('source: manage SKILL.md describes the regression audit', /regression/i.test((manageSkillSrc190.match(/^description: .+$/m) || [''])[0]) || /regression/i.test((manageSkillSrc190.match(/\*\*`audit[^\n]*/) || [''])[0]));
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1601
|
+
// Il sito è diviso: commands.md è solo l'indice, il dettaglio di audit vive
|
|
1602
|
+
// nella sezione `### /trailhead:audit` della guida manage.
|
|
1603
|
+
const siteManageDoc = path.join('site', 'src', 'content', 'docs', 'docs', 'commands', 'manage.md');
|
|
1604
|
+
const auditMention = (doc) => {
|
|
1605
|
+
const src = fs.readFileSync(path.join(repoRoot, doc), 'utf8');
|
|
1606
|
+
if (doc === siteManageDoc) return (src.match(/### \/trailhead:audit[\s\S]*?(?=\n### |\nNext:|$)/) || [''])[0];
|
|
1607
|
+
return src.split('\n').filter((l) => l.includes('/trailhead:audit')).join('\n');
|
|
1608
|
+
};
|
|
1609
|
+
for (const doc of ['README.md', 'README.it.md', siteManageDoc]) {
|
|
1610
|
+
const auditPara192 = auditMention(doc);
|
|
1589
1611
|
ok(`docs: ${doc} audit mention covers unchanged and evolved`, /unchanged|invariat/i.test(auditPara192) && /evolved|evolut/i.test(auditPara192));
|
|
1590
1612
|
}
|
|
1591
1613
|
|
|
@@ -1622,9 +1644,8 @@ ok('source: auditing.md step 5 dispatches the agent-bound set and reads code at
|
|
|
1622
1644
|
ok('source: auditing.md step 8 counts sum to the audited set with the light-check share',
|
|
1623
1645
|
/light check/i.test(step8Slice192) && /light-check share/.test(step8Slice192) && /not implemented, unverifiable/.test(step8Slice192));
|
|
1624
1646
|
ok('source: auditing.md step 10 flags unverifiable from step 4 or step 6', /\(step 4 or step 6\)/.test(step10Slice190));
|
|
1625
|
-
for (const doc of ['README.md', 'README.it.md',
|
|
1626
|
-
const
|
|
1627
|
-
const para = docSrc.split('\n').filter((l) => l.includes('/trailhead:audit')).join('\n');
|
|
1647
|
+
for (const doc of ['README.md', 'README.it.md', siteManageDoc]) {
|
|
1648
|
+
const para = auditMention(doc);
|
|
1628
1649
|
ok(`docs: ${doc} audit mention names the Refs trailer fast path`, /Refs: #<n>/.test(para));
|
|
1629
1650
|
ok(`docs: ${doc} audit mention covers tasks and prototypes in the light check`, /task/i.test(para) && /prototyp|prototipi/i.test(para));
|
|
1630
1651
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marcomigozzi/trailhead",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Start and drive large projects as a map of tickets on GitHub Issues. Map-based onboarding + a discuss→plan→execute→verify engine, all on the Issues. Installs the trailhead skill, commands, and hooks into your agent's config dir.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"trailhead": "bin/trailhead.js"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trailhead",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Start and drive large projects as a map of tickets on GitHub Issues, resolving one at a time. Map-based onboarding + a discuss→plan→execute→verify engine with atomic commits, all on the Issues. Ships as a dispatcher plus cohesion-clustered skills over a shared core; depends on nothing beyond an authenticated gh CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Marco Migozzi",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# trailhead label guard: repo-side enforcement.
|
|
2
|
+
# Copy this file to your repository's .github/workflows/ directory.
|
|
3
|
+
# It strips any `trailhead:*` label applied by an actor without write access,
|
|
4
|
+
# so external / triage users (and stray automations) can't inject tickets into
|
|
5
|
+
# the trailhead map. Maintainers (write/admin/maintain) apply labels normally.
|
|
6
|
+
#
|
|
7
|
+
# Optional: set a repo variable TRAILHEAD_LABEL_ALLOWLIST to a comma-separated
|
|
8
|
+
# list of extra logins that are always allowed (e.g. a bot account trailhead runs as).
|
|
9
|
+
|
|
10
|
+
name: trailhead label guard
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
issues:
|
|
14
|
+
types: [labeled]
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
issues: write
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
guard:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/github-script@v7
|
|
24
|
+
env:
|
|
25
|
+
ALLOWLIST: ${{ vars.TRAILHEAD_LABEL_ALLOWLIST }}
|
|
26
|
+
with:
|
|
27
|
+
script: |
|
|
28
|
+
const label = context.payload.label && context.payload.label.name || '';
|
|
29
|
+
if (!label.startsWith('trailhead:')) return;
|
|
30
|
+
|
|
31
|
+
const actor = context.payload.sender.login;
|
|
32
|
+
const extra = (process.env.ALLOWLIST || '')
|
|
33
|
+
.split(',').map(s => s.trim().toLowerCase()).filter(Boolean);
|
|
34
|
+
if (extra.includes(actor.toLowerCase())) return;
|
|
35
|
+
|
|
36
|
+
let permission = 'none';
|
|
37
|
+
try {
|
|
38
|
+
const r = await github.rest.repos.getCollaboratorPermissionLevel({
|
|
39
|
+
owner: context.repo.owner, repo: context.repo.repo, username: actor });
|
|
40
|
+
permission = r.data.permission; // admin | write | read | none (triage→read, maintain→write)
|
|
41
|
+
} catch (e) { /* not a collaborator → none */ }
|
|
42
|
+
|
|
43
|
+
if (['admin', 'write', 'maintain'].includes(permission)) return; // authorized
|
|
44
|
+
|
|
45
|
+
// Unauthorized: strip the reserved label and explain.
|
|
46
|
+
await github.rest.issues.removeLabel({
|
|
47
|
+
owner: context.repo.owner, repo: context.repo.repo,
|
|
48
|
+
issue_number: context.payload.issue.number, name: label,
|
|
49
|
+
}).catch(() => {});
|
|
50
|
+
|
|
51
|
+
await github.rest.issues.createComment({
|
|
52
|
+
owner: context.repo.owner, repo: context.repo.repo,
|
|
53
|
+
issue_number: context.payload.issue.number,
|
|
54
|
+
body: `⚠️ \`${label}\` is a **trailhead-reserved** label and was removed automatically: only repo maintainers (write access) apply \`trailhead:*\` labels. It was applied by @${actor}, who lacks write access. If this issue should join the trailhead map, a maintainer will adopt it.`,
|
|
55
|
+
}).catch(() => {});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Tests for templates/trailhead-label-guard.yml and
|
|
3
|
+
// templates/trailhead-label-guard-sync.js. Run: node label-guard-sync.test.js
|
|
4
|
+
// No framework: plain asserts, mirrors the style of commit-msg-sync.test.js.
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
|
|
11
|
+
const templatesDir = path.resolve(__dirname, '..', '..', 'templates');
|
|
12
|
+
const scriptPath = path.join(templatesDir, 'trailhead-label-guard-sync.js');
|
|
13
|
+
const templatePath = path.join(templatesDir, 'trailhead-label-guard.yml');
|
|
14
|
+
const oldFixturePath = path.join(__dirname, 'fixtures', 'label-guard-0.12.0.yml');
|
|
15
|
+
const repoGuardPath = path.resolve(__dirname, '..', '..', '..', '..', '.github', 'workflows', 'trailhead-label-guard.yml');
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
isTrailheadGuard, jobGuardIf, hasJobLevelFilter, classify, syncLabelGuard, sessionNotice, ackGuard, parseArgs, GUARD_REL, ACK_REL,
|
|
19
|
+
} = require(scriptPath);
|
|
20
|
+
|
|
21
|
+
let passed = 0;
|
|
22
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
23
|
+
|
|
24
|
+
const templateText = fs.readFileSync(templatePath, 'utf8');
|
|
25
|
+
const oldText = fs.readFileSync(oldFixturePath, 'utf8');
|
|
26
|
+
|
|
27
|
+
function mktmp() {
|
|
28
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'trailhead-label-guard-sync-test-'));
|
|
29
|
+
}
|
|
30
|
+
function writeGuard(repoDir, text) {
|
|
31
|
+
const file = path.join(repoDir, GUARD_REL);
|
|
32
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
33
|
+
fs.writeFileSync(file, text);
|
|
34
|
+
return file;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- il template filtra a livello di job (i job saltati non sono fatturati) ---
|
|
38
|
+
const cond = jobGuardIf(templateText);
|
|
39
|
+
ok('template: jobs.guard has a job-level if', cond !== null);
|
|
40
|
+
ok('template: job-level if filters on the trailhead: prefix',
|
|
41
|
+
cond.includes("startsWith(github.event.label.name, 'trailhead:')"));
|
|
42
|
+
ok('template: job-level if skips the repository owner',
|
|
43
|
+
cond.includes('github.event.sender.login != github.repository_owner'));
|
|
44
|
+
ok('template: job-level if skips allowlisted logins',
|
|
45
|
+
cond.includes("!contains(format(',{0},', vars.TRAILHEAD_LABEL_ALLOWLIST), format(',{0},', github.event.sender.login))"));
|
|
46
|
+
ok('template: the job-level if sits before runs-on',
|
|
47
|
+
templateText.indexOf(' if: >-') < templateText.indexOf(' runs-on: ubuntu-latest'));
|
|
48
|
+
ok('template: the in-script permission check is kept',
|
|
49
|
+
templateText.includes('getCollaboratorPermissionLevel') && templateText.includes("if (!label.startsWith('trailhead:')) return;"));
|
|
50
|
+
ok("template: the repo's own guard is byte-identical to the template",
|
|
51
|
+
fs.readFileSync(repoGuardPath, 'utf8') === templateText);
|
|
52
|
+
|
|
53
|
+
// --- riconoscimento e classificazione ----------------------------------------
|
|
54
|
+
ok('isTrailheadGuard: template recognised', isTrailheadGuard(templateText));
|
|
55
|
+
ok('isTrailheadGuard: 0.12.0 guard recognised', isTrailheadGuard(oldText));
|
|
56
|
+
ok('isTrailheadGuard: foreign workflow rejected', !isTrailheadGuard('name: ci\non: push\n'));
|
|
57
|
+
ok('jobGuardIf: 0.12.0 guard has no job-level if', jobGuardIf(oldText) === null);
|
|
58
|
+
ok('jobGuardIf: a step-level if does not count',
|
|
59
|
+
jobGuardIf('jobs:\n guard:\n runs-on: x\n steps:\n - if: startsWith(github.event.label.name, \'trailhead:\')\n') === null);
|
|
60
|
+
ok('jobGuardIf: an if on another job does not count',
|
|
61
|
+
jobGuardIf("jobs:\n guard:\n runs-on: x\n other:\n if: startsWith(github.event.label.name, 'trailhead:')\n") === null);
|
|
62
|
+
ok('hasJobLevelFilter: single-line if accepted',
|
|
63
|
+
hasJobLevelFilter("jobs:\n guard:\n if: startsWith(github.event.label.name, 'trailhead:')\n runs-on: x\n"));
|
|
64
|
+
|
|
65
|
+
ok('classify: missing file -> absent', classify({ existingText: null, templateText }) === 'absent');
|
|
66
|
+
ok('classify: template -> current', classify({ existingText: templateText, templateText }) === 'current');
|
|
67
|
+
ok('classify: CRLF template -> current', classify({ existingText: templateText.replace(/\n/g, '\r\n'), templateText }) === 'current');
|
|
68
|
+
ok('classify: 0.12.0 guard -> outdated', classify({ existingText: oldText, templateText }) === 'outdated');
|
|
69
|
+
ok('classify: filtered but edited -> custom',
|
|
70
|
+
classify({ existingText: templateText + '# local tweak\n', templateText }) === 'custom');
|
|
71
|
+
ok('classify: foreign workflow -> foreign', classify({ existingText: 'name: x\n', templateText }) === 'foreign');
|
|
72
|
+
|
|
73
|
+
// --- syncLabelGuard: check-only non scrive, --apply migra --------------------
|
|
74
|
+
{
|
|
75
|
+
const repo = mktmp();
|
|
76
|
+
const file = writeGuard(repo, oldText);
|
|
77
|
+
const r = syncLabelGuard({ repoDir: repo, templateText });
|
|
78
|
+
ok('sync check: outdated reported', r.status === 'outdated' && r.file === file);
|
|
79
|
+
ok('sync check: file untouched', fs.readFileSync(file, 'utf8') === oldText);
|
|
80
|
+
const a = syncLabelGuard({ repoDir: repo, templateText, apply: true });
|
|
81
|
+
ok('sync apply: outdated -> upgraded', a.status === 'upgraded');
|
|
82
|
+
ok('sync apply: file now equals the template', fs.readFileSync(file, 'utf8') === templateText);
|
|
83
|
+
ok('sync apply: no temp file left', fs.readdirSync(path.dirname(file)).length === 1);
|
|
84
|
+
ok('sync apply: second run -> current', syncLabelGuard({ repoDir: repo, templateText, apply: true }).status === 'current');
|
|
85
|
+
}
|
|
86
|
+
{
|
|
87
|
+
const repo = mktmp();
|
|
88
|
+
const a = syncLabelGuard({ repoDir: repo, templateText, apply: true });
|
|
89
|
+
ok('sync apply: absent -> installed', a.status === 'installed' && fs.readFileSync(a.file, 'utf8') === templateText);
|
|
90
|
+
}
|
|
91
|
+
for (const [label, text] of [['custom', templateText + '# local\n'], ['foreign', 'name: x\n']]) {
|
|
92
|
+
const repo = mktmp();
|
|
93
|
+
const file = writeGuard(repo, text);
|
|
94
|
+
const a = syncLabelGuard({ repoDir: repo, templateText, apply: true });
|
|
95
|
+
ok(`sync apply: ${label} left untouched`, a.status === label && fs.readFileSync(file, 'utf8') === text);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- offerta di sessione + ack ----------------------------------------------
|
|
99
|
+
{
|
|
100
|
+
const repo = mktmp();
|
|
101
|
+
const file = writeGuard(repo, oldText);
|
|
102
|
+
const notice = sessionNotice({ repoDir: repo, templateText, scriptPath: scriptPath });
|
|
103
|
+
ok('notice: outdated guard -> offer', notice !== null && notice.includes('--apply') && notice.includes('--ack') && notice.includes('job-level'));
|
|
104
|
+
ok('notice: asks the agent to explain why (billed minutes) in the question', notice.includes('put the reason in the question') && notice.includes('at least one minute') && notice.includes('skipped runs are free'));
|
|
105
|
+
ok('notice: read-only (no ack written)', !fs.existsSync(path.join(repo, ACK_REL)));
|
|
106
|
+
ok('notice: still offered on the next session', sessionNotice({ repoDir: repo, templateText, scriptPath }) !== null);
|
|
107
|
+
ok('ack: records the answer', ackGuard({ repoDir: repo }) === true && fs.existsSync(path.join(repo, ACK_REL)));
|
|
108
|
+
ok('notice: acked guard -> null', sessionNotice({ repoDir: repo, templateText, scriptPath }) === null);
|
|
109
|
+
fs.writeFileSync(file, oldText + '# edited\n');
|
|
110
|
+
ok('notice: comes back when the guard file changes', sessionNotice({ repoDir: repo, templateText, scriptPath }) !== null);
|
|
111
|
+
}
|
|
112
|
+
for (const [label, text] of [['current', templateText], ['custom', templateText + '# x\n'], ['foreign', 'name: x\n']]) {
|
|
113
|
+
const repo = mktmp();
|
|
114
|
+
writeGuard(repo, text);
|
|
115
|
+
ok(`notice: ${label} guard -> null`, sessionNotice({ repoDir: repo, templateText, scriptPath }) === null);
|
|
116
|
+
}
|
|
117
|
+
ok('notice: no guard -> null', sessionNotice({ repoDir: mktmp(), templateText, scriptPath }) === null);
|
|
118
|
+
ok('ack: no guard -> false', ackGuard({ repoDir: mktmp() }) === false);
|
|
119
|
+
|
|
120
|
+
// --- CLI ---------------------------------------------------------------------
|
|
121
|
+
ok('parseArgs: --repo needs a value', Boolean(parseArgs(['--repo']).error));
|
|
122
|
+
ok('parseArgs: unknown flag rejected', Boolean(parseArgs(['--nope']).error));
|
|
123
|
+
ok('parseArgs: --apply with --notice rejected', Boolean(parseArgs(['--notice', '--apply']).error));
|
|
124
|
+
ok('parseArgs: --notice with --ack rejected', Boolean(parseArgs(['--notice', '--ack']).error));
|
|
125
|
+
{
|
|
126
|
+
const repo = mktmp();
|
|
127
|
+
writeGuard(repo, oldText);
|
|
128
|
+
const out = JSON.parse(execFileSync('node', [scriptPath, '--repo', repo], { encoding: 'utf8' }));
|
|
129
|
+
ok('cli: outdated reported as JSON', out.status === 'outdated');
|
|
130
|
+
const applied = JSON.parse(execFileSync('node', [scriptPath, '--repo', repo, '--apply'], { encoding: 'utf8' }));
|
|
131
|
+
ok('cli: --apply upgrades', applied.status === 'upgraded');
|
|
132
|
+
}
|
|
133
|
+
{
|
|
134
|
+
const repo = mktmp();
|
|
135
|
+
writeGuard(repo, oldText);
|
|
136
|
+
ok('cli: --notice prints the offer', execFileSync('node', [scriptPath, '--repo', repo, '--notice'], { encoding: 'utf8' }).includes('outdated'));
|
|
137
|
+
execFileSync('node', [scriptPath, '--repo', repo, '--ack']);
|
|
138
|
+
ok('cli: --notice silent after --ack', execFileSync('node', [scriptPath, '--repo', repo, '--notice'], { encoding: 'utf8' }) === '');
|
|
139
|
+
}
|
|
140
|
+
// L'hook SessionStart porta l'offerta nel contesto della sessione.
|
|
141
|
+
{
|
|
142
|
+
const repo = mktmp();
|
|
143
|
+
execFileSync('git', ['init', '-q', repo]);
|
|
144
|
+
writeGuard(repo, oldText);
|
|
145
|
+
const hookPath = path.resolve(__dirname, '..', 'trailhead-check-update.js');
|
|
146
|
+
const run = () => execFileSync('node', [hookPath], {
|
|
147
|
+
input: JSON.stringify({ cwd: repo }), encoding: 'utf8',
|
|
148
|
+
env: { ...process.env, XDG_CACHE_HOME: path.join(repo, '.cache'), CLAUDE_CONFIG_DIR: path.join(repo, '.cfg') },
|
|
149
|
+
});
|
|
150
|
+
const ctx = (s) => (s ? JSON.parse(s).hookSpecificOutput.additionalContext : '');
|
|
151
|
+
ok('hook: SessionStart surfaces the label guard offer', ctx(run()).includes('label guard in this repo'));
|
|
152
|
+
ackGuard({ repoDir: repo });
|
|
153
|
+
ok('hook: no offer once acked', !ctx(run()).includes('label guard in this repo'));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log(`label-guard-sync: ${passed} passed`);
|
|
@@ -163,6 +163,29 @@ function reviewNotice(cwd) {
|
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// Offerta di upgrade del label guard del repo (stesso schema della review dei
|
|
167
|
+
// modelli): solo quando il guard committato è la versione senza `if:` a
|
|
168
|
+
// livello di job e l'utente non ha già risposto per quel file. Lo script di
|
|
169
|
+
// sync vive in templates/: ../templates per plugin, dev e Codex, la copia npm
|
|
170
|
+
// sotto <configDir>/trailhead/templates. Sola lettura; ogni errore -> null.
|
|
171
|
+
function labelGuardNotice(cwd) {
|
|
172
|
+
try {
|
|
173
|
+
const candidates = [
|
|
174
|
+
path.join(__dirname, '..', 'templates'),
|
|
175
|
+
path.join(configDir(), 'trailhead', 'templates'),
|
|
176
|
+
];
|
|
177
|
+
const dir = candidates.find((d) => fs.existsSync(path.join(d, 'trailhead-label-guard-sync.js')));
|
|
178
|
+
if (!dir) return null;
|
|
179
|
+
const repoDir = run('git', ['-C', cwd, 'rev-parse', '--show-toplevel']);
|
|
180
|
+
if (!repoDir) return null;
|
|
181
|
+
const scriptPath = path.join(dir, 'trailhead-label-guard-sync.js');
|
|
182
|
+
const templateText = fs.readFileSync(path.join(dir, 'trailhead-label-guard.yml'), 'utf8');
|
|
183
|
+
return require(scriptPath).sessionNotice({ repoDir, templateText, scriptPath }) || null;
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
166
189
|
// Legge il JSON di SessionStart da stdin in modo asincrono, con timeout, come
|
|
167
190
|
// gli altri hook: una lettura sincrona di fd 0 si bloccherebbe per sempre su
|
|
168
191
|
// un TTY o su una pipe mai chiusa. Su TTY o input assente si prosegue con {}.
|
|
@@ -190,9 +213,10 @@ function main(input) {
|
|
|
190
213
|
const cwd = (input && typeof input.cwd === 'string' && input.cwd) || process.cwd();
|
|
191
214
|
|
|
192
215
|
const review = reviewNotice(cwd);
|
|
216
|
+
const guard = labelGuardNotice(cwd);
|
|
193
217
|
const update = updateNotice();
|
|
194
218
|
|
|
195
|
-
const parts = [review, update].filter(Boolean);
|
|
219
|
+
const parts = [review, guard, update].filter(Boolean);
|
|
196
220
|
if (!parts.length) return;
|
|
197
221
|
|
|
198
222
|
process.stdout.write(JSON.stringify({
|
|
@@ -52,7 +52,7 @@ ok('mockup offer whose option description mentions approval is NOT an approval a
|
|
|
52
52
|
header: 'Scope',
|
|
53
53
|
options: [
|
|
54
54
|
{ label: 'Va bene così', description: 'Procedo con lo scope descritto' },
|
|
55
|
-
{ label: 'Prima un mockup', description: 'Disegno la pagina su
|
|
55
|
+
{ label: 'Prima un mockup', description: 'Disegno la pagina su un canvas Artifacts e attendo approvazione prima di scriverla' },
|
|
56
56
|
],
|
|
57
57
|
}],
|
|
58
58
|
}));
|
|
@@ -68,7 +68,7 @@ ok('unrelated ask is not detected', !isMockupApprovalAsk({
|
|
|
68
68
|
ok('malformed input is not detected', !isMockupApprovalAsk({}) && !isMockupApprovalAsk(null));
|
|
69
69
|
|
|
70
70
|
// --- hasLink ---
|
|
71
|
-
ok('https URL counts as a link', hasLink('Ecco: https://claude.ai/
|
|
71
|
+
ok('https URL counts as a link', hasLink('Ecco: https://claude.ai/artifact/AbC123xyz'));
|
|
72
72
|
ok('absolute html path counts as a link', hasLink('Mockup in /home/u/app/mockups/faq.html'));
|
|
73
73
|
ok('relative html path counts as a link', hasLink('apri `mockups/faq.html`'));
|
|
74
74
|
ok('markdown link to a file counts', hasLink('[faq](mockups/faq.html)'));
|
|
@@ -88,7 +88,7 @@ ok('prose right before the ask is collected', /pronto su MyApp/.test(proseBefore
|
|
|
88
88
|
|
|
89
89
|
const linkBeforeTools = [
|
|
90
90
|
userPrompt('lavora #12'),
|
|
91
|
-
text('Mockup: https://claude.ai/
|
|
91
|
+
text('Mockup: https://claude.ai/artifact/AbC123xyz'),
|
|
92
92
|
toolUse('Bash'),
|
|
93
93
|
toolResult('ok'),
|
|
94
94
|
text('Pronto.'),
|
|
@@ -125,7 +125,7 @@ function runHook(payload) {
|
|
|
125
125
|
const r1 = runHook({ tool_name: 'AskUserQuestion', tool_input: approvalInput, transcript_path: writeTranscript(failing) });
|
|
126
126
|
ok('approval ask without a link in the prose is blocked (exit 2)', r1.code === 2 && /MOCKUP_LINK_MISSING/.test(r1.out));
|
|
127
127
|
|
|
128
|
-
const withLink = [...failing.slice(0, 4), text('Mockup: https://claude.ai/
|
|
128
|
+
const withLink = [...failing.slice(0, 4), text('Mockup: https://claude.ai/artifact/AbC123xyz'), failing[5]];
|
|
129
129
|
const r2 = runHook({ tool_name: 'AskUserQuestion', tool_input: approvalInput, transcript_path: writeTranscript(withLink) });
|
|
130
130
|
ok('approval ask with a link in the prose is allowed', r2.code === 0 && r2.out.trim() === '');
|
|
131
131
|
|
|
@@ -19,7 +19,7 @@ const noLink = [
|
|
|
19
19
|
'1. Approvo',
|
|
20
20
|
'2. Chiedo modifiche',
|
|
21
21
|
].join('\n');
|
|
22
|
-
const withLink = noLink.replace('pronto su MyApp', 'pronto: https://claude.ai/
|
|
22
|
+
const withLink = noLink.replace('pronto su MyApp', 'pronto: https://claude.ai/artifact/AbC123xyz');
|
|
23
23
|
|
|
24
24
|
// --- isMockupApprovalMessage ---
|
|
25
25
|
ok('IT approval reply is detected', isMockupApprovalMessage(noLink));
|