@marcomigozzi/trailhead 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.it.md +67 -18
  2. package/README.md +67 -18
  3. package/bin/capture-action.test.js +167 -0
  4. package/bin/capture-docs.test.js +32 -0
  5. package/bin/config-dir-templates-docs.test.js +52 -0
  6. package/bin/decision-builds-docs.test.js +75 -0
  7. package/bin/engine-route-docs.test.js +148 -0
  8. package/bin/fog-vs-ticket-docs.test.js +56 -0
  9. package/bin/grill-model-docs.test.js +155 -0
  10. package/bin/grilling-holistic-review-docs.test.js +7 -3
  11. package/bin/lib/codex-projection.js +30 -14
  12. package/bin/lib/codex-projection.test.js +81 -17
  13. package/bin/lib/host-descriptor.js +15 -2
  14. package/bin/lib/host-descriptor.test.js +24 -0
  15. package/bin/lib/runtime-command-literals.test.js +184 -0
  16. package/bin/report-status-docs.test.js +11 -3
  17. package/bin/session-trust-docs.test.js +37 -0
  18. package/bin/surfaced-scope-docs.test.js +57 -0
  19. package/bin/trailhead.js +7 -5
  20. package/bin/trailhead.test.js +53 -11
  21. package/bin/unblock-docs.test.js +56 -0
  22. package/package.json +1 -1
  23. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  24. package/plugins/trailhead/agents/trailhead-code-review.md +2 -2
  25. package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
  26. package/plugins/trailhead/agents/trailhead-grill.md +41 -0
  27. package/plugins/trailhead/agents/trailhead-verify.md +2 -2
  28. package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
  29. package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
  30. package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
  31. package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
  32. package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
  33. package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
  34. package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
  35. package/plugins/trailhead/hooks/lib/issue-body.test.js +84 -0
  36. package/plugins/trailhead/hooks/lib/model-defaults-review.js +72 -22
  37. package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +113 -0
  38. package/plugins/trailhead/hooks/lib/model-route.js +207 -0
  39. package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
  40. package/plugins/trailhead/hooks/lib/report-status.test.js +124 -2
  41. package/plugins/trailhead/hooks/lib/shell-scan.js +25 -0
  42. package/plugins/trailhead/hooks/lib/statusline-context.test.js +22 -1
  43. package/plugins/trailhead/hooks/lib/unmodelled.js +6 -1
  44. package/plugins/trailhead/hooks/mod/README.md +24 -10
  45. package/plugins/trailhead/hooks/mod/map-pane.tsx +124 -44
  46. package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
  47. package/plugins/trailhead/hooks/mod/repo-pin.ts +8 -5
  48. package/plugins/trailhead/hooks/trailhead-check-update.js +17 -1
  49. package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
  50. package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +40 -1
  51. package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +68 -0
  52. package/plugins/trailhead/skills/_shared/capture-core.md +118 -0
  53. package/plugins/trailhead/skills/_shared/choices.md +2 -2
  54. package/plugins/trailhead/skills/_shared/configuration-reference.md +20 -18
  55. package/plugins/trailhead/skills/_shared/configuration.md +1 -1
  56. package/plugins/trailhead/skills/_shared/session-handoff.md +2 -2
  57. package/plugins/trailhead/skills/_shared/substrate-commands.md +8 -4
  58. package/plugins/trailhead/skills/_shared/substrate.md +1 -1
  59. package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
  60. package/plugins/trailhead/skills/_shared/teamwork.md +9 -5
  61. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
  62. package/plugins/trailhead/skills/_shared/techniques/code-review.md +5 -2
  63. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +4 -1
  64. package/plugins/trailhead/skills/_shared/techniques/debug.md +4 -1
  65. package/plugins/trailhead/skills/_shared/techniques/grilling.md +26 -1
  66. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
  67. package/plugins/trailhead/skills/_shared/techniques/verify.md +4 -2
  68. package/plugins/trailhead/skills/_shared/techniques.md +7 -5
  69. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
  70. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
  71. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
  72. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
  73. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +3 -3
  74. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +5 -4
  75. package/plugins/trailhead/skills/trailhead-work/SKILL.md +13 -11
  76. package/plugins/trailhead/skills/trailhead-work/references/auto.md +1 -1
  77. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +11 -10
  78. package/plugins/trailhead/templates/statusline-trailhead.sh +3 -3
  79. package/plugins/trailhead/templates/trailhead-capture.js +418 -0
  80. package/plugins/trailhead/templates/trailhead-checkout-find.js +256 -0
  81. package/plugins/trailhead/templates/trailhead-checkout-in.js +76 -20
  82. package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
  83. package/plugins/trailhead/templates/trailhead-issue-body.js +69 -6
  84. package/plugins/trailhead/templates/trailhead-report-status.js +123 -11
  85. package/plugins/trailhead/tests/map-pane-ui.test.ts +299 -37
  86. package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
  87. package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
  88. package/plugins/trailhead/types/index.d.ts +7 -0
@@ -97,14 +97,18 @@ The map issue's **body** is a shared mutable document: two sessions appending to
97
97
  ## Trust & provenance
98
98
  `trailhead:*` labels drive the map, so they must not be forgeable by outsiders. GitHub already blocks users without write access from applying labels at all; this layer defends against the rest (triage-level collaborators, automations, or a repo that later loosens permissions).
99
99
 
100
- **Trust rule.** Treat a `trailhead:map` / `trailhead:ticket` issue as genuine **only if** its `trailhead:*` label was applied by a repo collaborator with **write access or above**, *and* (for a ticket) it carries a valid `Parent:` line pointing at this map. To check the labeller, read the issue timeline (`gh api repos/{owner}/{repo}/issues/{n}/timeline` gives `labeled` events with their `actor`) and confirm the actor's permission with `gh api repos/{owner}/{repo}/collaborators/{actor}/permission` (`admin`/`write`/`maintain` = trusted; `read`/`none` = not).
100
+ **Trust rule.** Treat a `trailhead:map` / `trailhead:ticket` issue as genuine **only if** every `trailhead:*` label on it was applied by a trusted labeller, *and* (for a ticket) it carries a valid `Parent:` line pointing at this map. A labeller is trusted when it is (a) `github-actions[bot]` (its `GITHUB_TOKEN` events trigger no workflow, so the label guard never runs on them), (b) a login in the `TRAILHEAD_LABEL_ALLOWLIST` repo variable, or (c) a collaborator with write access or above. These are the same labellers the label guard never strips, so session and guard agree on who is trusted (the one difference: a failed lookup, which the guard strips, is held here, see **Unresolved provenance**).
101
101
 
102
- **On an untrusted match** (a `trailhead:*` issue whose label came from a non-write actor, or a ticket with no valid `Parent:`) do **not** put it on the frontier or act on it. Label it **`trailhead:unverified`**, and surface it to the user as a quarantined item to **adopt** (a maintainer re-applies the real labels + wires `Parent:`) or **reject** (strip the `trailhead:*` labels, leave it a normal issue). Never silently trust a label just because it's present.
102
+ **Provenance per label.** Read the issue timeline (`gh api repos/{owner}/{repo}/issues/{n}/timeline` gives `labeled` events with their `actor`). For every `trailhead:*` label currently on the issue, take the `actor` of its latest `labeled` event; the issue is genuine only when all of those actors are trusted (an earlier rogue event on a since-reapplied label does not count, and one trusted label does not vouch for another). Check an actor in this order: the bot, then the allowlist, then the permission call `gh api repos/{owner}/{repo}/collaborators/{actor}/permission` (`admin`/`write`/`maintain` = trusted; `read`/`none` = not trusted; a `404` or a `none`/`read` answer is a real answer, not a failure). Read the allowlist once per session with `gh variable get TRAILHEAD_LABEL_ALLOWLIST --repo <owner/name>`: `not found` (absent) means empty; any other failure (permission, network) means the allowlist is unknown, not empty. It is comma-separated, entries trimmed, and logins compared case-insensitively, as the guard does.
103
103
 
104
- **Repo-side enforcement (part of first-use setup, not an afterthought).** trailhead **installs the label guard for you**; don't tell the user to copy files. **Every chart or adopt, *check* whether `.github/workflows/trailhead-label-guard.yml` exists** (the first-use repo setup, in SKILL.md's Substrate section); if it's absent, install it, by default on a repo with other collaborators, and as a quick offer on a clearly solo/private one (cheap insurance), but **never silently skip the check**: that's exactly how a repo ends up with trailhead labels and no guard. On go-ahead, write the shipped label-guard template (`${CLAUDE_PLUGIN_ROOT}/templates/trailhead-label-guard.yml` for a plugin install, or `~/.claude/trailhead/templates/trailhead-label-guard.yml` for an npm install, take whichever exists) to the repo's `.github/workflows/trailhead-label-guard.yml` and commit it (conventional message; the commit-guard hook keeps it clean). The Action strips any `trailhead:*` label added by a non-write actor and comments why; set an optional `TRAILHEAD_LABEL_ALLOWLIST` repo variable to whitelist extra logins (e.g. a bot trailhead runs as). **Caveat:** pushing a workflow file needs a token with the `workflow` scope; if the push is rejected, run `gh auth refresh -s workflow` and retry (or, as a last resort, tell the user to add the file by hand).
104
+ **Unresolved provenance.** With an unknown allowlist, an actor that is neither the bot nor write+ cannot be judged; the same holds for an actor whose permission lookup failed. Such an issue is held off the frontier and surfaced to the user as "provenance not checkable"; never label it `trailhead:unverified` on a guess.
105
+
106
+ **On an untrusted match** (a `trailhead:*` issue whose label came from a labeller outside the trusted set, or a ticket with no valid `Parent:`) do **not** put it on the frontier or act on it. Label it **`trailhead:unverified`**, and surface it to the user as a quarantined item to **adopt** (a maintainer re-applies the real labels + wires `Parent:`) or **reject** (strip the `trailhead:*` labels, leave it a normal issue). Never silently trust a label just because it's present.
107
+
108
+ **Repo-side enforcement (part of first-use setup, not an afterthought).** trailhead **installs the label guard for you**; don't tell the user to copy files. **Every chart or adopt, *check* whether `.github/workflows/trailhead-label-guard.yml` exists** (the first-use repo setup, in SKILL.md's Substrate section); if it's absent, install it, by default on a repo with other collaborators, and as a quick offer on a clearly solo/private one (cheap insurance), but **never silently skip the check**: that's exactly how a repo ends up with trailhead labels and no guard. On go-ahead, write the shipped label-guard template (`${CLAUDE_PLUGIN_ROOT}/templates/trailhead-label-guard.yml` for a plugin install, or `${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/trailhead-label-guard.yml` for an npm install, take whichever exists) to the repo's `.github/workflows/trailhead-label-guard.yml` and commit it (conventional message; the commit-guard hook keeps it clean). The Action strips any `trailhead:*` label added by a non-write actor and comments why; set an optional `TRAILHEAD_LABEL_ALLOWLIST` repo variable to whitelist extra logins (e.g. a bot trailhead runs as). **Caveat:** pushing a workflow file needs a token with the `workflow` scope; if the push is rejected, run `gh auth refresh -s workflow` and retry (or, as a last resort, tell the user to add the file by hand).
105
109
 
106
110
  **Keeping the guard current (migration).** The guard filters at **job level** (`jobs.guard.if`: a `trailhead:` label, a sender other than the repo owner, not in `TRAILHEAD_LABEL_ALLOWLIST`), because a skipped job is not billed while a job that starts and returns early from its script costs at least one Actions minute; the in-script permission check stays the real defence. Guards installed before 0.12.1 lack that `if:` and start a runner on **every** label event, which with trailhead labelling all the time runs into thousands of billed runs a month. So whenever the guard is present (every chart/adopt, and after `/trailhead:update`), run `node <templates>/trailhead-label-guard-sync.js --repo <working-root>` (same `<templates>` lookup as the commit-msg sync script below). It prints one JSON status line: `current` (nothing to do), `outdated` (offer the upgrade and always explain why in the question, in plain words: today every label added to an issue starts a run billed at least one minute even when it does nothing, trailhead labels constantly so that adds up to thousands of billed runs a month and can exhaust the Actions minutes, and the updated guard skips irrelevant events before a runner starts with the same protection), `custom` (has the job-level filter but differs from the template: leave it, mention it only if asked), `foreign` (not a trailhead workflow: never touched), `absent` (install as above). On go-ahead for `outdated`, re-run it with `--apply` (it rewrites the file from the template in the working tree only, never a foreign or customised guard), then commit it (`fix: skip trailhead label guard job for irrelevant label events`) and push, with the same `workflow`-scope caveat. The SessionStart hook (or, on a host with no hook bus, the load-first check) also runs it with `--notice` at every session start and surfaces the same upgrade offer until the user answers it; `--ack` records the answer in the gitignored `.trailhead/label-guard-ack` (the guard's hash), so a declined offer comes back only if the file changes. If the script is missing (an outdated install), fall back to reading the file and checking `jobs.guard` for an `if:` containing `startsWith(github.event.label.name, 'trailhead:')`.
107
111
 
108
- **The commit-msg git hook (host-independent commit guard).** On every host trailhead ALSO installs a git **`commit-msg`** hook as a host-independent backstop (a small `sh` wrapper, with its logic installed beside it as `trailhead-commit-msg.cjs`: a `.cjs` is always CommonJS, so a repo whose `package.json` says `"type": "module"` cannot break it; the wrapper never blocks a commit on a startup failure such as node or the `.cjs` missing, and then prints a one-line `trailhead: commit-msg hook inactive (<reason>)` notice to stderr) (defence in depth alongside the host's own `PreToolUse` guard on Claude Code and Codex alike), enforcing the two rules (a Conventional Commits subject of at most 72 chars; never a `Co-Authored-By` trailer) plus, while a `.trailhead/session-ticket` marker is present at the worktree root, a `Refs: #<n>` trailer matching its number, on every `git commit`, whatever host drives it (decision #16). **Every chart or adopt, and at every `work`/`quick` ticket start, run the sync script**: `node <templates>/trailhead-commit-msg-sync.js --repo <working-root>`, where `<templates>` is the first of `${CLAUDE_PLUGIN_ROOT}/templates/`, `~/.claude/trailhead/templates/`, `~/.codex/skills/trailhead/templates/` that contains `trailhead-commit-msg-sync.js`: try all three in order, skipping the first when `CLAUDE_PLUGIN_ROOT` is unset or empty (it is often absent from the Bash environment), and never stop at the first miss. It prints one JSON status line and handles every case itself: absent -> installs it; an older trailhead hook (the legacy single-file JS hook, or an outdated wrapper or logic file; recognised by its own header line, not by trusting the filename) -> upgraded in place to the current layout; already current -> left alone; a foreign (non-trailhead) hook -> never touched, only reported (offer to chain it); under a repo's `core.hooksPath`, an existing trailhead hook there is still upgraded in place, but nothing is newly added to that directory. Unlike the label guard, `.git/hooks/` is not version-controlled, so this is a per-clone local install with nothing to commit. **If the sync script is missing (none of the three dirs has it: an outdated install) or node errors, never claim git enforces nothing before looking**: resolve the effective hooks dir with `git -C <working-root> rev-parse --git-path hooks` (resolve a relative output against the working root; it honours `core.hooksPath` and the git common dir shared by linked worktrees), read `<hooks dir>/commit-msg`, and classify it by its header line (a trailhead hook has `# trailhead commit-msg hook (git).` (the `sh` wrapper) or, for the legacy single-file JS hook, `// trailhead commit-msg hook (git).` among its first 5 lines, trimmed; never trust the filename alone; a symlinked `commit-msg` counts as foreign, as the script treats it). The wrapper also needs `<hooks dir>/trailhead-commit-msg.cjs` beside it: without it the wrapper lets every commit through, so treat a missing `.cjs` (or a legacy JS hook, which breaks in `"type": "module"` repos) as present-but-stale: suggest updating the install so the sync script can repair it. Report one result-oriented line: (a) trailhead hook present and executable (`test -x`): git does enforce trailhead's rules here (provided node is on the PATH git runs with; a GUI git client may lack it, and then the wrapper prints the inactive notice on each commit), but its freshness could not be checked or upgraded, so suggest updating the trailhead install; a trailhead hook that is NOT executable: git skips it, so it does not enforce trailhead's rules here (suggest updating the install so the sync script can fix the mode); (b) a foreign hook (or a symlink): left untouched, git does not enforce trailhead's rules here; (c) no hook: git does not enforce trailhead's rules here (suggest updating the install so the sync script can install it). Never blocks the work.
112
+ **The commit-msg git hook (host-independent commit guard).** On every host trailhead ALSO installs a git **`commit-msg`** hook as a host-independent backstop (a small `sh` wrapper, with its logic installed beside it as `trailhead-commit-msg.cjs`: a `.cjs` is always CommonJS, so a repo whose `package.json` says `"type": "module"` cannot break it; the wrapper never blocks a commit on a startup failure such as node or the `.cjs` missing, and then prints a one-line `trailhead: commit-msg hook inactive (<reason>)` notice to stderr) (defence in depth alongside the host's own `PreToolUse` guard on Claude Code and Codex alike), enforcing the two rules (a Conventional Commits subject of at most 72 chars; never a `Co-Authored-By` trailer) plus, while a `.trailhead/session-ticket` marker is present at the worktree root, a `Refs: #<n>` trailer matching its number, on every `git commit`, whatever host drives it (decision #16). **Every chart or adopt, and at every `work`/`quick` ticket start, run the sync script**: `node <templates>/trailhead-commit-msg-sync.js --repo <working-root>`, where `<templates>` is the first of `${CLAUDE_PLUGIN_ROOT}/templates/`, `${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/`, `~/.codex/skills/trailhead/templates/` that contains `trailhead-commit-msg-sync.js`: try all three in order, skipping the first when `CLAUDE_PLUGIN_ROOT` is unset or empty (it is often absent from the Bash environment), and never stop at the first miss. It prints one JSON status line and handles every case itself: absent -> installs it; an older trailhead hook (the legacy single-file JS hook, or an outdated wrapper or logic file; recognised by its own header line, not by trusting the filename) -> upgraded in place to the current layout; already current -> left alone; a foreign (non-trailhead) hook -> never touched, only reported (offer to chain it); under a repo's `core.hooksPath`, an existing trailhead hook there is still upgraded in place, but nothing is newly added to that directory. Unlike the label guard, `.git/hooks/` is not version-controlled, so this is a per-clone local install with nothing to commit. **If the sync script is missing (none of the three dirs has it: an outdated install) or node errors, never claim git enforces nothing before looking**: resolve the effective hooks dir with `git -C <working-root> rev-parse --git-path hooks` (resolve a relative output against the working root; it honours `core.hooksPath` and the git common dir shared by linked worktrees), read `<hooks dir>/commit-msg`, and classify it by its header line (a trailhead hook has `# trailhead commit-msg hook (git).` (the `sh` wrapper) or, for the legacy single-file JS hook, `// trailhead commit-msg hook (git).` among its first 5 lines, trimmed; never trust the filename alone; a symlinked `commit-msg` counts as foreign, as the script treats it). The wrapper also needs `<hooks dir>/trailhead-commit-msg.cjs` beside it: without it the wrapper lets every commit through, so treat a missing `.cjs` (or a legacy JS hook, which breaks in `"type": "module"` repos) as present-but-stale: suggest updating the install so the sync script can repair it. Report one result-oriented line: (a) trailhead hook present and executable (`test -x`): git does enforce trailhead's rules here (provided node is on the PATH git runs with; a GUI git client may lack it, and then the wrapper prints the inactive notice on each commit), but its freshness could not be checked or upgraded, so suggest updating the trailhead install; a trailhead hook that is NOT executable: git skips it, so it does not enforce trailhead's rules here (suggest updating the install so the sync script can fix the mode); (b) a foreign hook (or a symlink): left untouched, git does not enforce trailhead's rules here; (c) no hook: git does not enforce trailhead's rules here (suggest updating the install so the sync script can install it). Never blocks the work.
109
113
 
110
- **Work state file (the ticket in play, for tooling).** Alongside the session-ticket marker, trailhead keeps a gitignored `.trailhead/work-state.json` at the working root recording the one ticket in play: `{map, ticket, title, step, auto, autoDone, session, updatedAt}` (`autoDone` is the number of tickets resolved so far in an autonomous run, `0` otherwise). It is independent of `session-ticket` and of the commit-msg hook, and written the same way on Claude Code and Codex by the template helper `trailhead-work-state.js`, found with the same three-dir `<templates>` lookup as the commit-msg sync above (the first of `${CLAUDE_PLUGIN_ROOT}/templates/`, `~/.claude/trailhead/templates/`, `~/.codex/skills/trailhead/templates/` that contains it). Forms: `node <templates>/trailhead-work-state.js set --repo <working-root> --map <n|none> --ticket <n> --title "<title>" --step discuss` at work-start (`--map none` for a whiteboard ticket), `node <templates>/trailhead-work-state.js set --repo <working-root> --step <step>` at each step change (`discuss`, `plan`, `execute`, `verify`, `resolve`, `paused`; it merges onto the file, and a different `--ticket` starts it fresh; it needs a ticket on file, so where the file may be absent, as at resume or a pause in a fresh workspace, pass the full work-start form with that step), and `node <templates>/trailhead-work-state.js clear --repo <working-root>` at Resolve, which removes both the work state file and the `.trailhead/session-ticket` marker in one call (each removal attempted independently; it prints `sessionTicket: removed|absent|skipped` next to the usual status). During an autonomous run every `set` adds `--auto true --auto-done <n>`, `n` being the tickets resolved so far in this run. The step-to-state mapping per ticket type lives in `trailhead-work/references/ticket-engines.md`. A missing helper or a non-zero exit is silent and never blocks the work (no user-facing line).
114
+ **Work state file (the ticket in play, for tooling).** Alongside the session-ticket marker, trailhead keeps a gitignored `.trailhead/work-state.json` at the working root recording the one ticket in play: `{map, ticket, title, step, auto, autoDone, session, updatedAt}` (`autoDone` is the number of tickets resolved so far in an autonomous run, `0` otherwise). It is independent of `session-ticket` and of the commit-msg hook, and written the same way on Claude Code and Codex by the template helper `trailhead-work-state.js`, found with the same three-dir `<templates>` lookup as the commit-msg sync above (the first of `${CLAUDE_PLUGIN_ROOT}/templates/`, `${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/`, `~/.codex/skills/trailhead/templates/` that contains it). Forms: `node <templates>/trailhead-work-state.js set --repo <working-root> --map <n|none> --ticket <n> --title "<title>" --step discuss` at work-start (`--map none` for a whiteboard ticket), `node <templates>/trailhead-work-state.js set --repo <working-root> --step <step>` at each step change (`discuss`, `plan`, `execute`, `verify`, `resolve`, `paused`; it merges onto the file, and a different `--ticket` starts it fresh; it needs a ticket on file, so where the file may be absent, as at resume or a pause in a fresh workspace, pass the full work-start form with that step), and `node <templates>/trailhead-work-state.js clear --repo <working-root>` at Resolve, which removes both the work state file and the `.trailhead/session-ticket` marker in one call (each removal attempted independently; it prints `sessionTicket: removed|absent|skipped` next to the usual status). During an autonomous run every `set` adds `--auto true --auto-done <n>`, `n` being the tickets resolved so far in this run. The step-to-state mapping per ticket type lives in `trailhead-work/references/ticket-engines.md`. A missing helper or a non-zero exit is silent and never blocks the work (no user-facing line).
@@ -20,7 +20,7 @@ Automated tests prove the code does what the *code* intended; acceptance testing
20
20
  - **AI-driven, browser-drivable web app → drive it yourself.** Governed by `config.acceptance.browser` + `config.testing`: when it's `auto`/`on` and `testing.webapp` is set with `testing.url` set or resolvable (see below), use the browser-automation tools to exercise the actual change end-to-end (navigate, fill, click, assert visible state; capture a screenshot as evidence for part 3 of the `VERIFY` comment). Prefer this over asking the human when the flow is scriptable. Never trigger native/JS dialogs that would wedge the session.
21
21
  - **Auto mode resolves the URL before falling back.** Under `acceptance.browser: auto`/`on`, when `testing.webapp` is set but `testing.url` is not, do not drop straight to asking: first try to determine the target URL from the project. Probe, cheapest first, a dev server already running (the likely local ports, or a server log / PID the project writes), package scripts (`dev`/`start`/`preview` in `package.json` or the project's task runner), dev-server and framework config (the configured host/port in e.g. Vite/Next/Astro/Expo config), and project config. When one is found, **propose it** ("acceptance will run against `http://localhost:5173`, correct?") for the user to accept or correct, then drive the browser against the accepted URL. Only when no URL can be determined fall back to asking the human, or to the guided UAT below.
22
22
  - **Human-driven, or an AI-driven step that isn't performable → guided UAT is MANDATORY, and it is CONVERSATIONAL.** This is the human-driven mode from the drive-mode ask above, and it is also what an AI-driven run falls back to for the specific step(s) it cannot perform: when `config.acceptance.browser` is `off`, or no target URL could be determined (the auto attempt above included), or the target can't be driven this session (a device or hardware the session cannot reach, real credentials it does not hold; a mobile/native app or a TV on a connected device or emulator *is* drivable, per the AI-driven rule above), **`off` means "acceptance via a guided walkthrough," not "skip acceptance."** For any user-facing change you MUST **run the human through the UAT one step at a time, in the chat**: state step 1 (what to do, where to tap/look, the expected result), then **stop and wait** for the human to report what they actually saw; only then move to step 2. It is a dialogue *you* drive, turn by turn, not a list you hand over. **Anti-pattern, never do this:** post the full numbered checklist and point the user at the ticket to self-serve ("follow the 9 steps in the comment of #NN"). That is exactly the failure this rule kills: the checklist is the *record*, never the *interaction*. Mirror each step's outcome into the `VERIFY` comment as you go; don't close on green you didn't actually see.
23
- - **On a failing step: stop, take note, open the closing ticket then and there.** A failed step is an acceptance finding, not a footnote: record expected-vs-seen, and immediately open the follow-up that will carry it to resolution: a `bug` ticket (with `Regression of:`/`Surfaced from:` as fits), or a fix on this ticket before it resolves if it's small and in-scope. Then ask the human whether to keep going through the remaining steps or stop here. Never silently log a fail and carry on to Resolve.
23
+ - **On a failing step: stop, take note, open the closing ticket then and there.** A failed step is an acceptance finding, not a footnote: record expected-vs-seen, and immediately open the follow-up that will carry it to resolution: a `bug` ticket (with `Regression of:`/`Surfaced from:` as fits, landed and announced per `../surfaced-scope.md`: the current map when the topic is its own, an ask when unsure, never the whiteboard silently), or a fix on this ticket before it resolves if it's small and in-scope. Then ask the human whether to keep going through the remaining steps or stop here. Never silently log a fail and carry on to Resolve.
24
24
  - **When only some steps are runnable now** (e.g. a few need a backend deployed first), walk the runnable ones conversationally **now**, and mark the rest **explicitly pending** with what unblocks them, tracked as below. Don't defer the whole UAT just because part of it is blocked.
25
25
  - **When the human can only run it later** (on-device, hardware you don't have right now), record the full numbered checklist in part 3 of the `VERIFY` comment as the durable artifact and mark UAT **explicitly pending**, but the walkthrough stays conversational **when they come to run it**: never shrink it to a vague one-liner ("just check it on the device"), and never let "automated passed" quietly stand in for it. Deferring *where/when* the human runs it is fine; replacing the live step-by-step with a self-serve checklist is not.
26
26
  - **Pending UAT must stay tracked: a closed ticket is not tracking.** Don't resolve/close a user-facing ticket on unconfirmed acceptance by your own choice and leave the checklist as a comment on the closed issue, where nobody will look. The one exception is an explicit waiver: **a waiver is not a deferral**. The human picked skip at the drive-mode ask and forwent acceptance, recorded as unconfirmed in `VERIFY`, so nothing is owed and nothing is opened. The default is otherwise to **keep the ticket open** until the human confirms the UAT passes (closing on green you didn't see is a premature close). **When UAT hasn't been run at Resolve, pose the choice to the human and wait for an explicit answer, never decide it for them:** (a) **run the UAT now**, in this session, with no ticket (opens with the drive-mode ask above, then the guided conversational walkthrough or AI-driven checks, resolving on a confirmed pass, or on an explicit waiver if the user skips at the ask (recorded as unconfirmed; the resolution comment states it); a failing step becomes a fix or a `bug` follow-up and an incomplete/blocked step is marked pending and tracked, per the failing-step and partial-run rules above, never a silent close), or (b) **open a UAT ticket** to run it later. **Never open a UAT ticket without asking, and never let silence or an ambiguous reply default to either path.** The one stated exception is an autonomous run (`auto`): the run itself is the human's go-ahead, so it takes option (b) on its own for the human-only steps, reusing an open follow-up if one exists (`../../trailhead-work/references/auto.md`). Interactive sessions keep the ask. Only when the human picks (b) do you **open a follow-up ticket before that close** that carries the pending UAT and links back to the parent with a `UAT of: [<parent name>](<parent url>)` pointer line in the body, and carries the `trailhead:uat` label: put it in the `--label` list of the `gh issue create` (next to the `trailhead:ticket`, type and destination labels: the parent's `trailhead:map-<n>`, or `trailhead:whiteboard` for a whiteboard parent), ensuring the `trailhead:uat` label idempotently first (`substrate-commands.md`). Route it the way `../choices.md` routes a deferred choice: a **`trailhead:seed`** (kept blocked) when the run is gated on a future external trigger not yet true (hardware you don't have, a device out of reach), or a follow-up **`task`** on the frontier when it's just scheduled or batched work you could pick up any time. Either way the follow-up **carries the full numbered checklist and, when it comes to resolution, opens with the drive-mode ask above before running the guided conversational walkthrough**, never a self-serve check-off. **Never close a user-facing ticket on a chosen-deferred UAT with nothing opened**: the parent may close as agreed, but the owed UAT stays visible on the tracker as its own ticket, linked to the parent, until it is actually performed. The pending check must be visible on the tracker, not buried.
@@ -1,5 +1,8 @@
1
1
  # Code review
2
- Runs after execute, before closing a `build`/`bug` ticket: as a subagent when the diff is non-trivial, inline for a tiny change. Dispatch the **`trailhead-code-review`** agent (read-only: it reviews and reports, it must never Edit/Write or commit), passing `config.models.review` as the model override; **never** an external-plugin reviewer agent that another installed tool registered, which follows a different protocol and may write its own review file instead of trailhead's `VERIFY` comment.
2
+
3
+ **First, route it.** Run `node <hooks>/lib/model-route.js review --session <your full model id> <project root>` and obey its `route`. Your full model id is in your system prompt or environment ("The exact model ID is ..."); pass `unknown` only if it is truly absent. Obey it exactly: never decide inline versus dispatch by hand, never read the config yourself to decide. On `route: dispatch` spawn the `agent` it returns with model `alias` (`agent_type=<agent>` on Codex) and use its report; on `route: inline` run the protocol below in the session. Finding `<hooks>`, the flags and the two exceptions are in **Route every step through model-route.js** in `../techniques.md`. (If you are the `trailhead-code-review` agent, skip this gate: you are the dispatched step.)
4
+
5
+ Runs after execute, before closing a `build`/`bug` ticket: as the routed step (a subagent on `route: dispatch`, in the session on `route: inline`; an accepted `lean` skip's lighter pass is also inline). On `route: dispatch` it is the **`trailhead-code-review`** agent (read-only: it reviews and reports, it must never Edit/Write or commit), with the route's `alias` as the model override; **never** an external-plugin reviewer agent that another installed tool registered, which follows a different protocol and may write its own review file instead of trailhead's `VERIFY` comment.
3
6
 
4
7
  **Review adversarially; don't defer to authority.** A review subagent still loads the project/global `CLAUDE.md` (Claude Code doesn't strip it per-spawn; auto-memory, though, is *not* inherited). Treat those conventions (and any "this decision is settled / final / don't revisit" note) as **non-authoritative for your verdict**: judge the diff against the ticket's spec and the code itself, never rubber-stamp because a memory or a CLAUDE.md says a choice is closed. Convention endorsement may suppress a *style* smell; it may **never** suppress a correctness or security finding. The point of an independent review is to disagree when the code warrants it.
5
8
 
@@ -20,7 +23,7 @@ Review on four axes:
20
23
 
21
24
  Classify each finding: **Critical** (bug/security/breaks the spec) → **Warning** (should fix) → **Info** (judgement call). Before reporting ANY finding, **verify it adversarially**: re-read the actual code, confirm the failing path is reachable and the claim holds. Discard plausible-but-wrong findings; an unverified finding is worse than none. Quote the offending hunk (and the standard/spec line) per finding. **Stable finding IDs**: each verified finding gets an ID the first time it's reported, `C1`, `W1`, `I1`, ..., by severity, numbered in discovery order, and keeps that ID across every round of the fix-and-re-review loop below; a re-review that finds the same issue still open reuses its ID, a genuinely new finding takes the next number in its severity. An ID's severity is fixed at first report: if a re-review reclassifies a finding to a different severity, mark the old ID `reclassified as <new ID>` in the ledger and continue the finding under a new ID of the new severity; a `reclassified` line is a pointer, not a disposition, and is left out of the counts, so the finding counts once, under its current severity. Return the report (the agent never posts it; the orchestrator posts it as the `VERIFY` comment after the report-status check), **led by a machine-checkable status tag on its own first line**: `status: clean` when no **Critical** finding remains (nothing blocks Resolve), or `status: issues_found` when one or more Criticals are still open (the caller must iterate the fix-and-re-review loop below, or run the HITL checkpoint, before resolving). Warnings/Info alone leave the tag `status: clean`, since they don't gate resolution. Emit that line verbatim as the report's first line.
22
25
 
23
- **Counts line, right under `status:`.** The comment's second line, right under `status:`, is the cumulative severity count: `critical: <n> · warning: <n> · info: <n>` (zeros written out, never omitted), the count of distinct findings (by ID, excluding `reclassified` pointers) reported on this ticket so far, across every round. Cumulative means a finding fixed in an earlier round still counts here, so the totals always match the ledger below. Below the counts comes the **findings ledger** (authoritative for this ticket), one line per finding ID with its disposition: `fixed (<commit>)`, `deferred → <ticket>` (a ticket that exists, named and linked: an already-open ticket, or one opened on the user's explicit yes per **Scope that surfaces while working a ticket** (served by the capture cluster), never unasked; when the user declines or leaves it unanswered, and under an autonomous run, where no ticket is opened unasked, use `accepted: <one-line reason>` instead), `accepted: <one-line reason>`, or `open` while the disposition is still undecided. The ledger lives in the latest `VERIFY` comment of the ticket and is rewritten each round; the resolution comment links to it rather than duplicating it. Then the findings grouped by severity with file:line and the fix, as before, so the Verify step branches on the tag without parsing prose.
26
+ **Counts line, right under `status:`.** The comment's second line, right under `status:`, is the cumulative severity count: `critical: <n> · warning: <n> · info: <n>` (zeros written out, never omitted), the count of distinct findings (by ID, excluding `reclassified` pointers) reported on this ticket so far, across every round. Cumulative means a finding fixed in an earlier round still counts here, so the totals always match the ledger below. Below the counts comes the **findings ledger** (authoritative for this ticket), one line per finding ID with its disposition: `fixed (<commit>)`, `deferred → <ticket>` (a ticket that exists, named and linked: an already-open ticket, or one opened on the user's explicit yes per **Scope that surfaces while working a ticket** (`../surfaced-scope.md`), never unasked; when the user declines or leaves it unanswered, and under an autonomous run, where no ticket is opened unasked, use `accepted: <one-line reason>` instead), `accepted: <one-line reason>`, or `open` while the disposition is still undecided. The ledger lives in the latest `VERIFY` comment of the ticket and is rewritten each round; the resolution comment links to it rather than duplicating it. Then the findings grouped by severity with file:line and the fix, as before, so the Verify step branches on the tag without parsing prose. A `status: clean` report whose counts line has Critical above zero must carry a ledger line closing each Critical (`fixed`, `deferred` or `accepted`); otherwise the report-status check fails it closed to `issues_found`.
24
27
 
25
28
  **Converge on the blockers with a bounded budget and an explicit boundary.** A **Critical** finding is *blocking*: it must be fixed before Resolve. Fixing one can introduce or reveal another, so the review is a **loop, not a single pass**: fix every Critical, then re-review the recomputed **Ticket diff** set, up to `config.code_review.rounds` (default 2) fix-and-re-review rounds. Stop when no Critical remains, then Resolve, giving each remaining Warning/Info its disposition in the ledger. **If the rounds are exhausted with Critical findings still open**, never resolve silently and never silently keep going: surface an explicit HITL checkpoint in the chat, **name each surviving blocker**, and put the choice to the user, (a) **run another 2 review rounds** on the remaining blockers (the point of this ask), or (b) **proceed with the blockers recorded** (each surviving Critical is recorded in the ledger as `deferred` or `accepted`), or (c) **run until it converges** (keep the fix + re-review loop going with no round cap, stopping only when no Critical remains; as a guard against an endless loop, if a round makes no progress, i.e. the same Critical findings come back unfixed, stop and bring the choice back to this checkpoint, naming them). **Wait for their pick; never auto-advance in either direction over open blocking findings.** (This mirrors **Cross-AI plan review**'s boundary ask: see `plan-review.md` step 4.)
26
29
 
@@ -1,5 +1,8 @@
1
1
  # Codebase map (fan-out)
2
- One-time at project adoption. Fan out 5 read-only reader subagents in parallel (single message), each owning ONE non-overlapping facet; no facet reads another's territory. Dispatch each reader as the **`trailhead-codebase-map`** agent (read-only by construction); **never** an external-plugin mapper agent that another installed tool registered.
2
+
3
+ **First, route it.** Run `node <hooks>/lib/model-route.js codebase-map --session <your full model id> <project root>` and obey its `route`. Your full model id is in your system prompt or environment ("The exact model ID is ..."); pass `unknown` only if it is truly absent. Obey it exactly: never decide inline versus dispatch by hand, never read the config yourself to decide. On `route: dispatch` spawn the `agent` it returns with model `alias` (`agent_type=<agent>` on Codex) and use its report; on `route: inline` run the protocol below in the session. Finding `<hooks>`, the flags and the two exceptions are in **Route every step through model-route.js** in `../techniques.md`. (If you are the `trailhead-codebase-map` agent, skip this gate: you are the dispatched step.)
4
+
5
+ One-time at project adoption. Fan out 5 read-only reader subagents in parallel (single message), each owning ONE non-overlapping facet; no facet reads another's territory. Route once for the whole fan-out; on `route: dispatch` each reader is the **`trailhead-codebase-map`** agent (read-only by construction), with the route's `alias` as the model override; **never** an external-plugin mapper agent that another installed tool registered.
3
6
 
4
7
  **The repo to map may be handed in by absolute path** (the scan root), so **never assume the current working directory is that repo**: search and read it by absolute path and run git as `git -C <scan root> ...`, and **never `cd` (or `pushd`/`env -C`) into it in any shape** (no `cd <root> && grep <relative>`, no bare `cd <root>` line then a relative read, no heredoc/multi-statement block, and not as a standalone `cd <root>` in an earlier Bash call: the cwd persists across calls). This keeps every read target statically determinable, so a bypass-permissions scan of an out-of-tree repo runs without a manual approval prompt. See **Search-command hygiene** in `../techniques.md`.
5
8
 
@@ -1,7 +1,10 @@
1
1
  # Debug (scientific method)
2
+
3
+ **First, route it.** Run `node <hooks>/lib/model-route.js debug --session <your full model id> <project root>` and obey its `route`. Your full model id is in your system prompt or environment ("The exact model ID is ..."); pass `unknown` only if it is truly absent. Obey it exactly: never decide inline versus dispatch by hand, never read the config yourself to decide. On `route: dispatch` spawn the `agent` it returns with model `alias` (`agent_type=<agent>` on Codex) and use its report; on `route: inline` run the protocol below in the session. Finding `<hooks>`, the flags and the two exceptions are in **Route every step through model-route.js** in `../techniques.md`. (If you are the `trailhead-debug` agent, skip this gate: you are the dispatched step.)
4
+
2
5
  A bug is a science problem: observe, hypothesize, test, confirm, verify. Never patch on a hunch: an unconfirmed fix is a guess that recurs. Treat the issue as persistent debug state: log every hypothesis, test, and eliminated cause as comments, so a context reset never restarts the investigation.
3
6
 
4
- When run as a subagent, dispatch the **`trailhead-debug`** agent (full tools: it adds instrumentation and regression tests, so it needs Write), passing `config.models.debug` as the model override; **never** an external-plugin debugger agent that another installed tool registered.
7
+ When run as a subagent (`route: dispatch`), it is the **`trailhead-debug`** agent (full tools: it adds instrumentation and regression tests, so it needs Write), with the route's `alias` as the model override; **never** an external-plugin debugger agent that another installed tool registered.
5
8
 
6
9
  **Report.** The agent returns an investigating report (`Return contract` in its definition): `answer:` the confirmed cause stated as a falsifiable hypothesis, `evidence:` the falsification test and its result, `open questions:` what stays unsettled. It never commits or creates a branch: the orchestrator turns the report into the `DIAGNOSIS`.
7
10
 
@@ -1,6 +1,8 @@
1
1
  # Grilling
2
2
  Interrogate the human live, one question at a time, until the decision is unambiguous and its rationale is captured. This is human-in-the-loop: the decision is theirs, so never answer on their behalf, never assume an answer you haven't heard, never proceed on silence.
3
3
 
4
+ **First, before any question: route the round.** Run `node <hooks>/lib/model-route.js grill --session <your full model id> <project root>` and obey its `route`. Your full model id is in your system prompt or environment ("The exact model ID is ..."); pass `unknown` only if it is truly absent. Never decide inline versus dispatch by hand, never skip the helper, never read the config yourself to decide. On `route: dispatch` you do not write the questions yourself: spawn `agent` with model `alias` and relay its report. On `route: inline`, continue with the bullets below. Finding `<hooks>`, the flags, each round and the checker are in **Who runs a round** at the end of this file. (If you are the `trailhead-grill` agent, skip this gate: you are the dispatched round.)
5
+
4
6
  - **Map the decision as a tree.** Every choice branches into the choices that hang off it. The *frontier* is every question whose prerequisites are already settled: the ones you can ask now without guessing. A question whose answer depends on another still-open one isn't on the frontier yet; it arrives there the moment that answer lands. You don't ask the frontier in a batch: you pick one from it, and recompute after the answer.
5
7
  - **Breadth before depth: enumerate the whole surface first.** When the subject is a space with many parts (mapping an app or a project's scope, not a single isolated choice), open by enumerating every functional area it implies (for a video player: media library, search, thumbnails, filters, genres, the player itself, ...) and get them all onto the record before deep-diving any one. Depth on the current thread is seductive, but the tree only ever grows the questions you actually raise, so an area you never name never enters it and stays silently missing. Fan out first (name every area), then converge each. The deliverable is coverage of the whole surface, not a deep answer on one corner of it.
6
8
  - **One question at a time, with your pick.** Number the question, state it, and offer your recommended answer, stress-tested per the next bullet. Then stop and wait. Each answer reshapes the tree: settled decisions push the frontier outward. Recompute and ask the next. Grilling stays human-decides: the **grilled questions themselves never offer the delegate option** (a recommended answer is not the delegate option), because converging a decision is the human's. A separate meta-ask a call-site may put *before* grilling (e.g. the `build`/`quick` Discuss "do you want a grilling round?") is its own advisory choice and can offer delegate/defer per that call-site. **When a grilled decision is genuinely blocked on a prerequisite, offer defer in prose** (state it in words as part of the question), per the recipe in `../choices.md`.
@@ -13,5 +15,28 @@ Interrogate the human live, one question at a time, until the decision is unambi
13
15
  - **Detect and open up tradeoffs.** When "or / versus / torn between / which is better" appears, pause: name what each side optimizes for, what each sacrifices, which aligns with the goal, then recommend with reasoning. Skip for trivial or already-decided choices.
14
16
  - **Play back your understanding** before closing each branch: restate the decision and its rationale in your words and have them confirm or correct.
15
17
  - **Stop only when the frontier is empty**: every branch visited, nothing silently assumed, the choice unambiguous *and* the why recorded. **For a multi-part space** (per the breadth bullet above) also require every functional area to have been enumerated and visited: an empty frontier reached while whole areas were never raised is a false finish, because the tree only ever grew the questions you raised. A single isolated choice has no areas to enumerate, so its empty frontier is a real finish. The empty frontier is then followed by the **Holistic review before the final confirmation** below: the final set is not confirmed or recorded before it has run.
16
- - **Holistic review before the final confirmation.** Once the frontier is empty and before the final confirmation, re-read all settled choices as a whole, against each other and against the destination (or the ticket's goal), because answers taken one at a time can fit badly together. Report (a) the proposed amendments: what you would change in the set where choices do not fit together; and (b) the non-working combinations: contradictions, gaps, choices that break each other or the destination. Each item names the choice(s) it touches by question number. **Nothing found:** say so in one line and go straight to the confirmation. **Otherwise** ask the human to accept, amend or reject: accept applies the proposed amendments; amend lets them rewrite an item or pick among them (mixed per-item answers go here); reject keeps the choices as they were. Whatever the answer, every non-working combination still standing afterwards (not resolved by the applied amendments, or rejected) is recorded with the set as a consciously accepted risk, never silently dropped. An applied amendment is re-confirmed in one play-back; it reopens that branch (the grill resumes from there) only when it undoes the rationale the branch was settled on, and then the review runs again on the new set, at most once more: whatever that second review still finds is put to the human the same way, and anything left standing is recorded as accepted risk, with no third pass. Only then is the final set played back, confirmed and recorded. **Format:** a forced choice of 3 real options, so an `AskUserQuestion` menu per the "Which format" rule in `../choices.md` (a numbered list on a host without one), without the delegate option (the set is the human's) and without defer; the grilled questions themselves stay conversational prose. A single isolated choice still runs it (usually the one-line nothing found).
18
+ - **Holistic review before the final confirmation.** Once the frontier is empty and before the final confirmation, re-read all settled choices as a whole, against each other and against the destination (or the ticket's goal), because answers taken one at a time can fit badly together. Report (a) the proposed amendments: what you would change in the set where choices do not fit together; and (b) the non-working combinations: contradictions, gaps, choices that break each other or the destination. Each item names the choice(s) it touches by question number. **Every non-working combination carries its fix**, stress-tested like any pick: the choice to change and to what, the question to reopen, or the ticket that will carry it. Only when no fix exists inside the set (it needs a fact not yet known, or it lies beyond the destination) say why in one line and name where it goes (a `research` ticket, a seed, the ticket that already tracks it). Accepting a combination as a risk is never the proposed fix and never a plain pointer to another ticket: it is the human's call, made on the menu below. **Nothing found:** say so in one line and go straight to the confirmation. **Otherwise** ask the human to apply, amend or keep: apply (the first option, recommended) applies every proposed amendment and fix; amend lets them rewrite an item or pick among them (mixed per-item answers go here); keep leaves the choices as they were. Whatever the answer, every non-working combination still standing afterwards (its fix not applied, or kept) is recorded with the set as a consciously accepted risk, never silently dropped. An applied amendment or fix is re-confirmed in one play-back; it reopens that branch (the grill resumes from there) only when it undoes the rationale the branch was settled on, and then the review runs again on the new set, at most once more: whatever that second review still finds is put to the human the same way, and anything left standing is recorded as accepted risk, with no third pass. Only then is the final set played back, confirmed and recorded. **Format:** a forced choice of 3 real options, so an `AskUserQuestion` menu per the "Which format" rule in `../choices.md` (a numbered list on a host without one), without the delegate option (the set is the human's) and without defer; the grilled questions themselves stay conversational prose. A single isolated choice still runs it (usually the one-line nothing found).
19
+
20
+ ## Who runs a round
21
+ The bullets above are the protocol of one round. Where a round runs is set by `models.grill`: the thinking behind each question can run on the read-only `trailhead-grill` agent, but **the main session stays the HITL orchestrator**. It alone asks the user, collects the answer and starts the next round. Every entry point points here instead of restating this.
22
+
23
+ When it runs as `trailhead-grill`, its output is always the report: it never asks the user live and never shows a menu (the orchestrator does, from the report). The rest of this section is the orchestrator's.
24
+
25
+ **Route.** Before the first round run `node <hooks>/lib/model-route.js grill --session <the session's full model id, or unknown> [--session-effort <effort>] [--host codex] [--no-subagents] <project root>`. `<hooks>` is the first of `../../hooks/lib/` and `../trailhead/hooks/lib/` (relative to the `_shared/` dir, as an absolute path) that contains it: the same two-path lookup `load-first.md` uses for `model-defaults-review.js`. Pass `unknown` when the session model is not known (a trailing `[...]` suffix such as `[1m]` is ignored, so pass the id as the session reports it), `--session-effort` whenever the session's reasoning effort is known, `--host codex` on Codex and `--no-subagents` on a subagent-less host. `models.grill` unset follows `models.plan` (and `models.codex.grill` follows `models.codex.plan`); the helper applies this (per layer: a config layer that sets `plan` but not `grill` implicitly sets `grill` to that `plan` for that layer, before the project-over-global merge (so a project that picked its `plan` grills on it even when the global config pins a `grill`; the same for `models.codex.*`)), so never resolve it by hand. The helper prints one JSON line `{"route","agent","model","alias","suggest","warning"}`.
26
+ - `route: inline`: run the technique in the session, as the bullets above say. When the helper returns `suggest`, first show one line suggesting the user switch the session to that model before the round (this is the subagent-less fallback). A `warning` is shown once.
27
+ - `route: dispatch`: spawn the agent and use its `agent` and `alias` verbatim: `agent` as the subagent and `alias` as the `Agent` model override on Claude Code, `agent_type=<agent>` on Codex. Never substitute another agent or model.
28
+ - If the agent is not registered in this environment, run inline, per the dispatch rule in `../techniques.md`.
29
+
30
+ **What the model pin guarantees.** On Claude Code a spawn gets only a family alias, so a dispatched grill is guaranteed the family of the resolved id, not its exact version. The full id still decides inline versus dispatch.
31
+
32
+ **Each round.**
33
+ 1. Send the agent the full state: this protocol (plus `domain-vocabulary.md` where the call site pairs it); the topic or the ticket body, the relevant decisions, the tickets the subject names and the destination; whether the subject is a single choice or a multi-part space; the numbered Q&A so far with the last `settled`, `frontier`, `areas` and `facts`; the review round (0, 1 or 2) and the last apply/amend/keep answer.
34
+ 2. To keep rounds stateless, re-dispatch with the full state each round. Continuing the same agent is allowed, but it still gets the full state. Carrying `facts` and `settled` forward keeps re-reading down.
35
+ 3. Check the report with `node <templates>/trailhead-report-status.js --technique grill --file <report>` (see *Malformed review report* in `../techniques.md`), then branch on the returned verdict:
36
+ - `question`: relay `question` and `recommended` to the user in prose. Never answer on the user's behalf and never proceed on silence. A vague answer goes back to the agent to probe. A `not looked up:` fact is fetched by the orchestrator and passed in the next round.
37
+ - `review` with `amendments` or `conflicts` not `none`: put the apply/amend/keep `AskUserQuestion` menu to the user (no delegate, no defer), then re-dispatch with the answer. The orchestrator caps reviews at two; anything still standing is recorded as accepted risk.
38
+ - `review` with both fields `none`: play back `settled`, confirm with the user and record it where the call site says.
39
+
40
+ **Inline rounds.** An inline round writes no report: the session asks the user directly, as the bullets above say, and the checker is not run.
17
41
 
42
+ **Codex.** An unset `models.codex.grill` follows `models.codex.plan` when that is pinned (the projected `trailhead-grill.toml` carries that pin), else inherits the session model, like every `models.codex.*` key. A pin equal to the session model (same effort, or no effort) routes inline.
@@ -1,5 +1,8 @@
1
1
  # Research
2
- Spawn a focused subagent (AFK, parallelisable) with exactly ONE clear question: sharp enough that the answer is a decision, not a survey. Vague scope breeds rabbit holes; a narrow question is its own leash. Dispatch the **`trailhead-research`** agent (it commits nothing and creates no branch: the report is the artifact), passing `config.models.research` as the model override; **never** an external-plugin researcher agent that another installed tool registered.
2
+
3
+ **First, route it.** Run `node <hooks>/lib/model-route.js research --session <your full model id> <project root>` and obey its `route`. Your full model id is in your system prompt or environment ("The exact model ID is ..."); pass `unknown` only if it is truly absent. Obey it exactly: never decide inline versus dispatch by hand, never read the config yourself to decide. On `route: dispatch` spawn the `agent` it returns with model `alias` (`agent_type=<agent>` on Codex) and use its report; on `route: inline` run the protocol below in the session. Finding `<hooks>`, the flags and the two exceptions are in **Route every step through model-route.js** in `../techniques.md`. (If you are the `trailhead-research` agent, skip this gate: you are the dispatched step.)
4
+
5
+ Spawn a focused subagent (AFK, parallelisable) with exactly ONE clear question: sharp enough that the answer is a decision, not a survey. Vague scope breeds rabbit holes; a narrow question is its own leash. On `route: dispatch` it is the **`trailhead-research`** agent (it commits nothing and creates no branch: the report is the artifact), with the route's `alias` as the model override; route once, then reuse it for every parallel question; **never** an external-plugin researcher agent that another installed tool registered.
3
6
 
4
7
  - **Treat your own prior knowledge as hypothesis, not fact**: training is stale. "As of my training" is a warning flag, not a citation.
5
8
  - **Chase primary, high-trust sources**: official docs, source code, specs, first-party APIs; never a secondary write-up. Follow every claim back to the source that owns it; resolve *current* docs rather than reciting from memory.
@@ -1,8 +1,10 @@
1
1
  # Goal-backward verification
2
2
 
3
+ **First, route it.** Run `node <hooks>/lib/model-route.js verify --session <your full model id> <project root>` and obey its `route`. Your full model id is in your system prompt or environment ("The exact model ID is ..."); pass `unknown` only if it is truly absent. Obey it exactly: never decide inline versus dispatch by hand, never read the config yourself to decide. On `route: dispatch` spawn the `agent` it returns with model `alias` (`agent_type=<agent>` on Codex) and use its report; on `route: inline` run the protocol below in the session. Finding `<hooks>`, the flags and the two exceptions are in **Route every step through model-route.js** in `../techniques.md`. (If you are the `trailhead-verify` agent, skip this gate: you are the dispatched step.)
4
+
3
5
  Prove the change **delivers what the ticket promised**, not merely that steps ran or the tests pass. This is distinct from **Code review** (which hunts bugs and quality issues adversarially) and from **Acceptance testing** (which proves it does what the *user* wanted, interactively): this is a **goal-backward** reasoning check against the ticket's own stated intent.
4
6
 
5
- Run it in the `build`/`bug` **Verify** step, after the tests / the plan's criterion have run, as a subagent on `config.models.verify` (dispatch the **`trailhead-verify`** agent, read-only, passing `config.models.verify` as the model override; inline when the model is unset or equals the session model, and inline on a `subagentToolkit == none` host). Like the other reviews it **never blocks the session**: run it in the background and collect it by polling (see *Reviews never block the session* in `../techniques.md`).
7
+ Run it in the `build`/`bug` **Verify** step, after the tests / the plan's criterion have run, as the routed subagent on `config.models.verify` (on `route: dispatch` the **`trailhead-verify`** agent, read-only, with the route's `alias` as the model override; a `subagentToolkit == none` host routes inline). Like the other reviews it **never blocks the session**: run it in the background and collect it by polling (see *Reviews never block the session* in `../techniques.md`).
6
8
 
7
9
  1. **Restate the goal.** From the ticket's `## Question` and the `PLAN`'s verification criteria, list what "done" means as concrete, checkable claims. If the ticket named acceptance criteria, those are the claims. **When the ticket had an approved mockup**, add one explicit claim: the built UI is faithful to the approved mockup (layout, spacing, sizing, colour, typography, copy, and every state it shows). Being read-only and running before Acceptance testing, check it to the extent statically evaluable: does the built code carry the mockup's specified copy, states, and structural elements? A dropped element or missing state surfaces as `gaps_found`, and **defer the visual, pixel-level verdict to Acceptance testing's mockup-fidelity check** (a screenshot, not the diff, settles that) rather than forcing it here.
8
10
  2. **Work backward from each claim to the ticket's own commits.** Take the change from the **Ticket diff** rule (canonical in `code-review.md`; restated here because this protocol is loaded on its own): the commits carrying this ticket's `Refs: #<n>` trailer, computed with exactly `git -C <repo> log --reverse --basic-regexp --grep='^Refs: #<n>$' --format=%H HEAD` and read per commit, oldest first, with `git -C <repo> show --remerge-diff <sha>`; recompute it on every run, never use a `<base>...HEAD` range, and treat an empty set as an error surfaced to the caller, never a `met`. For every claim, find the commit(s) and lines that satisfy it, record it as **unmet** if the code does not satisfy it, or as **inconclusive** (with a reason-class, see the Verdict) if you could not actually evaluate it: the check would not run, the evidence was unreadable, or the criterion is not falsifiable as written. Read the actual diff and the resulting code, not the commit messages.
@@ -20,7 +22,7 @@ Run it in the `build`/`bug` **Verify** step, after the tests / the plan's criter
20
22
 
21
23
  **Golden rule:** a claim that was **not evaluated** never becomes `met` nor `gaps_found`; it becomes `inconclusive`. `gaps_found` keeps its exact meaning, "evaluated, and the code does not satisfy the claim", and never absorbs a check that simply could not run.
22
24
 
23
- **Mixed states.** With claims falling in more than one class, emit **one** leading `status:` tag by precedence **`gaps_found` > `inconclusive` > `met`**, and in the body list every claim with its own class (and its one `reason:` where inconclusive). The engine clears the gaps first, autonomously (the Execute/Fix loop, AFK-friendly); the `inconclusive` claims resurface on re-verify and escalate to the human only when nothing else remains, so the cycle always converges.
25
+ **Mixed states.** With claims falling in more than one class, emit **one** leading `status:` tag by precedence **`gaps_found` > `inconclusive` > `met`**, and in the body list every claim with its own class (and its one `reason:` where inconclusive). The engine clears the gaps first, autonomously (the Execute/Fix loop, AFK-friendly); the `inconclusive` claims resurface on re-verify and escalate to the human only when nothing else remains, so the cycle always converges. Every claim listed in the body is one line carrying its class as `class: met | unmet | partial | inconclusive` (an inconclusive one also its one `reason:`), and a `met` report lists no `unmet`, `partial` or `inconclusive` claim: the report-status check raises a leading tag that contradicts the listed classes to the highest class by the precedence above (fail closed).
24
26
 
25
27
  **Routing.** On `gaps_found` the engine loops back to Execute/Fix for the gap, or runs the HITL checkpoint, before Resolve; it never resolves over an unmet goal silently. When the **leading** tag is `inconclusive` (every gap already cleared, so no `gaps_found` outranks it), the goal is **unevaluated, not satisfied**: it is a **blocking gate too** and is never auto-resolved over. (While `gaps_found` still leads, the Fix loop runs first per Mixed states above; the inconclusive claims only reach this checkpoint once no gap remains.) Because nothing changes between runs of a read-only check, there is **no auto-retry**; go straight to the HITL checkpoint, framed per reason-class (reusing the shared checkpoint mechanics in `code-review.md`: name each surviving blocker, put the choice to the user, never auto-advance):
26
28
  - `unrunnable-check` / `unreadable-evidence`: name the failed command / unreadable file, then offer (i) fix the environment and re-verify, or (ii) resolve accepting the claim as unverified (recorded in the `VERIFY` comment, the annotate-in-`VERIFY` path code review uses).
@@ -2,14 +2,16 @@
2
2
 
3
3
  The ticket engines call these by name. Each technique's full protocol lives in its own file under `techniques/`: **read that file the first time a session needs the technique**, so only the ones in play load into context (don't preload them). They are `trailhead`'s own, self-contained; each technique is dispatched to its **trailhead-owned agent** (`trailhead-<technique>`, shipped as committed `agents/trailhead-*.md`) via the `Agent` tool, with the technique's full protocol injected at spawn.
4
4
 
5
- **Dispatch each technique to its trailhead-owned agent.** Every technique subagent carries its full protocol in the prompt (from its `techniques/` file), so the agent definition stays **thin**: identity + tool policy, model-agnostic. Dispatch each technique to **its own `trailhead-<technique>` agent** (set `subagent_type` accordingly): the Plan step → `trailhead-plan`, Execute → `trailhead-executor`, Research → `trailhead-research`, Code review → `trailhead-code-review`, Debug → `trailhead-debug`, Fix → `trailhead-fix`, Codebase map → `trailhead-codebase-map`, Goal-backward verification → `trailhead-verify`. These agents ship with trailhead (committed `agents/trailhead-*.md`) and are registered wherever trailhead is installed as a Claude Code **plugin** (its `agents/` load automatically), or its agent files are otherwise present as subagent types. **If a `trailhead-<technique>` agent is NOT registered in the current environment** (the `Agent`/`Task` tool reports the type unknown, or it is absent from the available-agents list, as on a skills-only install that did not register the agents), **run that technique inline in the main session**, following its `techniques/` protocol directly. Do **not** divert to a host built-in (`general-purpose`/`Explore`) or any external-plugin agent as the engine target. Inline is a graceful degradation: the technique still runs, only on the session model rather than its `config.models.<key>` pin (the same inline path used when that model equals the session model). The dispatch rule is therefore: the real `trailhead-<technique>` agent when it is registered, else inline in the main session, never a foreign agent. **Pass the `config.models.<key>` model as the `Agent`-tool `model` override at spawn** so config stays authoritative (the `.md` is model-agnostic; the override wins), running inline instead when that model is unset or equals the session model. Execute and Fix inject `trailhead-work/references/executor-brief.md` into their agent as the protocol text, not the whole step (the orchestrator part of the step stays with the session). On Claude Code the `Agent` tool's `model` parameter accepts only the short family aliases (`opus` / `sonnet` / `haiku` / `fable`), not the full ids `config.models.*` stores, so **map the configured id to its family alias only for the literal `model` value passed at spawn**: `claude-opus-*` -> `opus`, `claude-sonnet-*` -> `sonnet`, `claude-haiku-*` -> `haiku`, `claude-fable-*` -> `fable`; if an id matches none of these, don't guess: warn and inherit the session model (per `configuration-reference.md`). The alias names a family, not a version, so the host resolves it to that family's current model: the exact version the id pinned cannot be conveyed to the `Agent` tool and is not honoured at subagent spawn. **Keep the inline-vs-subagent decision on the full id**, running inline only when `config.models.<key>` is unset or its full id equals the session model's, never on the alias, so a deliberate same-family but different-version pin (e.g. `claude-opus-5` under a `claude-opus-4-8` session) still dispatches instead of collapsing to inline. (Codex projects `models.codex.*`, OpenAI ids, separately and is unaffected by this.) **Never dispatch to an external-plugin agent** (a `gsd-*`, `*-reviewer`, `*-researcher`, `*-mapper`, or similarly named agent another installed tool registered): it follows *its own* protocol and artifact conventions (a foreign code-review agent writes its own review file, not trailhead's `VERIFY` ticket comment), silently swapping trailhead's engine for another tool's. Host built-ins (`Explore` / `general-purpose`) stay available in general but are **no longer the engine-dispatch target**. The tool policy is baked into each agent's `tools:` field, mirroring the old split: **read-only** (`trailhead-plan`, `trailhead-code-review`, `trailhead-codebase-map`, `trailhead-verify`: reads, searches, runs tests; no Edit/Write) vs **full** (`trailhead-executor`, `trailhead-research`, `trailhead-debug`, `trailhead-fix`: can Edit/Write/commit). **Search-command hygiene (every fan-out subagent, and any built-in `Explore`/`general-purpose` fan-out the engine leans on).** Prefer the Grep and Read tools over shelling out to search or read a file; when a Bash `grep`/read is genuinely needed, pass the path as an **explicit argument** (`grep -niE "<patterns>" <abs>/<file>`, `wc -l <abs>/<file>`) and run git as `git -C <abs> ...`. **Never `cd` (or `pushd`/`env -C`) into the target directory first, in any shape**: not `cd <abs> && grep <relative>`, not a bare `cd <abs>` line followed by a relative `grep`/`wc` on the next line or after a `;`, not inside a heredoc or any other multi-statement block, and not as its own separate Bash call or turn (the shell working directory persists across Bash calls, so a standalone `cd <abs>` still leaves a later relative read non-static). Any `cd` makes the post-`cd` working directory non-static, so under bypass permissions with a `Read()` deny rule the harness cannot resolve the read target against the allow rules and falls back to a manual approval prompt (defeating headless operation); a path-explicit target (absolute-path argument, `git -C`) stays statically determinable and runs uninterrupted regardless of how the statements are separated. When the engine spawns a built-in `Explore`/`general-purpose` search, state this rule in its spawn prompt. On a `subagentToolkit == none` host there is no fan-out and this is moot: every technique runs inline in the one session. (Codex is **not** such a host: it has the native multi_agent toolkit, so techniques fan out there too, dispatched by `agent_type` to the projected `~/.codex/agents/trailhead-*.toml`; see the Codex adapter's §D.)
5
+ **Dispatch each technique to its trailhead-owned agent.** Every technique subagent carries its full protocol in the prompt (from its `techniques/` file), so the agent definition stays **thin**: identity + tool policy, model-agnostic. Dispatch each technique to **its own `trailhead-<technique>` agent** (set `subagent_type` accordingly): the Plan step → `trailhead-plan`, Execute → `trailhead-executor`, Research → `trailhead-research`, Code review → `trailhead-code-review`, Debug → `trailhead-debug`, Fix → `trailhead-fix`, Codebase map → `trailhead-codebase-map`, Goal-backward verification → `trailhead-verify`, Grilling → `trailhead-grill` (one round per spawn, the session stays the HITL orchestrator; see **Who runs a round** in `techniques/grilling.md`). These agents ship with trailhead (committed `agents/trailhead-*.md`) and are registered wherever trailhead is installed as a Claude Code **plugin** (its `agents/` load automatically), or its agent files are otherwise present as subagent types. **If a `trailhead-<technique>` agent is NOT registered in the current environment** (the `Agent`/`Task` tool reports the type unknown, or it is absent from the available-agents list, as on a skills-only install that did not register the agents), **run that technique inline in the main session**, following its `techniques/` protocol directly. Do **not** divert to a host built-in (`general-purpose`/`Explore`) or any external-plugin agent as the engine target. Inline is a graceful degradation: the technique still runs, only on the session model rather than its `config.models.<key>` pin (the same inline path used when that model equals the session model). The dispatch rule is therefore: the real `trailhead-<technique>` agent when it is registered, else inline in the main session, never a foreign agent. **Whether a step spawns, and on which model, is decided by `model-route.js`, never by hand**: see **Route every step through model-route.js** below. The route's `alias` is the `Agent`-tool `model` override at spawn, so config stays authoritative (the `.md` is model-agnostic; the override wins). **An unset key is not `inherit session`:** the helper resolves it to its Balanced default (`configuration-reference.md`), so with nothing configured the planner still spawns on the strong tier even from a standard-tier session; never spawn with defaults. Execute and Fix inject `trailhead-work/references/executor-brief.md` into their agent as the protocol text, not the whole step (the orchestrator part of the step stays with the session). On Claude Code the `Agent` tool's `model` parameter accepts only the short family aliases (`opus` / `sonnet` / `haiku` / `fable`), not the full ids `config.models.*` stores, so **map the configured id to its family alias only for the literal `model` value passed at spawn**: `claude-opus-*` -> `opus`, `claude-sonnet-*` -> `sonnet`, `claude-haiku-*` -> `haiku`, `claude-fable-*` -> `fable`; if an id matches none of these, don't guess: warn and inherit the session model (per `configuration-reference.md`). The alias names a family, not a version, so the host resolves it to that family's current model: the exact version the id pinned cannot be conveyed to the `Agent` tool and is not honoured at subagent spawn. The helper makes the inline-versus-dispatch decision on the full id, never on the alias, so a deliberate same-family but different-version pin (e.g. `claude-opus-5` under a `claude-opus-4-8` session) still dispatches instead of collapsing to inline. (Codex projects `models.codex.*`, OpenAI ids, separately and is unaffected by this.) **Never dispatch to an external-plugin agent** (a `gsd-*`, `*-reviewer`, `*-researcher`, `*-mapper`, or similarly named agent another installed tool registered): it follows *its own* protocol and artifact conventions (a foreign code-review agent writes its own review file, not trailhead's `VERIFY` ticket comment), silently swapping trailhead's engine for another tool's. Host built-ins (`Explore` / `general-purpose`) stay available in general but are **no longer the engine-dispatch target**. The tool policy is baked into each agent's `tools:` field, mirroring the old split: **read-only** (`trailhead-plan`, `trailhead-code-review`, `trailhead-codebase-map`, `trailhead-verify`: reads, searches, runs tests; no Edit/Write; `trailhead-grill`: reads and searches only, no Bash) vs **full** (`trailhead-executor`, `trailhead-research`, `trailhead-debug`, `trailhead-fix`: can Edit/Write/commit). **Search-command hygiene (every fan-out subagent, and any built-in `Explore`/`general-purpose` fan-out the engine leans on).** Prefer the Grep and Read tools over shelling out to search or read a file; when a Bash `grep`/read is genuinely needed, pass the path as an **explicit argument** (`grep -niE "<patterns>" <abs>/<file>`, `wc -l <abs>/<file>`) and run git as `git -C <abs> ...`. **Never `cd` (or `pushd`/`env -C`) into the target directory first, in any shape**: not `cd <abs> && grep <relative>`, not a bare `cd <abs>` line followed by a relative `grep`/`wc` on the next line or after a `;`, not inside a heredoc or any other multi-statement block, and not as its own separate Bash call or turn (the shell working directory persists across Bash calls, so a standalone `cd <abs>` still leaves a later relative read non-static). Any `cd` makes the post-`cd` working directory non-static, so under bypass permissions with a `Read()` deny rule the harness cannot resolve the read target against the allow rules and falls back to a manual approval prompt (defeating headless operation); a path-explicit target (absolute-path argument, `git -C`) stays statically determinable and runs uninterrupted regardless of how the statements are separated. When the engine spawns a built-in `Explore`/`general-purpose` search, state this rule in its spawn prompt. On a `subagentToolkit == none` host there is no fan-out and this is moot: every technique runs inline in the one session. (Codex is **not** such a host: it has the native multi_agent toolkit, so techniques fan out there too, dispatched by `agent_type` to the projected `~/.codex/agents/trailhead-*.toml`; see the Codex adapter's §D.)
6
+
7
+ **Route every step through model-route.js.** Before dispatching any engine step, run `node <hooks>/lib/model-route.js <key> --session <your full model id> [--session-effort <e>] [--host codex] [--no-subagents] <project root>` and obey its `route`. `<hooks>` is the first of `../../hooks/lib/` and `../trailhead/hooks/lib/` (relative to the `_shared/` dir, as an absolute path) that contains it: the same two-path lookup `load-first.md` uses for `model-defaults-review.js`. Your full model id is in your system prompt or environment ("The exact model ID is ..."); pass `unknown` only if it is truly absent (a trailing `[...]` suffix such as `[1m]` is ignored). Pass `--session-effort` whenever the session's reasoning effort is known, `--host codex` on Codex and `--no-subagents` on a subagent-less host. The key per step: Plan -> `plan`, Execute -> `execute`, Fix -> `execute` (spawn `trailhead-fix` in place of the returned `agent`), Code review -> `review`, Goal-backward verification -> `verify`, Research -> `research`, Debug -> `debug`, Codebase map -> `codebase-map`, Grilling -> `grill` (through **Who runs a round** in `techniques/grilling.md`). The helper prints one JSON line `{"route","agent","model","alias","suggest","warning"}`. `route: inline`: run the protocol in the session; when the helper returns `suggest`, show it as one line, and a `warning` is shown once. `route: dispatch`: spawn the step's agent and use its `agent` and `alias` verbatim: `agent` as the subagent and `alias` as the `Agent` model override on Claude Code (a family, not the exact version), `agent_type=<agent>` on Codex (where `alias` is null). Never substitute another agent or model. If the agent is not registered in this environment, run inline, per the dispatch rule above. Never decide inline versus dispatch by hand, and never read the config yourself to decide. Two cases sit outside the route, and only these two: an accepted `lean` skip (the user chose a lighter inline pass, so there is no subagent step to route), and an autonomous run with more than one slot (`trailhead-work/references/auto.md`: every step dispatches, so on `route: inline` it spawns the agent with no model override, inheriting the session model, which is the resolved one; the helper is still run).
6
8
 
7
9
  **Reviews never block the session (engine-wide, every host).** The three **review** techniques, **Code review** (build/bug Verify), **Goal-backward verification** (build/bug Verify), and **Cross-AI plan review** (build Plan), always run as a **background job collected by polling**, never a blind open-ended blocking wait. This is a hard default: not a `config` toggle, and not a Codex-only or fallback posture. Because trailhead works one ticket at a time and Resolve depends on the review's outcome, "background" here means a **poll loop with a one-line progress note, the session responsive and interruptible between polls**, the outcome always awaited; what is banned is the blind freeze, never "proceed without the result." On Claude Code the `Agent` tool already backgrounds the review and notifies on completion, so poll its progress instead of awaiting it blindly; on Codex, collect it through the §D bounded-wait poll loop (external plan-review CLIs via §H). **Acceptance testing is excluded**: it is interactive by design (guided UAT / browser-drive with the user in the loop), not a blind wait to background.
8
10
 
9
- **Malformed review report (every engine-agent report).** The leading `status:` line, and the fields each report must carry, are machine-checked before the engine branches on them. This MUST run on every Code review and Goal-backward verification report and on every plan, executor, fix, debug, research and codebase-map report, whether it came from the subagent, an inline pass, or the lean inline pass; the report-status checker is `trailhead-report-status.js`, one JSON line `{"action","verdict","problem","report","fields","body"}` with `action` one of `accept` / `normalise` / `reask`. The return contract of each family (statuses, required fields, invariants) is stated once in the agents' `Return contract` blocks; the checker enforces it.
11
+ **Malformed review report (every engine-agent report).** The leading `status:` line, and the fields each report must carry, are machine-checked before the engine branches on them. This MUST run on every Code review and Goal-backward verification report and on every plan, executor, fix, debug, research, codebase-map and grill report (a dispatched `trailhead-grill` round), whether it came from the subagent, an inline pass, or the lean inline pass; the report-status checker is `trailhead-report-status.js`, one JSON line `{"action","verdict","problem","report","fields","body"}` with `action` one of `accept` / `normalise` / `reask`. The return contract of each family (statuses, required fields, invariants) is stated once in the agents' `Return contract` blocks; the checker enforces it. An inline `grill` round writes no report, so it is not checked: only a dispatched `trailhead-grill` report is checked, because the checker guards the handoff from agent to orchestrator and an inline round has none.
10
12
 
11
- 1. **Check.** Per report run `mktemp -d` once (a private 0700 random directory); write the raw report to `<that dir>/report.md` with the Write tool at the literal printed path; run `node <templates>/trailhead-report-status.js --technique <verify|code-review|plan|executor|fix|debug|research|codebase-map> --file <that dir>/report.md`, where `<templates>` is the first of `${CLAUDE_PLUGIN_ROOT}/templates/` (skipped when unset), `~/.claude/trailhead/templates/` and `~/.codex/skills/trailhead/templates/` that contains it; then `rm -rf <that dir>` with the literal absolute path. Cleanup runs after every path: accept, normalise, each re-ask check, a second failure, and a checker failure. Never a fixed or predictable path, never reused. The checker also reads stdin, but this protocol does not use it (a heredoc would put the report into the shell-scanned command and needs a delimiter the report cannot contain).
12
- 2. **Accept or normalise.** The checker-returned `report` replaces the raw report where the technique places it (the layout below); the engine branches only on the returned `verdict`, and reads the returned `fields` (and, for a `ready` plan, `body`) instead of re-deriving them from the prose of the report.
13
+ 1. **Check.** A background agent whose host writes its transcript to a task output file (Claude Code's `<tasks dir>/<id>.output`, named in the completion notice): run `node <templates>/trailhead-report-status.js --technique <technique> --task-output <that output file>` at the literal path, once the agent has finished; the checker reads the agent's last assistant message itself, so no temp dir, no copy and no cleanup. Any other report (inline, foreground, or a host without that file): per report run `mktemp -d` once (a private 0700 random directory); write the raw report to `<that dir>/report.md` with the Write tool at the literal printed path; run `node <templates>/trailhead-report-status.js --technique <verify|code-review|plan|executor|fix|debug|research|codebase-map|grill> --file <that dir>/report.md`, where `<templates>` is the first of `${CLAUDE_PLUGIN_ROOT}/templates/` (skipped when unset), `${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/` and `~/.codex/skills/trailhead/templates/` that contains it; then `rm -rf <that dir>` with the literal absolute path. Cleanup runs after every path: accept, normalise, each re-ask check, a second failure, and a checker failure. Never a fixed or predictable path, never reused. The checker also reads stdin, but this protocol does not use it (a heredoc would put the report into the shell-scanned command and needs a delimiter the report cannot contain). Each of these is its own Bash call: never wrapped in `bash -c` / `sh -c`, never chained with `;`, `&&` or a pipe to other commands (git, `jq`, the work-state write), because the host cannot statically check such a script and asks the user to approve every one.
14
+ 2. **Accept or normalise.** A review verdict contradicted by its own ledger or claim classes is normalised to the stricter verdict. The checker-returned `report` replaces the raw report where the technique places it (the layout below); the engine branches only on the returned `verdict`, and reads the returned `fields` (and, for a `ready` plan, `body`) instead of re-deriving them from the prose of the report.
13
15
  3. **Reask.** Ask once: continue the same agent where the host allows it, else re-dispatch the same `trailhead-<technique>` agent on the same model override with the malformed report quoted, using this corrective prompt verbatim (placeholders filled): "Your report could not be accepted: `<problem>` (missing: no `status:` line; unknown-token: `<found>` is not an allowed token; conflicting: more than one `status:` line, so quote or reword any status text in the body, which must not start a line with `status:` outside a code fence or blockquote; counts: the counts line is missing or repeated; fields: a required field is missing or an invariant is broken). Return the complete report again with its first line exactly `status: <token>`, where `<token>` is one of `<allowed tokens>`, and no other `status:` line<for code-review: , and its second line exactly `critical: <n> · warning: <n> · info: <n>`><for fields: , with each of `<required fields>` as a top-level `<label>:` line and `<the broken invariant>` holding, with nothing after the last field>. Do not post it anywhere." For code-review the prompt always restates both required lines; for `fields` it names the missing or empty fields and the invariant that broke.
14
16
  4. **Re-check the reply** the same way. Still a reask: no verdict, nothing posted for that round, no third ask, no inference from the body. Surface a HITL checkpoint in the chat naming the technique and the problem; Resolve stays blocked, and so does any step that needed the report; for a review the previous `VERIFY` comment stays latest.
15
17
  5. **No checker.** If the checker is missing, fails with exit 2, or prints non-JSON, apply the same rules by hand: the prose above stays the spec. Never guess a verdict.
@@ -18,7 +20,7 @@ The ticket engines call these by name. Each technique's full protocol lives in i
18
20
 
19
21
  | Technique | File | In one line |
20
22
  |---|---|---|
21
- | **Grilling** | `techniques/grilling.md` | interrogate the human, one question at a time, to converge on a decision |
23
+ | **Grilling** | `techniques/grilling.md` | interrogate the human, one question at a time, to converge on a decision; rounds run on `models.grill` (`trailhead-grill`), the session asks |
22
24
  | **Domain vocabulary** | `techniques/domain-vocabulary.md` | build a precise shared glossary so each term means one thing |
23
25
  | **Prototype** | `techniques/prototype.md` | throwaway artifact for "how should it look/behave"; routes UI mockups (disk / artifacts / stitch) |
24
26
  | **Research** | `techniques/research.md` | a focused subagent gathers a decision-ready fact from primary sources |
@@ -9,7 +9,7 @@ argument-hint: "[bug|todo|idea|seed|note] [--of <ticket>] <text>"
9
9
 
10
10
  ## Load first, in order
11
11
 
12
- Before doing anything, load `<skill base>/../_shared/load-first.md` and follow it: the shared-core load contract (the six core files, in order, then the effective config). `<skill base>` is this skill's **base directory** (Claude Code prints it as `Base directory for this skill:` when the skill loads; on another host, the directory holding this `SKILL.md`): resolve the path to an **absolute path** and open it with the **Read tool** on that absolute path. Never `cd` into a skills directory and never read a relative path from the shell (a `cd` compound followed by a read is denied by the search guard). The same rule covers every later `../_shared/...` or `references/...` path in this skill: resolve it against the base directory to an absolute path before reading. `_shared/` is a **sibling** of this cluster's own directory (at `../_shared/`), never a child of it; its absence from a listing of the cluster dir is expected, not a missing core.
12
+ Before doing anything, read two files, in order, and nothing else from `_shared/`: `<skill base>/../_shared/capture-core.md` (the compact capture reference: the standing rules, one lookup, the labels and create commands, the ticket body), then `<skill base>/references/capture.md`. `<skill base>` is this skill's **base directory** (Claude Code prints it as `Base directory for this skill:` when the skill loads; on another host, the directory holding this `SKILL.md`): resolve each path to an **absolute path** and open it with the **Read tool** on that absolute path. Never `cd` into a skills directory and never read a relative path from the shell (a `cd` compound followed by a read is denied by the search guard). `_shared/` is a **sibling** of this cluster's own directory (at `../_shared/`), never a child of it; its absence from a listing of the cluster dir is expected, not a missing core.
13
13
 
14
14
  A capture writes at most one issue; no isolation workspace is set up.
15
15
 
@@ -17,6 +17,4 @@ A capture writes at most one issue; no isolation workspace is set up.
17
17
 
18
18
  The **first word** of the arguments is the verb (`bug`, `todo`, `idea`, `seed`, or `note`); the rest is the text (or `--of <ticket> <text>` for `bug`).
19
19
 
20
- - **`bug`** / **`todo`** / **`idea`** / **`seed`** / **`note`** to **`references/capture.md`**: the full per-verb protocol and the note/idea/seed/todo spectrum. Read that file and follow it.
21
-
22
- The cross-cluster situational references and technique bodies these captures call live in `_shared/`; this cluster names each by its `../_shared/...` path where it needs one.
20
+ - **`bug`** / **`todo`** / **`idea`** / **`seed`** / **`note`** to **`references/capture.md`**: the full per-verb protocol and the note/idea/seed/todo spectrum. Follow it.
@@ -1,12 +1,12 @@
1
1
  # Capture: the verbs in detail
2
2
 
3
- Capture without breaking flow, but with the **tracker** as destination, not scattered files. The verbs are named in this cluster's `../SKILL.md`; here is the detail of each. One capture = one action + one confirmation line; it resolves nothing.
3
+ Capture without breaking flow, but with the **tracker** as destination, not scattered files. Here is the detail of each verb. One capture = one action + one confirmation line; it resolves nothing.
4
4
 
5
- **Confirmation line.** Confirm the capture by the ticket's name and its number, both visible (e.g. *Add login rate limit* (#45), never the name alone nor a bare `#number`), and where it landed. **If you point at working it later, lead with `/clear`**: the confirmation is a place you point at the next ticket, so the [Session handoff](../../_shared/session-handoff.md)'s `/clear`-first rule applies: a compact, future-conditional one-liner is fine (a capture is not a resolution, so no full labelled block), along the lines of *"to work it later: `/clear`, then `/trailhead:work 45`; or, for the lighter path, `/trailhead:quick 45`"* (in the user's conversation language). Never a bare `/trailhead:work 45` with no `/clear`, and (the same post-`/clear` invariant the [Session handoff](../../_shared/session-handoff.md) states) **never place after `/clear` an action the agent must perform that depends on context `/clear` erases**: a capture confirmation points only at user-runnable commands, which satisfies this by construction, so keep it that way. **Whenever the confirmation does point at working the ticket (a `seed` parked on its trigger, or a fog `note`/`idea` that is not a ticket yet, is the case where it does not), anchor the next step to `/trailhead:work <n>`, always:** it is the recommendation (the full engine, which can split and keeps the map record), and the confirmation names it first. `/trailhead:quick <n>` is the **lighter alternative** (a single alternative, not two): it works that same ticket whole right now, always size-triaged so on a small change it offers to skip the heavy steps (plan pass, TDD, full code review, goal-backward verify) on an explicit yes, grills only if needed, skips the map narrative book-keeping, and never splits; a map ticket stays on its map, so never call it "off the map". **Describe it by that purpose (the lighter, faster path), never as merely "without splitting"**: not splitting is a side effect, not what `quick` is for. It is offered *alongside* `work`, **never in its place** and **never rendered twice on one line**. This holds for a **whiteboard** capture too, where both apply (`quick` is the no-split variant of `work`): the anchor stays `/trailhead:work <n>` for consistency. Same spirit as chart's codified close (#147). Render the command in this host's command forms (the **Host command forms** rule in `../../_shared/load-first.md`).
5
+ **Confirmation line.** Confirm the capture by the ticket's name and its number, both visible (e.g. *Add login rate limit* (#45), never the name alone nor a bare `#number`), and where it landed. **If you point at working it later, lead with `/clear`**: the confirmation is a place you point at the next ticket, so the `/clear`-first rule in `capture-core.md` applies: a compact, future-conditional one-liner is fine (a capture is not a resolution, so no full labelled block), along the lines of *"to work it later: `/clear`, then `/trailhead:work 45`; or, for the lighter path, `/trailhead:quick 45`"* (in the user's conversation language). Never a bare `/trailhead:work 45` with no `/clear`, and (the same post-`/clear` invariant: nothing the agent must do after a `/clear`) **never place after `/clear` an action the agent must perform that depends on context `/clear` erases**: a capture confirmation points only at user-runnable commands, which satisfies this by construction, so keep it that way. **Whenever the confirmation does point at working the ticket (a `seed` parked on its trigger, or a fog `note`/`idea` that is not a ticket yet, is the case where it does not), anchor the next step to `/trailhead:work <n>`, always:** it is the recommendation (the full engine, which can split and keeps the map record), and the confirmation names it first. `/trailhead:quick <n>` is the **lighter alternative** (a single alternative, not two): it works that same ticket whole right now, always size-triaged so on a small change it offers to skip the heavy steps (plan pass, TDD, full code review, goal-backward verify) on an explicit yes, grills only if needed, skips the map narrative book-keeping, and never splits; a map ticket stays on its map, so never call it "off the map". **Describe it by that purpose (the lighter, faster path), never as merely "without splitting"**: not splitting is a side effect, not what `quick` is for. It is offered *alongside* `work`, **never in its place** and **never rendered twice on one line**. This holds for a **whiteboard** capture too, where both apply (`quick` is the no-split variant of `work`): the anchor stays `/trailhead:work <n>` for consistency. Same spirit as chart's codified close (#147). Render the command in this host's command forms (the host command forms rule in `capture-core.md`).
6
6
 
7
- **Language (do not skip):** the captured ticket (title + body) is written in `config.ticket.language` (default `en`), **regardless of the language you are chatting in**. A capture fired in Italian still lands as an English ticket. This is the easiest place to slip, because a capture echoes the user's one-line phrasing; translate it, do not mirror it. See the *Ticket language* rule in `../../_shared/ticket-language.md`.
7
+ **Language (do not skip):** the captured ticket (title + body) is written in `config.ticket.language` (default `en`), **regardless of the language you are chatting in**. A capture fired in Italian still lands as an English ticket. This is the easiest place to slip, because a capture echoes the user's one-line phrasing; translate it, do not mirror it. See the ticket language rule in `capture-core.md`.
8
8
 
9
- **Map or whiteboard:** a capture that produces a **ticket** (`todo`, `bug`, `seed`, and `idea` when it is sharp enough) must land on a **map** or on the **whiteboard** (map-less). **When a map is open, ask the user which** before creating it (active map vs whiteboard; several maps open -> the ask also picks the map). **Map eligibility is "is the map open", not "is it still in progress".** An **exhausted** map (destination reached) that is **still open** stays a valid capture target: a capture thematically tied to it is offered that map on the which-map ask, never silently dropped to the whiteboard just because the destination was reached (that would sever the thematic link). The exhausted state is a reason to *offer to close the map* (the dashboard's `exhausted · closeable` flag, see the dashboard freshness rule in `../../_shared/substrate.md`), never a reason to drop it as a capture target. Only a **closed** map is no longer eligible: then the whiteboard is the right fallback. Whiteboard tickets get the `trailhead:whiteboard` label and **no** map label or `Parent:`; map tickets get the map's `trailhead:map-<n>` label + native sub-issue edge. **No map open at all -> whiteboard, no ask** (nowhere else to go). The ask is also skipped for a `bug --of` whose origin resolves to an open map or the whiteboard (see the `bug` bullet in the capture forms). `note` and a non-sharp `idea` stay map-fog and skip this; with no open map they fall back to the global notes location (`$HOME/.claude/notes/`), or a now-sharp `idea` becomes a whiteboard ticket. **When a capture creates a whiteboard ticket, refresh the pinned dashboard** afterwards (a structural event: the whiteboard surface the dashboard indexes grew, and a whiteboard ticket has no native progress bar, so the dashboard is the only place it shows); a capture landing on a *map* does not (native sub-issue progress bars track map tickets). See *The whiteboard* and the dashboard freshness rule in `../../_shared/substrate.md`.
9
+ **Map or whiteboard:** a capture that produces a **ticket** (`todo`, `bug`, `seed`, and `idea` when it is sharp enough) must land on a **map** or on the **whiteboard** (map-less). **When a map is open, ask the user which** before creating it (active map vs whiteboard; several maps open -> the ask also picks the map). **Map eligibility is "is the map open", not "is it still in progress".** An **exhausted** map (destination reached) that is **still open** stays a valid capture target: a capture thematically tied to it is offered that map on the which-map ask, never silently dropped to the whiteboard just because the destination was reached (that would sever the thematic link). The exhausted state is a reason to *offer to close the map* (the dashboard's `exhausted · closeable` flag, see the dashboard freshness rule: a structural event refreshes it, a map ticket's resolve does not), never a reason to drop it as a capture target. Only a **closed** map is no longer eligible: then the whiteboard is the right fallback. Whiteboard tickets get the `trailhead:whiteboard` label and **no** map label or `Parent:`; map tickets get the map's `trailhead:map-<n>` label + native sub-issue edge. **No map open at all -> whiteboard, no ask** (nowhere else to go). **A capture fired mid-ticket** (scope that surfaced while working another ticket) follows this routing instead: the current map when the new work serves its destination and is not a line its *Out of scope* rules out, or another open map when the topic plainly is that map's (read the maps' titles and destinations, not their tickets), no ask; otherwise the which-map ask above, listing every open map by name and number (the current one first) and then the whiteboard; never the whiteboard silently while a map is open. Under an autonomous run a case that needs the ask is a human decision: record it as a pending follow-up, never guess. The ticket carries a `Surfaced from: <ticket name+link>` line, and you say in one line what you opened and where it landed. The ask is also skipped for a `bug --of` whose origin resolves to an open map or the whiteboard (see the `bug` bullet in the capture forms). `note` and a non-sharp `idea` stay map-fog and skip this; with no open map they fall back to the global notes location (`$HOME/.claude/notes/`), or a now-sharp `idea` becomes a whiteboard ticket. **When a capture creates a whiteboard ticket, refresh the pinned dashboard** afterwards with `node <templates>/trailhead-dashboard.js --update-only` (it never opens a dashboard of its own, see `capture-core.md`) (a structural event: the whiteboard surface the dashboard indexes grew, and a whiteboard ticket has no native progress bar, so the dashboard is the only place it shows); a capture landing on a *map* does not (native sub-issue progress bars track map tickets). The whiteboard has no native progress bar, which is why its births refresh the dashboard.
10
10
 
11
11
  - **`note`** → a verbatim line in `## Not yet specified` (outside a project → `$HOME/.claude/notes/`). No questions, no rewriting.
12
12
  - **`idea`** → fog by default in `## Not yet specified`; becomes a ticket only if already phrasable as a sharp question. An idea in the fog is **not executable**: to work it, it must first **graduate** into a ticket (which happens when the frontier reaches it, or when you can now phrase it sharply). A `todo`, by contrast, is born a `build` ticket and is workable immediately.
@@ -24,3 +24,28 @@ Capture without breaking flow, but with the **tracker** as destination, not scat
24
24
  | **todo** | a frontier ticket | **now** | *I'm doing it* |
25
25
 
26
26
  Discriminator: not even work → `note`; maybe, once it's clear → `idea`; yes, when X happens → `seed`; yes, now → `todo`. The close pairs: `note` vs `idea`: raw text vs a work-candidate meant to graduate; `idea` vs `seed`: waiting on *clarity* vs waiting on a *condition*; `seed` vs `todo`: committed *later* vs committed *now*.
27
+
28
+ ## Scripted capture for automations
29
+
30
+ For a run with no chat session (CI, cron, a bot), capture through the helper instead of hand-building labels: `node <templates>/trailhead-capture.js --type bug|todo|idea|seed --title <t> (--body <b> | --body-file <path|->) --repo <owner/name> [--map <n> | --of <n>] [--trigger <text>] [--unverified] [--dedupe-key <key>] [--no-dashboard]`. `--trigger` is required for `seed` and rejected otherwise; `--of` is only for `bug` and excludes `--map`; `--unverified` is rejected for `idea` (fog is already off the frontier); `--dedupe-key` matches `^[A-Za-z0-9._:-]{1,100}$`. Nothing is created when a pre-check fails.
31
+
32
+ For GitHub workflows, the composite Action `actions/capture` (`uses: ToRvaLDz/trailhead/actions/capture@<tag>`) is the ready-made wrapper: it maps its inputs 1:1 to these flags, runs this helper from the action checkout and exposes `issue`, `url` and `status` as outputs. It adds no logic of its own and needs `permissions: issues: write`.
33
+
34
+ **Output and exit codes.** One JSON line on stdout, always:
35
+ - `{"status":"created",...}`, exit 0: `issue`, `url`, `type`, `destination` (`map-<n>` or `whiteboard`; `none` for an `idea` without `--map`), `labels`, `trust` (`ok`, `allowlisted` or `actions-token`), `edge` (map tickets), `dashboard`, `warnings`. Printed only once the labels are read back and the trust is verified.
36
+ - `{"status":"existing",...}`, exit 0: the dedupe key matched an open issue, nothing was created.
37
+ - `{"status":"partial",...}`, exit 3: the issue is created and labelled but the native sub-issue edge failed after one retry (for example the 100 sub-issue cap). The `trailhead:map-<n>` label keeps it on the map frontier; the warning points at the prune command (`/trailhead:prune`). The exit is non-zero so a caller checking only the status never mistakes it for success.
38
+ - `{"status":"error","reason":...}`, exit 1 (`"trust":"insufficient"` on a trust failure, `issue` and `url` when an issue was created before the failure).
39
+ - `{"status":"usage","reason":...}`, exit 2, the message also on stderr.
40
+
41
+ **Trust.** The identity running the helper must be one the label guard Action never strips: write, maintain or admin access, or a login in the repository's own `TRAILHEAD_LABEL_ALLOWLIST` variable (the helper never consults its local environment). An identity `gh api user` cannot read (an Actions `GITHUB_TOKEN` or a GitHub App token) is verified after the create from the server-side record of who applied the labels: `github-actions[bot]` (its events trigger no workflow), an allowlisted or a write actor pass; anyone else, or unreadable events, exits 1 with `trust: insufficient` and no edge, leaving a plain inbox issue.
42
+
43
+ **Shapes.** `bug` is `trailhead:ticket` + `trailhead:bug`, `todo` is `trailhead:ticket` + `trailhead:build`, `seed` adds `trailhead:seed` + `trailhead:blocked` and a `Trigger:` line with a `## Blocked by` section. `--map <n>` adds `trailhead:map-<n>`, the `Parent:` first line and the native edge; otherwise `trailhead:whiteboard`. `--unverified` adds `trailhead:unverified`. `--of <n>` inherits the origin's map or the whiteboard (else exit 1, `origin destination unresolved`) and adds a `Regression of:` line.
44
+
45
+ **`idea` is parked fog, not a ticket.** It becomes a `trailhead:fog` issue (off the frontier, a clarification space) with no type label, no map label, no edge and no dashboard refresh; with `--map` its body opens with a `Parent:` line. It shows in the inbox's parked-fog list and the map render's fog count, but not in the map body's own `Not yet specified` list.
46
+
47
+ **Dedupe.** With `--dedupe-key` the body carries `<!-- trailhead-dedupe: <key> -->` and a rerun returns `existing`. The search is the fast path; when it returns its 1000-result cap the helper scans every open issue instead. The one residual is search-index lag: a near-simultaneous run may not yet see an issue created seconds earlier.
48
+
49
+ **Dashboard.** Only a whiteboard ticket refreshes the dashboard (`--update-only`, never opening one), unless `--no-dashboard`. A refresh failure leaves the status unchanged and reports `dashboard: "failed"`. Map tickets and fog report `not-applicable`.
50
+
51
+ **Settle residual.** There is no settle window: the labelling actor is verified as one the guard never acts on before any edge and before `created` is printed, so no asynchronous strip can follow a `created`.
@@ -36,7 +36,7 @@ Read **`references/inbox.md`** and follow it. It triages issues opened by others
36
36
 
37
37
  ## Standalone grill: `grill [topic|ticket]`
38
38
 
39
- Run a standalone **Grilling** (plus **Domain vocabulary**) session on a decision or topic, or on a named ticket, without committing to the full work cycle. Read the two techniques and follow them: `../_shared/techniques/grilling.md` and `../_shared/techniques/domain-vocabulary.md` (these are multi-cluster techniques, so their bodies live in `_shared/`). Before the final confirmation of the choices, run the **Holistic review before the final confirmation** defined in that technique. Record the outcome where it belongs: a ticket's resolution, the map's `Decisions so far`, or a fresh `decision` ticket. **When the session's decision unblocks or spawns follow-on tickets** (builds graduating from a resolved blocker, or siblings the decision implies), opening them **defers to the shared "Map or whiteboard" rule** (`../trailhead-capture/references/capture.md`): when one or more maps are open, ask the user which destination (a thematically eligible map vs the whiteboard), never silently picking the whiteboard and never treating "no coherent map" as a reason to auto-drop them there. Only with **no** open map does the whiteboard follow without asking.
39
+ Run a standalone **Grilling** (plus **Domain vocabulary**) session on a decision or topic, or on a named ticket, without committing to the full work cycle. Read the two techniques and follow them: `../_shared/techniques/grilling.md` and `../_shared/techniques/domain-vocabulary.md` (these are multi-cluster techniques, so their bodies live in `_shared/`). Before any question, first run the routing step at the top of `grilling.md`, then follow **Who runs a round** there (`models.grill`); this session keeps asking the user. Before the final confirmation of the choices, run the **Holistic review before the final confirmation** defined in that technique. Record the outcome where it belongs: a ticket's resolution, the map's `Decisions so far`, or a fresh `decision` ticket. **When the session resolves a `decision` ticket**, its close follows the decision engine's **Carry it into build** step (`../trailhead-work/references/ticket-engines.md`): the build split goes into the final confirmation, the build tickets are created, and the resolution comment carries the `Builds:` line before `gh issue close`. **When the session's decision unblocks or spawns follow-on tickets** (builds graduating from a resolved blocker, or siblings the decision implies), opening them **defers to the shared "Map or whiteboard" rule** (`../trailhead-capture/references/capture.md`): when one or more maps are open, ask the user which destination (a thematically eligible map vs the whiteboard), never silently picking the whiteboard and never treating "no coherent map" as a reason to auto-drop them there. Only with **no** open map does the whiteboard follow without asking.
40
40
 
41
41
  ## First-time config offer (end of chart / adopt)
42
42
 
@@ -6,10 +6,10 @@ The two ways a map is born. Read this when the invocation is `/trailhead:new` or
6
6
  The user invokes with a loose idea.
7
7
 
8
8
  0. **Ensure a tracker repo exists (offer to create one if missing).** Everything trailhead writes lives on a GitHub repo's Issues, so before charting, confirm the current directory has one. Detect it with `gh repo view`: on success a GitHub repo with a remote is present, so say nothing and proceed. If there is **no GitHub repo** (no `origin` / GitHub remote, or `gh repo view` fails), **stop and ask the user** whether to create one, offering **private (default)** or **public**. On their choice: if the directory is not yet a git repo, `git init` and make an initial commit first; then `gh repo create <name> --private|--public --source=. --remote=origin --push` (take `<name>` from the directory, confirmed with the user), so the repo exists on GitHub with `origin` set. Charting continues only once a GitHub repo is in place: every step below writes to its Issues. If the user declines to create one, there is nowhere to chart, so stop and say so. This never fires when a repo already exists (`/trailhead:adopt` and re-charting on an existing repo skip straight past it).
9
- 1. **Name the destination.** Run the **Grilling** + **Domain vocabulary** techniques to pin down what this map tends toward. The destination fixes the scope, so it's settled first. Default: a working artifact.
10
- 2. **Map the frontier.** Grill again, **breadth-first**: **enumerate every functional area the destination implies** and **capture each one (headed for a fog entry under *Not yet specified* at minimum) before deep-diving any single area**. Fan out across the whole space rather than deep on one thread, surfacing the open decisions and the first steps takeable now. **The deliverable is the coverage the map will carry, not the grilling transcript: every area the destination implies must land on the map at step 3, as fog or as a ticket, so nothing is silently missing.** **If no fog surfaces** (the way is already clear, the whole thing fits one session) you don't need a map: stop and ask how to proceed. **Harvest parked work:** from **earlier maps on this repo**, gather the artifacts their *Out of scope* `→ future map` breadcrumbs point at (**parked seeds/todos**, see **Out of scope**) **and the ideas** left in their *Not yet specified* fog, then **ask the user which belong in *this* map**; pull the chosen seeds/todos in as tickets and the chosen ideas in as fog (or as tickets, if now sharp enough), and leave (or update the destination on) the rest.
9
+ 1. **Name the destination.** Run the **Grilling** + **Domain vocabulary** techniques (first run the routing step at the top of `../../_shared/techniques/grilling.md`, then the rounds as **Who runs a round** there says) to pin down what this map tends toward. The destination fixes the scope, so it's settled first. Default: a working artifact.
10
+ 2. **Map the frontier.** Grill again (first run the routing step at the top of `../../_shared/techniques/grilling.md`, then the rounds as **Who runs a round** there says), **breadth-first**: **enumerate every functional area the destination implies** and **capture each one (headed for a fog entry under *Not yet specified* at minimum) before deep-diving any single area**. Fan out across the whole space rather than deep on one thread, surfacing the open decisions and the first steps takeable now. **The deliverable is the coverage the map will carry, not the grilling transcript: every area the destination implies must land on the map at step 3, as fog or as a ticket, so nothing is silently missing.** **If no fog surfaces** (the way is already clear, the whole thing fits one session) you don't need a map: stop and ask how to proceed. **Harvest parked work:** from **earlier maps on this repo**, gather the artifacts their *Out of scope* `→ future map` breadcrumbs point at (**parked seeds/todos**, see **Out of scope**) **and the ideas** left in their *Not yet specified* fog, then **ask the user which belong in *this* map**; pull the chosen seeds/todos in as tickets and the chosen ideas in as fog (or as tickets, if now sharp enough), and leave (or update the destination on) the rest.
11
11
  3. **Create the map** (`trailhead:map`), after the **Holistic review before the final confirmation** of the Grilling technique has run on the step 1 destination and the step 2 choices together: Destination and Notes filled in, Decisions-so-far empty, the fog sketched into *Not yet specified*. **Every functional area enumerated in step 2 must appear here, as a fog entry or a ticket, before moving on: the map is the coverage deliverable.** Run the **first-use repo setup** (see **Substrate** → `../../_shared/substrate-commands.md`) now: labels **and** the label-guard check/install. Once the map issue has its number `<n>`, **create its per-map label `trailhead:map-<n>`** (every ticket of this map carries it and is a native sub-issue of the map; the map issue itself is found by its number, so it does **not** carry the label; see **Multiple maps on one repo**). **If this repo already has another open `trailhead:map`**, tell the user the new map runs concurrently (its own scoped frontier) and set it as the active map (`.trailhead/active-map`). Then set up **conventions**: if no `trailhead:conventions` issue exists (singleton lookup: see **Singletons** in `../../_shared/substrate.md`), run a **brief conventions brainstorming** (breadth-first, in the spirit of grilling but short: a handful of questions, not a full session) to surface the project's way of working: `git:` `main`|`pr`, `release:` `command`|`auto`, and `isolation:` `auto`|`none`|`worktree`|`clone` (default **`auto`**, which decides per ticket at work-start by asking *"will more than one ticket be worked at once on this machine this session?"* and picking `worktree` for a plain repo or `clone` for a submodule superrepo; a project can force `none`/`worktree`/`clone` explicitly), **plus** the narrative a new contributor would need: release/deploy specifics (environments, manual steps, OTA vs full build, versioning), tooling that must be driven a certain way (a specific MCP/CLI, e.g. a Lovable project piloted via its MCP), code & style rules, testing/UAT norms. Distil into the issue (header + prose); link it from the map's Notes. Don't invent: leave out or ask what you can't determine. If one already exists, just link it (reuse across maps). Finally, **ensure the repo's `trailhead:dashboard` index issue exists** (see **The dashboard**): create and pin it if missing (the fixed 3rd pinned slot, alongside codebase + conventions), then **write it to list this map** (charting a map is a structural event); if it already exists, refresh it.
12
- 4. **Create the tickets you can specify now** as child issues (each with `trailhead:ticket` + its type + this map's `trailhead:map-<n>` label, **plus `trailhead:blocked` when the mapped dependency graph already shows it has a blocker**, and set as a **native sub-issue** of the map, see **Child→map link** under Substrate), then wire the blocking in a **second pass** (issues need ids before they can reference each other): for each ticket **read its Question and wire a blocker for every still-open ticket whose output it consumes** (don't stop at the first/obvious one) by wiring its `## Blocked by` line + native dependency together (moves 1-2 of the three-move wiring; the `trailhead:blocked` label, move 3, already rode in at creation, see **Substrate**). **Set the blocked label at creation, not via a post-creation `--add-label` pass:** a bulk `--add-label` burst spawns one repo-side label-guard run per ticket and reddens the Actions history with sheddable no-op runs; riding the label in the ticket's `issues.opened` event avoids that entirely. Wiring sorts them into frontier (unblocked) and blocked; the rest stays fog.
12
+ 4. **Create the tickets you can specify now** (the **Ticket or fog** test below) as child issues (each with `trailhead:ticket` + its type + this map's `trailhead:map-<n>` label, **plus `trailhead:blocked` when the mapped dependency graph already shows it has a blocker**, and set as a **native sub-issue** of the map, see **Child→map link** under Substrate), then wire the blocking in a **second pass** (issues need ids before they can reference each other): for each ticket **read its Question and wire a blocker for every still-open ticket whose output it consumes** (don't stop at the first/obvious one) by wiring its `## Blocked by` line + native dependency together (moves 1-2 of the three-move wiring; the `trailhead:blocked` label, move 3, already rode in at creation, see **Substrate**). **Set the blocked label at creation, not via a post-creation `--add-label` pass:** a bulk `--add-label` burst spawns one repo-side label-guard run per ticket and reddens the Actions history with sheddable no-op runs; riding the label in the ticket's `issues.opened` event avoids that entirely. Wiring sorts them into frontier (unblocked) and blocked; the rest stays fog. **Ticket or fog.** Run it on every area from step 2: "specify now" means you can write the area's `## Question` now, sized to one session, not that it can be answered now. An area whose Question you can write is a ticket, created already blocked (`trailhead:blocked` plus its wiring) on the tickets whose answers it needs: being blocked is not a reason to leave it in the fog, because a blocked ticket graduates onto the frontier by itself when its blockers close, while a fog entry waits for someone to dissolve it. An area stays fog only when you **cannot phrase** its Question yet (you don't know what to ask: "Monetization" before the audience is known), or when an open decision could **reshape** the question itself (not just its answer), in which case the fog entry names that decision so it is re-examined when that decision closes. An area too big for one session is several tickets, or one ticket plus fog for the rest. Volume is not a reason to keep an area in the fog: the only brake is GitHub's 100 sub-issue cap per map, and near it, say so and keep the furthest-out areas as fog.
13
13
  5. **Fire the research subagents.** For each `research` ticket created, run the **Research** technique in parallel; each agent returns its report, and the orchestrator posts it on the ticket as a `RESEARCH` comment (no branch: the report is the artifact; see the technique for the `found` / `inconclusive` / `needs_input` branches).
14
14
  6. **Offer config, then stop.** Charting is one session's work; it hand-resolves nothing. Before ending, make the **first-time config offer** (see **First-time config offer** in `../SKILL.md`): the project now has a fresh map and no `.trailhead/config.json`, so put the **two-way choice** to the user, **accept as-is** (writes `{}`) or **run `/trailhead:config`** for the guided menu, and let them pick. Name both paths; don't pre-fill the config keys and ask for an inline edit, and don't interview them key-by-key here (that's `/trailhead:config`'s job). Once the file exists it's never offered again. **No `trailhead:codebase` issue is created here**: greenfield has no code to map yet. It's created later, once the repo has substantial code (see Mode 2 step 1 and the **Codebase** row under Substrate), so at chart two of the three permanent pins (the dashboard + conventions) are filled and the codebase slot fills in later. Maps are never pinned.
15
15
 
@@ -27,8 +27,8 @@ Same rules as the handoff block: **name the ticket, put its number (never the ti
27
27
  When the code already exists (project in progress). Like Mode 1, but start from **reality**, not a blank page. One heavy step at entry, then all lean.
28
28
 
29
29
  1. **Map the codebase (once per repo).** Heavy step, *only* at adoption: it does not repeat per ticket, and **not per map**. First check for an existing **`trailhead:codebase`** issue (singleton lookup: see **Singletons** in `../../_shared/substrate.md`): if one exists (an earlier map on this repo created it) and the code hasn't materially drifted, just **link it** from this map's Notes and skip the fan-out. Otherwise run the **Codebase map** technique (a parallel fan-out of reader subagents) for a structured understanding (architecture, stack, decisions already embodied, risk areas) and distil the essence into the repo's **`trailhead:codebase` issue** (create it, label `trailhead:codebase`), then link that issue from the map's Notes. Discard the raw. Because it's a standalone repo-scoped issue, the next map reuses it instead of re-deriving or losing it.
30
- 2. **Name the destination of the remaining stretch.** **Grilling** + **Domain vocabulary**, **seeded by the map you just built**: not "what is the project" but "what's left to reach working / the next milestone"; close with the **Holistic review before the final confirmation** of the Grilling technique.
30
+ 2. **Name the destination of the remaining stretch.** **Grilling** + **Domain vocabulary** (first run the routing step at the top of `../../_shared/techniques/grilling.md`, then the rounds as **Who runs a round** there says), **seeded by the map you just built**: not "what is the project" but "what's left to reach working / the next milestone"; close with the **Holistic review before the final confirmation** of the Grilling technique.
31
31
  3. **Backfill the decisions already made.** Choices already embodied in the code (now visible from the map) or stated by the user go into `Decisions so far` as **ticket-less** lines (they're already closed), so the map reflects reality and doesn't pretend greenfield. Link to code/commits where useful.
32
- 4. **Map the frontier of the remainder**, **breadth-first as in Mode 1 step 2**, closing with the **Holistic review before the final confirmation** of the Grilling technique over this step's choices and step 2's destination together: enumerate every functional area still standing between the code and the destination and **land each on the map (fog in *Not yet specified* at minimum) before deep-diving any one**, so the map makes every remaining section visible; the map, not the transcript, is the deliverable to check for that coverage. This yields → tickets specifiable now + fog in *Not yet specified*, then wire the blocking and fire the research. **Harvest parked work** (todos *and* ideas) from earlier maps on this repo (their `→ future map` *Out of scope* lines and their *Not yet specified* fog), asking the user which belong here, as in Mode 1 step 2. As Mode 1, steps 4–6.
32
+ 4. **Map the frontier of the remainder**, **breadth-first as in Mode 1 step 2** (first run the routing step at the top of `../../_shared/techniques/grilling.md`, then the rounds as **Who runs a round** there says), closing with the **Holistic review before the final confirmation** of the Grilling technique over this step's choices and step 2's destination together: enumerate every functional area still standing between the code and the destination and **land each on the map (fog in *Not yet specified* at minimum) before deep-diving any one**, so the map makes every remaining section visible; the map, not the transcript, is the deliverable to check for that coverage. This yields → tickets specifiable now + fog in *Not yet specified*, then wire the blocking and fire the research. **Harvest parked work** (todos *and* ideas) from earlier maps on this repo (their `→ future map` *Out of scope* lines and their *Not yet specified* fog), asking the user which belong here, as in Mode 1 step 2. As Mode 1, steps 4–6.
33
33
 
34
34
  The tracker is the existing repo's (`gh` in its directory); run the **first-use repo setup** (see `../../_shared/substrate-commands.md`) on first use (missing `trailhead:*` labels **and** the label-guard check/install (adopting an existing repo is exactly when the guard tends to be missing)) and set up the `trailhead:conventions` issue via a **brief conventions brainstorming** (see Mode 1 step 3), but here, **seed the questions from what's discoverable** so you confirm rather than ask blind: an existing `CLAUDE.md`/`AGENTS.md`, CI workflows, the PR-vs-`main` git history, package scripts, deploy config. **Detect a monorepo** (a `pnpm-workspace.yaml`, a `workspaces` field, an `nx.json`/`turbo.json`, or several packages under `packages/`|`apps/`) and, if found, keep the default `isolation: auto` (it picks `worktree` for such a plain/monorepo checkout at work-start) and note that tickets should carry a `Scope:` line, **unless the buildable units are path-bound** (see below). **Detect git submodules** (a `.gitmodules` file) and, if found, add the prose rule that submodule work is committed **inside the submodule (on a branch), published, then a gitlink bump in the parent**, and that a **submodule-bearing superrepo resolves to `clone`** under the default `auto` (a superproject `worktree` cannot safely carry submodule state, so worktree is escalated to `clone`); a submodule path is a natural `Scope:` (see **Working as a team**). **For path-bound tooling** (a React Native/Expo app, native toolchains, a package whose build depends on a local `node_modules`/bundler cache/absolute-path config): those don't build from a worktree, so for them prefer an explicit **`isolation: clone`** (a per-ticket clone that installs its own deps and builds), or `isolation: none` + **serialize** if the user doesn't want per-ticket clones. Worktree is for units that build from any path. Propose the values you inferred and let the human correct/complete them; create the issue if absent, link it from the map's Notes. Also **ensure the repo's `trailhead:dashboard` index issue exists** (see **The dashboard**): create and pin it if missing (the fixed 3rd pinned slot, alongside codebase + conventions) and write it to list this map.
@@ -4,10 +4,10 @@
4
4
 
5
5
  `/trailhead:inbox` presents **three sections**, each named for what it holds. **Present them to the user by these descriptive names, never as a bare letter or "section A/B/C"** (result-oriented output: no internal labels). The three:
6
6
 
7
- **Incoming issues to triage.** Open issues carrying **no** `trailhead:*` label (plus any `trailhead:unverified` from the trust guard):
7
+ **Incoming issues to triage.** Open issues carrying **no** `trailhead:*` label (plus any `trailhead:unverified`, applied by a session under the Trust rule in `../../_shared/teamwork.md`; the guard Action strips labels instead):
8
8
  ```bash
9
9
  gh issue list --state open --json number,title,author,labels \
10
- | jq '[.[] | select([.labels[].name] | any(startswith("trailhead:")) | not)]'
10
+ | jq '[.[] | select(([.labels[].name] | any(startswith("trailhead:")) | not) or ([.labels[].name] | any(. == "trailhead:unverified")))]'
11
11
  ```
12
12
 
13
13
  **Parked fog ready to graduate?** The open `trailhead:fog` issues, **most-recently-active first**: recent discussion is exactly where the fog has likely cleared, so you review those, not all of them:
@@ -26,4 +26,4 @@ For each issue in the first two sections (incoming issues, parked fog), propose
26
26
  - **Duplicate.** In the proposal, name what each one covers (the issue and the existing ticket) and where they differ; only when the existing ticket covers all of it, link it and close. A partial overlap is not a duplicate: link the two and triage the issue by the other moves.
27
27
  - **Needs info.** Ask the reporter for a repro or specifics; leave it open.
28
28
 
29
- A rogue issue wearing a `trailhead:*` label it shouldn't (now `trailhead:unverified`) is triaged the same way: **adopt** (a maintainer applies the real labels + `Parent:`) or **reject** (strip the `trailhead:*` labels, leave it a normal inbound issue). Never adopt by trusting a label that was already there; you apply the labels on adoption.
29
+ A rogue issue wearing a `trailhead:*` label it shouldn't (one whose label came from outside the trusted labellers of the Trust rule in `../../_shared/teamwork.md#trust--provenance`, so a label from the bot, an allowlisted login or a write+ collaborator is not rogue; now `trailhead:unverified`) is triaged the same way: **adopt** (a maintainer applies the real labels + `Parent:`) or **reject** (strip the `trailhead:*` labels, leave it a normal inbound issue). Never adopt by trusting a label that was already there; you apply the labels on adoption.