@marcomigozzi/trailhead 0.16.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.it.md +57 -10
  2. package/README.md +57 -10
  3. package/bin/capture-action.test.js +167 -0
  4. package/bin/capture-docs.test.js +32 -0
  5. package/bin/engine-route-docs.test.js +148 -0
  6. package/bin/grill-model-docs.test.js +155 -0
  7. package/bin/grilling-holistic-review-docs.test.js +7 -3
  8. package/bin/lib/codex-projection.js +22 -13
  9. package/bin/lib/codex-projection.test.js +58 -17
  10. package/bin/lib/host-descriptor.js +1 -1
  11. package/bin/lib/host-descriptor.test.js +6 -0
  12. package/bin/report-status-docs.test.js +10 -2
  13. package/bin/session-trust-docs.test.js +37 -0
  14. package/bin/surfaced-scope-docs.test.js +57 -0
  15. package/bin/trailhead.js +7 -5
  16. package/bin/trailhead.test.js +53 -11
  17. package/bin/unblock-docs.test.js +56 -0
  18. package/package.json +1 -1
  19. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  20. package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
  21. package/plugins/trailhead/agents/trailhead-grill.md +41 -0
  22. package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
  23. package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
  24. package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
  25. package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
  26. package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
  27. package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
  28. package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
  29. package/plugins/trailhead/hooks/lib/issue-body.test.js +84 -0
  30. package/plugins/trailhead/hooks/lib/model-defaults-review.js +22 -1
  31. package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +64 -1
  32. package/plugins/trailhead/hooks/lib/model-route.js +207 -0
  33. package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
  34. package/plugins/trailhead/hooks/lib/report-status.test.js +56 -1
  35. package/plugins/trailhead/hooks/mod/README.md +16 -7
  36. package/plugins/trailhead/hooks/mod/map-pane.tsx +72 -16
  37. package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
  38. package/plugins/trailhead/hooks/mod/repo-pin.ts +8 -5
  39. package/plugins/trailhead/hooks/trailhead-check-update.js +17 -1
  40. package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
  41. package/plugins/trailhead/skills/_shared/capture-core.md +118 -0
  42. package/plugins/trailhead/skills/_shared/choices.md +2 -2
  43. package/plugins/trailhead/skills/_shared/configuration-reference.md +18 -16
  44. package/plugins/trailhead/skills/_shared/configuration.md +1 -1
  45. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  46. package/plugins/trailhead/skills/_shared/substrate-commands.md +8 -4
  47. package/plugins/trailhead/skills/_shared/substrate.md +1 -1
  48. package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
  49. package/plugins/trailhead/skills/_shared/teamwork.md +6 -2
  50. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
  51. package/plugins/trailhead/skills/_shared/techniques/code-review.md +5 -2
  52. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +4 -1
  53. package/plugins/trailhead/skills/_shared/techniques/debug.md +4 -1
  54. package/plugins/trailhead/skills/_shared/techniques/grilling.md +26 -1
  55. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
  56. package/plugins/trailhead/skills/_shared/techniques/verify.md +3 -1
  57. package/plugins/trailhead/skills/_shared/techniques.md +6 -4
  58. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
  59. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
  60. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
  61. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +4 -4
  62. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +3 -3
  63. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
  64. package/plugins/trailhead/skills/trailhead-work/SKILL.md +11 -9
  65. package/plugins/trailhead/skills/trailhead-work/references/auto.md +1 -1
  66. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +10 -10
  67. package/plugins/trailhead/templates/trailhead-capture.js +418 -0
  68. package/plugins/trailhead/templates/trailhead-checkout-find.js +256 -0
  69. package/plugins/trailhead/templates/trailhead-checkout-in.js +76 -20
  70. package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
  71. package/plugins/trailhead/templates/trailhead-issue-body.js +69 -6
  72. package/plugins/trailhead/templates/trailhead-report-status.js +57 -9
  73. package/plugins/trailhead/tests/map-pane-ui.test.ts +122 -37
  74. package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
  75. package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
  76. package/plugins/trailhead/types/index.d.ts +7 -0
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+ // Docs test per #325: ogni step del motore decide inline vs dispatch con
3
+ // model-route.js, non con la regola in prosa. Il testo canonico vive una volta
4
+ // in techniques.md; ogni punto di dispatch ha un gate imperativo che lo richiama.
5
+ // Run: node engine-route-docs.test.js
6
+ const assert = require('assert');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const root = path.join(__dirname, '..');
11
+ const skills = path.join(root, 'plugins', 'trailhead', 'skills');
12
+ const read = (rel) => fs.readFileSync(path.join(skills, rel), 'utf8');
13
+
14
+ let passed = 0;
15
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
16
+
17
+ const SECTION = '**Route every step through model-route.js.**';
18
+ const SECTION_NAME = 'Route every step through model-route.js';
19
+
20
+ // --- Parte A: la sezione canonica in techniques.md -----------------------------------
21
+ {
22
+ const techniques = read('_shared/techniques.md');
23
+ const at = techniques.indexOf(SECTION);
24
+ ok('A: techniques.md has the canonical routing paragraph', at !== -1);
25
+ const sec = at === -1 ? '' : techniques.slice(at, techniques.indexOf('\n\n', at));
26
+ const has = (...needles) => needles.every((n) => sec.includes(n));
27
+ ok('A: the command with every flag',
28
+ has('model-route.js <key> --session <your full model id>', '--session-effort', '--host codex', '--no-subagents', '<project root>'));
29
+ ok('A: the two-path <hooks> lookup', has('../../hooks/lib/', '../trailhead/hooks/lib/', 'model-defaults-review.js'));
30
+ ok('A: where the model id comes from', has('The exact model ID is', 'pass `unknown` only if it is truly absent'));
31
+ ok('A: the key per step', has(
32
+ 'Plan -> `plan`', 'Execute -> `execute`', 'Fix -> `execute`', 'Code review -> `review`',
33
+ 'Goal-backward verification -> `verify`', 'Research -> `research`', 'Debug -> `debug`',
34
+ 'Codebase map -> `codebase-map`', 'Grilling -> `grill`'));
35
+ ok('A: Fix spawns trailhead-fix in place of the returned agent', has('spawn `trailhead-fix` in place of the returned `agent`'));
36
+ ok('A: Grilling keeps its own section', has('**Who runs a round**'));
37
+ ok('A: route inline, suggest and warning', has('`route: inline`', '`suggest`', '`warning`'));
38
+ ok('A: route dispatch uses agent and alias verbatim', has('`route: dispatch`', 'use its `agent` and `alias` verbatim'));
39
+ ok('A: no hand decision', has('Never decide inline versus dispatch by hand', 'never read the config yourself'));
40
+ ok('A: an unregistered agent runs inline', has('not registered', 'inline'));
41
+ ok('A: the lean exception', has('accepted `lean` skip'));
42
+ ok('A: the autonomous-run exception dispatches with no model override on an inline route',
43
+ has('more than one slot', 'no model override'));
44
+ ok('A: Codex spawns by agent_type, with a null alias', has('`agent_type=<agent>` on Codex', '`alias` is null'));
45
+ ok('A: Claude Code passes alias as the Agent model override', has('`alias` as the `Agent` model override on Claude Code'));
46
+ const rule = sec.indexOf('Never decide inline versus dispatch by hand');
47
+ const lean = sec.indexOf('accepted `lean` skip');
48
+ const auto = sec.indexOf('more than one slot');
49
+ ok('A: both exclusions are stated after the rule, inside the paragraph',
50
+ rule !== -1 && lean > rule && auto > rule && sec.indexOf('Two cases sit outside the route') > rule);
51
+ ok('A: the helper still runs in the autonomous case', sec.slice(auto).includes('the helper is still run'));
52
+ const dispatch = techniques.split('\n')[4] || '';
53
+ ok('A: the dispatch paragraph points at the routing paragraph', dispatch.includes(SECTION_NAME));
54
+ ok('A: the dispatch paragraph no longer restates the hand rule',
55
+ !dispatch.includes('running inline instead only when') && !dispatch.includes('Keep the inline-vs-subagent decision on the full id'));
56
+ }
57
+
58
+ // --- Parte B: il gate a ogni punto di dispatch ---------------------------------------
59
+ const gateOk = (text, key) =>
60
+ text.includes(`model-route.js ${key} --session <your full model id> <project root>`) &&
61
+ text.includes('obey its `route`') &&
62
+ text.includes('never decide inline versus dispatch by hand') &&
63
+ text.includes(SECTION_NAME);
64
+
65
+ {
66
+ const engines = read('trailhead-work/references/ticket-engines.md');
67
+ const lines = engines.split('\n');
68
+ const line = (prefix) => lines.find((l) => l.startsWith(prefix)) || '';
69
+ ok('B: build Plan gates on plan', gateOk(line('2. **Plan**:'), 'plan'));
70
+ ok('B: build Execute Dispatch gates on execute', gateOk(line(' - **Dispatch.** Spawn the **`trailhead-executor`**'), 'execute'));
71
+ ok('B: bug Diagnose gates on debug', gateOk(line('2. **Diagnose**:'), 'debug'));
72
+ const fix = line(' - **Dispatch.** Spawn the **`trailhead-fix`**');
73
+ ok('B: bug Fix Dispatch gates on execute and spawns trailhead-fix', gateOk(fix, 'execute') && fix.includes('`trailhead-fix`'));
74
+ ok('B: ticket-engines.md no longer restates the hand rule', !engines.includes('Inline only when the resolved model'));
75
+ const GATE = '**First, route it.**';
76
+ for (const [name, l] of [['Plan', line('2. **Plan**:')], ['Execute', line(' - **Dispatch.** Spawn the **`trailhead-executor`**')],
77
+ ['Diagnose', line('2. **Diagnose**:')], ['Fix', fix]]) {
78
+ ok(`B: ${name} spawns by agent_type on Codex`, l.includes('agent_type='));
79
+ const g = l.indexOf(GATE);
80
+ ok(`B: ${name} gate comes before any lean-skip or exception wording`, g !== -1 && (l.indexOf('`lean`') === -1 || g < l.indexOf('`lean`')));
81
+ }
82
+ }
83
+
84
+ for (const [file, key, agent] of [
85
+ ['code-review.md', 'review', 'trailhead-code-review'],
86
+ ['verify.md', 'verify', 'trailhead-verify'],
87
+ ['research.md', 'research', 'trailhead-research'],
88
+ ['debug.md', 'debug', 'trailhead-debug'],
89
+ ['codebase-map.md', 'codebase-map', 'trailhead-codebase-map'],
90
+ ]) {
91
+ const text = read(`_shared/techniques/${file}`);
92
+ const gateAt = text.indexOf('**First, route it.**');
93
+ const dispatchAt = text.indexOf(`**\`${agent}\`** agent`);
94
+ const gate = gateAt === -1 ? '' : text.slice(gateAt, text.indexOf('\n', gateAt));
95
+ ok(`B: ${file} has a routing gate before its dispatch sentence`, gateAt !== -1 && dispatchAt !== -1 && gateAt < dispatchAt);
96
+ ok(`B: ${file} gate runs model-route.js ${key}`, gateOk(gate, key));
97
+ ok(`B: ${file} gate exempts the dispatched ${agent}`, gate.includes(`If you are the \`${agent}\` agent, skip this gate`));
98
+ ok(`B: ${file} no longer restates the hand rule`, !text.includes('inline only when the resolved model'));
99
+ ok(`B: ${file} gate spawns by agent_type on Codex`, gate.includes('agent_type=<agent>'));
100
+ ok(`B: ${file} gate points at the canonical paragraph`, gate.includes('Route every step through model-route.js'));
101
+ ok(`B: ${file} gate comes before any lean-skip wording`, text.indexOf('`lean`') === -1 || gateAt < text.indexOf('`lean`'));
102
+ }
103
+
104
+ // --- Parte B2: auto.md ---------------------------------------------------------------
105
+ {
106
+ const auto = read('trailhead-work/references/auto.md');
107
+ const line = auto.split('\n').find((l) => l.startsWith('- **Background dispatch, never inline.**')) || '';
108
+ ok('B: auto.md still dispatches every step in the background', line.includes('background subagent'));
109
+ ok('B: auto.md runs the helper per step', line.includes('model-route.js <key> --session <your full model id> <project root>') && line.includes('Route every step through model-route.js'));
110
+ ok('B: auto.md spawns with no model override on an inline route', line.includes('`route: inline`') && line.includes('no model override'));
111
+ }
112
+
113
+ // --- Parte B3: rifiniture della review di #325 --------------------------------------
114
+ {
115
+ const cr = read('_shared/techniques/code-review.md');
116
+ ok('B: code-review.md no longer says a lean skip is the only inline review', !cr.includes('only inline review'));
117
+ ok('B: code-review.md says the review runs inline on route: inline', cr.includes('in the session on `route: inline`'));
118
+ const audit = read('trailhead-manage/references/auditing.md');
119
+ ok('B: auditing.md routes verify once per run through model-route.js',
120
+ audit.includes('routes `verify` once per run through `model-route.js`') && audit.includes('Route every step through model-route.js'));
121
+ const agent = fs.readFileSync(path.join(root, 'plugins', 'trailhead', 'agents', 'trailhead-codebase-map.md'), 'utf8');
122
+ ok('B: the codebase-map agent no longer claims it has no model key', !agent.includes('no dedicated model key') && agent.includes('config.models.codebase-map'));
123
+ const cfg = read('_shared/configuration.md');
124
+ const ref = read('_shared/configuration-reference.md');
125
+ ok('B: configuration docs say every key routes through model-route.js',
126
+ !cfg.includes('every model key runs as a subagent') && !ref.includes('Every model key runs as a subagent') &&
127
+ cfg.includes('every model key routes through `model-route.js`') && ref.includes('Every model key routes through `model-route.js`'));
128
+ }
129
+
130
+ // --- Parte C: la config descrive l'helper, non una decisione a mano -------------------
131
+ {
132
+ const cfg = read('_shared/configuration.md');
133
+ const ref = read('_shared/configuration-reference.md');
134
+ ok('C: configuration.md lets the helper decide', cfg.includes('model-route.js') && !cfg.includes('Run Execute inline only when'));
135
+ const bullet = (k) => ref.split('\n').find((l) => l.startsWith(`- \`${k}\` →`)) || '';
136
+ ok('C: configuration-reference.md plan bullet points at the helper', bullet('plan').includes('model-route.js') && !bullet('plan').includes('inline only when'));
137
+ ok('C: configuration-reference.md execute bullet points at the helper', bullet('execute').includes('model-route.js') && !bullet('execute').includes('inline in the session instead when'));
138
+ }
139
+
140
+ // --- Parte D: l'helper copre ogni chiave nominata ------------------------------------
141
+ {
142
+ const { AGENT_OF } = require(path.join(root, 'plugins', 'trailhead', 'hooks', 'lib', 'model-route.js'));
143
+ for (const key of ['plan', 'execute', 'review', 'verify', 'research', 'debug', 'codebase-map', 'grill']) {
144
+ ok(`D: model-route.js knows ${key} on both hosts`, !!AGENT_OF.claude[key] && !!AGENT_OF.codex[key]);
145
+ }
146
+ }
147
+
148
+ console.log(`engine-route-docs.test.js: ${passed} checks passed`);
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+ // Docs test per #323: Grilling gira su models.grill / trailhead-grill, con la sessione
3
+ // principale come orchestratore HITL. Il protocollo e' scritto una volta (grilling.md);
4
+ // i punti d'ingresso lo richiamano soltanto.
5
+ // Run: node grill-model-docs.test.js
6
+ const assert = require('assert');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const root = path.join(__dirname, '..');
11
+ const skills = path.join(root, 'plugins', 'trailhead', 'skills');
12
+ const read = (rel) => fs.readFileSync(path.join(skills, rel), 'utf8');
13
+
14
+ let passed = 0;
15
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
16
+
17
+ // --- Parte A: configuration-reference.md e configuration.md --------------------------
18
+ {
19
+ const ref = read('_shared/configuration-reference.md');
20
+ const cfg = read('_shared/configuration.md');
21
+ const lines = ref.split('\n');
22
+ const rowStart = (prefix) => lines.find((l) => l.startsWith(prefix)) || '';
23
+ ok('A: models.{...} key-table row lists grill', /^\| `models\.\{[^}]*\bgrill\b[^}]*\}`/.test(rowStart('| `models.{')));
24
+ ok('A: models.codex.{...} key-table row lists grill', /^\| `models\.codex\.\{[^}]*\bgrill\b[^}]*\}`/.test(rowStart('| `models.codex.{')));
25
+ ok('A: Models bullet for grill', lines.some((l) => /^- `grill`:/.test(l) && l.includes('Grilling')));
26
+ ok('A: subagent bullet maps grill to trailhead-grill and says the session keeps asking the user',
27
+ lines.some((l) => /^- `grill`/.test(l) && l.includes('trailhead-grill') && /session (keeps|still) ask/.test(l)));
28
+ const header = lines.find((l) => l.startsWith('| profile |')) || '';
29
+ ok('A: profile table has a grill column', header.includes('| grill |'));
30
+ const col = (name) => {
31
+ const idx = header.split('|').map((c) => c.trim()).indexOf('grill');
32
+ const row = lines.find((l) => l.startsWith(`| **${name}** |`)) || '';
33
+ return row.split('|').map((c) => c.trim())[idx];
34
+ };
35
+ ok('A: grill column reads High strong, Balanced strong, Low standard',
36
+ col('High') === 'strong' && col('Balanced') === 'strong' && col('Low') === 'standard');
37
+ ok('A: a profile writes no grill entry, so grill keeps following plan',
38
+ ref.includes('a profile writes **no `grill` entry**') && ref.includes('keeps following `plan`'));
39
+ ok('A: the key rows say an unset grill follows plan',
40
+ rowStart('| `models.{').includes('unset `grill` follows `plan`') && rowStart('| `models.codex.{').includes('unset `grill` follows `models.codex.plan`'));
41
+ ok('A: Models bullet says unset grill is the same model as plan', lines.some((l) => /^- `grill`:/.test(l) && l.includes('Unset, it uses the same model as `plan`')));
42
+ ok('A: the unset-default paragraph names the grill exception',
43
+ ref.includes('the one exception is `grill`: unset, it follows the resolved `models.plan`'));
44
+ ok('A: Manual path offers grill with "same as plan (Recommended)" first, leaving the key unset',
45
+ ref.includes('**same as plan (Recommended)** as the first option') && ref.includes('leaves `models.grill` unset') &&
46
+ ref.includes('leaves `models.codex.grill` unset'));
47
+ ok('A: a layer that sets plan but not grill implies grill = plan before the merge',
48
+ ref.includes('a config layer that sets `plan` but not `grill` implicitly sets `grill` to that `plan` for that layer, before the project-over-global merge'));
49
+ ok('A: existing configs without grill need no action or notice',
50
+ ref.includes('An existing config with no `grill` key grills on its `plan` model with no action and no notice'));
51
+ const advanced = lines.filter((l) => l.includes('behind an "Advanced" option') || l.includes('behind Advanced'));
52
+ ok('A: Advanced lists name grill for Claude and Codex', advanced.length === 2 && advanced.every((l) => l.includes('`grill`')));
53
+ const everything = lines.find((l) => l.startsWith('**Everything else runs in the main session')) || '';
54
+ ok('A: Everything else paragraph no longer lists grilling', everything.length > 0 && !/grilling/i.test(everything.split("That's deliberate")[0]));
55
+ ok('A: Codex sentence (unset inherits, profile or pin brings strong, equal pin runs inline)',
56
+ /unset `models\.codex\.grill` follows `models\.codex\.plan` when that is pinned/.test(ref) &&
57
+ ref.includes('else inherits the session model') && ref.includes('an equal pin (same model as the session'));
58
+ ok('A: a spawn gets the family, not the exact version', ref.includes('A dispatched grill is guaranteed the family of the resolved id, not its exact version'));
59
+ ok('A: configuration.md names the grill exception to the Balanced default', cfg.includes('except `grill`, which follows `plan`'));
60
+ ok('A: configuration.md lists grill in the subagent list', /`research`, `review`, `debug`, `verify`, `codebase-map`, `grill`/.test(cfg));
61
+ }
62
+
63
+ // --- Parte B: grilling.md "Who runs a round" e techniques.md ---------------------------
64
+ {
65
+ const grilling = read('_shared/techniques/grilling.md');
66
+ const techniques = read('_shared/techniques.md');
67
+ const at = grilling.indexOf('\n## Who runs a round');
68
+ ok('B: grilling.md has a Who runs a round section', at !== -1);
69
+ const sec = at === -1 ? '' : grilling.slice(at);
70
+ const has = (...needles) => needles.every((n) => sec.includes(n));
71
+ const gateAt = grilling.indexOf('**First, before any question: route the round.**');
72
+ const firstBullet = grilling.indexOf('\n- **Map the decision as a tree.**');
73
+ ok('B: the routing gate exists and precedes the first round bullet', gateAt !== -1 && firstBullet !== -1 && gateAt < firstBullet);
74
+ const gate = gateAt === -1 ? '' : grilling.slice(gateAt, firstBullet);
75
+ ok('B: the gate runs model-route.js grill --session with the session full model id and obeys route',
76
+ gate.includes('model-route.js grill --session <your full model id> <project root>') && gate.includes('obey its `route`'));
77
+ ok('B: the gate says where the model id comes from and when to pass unknown',
78
+ gate.includes('system prompt or environment') && gate.includes('The exact model ID is') && gate.includes('pass `unknown` only if it is truly absent'));
79
+ ok('B: the gate forbids hand decisions', gate.includes('Never decide inline versus dispatch by hand') && gate.includes('never skip the helper') && gate.includes('never read the config yourself'));
80
+ ok('B: on dispatch the session does not write the questions and relays the report',
81
+ gate.includes('you do not write the questions yourself') && gate.includes('spawn `agent` with model `alias`') && gate.includes('relay its report'));
82
+ ok('B: the gate points at Who runs a round and exempts the dispatched agent', gate.includes('**Who runs a round**') && gate.includes('If you are the `trailhead-grill` agent, skip this gate'));
83
+ ok('B: the route call, with --session-effort and unknown',
84
+ has('model-route.js grill --session', '--session-effort', 'unknown', '--no-subagents', '--host codex'));
85
+ ok('B: the helper is found with the same two-path lookup as model-defaults-review.js',
86
+ has('../../hooks/lib/', '../trailhead/hooks/lib/', 'model-defaults-review.js'));
87
+ ok('B: a context-window suffix on the session id is documented as ignored', has('a trailing `[...]` suffix such as `[1m]` is ignored'));
88
+ ok('B: the per-layer implicit grill rule', has('a config layer that sets `plan` but not `grill` implicitly sets `grill` to that `plan` for that layer, before the project-over-global merge'));
89
+ ok('B: an unset models.grill follows models.plan', has('`models.grill` unset follows `models.plan`'));
90
+ ok('B: use its agent and alias verbatim', has('use its `agent` and `alias` verbatim'));
91
+ ok('B: route inline with the suggest line', has('`route: inline`', '`suggest`', 'suggesting the user switch the session'));
92
+ ok('B: an unregistered agent runs inline', has('not registered', 'inline'));
93
+ ok('B: the report is checked with --technique grill', has('--technique grill'));
94
+ ok('B: the relay never answers on the user\'s behalf', has('relay', 'Never answer on the user\'s behalf', 'never proceed on silence'));
95
+ ok('B: re-dispatch with the full state', has('re-dispatch with the full state'));
96
+ ok('B: the orchestrator holds the review cap', has('The orchestrator caps reviews at two', 'accepted risk'));
97
+ ok('B: the apply/amend/keep menu is the orchestrator\'s', has('apply', 'amend', 'keep', 'AskUserQuestion'));
98
+ ok('B: the family-not-version note', has('the family of the resolved id, not its exact version'));
99
+ ok('B: an inline round writes no report', has('An inline round writes no report'));
100
+ ok('B: Codex semantics', has('unset `models.codex.grill` follows `models.codex.plan` when that is pinned', 'else inherits the session model', 'equal to the session model'));
101
+ ok('B: the protocol bullets above stay the agent\'s protocol', grilling.indexOf('Holistic review before the final confirmation') < at);
102
+
103
+ ok('B: grilling.md states that trailhead-grill output is always the report',
104
+ has('When it runs as `trailhead-grill`, its output is always the report', 'never asks the user live and never shows a menu'));
105
+ const agent = fs.readFileSync(path.join(root, 'plugins', 'trailhead', 'agents', 'trailhead-grill.md'), 'utf8');
106
+ ok('B: the agent follows the round bullets and its return contract, not the orchestrator section',
107
+ agent.includes('round bullets') && agent.includes('Return contract') && !agent.includes('Who runs a round'));
108
+
109
+ const dispatch = techniques.split('\n')[4] || '';
110
+ ok('B: techniques.md dispatch list maps Grilling to trailhead-grill', /Grilling → `trailhead-grill`/.test(dispatch));
111
+ ok('B: techniques.md read-only list includes trailhead-grill', /\*\*read-only\*\*[^)]*`trailhead-grill`/.test(dispatch));
112
+ const row = techniques.split('\n').find((l) => l.startsWith('| **Grilling** |')) || '';
113
+ ok('B: the Grilling index row names models.grill', row.includes('`models.grill`'));
114
+ }
115
+
116
+ // --- Parte C: i punti d'ingresso puntano a "Who runs a round" per nome -----------------
117
+ {
118
+ const POINTER = 'Who runs a round';
119
+ const lineWith = (text, needle) => text.split('\n').find((l) => l.includes(needle)) || '';
120
+ const points = (label, line) => ok(`C: ${label} points at ${POINTER} with the imperative routing wording`, line.length > 0 && line.includes(POINTER) && line.includes('grilling.md') && line.includes('first run the routing step at the top of'));
121
+ const chart = read('trailhead-chart/SKILL.md');
122
+ const standalone = chart.slice(chart.indexOf('## Standalone grill'));
123
+ points('chart SKILL.md Standalone grill', lineWith(standalone, 'Run a standalone **Grilling**'));
124
+ const charting = read('trailhead-chart/references/charting.md');
125
+ points('charting.md Mode 1 Name the destination', lineWith(charting, '1. **Name the destination.**'));
126
+ points('charting.md Mode 1 Map the frontier', lineWith(charting, '2. **Map the frontier.**'));
127
+ points('charting.md Mode 1-bis Map the frontier of the remainder', lineWith(charting, '4. **Map the frontier of the remainder**'));
128
+ points('charting.md Mode 1-bis Name the destination of the remaining stretch', lineWith(charting, '2. **Name the destination of the remaining stretch.**'));
129
+ const engines = read('trailhead-work/references/ticket-engines.md');
130
+ points('ticket-engines.md build Discuss', lineWith(engines, '1. **Discuss**: **never auto-grill.**'));
131
+ points('ticket-engines.md Plan needs_input grill option', lineWith(engines, 'needs_input` -> ask the user the `questions:`'));
132
+ ok('C: the Plan needs_input pointer is about opting to grill the planner questions',
133
+ /opts? to grill/.test(lineWith(engines, 'needs_input` -> ask the user the `questions:`')));
134
+ points('ticket-engines.md decision Converge', lineWith(engines, '2. **Converge.**'));
135
+ const work = read('trailhead-work/SKILL.md');
136
+ points('work SKILL.md quick Grill only if needed', lineWith(work, '- **Grill only if needed** (Discuss)'));
137
+ }
138
+
139
+ // --- Parte D: README e README.it.md ---------------------------------------------------
140
+ {
141
+ for (const file of ['README.md', 'README.it.md']) {
142
+ const text = fs.readFileSync(path.join(root, file), 'utf8');
143
+ const lines = text.split('\n');
144
+ const row = (prefix) => lines.find((l) => l.startsWith(prefix)) || '';
145
+ ok(`D: ${file} models.{...} row lists grill`, /^\| `models\.\{[^}]*codebase-map,grill\}`/.test(row('| `models.{')));
146
+ ok(`D: ${file} models.codex.{...} row lists grill`, /^\| `models\.codex\.\{[^}]*codebase-map,grill\}`/.test(row('| `models.codex.{')));
147
+ ok(`D: ${file} Models bullets carry a grill bullet`, lines.some((l) => /^- `grill`/.test(l) && l.includes('trailhead-grill')));
148
+ ok(`D: ${file} no longer says grilling runs on the session model`,
149
+ !/charting, grilling/.test(text));
150
+ ok(`D: ${file} says grill follows plan when unset`, lines.some((l) => /^- `grill`/.test(l) && l.includes('`plan`') && /unset|non impostat/.test(l)));
151
+ ok(`D: ${file} carries the Codex note`, text.includes('models.codex.grill') && /models\.codex\.plan/.test(lines.find((l) => l.includes('models.codex.grill') && !l.startsWith('|')) || ''));
152
+ }
153
+ }
154
+
155
+ console.log(`grill-model-docs.test.js: ${passed} checks passed`);
@@ -37,8 +37,12 @@ for (const frag of [
37
37
  'contradictions, gaps, choices that break each other or the destination',
38
38
  'by question number',
39
39
  'say so in one line and go straight to the confirmation',
40
- 'accept, amend or reject',
41
- 'reject keeps the choices as they were',
40
+ 'Every non-working combination carries its fix',
41
+ 'say why in one line and name where it goes',
42
+ 'Accepting a combination as a risk is never the proposed fix',
43
+ 'apply, amend or keep',
44
+ 'apply (the first option, recommended) applies every proposed amendment and fix',
45
+ 'keep leaves the choices as they were',
42
46
  'consciously accepted risk',
43
47
  'reopens that branch',
44
48
  'the review runs again on the new set',
@@ -60,7 +64,7 @@ const pointer = (name, text, startsWith) => {
60
64
  const l = text.split('\n').find((x) => x.startsWith(startsWith));
61
65
  ok(`${name} found`, !!l);
62
66
  ok(`${name} references the anchor`, l.includes(ANCHOR));
63
- ok(`${name} does not restate the rule`, !l.includes('accepted risk') && !l.includes('reject keeps'));
67
+ ok(`${name} does not restate the rule`, !l.includes('accepted risk') && !l.includes('keep leaves'));
64
68
  };
65
69
  pointer('chart SKILL standalone grill', read('trailhead-chart/SKILL.md'), 'Run a standalone **Grilling**');
66
70
  const engines = read('trailhead-work/references/ticket-engines.md');
@@ -108,11 +108,11 @@ Where the engine uses \`AskUserQuestion\`, pose the choice as a **plain-text num
108
108
  After presenting the numbered list, STOP and wait: the user's **next message** is the answer to this pending question, not a fresh unrelated turn. Resolve it by matching either the **number typed** (e.g. "2") or the option's **label text** back onto that option, then continue the engine exactly as it would from a native \`AskUserQuestion\` response (e.g. write the chosen config value, or proceed down the chosen branch). If the reply matches no option, ask again rather than guessing. Never silently pick a default and proceed.
109
109
 
110
110
  ## D. Subagents (native multi_agent)
111
- Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_input\`, \`wait_agent\`, \`resume_agent\`, \`close_agent\`), stable and on by default on supported Codex (the installer gates on the floor that guarantees them). Where the engine delegates to a subagent (research, codebase-map, review, plan, execute, debug), spawn a real Codex subagent with these tools and the technique's own protocol (each technique carries its full protocol in-prompt, so no specialised agent is needed; pass the ticket's \`Scope:\` when relevant). The Claude-only Explore-vs-general-purpose split does not apply: Codex has one subagent kind. A subagent runs as a FRESH session that receives a task prompt, not this conversation; run independent ones in parallel (e.g. the codebase-map readers) and \`wait_agent\` on them.
111
+ Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_input\`, \`wait_agent\`, \`resume_agent\`, \`close_agent\`), stable and on by default on supported Codex (the installer gates on the floor that guarantees them). Where the engine delegates to a subagent (research, codebase-map, review, plan, execute, debug, grill), spawn a real Codex subagent with these tools and the technique's own protocol (each technique carries its full protocol in-prompt, so no specialised agent is needed; pass the ticket's \`Scope:\` when relevant). The Claude-only Explore-vs-general-purpose split does not apply: Codex has one subagent kind. A subagent runs as a FRESH session that receives a task prompt, not this conversation; run independent ones in parallel (e.g. the codebase-map readers) and \`wait_agent\` on them.
112
112
 
113
113
  **Never surrender the session to one open-ended \`wait_agent\`; poll instead.** \`wait_agent\` blocks until the subagent returns, so a single unconditional \`wait_agent\` on a long or stalling subagent freezes the whole session with nothing to check: this is the review / plan-review hang. Collect every delegated subagent through a **bounded wait in a poll loop**, not one open-ended blocking call. Give \`wait_agent\` a short timeout (30 to 60s); on each timeout, report a one-line "still running" progress note and loop again, so the session stays responsive and can be checked or interrupted between polls. The two **review** steps, **Code review** and **Cross-AI plan review**, always run this way: backgrounding and polling them (never a blind \`wait_agent\`) is the engine's hard cross-host default, not merely the case where it matters most. They are also the longest quiet activities in the cycle (a full-diff adversarial review, or an external CLI) and the ones most often seen to hang. If your Codex build's \`wait_agent\` exposes no timeout, poll the agent's status/output at intervals instead of blocking, or run the review as a **background job** and check back on it at intervals; the rule is the same either way. When several subagents run in parallel, poll them as a set, a bounded wait cycling across the outstanding handles, reporting which are still running.
114
114
 
115
- **Per-technique agent registry (\`agent_type\` dispatch).** The installer always projects all 8 committed trailhead agents as \`~/.codex/agents/trailhead-<technique>.toml\` (one per technique) and enables \`features.multi_agent_v2\`, so Codex always exposes a per-technique agent registry. A TOML whose technique has a \`models.codex.<key>\` pin set carries that OpenAI model (plus its reasoning effort); every other TOML — an unpinned keyed technique, or the 1 keyless agent — is pin-less and its subagent inherits the session model. Detect the registry at runtime by introspecting the visible \`spawn_agent\` tool schema (GSD-style, never a config read): if its parameters expose an \`agent_type\` field, spawn each delegated activity as \`spawn_agent(agent_type="trailhead-<technique>", reasoning_effort=…)\` (\`trailhead-plan\` for the Plan step, \`trailhead-execute\` for Execute, likewise research/review/debug/verify and \`trailhead-codebase-map\` for the Codebase-map step, plus the 1 keyless \`trailhead-fix\` for the Fix step), which routes it onto that technique's pinned model when one is set, or the session model otherwise. If the \`spawn_agent\` schema exposes NO \`agent_type\` field at all (a Codex build with only base multi_agent v1), spawn with base \`spawn_agent\` and the subagents inherit the one session model. Trust the tool surface you actually see and degrade gracefully: \`multi_agent_v2\` is still under development upstream, so v1 stays a supported runtime.
115
+ **Per-technique agent registry (\`agent_type\` dispatch).** The installer always projects all 9 committed trailhead agents as \`~/.codex/agents/trailhead-<technique>.toml\` (one per technique) and enables \`features.multi_agent_v2\`, so Codex always exposes a per-technique agent registry. A TOML whose technique has a \`models.codex.<key>\` pin set carries that OpenAI model (plus its reasoning effort); every other TOML (an unpinned keyed technique, or fix with execute unset) is pin-less and its subagent inherits the session model. Detect the registry at runtime by introspecting the visible \`spawn_agent\` tool schema (GSD-style, never a config read): if its parameters expose an \`agent_type\` field, spawn each delegated activity as \`spawn_agent(agent_type="trailhead-<technique>", reasoning_effort=…)\` (\`trailhead-plan\` for the Plan step, \`trailhead-execute\` for Execute, likewise research/review/debug/verify \`trailhead-codebase-map\` for the Codebase-map step and \`trailhead-grill\` for a Grilling round, plus \`trailhead-fix\` for the Fix step, which tracks the Execute pin), which routes it onto that technique's pinned model when one is set, or the session model otherwise. An unpinned or session-equal grill runs inline in the main session (no spawn): the main session stays the HITL orchestrator and asks the user itself. If the \`spawn_agent\` schema exposes NO \`agent_type\` field at all (a Codex build with only base multi_agent v1), spawn with base \`spawn_agent\` and the subagents inherit the one session model. Trust the tool surface you actually see and degrade gracefully: \`multi_agent_v2\` is still under development upstream, so v1 stays a supported runtime.
116
116
 
117
117
  \`config.models.*\` (Claude model ids) still cannot take effect here regardless: Codex's subagent model registry is OpenAI-only, so a Claude id cannot run and those keys collapse to the session model. The honoured namespace on Codex is \`models.codex.*\` (above); the one-time models-collapse notice still applies while a stray \`models.*\` is set and \`models.codex.*\` is empty.
118
118
 
@@ -191,10 +191,11 @@ function codexTechniqueKey(name) {
191
191
  }
192
192
 
193
193
  // --- MODEL_KEYS --------------------------------------------------------------
194
- // The 7 pinnable technique keys, mirroring the models.codex.* namespace. The
195
- // other committed agent (fix) has no pin key and is always
196
- // projected pin-less (session model).
197
- const MODEL_KEYS = new Set(['plan', 'execute', 'research', 'review', 'debug', 'verify', 'codebase-map']);
194
+ // The 8 pinnable technique keys, mirroring the models.codex.* namespace. The
195
+ // other committed agent (fix) has no pin key of its own: it tracks execute.
196
+ // Riconoscimento condiviso di "inherit session" (stesso del router, hooks/lib); modulo senza dipendenze, la proiezione resta pura.
197
+ const { isInheritSession } = require('../../plugins/trailhead/hooks/lib/inherit-session.js');
198
+ const MODEL_KEYS = new Set(['plan', 'execute', 'research', 'review', 'debug', 'verify', 'codebase-map', 'grill']);
198
199
 
199
200
  // --- codexAgentToml ---------------------------------------------------------
200
201
  // Render one Codex custom-agent TOML. When `model` is empty/absent, the TOML
@@ -238,9 +239,10 @@ function codexAgentToml({ name, description, developerInstructions, model, effor
238
239
  // readAgentDefs(): every committed plugins/trailhead/agents/trailhead-*.md,
239
240
  // each { name, description, tools, body }), return the set of
240
241
  // trailhead-<technique>.toml writes the installer should make: ONE per agent,
241
- // uniformly (all 8 project, always). A keyed agent (plan/execute/research/
242
- // review/debug/verify/codebase-map) carries its models.codex.<key> pin when set, else is
243
- // pin-less; the 1 keyless agent (fix) is always pin-less.
242
+ // uniformly (all 9 project, always). A keyed agent (plan/execute/research/
243
+ // review/debug/verify/codebase-map/grill) carries its models.codex.<key> pin when set, else is
244
+ // pin-less; fix has no key and carries the models.codex.execute pin (same
245
+ // string/object/effort/inherit-session handling), so no agent is keyless.
244
246
  // Iterates agentDefs in the given order (readAgentDefs sorts by name) so the
245
247
  // write order is deterministic.
246
248
  function codexAgentTomlPlan(codexHome, codexModels, agentDefs) {
@@ -256,13 +258,19 @@ function codexAgentTomlPlan(codexHome, codexModels, agentDefs) {
256
258
 
257
259
  let model = null;
258
260
  let effort = null;
259
- if (MODEL_KEYS.has(key) && models[key] != null) {
260
- const raw = models[key];
261
+ // grill non impostato segue models.codex.plan (come model-route.js).
262
+ // fix non ha chiave propria: segue models.codex.execute (come model-route.js).
263
+ let pinKey = key === 'grill' && (models.grill == null || (typeof models.grill === 'string' && models.grill.trim() === '')) ? 'plan' : key;
264
+ if (key === 'fix') pinKey = 'execute';
265
+ if ((MODEL_KEYS.has(key) || key === 'fix') && models[pinKey] != null) {
266
+ const raw = models[pinKey];
267
+ // "inherit session" e' il default Codex documentato: nessun pin.
268
+ const isInherit = isInheritSession;
261
269
  if (typeof raw === 'string') {
262
- if (raw.trim() !== '') model = raw;
270
+ if (raw.trim() !== '' && !isInherit(raw)) model = raw;
263
271
  } else if (typeof raw === 'object') {
264
272
  const m = typeof raw.model === 'string' ? raw.model : '';
265
- if (m.trim() !== '') {
273
+ if (m.trim() !== '' && !isInherit(m)) {
266
274
  model = m;
267
275
  effort = typeof raw.effort === 'string' ? raw.effort : null;
268
276
  }
@@ -457,6 +465,7 @@ module.exports = {
457
465
  codexClusterAgentsYaml,
458
466
  codexAgentToml,
459
467
  codexAgentTomlPlan,
468
+ MODEL_KEYS,
460
469
  codexVerbSkillContent,
461
470
  codexVerbSkillAgentsYaml,
462
471
  codexVerbSkillPlan,
@@ -96,6 +96,8 @@ ok('codexSkillAdapterHeader §D pins the exact agent_type dispatch call shape',
96
96
  header.includes('spawn_agent(agent_type="trailhead-<technique>"'));
97
97
  ok('codexSkillAdapterHeader §D keeps the base spawn_agent v1 fallback path',
98
98
  header.includes('base multi_agent v1') && header.includes('inherit the one session model'));
99
+ ok('codexSkillAdapterHeader §D says all 9 committed agents, names trailhead-grill, and runs an unpinned grill inline',
100
+ header.includes('all 9 committed') && header.includes('trailhead-grill') && /unpinned or session-equal grill runs inline/.test(header));
99
101
  ok('codexSkillAdapterHeader §D warns against a single blocking wait_agent with a poll loop',
100
102
  header.includes('bounded wait') && header.includes('poll'));
101
103
  ok('codexSkillAdapterHeader §D offers a background-job fallback for a stalling review',
@@ -261,10 +263,10 @@ ok('codexAgentToml escapes a " and \\ in name/description so the TOML stays vali
261
263
  })());
262
264
 
263
265
  // --- codexAgentTomlPlan ---
264
- // 7-entry stub agentDefs mirroring the real plugins/trailhead/agents/*.md
265
- // sources: 6 keyed techniques (plan/execute/research/review/debug/codebase-map,
266
+ // 8-entry stub agentDefs mirroring the real plugins/trailhead/agents/*.md
267
+ // sources: 7 keyed techniques (plan/execute/research/review/debug/codebase-map/grill,
266
268
  // whose filenames derive from trailhead-executor -> execute and
267
- // trailhead-code-review -> review) + 1 always-keyless agent (fix).
269
+ // trailhead-code-review -> review) + fix, which tracks the execute pin.
268
270
  const STUB_AGENT_DEFS = [
269
271
  { name: 'trailhead-plan', description: 'plan desc', tools: 'Read', body: 'Plan body.' },
270
272
  { name: 'trailhead-executor', description: 'execute desc', tools: 'Read, Write', body: 'Execute body.' },
@@ -273,37 +275,76 @@ const STUB_AGENT_DEFS = [
273
275
  { name: 'trailhead-debug', description: 'debug desc', tools: 'Read', body: 'Debug body.' },
274
276
  { name: 'trailhead-fix', description: 'fix desc', tools: 'Read, Write', body: 'Fix body.' },
275
277
  { name: 'trailhead-codebase-map', description: 'map desc', tools: 'Read', body: 'Map body.' },
278
+ { name: 'trailhead-grill', description: 'grill desc', tools: 'Read', body: 'Grill body.' },
276
279
  ];
277
280
 
278
- ok('codexAgentTomlPlan: 7 agentDefs -> 7 writes with only the pinned key carrying a model', (() => {
281
+ ok('codexAgentTomlPlan: 8 agentDefs -> 8 writes with only execute (and fix, which tracks it) carrying a model', (() => {
279
282
  const plan = codexAgentTomlPlan('/c', { execute: 'gpt-5.6-terra' }, STUB_AGENT_DEFS);
280
- if (plan.writes.length !== 7) return false;
283
+ if (plan.writes.length !== 8) return false;
281
284
  const byName = Object.fromEntries(plan.writes.map((w) => [w.name, w]));
282
285
  const execute = byName['trailhead-execute'];
283
286
  if (!execute || execute.path !== '/c/agents/trailhead-execute.toml') return false;
284
287
  if (!execute.content.includes('model = "gpt-5.6-terra"')) return false;
285
- const others = plan.writes.filter((w) => w.name !== 'trailhead-execute');
288
+ const fix = byName['trailhead-fix'];
289
+ if (!fix || !fix.content.includes('model = "gpt-5.6-terra"')) return false;
290
+ const others = plan.writes.filter((w) => w.name !== 'trailhead-execute' && w.name !== 'trailhead-fix');
286
291
  return others.length === 6 && others.every((w) => !/^model = /m.test(w.content));
287
292
  })());
288
293
 
289
- ok('codexAgentTomlPlan: keyless trailhead-fix stays pin-less; trailhead-codebase-map now pins on codebase-map', (() => {
290
- const plan = codexAgentTomlPlan('/c', { fix: 'gpt-5.6-terra', 'codebase-map': 'gpt-5.6-terra' }, STUB_AGENT_DEFS);
294
+ ok('codexAgentTomlPlan: models.codex.grill pins trailhead-grill.toml; unset leaves it pin-less', (() => {
295
+ const pinned = codexAgentTomlPlan('/c', { grill: 'gpt-5.6-sol' }, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-grill');
296
+ const unset = codexAgentTomlPlan('/c', {}, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-grill');
297
+ return pinned && pinned.path === '/c/agents/trailhead-grill.toml' && pinned.content.includes('model = "gpt-5.6-sol"') &&
298
+ unset && !/^model = /m.test(unset.content);
299
+ })());
300
+
301
+ ok('codexAgentTomlPlan: an unset models.codex.grill follows models.codex.plan; a set grill wins', (() => {
302
+ const find = (m) => codexAgentTomlPlan('/c', m, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-grill');
303
+ const follow = find({ plan: { model: 'gpt-5.6-sol', effort: 'high' } });
304
+ const own = find({ plan: 'gpt-5.6-sol', grill: 'gpt-5.6-terra' });
305
+ const none = find({ execute: 'gpt-5.6-sol' });
306
+ const plan = codexAgentTomlPlan('/c', { plan: 'gpt-5.6-sol' }, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-plan');
307
+ return follow.content.includes('model = "gpt-5.6-sol"') && follow.content.includes('model_reasoning_effort = "high"') &&
308
+ own.content.includes('model = "gpt-5.6-terra"') && !/^model = /m.test(none.content) && plan.content.includes('model = "gpt-5.6-sol"');
309
+ })());
310
+
311
+ ok('codexAgentTomlPlan: a literal inherit session is pin-less for every key, and grill follows it', (() => {
312
+ const find = (m, n) => codexAgentTomlPlan('/c', m, STUB_AGENT_DEFS).writes.find((w) => w.name === n);
313
+ const pinned = (w) => /^model = /m.test(w.content);
314
+ const a = { plan: 'inherit session' };
315
+ const b = { plan: 'gpt-5.6-sol', grill: 'inherit session' };
316
+ const c = { execute: ' Inherit Session ', research: { model: 'inherit session', effort: 'high' } };
317
+ return !pinned(find(a, 'trailhead-plan')) && !pinned(find(a, 'trailhead-grill')) &&
318
+ !pinned(find(b, 'trailhead-grill')) && find(b, 'trailhead-plan').content.includes('model = "gpt-5.6-sol"') &&
319
+ !pinned(find(c, 'trailhead-execute')) && !pinned(find(c, 'trailhead-research')) &&
320
+ !find(c, 'trailhead-research').content.includes('model_reasoning_effort');
321
+ })());
322
+
323
+ ok('codexAgentTomlPlan: trailhead-fix carries the execute pin (not a fix key); trailhead-codebase-map pins on codebase-map', (() => {
324
+ const plan = codexAgentTomlPlan('/c', { execute: { model: 'gpt-5.6-sol', effort: 'high' }, fix: 'gpt-5.6-luna', 'codebase-map': 'gpt-5.6-terra' }, STUB_AGENT_DEFS);
291
325
  const byName = Object.fromEntries(plan.writes.map((w) => [w.name, w]));
292
326
  const fix = byName['trailhead-fix'];
293
327
  const map = byName['trailhead-codebase-map'];
294
328
  return fix && map &&
295
- fix.path === '/c/agents/trailhead-fix.toml' && !/^model = /m.test(fix.content) &&
329
+ fix.path === '/c/agents/trailhead-fix.toml' && fix.content.includes('model = "gpt-5.6-sol"') &&
330
+ fix.content.includes('model_reasoning_effort = "high"') && !fix.content.includes('gpt-5.6-luna') &&
296
331
  map.path === '/c/agents/trailhead-codebase-map.toml' && map.content.includes('model = "gpt-5.6-terra"');
297
332
  })());
298
333
 
299
- ok('codexAgentTomlPlan: empty models object -> 7 pin-less writes (not zero)', (() => {
334
+ ok('codexAgentTomlPlan: trailhead-fix is pin-less when execute is unset or inherit session', (() => {
335
+ const find = (m) => codexAgentTomlPlan('/c', m, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-fix');
336
+ return ![{}, { plan: 'gpt-5.6-sol' }, { execute: 'inherit session' }, { execute: { model: ' Inherit Session ', effort: 'high' } }]
337
+ .some((m) => /^model = /m.test(find(m).content) || find(m).content.includes('model_reasoning_effort'));
338
+ })());
339
+
340
+ ok('codexAgentTomlPlan: empty models object -> 8 pin-less writes (not zero)', (() => {
300
341
  const plan = codexAgentTomlPlan('/c', {}, STUB_AGENT_DEFS);
301
- return plan.writes.length === 7 && plan.writes.every((w) => !/^model = /m.test(w.content));
342
+ return plan.writes.length === 8 && plan.writes.every((w) => !/^model = /m.test(w.content));
302
343
  })());
303
344
 
304
- ok('codexAgentTomlPlan: null models -> 7 pin-less writes (not zero)', (() => {
345
+ ok('codexAgentTomlPlan: null models -> 8 pin-less writes (not zero)', (() => {
305
346
  const plan = codexAgentTomlPlan('/c', null, STUB_AGENT_DEFS);
306
- return plan.writes.length === 7 && plan.writes.every((w) => !/^model = /m.test(w.content));
347
+ return plan.writes.length === 8 && plan.writes.every((w) => !/^model = /m.test(w.content));
307
348
  })());
308
349
 
309
350
  ok('codexAgentTomlPlan: object model value on the execute key carries model + model_reasoning_effort', (() => {
@@ -315,7 +356,7 @@ ok('codexAgentTomlPlan: object model value on the execute key carries model + mo
315
356
  ok('codexAgentTomlPlan: a keyed key whose object value has no model stays pin-less', (() => {
316
357
  const plan = codexAgentTomlPlan('/c', { execute: { effort: 'high' } }, STUB_AGENT_DEFS);
317
358
  const execute = plan.writes.find((w) => w.name === 'trailhead-execute');
318
- return plan.writes.length === 7 && !/^model = /m.test(execute.content) && !execute.content.includes('model_reasoning_effort');
359
+ return plan.writes.length === 8 && !/^model = /m.test(execute.content) && !execute.content.includes('model_reasoning_effort');
319
360
  })());
320
361
 
321
362
  ok('codexAgentTomlPlan: description is routed through convertToCodex (host paths rewritten)', (() => {
@@ -470,16 +511,16 @@ ok('convertToCodex keeps CLAUDE_CODE_SESSION_ID intact',
470
511
  const shared = parseSource(fs.readFileSync(path.join(__dirname, '../../scripts/agent-blocks.md'), 'utf8'));
471
512
  const agentsDir = path.join(__dirname, '../../plugins/trailhead/agents');
472
513
  const files = fs.readdirSync(agentsDir).filter((f) => /^trailhead-.*\.md$/.test(f));
473
- ok('projection boundaries: found the eight real agents', files.length === 8);
514
+ ok('projection boundaries: found the nine real agents', files.length === 9);
474
515
  ok('projection boundaries: source has common and implementing blocks', !!shared.common && !!shared.implementing);
475
516
  // #249: every agent's projected TOML carries exactly its family block, whole.
476
517
  const familyOf = {
477
518
  'trailhead-verify': 'reviewing', 'trailhead-code-review': 'reviewing',
478
519
  'trailhead-executor': 'implementing', 'trailhead-fix': 'implementing',
479
520
  'trailhead-debug': 'investigating', 'trailhead-research': 'investigating', 'trailhead-codebase-map': 'investigating',
480
- 'trailhead-plan': 'plan',
521
+ 'trailhead-plan': 'plan', 'trailhead-grill': 'grilling',
481
522
  };
482
- const families = ['reviewing', 'implementing', 'investigating', 'plan'];
523
+ const families = ['reviewing', 'implementing', 'investigating', 'plan', 'grilling'];
483
524
  ok('projection boundaries: source has every family block', families.every((f) => !!shared[f]));
484
525
  for (const f of files) {
485
526
  const name = f.slice(0, -3);
@@ -196,7 +196,7 @@ function degradations(host) {
196
196
 
197
197
  if (descriptor.axes.subagentToolkit === 'none') {
198
198
  entries.push({
199
- from: 'subagent fan-out (research, codebase-map, review, plan, execute)',
199
+ from: 'subagent fan-out (research, codebase-map, review, plan, execute, grill)',
200
200
  to: 'inline sequential execution in the main session',
201
201
  });
202
202
  }
@@ -105,6 +105,12 @@ ok('modelsCollapseNotice(codex, models set) names the set keys', (() => {
105
105
  const msg = modelsCollapseNotice('codex', { models: { plan: 'x', execute: 'y' } });
106
106
  return typeof msg === 'string' && msg.includes('plan') && msg.includes('execute') && msg.includes('Codex');
107
107
  })());
108
+ ok('modelsCollapseNotice(codex, models.grill set) names grill', (() => {
109
+ const msg = modelsCollapseNotice('codex', { models: { grill: 'claude-opus-5-5' } });
110
+ return typeof msg === 'string' && msg.includes('(grill)') && msg.includes('models.codex');
111
+ })());
112
+ ok('modelsCollapseNotice is null once models.codex.grill is set',
113
+ modelsCollapseNotice('codex', { models: { grill: 'claude-opus-5-5', codex: { grill: 'gpt-5.6-sol' } } }) === null);
108
114
  ok('modelsCollapseNotice(codex, undefined config) is null',
109
115
  modelsCollapseNotice('codex') === null);
110
116
  ok('modelsCollapseNotice(claude, models set) is null (Claude honours the keys)',