@marcomigozzi/trailhead 0.5.0 → 0.5.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.
Files changed (35) hide show
  1. package/README.it.md +4 -2
  2. package/README.md +4 -2
  3. package/bin/lib/codex-projection.js +79 -58
  4. package/bin/lib/codex-projection.test.js +103 -15
  5. package/bin/trailhead.js +106 -15
  6. package/bin/trailhead.test.js +65 -10
  7. package/package.json +1 -1
  8. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  9. package/plugins/trailhead/agents/trailhead-code-review.md +9 -0
  10. package/plugins/trailhead/agents/trailhead-codebase-map.md +9 -0
  11. package/plugins/trailhead/agents/trailhead-debug.md +9 -0
  12. package/plugins/trailhead/agents/trailhead-executor.md +9 -0
  13. package/plugins/trailhead/agents/trailhead-fix.md +9 -0
  14. package/plugins/trailhead/agents/trailhead-plan.md +9 -0
  15. package/plugins/trailhead/agents/trailhead-research.md +9 -0
  16. package/plugins/trailhead/hooks/hooks.json +4 -0
  17. package/plugins/trailhead/hooks/trailhead-install-guard.js +176 -0
  18. package/plugins/trailhead/hooks/trailhead-install-guard.test.js +118 -0
  19. package/plugins/trailhead/skills/_shared/configuration-reference.md +20 -6
  20. package/plugins/trailhead/skills/_shared/techniques/code-review.md +2 -2
  21. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
  22. package/plugins/trailhead/skills/_shared/techniques/debug.md +4 -1
  23. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +1 -1
  24. package/plugins/trailhead/skills/_shared/techniques/prototype.md +5 -2
  25. package/plugins/trailhead/skills/_shared/techniques/research.md +2 -1
  26. package/plugins/trailhead/skills/_shared/techniques.md +2 -2
  27. package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
  28. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -0
  29. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -0
  30. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +6 -6
  31. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -0
  32. package/plugins/trailhead/skills/trailhead-manage/references/updating.md +12 -0
  33. package/plugins/trailhead/skills/trailhead-view/SKILL.md +1 -0
  34. package/plugins/trailhead/skills/trailhead-work/SKILL.md +1 -0
  35. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +8 -8
package/README.it.md CHANGED
@@ -1,5 +1,7 @@
1
1
  <div align="center">
2
2
 
3
+ ![trailhead](assets/social-preview.png)
4
+
3
5
  # Trailhead
4
6
 
5
7
  🇬🇧 [English](README.md) · 🇮🇹 **Italiano**
@@ -24,7 +26,7 @@ Due approcci al lavoro di progetto guidato dagli agenti risolvono ciascuno una m
24
26
 
25
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.
26
28
 
27
- > **trailhead è self-contained.** Wayfinder e GSD sono *ispirazione*, non dipendenze. Ogni tecnica che quei sistemi impacchettano come skill separata (grilling, TDD, debugging sistematico, codebase mapping, code review) è **integrata qui come protocollo inline**. Il core di trailhead non invoca nessun'altra skill; i suoi subagent (research, codebase-map, review) usano i subagent nativi dell'host (il tool `Agent` integrato di Claude Code, oppure il toolkit `multi_agent` di Codex). L'unica cosa da installare è trailhead stesso. C'è un'unica eccezione opt-in: la modalità mockup hosted. Quando abilitata, `claude.ai/design` costruisce il mockup su claude.ai/design (solo Claude): per default una canvas (la skill integrata `/design` pubblicata come Artifact, o un progetto creato a mano nella web app, non guidato da MCP) o, per una libreria di componenti condivisa, un progetto design-system tramite DesignSync (l'MCP `claude-design` più la skill `/design-sync`, l'unica superficie guidata da MCP). Una terza modalità, `stitch`, usa l'MCP Google Stitch e funziona sia su **Claude Code sia su Codex**, quindi è l'opzione hosted cross-host. Se l'MCP della modalità configurata non è connesso, offre un'installazione una tantum e ripiega sui mockup su disco locale se rifiuti; su Codex, dove `claude.ai/design` non è disponibile, offre di passare a `stitch` prima di ripiegare sul disco locale.
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, `claude.ai/design` costruisce il mockup su claude.ai/design (solo Claude): per default una canvas (la skill integrata `/design` pubblicata come Artifact, o un progetto creato a mano nella web app, non guidato da MCP) o, per una libreria di componenti condivisa, un progetto design-system tramite DesignSync (l'MCP `claude-design` più la skill `/design-sync`, l'unica superficie guidata da MCP). Una terza modalità, `stitch`, usa l'MCP Google Stitch e funziona sia su **Claude Code sia su Codex**, quindi è l'opzione hosted cross-host. Se l'MCP della modalità configurata non è connesso, offre un'installazione una tantum e ripiega sui mockup su disco locale se rifiuti; su Codex, dove `claude.ai/design` non è disponibile, offre di passare a `stitch` prima di ripiegare sul disco locale.
28
30
 
29
31
  Le scelte di design che ne derivano:
30
32
 
@@ -73,7 +75,7 @@ Su **Codex CLI** (`--codex`) proietta una skill nativa Codex invocata come **`$t
73
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).
74
76
 
75
77
  ### Dopo l'installazione
76
- Riavvia o ricarica il tuo agente così i comandi si registrano, poi esegui **`/trailhead`** per iniziare (smart entry), o `/trailhead:new "<idea>"` per tracciare una mappa. **Su Codex** la superficie è `$trailhead`: esegui **`$trailhead`** per lo smart entry, `$trailhead new "<idea>"` per tracciare, o una skill per-verbo `$trailhead-<verb>`. Nota: una volta installato, l'**hook di commit guard gira a ogni `git commit`** (imponendo i Conventional Commits e bloccando `Co-Authored-By`); disabilita gli hook del plugin nelle impostazioni se non lo vuoi. In ogni caso trailhead è self-contained: nessun'altra skill o plugin è richiesta, niente preflight, niente version drift.
78
+ Riavvia o ricarica il tuo agente così i comandi si registrano, poi esegui **`/trailhead`** per iniziare (smart entry), o `/trailhead:new "<idea>"` per tracciare una mappa. **Su Codex** la superficie è `$trailhead`: esegui **`$trailhead`** per lo smart entry, `$trailhead new "<idea>"` per tracciare, o una skill per-verbo `$trailhead-<verb>`. Nota: una volta installato, l'**hook di commit guard gira a ogni `git commit`** (imponendo i Conventional Commits e bloccando `Co-Authored-By`); disabilita gli hook del plugin nelle impostazioni se non lo vuoi. In ogni caso trailhead è self-contained: nessuna skill o plugin esterna è richiesta, niente preflight, niente version drift.
77
79
 
78
80
  ---
79
81
 
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  <div align="center">
2
2
 
3
+ ![trailhead](assets/social-preview.png)
4
+
3
5
  # Trailhead
4
6
 
5
7
  🇬🇧 **English** · 🇮🇹 [Italiano](README.it.md)
@@ -24,7 +26,7 @@ Two approaches to agent-driven project work each nail one half of the problem:
24
26
 
25
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.
26
28
 
27
- > **trailhead is self-contained.** Wayfinder and GSD are *inspiration*, not dependencies. Every technique those systems package as a separate skill (grilling, TDD, systematic debugging, codebase mapping, code review) is **built in here as an inline protocol**. trailhead's core invokes no other skill; its subagents (research, codebase-map, review) use the host's native subagents (Claude Code's built-in `Agent` tool, or Codex's `multi_agent` toolkit). The only thing to install is trailhead itself. There is one opt-in exception: hosted mockup mode. When enabled, `claude.ai/design` builds the mockup on claude.ai/design (Claude only): by default a canvas (the built-in `/design` skill published as an Artifact, or a hand-made web-app project, not MCP-driven) or, for a shared component library, a design-system project via DesignSync (the `claude-design` MCP plus the `/design-sync` skill, the only MCP-driven surface). A third mode, `stitch`, uses the Google Stitch MCP and works on **both Claude Code and Codex**, so it's the cross-host hosted option. If a hosted mode's MCP isn't connected, it offers a one-time install and falls back to local-disk mockups if you decline; on Codex, where `claude.ai/design` isn't available, it offers to switch to `stitch` before falling to local disk.
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, `claude.ai/design` builds the mockup on claude.ai/design (Claude only): by default a canvas (the built-in `/design` skill published as an Artifact, or a hand-made web-app project, not MCP-driven) or, for a shared component library, a design-system project via DesignSync (the `claude-design` MCP plus the `/design-sync` skill, the only MCP-driven surface). A third mode, `stitch`, uses the Google Stitch MCP and works on **both Claude Code and Codex**, so it's the cross-host hosted option. If a hosted mode's MCP isn't connected, it offers a one-time install and falls back to local-disk mockups if you decline; on Codex, where `claude.ai/design` isn't available, it offers to switch to `stitch` before falling to local disk.
28
30
 
29
31
  The design choices that fall out of it:
30
32
 
@@ -73,7 +75,7 @@ On **Codex CLI** (`--codex`) it projects a native Codex skill invoked as **`$tra
73
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).
74
76
 
75
77
  ### After installing
76
- Restart or reload your agent so the commands register, then run **`/trailhead`** to start (smart entry), or `/trailhead:new "<idea>"` to chart a map. **On Codex** the surface is `$trailhead` instead: run **`$trailhead`** for smart entry, `$trailhead new "<idea>"` to chart, or a per-verb `$trailhead-<verb>` skill. Note: once installed, the **commit guard hook runs on every `git commit`** (enforcing Conventional Commits and blocking `Co-Authored-By`); disable the plugin's hooks in settings if you don't want that. Either way trailhead is self-contained: no other skill or plugin is required, no preflight, no version drift.
78
+ Restart or reload your agent so the commands register, then run **`/trailhead`** to start (smart entry), or `/trailhead:new "<idea>"` to chart a map. **On Codex** the surface is `$trailhead` instead: run **`$trailhead`** for smart entry, `$trailhead new "<idea>"` to chart, or a per-verb `$trailhead-<verb>` skill. Note: once installed, the **commit guard hook runs on every `git commit`** (enforcing Conventional Commits and blocking `Co-Authored-By`); disable the plugin's hooks in settings if you don't want that. Either way trailhead is self-contained: no external skill or plugin is required, no preflight, no version drift.
77
79
 
78
80
  ---
79
81
 
@@ -101,7 +101,7 @@ Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_inp
101
101
 
102
102
  **Never surrender the session to one open-ended \`wait_agent\`; poll instead.** \`wait_agent\` blocks until the subagent returns, so a single unconditional \`wait_agent\` on a long or stalling subagent freezes the whole session with nothing to check: this is the review / plan-review hang. Collect every delegated subagent through a **bounded wait in a poll loop**, not one open-ended blocking call. Give \`wait_agent\` a short timeout (30 to 60s); on each timeout, report a one-line "still running" progress note and loop again, so the session stays responsive and can be checked or interrupted between polls. The two **review** steps, **Code review** and **Cross-AI plan review**, always run this way: backgrounding and polling them (never a blind \`wait_agent\`) is the engine's hard cross-host default, not merely the case where it matters most. They are also the longest quiet activities in the cycle (a full-diff adversarial review, or an external CLI) and the ones most often seen to hang. If your Codex build's \`wait_agent\` exposes no timeout, poll the agent's status/output at intervals instead of blocking, or run the review as a **background job** and check back on it at intervals; the rule is the same either way. When several subagents run in parallel, poll them as a set, a bounded wait cycling across the outstanding handles, reporting which are still running.
103
103
 
104
- **Per-technique model pins (\`agent_type\` dispatch).** When \`models.codex.*\` is set, the installer projects one \`~/.codex/agents/trailhead-<technique>.toml\` per key (that technique's OpenAI model, plus its reasoning effort) and enables \`features.multi_agent_v2\`, so Codex exposes a per-technique agent registry. Detect that registry at runtime by introspecting the visible \`spawn_agent\` tool schema (GSD-style, never a config read): if its parameters expose an \`agent_type\` field, \`multi_agent_v2\` is live, so spawn each delegated activity as \`spawn_agent(agent_type="trailhead-<technique>", reasoning_effort=…)\` (\`trailhead-plan\` for the Plan step, \`trailhead-execute\` for Execute, likewise research/review/debug), which routes it onto its pinned model. If the \`spawn_agent\` schema exposes NO \`agent_type\` field (only base multi_agent v1 is present, or no \`models.codex.*\` pin was projected), spawn with base \`spawn_agent\` and the subagents inherit the one session model. Trust the tool surface you actually see and degrade gracefully: \`multi_agent_v2\` is still under development upstream, so v1 stays a supported runtime.
104
+ **Per-technique agent registry (\`agent_type\` dispatch).** The installer always projects all 7 committed trailhead agents as \`~/.codex/agents/trailhead-<technique>.toml\` (one per technique) and enables \`features.multi_agent_v2\`, so Codex always exposes a per-technique agent registry. A TOML whose technique has a \`models.codex.<key>\` pin set carries that OpenAI model (plus its reasoning effort); every other TOML — an unpinned keyed technique, or one of the 2 keyless agents — is pin-less and its subagent inherits the session model. Detect the registry at runtime by introspecting the visible \`spawn_agent\` tool schema (GSD-style, never a config read): if its parameters expose an \`agent_type\` field, spawn each delegated activity as \`spawn_agent(agent_type="trailhead-<technique>", reasoning_effort=…)\` (\`trailhead-plan\` for the Plan step, \`trailhead-execute\` for Execute, likewise research/review/debug, plus the 2 keyless \`trailhead-fix\` and \`trailhead-codebase-map\` for the Fix and Codebase-map steps), which routes it onto that technique's pinned model when one is set, or the session model otherwise. If the \`spawn_agent\` schema exposes NO \`agent_type\` field at all (a Codex build with only base multi_agent v1), spawn with base \`spawn_agent\` and the subagents inherit the one session model. Trust the tool surface you actually see and degrade gracefully: \`multi_agent_v2\` is still under development upstream, so v1 stays a supported runtime.
105
105
 
106
106
  \`config.models.*\` (Claude model ids) still cannot take effect here regardless: Codex's subagent model registry is OpenAI-only, so a Claude id cannot run and those keys collapse to the session model. The honoured namespace on Codex is \`models.codex.*\` (above); the one-time models-collapse notice still applies while a stray \`models.*\` is set and \`models.codex.*\` is empty.
107
107
 
@@ -109,7 +109,7 @@ Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_inp
109
109
  The engine's \`/clear\` is Codex's \`/new\` (start a fresh session). Command references in a handoff use the skill form (\`$trailhead work <n>\`).
110
110
 
111
111
  ## F. Lifecycle hooks (native Codex hooks)
112
- 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 and secret-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs, the injection-scanner as a \`PostToolUse\` (matcher \`Bash\`) advisory, and check-update as a \`SessionStart\` hook. 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.
112
+ 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 and install-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs, the injection-scanner as a \`PostToolUse\` (matcher \`Bash\`) advisory, and check-update as a \`SessionStart\` hook. 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.
113
113
 
114
114
  ## G. Design / UI mockups -> local disk
115
115
  The \`claude.ai/design\` mockup mode (DesignSync: the \`claude_design\` MCP + the \`/design-sync\` skill) does not exist on Codex, so it **collapses to local-disk mockups** here: whatever \`config.design\` says, the Prototype technique's \`disk\` path is the only one available. Produce a throwaway static HTML mockup next to the code, link it from the ticket, and never attempt DesignSync / claude.ai/design on Codex (there is no \`claude_design\` MCP or \`/design-sync\` skill to call). \`config.design.approval\` still applies unchanged: \`explicit\` waits for the user's go-ahead before any UI code, \`auto\` surfaces the mockup and proceeds.
@@ -165,47 +165,56 @@ policy:
165
165
  `;
166
166
  }
167
167
 
168
- // --- CODEX_AGENT_TECHNIQUES ------------------------------------------------
169
- // Metadata for the 5 trailhead activities that can get a per-technique Codex
170
- // custom-agent TOML: a short UI description and the developer_instructions
171
- // that steer that subagent on Codex, pointing back at the projected skill.
172
- const CODEX_AGENT_TECHNIQUES = Object.freeze({
173
- plan: Object.freeze({
174
- description: "trailhead planning subagent: produces a build ticket's PLAN (steps, seams, files, verification)",
175
- developerInstructions: "You are trailhead's planning subagent on Codex. Produce the PLAN for a trailhead build ticket: the steps, the seams, the files touched, and the verification criteria, applying TDD per the ticket's config. Follow the Plan step and the referenced technique in the trailhead skill at ~/.codex/skills/trailhead. Return the plan; do not implement.",
176
- }),
177
- execute: Object.freeze({
178
- description: 'trailhead execute subagent: implements a PLAN with atomic commits',
179
- developerInstructions: "You are trailhead's execute subagent on Codex. Implement the approved PLAN for a trailhead build ticket with atomic conventional commits, each carrying a Refs: #<n> trailer for the ticket, following the repo's conventions. Follow the Execute step in the trailhead skill at ~/.codex/skills/trailhead.",
180
- }),
181
- research: Object.freeze({
182
- description: 'trailhead research subagent: gathers a decision-ready fact from primary sources',
183
- developerInstructions: "You are trailhead's research subagent on Codex. Gather the decision-ready fact a ticket waits on from primary sources, per the Research technique in the trailhead skill at ~/.codex/skills/trailhead. Record findings with citations; change no product code beyond the research artifact.",
184
- }),
185
- review: Object.freeze({
186
- description: "trailhead code-review subagent: adversarially reviews a ticket's diff",
187
- developerInstructions: "You are trailhead's code-review subagent on Codex. Review the ticket's diff adversarially on the trailhead Code review technique's axes, verifying each finding before reporting, per the trailhead skill at ~/.codex/skills/trailhead. Do not defer to a convention or a settled-decision memory.",
188
- }),
189
- debug: Object.freeze({
190
- description: "trailhead debug subagent: finds a defect's root cause by the scientific method",
191
- developerInstructions: "You are trailhead's debug subagent on Codex. Find the root cause of the ticket's defect by the scientific method (reproduce, localise, falsifiable hypotheses, confirm, verify), per the Debug technique in the trailhead skill at ~/.codex/skills/trailhead.",
192
- }),
168
+ // --- CODEX_AGENT_NAME_ALIASES / codexTechniqueKey ---------------------------
169
+ // The 7 committed agent sources (plugins/trailhead/agents/trailhead-*.md) are
170
+ // keyed by their trailhead-<name>.md filename stem. Two of those stems don't
171
+ // match the models.codex.<key> pin namespace, so alias them; every other
172
+ // agent's key is just its name with the trailhead- prefix stripped.
173
+ const CODEX_AGENT_NAME_ALIASES = Object.freeze({
174
+ 'trailhead-executor': 'execute',
175
+ 'trailhead-code-review': 'review',
193
176
  });
194
177
 
178
+ function codexTechniqueKey(name) {
179
+ return CODEX_AGENT_NAME_ALIASES[name] || String(name).replace(/^trailhead-/, '');
180
+ }
181
+
182
+ // --- MODEL_KEYS --------------------------------------------------------------
183
+ // The 5 pinnable technique keys, mirroring the models.codex.* namespace. The
184
+ // other 2 committed agents (fix, codebase-map) have no pin key and are always
185
+ // projected pin-less (session model).
186
+ const MODEL_KEYS = new Set(['plan', 'execute', 'research', 'review', 'debug']);
187
+
195
188
  // --- codexAgentToml ---------------------------------------------------------
196
- // Render one Codex custom-agent TOML. model_reasoning_effort is emitted only
197
- // when effort is a non-empty string. developer_instructions is a TOML
198
- // multi-line literal block ('''...'''); a stray ''' inside the text (none of
199
- // ours has one, but be safe) is neutralised so it can't close the block early.
189
+ // Render one Codex custom-agent TOML. When `model` is empty/absent, the TOML
190
+ // is pin-less: no `model =` line (and so no model_reasoning_effort either),
191
+ // so the subagent inherits the session model. When `model` is present,
192
+ // model_reasoning_effort is emitted only when effort is a non-empty string.
193
+ // developer_instructions is a TOML multi-line literal block ('''...'''); a
194
+ // stray ''' inside the text (none of ours has one, but be safe) is
195
+ // neutralised so it can't close the block early. The basic-string fields
196
+ // (name/description/model/effort) are escaped so a stray " or \ in a source
197
+ // value (description comes from the agent .md frontmatter) can't break the TOML.
198
+ function tomlBasicString(s) {
199
+ return String(s)
200
+ .replace(/\\/g, '\\\\')
201
+ .replace(/"/g, '\\"')
202
+ .replace(/\n/g, '\\n')
203
+ .replace(/\r/g, '\\r')
204
+ .replace(/\t/g, '\\t');
205
+ }
206
+
200
207
  function codexAgentToml({ name, description, developerInstructions, model, effort }) {
201
208
  const safeInstructions = String(developerInstructions).split("'''").join("''");
202
209
  const lines = [
203
- `name = "${name}"`,
204
- `description = "${description}"`,
205
- `model = "${model}"`,
210
+ `name = "${tomlBasicString(name)}"`,
211
+ `description = "${tomlBasicString(description)}"`,
206
212
  ];
207
- if (typeof effort === 'string' && effort.trim() !== '') {
208
- lines.push(`model_reasoning_effort = "${effort}"`);
213
+ if (typeof model === 'string' && model.trim() !== '') {
214
+ lines.push(`model = "${tomlBasicString(model)}"`);
215
+ if (typeof effort === 'string' && effort.trim() !== '') {
216
+ lines.push(`model_reasoning_effort = "${tomlBasicString(effort)}"`);
217
+ }
209
218
  }
210
219
  lines.push(`developer_instructions = '''\n${safeInstructions}\n'''`);
211
220
  return lines.join('\n') + '\n';
@@ -214,43 +223,54 @@ function codexAgentToml({ name, description, developerInstructions, model, effor
214
223
  // --- codexAgentTomlPlan ------------------------------------------------------
215
224
  // Given the resolved models.codex config (a plain object like
216
225
  // { plan: "gpt-5.6-terra", execute: { model: "gpt-5.6-sol", effort: "high" } },
217
- // or null/undefined/{}), return the set of trailhead-<technique>.toml writes
218
- // the installer should make. Skips unknown keys and empty values. Iterates
219
- // CODEX_AGENT_TECHNIQUES in declaration order (not the config's key order) so
220
- // the write order is deterministic regardless of how the config was authored.
221
- function codexAgentTomlPlan(codexHome, codexModels) {
226
+ // or null/undefined/{}) and `agentDefs` (the array from the installer's
227
+ // readAgentDefs(): every committed plugins/trailhead/agents/trailhead-*.md,
228
+ // each { name, description, tools, body }), return the set of
229
+ // trailhead-<technique>.toml writes the installer should make: ONE per agent,
230
+ // uniformly (all 7 project, always). A keyed agent (plan/execute/research/
231
+ // review/debug) carries its models.codex.<key> pin when set, else is
232
+ // pin-less; the 2 keyless agents (fix, codebase-map) are always pin-less.
233
+ // Iterates agentDefs in the given order (readAgentDefs sorts by name) so the
234
+ // write order is deterministic.
235
+ function codexAgentTomlPlan(codexHome, codexModels, agentDefs) {
222
236
  const models = codexModels && typeof codexModels === 'object' ? codexModels : {};
223
237
  const dir = codexLayout(codexHome).codexAgentsDir;
238
+ const defs = Array.isArray(agentDefs) ? agentDefs : [];
224
239
  const writes = [];
225
240
 
226
- for (const technique of Object.keys(CODEX_AGENT_TECHNIQUES)) {
227
- const raw = models[technique];
228
- if (raw == null) continue;
241
+ for (const def of defs) {
242
+ const key = codexTechniqueKey(def.name);
243
+ const name = `trailhead-${key}`;
244
+ const filePath = path.join(dir, `${name}.toml`);
229
245
 
230
246
  let model = null;
231
247
  let effort = null;
232
- if (typeof raw === 'string') {
233
- if (raw.trim() !== '') model = raw;
234
- } else if (typeof raw === 'object') {
235
- const m = typeof raw.model === 'string' ? raw.model : '';
236
- if (m.trim() !== '') {
237
- model = m;
238
- effort = typeof raw.effort === 'string' ? raw.effort : null;
248
+ if (MODEL_KEYS.has(key) && models[key] != null) {
249
+ const raw = models[key];
250
+ if (typeof raw === 'string') {
251
+ if (raw.trim() !== '') model = raw;
252
+ } else if (typeof raw === 'object') {
253
+ const m = typeof raw.model === 'string' ? raw.model : '';
254
+ if (m.trim() !== '') {
255
+ model = m;
256
+ effort = typeof raw.effort === 'string' ? raw.effort : null;
257
+ }
239
258
  }
240
259
  }
241
- if (!model) continue;
242
260
 
243
- const meta = CODEX_AGENT_TECHNIQUES[technique];
244
- const name = `trailhead-${technique}`;
245
- const filePath = path.join(dir, `${name}.toml`);
261
+ const lead = 'You are running on Codex CLI; the trailhead skill lives at ~/.codex/skills/trailhead.\n\n';
262
+ const developerInstructions = lead + convertToCodex(String(def.body || '')).trim();
263
+
246
264
  const content = codexAgentToml({
247
265
  name,
248
- description: meta.description,
249
- developerInstructions: meta.developerInstructions,
266
+ // Route the description through convertToCodex too (like the body), and
267
+ // default a missing frontmatter description to '' rather than "undefined".
268
+ description: convertToCodex(String(def.description || '')),
269
+ developerInstructions,
250
270
  model,
251
271
  effort,
252
272
  });
253
- writes.push({ technique, name, path: filePath, content });
273
+ writes.push({ technique: key, name, path: filePath, content });
254
274
  }
255
275
 
256
276
  return { writes };
@@ -345,6 +365,7 @@ function codexHookEntries(hooksScriptsDir) {
345
365
  return [
346
366
  { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-commit-guard.js') },
347
367
  { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-secret-guard.js') },
368
+ { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-install-guard.js') },
348
369
  { event: 'PostToolUse', matcher: 'Bash', command: cmd('trailhead-issue-injection-scanner.js') },
349
370
  { event: 'SessionStart', matcher: '', command: cmd('trailhead-check-update.js') },
350
371
  ];
@@ -143,11 +143,13 @@ ok('codexVerbSkillPlan: null verbs yields nothing', codexVerbSkillPlan('/c', nul
143
143
 
144
144
  // --- codexHookEntries ---
145
145
  const entries = codexHookEntries('/h');
146
- ok('codexHookEntries returns 4 entries', Array.isArray(entries) && entries.length === 4);
146
+ ok('codexHookEntries returns 5 entries', Array.isArray(entries) && entries.length === 5);
147
147
  ok('codexHookEntries: commit-guard is PreToolUse/Bash', entries.some((e) =>
148
148
  e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-commit-guard.js') && e.command.includes('/h')));
149
149
  ok('codexHookEntries: secret-guard is PreToolUse/Bash', entries.some((e) =>
150
150
  e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-secret-guard.js')));
151
+ ok('codexHookEntries: install-guard is PreToolUse/Bash', entries.some((e) =>
152
+ e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-install-guard.js')));
151
153
  ok('codexHookEntries: injection-scanner is PostToolUse/Bash', entries.some((e) =>
152
154
  e.event === 'PostToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-issue-injection-scanner.js')));
153
155
  ok('codexHookEntries: check-update is SessionStart', entries.some((e) =>
@@ -198,24 +200,110 @@ ok('codexAgentToml without effort omits model_reasoning_effort', (() => {
198
200
  });
199
201
  return out.includes('model = "gpt-5.6-terra"') && !out.includes('model_reasoning_effort');
200
202
  })());
203
+ ok('codexAgentToml with no model emits a pin-less TOML (no model line, no effort line, keeps the rest)', (() => {
204
+ const out = codexAgentToml({
205
+ name: 'trailhead-fix',
206
+ description: 'desc',
207
+ developerInstructions: 'fix the thing',
208
+ model: null,
209
+ effort: null,
210
+ });
211
+ return out.includes('name = "trailhead-fix"') &&
212
+ out.includes('description = "desc"') &&
213
+ !/^model = /m.test(out) &&
214
+ !out.includes('model_reasoning_effort') &&
215
+ out.includes("developer_instructions = '''") &&
216
+ out.includes('fix the thing');
217
+ })());
218
+
219
+ ok('codexAgentToml escapes a " and \\ in name/description so the TOML stays valid', (() => {
220
+ const out = codexAgentToml({
221
+ name: 'trailhead-plan',
222
+ description: 'has a "quote" and a back\\slash',
223
+ developerInstructions: 'body',
224
+ model: null,
225
+ effort: null,
226
+ });
227
+ // The emitted basic string must carry escaped forms, never a raw " that
228
+ // would prematurely close the TOML string.
229
+ return out.includes('description = "has a \\"quote\\" and a back\\\\slash"');
230
+ })());
201
231
 
202
232
  // --- codexAgentTomlPlan ---
203
- ok('codexAgentTomlPlan: string value produces one write with the right path/name/model', (() => {
204
- const plan = codexAgentTomlPlan('/c', { execute: 'gpt-5.6-terra' });
205
- return plan.writes.length === 1 &&
206
- plan.writes[0].path === '/c/agents/trailhead-execute.toml' &&
207
- plan.writes[0].name === 'trailhead-execute' &&
208
- plan.writes[0].content.includes('model = "gpt-5.6-terra"');
233
+ // 7-entry stub agentDefs mirroring the real plugins/trailhead/agents/*.md
234
+ // sources: 5 keyed techniques (plan/execute/research/review/debug, whose
235
+ // filenames derive from trailhead-executor -> execute and
236
+ // trailhead-code-review -> review) + 2 always-keyless agents.
237
+ const STUB_AGENT_DEFS = [
238
+ { name: 'trailhead-plan', description: 'plan desc', tools: 'Read', body: 'Plan body.' },
239
+ { name: 'trailhead-executor', description: 'execute desc', tools: 'Read, Write', body: 'Execute body.' },
240
+ { name: 'trailhead-research', description: 'research desc', tools: 'Read', body: 'Research body.' },
241
+ { name: 'trailhead-code-review', description: 'review desc', tools: 'Read', body: 'Review body.' },
242
+ { name: 'trailhead-debug', description: 'debug desc', tools: 'Read', body: 'Debug body.' },
243
+ { name: 'trailhead-fix', description: 'fix desc', tools: 'Read, Write', body: 'Fix body.' },
244
+ { name: 'trailhead-codebase-map', description: 'map desc', tools: 'Read', body: 'Map body.' },
245
+ ];
246
+
247
+ ok('codexAgentTomlPlan: 7 agentDefs -> 7 writes with only the pinned key carrying a model', (() => {
248
+ const plan = codexAgentTomlPlan('/c', { execute: 'gpt-5.6-terra' }, STUB_AGENT_DEFS);
249
+ if (plan.writes.length !== 7) return false;
250
+ const byName = Object.fromEntries(plan.writes.map((w) => [w.name, w]));
251
+ const execute = byName['trailhead-execute'];
252
+ if (!execute || execute.path !== '/c/agents/trailhead-execute.toml') return false;
253
+ if (!execute.content.includes('model = "gpt-5.6-terra"')) return false;
254
+ const others = plan.writes.filter((w) => w.name !== 'trailhead-execute');
255
+ return others.length === 6 && others.every((w) => !/^model = /m.test(w.content));
256
+ })());
257
+
258
+ ok('codexAgentTomlPlan: keyless trailhead-fix / trailhead-codebase-map are always pin-less', (() => {
259
+ const plan = codexAgentTomlPlan('/c', { fix: 'gpt-5.6-terra', 'codebase-map': 'gpt-5.6-terra' }, STUB_AGENT_DEFS);
260
+ const byName = Object.fromEntries(plan.writes.map((w) => [w.name, w]));
261
+ const fix = byName['trailhead-fix'];
262
+ const map = byName['trailhead-codebase-map'];
263
+ return fix && map &&
264
+ fix.path === '/c/agents/trailhead-fix.toml' && !/^model = /m.test(fix.content) &&
265
+ map.path === '/c/agents/trailhead-codebase-map.toml' && !/^model = /m.test(map.content);
266
+ })());
267
+
268
+ ok('codexAgentTomlPlan: empty models object -> 7 pin-less writes (not zero)', (() => {
269
+ const plan = codexAgentTomlPlan('/c', {}, STUB_AGENT_DEFS);
270
+ return plan.writes.length === 7 && plan.writes.every((w) => !/^model = /m.test(w.content));
271
+ })());
272
+
273
+ ok('codexAgentTomlPlan: null models -> 7 pin-less writes (not zero)', (() => {
274
+ const plan = codexAgentTomlPlan('/c', null, STUB_AGENT_DEFS);
275
+ return plan.writes.length === 7 && plan.writes.every((w) => !/^model = /m.test(w.content));
276
+ })());
277
+
278
+ ok('codexAgentTomlPlan: object model value on the execute key carries model + model_reasoning_effort', (() => {
279
+ const plan = codexAgentTomlPlan('/c', { execute: { model: 'gpt-5.6-sol', effort: 'high' } }, STUB_AGENT_DEFS);
280
+ const execute = plan.writes.find((w) => w.name === 'trailhead-execute');
281
+ return execute.content.includes('model = "gpt-5.6-sol"') && execute.content.includes('model_reasoning_effort = "high"');
209
282
  })());
210
- ok('codexAgentTomlPlan: object value includes model + effort', (() => {
211
- const plan = codexAgentTomlPlan('/c', { execute: { model: 'gpt-5.6-sol', effort: 'high' } });
212
- const c = plan.writes[0].content;
213
- return c.includes('model = "gpt-5.6-sol"') && c.includes('model_reasoning_effort = "high"');
283
+
284
+ ok('codexAgentTomlPlan: a keyed key whose object value has no model stays pin-less', (() => {
285
+ const plan = codexAgentTomlPlan('/c', { execute: { effort: 'high' } }, STUB_AGENT_DEFS);
286
+ const execute = plan.writes.find((w) => w.name === 'trailhead-execute');
287
+ return plan.writes.length === 7 && !/^model = /m.test(execute.content) && !execute.content.includes('model_reasoning_effort');
288
+ })());
289
+
290
+ ok('codexAgentTomlPlan: description is routed through convertToCodex (host paths rewritten)', (() => {
291
+ const defs = [{ name: 'trailhead-plan', description: 'see ~/.claude/skills', tools: 'Read', body: 'b' }];
292
+ const plan = codexAgentTomlPlan('/c', {}, defs);
293
+ return plan.writes[0].content.includes('description = "see ~/.codex/skills"');
294
+ })());
295
+
296
+ ok('codexAgentTomlPlan: a missing description renders as an empty string, not "undefined"', (() => {
297
+ const defs = [{ name: 'trailhead-plan', tools: 'Read', body: 'b' }];
298
+ const plan = codexAgentTomlPlan('/c', {}, defs);
299
+ return plan.writes[0].content.includes('description = ""') && !plan.writes[0].content.includes('description = "undefined"');
300
+ })());
301
+
302
+ ok('codexAgentTomlPlan: empty/undefined agentDefs -> no writes (installer then leaves multi_agent_v2 off)', (() => {
303
+ return codexAgentTomlPlan('/c', {}, []).writes.length === 0 &&
304
+ codexAgentTomlPlan('/c', {}).writes.length === 0 &&
305
+ codexAgentTomlPlan('/c', {}, null).writes.length === 0;
214
306
  })());
215
- ok('codexAgentTomlPlan: empty object -> no writes', codexAgentTomlPlan('/c', {}).writes.length === 0);
216
- ok('codexAgentTomlPlan: null -> no writes', codexAgentTomlPlan('/c', null).writes.length === 0);
217
- ok('codexAgentTomlPlan: unknown key skipped', codexAgentTomlPlan('/c', { bogus: 'x' }).writes.length === 0);
218
- ok('codexAgentTomlPlan: object value with no model is skipped', codexAgentTomlPlan('/c', { execute: { effort: 'high' } }).writes.length === 0);
219
307
 
220
308
  // --- enableCodexMultiAgentV2Feature ---
221
309
  ok('enableCodexMultiAgentV2Feature: dotted form already true -> null',