@marcomigozzi/trailhead 0.16.0 → 0.16.1
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 +10 -8
- package/README.md +10 -8
- package/bin/config-dir-templates-docs.test.js +52 -0
- package/bin/decision-builds-docs.test.js +75 -0
- package/bin/fog-vs-ticket-docs.test.js +56 -0
- package/bin/lib/codex-projection.js +8 -1
- package/bin/lib/codex-projection.test.js +23 -0
- package/bin/lib/host-descriptor.js +14 -1
- package/bin/lib/host-descriptor.test.js +18 -0
- package/bin/lib/runtime-command-literals.test.js +184 -0
- package/bin/report-status-docs.test.js +1 -1
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +2 -2
- package/plugins/trailhead/agents/trailhead-verify.md +2 -2
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +50 -21
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +50 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +68 -1
- package/plugins/trailhead/hooks/lib/shell-scan.js +25 -0
- package/plugins/trailhead/hooks/lib/statusline-context.test.js +22 -1
- package/plugins/trailhead/hooks/lib/unmodelled.js +6 -1
- package/plugins/trailhead/hooks/mod/README.md +8 -3
- package/plugins/trailhead/hooks/mod/map-pane.tsx +52 -28
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +40 -1
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +68 -0
- package/plugins/trailhead/skills/_shared/configuration-reference.md +2 -2
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +3 -3
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques.md +2 -2
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +4 -3
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +3 -3
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +1 -0
- package/plugins/trailhead/templates/statusline-trailhead.sh +3 -3
- package/plugins/trailhead/templates/trailhead-report-status.js +67 -3
- package/plugins/trailhead/tests/map-pane-ui.test.ts +179 -2
package/README.it.md
CHANGED
|
@@ -90,7 +90,7 @@ Ciò che **non** ha bisogno di una propria mappa è un singolo pezzo di lavoro d
|
|
|
90
90
|
|
|
91
91
|
Ogni **ticket** è una child issue con una label di tipo e un corpo di una sola domanda. La **frontier** è l'insieme dei ticket aperti, non assegnati, i cui blocker sono tutti chiusi e che non sono in pausa: ciò che è prendibile proprio ora. Rivendichi un ticket assegnandolo a te stesso, lo risolvi con il motore per il suo tipo, poi registri la risposta come commento, lo chiudi, e ne fai il gist di nuovo sulla mappa.
|
|
92
92
|
|
|
93
|
-
Man mano che i ticket si risolvono, la fog si dirada:
|
|
93
|
+
La fog contiene solo ciò che non si sa ancora formulare: una domanda già formulabile diventa un ticket al charting, creato bloccato quando aspetta un'altra risposta, così entra in frontier da solo appena quella risposta arriva. Man mano che i ticket si risolvono, la fog si dirada: dopo ogni risoluzione ogni voce della fog viene riletta, e quelle ormai abbastanza nitide da diventare ticket vengono proposte insieme, finché non resta nulla da decidere o costruire e la destination è raggiunta.
|
|
94
94
|
|
|
95
95
|
Una mappa ha come scope **un** solo sforzo, quindi la conoscenza che appartiene al *repo* (non a una singola mappa) vive in **anchor issue con scope di repo**, create una volta e condivise da ogni mappa (le Notes di ciascuna mappa le collegano soltanto, così nulla resta orfano quando una mappa finisce):
|
|
96
96
|
|
|
@@ -107,7 +107,7 @@ Questi tre riempiono i **3 slot di issue pinnate** di GitHub, così gli anchor d
|
|
|
107
107
|
**Il ciclo di vita.** Ogni progetto percorre lo stesso loop:
|
|
108
108
|
|
|
109
109
|
1. **Start**: `/trailhead:new "<idea>"` (greenfield) o `/trailhead:adopt` (codice esistente: mappa la codebase una volta, poi vai lean). Questo *traccia la mappa*: dai un nome alla destination, poi mappa la frontier in ampiezza (breadth-first) nei primi ticket e nella fog.
|
|
110
|
-
2. **Lavora la frontier, un ticket per sessione**: `/trailhead:work` prende il prossimo ticket prendibile (o quello che nomini) ed esegue il motore per il suo tipo: `research` raccoglie un fatto · `decision` dirama le opzioni poi fa grilling per scegliere · `prototype` crea un artefatto grezzo a cui reagire · `build` esegue discuss → plan → execute → verify · `bug` esegue repro → diagnose → fix → verify · `task` è plumbing manuale che sblocca una decision. `/trailhead:auto` esegue in autonomia uno scope scelto, fino a 4 slot paralleli, finché la frontier è esaurita o lo fermi tu, parcheggiando ciò che serve a te (un'azione del safety rail, la fog, una decisione che serve l'umano; vedi sotto). All'avvio di un ticket, `work` e `quick` mostrano un breve riepilogo con la scelta **Proceed / Change something**; viene saltata (al suo posto una riga di nota) per un ticket che hai creato o appena chiarito con un grilling in questa sessione, e `auto` non lo mostra mai.
|
|
110
|
+
2. **Lavora la frontier, un ticket per sessione**: `/trailhead:work` prende il prossimo ticket prendibile (o quello che nomini) ed esegue il motore per il suo tipo: `research` raccoglie un fatto · `decision` dirama le opzioni poi fa grilling per scegliere, poi apre i ticket build che portano avanti la risposta · `prototype` crea un artefatto grezzo a cui reagire · `build` esegue discuss → plan → execute → verify · `bug` esegue repro → diagnose → fix → verify · `task` è plumbing manuale che sblocca una decision. `/trailhead:auto` esegue in autonomia uno scope scelto, fino a 4 slot paralleli, finché la frontier è esaurita o lo fermi tu, parcheggiando ciò che serve a te (un'azione del safety rail, la fog, una decisione che serve l'umano; vedi sotto). All'avvio di un ticket, `work` e `quick` mostrano un breve riepilogo con la scelta **Proceed / Change something**; viene saltata (al suo posto una riga di nota) per un ticket che hai creato o appena chiarito con un grilling in questa sessione, e `auto` non lo mostra mai.
|
|
111
111
|
3. **Chiudi e sblocca**: risolvi il ticket (commento + chiusura), fanne il gist in `Decisions so far`, e rimuovi `trailhead:blocked` da ogni dipendente il cui ultimo blocker si è appena chiuso, diplomandolo sulla frontier.
|
|
112
112
|
4. **Ripeti** finché la frontier è vuota e la fog si è diradata: la destination (un artefatto funzionante) è raggiunta.
|
|
113
113
|
|
|
@@ -115,9 +115,11 @@ Lungo il cammino: **cattura** idee in corso d'opera senza deragliare, **splitta*
|
|
|
115
115
|
|
|
116
116
|
### 🤖 Run autonoma: `auto`
|
|
117
117
|
|
|
118
|
-
`/trailhead:auto [map]`
|
|
118
|
+
`/trailhead:auto [map]` svuota una mappa (o la whiteboard, o tutto) senza supervisione: prende un ticket dopo l'altro, in 1-4 slot paralleli, e risolve ciascuno con lo stesso motore che userebbe `/trailhead:work`, senza restituirti il controllo nel mezzo. Non parte mai da sola: ogni run la avvii tu.
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
**`auto` o `work`?** Entrambi risolvono ticket; cambia chi resta nel loop. `/trailhead:work` è la modalità interattiva: un ticket per sessione, e si ferma a chiederti le cose lungo la strada (il brief dello scope, il grilling, l'acceptance). `auto` chiede solo all'inizio, poi fa da sé le scelte di processo e parcheggia per te ciò che solo tu puoi decidere. Usa `auto` per ticket build e bug ben specificati che non devi guidare; usa `work` per le decisioni, i ticket vaghi o quando vuoi guidare tu.
|
|
121
|
+
|
|
122
|
+
**Quando torni** trovi il run report in chat (postato anche su ogni mappa toccata), i ticket che hanno bisogno di te con label `trailhead:needs-you` e un commento `NEEDS YOU`, e i commit locali che aspettano il tuo push: la run non pusha mai.
|
|
121
123
|
|
|
122
124
|
Mentre gira, la chat mostra una riga di avanzamento con i ticket negli slot e cosa si è appena risolto o viene dopo; alla fine, il run report. Qualcosa come:
|
|
123
125
|
|
|
@@ -427,14 +429,14 @@ I ticket che nascono da altro lavoro portano un puntatore di lignaggio nel loro
|
|
|
427
429
|
Tre layer: **il più vicino vince**, chiave per chiave (una chiave non impostata a un layer eredita il successivo):
|
|
428
430
|
|
|
429
431
|
- **Project**: un file `.trailhead/config.json` alla radice del repo: override per questo progetto, committato così un team condivide una config. La config vive in un file semplice, **mai nella issue della mappa**; è tua da cambiare in qualsiasi momento.
|
|
430
|
-
- **Global**:
|
|
432
|
+
- **Global**: `trailhead/config.json` nella directory di config dell'host: `$CLAUDE_CONFIG_DIR` (default `~/.claude`) su Claude Code, `$CODEX_HOME` (default `~/.codex`) su Codex. I tuoi default permanenti su ogni progetto.
|
|
431
433
|
- **Defaults**: i valori integrati.
|
|
432
434
|
|
|
433
435
|
`/trailhead:config` esegue un **setup guidato, a menu**: scegli lo scope, poi percorri ogni impostazione (⚙️ modo di lavorare (git · release · isolation) · 🌐 ticket language · 🧠 models (un profilo High / Balanced / Low, o Manual) · 🎨 design + approval · 🧪 TDD · ⚖️ effort · 🖥️ acceptance testing (browser, setup UAT, un account di test) · 🧑⚖️ plan review · 📊 statusline) come un menu con icone; niente editing manuale del JSON. Ogni passo è chiesto (nessuno saltato), e i **modelli plan ed execute sono sempre due scelte separate e version-pinned**. `config get` stampa la config effettiva unita; `config set <key> <value>` scrive una chiave.
|
|
434
436
|
|
|
435
437
|
`/trailhead:prune [map]` recupera gli slot di sub-issue di una mappa sotto il cap di 100 di GitHub (elimina gli edge chiusi/orfani, mantiene ogni riferimento). Offerto automaticamente quando una mappa è vicina o al cap, ed eseguibile anche su richiesta.
|
|
436
438
|
|
|
437
|
-
`/trailhead:audit [mappa] [--all]` cerca le regressioni: verifica che i ticket chiusi dall'ultimo audit siano ancora veri rispetto al codice attuale. Il codice che i commit di un ticket non hanno più toccato è classificato invariato, senza agente (un controllo git veloce e deterministico che si basa sui trailer `Refs: #<n>` dei commit), decisioni, ricerche, task e prototipi ricevono un controllo leggero inline, e un agente di verifica completo parte solo dove il codice è cambiato o il controllo veloce non può decidere (per esempio senza commit `Refs:`). Ogni ticket è classificato come implementato, invariato, evoluto (un ticket successivo l'ha cambiato di proposito, elencato senza proposte), implementato male o non implementato. Poi confronta quelli aperti per trovare duplicati e cluster raggruppabili. Resta in sola lettura finché non confermi: ogni correzione (un bug di regressione, una riapertura, la chiusura di un duplicato, un supersede-and-merge) è una proposta applicata solo con un sì esplicito. `--all` riverifica tutti i ticket chiusi.
|
|
439
|
+
`/trailhead:audit [mappa] [--all]` cerca le regressioni: verifica che i ticket chiusi dall'ultimo audit siano ancora veri rispetto al codice attuale. Il codice che i commit di un ticket non hanno più toccato è classificato invariato, senza agente (un controllo git veloce e deterministico che si basa sui trailer `Refs: #<n>` dei commit), decisioni, ricerche, task e prototipi ricevono un controllo leggero inline, e un agente di verifica completo parte solo dove il codice è cambiato o il controllo veloce non può decidere (per esempio senza commit `Refs:`). Ogni ticket è classificato come implementato, invariato, evoluto (un ticket successivo l'ha cambiato di proposito, elencato senza proposte), implementato male o non implementato. Segnala anche le decisioni chiuse senza build, cioè senza alcun ticket che porti avanti la loro risposta. Poi confronta quelli aperti per trovare duplicati e cluster raggruppabili. Resta in sola lettura finché non confermi: ogni correzione (un bug di regressione, una riapertura, i ticket build mancanti di una decisione, la chiusura di un duplicato, un supersede-and-merge) è una proposta applicata solo con un sì esplicito. `--all` riverifica tutti i ticket chiusi.
|
|
438
440
|
|
|
439
441
|
Il passo **📊 statusline** offre di installare la status bar di trailhead per Claude Code: una riga con **model · project · branch · plan usage (`5h %` · reset · `7d %`) · una barra della context-window**, più una **seconda riga con il ticket attivo** (`▸ #N Title`) ogni volta che ne stai lavorando uno, e un flag `⬆ trailhead <version>` quando è disponibile un trailhead più recente (esegui `/trailhead:update`). Il project è sempre il nome del repo principale anche da un checkout isolato, e il branch porta un tag `(WT)` in un worktree o `(C)` in un clone per-ticket (niente sul checkout originale). È un'impostazione globale di Claude Code; se già usi una statusline (es. `ccstatusline`) il setup chiede prima di rimpiazzarla, e lo script espone anche i segmenti `--ticket-only` / `--context-only` / `--usage-only` da inserire in un tool esistente.
|
|
440
442
|
|
|
@@ -471,7 +473,7 @@ Scegli `plan` ed `execute` separatamente, ciascuno per **id completo versionato*
|
|
|
471
473
|
|
|
472
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.
|
|
473
475
|
|
|
474
|
-
Esempio di file di config (la stessa forma funziona per il `.trailhead/config.json` di progetto e il
|
|
476
|
+
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):
|
|
475
477
|
|
|
476
478
|
```json
|
|
477
479
|
{ "ticket": { "language": "en" }, "models": { "plan": "claude-opus-5-5", "execute": "claude-sonnet-5-5" }, "tdd": "seams", "acceptance": { "browser": "auto" } }
|
|
@@ -493,7 +495,7 @@ Oltre alle istruzioni della skill, trailhead include nove hook propri (in `hooks
|
|
|
493
495
|
- **Search guard** (`PreToolUse` su `Bash`): un backstop strutturale per la regola di igiene dei comandi di ricerca (mai `cd` seguito da una lettura/ricerca su percorso relativo). Blocca in modo netto un singolo comando Bash che sia insieme un cambio di directory (`cd`/`pushd`/`env -C`) e, in un'istruzione unita o successiva, una lettura/ricerca su percorso relativo (`grep`/`rg`/`ag`/`cat`/`head`/`tail`/`wc`/`nl`/`sed`/`awk`/`less`/`more`, o `git grep`/`log`/`show`/`diff` senza `-C`), così la forma che fa scattare una regola di deny su `Read()` sotto bypass permissions (ripiegando su un prompt di approvazione manuale invece di girare headless) non viene mai eseguita, anziché essere solo scoraggiata nella prosa dell'agente. I target di redirezione (`2>/dev/null`) non contano come letture, e un `cd` alla directory della sessione stessa non la attiva. Solo Claude Code; non è proiettata su Codex, che non ha una regola di deny su `Read()`.
|
|
494
496
|
- **Body guard** (`PreToolUse` sulle scritture `gh` di body issue/PR): blocca in modo netto una scrittura `gh issue edit` / `gh pr edit` / `gh api` che imposterebbe il body di un issue a vuoto o solo spazi, così un read-modify-write la cui lettura è tornata con un body vuoto/degradato (es. un HTTP 5xx transitorio su `gh issue view --json body`) non può mai cancellare il body esistente.
|
|
495
497
|
- **Labels guard** (`PreToolUse` su `gh issue create`): controlla il set di label di un ticket già alla creazione. Una create con un'etichetta `trailhead:*` deve portare la forma completa del ticket: `trailhead:ticket`, esattamente un tipo (`decision`, `research`, `prototype`, `build`, `bug`, `task`) e esattamente una destinazione (una `trailhead:map-<n>` oppure `trailhead:whiteboard`, mai entrambe, mai due mappe); il blocco nomina cosa manca o è in conflitto e chiede di riprovare con tutte le label nella stessa create, mai con un `--add-label` successivo. Gli issue non-ticket (`trailhead:map`, `dashboard`, `codebase`, `conventions`, `fog`, `unverified`) sono esenti. Anche una create senza alcuna label `trailhead:*` viene bloccata, ma solo dentro un repo trailhead (la working directory è in un work tree git il cui top level ha `.trailhead/`) e non quando punta a un altro repo con `-R`/`--repo`; un issue non-trailhead voluto si esenta anteponendo `TRAILHEAD_PLAIN_ISSUE=1` al comando. Un valore di label non risolvibile staticamente (`$VAR`, backtick, `$(...)`) non viene mai bloccato.
|
|
496
|
-
- **Secret-read guard** (`PreToolUse` su `Read|Bash`): blocca in modo netto la lettura di `.env`/`.env.*`/`.secrets`, sia tramite il tool Read sia tramite un comando Bash (un operando file, il valore di un'opzione, o un target di redirect `<`). È una decisione `deny` a livello di hook anziché una regola di permesso su `Read()`, quindi i segreti restano illeggibili senza attivare il prompt di approvazione `cd-compound-read` di Claude Code 2.1.259.
|
|
498
|
+
- **Secret-read guard** (`PreToolUse` su `Read|Bash`): blocca in modo netto la lettura di `.env`/`.env.*`/`.secrets`, sia tramite il tool Read sia tramite un comando Bash (un operando file, il valore di un'opzione, o un target di redirect `<`), compreso il file di script o di pattern passato con `-f`/`--file` a `grep`, `sed` e `awk`. Viene bloccato anche un wrapper di comando (`env`, `nice`, `timeout`, `nohup`, `sudo`, `stdbuf`, `time`, `setsid`, `doas`) davanti a una shell annidata (`bash -c`) o a `eval` vicino a un path che sembra un segreto, perché il wrapper nasconderebbe il comando interno. È una decisione `deny` a livello di hook anziché una regola di permesso su `Read()`, quindi i segreti restano illeggibili senza attivare il prompt di approvazione `cd-compound-read` di Claude Code 2.1.259.
|
|
497
499
|
- **Mockup-link guard** (`PreToolUse` su `AskUserQuestion`): rete di sicurezza strutturale per la regola della tecnica Prototype secondo cui la richiesta di approvazione di un mockup porta con sé il link al mockup. Blocca un `AskUserQuestion` che chiede di approvare un mockup (o di scegliere tra varianti) quando né il testo della risposta subito prima né la domanda di approvazione stessa contengono un URL o un path (se il messaggio corrente non è ancora nel transcript, la guardia non può giudicare e lascia passare), così l'agente mette il link cliccabile nel messaggio prima di richiedere. Su Codex, che non ha il tool `AskUserQuestion` e pone le domande come elenco numerato in testo, il gemello `mockup-link-stop` gira su `Stop`: se la risposta finale chiede quell'approvazione senza link, Codex fa proseguire il turno e l'agente riscrive la domanda col link.
|
|
498
500
|
- **Issue injection scanner** (`PostToolUse` sulle letture `gh`): trailhead legge testo di issue/PR/commenti scritto da chiunque abbia accesso al repo, cioè input non fidato. Quando quel testo contiene frasi di prompt-injection, l'hook inietta un avviso che ricorda all'agente di trattarlo come dati, mai come comandi. Solo advisory: non blocca mai.
|
|
499
501
|
|
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ What does **not** need its own map is a single, defined piece of work with no op
|
|
|
90
90
|
|
|
91
91
|
Each **ticket** is a child issue with a type label and a one-question body. The **frontier** is the set of open, unassigned tickets whose blockers are all closed and that are not paused: what's takeable right now. You claim a ticket by assigning it to yourself, resolve it with the engine for its type, then record the answer as a comment, close it, and gist it back onto the map.
|
|
92
92
|
|
|
93
|
-
As tickets resolve, the fog clears:
|
|
93
|
+
The fog holds only what you cannot phrase yet: a question you can already phrase becomes a ticket at charting, created blocked when it waits on another answer, so it reaches the frontier by itself once that answer lands. As tickets resolve, the fog clears: after each resolution every fog entry is re-read, and the ones now sharp enough to ticket are proposed together, until nothing is left to decide or build and the destination is reached.
|
|
94
94
|
|
|
95
95
|
A map is scoped to **one** effort, so knowledge that belongs to the *repo* (not to any single map) lives in **repo-scoped anchor issues**, created once and shared by every map (each map's Notes just links them, so nothing is stranded when a map finishes):
|
|
96
96
|
|
|
@@ -107,7 +107,7 @@ These three fill GitHub's **3 pinned-issue slots**, so a repo's trailhead anchor
|
|
|
107
107
|
**The lifecycle.** Every project walks the same loop:
|
|
108
108
|
|
|
109
109
|
1. **Start**: `/trailhead:new "<idea>"` (greenfield) or `/trailhead:adopt` (existing code: map the codebase once, then go lean). This *charts the map*: name the destination, then map the frontier breadth-first into the first tickets and the fog.
|
|
110
|
-
2. **Work the frontier, one ticket per session**: `/trailhead:work` takes the next takeable ticket (or one you name) and runs the engine for its type: `research` gathers a fact · `decision` diverges options then grills to choose · `prototype` makes a rough artifact to react to · `build` runs discuss → plan → execute → verify · `bug` runs repro → diagnose → fix → verify · `task` is manual plumbing that unblocks a decision. `/trailhead:auto` runs a chosen scope unattended, in up to 4 parallel slots, until the frontier is exhausted or you stop it, parking whatever needs you (a safety-rail action, fog, a human-necessary decision; see below). At a ticket's start, `work` and `quick` show a short brief with a **Proceed / Change something** choice; it is skipped (a one-line note instead) for a ticket you created or just settled in a grilling this session, and `auto` never shows it.
|
|
110
|
+
2. **Work the frontier, one ticket per session**: `/trailhead:work` takes the next takeable ticket (or one you name) and runs the engine for its type: `research` gathers a fact · `decision` diverges options then grills to choose, then opens the build tickets that carry the answer · `prototype` makes a rough artifact to react to · `build` runs discuss → plan → execute → verify · `bug` runs repro → diagnose → fix → verify · `task` is manual plumbing that unblocks a decision. `/trailhead:auto` runs a chosen scope unattended, in up to 4 parallel slots, until the frontier is exhausted or you stop it, parking whatever needs you (a safety-rail action, fog, a human-necessary decision; see below). At a ticket's start, `work` and `quick` show a short brief with a **Proceed / Change something** choice; it is skipped (a one-line note instead) for a ticket you created or just settled in a grilling this session, and `auto` never shows it.
|
|
111
111
|
3. **Close & unblock**: resolve the ticket (comment + close), gist it into `Decisions so far`, and drop `trailhead:blocked` from any dependent whose last blocker just closed, graduating it onto the frontier.
|
|
112
112
|
4. **Repeat** until the frontier is empty and the fog has cleared: the destination (a working artifact) is reached.
|
|
113
113
|
|
|
@@ -115,9 +115,11 @@ Along the way: **capture** ideas mid-work without derailing, **split** a ticket
|
|
|
115
115
|
|
|
116
116
|
### 🤖 Autonomous run: `auto`
|
|
117
117
|
|
|
118
|
-
`/trailhead:auto [map]`
|
|
118
|
+
`/trailhead:auto [map]` drains a map (or the whiteboard, or everything) unattended: it takes ticket after ticket, in 1 to 4 parallel slots, and resolves each with the same engine `/trailhead:work` would run, without handing back to you in between. It never starts on its own: you launch every run.
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
**`auto` or `work`?** Both resolve tickets; what changes is who stays in the loop. `/trailhead:work` is the interactive mode: one ticket per session, and it stops to ask you along the way (the scope brief, grilling, acceptance). `auto` asks only at the start, then makes the process choices itself and parks for you whatever only you can settle. Use `auto` for well-specified build and bug tickets you don't need to steer; use `work` for decisions, fuzzy tickets, or when you want to drive.
|
|
121
|
+
|
|
122
|
+
**When you come back** you find the run report in chat (also posted on each touched map), the tickets that need you labelled `trailhead:needs-you` with a `NEEDS YOU` comment, and local commits waiting for you to push: the run never pushes.
|
|
121
123
|
|
|
122
124
|
While it runs, the chat shows a progress line naming the tickets in the slots and what just resolved or comes next; at the end, the run report. Something like:
|
|
123
125
|
|
|
@@ -427,14 +429,14 @@ Tickets that spin off from other work carry a lineage pointer in their body, so
|
|
|
427
429
|
Three layers: **nearest wins**, key by key (a key unset at one layer inherits the next):
|
|
428
430
|
|
|
429
431
|
- **Project**: a `.trailhead/config.json` file at the repo root: overrides for this project, committed so a team shares one config. Config lives in a plain file, **never in the map issue**; it's yours to change at any time.
|
|
430
|
-
- **Global**:
|
|
432
|
+
- **Global**: `trailhead/config.json` in your host's config directory: `$CLAUDE_CONFIG_DIR` (default `~/.claude`) on Claude Code, `$CODEX_HOME` (default `~/.codex`) on Codex. Your standing defaults across every project.
|
|
431
433
|
- **Defaults**: the built-in values.
|
|
432
434
|
|
|
433
435
|
`/trailhead:config` runs a **guided, menu-driven setup**: pick the scope, then walk each setting (⚙️ way of working (git · release · isolation) · 🌐 ticket language · 🧠 models (a High / Balanced / Low profile, or Manual) · 🎨 design + approval · 🧪 TDD · ⚖️ effort · 🖥️ acceptance testing (browser, UAT setup, a test account) · 🧑⚖️ plan review · 📊 statusline) as an icon-labelled menu; no hand-editing JSON. Every step is asked (none skipped), and **plan and execute models are always two separate, version-pinned choices**. `config get` prints the effective merged config; `config set <key> <value>` writes one key.
|
|
434
436
|
|
|
435
437
|
`/trailhead:prune [map]` reclaims a map's sub-issue slots under GitHub's 100-cap (drops closed/orphan edges, keeps every reference). Offered automatically when a map is near or at the cap, and runnable on demand.
|
|
436
438
|
|
|
437
|
-
`/trailhead:audit [map] [--all]` hunts regressions: it checks that the tickets closed since the last audit are still true against the current code. Code a ticket's commits left untouched is classed unchanged with no agent (a fast, deterministic git check that relies on the `Refs: #<n>` commit trailers), decisions, research, tasks and prototypes get a light inline check, and a full verification agent runs only where the code changed or the fast path cannot decide (no `Refs:` commits, for example). Each ticket is classed implemented, unchanged, evolved (a later ticket changed it on purpose, listed with no proposal), implemented wrong, or not implemented. It then compares the open ones for duplicates and groupable clusters. It is read-only until you confirm: every fix (a regression bug, a reopen, a duplicate close, a supersede-and-merge) is a proposal applied only on an explicit yes. `--all` re-audits every closed ticket.
|
|
439
|
+
`/trailhead:audit [map] [--all]` hunts regressions: it checks that the tickets closed since the last audit are still true against the current code. Code a ticket's commits left untouched is classed unchanged with no agent (a fast, deterministic git check that relies on the `Refs: #<n>` commit trailers), decisions, research, tasks and prototypes get a light inline check, and a full verification agent runs only where the code changed or the fast path cannot decide (no `Refs:` commits, for example). Each ticket is classed implemented, unchanged, evolved (a later ticket changed it on purpose, listed with no proposal), implemented wrong, or not implemented. It also flags decisions closed with no build ticket carrying their answer. It then compares the open ones for duplicates and groupable clusters. It is read-only until you confirm: every fix (a regression bug, a reopen, the missing build tickets of a decision, a duplicate close, a supersede-and-merge) is a proposal applied only on an explicit yes. `--all` re-audits every closed ticket.
|
|
438
440
|
|
|
439
441
|
The **📊 statusline** step offers to install trailhead's Claude Code status bar: one line with **model · project · branch · plan usage (`5h %` · reset · `7d %`) · a context-window bar**, plus a **second line with the active ticket** (`▸ #N Title`) whenever you're working one, and a `⬆ trailhead <version>` flag when a newer trailhead is available (run `/trailhead:update`). The project is always the main repo's name even from an isolated checkout, and the branch carries a `(WT)` tag in a worktree or `(C)` in a per-ticket clone (nothing on the original checkout). It's a global Claude Code setting; if you already run a statusline (e.g. `ccstatusline`) the setup asks before replacing it, and the script also exposes `--ticket-only` / `--context-only` / `--usage-only` segments to slot into an existing tool.
|
|
440
442
|
|
|
@@ -471,7 +473,7 @@ Choose `plan` and `execute` separately, each by **full versioned id** (never a b
|
|
|
471
473
|
|
|
472
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.
|
|
473
475
|
|
|
474
|
-
Example config file (the same shape works for the project `.trailhead/config.json` and the global
|
|
476
|
+
Example config file (the same shape works for the project `.trailhead/config.json` and the global `trailhead/config.json` in your config directory):
|
|
475
477
|
|
|
476
478
|
```json
|
|
477
479
|
{ "ticket": { "language": "en" }, "models": { "plan": "claude-opus-5-5", "execute": "claude-sonnet-5-5" }, "tdd": "seams", "acceptance": { "browser": "auto" } }
|
|
@@ -493,7 +495,7 @@ Beyond the skill's instructions, trailhead ships nine of its own hooks (in `hook
|
|
|
493
495
|
- **Search guard** (`PreToolUse` on `Bash`): structural backstop for the search-command-hygiene rule (never `cd` then read/search a relative path). It hard-blocks a single Bash command that both changes directory (`cd`/`pushd`/`env -C`) and, in a joined or later statement, reads/searches a relative path (`grep`/`rg`/`ag`/`cat`/`head`/`tail`/`wc`/`nl`/`sed`/`awk`/`less`/`more`, or `git grep`/`log`/`show`/`diff` without a `-C`), so the shape that trips a `Read()` deny rule under bypass permissions (falling back to a manual approval prompt instead of running headless) never runs at all, rather than merely being discouraged in the agent's own prose. Redirection targets (`2>/dev/null`) are not treated as reads, and a `cd` to the session's own directory does not arm it. Claude Code only; not projected to Codex, which has no such `Read()` deny rule.
|
|
494
496
|
- **Body guard** (`PreToolUse` on `gh` issue/PR body writes): hard-blocks a `gh issue edit` / `gh pr edit` / `gh api` write that would set an issue body to empty or whitespace-only, so a read-modify-write whose read returned an empty/degraded body (e.g. a transient HTTP 5xx on `gh issue view --json body`) can never clobber the existing body.
|
|
495
497
|
- **Labels guard** (`PreToolUse` on `gh issue create`): checks the label set of a ticket at the create call. A create carrying any `trailhead:*` label must carry the whole ticket shape: `trailhead:ticket`, exactly one type (`decision`, `research`, `prototype`, `build`, `bug`, `task`) and exactly one destination (one `trailhead:map-<n>` or `trailhead:whiteboard`, never both, never two maps); the block names what is missing or conflicting and asks to retry with every label in the same create call, never a later `--add-label`. Non-ticket issues (`trailhead:map`, `dashboard`, `codebase`, `conventions`, `fog`, `unverified`) are exempt. A create with no `trailhead:*` label at all is blocked too, but only inside a trailhead repo (the working directory sits in a git work tree whose top level has `.trailhead/`) and not when aimed at another repo with `-R`/`--repo`; a deliberate non-trailhead issue opts out by prefixing the command with `TRAILHEAD_PLAIN_ISSUE=1`. A label value the guard cannot resolve statically (`$VAR`, backticks, `$(...)`) is never blocked.
|
|
496
|
-
- **Secret-read guard** (`PreToolUse` on `Read|Bash`): denies reads of `.env`/`.env.*`/`.secrets`, whether via the Read tool or a Bash command (a file operand, an option value, or a `<` redirect target). This is a hook `deny` decision rather than a `Read()` permission rule, so secrets stay unreadable without arming Claude Code 2.1.259's `cd-compound-read` approval prompt.
|
|
498
|
+
- **Secret-read guard** (`PreToolUse` on `Read|Bash`): denies reads of `.env`/`.env.*`/`.secrets`, whether via the Read tool or a Bash command (a file operand, an option value, or a `<` redirect target), including the `-f`/`--file` script or pattern file of `grep`, `sed`, and `awk`. A command wrapper (`env`, `nice`, `timeout`, `nohup`, `sudo`, `stdbuf`, `time`, `setsid`, `doas`) in front of a nested shell (`bash -c`) or `eval` next to a secret-looking path is blocked too, since the wrapper would hide the inner command. This is a hook `deny` decision rather than a `Read()` permission rule, so secrets stay unreadable without arming Claude Code 2.1.259's `cd-compound-read` approval prompt.
|
|
497
499
|
- **Mockup-link guard** (`PreToolUse` on `AskUserQuestion`): structural backstop for the Prototype rule that a mockup approval ask carries the mockup's link. It hard-blocks an `AskUserQuestion` that asks to approve a mockup (or pick between mockup variants) when neither the reply text right before it nor the approval question itself holds a URL or file path (when the current message is not yet in the transcript, the guard cannot judge and allows), so the agent puts the clickable link in the message before asking again. On Codex, which has no `AskUserQuestion` tool and asks as a plain-text numbered list, its twin `mockup-link-stop` runs on `Stop`: when the final reply asks for that approval without a link, Codex continues the turn so the agent rewrites the ask with it.
|
|
498
500
|
- **Issue injection scanner** (`PostToolUse` on `gh` reads): trailhead reads issue/PR/comment text written by anyone with repo access, i.e. untrusted input. When that text contains prompt-injection phrases, the hook injects an advisory reminding the agent to treat it as data, never as commands. Advisory only: it never blocks.
|
|
499
501
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test: il lookup di <templates> negli skill rispetta CLAUDE_CONFIG_DIR (npm install),
|
|
3
|
+
// quindi nessun percorso nudo ~/.claude/trailhead/templates; il candidato Codex resta.
|
|
4
|
+
// Plain asserts, no framework. Run: node bin/config-dir-templates-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 SKILLS = path.join(root, 'plugins/trailhead/skills');
|
|
11
|
+
let passed = 0;
|
|
12
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
13
|
+
|
|
14
|
+
const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
|
|
15
|
+
e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]);
|
|
16
|
+
// Radici doc che il motore legge: skill, agent e comandi
|
|
17
|
+
const DOC_ROOTS = ['skills', 'agents', 'commands'].map((d) => path.join(root, 'plugins/trailhead', d));
|
|
18
|
+
const files = DOC_ROOTS.flatMap(walk).filter((f) => f.endsWith('.md'));
|
|
19
|
+
|
|
20
|
+
// Ogni grafia nuda della home: ~ o $HOME (la forma buona ha `}` dopo .claude, quindi non combacia)
|
|
21
|
+
const BARE = /(?:~|\$HOME)\/\.claude\/trailhead\/templates/g;
|
|
22
|
+
const GOOD = '${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/';
|
|
23
|
+
const CODEX = '~/.codex/skills/trailhead/templates/';
|
|
24
|
+
|
|
25
|
+
for (const f of files) {
|
|
26
|
+
const rel = path.relative(root, f);
|
|
27
|
+
ok(`${rel}: no bare ~/.claude/trailhead/templates`, !(fs.readFileSync(f, 'utf8').match(BARE)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Siti noti: candidato Claude con CLAUDE_CONFIG_DIR, candidato Codex intatto
|
|
31
|
+
for (const rel of [
|
|
32
|
+
'trailhead-work/SKILL.md',
|
|
33
|
+
'_shared/techniques.md',
|
|
34
|
+
'_shared/teamwork.md',
|
|
35
|
+
'trailhead-manage/references/auditing.md',
|
|
36
|
+
]) {
|
|
37
|
+
const t = fs.readFileSync(path.join(SKILLS, rel), 'utf8');
|
|
38
|
+
ok(`${rel}: config-dir candidate present`, t.includes(GOOD));
|
|
39
|
+
ok(`${rel}: codex candidate untouched`, t.includes(CODEX));
|
|
40
|
+
}
|
|
41
|
+
const cfg = fs.readFileSync(path.join(SKILLS, '_shared/configuration-reference.md'), 'utf8');
|
|
42
|
+
ok('configuration-reference: statusline lookup uses config dir', cfg.includes(GOOD + 'statusline-trailhead.sh'));
|
|
43
|
+
|
|
44
|
+
// #319: l'install della statusline va nella config dir, non in ~/.claude nudo
|
|
45
|
+
const stl = cfg.split('\n').find((l) => l.includes('On **Enable**, install it')) || '';
|
|
46
|
+
ok('configuration-reference: statusline install line found', stl.length > 0);
|
|
47
|
+
ok('configuration-reference: statusline target and settings use config dir',
|
|
48
|
+
stl.includes('${CLAUDE_CONFIG_DIR:-~/.claude}/statusline-trailhead.sh') && stl.includes('${CLAUDE_CONFIG_DIR:-~/.claude}/settings.json'));
|
|
49
|
+
ok('configuration-reference: statusline install has no bare ~/.claude target or $HOME command',
|
|
50
|
+
!/(?<!:-)~\/\.claude\/(settings\.json|statusline-trailhead\.sh)/.test(stl) && !stl.includes('$HOME/.claude'));
|
|
51
|
+
|
|
52
|
+
console.log(`config-dir-templates-docs.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test: una decision chiusa porta il suo lavoro nei build (riga `Builds:`), il grill
|
|
3
|
+
// che risolve un ticket fa lo stesso, e l'audit segnala le decisioni chiuse senza build.
|
|
4
|
+
// Plain asserts, no framework. Run: node bin/decision-builds-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 read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
|
11
|
+
let passed = 0;
|
|
12
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
13
|
+
const EM = String.fromCharCode(0x2014);
|
|
14
|
+
|
|
15
|
+
// Sezione dal suo heading fino al prossimo heading dello stesso livello (o superiore).
|
|
16
|
+
const section = (text, headingRe) => {
|
|
17
|
+
const m = headingRe.exec(text);
|
|
18
|
+
if (!m) return '';
|
|
19
|
+
const level = m[0].match(/^#+/)[0].length;
|
|
20
|
+
const rest = text.slice(m.index + m[0].length);
|
|
21
|
+
const next = new RegExp(`^#{1,${level}} `, 'm').exec(rest);
|
|
22
|
+
return next ? rest.slice(0, next.index) : rest;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const SK = 'plugins/trailhead/skills';
|
|
26
|
+
|
|
27
|
+
// Motore decision
|
|
28
|
+
const engines = read(`${SK}/trailhead-work/references/ticket-engines.md`);
|
|
29
|
+
const dec = section(engines, /^## `decision`.*$/m);
|
|
30
|
+
ok('decision engine: section found', dec.length > 0);
|
|
31
|
+
ok('decision engine: a build follow-through step', /Carry it into build/.test(dec));
|
|
32
|
+
ok('decision engine: records a `Builds:` line', dec.includes('`Builds:'));
|
|
33
|
+
ok('decision engine: allows `Builds: none` only with a reason', /`Builds: none \(<why>\)`/.test(dec));
|
|
34
|
+
ok('decision engine: links existing tickets or creates build tickets', /existing/i.test(dec) && /create-then-wire/.test(dec));
|
|
35
|
+
ok('decision engine: the human confirms the build split in the final confirmation', /final confirmation/i.test(dec));
|
|
36
|
+
ok('decision engine: no close without the line', /never `gh issue close`/i.test(dec));
|
|
37
|
+
ok('decision engine: explains the fog that vanishes', /Not yet specified/.test(dec));
|
|
38
|
+
ok('decision engine: no em-dash', !dec.includes(EM));
|
|
39
|
+
|
|
40
|
+
// Work the map
|
|
41
|
+
const work = read(`${SK}/trailhead-work/SKILL.md`);
|
|
42
|
+
const step5 = (work.match(/^5\. Record the resolution:.*$/m) || [''])[0];
|
|
43
|
+
ok('work step 5: decision close waits on the `Builds:` line', step5.includes('`Builds:') && /Carry it into build/.test(step5));
|
|
44
|
+
|
|
45
|
+
// Grill su un ticket
|
|
46
|
+
const chart = read(`${SK}/trailhead-chart/SKILL.md`);
|
|
47
|
+
const grill = (chart.match(/^Run a standalone \*\*Grilling\*\*.*$/m) || [''])[0];
|
|
48
|
+
ok('grill: resolving a decision ticket carries it into build', /Carry it into build/.test(grill) && grill.includes('`Builds:'));
|
|
49
|
+
|
|
50
|
+
// Audit
|
|
51
|
+
const audit = read(`${SK}/trailhead-manage/references/auditing.md`);
|
|
52
|
+
const light = (audit.match(/^\*\*Light check\*\*.*$/m) || [''])[0];
|
|
53
|
+
ok('audit light check: flags a closed decision with no build', /no build/i.test(light) && light.includes('`Builds:'));
|
|
54
|
+
ok('audit light check: an older decision counts a build that links it', /links? (it|back)/i.test(light));
|
|
55
|
+
const report = section(audit, /^## 8\. Report$/m);
|
|
56
|
+
ok('audit report: lists decisions with no build', /no build/i.test(report));
|
|
57
|
+
const props = section(audit, /^## 9\. Proposals.*$/m);
|
|
58
|
+
ok('audit proposals: open the missing build tickets on a yes', /No build/.test(props) && /build/.test(props) && /`Builds:`/.test(props));
|
|
59
|
+
|
|
60
|
+
// Docs utente
|
|
61
|
+
const readme = read('README.md');
|
|
62
|
+
ok('README: decision engine leaves build tickets', /`decision` diverges options then grills to choose, then opens the build tickets/.test(readme));
|
|
63
|
+
ok('README: audit names decisions with no build', /decisions closed with no build/i.test(readme));
|
|
64
|
+
const readmeIt = read('README.it.md');
|
|
65
|
+
ok('README.it: decision engine leaves build tickets', /`decision` dirama le opzioni poi fa grilling per scegliere, poi apre i ticket build/.test(readmeIt));
|
|
66
|
+
ok('README.it: audit names decisions with no build', /decisioni chiuse senza build/i.test(readmeIt));
|
|
67
|
+
const wf = read('site/src/content/docs/docs/workflow.md');
|
|
68
|
+
ok('workflow.md: decision engine leaves build tickets', /`decision` diverges options then grills to choose, then opens the build tickets/.test(wf));
|
|
69
|
+
const manage = read('site/src/content/docs/docs/commands/manage.md');
|
|
70
|
+
ok('manage.md: audit names decisions with no build', /closed with no build/i.test(manage));
|
|
71
|
+
for (const [n, t] of [['README.md', readme], ['README.it.md', readmeIt], ['workflow.md', wf], ['manage.md', manage]]) {
|
|
72
|
+
ok(`${n}: no em-dash`, !t.includes(EM));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`decision-builds-docs.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test: il charting apre come ticket (bloccati se serve) le aree con una Question
|
|
3
|
+
// già scrivibile, lascia in fog solo il resto; dopo ogni risoluzione la fog viene
|
|
4
|
+
// riesaminata tutta; a frontier vuota si propone un gruppo di voci da graduare.
|
|
5
|
+
// Plain asserts, no framework. Run: node bin/fog-vs-ticket-docs.test.js
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const root = path.resolve(__dirname, '..');
|
|
11
|
+
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
|
12
|
+
let passed = 0;
|
|
13
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
14
|
+
const EM = String.fromCharCode(0x2014);
|
|
15
|
+
const line = (text, re) => (text.match(re) || [''])[0];
|
|
16
|
+
|
|
17
|
+
const SK = 'plugins/trailhead/skills';
|
|
18
|
+
|
|
19
|
+
// Charting: il criterio ticket vs fog
|
|
20
|
+
const charting = read(`${SK}/trailhead-chart/references/charting.md`);
|
|
21
|
+
const step4 = line(charting, /^4\. \*\*Create the tickets you can specify now\*\*.*$/m);
|
|
22
|
+
ok('charting step 4: names the ticket-or-fog test', /Ticket or fog/.test(step4));
|
|
23
|
+
ok('charting step 4: a writable Question makes a ticket even when blocked', /Question/.test(step4) && /blocked/.test(step4) && /not a reason to leave it in the fog/i.test(step4));
|
|
24
|
+
ok('charting step 4: fog only when the question cannot be phrased yet', /cannot phrase/i.test(step4));
|
|
25
|
+
ok('charting step 4: fog when an open decision could reshape the question, and the entry says which', /reshape/i.test(step4) && /names? (that|the) (decision|ticket)/i.test(step4));
|
|
26
|
+
ok('charting step 4: the 100 sub-issue cap is the only volume brake', /100/.test(step4));
|
|
27
|
+
ok('charting: no em-dash', !charting.includes(EM));
|
|
28
|
+
|
|
29
|
+
// Work the map: riesame di tutta la fog dopo ogni risoluzione
|
|
30
|
+
const work = read(`${SK}/trailhead-work/SKILL.md`);
|
|
31
|
+
const step6 = line(work, /^6\. Add newly-surfaced tickets.*$/m);
|
|
32
|
+
ok('work step 6: re-scans every fog entry', /every entry/i.test(step6) && /Not yet specified/.test(step6));
|
|
33
|
+
ok('work step 6: applies the charting test', /Ticket or fog/.test(step6));
|
|
34
|
+
ok('work step 6: proposes the graduations as one batch', /one batch/i.test(step6));
|
|
35
|
+
|
|
36
|
+
// Handoff a frontier vuota: un gruppo, non una voce
|
|
37
|
+
const handoff = read(`${SK}/_shared/session-handoff.md`);
|
|
38
|
+
const empty = line(handoff, /^\s*\*\*If the frontier is empty\*\*.*$/m);
|
|
39
|
+
ok('handoff: empty frontier proposes a batch of ripe fog entries', /batch/i.test(empty) && /Ticket or fog/.test(empty));
|
|
40
|
+
ok('handoff: no em-dash', !handoff.includes(EM));
|
|
41
|
+
|
|
42
|
+
// Docs utente
|
|
43
|
+
const docs = [
|
|
44
|
+
['README.md', read('README.md'), false],
|
|
45
|
+
['README.it.md', read('README.it.md'), true],
|
|
46
|
+
['concepts.md', read('site/src/content/docs/docs/concepts.md'), false],
|
|
47
|
+
];
|
|
48
|
+
for (const [n, t, it] of docs) {
|
|
49
|
+
ok(`${n}: fog no longer clears "one at a time"`, it ? !/una alla volta, finché non resta/.test(t) : !/ticket, one at a time, until nothing/.test(t));
|
|
50
|
+
ok(`${n}: a phrasable question is a ticket, blocked if needed`, it
|
|
51
|
+
? /domanda già formulabile[^.]*ticket[^.]*bloccat/i.test(t)
|
|
52
|
+
: /question you can already phrase[^.]*ticket[^.]*blocked/i.test(t));
|
|
53
|
+
ok(`${n}: no em-dash`, !t.includes(EM));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log(`fog-vs-ticket-docs.test.js: ${passed} checks passed`);
|
|
@@ -69,7 +69,14 @@ function convertToCodex(md) {
|
|
|
69
69
|
// 2. Claude's /clear -> Codex's /new.
|
|
70
70
|
out = out.replace(/\/clear\b/g, '/new');
|
|
71
71
|
|
|
72
|
-
//
|
|
72
|
+
// 3a. Espansione della config dir: CLAUDE_CONFIG_DIR -> CODEX_HOME per intero,
|
|
73
|
+
// prima dei path nudi (altrimenti resta CLAUDE_CONFIG_DIR con fallback ~/.codex).
|
|
74
|
+
out = out.replace(/\$\{CLAUDE_CONFIG_DIR:-(~|\$HOME)\/\.claude\}/g, '${CODEX_HOME:-$1/.codex}');
|
|
75
|
+
// Forme nude rimaste in prosa: $CLAUDE_CONFIG_DIR e ${CLAUDE_CONFIG_DIR} -> CODEX_HOME.
|
|
76
|
+
out = out.replace(/\$\{CLAUDE_CONFIG_DIR\}/g, '${CODEX_HOME}');
|
|
77
|
+
out = out.replace(/\$CLAUDE_CONFIG_DIR\b/g, '$CODEX_HOME');
|
|
78
|
+
|
|
79
|
+
// 3b. Paths: anchored forms first, then the bare form.
|
|
73
80
|
out = out.replace(/\$HOME\/\.claude\b/g, '$HOME/.codex');
|
|
74
81
|
out = out.replace(/~\/\.claude\b/g, '~/.codex');
|
|
75
82
|
out = out.replace(/(?<![A-Za-z0-9_\-.\/~$])\.claude\//g, '.codex/');
|
|
@@ -441,6 +441,29 @@ ok('convertToCodex keeps CLAUDE_CODE_SESSION_ID intact',
|
|
|
441
441
|
autoRef.includes('--auto true --auto-done <n>'));
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
+
// #319: l'espansione CLAUDE_CONFIG_DIR diventa CODEX_HOME, mai un ibrido che punta alla config di Claude.
|
|
445
|
+
{
|
|
446
|
+
const a = convertToCodex('${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/x');
|
|
447
|
+
const b = convertToCodex('${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hooks/y');
|
|
448
|
+
ok('convertToCodex maps the ~ config-dir expansion to CODEX_HOME', a === '${CODEX_HOME:-~/.codex}/trailhead/templates/x');
|
|
449
|
+
ok('convertToCodex maps the $HOME config-dir expansion to CODEX_HOME', b === '${CODEX_HOME:-$HOME/.codex}/hooks/y');
|
|
450
|
+
ok('convertToCodex leaves no CLAUDE_CONFIG_DIR behind', !/CLAUDE_CONFIG_DIR/.test(a + b));
|
|
451
|
+
const c = convertToCodex('config dir = `$CLAUDE_CONFIG_DIR` or `~/.claude`; `${CLAUDE_CONFIG_DIR}/x`');
|
|
452
|
+
ok('convertToCodex maps the bare config-dir forms to CODEX_HOME',
|
|
453
|
+
c === 'config dir = `$CODEX_HOME` or `~/.codex`; `${CODEX_HOME}/x`');
|
|
454
|
+
ok('convertToCodex leaves a longer variable name untouched',
|
|
455
|
+
convertToCodex('$CLAUDE_CONFIG_DIRX') === '$CLAUDE_CONFIG_DIRX');
|
|
456
|
+
|
|
457
|
+
// Ogni .md reale proiettato non deve contenere CLAUDE_CONFIG_DIR.
|
|
458
|
+
const root = path.join(__dirname, '../../plugins/trailhead');
|
|
459
|
+
const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap((e) =>
|
|
460
|
+
e.isDirectory() ? walk(path.join(d, e.name)) : e.name.endsWith('.md') ? [path.join(d, e.name)] : []);
|
|
461
|
+
const files = ['skills', 'agents', 'commands'].flatMap((n) => walk(path.join(root, n)));
|
|
462
|
+
const leaks = files.filter((f) => /CLAUDE_CONFIG_DIR/.test(convertToCodex(fs.readFileSync(f, 'utf8'))));
|
|
463
|
+
ok('real plugin .md files were found', files.length > 0);
|
|
464
|
+
ok('no projected plugin .md keeps CLAUDE_CONFIG_DIR: ' + leaks.join(', '), leaks.length === 0);
|
|
465
|
+
}
|
|
466
|
+
|
|
444
467
|
// #248: the shared boundary blocks survive the Codex projection of every real agent.
|
|
445
468
|
{
|
|
446
469
|
const { parseSource } = require('../../scripts/sync-agent-blocks.js');
|
|
@@ -65,6 +65,8 @@ const HOSTS = Object.freeze({
|
|
|
65
65
|
label: 'Claude Code',
|
|
66
66
|
configDirEnv: 'CLAUDE_CONFIG_DIR',
|
|
67
67
|
configDirDefault: '.claude',
|
|
68
|
+
// Prefisso con cui l'host invoca un verbo trailhead (vedi commandForm).
|
|
69
|
+
commandPrefix: '/trailhead:',
|
|
68
70
|
axes: Object.freeze({
|
|
69
71
|
commandSurface: 'slash-file',
|
|
70
72
|
subagentToolkit: 'full',
|
|
@@ -93,6 +95,7 @@ const HOSTS = Object.freeze({
|
|
|
93
95
|
label: 'Codex',
|
|
94
96
|
configDirEnv: 'CODEX_HOME',
|
|
95
97
|
configDirDefault: '.codex',
|
|
98
|
+
commandPrefix: '$trailhead ',
|
|
96
99
|
axes: Object.freeze({
|
|
97
100
|
commandSurface: 'skill',
|
|
98
101
|
subagentToolkit: 'full',
|
|
@@ -168,6 +171,15 @@ function hyphenateCommand(name) {
|
|
|
168
171
|
return String(name).trim().replace(/^\//, '').replace(/:/g, '-');
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
// --- Helper: commandForm ------------------------------------------------------
|
|
175
|
+
// Forma di invocazione di un verbo per l'host: la scrittura da usare in ogni
|
|
176
|
+
// testo runtime, mai un literal `/trailhead:<verb>` hardcoded.
|
|
177
|
+
// commandForm('claude', 'update') -> '/trailhead:update'
|
|
178
|
+
// commandForm('codex', 'update') -> '$trailhead update'
|
|
179
|
+
function commandForm(host, verb) {
|
|
180
|
+
return `${resolveHost(host).commandPrefix}${verb}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
171
183
|
// --- Helper: emitsAgentToml ---------------------------------------------------
|
|
172
184
|
function emitsAgentToml(host) {
|
|
173
185
|
return resolveHost(host).emitsAgentToml;
|
|
@@ -275,7 +287,7 @@ function updateNotice(host, cache) {
|
|
|
275
287
|
const installed = String(cache.installed || '').trim();
|
|
276
288
|
const ver = latest ? ` ${latest}` : '';
|
|
277
289
|
const from = installed ? ` (you have ${installed})` : '';
|
|
278
|
-
return `Heads up: trailhead${ver} is available${from}. Run
|
|
290
|
+
return `Heads up: trailhead${ver} is available${from}. Run ${commandForm(host, 'update')} to install it.`;
|
|
279
291
|
}
|
|
280
292
|
|
|
281
293
|
// --- Codex version floor gate (decision #27) --------------------------------
|
|
@@ -390,6 +402,7 @@ module.exports = {
|
|
|
390
402
|
getHost,
|
|
391
403
|
configDirFor,
|
|
392
404
|
hyphenateCommand,
|
|
405
|
+
commandForm,
|
|
393
406
|
emitsAgentToml,
|
|
394
407
|
degradations,
|
|
395
408
|
modelsCollapseNotice,
|
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
getHost,
|
|
9
9
|
configDirFor,
|
|
10
10
|
hyphenateCommand,
|
|
11
|
+
commandForm,
|
|
11
12
|
emitsAgentToml,
|
|
12
13
|
degradations,
|
|
13
14
|
modelsCollapseNotice,
|
|
@@ -127,6 +128,23 @@ ok('modelsCollapseNotice: codex namespace with only empty/nullish values does no
|
|
|
127
128
|
return typeof msg === 'string' && msg.includes('plan') && !msg.includes('codex)');
|
|
128
129
|
})());
|
|
129
130
|
|
|
131
|
+
// --- commandForm ---
|
|
132
|
+
ok('commandForm(claude, update) is the slash form', commandForm('claude', 'update') === '/trailhead:update');
|
|
133
|
+
ok('commandForm(codex, update) is the dollar form', commandForm('codex', 'update') === '$trailhead update');
|
|
134
|
+
ok('commandForm throws on an unknown host', (() => { try { commandForm('nope', 'x'); return false; } catch { return true; } })());
|
|
135
|
+
|
|
136
|
+
// Drift guard: la lib degli hook ha la propria copia di commandForm (non puo'
|
|
137
|
+
// richiedere bin/ una volta installata); le due devono restare identiche.
|
|
138
|
+
{
|
|
139
|
+
const lib = require('../../plugins/trailhead/hooks/lib/model-defaults-review.js');
|
|
140
|
+
for (const host of ['claude', 'codex']) {
|
|
141
|
+
for (const verb of ['config', 'update']) {
|
|
142
|
+
ok(`commandForm drift: hook lib matches host-descriptor (${host}, ${verb})`,
|
|
143
|
+
lib.commandForm(host, verb) === commandForm(host, verb));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
130
148
|
// --- updateNotice ---
|
|
131
149
|
// Both hosts now have a hook bus (hookBus !== 'none'), so updateNotice never
|
|
132
150
|
// fires for either: the SessionStart hook + (on Codex) its stdout
|