@marcomigozzi/trailhead 0.9.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.
- package/README.it.md +11 -11
- package/README.md +11 -11
- package/bin/lib/codex-projection.js +13 -6
- package/bin/lib/codex-projection.test.js +45 -3
- package/bin/lib/install-verify.js +257 -0
- package/bin/lib/install-verify.test.js +379 -0
- package/bin/trailhead.js +91 -5
- package/bin/trailhead.test.js +588 -4
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/commands/quick.md +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.js +58 -2
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +103 -13
- package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +375 -0
- package/plugins/trailhead/hooks/lib/fixtures/commit-msg-a319156 +57 -0
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +32 -3
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +41 -0
- package/plugins/trailhead/skills/_shared/choices.md +3 -0
- package/plugins/trailhead/skills/_shared/configuration-reference.md +27 -20
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/principles.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +8 -6
- package/plugins/trailhead/skills/_shared/substrate-commands.md +1 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +2 -2
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +13 -4
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +15 -3
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/verify.md +2 -2
- package/plugins/trailhead/skills/_shared/techniques.md +2 -2
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +11 -10
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +3 -1
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +12 -9
- package/plugins/trailhead/templates/trailhead-commit-msg +49 -2
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +171 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// trailhead commit-msg hook (git). Host-independent guardrail: installed into
|
|
4
|
+
// .git/hooks/commit-msg at repo first-use so trailhead's commit discipline is
|
|
5
|
+
// enforced on EVERY `git commit`, whatever agent host drives it (Codex has no
|
|
6
|
+
// hook bus; this git hook is how the commit-guard survives there). Decision #16.
|
|
7
|
+
//
|
|
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.
|
|
11
|
+
//
|
|
12
|
+
// git passes the commit-message file path as argv[2]. On a violation: print the
|
|
13
|
+
// reason to stderr and exit 1 (git aborts the commit). Otherwise exit 0.
|
|
14
|
+
// Crash-safe: any unexpected error exits 0 (never wedge a commit).
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
|
|
18
|
+
const TYPES = 'feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert';
|
|
19
|
+
const CONVENTIONAL = new RegExp(`^(${TYPES})(\\([^)]+\\))?!?:\\s.+`);
|
|
20
|
+
const CO_AUTHORED = /co-authored-by\s*:/i;
|
|
21
|
+
const SCISSORS = '# ------------------------ >8';
|
|
22
|
+
|
|
23
|
+
function messageRegion(raw) {
|
|
24
|
+
const text = String(raw == null ? '' : raw);
|
|
25
|
+
const i = text.indexOf(SCISSORS);
|
|
26
|
+
return i === -1 ? text : text.slice(0, i);
|
|
27
|
+
}
|
|
28
|
+
function subjectOf(raw) {
|
|
29
|
+
for (const line of messageRegion(raw).split('\n')) {
|
|
30
|
+
if (line.trim() === '' || line.startsWith('#')) continue;
|
|
31
|
+
return line.trim();
|
|
32
|
+
}
|
|
33
|
+
return '';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const file = process.argv[2];
|
|
38
|
+
const raw = file ? fs.readFileSync(file, 'utf8') : '';
|
|
39
|
+
const region = messageRegion(raw);
|
|
40
|
+
|
|
41
|
+
if (CO_AUTHORED.test(region)) {
|
|
42
|
+
process.stderr.write('trailhead: do not add a Co-Authored-By trailer to commits. Remove it and commit again.\n');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
const subject = subjectOf(raw);
|
|
46
|
+
if (subject && !CONVENTIONAL.test(subject)) {
|
|
47
|
+
process.stderr.write(`trailhead: commit subject must be Conventional Commits: <type>(<scope>)?: <subject>. Valid types: ${TYPES.replace(/\|/g, ', ')}.\n`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
if (subject.length > 72) {
|
|
51
|
+
process.stderr.write('trailhead: commit subject must be 72 characters or less.\n');
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
process.exit(0); // never wedge a commit on an unexpected error
|
|
56
|
+
}
|
|
57
|
+
process.exit(0);
|
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
// are shared with trailhead-search-guard.js via lib/shell-scan.js, so the two
|
|
45
45
|
// guards can never drift into two divergent tokenizers again.
|
|
46
46
|
//
|
|
47
|
+
// #180 fix: `docker`/`podman` `--env-file .env` (or `--env-file=.env`) was a
|
|
48
|
+
// false positive: the container RUNTIME reads that file, not the agent. Only
|
|
49
|
+
// that flag's value is exempted, and only for docker/podman verbs; see
|
|
50
|
+
// ENV_FILE_EXEMPT_VERBS below.
|
|
51
|
+
//
|
|
47
52
|
// Deliberately best-effort on the Bash leg, like the sibling guards: not a
|
|
48
53
|
// full shell parser. It does not care about `cd` shape at all (that is
|
|
49
54
|
// trailhead-search-guard.js's job) — it only asks "does any token here
|
|
@@ -94,6 +99,18 @@ function isSecretPathArg(t) {
|
|
|
94
99
|
// every tool's flag surface, just the common file-value shapes.
|
|
95
100
|
const VALUE_FLAGS = new Set(['-f', '--file']);
|
|
96
101
|
|
|
102
|
+
// #180: `docker run --env-file .env img` / `docker compose --env-file
|
|
103
|
+
// .env.prod up` / `podman run --env-file=.env img` are false positives. The
|
|
104
|
+
// CONTAINER RUNTIME reads that file directly to populate the container's
|
|
105
|
+
// environment; its contents never pass through the agent, so it is not a
|
|
106
|
+
// secret READ by the agent (unlike a bare `cat .env`, which is). Scoped
|
|
107
|
+
// narrowly: only the value of `--env-file` is exempt, and only for these
|
|
108
|
+
// verbs; every other flag/positional in the same statement (image name,
|
|
109
|
+
// `-v`, trailing `cat .env`, etc.) is still scanned exactly as before.
|
|
110
|
+
const ENV_FILE_EXEMPT_VERBS = new Set(['docker', 'podman', 'docker-compose', 'podman-compose']);
|
|
111
|
+
const ENV_FILE_EXEMPT_FLAGS = new Set(['--env-file']);
|
|
112
|
+
const NO_EXEMPT_FLAGS = new Set();
|
|
113
|
+
|
|
97
114
|
// The pattern-first search verbs (grep/rg/ag/sed/awk), shared with
|
|
98
115
|
// trailhead-search-guard.js. Their first positional is a PATTERN, not a file
|
|
99
116
|
// operand, so it must be skipped before scanning for a secret FILE argument
|
|
@@ -119,8 +136,11 @@ function stripTrailingPunct(t) {
|
|
|
119
136
|
// Scan one already-tokenized statement's argument tokens (verb NOT stripped)
|
|
120
137
|
// for a secret path used as a plain positional, an option value
|
|
121
138
|
// (`--file=.env`, `-f .env`, glued `-f.env`), or a `<` (or glued `<file`)
|
|
122
|
-
// redirect target. Returns the offending token or null.
|
|
123
|
-
|
|
139
|
+
// redirect target. Returns the offending token or null. `exemptFlags` (#180)
|
|
140
|
+
// names flags whose value must be skipped entirely rather than scanned, in
|
|
141
|
+
// both the `--flag=value` and spaced `--flag value` forms.
|
|
142
|
+
function scanTokensForSecretPath(tokens, exemptFlags) {
|
|
143
|
+
const exempt = exemptFlags || NO_EXEMPT_FLAGS;
|
|
124
144
|
for (let i = 0; i < tokens.length; i++) {
|
|
125
145
|
const t = stripTrailingPunct(tokens[i]);
|
|
126
146
|
if (!t) continue;
|
|
@@ -143,10 +163,15 @@ function scanTokensForSecretPath(tokens) {
|
|
|
143
163
|
// `--flag=value` form.
|
|
144
164
|
const eq = t.indexOf('=');
|
|
145
165
|
if (eq > 0) {
|
|
166
|
+
const flagName = t.slice(0, eq);
|
|
167
|
+
if (exempt.has(flagName)) continue; // #180: value never scanned
|
|
146
168
|
const val = t.slice(eq + 1);
|
|
147
169
|
if (isSecretPathArg(val)) return val;
|
|
148
170
|
continue;
|
|
149
171
|
}
|
|
172
|
+
// #180: exempt flag's separate-token value is skipped entirely, not
|
|
173
|
+
// scanned as a plain positional on the next loop iteration.
|
|
174
|
+
if (exempt.has(t)) { i++; continue; }
|
|
150
175
|
// `-f value` / `--file value` (separate-token value).
|
|
151
176
|
if (VALUE_FLAGS.has(t)) {
|
|
152
177
|
const next = i + 1 < tokens.length ? stripTrailingPunct(tokens[i + 1]) : null;
|
|
@@ -214,7 +239,11 @@ function scanStatementForSecretPath(stmt) {
|
|
|
214
239
|
if (SEARCH_VERBS.has(bin)) {
|
|
215
240
|
return scanArgsSkippingPattern(tokens.slice(1), SEARCH_VERB_CONFIG[bin], isSecretPathArg);
|
|
216
241
|
}
|
|
217
|
-
|
|
242
|
+
// #180: docker/podman (incl. `docker compose` as a subcommand, and the
|
|
243
|
+
// standalone docker-compose/podman-compose binaries) get --env-file's value
|
|
244
|
+
// exempted; every other verb keeps today's behaviour unchanged.
|
|
245
|
+
const exempt = ENV_FILE_EXEMPT_VERBS.has(bin) ? ENV_FILE_EXEMPT_FLAGS : NO_EXEMPT_FLAGS;
|
|
246
|
+
return scanTokensForSecretPath(tokens, exempt);
|
|
218
247
|
}
|
|
219
248
|
|
|
220
249
|
// Walk the command's top-level statements (quote-aware; `|`, `;`, `&&`,
|
|
@@ -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);
|
|
@@ -46,6 +46,8 @@ This is the operational recipe every applicable call-site follows. It is **manda
|
|
|
46
46
|
- Delegate -> "Let the agent pick (with rationale, then confirm)" (IT: "Lascia scegliere all'agente (con motivazione, poi conferma)", short "scegli tu").
|
|
47
47
|
- Defer -> "Defer this decision" (IT: "Rimanda questa decisione", short "scelgo dopo").
|
|
48
48
|
|
|
49
|
+
**Menu call-sites (an `AskUserQuestion` trailhead authors): the dedup pass.** A menu may batch several questions (the host tool accepts up to four), and nothing else collapses them. Before assembling the menu, dedup the batch: when two questions resolve the SAME underlying decision (one restates or subsumes the other), merge them into a single question instead of asking the same problem twice; keep genuinely distinct questions separate. The discriminator is "same decision", not merely "same topic": two questions that share a subject but settle different things stay separate. This pass runs once over the whole batch, before the per-question cap rule below.
|
|
50
|
+
|
|
49
51
|
**Menu call-sites (an `AskUserQuestion` trailhead authors): the cap rule.** The host tool accepts only 2-4 real options (plus its own auto escape), so the affordances can be crowded out. The rule is deterministic: **fill up to the cap, then fall back to prose.**
|
|
50
52
|
- If **(real options + applicable affordances) <= 4**, add each applicable affordance as an explicit extra option in the same menu.
|
|
51
53
|
- If they **would exceed 4**, keep the real options in the menu and offer the affordance(s) that did not fit in **one line of prose immediately after the menu** (or as an immediate follow-up question). The cap never silently swallows them.
|
|
@@ -66,6 +68,7 @@ Worked examples: 2 real options + delegate + defer = 4 -> all in the menu; 3 rea
|
|
|
66
68
|
| `/trailhead config` menus | Yes | No (no substrate) |
|
|
67
69
|
| Cross-AI plan-review convergence | Yes | Sometimes (a concern blocked on a prerequisite) |
|
|
68
70
|
| Verify checkpoint (which fix / how to proceed) | Yes for the advisory sub-choice; accept/resolve stays the human's | No |
|
|
71
|
+
| Acceptance testing drive-mode ask (AI-driven vs human-driven) | Yes | No (now-or-never at UAT start) |
|
|
69
72
|
| Prototype "want mockups?" | Yes | No |
|
|
70
73
|
| Teamwork split shape / submodule split | Yes (the shape is advisory) | No |
|
|
71
74
|
| Claim collision | No (coordination stop) | No |
|
|
@@ -16,7 +16,9 @@ Effective config = built-in defaults ← global `~/.claude/trailhead/config.json
|
|
|
16
16
|
| `plan_review` | **`off`** \| `on` \| CLI list (`gemini,codex`) | send `build` PLANs to external AI CLIs for review (**Cross-AI plan review**) |
|
|
17
17
|
| `plan_review.rounds` | integer (**`2`**) | max converge-and-re-review rounds |
|
|
18
18
|
| `code_review.rounds` | integer (**`2`**) | max fix-and-re-review rounds in **Code review** before the boundary HITL checkpoint |
|
|
19
|
-
| `effort` | **`standard`** \| `lean` | how hard the `build`/`bug
|
|
19
|
+
| `effort` | **`standard`** \| `lean` | how hard the `build`/`bug` engine works a change: `standard` runs every step; `lean` size-triages and offers to skip heavy steps (plan pass, TDD, cross-AI review, full code review, goal-backward verify) on small changes. **Governs `build`/`bug` only; `quick` always size-triages regardless of this key** (see effort below) |
|
|
20
|
+
|
|
21
|
+
**`git`, `release`, and `isolation` are not config.json keys.** They are **conventions-header keys**, stored in the repo's `trailhead:conventions` issue header (documented in `substrate.md`), and are reachable from the guided setup's **Way of working** step, not from this table.
|
|
20
22
|
|
|
21
23
|
**Models.** Each key targets a specific activity, so you can send the heavy reasoning to a strong model and the cheap legwork to a fast one:
|
|
22
24
|
- `plan` / `execute`: the `build`/`bug` engine's **Plan** and **Execute** steps (see below).
|
|
@@ -35,11 +37,11 @@ Effective config = built-in defaults ← global `~/.claude/trailhead/config.json
|
|
|
35
37
|
|
|
36
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.
|
|
37
39
|
|
|
38
|
-
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-
|
|
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.
|
|
39
41
|
|
|
40
|
-
**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.
|
|
41
43
|
|
|
42
|
-
**Model-tier profiles.** Three profile seeds for the guided setup's Models step (§
|
|
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.
|
|
43
45
|
|
|
44
46
|
| profile | plan | execute | research | review | debug | verify | codebase-map |
|
|
45
47
|
|---|---|---|---|---|---|---|---|
|
|
@@ -48,14 +50,14 @@ If a value is unset, it resolves to its **Balanced**-profile default for that ac
|
|
|
48
50
|
| **Low** | standard | standard | fast | fast | standard | fast | fast |
|
|
49
51
|
|
|
50
52
|
**Tier-class -> id**, resolved from the lineup actually reachable at setup time (verify, don't trust these verbatim):
|
|
51
|
-
- **Claude** (`models.*`, ids only, no effort): strong = Opus-class, resolved to **`claude-opus-
|
|
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.
|
|
52
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`.
|
|
53
55
|
|
|
54
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.
|
|
55
57
|
|
|
56
58
|
**tdd.** `seams` (default) = TDD at critical seams only; `on` = TDD for all behaviour with a definable input→output; `off` = tests after, or none for throwaway. The `build` engine's Plan/Execute honour this.
|
|
57
59
|
|
|
58
|
-
**acceptance.browser.** `auto` (default) = drive
|
|
60
|
+
**acceptance.browser.** The drive-mode ask (AI-driven vs human-driven, see the Acceptance testing technique) **always comes first, before any step runs**: no value of this key pre-answers it. This key only governs whether and how an AI-driven step uses the browser once that ask is answered AI-driven: `auto` (default) = browser-drive when `testing.webapp` + `testing.url` are set, or (when `testing.webapp` is set but `testing.url` is unset) first try to auto-detect the target URL (running dev server, package scripts, dev-server/framework config, project config) and propose it, driving against the accepted URL; when no URL can be determined, fall back per the Acceptance testing technique (ask the human for the URL, or the guided UAT for that step); `on` = always attempt browser-driving, auto-detecting the URL the same way when `testing.url` is unset; `off` = never drive a browser. **`off` disables *browser-driving*, not acceptance, and not AI-driven checking**: AI-driven still runs its non-browser checks via commands / the test suite for whatever steps allow it; only a step that genuinely needs the human (or the whole UAT, under human-driven) gets the guided UAT walkthrough. The Verify step honours this.
|
|
59
61
|
|
|
60
62
|
**design.surface.** Under `design: claude.ai/design`, which Claude Design surface the mockup lives on. `canvas` (default) = a one-off screen to react to, edited visually and exportable as a live prototype; under `claude.ai/design` it is **not** MCP-driven (no MCP creates a regular canvas project): it's the built-in `/design` skill published as an **Artifact**, or a claude.ai/design project made by hand in the web app whose `/p/<id>` URL you paste. `design-system` = a `PROJECT_TYPE_DESIGN_SYSTEM` project driven by DesignSync (the `/design-sync` slice of the official Claude Design MCP), for a shared component library kept in sync. Unset, it infers from `design.project`: a cached project (which, before this key, was always a design system) means `design-system`; no cached project means `canvas`. The Prototype technique honours this; when the **`design-system`** surface's MCP isn't connected it **offers a one-time install** (`claude mcp add ... claude-design`, then `/design-login`) before falling to disk (the `canvas` surface needs no MCP, so no install offer). Under `design: stitch` the same key applies: `canvas` = a Stitch project with generated screens, `design-system` = a Stitch design system; the install offer and disk fallback work the same way (no intra-provider chain, the Stitch MCP either connects or falls to disk). Ignored under `design: disk`.
|
|
61
63
|
|
|
@@ -65,12 +67,12 @@ If a value is unset, it resolves to its **Balanced**-profile default for that ac
|
|
|
65
67
|
|
|
66
68
|
**code_review.rounds.** Integer, default 2. The max fix-and-re-review rounds the **Code review** technique spends chasing **Critical** (blocking) findings during the `build`/`bug` Verify step. When the budget is exhausted with Critical findings still open, the technique never resolves or continues silently: it surfaces an explicit HITL checkpoint (run another 2 rounds vs proceed with the blockers recorded). Mirrors `plan_review.rounds`.
|
|
67
69
|
|
|
68
|
-
**effort (cost-aware gating).** `standard` (default) = the full flow: every `build`/`bug
|
|
70
|
+
**effort (cost-aware gating).** `standard` (default) = the full flow for the map cycle: every `build`/`bug` step runs at full cost, no triage (today's behaviour, backward-compatible). `lean` = **cost-aware**, defined **canonically here**; the `build`/`bug` engine references this section under `lean` and adds only its own timing. **`quick` is the exception: it runs the size triage and the offer below on *every* ticket, independent of `effort`** (reaching for `quick` is itself the small-piece signal; `effort` governs the map cycle, not `quick`, see `../trailhead-work/SKILL.md`). Under `lean` (and always for `quick`) the engine runs a **size triage** in the orchestrator (main session, inline, no subagent, so the triage itself costs almost nothing), classifying the change predictively from the ticket scope and a quick glance at the target files (the real diff is unknown before Execute):
|
|
69
71
|
- **trivial**: a single file, a handful of lines, no new seam, no new dependency, no architectural change (a copy/CSS/doc/constant edit).
|
|
70
72
|
- **small**: localized, one or two files, within existing seams, no new dependency, no architectural change.
|
|
71
73
|
- **normal**: anything else. **Risk floor:** a change is **never** trivial/small, however few its lines, when it touches security or auth, a data migration, a public API or contract, money or permissions, or concurrency, or when it adds or updates a dependency; such a change is always `normal`. When in doubt, classify up.
|
|
72
74
|
|
|
73
|
-
On **trivial/small** the engine surfaces **one consolidated skip offer** and acts only on an **explicit opt-in**: it names the concrete heavy steps proposed to skip for *this* change and the safety trade-off, then goes lean **only on a clear yes**. On a no, on silence, or on any ambiguity, it keeps the **full flow** (the fail-safe is always toward more checking, never less). The heavy steps it may offer to skip are the **plan subagent** (replaced by a short inline plan, never zero plan), **TDD**, **Cross-AI plan review**, the full adversarial **Code review** subagent (replaced by a lighter inline pass), and the **Goal-backward verification** subagent (replaced by a quick inline goal check); the two inline replacements are dropped entirely only when the change is truly cosmetic. The offer lists **only steps actually in play** for this ticket and config (it will not mention Cross-AI plan review when `plan_review` is off, or TDD when `tdd: off`). On **normal
|
|
75
|
+
On **trivial/small** the engine surfaces **one consolidated skip offer** and acts only on an **explicit opt-in**: it names the concrete heavy steps proposed to skip for *this* change and the safety trade-off, then goes lean **only on a clear yes**. On a no, on silence, or on any ambiguity, it keeps the **full flow** (the fail-safe is always toward more checking, never less). The heavy steps it may offer to skip are the **plan subagent** (replaced by a short inline plan, never zero plan), **TDD**, **Cross-AI plan review**, the full adversarial **Code review** subagent (replaced by a lighter inline pass), and the **Goal-backward verification** subagent (replaced by a quick inline goal check); the two inline replacements are dropped entirely only when the change is truly cosmetic. The offer lists **only steps actually in play** for this ticket and config (it will not mention Cross-AI plan review when `plan_review` is off, or TDD when `tdd: off`). On **normal** there is no offer and the full flow runs; the `build`/`bug` engine likewise runs the full flow with no triage under `effort: standard`, whereas `quick` always triages (it just finds nothing to offer on a `normal` ticket). This offer **always includes** the "let the agent pick (with rationale, then confirm)" delegate option, per the injection recipe in `choices.md`.
|
|
74
76
|
|
|
75
77
|
**Never in the offer** (always kept, whatever the size or the answer): atomic `Refs:` commits, running the tests / the plan's verification criterion, the post-commit self-check, `bug`'s Diagnose (a confirmed root cause is mandatory before Fix), and Resolve. `effort` scales *redundant heavy* work to the change; it never drops a correctness check.
|
|
76
78
|
|
|
@@ -78,33 +80,38 @@ On **trivial/small** the engine surfaces **one consolidated skip offer** and act
|
|
|
78
80
|
|
|
79
81
|
Example `config.json` (same shape for the project `.trailhead/config.json` and the global `~/.claude/trailhead/config.json`):
|
|
80
82
|
```json
|
|
81
|
-
{ "models": { "plan": "claude-opus-
|
|
83
|
+
{ "models": { "plan": "claude-opus-5-5", "execute": "claude-sonnet-5-5" }, "tdd": "seams", "acceptance": { "browser": "auto" } }
|
|
82
84
|
```
|
|
83
85
|
|
|
84
86
|
## Guided setup
|
|
85
87
|
|
|
86
|
-
`/trailhead config` (no args) runs an **interactive walkthrough**: never make the user hand-edit JSON. Present each step as an `AskUserQuestion` menu with **icon-labelled options** and the current value pre-selected; write the answers to the chosen scope at the end and show a summary. These menus **offer** the delegate option (agent picks, with rationale, then confirm), per the injection recipe in `choices.md`: as an explicit menu option where the cap allows, otherwise in a prose line right after the menu (several of these steps already list 4 or more real options, so there delegate is offered in prose).
|
|
88
|
+
`/trailhead config` (no args) runs an **interactive walkthrough**: never make the user hand-edit JSON. Present each step, the config **scope** step 1 included, as an `AskUserQuestion` menu with **icon-labelled options** and the current value pre-selected (the scope is a selectable choice too, not a free-text prose lead-in); write the answers to the chosen scope at the end and show a summary. The **Way of working** step (step 2) is the one exception to "the chosen scope": its keys (`git`/`release`/`isolation`) are written to the repo's `trailhead:conventions` issue header, never to `.trailhead/config.json`, because they are repo-shared conventions, not per-scope config. These menus **offer** the delegate option (agent picks, with rationale, then confirm), per the injection recipe in `choices.md`: as an explicit menu option where the cap allows, otherwise in a prose line right after the menu (several of these steps already list 4 or more real options, so there delegate is offered in prose).
|
|
87
89
|
|
|
88
90
|
**Never skip a step.** Walk every step in order **and** every conditional sub-step its branch activates; never omit one, and **never silently take a default for a step the flow reaches**. The pre-selected value is a convenience, not licence to not ask; a step counts as done only once the user has actually answered it. If a choice opens follow-ups (e.g. *pick specific CLIs* → which ones **and** how many rounds; *browser on* → base URL), ask **each** before moving on.
|
|
89
91
|
|
|
90
|
-
1. **Scope
|
|
91
|
-
2.
|
|
92
|
-
|
|
92
|
+
1. **Scope** (a selectable `AskUserQuestion` menu like every config-key step below, rendered as a numbered list on Codex and the native picker on Claude, **never a free-text prose question**): 📁 This project (`.trailhead/config.json` at the repo root, the in-repo default) · 🌍 Global default (`~/.claude/trailhead/config.json`). Skip the menu and write global directly only when run outside a repo (there is no project scope to offer there).
|
|
93
|
+
2. **⚙️ Way of working** (`git` · `release` · `isolation`): the conventions-header keys that govern how work is committed, released, and isolated. **These live in the repo's `trailhead:conventions` issue header, not in `.trailhead/config.json`**, so they are written there (repo-scoped, shared by every map of this repo), independent of the Scope chosen in step 1. **Skip this step entirely when run outside a repo** (there is no conventions issue to write). Present each as its own `AskUserQuestion` menu, current header value pre-selected:
|
|
94
|
+
- **🔀 Git**: 🌱 `main` (commit straight to the trunk) · 🌿 `pr` (feature branch + PR).
|
|
95
|
+
- **🚀 Release**: 🎯 `command` (release only on an explicit command) · ♻️ `auto` (release automatically per the project's flow).
|
|
96
|
+
- **🧬 Isolation**: 🤖 `auto` (default; decide per ticket at work-start) · 📍 `none` (work in the current checkout) · 🌳 `worktree` (a `git worktree` + `trailhead/t<n>` branch per ticket) · 📦 `clone` (a dedicated per-ticket clone, for path-bound apps a worktree can't build and for submodule superrepos).
|
|
97
|
+
Write any changed value back into the `trailhead:conventions` issue header (a body read-modify-write: obey the **Fail-safe body rewrites** rule in `substrate.md`), leaving the human prose below the header untouched. **If no `trailhead:conventions` issue exists yet** (a repo not charted/adopted): say conventions are normally set at `/trailhead:new`/`:adopt`, and offer to create a minimal header issue with the chosen values, or skip. Creating it here first runs the **first-use repo setup** (see `substrate-commands.md`) so the `trailhead:conventions` label exists before `gh issue create --label trailhead:conventions` (an uncharted repo has no `trailhead:*` labels yet, so the create would otherwise fail).
|
|
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
|
+
4. **🧠 Models**: a **profile** (Balanced by default) applied whole, or **Manual** for the per-key questions.
|
|
93
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`.
|
|
94
|
-
- **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-
|
|
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.
|
|
95
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.)
|
|
96
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`.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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).
|
|
105
|
+
6. **🧪 TDD**: 🎯 Seams only · ✅ On (all definable behaviour) · ⛔ Off.
|
|
106
|
+
7. **⚖️ Effort**: how hard the `build`/`bug` engine works each change (`effort`). 🛠️ **Standard** (default; every step runs) · 🍃 **Lean** (size-triage small changes and offer to skip heavy steps, on an explicit yes only: the plan pass, TDD, cross-AI review, the full code review, the goal-backward verify; correctness checks always stay). Governs `build`/`bug` only: `quick` always size-triages regardless.
|
|
107
|
+
8. **🖥️ Acceptance testing**: 🤖 Auto · ▶️ Always browser · 🚫 No browser (acceptance still runs, starting with the drive-mode ask; only browser-driving is off, see `acceptance.browser` above). If browser is enabled, ask **🔗 base URL** and confirm it's a web app.
|
|
108
|
+
9. **🧑⚖️ Plan review**: **before presenting this menu, first probe the PATH for known external AI CLIs** (probe **every** CLI in the Cross-AI plan review roster, do not shorten the list: `claude`, `gemini`, `codex`, `qwen`, `kimi`, `agy`, `opencode`, `cursor-agent`, `coderabbit`, plus any OpenAI-compatible local server configured; `kimi` counts only when `kimi --help` mentions `--output-format`, per the technique): `command -v` each one, then build the menu from what was actually found. **Exclude the current host's own CLI** (never offer `codex` when running on Codex, nor `claude` when running on Claude Code): a session reviewing a plan with its own model is not cross-AI, so the host's own CLI must never appear as an option. Never present this step with a hard-coded two-option list; the options are computed from the probe:
|
|
102
109
|
- **No CLI found**: offer only ⛔ Off, and say in the prompt that no external AI CLI was detected on the PATH (so the other choices would do nothing).
|
|
103
110
|
- **One CLI found**: ⛔ Off · 🤝 Use `<name>`. (With a single CLI, "all" and "pick specific" collapse to the same thing, so don't show both.)
|
|
104
111
|
- **Two or more found**: ⛔ Off · 🤝 All detected (`<name>`, `<name>`, …) · 🎯 Pick specific. Always show all three; never drop *Pick specific*. On *Pick specific*, ask **which CLIs** as a **multi-select** listing each detected CLI by name (never a free-text field). List the actual names found by the probe, not the placeholder examples from this file.
|
|
105
112
|
|
|
106
113
|
**Anything but Off has mandatory follow-ups, never skip them:** after the CLIs are settled (whether via *All detected* or *Pick specific*), ask **🔁 rounds** (default 2). Off is the only branch with no follow-up; every other path asks rounds explicitly rather than taking the default.
|
|
107
|
-
|
|
114
|
+
10. **📊 Statusline**: offer trailhead's **Claude Code status bar**, and **say in the prompt what it shows** so the choice is informed. It renders one line: **model · project · branch · plan usage (`5h %` · `⏳ reset` · `7d %`) · a context-window bar (`362k/1000k (45%)`)**, plus a **second line with the active ticket** (`▸ #N Title`) whenever one is in play (from the `trailhead/t<n>` branch or the `.trailhead/session-ticket` marker). The plan-usage % comes from Anthropic's own usage endpoint (the same source the Claude UI uses); the context bar and ticket are computed locally. Options: ⛔ **Off** (default; leave your statusline untouched) · ✅ **Enable**.
|
|
108
115
|
- On **Enable**, install it: copy the shipped script (`${CLAUDE_PLUGIN_ROOT}/templates/statusline-trailhead.sh` for a plugin install, or `~/.claude/trailhead/templates/statusline-trailhead.sh` for an npm install, whichever exists) to `~/.claude/statusline-trailhead.sh` (executable), then set `statusLine` in `~/.claude/settings.json` to `{ "type": "command", "command": "bash \"$HOME/.claude/statusline-trailhead.sh\"" }`. **This is a global Claude Code setting, not per-project**, so it always lands in `~/.claude/settings.json` regardless of the scope chosen in step 1.
|
|
109
116
|
- **If a `statusLine` is already configured** (e.g. `ccstatusline`), **do not clobber it silently**: show what it is and ask whether to **replace** it with trailhead's, or **keep** the current one (in which case, mention that the script also supports single-segment widgets, `--ticket-only` / `--context-only` / `--usage-only`, to add into an existing statusline tool). Needs `jq` and `python3` on the PATH; if either is missing, say so and skip the enable rather than wiring a broken command.
|
|
110
117
|
|
|
@@ -13,4 +13,6 @@ Before doing anything, read the shared core, in order:
|
|
|
13
13
|
5. `../_shared/configuration.md`: the three config layers and the load contract.
|
|
14
14
|
6. `../_shared/techniques.md`: the technique index and the subagent-type rule.
|
|
15
15
|
|
|
16
|
+
**Host command forms (standing render rule).** Every trailhead command you write in prose you generate at runtime, the session handoff, a capture confirmation, any next-step suggestion, and not only where a doc example already shows one, uses **this host's command forms**: `/clear` to clear context, and the `/trailhead:<verb>` family (`/trailhead:work <n>`, `/trailhead:quick <n>`, `/trailhead:map`, `/trailhead:dashboard`). These literals are this host's surface. **Never emit a command form that belongs to a different host** (one you may have read elsewhere in this repository, such as in the projection code and its tests, or earlier in the session): the forms stated in this contract are the only ones correct for the host you are running on.
|
|
17
|
+
|
|
16
18
|
Load the **effective config** (per `../_shared/configuration.md`) at session start, from the map's project root (`.trailhead/config.json` plus the global file), before any isolation workspace is set up.
|
|
@@ -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.
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
## Session handoff
|
|
2
2
|
|
|
3
|
-
Resolving a ticket **ends the session**: one ticket per session is a hard rule (see [Principles](principles.md)). So every resolution closes the same way. After the resolution comment + `gh issue close` + the `Decisions so far` update, **remove the session-ticket marker** (`.trailhead/session-ticket` at the working root, if you wrote one, see Mode 2), then **always** sign off with, in order:
|
|
3
|
+
Resolving a ticket **ends the session**: one ticket per session is a hard rule (see [Principles](principles.md)). So every resolution closes the same way. Render every command in this ritual in this host's command forms (the **Host command forms** rule in `load-first.md`). After the resolution comment + `gh issue close` + the `Decisions so far` update, **remove the session-ticket marker** (`.trailhead/session-ticket` at the working root, if you wrote one, see Mode 2), then **always** sign off with, in order:
|
|
4
4
|
|
|
5
|
-
1. **A short recap of what was done** (2 to 4 lines) for the resolved ticket: the change in plain language, the files or areas touched (when applicable), and the outcome (verified / released / next step). Keep it **result-oriented**: say what changed and what it means, never trailhead's internal machinery (no mode or step names, no engine-role or technique names). It **augments** the ritual (it does not replace anything below): the recap sits above the one-line confirmation and gives the user a quick account of the ticket without reconstructing it from the commits or the issue timeline. It rides with a **resolution** only; a decision-ticket resolution may touch no files, hence "when applicable", and a capture or research batch (which never reach this ritual) carry no recap.
|
|
5
|
+
1. **A short recap of what was done** (2 to 4 lines) for the resolved ticket: the change in plain language, the files or areas touched (when applicable), and the outcome (verified / released / next step). Keep it **result-oriented**: say what changed and what it means, never trailhead's internal machinery (no mode or step names, no engine-role or technique names). It **augments** the ritual (it does not replace anything below): the recap sits above the one-line confirmation and gives the user a quick account of the ticket without reconstructing it from the commits or the issue timeline. It rides with a **resolution** only; a decision-ticket resolution may touch no files, hence "when applicable", and a capture or research batch (which never reach this ritual) carry no recap. It also carries forward any **pending follow-up** the Pending follow-up guard below identified, so a follow-up surfaced while working never has to be reconstructed from scrollback.
|
|
6
6
|
2. **A one-line confirmation** the ticket is resolved, by name (not a bare `#number`).
|
|
7
7
|
3. **The next step: a scannable block whose FIRST line is `/clear`, always.** Never bury it in prose, and **never point at the next ticket without leading with `/clear`**. `/clear` is its own first bullet, phrased as the first thing to do ("fai `/clear`, poi..."): **never demote it to a trailing parenthetical** on the next command. `/trailhead:work 29 (dopo /clear)` is the bug, not the fix: the reader runs the command and only then reads the note, when the context is already dirty. State the `/clear` **every time** (not only when context is heavy): it must come *before* the next command, on its own line first, so the next ticket starts on clean context. Render it as a short labelled list in the user's conversation language, along these lines:
|
|
8
8
|
|
|
9
9
|
> **Prossimo passo:**
|
|
10
10
|
> - `/clear`: per liberare il contesto
|
|
11
|
-
> - poi `/trailhead:work <numero>` per lavorare **<nome del prossimo ticket>** (oppure `/trailhead:work` che prende **<numero>** in automatico)
|
|
11
|
+
> - poi `/trailhead:work <numero>` per lavorare **<nome del prossimo ticket>** (il ciclo completo; oppure `/trailhead:work` che prende **<numero>** in automatico)
|
|
12
12
|
> - in alternativa `/trailhead:map` per la vista completa e scegliere tu, o `/trailhead:dashboard` per l'indice di tutte le mappe e del whiteboard
|
|
13
|
-
> - oppure `/trailhead:quick
|
|
13
|
+
> - oppure `/trailhead:quick "<il pezzo>"` per lavorare un pezzo fuori mappa: lo stesso lavoro di `work`, senza split, aperto e portato a termine subito
|
|
14
14
|
|
|
15
|
-
Name the next frontier ticket so the user knows what they're taking; put its **number** in the command (never the title). **When the frontier holds several mutually independent tickets, don't crown one: present them as a set to choose from**, naming each with its number (see **Frontier order carries no priority**). Bare `/trailhead:work` still acts on one, but the block keeps the choice visible. The block is the list of things to do **after the `/clear`** (it leads with it) and stays on screen in the terminal to read from, so each command must be either self-sufficient (a number already known) or safe to type by hand. **This self-sufficiency governs *every* line after `/clear`, not only the commands: never sequence after `/clear` an action the *agent* must perform that depends on context `/clear` will erase** (an "I'll open the ticket for you" gated on a decision taken this session). `/clear` wipes that context, so the agent cannot honour it and the user is stuck: clearing loses what the step needs, while not clearing violates the `/clear`-first rule. When an agent-side follow-up is genuinely needed, resolve it one of two ways: **do the agent action *before* the handoff** (perform it now, then render the `/clear`-first block pointing at its result), or **convert the next step into a user-runnable command** (`/trailhead:work <n>` with the resulting number, `/trailhead:map` for the user to choose, or the diverge-first `/trailhead:ticket` framing); never "I'll do it after you clear." The commands a handoff block may contain are `/clear`, `/trailhead:work` (bare or `<number>`), `/trailhead:map`, `/trailhead:dashboard`, and `/trailhead:quick`; **in a whiteboard session** (the resolved ticket was `trailhead:whiteboard`), also `/trailhead:whiteboard`. **Always offer the read-only views alongside the next action**: `/trailhead:map` for the low-res map and `/trailhead:dashboard` for the repo-wide index (all maps + the whiteboard), so the user can look before choosing. **`/trailhead:quick` is the one command that may carry a free-text argument here**: `/trailhead:quick "<short seed>"` when a concrete off-map piece is actually at hand (its argument is forgiving freeform seed text, not an exact title to match, so pre-filling it is helpful, not error-prone), or **bare `/trailhead:quick`** as the generic "if you want to work a piece off the map" offer, which prompts for the piece itself; keep any seed short and quote-safe. **`/trailhead:work` always takes the number, never the title** of an existing ticket, and **never hand the user a `/trailhead:ticket <type> "<title>"`** (or any other title-bearing command): adding a map ticket needs the diverge-first framing (Mode 3), so it is never pre-filled.
|
|
15
|
+
Name the next frontier ticket so the user knows what they're taking; put its **number** in the command (never the title). **When the frontier holds several mutually independent tickets, don't crown one: present them as a set to choose from**, naming each with its number (see **Frontier order carries no priority**). Bare `/trailhead:work` still acts on one, but the block keeps the choice visible. The block is the list of things to do **after the `/clear`** (it leads with it) and stays on screen in the terminal to read from, so each command must be either self-sufficient (a number already known) or safe to type by hand. **This self-sufficiency governs *every* line after `/clear`, not only the commands: never sequence after `/clear` an action the *agent* must perform that depends on context `/clear` will erase** (an "I'll open the ticket for you" gated on a decision taken this session). `/clear` wipes that context, so the agent cannot honour it and the user is stuck: clearing loses what the step needs, while not clearing violates the `/clear`-first rule. When an agent-side follow-up is genuinely needed, resolve it one of two ways: **do the agent action *before* the handoff** (perform it now, then render the `/clear`-first block pointing at its result), or **convert the next step into a user-runnable command** (`/trailhead:work <n>` with the resulting number, `/trailhead:map` for the user to choose, or the diverge-first `/trailhead:ticket` framing); never "I'll do it after you clear." The commands a handoff block may contain are `/clear`, `/trailhead:work` (bare or `<number>`), `/trailhead:map`, `/trailhead:dashboard`, and `/trailhead:quick`; **in a whiteboard session** (the resolved ticket was `trailhead:whiteboard`), also `/trailhead:whiteboard`. **Always offer the read-only views alongside the next action**: `/trailhead:map` for the low-res map and `/trailhead:dashboard` for the repo-wide index (all maps + the whiteboard), so the user can look before choosing. **`/trailhead:quick` is the one command that may carry a free-text argument here**: `/trailhead:quick "<short seed>"` when a concrete off-map piece is actually at hand (its argument is forgiving freeform seed text, not an exact title to match, so pre-filling it is helpful, not error-prone), or **bare `/trailhead:quick`** as the generic "if you want to work a piece off the map" offer, which prompts for the piece itself; keep any seed short and quote-safe. **Describe `work` and `quick` correctly wherever the block characterizes them:** `work` is the full cycle (Discuss -> Plan -> Execute -> Verify, with TDD during Execute per `config.tdd`); `quick <n>` works that same existing ticket whole via the lighter, no-split path (always size-triaged), a map ticket stays on its map; only `quick "<text>"` opens a new ticket off the map (the whiteboard). Never describe `quick <n>` on a map ticket as off the map. Neither is "the split command" (that is the separate `split` verb), so never frame `work` as splitting. Anchor `/trailhead:work <n>` first and offer `/trailhead:quick <n>` alongside as the no-split alternative, never in its place and never rendered twice on one line. **`/trailhead:work` always takes the number, never the title** of an existing ticket, and **never hand the user a `/trailhead:ticket <type> "<title>"`** (or any other title-bearing command): adding a map ticket needs the diverge-first framing (Mode 3), so it is never pre-filled.
|
|
16
16
|
|
|
17
|
-
**If the frontier is empty**, that usually means fog that this resolution made specifiable should have been **graduated into a ticket already** (Mode 2 step 6), so the frontier isn't actually empty. If it genuinely is (nothing ticketable yet, or the user must choose which fog item to take next), point the block to **`/trailhead:map`** and offer to open the next ticket *yourself* once they say which one (named by its number in the numbered `/trailhead:map` fog list), rather than pre-filling a `/trailhead:ticket` command for them. **Fog still on the map takes priority over loose whiteboard work.** If the map still has fog (unspecified areas in *Not yet specified*), lead with **`/trailhead:map`** to dissolve it (its fog list numbers each entry, so a specific point can be named by number): that fog is the map's own remaining work, so graduating it comes before any off-map ticket. **Only when the map is genuinely complete** (frontier empty *and* no fog left) and the whiteboard still holds open loose tickets, do not sign off as if nothing remains: check for them and, if any exist, **surface them**, pointing the block at **`/trailhead:whiteboard`** (or **`/trailhead:dashboard`** for the full index) and naming the loose tickets, anchored on **`/trailhead:work <n>`** (the full
|
|
17
|
+
**If the frontier is empty**, that usually means fog that this resolution made specifiable should have been **graduated into a ticket already** (Mode 2 step 6), so the frontier isn't actually empty. If it genuinely is (nothing ticketable yet, or the user must choose which fog item to take next), point the block to **`/trailhead:map`** and offer to open the next ticket *yourself* once they say which one (named by its number in the numbered `/trailhead:map` fog list), rather than pre-filling a `/trailhead:ticket` command for them. **Fog still on the map takes priority over loose whiteboard work.** If the map still has fog (unspecified areas in *Not yet specified*), lead with **`/trailhead:map`** to dissolve it (its fog list numbers each entry, so a specific point can be named by number): that fog is the map's own remaining work, so graduating it comes before any off-map ticket. **Only when the map is genuinely complete** (frontier empty *and* no fog left) and the whiteboard still holds open loose tickets, do not sign off as if nothing remains: check for them and, if any exist, **surface them**, pointing the block at **`/trailhead:whiteboard`** (or **`/trailhead:dashboard`** for the full index) and naming the loose tickets, anchored on **`/trailhead:work <n>`** (the full cycle Discuss -> Plan -> Execute -> Verify; for a whiteboard ticket it works the ticket whole, and split stays available only if it proves oversized, but `work` is not itself the split command) with `/trailhead:quick <n>` offered alongside as the no-split alternative (the same ticket, worked whole via the lighter no-split path), never in its place. **In a whiteboard session** the fog-graduation reasoning does not apply (the whiteboard has no fog): point the block at **`/trailhead:whiteboard`**, anchored on **`/trailhead:work <n>`** for the next loose ticket (the full cycle; split stays available only if it proves oversized) with `/trailhead:quick <n>` alongside as the no-split alternative (the same ticket, worked whole via the lighter no-split path), not the map. (The example is Italian; match whatever language you're conversing in.)
|
|
18
18
|
|
|
19
19
|
**Pending codebase offer (greenfield): resolve it *before* this block, exactly once.** If this session's Work-the-map step 1 marked a greenfield codebase-issue offer **pending** (the repo has no `trailhead:codebase` issue but now holds substantial code), surface the offer and resolve it **before rendering the `/clear`-first next-step block**, never as a line inside that block. This is deliberate: its "yes" triggers an **agent action** (generate and pin the codebase issue), and the self-sufficiency rule above forbids an agent-side action sequenced after `/clear`, so the offer is settled while the agent still holds context, exactly the "do the agent action before the handoff" resolution that rule prescribes. It is **an offer the user accepts with a yes, not a command they type** (there is no `/trailhead:codebase` command), so pose it as a yes/no, along the lines of *"vuoi che generi l'issue codebase adesso? (una mappa del repo, fissata tra i pin)"* (in the user's conversation language). Ask it **exactly once**: on a yes, generate and pin it now via the **Codebase map** technique, *then* render the `/clear`-first block (which may point at the freshly pinned issue); on a no or silence, render the block without it and don't nag again this session (the pending state is re-derived from repo state each session, so an offer left unresolved, whether declined or simply unanswered, re-surfaces next session until a `trailhead:codebase` issue exists, and nothing is lost). A whiteboard handoff never carries it (even if `work <n>` ran step 1 against a background map, the offer belongs to that map, not this loose ticket).
|
|
20
20
|
|
|
21
|
+
**Pending follow-up guard: re-pose it *before* this block, exactly once.** If, while working this ticket, a **follow-up worth opening surfaced but was not opened** (a divergent or out-of-scope request raised at the [work brief gate](../trailhead-work/SKILL.md) or mid-work under *Scope that surfaces while working a ticket*, that the user deferred or left unsettled rather than capturing on the spot), **carry it into the recap and re-pose the choice at the handoff, before rendering the `/clear`-first block**, never as a line inside that block. This mirrors the **Pending-UAT guard** (the `build`/`bug` Resolve step): a surfaced-but-unopened follow-up gets the same "do not let it die silently" guarantee. Pose it as an explicit two-way choice, in the user's conversation language: **(a) open a tracked ticket for it now** (a `todo` / `bug` / `seed` via the capture cluster, or a map ticket via the chart cluster's `/trailhead:ticket`, as fits), or **(b) let it go, or do it later as `/trailhead:quick`**. Keep **both** paths on the table: `quick` alone is not the whole offer, opening a tracked ticket must stay available, and that is the difference from the plain next-step block, which only offers `quick`. Because opening a ticket is an **agent action**, settle it here while the agent still holds context (exactly the "do the agent action before the handoff" resolution the self-sufficiency rule above prescribes, since `/clear` would erase what a post-clear capture needs): on **(a)**, open the ticket now (via the capture cluster for a `todo`/`bug`/`seed`, or the chart cluster's `/trailhead:ticket` for a map ticket), *then* render the `/clear`-first block anchored on `/trailhead:work <n>` for the freshly opened ticket (with `/trailhead:quick <n>` alongside as the no-split alternative, never in its place); on **(b)**, render the block without it, and when a concrete piece is at hand offer `/trailhead:quick "<the follow-up>"` as the off-map alternative. Re-pose it **exactly once** and **wait for an explicit answer**: never open a tracked ticket without the user's explicit yes. On **silence or an ambiguous reply**, treat it as (b) (render the block, offer `quick`) and do not nag again, but never silently invent a ticket; a follow-up the user explicitly lets go is likewise not nagged again. When no such follow-up surfaced this guard does not fire.
|
|
22
|
+
|
|
21
23
|
**This block is required whenever you point the user at the next ticket**, including in a **later follow-up** (after answering their questions, not only in the instant you resolve the ticket) **and in a capture's confirmation line** when it tells the user how to work the just-captured ticket. Never write "next: `/trailhead:work …`" without the `/clear` before it. **A capture is not a resolution**, so its confirmation needn't render the full labelled block: a compact, future-conditional one-liner is right, along the lines of *"to work it later: `/clear`, then `/trailhead:work 45`"* (in the user's conversation language), but the `/clear`-before-`/trailhead:work` order is non-negotiable there too. A bare `/trailhead:work 45` with no `/clear`, or a `/trailhead:work 45 (dopo /clear)` that hides `/clear` in a trailing parenthetical instead of leading with it, is the bug: `/clear` is always the first thing, on its own, before the next command.
|
|
22
24
|
|
|
23
25
|
This ritual fires from every **Resolve** step (`build`, `bug`) and from **Mode 2**'s close (which resolves `decision` / `prototype` / `task` too). It does **not** fire after a `capture` (resolves nothing, so no recap either) or a parallel `research` batch (AFK, not session-ending).
|
|
@@ -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. **
|
|
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.
|
|
@@ -21,7 +21,7 @@ Whoever claims a ticket **owns it end to end**: they work it and close it themse
|
|
|
21
21
|
Claiming stops two sessions from working the *same* ticket; it does nothing about two sessions working *different* tickets in the **same checkout**. The working tree and the git index are process-global: two agents editing and committing in one clone corrupt each other's state regardless of ticket type (a `bug` is no more or less exposed than a `build`). The claim is the ticket-level lock; the **worktree is the filesystem-level one**.
|
|
22
22
|
|
|
23
23
|
The repo's `trailhead:conventions` `isolation:` header decides:
|
|
24
|
-
- **`auto`** (default): resolve the mode at a ticket's **work-start, before any edit**. Ask once whether more than one ticket will be worked at once on this machine this session (this **is** the `clone` gate below, generalised to the whole mode, so a `clone` resolution never re-asks). **No** (a lone ticket): work in the current checkout, exactly like `none`. **Yes**: resolve by repo shape, a plain repo -> `worktree`, a **superrepo** (a `.gitmodules` at the checkout root that declares at least one submodule; equivalently `git submodule status` lists one) -> `clone` (the superproject clone + submodule provisioning below). Because trailhead resolves **one ticket per session**, the ask fires at most once per ticket work-start (so once per session in normal one-ticket-per-session work; a within-session ticket switch, discouraged, re-asks at the new ticket's work-start); the autonomous run (`/trailhead:auto`) **never asks** (it works tickets sequentially, so there is no concurrency) and resolves `auto` to the current checkout. `auto` is a **runtime interpretation**: the engine never writes `isolation: auto` into the conventions.
|
|
24
|
+
- **`auto`** (default): resolve the mode at a ticket's **work-start, before any edit**. First run a read-only, best-effort **assigned-ticket pre-check** to inform (never decide) the ask: `gh issue list --assignee @me --state open --json number,title,body`, then drop the current ticket's own number from the result (the claim precedes this resolution, so the current ticket is already assigned to `@me`, and must be excluded or the pre-check always sees a false "other"); on any `gh` error skip the pre-check and put the ask neutrally rather than failing the work. An assigned ticket is a **heuristic** for concurrent work, never proof (it may be a live parallel session or a parked/paused claim). **0 other assigned tickets**: put the ask neutrally, as below. **One or more other open assigned tickets**: **pre-bias** the ask, name the already-assigned ticket(s) and default the answer toward *yes* (`worktree`/`clone`), but **still ask**, with an easy neutral/no; a ticket whose `Scope:` is absent is presented as *overlap unknown*, never as evidence of safe concurrency. **Skip the ask entirely only in the one unambiguous, self-referential case**: the session is already inside **this** ticket's linked worktree, that is all of (a) `git rev-parse --git-common-dir` differs from `git rev-parse --git-dir` (a linked worktree), (b) the checked-out branch is exactly `trailhead/t<N>` with `N` equal to the current ticket number, and (c) the working-root `.trailhead/session-ticket` marker names the current ticket; then isolation is already this ticket's operating mode, so resolve to that worktree and announce it without asking. A per-ticket **clone** is deliberately **not** a skip trigger (no reliable read-only predicate separates a dedicated clone from the primary checkout switched onto a `trailhead/t<n>` branch), so it takes the pre-bias-and-ask path. This pre-check is read-only and changes no `worktree`/`clone` mechanic, and **scope overlap with another in-progress ticket is out of its remit** (the existing serialise / undeclared-coupling handling owns that). Ask once whether more than one ticket will be worked at once on this machine this session (this **is** the `clone` gate below, generalised to the whole mode, so a `clone` resolution never re-asks). **No** (a lone ticket): work in the current checkout, exactly like `none`. **Yes**: resolve by repo shape, a plain repo -> `worktree`, a **superrepo** (a `.gitmodules` at the checkout root that declares at least one submodule; equivalently `git submodule status` lists one) -> `clone` (the superproject clone + submodule provisioning below). Because trailhead resolves **one ticket per session**, the ask fires at most once per ticket work-start (so once per session in normal one-ticket-per-session work; a within-session ticket switch, discouraged, re-asks at the new ticket's work-start); the autonomous run (`/trailhead:auto`) **never asks** and never runs the pre-check (it works tickets sequentially, so there is no concurrency) and resolves `auto` to the current checkout. `auto` is a **runtime interpretation**: the engine never writes `isolation: auto` into the conventions.
|
|
25
25
|
- **`none`**: one session at a time per checkout. Simplest, and correct for a repo worked one ticket at a time. Two concurrent sessions here must use **separate clones**, or they collide.
|
|
26
26
|
- **`worktree`**: each executing ticket runs in its own `git worktree` on a `trailhead/t<n>` branch (`git worktree add ../<repo>-t<n> -b trailhead/t<n>`), so concurrent sessions never share a working tree or index. Because git refuses to check out one branch in two worktrees, **worktree isolation implies a branch per ticket even under `git: main`**: the trunk is never checked out twice. At **Resolve**, integrate the branch to the trunk per `git:` (fast-forward or merge into `main`, or open the PR when `git: pr`), then `git worktree remove` it. On **Pause**, the `PAUSED` checkpoint records the worktree path and branch so a resume (by anyone, once released) lands back in it.
|
|
27
27
|
- **`clone`**: each executing ticket works in its own **independent working copy** at `../<repo>-t<n>` on a `trailhead/t<n>` branch (for a submodule-bearing superrepo the clone is of the **superproject** with `--recurse-submodules`, never the submodule alone, so the gitlink bump has a home and each ticket gets independent submodule state; see **Git submodules force `clone`** below). That is the point: it has its own installed dependencies, so it **builds and UATs like the real thing**: the isolation for **path-bound apps a worktree can't run** (React Native/Expo, native toolchains, a local `node_modules`). **A clone is heavy (a full clone + an install), so gate it every time: before cloning, ask whether more than one ticket will be worked at once on this machine this session. If not, skip the clone and work this ticket in the current checkout** (isolation only guards concurrent workspaces; a lone ticket has nothing to collide with). If yes, set it up at work-start by **whichever is faster**:
|
|
@@ -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
|
|
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.
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
# Acceptance testing
|
|
2
2
|
Automated tests prove the code does what the *code* intended; acceptance testing proves it does what the *user* wanted. After tests + code review pass, decide whether the change needs acceptance testing (user-facing behaviour usually does; a pure refactor or internal util usually doesn't) and pick the cheapest sufficient path:
|
|
3
3
|
|
|
4
|
-
- **Automated first, but it does NOT discharge acceptance for user-facing behaviour.** Run the ticket's test/criterion and the suite. Passing tests prove *code-intent*; they close acceptance only for a change the user never sees (refactor, internal util). For anything the user perceives (a new screen, data that now appears/updates, a changed flow) automated green is *not* "done": you still owe a browser-drive or a guided UAT.
|
|
5
|
-
- **
|
|
4
|
+
- **Automated first, but it does NOT discharge acceptance for user-facing behaviour.** Run the ticket's test/criterion and the suite. Passing tests prove *code-intent*; they close acceptance only for a change the user never sees (refactor, internal util). For anything the user perceives (a new screen, data that now appears/updates, a changed flow) automated green is *not* "done": you still owe the drive-mode ask below, then a browser-drive, AI-driven checks, or a guided UAT.
|
|
5
|
+
- **Drive mode: ask who drives the UAT.** Every UAT start puts one explicit ask to the human, **before any step runs**: AI-driven or human-driven (plus the standard delegate option, per `../choices.md`; no defer, this is now-or-never at UAT start). It fires at three start paths: `build`/`bug` Verify, when acceptance testing is owed; the Pending-UAT guard's option (a) (run the UAT now); and resolving a UAT follow-up (any ticket carrying `UAT of:` in its body). The ask must **wait for an explicit answer**: **silence or an ambiguous reply re-asks**, it never defaults to either mode. Never fires when acceptance is not owed.
|
|
6
|
+
- **One-path skip**: a step is agent-performable unless it needs something the agent cannot access this session: a physical device or hardware, credentials or accounts it does not hold, or a human sensory judgement (feel, sound, haptics, subjective look), evaluated over the checklist before asking. No step performable: skip the ask, go human-driven, and **state the reason in one line** naming the blocking class. Any step performable: ask.
|
|
7
|
+
- **AI-driven.** Per step: browser-drive when drivable (the URL resolution below), else commands, the suite, or scripted checks; record each result and its evidence in the `VERIFY` comment as it goes; ask the human only for the steps it cannot perform, one at a time, conversationally, exactly like the guided walkthrough below.
|
|
8
|
+
- **Human-driven.** The guided conversational walkthrough below runs unchanged: one step at a time, wait for the human's report, never a checklist to self-serve.
|
|
9
|
+
- Applies **in both modes**: the failing-step handling, the partial-run / pending tracking, and the mockup-fidelity check below apply the same way, whichever mode drove the run.
|
|
10
|
+
- **Config.** `acceptance.browser: off` disables browser-driving, not AI-driven checks run via commands or the test suite, and it does not pre-answer this ask: the ask still fires, and an AI-driven answer still runs its non-browser checks.
|
|
11
|
+
- **`auto` takes this ask as the delegate option without the confirm gate** (per `../choices.md`): AI-driven when any step is agent-performable; human-only steps stay incomplete and the ticket is set aside, listing them in the run-end summary (never faked); no performable step means the whole UAT is human-only and the ticket is set aside. Full autonomous-run behaviour lives in `../../trailhead-work/references/auto.md`.
|
|
12
|
+
- **AI-driven, browser-drivable web app → drive it yourself.** Governed by `config.acceptance.browser` + `config.testing`: when it's `auto`/`on` and `testing.webapp` is set with `testing.url` set or resolvable (see below), use the browser-automation tools to exercise the actual change end-to-end (navigate, fill, click, assert visible state; capture a screenshot into the `VERIFY` comment). Prefer this over asking the human when the flow is scriptable. Never trigger native/JS dialogs that would wedge the session.
|
|
6
13
|
- **Auto mode resolves the URL before falling back.** Under `acceptance.browser: auto`/`on`, when `testing.webapp` is set but `testing.url` is not, do not drop straight to asking: first try to determine the target URL from the project. Probe, cheapest first, a dev server already running (the likely local ports, or a server log / PID the project writes), package scripts (`dev`/`start`/`preview` in `package.json` or the project's task runner), dev-server and framework config (the configured host/port in e.g. Vite/Next/Astro/Expo config), and project config. When one is found, **propose it** ("acceptance will run against `http://localhost:5173`, correct?") for the user to accept or correct, then drive the browser against the accepted URL. Only when no URL can be determined fall back to asking the human, or to the guided UAT below.
|
|
7
|
-
- **
|
|
14
|
+
- **Human-driven, or an AI-driven step that isn't performable → guided UAT is MANDATORY, and it is CONVERSATIONAL.** This is the human-driven mode from the drive-mode ask above, and it is also what an AI-driven run falls back to for the specific step(s) it cannot perform: when `config.acceptance.browser` is `off`, or no target URL could be determined (the auto attempt above included), or the target isn't browser-drivable (a mobile/native app, a CLI, real hardware, real credentials), **`off` means "acceptance via a guided walkthrough," not "skip acceptance."** For any user-facing change you MUST **run the human through the UAT one step at a time, in the chat**: state step 1 (what to do, where to tap/look, the expected result), then **stop and wait** for the human to report what they actually saw; only then move to step 2. It is a dialogue *you* drive, turn by turn, not a list you hand over. **Anti-pattern, never do this:** post the full numbered checklist and point the user at the ticket to self-serve ("follow the 9 steps in the comment of #NN"). That is exactly the failure this rule kills: the checklist is the *record*, never the *interaction*. Mirror each step's outcome into the `VERIFY` comment as you go; don't close on green you didn't actually see.
|
|
8
15
|
- **On a failing step: stop, take note, open the closing ticket then and there.** A failed step is an acceptance finding, not a footnote: record expected-vs-seen, and immediately open the follow-up that will carry it to resolution: a `bug` ticket (with `Regression of:`/`Surfaced from:` as fits), or a fix on this ticket before it resolves if it's small and in-scope. Then ask the human whether to keep going through the remaining steps or stop here. Never silently log a fail and carry on to Resolve.
|
|
9
16
|
- **When only some steps are runnable now** (e.g. a few need a backend deployed first), walk the runnable ones conversationally **now**, and mark the rest **explicitly pending** with what unblocks them, tracked as below. Don't defer the whole UAT just because part of it is blocked.
|
|
10
17
|
- **When the human can only run it later** (on-device, hardware you don't have right now), record the full numbered checklist in the `VERIFY` comment as the durable artifact and mark UAT **explicitly pending**, but the walkthrough stays conversational **when they come to run it**: never shrink it to a vague one-liner ("just check it on the device"), and never let "automated passed" quietly stand in for it. Deferring *where/when* the human runs it is fine; replacing the live step-by-step with a self-serve checklist is not.
|
|
11
|
-
- **Pending UAT must stay tracked: a closed ticket is not tracking.** Don't resolve/close a user-facing ticket on unconfirmed acceptance and leave the checklist as a comment on the closed issue, where nobody will look. The default is to **keep the ticket open** until the human confirms the UAT passes (closing on green you didn't see is a premature close). **
|
|
18
|
+
- **Pending UAT must stay tracked: a closed ticket is not tracking.** Don't resolve/close a user-facing ticket on unconfirmed acceptance and leave the checklist as a comment on the closed issue, where nobody will look. The default is to **keep the ticket open** until the human confirms the UAT passes (closing on green you didn't see is a premature close). **When UAT hasn't been run at Resolve, pose the choice to the human and wait for an explicit answer, never decide it for them:** (a) **run the UAT now**, in this session, with no ticket (opens with the drive-mode ask above, then the guided conversational walkthrough or AI-driven checks, resolving 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 failing-step and partial-run rules above, never a silent close), or (b) **open a UAT ticket** to run it later. **Never open a UAT ticket without asking, and never let silence or an ambiguous reply default to either path.** Only when the human picks (b) do you **open a follow-up ticket before that close** that carries the pending UAT and links back to the parent with a `UAT of: <parent name>(link)` pointer in the body. Route it the way `../choices.md` routes a deferred choice: a **`trailhead:seed`** (kept blocked) when the run is gated on a future external trigger not yet true (hardware you don't have, a device out of reach), or a follow-up **`task`** on the frontier when it's just scheduled or batched work you could pick up any time. Either way the follow-up **carries the full numbered checklist and, when it comes to resolution, opens with the drive-mode ask above before running the guided conversational walkthrough**, never a self-serve check-off. **Never close a user-facing ticket on a chosen-deferred UAT with nothing opened**: the parent may close as agreed, but the owed UAT stays visible on the tracker as its own ticket, linked to the parent, until it is actually performed. The pending check must be visible on the tracker, not buried.
|
|
19
|
+
|
|
20
|
+
**Mockup-fidelity check (when the ticket had an approved mockup).** If the change was preceded by an approved mockup (the **Prototype** technique produced one and it was approved and recorded at Execute), acceptance testing MUST check the built screen **against that approved mockup**, not merely against "what the user wanted" in the abstract. Pull the mockup's current state to compare against (re-fetch it in `claude.ai/design` / `stitch` mode via the technique's re-fetch rule, or open the linked `disk` mockup); never compare from memory. Walk it **element by element and state by state**, layout, spacing, sizing, colour, typography, copy, and every state it shows (empty, loading, error, hover, active, disabled, and the like), enumerating from the approved mockup itself (the spec) and cross-checking the executor's recorded Execute-step fidelity list and any element it flagged infeasible, and confirm each is reproduced. Record the per-element and per-state result in the `VERIFY` comment. Any **unforced** deviation is an acceptance finding, never waved through as "close enough"; a genuinely infeasible part must already have been raised with the user at Execute (per the Prototype technique), not surface here as a silent drift.
|
|
12
21
|
|
|
13
22
|
Acceptance failures are findings like any other: a `bug` follow-up or a fix on this ticket before it resolves.
|
|
14
23
|
|