@marcomigozzi/trailhead 0.16.0 → 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 +67 -18
- package/README.md +67 -18
- package/bin/capture-action.test.js +167 -0
- package/bin/capture-docs.test.js +32 -0
- package/bin/config-dir-templates-docs.test.js +52 -0
- package/bin/decision-builds-docs.test.js +75 -0
- package/bin/engine-route-docs.test.js +148 -0
- package/bin/fog-vs-ticket-docs.test.js +56 -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 +30 -14
- package/bin/lib/codex-projection.test.js +81 -17
- package/bin/lib/host-descriptor.js +15 -2
- package/bin/lib/host-descriptor.test.js +24 -0
- package/bin/lib/runtime-command-literals.test.js +184 -0
- package/bin/report-status-docs.test.js +11 -3
- 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-code-review.md +2 -2
- package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
- package/plugins/trailhead/agents/trailhead-grill.md +41 -0
- package/plugins/trailhead/agents/trailhead-verify.md +2 -2
- 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 +72 -22
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +113 -0
- 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 +124 -2
- package/plugins/trailhead/hooks/lib/shell-scan.js +25 -0
- package/plugins/trailhead/hooks/lib/statusline-context.test.js +22 -1
- package/plugins/trailhead/hooks/lib/unmodelled.js +6 -1
- package/plugins/trailhead/hooks/mod/README.md +24 -10
- package/plugins/trailhead/hooks/mod/map-pane.tsx +124 -44
- 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/hooks/trailhead-secret-read-guard.js +40 -1
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +68 -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 +20 -18
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +2 -2
- 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 +9 -5
- 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 +4 -2
- package/plugins/trailhead/skills/_shared/techniques.md +7 -5
- 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 +5 -5
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +3 -3
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +5 -4
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +13 -11
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +11 -10
- package/plugins/trailhead/templates/statusline-trailhead.sh +3 -3
- 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 +123 -11
- package/plugins/trailhead/tests/map-pane-ui.test.ts +299 -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,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');
|
|
@@ -69,7 +69,14 @@ function convertToCodex(md) {
|
|
|
69
69
|
// 2. Claude's /clear -> Codex's /new.
|
|
70
70
|
out = out.replace(/\/clear\b/g, '/new');
|
|
71
71
|
|
|
72
|
-
//
|
|
72
|
+
// 3a. Espansione della config dir: CLAUDE_CONFIG_DIR -> CODEX_HOME per intero,
|
|
73
|
+
// prima dei path nudi (altrimenti resta CLAUDE_CONFIG_DIR con fallback ~/.codex).
|
|
74
|
+
out = out.replace(/\$\{CLAUDE_CONFIG_DIR:-(~|\$HOME)\/\.claude\}/g, '${CODEX_HOME:-$1/.codex}');
|
|
75
|
+
// Forme nude rimaste in prosa: $CLAUDE_CONFIG_DIR e ${CLAUDE_CONFIG_DIR} -> CODEX_HOME.
|
|
76
|
+
out = out.replace(/\$\{CLAUDE_CONFIG_DIR\}/g, '${CODEX_HOME}');
|
|
77
|
+
out = out.replace(/\$CLAUDE_CONFIG_DIR\b/g, '$CODEX_HOME');
|
|
78
|
+
|
|
79
|
+
// 3b. Paths: anchored forms first, then the bare form.
|
|
73
80
|
out = out.replace(/\$HOME\/\.claude\b/g, '$HOME/.codex');
|
|
74
81
|
out = out.replace(/~\/\.claude\b/g, '~/.codex');
|
|
75
82
|
out = out.replace(/(?<![A-Za-z0-9_\-.\/~$])\.claude\//g, '.codex/');
|
|
@@ -101,11 +108,11 @@ Where the engine uses \`AskUserQuestion\`, pose the choice as a **plain-text num
|
|
|
101
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.
|
|
102
109
|
|
|
103
110
|
## D. Subagents (native multi_agent)
|
|
104
|
-
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.
|
|
105
112
|
|
|
106
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.
|
|
107
114
|
|
|
108
|
-
**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.
|
|
109
116
|
|
|
110
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.
|
|
111
118
|
|
|
@@ -184,10 +191,11 @@ function codexTechniqueKey(name) {
|
|
|
184
191
|
}
|
|
185
192
|
|
|
186
193
|
// --- MODEL_KEYS --------------------------------------------------------------
|
|
187
|
-
// The
|
|
188
|
-
// other committed agent (fix) has no pin key
|
|
189
|
-
//
|
|
190
|
-
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']);
|
|
191
199
|
|
|
192
200
|
// --- codexAgentToml ---------------------------------------------------------
|
|
193
201
|
// Render one Codex custom-agent TOML. When `model` is empty/absent, the TOML
|
|
@@ -231,9 +239,10 @@ function codexAgentToml({ name, description, developerInstructions, model, effor
|
|
|
231
239
|
// readAgentDefs(): every committed plugins/trailhead/agents/trailhead-*.md,
|
|
232
240
|
// each { name, description, tools, body }), return the set of
|
|
233
241
|
// trailhead-<technique>.toml writes the installer should make: ONE per agent,
|
|
234
|
-
// uniformly (all
|
|
235
|
-
// review/debug/verify/codebase-map) carries its models.codex.<key> pin when set, else is
|
|
236
|
-
// 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.
|
|
237
246
|
// Iterates agentDefs in the given order (readAgentDefs sorts by name) so the
|
|
238
247
|
// write order is deterministic.
|
|
239
248
|
function codexAgentTomlPlan(codexHome, codexModels, agentDefs) {
|
|
@@ -249,13 +258,19 @@ function codexAgentTomlPlan(codexHome, codexModels, agentDefs) {
|
|
|
249
258
|
|
|
250
259
|
let model = null;
|
|
251
260
|
let effort = null;
|
|
252
|
-
|
|
253
|
-
|
|
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;
|
|
254
269
|
if (typeof raw === 'string') {
|
|
255
|
-
if (raw.trim() !== '') model = raw;
|
|
270
|
+
if (raw.trim() !== '' && !isInherit(raw)) model = raw;
|
|
256
271
|
} else if (typeof raw === 'object') {
|
|
257
272
|
const m = typeof raw.model === 'string' ? raw.model : '';
|
|
258
|
-
if (m.trim() !== '') {
|
|
273
|
+
if (m.trim() !== '' && !isInherit(m)) {
|
|
259
274
|
model = m;
|
|
260
275
|
effort = typeof raw.effort === 'string' ? raw.effort : null;
|
|
261
276
|
}
|
|
@@ -450,6 +465,7 @@ module.exports = {
|
|
|
450
465
|
codexClusterAgentsYaml,
|
|
451
466
|
codexAgentToml,
|
|
452
467
|
codexAgentTomlPlan,
|
|
468
|
+
MODEL_KEYS,
|
|
453
469
|
codexVerbSkillContent,
|
|
454
470
|
codexVerbSkillAgentsYaml,
|
|
455
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)', (() => {
|
|
@@ -441,22 +482,45 @@ ok('convertToCodex keeps CLAUDE_CODE_SESSION_ID intact',
|
|
|
441
482
|
autoRef.includes('--auto true --auto-done <n>'));
|
|
442
483
|
}
|
|
443
484
|
|
|
485
|
+
// #319: l'espansione CLAUDE_CONFIG_DIR diventa CODEX_HOME, mai un ibrido che punta alla config di Claude.
|
|
486
|
+
{
|
|
487
|
+
const a = convertToCodex('${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/x');
|
|
488
|
+
const b = convertToCodex('${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hooks/y');
|
|
489
|
+
ok('convertToCodex maps the ~ config-dir expansion to CODEX_HOME', a === '${CODEX_HOME:-~/.codex}/trailhead/templates/x');
|
|
490
|
+
ok('convertToCodex maps the $HOME config-dir expansion to CODEX_HOME', b === '${CODEX_HOME:-$HOME/.codex}/hooks/y');
|
|
491
|
+
ok('convertToCodex leaves no CLAUDE_CONFIG_DIR behind', !/CLAUDE_CONFIG_DIR/.test(a + b));
|
|
492
|
+
const c = convertToCodex('config dir = `$CLAUDE_CONFIG_DIR` or `~/.claude`; `${CLAUDE_CONFIG_DIR}/x`');
|
|
493
|
+
ok('convertToCodex maps the bare config-dir forms to CODEX_HOME',
|
|
494
|
+
c === 'config dir = `$CODEX_HOME` or `~/.codex`; `${CODEX_HOME}/x`');
|
|
495
|
+
ok('convertToCodex leaves a longer variable name untouched',
|
|
496
|
+
convertToCodex('$CLAUDE_CONFIG_DIRX') === '$CLAUDE_CONFIG_DIRX');
|
|
497
|
+
|
|
498
|
+
// Ogni .md reale proiettato non deve contenere CLAUDE_CONFIG_DIR.
|
|
499
|
+
const root = path.join(__dirname, '../../plugins/trailhead');
|
|
500
|
+
const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap((e) =>
|
|
501
|
+
e.isDirectory() ? walk(path.join(d, e.name)) : e.name.endsWith('.md') ? [path.join(d, e.name)] : []);
|
|
502
|
+
const files = ['skills', 'agents', 'commands'].flatMap((n) => walk(path.join(root, n)));
|
|
503
|
+
const leaks = files.filter((f) => /CLAUDE_CONFIG_DIR/.test(convertToCodex(fs.readFileSync(f, 'utf8'))));
|
|
504
|
+
ok('real plugin .md files were found', files.length > 0);
|
|
505
|
+
ok('no projected plugin .md keeps CLAUDE_CONFIG_DIR: ' + leaks.join(', '), leaks.length === 0);
|
|
506
|
+
}
|
|
507
|
+
|
|
444
508
|
// #248: the shared boundary blocks survive the Codex projection of every real agent.
|
|
445
509
|
{
|
|
446
510
|
const { parseSource } = require('../../scripts/sync-agent-blocks.js');
|
|
447
511
|
const shared = parseSource(fs.readFileSync(path.join(__dirname, '../../scripts/agent-blocks.md'), 'utf8'));
|
|
448
512
|
const agentsDir = path.join(__dirname, '../../plugins/trailhead/agents');
|
|
449
513
|
const files = fs.readdirSync(agentsDir).filter((f) => /^trailhead-.*\.md$/.test(f));
|
|
450
|
-
ok('projection boundaries: found the
|
|
514
|
+
ok('projection boundaries: found the nine real agents', files.length === 9);
|
|
451
515
|
ok('projection boundaries: source has common and implementing blocks', !!shared.common && !!shared.implementing);
|
|
452
516
|
// #249: every agent's projected TOML carries exactly its family block, whole.
|
|
453
517
|
const familyOf = {
|
|
454
518
|
'trailhead-verify': 'reviewing', 'trailhead-code-review': 'reviewing',
|
|
455
519
|
'trailhead-executor': 'implementing', 'trailhead-fix': 'implementing',
|
|
456
520
|
'trailhead-debug': 'investigating', 'trailhead-research': 'investigating', 'trailhead-codebase-map': 'investigating',
|
|
457
|
-
'trailhead-plan': 'plan',
|
|
521
|
+
'trailhead-plan': 'plan', 'trailhead-grill': 'grilling',
|
|
458
522
|
};
|
|
459
|
-
const families = ['reviewing', 'implementing', 'investigating', 'plan'];
|
|
523
|
+
const families = ['reviewing', 'implementing', 'investigating', 'plan', 'grilling'];
|
|
460
524
|
ok('projection boundaries: source has every family block', families.every((f) => !!shared[f]));
|
|
461
525
|
for (const f of files) {
|
|
462
526
|
const name = f.slice(0, -3);
|
|
@@ -65,6 +65,8 @@ const HOSTS = Object.freeze({
|
|
|
65
65
|
label: 'Claude Code',
|
|
66
66
|
configDirEnv: 'CLAUDE_CONFIG_DIR',
|
|
67
67
|
configDirDefault: '.claude',
|
|
68
|
+
// Prefisso con cui l'host invoca un verbo trailhead (vedi commandForm).
|
|
69
|
+
commandPrefix: '/trailhead:',
|
|
68
70
|
axes: Object.freeze({
|
|
69
71
|
commandSurface: 'slash-file',
|
|
70
72
|
subagentToolkit: 'full',
|
|
@@ -93,6 +95,7 @@ const HOSTS = Object.freeze({
|
|
|
93
95
|
label: 'Codex',
|
|
94
96
|
configDirEnv: 'CODEX_HOME',
|
|
95
97
|
configDirDefault: '.codex',
|
|
98
|
+
commandPrefix: '$trailhead ',
|
|
96
99
|
axes: Object.freeze({
|
|
97
100
|
commandSurface: 'skill',
|
|
98
101
|
subagentToolkit: 'full',
|
|
@@ -168,6 +171,15 @@ function hyphenateCommand(name) {
|
|
|
168
171
|
return String(name).trim().replace(/^\//, '').replace(/:/g, '-');
|
|
169
172
|
}
|
|
170
173
|
|
|
174
|
+
// --- Helper: commandForm ------------------------------------------------------
|
|
175
|
+
// Forma di invocazione di un verbo per l'host: la scrittura da usare in ogni
|
|
176
|
+
// testo runtime, mai un literal `/trailhead:<verb>` hardcoded.
|
|
177
|
+
// commandForm('claude', 'update') -> '/trailhead:update'
|
|
178
|
+
// commandForm('codex', 'update') -> '$trailhead update'
|
|
179
|
+
function commandForm(host, verb) {
|
|
180
|
+
return `${resolveHost(host).commandPrefix}${verb}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
171
183
|
// --- Helper: emitsAgentToml ---------------------------------------------------
|
|
172
184
|
function emitsAgentToml(host) {
|
|
173
185
|
return resolveHost(host).emitsAgentToml;
|
|
@@ -184,7 +196,7 @@ function degradations(host) {
|
|
|
184
196
|
|
|
185
197
|
if (descriptor.axes.subagentToolkit === 'none') {
|
|
186
198
|
entries.push({
|
|
187
|
-
from: 'subagent fan-out (research, codebase-map, review, plan, execute)',
|
|
199
|
+
from: 'subagent fan-out (research, codebase-map, review, plan, execute, grill)',
|
|
188
200
|
to: 'inline sequential execution in the main session',
|
|
189
201
|
});
|
|
190
202
|
}
|
|
@@ -275,7 +287,7 @@ function updateNotice(host, cache) {
|
|
|
275
287
|
const installed = String(cache.installed || '').trim();
|
|
276
288
|
const ver = latest ? ` ${latest}` : '';
|
|
277
289
|
const from = installed ? ` (you have ${installed})` : '';
|
|
278
|
-
return `Heads up: trailhead${ver} is available${from}. Run
|
|
290
|
+
return `Heads up: trailhead${ver} is available${from}. Run ${commandForm(host, 'update')} to install it.`;
|
|
279
291
|
}
|
|
280
292
|
|
|
281
293
|
// --- Codex version floor gate (decision #27) --------------------------------
|
|
@@ -390,6 +402,7 @@ module.exports = {
|
|
|
390
402
|
getHost,
|
|
391
403
|
configDirFor,
|
|
392
404
|
hyphenateCommand,
|
|
405
|
+
commandForm,
|
|
393
406
|
emitsAgentToml,
|
|
394
407
|
degradations,
|
|
395
408
|
modelsCollapseNotice,
|
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
getHost,
|
|
9
9
|
configDirFor,
|
|
10
10
|
hyphenateCommand,
|
|
11
|
+
commandForm,
|
|
11
12
|
emitsAgentToml,
|
|
12
13
|
degradations,
|
|
13
14
|
modelsCollapseNotice,
|
|
@@ -104,6 +105,12 @@ ok('modelsCollapseNotice(codex, models set) names the set keys', (() => {
|
|
|
104
105
|
const msg = modelsCollapseNotice('codex', { models: { plan: 'x', execute: 'y' } });
|
|
105
106
|
return typeof msg === 'string' && msg.includes('plan') && msg.includes('execute') && msg.includes('Codex');
|
|
106
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);
|
|
107
114
|
ok('modelsCollapseNotice(codex, undefined config) is null',
|
|
108
115
|
modelsCollapseNotice('codex') === null);
|
|
109
116
|
ok('modelsCollapseNotice(claude, models set) is null (Claude honours the keys)',
|
|
@@ -127,6 +134,23 @@ ok('modelsCollapseNotice: codex namespace with only empty/nullish values does no
|
|
|
127
134
|
return typeof msg === 'string' && msg.includes('plan') && !msg.includes('codex)');
|
|
128
135
|
})());
|
|
129
136
|
|
|
137
|
+
// --- commandForm ---
|
|
138
|
+
ok('commandForm(claude, update) is the slash form', commandForm('claude', 'update') === '/trailhead:update');
|
|
139
|
+
ok('commandForm(codex, update) is the dollar form', commandForm('codex', 'update') === '$trailhead update');
|
|
140
|
+
ok('commandForm throws on an unknown host', (() => { try { commandForm('nope', 'x'); return false; } catch { return true; } })());
|
|
141
|
+
|
|
142
|
+
// Drift guard: la lib degli hook ha la propria copia di commandForm (non puo'
|
|
143
|
+
// richiedere bin/ una volta installata); le due devono restare identiche.
|
|
144
|
+
{
|
|
145
|
+
const lib = require('../../plugins/trailhead/hooks/lib/model-defaults-review.js');
|
|
146
|
+
for (const host of ['claude', 'codex']) {
|
|
147
|
+
for (const verb of ['config', 'update']) {
|
|
148
|
+
ok(`commandForm drift: hook lib matches host-descriptor (${host}, ${verb})`,
|
|
149
|
+
lib.commandForm(host, verb) === commandForm(host, verb));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
130
154
|
// --- updateNotice ---
|
|
131
155
|
// Both hosts now have a hook bus (hookBus !== 'none'), so updateNotice never
|
|
132
156
|
// fires for either: the SessionStart hook + (on Codex) its stdout
|