@marcomigozzi/trailhead 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.it.md +82 -25
  2. package/README.md +82 -25
  3. package/bin/base-preflight-docs.test.js +4 -1
  4. package/bin/bulk-cross-reference-docs.test.js +36 -0
  5. package/bin/capture-action.test.js +167 -0
  6. package/bin/capture-docs.test.js +32 -0
  7. package/bin/engine-route-docs.test.js +155 -0
  8. package/bin/facet-stamp-docs.test.js +79 -0
  9. package/bin/forced-mutation-docs.test.js +38 -0
  10. package/bin/grill-model-docs.test.js +155 -0
  11. package/bin/grilling-holistic-review-docs.test.js +7 -3
  12. package/bin/guard-axis-docs.test.js +69 -0
  13. package/bin/inbox-needs-info-docs.test.js +68 -0
  14. package/bin/lib/codex-projection.js +28 -19
  15. package/bin/lib/codex-projection.test.js +77 -20
  16. package/bin/lib/host-descriptor.js +1 -1
  17. package/bin/lib/host-descriptor.test.js +6 -0
  18. package/bin/needs-you-docs.test.js +9 -2
  19. package/bin/options-in-chat-docs.test.js +65 -0
  20. package/bin/pick-stress-test-docs.test.js +1 -1
  21. package/bin/pipeline-review-docs.test.js +118 -0
  22. package/bin/plan-review-budget-docs.test.js +63 -0
  23. package/bin/repo-target-docs.test.js +41 -0
  24. package/bin/report-status-docs.test.js +10 -2
  25. package/bin/seam-coverage-docs.test.js +46 -0
  26. package/bin/session-trust-docs.test.js +37 -0
  27. package/bin/standards-brief-docs.test.js +112 -0
  28. package/bin/surfaced-scope-docs.test.js +57 -0
  29. package/bin/test-once-docs.test.js +101 -0
  30. package/bin/test-run-docs.test.js +47 -0
  31. package/bin/trailhead.js +15 -8
  32. package/bin/trailhead.test.js +71 -17
  33. package/bin/unblock-docs.test.js +56 -0
  34. package/bin/yes-accepts-recommended-docs.test.js +54 -0
  35. package/package.json +1 -1
  36. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  37. package/plugins/trailhead/agents/trailhead-code-review.md +2 -1
  38. package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
  39. package/plugins/trailhead/agents/trailhead-grill.md +41 -0
  40. package/plugins/trailhead/agents/trailhead-plan.md +1 -1
  41. package/plugins/trailhead/agents/trailhead-verify.md +1 -0
  42. package/plugins/trailhead/hooks/lib/audit-walk.test.js +62 -0
  43. package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
  44. package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
  45. package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
  46. package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
  47. package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
  48. package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
  49. package/plugins/trailhead/hooks/lib/git-env-mod.test.js +55 -0
  50. package/plugins/trailhead/hooks/lib/git-env-spawn.test.js +73 -0
  51. package/plugins/trailhead/hooks/lib/git-env.test.js +51 -0
  52. package/plugins/trailhead/hooks/lib/global-config-dir.test.js +48 -0
  53. package/plugins/trailhead/hooks/lib/heredoc-mod.test.js +85 -0
  54. package/plugins/trailhead/hooks/lib/inbox-needs-info.test.js +179 -0
  55. package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
  56. package/plugins/trailhead/hooks/lib/issue-body.test.js +165 -0
  57. package/plugins/trailhead/hooks/lib/model-defaults-review.js +22 -1
  58. package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +64 -1
  59. package/plugins/trailhead/hooks/lib/model-route.js +207 -0
  60. package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
  61. package/plugins/trailhead/hooks/lib/needs-you.test.js +45 -1
  62. package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +272 -0
  63. package/plugins/trailhead/hooks/lib/repo-target.test.js +256 -0
  64. package/plugins/trailhead/hooks/lib/report-status.test.js +56 -1
  65. package/plugins/trailhead/hooks/lib/statusline-git-env.test.js +47 -0
  66. package/plugins/trailhead/hooks/lib/test-run.test.js +757 -0
  67. package/plugins/trailhead/hooks/lib/trailhead-git-env.js +40 -0
  68. package/plugins/trailhead/hooks/mod/README.md +42 -17
  69. package/plugins/trailhead/hooks/mod/auto-run.ts +2 -1
  70. package/plugins/trailhead/hooks/mod/git-env.ts +31 -0
  71. package/plugins/trailhead/hooks/mod/heredoc.ts +79 -0
  72. package/plugins/trailhead/hooks/mod/map-pane-view.ts +3 -1
  73. package/plugins/trailhead/hooks/mod/map-pane.tsx +204 -49
  74. package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
  75. package/plugins/trailhead/hooks/mod/repo-pin.ts +113 -9
  76. package/plugins/trailhead/hooks/mod/work-state-locate.ts +2 -1
  77. package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
  78. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +28 -14
  79. package/plugins/trailhead/hooks/trailhead-check-update.js +20 -2
  80. package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
  81. package/plugins/trailhead/hooks/trailhead-labels-guard.js +2 -2
  82. package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +1 -1
  83. package/plugins/trailhead/hooks/trailhead-test-guard.js +232 -0
  84. package/plugins/trailhead/hooks/trailhead-test-guard.test.js +172 -0
  85. package/plugins/trailhead/skills/_shared/capture-core.md +123 -0
  86. package/plugins/trailhead/skills/_shared/choices.md +2 -2
  87. package/plugins/trailhead/skills/_shared/configuration-reference.md +30 -16
  88. package/plugins/trailhead/skills/_shared/configuration.md +1 -1
  89. package/plugins/trailhead/skills/_shared/load-first.md +2 -0
  90. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  91. package/plugins/trailhead/skills/_shared/substrate-commands.md +19 -6
  92. package/plugins/trailhead/skills/_shared/substrate.md +4 -3
  93. package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
  94. package/plugins/trailhead/skills/_shared/teamwork.md +11 -5
  95. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
  96. package/plugins/trailhead/skills/_shared/techniques/code-review-pipeline.md +24 -0
  97. package/plugins/trailhead/skills/_shared/techniques/code-review.md +22 -6
  98. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +6 -3
  99. package/plugins/trailhead/skills/_shared/techniques/debug.md +5 -2
  100. package/plugins/trailhead/skills/_shared/techniques/grilling.md +28 -3
  101. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +16 -4
  102. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
  103. package/plugins/trailhead/skills/_shared/techniques/tdd.md +4 -2
  104. package/plugins/trailhead/skills/_shared/techniques/verify.md +5 -1
  105. package/plugins/trailhead/skills/_shared/techniques.md +8 -6
  106. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
  107. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
  108. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
  109. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
  110. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +11 -6
  111. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +18 -3
  112. package/plugins/trailhead/skills/trailhead-work/SKILL.md +12 -9
  113. package/plugins/trailhead/skills/trailhead-work/references/auto.md +3 -1
  114. package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +2 -0
  115. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +13 -13
  116. package/plugins/trailhead/templates/statusline-trailhead.sh +10 -3
  117. package/plugins/trailhead/templates/trailhead-audit-walk.js +56 -4
  118. package/plugins/trailhead/templates/trailhead-auto-merge.js +2 -1
  119. package/plugins/trailhead/templates/trailhead-auto-queue.js +2 -1
  120. package/plugins/trailhead/templates/trailhead-auto-slot.js +2 -1
  121. package/plugins/trailhead/templates/trailhead-base-preflight.js +10 -5
  122. package/plugins/trailhead/templates/trailhead-capture.js +419 -0
  123. package/plugins/trailhead/templates/trailhead-checkout-find.js +257 -0
  124. package/plugins/trailhead/templates/trailhead-checkout-in.js +78 -21
  125. package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +2 -1
  126. package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
  127. package/plugins/trailhead/templates/trailhead-git-env.js +40 -0
  128. package/plugins/trailhead/templates/trailhead-inbox-needs-info.js +161 -0
  129. package/plugins/trailhead/templates/trailhead-isolation-answer.js +2 -1
  130. package/plugins/trailhead/templates/trailhead-isolation-copy.js +9 -4
  131. package/plugins/trailhead/templates/trailhead-issue-body.js +161 -12
  132. package/plugins/trailhead/templates/trailhead-merge-ticket.js +2 -1
  133. package/plugins/trailhead/templates/trailhead-needs-you.js +38 -4
  134. package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +261 -7
  135. package/plugins/trailhead/templates/trailhead-repo-target.js +302 -0
  136. package/plugins/trailhead/templates/trailhead-report-status.js +57 -9
  137. package/plugins/trailhead/templates/trailhead-test-run.js +752 -0
  138. package/plugins/trailhead/templates/trailhead-uat-backfill.js +2 -1
  139. package/plugins/trailhead/templates/trailhead-uat-followup.js +2 -1
  140. package/plugins/trailhead/templates/trailhead-uat-setup.js +9 -4
  141. package/plugins/trailhead/templates/trailhead-unblock.js +2 -1
  142. package/plugins/trailhead/tests/auto-run-ui.test.ts +2 -1
  143. package/plugins/trailhead/tests/git-env.test.ts +20 -0
  144. package/plugins/trailhead/tests/map-pane-ui.test.ts +386 -50
  145. package/plugins/trailhead/tests/map-pane-view.test.ts +6 -0
  146. package/plugins/trailhead/tests/map-pane-write.test.ts +57 -1
  147. package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
  148. package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +3 -2
  149. package/plugins/trailhead/tests/repo-pin-relocate.test.ts +175 -0
  150. package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
  151. package/plugins/trailhead/tests/unscrub.ts +8 -0
  152. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +3 -2
  153. package/plugins/trailhead/tests/work-state-worktree.test.ts +2 -1
  154. package/plugins/trailhead/types/index.d.ts +7 -0
@@ -108,11 +108,11 @@ Where the engine uses \`AskUserQuestion\`, pose the choice as a **plain-text num
108
108
  After presenting the numbered list, STOP and wait: the user's **next message** is the answer to this pending question, not a fresh unrelated turn. Resolve it by matching either the **number typed** (e.g. "2") or the option's **label text** back onto that option, then continue the engine exactly as it would from a native \`AskUserQuestion\` response (e.g. write the chosen config value, or proceed down the chosen branch). If the reply matches no option, ask again rather than guessing. Never silently pick a default and proceed.
109
109
 
110
110
  ## D. Subagents (native multi_agent)
111
- Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_input\`, \`wait_agent\`, \`resume_agent\`, \`close_agent\`), stable and on by default on supported Codex (the installer gates on the floor that guarantees them). Where the engine delegates to a subagent (research, codebase-map, review, plan, execute, debug), spawn a real Codex subagent with these tools and the technique's own protocol (each technique carries its full protocol in-prompt, so no specialised agent is needed; pass the ticket's \`Scope:\` when relevant). The Claude-only Explore-vs-general-purpose split does not apply: Codex has one subagent kind. A subagent runs as a FRESH session that receives a task prompt, not this conversation; run independent ones in parallel (e.g. the codebase-map readers) and \`wait_agent\` on them.
111
+ Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_input\`, \`wait_agent\`, \`resume_agent\`, \`close_agent\`), stable and on by default on supported Codex (the installer gates on the floor that guarantees them). Where the engine delegates to a subagent (research, codebase-map, review, plan, execute, debug, grill), spawn a real Codex subagent with these tools and the technique's own protocol (each technique carries its full protocol in-prompt, so no specialised agent is needed; pass the ticket's \`Scope:\` when relevant). The Claude-only Explore-vs-general-purpose split does not apply: Codex has one subagent kind. Every engine subagent must start FRESH (the task prompt only, not this conversation): the spawn turns forking off, because without that \`multi_agent_v2\` defaults \`fork_turns\` to \`all\`, a full-history fork that inherits this conversation and ignores the model/effort override. So **every** \`spawn_agent\` (engine agents, the base v1 fallback, and generic spawns such as research or the codebase-map readers) passes \`fork_turns="none"\` when the visible schema exposes \`fork_turns\` (v2), or \`fork_context=false\` when it exposes \`fork_context\` (v1), and neither when the schema exposes neither (no fork option, so pass neither); key it on the context parameter the schema shows, not on whether \`agent_type\` is present. Run independent ones in parallel (e.g. the codebase-map readers) and \`wait_agent\` on them.
112
112
 
113
113
  **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.
114
114
 
115
- **Per-technique agent registry (\`agent_type\` dispatch).** The installer always projects all 8 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 the 1 keyless agent — 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/verify and \`trailhead-codebase-map\` for the Codebase-map step, plus the 1 keyless \`trailhead-fix\` for the Fix step), 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.
115
+ **Per-technique agent registry (\`agent_type\` dispatch).** The installer always projects all 9 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 fix with execute unset) 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>", fork_turns="none", reasoning_effort=…)\` (the v2 shape: a schema exposing only \`fork_context\` takes \`fork_context=false\` instead of \`fork_turns="none"\`) (\`trailhead-plan\` for the Plan step, \`trailhead-execute\` for Execute, likewise research/review/debug/verify \`trailhead-codebase-map\` for the Codebase-map step and \`trailhead-grill\` for a Grilling round, plus \`trailhead-fix\` for the Fix step, which tracks the Execute pin), which routes it onto that technique's pinned model when one is set, or the session model otherwise. An unpinned or session-equal grill runs inline in the main session (no spawn): the main session stays the HITL orchestrator and asks the user itself. 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\` (still with \`fork_context=false\`, or \`fork_turns="none"\` if that is what the schema exposes) 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.
116
116
 
117
117
  \`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.
118
118
 
@@ -120,13 +120,13 @@ Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_inp
120
120
  The engine's \`/clear\` is Codex's \`/new\` (start a fresh session). Command references in a handoff use the skill form (\`$trailhead work <n>\`).
121
121
 
122
122
  ## F. Lifecycle hooks (native Codex hooks)
123
- Codex has a hook bus, so trailhead's guardrails run as **real Codex hooks**, not degraded prose. The installer registers them in \`~/.codex/hooks.json\`: commit-guard, secret-guard, install-guard, body-guard and labels-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs (the five run in one process, a single \`trailhead-bash-guards.js --guards=commit,secret,install,body,labels\` registration, so a Bash call pays one start-up instead of five), secret-read-guard as a \`PreToolUse\` (matcher \`Read|Bash\`) hook that denies reads of \`.env\`/\`.env.*\`/\`.secrets\`, the injection-scanner as a \`PostToolUse\` (matcher \`Bash\`) advisory, mockup-link-stop as a \`Stop\` hook (when your final reply asks to approve a mockup without carrying its link, it continues the turn so you rewrite the ask with the URL or file path in the prose), and check-update as a \`SessionStart\` hook. On Codex, file reads happen through shell commands rather than a native Read-tool event, so the secret-read guard's enforcement here runs via its **Bash** arm (protection is not lost); the \`Read\` half of its matcher is a Claude-Code-native path, registered on Codex too only so it fires without changes if a future Codex version adds an equivalent event. search-guard is Claude-Code-specific (it only prevents arming Claude's \`Read()\` deny rule under bypass permissions, a mechanism Codex lacks), so it is not projected to Codex at all. They use the same wire format they use on Claude Code (JSON on stdin, a \`decision\`/\`permissionDecision\` verdict on stdout), which Codex accepts. The host-independent git \`commit-msg\` hook is still installed at repo first-use as well (defence in depth), exactly as on Claude. Codex gates hooks behind \`features.hooks = true\` and reviews them for trust on first start: the installer enables the flag, and Codex will ask you to trust trailhead's hooks the next time it starts.
123
+ Codex has a hook bus, so trailhead's guardrails run as **real Codex hooks**, not degraded prose. The installer registers them in \`~/.codex/hooks.json\`: commit-guard, secret-guard, install-guard, body-guard, labels-guard and test-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs (the six run in one process, a single \`trailhead-bash-guards.js --guards=commit,secret,install,body,labels,test\` registration, so a Bash call pays one start-up instead of six), secret-read-guard as a \`PreToolUse\` (matcher \`Read|Bash\`) hook that denies reads of \`.env\`/\`.env.*\`/\`.secrets\`, the injection-scanner as a \`PostToolUse\` (matcher \`Bash\`) advisory, mockup-link-stop as a \`Stop\` hook (when your final reply asks to approve a mockup without carrying its link, it continues the turn so you rewrite the ask with the URL or file path in the prose), and check-update as a \`SessionStart\` hook. On Codex, file reads happen through shell commands rather than a native Read-tool event, so the secret-read guard's enforcement here runs via its **Bash** arm (protection is not lost); the \`Read\` half of its matcher is a Claude-Code-native path, registered on Codex too only so it fires without changes if a future Codex version adds an equivalent event. search-guard is Claude-Code-specific (it only prevents arming Claude's \`Read()\` deny rule under bypass permissions, a mechanism Codex lacks), so it is not projected to Codex at all. They use the same wire format they use on Claude Code (JSON on stdin, a \`decision\`/\`permissionDecision\` verdict on stdout), which Codex accepts. The host-independent git \`commit-msg\` hook is still installed at repo first-use as well (defence in depth), exactly as on Claude. Codex gates hooks behind \`features.hooks = true\` and reviews them for trust on first start: the installer enables the flag, and Codex will ask you to trust trailhead's hooks the next time it starts.
124
124
 
125
125
  ## G. Design / UI mockups: artifacts -> stitch offer, else local disk
126
126
  The \`artifacts\` mockup mode (a Design canvas Artifact built with Claude Code's \`Artifact\` tool; the legacy \`claude.ai/design\` value means the same) does not exist on Codex: there is no \`Artifact\` tool here. When \`config.design\` names it, follow the Prototype technique's fallback: **offer once to switch this project to \`design: stitch\`** (the Google Stitch MCP runs on Codex too), and only on decline produce a throwaway static HTML mockup next to the code (\`disk\`), link it from the ticket, and say why. Never try to create or edit an Artifact on Codex. \`config.design.approval\` still applies unchanged: \`explicit\` waits for the user's go-ahead before any UI code, \`auto\` surfaces the mockup and proceeds.
127
127
 
128
128
  ## H. Shell timeouts & background jobs (plan-review's external CLIs)
129
- The **Cross-AI plan review** technique invokes each external reviewer CLI (\`codex exec\`, \`gemini -p\`, \`qwen -p\`, …) through Claude Code's **Bash tool**, and tells the engine to beat that tool's ~2-minute default cut by setting its \`timeout\` parameter to the 10-minute max, or launching the call with \`run_in_background: true\` and collecting it later. Those are Claude Bash-tool knobs with no Codex equivalent, so map the *intent* onto Codex's own shell/exec tool. Reviews never block the session (the engine's hard cross-host default), so **the default is to background the reviewers** and collect them by polling, never block on them: launch each with a trailing \`&\` (or your shell tool's background form), **redirect its stdin from \`/dev/null\` and its output to a file** (\`codex exec … "<prompt>" < /dev/null > out.txt 2>&1 &\`), and poll/collect when it finishes, so several reviewers run in parallel instead of serially eating the wall-clock; or wrap each reviewer invocation in a §D subagent so the long-running call sits behind the multi_agent poll loop. Where a reviewer must run synchronously instead, give that call its **own long timeout** (the \`timeout_ms\` / equivalent on Codex's shell/exec tool, set to several minutes, never the short default), so a reviewer that legitimately runs for minutes is not cut mid-review. Either way plan-review's own rule is unchanged: never rely on a short default timeout, retry once at the wide margin, then drop a still-slow reviewer rather than let it wedge the build. **Close the reviewer's stdin when backgrounding it** (\`< /dev/null\`): \`codex exec\` reads additional prompt input from stdin and blocks on EOF, so a backgrounded reviewer that inherits an open or attached stdin sits idle at "Reading additional input from stdin..." (0% CPU) forever instead of running (passing the prompt as an argument does not exempt it). Guard the poll loop too: a reviewer pinned at 0% CPU with no output past a threshold is stuck, so surface it and drop it after the one retry rather than reporting "still running" indefinitely. This is the external-CLI **shell** path; §D covers the separate **subagent** (\`wait_agent\`) path, and the two are distinct (an external reviewer CLI is a plain shell call, not a spawned Codex subagent).
129
+ The **Cross-AI plan review** technique invokes each external reviewer CLI (\`codex exec\`, \`gemini -p\`, \`qwen -p\`, …) through Claude Code's **Bash tool**, and tells the engine to beat that tool's ~2-minute default cut by setting its \`timeout\` parameter to the 10-minute max, or launching the call with \`run_in_background: true\` and collecting it later. Those are Claude Bash-tool knobs with no Codex equivalent, so map the *intent* onto Codex's own shell/exec tool. Reviews never block the session (the engine's hard cross-host default), so **the default is to background the reviewers** and collect them by polling, never block on them: launch each through the plan-review **\`run\` subcommand** (\`node <templates>/trailhead-plan-review-rounds.js run --output <dir>/<cli>.out -- <the CLI's headless command>\`), backgrounded with a trailing \`&\` (or your shell tool's background form) and the helper's one JSON line sent to a file (\`… run --output out.txt -- codex exec … "<prompt>" > run.json &\`), then poll/collect when it finishes, so several reviewers run in parallel instead of serially eating the wall-clock; or wrap each \`run\` invocation in a §D subagent so the long-running call sits behind the multi_agent poll loop. The helper is what bounds the call (a 600-second wall-clock deadline, SIGTERM then SIGKILL to the reviewer's process group); nothing else on Codex limits a backgrounded call. Where a reviewer must run synchronously instead, it still goes through \`run\`, and the call gets its **own long timeout** (the \`timeout_ms\` / equivalent on Codex's shell/exec tool, set above the helper's deadline, never the short default), so the helper rather than the tool ends it. Either way plan-review's own rule is unchanged: branch on the JSON \`status\` (a \`timeout\` skips the \`overflow\` check), retry once, then drop a still-slow reviewer, recorded as skipped, rather than let it wedge the build; a reviewer is never reported as "still running" past the bound. **Stdin must be closed on every reviewer call**: \`codex exec\` also reads prompt input from stdin, so a call that inherits an open or attached stdin may wait for EOF (passing the prompt as an argument does not exempt it). The \`run\` helper closes it; the raw fallback (\`timeout -k 10 600 <cmd> < /dev/null > out.txt 2>&1\`, used only when the helper is missing, exits non-zero with no JSON line, or prints non-JSON output; a \`status: error\` JSON line is handled by the \`failed\`/\`error\` branch instead) appends \`< /dev/null\` itself. The line "Reading additional input from stdin..." is not evidence of a stdin hang (codex-cli prints it with stdin already closed), so never diagnose a stall from it. This is the external-CLI **shell** path; §D covers the separate **subagent** (\`wait_agent\`) path, and the two are distinct (an external reviewer CLI is a plain shell call, not a spawned Codex subagent).
130
130
  </codex_skill_adapter>`;
131
131
  }
132
132
 
@@ -191,10 +191,11 @@ function codexTechniqueKey(name) {
191
191
  }
192
192
 
193
193
  // --- MODEL_KEYS --------------------------------------------------------------
194
- // The 7 pinnable technique keys, mirroring the models.codex.* namespace. The
195
- // other committed agent (fix) has no pin key and is always
196
- // projected pin-less (session model).
197
- const MODEL_KEYS = new Set(['plan', 'execute', 'research', 'review', 'debug', 'verify', 'codebase-map']);
194
+ // The 8 pinnable technique keys, mirroring the models.codex.* namespace. The
195
+ // other committed agent (fix) has no pin key of its own: it tracks execute.
196
+ // Riconoscimento condiviso di "inherit session" (stesso del router, hooks/lib); modulo senza dipendenze, la proiezione resta pura.
197
+ const { isInheritSession } = require('../../plugins/trailhead/hooks/lib/inherit-session.js');
198
+ const MODEL_KEYS = new Set(['plan', 'execute', 'research', 'review', 'debug', 'verify', 'codebase-map', 'grill']);
198
199
 
199
200
  // --- codexAgentToml ---------------------------------------------------------
200
201
  // Render one Codex custom-agent TOML. When `model` is empty/absent, the TOML
@@ -238,9 +239,10 @@ function codexAgentToml({ name, description, developerInstructions, model, effor
238
239
  // readAgentDefs(): every committed plugins/trailhead/agents/trailhead-*.md,
239
240
  // each { name, description, tools, body }), return the set of
240
241
  // trailhead-<technique>.toml writes the installer should make: ONE per agent,
241
- // uniformly (all 8 project, always). A keyed agent (plan/execute/research/
242
- // review/debug/verify/codebase-map) carries its models.codex.<key> pin when set, else is
243
- // pin-less; the 1 keyless agent (fix) is always pin-less.
242
+ // uniformly (all 9 project, always). A keyed agent (plan/execute/research/
243
+ // review/debug/verify/codebase-map/grill) carries its models.codex.<key> pin when set, else is
244
+ // pin-less; fix has no key and carries the models.codex.execute pin (same
245
+ // string/object/effort/inherit-session handling), so no agent is keyless.
244
246
  // Iterates agentDefs in the given order (readAgentDefs sorts by name) so the
245
247
  // write order is deterministic.
246
248
  function codexAgentTomlPlan(codexHome, codexModels, agentDefs) {
@@ -256,13 +258,19 @@ function codexAgentTomlPlan(codexHome, codexModels, agentDefs) {
256
258
 
257
259
  let model = null;
258
260
  let effort = null;
259
- if (MODEL_KEYS.has(key) && models[key] != null) {
260
- const raw = models[key];
261
+ // grill non impostato segue models.codex.plan (come model-route.js).
262
+ // fix non ha chiave propria: segue models.codex.execute (come model-route.js).
263
+ let pinKey = key === 'grill' && (models.grill == null || (typeof models.grill === 'string' && models.grill.trim() === '')) ? 'plan' : key;
264
+ if (key === 'fix') pinKey = 'execute';
265
+ if ((MODEL_KEYS.has(key) || key === 'fix') && models[pinKey] != null) {
266
+ const raw = models[pinKey];
267
+ // "inherit session" e' il default Codex documentato: nessun pin.
268
+ const isInherit = isInheritSession;
261
269
  if (typeof raw === 'string') {
262
- if (raw.trim() !== '') model = raw;
270
+ if (raw.trim() !== '' && !isInherit(raw)) model = raw;
263
271
  } else if (typeof raw === 'object') {
264
272
  const m = typeof raw.model === 'string' ? raw.model : '';
265
- if (m.trim() !== '') {
273
+ if (m.trim() !== '' && !isInherit(m)) {
266
274
  model = m;
267
275
  effort = typeof raw.effort === 'string' ? raw.effort : null;
268
276
  }
@@ -373,9 +381,9 @@ function codexVerbSkillPlan(codexHome, verbs) {
373
381
  // --- codexHookEntries -----------------------------------------------------
374
382
  // The guardrail hooks trailhead registers on Codex, as {event, matcher,
375
383
  // command} records. Commands run the copied hook scripts under hooksScriptsDir
376
- // via node, matching Claude's `node "<path>"` form. The five Bash guards
377
- // (commit, secret, install, body, labels) are ONE registration, the merged
378
- // trailhead-bash-guards.js with `--guards=commit,secret,install,body,labels` (a single
384
+ // via node, matching Claude's `node "<path>"` form. The six Bash guards
385
+ // (commit, secret, install, body, labels, test) are ONE registration, the merged
386
+ // trailhead-bash-guards.js with `--guards=commit,secret,install,body,labels,test` (a single
379
387
  // node process per Bash call); secret-read is a separate PreToolUse hook. All
380
388
  // PreToolUse guards can veto; the injection-scanner
381
389
  // is a PostToolUse advisory; check-update runs at SessionStart. Reused by the
@@ -388,7 +396,7 @@ function codexVerbSkillPlan(codexHome, verbs) {
388
396
  function codexHookEntries(hooksScriptsDir) {
389
397
  const cmd = (name) => `node "${path.join(hooksScriptsDir, name)}"`;
390
398
  return [
391
- { event: 'PreToolUse', matcher: 'Bash', command: `${cmd('trailhead-bash-guards.js')} --guards=commit,secret,install,body,labels` },
399
+ { event: 'PreToolUse', matcher: 'Bash', command: `${cmd('trailhead-bash-guards.js')} --guards=commit,secret,install,body,labels,test` },
392
400
  { event: 'PreToolUse', matcher: 'Read|Bash', command: cmd('trailhead-secret-read-guard.js') },
393
401
  { event: 'PostToolUse', matcher: 'Bash', command: cmd('trailhead-issue-injection-scanner.js') },
394
402
  { event: 'Stop', matcher: '', command: cmd('trailhead-mockup-link-stop.js') },
@@ -457,6 +465,7 @@ module.exports = {
457
465
  codexClusterAgentsYaml,
458
466
  codexAgentToml,
459
467
  codexAgentTomlPlan,
468
+ MODEL_KEYS,
460
469
  codexVerbSkillContent,
461
470
  codexVerbSkillAgentsYaml,
462
471
  codexVerbSkillPlan,
@@ -94,8 +94,22 @@ ok('codexSkillAdapterHeader §D no longer calls per-subagent model pinning defer
94
94
  !header.includes('deferred'));
95
95
  ok('codexSkillAdapterHeader §D pins the exact agent_type dispatch call shape',
96
96
  header.includes('spawn_agent(agent_type="trailhead-<technique>"'));
97
+ ok('codexSkillAdapterHeader §D passes fork_turns="none" (v2) and fork_context=false (v1) on every spawn',
98
+ header.includes('fork_turns="none"') && header.includes('fork_context=false'));
99
+ ok('codexSkillAdapterHeader §D pins the full v2 call shape with fork_turns="none"',
100
+ header.includes('spawn_agent(agent_type="trailhead-<technique>", fork_turns="none"'));
101
+ ok('codexSkillAdapterHeader §D keys the no-fork rule on the context parameter, not on agent_type',
102
+ header.includes('key it on the context parameter the schema shows, not on whether `agent_type` is present'));
103
+ ok('codexSkillAdapterHeader §D passes neither fork parameter when the schema exposes neither',
104
+ header.includes('exposes neither') && header.includes('pass neither'));
105
+ ok('codexSkillAdapterHeader §D says a fork_context-only schema takes fork_context=false in the agent_type call',
106
+ header.includes('v2 shape') && header.includes('takes `fork_context=false` instead'));
107
+ ok('codexSkillAdapterHeader §D no longer claims a subagent is unconditionally FRESH',
108
+ !header.includes('A subagent runs as a FRESH session'));
97
109
  ok('codexSkillAdapterHeader §D keeps the base spawn_agent v1 fallback path',
98
110
  header.includes('base multi_agent v1') && header.includes('inherit the one session model'));
111
+ ok('codexSkillAdapterHeader §D says all 9 committed agents, names trailhead-grill, and runs an unpinned grill inline',
112
+ header.includes('all 9 committed') && header.includes('trailhead-grill') && /unpinned or session-equal grill runs inline/.test(header));
99
113
  ok('codexSkillAdapterHeader §D warns against a single blocking wait_agent with a poll loop',
100
114
  header.includes('bounded wait') && header.includes('poll'));
101
115
  ok('codexSkillAdapterHeader §D offers a background-job fallback for a stalling review',
@@ -117,6 +131,10 @@ ok('codexSkillAdapterHeader §H makes backgrounding reviewers the default',
117
131
  header.includes('the default is to background the reviewers'));
118
132
  ok('codexSkillAdapterHeader §H closes reviewer stdin to avoid the EOF hang',
119
133
  header.includes('/dev/null') && header.toLowerCase().includes('stdin'));
134
+ ok('codexSkillAdapterHeader §H bounds every reviewer call through the run subcommand (#368)',
135
+ header.includes('trailhead-plan-review-rounds.js run --output') && header.includes('timeout_ms') && header.includes('above the helper') &&
136
+ header.includes('a `timeout` skips the `overflow` check') && header.includes('timeout -k 10 600') &&
137
+ header.includes('not evidence of a stdin hang') && !header.includes('0% CPU'));
120
138
 
121
139
  // --- codexVerbSkillContent (#46) ---
122
140
  ok('codexVerbSkillContent: SKILL.md frontmatter is at byte 0 with name+description', (() => {
@@ -173,10 +191,10 @@ const entries = codexHookEntries('/h');
173
191
  ok('codexHookEntries returns 5 entries', Array.isArray(entries) && entries.length === 5);
174
192
  const bashEntries = entries.filter((e) => e.event === 'PreToolUse' && e.matcher === 'Bash');
175
193
  ok('codexHookEntries: exactly one PreToolUse/Bash registration', bashEntries.length === 1);
176
- ok('codexHookEntries: the Bash entry is the merged hook with exactly --guards=commit,secret,install,body,labels',
177
- bashEntries[0].command === 'node "/h/trailhead-bash-guards.js" --guards=commit,secret,install,body,labels');
194
+ ok('codexHookEntries: the Bash entry is the merged hook with exactly --guards=commit,secret,install,body,labels,test',
195
+ bashEntries[0].command === 'node "/h/trailhead-bash-guards.js" --guards=commit,secret,install,body,labels,test');
178
196
  ok('codexHookEntries: no per-guard script is registered on its own', entries.every((e) =>
179
- !/trailhead-(commit|secret|install|search|body)-guard\.js/.test(e.command)));
197
+ !/trailhead-(commit|secret|install|search|body|test)-guard\.js/.test(e.command)));
180
198
  ok('codexHookEntries: secret-read-guard is PreToolUse/Read|Bash', entries.some((e) =>
181
199
  e.event === 'PreToolUse' && e.matcher === 'Read|Bash' && e.command.includes('trailhead-secret-read-guard.js')));
182
200
  ok('codexHookEntries: injection-scanner is PostToolUse/Bash', entries.some((e) =>
@@ -261,10 +279,10 @@ ok('codexAgentToml escapes a " and \\ in name/description so the TOML stays vali
261
279
  })());
262
280
 
263
281
  // --- codexAgentTomlPlan ---
264
- // 7-entry stub agentDefs mirroring the real plugins/trailhead/agents/*.md
265
- // sources: 6 keyed techniques (plan/execute/research/review/debug/codebase-map,
282
+ // 8-entry stub agentDefs mirroring the real plugins/trailhead/agents/*.md
283
+ // sources: 7 keyed techniques (plan/execute/research/review/debug/codebase-map/grill,
266
284
  // whose filenames derive from trailhead-executor -> execute and
267
- // trailhead-code-review -> review) + 1 always-keyless agent (fix).
285
+ // trailhead-code-review -> review) + fix, which tracks the execute pin.
268
286
  const STUB_AGENT_DEFS = [
269
287
  { name: 'trailhead-plan', description: 'plan desc', tools: 'Read', body: 'Plan body.' },
270
288
  { name: 'trailhead-executor', description: 'execute desc', tools: 'Read, Write', body: 'Execute body.' },
@@ -273,37 +291,76 @@ const STUB_AGENT_DEFS = [
273
291
  { name: 'trailhead-debug', description: 'debug desc', tools: 'Read', body: 'Debug body.' },
274
292
  { name: 'trailhead-fix', description: 'fix desc', tools: 'Read, Write', body: 'Fix body.' },
275
293
  { name: 'trailhead-codebase-map', description: 'map desc', tools: 'Read', body: 'Map body.' },
294
+ { name: 'trailhead-grill', description: 'grill desc', tools: 'Read', body: 'Grill body.' },
276
295
  ];
277
296
 
278
- ok('codexAgentTomlPlan: 7 agentDefs -> 7 writes with only the pinned key carrying a model', (() => {
297
+ ok('codexAgentTomlPlan: 8 agentDefs -> 8 writes with only execute (and fix, which tracks it) carrying a model', (() => {
279
298
  const plan = codexAgentTomlPlan('/c', { execute: 'gpt-5.6-terra' }, STUB_AGENT_DEFS);
280
- if (plan.writes.length !== 7) return false;
299
+ if (plan.writes.length !== 8) return false;
281
300
  const byName = Object.fromEntries(plan.writes.map((w) => [w.name, w]));
282
301
  const execute = byName['trailhead-execute'];
283
302
  if (!execute || execute.path !== '/c/agents/trailhead-execute.toml') return false;
284
303
  if (!execute.content.includes('model = "gpt-5.6-terra"')) return false;
285
- const others = plan.writes.filter((w) => w.name !== 'trailhead-execute');
304
+ const fix = byName['trailhead-fix'];
305
+ if (!fix || !fix.content.includes('model = "gpt-5.6-terra"')) return false;
306
+ const others = plan.writes.filter((w) => w.name !== 'trailhead-execute' && w.name !== 'trailhead-fix');
286
307
  return others.length === 6 && others.every((w) => !/^model = /m.test(w.content));
287
308
  })());
288
309
 
289
- ok('codexAgentTomlPlan: keyless trailhead-fix stays pin-less; trailhead-codebase-map now pins on codebase-map', (() => {
290
- const plan = codexAgentTomlPlan('/c', { fix: 'gpt-5.6-terra', 'codebase-map': 'gpt-5.6-terra' }, STUB_AGENT_DEFS);
310
+ ok('codexAgentTomlPlan: models.codex.grill pins trailhead-grill.toml; unset leaves it pin-less', (() => {
311
+ const pinned = codexAgentTomlPlan('/c', { grill: 'gpt-5.6-sol' }, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-grill');
312
+ const unset = codexAgentTomlPlan('/c', {}, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-grill');
313
+ return pinned && pinned.path === '/c/agents/trailhead-grill.toml' && pinned.content.includes('model = "gpt-5.6-sol"') &&
314
+ unset && !/^model = /m.test(unset.content);
315
+ })());
316
+
317
+ ok('codexAgentTomlPlan: an unset models.codex.grill follows models.codex.plan; a set grill wins', (() => {
318
+ const find = (m) => codexAgentTomlPlan('/c', m, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-grill');
319
+ const follow = find({ plan: { model: 'gpt-5.6-sol', effort: 'high' } });
320
+ const own = find({ plan: 'gpt-5.6-sol', grill: 'gpt-5.6-terra' });
321
+ const none = find({ execute: 'gpt-5.6-sol' });
322
+ const plan = codexAgentTomlPlan('/c', { plan: 'gpt-5.6-sol' }, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-plan');
323
+ return follow.content.includes('model = "gpt-5.6-sol"') && follow.content.includes('model_reasoning_effort = "high"') &&
324
+ own.content.includes('model = "gpt-5.6-terra"') && !/^model = /m.test(none.content) && plan.content.includes('model = "gpt-5.6-sol"');
325
+ })());
326
+
327
+ ok('codexAgentTomlPlan: a literal inherit session is pin-less for every key, and grill follows it', (() => {
328
+ const find = (m, n) => codexAgentTomlPlan('/c', m, STUB_AGENT_DEFS).writes.find((w) => w.name === n);
329
+ const pinned = (w) => /^model = /m.test(w.content);
330
+ const a = { plan: 'inherit session' };
331
+ const b = { plan: 'gpt-5.6-sol', grill: 'inherit session' };
332
+ const c = { execute: ' Inherit Session ', research: { model: 'inherit session', effort: 'high' } };
333
+ return !pinned(find(a, 'trailhead-plan')) && !pinned(find(a, 'trailhead-grill')) &&
334
+ !pinned(find(b, 'trailhead-grill')) && find(b, 'trailhead-plan').content.includes('model = "gpt-5.6-sol"') &&
335
+ !pinned(find(c, 'trailhead-execute')) && !pinned(find(c, 'trailhead-research')) &&
336
+ !find(c, 'trailhead-research').content.includes('model_reasoning_effort');
337
+ })());
338
+
339
+ ok('codexAgentTomlPlan: trailhead-fix carries the execute pin (not a fix key); trailhead-codebase-map pins on codebase-map', (() => {
340
+ const plan = codexAgentTomlPlan('/c', { execute: { model: 'gpt-5.6-sol', effort: 'high' }, fix: 'gpt-5.6-luna', 'codebase-map': 'gpt-5.6-terra' }, STUB_AGENT_DEFS);
291
341
  const byName = Object.fromEntries(plan.writes.map((w) => [w.name, w]));
292
342
  const fix = byName['trailhead-fix'];
293
343
  const map = byName['trailhead-codebase-map'];
294
344
  return fix && map &&
295
- fix.path === '/c/agents/trailhead-fix.toml' && !/^model = /m.test(fix.content) &&
345
+ fix.path === '/c/agents/trailhead-fix.toml' && fix.content.includes('model = "gpt-5.6-sol"') &&
346
+ fix.content.includes('model_reasoning_effort = "high"') && !fix.content.includes('gpt-5.6-luna') &&
296
347
  map.path === '/c/agents/trailhead-codebase-map.toml' && map.content.includes('model = "gpt-5.6-terra"');
297
348
  })());
298
349
 
299
- ok('codexAgentTomlPlan: empty models object -> 7 pin-less writes (not zero)', (() => {
350
+ ok('codexAgentTomlPlan: trailhead-fix is pin-less when execute is unset or inherit session', (() => {
351
+ const find = (m) => codexAgentTomlPlan('/c', m, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-fix');
352
+ return ![{}, { plan: 'gpt-5.6-sol' }, { execute: 'inherit session' }, { execute: { model: ' Inherit Session ', effort: 'high' } }]
353
+ .some((m) => /^model = /m.test(find(m).content) || find(m).content.includes('model_reasoning_effort'));
354
+ })());
355
+
356
+ ok('codexAgentTomlPlan: empty models object -> 8 pin-less writes (not zero)', (() => {
300
357
  const plan = codexAgentTomlPlan('/c', {}, STUB_AGENT_DEFS);
301
- return plan.writes.length === 7 && plan.writes.every((w) => !/^model = /m.test(w.content));
358
+ return plan.writes.length === 8 && plan.writes.every((w) => !/^model = /m.test(w.content));
302
359
  })());
303
360
 
304
- ok('codexAgentTomlPlan: null models -> 7 pin-less writes (not zero)', (() => {
361
+ ok('codexAgentTomlPlan: null models -> 8 pin-less writes (not zero)', (() => {
305
362
  const plan = codexAgentTomlPlan('/c', null, STUB_AGENT_DEFS);
306
- return plan.writes.length === 7 && plan.writes.every((w) => !/^model = /m.test(w.content));
363
+ return plan.writes.length === 8 && plan.writes.every((w) => !/^model = /m.test(w.content));
307
364
  })());
308
365
 
309
366
  ok('codexAgentTomlPlan: object model value on the execute key carries model + model_reasoning_effort', (() => {
@@ -315,7 +372,7 @@ ok('codexAgentTomlPlan: object model value on the execute key carries model + mo
315
372
  ok('codexAgentTomlPlan: a keyed key whose object value has no model stays pin-less', (() => {
316
373
  const plan = codexAgentTomlPlan('/c', { execute: { effort: 'high' } }, STUB_AGENT_DEFS);
317
374
  const execute = plan.writes.find((w) => w.name === 'trailhead-execute');
318
- return plan.writes.length === 7 && !/^model = /m.test(execute.content) && !execute.content.includes('model_reasoning_effort');
375
+ return plan.writes.length === 8 && !/^model = /m.test(execute.content) && !execute.content.includes('model_reasoning_effort');
319
376
  })());
320
377
 
321
378
  ok('codexAgentTomlPlan: description is routed through convertToCodex (host paths rewritten)', (() => {
@@ -470,16 +527,16 @@ ok('convertToCodex keeps CLAUDE_CODE_SESSION_ID intact',
470
527
  const shared = parseSource(fs.readFileSync(path.join(__dirname, '../../scripts/agent-blocks.md'), 'utf8'));
471
528
  const agentsDir = path.join(__dirname, '../../plugins/trailhead/agents');
472
529
  const files = fs.readdirSync(agentsDir).filter((f) => /^trailhead-.*\.md$/.test(f));
473
- ok('projection boundaries: found the eight real agents', files.length === 8);
530
+ ok('projection boundaries: found the nine real agents', files.length === 9);
474
531
  ok('projection boundaries: source has common and implementing blocks', !!shared.common && !!shared.implementing);
475
532
  // #249: every agent's projected TOML carries exactly its family block, whole.
476
533
  const familyOf = {
477
534
  'trailhead-verify': 'reviewing', 'trailhead-code-review': 'reviewing',
478
535
  'trailhead-executor': 'implementing', 'trailhead-fix': 'implementing',
479
536
  'trailhead-debug': 'investigating', 'trailhead-research': 'investigating', 'trailhead-codebase-map': 'investigating',
480
- 'trailhead-plan': 'plan',
537
+ 'trailhead-plan': 'plan', 'trailhead-grill': 'grilling',
481
538
  };
482
- const families = ['reviewing', 'implementing', 'investigating', 'plan'];
539
+ const families = ['reviewing', 'implementing', 'investigating', 'plan', 'grilling'];
483
540
  ok('projection boundaries: source has every family block', families.every((f) => !!shared[f]));
484
541
  for (const f of files) {
485
542
  const name = f.slice(0, -3);
@@ -196,7 +196,7 @@ function degradations(host) {
196
196
 
197
197
  if (descriptor.axes.subagentToolkit === 'none') {
198
198
  entries.push({
199
- from: 'subagent fan-out (research, codebase-map, review, plan, execute)',
199
+ from: 'subagent fan-out (research, codebase-map, review, plan, execute, grill)',
200
200
  to: 'inline sequential execution in the main session',
201
201
  });
202
202
  }
@@ -105,6 +105,12 @@ ok('modelsCollapseNotice(codex, models set) names the set keys', (() => {
105
105
  const msg = modelsCollapseNotice('codex', { models: { plan: 'x', execute: 'y' } });
106
106
  return typeof msg === 'string' && msg.includes('plan') && msg.includes('execute') && msg.includes('Codex');
107
107
  })());
108
+ ok('modelsCollapseNotice(codex, models.grill set) names grill', (() => {
109
+ const msg = modelsCollapseNotice('codex', { models: { grill: 'claude-opus-5-5' } });
110
+ return typeof msg === 'string' && msg.includes('(grill)') && msg.includes('models.codex');
111
+ })());
112
+ ok('modelsCollapseNotice is null once models.codex.grill is set',
113
+ modelsCollapseNotice('codex', { models: { grill: 'claude-opus-5-5', codex: { grill: 'gpt-5.6-sol' } } }) === null);
108
114
  ok('modelsCollapseNotice(codex, undefined config) is null',
109
115
  modelsCollapseNotice('codex') === null);
110
116
  ok('modelsCollapseNotice(claude, models set) is null (Claude honours the keys)',
@@ -30,10 +30,17 @@ ok('work claim step has the by-hand fallback', claimStep.includes('gh issue edit
30
30
  ok('a failed clear is reported, never blocking the claim (work)', /failed `clear` is reported to the user in one line but never blocks the claim/.test(claimStep));
31
31
  ok('a failed clear is reported, never blocking the claim (quick)', /failed `clear` is reported to the user in one line but never blocks the claim/.test(quickBullet));
32
32
 
33
- ok('substrate.md documents the label, off every run, on the human frontier', /\*\*`trailhead:needs-you`\*\*[\s\S]{0,900}not a frontier exclusion for humans/.test(substrate) && substrate.includes('twenty-one labels'));
33
+ ok('substrate.md documents the label, off every run, on the human frontier', /\*\*`trailhead:needs-you`\*\*[\s\S]{0,900}not a frontier exclusion for humans/.test(substrate) && substrate.includes('twenty-two labels'));
34
34
  ok('substrate-commands.md ensures the label idempotently', commands.includes('gh label create trailhead:needs-you --color D93F0B --description "Needs you: an autonomous run parked it for the human" --force'));
35
35
  ok('substrate-commands.md documents park and clear', commands.includes('trailhead-needs-you.js park --issue <n> --reason <text> --repo <owner/name>') && commands.includes('trailhead-needs-you.js clear --issue <n> --repo <owner/name>'));
36
- ok('substrate-commands.md first-use list has twenty-one labels', /all twenty-one:[^\n]*`trailhead:needs-you`/.test(commands));
36
+ ok('substrate-commands.md first-use list has twenty-two labels', /all twenty-two:[^\n]*`trailhead:needs-you`/.test(commands));
37
+
38
+ // Un ticket chiuso non tiene mai il mark, comunque sia stato chiuso.
39
+ const SWEEP = 'node <templates>/trailhead-needs-you.js sweep --repo <owner/name>';
40
+ ok('substrate-commands.md: Resolve clears needs-you right after the close', /gh issue close <n>\n[^\n]*\nnode <templates>\/trailhead-needs-you\.js clear --issue <n> --repo <owner\/name>/.test(commands));
41
+ ok('substrate-commands.md documents sweep', commands.includes(SWEEP) && /however it was closed/.test(commands));
42
+ ok('work SKILL.md sweeps at every work-start', work.includes(SWEEP) && /Sweep stale needs-you marks/.test(work));
43
+ ok('auto.md sweeps at run start, before the queue', auto.includes(SWEEP) && /before the queue is read/.test(auto));
37
44
 
38
45
  for (const [name, text] of [['auto.md', auto], ['SKILL.md', work], ['substrate.md', substrate], ['substrate-commands.md', commands]]) {
39
46
  ok(`${name}: no em-dash`, !text.includes(String.fromCharCode(0x2014)));
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #365: options posted on the ticket are the record, never the ask surface.
3
+ // The rule is stated once (Grilling technique); call-sites only point at it.
4
+ // Run: node options-in-chat-docs.test.js
5
+ const assert = require('assert');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const root = path.join(__dirname, '..', 'plugins', 'trailhead');
10
+ const skills = path.join(root, 'skills');
11
+ const read = (rel) => fs.readFileSync(path.join(skills, rel), 'utf8');
12
+
13
+ let passed = 0;
14
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
15
+
16
+ const ANCHOR = 'The ask is answerable from the chat alone';
17
+ const DEFINITION = `**${ANCHOR}.**`;
18
+
19
+ const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
20
+ e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]);
21
+
22
+ // 1. defined exactly once, in the Grilling technique
23
+ const defs = walk(root).filter((f) => fs.readFileSync(f, 'utf8').includes(DEFINITION));
24
+ ok('definition appears in exactly one file', defs.length === 1);
25
+ ok('definition lives in grilling.md', defs[0] === path.join(skills, '_shared', 'techniques', 'grilling.md'));
26
+ const grilling = read('_shared/techniques/grilling.md');
27
+ ok('definition appears once', grilling.split(DEFINITION).length === 2);
28
+
29
+ // 2. the definition carries the normative fragments
30
+ const rule = grilling.split('\n').find((l) => l.includes(DEFINITION));
31
+ for (const frag of [
32
+ 'an `OPTIONS` comment, or any comment',
33
+ 'the record, never the ask surface',
34
+ 'renders every option asked about in chat',
35
+ 'a one-line summary',
36
+ 'its trade-off',
37
+ 'the recommendation',
38
+ 'structured question tool',
39
+ 'with no option text',
40
+ ]) ok(`rule carries: ${frag}`, rule.includes(frag));
41
+
42
+ // 3. pointers name the rule without restating it
43
+ const pointer = (name, line) => {
44
+ ok(`${name} found`, !!line);
45
+ ok(`${name} references the anchor`, line.includes(ANCHOR));
46
+ ok(`${name} does not restate the rule`, !line.includes('never the ask surface'));
47
+ };
48
+ const engines = read('trailhead-work/references/ticket-engines.md');
49
+ pointer('decision Diverge', engines.split('\n').find((l) => l.startsWith('1. **Diverge')));
50
+ pointer('decision Converge', engines.split('\n').find((l) => l.startsWith('2. **Converge.**')));
51
+ const relay = grilling.split('\n').find((l) => l.startsWith(' - `question`: relay'));
52
+ pointer('grilling relay of question', relay);
53
+ ok('relay covers a question that only names the options', relay.includes('re-render'));
54
+ pointer('chart standalone grill', read('trailhead-chart/SKILL.md').split('\n').find((l) => l.startsWith('Run a standalone **Grilling**')));
55
+ const agent = fs.readFileSync(path.join(root, 'agents', 'trailhead-grill.md'), 'utf8');
56
+ const q = agent.split('\n').find((l) => l.startsWith('- `question:`'));
57
+ pointer('trailhead-grill question field', q);
58
+ ok('question field asks label, summary, trade-off', q.includes('label') && q.includes('summary') && q.includes('trade-off'));
59
+
60
+ // 4. no em-dash
61
+ for (const f of [path.join(skills, '_shared/techniques/grilling.md'), path.join(skills, 'trailhead-work/references/ticket-engines.md'),
62
+ path.join(skills, 'trailhead-chart/SKILL.md'), path.join(root, 'agents', 'trailhead-grill.md'), __filename])
63
+ ok(`${path.basename(f)} has no em-dash`, !fs.readFileSync(f, 'utf8').includes(String.fromCharCode(8212)));
64
+
65
+ console.log(`options-in-chat-docs.test.js: ${passed} checks passed`);
@@ -89,7 +89,7 @@ for (const frag of ['still current and complete', 'Decisions so far', 'status qu
89
89
  // 6. Inbox: nessuna chiusura o commento pubblico senza un si'
90
90
  const inbox = read('trailhead-chart/references/inbox.md');
91
91
  ok('inbox drops "on your own for clear ones"', !inbox.includes('on your own for clear ones'));
92
- const intro = lineOf(inbox, (l) => l.startsWith('For each issue in the first two sections'));
92
+ const intro = lineOf(inbox, (l) => l.startsWith('For each issue in the first three sections'));
93
93
  for (const frag of ['propose a disposition', 'one list', 'explicit yes', 'never close', 'comment on', 'out-of-scope ruling'])
94
94
  ok(`inbox intro carries: ${frag}`, intro.includes(frag));
95
95
  const dup = lineOf(inbox, (l) => l.startsWith('- **Duplicate.**'));
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #355: code review loads a CI/CD pipeline reliability reference
3
+ // on a pipeline-touching diff only, every finding capped at Warning, and the
4
+ // review brief, the techniques table and the agent all point at it.
5
+ // Run: node pipeline-review-docs.test.js
6
+ const assert = require('assert');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const root = path.join(__dirname, '..');
11
+ const skills = path.join(root, 'plugins', 'trailhead', 'skills');
12
+ const agents = path.join(root, 'plugins', 'trailhead', 'agents');
13
+ const read = (...p) => fs.readFileSync(path.join(...p), 'utf8');
14
+
15
+ let passed = 0;
16
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
17
+
18
+ const refPath = path.join(skills, '_shared', 'techniques', 'code-review-pipeline.md');
19
+ ok('reference file exists', fs.existsSync(refPath));
20
+ const ref = read(refPath);
21
+ const review = read(skills, '_shared', 'techniques', 'code-review.md');
22
+ const techniques = read(skills, '_shared', 'techniques.md');
23
+ const agent = read(agents, 'trailhead-code-review.md');
24
+
25
+ const route = review.split('\n').find((l) => l.startsWith('**First, route it.**')) || '';
26
+ const para = review.split('\n').find((l) => l.startsWith('**Pipeline reliability (only on a pipeline-touching diff)**')) || '';
27
+ const lines = review.split('\n');
28
+ const classifyIdx = lines.findIndex((l) => l.startsWith('Classify each finding'));
29
+ const paraIdx = lines.findIndex((l) => l.startsWith('**Pipeline reliability (only on a pipeline-touching diff)**'));
30
+ const guardIdx = lines.findIndex((l) => l.startsWith('- **Impossible-state guards**'));
31
+
32
+ // The reference: the eight checks.
33
+ for (const sub of [
34
+ '`npm install` where a lockfile exists (use `npm ci` instead)',
35
+ 'a job without `timeout-minutes`',
36
+ 'a deploy without a `concurrency` group',
37
+ 'a rebuild per environment instead of build once, promote',
38
+ 'applies only when the workflow targets more than one environment',
39
+ 'a deploy with neither a health check nor a rollback',
40
+ '`continue-on-error` on a step that later steps depend on',
41
+ 'static cloud keys where the provider supports OIDC',
42
+ 'a third-party action on `@main` or `@latest`',
43
+ ]) {
44
+ ok(`reference check: ${sub}`, ref.includes(sub));
45
+ }
46
+
47
+ ok('reference: every finding is a Warning, never Critical',
48
+ ref.includes('Every finding is a Warning, never Critical, so it never enters the Critical-only fix-and-re-review loop'));
49
+
50
+ for (const sub of [
51
+ 'global tool installs (`npm install -g`)',
52
+ 'reusable workflows (a job calling one takes neither `timeout-minutes` nor steps of its own)',
53
+ 'actions already pinned to a tag',
54
+ 'test-only workflows (a workflow that runs tests and publishes or deploys nothing: none of the checks apply)',
55
+ ]) {
56
+ ok(`reference non-finding: ${sub}`, ref.includes(sub));
57
+ }
58
+
59
+ ok('reference: hardcoded secrets stay Critical under Security',
60
+ ref.includes('Hardcoded secrets are not in this reference: they stay Critical under the Security axis'));
61
+ ok('reference: a deploy finding names what makes it a deploy one',
62
+ ref.includes('A deploy finding names what makes the workflow a deploy one (a quoted command, an `environment:`, a target)'));
63
+ ok('reference: scope is the ticket diff',
64
+ ref.includes("only jobs and steps the ticket's commits add or change, per the Ticket diff rule"));
65
+ ok('reference: not a sixth axis', ref.includes('not a sixth axis'));
66
+
67
+ // The brief: route paragraph.
68
+ ok('route: first-line route structure kept', route.includes('**First, route it.** Run `node <hooks>/lib/model-route.js review'));
69
+ ok('route: spawn prompt carries the pipeline reference line', route.includes('`pipeline reference: <absolute path>`'));
70
+ ok('route: path is code-review-pipeline.md beside the code-review.md just read',
71
+ route.includes('`code-review-pipeline.md` in the same directory as the `code-review.md` you just read by absolute path'));
72
+ ok('route: path only, never the contents', route.includes('never the reference\'s contents'));
73
+
74
+ // The brief: the new paragraph.
75
+ ok('five axes wording kept', review.includes('Review on five axes:'));
76
+ ok('paragraph exists', para.length > 0);
77
+ ok('paragraph sits after the axes and before Classify', guardIdx >= 0 && paraIdx > guardIdx && paraIdx < classifyIdx);
78
+ for (const sub of [
79
+ 'git -C <repo> show --format= --name-only <sha>',
80
+ '`.github/workflows/*`',
81
+ '`Dockerfile*`',
82
+ '`docker-compose*`',
83
+ '(basename, at any depth)',
84
+ 'a script with `deploy` in its file name',
85
+ 'a release script under another name is not matched',
86
+ 'on every pass including the lean inline one',
87
+ 'otherwise do not read it',
88
+ 'Inline, it sits beside this file',
89
+ 'the review is incomplete and fails closed',
90
+ 'a Critical finding `pipeline checks did not run` naming the matched path',
91
+ 'status is `issues_found` and Resolve stays blocked',
92
+ 'discards that incomplete report',
93
+ 'never enters the ledger',
94
+ 're-spawns with the path',
95
+ 'HITL checkpoint naming the path',
96
+ 'It is not a sixth axis',
97
+ ]) {
98
+ ok(`paragraph: ${sub}`, para.includes(sub));
99
+ }
100
+
101
+ // Pointers.
102
+ ok('techniques.md: Code review row on 5 axes kept', techniques.includes("review the ticket's own commits (its `Refs: #<n>` set) on 5 axes"));
103
+ ok('techniques.md: row names the conditional reference', techniques.includes('code-review-pipeline.md'));
104
+ ok('agent: names the reference at the path the spawn names',
105
+ agent.includes('`code-review-pipeline.md`') && agent.includes('the path the spawn names'));
106
+ ok('agent: injected single source kept', agent.includes('injected at spawn from the trailhead skill\'s single source (`_shared/techniques/code-review.md`)'));
107
+
108
+ for (const [name, text] of Object.entries({
109
+ 'code-review-pipeline.md': ref,
110
+ 'code-review.md': review,
111
+ 'techniques.md': techniques,
112
+ 'trailhead-code-review.md': agent,
113
+ 'pipeline-review-docs.test.js': read(__dirname, 'pipeline-review-docs.test.js'),
114
+ })) {
115
+ ok(`${name} has no em-dash`, !text.includes(String.fromCharCode(0x2014)));
116
+ }
117
+
118
+ console.log(`pipeline-review-docs.test.js: ${passed} checks passed`);