@marcomigozzi/trailhead 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.it.md +8 -5
- package/README.md +8 -5
- package/bin/lib/codex-projection.js +79 -58
- package/bin/lib/codex-projection.test.js +103 -15
- package/bin/trailhead.js +106 -15
- package/bin/trailhead.test.js +76 -11
- package/package.json +2 -2
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +10 -0
- package/plugins/trailhead/agents/trailhead-codebase-map.md +10 -0
- package/plugins/trailhead/agents/trailhead-debug.md +10 -0
- package/plugins/trailhead/agents/trailhead-executor.md +10 -0
- package/plugins/trailhead/agents/trailhead-fix.md +10 -0
- package/plugins/trailhead/agents/trailhead-plan.md +10 -0
- package/plugins/trailhead/agents/trailhead-research.md +10 -0
- package/plugins/trailhead/agents/trailhead-verify.md +10 -0
- package/plugins/trailhead/hooks/hooks.json +4 -0
- package/plugins/trailhead/hooks/trailhead-install-guard.js +176 -0
- package/plugins/trailhead/hooks/trailhead-install-guard.test.js +118 -0
- package/plugins/trailhead/skills/_shared/configuration-reference.md +39 -12
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +4 -2
- package/plugins/trailhead/skills/_shared/substrate.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +2 -2
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/debug.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +6 -3
- package/plugins/trailhead/skills/_shared/techniques/research.md +2 -1
- package/plugins/trailhead/skills/_shared/techniques/verify.md +12 -0
- package/plugins/trailhead/skills/_shared/techniques.md +4 -3
- package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -0
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -1
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +6 -6
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -0
- package/plugins/trailhead/skills/trailhead-manage/references/updating.md +12 -0
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +2 -1
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +2 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +12 -8
package/bin/trailhead.js
CHANGED
|
@@ -172,10 +172,11 @@ function claudePaths(configDir) {
|
|
|
172
172
|
commands: path.join(configDir, 'commands', 'trailhead'),
|
|
173
173
|
hooksDir: path.join(configDir, 'hooks'),
|
|
174
174
|
templates: path.join(configDir, 'trailhead', 'templates'),
|
|
175
|
+
agents: path.join(configDir, 'agents'),
|
|
175
176
|
settings: path.join(configDir, 'settings.json'),
|
|
176
177
|
};
|
|
177
178
|
}
|
|
178
|
-
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'];
|
|
179
180
|
// Runtime libs the hook scripts require (trailhead-commit-guard.js does
|
|
180
181
|
// require('./lib/commit-message-check.js')). Copied by name, never a recursive
|
|
181
182
|
// sweep: the Claude hooks/lib dir is shared with other plugins, so we must not
|
|
@@ -217,6 +218,43 @@ function place(src, dest, symlink) {
|
|
|
217
218
|
else fs.cpSync(src, dest, { recursive: true });
|
|
218
219
|
}
|
|
219
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
|
+
|
|
220
258
|
function addHook(s, event, matcher, command) {
|
|
221
259
|
s.hooks = s.hooks || {};
|
|
222
260
|
s.hooks[event] = s.hooks[event] || [];
|
|
@@ -239,6 +277,9 @@ function uninstallClaude(configDir) {
|
|
|
239
277
|
// layout left that this package no longer ships), never the shared dir itself.
|
|
240
278
|
sweepTrailheadSkills(path.join(configDir, 'skills'));
|
|
241
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);
|
|
242
283
|
HOOK_FILES.forEach((f) => rmrf(path.join(P.hooksDir, f)));
|
|
243
284
|
// Remove only trailhead's own lib files by name: the hooks/lib dir is shared
|
|
244
285
|
// with other plugins, so never rmrf the whole dir.
|
|
@@ -246,6 +287,7 @@ function uninstallClaude(configDir) {
|
|
|
246
287
|
const s = readJSON(P.settings);
|
|
247
288
|
stripHook(s, 'PreToolUse', 'trailhead-commit-guard.js');
|
|
248
289
|
stripHook(s, 'PreToolUse', 'trailhead-secret-guard.js');
|
|
290
|
+
stripHook(s, 'PreToolUse', 'trailhead-install-guard.js');
|
|
249
291
|
stripHook(s, 'PostToolUse', 'trailhead-issue-injection-scanner.js');
|
|
250
292
|
stripHook(s, 'SessionStart', 'trailhead-check-update.js');
|
|
251
293
|
writeJSON(P.settings, s);
|
|
@@ -271,6 +313,14 @@ function installClaude(configDir, { useSymlink }) {
|
|
|
271
313
|
place(path.join(SRC, 'commands'), P.commands, useSymlink);
|
|
272
314
|
place(path.join(SRC, 'templates'), P.templates, useSymlink);
|
|
273
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 });
|
|
274
324
|
// version marker: serve al check-update hook per la versione installata (canale npm)
|
|
275
325
|
const pkgVersion = (readJSON(path.join(SRC, '.claude-plugin', 'plugin.json')).version || '').trim();
|
|
276
326
|
if (pkgVersion) {
|
|
@@ -282,6 +332,7 @@ function installClaude(configDir, { useSymlink }) {
|
|
|
282
332
|
const added = [
|
|
283
333
|
addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-commit-guard.js')),
|
|
284
334
|
addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-secret-guard.js')),
|
|
335
|
+
addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-install-guard.js')),
|
|
285
336
|
addHook(s, 'PostToolUse', 'Bash', hookCmd('trailhead-issue-injection-scanner.js')),
|
|
286
337
|
addHook(s, 'SessionStart', '', hookCmd('trailhead-check-update.js')),
|
|
287
338
|
].some(Boolean);
|
|
@@ -290,6 +341,7 @@ function installClaude(configDir, { useSymlink }) {
|
|
|
290
341
|
console.log(`✓ trailhead installed for Claude Code → ${configDir}`);
|
|
291
342
|
console.log(` skills → ${path.join(configDir, 'skills')}/ (${skillDirs.join(', ')})${useSymlink ? ' (symlink)' : ''}`);
|
|
292
343
|
console.log(` commands → ${P.commands} (/trailhead:*)`);
|
|
344
|
+
console.log(` agents → ${P.agents}/ (${agentFiles.length} agent(s): trailhead-*)${useSymlink ? ' (symlink)' : ''}`);
|
|
293
345
|
console.log(` hooks → ${P.hooksDir}/ (${added ? 'registered' : 'already present'} in settings.json)`);
|
|
294
346
|
console.log(` templates → ${P.templates}`);
|
|
295
347
|
console.log('\nRestart or reload your agent to pick up the commands, then run /trailhead to start.');
|
|
@@ -349,6 +401,39 @@ function readCommandVerbs() {
|
|
|
349
401
|
.sort((a, b) => a.verb.localeCompare(b.verb));
|
|
350
402
|
}
|
|
351
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 8 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
|
+
|
|
352
437
|
// Extract one frontmatter scalar's raw value verbatim (an already-valid YAML
|
|
353
438
|
// scalar re-serialises unchanged). Looks only inside the leading ---...--- block.
|
|
354
439
|
// Returns undefined when the key is absent or its value is empty.
|
|
@@ -464,9 +549,13 @@ function installCodex(configDir, { useSymlink }) {
|
|
|
464
549
|
featureManual = true;
|
|
465
550
|
}
|
|
466
551
|
|
|
467
|
-
// Per-technique
|
|
468
|
-
//
|
|
469
|
-
//
|
|
552
|
+
// Per-technique agent registry: project ALL 8 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/verify/codebase-map)
|
|
556
|
+
// carries its models.codex.<key> pin (merged project + global config) when
|
|
557
|
+
// set, else is pin-less; the 1 keyless agent (fix) is always pin-less.
|
|
558
|
+
// Sweep only trailhead's own files first, so a stale pin from a prior
|
|
470
559
|
// install (or a removed technique) never lingers, without touching the
|
|
471
560
|
// user's other custom-agent TOMLs.
|
|
472
561
|
const codexModels = readCodexModels(configDir);
|
|
@@ -474,16 +563,15 @@ function installCodex(configDir, { useSymlink }) {
|
|
|
474
563
|
for (const f of fs.readdirSync(L.codexAgentsDir)) {
|
|
475
564
|
if (/^trailhead-.*\.toml$/.test(f)) rmrf(path.join(L.codexAgentsDir, f));
|
|
476
565
|
}
|
|
477
|
-
const plan = codexAgentTomlPlan(configDir, codexModels);
|
|
566
|
+
const plan = codexAgentTomlPlan(configDir, codexModels, readAgentDefs());
|
|
478
567
|
for (const w of plan.writes) fs.writeFileSync(w.path, w.content);
|
|
479
568
|
|
|
480
569
|
// Feature flag: Codex gates per-subagent model pinning behind
|
|
481
|
-
// features.multi_agent_v2 = true.
|
|
482
|
-
//
|
|
483
|
-
// adapter's §D runtime detection expects
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
// 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.
|
|
487
575
|
let featureManualV2 = false;
|
|
488
576
|
if (plan.writes.length > 0) {
|
|
489
577
|
const updatedTomlV2 = enableCodexMultiAgentV2Feature(current);
|
|
@@ -500,10 +588,12 @@ function installCodex(configDir, { useSymlink }) {
|
|
|
500
588
|
console.log(` templates → ${L.templatesDir}/`);
|
|
501
589
|
console.log(` verb skills → ${L.skillsRoot}/trailhead-<verb>/ (${verbSkillPlan.dirs.length} discoverability skills: invoke $trailhead-<verb>)`);
|
|
502
590
|
console.log(` hooks → ${L.hooksScriptsDir}/ (registered in hooks.json)`);
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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})`);
|
|
507
597
|
}
|
|
508
598
|
console.log('\nRestart or reload Codex to pick up the skill, then run $trailhead to start.');
|
|
509
599
|
// The trust prompt only fires once the feature flag is on. When we could not
|
|
@@ -545,6 +635,7 @@ function uninstallCodex(configDir) {
|
|
|
545
635
|
const h = readJSON(L.hooksJson);
|
|
546
636
|
stripHook(h, 'PreToolUse', 'trailhead-commit-guard.js');
|
|
547
637
|
stripHook(h, 'PreToolUse', 'trailhead-secret-guard.js');
|
|
638
|
+
stripHook(h, 'PreToolUse', 'trailhead-install-guard.js');
|
|
548
639
|
stripHook(h, 'PostToolUse', 'trailhead-issue-injection-scanner.js');
|
|
549
640
|
stripHook(h, 'SessionStart', 'trailhead-check-update.js');
|
|
550
641
|
writeJSON(L.hooksJson, h);
|
package/bin/trailhead.test.js
CHANGED
|
@@ -131,11 +131,19 @@ ok('codex: skills/trailhead/hooks/lib/commit-message-check.js exists (commit-gua
|
|
|
131
131
|
const codexConfigTomlPath = path.join(codexDir, 'config.toml');
|
|
132
132
|
ok('codex: config.toml exists', fs.existsSync(codexConfigTomlPath));
|
|
133
133
|
ok('codex: config.toml enables hooks feature', fs.readFileSync(codexConfigTomlPath, 'utf8').includes('hooks = true'));
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
ok('codex: config.toml
|
|
138
|
-
|
|
134
|
+
// All 8 agents are always projected uniformly (pinned or pin-less), so
|
|
135
|
+
// multi_agent_v2 is ON even with no models.codex.* set (this install,
|
|
136
|
+
// repoRoot, sets none): the adapter §D registry is always present.
|
|
137
|
+
ok('codex: config.toml enables multi_agent_v2 even with no models.codex.* pins projected',
|
|
138
|
+
fs.readFileSync(codexConfigTomlPath, 'utf8').includes('multi_agent_v2 = true'));
|
|
139
|
+
|
|
140
|
+
const codexAgentsDirPath = path.join(codexDir, 'agents');
|
|
141
|
+
const noPinFixTomlPath = path.join(codexAgentsDirPath, 'trailhead-fix.toml');
|
|
142
|
+
const noPinMapTomlPath = path.join(codexAgentsDirPath, 'trailhead-codebase-map.toml');
|
|
143
|
+
ok('codex: trailhead-fix.toml is projected with no models.codex.* set', fs.existsSync(noPinFixTomlPath));
|
|
144
|
+
ok('codex: trailhead-codebase-map.toml is projected with no models.codex.* set', fs.existsSync(noPinMapTomlPath));
|
|
145
|
+
ok('codex: trailhead-fix.toml is pin-less (no model = line) with no models.codex.* set',
|
|
146
|
+
fs.existsSync(noPinFixTomlPath) && !/^model = /m.test(fs.readFileSync(noPinFixTomlPath, 'utf8')));
|
|
139
147
|
|
|
140
148
|
// --- codex --symlink: link verbatim artifacts (hooks + templates), keep skills projected ---
|
|
141
149
|
const codexSymDir = mktmp();
|
|
@@ -161,12 +169,31 @@ ok('codex copy: hooks are regular files (not symlinks)',
|
|
|
161
169
|
ok('codex copy: templates is a regular dir (not a symlink)',
|
|
162
170
|
!fs.lstatSync(path.join(codexDir, 'skills', 'trailhead', 'templates')).isSymbolicLink());
|
|
163
171
|
|
|
164
|
-
// --- codex agent TOML projection (#38)
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
// --- codex agent TOML projection (#38, #88) -------------------------------------
|
|
173
|
+
// With NO models.codex.* set, a codex install still projects all 8 agents
|
|
174
|
+
// uniformly, pin-less (uniform projection, #88), never zero. Install from an
|
|
175
|
+
// isolated cwd whose .trailhead/config.json sets no models.codex.*, not
|
|
176
|
+
// repoRoot (whose own config now pins models.codex.*, so it would project
|
|
177
|
+
// pinned TOMLs rather than pin-less ones).
|
|
178
|
+
const unsetProjectDir = mktmp();
|
|
179
|
+
fs.mkdirSync(path.join(unsetProjectDir, '.trailhead'), { recursive: true });
|
|
180
|
+
fs.writeFileSync(
|
|
181
|
+
path.join(unsetProjectDir, '.trailhead', 'config.json'),
|
|
182
|
+
JSON.stringify({ models: {} }, null, 2) + '\n'
|
|
183
|
+
);
|
|
184
|
+
const unsetCodexHomeDir = mktmp();
|
|
185
|
+
runInstaller([`--codex`, `--dir=${unsetCodexHomeDir}`], { cwd: unsetProjectDir });
|
|
186
|
+
ok('codex: all 8 trailhead-*.toml exist under agents/ even with models.codex.* unset', (() => {
|
|
187
|
+
const dir = path.join(unsetCodexHomeDir, 'agents');
|
|
188
|
+
if (!fs.existsSync(dir)) return false;
|
|
189
|
+
const tomls = fs.readdirSync(dir).filter((f) => /^trailhead-.*\.toml$/.test(f));
|
|
190
|
+
return tomls.length === 8;
|
|
191
|
+
})());
|
|
192
|
+
ok('codex: every trailhead-*.toml is pin-less when models.codex.* is unset', (() => {
|
|
193
|
+
const dir = path.join(unsetCodexHomeDir, 'agents');
|
|
194
|
+
const tomls = fs.readdirSync(dir).filter((f) => /^trailhead-.*\.toml$/.test(f));
|
|
195
|
+
return tomls.every((f) => !/^model = /m.test(fs.readFileSync(path.join(dir, f), 'utf8')));
|
|
196
|
+
})());
|
|
170
197
|
|
|
171
198
|
// --- codex migration (#46): install over old prompt shims sweeps them --------
|
|
172
199
|
const migCodexDir = mktmp();
|
|
@@ -236,6 +263,20 @@ ok('claude: trailhead-commit-guard.js loads without MODULE_NOT_FOUND', (() => {
|
|
|
236
263
|
})());
|
|
237
264
|
ok('claude: SKILL.md does not start with the codex adapter header', !fs.readFileSync(claudeSkillPath, 'utf8').startsWith('<codex_skill_adapter>'));
|
|
238
265
|
|
|
266
|
+
// --- claude: engine agents registered as user subagents -----------------------
|
|
267
|
+
// Without these under <configDir>/agents/, every technique dispatch fails and
|
|
268
|
+
// falls back to running inline on the session model, so the per-technique
|
|
269
|
+
// config.models.* split silently never applies.
|
|
270
|
+
const claudeAgentsDir = path.join(claudeDir, 'agents');
|
|
271
|
+
const expectedAgents = ['trailhead-plan', 'trailhead-executor', 'trailhead-research', 'trailhead-code-review', 'trailhead-debug', 'trailhead-fix', 'trailhead-codebase-map'];
|
|
272
|
+
for (const a of expectedAgents) {
|
|
273
|
+
ok(`claude: agents/${a}.md placed`, fs.existsSync(path.join(claudeAgentsDir, `${a}.md`)));
|
|
274
|
+
}
|
|
275
|
+
ok('claude: agent file registers its subagent name in frontmatter',
|
|
276
|
+
fs.readFileSync(path.join(claudeAgentsDir, 'trailhead-executor.md'), 'utf8').startsWith('---\nname: trailhead-executor\n'));
|
|
277
|
+
ok('claude: agents are real files on a copy install (not symlinks)',
|
|
278
|
+
!fs.lstatSync(path.join(claudeAgentsDir, 'trailhead-plan.md')).isSymbolicLink());
|
|
279
|
+
|
|
239
280
|
// --- claude --symlink: dev install links hooks live too (not just skills/commands) ---
|
|
240
281
|
const symDir = mktmp();
|
|
241
282
|
runInstaller([`--claude`, `--symlink`, `--dir=${symDir}`]);
|
|
@@ -260,6 +301,11 @@ ok('claude symlink: a symlinked commit-guard still loads its lib (no MODULE_NOT_
|
|
|
260
301
|
runInstaller([`--claude`, `--symlink`, `--dir=${symDir}`]);
|
|
261
302
|
ok('claude symlink: reinstall over existing symlinks succeeds',
|
|
262
303
|
fs.lstatSync(path.join(symDir, 'hooks', 'trailhead-secret-guard.js')).isSymbolicLink());
|
|
304
|
+
ok('claude symlink: agents/trailhead-plan.md is a symlink',
|
|
305
|
+
fs.lstatSync(path.join(symDir, 'agents', 'trailhead-plan.md')).isSymbolicLink());
|
|
306
|
+
ok('claude symlink: agent symlink resolves into the package source',
|
|
307
|
+
fs.realpathSync(path.join(symDir, 'agents', 'trailhead-plan.md')) ===
|
|
308
|
+
fs.realpathSync(path.join(repoRoot, 'plugins', 'trailhead', 'agents', 'trailhead-plan.md')));
|
|
263
309
|
// Default (copy) install keeps hooks as real files, not symlinks.
|
|
264
310
|
ok('claude copy: hooks/trailhead-secret-guard.js is a regular file (not a symlink)',
|
|
265
311
|
!fs.lstatSync(path.join(claudeDir, 'hooks', 'trailhead-secret-guard.js')).isSymbolicLink());
|
|
@@ -283,6 +329,14 @@ ok('claude migration: stale trailhead-legacy cluster swept on reinstall', !fs.ex
|
|
|
283
329
|
ok('claude migration: co-tenant skill preserved (only trailhead names swept)', fs.existsSync(path.join(migClaudeDir, 'skills', 'other-plugin')));
|
|
284
330
|
ok('claude migration: current dispatcher present after reinstall', fs.existsSync(path.join(migClaudeDir, 'skills', 'trailhead', 'SKILL.md')));
|
|
285
331
|
ok('claude migration: current cluster present after reinstall', fs.existsSync(path.join(migClaudeDir, 'skills', 'trailhead-work', 'SKILL.md')));
|
|
332
|
+
// The agents dir gets the same name-only sweep on reinstall: a stale trailhead
|
|
333
|
+
// agent (renamed/removed) is swept, a co-tenant agent left alone.
|
|
334
|
+
fs.writeFileSync(path.join(migClaudeDir, 'agents', 'trailhead-oldagent.md'), '---\nname: trailhead-oldagent\n---\n');
|
|
335
|
+
fs.writeFileSync(path.join(migClaudeDir, 'agents', 'other-plugin-agent.md'), '---\nname: other-plugin-agent\n---\n');
|
|
336
|
+
runInstaller([`--claude`, `--dir=${migClaudeDir}`]);
|
|
337
|
+
ok('claude migration: stale trailhead-oldagent.md swept on reinstall', !fs.existsSync(path.join(migClaudeDir, 'agents', 'trailhead-oldagent.md')));
|
|
338
|
+
ok('claude migration: co-tenant agent preserved on reinstall', fs.existsSync(path.join(migClaudeDir, 'agents', 'other-plugin-agent.md')));
|
|
339
|
+
ok('claude migration: engine agents present after reinstall', fs.existsSync(path.join(migClaudeDir, 'agents', 'trailhead-executor.md')));
|
|
286
340
|
|
|
287
341
|
// Uninstall also sweeps a stale trailhead-owned dir the current package no
|
|
288
342
|
// longer ships (monolith), not just the current engineSkillDirs() set.
|
|
@@ -305,6 +359,17 @@ runInstaller([`--claude`, `--dir=${coTenantDir}`, '--uninstall']);
|
|
|
305
359
|
ok('claude uninstall: trailhead lib removed', !fs.existsSync(path.join(coTenantDir, 'hooks', 'lib', 'commit-message-check.js')));
|
|
306
360
|
ok('claude uninstall: co-tenant lib preserved (no recursive wipe)', fs.existsSync(coTenantLib));
|
|
307
361
|
|
|
362
|
+
// The agents dir is shared too: a co-tenant's own agent survives uninstall,
|
|
363
|
+
// while trailhead's are removed by name.
|
|
364
|
+
const coTenantAgentDir = mktmp();
|
|
365
|
+
runInstaller([`--claude`, `--dir=${coTenantAgentDir}`]);
|
|
366
|
+
const foreignAgent = path.join(coTenantAgentDir, 'agents', 'other-plugin-agent.md');
|
|
367
|
+
fs.writeFileSync(foreignAgent, '---\nname: other-plugin-agent\n---\n');
|
|
368
|
+
ok('claude: engine agent present before uninstall', fs.existsSync(path.join(coTenantAgentDir, 'agents', 'trailhead-plan.md')));
|
|
369
|
+
runInstaller([`--claude`, `--dir=${coTenantAgentDir}`, '--uninstall']);
|
|
370
|
+
ok('claude uninstall: trailhead agents removed by name', !fs.existsSync(path.join(coTenantAgentDir, 'agents', 'trailhead-plan.md')));
|
|
371
|
+
ok('claude uninstall: co-tenant agent preserved (no recursive wipe)', fs.existsSync(foreignAgent));
|
|
372
|
+
|
|
308
373
|
// Uninstall removes every split skill (dispatcher + clusters + _shared) by name.
|
|
309
374
|
const splitDir = mktmp();
|
|
310
375
|
runInstaller([`--claude`, `--dir=${splitDir}`]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marcomigozzi/trailhead",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Start and drive large projects as a map of tickets on GitHub Issues. Map-based onboarding + a discuss→plan→execute→verify engine, all on the Issues. Installs the trailhead skill, commands, and hooks into your agent's config dir.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"trailhead": "bin/trailhead.js"
|
|
@@ -30,5 +30,5 @@
|
|
|
30
30
|
"type": "git",
|
|
31
31
|
"url": "git+https://github.com/ToRvaLDz/trailhead.git"
|
|
32
32
|
},
|
|
33
|
-
"homepage": "https://
|
|
33
|
+
"homepage": "https://trailhead.marcomigozzi.it"
|
|
34
34
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trailhead",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Start and drive large projects as a map of tickets on GitHub Issues, resolving one at a time. Map-based onboarding + a discuss→plan→execute→verify engine with atomic commits, all on the Issues. Ships as a dispatcher plus cohesion-clustered skills over a shared core; depends on nothing beyond an authenticated gh CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Marco Migozzi",
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trailhead-code-review
|
|
3
|
+
color: purple
|
|
4
|
+
description: "trailhead code-review subagent: adversarially reviews a ticket's diff and reports; never edits or commits."
|
|
5
|
+
tools: Read, Grep, Glob, Bash, WebFetch, WebSearch, TodoWrite
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
trailhead's code-review subagent. Review the ticket's diff adversarially on the Code review technique's axes, verifying each finding before reporting. Do not defer to a convention or a settled-decision memory.
|
|
9
|
+
|
|
10
|
+
The full protocol is injected at spawn from the trailhead skill's single source (`_shared/techniques/code-review.md`); follow that, not any embedded copy. Read-only by policy: it reviews and reports into trailhead's `VERIFY` comment, it must never Edit/Write or commit. Model-agnostic by design: the engine passes the `config.models.review` model as the spawn-time override, so config stays authoritative.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trailhead-codebase-map
|
|
3
|
+
color: yellow
|
|
4
|
+
description: "trailhead codebase-map reader subagent: maps one non-overlapping facet of the repo. Read-only."
|
|
5
|
+
tools: Read, Grep, Glob, Bash, WebFetch, WebSearch, TodoWrite
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
trailhead's codebase-map reader subagent. Map ONE non-overlapping facet of the repo for the distilled `trailhead:codebase` issue; do not read another reader's territory.
|
|
9
|
+
|
|
10
|
+
The full protocol is injected at spawn from the trailhead skill's single source (`_shared/techniques/codebase-map.md`); follow that, not any embedded copy. Read-only by policy: read-only by construction. Model-agnostic by design: the engine passes the session model at spawn (no dedicated model key), so config stays authoritative.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trailhead-debug
|
|
3
|
+
color: red
|
|
4
|
+
description: "trailhead debug subagent: finds a defect's root cause by the scientific method."
|
|
5
|
+
tools: Read, Write, Edit, Grep, Glob, Bash, WebFetch, WebSearch, TodoWrite
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
trailhead's debug subagent. Find the root cause of the ticket's defect by the scientific method: reproduce, localise, falsifiable hypotheses, confirm cause, verify.
|
|
9
|
+
|
|
10
|
+
The full protocol is injected at spawn from the trailhead skill's single source (`_shared/techniques/debug.md`); follow that, not any embedded copy. Full tools by policy: it adds instrumentation and regression tests, so it needs Write. Model-agnostic by design: the engine passes the `config.models.debug` model as the spawn-time override, so config stays authoritative.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trailhead-executor
|
|
3
|
+
color: orange
|
|
4
|
+
description: "trailhead execute subagent: implements an approved PLAN with atomic conventional commits (each carrying a Refs: #<n> trailer)."
|
|
5
|
+
tools: Read, Write, Edit, Grep, Glob, Bash, WebFetch, WebSearch, TodoWrite
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
trailhead's execute subagent. Implement the approved PLAN for a build ticket with atomic conventional commits, each carrying a `Refs: #<n>` trailer for the ticket, following the repo's `trailhead:conventions`.
|
|
9
|
+
|
|
10
|
+
The full protocol is injected at spawn from the trailhead skill's single source (the Execute step in `trailhead-work/references/ticket-engines.md`); follow that, not any embedded copy. Full tools by policy: it can Edit/Write/commit. Model-agnostic by design: the engine passes the `config.models.execute` model as the spawn-time override, so config stays authoritative.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trailhead-fix
|
|
3
|
+
color: green
|
|
4
|
+
description: "trailhead fix subagent: implements a bug fix with atomic commits (each carrying a Refs: #<n> trailer)."
|
|
5
|
+
tools: Read, Write, Edit, Grep, Glob, Bash, WebFetch, WebSearch, TodoWrite
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
trailhead's fix subagent. Implement the correction for a bug ticket with atomic conventional commits, each carrying a `Refs: #<n>` trailer for the ticket, following the repo's `trailhead:conventions`; where sensible, a test that fails before and passes after.
|
|
9
|
+
|
|
10
|
+
The full protocol is injected at spawn from the trailhead skill's single source (the Fix step in `trailhead-work/references/ticket-engines.md`); follow that, not any embedded copy. Full tools by policy: it can Edit/Write/commit. Model-agnostic by design: the engine passes the `config.models.execute` model as the spawn-time override (fix tracks the execute key), so config stays authoritative.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trailhead-plan
|
|
3
|
+
color: blue
|
|
4
|
+
description: "trailhead planning subagent: produces a build ticket's PLAN (steps, seams, files touched, verification criteria). Read-only, never implements."
|
|
5
|
+
tools: Read, Grep, Glob, Bash, WebFetch, WebSearch, TodoWrite
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
trailhead's planning subagent. Produce the PLAN for a build ticket: the steps, the seams, the files touched, and the verification criteria, applying TDD per the ticket's config.
|
|
9
|
+
|
|
10
|
+
The full protocol is injected at spawn from the trailhead skill's single source (the Plan step in `trailhead-work/references/ticket-engines.md`); follow that, not any embedded copy. Read-only by policy: plan, do not implement or commit. Model-agnostic by design: the engine passes the `config.models.plan` model as the spawn-time override, so config stays authoritative.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trailhead-research
|
|
3
|
+
color: cyan
|
|
4
|
+
description: "trailhead research subagent: gathers a decision-ready fact from primary sources."
|
|
5
|
+
tools: Read, Write, Edit, Grep, Glob, Bash, WebFetch, WebSearch, TodoWrite
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
trailhead's research subagent. Gather the decision-ready fact a ticket waits on from primary sources, recording findings with citations to a throwaway `research/<name>` branch; change no product code beyond the research artifact.
|
|
9
|
+
|
|
10
|
+
The full protocol is injected at spawn from the trailhead skill's single source (`_shared/techniques/research.md`); follow that, not any embedded copy. Full tools by policy: it needs Write for the research artifact. Model-agnostic by design: the engine passes the `config.models.research` model as the spawn-time override, so config stays authoritative.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trailhead-verify
|
|
3
|
+
color: pink
|
|
4
|
+
description: "trailhead verify subagent: goal-backward check that a ticket's change delivers what it promised. Read-only, never edits or commits."
|
|
5
|
+
tools: Read, Grep, Glob, Bash, WebFetch, WebSearch, TodoWrite
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
trailhead's verification subagent. Run the Goal-backward verification: check that the ticket's implemented change actually delivers what the ticket promised (its `Question` and the `PLAN`'s verification criteria), not merely that steps ran or the tests are green. Report the verdict into trailhead's `VERIFY` comment with a leading `status:` tag.
|
|
9
|
+
|
|
10
|
+
The full protocol is injected at spawn from the trailhead skill's single source (`_shared/techniques/verify.md`); follow that, not any embedded copy. Read-only by policy: it verifies and reports, it must never Edit/Write or commit. Model-agnostic by design: the engine passes the `config.models.verify` model as the spawn-time override, so config stays authoritative.
|