@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.
- package/README.it.md +57 -10
- package/README.md +57 -10
- package/bin/capture-action.test.js +167 -0
- package/bin/capture-docs.test.js +32 -0
- package/bin/engine-route-docs.test.js +148 -0
- package/bin/grill-model-docs.test.js +155 -0
- package/bin/grilling-holistic-review-docs.test.js +7 -3
- package/bin/lib/codex-projection.js +22 -13
- package/bin/lib/codex-projection.test.js +58 -17
- package/bin/lib/host-descriptor.js +1 -1
- package/bin/lib/host-descriptor.test.js +6 -0
- package/bin/report-status-docs.test.js +10 -2
- package/bin/session-trust-docs.test.js +37 -0
- package/bin/surfaced-scope-docs.test.js +57 -0
- package/bin/trailhead.js +7 -5
- package/bin/trailhead.test.js +53 -11
- package/bin/unblock-docs.test.js +56 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
- package/plugins/trailhead/agents/trailhead-grill.md +41 -0
- package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
- package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
- package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
- package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +84 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +22 -1
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +64 -1
- package/plugins/trailhead/hooks/lib/model-route.js +207 -0
- package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +56 -1
- package/plugins/trailhead/hooks/mod/README.md +16 -7
- package/plugins/trailhead/hooks/mod/map-pane.tsx +72 -16
- package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
- package/plugins/trailhead/hooks/mod/repo-pin.ts +8 -5
- package/plugins/trailhead/hooks/trailhead-check-update.js +17 -1
- package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
- package/plugins/trailhead/skills/_shared/capture-core.md +118 -0
- package/plugins/trailhead/skills/_shared/choices.md +2 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +18 -16
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +8 -4
- package/plugins/trailhead/skills/_shared/substrate.md +1 -1
- package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
- package/plugins/trailhead/skills/_shared/teamwork.md +6 -2
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +5 -2
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/debug.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +26 -1
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/verify.md +3 -1
- package/plugins/trailhead/skills/_shared/techniques.md +6 -4
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +4 -4
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +3 -3
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +11 -9
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +10 -10
- package/plugins/trailhead/templates/trailhead-capture.js +418 -0
- package/plugins/trailhead/templates/trailhead-checkout-find.js +256 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +76 -20
- package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
- package/plugins/trailhead/templates/trailhead-issue-body.js +69 -6
- package/plugins/trailhead/templates/trailhead-report-status.js +57 -9
- package/plugins/trailhead/tests/map-pane-ui.test.ts +122 -37
- package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
- 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
|
-
'
|
|
41
|
-
'
|
|
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('
|
|
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
|
|
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
|
|
195
|
-
// other committed agent (fix) has no pin key
|
|
196
|
-
//
|
|
197
|
-
const
|
|
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
|
|
242
|
-
// review/debug/verify/codebase-map) carries its models.codex.<key> pin when set, else is
|
|
243
|
-
// 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
|
-
|
|
260
|
-
|
|
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
|
-
//
|
|
265
|
-
// sources:
|
|
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) +
|
|
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:
|
|
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 !==
|
|
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
|
|
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:
|
|
290
|
-
const
|
|
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' &&
|
|
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:
|
|
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 ===
|
|
342
|
+
return plan.writes.length === 8 && plan.writes.every((w) => !/^model = /m.test(w.content));
|
|
302
343
|
})());
|
|
303
344
|
|
|
304
|
-
ok('codexAgentTomlPlan: null models ->
|
|
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 ===
|
|
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 ===
|
|
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
|
|
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)',
|