@marcomigozzi/trailhead 0.4.0 → 0.5.1
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 +22 -15
- package/README.md +23 -16
- package/bin/lib/codex-projection.js +102 -60
- package/bin/lib/codex-projection.test.js +119 -17
- package/bin/trailhead.js +205 -51
- package/bin/trailhead.test.js +195 -15
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +8 -3
- package/plugins/trailhead/agents/trailhead-code-review.md +9 -0
- package/plugins/trailhead/agents/trailhead-codebase-map.md +9 -0
- package/plugins/trailhead/agents/trailhead-debug.md +9 -0
- package/plugins/trailhead/agents/trailhead-executor.md +9 -0
- package/plugins/trailhead/agents/trailhead-fix.md +9 -0
- package/plugins/trailhead/agents/trailhead-plan.md +9 -0
- package/plugins/trailhead/agents/trailhead-research.md +9 -0
- package/plugins/trailhead/commands/adopt.md +1 -1
- package/plugins/trailhead/commands/bug.md +1 -1
- package/plugins/trailhead/commands/config.md +1 -1
- package/plugins/trailhead/commands/dashboard.md +1 -1
- package/plugins/trailhead/commands/grill.md +1 -1
- package/plugins/trailhead/commands/idea.md +1 -1
- package/plugins/trailhead/commands/inbox.md +1 -1
- package/plugins/trailhead/commands/map.md +1 -1
- package/plugins/trailhead/commands/new.md +1 -1
- package/plugins/trailhead/commands/note.md +1 -1
- package/plugins/trailhead/commands/pause.md +1 -1
- package/plugins/trailhead/commands/quick.md +3 -1
- package/plugins/trailhead/commands/resume.md +1 -1
- package/plugins/trailhead/commands/seed.md +1 -1
- package/plugins/trailhead/commands/split.md +1 -1
- package/plugins/trailhead/commands/ticket.md +1 -1
- package/plugins/trailhead/commands/todo.md +1 -1
- package/plugins/trailhead/commands/update.md +1 -1
- package/plugins/trailhead/commands/whiteboard.md +1 -1
- package/plugins/trailhead/commands/work.md +1 -1
- package/plugins/trailhead/hooks/hooks.json +4 -0
- package/plugins/trailhead/hooks/trailhead-commit-guard.js +1 -1
- package/plugins/trailhead/hooks/trailhead-install-guard.js +176 -0
- package/plugins/trailhead/hooks/trailhead-install-guard.test.js +118 -0
- package/plugins/trailhead/hooks/trailhead-secret-guard.js +19 -4
- package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +41 -1
- package/plugins/trailhead/skills/{trailhead/references/configuration.md → _shared/configuration-reference.md} +29 -9
- package/plugins/trailhead/skills/_shared/configuration.md +8 -0
- package/plugins/trailhead/skills/_shared/multi-map.md +27 -0
- package/plugins/trailhead/skills/_shared/out-of-scope.md +11 -0
- package/plugins/trailhead/skills/_shared/principles.md +8 -0
- package/plugins/trailhead/skills/_shared/session-handoff.md +22 -0
- package/plugins/trailhead/skills/_shared/substrate-commands.md +159 -0
- package/plugins/trailhead/skills/_shared/substrate.md +35 -0
- package/plugins/trailhead/skills/{trailhead/references → _shared}/teamwork.md +5 -4
- package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/acceptance-testing.md +3 -2
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +18 -0
- package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/codebase-map.md +1 -1
- package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/debug.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +21 -0
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +39 -0
- package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/research.md +2 -1
- package/plugins/trailhead/skills/_shared/techniques.md +20 -0
- package/plugins/trailhead/skills/_shared/ticket-anatomy.md +32 -0
- package/plugins/trailhead/skills/_shared/ticket-language.md +3 -0
- package/plugins/trailhead/skills/trailhead/SKILL.md +26 -453
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +29 -0
- package/plugins/trailhead/skills/{trailhead → trailhead-capture}/references/capture.md +4 -4
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +52 -0
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +24 -0
- package/plugins/trailhead/skills/{trailhead → trailhead-chart}/references/inbox.md +6 -6
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +30 -0
- package/plugins/trailhead/skills/{trailhead → trailhead-manage}/references/updating.md +12 -0
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +113 -0
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +66 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +52 -0
- package/plugins/trailhead/skills/trailhead/references/techniques/code-review.md +0 -14
- package/plugins/trailhead/skills/trailhead/references/techniques/plan-review.md +0 -24
- package/plugins/trailhead/skills/trailhead/references/techniques/prototype.md +0 -18
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/domain-vocabulary.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/grilling.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/tdd.md +0 -0
|
@@ -81,8 +81,22 @@ ok('codexSkillAdapterHeader §D pins the exact agent_type dispatch call shape',
|
|
|
81
81
|
header.includes('spawn_agent(agent_type="trailhead-<technique>"'));
|
|
82
82
|
ok('codexSkillAdapterHeader §D keeps the base spawn_agent v1 fallback path',
|
|
83
83
|
header.includes('base multi_agent v1') && header.includes('inherit the one session model'));
|
|
84
|
+
ok('codexSkillAdapterHeader §D warns against a single blocking wait_agent with a poll loop',
|
|
85
|
+
header.includes('bounded wait') && header.includes('poll'));
|
|
86
|
+
ok('codexSkillAdapterHeader §D offers a background-job fallback for a stalling review',
|
|
87
|
+
header.includes('background job'));
|
|
88
|
+
ok('codexSkillAdapterHeader §D makes background+poll the hard cross-host default for reviews',
|
|
89
|
+
header.includes('always run this way') && header.includes('hard cross-host default'));
|
|
84
90
|
ok('codexSkillAdapterHeader §F mentions real Codex hooks', header.includes('real Codex hooks'));
|
|
85
91
|
ok('codexSkillAdapterHeader §F no longer says Codex has no hook bus', !header.includes('Codex has no hook bus'));
|
|
92
|
+
ok('codexSkillAdapterHeader §H maps plan-review external-CLI Bash timeout/background onto Codex shell',
|
|
93
|
+
header.includes('## H.') && header.includes('Cross-AI plan review') && header.includes('run_in_background') && header.includes('shell/exec'));
|
|
94
|
+
ok('codexSkillAdapterHeader §H distinguishes the external-CLI shell path from the §D subagent path',
|
|
95
|
+
header.includes('external-CLI') && header.includes('not a spawned Codex subagent'));
|
|
96
|
+
ok('codexSkillAdapterHeader §H makes backgrounding reviewers the default',
|
|
97
|
+
header.includes('the default is to background the reviewers'));
|
|
98
|
+
ok('codexSkillAdapterHeader §H closes reviewer stdin to avoid the EOF hang',
|
|
99
|
+
header.includes('/dev/null') && header.toLowerCase().includes('stdin'));
|
|
86
100
|
|
|
87
101
|
// --- codexVerbSkillContent (#46) ---
|
|
88
102
|
ok('codexVerbSkillContent: SKILL.md frontmatter is at byte 0 with name+description', (() => {
|
|
@@ -129,11 +143,13 @@ ok('codexVerbSkillPlan: null verbs yields nothing', codexVerbSkillPlan('/c', nul
|
|
|
129
143
|
|
|
130
144
|
// --- codexHookEntries ---
|
|
131
145
|
const entries = codexHookEntries('/h');
|
|
132
|
-
ok('codexHookEntries returns
|
|
146
|
+
ok('codexHookEntries returns 5 entries', Array.isArray(entries) && entries.length === 5);
|
|
133
147
|
ok('codexHookEntries: commit-guard is PreToolUse/Bash', entries.some((e) =>
|
|
134
148
|
e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-commit-guard.js') && e.command.includes('/h')));
|
|
135
149
|
ok('codexHookEntries: secret-guard is PreToolUse/Bash', entries.some((e) =>
|
|
136
150
|
e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-secret-guard.js')));
|
|
151
|
+
ok('codexHookEntries: install-guard is PreToolUse/Bash', entries.some((e) =>
|
|
152
|
+
e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-install-guard.js')));
|
|
137
153
|
ok('codexHookEntries: injection-scanner is PostToolUse/Bash', entries.some((e) =>
|
|
138
154
|
e.event === 'PostToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-issue-injection-scanner.js')));
|
|
139
155
|
ok('codexHookEntries: check-update is SessionStart', entries.some((e) =>
|
|
@@ -184,24 +200,110 @@ ok('codexAgentToml without effort omits model_reasoning_effort', (() => {
|
|
|
184
200
|
});
|
|
185
201
|
return out.includes('model = "gpt-5.6-terra"') && !out.includes('model_reasoning_effort');
|
|
186
202
|
})());
|
|
203
|
+
ok('codexAgentToml with no model emits a pin-less TOML (no model line, no effort line, keeps the rest)', (() => {
|
|
204
|
+
const out = codexAgentToml({
|
|
205
|
+
name: 'trailhead-fix',
|
|
206
|
+
description: 'desc',
|
|
207
|
+
developerInstructions: 'fix the thing',
|
|
208
|
+
model: null,
|
|
209
|
+
effort: null,
|
|
210
|
+
});
|
|
211
|
+
return out.includes('name = "trailhead-fix"') &&
|
|
212
|
+
out.includes('description = "desc"') &&
|
|
213
|
+
!/^model = /m.test(out) &&
|
|
214
|
+
!out.includes('model_reasoning_effort') &&
|
|
215
|
+
out.includes("developer_instructions = '''") &&
|
|
216
|
+
out.includes('fix the thing');
|
|
217
|
+
})());
|
|
218
|
+
|
|
219
|
+
ok('codexAgentToml escapes a " and \\ in name/description so the TOML stays valid', (() => {
|
|
220
|
+
const out = codexAgentToml({
|
|
221
|
+
name: 'trailhead-plan',
|
|
222
|
+
description: 'has a "quote" and a back\\slash',
|
|
223
|
+
developerInstructions: 'body',
|
|
224
|
+
model: null,
|
|
225
|
+
effort: null,
|
|
226
|
+
});
|
|
227
|
+
// The emitted basic string must carry escaped forms, never a raw " that
|
|
228
|
+
// would prematurely close the TOML string.
|
|
229
|
+
return out.includes('description = "has a \\"quote\\" and a back\\\\slash"');
|
|
230
|
+
})());
|
|
187
231
|
|
|
188
232
|
// --- codexAgentTomlPlan ---
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
ok('codexAgentTomlPlan:
|
|
204
|
-
|
|
233
|
+
// 7-entry stub agentDefs mirroring the real plugins/trailhead/agents/*.md
|
|
234
|
+
// sources: 5 keyed techniques (plan/execute/research/review/debug, whose
|
|
235
|
+
// filenames derive from trailhead-executor -> execute and
|
|
236
|
+
// trailhead-code-review -> review) + 2 always-keyless agents.
|
|
237
|
+
const STUB_AGENT_DEFS = [
|
|
238
|
+
{ name: 'trailhead-plan', description: 'plan desc', tools: 'Read', body: 'Plan body.' },
|
|
239
|
+
{ name: 'trailhead-executor', description: 'execute desc', tools: 'Read, Write', body: 'Execute body.' },
|
|
240
|
+
{ name: 'trailhead-research', description: 'research desc', tools: 'Read', body: 'Research body.' },
|
|
241
|
+
{ name: 'trailhead-code-review', description: 'review desc', tools: 'Read', body: 'Review body.' },
|
|
242
|
+
{ name: 'trailhead-debug', description: 'debug desc', tools: 'Read', body: 'Debug body.' },
|
|
243
|
+
{ name: 'trailhead-fix', description: 'fix desc', tools: 'Read, Write', body: 'Fix body.' },
|
|
244
|
+
{ name: 'trailhead-codebase-map', description: 'map desc', tools: 'Read', body: 'Map body.' },
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
ok('codexAgentTomlPlan: 7 agentDefs -> 7 writes with only the pinned key carrying a model', (() => {
|
|
248
|
+
const plan = codexAgentTomlPlan('/c', { execute: 'gpt-5.6-terra' }, STUB_AGENT_DEFS);
|
|
249
|
+
if (plan.writes.length !== 7) return false;
|
|
250
|
+
const byName = Object.fromEntries(plan.writes.map((w) => [w.name, w]));
|
|
251
|
+
const execute = byName['trailhead-execute'];
|
|
252
|
+
if (!execute || execute.path !== '/c/agents/trailhead-execute.toml') return false;
|
|
253
|
+
if (!execute.content.includes('model = "gpt-5.6-terra"')) return false;
|
|
254
|
+
const others = plan.writes.filter((w) => w.name !== 'trailhead-execute');
|
|
255
|
+
return others.length === 6 && others.every((w) => !/^model = /m.test(w.content));
|
|
256
|
+
})());
|
|
257
|
+
|
|
258
|
+
ok('codexAgentTomlPlan: keyless trailhead-fix / trailhead-codebase-map are always pin-less', (() => {
|
|
259
|
+
const plan = codexAgentTomlPlan('/c', { fix: 'gpt-5.6-terra', 'codebase-map': 'gpt-5.6-terra' }, STUB_AGENT_DEFS);
|
|
260
|
+
const byName = Object.fromEntries(plan.writes.map((w) => [w.name, w]));
|
|
261
|
+
const fix = byName['trailhead-fix'];
|
|
262
|
+
const map = byName['trailhead-codebase-map'];
|
|
263
|
+
return fix && map &&
|
|
264
|
+
fix.path === '/c/agents/trailhead-fix.toml' && !/^model = /m.test(fix.content) &&
|
|
265
|
+
map.path === '/c/agents/trailhead-codebase-map.toml' && !/^model = /m.test(map.content);
|
|
266
|
+
})());
|
|
267
|
+
|
|
268
|
+
ok('codexAgentTomlPlan: empty models object -> 7 pin-less writes (not zero)', (() => {
|
|
269
|
+
const plan = codexAgentTomlPlan('/c', {}, STUB_AGENT_DEFS);
|
|
270
|
+
return plan.writes.length === 7 && plan.writes.every((w) => !/^model = /m.test(w.content));
|
|
271
|
+
})());
|
|
272
|
+
|
|
273
|
+
ok('codexAgentTomlPlan: null models -> 7 pin-less writes (not zero)', (() => {
|
|
274
|
+
const plan = codexAgentTomlPlan('/c', null, STUB_AGENT_DEFS);
|
|
275
|
+
return plan.writes.length === 7 && plan.writes.every((w) => !/^model = /m.test(w.content));
|
|
276
|
+
})());
|
|
277
|
+
|
|
278
|
+
ok('codexAgentTomlPlan: object model value on the execute key carries model + model_reasoning_effort', (() => {
|
|
279
|
+
const plan = codexAgentTomlPlan('/c', { execute: { model: 'gpt-5.6-sol', effort: 'high' } }, STUB_AGENT_DEFS);
|
|
280
|
+
const execute = plan.writes.find((w) => w.name === 'trailhead-execute');
|
|
281
|
+
return execute.content.includes('model = "gpt-5.6-sol"') && execute.content.includes('model_reasoning_effort = "high"');
|
|
282
|
+
})());
|
|
283
|
+
|
|
284
|
+
ok('codexAgentTomlPlan: a keyed key whose object value has no model stays pin-less', (() => {
|
|
285
|
+
const plan = codexAgentTomlPlan('/c', { execute: { effort: 'high' } }, STUB_AGENT_DEFS);
|
|
286
|
+
const execute = plan.writes.find((w) => w.name === 'trailhead-execute');
|
|
287
|
+
return plan.writes.length === 7 && !/^model = /m.test(execute.content) && !execute.content.includes('model_reasoning_effort');
|
|
288
|
+
})());
|
|
289
|
+
|
|
290
|
+
ok('codexAgentTomlPlan: description is routed through convertToCodex (host paths rewritten)', (() => {
|
|
291
|
+
const defs = [{ name: 'trailhead-plan', description: 'see ~/.claude/skills', tools: 'Read', body: 'b' }];
|
|
292
|
+
const plan = codexAgentTomlPlan('/c', {}, defs);
|
|
293
|
+
return plan.writes[0].content.includes('description = "see ~/.codex/skills"');
|
|
294
|
+
})());
|
|
295
|
+
|
|
296
|
+
ok('codexAgentTomlPlan: a missing description renders as an empty string, not "undefined"', (() => {
|
|
297
|
+
const defs = [{ name: 'trailhead-plan', tools: 'Read', body: 'b' }];
|
|
298
|
+
const plan = codexAgentTomlPlan('/c', {}, defs);
|
|
299
|
+
return plan.writes[0].content.includes('description = ""') && !plan.writes[0].content.includes('description = "undefined"');
|
|
300
|
+
})());
|
|
301
|
+
|
|
302
|
+
ok('codexAgentTomlPlan: empty/undefined agentDefs -> no writes (installer then leaves multi_agent_v2 off)', (() => {
|
|
303
|
+
return codexAgentTomlPlan('/c', {}, []).writes.length === 0 &&
|
|
304
|
+
codexAgentTomlPlan('/c', {}).writes.length === 0 &&
|
|
305
|
+
codexAgentTomlPlan('/c', {}, null).writes.length === 0;
|
|
306
|
+
})());
|
|
205
307
|
|
|
206
308
|
// --- enableCodexMultiAgentV2Feature ---
|
|
207
309
|
ok('enableCodexMultiAgentV2Feature: dotted form already true -> null',
|
package/bin/trailhead.js
CHANGED
|
@@ -23,10 +23,41 @@ const readline = require('readline');
|
|
|
23
23
|
const { spawnSync } = require('child_process');
|
|
24
24
|
|
|
25
25
|
const { getHost, configDirFor, codexVersionGate } = require('./lib/host-descriptor.js');
|
|
26
|
-
const { codexLayout, convertToCodex, injectCodexAdapterHeader, codexAgentsYaml, codexHookEntries, enableCodexHooksFeature, codexAgentTomlPlan, codexVerbSkillPlan, enableCodexMultiAgentV2Feature } = require('./lib/codex-projection.js');
|
|
26
|
+
const { codexLayout, convertToCodex, injectCodexAdapterHeader, codexAgentsYaml, codexClusterAgentsYaml, codexHookEntries, enableCodexHooksFeature, codexAgentTomlPlan, codexVerbSkillPlan, enableCodexMultiAgentV2Feature } = require('./lib/codex-projection.js');
|
|
27
27
|
|
|
28
28
|
const PKG = path.resolve(__dirname, '..');
|
|
29
29
|
const SRC = path.join(PKG, 'plugins', 'trailhead');
|
|
30
|
+
|
|
31
|
+
// Enumerate the engine skill dirs shipped under plugins/trailhead/skills: the
|
|
32
|
+
// dispatcher, the cluster skills, and the shared core. A subdir qualifies when
|
|
33
|
+
// it is `_shared` (the shared core, no SKILL.md) or contains a SKILL.md, so a
|
|
34
|
+
// stray empty dir is skipped and a new cluster is picked up automatically.
|
|
35
|
+
function engineSkillDirs() {
|
|
36
|
+
const skillsRoot = path.join(SRC, 'skills');
|
|
37
|
+
if (!fs.existsSync(skillsRoot)) return [];
|
|
38
|
+
return fs.readdirSync(skillsRoot, { withFileTypes: true })
|
|
39
|
+
.filter((e) => e.isDirectory())
|
|
40
|
+
.map((e) => e.name)
|
|
41
|
+
.filter((name) => name === '_shared' || fs.existsSync(path.join(skillsRoot, name, 'SKILL.md')))
|
|
42
|
+
.sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Sweep every trailhead-owned skill dir under a skills root, by name: the
|
|
46
|
+
// dispatcher (`trailhead`), the shared core (`_shared`), and every
|
|
47
|
+
// `trailhead-<cluster|verb>` sibling. Clears a prior layout's stale dirs (an
|
|
48
|
+
// old monolith fallback, a removed cluster, a renamed verb skill) the current
|
|
49
|
+
// install no longer ships, so a re-install or /trailhead:update never leaves a
|
|
50
|
+
// dangling skill. The skills root is shared with other plugins, so only
|
|
51
|
+
// trailhead's own names are touched, never the whole dir.
|
|
52
|
+
function sweepTrailheadSkills(skillsRoot) {
|
|
53
|
+
rmrf(path.join(skillsRoot, 'trailhead'));
|
|
54
|
+
rmrf(path.join(skillsRoot, '_shared'));
|
|
55
|
+
if (!fs.existsSync(skillsRoot)) return;
|
|
56
|
+
for (const f of fs.readdirSync(skillsRoot)) {
|
|
57
|
+
if (f.startsWith('trailhead-')) rmrf(path.join(skillsRoot, f));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
30
61
|
const args = process.argv.slice(2);
|
|
31
62
|
const has = (f) => args.includes(f);
|
|
32
63
|
const useSymlink = has('--symlink');
|
|
@@ -138,14 +169,14 @@ async function resolveHostName() {
|
|
|
138
169
|
// --- Claude adapter (unchanged behaviour) -----------------------------------
|
|
139
170
|
function claudePaths(configDir) {
|
|
140
171
|
return {
|
|
141
|
-
skill: path.join(configDir, 'skills', 'trailhead'),
|
|
142
172
|
commands: path.join(configDir, 'commands', 'trailhead'),
|
|
143
173
|
hooksDir: path.join(configDir, 'hooks'),
|
|
144
174
|
templates: path.join(configDir, 'trailhead', 'templates'),
|
|
175
|
+
agents: path.join(configDir, 'agents'),
|
|
145
176
|
settings: path.join(configDir, 'settings.json'),
|
|
146
177
|
};
|
|
147
178
|
}
|
|
148
|
-
const HOOK_FILES = ['trailhead-commit-guard.js', 'trailhead-issue-injection-scanner.js', 'trailhead-secret-guard.js', 'trailhead-check-update.js'];
|
|
179
|
+
const HOOK_FILES = ['trailhead-commit-guard.js', 'trailhead-issue-injection-scanner.js', 'trailhead-secret-guard.js', 'trailhead-install-guard.js', 'trailhead-check-update.js'];
|
|
149
180
|
// Runtime libs the hook scripts require (trailhead-commit-guard.js does
|
|
150
181
|
// require('./lib/commit-message-check.js')). Copied by name, never a recursive
|
|
151
182
|
// sweep: the Claude hooks/lib dir is shared with other plugins, so we must not
|
|
@@ -155,18 +186,28 @@ const HOOK_LIB_FILES = ['commit-message-check.js'];
|
|
|
155
186
|
// Copy the hook scripts and their runtime libs into a destination hooks dir
|
|
156
187
|
// (Claude's ~/.claude/hooks or Codex's skills/trailhead/hooks). Shared by both
|
|
157
188
|
// install paths so the lib copy can never drift from the script copy again.
|
|
158
|
-
|
|
189
|
+
// Place the allow-listed hook scripts into destHooksDir. Normal install copies
|
|
190
|
+
// (+chmod so the exec bit is set regardless of the source mode); a --symlink dev
|
|
191
|
+
// install links each file per-file into the package source so hook edits are
|
|
192
|
+
// live too, matching skills/commands. Never symlink the whole hooks/ dir: it is
|
|
193
|
+
// shared with other plugins, so only the curated allowlist is touched. rmrf the
|
|
194
|
+
// dest first so a reinstall over an existing file or symlink never hits EEXIST.
|
|
195
|
+
function copyHookScripts(destHooksDir, { useSymlink = false } = {}) {
|
|
159
196
|
ensure(destHooksDir);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
197
|
+
const put = (src, dest) => {
|
|
198
|
+
rmrf(dest);
|
|
199
|
+
if (useSymlink) {
|
|
200
|
+
fs.symlinkSync(src, dest, 'file');
|
|
201
|
+
} else {
|
|
202
|
+
fs.copyFileSync(src, dest);
|
|
203
|
+
fs.chmodSync(dest, 0o755);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
for (const f of HOOK_FILES) put(path.join(SRC, 'hooks', f), path.join(destHooksDir, f));
|
|
164
207
|
if (HOOK_LIB_FILES.length) {
|
|
165
208
|
const libDest = path.join(destHooksDir, 'lib');
|
|
166
209
|
ensure(libDest);
|
|
167
|
-
for (const f of HOOK_LIB_FILES)
|
|
168
|
-
fs.copyFileSync(path.join(SRC, 'hooks', 'lib', f), path.join(libDest, f));
|
|
169
|
-
}
|
|
210
|
+
for (const f of HOOK_LIB_FILES) put(path.join(SRC, 'hooks', 'lib', f), path.join(libDest, f));
|
|
170
211
|
}
|
|
171
212
|
}
|
|
172
213
|
|
|
@@ -177,6 +218,43 @@ function place(src, dest, symlink) {
|
|
|
177
218
|
else fs.cpSync(src, dest, { recursive: true });
|
|
178
219
|
}
|
|
179
220
|
|
|
221
|
+
// The committed engine agents (plugins/trailhead/agents/trailhead-*.md). On a
|
|
222
|
+
// Claude install they must register as user subagent types under
|
|
223
|
+
// <configDir>/agents/, so the engine can dispatch each technique to its
|
|
224
|
+
// trailhead-<technique> agent and honour config.models.<key>; without them
|
|
225
|
+
// every technique falls back to running inline on the session model (the
|
|
226
|
+
// per-technique model split silently never applies). The agents dir is shared
|
|
227
|
+
// with the user's own agents and other plugins, so touch trailhead's own files
|
|
228
|
+
// by name only, never the whole dir (mirrors the skills/hooks discipline).
|
|
229
|
+
function agentFileNames() {
|
|
230
|
+
const dir = path.join(SRC, 'agents');
|
|
231
|
+
if (!fs.existsSync(dir)) return [];
|
|
232
|
+
return fs.readdirSync(dir).filter((f) => f.startsWith('trailhead-') && f.endsWith('.md'));
|
|
233
|
+
}
|
|
234
|
+
function sweepTrailheadAgents(agentsDir) {
|
|
235
|
+
if (!fs.existsSync(agentsDir)) return;
|
|
236
|
+
for (const f of fs.readdirSync(agentsDir)) {
|
|
237
|
+
if (f.startsWith('trailhead-') && f.endsWith('.md')) rmrf(path.join(agentsDir, f));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Place the engine agents into destAgentsDir by name (copy, or per-file symlink
|
|
241
|
+
// under a --symlink dev install so agent edits are live). Returns the names
|
|
242
|
+
// placed. rmrf each dest first so a reinstall over a file or symlink never
|
|
243
|
+
// hits EEXIST.
|
|
244
|
+
function copyAgentFiles(destAgentsDir, { useSymlink = false } = {}) {
|
|
245
|
+
const names = agentFileNames();
|
|
246
|
+
if (!names.length) return names;
|
|
247
|
+
ensure(destAgentsDir);
|
|
248
|
+
for (const f of names) {
|
|
249
|
+
const src = path.join(SRC, 'agents', f);
|
|
250
|
+
const dest = path.join(destAgentsDir, f);
|
|
251
|
+
rmrf(dest);
|
|
252
|
+
if (useSymlink) fs.symlinkSync(src, dest, 'file');
|
|
253
|
+
else fs.copyFileSync(src, dest);
|
|
254
|
+
}
|
|
255
|
+
return names;
|
|
256
|
+
}
|
|
257
|
+
|
|
180
258
|
function addHook(s, event, matcher, command) {
|
|
181
259
|
s.hooks = s.hooks || {};
|
|
182
260
|
s.hooks[event] = s.hooks[event] || [];
|
|
@@ -195,7 +273,13 @@ function stripHook(s, event, substr) {
|
|
|
195
273
|
|
|
196
274
|
function uninstallClaude(configDir) {
|
|
197
275
|
const P = claudePaths(configDir);
|
|
198
|
-
|
|
276
|
+
// Sweep every trailhead-owned skill dir (current set + any stale dir a prior
|
|
277
|
+
// layout left that this package no longer ships), never the shared dir itself.
|
|
278
|
+
sweepTrailheadSkills(path.join(configDir, 'skills'));
|
|
279
|
+
[P.commands, path.join(configDir, 'trailhead')].forEach(rmrf);
|
|
280
|
+
// Remove trailhead's own agents by name; the agents dir is shared, so never
|
|
281
|
+
// rmrf the whole dir (same discipline as skills/hooks).
|
|
282
|
+
sweepTrailheadAgents(P.agents);
|
|
199
283
|
HOOK_FILES.forEach((f) => rmrf(path.join(P.hooksDir, f)));
|
|
200
284
|
// Remove only trailhead's own lib files by name: the hooks/lib dir is shared
|
|
201
285
|
// with other plugins, so never rmrf the whole dir.
|
|
@@ -203,6 +287,7 @@ function uninstallClaude(configDir) {
|
|
|
203
287
|
const s = readJSON(P.settings);
|
|
204
288
|
stripHook(s, 'PreToolUse', 'trailhead-commit-guard.js');
|
|
205
289
|
stripHook(s, 'PreToolUse', 'trailhead-secret-guard.js');
|
|
290
|
+
stripHook(s, 'PreToolUse', 'trailhead-install-guard.js');
|
|
206
291
|
stripHook(s, 'PostToolUse', 'trailhead-issue-injection-scanner.js');
|
|
207
292
|
stripHook(s, 'SessionStart', 'trailhead-check-update.js');
|
|
208
293
|
writeJSON(P.settings, s);
|
|
@@ -213,10 +298,29 @@ function installClaude(configDir, { useSymlink }) {
|
|
|
213
298
|
const P = claudePaths(configDir);
|
|
214
299
|
const hookCmd = (name) => `node "${path.join(P.hooksDir, name)}"`;
|
|
215
300
|
|
|
216
|
-
|
|
301
|
+
// Place each engine skill dir as a sibling under configDir/skills so the
|
|
302
|
+
// ../_shared/ and ../trailhead-<cluster>/ relative refs resolve. The skills
|
|
303
|
+
// dir is shared with other plugins: place by name, never touch the whole dir.
|
|
304
|
+
// Migration: sweep any stale trailhead-owned skill dirs from a prior layout
|
|
305
|
+
// (an old monolith, a removed cluster) before laying the current set down, so
|
|
306
|
+
// an update over an old install leaves no dangling skill. The skills dir is
|
|
307
|
+
// shared with other plugins: place by name, never touch the whole dir.
|
|
308
|
+
const skillDirs = engineSkillDirs();
|
|
309
|
+
sweepTrailheadSkills(path.join(configDir, 'skills'));
|
|
310
|
+
for (const name of skillDirs) {
|
|
311
|
+
place(path.join(SRC, 'skills', name), path.join(configDir, 'skills', name), useSymlink);
|
|
312
|
+
}
|
|
217
313
|
place(path.join(SRC, 'commands'), P.commands, useSymlink);
|
|
218
314
|
place(path.join(SRC, 'templates'), P.templates, useSymlink);
|
|
219
|
-
copyHookScripts(P.hooksDir);
|
|
315
|
+
copyHookScripts(P.hooksDir, { useSymlink });
|
|
316
|
+
// Register the engine agents as Claude subagents: sweep any stale
|
|
317
|
+
// trailhead-*.md first (a removed/renamed agent), then place the current set
|
|
318
|
+
// by name. This is what makes config.models.<key> take effect on the
|
|
319
|
+
// skills/npm channel; without it every technique runs inline (see
|
|
320
|
+
// agentFileNames above). The agents dir is shared, so only our names are
|
|
321
|
+
// touched.
|
|
322
|
+
sweepTrailheadAgents(P.agents);
|
|
323
|
+
const agentFiles = copyAgentFiles(P.agents, { useSymlink });
|
|
220
324
|
// version marker: serve al check-update hook per la versione installata (canale npm)
|
|
221
325
|
const pkgVersion = (readJSON(path.join(SRC, '.claude-plugin', 'plugin.json')).version || '').trim();
|
|
222
326
|
if (pkgVersion) {
|
|
@@ -228,14 +332,16 @@ function installClaude(configDir, { useSymlink }) {
|
|
|
228
332
|
const added = [
|
|
229
333
|
addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-commit-guard.js')),
|
|
230
334
|
addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-secret-guard.js')),
|
|
335
|
+
addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-install-guard.js')),
|
|
231
336
|
addHook(s, 'PostToolUse', 'Bash', hookCmd('trailhead-issue-injection-scanner.js')),
|
|
232
337
|
addHook(s, 'SessionStart', '', hookCmd('trailhead-check-update.js')),
|
|
233
338
|
].some(Boolean);
|
|
234
339
|
writeJSON(P.settings, s);
|
|
235
340
|
|
|
236
341
|
console.log(`✓ trailhead installed for Claude Code → ${configDir}`);
|
|
237
|
-
console.log(`
|
|
342
|
+
console.log(` skills → ${path.join(configDir, 'skills')}/ (${skillDirs.join(', ')})${useSymlink ? ' (symlink)' : ''}`);
|
|
238
343
|
console.log(` commands → ${P.commands} (/trailhead:*)`);
|
|
344
|
+
console.log(` agents → ${P.agents}/ (${agentFiles.length} agent(s): trailhead-*)${useSymlink ? ' (symlink)' : ''}`);
|
|
239
345
|
console.log(` hooks → ${P.hooksDir}/ (${added ? 'registered' : 'already present'} in settings.json)`);
|
|
240
346
|
console.log(` templates → ${P.templates}`);
|
|
241
347
|
console.log('\nRestart or reload your agent to pick up the commands, then run /trailhead to start.');
|
|
@@ -295,6 +401,39 @@ function readCommandVerbs() {
|
|
|
295
401
|
.sort((a, b) => a.verb.localeCompare(b.verb));
|
|
296
402
|
}
|
|
297
403
|
|
|
404
|
+
// Unquote a YAML double-quoted scalar (unlike commands/*.md, the agent .md
|
|
405
|
+
// frontmatter wraps `description:` in double quotes). Leaves a plain/
|
|
406
|
+
// unquoted scalar untouched, so this is safe to apply unconditionally.
|
|
407
|
+
function unquoteYamlScalar(v) {
|
|
408
|
+
if (typeof v !== 'string' || v.length < 2 || v[0] !== '"' || v[v.length - 1] !== '"') return v;
|
|
409
|
+
return v.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Read the 7 committed agent sources from plugins/trailhead/agents/*.md: each
|
|
413
|
+
// file's basename is the agent's name; frontmatter description/tools ride
|
|
414
|
+
// along, and `body` is the markdown after the closing frontmatter `---`.
|
|
415
|
+
// Single-sources the Codex agent-TOML projection so it never drifts from the
|
|
416
|
+
// committed agent prose. Mirrors readCommandVerbs's shape. Pure fs; kept out
|
|
417
|
+
// of codex-projection.js, which must stay fs-free.
|
|
418
|
+
function readAgentDefs() {
|
|
419
|
+
const dir = path.join(SRC, 'agents');
|
|
420
|
+
if (!fs.existsSync(dir)) return [];
|
|
421
|
+
return fs.readdirSync(dir)
|
|
422
|
+
.filter((f) => f.startsWith('trailhead-') && f.endsWith('.md'))
|
|
423
|
+
.map((f) => {
|
|
424
|
+
const raw = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
425
|
+
const fm = /^---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n?/.exec(raw);
|
|
426
|
+
const body = fm ? raw.slice(fm[0].length) : raw;
|
|
427
|
+
return {
|
|
428
|
+
name: f.slice(0, -3),
|
|
429
|
+
description: unquoteYamlScalar(fmValue(raw, 'description')),
|
|
430
|
+
tools: fmValue(raw, 'tools'),
|
|
431
|
+
body,
|
|
432
|
+
};
|
|
433
|
+
})
|
|
434
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
435
|
+
}
|
|
436
|
+
|
|
298
437
|
// Extract one frontmatter scalar's raw value verbatim (an already-valid YAML
|
|
299
438
|
// scalar re-serialises unchanged). Looks only inside the leading ---...--- block.
|
|
300
439
|
// Returns undefined when the key is absent or its value is empty.
|
|
@@ -333,7 +472,7 @@ function installCodex(configDir, { useSymlink }) {
|
|
|
333
472
|
const L = codexLayout(configDir);
|
|
334
473
|
|
|
335
474
|
if (useSymlink) {
|
|
336
|
-
console.log('Note: symlink
|
|
475
|
+
console.log('Note: on Codex, --symlink links the verbatim artifacts (hooks, templates) into the package source for live edits; the skill files are text-converted at install, so they are written (projected), not linked.');
|
|
337
476
|
}
|
|
338
477
|
|
|
339
478
|
// Migration: sweep the pre-#25 layout AND the #42 prompt shims that never
|
|
@@ -346,31 +485,44 @@ function installCodex(configDir, { useSymlink }) {
|
|
|
346
485
|
}
|
|
347
486
|
rmrf(L.legacyEngineDir);
|
|
348
487
|
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
488
|
+
// Sweep the whole engine + any prior per-verb skills, then project fresh.
|
|
489
|
+
sweepTrailheadSkills(L.skillsRoot);
|
|
490
|
+
|
|
491
|
+
// Project each engine skill dir (dispatcher + 5 clusters + _shared) as a
|
|
492
|
+
// sibling under skills/, converting text and injecting the adapter header into
|
|
493
|
+
// each top-level SKILL.md. The ../_shared/ relative refs survive conversion and
|
|
494
|
+
// resolve as siblings (decision #54). _shared has no SKILL.md, so no header.
|
|
495
|
+
const engineDirs = engineSkillDirs();
|
|
496
|
+
for (const name of engineDirs) {
|
|
497
|
+
copySkillConverted(path.join(SRC, 'skills', name), path.join(L.skillsRoot, name), true);
|
|
498
|
+
}
|
|
353
499
|
|
|
354
500
|
// Templates (verbatim, never converted), bundled inside the skill dir so
|
|
355
501
|
// ${CLAUDE_PLUGIN_ROOT}/templates resolves to ~/.codex/skills/trailhead/templates.
|
|
356
|
-
|
|
357
|
-
fs.cpSync(path.join(SRC, 'templates'), L.templatesDir, { recursive: true });
|
|
502
|
+
place(path.join(SRC, 'templates'), L.templatesDir, useSymlink);
|
|
358
503
|
|
|
359
504
|
// agents/openai.yaml: UI metadata + explicit-only invocation (no auto-trigger).
|
|
360
505
|
ensure(L.agentsDir);
|
|
361
506
|
fs.writeFileSync(L.agentsYaml, codexAgentsYaml());
|
|
507
|
+
// Each cluster skill also gets explicit-only UI metadata (never auto-invoked).
|
|
508
|
+
for (const name of engineDirs) {
|
|
509
|
+
if (name === 'trailhead' || name === '_shared') continue;
|
|
510
|
+
const dir = path.join(L.skillsRoot, name, 'agents');
|
|
511
|
+
ensure(dir);
|
|
512
|
+
fs.writeFileSync(path.join(dir, 'openai.yaml'), codexClusterAgentsYaml(name));
|
|
513
|
+
}
|
|
362
514
|
|
|
363
515
|
// Per-verb discoverability (#46): one thin Codex skill per verb
|
|
364
516
|
// (skills/trailhead-<verb>/, invocable as $trailhead-<verb>), each delegating
|
|
365
517
|
// to $trailhead <verb>. Codex custom prompts (~/.codex/prompts/) never surface
|
|
366
518
|
// as slash commands, so per-verb skills carry the surface, exactly as GSD does.
|
|
367
|
-
//
|
|
368
|
-
//
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}
|
|
373
|
-
const verbSkillPlan = codexVerbSkillPlan(configDir,
|
|
519
|
+
// Verbs come from the canonical commands/*.md so the surfaces never drift.
|
|
520
|
+
// Skip any verb whose skill dir would collide with a projected cluster skill
|
|
521
|
+
// (e.g. verb `work` vs the trailhead-work cluster): the cluster skill is its
|
|
522
|
+
// own $trailhead-<name> entry. Compute the skip-set from the projected dirs.
|
|
523
|
+
const clusterDirNames = new Set(engineDirs);
|
|
524
|
+
const verbList = readCommandVerbs().filter((c) => !clusterDirNames.has(`trailhead-${c.verb}`));
|
|
525
|
+
const verbSkillPlan = codexVerbSkillPlan(configDir, verbList);
|
|
374
526
|
for (const d of verbSkillPlan.dirs) ensure(path.join(d, 'agents'));
|
|
375
527
|
for (const w of verbSkillPlan.writes) fs.writeFileSync(w.path, w.content);
|
|
376
528
|
|
|
@@ -379,7 +531,7 @@ function installCodex(configDir, { useSymlink }) {
|
|
|
379
531
|
|
|
380
532
|
// Hooks: copy the 4 guard scripts into the skill dir and register them in
|
|
381
533
|
// ~/.codex/hooks.json (same shape as Claude's settings.json hooks block).
|
|
382
|
-
copyHookScripts(L.hooksScriptsDir);
|
|
534
|
+
copyHookScripts(L.hooksScriptsDir, { useSymlink });
|
|
383
535
|
const h = readJSON(L.hooksJson);
|
|
384
536
|
for (const e of codexHookEntries(L.hooksScriptsDir)) addHook(h, e.event, e.matcher, e.command);
|
|
385
537
|
writeJSON(L.hooksJson, h);
|
|
@@ -397,9 +549,13 @@ function installCodex(configDir, { useSymlink }) {
|
|
|
397
549
|
featureManual = true;
|
|
398
550
|
}
|
|
399
551
|
|
|
400
|
-
// Per-technique
|
|
401
|
-
//
|
|
402
|
-
//
|
|
552
|
+
// Per-technique agent registry: project ALL 7 committed agents
|
|
553
|
+
// (readAgentDefs, sourced from plugins/trailhead/agents/*.md) as
|
|
554
|
+
// trailhead-<technique>.toml under the real ~/.codex/agents/ dir, uniformly.
|
|
555
|
+
// A keyed technique (plan/execute/research/review/debug) carries its
|
|
556
|
+
// models.codex.<key> pin (merged project + global config) when set, else is
|
|
557
|
+
// pin-less; the 2 keyless agents (fix, codebase-map) are always pin-less.
|
|
558
|
+
// Sweep only trailhead's own files first, so a stale pin from a prior
|
|
403
559
|
// install (or a removed technique) never lingers, without touching the
|
|
404
560
|
// user's other custom-agent TOMLs.
|
|
405
561
|
const codexModels = readCodexModels(configDir);
|
|
@@ -407,16 +563,15 @@ function installCodex(configDir, { useSymlink }) {
|
|
|
407
563
|
for (const f of fs.readdirSync(L.codexAgentsDir)) {
|
|
408
564
|
if (/^trailhead-.*\.toml$/.test(f)) rmrf(path.join(L.codexAgentsDir, f));
|
|
409
565
|
}
|
|
410
|
-
const plan = codexAgentTomlPlan(configDir, codexModels);
|
|
566
|
+
const plan = codexAgentTomlPlan(configDir, codexModels, readAgentDefs());
|
|
411
567
|
for (const w of plan.writes) fs.writeFileSync(w.path, w.content);
|
|
412
568
|
|
|
413
569
|
// Feature flag: Codex gates per-subagent model pinning behind
|
|
414
|
-
// features.multi_agent_v2 = true.
|
|
415
|
-
//
|
|
416
|
-
// adapter's §D runtime detection expects
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
// no trailhead-* agents remain for the runtime to dispatch to.)
|
|
570
|
+
// features.multi_agent_v2 = true. All 7 agent TOMLs are normally projected
|
|
571
|
+
// (pinned or pin-less), so v2 is enabled on every healthy Codex install: the
|
|
572
|
+
// adapter's §D runtime detection expects a trailhead agent_type registry,
|
|
573
|
+
// pins or not. Still guarded on writes so a degenerate case (no agent .md
|
|
574
|
+
// found, zero writes) never flips the flag on with an empty registry.
|
|
420
575
|
let featureManualV2 = false;
|
|
421
576
|
if (plan.writes.length > 0) {
|
|
422
577
|
const updatedTomlV2 = enableCodexMultiAgentV2Feature(current);
|
|
@@ -433,17 +588,19 @@ function installCodex(configDir, { useSymlink }) {
|
|
|
433
588
|
console.log(` templates → ${L.templatesDir}/`);
|
|
434
589
|
console.log(` verb skills → ${L.skillsRoot}/trailhead-<verb>/ (${verbSkillPlan.dirs.length} discoverability skills: invoke $trailhead-<verb>)`);
|
|
435
590
|
console.log(` hooks → ${L.hooksScriptsDir}/ (registered in hooks.json)`);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
591
|
+
{
|
|
592
|
+
const pinned = plan.writes.filter((w) => w.content.includes('\nmodel = '));
|
|
593
|
+
const pinsNote = pinned.length > 0
|
|
594
|
+
? `${pinned.length} pinned: ${pinned.map((w) => w.name + '.toml').join(', ')}`
|
|
595
|
+
: 'no models.codex.* set, all pin-less (session model)';
|
|
596
|
+
console.log(` agents → ${L.codexAgentsDir}/ (${plan.writes.length} agent(s) projected; ${pinsNote})`);
|
|
440
597
|
}
|
|
441
598
|
console.log('\nRestart or reload Codex to pick up the skill, then run $trailhead to start.');
|
|
442
599
|
// The trust prompt only fires once the feature flag is on. When we could not
|
|
443
600
|
// edit config.toml, the hooks stay inert until the user sets it by hand, so
|
|
444
601
|
// don't reassure them about a trust review that won't happen yet.
|
|
445
602
|
if (featureManual) {
|
|
446
|
-
console.log(` ⚠ could not edit ${L.configToml} automatically
|
|
603
|
+
console.log(` ⚠ could not edit ${L.configToml} automatically; add \`features.hooks = true\` under [features] by hand, then Codex will ask you to trust trailhead's hooks on next start.`);
|
|
447
604
|
} else {
|
|
448
605
|
console.log('Note: Codex will ask you to trust trailhead\'s hooks on next start (feature `features.hooks`).');
|
|
449
606
|
}
|
|
@@ -454,12 +611,8 @@ function installCodex(configDir, { useSymlink }) {
|
|
|
454
611
|
|
|
455
612
|
function uninstallCodex(configDir) {
|
|
456
613
|
const L = codexLayout(configDir);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
// never the main skills/trailhead (already removed above; does not match).
|
|
460
|
-
for (const f of (fs.existsSync(L.skillsRoot) ? fs.readdirSync(L.skillsRoot) : [])) {
|
|
461
|
-
if (f.startsWith('trailhead-')) rmrf(path.join(L.skillsRoot, f));
|
|
462
|
-
}
|
|
614
|
+
// Sweep the dispatcher, _shared, the clusters, and the per-verb skills.
|
|
615
|
+
sweepTrailheadSkills(L.skillsRoot);
|
|
463
616
|
// sweep any pre-#25 layout AND the #42 prompt shims from prompts/
|
|
464
617
|
rmrf(path.join(L.promptsDir, 'trailhead.md'));
|
|
465
618
|
for (const f of (fs.existsSync(L.promptsDir) ? fs.readdirSync(L.promptsDir) : [])) {
|
|
@@ -482,6 +635,7 @@ function uninstallCodex(configDir) {
|
|
|
482
635
|
const h = readJSON(L.hooksJson);
|
|
483
636
|
stripHook(h, 'PreToolUse', 'trailhead-commit-guard.js');
|
|
484
637
|
stripHook(h, 'PreToolUse', 'trailhead-secret-guard.js');
|
|
638
|
+
stripHook(h, 'PreToolUse', 'trailhead-install-guard.js');
|
|
485
639
|
stripHook(h, 'PostToolUse', 'trailhead-issue-injection-scanner.js');
|
|
486
640
|
stripHook(h, 'SessionStart', 'trailhead-check-update.js');
|
|
487
641
|
writeJSON(L.hooksJson, h);
|