@marcomigozzi/trailhead 0.10.0 → 0.11.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.
@@ -75,6 +75,34 @@ ok('Bash cat .SECRETS is denied (case-insensitive)', !!detectSecretRead('Bash',
75
75
  ok('Read ENV.SAMPLE (near-miss) is still allowed regardless of case', !detectSecretRead('Read', { file_path: 'ENV.SAMPLE' }));
76
76
  ok('Read .ENVIRONMENT (near-miss) is still allowed regardless of case', !detectSecretRead('Read', { file_path: '.ENVIRONMENT' }));
77
77
 
78
+ // --- #180: docker/podman --env-file must not be treated as a secret read ---
79
+ // The container runtime reads the file itself; its contents never reach the
80
+ // agent, so `--env-file .env` (or `=`-glued) is not a secret READ by the
81
+ // agent, unlike every other flag/positional in the same statement.
82
+ ok('Bash docker run --env-file .env img is allowed (runtime reads it, not the agent)',
83
+ !detectSecretRead('Bash', { command: 'docker run --env-file .env img' }));
84
+ ok('Bash docker compose --env-file .env.prod up is allowed',
85
+ !detectSecretRead('Bash', { command: 'docker compose --env-file .env.prod up' }));
86
+ ok('Bash podman run --env-file=.env img is allowed (glued = form)',
87
+ !detectSecretRead('Bash', { command: 'podman run --env-file=.env img' }));
88
+ ok('Bash docker-compose --env-file .env up is allowed (standalone binary)',
89
+ !detectSecretRead('Bash', { command: 'docker-compose --env-file .env up' }));
90
+ ok('Bash bash -c "docker run --env-file .env img" is allowed (recursion into nested command)',
91
+ !detectSecretRead('Bash', { command: 'bash -c "docker run --env-file .env img"' }));
92
+
93
+ // --- #180: everything else in a docker/podman statement is still scanned ---
94
+ ok('Bash cat .env is still denied', !!detectSecretRead('Bash', { command: 'cat .env' }));
95
+ ok('Bash foo --env-file .env is still denied (only docker/podman are exempt)',
96
+ !!detectSecretRead('Bash', { command: 'foo --env-file .env' }));
97
+ ok('Bash foo --env-file=.env is still denied (only docker/podman are exempt)',
98
+ !!detectSecretRead('Bash', { command: 'foo --env-file=.env' }));
99
+ ok('Bash docker run --env-file .env img && cat .env is still denied (second statement)',
100
+ !!detectSecretRead('Bash', { command: 'docker run --env-file .env img && cat .env' }));
101
+ ok('Bash docker run -v x img cat .env is still denied (positional after image, conservative)',
102
+ !!detectSecretRead('Bash', { command: 'docker run -v x img cat .env' }));
103
+ ok('Bash docker cp .env ctr:/x is still denied (plain positional, not --env-file)',
104
+ !!detectSecretRead('Bash', { command: 'docker cp .env ctr:/x' }));
105
+
78
106
  // --- end-to-end hook wire format ---
79
107
  const d1 = runHook('Read', { file_path: '.env' });
80
108
  ok('hook denies Read(.env) end-to-end (exit 2, block decision)', d1.code === 2 && /"decision":"block"/.test(d1.out));
@@ -88,6 +116,19 @@ ok('hook allows Read(src/app.ts) end-to-end (exit 0, no output)', a1.code === 0
88
116
  const a2 = runHook('Bash', { command: 'cd app && grep x pubspec.yaml' });
89
117
  ok('hook allows Bash(cd app && grep x pubspec.yaml) end-to-end (exit 0, no output)', a2.code === 0 && a2.out.trim() === '');
90
118
 
119
+ // --- #180 end-to-end ---
120
+ const a3 = runHook('Bash', { command: 'docker run --env-file .env img' });
121
+ ok('hook allows Bash(docker run --env-file .env img) end-to-end (exit 0, no output)', a3.code === 0 && a3.out.trim() === '');
122
+
123
+ const a4 = runHook('Bash', { command: 'docker compose --env-file .env.prod up' });
124
+ ok('hook allows Bash(docker compose --env-file .env.prod up) end-to-end (exit 0, no output)', a4.code === 0 && a4.out.trim() === '');
125
+
126
+ const a5 = runHook('Bash', { command: 'podman run --env-file=.env img' });
127
+ ok('hook allows Bash(podman run --env-file=.env img) end-to-end (exit 0, no output)', a5.code === 0 && a5.out.trim() === '');
128
+
129
+ const d3 = runHook('Bash', { command: 'cat .env' });
130
+ ok('hook still denies Bash(cat .env) end-to-end (exit 2, block decision)', d3.code === 2 && /"decision":"block"/.test(d3.out));
131
+
91
132
  // --- crash safety ---
92
133
  const crash1 = runHook('Read', undefined);
93
134
  ok('missing tool_input never crashes (exit 0)', crash1.code === 0);
@@ -37,9 +37,9 @@ Effective config = built-in defaults ← global `~/.claude/trailhead/config.json
37
37
 
38
38
  **Inline update check (hookless hosts).** On a host without a hook bus (the descriptor's `hookBus == 'none'`) the `trailhead-check-update.js` SessionStart hook and the statusline flag are both absent, so the engine does the check **inline at session start**, co-located with the models-collapse notice above. It is lightweight and degrades silently: read the shared cache (`$XDG_CACHE_HOME/trailhead/update-check.json`, the same file the hook writes on Claude Code) and, if it reports an update, surface a one-line notice pointing at `/trailhead:update` (the pure `updateNotice(host, cache)` helper in `bin/lib/host-descriptor.js` is the canonical wording, and returns null on a host that already notifies via the hook, so it never double-fires); if the cache is absent or stale (older than a few hours), do a minimal throttled compare of the installed `VERSION` against the latest (npm registry or the GitHub release, per install channel) and rewrite the cache **atomically** (write a temp file in the same directory as the target, then rename it over the target, exactly as the `trailhead-check-update.js` SessionStart hook does with `out + '.tmp'` then `renameSync`), so a crash mid-write cannot leave a truncated or corrupt cache. Never block or fail the session on a network or parse error. `/trailhead:update` stays the on-demand installer.
39
39
 
40
- If a value is unset, it resolves to its **Balanced**-profile default for that activity (the default profile: a project with no config still gets the Balanced tier per key, resolved to a concrete id at load time, strong → `claude-opus-4-8`, standard → Sonnet-class, fast → Haiku-class). To use the session model instead of the Balanced default, set the key explicitly to `inherit session`. Never hard-fail on an unknown id: warn and inherit the session model.
40
+ If a value is unset, it resolves to its **Balanced**-profile default for that activity (the default profile: a project with no config still gets the Balanced tier per key, resolved to a concrete id at load time, strong → `claude-opus-5-5`, standard → `claude-sonnet-5-5`, fast → Haiku-class). To use the session model instead of the Balanced default, set the key explicitly to `inherit session`. Never hard-fail on an unknown id: warn and inherit the session model.
41
41
 
42
- **Everything else runs in the main session on its own model, there is no key for it:** charting, grilling, naming the destination, framing and **writing the tickets**, graduating fog, and map/decision operations. That's deliberate: it's HITL/orchestration work (you reason *with* the user), so the model that writes a ticket is the one grilling you; it can't fan out to a subagent. Practical upshot: **run a charting session (`/trailhead:new`/`:adopt`/`:ticket`) on your strong model**. The `models.*` keys above govern only the fan-out activities, not the quality of the tickets themselves. **Store a full model id** (e.g. `claude-opus-5`, `claude-fable-5`), never a bare family alias like `opus`; a family now spans several live versions, so an alias can't say which one. (On Claude Code the `Agent` tool takes only a family alias, so at subagent spawn the stored id is mapped to its alias, see the dispatch rule in `techniques.md`; the full id is still what gets stored, so the exact version stays on record and drives the inline-vs-subagent decision, even though the alias-only spawn cannot convey that version to the subagent.) **`models.plan` and `models.execute` are always distinct choices**: never collapse them into one shared model pick, even when the user would pick the same id for both.
42
+ **Everything else runs in the main session on its own model, there is no key for it:** charting, grilling, naming the destination, framing and **writing the tickets**, graduating fog, and map/decision operations. That's deliberate: it's HITL/orchestration work (you reason *with* the user), so the model that writes a ticket is the one grilling you; it can't fan out to a subagent. Practical upshot: **run a charting session (`/trailhead:new`/`:adopt`/`:ticket`) on your strong model**. The `models.*` keys above govern only the fan-out activities, not the quality of the tickets themselves. **Store a full model id** (e.g. `claude-opus-5-5`, `claude-fable-5`), never a bare family alias like `opus`; a family now spans several live versions, so an alias can't say which one. (On Claude Code the `Agent` tool takes only a family alias, so at subagent spawn the stored id is mapped to its alias, see the dispatch rule in `techniques.md`; the full id is still what gets stored, so the exact version stays on record and drives the inline-vs-subagent decision, even though the alias-only spawn cannot convey that version to the subagent.) **`models.plan` and `models.execute` are always distinct choices**: never collapse them into one shared model pick, even when the user would pick the same id for both.
43
43
 
44
44
  **Model-tier profiles.** Three profile seeds for the guided setup's Models step (§4), one philosophy: *smart at every level* (research is never the strongest tier; execute and debug are never the fastest). Choosing a profile **applies all seven keys directly** (no further per-key questions); the separate **Manual** path is where `plan`/`execute`/the rest are picked one by one. **The default profile is `Balanced`** (the **(consigliato)** one): a project with no config resolves its seven model keys as Balanced. **High** and **Low** are the alternatives, and **Manual** lets you set each key yourself (including `inherit session` per key). `fix` follows `execute`, so it needs no profile entry of its own.
45
45
 
@@ -50,7 +50,7 @@ If a value is unset, it resolves to its **Balanced**-profile default for that ac
50
50
  | **Low** | standard | standard | fast | fast | standard | fast | fast |
51
51
 
52
52
  **Tier-class -> id**, resolved from the lineup actually reachable at setup time (verify, don't trust these verbatim):
53
- - **Claude** (`models.*`, ids only, no effort): strong = Opus-class, resolved to **`claude-opus-4-8`** by default (the newer `claude-opus-5` stays offered in the per-key menus, but is not the default pick) · standard = Sonnet-class · fast = Haiku-class.
53
+ - **Claude** (`models.*`, ids only, no effort): strong = Opus-class, resolved to **`claude-opus-5-5`** by default (`claude-opus-5` stays offered in the per-key menus) · standard = Sonnet-class, resolved to **`claude-sonnet-5-5`** by default (so Balanced's `execute` runs on Sonnet 5.5; `claude-sonnet-5` stays offered in the per-key menus) · fast = Haiku-class.
54
54
  - **Codex** (`models.codex.*`, `{model, effort}`): strong = `gpt-5.6-sol` @ `high` (debug on **High** may be `xhigh`) · standard = `gpt-5.6-terra` @ `medium` · fast = `gpt-5.6-luna` @ `low`.
55
55
 
56
56
  **Ticket language.** `ticket.language` (ISO 639-1, default `en`) sets the language trailhead **writes** in on the Issues: ticket titles and bodies, engine comments (DISCUSS/PLAN/VERIFY/OPTIONS/REPRO/DIAGNOSIS/PAUSED/resolution), the map body sections, and commit message **descriptions**. It's fully decoupled from the language the agent *converses* in (its session/global setting); setting `ticket.language` never changes how the agent talks to the user. **Untouched by it:** code and identifiers, the fixed `trailhead:*` label names, config keys/values, and the conventional-commit type prefix (`feat:`/`fix:` stay English). The skill's own source docs (SKILL.md, `references/`) are not project prose and stay English.
@@ -80,7 +80,7 @@ On **trivial/small** the engine surfaces **one consolidated skip offer** and act
80
80
 
81
81
  Example `config.json` (same shape for the project `.trailhead/config.json` and the global `~/.claude/trailhead/config.json`):
82
82
  ```json
83
- { "models": { "plan": "claude-opus-4-8", "execute": "claude-sonnet-5" }, "tdd": "seams", "acceptance": { "browser": "auto" } }
83
+ { "models": { "plan": "claude-opus-5-5", "execute": "claude-sonnet-5-5" }, "tdd": "seams", "acceptance": { "browser": "auto" } }
84
84
  ```
85
85
 
86
86
  ## Guided setup
@@ -98,7 +98,7 @@ Example `config.json` (same shape for the project `.trailhead/config.json` and t
98
98
  3. **🌐 Ticket language**: the language trailhead writes its GitHub prose & commit descriptions in (`ticket.language`, ISO 639-1). Offer 🇬🇧 English (`en`, default) · 🇮🇹 Italiano (`it`) · 🇪🇸 Español (`es`) · 🇫🇷 Français (`fr`) · 🇩🇪 Deutsch (`de`) · 🇵🇹 Português (`pt`), plus a free-code option for any other ISO 639-1 code. Note in the prompt that this is *not* the language the agent converses in.
99
99
  4. **🧠 Models**: a **profile** (Balanced by default) applied whole, or **Manual** for the per-key questions.
100
100
  - **🎚️ Profile, asked first (and often the only Models question).** Present one `AskUserQuestion` menu: 🏔️ **High** · ⚖️ **Balanced (consigliato)** · 🍃 **Low** · ✍️ **Manual (specify each model)**. **On a project with no existing config, pre-select ⚖️ Balanced** (the default profile); when a config already exists, pre-select the profile its keys match (or **Manual** when they match none). **On High / Balanced / Low this is terminal:** seed the seven model keys from the **Model-tier profiles** table above, resolving each tier-class to a concrete id **at setup time** (verify the lineup, don't trust ids verbatim), **write them, and do NOT ask the per-key questions**. Only **Manual** proceeds to the per-key questions below (where any key may be left at `inherit session`). **Host branch:** on **Claude** the profile seeds `models.*` with tier-class ids; on **Codex** it seeds `models.codex.*` as `{model, effort}` (effort is the Codex-only face), and after applying keep the **re-run the installer** instruction from the Codex per-key path below so the pins are re-projected into `~/.codex/agents/trailhead-*.toml`.
101
- - **The per-key questions (Manual path only): always two separate questions, one for `plan` and one for `execute`; never merge them into a single model pick.** Ask each as its own `AskUserQuestion` menu (current value pre-selected), and offer the **same versioned lineup** to both. Each option is a **full model id with its version**; never a bare family alias (`opus`/`sonnet`/`haiku`), which can't say *which* live version. Build the list from the models actually reachable **at setup time** (verify, don't trust this file verbatim), offering **at least the two most recent versions per family**, e.g. `claude-fable-5`, `claude-opus-5` / `claude-opus-4-8`, `claude-sonnet-5` / `claude-sonnet-4-8`, `claude-haiku-4-5`, plus **inherit session** (the no-override option, the session model). If that lineup exceeds the menu's option cap, drill **family → version** or include a free-id "Other" so every recent version stays reachable. The rarer `research`/`review`/`debug`/`verify`/`codebase-map` sit behind an "Advanced" option, each its own separate choice, same versioned rule.
101
+ - **The per-key questions (Manual path only): always two separate questions, one for `plan` and one for `execute`; never merge them into a single model pick.** Ask each as its own `AskUserQuestion` menu (current value pre-selected), and offer the **same versioned lineup** to both. Each option is a **full model id with its version**; never a bare family alias (`opus`/`sonnet`/`haiku`), which can't say *which* live version. Build the list from the models actually reachable **at setup time** (verify, don't trust this file verbatim), offering **at least the two most recent versions per family**, e.g. `claude-fable-5`, `claude-opus-5-5` / `claude-opus-5`, `claude-sonnet-5-5` / `claude-sonnet-5`, `claude-haiku-4-5`, plus **inherit session** (the no-override option, the session model). If that lineup exceeds the menu's option cap, drill **family → version** or include a free-id "Other" so every recent version stays reachable. The rarer `research`/`review`/`debug`/`verify`/`codebase-map` sit behind an "Advanced" option, each its own separate choice, same versioned rule.
102
102
  - **Anti-pattern, never do this (in the Manual path):** a single screen whose options are *combined* plan+execute presets, e.g. `🧠 Plan 4.8 / Exec Sonnet 5`, `🚀 Tutto Opus 5`, `Plan Opus 5 / Exec Sonnet 5`. That bundles the two decisions into one pick. Forbidden. In Manual, Plan is asked, answered, and only then is Execute asked, as a second menu: two questions, two answers, always. (A **named tier profile** applied whole is **not** this anti-pattern: it is a coherent per-key tier assignment chosen deliberately, not an ad-hoc plan+execute combo, which is why picking a profile is terminal.)
103
103
  - **On Codex (host-detected), the Manual path asks `models.codex.*` instead of the Claude lineup.** Same discipline: `plan` and `execute` as two separate `AskUserQuestion` menus, never merged; the rarer `research`/`review`/`debug`/`verify`/`codebase-map` behind Advanced, each its own choice. Offer OpenAI model ids verified reachable at setup time (do not trust this list verbatim): `gpt-5.6-sol` (flagship) / `gpt-5.6-terra` (balanced) / `gpt-5.6-luna` (fast), plus **inherit session** (default). Avoid the retiring `gpt-5.4` / `gpt-5.4-mini` and the deprecated `gpt-5.3-codex`. Write the answers to `models.codex.*`; the Claude `models.*` lineup is not asked on Codex (those ids cannot run there). After writing, tell the user to **re-run the installer** (the same `--host=codex` install command) so the pins are re-projected into `~/.codex/agents/trailhead-*.toml`: the menu updates `.trailhead/config.json`, the installer is what regenerates the TOMLs and enables `features.multi_agent_v2`.
104
104
  5. **🎨 Design**: 💾 Local disk · 🖼️ claude.ai/design (Anthropic's official Claude Design MCP, Claude only) · 🧵 Stitch (Google Stitch MCP, cross-host: Claude Code + Codex). *Mode only, don't ask for a URL here; the concrete project is created or reused at execution time and its projectId cached in `design.project` (see Prototype).* If claude.ai/design or Stitch is picked, ask the **🖼️ Design surface**: 🖌️ Canvas (a single project of screens to react to; default; **who makes it:** on claude.ai/design you do, the `/design` Artifact or a hand-made web-app project, not MCP-driven; on Stitch the MCP generates the screens) · 🧩 Design system (push components into a shared design-system project, for a shared library; **the MCP creates and syncs it**) → `design.surface`. Then **✅ Mockup approval**: 🖐️ Explicit (wait for a go-ahead before UI code) · ⏩ Auto (proceed without blocking).
@@ -5,4 +5,4 @@
5
5
  - **Result-oriented output.** In the chat, report **what was done and the next step**: name tickets, give the concrete next command. Never expose trailhead's internal machinery: Mode labels (`Mode 1`), protocol/step names (`DISCUSS`/`PLAN`/`VERIFY`, "the build engine", "graduate the fog", "the frontier"), wrapper/SKILL.md mechanics, or the conventions and config you are silently obeying: the git mode (`main`/`pr`), the isolation/worktree decision, submodule commit mechanics (commit-inside + gitlink bump), claim/assignment plumbing, and **how work fans out to subagents** (whether a step ran inline or as a subagent, the role name `executor`, the model `claude-sonnet-5`, the config key `models.execute`). To the user it is just **"an agent"**: say "I'll hand this to an agent with the exact spec, then run the code review over the whole diff", never "an executor on `claude-sonnet-5` per `models.execute`". Just act, then report the outcome. Surface a convention only when it **changes what the user must do** (a UAT they run, a release awaiting their command, a genuine claim collision with *another* login) or when they explicitly ask.
6
6
  - **No em-dashes.** Write with ordinary punctuation: commas, colons, semicolons, parentheses, periods. Never use an em-dash (U+2014), and never a hyphen standing in for a comma. This holds for **everything trailhead produces**: ticket titles and bodies, engine comments, the map body, commit descriptions, chat output, and the skill's own source docs. En-dashes in numeric ranges (`1–2`) and the arrow `→` in trailhead's notation stay; only the em-dash is banned.
7
7
  - **One ticket per session: hard rule.** Resolve **at most one** ticket per session; once one is resolved, do **not** start a second; end the session instead. The only exceptions: `research` tickets (AFK, run in parallel) and `capture` operations (they resolve nothing). On resolving a ticket, close the session with the **[Session handoff](session-handoff.md)** ritual.
8
- - **Git per the repo's conventions.** The default is commit straight to `main`, conventional commits, no feature branch/PR, but the repo's `trailhead:conventions` issue governs: honour its `git:` (`main` | `pr`), `release:` (`command` | `auto`), and `isolation:` (`auto` | `none` | `worktree` | `clone`) header. Never `Co-Authored-By`. **Every commit made while resolving a ticket carries a `Refs: #<n>` trailer** (the ticket's issue number) as its last line, so GitHub threads the commit into that ticket's timeline (the commits show up *in* the ticket). Use a **non-closing** reference (`Refs:`): never a closing keyword (`fixes`/`closes`/`resolves #n`), because the ticket is closed **explicitly at Resolve** after Verify passes, not by a commit landing on the trunk. (Charting/config commits not tied to a ticket carry no `Refs:`.)
8
+ - **Git per the repo's conventions.** The default is commit straight to `main`, conventional commits, no feature branch/PR, but the repo's `trailhead:conventions` issue governs: honour its `git:` (`main` | `pr`), `release:` (`command` | `auto`), and `isolation:` (`auto` | `none` | `worktree` | `clone`) header. Never `Co-Authored-By`. **Every commit made while resolving a ticket carries a `Refs: #<n>` trailer** (the ticket's issue number) as its last line, so GitHub threads the commit into that ticket's timeline (the commits show up *in* the ticket). Use a **non-closing** reference (`Refs:`): never a closing keyword (`fixes`/`closes`/`resolves #n`), because the ticket is closed **explicitly at Resolve** after Verify passes, not by a commit landing on the trunk. (Charting/config commits not tied to a ticket carry no `Refs:`.) The commit-msg git hook enforces this trailer as long as the `.trailhead/session-ticket` marker exists; a commit not tied to any ticket is made with no marker present, so the rule stays off.
@@ -194,4 +194,4 @@ gh api repos/{owner}/{repo}/issues/<map> --jq '.sub_issues_summary.total'
194
194
  ## First-use repo setup (do ALL THREE, every chart or adopt, never skip any)
195
195
  1. Create any missing labels with `gh label create` (all eighteen: `trailhead:map`, `trailhead:codebase`, `trailhead:conventions`, `trailhead:dashboard`, `trailhead:ticket`, the six type labels, `trailhead:blocked`, `trailhead:seed`, `trailhead:out-of-scope`, `trailhead:superseded`, `trailhead:unverified`, `trailhead:fog`, `trailhead:whiteboard`).
196
196
  2. **Check the label guard is installed**: `gh api repos/{owner}/{repo}/contents/.github/workflows/trailhead-label-guard.yml`; if it's absent (404), install it: **read `teamwork.md` (Trust & provenance → Repo-side enforcement) for the exact steps**. This is part of standing up trailhead in a repo, not an optional extra: *check every time*, so a repo can never end up with the labels but no guard.
197
- 3. **Install the commit-msg git hook**: check for `.git/hooks/commit-msg`; if absent, install the shipped host-independent hook so trailhead's commit discipline (Conventional Commits subject, no `Co-Authored-By`) runs on **every** `git commit` regardless of host (the Claude-Code `PreToolUse` guard stays too, defence in depth): **read `teamwork.md` (Trust & provenance → Repo-side enforcement) for the exact steps**. Like the label guard, *check every time*; unlike it, `.git/hooks/` is per-clone local state (untracked), so it is installed, never committed.
197
+ 3. **Sync the commit-msg git hook**: run the commit-msg hook sync script (`trailhead-commit-msg-sync.js`; installs it if absent, upgrades an outdated trailhead hook in place, never touches a foreign one) so trailhead's commit discipline (Conventional Commits subject, no `Co-Authored-By`, and a `Refs: #<n>` trailer while a `.trailhead/session-ticket` marker is active) runs on **every** `git commit` regardless of host (the Claude-Code `PreToolUse` guard stays too, defence in depth): **read `teamwork.md` (Trust & provenance → Repo-side enforcement) for the exact steps**. Like the label guard, *check every time*; unlike it, `.git/hooks/` is per-clone local state (untracked), so it is installed, never committed.
@@ -94,4 +94,4 @@ The map issue's **body** is a shared mutable document: two sessions appending to
94
94
 
95
95
  **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).
96
96
 
97
- **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 (defence in depth alongside the host's own `PreToolUse` guard on Claude Code and Codex alike), enforcing the same two rules (a Conventional Commits subject of at most 72 chars; never a `Co-Authored-By` trailer) on every `git commit`, whatever host drives it (decision #16). **Every chart or adopt, check `.git/hooks/commit-msg`**; if absent, copy the shipped template (`${CLAUDE_PLUGIN_ROOT}/templates/trailhead-commit-msg` for a plugin install, `~/.claude/trailhead/templates/trailhead-commit-msg` for npm, `~/.codex/skills/trailhead/templates/trailhead-commit-msg` for Codex, take whichever exists) to `.git/hooks/commit-msg` and `chmod +x` it. Never clobber a pre-existing non-trailhead `commit-msg` hook: if one is present and is not trailhead's, tell the user and skip (or offer to chain it). Unlike the label guard, `.git/hooks/` is not version-controlled, so this is a per-clone local install with nothing to commit.
97
+ **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 (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 whichever of `${CLAUDE_PLUGIN_ROOT}/templates/`, `~/.claude/trailhead/templates/`, or `~/.codex/skills/trailhead/templates/` exists. It prints one JSON status line and handles every case itself: absent -> installs it; an older trailhead hook (recognised by its own header line, not by trusting the filename) -> upgraded in place; 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.
@@ -3,16 +3,28 @@ Runs after execute, before closing a `build`/`bug` ticket: as a subagent when th
3
3
 
4
4
  **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
5
 
6
- Review the DIFF, not the whole tree: pin a fixed point (the ticket's base commit / branch point / merge-base) and scope to `git diff <base>...HEAD`. Confirm the ref resolves and the diff is non-empty first. Review on four axes:
6
+ **Ticket diff: review the ticket's own commits, never a range.** Scope the review to the commits that carry this ticket's `Refs: #<n>` trailer (every ticket commit carries it as its last line, see **Principles** in `../principles.md`), never to a `<base>...HEAD` range: under `git: main` with concurrent sessions, or a ticket resumed after other tickets landed, a range also sweeps in other tickets' commits. Compute the set with exactly this command, run against the ticket's working copy (`<repo>` is its absolute path: the main checkout, or under `isolation: worktree`/`clone` the ticket's worktree/clone whose `HEAD` is `trailhead/t<n>`; the same under `git: pr`):
7
+
8
+ ```
9
+ git -C <repo> log --reverse --basic-regexp --grep='^Refs: #<n>$' --format=%H HEAD
10
+ ```
11
+
12
+ git applies `--grep` per message line, so the `^...$` anchors match only the whole trailer line: `#<n>` never matches `#<n>0` or a mid-sentence mention, and `--basic-regexp` overrides any user `grep.patternType`. The set may be non-contiguous (other tickets' commits interleaved on the trunk), so consume it **per commit**, oldest first, with `git -C <repo> show --remerge-diff <sha>` (git 2.36+): for a regular commit that is its plain patch; for a trunk-sync merge carrying the trailer it diffs git's automatic re-merge against the recorded result, so it shows exactly the conflict resolution the ticket authored and nothing the merge brought in cleanly (an octopus merge carrying the trailer is surfaced to the user, not judged). The per-commit patches define what is attributed to the ticket; read the touched files at `HEAD` only to judge how the change integrates, and treat later foreign edits there as context, not findings. Never collapse the set into one `git diff <first>^..<last>` span, which re-includes the interleaved foreign changes. A commit without the trailer is, by this rule, not part of the ticket's diff; only an entirely empty set is detectable. **An empty set is an error**: stop and surface it (name the ticket and the command run), never fall back silently to a base range or the whole tree. Recompute the set on every review round and every verify run, never reuse a cached list: fix commits carry the same trailer, so the set grows with each fix-and-re-review round.
13
+
14
+ Review on four axes:
7
15
 
8
16
  - **Correctness** (real bugs): broken logic, unhandled errors, edge cases, race conditions, wrong data flow.
9
17
  - **Security**: injection, secret leakage, auth/authz gaps, unvalidated boundary input.
10
18
  - **Reuse & simplification**: hunt the classic smells, each a *judgement call* (never a hard violation), and for each name it → say how to simplify: duplicated code (extract), mysterious name (rename), primitive obsession & data clumps (introduce a type), long parameter list, feature envy / message chains (move or collapse), repeated switches, speculative generality (delete), shotgun surgery & divergent change, middle-man / shallow pass-through (delete or deepen). Suppress any a documented standard endorses; skip what tooling enforces.
11
19
  - **Standards & spec**: does it match the repo's documented conventions AND do what the ticket actually asked (missing requirements, scope creep, plausibly-wrong implementations)? Suppress smells a documented standard endorses; skip anything tooling already enforces.
12
20
 
13
- 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. Report in the ticket's `VERIFY` comment, **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 comment's first line, then the findings grouped by severity with file:line and the fix, so the Verify step branches on the tag without parsing prose.
21
+ 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. Report in the ticket's `VERIFY` comment, **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 comment's first line.
22
+
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.
24
+
25
+ **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`). **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.)
14
26
 
15
- **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 new diff, up to `config.code_review.rounds` (default 2) fix-and-re-review rounds. Stop when no Critical remains, then Resolve, leaving Warnings/Info noted for the caller to weigh. **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** (resolve consciously accepting or deferring them, noting each in the `VERIFY` comment). **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.)
27
+ **Disposition gate**: no finding reaches Resolve with `open`, or without a disposition: the ledger lists every ID the counts line counts (their sum equals the number of ledger lines other than `reclassified` pointers) and each carries a disposition other than `open`. A Critical leaves `open` only by becoming `fixed`, or, via the rounds-exhausted HITL option (b) above, `deferred` or `accepted`.
16
28
 
17
29
  **Always run this review in the background and collect it by polling; never a blind blocking wait.** This is a hard engine default on every host (see *Reviews never block the session* in `../techniques.md`), not an optimisation reserved for the longest reviews. Spawn the review as a background job and poll it at intervals (a bounded wait or a status check) so a slow or stalled review reports one-line progress instead of freezing the session; the outcome is always awaited before Resolve. On Claude Code the `Agent` subagent already runs in the background and notifies on completion, so check its progress rather than awaiting it blindly. This is still the single longest quiet activity in the cycle. On Codex, collect it through the §D bounded-wait poll loop in the projected adapter header.
18
30
 
@@ -5,7 +5,7 @@ Prove the change **delivers what the ticket promised**, not merely that steps ra
5
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`).
6
6
 
7
7
  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
- 2. **Work backward from each claim to the diff.** 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.
8
+ 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.
9
9
  3. **Check for silent gaps.** A task marked done whose criterion no code satisfies; a criterion the tests do not actually exercise; a partial implementation that passes the tests but misses an edge the ticket called out. A claim you **could not evaluate at all** is not a gap: it is `inconclusive` (step 4), never forced to `gaps_found`.
10
10
  4. **Verdict.** Post the result with a leading `status:` tag the engine branches on, one of three:
11
11
  - `status: met`: every claim was evaluated and satisfied.
@@ -15,7 +15,7 @@ The ticket engines call these by name. Each technique's full protocol lives in i
15
15
  | **TDD** | `techniques/tdd.md` | RED → GREEN → REFACTOR at the seams; no implementation before a failing test |
16
16
  | **Codebase map** | `techniques/codebase-map.md` | one-time fan-out of 5 read-only readers, distilled into the repo's `trailhead:codebase` issue |
17
17
  | **Debug** | `techniques/debug.md` | scientific method: reproduce → localise → falsifiable hypotheses → confirm cause → verify |
18
- | **Code review** | `techniques/code-review.md` | review the diff on 4 axes, adversarially verify each finding before reporting |
18
+ | **Code review** | `techniques/code-review.md` | review the ticket's own commits (its `Refs: #<n>` set) on 4 axes, adversarially verify each finding before reporting |
19
19
  | **Goal-backward verification** | `techniques/verify.md` | check the change delivers what the ticket promised, working backward from its stated goal |
20
20
  | **Acceptance testing** | `techniques/acceptance-testing.md` | prove it does what the *user* wanted: ask who drives (AI or human), then automated → browser-drive → guided UAT |
21
21
  | **Cross-AI plan review** | `techniques/plan-review.md` | send a PLAN to external AI CLIs, converge on their concerns (opt-in via `config.plan_review`) |
@@ -41,7 +41,8 @@ The user invokes with a map (URL or number). A ticket is optional: without one,
41
41
  2. Choose the ticket. **If the user named a ticket, the Preflight above already confirmed it is open** (a closed, superseded, out-of-scope, or nonexistent number stopped there, before any setup ran). If the user names one, use it (a **named** ticket may be a `trailhead:whiteboard` one: `work <n>` works it whole, off the map, with the normal cycle and split allowed; use `quick <n>` for the no-split variant). Otherwise take a frontier ticket to act on, **scoped to this map** (add its `trailhead:map-<n>` label to the frontier query when several maps are live) and **never a whiteboard ticket** (bare `work` stays on the map; the whiteboard frontier is reached via `/trailhead:whiteboard` and `quick`). Bare `work` may act on the first ticket the query returns, but that order is `gh`'s newest-first, not a priority: present several independent tickets as a **set to choose from**, not the graph's natural next step (see **Frontier order carries no priority** in `../_shared/substrate.md`). **Claim** it: assign it to yourself before any work, then re-read the assignee before starting; on a collision, stop and ask the user (see `../_shared/teamwork.md`). If the ticket proves too big once you're in it, propose a **split** rather than grind, but never split on your own: present the children you would create and wait for the user's go-ahead before creating anything (same file).
42
42
  - **First, resolve the effective isolation mode.** The `isolation:` header defaults to **`auto`** (an absent key means `auto`; the engine never writes the key). Under **`auto`**, at work-start **before any file edit or diagnostic probe**, first run the read-only **assigned-ticket pre-check** (full rule in `../_shared/teamwork.md`): other open tickets already assigned to you **pre-bias** the ask toward `worktree`/`clone` (still asking, easy neutral), and the ask is **skipped** only when the session is already inside this ticket's own linked worktree (`trailhead/t<N>`, `N` the current ticket). Then ask the user once whether they will work more than one ticket at once on this machine this session. **No** (a lone ticket): the mode resolves to the current checkout, exactly like `none` (the scope-free check below applies). **Yes**: resolve by repo shape, a plain repo -> `worktree`, a **superrepo** (a `.gitmodules` at the checkout root declaring at least one submodule) -> `clone`; that "yes" **is** the clone gate's answer, so the resolved `clone` path proceeds without re-asking. Because trailhead resolves one ticket per session, this asks at most once per ticket work-start (so once per session in normal work; a within-session ticket switch re-asks at the new ticket's work-start), and an autonomous run (`/trailhead:auto`) never asks and never runs the pre-check (sequential work, no concurrency) and resolves to the current checkout. If the header carries an **explicit** `none`/`worktree`/`clone`, honour it as-is (never subject it to auto's ask): explicit `clone` keeps its own gate, explicit `worktree` is eager. See `../_shared/teamwork.md`.
43
43
  - **When the resolved mode is the current checkout (`isolation: none`, or `auto` answered no), check the scope is free before starting** (this is how a submodule/package gets serialised when worktrees don't fit): scan the other in-progress tickets' `Scope:` lines, and if one **overlaps** this ticket's scope, **stop and tell the user** the scope is busy (name the ticket + holder) and offer a disjoint-scope frontier ticket or to wait. See `../_shared/teamwork.md`.
44
- - **Write the session-ticket marker** (a cheap hint for tooling like a statusline): at the **working root** (the checkout, worktree, or clone you'll edit in), write `.trailhead/session-ticket` as **one line** `#<n> <ticket title>`. **Keep it gitignored** (add `.trailhead/session-ticket` to that repo's `.gitignore` if absent): it's per-session local state, **never committed**. It just lets an external tool show "what am I on" offline; the tracker stays the source of truth. Remove it at Resolve/handoff (see `../_shared/session-handoff.md`); on **Pause** it may stay (you'll return) or be cleared if you release the claim. The **`.trailhead/active-map`** marker (which map this session works) follows the same rules: gitignored, per-session, never committed; see **Multiple maps on one repo** in `../_shared/multi-map.md`.
44
+ - **Write the session-ticket marker**: at the **working root** (the checkout, worktree, or clone you'll edit in), write `.trailhead/session-ticket` as **one line** `#<n> <ticket title>`. **Keep it gitignored** (add `.trailhead/session-ticket` to that repo's `.gitignore` if absent): it's per-session local state, **never committed**. It lets an external tool show "what am I on" offline (the tracker stays the source of truth), but it is not merely that cheap hint: while it is present, the git commit-msg hook also requires every commit to carry a `Refs: #<n>` trailer matching it, so a stale marker left behind would wrongly demand that trailer on unrelated commits. Remove it at Resolve/handoff (see `../_shared/session-handoff.md`); on **Pause** it may stay (you'll return) or be cleared if you release the claim. The **`.trailhead/active-map`** marker (which map this session works) follows the same rules: gitignored, per-session, never committed; see **Multiple maps on one repo** in `../_shared/multi-map.md`.
45
+ - **Sync the commit-msg hook**: at the working root, run `node <templates>/trailhead-commit-msg-sync.js --repo <working-root>` (where `<templates>` lives and what each status means: the commit-msg hook paragraph in `../_shared/teamwork.md`). Stay silent when it reports installed or current. On **upgraded**, give the user one result-oriented line: this clone's commit-msg hook was updated to the current rules. On **foreign**, one line that the user's own hook was left untouched and git does not enforce trailhead's rules here. On **skipped**, one line that nothing was set up here (a `core.hooksPath` without a trailhead hook, or a working root that is not a git repo) and git does not enforce trailhead's rules. A missing script or a node error never blocks the work.
45
46
  - **When the resolved mode is `worktree` or `clone` (explicit, or `auto` answered yes), set up the isolated workspace NOW, before any file edit or diagnostic probe** (not later at Execute/Fix): **if the repo has submodules (a `.gitmodules`), `isolation: worktree` is escalated to `clone`** (a superproject worktree cannot safely carry submodule state; see `../_shared/teamwork.md`). For `worktree` (no submodules), create/enter this ticket's `git worktree` on a `trailhead/t<n>` branch for the package/path the ticket's `Scope:` names (a submodule-bearing repo would have escalated to `clone` above), e.g. `git worktree add ../<repo>-t<n> -b trailhead/t<n>`; for `clone`, make an independent working copy at `../<repo>-t<n>` (branch `trailhead/t<n>`) by whichever is faster: `git clone` (add `--recurse-submodules` when the repo has submodules) + the install step, or a **folder copy that brings `node_modules`** (and the submodule working trees) to skip the reinstall (then verify the copy's git is independent, `git -C <copy> rev-parse --absolute-git-dir` resolves inside it, and the same inside every submodule, else commits leak back to the original); a clone with submodules is provisioned via `git submodule update --init --recursive` (nested included), each scoped submodule pinned at the superproject's recorded commit. **If this `clone` was reached via `auto`'s work-start ask (answered yes), that ask already served as the clone gate: do not re-ask, proceed to set up the clone. Under an explicit `isolation: clone`, run the gate as follows.** **Because `clone` is heavy (a full clone + an install), gate it, every time, before cloning: ask the user whether they expect to work more than one ticket at once on this machine this session.** If **no**, **skip the clone** and work this ticket in the current checkout (isolation only exists to stop concurrent workspaces from colliding; a lone ticket has nothing to collide with, so the shared-checkout caveats apply and that is fine, and `git:` still decides the branch as usual); if **yes**, clone as above. The gate is **`clone`-only**: `worktree` is cheap, so set it up without asking. Do every subsequent step in whatever workspace you ended up in. This is the whole point of isolation: if you diagnose and edit in the shared checkout "just to start" while a concurrent workspace exists, you are already colliding. Set the workspace up silently (the `clone` gate-question is the one exception you voice); it's undone at Resolve when the branch integrates and the worktree/clone is removed. See `../_shared/teamwork.md`.
46
47
  3. **Brief the ticket and confirm scope, before the engine runs.** With the ticket claimed, read it and give the user a **brief, result-oriented summary of what resolving it will do** (its goal and the shape of the work, not a plan yet), then **ask whether they want to change anything**. This is a HITL confirm gate: on assent, proceed to the engine. **If the user asks for a change, hold it against the ticket.** A change **consistent** with the ticket is folded in and the engine proceeds. A change that **diverges** from the ticket is surfaced as a mismatch, and the user is **asked, never forced**, whether to (a) work the ticket **as-is**, or (b) open a **separate new ticket** for the divergent request. Offer both and decide neither: open the separate ticket **only if the user chooses it** (via **Scope that surfaces while working a ticket**, served by the capture cluster, the same capture-or-split path used for scope that surfaces mid-work), never automatically. **If the user defers the choice rather than settling it here, mark the divergent follow-up pending for the Pending follow-up guard (`../_shared/session-handoff.md`), so it is re-posed at the handoff rather than lost.** **Under an autonomous run (`auto`) this is a process confirm gate**, so it follows the advisory boundary in `references/auto.md`: `auto` proceeds (takes the delegate option) without stopping to brief.
47
48
  4. Resolve it with its type's engine in **`references/ticket-engines.md`**, **zoom as needed**: fetch the full body of related/closed tickets on demand. If in doubt on a `decision` ticket, run **Grilling** + **Domain vocabulary**. If in doubt on a `build`, **stop and ask** (see the Discuss step): never auto-grill.
@@ -58,7 +59,7 @@ It runs the **full engine** (the `build`/`bug` cycle in `references/ticket-engin
58
59
  - **Grill only if needed** (Discuss): never auto-grill; start from Plan and stop to ask only if a blocking ambiguity surfaces, exactly as the `build` Discuss. The user may ask to grill at any time.
59
60
  - **Never splits.** If the ticket turns out large, work it whole anyway: that is the point of `quick`. Do not `split` it and do not spin children. (New scope that surfaces mid-work is still captured out, never folded in, see **Scope that surfaces while working a ticket**, served by the capture cluster.)
60
61
  - **No map narrative book-keeping.** A whiteboard ticket has no map, so there is no `Decisions so far` update, no fog to graduate, no frontier re-scan. Everything else holds: atomic commits with `Refs: #<n>`, TDD / Code review / Acceptance testing per the cycle, the resolution comment + `gh issue close`, and the **Session handoff** (`../_shared/session-handoff.md`). **Exception, `quick <n>` on a *map* ticket: dependent-unblocking still runs** (remove `trailhead:blocked` from any dependent whose last blocker just closed, **Work the map** step 5 above); skipping it would strand those dependents off the frontier for good. Only the *narrative* book-keeping above and the split are skipped, so prefer `work <n>` for a map ticket that has dependents or belongs in the map record.
61
- - **Same isolation setup as `work`.** `quick` **writes the `.trailhead/session-ticket` marker at the working root in every isolation mode** (exactly as **Work the map** step 2, so tooling like the statusline shows what's in play; no active-map marker, though: a whiteboard ticket has no map). Under `auto` it runs the same work-start concurrency ask **before any edit** and resolves to current-checkout / worktree / clone by the same rule; under an explicit `isolation: worktree`/`clone` it sets up the ticket's `trailhead/t<n>` workspace before any edit (including the submodule-forces-`clone` escalation and submodule provisioning) and re-checks the claim; under `isolation: none` (or `auto` answered no), the scope-free check applies too.
62
+ - **Same isolation setup as `work`.** `quick` **writes the `.trailhead/session-ticket` marker at the working root in every isolation mode** (exactly as **Work the map** step 2, so tooling like the statusline shows what's in play; no active-map marker, though: a whiteboard ticket has no map). Under `auto` it runs the same work-start concurrency ask **before any edit** and resolves to current-checkout / worktree / clone by the same rule; under an explicit `isolation: worktree`/`clone` it sets up the ticket's `trailhead/t<n>` workspace before any edit (including the submodule-forces-`clone` escalation and submodule provisioning) and re-checks the claim; under `isolation: none` (or `auto` answered no), the scope-free check applies too. `quick` also runs the same commit-msg hook sync at the working root, exactly as **Work the map** step 2.
62
63
  - **Refreshes the dashboard at the handoff, not mid-work.** A `quick "<text>"` whiteboard ticket is born and resolved in the **same** session, so its dashboard refresh rides with the session's end, not with ticket creation: **do not refresh right after the ticket exists** (that lands a structural pinned-issue write ahead of the work `quick` exists to just-get-done, and only duplicates the resolution refresh a moment later). Refresh at **Resolve** (the handoff), as a **full regeneration from the live tracker** (never a partial patch that leaves other sections knowingly stale), so the whiteboard ticket lands on the dashboard as the session closes. **If the session pauses before Resolve** (`/trailhead:pause`), refresh there instead, so a deliberately-unfinished quick still shows (the read-only renders never rewrite the dashboard body, so nothing else would surface it). `quick <n>` on a *map* ticket does **not** refresh (map ticket progress shows via the native bar). See **The dashboard** and its freshness rule in `../_shared/substrate.md`.
63
64
  - **Always size-triages, independent of `config.effort`.** `quick` is the home of the cost-aware path: reaching for `quick` is itself the signal the piece is small, so it runs the **same size triage and ask-to-skip offer defined canonically under effort in `../_shared/configuration-reference.md`** (the same one the `build`/`bug` engine runs, `references/ticket-engines.md`) **every time, at the start before planning, whatever `config.effort` is set to**. `quick` does not wait for `effort: lean`, and an explicit `effort: standard` does not suppress it: `effort` governs `work` / the map cycle, not `quick`. On a trivial/small ticket it surfaces the one consolidated offer (the plan pass, TDD, cross-AI review, the full code review, the goal-backward verify) and goes lean **only on an explicit yes**; on a large ticket there is no offer and the full cycle runs. The correctness spine (atomic `Refs:` commits, tests/criterion, post-commit self-check, Resolve) is never in the offer, whatever the size or the answer.
64
65
 
@@ -30,9 +30,9 @@ The ticket that *builds*. Lean cycle, all in **ticket comments**:
30
30
 
31
31
  1. **Discuss**: **never auto-grill.** Start from Plan. If reading the ticket surfaces blocking ambiguity, **stop and ask the user** whether they want a **Grilling** round: don't launch it on your own initiative. Only on their assent run the short round and post a `DISCUSS` comment with the closed choices. The user may also ask for it themselves at any time. Advisory choices raised here **offer** the delegate/defer options, per the injection recipe in `../../_shared/choices.md`.
32
32
  2. **Plan**: a `PLAN` comment: the steps, the seams, the files touched, the verification criteria. Apply **TDD** per `config.tdd` (`seams`/`on`/`off`). Produce this plan via a *planner subagent* on `config.models.plan` (dispatch the **`trailhead-plan`** agent, read-only, passing `config.models.plan` as the model override; inline when the model is unset or equals the session model; Execute then runs as its own subagent, see below). If `config.plan_review` is on, run **Cross-AI plan review** before Execute: send the plan to external AI CLIs and converge on their concerns. Now that the files/seam are known, run the **undeclared-coupling check** (`../../_shared/teamwork.md`) against in-progress tickets. **Plan to sufficiency:** stop once you can name the steps, seams, and files with confidence, and never re-read a file already read in this planning pass; act when you have enough to plan, rather than re-deriving what the pass already established. **The single exception:** under an accepted `lean` skip (see the Size triage above), produce a short inline plan here instead of the planner subagent, and skip Cross-AI plan review; absent that accepted skip, this step runs in full as described.
33
- 3. **Execute**: **if this ticket changes user-facing UI (a new screen, a redesign, a changed layout), run the Prototype technique FIRST, before any real UI code**: surface a mockup to react to, routed by `config.design` (disk / claude.ai/design / stitch) and gated by `config.design.approval` (`explicit` waits for a go-ahead, `auto` proceeds after surfacing). **Route by `config.design` and never shortcut a hosted mode to a local disk mockup**: when `config.design` is `claude.ai/design` or `stitch`, the mockup is built there (reusing a cached `design.project`); `disk` is only for `config.design: disk` or the technique's explicit, announced fallback, never a convenience default. Read `config.design` before producing anything. For a redesign or new screen the default is to offer mockups; don't jump straight to code on a UI ticket. **Once the mockup is approved, reproduce it with 100% fidelity** (layout, spacing, sizing, colour, type, copy, and every state), never a loose approximation "in the spirit of" it; if part of the approved mockup is genuinely infeasible, stop and raise it rather than silently deviate (full statement in the **Prototype** technique, `../../_shared/techniques/prototype.md`). **Make fidelity verifiable, not a hope:** before finishing Execute, enumerate the approved mockup's elements and states, confirm each is reproduced, and **record that enumerated fidelity list as a ticket comment** (noting any element raised as infeasible with its resolution), so Verify works from a durable checklist instead of reconstructing intent. At Verify, **Acceptance testing** checks the built screen against the approved mockup (element by element, state by state) and **Goal-backward verification** carries fidelity as an explicit goal claim (see `../../_shared/techniques/acceptance-testing.md` and `../../_shared/techniques/verify.md`). Then implement with **atomic commits** (conventional commits, each with a `Refs: #<n>` trailer for this ticket, see **Principles** in `../../_shared/principles.md`), following the repo's `trailhead:conventions` `git:`, straight to `main` by default, or a feature branch + PR when `git: pr`. **Under `isolation: worktree`, do the work in this ticket's `git worktree`** (`trailhead/t<n>` branch), integrating to the trunk at Resolve (see `../../_shared/teamwork.md`). One commit = one verifiable step. Run this via an **executor subagent on `config.models.execute`** (dispatch the **`trailhead-executor`** agent, full tools that can Edit/Write/commit, passing `config.models.execute` as the model override; inline when the model is unset or equals the session model); the commits land on `main`, visible. **Before installing any new dependency, run the verify-before-install gate** (slopsquatting barrier): check the package's legitimacy (name / typosquat similarity to popular packages, age / first-seen date, download counts, repository metadata present and matching) and **stop and ask the user on a suspicious result** (block-and-ask), never installing on your own judgement. On a host with a hook bus the `trailhead-install-guard` `PreToolUse` hook enforces this, blocking a named public-registry install (npm / PyPI / crates.io / Go / RubyGems) until it is vetted; once vetted, proceed by re-running the exact install command prefixed with `TRAILHEAD_VERIFIED_INSTALL=1`. New scope surfacing mid-execute is captured/split, not worked here.
34
- 4. **Verify**: a `VERIFY` comment: run the tests / the plan's criterion; then the **Code review** technique (its subagent reviews *adversarially*: it doesn't defer to a `CLAUDE.md` convention or a "settled decision" memory), **run in the background and collected by polling, never a blind blocking wait** (a hard engine default on every host, see *Reviews never block the session* in `../../_shared/techniques.md`); **read the review's leading `status:` tag to branch deterministically: `status: clean` -> proceed to Resolve, `status: issues_found` -> iterate the fix-and-re-review loop (or run the HITL checkpoint) before resolving**; then the **Goal-backward verification** technique on `config.models.verify` (does the change deliver what the ticket promised, backward from its `Question` and the `PLAN`'s criteria; the read-only `trailhead-verify` subagent), **run in the background and collected by polling** like Code review, branching on its leading `status:` tag (`status: met` -> proceed, `status: gaps_found` -> loop back to Execute for the gap or run the HITL checkpoint before resolving, `status: inconclusive` as the leading tag (all gaps cleared) -> a per-reason-class HITL checkpoint with no auto-retry, never auto-resolving over the unevaluated goal); then **Acceptance testing** if the change is user-facing, opening with the drive-mode ask (`../../_shared/techniques/acceptance-testing.md`): AI-driven, or **walk the user through a guided UAT conversationally, one step at a time**, never a checklist dumped in a comment: interactive by design, so it runs inline, not backgrounded. Report the outcome honestly (if a test fails, say so). **The single exception:** under an accepted `lean` skip, run a lighter inline review pass instead of the full Code-review subagent and a quick inline goal check instead of the full Goal-backward verification subagent (or skip either only when the change is truly cosmetic); the tests / criterion and the honest outcome report still run regardless. At a HITL checkpoint here, an advisory sub-choice (which fix, how to proceed) **offers** the delegate option, per the injection recipe in `../../_shared/choices.md`; the accept/resolve decision stays the human's.
35
- 5. **Resolve**: a resolution comment with what was done → `gh issue close` → update `Decisions so far` on the map (a body read-modify-write: obey the **Fail-safe body rewrites** rule in `../../_shared/substrate.md`). **Pending-UAT guard (before the close).** If Verify left acceptance testing **not yet run** (it would otherwise be deferred), **pose the two-way choice to the user and wait for an explicit answer, never open a ticket unilaterally**: (a) run the UAT now, in this session, with no ticket (opens with the drive-mode ask, `../../_shared/techniques/acceptance-testing.md`), or (b) open a UAT follow-up ticket to run it later. On (a), run the guided walkthrough now and resolve **only on a confirmed pass** (a failing step becomes a fix or a `bug` follow-up and an incomplete/blocked step is marked pending and tracked, per the guided-UAT rules in `../../_shared/techniques/acceptance-testing.md`, not a silent close); on (b), **open the follow-up ticket carrying the owed UAT first**, linked back to this parent (a `task` on the frontier or a `trailhead:seed` gated on the run, per *Pending UAT must stay tracked* in `../../_shared/techniques/acceptance-testing.md`). **Never open a UAT ticket without asking, never `gh issue close` on a chosen-deferred UAT with nothing opened, and never let silence or an ambiguous reply default to either path.** When acceptance was already completed at Verify, this guard doesn't fire. **Post-commit self-check (before the comment goes out).** For each commit the comment cites, confirm the hash resolves (`git cat-file -e <hash>^{commit}`, or `git show --stat <hash>`) and that every file the comment names as touched actually appears in that commit's diff (`git -c core.quotepath=false show --name-only --format= <hash>`, with `core.quotepath=false` so a non-ASCII filename compares plainly instead of in git's C-quoted form); correct or drop any citation that fails the check, so a hallucinated or mis-stated commit never lands in the tracker record. **If this change materially altered the codebase** (a new module/layer, dependency, seam, or build/test command), **patch the affected facet of the `trailhead:codebase` issue** (see **Codebase** in `../../_shared/substrate.md`) in place (a line or two, not a re-map). Then close the session with the **Session handoff** ritual (`../../_shared/session-handoff.md`): `/clear` + next command.
33
+ 3. **Execute**: **if this ticket changes user-facing UI (a new screen, a redesign, a changed layout), run the Prototype technique FIRST, before any real UI code**: surface a mockup to react to, routed by `config.design` (disk / claude.ai/design / stitch) and gated by `config.design.approval` (`explicit` waits for a go-ahead, `auto` proceeds after surfacing). **Route by `config.design` and never shortcut a hosted mode to a local disk mockup**: when `config.design` is `claude.ai/design` or `stitch`, the mockup is built there (reusing a cached `design.project`); `disk` is only for `config.design: disk` or the technique's explicit, announced fallback, never a convenience default. Read `config.design` before producing anything. For a redesign or new screen the default is to offer mockups; don't jump straight to code on a UI ticket. **Once the mockup is approved, reproduce it with 100% fidelity** (layout, spacing, sizing, colour, type, copy, and every state), never a loose approximation "in the spirit of" it; if part of the approved mockup is genuinely infeasible, stop and raise it rather than silently deviate (full statement in the **Prototype** technique, `../../_shared/techniques/prototype.md`). **Make fidelity verifiable, not a hope:** before finishing Execute, enumerate the approved mockup's elements and states, confirm each is reproduced, and **record that enumerated fidelity list as a ticket comment** (noting any element raised as infeasible with its resolution), so Verify works from a durable checklist instead of reconstructing intent. At Verify, **Acceptance testing** checks the built screen against the approved mockup (element by element, state by state) and **Goal-backward verification** carries fidelity as an explicit goal claim (see `../../_shared/techniques/acceptance-testing.md` and `../../_shared/techniques/verify.md`). Then implement with **atomic commits** (conventional commits, each with a `Refs: #<n>` trailer for this ticket, see **Principles** in `../../_shared/principles.md`; the trailer is also what scopes Verify's review and goal check, per **Ticket diff** in `../../_shared/techniques/code-review.md`), following the repo's `trailhead:conventions` `git:`, straight to `main` by default, or a feature branch + PR when `git: pr`. **Under `isolation: worktree`, do the work in this ticket's `git worktree`** (`trailhead/t<n>` branch), integrating to the trunk at Resolve (see `../../_shared/teamwork.md`). One commit = one verifiable step. Run this via an **executor subagent on `config.models.execute`** (dispatch the **`trailhead-executor`** agent, full tools that can Edit/Write/commit, passing `config.models.execute` as the model override; inline when the model is unset or equals the session model); the commits land on `main`, visible. **Before installing any new dependency, run the verify-before-install gate** (slopsquatting barrier): check the package's legitimacy (name / typosquat similarity to popular packages, age / first-seen date, download counts, repository metadata present and matching) and **stop and ask the user on a suspicious result** (block-and-ask), never installing on your own judgement. On a host with a hook bus the `trailhead-install-guard` `PreToolUse` hook enforces this, blocking a named public-registry install (npm / PyPI / crates.io / Go / RubyGems) until it is vetted; once vetted, proceed by re-running the exact install command prefixed with `TRAILHEAD_VERIFIED_INSTALL=1`. New scope surfacing mid-execute is captured/split, not worked here.
34
+ 4. **Verify**: a `VERIFY` comment: run the tests / the plan's criterion; then the **Code review** technique, scoped to the ticket's own commits per its **Ticket diff** rule (`../../_shared/techniques/code-review.md`: the `Refs: #<n>` set, read per commit, recomputed each round, an empty set surfaced as an error, never a `<base>...HEAD` range) (its subagent reviews *adversarially*: it doesn't defer to a `CLAUDE.md` convention or a "settled decision" memory), and carries the review's counts line and findings ledger, per code-review.md, **run in the background and collected by polling, never a blind blocking wait** (a hard engine default on every host, see *Reviews never block the session* in `../../_shared/techniques.md`); **read the review's leading `status:` tag to branch deterministically: `status: clean` -> proceed to Resolve, `status: issues_found` -> iterate the fix-and-re-review loop (or run the HITL checkpoint) before resolving**; then the **Goal-backward verification** technique on `config.models.verify` (does the change deliver what the ticket promised, backward from its `Question` and the `PLAN`'s criteria; the read-only `trailhead-verify` subagent, on the same **Ticket diff** set), **run in the background and collected by polling** like Code review, branching on its leading `status:` tag (`status: met` -> proceed, `status: gaps_found` -> loop back to Execute for the gap or run the HITL checkpoint before resolving, `status: inconclusive` as the leading tag (all gaps cleared) -> a per-reason-class HITL checkpoint with no auto-retry, never auto-resolving over the unevaluated goal); then **Acceptance testing** if the change is user-facing, opening with the drive-mode ask (`../../_shared/techniques/acceptance-testing.md`): AI-driven, or **walk the user through a guided UAT conversationally, one step at a time**, never a checklist dumped in a comment: interactive by design, so it runs inline, not backgrounded. Report the outcome honestly (if a test fails, say so). **The single exception:** under an accepted `lean` skip, run a lighter inline review pass instead of the full Code-review subagent and a quick inline goal check instead of the full Goal-backward verification subagent (or skip either only when the change is truly cosmetic); the lighter inline review still uses the same finding IDs, counts line, and ledger, and the same no-`open` gate, per code-review.md; the tests / criterion and the honest outcome report still run regardless. At a HITL checkpoint here, an advisory sub-choice (which fix, how to proceed) **offers** the delegate option, per the injection recipe in `../../_shared/choices.md`; the accept/resolve decision stays the human's.
35
+ 5. **Resolve**: a resolution comment with what was done → `gh issue close` → update `Decisions so far` on the map (a body read-modify-write: obey the **Fail-safe body rewrites** rule in `../../_shared/substrate.md`). **Disposition gate (before the close)**: the latest `VERIFY` comment's findings ledger covers every finding the counts line counts and none of them is `open`; the resolution comment links that ledger rather than duplicating it; otherwise do not `gh issue close` (per code-review.md's Disposition gate). **Pending-UAT guard (before the close).** If Verify left acceptance testing **not yet run** (it would otherwise be deferred), **pose the two-way choice to the user and wait for an explicit answer, never open a ticket unilaterally**: (a) run the UAT now, in this session, with no ticket (opens with the drive-mode ask, `../../_shared/techniques/acceptance-testing.md`), or (b) open a UAT follow-up ticket to run it later. On (a), run the guided walkthrough now and resolve **only on a confirmed pass** (a failing step becomes a fix or a `bug` follow-up and an incomplete/blocked step is marked pending and tracked, per the guided-UAT rules in `../../_shared/techniques/acceptance-testing.md`, not a silent close); on (b), **open the follow-up ticket carrying the owed UAT first**, linked back to this parent (a `task` on the frontier or a `trailhead:seed` gated on the run, per *Pending UAT must stay tracked* in `../../_shared/techniques/acceptance-testing.md`). **Never open a UAT ticket without asking, never `gh issue close` on a chosen-deferred UAT with nothing opened, and never let silence or an ambiguous reply default to either path.** When acceptance was already completed at Verify, this guard doesn't fire. **Post-commit self-check (before the comment goes out).** For each commit the comment cites, confirm the hash resolves (`git cat-file -e <hash>^{commit}`, or `git show --stat <hash>`) and that every file the comment names as touched actually appears in that commit's diff (`git -c core.quotepath=false show --name-only --format= <hash>`, with `core.quotepath=false` so a non-ASCII filename compares plainly instead of in git's C-quoted form); correct or drop any citation that fails the check, so a hallucinated or mis-stated commit never lands in the tracker record. **If this change materially altered the codebase** (a new module/layer, dependency, seam, or build/test command), **patch the affected facet of the `trailhead:codebase` issue** (see **Codebase** in `../../_shared/substrate.md`) in place (a line or two, not a re-map). Then close the session with the **Session handoff** ritual (`../../_shared/session-handoff.md`): `/clear` + next command.
36
36
 
37
37
  New scope that surfaces mid-build (while planning, executing, or testing) → **capture or split, never expand this ticket in flight** (see **Scope that surfaces while working a ticket**, served by the capture cluster). If the user defers that in-the-moment choice rather than settling it, mark the follow-up **pending** so the **Pending follow-up guard** re-poses it at handoff (`../../_shared/session-handoff.md`), instead of losing it to scrollback.
38
38
 
@@ -49,9 +49,9 @@ Then run the cycle, all in ticket comments:
49
49
 
50
50
  1. **Repro**: a `REPRO` comment: how to reproduce, expected vs observed behaviour.
51
51
  2. **Diagnose**: run the **Debug** technique. A `DIAGNOSIS` comment with the root cause **stated as a falsifiable hypothesis plus the falsification test that confirmed it**. **Pre-fix checkpoint: only a confirmed cause proceeds to Fix** (a cause that has not survived its falsification test stays in diagnosis, looping the hypothesis steps); the full gate lives in the **Debug** technique (`../../_shared/techniques/debug.md`).
52
- 3. **Fix**: implement the correction with **atomic commits** (each with a `Refs: #<n>` trailer for this ticket, see **Principles** in `../../_shared/principles.md`; per the repo's `trailhead:conventions` `git:`, `main` by default, else a branch + PR; in this ticket's `git worktree` under `isolation: worktree`), via an **executor subagent on `config.models.execute`** (dispatch the **`trailhead-fix`** agent, full tools, passing `config.models.execute` as the model override; inline when unset/same as session). Where sensible, a test that fails before and passes after (**TDD**). The **verify-before-install gate** from the `build` Execute step applies here too: vet any new dependency (typosquat similarity, age, downloads, repository metadata) and stop and ask the user on a suspicious result, before installing. **For a user-facing fix that changes UI, the `build` Execute step's Prototype + approved-mockup fidelity rule applies here too:** where the fix changes a screen, surface a mockup, and once approved reproduce it with 100% fidelity with the same enumerated, Verify-checked fidelity list.
53
- 4. **Verify**: a `VERIFY` comment: the repro now passes, no regressions; **Code review** on the fix, **run in the background and collected by polling, never a blind blocking wait** (a hard engine default on every host, see *Reviews never block the session* in `../../_shared/techniques.md`); **read the review's leading `status:` tag to branch deterministically (`status: clean` -> Resolve, `status: issues_found` -> iterate the loop or run the checkpoint first)**; then the **Goal-backward verification** technique on `config.models.verify` (does the fix deliver what the ticket promised, backward from its `Question`; the read-only `trailhead-verify` subagent), background and collected by polling, branching on its leading `status:` tag (`status: met` -> proceed, `status: gaps_found` -> loop back to Fix for the gap or run the checkpoint, `status: inconclusive` as the leading tag (all gaps cleared) -> a per-reason-class HITL checkpoint with no auto-retry, never auto-resolving over the unevaluated goal); **Acceptance testing** if the bug was user-facing, opening with the drive-mode ask (`../../_shared/techniques/acceptance-testing.md`): AI-driven, or **walk the user through a guided UAT conversationally, step by step**, not a checklist to self-serve: interactive by design, so it runs inline, not backgrounded. At a HITL checkpoint here, an advisory sub-choice (which fix, how to proceed) **offers** the delegate option, per the injection recipe in `../../_shared/choices.md`; the accept/resolve decision stays the human's.
54
- 5. **Resolve**: a resolution comment (cause + fix) → `gh issue close` → update `Decisions so far` (a body read-modify-write: obey the **Fail-safe body rewrites** rule in `../../_shared/substrate.md`). **Pending-UAT guard (before the close).** If Verify left acceptance testing **not yet run** (it would otherwise be deferred), **pose the two-way choice to the user and wait for an explicit answer, never open a ticket unilaterally**: (a) run the UAT now, in this session, with no ticket (opens with the drive-mode ask, `../../_shared/techniques/acceptance-testing.md`), or (b) open a UAT follow-up ticket to run it later. On (a), run the guided walkthrough now and resolve **only on a confirmed pass** (a failing step becomes a fix or a `bug` follow-up and an incomplete/blocked step is marked pending and tracked, per the guided-UAT rules in `../../_shared/techniques/acceptance-testing.md`, not a silent close); on (b), **open the follow-up ticket carrying the owed UAT first**, linked back to this parent (a `task` on the frontier or a `trailhead:seed` gated on the run, per *Pending UAT must stay tracked* in `../../_shared/techniques/acceptance-testing.md`). **Never open a UAT ticket without asking, never `gh issue close` on a chosen-deferred UAT with nothing opened, and never let silence or an ambiguous reply default to either path.** When acceptance was already completed at Verify, this guard doesn't fire. **Run the post-commit self-check before posting, exactly as in the `build` Resolve step** (each cited hash resolves and the files it names appear in that commit's diff; correct or drop any citation that fails). **If the fix materially altered the codebase**, patch the affected facet of the `trailhead:codebase` issue (see **Codebase** in `../../_shared/substrate.md`; also a body read-modify-write, same **Fail-safe body rewrites** rule) in place (a line or two). Then close the session with the **Session handoff** ritual (`../../_shared/session-handoff.md`): `/clear` + next command.
52
+ 3. **Fix**: implement the correction with **atomic commits** (each with a `Refs: #<n>` trailer for this ticket, see **Principles** in `../../_shared/principles.md`; the trailer is also what scopes Verify's review and goal check, per **Ticket diff** in `../../_shared/techniques/code-review.md`; per the repo's `trailhead:conventions` `git:`, `main` by default, else a branch + PR; in this ticket's `git worktree` under `isolation: worktree`), via an **executor subagent on `config.models.execute`** (dispatch the **`trailhead-fix`** agent, full tools, passing `config.models.execute` as the model override; inline when unset/same as session). Where sensible, a test that fails before and passes after (**TDD**). The **verify-before-install gate** from the `build` Execute step applies here too: vet any new dependency (typosquat similarity, age, downloads, repository metadata) and stop and ask the user on a suspicious result, before installing. **For a user-facing fix that changes UI, the `build` Execute step's Prototype + approved-mockup fidelity rule applies here too:** where the fix changes a screen, surface a mockup, and once approved reproduce it with 100% fidelity with the same enumerated, Verify-checked fidelity list.
53
+ 4. **Verify**: a `VERIFY` comment: the repro now passes, no regressions; **Code review** on the fix, scoped to the ticket's own commits per its **Ticket diff** rule (`../../_shared/techniques/code-review.md`: the `Refs: #<n>` set, read per commit, recomputed each round, an empty set surfaced as an error, never a `<base>...HEAD` range), and carries the review's counts line and findings ledger, per code-review.md, **run in the background and collected by polling, never a blind blocking wait** (a hard engine default on every host, see *Reviews never block the session* in `../../_shared/techniques.md`); **read the review's leading `status:` tag to branch deterministically (`status: clean` -> Resolve, `status: issues_found` -> iterate the loop or run the checkpoint first)**; then the **Goal-backward verification** technique on `config.models.verify` (does the fix deliver what the ticket promised, backward from its `Question`; the read-only `trailhead-verify` subagent, on the same **Ticket diff** set), background and collected by polling, branching on its leading `status:` tag (`status: met` -> proceed, `status: gaps_found` -> loop back to Fix for the gap or run the checkpoint, `status: inconclusive` as the leading tag (all gaps cleared) -> a per-reason-class HITL checkpoint with no auto-retry, never auto-resolving over the unevaluated goal); **Acceptance testing** if the bug was user-facing, opening with the drive-mode ask (`../../_shared/techniques/acceptance-testing.md`): AI-driven, or **walk the user through a guided UAT conversationally, step by step**, not a checklist to self-serve: interactive by design, so it runs inline, not backgrounded. At a HITL checkpoint here, an advisory sub-choice (which fix, how to proceed) **offers** the delegate option, per the injection recipe in `../../_shared/choices.md`; the accept/resolve decision stays the human's.
54
+ 5. **Resolve**: a resolution comment (cause + fix) → `gh issue close` → update `Decisions so far` (a body read-modify-write: obey the **Fail-safe body rewrites** rule in `../../_shared/substrate.md`). **Disposition gate (before the close)**: the latest `VERIFY` comment's findings ledger covers every finding the counts line counts and none of them is `open`; the resolution comment links that ledger rather than duplicating it; otherwise do not `gh issue close` (per code-review.md's Disposition gate). **Pending-UAT guard (before the close).** If Verify left acceptance testing **not yet run** (it would otherwise be deferred), **pose the two-way choice to the user and wait for an explicit answer, never open a ticket unilaterally**: (a) run the UAT now, in this session, with no ticket (opens with the drive-mode ask, `../../_shared/techniques/acceptance-testing.md`), or (b) open a UAT follow-up ticket to run it later. On (a), run the guided walkthrough now and resolve **only on a confirmed pass** (a failing step becomes a fix or a `bug` follow-up and an incomplete/blocked step is marked pending and tracked, per the guided-UAT rules in `../../_shared/techniques/acceptance-testing.md`, not a silent close); on (b), **open the follow-up ticket carrying the owed UAT first**, linked back to this parent (a `task` on the frontier or a `trailhead:seed` gated on the run, per *Pending UAT must stay tracked* in `../../_shared/techniques/acceptance-testing.md`). **Never open a UAT ticket without asking, never `gh issue close` on a chosen-deferred UAT with nothing opened, and never let silence or an ambiguous reply default to either path.** When acceptance was already completed at Verify, this guard doesn't fire. **Run the post-commit self-check before posting, exactly as in the `build` Resolve step** (each cited hash resolves and the files it names appear in that commit's diff; correct or drop any citation that fails). **If the fix materially altered the codebase**, patch the affected facet of the `trailhead:codebase` issue (see **Codebase** in `../../_shared/substrate.md`; also a body read-modify-write, same **Fail-safe body rewrites** rule) in place (a line or two). Then close the session with the **Session handoff** ritual (`../../_shared/session-handoff.md`): `/clear` + next command.
55
55
 
56
56
  A **blocking** bug that halts other work is worked immediately; an isolated one is a normal frontier ticket. New scope surfacing while fixing → capture or split, never expand this ticket in flight (see **Scope that surfaces while working a ticket**). A follow-up raised but left unopened is carried to the **Pending follow-up guard** at handoff (`../../_shared/session-handoff.md`), re-posed at Resolve rather than dropped.
57
57
 
@@ -6,19 +6,26 @@
6
6
  // hook bus; this git hook is how the commit-guard survives there). Decision #16.
7
7
  //
8
8
  // Self-contained by design: detached from the trailhead install, it inlines the
9
- // same two rules as plugins/trailhead/hooks/lib/commit-message-check.js. That
10
- // lib's test drives this hook end-to-end so the two copies cannot drift.
9
+ // same rules as plugins/trailhead/hooks/lib/commit-message-check.js. That
10
+ // lib's test drives this hook end-to-end so the copies cannot drift.
11
11
  //
12
12
  // git passes the commit-message file path as argv[2]. On a violation: print the
13
13
  // reason to stderr and exit 1 (git aborts the commit). Otherwise exit 0.
14
14
  // Crash-safe: any unexpected error exits 0 (never wedge a commit).
15
+ //
16
+ // Note: a merge/revert/fixup message already fails the conventional-subject
17
+ // rule below, so it needs no special case; `git commit --amend` keeps whatever
18
+ // trailer the original message carried; `git commit --no-verify` bypasses this
19
+ // hook entirely, as before.
15
20
 
16
21
  const fs = require('fs');
22
+ const path = require('path');
17
23
 
18
24
  const TYPES = 'feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert';
19
25
  const CONVENTIONAL = new RegExp(`^(${TYPES})(\\([^)]+\\))?!?:\\s.+`);
20
26
  const CO_AUTHORED = /co-authored-by\s*:/i;
21
27
  const SCISSORS = '# ------------------------ >8';
28
+ const REFS_LINE = /^\s*refs\s*:(.*)$/i;
22
29
 
23
30
  function messageRegion(raw) {
24
31
  const text = String(raw == null ? '' : raw);
@@ -33,6 +40,40 @@ function subjectOf(raw) {
33
40
  return '';
34
41
  }
35
42
 
43
+ // Mirrors parseSessionTicket() in commit-message-check.js: parses the first
44
+ // line of a .trailhead/session-ticket marker (`#<n> <title>`).
45
+ function parseSessionTicket(text) {
46
+ const raw = String(text == null ? '' : text).replace(/^/, '');
47
+ const firstLine = raw.split('\n')[0].replace(/\r$/, '');
48
+ const m = firstLine.match(/^#([1-9]\d*)(?:\s|$)/);
49
+ return m ? Number(m[1]) : null;
50
+ }
51
+
52
+ // Mirrors hasRefsFor() in commit-message-check.js.
53
+ function hasRefsFor(message, n) {
54
+ const target = new RegExp('#' + n + '(?![\\w])');
55
+ for (const line of messageRegion(message).split('\n')) {
56
+ if (line.trim().startsWith('#')) continue;
57
+ const m = line.match(REFS_LINE);
58
+ if (m && target.test(m[1])) return true;
59
+ }
60
+ return false;
61
+ }
62
+
63
+ // Reads the session-ticket marker at the worktree top-level. git runs
64
+ // commit-msg from the worktree top-level (linked worktrees included), so
65
+ // process.cwd() is the right root here. Its own try/catch: a read error
66
+ // (marker absent, unreadable, etc.) must never skip the other rules below,
67
+ // it just leaves the Refs rule off.
68
+ function readTicket() {
69
+ try {
70
+ const markerPath = path.join(process.cwd(), '.trailhead', 'session-ticket');
71
+ return parseSessionTicket(fs.readFileSync(markerPath, 'utf8'));
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
36
77
  try {
37
78
  const file = process.argv[2];
38
79
  const raw = file ? fs.readFileSync(file, 'utf8') : '';
@@ -51,6 +92,12 @@ try {
51
92
  process.stderr.write('trailhead: commit subject must be 72 characters or less.\n');
52
93
  process.exit(1);
53
94
  }
95
+
96
+ const ticket = readTicket();
97
+ if (Number.isInteger(ticket) && ticket > 0 && !hasRefsFor(raw, ticket)) {
98
+ process.stderr.write(`trailhead: this commit is on ticket #${ticket} (per .trailhead/session-ticket) but has no "Refs: #${ticket}" trailer. Add "Refs: #${ticket}" as the last line of the message and commit again. If this commit is not part of ticket #${ticket}, remove the stale .trailhead/session-ticket marker first.\n`);
99
+ process.exit(1);
100
+ }
54
101
  } catch {
55
102
  process.exit(0); // never wedge a commit on an unexpected error
56
103
  }