@marcomigozzi/trailhead 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.it.md +8 -5
  2. package/README.md +8 -5
  3. package/bin/lib/codex-projection.js +79 -58
  4. package/bin/lib/codex-projection.test.js +103 -15
  5. package/bin/trailhead.js +106 -15
  6. package/bin/trailhead.test.js +76 -11
  7. package/package.json +2 -2
  8. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  9. package/plugins/trailhead/agents/trailhead-code-review.md +10 -0
  10. package/plugins/trailhead/agents/trailhead-codebase-map.md +10 -0
  11. package/plugins/trailhead/agents/trailhead-debug.md +10 -0
  12. package/plugins/trailhead/agents/trailhead-executor.md +10 -0
  13. package/plugins/trailhead/agents/trailhead-fix.md +10 -0
  14. package/plugins/trailhead/agents/trailhead-plan.md +10 -0
  15. package/plugins/trailhead/agents/trailhead-research.md +10 -0
  16. package/plugins/trailhead/agents/trailhead-verify.md +10 -0
  17. package/plugins/trailhead/hooks/hooks.json +4 -0
  18. package/plugins/trailhead/hooks/trailhead-install-guard.js +176 -0
  19. package/plugins/trailhead/hooks/trailhead-install-guard.test.js +118 -0
  20. package/plugins/trailhead/skills/_shared/configuration-reference.md +39 -12
  21. package/plugins/trailhead/skills/_shared/configuration.md +1 -1
  22. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  23. package/plugins/trailhead/skills/_shared/substrate-commands.md +4 -2
  24. package/plugins/trailhead/skills/_shared/substrate.md +1 -1
  25. package/plugins/trailhead/skills/_shared/techniques/code-review.md +2 -2
  26. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
  27. package/plugins/trailhead/skills/_shared/techniques/debug.md +4 -1
  28. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +1 -1
  29. package/plugins/trailhead/skills/_shared/techniques/prototype.md +6 -3
  30. package/plugins/trailhead/skills/_shared/techniques/research.md +2 -1
  31. package/plugins/trailhead/skills/_shared/techniques/verify.md +12 -0
  32. package/plugins/trailhead/skills/_shared/techniques.md +4 -3
  33. package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
  34. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -0
  35. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -1
  36. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
  37. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +6 -6
  38. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -0
  39. package/plugins/trailhead/skills/trailhead-manage/references/updating.md +12 -0
  40. package/plugins/trailhead/skills/trailhead-view/SKILL.md +2 -1
  41. package/plugins/trailhead/skills/trailhead-work/SKILL.md +2 -0
  42. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +12 -8
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ // trailhead-install-guard.js: PreToolUse(Bash) hook.
3
+ // Slopsquatting barrier. trailhead's executor/fix agents can install packages
4
+ // (npm / PyPI / crates.io / Go / RubyGems) while resolving a ticket, so a
5
+ // hallucinated or typosquatted package NAME could be fetched silently. This gate
6
+ // BLOCKS any public-registry install that names a package until the agent has
7
+ // vetted the name, then instructs it to verify legitimacy (typosquat similarity,
8
+ // package age, download counts, repo metadata) and checkpoint the human on a
9
+ // suspicious result (block-and-ask). A vetted install proceeds by re-running the
10
+ // command prefixed with the sentinel `TRAILHEAD_VERIFIED_INSTALL=1`.
11
+ // Only a NAMED install is gated: lockfile/manifest installs (bare `npm install`,
12
+ // `npm ci`, `pip install -r`, `cargo build`, `go mod download`, `bundle install`)
13
+ // and local paths / URLs / VCS refs carry no typed name and pass through.
14
+ // Blocks with exit 2 + {"decision":"block",...}; allows everything else.
15
+ // Crash-safe: any error → exit 0 (never wedge the user's workflow).
16
+ // Self-contained, no dependencies. Pure functions exported for tests; runs as a
17
+ // hook when executed directly.
18
+
19
+ const SENTINEL = /(?:^|\s)TRAILHEAD_VERIFIED_INSTALL=1(?:\s|$)/;
20
+
21
+ // Per-ecosystem: the binaries that own it, and the install verbs that fetch a
22
+ // named package. `add` covers pnpm/yarn/bun/uv/poetry/cargo; `install`/`i` npm
23
+ // and pip; `get`/`install` go.
24
+ const ECOSYSTEMS = {
25
+ npm: { bins: ['npm', 'pnpm', 'yarn', 'bun'], verbs: ['install', 'i', 'add'] },
26
+ pip: { bins: ['pip', 'pip3', 'pipx', 'uv', 'poetry'], verbs: ['install', 'add'] },
27
+ crates: { bins: ['cargo'], verbs: ['add', 'install'] },
28
+ go: { bins: ['go'], verbs: ['get', 'install'] },
29
+ gem: { bins: ['gem'], verbs: ['install'] },
30
+ };
31
+
32
+ // Flags that consume a SEPARATE-TOKEN value (a file, url, or dir), so that value
33
+ // is never mistaken for a package name (e.g. `pip install -r requirements.txt`).
34
+ const VALUE_FLAGS = new Set([
35
+ '-r', '--requirement', '-c', '--constraint', '-e', '--editable',
36
+ '-i', '--index-url', '--extra-index-url', '-f', '--find-links',
37
+ '-t', '--target', '--prefix', '--root', '--python', '-d', '--dest',
38
+ '-w', '--workspace', '--registry', '-C', '--directory', '--chdir',
39
+ '--no-binary', '--only-binary',
40
+ ]);
41
+
42
+ function basename(p) {
43
+ const s = String(p).split('/');
44
+ return s[s.length - 1];
45
+ }
46
+
47
+ // A positional that is NOT a public-registry name: local path, url, vcs ref, or a
48
+ // file artifact. Go module paths (example.com/foo) look url-ish but ARE the named
49
+ // module, so for go only a leading . / ~ disqualifies.
50
+ function isLocalOrUrl(t, ecosystem) {
51
+ if (t.startsWith('.') || t.startsWith('/') || t.startsWith('~')) return true;
52
+ if (ecosystem === 'go') return false; // a bare module path is the name; gate it
53
+ if (t.includes('://')) return true;
54
+ if (/^(?:git\+|file:|https?:|hg\+|svn\+|bzr\+)/.test(t)) return true;
55
+ if (/\.(?:tgz|tar\.gz|whl|gz|zip|txt|toml|lock|cfg)$/.test(t)) return true;
56
+ return false;
57
+ }
58
+
59
+ // Best-effort bare name for the human-facing reason (detection does not depend on
60
+ // this being exact). Strips pip version specifiers / extras and npm @version.
61
+ function bareName(t) {
62
+ let n = t.split(/[=<>~!\[]/)[0]; // pip: pkg==1, pkg>=1, pkg[extra]
63
+ if (n.startsWith('@')) {
64
+ const at = n.indexOf('@', 1); // scoped @scope/pkg@1 → cut the version @
65
+ if (at !== -1) n = n.slice(0, at);
66
+ } else {
67
+ const at = n.indexOf('@'); // npm pkg@1.2.3
68
+ if (at !== -1) n = n.slice(0, at);
69
+ }
70
+ return n;
71
+ }
72
+
73
+ // Detect a public-registry install that NAMES at least one package, in a single
74
+ // shell segment. Returns { ecosystem, packages: [names] } or null.
75
+ function detectSegment(cmd) {
76
+ const toks = String(cmd).trim().split(/\s+/).filter(Boolean);
77
+ let i = 0;
78
+ while (i < toks.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[i])) i++; // env prefix
79
+ if (i >= toks.length) return null;
80
+
81
+ const bin = basename(toks[i]);
82
+ let ecosystem = null;
83
+ for (const [name, spec] of Object.entries(ECOSYSTEMS)) {
84
+ if (spec.bins.includes(bin)) { ecosystem = name; break; }
85
+ }
86
+ // `python -m pip install …` → treat as pip.
87
+ if (!ecosystem && /^python3?$/.test(bin)) {
88
+ if (toks[i + 1] === '-m' && /^pip3?$/.test(toks[i + 2] || '')) {
89
+ ecosystem = 'pip';
90
+ i += 2; // land on `pip`
91
+ } else {
92
+ return null;
93
+ }
94
+ }
95
+ if (!ecosystem) return null;
96
+ i++; // past the binary (or the `pip` peeled above)
97
+
98
+ // `uv pip install …` → peel the `pip`.
99
+ if (bin === 'uv' && toks[i] === 'pip') i++;
100
+
101
+ const verb = toks[i];
102
+ if (!ECOSYSTEMS[ecosystem].verbs.includes(verb)) return null;
103
+ i++;
104
+
105
+ const packages = [];
106
+ for (let j = i; j < toks.length; j++) {
107
+ const t = toks[j];
108
+ if (t.startsWith('-')) {
109
+ if (VALUE_FLAGS.has(t)) j++; // consume its separate-token value
110
+ continue;
111
+ }
112
+ if (isLocalOrUrl(t, ecosystem)) continue;
113
+ packages.push(bareName(t));
114
+ }
115
+ if (packages.length === 0) return null; // lockfile/manifest install: nothing typed
116
+ return { ecosystem, packages };
117
+ }
118
+
119
+ // Agents chain commands (`cd app && npm install foo`), so check every shell
120
+ // segment, not just the first. Split on the common separators (&& || ; | and
121
+ // newlines); a registry name never contains one, so a stray split is harmless.
122
+ // Returns the first segment's hit, or null.
123
+ function detectInstall(cmd) {
124
+ for (const seg of String(cmd).split(/&&|\|\||;|\n|\|/)) {
125
+ const hit = detectSegment(seg);
126
+ if (hit) return hit;
127
+ }
128
+ return null;
129
+ }
130
+
131
+ // The agent's "I have vetted these" acknowledgement.
132
+ function isVerified(cmd) {
133
+ return SENTINEL.test(String(cmd));
134
+ }
135
+
136
+ function block(hit) {
137
+ const list = hit.packages.join(', ');
138
+ const reason =
139
+ `trailhead: this ${hit.ecosystem} install names package(s) not yet vetted (${list}). ` +
140
+ 'Guard against slopsquatting before installing: verify each package is legitimate, weighing ' +
141
+ 'name / typosquat similarity to popular packages, package age / first-seen date, download counts / popularity, ' +
142
+ 'and repository metadata (present and matching). ' +
143
+ 'STOP and ask the human on anything suspicious (block-and-ask): do not install on your own judgement. ' +
144
+ 'Once vetted, re-run the exact command prefixed with `TRAILHEAD_VERIFIED_INSTALL=1` to proceed.';
145
+ process.stdout.write(JSON.stringify({ decision: 'block', code: 'UNVETTED_PACKAGE_INSTALL', reason }));
146
+ process.exit(2);
147
+ }
148
+
149
+ function run(data) {
150
+ let cmd = '';
151
+ try {
152
+ cmd = (JSON.parse(data).tool_input || {}).command || '';
153
+ } catch {
154
+ process.exit(0); // unparseable input: nothing to gate
155
+ }
156
+ try {
157
+ if (isVerified(cmd)) process.exit(0); // agent has vetted these
158
+ const hit = detectInstall(cmd);
159
+ if (hit) block(hit);
160
+ } catch {
161
+ // fall through to allow: a guard must never wedge the workflow
162
+ }
163
+ process.exit(0);
164
+ }
165
+
166
+ if (require.main === module) {
167
+ let data = '';
168
+ const timer = setTimeout(() => process.exit(0), 5000); // stdin never ends → give up
169
+ process.stdin.on('data', (c) => (data += c));
170
+ process.stdin.on('end', () => {
171
+ clearTimeout(timer);
172
+ run(data);
173
+ });
174
+ }
175
+
176
+ module.exports = { detectInstall, isVerified };
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ // Tests for trailhead-install-guard.js. Run: node trailhead-install-guard.test.js
3
+ // No framework: plain asserts + child_process for the end-to-end hook behaviour.
4
+ const assert = require('assert');
5
+ const { execFileSync } = require('child_process');
6
+ const path = require('path');
7
+
8
+ const HOOK = path.join(__dirname, 'trailhead-install-guard.js');
9
+ const { detectInstall, isVerified } = require('./trailhead-install-guard.js');
10
+
11
+ let passed = 0;
12
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
13
+
14
+ // Run the hook end-to-end: pipe tool_input JSON on stdin, capture {code, out}.
15
+ function runHook(command) {
16
+ const input = JSON.stringify({ tool_input: { command } });
17
+ try {
18
+ const out = execFileSync('node', [HOOK], { input });
19
+ return { code: 0, out: out.toString() };
20
+ } catch (e) {
21
+ return { code: e.status, out: (e.stdout || '').toString(), err: (e.stderr || '').toString() };
22
+ }
23
+ }
24
+
25
+ // --- unit: detectInstall positives (a NAMED public-registry install) ---
26
+ const pos = [
27
+ ['npm install lodash', 'npm', 'lodash'],
28
+ ['npm i react', 'npm', 'react'],
29
+ ['pnpm add vite', 'npm', 'vite'],
30
+ ['yarn add left-pad', 'npm', 'left-pad'],
31
+ ['bun add hono', 'npm', 'hono'],
32
+ ['pip install requests', 'pip', 'requests'],
33
+ ['pip3 install numpy', 'pip', 'numpy'],
34
+ ['python -m pip install flask', 'pip', 'flask'],
35
+ ['python3 -m pip install flask', 'pip', 'flask'],
36
+ ['uv add httpx', 'pip', 'httpx'],
37
+ ['uv pip install httpx', 'pip', 'httpx'],
38
+ ['poetry add django', 'pip', 'django'],
39
+ ['pipx install black', 'pip', 'black'],
40
+ ['cargo add serde', 'crates', 'serde'],
41
+ ['cargo install ripgrep', 'crates', 'ripgrep'],
42
+ ['go get example.com/foo/bar', 'go', 'example.com/foo/bar'],
43
+ ['go install golang.org/x/tools/cmd/stringer', 'go', 'golang.org/x/tools/cmd/stringer'],
44
+ ['gem install rails', 'gem', 'rails'],
45
+ ];
46
+ for (const [cmd, eco, name] of pos) {
47
+ const d = detectInstall(cmd);
48
+ ok(`detects: ${cmd}`, d && d.ecosystem === eco && d.packages.includes(name));
49
+ }
50
+
51
+ // Scoped and versioned names are still names (bare name reported).
52
+ ok('detects scoped npm name', (() => { const d = detectInstall('npm install @scope/pkg'); return d && d.packages[0] === '@scope/pkg'; })());
53
+ ok('strips npm @version', (() => { const d = detectInstall('npm install lodash@4.17.21'); return d && d.packages[0] === 'lodash'; })());
54
+ ok('strips pip == specifier', (() => { const d = detectInstall('pip install requests==2.0'); return d && d.packages[0] === 'requests'; })());
55
+ ok('strips pip extra', (() => { const d = detectInstall('pip install requests[security]'); return d && d.packages[0] === 'requests'; })());
56
+ ok('scoped versioned npm keeps scope', (() => { const d = detectInstall('npm install @scope/pkg@1.2.3'); return d && d.packages[0] === '@scope/pkg'; })());
57
+
58
+ // --- unit: detectInstall negatives (no typed registry name) ---
59
+ const neg = [
60
+ 'npm install', // bare: from package.json/lock
61
+ 'npm ci', // lockfile install
62
+ 'pip install -r requirements.txt', // requirements file
63
+ 'pip install --requirement req.txt',
64
+ 'pip install -e .', // editable local
65
+ 'npm install ./local-tarball.tgz', // local path
66
+ 'npm install file:../pkg', // file: ref
67
+ 'cargo build', // not an install verb
68
+ 'go mod download', // not get/install
69
+ 'bundle install', // unrecognised binary
70
+ 'pip install git+https://github.com/x/y', // vcs url
71
+ 'npm install --save-dev', // flag only, no name
72
+ 'ls -la', // unrelated
73
+ 'go build ./...', // not get/install
74
+ ];
75
+ for (const cmd of neg) {
76
+ ok(`ignores: ${cmd}`, detectInstall(cmd) === null);
77
+ }
78
+
79
+ // `-r`'s value must not be read as a package even with a trailing name absent.
80
+ ok('consumes -r value, no phantom name', detectInstall('pip install -r requirements.txt') === null);
81
+
82
+ // Compound commands: an install in any segment is caught, not just the first.
83
+ ok('detects install after && chain', (() => { const d = detectInstall('cd app && npm install lodash'); return d && d.packages.includes('lodash'); })());
84
+ ok('detects install after ; chain', (() => { const d = detectInstall('echo hi; pip install requests'); return d && d.packages.includes('requests'); })());
85
+ ok('detects install in a piped tail', (() => { const d = detectInstall('true | cargo add serde'); return d && d.packages.includes('serde'); })());
86
+ ok('compound with no install stays null', detectInstall('cd app && npm run build && ls') === null);
87
+
88
+ // --- unit: isVerified ---
89
+ ok('sentinel recognised', isVerified('TRAILHEAD_VERIFIED_INSTALL=1 npm install lodash'));
90
+ ok('no sentinel', !isVerified('npm install lodash'));
91
+
92
+ // --- end-to-end: block ---
93
+ const b1 = runHook('npm install lodash');
94
+ ok('blocks an unvetted named install (exit 2)', b1.code === 2 && /UNVETTED_PACKAGE_INSTALL/.test(b1.out));
95
+ ok('block names the package', /lodash/.test(b1.out) && /"decision":"block"/.test(b1.out));
96
+ const b2 = runHook('pip install some-obscure-pkg');
97
+ ok('blocks a pip named install (exit 2)', b2.code === 2 && /some-obscure-pkg/.test(b2.out));
98
+
99
+ // --- end-to-end: allow ---
100
+ const a1 = runHook('TRAILHEAD_VERIFIED_INSTALL=1 npm install lodash');
101
+ ok('sentinel allows a vetted install (exit 0)', a1.code === 0 && a1.out.trim() === '');
102
+ const a2 = runHook('npm install');
103
+ ok('bare install allowed (exit 0)', a2.code === 0 && a2.out.trim() === '');
104
+ const a3 = runHook('ls -la');
105
+ ok('non-install allowed (exit 0)', a3.code === 0 && a3.out.trim() === '');
106
+ const a4 = runHook('pip install -r requirements.txt');
107
+ ok('requirements-file install allowed (exit 0)', a4.code === 0 && a4.out.trim() === '');
108
+
109
+ // --- end-to-end: unparseable stdin allowed ---
110
+ const u = (() => {
111
+ try {
112
+ const out = execFileSync('node', [HOOK], { input: 'not json' });
113
+ return { code: 0, out: out.toString() };
114
+ } catch (e) { return { code: e.status, out: (e.stdout || '').toString() }; }
115
+ })();
116
+ ok('unparseable stdin allowed (exit 0)', u.code === 0);
117
+
118
+ console.log(`✓ install-guard: ${passed} assertions passed`);
@@ -4,8 +4,8 @@ Effective config = built-in defaults ← global `~/.claude/trailhead/config.json
4
4
 
5
5
  | Key | Values (default **bold**) | Effect |
6
6
  |---|---|---|
7
- | `models.{plan,execute,research,review,debug}` | a model id (**inherit session**) | model for that activity, see note |
8
- | `models.codex.{plan,execute,research,review,debug}` | an OpenAI model id (**inherit session**) | per-activity subagent model on **Codex** (OpenAI ids), the honoured namespace there; independent of `models.*` (Claude ids); projected into `~/.codex/agents/trailhead-*.toml` and dispatched by `agent_type`. See note |
7
+ | `models.{plan,execute,research,review,debug,verify,codebase-map}` | a model id (**Balanced-profile default**) | model for that activity; an unset key resolves to its default **Balanced** tier, see note |
8
+ | `models.codex.{plan,execute,research,review,debug,verify,codebase-map}` | an OpenAI model id (**inherit session**) | per-activity subagent model on **Codex** (OpenAI ids), the honoured namespace there; independent of `models.*` (Claude ids); projected into `~/.codex/agents/trailhead-*.toml` and dispatched by `agent_type`. See note |
9
9
  | `ticket.language` | an ISO 639-1 code (**`en`**) | the language trailhead **writes** its GitHub prose in (ticket titles/bodies, engine comments, map body & commit descriptions), see note. Independent of the language the agent converses in |
10
10
  | `design` | **`disk`** \| `claude.ai/design` \| `stitch` | mode for UI mockups (**Prototype**). `disk` = throwaway local HTML (any host); `claude.ai/design` = create/edit the mockup on claude.ai/design via Anthropic's **official Claude Design MCP** (`claude mcp add --scope user --transport http claude-design https://api.anthropic.com/v1/design/mcp`, auth `/design-login`), **Claude only**; `stitch` = create/edit the mockup via the **Google Stitch MCP** (`claude mcp add --scope user --transport http stitch https://stitch.googleapis.com/mcp`), the **cross-host** hosted option (Claude Code **and** Codex). A hosted mode's projectId is created/reused at execution and cached in `design.project`; which surface it uses is `design.surface`. If the configured mode's MCP isn't connected, Prototype offers a one-time install before falling back (see Prototype). **On Codex** `claude.ai/design` isn't available, so Prototype **offers to switch to `stitch`** (cross-host) before degrading to `disk`; `stitch` and `disk` run on Codex unchanged |
11
11
  | `design.surface` | **`canvas`** \| `design-system` | under a hosted `design` mode (`claude.ai/design` or `stitch`), which surface the mockup lives on. `canvas` = a single surface of screens to react to (under `claude.ai/design`: a `/design` **Artifact** or a hand-made web-app project, **not** MCP-created; under `stitch`: a Stitch project), editable visually and exportable as a live prototype. `design-system` = push components into a shared design-system project (a `PROJECT_TYPE_DESIGN_SYSTEM` synced via DesignSync `/design-sync`, or a Stitch design system), for a shared component library. **Back-compat:** unset, a cached `design.project` infers `design-system` (that's what the key meant before this key existed); with no cached project, `canvas`. See Prototype |
@@ -16,25 +16,40 @@ 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`/`quick` 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 |
19
20
 
20
21
  **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:
21
22
  - `plan` / `execute`: the `build`/`bug` engine's **Plan** and **Execute** steps (see below).
22
- - `research`: the **Research** subagents **and** the **Codebase map** reader fan-out (both gather/read).
23
+ - `research`: the **Research** subagents.
24
+ - `codebase-map`: the **Codebase map** reader fan-out (the read-only `trailhead-codebase-map` subagent), split from `research` so the mapper can run on its own tier.
23
25
  - `review`: the **Code review** subagent.
26
+ - `verify`: the **Goal-backward verification** subagent (`trailhead-verify`) in the `build`/`bug` **Verify** step: does the change deliver what the ticket promised?
24
27
  - `debug`: the **Debug** technique's diagnosis in the `bug` engine.
25
28
 
26
29
  **Every model key runs as a subagent, so all of them apply in one session whatever model it runs on.** The main session is the **orchestrator**: it holds the genuinely interactive moments, the `build`/`bug` **Discuss** (the "stop and ask" on blocking ambiguity, *before* implementing) and **Verify**'s acceptance/UAT (*after*), and fans the rest out:
27
30
  - `plan` → a *planner subagent* on `models.plan` (inline when it's unset or equals the session model).
28
31
  - `execute` → an **executor subagent** that implements the plan with atomic commits on `models.execute`. Execute itself is autonomous (no live user exchange happens *during* implementation; new scope surfacing is captured/split, not grilled), so it fans out cleanly; the commits still land on `main`, visible. Run it **inline in the session instead when `models.execute` is unset or equals the session model** (nothing to gain from spawning).
29
- - `research`/`review`/`debug`/codebase-map → their own subagents.
32
+ - `research`/`review`/`debug`/`verify`/`codebase-map` → their own subagents.
30
33
 
31
34
  **Two independent host facts govern this, keyed on the descriptor (`bin/lib/host-descriptor.js`), not on a host name.** (1) A **subagent-less host** (`subagentToolkit == none`) has nothing to fan out into, so **every activity runs inline and sequentially** in the one session. (2) A host that **can't honour the model keys** (`honorsModelKeys == false`) can't run a subagent on a trailhead `models.*` value, so **every `models.*` key collapses to the single session model**. The two are independent: **Codex** is `subagentToolkit == full` (it fans out, via the native multi_agent toolkit) yet `honorsModelKeys == false` (its subagent model registry is OpenAI-only, so a Claude id like `claude-sonnet-5` can't run there and every subagent inherits the one session model). So on Codex the techniques *do* fan out; only the per-activity model *choice* is lost. The keys **stay in `.trailhead/config.json` untouched**: Claude Code honours them, a host that can't simply ignores them, and they apply again the next time the project runs on Claude Code. Nothing is stripped from the Claude `models.*` keys: they carry no `agents/*.toml` of their own and simply stay in config, ignored on Codex. The per-subagent OpenAI-model pinning once described here as a future upgrade has since shipped for `models.codex.*`: when those keys are set the installer emits one `~/.codex/agents/trailhead-<technique>.toml` per key and enables `features.multi_agent_v2`, and the runtime dispatches each activity by `agent_type` (adapter §D). When `models.*` is set while running on a host that can't honour it, the engine surfaces a **one-time notice** at session start (co-located with the inline check-update), so the user knows their per-activity split is not taking effect here; it fires once per project, tracked by a gitignored `.trailhead/models-collapse-notice` marker. On Codex the per-activity split is not lost outright, it moves to a parallel `models.codex.*` namespace holding OpenAI model ids (each key a bare model id, or a `{model, effort}` pair projected as `model_reasoning_effort`); `models.*` (Claude ids) still collapses to the session model as described above. The one-time collapse notice now points the user at `models.codex.*` as the fix, and stays silent once any `models.codex.*` key is set, since that means the user has already opted into the honoured namespace there.
32
35
 
33
36
  **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. Never block or fail the session on a network or parse error. `/trailhead:update` stays the on-demand installer.
34
37
 
35
- If a value is unset, inherit the session model (never hard-fail on an unknown id: warn and inherit).
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-4-8`, standard → Sonnet-class, fast → Haiku-class). To use the session model instead of the Balanced default, set the key explicitly to `inherit session`. Never hard-fail on an unknown id: warn and inherit the session model.
36
39
 
37
- **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. **`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.
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.
41
+
42
+ **Model-tier profiles.** Three profile seeds for the guided setup's Models step (§3), 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
+
44
+ | profile | plan | execute | research | review | debug | verify | codebase-map |
45
+ |---|---|---|---|---|---|---|---|
46
+ | **High** | strong | strong | standard | strong | strong | strong | standard |
47
+ | **Balanced** | strong | standard | standard | standard | standard | standard | fast |
48
+ | **Low** | standard | standard | fast | fast | standard | fast | fast |
49
+
50
+ **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-4-8`** by default (the newer `claude-opus-5` stays offered in the per-key menus, but is not the default pick) · standard = Sonnet-class · fast = Haiku-class.
52
+ - **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`.
38
53
 
39
54
  **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.
40
55
 
@@ -50,6 +65,15 @@ If a value is unset, inherit the session model (never hard-fail on an unknown id
50
65
 
51
66
  **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`.
52
67
 
68
+ **effort (cost-aware gating).** `standard` (default) = the full flow: every `build`/`bug`/`quick` step runs at full cost, no triage (today's behaviour, backward-compatible). `lean` = **cost-aware**, defined **canonically here**; the `build`/`bug` engine and `quick` reference this section and add only their own timing. Under `lean` 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
+ - **trivial**: a single file, a handful of lines, no new seam, no new dependency, no architectural change (a copy/CSS/doc/constant edit).
70
+ - **small**: localized, one or two files, within existing seams, no new dependency, no architectural change.
71
+ - **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
+
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**, and always under `effort: standard`, there is no offer and the full flow runs.
74
+
75
+ **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
+
53
77
  Example `config.json` (same shape for the project `.trailhead/config.json` and the global `~/.claude/trailhead/config.json`):
54
78
  ```json
55
79
  { "models": { "plan": "claude-opus-4-8", "execute": "claude-sonnet-5" }, "tdd": "seams", "acceptance": { "browser": "auto" } }
@@ -63,19 +87,22 @@ Example `config.json` (same shape for the project `.trailhead/config.json` and t
63
87
 
64
88
  1. **Scope**: 🌍 Global default (`~/.claude/trailhead/config.json`) · 📁 This project (`.trailhead/config.json` at the repo root). Skip to global outside a repo.
65
89
  2. **🌐 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.
66
- 3. **🧠 Models**: **always two separate questions, one for `plan` and one for `execute`; never merge them into a single model pick.** Ask each as its own `AskUserQuestion` menu (current value pre-selected), and offer the **same versioned lineup** to both. Each option is a **full model id with its version**; never a bare family alias (`opus`/`sonnet`/`haiku`), which can't say *which* live version. Build the list from the models actually reachable **at setup time** (verify, don't trust this file verbatim), offering **at least the two most recent versions per family**, e.g. `claude-fable-5`, `claude-opus-5` / `claude-opus-4-8`, `claude-sonnet-5` / `claude-sonnet-4-8`, `claude-haiku-4-5`, plus **inherit session** (the default). 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` sit behind an "Advanced" option, each its own separate choice, same versioned rule.
67
- - **Anti-pattern, never do this:** 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. Plan is asked, answered, and only then is Execute asked, as a second menu. Two questions, two answers, always.
68
- - **On Codex (host-detected), ask `models.codex.*` instead of the Claude lineup.** Same discipline: `plan` and `execute` as two separate `AskUserQuestion` menus, never merged; the rarer `research`/`review`/`debug` 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`.
90
+ 3. **🧠 Models**: a **profile** (Balanced by default) applied whole, or **Manual** for the per-key questions.
91
+ - **🎚️ 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`.
92
+ - **The per-key questions (Manual path only): always two separate questions, one for `plan` and one for `execute`; never merge them into a single model pick.** Ask each as its own `AskUserQuestion` menu (current value pre-selected), and offer the **same versioned lineup** to both. Each option is a **full model id with its version**; never a bare family alias (`opus`/`sonnet`/`haiku`), which can't say *which* live version. Build the list from the models actually reachable **at setup time** (verify, don't trust this file verbatim), offering **at least the two most recent versions per family**, e.g. `claude-fable-5`, `claude-opus-5` / `claude-opus-4-8`, `claude-sonnet-5` / `claude-sonnet-4-8`, `claude-haiku-4-5`, plus **inherit session** (the no-override option, the session model). If that lineup exceeds the menu's option cap, drill **family → version** or include a free-id "Other" so every recent version stays reachable. The rarer `research`/`review`/`debug`/`verify`/`codebase-map` sit behind an "Advanced" option, each its own separate choice, same versioned rule.
93
+ - **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.)
94
+ - **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`.
69
95
  4. **🎨 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).
70
96
  5. **🧪 TDD**: 🎯 Seams only · ✅ On (all definable behaviour) · ⛔ Off.
71
- 6. **🖥️ Acceptance testing**: 🤖 Auto · ▶️ Always browser · 🙋 Guided UAT only. If browser is enabled, ask **🔗 base URL** and confirm it's a web app.
72
- 7. **🧑‍⚖️ Plan review**: **before presenting this menu, first probe the PATH for known external AI CLIs** (e.g. `gemini`, `codex`, and any others the Cross-AI plan review technique recognises): run the detection, then build the menu from what was actually found. Never present this step with a hard-coded two-option list; the options are computed from the probe:
97
+ 6. **⚖️ Effort**: how hard the build 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).
98
+ 7. **🖥️ Acceptance testing**: 🤖 Auto · ▶️ Always browser · 🙋 Guided UAT only. If browser is enabled, ask **🔗 base URL** and confirm it's a web app.
99
+ 8. **🧑‍⚖️ 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:
73
100
  - **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).
74
101
  - **One CLI found**: ⛔ Off · 🤝 Use `<name>`. (With a single CLI, "all" and "pick specific" collapse to the same thing, so don't show both.)
75
102
  - **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.
76
103
 
77
104
  **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.
78
- 8. **📊 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**.
105
+ 9. **📊 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**.
79
106
  - 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.
80
107
  - **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.
81
108
 
@@ -5,4 +5,4 @@ Three layers, **nearest wins**, key by key; a key unset at one layer inherits th
5
5
  - **Global**: `~/.claude/trailhead/config.json` (honour `$CLAUDE_CONFIG_DIR`): your standing defaults across every project.
6
6
  - **Defaults**: the built-in values documented in `configuration-reference.md`.
7
7
 
8
- Load the effective config at the start of a work session, **from the map's project root** (`.trailhead/config.json` at the superproject repo root) plus the global file, **before** any isolation workspace is set up. **Config is a property of the project/map, not the working directory**: an `isolation: worktree`/`clone` workspace **inherits** the config loaded here and must **never re-resolve it from the isolated copy's own root**, because a per-ticket clone (a submodule clone especially) has no `.trailhead/` of its own and would silently fall back to defaults (this is exactly how a `design: claude.ai/design` project ends up wrongly using `disk` in a clone). Config **writes** too (e.g. caching `design.project`) target the project's `.trailhead/config.json` in the original checkout, never the clone. **The main session orchestrates and holds the HITL moments** (the `build`/`bug` Discuss's "stop and ask", Verify's acceptance/UAT); **every model key runs as a subagent** (`plan`, `execute` (an *executor subagent* implementing with atomic commits on `models.execute`), `research`, `review`, `debug`) so the whole per-activity model split applies in one session whatever model it runs on. Run Execute inline only when `models.execute` is unset or equals the session model. **On a subagent-less host** (`subagentToolkit == none`) there is nothing to fan out into: every activity runs inline. **Separately, on a host that can't honour the model keys** (`honorsModelKeys == false`, e.g. Codex, whose subagents run on its own OpenAI models) every `models.*` key collapses to the single session model even though the activities still fan out; when any `models.*` is set there the engine surfaces a **one-time** session-start notice that the split is not taking effect (the keys stay in `.trailhead/config.json` for when the project next runs on Claude Code). On Codex the per-activity split is available through a parallel `models.codex.*` namespace (OpenAI model ids), the honoured namespace there (projected into per-technique `~/.codex/agents/*.toml` and dispatched at runtime by `agent_type`), while `models.*` (Claude ids) still collapse to the single session model; the collapse notice points at `models.codex.*` and goes silent once it is set. A host without a hook bus (`hookBus == none`) also runs a lightweight inline update check at session start (no SessionStart hook, no statusline). See `configuration-reference.md`. The **keys, their values, the model/tdd/acceptance semantics, and the guided menu setup live in `configuration-reference.md`**: read it when running `/trailhead:config` or when you need a key's exact meaning. `/trailhead:config` with no args runs the guided setup; `config get` prints the merged config; `config set <key> <value>` writes one key.
8
+ Load the effective config at the start of a work session, **from the map's project root** (`.trailhead/config.json` at the superproject repo root) plus the global file, **before** any isolation workspace is set up. **Config is a property of the project/map, not the working directory**: an `isolation: worktree`/`clone` workspace **inherits** the config loaded here and must **never re-resolve it from the isolated copy's own root**, because a per-ticket clone (a submodule clone especially) has no `.trailhead/` of its own and would silently fall back to defaults (this is exactly how a `design: claude.ai/design` project ends up wrongly using `disk` in a clone). Config **writes** too (e.g. caching `design.project`) target the project's `.trailhead/config.json` in the original checkout, never the clone. **The main session orchestrates and holds the HITL moments** (the `build`/`bug` Discuss's "stop and ask", Verify's acceptance/UAT); **every model key runs as a subagent** (`plan`, `execute` (an *executor subagent* implementing with atomic commits on `models.execute`), `research`, `review`, `debug`, `verify`, `codebase-map`) so the whole per-activity model split applies in one session whatever model it runs on. Run Execute inline only when `models.execute` is unset or equals the session model. **On a subagent-less host** (`subagentToolkit == none`) there is nothing to fan out into: every activity runs inline. **Separately, on a host that can't honour the model keys** (`honorsModelKeys == false`, e.g. Codex, whose subagents run on its own OpenAI models) every `models.*` key collapses to the single session model even though the activities still fan out; when any `models.*` is set there the engine surfaces a **one-time** session-start notice that the split is not taking effect (the keys stay in `.trailhead/config.json` for when the project next runs on Claude Code). On Codex the per-activity split is available through a parallel `models.codex.*` namespace (OpenAI model ids), the honoured namespace there (projected into per-technique `~/.codex/agents/*.toml` and dispatched at runtime by `agent_type`), while `models.*` (Claude ids) still collapse to the single session model; the collapse notice points at `models.codex.*` and goes silent once it is set. A host without a hook bus (`hookBus == none`) also runs a lightweight inline update check at session start (no SessionStart hook, no statusline). See `configuration-reference.md`. The **keys, their values, the model/tdd/acceptance semantics, and the guided menu setup live in `configuration-reference.md`**: read it when running `/trailhead:config` or when you need a key's exact meaning. `/trailhead:config` with no args runs the guided setup; `config get` prints the merged config; `config set <key> <value>` writes one key.
@@ -13,7 +13,7 @@ Resolving a ticket **ends the session**: one ticket per session is a hard rule (
13
13
 
14
14
  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. 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
15
 
16
- **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 5), 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, 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: 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 so the user can pick one up with `/trailhead:quick <n>`. **In a whiteboard session** the fog-graduation reasoning does not apply (the whiteboard has no fog): point the block at **`/trailhead:whiteboard`** (and `/trailhead:quick <n>` for the next loose ticket), not the map. (The example is Italian; match whatever language you're conversing in.)
16
+ **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 5), 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 so the user can pick one up with `/trailhead:quick <n>`. **In a whiteboard session** the fog-graduation reasoning does not apply (the whiteboard has no fog): point the block at **`/trailhead:whiteboard`** (and `/trailhead:quick <n>` for the next loose ticket), not the map. (The example is Italian; match whatever language you're conversing in.)
17
17
 
18
18
  **Pending codebase offer (greenfield): surface it here, 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 it **as a distinct, prominent line inside this block**, never as trailing prose after it. It is **an offer the user accepts with a yes, not a command they type** (there is no `/trailhead:codebase` command), so give it its own bullet phrased as a yes/no offer, 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). Show it **exactly once**: on a yes, generate and pin it via the **Codebase map** technique; on a no, don't nag again this session. This is the one non-command item the block may carry, and 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).
19
19
 
@@ -10,8 +10,9 @@ gh issue create --label "trailhead:map" --title "<destination>" --body-file <bod
10
10
  gh issue create --label "trailhead:ticket,trailhead:build,trailhead:map-<map>" --title "<question/goal>" --body-file <body>
11
11
  # link the ticket to its map: native sub-issue (structure/UI) + the map label above (the query)
12
12
  gh api --method POST repos/{owner}/{repo}/issues/<map>/sub_issues -F sub_issue_id=$(gh api repos/{owner}/{repo}/issues/<ticket> --jq .id)
13
- # wire a blocker: native dependency (visual frontier in the UI) + the label (the query)
13
+ # wire a blocker: native dependency (visual frontier in the UI) + the trailhead:blocked label (the query)
14
14
  gh api --method POST repos/{owner}/{repo}/issues/<blocked>/dependencies/blocked_by -F issue_id=$(gh api repos/{owner}/{repo}/issues/<blocker> --jq .id)
15
+ # the label rides in the --label list at creation for a ticket already known-blocked (fires no issues.labeled event); use --add-label ONLY when a ticket becomes blocked LATER (a single event, never a bulk burst):
15
16
  gh issue edit <blocked> --add-label "trailhead:blocked"
16
17
  # the frontier: open, unassigned, not blocked, not unverified, one query (add --label trailhead:map-<map> to scope to one map when several are live; add -label:trailhead:whiteboard to a single-map repo-wide query to keep loose tickets off it)
17
18
  gh issue list --label "trailhead:ticket" --state open --search "no:assignee -label:trailhead:blocked -label:trailhead:unverified -label:trailhead:whiteboard"
@@ -47,10 +48,11 @@ Wire all three in the same pass; a ticket carrying only one or two has drifted.
47
48
  ```bash
48
49
  gh api --method POST repos/{owner}/{repo}/issues/<blocked>/dependencies/blocked_by -F issue_id=$(gh api repos/{owner}/{repo}/issues/<blocker> --jq .id)
49
50
  ```
50
- 3. The `trailhead:blocked` label:
51
+ 3. The `trailhead:blocked` label. **Set it at creation** (include it in the `gh issue create --label` list) for any ticket known-blocked up front, which is every bulk path (the chart/adopt wiring second pass, split children, an on-the-fly ticket with a known blocker): the label then rides in the ticket's `issues.opened` event and fires **no** `issues.labeled` event. Reserve the post-creation `--add-label` form for a ticket that becomes blocked **later**, after it already exists (a single, non-bursty event):
51
52
  ```bash
52
53
  gh issue edit <blocked> --add-label "trailhead:blocked"
53
54
  ```
55
+ **Why at creation.** The repo-side label guard (`.github/workflows/trailhead-label-guard.yml`) fires on `issues.labeled`. A setup pass that adds `trailhead:blocked` to many freshly-created tickets via `--add-label` fires a burst of `issues.labeled` events, one guard run each; GitHub sheds a fraction of that concurrent burst as `startup_failure`/`cancelled`, reddening the Actions history with no-op runs (the labeler is the authorized maintainer). Riding the label in `issues.opened` avoids the burst; moves 1-2 (the `## Blocked by` prose and the native dependency edge) still wire together in the second pass and fire no label event.
54
56
 
55
57
  Same-blocker rule: all three must name the SAME blocker ticket, never a superseded parent, a split-origin, or a by-role placeholder ("child A"). Write the prose line and the native dependency from the same id, in the same pass, so they cannot point at different tickets.
56
58
 
@@ -11,7 +11,7 @@ The tracker is GitHub via the `gh` CLI (already authenticated). **Never post a s
11
11
  | Ticket | a child issue with label `trailhead:ticket` + exactly one **type** label: `trailhead:decision` / `trailhead:research` / `trailhead:prototype` / `trailhead:build` / `trailhead:bug` / `trailhead:task`, plus its map's `trailhead:map-<n>` label and native sub-issue edge (see Child→map link). **A `trailhead:whiteboard` ticket carries none of the map wiring** (no `trailhead:map-<n>`, no sub-issue edge, no `Parent:`); see [The whiteboard](../trailhead-view/SKILL.md#render-the-whiteboard-whiteboard). |
12
12
  | Child→map link | three views of one fact, wired in the same pass (like a blocker): a `Parent: <map name>(link)` line in the ticket body (human-readable); a **native sub-issue** edge to the map (`gh api --method POST repos/{owner}/{repo}/issues/<map>/sub_issues -F sub_issue_id=<ticket .id>`, the structure GitHub renders); and the map's **`trailhead:map-<n>` label** on the ticket (the queryable key that scopes its map's frontier). See [Multiple maps on one repo](multi-map.md#multiple-maps-on-one-repo-parking--concurrency). **A whiteboard ticket has none of the three** (it belongs to no map). |
13
13
  | Path scope (monorepo) | an optional `Scope: <path>[, <path>…]` line in the ticket body naming the package/dir(s), or **submodule path(s)**, the ticket works in. It sharpens the [undeclared-coupling check](teamwork.md#working-as-a-team) (disjoint scopes parallelize safely), scopes build/test to the affected package, and scopes an `isolation: worktree` worktree's commits. **List every one it touches** (a ticket spanning two submodules names both). **Omit it in a single-package repo** (the whole tree is the scope); add it only where a repo has independent packages/subtrees or submodules. |
14
- | Blocking | **Find every blocker first: from the ticket's own Question.** Each still-open ticket whose output this ticket *consumes as an input* is a blocker: a decision that needs another's answer, a build that needs a decision, a formula that needs the very thing another ticket defines. Read the Question and wire **all** of them: miss one and the ticket surfaces on the frontier prematurely and gets worked before its prerequisite. Then wiring a blocker is **three moves**: (1) list *which* tickets block in the `## Blocked by` body section (name+link); (2) create the **native GitHub dependency** so the frontier renders visually in GitHub's own UI: `gh api --method POST repos/{owner}/{repo}/issues/<blocked>/dependencies/blocked_by -F issue_id=<blocker's internal .id>`; (3) add the **`trailhead:blocked`** label. The **label is the frontier's source of truth**: native dependencies aren't cheaply queryable in one search, so the label is what keeps the frontier **one query**. On unblock, **remove the label the moment the last blocker closes** (that's what graduates the ticket onto the frontier); the native edge needs no cleanup: GitHub auto-reflects a closed blocker. **The three moves are one fact in three views: they must name the *same* blocker.** The `## Blocked by` line must link the **real blocker ticket** the native edge targets (its actual number), never a superseded parent, a split-origin, or a by-role placeholder like "child A"; wire the prose line and the native dependency in the same pass, from the same id, so they can't drift. |
14
+ | Blocking | **Find every blocker first: from the ticket's own Question.** Each still-open ticket whose output this ticket *consumes as an input* is a blocker: a decision that needs another's answer, a build that needs a decision, a formula that needs the very thing another ticket defines. Read the Question and wire **all** of them: miss one and the ticket surfaces on the frontier prematurely and gets worked before its prerequisite. Then wiring a blocker is **three moves**: (1) list *which* tickets block in the `## Blocked by` body section (name+link); (2) create the **native GitHub dependency** so the frontier renders visually in GitHub's own UI: `gh api --method POST repos/{owner}/{repo}/issues/<blocked>/dependencies/blocked_by -F issue_id=<blocker's internal .id>`; (3) apply the **`trailhead:blocked`** label, **at creation** (in the `gh issue create --label` list) for any ticket known-blocked up front, which is every bulk path (chart/adopt, split, an on-the-fly ticket with a known blocker): the label rides in `issues.opened` and fires **no** `issues.labeled` event, reserving the post-creation `--add-label` form for a ticket that becomes blocked *later* (a single event). This matters because the repo-side label guard fires on `issues.labeled`: a setup pass that `--add-label`s many fresh tickets spawns a burst of guard runs GitHub sheds as `startup_failure`/`cancelled` (all no-ops, the labeler is the maintainer), so riding the label in `issues.opened` keeps the Actions history clean. The **label is the frontier's source of truth**: native dependencies aren't cheaply queryable in one search, so the label is what keeps the frontier **one query**. On unblock, **remove the label the moment the last blocker closes** (that's what graduates the ticket onto the frontier); the native edge needs no cleanup: GitHub auto-reflects a closed blocker. **The three moves are one fact in three views: they must name the *same* blocker.** The `## Blocked by` line must link the **real blocker ticket** the native edge targets (its actual number), never a superseded parent, a split-origin, or a by-role placeholder like "child A"; wire the prose line and the native dependency in the same pass, from the same id, so they can't drift. |
15
15
  | Claim | assign the ticket to yourself: `gh issue edit <n> --add-assignee @me`: the assignee *is* the claim; the claimer owns it end to end and closes it (no approver). Re-check before starting; on a collision, stop and ask the user. See [Working as a team](teamwork.md#working-as-a-team). |
16
16
  | Resolution | comment with the answer → `gh issue close` → update `Decisions so far` on the map |
17
17
 
@@ -1,5 +1,5 @@
1
1
  # Code review
2
- Runs after execute, before closing a `build`/`bug` ticket: as a subagent when the diff is non-trivial, inline for a tiny change. Spawn it as the built-in **`Explore`** agent (read-only: it reviews and reports, it must never Edit/Write or commit); **never** a specialised reviewer agent that another installed tool registered, which follows a different protocol and may write its own review file instead of trailhead's `VERIFY` comment.
2
+ Runs after execute, before closing a `build`/`bug` ticket: as a subagent when the diff is non-trivial, inline for a tiny change. Dispatch the **`trailhead-code-review`** agent (read-only: it reviews and reports, it must never Edit/Write or commit), passing `config.models.review` as the model override; **never** an external-plugin reviewer agent that another installed tool registered, which follows a different protocol and may write its own review file instead of trailhead's `VERIFY` comment.
3
3
 
4
4
  **Review adversarially; don't defer to authority.** A review subagent still loads the project/global `CLAUDE.md` (Claude Code doesn't strip it per-spawn; auto-memory, though, is *not* inherited). Treat those conventions (and any "this decision is settled / final / don't revisit" note) as **non-authoritative for your verdict**: judge the diff against the ticket's spec and the code itself, never rubber-stamp because a memory or a CLAUDE.md says a choice is closed. Convention endorsement may suppress a *style* smell; it may **never** suppress a correctness or security finding. The point of an independent review is to disagree when the code warrants it.
5
5
 
@@ -10,7 +10,7 @@ Review the DIFF, not the whole tree: pin a fixed point (the ticket's base commit
10
10
  - **Reuse & simplification**: hunt the classic smells, each a *judgement call* (never a hard violation), and for each name it → say how to simplify: duplicated code (extract), mysterious name (rename), primitive obsession & data clumps (introduce a type), long parameter list, feature envy / message chains (move or collapse), repeated switches, speculative generality (delete), shotgun surgery & divergent change, middle-man / shallow pass-through (delete or deepen). Suppress any a documented standard endorses; skip what tooling enforces.
11
11
  - **Standards & spec**: does it match the repo's documented conventions AND do what the ticket actually asked (missing requirements, scope creep, plausibly-wrong implementations)? Suppress smells a documented standard endorses; skip anything tooling already enforces.
12
12
 
13
- Classify each finding: **Critical** (bug/security/breaks the spec) → **Warning** (should fix) → **Info** (judgement call). Before reporting ANY finding, **verify it adversarially**: re-read the actual code, confirm the failing path is reachable and the claim holds. Discard plausible-but-wrong findings; an unverified finding is worse than none. Quote the offending hunk (and the standard/spec line) per finding. Report in the ticket's `VERIFY` comment, grouped by severity with file:line and the fix.
13
+ Classify each finding: **Critical** (bug/security/breaks the spec) → **Warning** (should fix) → **Info** (judgement call). Before reporting ANY finding, **verify it adversarially**: re-read the actual code, confirm the failing path is reachable and the claim holds. Discard plausible-but-wrong findings; an unverified finding is worse than none. Quote the offending hunk (and the standard/spec line) per finding. Report in the ticket's `VERIFY` comment, **led by a machine-checkable status tag on its own first line**: `status: clean` when no **Critical** finding remains (nothing blocks Resolve), or `status: issues_found` when one or more Criticals are still open (the caller must iterate the fix-and-re-review loop below, or run the HITL checkpoint, before resolving). Warnings/Info alone leave the tag `status: clean`, since they don't gate resolution. Emit that line verbatim as the comment's first line, then the findings grouped by severity with file:line and the fix, so the Verify step branches on the tag without parsing prose.
14
14
 
15
15
  **Converge on the blockers with a bounded budget and an explicit boundary.** A **Critical** finding is *blocking*: it must be fixed before Resolve. Fixing one can introduce or reveal another, so the review is a **loop, not a single pass**: fix every Critical, then re-review the new diff, up to `config.code_review.rounds` (default 2) fix-and-re-review rounds. Stop when no Critical remains, then Resolve, leaving Warnings/Info noted for the caller to weigh. **If the rounds are exhausted with Critical findings still open**, never resolve silently and never silently keep going: surface an explicit HITL checkpoint in the chat, **name each surviving blocker**, and put the choice to the user, (a) **run another 2 review rounds** on the remaining blockers (the point of this ask), or (b) **proceed with the blockers recorded** (resolve consciously accepting or deferring them, noting each in the `VERIFY` comment). **Wait for their pick; never auto-advance in either direction over open blocking findings.** (This mirrors **Cross-AI plan review**'s boundary ask: see `plan-review.md` step 4.)
16
16
 
@@ -1,5 +1,5 @@
1
1
  # Codebase map (fan-out)
2
- One-time at project adoption. Fan out 5 read-only reader subagents in parallel (single message), each owning ONE non-overlapping facet; no facet reads another's territory. Spawn each reader as the built-in **`Explore`** agent (read-only by construction); **never** a specialised mapper agent that another installed tool registered.
2
+ One-time at project adoption. Fan out 5 read-only reader subagents in parallel (single message), each owning ONE non-overlapping facet; no facet reads another's territory. Dispatch each reader as the **`trailhead-codebase-map`** agent (read-only by construction); **never** an external-plugin mapper agent that another installed tool registered.
3
3
  - **Architecture**: pattern, layers, data flow, entry points, module seams; where the deep modules live.
4
4
  - **Stack & dependencies**: languages, runtime, frameworks, manifests, build/config, external integrations (DBs, APIs, auth, webhooks).
5
5
  - **Decisions already embodied / conventions**: style, naming, error handling, the repo's documented standards; patterns a new change must match.
@@ -1,7 +1,7 @@
1
1
  # Debug (scientific method)
2
2
  A bug is a science problem: observe, hypothesize, test, confirm, verify. Never patch on a hunch: an unconfirmed fix is a guess that recurs. Treat the issue as persistent debug state: log every hypothesis, test, and eliminated cause as comments, so a context reset never restarts the investigation.
3
3
 
4
- When run as a subagent, spawn it as the built-in **`general-purpose`** agent (it adds instrumentation and regression tests, so it needs Write); **never** a specialised debugger agent that another installed tool registered.
4
+ When run as a subagent, dispatch the **`trailhead-debug`** agent (full tools: it adds instrumentation and regression tests, so it needs Write), passing `config.models.debug` as the model override; **never** an external-plugin debugger agent that another installed tool registered.
5
5
 
6
6
  **Redact as you go.** These notes are posted on public Issues. When you show a command, output, or artifact, replace every secret with `<REDACTED>` and run loops against env vars, never inlined credentials; quote only the signal-carrying lines, not whole dumps. If a redacted artifact isn't enough to move forward, say so and ask the user; never paste the raw one to compensate.
7
7
 
@@ -13,5 +13,8 @@ When run as a subagent, spawn it as the built-in **`general-purpose`** agent (it
13
13
  6. **Instrument: one variable at a time.** Each probe maps to a specific prediction. Prefer a debugger/REPL breakpoint over ten logs; never "log everything and grep." Tag debug logs with a unique prefix (`[DEBUG-a4f2]`) so cleanup is one grep. For perf regressions, measure a baseline first (profiler, query plan), then bisect; logs mislead here.
14
14
  7. **Confirm root cause, not symptom.** Enumerate candidate causes across ≥2 categories (code / config / environment / data). Ask the AND-gate: *could this need more than one condition at once?* If yes, a single cause can't account for the symptom: find the co-occurring cause(s); the fix must address all or it recurs. Record confirmed cause(s); append eliminated branches (never delete: auditability).
15
15
  8. **RCA branching: when a hypothesis fails.** Disproof is progress: move it to Eliminated with the evidence, test the next ranked one. If 3+ fixes failed or you can't explain current behaviour, your mental model is wrong; restart: write what's certain, what's ruled out, generate *new* hypotheses, begin again from reproduction.
16
+
17
+ **Pre-fix checkpoint: a confirmed cause, or no fix.** Before any fix is written, the `DIAGNOSIS` must record (1) the root cause stated as a *falsifiable* hypothesis and (2) the falsification test whose result would disprove it, actually run with the prediction holding. A cause that has not survived its own falsification test is still a guess: stay in diagnosis (loop steps 5–8, generating new hypotheses), never proceed to Fix. This is the hard gate on the `bug` engine's Diagnose→Fix boundary: only a confirmed cause graduates to the fix.
18
+
16
19
  9. **Verify the fix.** State the oracle before the assertion (specified > derived/model > metamorphic > implicit-crash; never settle for "didn't crash"). Write the regression test at a correct seam exercising the real bug pattern; if none can exist, that absence IS a finding: flag it. Watch it fail, apply the fix, watch it pass. Then: original repro gone, full suite green, boundary neighbours (N-1/N/N+1, empty/singleton) pass, all `[DEBUG-]` probes and throwaway harnesses removed, winning hypothesis recorded in the commit.
17
20