@marcomigozzi/trailhead 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.it.md +359 -0
  2. package/README.md +35 -22
  3. package/bin/lib/codex-projection.js +421 -0
  4. package/bin/lib/codex-projection.test.js +266 -0
  5. package/bin/lib/host-descriptor.js +404 -0
  6. package/bin/lib/host-descriptor.test.js +172 -0
  7. package/bin/trailhead.js +518 -67
  8. package/bin/trailhead.test.js +399 -0
  9. package/package.json +1 -1
  10. package/plugins/trailhead/.claude-plugin/plugin.json +8 -3
  11. package/plugins/trailhead/commands/adopt.md +1 -1
  12. package/plugins/trailhead/commands/bug.md +1 -1
  13. package/plugins/trailhead/commands/config.md +1 -1
  14. package/plugins/trailhead/commands/dashboard.md +1 -1
  15. package/plugins/trailhead/commands/grill.md +1 -1
  16. package/plugins/trailhead/commands/idea.md +1 -1
  17. package/plugins/trailhead/commands/inbox.md +1 -1
  18. package/plugins/trailhead/commands/map.md +1 -1
  19. package/plugins/trailhead/commands/new.md +1 -1
  20. package/plugins/trailhead/commands/note.md +1 -1
  21. package/plugins/trailhead/commands/pause.md +1 -1
  22. package/plugins/trailhead/commands/quick.md +3 -1
  23. package/plugins/trailhead/commands/resume.md +1 -1
  24. package/plugins/trailhead/commands/seed.md +1 -1
  25. package/plugins/trailhead/commands/split.md +1 -1
  26. package/plugins/trailhead/commands/ticket.md +1 -1
  27. package/plugins/trailhead/commands/todo.md +1 -1
  28. package/plugins/trailhead/commands/update.md +1 -1
  29. package/plugins/trailhead/commands/whiteboard.md +1 -1
  30. package/plugins/trailhead/commands/work.md +1 -1
  31. package/plugins/trailhead/hooks/lib/commit-message-check.js +74 -0
  32. package/plugins/trailhead/hooks/lib/commit-message-check.test.js +83 -0
  33. package/plugins/trailhead/hooks/trailhead-check-update.js +54 -24
  34. package/plugins/trailhead/hooks/trailhead-commit-guard.js +7 -10
  35. package/plugins/trailhead/hooks/trailhead-secret-guard.js +19 -4
  36. package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +41 -1
  37. package/plugins/trailhead/skills/{trailhead/references/configuration.md → _shared/configuration-reference.md} +15 -3
  38. package/plugins/trailhead/skills/_shared/configuration.md +8 -0
  39. package/plugins/trailhead/skills/_shared/multi-map.md +27 -0
  40. package/plugins/trailhead/skills/_shared/out-of-scope.md +11 -0
  41. package/plugins/trailhead/skills/_shared/principles.md +8 -0
  42. package/plugins/trailhead/skills/_shared/session-handoff.md +22 -0
  43. package/plugins/trailhead/skills/_shared/substrate-commands.md +159 -0
  44. package/plugins/trailhead/skills/_shared/substrate.md +35 -0
  45. package/plugins/trailhead/skills/{trailhead/references → _shared}/teamwork.md +8 -5
  46. package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/acceptance-testing.md +3 -2
  47. package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/code-review.md +5 -1
  48. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +21 -0
  49. package/plugins/trailhead/skills/_shared/techniques/prototype.md +36 -0
  50. package/plugins/trailhead/skills/_shared/techniques.md +20 -0
  51. package/plugins/trailhead/skills/_shared/ticket-anatomy.md +32 -0
  52. package/plugins/trailhead/skills/_shared/ticket-language.md +3 -0
  53. package/plugins/trailhead/skills/trailhead/SKILL.md +25 -450
  54. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +28 -0
  55. package/plugins/trailhead/skills/{trailhead → trailhead-capture}/references/capture.md +5 -3
  56. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +51 -0
  57. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +24 -0
  58. package/plugins/trailhead/skills/{trailhead → trailhead-chart}/references/inbox.md +1 -1
  59. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +29 -0
  60. package/plugins/trailhead/skills/trailhead-view/SKILL.md +112 -0
  61. package/plugins/trailhead/skills/trailhead-work/SKILL.md +65 -0
  62. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +52 -0
  63. package/plugins/trailhead/templates/statusline-trailhead.sh +40 -8
  64. package/plugins/trailhead/templates/trailhead-commit-msg +57 -0
  65. package/plugins/trailhead/skills/trailhead/references/techniques/plan-review.md +0 -24
  66. package/plugins/trailhead/skills/trailhead/references/techniques/prototype.md +0 -18
  67. /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/codebase-map.md +0 -0
  68. /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/debug.md +0 -0
  69. /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/domain-vocabulary.md +0 -0
  70. /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/grilling.md +0 -0
  71. /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/research.md +0 -0
  72. /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/tdd.md +0 -0
  73. /package/plugins/trailhead/skills/{trailhead → trailhead-manage}/references/updating.md +0 -0
@@ -0,0 +1,399 @@
1
+ #!/usr/bin/env node
2
+ // Integration tests for trailhead.js (the installer). Run: node trailhead.test.js
3
+ // No framework: plain asserts, mirrors the style of host-descriptor.test.js.
4
+ // Drives the real CLI via child_process against fresh temp dirs, passing an
5
+ // explicit --codex/--claude flag (or a controlled $PATH for the auto-detect
6
+ // cases) plus --dir= so no TTY prompt can fire.
7
+ //
8
+ // Since #25, BOTH codex and claude installs produce <dir>/skills/trailhead/
9
+ // SKILL.md, so that path alone no longer tells them apart. Distinguish by:
10
+ // codex has NO commands/ dir and NO settings.json (still Claude-only), but
11
+ // (since #29) DOES have hooks.json + skills/trailhead/hooks/ + config.toml,
12
+ // and its SKILL.md STARTS WITH <codex_skill_adapter> plus a
13
+ // skills/trailhead/agents/openai.yaml; claude HAS commands/trailhead/,
14
+ // settings.json, hooks/, and its SKILL.md does NOT start with the adapter
15
+ // header.
16
+ const assert = require('assert');
17
+ const fs = require('fs');
18
+ const os = require('os');
19
+ const path = require('path');
20
+ const { execFileSync } = require('child_process');
21
+
22
+ let passed = 0;
23
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
24
+
25
+ const repoRoot = path.resolve(__dirname, '..');
26
+ const installerPath = path.join(repoRoot, 'bin', 'trailhead.js');
27
+ const tmpDirs = [];
28
+
29
+ function mktmp() {
30
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'th-'));
31
+ tmpDirs.push(d);
32
+ return d;
33
+ }
34
+
35
+ // process.execPath (not the bare 'node') so a test can hand a stripped-down
36
+ // $PATH to exercise the auto-detect branch without losing the node binary.
37
+ function runInstaller(extraArgs, opts = {}) {
38
+ const env = opts.env ? { ...process.env, ...opts.env } : process.env;
39
+ const cwd = opts.cwd || repoRoot;
40
+ return execFileSync(process.execPath, [installerPath, ...extraArgs], { cwd, stdio: 'pipe', env });
41
+ }
42
+
43
+ // A dir holding an executable stub for each named host CLI, so detectInstalledHosts
44
+ // finds them when this dir is the whole $PATH.
45
+ function fakeBinDir(names) {
46
+ const dir = mktmp();
47
+ for (const name of names) {
48
+ const p = path.join(dir, name);
49
+ fs.writeFileSync(p, '#!/bin/sh\n');
50
+ fs.chmodSync(p, 0o755);
51
+ }
52
+ return dir;
53
+ }
54
+
55
+ // --- codex install -----------------------------------------------------------
56
+ const codexDir = mktmp();
57
+ runInstaller([`--codex`, `--dir=${codexDir}`]);
58
+
59
+ const skillMainPath = path.join(codexDir, 'skills', 'trailhead', 'SKILL.md');
60
+ ok('codex: SKILL.md exists', fs.existsSync(skillMainPath));
61
+ const skillMainContent = fs.readFileSync(skillMainPath, 'utf8');
62
+ // #40: the YAML frontmatter must stay at the very top (Codex only registers a
63
+ // skill when `name:`/`description:` are the first bytes); the adapter header is
64
+ // injected AFTER the frontmatter, not above it.
65
+ ok('codex: SKILL.md starts with the YAML frontmatter', skillMainContent.startsWith('---\n'));
66
+ ok('codex: SKILL.md frontmatter precedes the adapter header',
67
+ skillMainContent.indexOf('name: trailhead') < skillMainContent.indexOf('<codex_skill_adapter>'));
68
+ ok('codex: SKILL.md still carries the adapter header', skillMainContent.includes('<codex_skill_adapter>'));
69
+ // The adapter header itself (mandated EXACT text) intentionally contrasts the
70
+ // $trailhead form with the old slash-namespace form ("Never /trailhead:<verb>"),
71
+ // so it legitimately contains that substring once. Scope the check to the
72
+ // converted engine body, after the header, which is where the command-surface
73
+ // rewrite actually applies.
74
+ const engineBody = skillMainContent.slice(skillMainContent.indexOf('</codex_skill_adapter>'));
75
+ ok('codex: SKILL.md engine body has no /trailhead: substring', !engineBody.includes('/trailhead:'));
76
+
77
+ ok('codex: _shared/techniques/grilling.md exists', fs.existsSync(path.join(codexDir, 'skills', '_shared', 'techniques', 'grilling.md')));
78
+
79
+ const agentsYamlPath = path.join(codexDir, 'skills', 'trailhead', 'agents', 'openai.yaml');
80
+ ok('codex: agents/openai.yaml exists', fs.existsSync(agentsYamlPath));
81
+ ok('codex: agents/openai.yaml disallows implicit invocation', fs.readFileSync(agentsYamlPath, 'utf8').includes('allow_implicit_invocation: false'));
82
+
83
+ const codexTemplate = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-commit-msg');
84
+ ok('codex: commit-msg template projected', fs.existsSync(codexTemplate));
85
+ ok('codex: commit-msg template is verbatim (not converted)',
86
+ fs.readFileSync(codexTemplate, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg'), 'utf8'));
87
+
88
+ ok('codex: no commands dir', !fs.existsSync(path.join(codexDir, 'commands')));
89
+ ok('codex: no settings.json file', !fs.existsSync(path.join(codexDir, 'settings.json')));
90
+ // #46: per-verb discoverability now projects one Codex SKILL per verb
91
+ // (skills/trailhead-<verb>/), invocable as $trailhead-<verb>. The old
92
+ // ~/.codex/prompts/ shims never surfaced as slash commands on Codex.
93
+ // #46 per-verb discoverability skills. `work` collides with the work CLUSTER
94
+ // dir, so its discoverability is the cluster itself; use a non-colliding verb.
95
+ ok('codex: per-verb skills/trailhead-bug/SKILL.md exists', fs.existsSync(path.join(codexDir, 'skills', 'trailhead-bug', 'SKILL.md')));
96
+ ok('codex: trailhead-bug SKILL.md frontmatter is at byte 0', fs.readFileSync(path.join(codexDir, 'skills', 'trailhead-bug', 'SKILL.md'), 'utf8').startsWith('---\nname: trailhead-bug\n'));
97
+ ok('codex: trailhead-bug skill delegates to $trailhead bug', fs.readFileSync(path.join(codexDir, 'skills', 'trailhead-bug', 'SKILL.md'), 'utf8').includes('$trailhead bug'));
98
+ ok('codex: per-verb skill is explicit-only', fs.readFileSync(path.join(codexDir, 'skills', 'trailhead-bug', 'agents', 'openai.yaml'), 'utf8').includes('allow_implicit_invocation: false'));
99
+ // The trailhead-work dir is the WORK CLUSTER skill (not a thin verb delegator).
100
+ ok('codex: trailhead-work is the cluster skill (frontmatter name at byte 0)', fs.readFileSync(path.join(codexDir, 'skills', 'trailhead-work', 'SKILL.md'), 'utf8').startsWith('---\nname: trailhead-work\n'));
101
+ ok('codex: trailhead-work cluster carries the adapter header', fs.readFileSync(path.join(codexDir, 'skills', 'trailhead-work', 'SKILL.md'), 'utf8').includes('<codex_skill_adapter>'));
102
+ ok('codex: NO thin trailhead-work verb-delegator (cluster occupies the name)', !fs.readFileSync(path.join(codexDir, 'skills', 'trailhead-work', 'SKILL.md'), 'utf8').includes('This is a thin discoverability entry'));
103
+ ok('codex: no ~/.codex/prompts shims left (old mechanism dropped)', !fs.existsSync(path.join(codexDir, 'prompts', 'trailhead.md')) && !fs.existsSync(path.join(codexDir, 'prompts', 'trailhead-work.md')));
104
+
105
+ // The split ships all sibling engine skills so the ../_shared/ relative refs resolve.
106
+ ok('codex: _shared/ projected', fs.existsSync(path.join(codexDir, 'skills', '_shared', 'substrate.md')));
107
+ for (const cl of ['trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage']) {
108
+ ok(`codex: cluster ${cl} SKILL.md projected`, fs.existsSync(path.join(codexDir, 'skills', cl, 'SKILL.md')));
109
+ ok(`codex: cluster ${cl} carries the adapter header`, fs.readFileSync(path.join(codexDir, 'skills', cl, 'SKILL.md'), 'utf8').includes('<codex_skill_adapter>'));
110
+ ok(`codex: cluster ${cl} frontmatter precedes the adapter header`, (() => { const c = fs.readFileSync(path.join(codexDir, 'skills', cl, 'SKILL.md'), 'utf8'); return c.startsWith('---\n') && c.indexOf('name:') < c.indexOf('<codex_skill_adapter>'); })());
111
+ ok(`codex: cluster ${cl} agents/openai.yaml explicit-only`, fs.readFileSync(path.join(codexDir, 'skills', cl, 'agents', 'openai.yaml'), 'utf8').includes('allow_implicit_invocation: false'));
112
+ }
113
+ // A cluster's ../_shared/ reference target actually resolves as a sibling.
114
+ ok('codex: trailhead-work/../_shared/substrate.md resolves', fs.existsSync(path.join(codexDir, 'skills', 'trailhead-work', '..', '_shared', 'substrate.md')));
115
+
116
+ // --- codex hooks (#29) --------------------------------------------------------
117
+ const codexHooksJsonPath = path.join(codexDir, 'hooks.json');
118
+ ok('codex: hooks.json exists', fs.existsSync(codexHooksJsonPath));
119
+ const codexHooksJson = JSON.parse(fs.readFileSync(codexHooksJsonPath, 'utf8'));
120
+ const codexHooksJsonStr = JSON.stringify(codexHooksJson);
121
+ ok('codex: hooks.json registers commit-guard under PreToolUse', codexHooksJsonStr.includes('trailhead-commit-guard.js') &&
122
+ (codexHooksJson.hooks.PreToolUse || []).some((g) => (g.hooks || []).some((h) => h.command.includes('trailhead-commit-guard.js'))));
123
+ ok('codex: hooks.json registers secret-guard under PreToolUse', (codexHooksJson.hooks.PreToolUse || []).some((g) => (g.hooks || []).some((h) => h.command.includes('trailhead-secret-guard.js'))));
124
+ ok('codex: hooks.json registers injection-scanner under PostToolUse', (codexHooksJson.hooks.PostToolUse || []).some((g) => (g.hooks || []).some((h) => h.command.includes('trailhead-issue-injection-scanner.js'))));
125
+ ok('codex: hooks.json registers check-update under SessionStart', (codexHooksJson.hooks.SessionStart || []).some((g) => (g.hooks || []).some((h) => h.command.includes('trailhead-check-update.js'))));
126
+
127
+ ok('codex: skills/trailhead/hooks/trailhead-secret-guard.js exists', fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'trailhead-secret-guard.js')));
128
+ ok('codex: skills/trailhead/hooks/lib/commit-message-check.js exists (commit-guard require target)',
129
+ fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'commit-message-check.js')));
130
+
131
+ const codexConfigTomlPath = path.join(codexDir, 'config.toml');
132
+ ok('codex: config.toml exists', fs.existsSync(codexConfigTomlPath));
133
+ ok('codex: config.toml enables hooks feature', fs.readFileSync(codexConfigTomlPath, 'utf8').includes('hooks = true'));
134
+ // v2 is gated on projected pins: this install (repoRoot, no models.codex.*)
135
+ // projects none, so multi_agent_v2 stays OFF (base multi_agent still fans out;
136
+ // adapter §D expects no trailhead agent_type registry in the no-pins case).
137
+ ok('codex: config.toml does NOT enable multi_agent_v2 when no models.codex.* pins are projected',
138
+ !fs.readFileSync(codexConfigTomlPath, 'utf8').includes('multi_agent_v2 = true'));
139
+
140
+ // --- codex --symlink: link verbatim artifacts (hooks + templates), keep skills projected ---
141
+ const codexSymDir = mktmp();
142
+ runInstaller([`--codex`, `--symlink`, `--dir=${codexSymDir}`]);
143
+ ok('codex symlink: skills/trailhead/hooks/trailhead-secret-guard.js is a symlink',
144
+ fs.lstatSync(path.join(codexSymDir, 'skills', 'trailhead', 'hooks', 'trailhead-secret-guard.js')).isSymbolicLink());
145
+ ok('codex symlink: skills/trailhead/hooks/lib/commit-message-check.js is a symlink',
146
+ fs.lstatSync(path.join(codexSymDir, 'skills', 'trailhead', 'hooks', 'lib', 'commit-message-check.js')).isSymbolicLink());
147
+ ok('codex symlink: skills/trailhead/templates is a symlink',
148
+ fs.lstatSync(path.join(codexSymDir, 'skills', 'trailhead', 'templates')).isSymbolicLink());
149
+ ok('codex symlink: a hook symlink resolves into the package source',
150
+ fs.realpathSync(path.join(codexSymDir, 'skills', 'trailhead', 'hooks', 'trailhead-secret-guard.js')) ===
151
+ fs.realpathSync(path.join(repoRoot, 'plugins', 'trailhead', 'hooks', 'trailhead-secret-guard.js')));
152
+ ok('codex symlink: converted SKILL.md stays a regular file (projected, not linked)',
153
+ !fs.lstatSync(path.join(codexSymDir, 'skills', 'trailhead', 'SKILL.md')).isSymbolicLink());
154
+ // Reinstall over the existing symlink install must not throw (EEXIST guard).
155
+ runInstaller([`--codex`, `--symlink`, `--dir=${codexSymDir}`]);
156
+ ok('codex symlink: reinstall over existing symlinks succeeds',
157
+ fs.lstatSync(path.join(codexSymDir, 'skills', 'trailhead', 'hooks', 'trailhead-secret-guard.js')).isSymbolicLink());
158
+ // Default (copy) codex install keeps hooks/templates as real files, not symlinks.
159
+ ok('codex copy: hooks are regular files (not symlinks)',
160
+ !fs.lstatSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'trailhead-secret-guard.js')).isSymbolicLink());
161
+ ok('codex copy: templates is a regular dir (not a symlink)',
162
+ !fs.lstatSync(path.join(codexDir, 'skills', 'trailhead', 'templates')).isSymbolicLink());
163
+
164
+ // --- codex agent TOML projection (#38) -----------------------------------------
165
+ // repoRoot's own .trailhead/config.json has no models.codex.*, so a plain
166
+ // install (cwd: repoRoot, no models.codex anywhere) projects no agent TOMLs.
167
+ ok('codex: no trailhead-*.toml under agents/ when models.codex.* is unset',
168
+ !fs.existsSync(path.join(codexDir, 'agents')) ||
169
+ !fs.readdirSync(path.join(codexDir, 'agents')).some((f) => /^trailhead-.*\.toml$/.test(f)));
170
+
171
+ // --- codex migration (#46): install over old prompt shims sweeps them --------
172
+ const migCodexDir = mktmp();
173
+ fs.mkdirSync(path.join(migCodexDir, 'prompts'), { recursive: true });
174
+ fs.writeFileSync(path.join(migCodexDir, 'prompts', 'trailhead.md'), 'stale bare shim\n');
175
+ fs.writeFileSync(path.join(migCodexDir, 'prompts', 'trailhead-work.md'), 'stale verb shim\n');
176
+ runInstaller([`--codex`, `--dir=${migCodexDir}`]);
177
+ ok('codex migration: stale prompts/trailhead.md swept on install', !fs.existsSync(path.join(migCodexDir, 'prompts', 'trailhead.md')));
178
+ ok('codex migration: stale prompts/trailhead-work.md swept on install', !fs.existsSync(path.join(migCodexDir, 'prompts', 'trailhead-work.md')));
179
+ ok('codex migration: per-verb skill projected in its place', fs.existsSync(path.join(migCodexDir, 'skills', 'trailhead-work', 'SKILL.md')));
180
+
181
+ // codex migration: a stale trailhead-owned skill dir (pre-split monolith /
182
+ // removed cluster) is swept on reinstall; a co-tenant skill is preserved.
183
+ const migCodexSkillDir = mktmp();
184
+ runInstaller([`--codex`, `--dir=${migCodexSkillDir}`]);
185
+ fs.mkdirSync(path.join(migCodexSkillDir, 'skills', 'trailhead-monolith'), { recursive: true });
186
+ fs.writeFileSync(path.join(migCodexSkillDir, 'skills', 'trailhead-monolith', 'SKILL.md'), 'stale\n');
187
+ fs.mkdirSync(path.join(migCodexSkillDir, 'skills', 'other-plugin'), { recursive: true });
188
+ fs.writeFileSync(path.join(migCodexSkillDir, 'skills', 'other-plugin', 'SKILL.md'), 'co-tenant\n');
189
+ runInstaller([`--codex`, `--dir=${migCodexSkillDir}`]);
190
+ ok('codex migration: stale trailhead-monolith skill dir swept on reinstall', !fs.existsSync(path.join(migCodexSkillDir, 'skills', 'trailhead-monolith')));
191
+ ok('codex migration: co-tenant skill preserved', fs.existsSync(path.join(migCodexSkillDir, 'skills', 'other-plugin')));
192
+
193
+ // --- codex uninstall (same tmp) ----------------------------------------------
194
+ runInstaller([`--codex`, `--dir=${codexDir}`, '--uninstall']);
195
+ ok('codex uninstall: skills/trailhead gone', !fs.existsSync(path.join(codexDir, 'skills', 'trailhead')));
196
+ ok('codex uninstall: legacy trailhead engine dir gone', !fs.existsSync(path.join(codexDir, 'trailhead')));
197
+ ok('codex uninstall: per-verb skills/trailhead-work gone', !fs.existsSync(path.join(codexDir, 'skills', 'trailhead-work')));
198
+ ok('codex uninstall: per-verb skills/trailhead-bug gone', !fs.existsSync(path.join(codexDir, 'skills', 'trailhead-bug')));
199
+ ok('codex uninstall: _shared gone', !fs.existsSync(path.join(codexDir, 'skills', '_shared')));
200
+ ok('codex uninstall: cluster trailhead-view gone', !fs.existsSync(path.join(codexDir, 'skills', 'trailhead-view')));
201
+ ok('codex uninstall: hooks.json no longer contains trailhead commands', (() => {
202
+ const h = JSON.parse(fs.readFileSync(codexHooksJsonPath, 'utf8'));
203
+ const str = JSON.stringify(h);
204
+ return !str.includes('trailhead-commit-guard.js') && !str.includes('trailhead-secret-guard.js') &&
205
+ !str.includes('trailhead-issue-injection-scanner.js') && !str.includes('trailhead-check-update.js');
206
+ })());
207
+
208
+ // --- claude regression --------------------------------------------------------
209
+ const claudeDir = mktmp();
210
+ runInstaller([`--claude`, `--dir=${claudeDir}`]);
211
+
212
+ const claudeSkillPath = path.join(claudeDir, 'skills', 'trailhead', 'SKILL.md');
213
+ ok('claude: skills/trailhead/SKILL.md exists', fs.existsSync(claudeSkillPath));
214
+ // The split ships all sibling engine skills on Claude too.
215
+ ok('claude: skills/_shared/substrate.md exists', fs.existsSync(path.join(claudeDir, 'skills', '_shared', 'substrate.md')));
216
+ for (const cl of ['trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage']) {
217
+ ok(`claude: skills/${cl}/SKILL.md exists`, fs.existsSync(path.join(claudeDir, 'skills', cl, 'SKILL.md')));
218
+ }
219
+ ok('claude: cluster ../_shared/ reference resolves', fs.existsSync(path.join(claudeDir, 'skills', 'trailhead-work', '..', '_shared', 'substrate.md')));
220
+ ok('claude: commands/trailhead/work.md exists', fs.existsSync(path.join(claudeDir, 'commands', 'trailhead', 'work.md')));
221
+ ok('claude: hooks/trailhead-commit-guard.js exists', fs.existsSync(path.join(claudeDir, 'hooks', 'trailhead-commit-guard.js')));
222
+ ok('claude: hooks/lib/commit-message-check.js exists (commit-guard require target)',
223
+ fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'commit-message-check.js')));
224
+ // Regression: the commit-guard does require('./lib/commit-message-check.js'), so
225
+ // it only loads if the lib was copied alongside it. Run it with a benign Bash
226
+ // payload and assert it does not crash with a missing-module error.
227
+ ok('claude: trailhead-commit-guard.js loads without MODULE_NOT_FOUND', (() => {
228
+ let out = '';
229
+ try {
230
+ out = String(execFileSync(process.execPath, [path.join(claudeDir, 'hooks', 'trailhead-commit-guard.js')],
231
+ { input: '{"tool_name":"Bash","tool_input":{"command":"echo hi"}}', stdio: 'pipe' }));
232
+ } catch (e) {
233
+ out = String(e.stdout || '') + String(e.stderr || '');
234
+ }
235
+ return !out.includes('MODULE_NOT_FOUND') && !out.includes('Cannot find module');
236
+ })());
237
+ ok('claude: SKILL.md does not start with the codex adapter header', !fs.readFileSync(claudeSkillPath, 'utf8').startsWith('<codex_skill_adapter>'));
238
+
239
+ // --- claude --symlink: dev install links hooks live too (not just skills/commands) ---
240
+ const symDir = mktmp();
241
+ runInstaller([`--claude`, `--symlink`, `--dir=${symDir}`]);
242
+ ok('claude symlink: skills/trailhead is a symlink',
243
+ fs.lstatSync(path.join(symDir, 'skills', 'trailhead')).isSymbolicLink());
244
+ ok('claude symlink: hooks/trailhead-secret-guard.js is a symlink',
245
+ fs.lstatSync(path.join(symDir, 'hooks', 'trailhead-secret-guard.js')).isSymbolicLink());
246
+ ok('claude symlink: hooks/lib/commit-message-check.js is a symlink',
247
+ fs.lstatSync(path.join(symDir, 'hooks', 'lib', 'commit-message-check.js')).isSymbolicLink());
248
+ ok('claude symlink: hook symlink resolves into the package source',
249
+ fs.realpathSync(path.join(symDir, 'hooks', 'trailhead-secret-guard.js')) ===
250
+ fs.realpathSync(path.join(repoRoot, 'plugins', 'trailhead', 'hooks', 'trailhead-secret-guard.js')));
251
+ ok('claude symlink: a symlinked commit-guard still loads its lib (no MODULE_NOT_FOUND)', (() => {
252
+ let out = '';
253
+ try {
254
+ out = String(execFileSync(process.execPath, [path.join(symDir, 'hooks', 'trailhead-commit-guard.js')],
255
+ { input: '{"tool_name":"Bash","tool_input":{"command":"echo hi"}}', stdio: 'pipe' }));
256
+ } catch (e) { out = String(e.stdout || '') + String(e.stderr || ''); }
257
+ return !out.includes('MODULE_NOT_FOUND') && !out.includes('Cannot find module');
258
+ })());
259
+ // Reinstalling over an existing symlink install must not throw (EEXIST guard).
260
+ runInstaller([`--claude`, `--symlink`, `--dir=${symDir}`]);
261
+ ok('claude symlink: reinstall over existing symlinks succeeds',
262
+ fs.lstatSync(path.join(symDir, 'hooks', 'trailhead-secret-guard.js')).isSymbolicLink());
263
+ // Default (copy) install keeps hooks as real files, not symlinks.
264
+ ok('claude copy: hooks/trailhead-secret-guard.js is a regular file (not a symlink)',
265
+ !fs.lstatSync(path.join(claudeDir, 'hooks', 'trailhead-secret-guard.js')).isSymbolicLink());
266
+
267
+ // --- claude migration: reinstall over an old layout sweeps stale skill dirs ---
268
+ // An old install may carry a pre-split monolith (skills/trailhead-monolith) or a
269
+ // removed cluster (skills/trailhead-legacy) the current install no longer ships.
270
+ // Reinstalling must sweep every stale trailhead-owned skill dir, while leaving a
271
+ // co-tenant plugin's own skill (skills/other-plugin) untouched.
272
+ const migClaudeDir = mktmp();
273
+ runInstaller([`--claude`, `--dir=${migClaudeDir}`]);
274
+ fs.mkdirSync(path.join(migClaudeDir, 'skills', 'trailhead-monolith'), { recursive: true });
275
+ fs.writeFileSync(path.join(migClaudeDir, 'skills', 'trailhead-monolith', 'SKILL.md'), 'stale monolith\n');
276
+ fs.mkdirSync(path.join(migClaudeDir, 'skills', 'trailhead-legacy'), { recursive: true });
277
+ fs.writeFileSync(path.join(migClaudeDir, 'skills', 'trailhead-legacy', 'SKILL.md'), 'stale cluster\n');
278
+ fs.mkdirSync(path.join(migClaudeDir, 'skills', 'other-plugin'), { recursive: true });
279
+ fs.writeFileSync(path.join(migClaudeDir, 'skills', 'other-plugin', 'SKILL.md'), 'co-tenant\n');
280
+ runInstaller([`--claude`, `--dir=${migClaudeDir}`]);
281
+ ok('claude migration: stale trailhead-monolith swept on reinstall', !fs.existsSync(path.join(migClaudeDir, 'skills', 'trailhead-monolith')));
282
+ ok('claude migration: stale trailhead-legacy cluster swept on reinstall', !fs.existsSync(path.join(migClaudeDir, 'skills', 'trailhead-legacy')));
283
+ ok('claude migration: co-tenant skill preserved (only trailhead names swept)', fs.existsSync(path.join(migClaudeDir, 'skills', 'other-plugin')));
284
+ ok('claude migration: current dispatcher present after reinstall', fs.existsSync(path.join(migClaudeDir, 'skills', 'trailhead', 'SKILL.md')));
285
+ ok('claude migration: current cluster present after reinstall', fs.existsSync(path.join(migClaudeDir, 'skills', 'trailhead-work', 'SKILL.md')));
286
+
287
+ // Uninstall also sweeps a stale trailhead-owned dir the current package no
288
+ // longer ships (monolith), not just the current engineSkillDirs() set.
289
+ const uninstMigDir = mktmp();
290
+ runInstaller([`--claude`, `--dir=${uninstMigDir}`]);
291
+ fs.mkdirSync(path.join(uninstMigDir, 'skills', 'trailhead-monolith'), { recursive: true });
292
+ fs.writeFileSync(path.join(uninstMigDir, 'skills', 'trailhead-monolith', 'SKILL.md'), 'stale\n');
293
+ runInstaller([`--claude`, `--dir=${uninstMigDir}`, '--uninstall']);
294
+ ok('claude uninstall: stale trailhead-monolith swept too', !fs.existsSync(path.join(uninstMigDir, 'skills', 'trailhead-monolith')));
295
+
296
+ // --- claude uninstall: remove trailhead's lib, keep a co-tenant's -------------
297
+ // The Claude hooks/lib dir is shared with other plugins. Drop a fake co-tenant
298
+ // lib next to trailhead's, uninstall, and assert only trailhead's own lib file
299
+ // is removed (never a recursive wipe of the shared dir).
300
+ const coTenantDir = mktmp();
301
+ runInstaller([`--claude`, `--dir=${coTenantDir}`]);
302
+ const coTenantLib = path.join(coTenantDir, 'hooks', 'lib', 'other-plugin-lib.js');
303
+ fs.writeFileSync(coTenantLib, '// not trailhead\n');
304
+ runInstaller([`--claude`, `--dir=${coTenantDir}`, '--uninstall']);
305
+ ok('claude uninstall: trailhead lib removed', !fs.existsSync(path.join(coTenantDir, 'hooks', 'lib', 'commit-message-check.js')));
306
+ ok('claude uninstall: co-tenant lib preserved (no recursive wipe)', fs.existsSync(coTenantLib));
307
+
308
+ // Uninstall removes every split skill (dispatcher + clusters + _shared) by name.
309
+ const splitDir = mktmp();
310
+ runInstaller([`--claude`, `--dir=${splitDir}`]);
311
+ runInstaller([`--claude`, `--dir=${splitDir}`, '--uninstall']);
312
+ for (const nm of ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage', '_shared']) {
313
+ ok(`claude uninstall: skills/${nm} removed`, !fs.existsSync(path.join(splitDir, 'skills', nm)));
314
+ }
315
+
316
+ const settingsPath = path.join(claudeDir, 'settings.json');
317
+ ok('claude: settings.json exists', fs.existsSync(settingsPath));
318
+ const settingsContent = fs.readFileSync(settingsPath, 'utf8');
319
+ ok('claude: settings.json references commit-guard', settingsContent.includes('trailhead-commit-guard.js'));
320
+
321
+ // --- auto-detect: exactly one CLI on $PATH -> install for it, no flag ----------
322
+ // Only a fake `codex` on $PATH: the installer must pick codex on its own.
323
+ const autoCodexDir = mktmp();
324
+ runInstaller([`--dir=${autoCodexDir}`], { env: { PATH: fakeBinDir(['codex']) } });
325
+ const autoCodexSkillPath = path.join(autoCodexDir, 'skills', 'trailhead', 'SKILL.md');
326
+ ok('auto-detect codex: skills/trailhead/SKILL.md exists', fs.existsSync(autoCodexSkillPath));
327
+ ok('auto-detect codex: SKILL.md carries the adapter header (proves codex layout)', fs.readFileSync(autoCodexSkillPath, 'utf8').includes('<codex_skill_adapter>'));
328
+ ok('auto-detect codex: did not install the Claude layout (no commands/trailhead)', !fs.existsSync(path.join(autoCodexDir, 'commands', 'trailhead')));
329
+ ok('auto-detect codex: did not install the Claude layout (no settings.json)', !fs.existsSync(path.join(autoCodexDir, 'settings.json')));
330
+
331
+ // Only a fake `claude` on $PATH: the installer must pick claude on its own.
332
+ const autoClaudeDir = mktmp();
333
+ runInstaller([`--dir=${autoClaudeDir}`], { env: { PATH: fakeBinDir(['claude']) } });
334
+ const autoClaudeSkillPath = path.join(autoClaudeDir, 'skills', 'trailhead', 'SKILL.md');
335
+ ok('auto-detect claude: skills/trailhead/SKILL.md exists', fs.existsSync(autoClaudeSkillPath));
336
+ ok('auto-detect claude: commands/trailhead/work.md exists (proves claude layout)', fs.existsSync(path.join(autoClaudeDir, 'commands', 'trailhead', 'work.md')));
337
+ ok('auto-detect claude: SKILL.md does not start with adapter header', !fs.readFileSync(autoClaudeSkillPath, 'utf8').startsWith('<codex_skill_adapter>'));
338
+
339
+ // --- auto-detect: ambiguous + non-interactive -> claude fallback --------------
340
+ // No CLI on $PATH and non-interactive (piped stdio): must fall back to claude.
341
+ const fallbackDir = mktmp();
342
+ runInstaller([`--dir=${fallbackDir}`], { env: { PATH: fakeBinDir([]) } });
343
+ const fallbackSkillPath = path.join(fallbackDir, 'skills', 'trailhead', 'SKILL.md');
344
+ ok('ambiguous fallback: installs the Claude layout', fs.existsSync(fallbackSkillPath));
345
+ ok('ambiguous fallback: settings.json exists (proves claude layout)', fs.existsSync(path.join(fallbackDir, 'settings.json')));
346
+ ok('ambiguous fallback: SKILL.md does not start with adapter header', !fs.readFileSync(fallbackSkillPath, 'utf8').startsWith('<codex_skill_adapter>'));
347
+
348
+ // --- both --codex and --claude -> hard error ----------------------------------
349
+ let bothRejected = false;
350
+ try {
351
+ runInstaller([`--codex`, `--claude`, `--dir=${mktmp()}`]);
352
+ } catch (e) {
353
+ bothRejected = true;
354
+ }
355
+ ok('conflicting --codex --claude is rejected', bothRejected);
356
+
357
+ // --- legacy --host= is no longer a host selector ------------------------------
358
+ // It must not silently install codex; with no real flag it auto-detects, and
359
+ // here $PATH has only claude, so a stray --host=codex still yields claude.
360
+ const legacyDir = mktmp();
361
+ runInstaller([`--host=codex`, `--dir=${legacyDir}`], { env: { PATH: fakeBinDir(['claude']) } });
362
+ const legacySkillPath = path.join(legacyDir, 'skills', 'trailhead', 'SKILL.md');
363
+ ok('legacy --host=codex is ignored (auto-detect wins)', fs.existsSync(legacySkillPath));
364
+ ok('legacy --host=codex is ignored: settings.json exists (proves claude layout)', fs.existsSync(path.join(legacyDir, 'settings.json')));
365
+ ok('legacy --host=codex is ignored: SKILL.md does not start with adapter header', !fs.readFileSync(legacySkillPath, 'utf8').startsWith('<codex_skill_adapter>'));
366
+
367
+ // --- codex agent TOML projection: a real models.codex.* pin (#38) --------------
368
+ // A temp "project" dir with .trailhead/config.json setting models.codex.execute;
369
+ // running the installer with that dir as cwd (and a separate codex-home tmp dir
370
+ // as --dir=) must project trailhead-execute.toml under <codexHome>/agents/.
371
+ const projectDir = mktmp();
372
+ fs.mkdirSync(path.join(projectDir, '.trailhead'), { recursive: true });
373
+ fs.writeFileSync(
374
+ path.join(projectDir, '.trailhead', 'config.json'),
375
+ JSON.stringify({ models: { codex: { execute: 'gpt-5.6-terra' } } }, null, 2) + '\n'
376
+ );
377
+ const pinCodexHomeDir = mktmp();
378
+ runInstaller([`--codex`, `--dir=${pinCodexHomeDir}`], { cwd: projectDir });
379
+
380
+ const pinnedTomlPath = path.join(pinCodexHomeDir, 'agents', 'trailhead-execute.toml');
381
+ ok('codex: models.codex.execute projects trailhead-execute.toml', fs.existsSync(pinnedTomlPath));
382
+ const pinnedTomlContent = fs.existsSync(pinnedTomlPath) ? fs.readFileSync(pinnedTomlPath, 'utf8') : '';
383
+ ok('codex: trailhead-execute.toml has the pinned model', pinnedTomlContent.includes('model = "gpt-5.6-terra"'));
384
+ ok('codex: trailhead-execute.toml has the right name', pinnedTomlContent.includes('name = "trailhead-execute"'));
385
+ // With a real pin projected, v2 IS enabled so Codex honours the agent_type registry.
386
+ const pinConfigTomlPath = path.join(pinCodexHomeDir, 'config.toml');
387
+ ok('codex: config.toml enables multi_agent_v2 when a models.codex.* pin is projected',
388
+ fs.existsSync(pinConfigTomlPath) && fs.readFileSync(pinConfigTomlPath, 'utf8').includes('multi_agent_v2 = true'));
389
+
390
+ // Uninstall must sweep the projected TOML too, without a --dir= cwd dependency.
391
+ runInstaller([`--codex`, `--dir=${pinCodexHomeDir}`, '--uninstall']);
392
+ ok('codex uninstall: trailhead-execute.toml is gone', !fs.existsSync(pinnedTomlPath));
393
+
394
+ // --- cleanup -------------------------------------------------------------------
395
+ for (const d of tmpDirs) {
396
+ fs.rmSync(d, { recursive: true, force: true });
397
+ }
398
+
399
+ console.log(`✓ trailhead.js (installer): ${passed} assertions passed`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcomigozzi/trailhead",
3
- "version": "0.3.0",
3
+ "version": "0.5.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"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trailhead",
3
- "version": "0.3.0",
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. Self-contained: depends on nothing beyond an authenticated gh CLI.",
3
+ "version": "0.4.0",
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",
7
7
  "url": "https://github.com/ToRvaLDz"
@@ -17,7 +17,12 @@
17
17
  "capture"
18
18
  ],
19
19
  "skills": [
20
- "./skills/trailhead"
20
+ "./skills/trailhead",
21
+ "./skills/trailhead-chart",
22
+ "./skills/trailhead-work",
23
+ "./skills/trailhead-view",
24
+ "./skills/trailhead-capture",
25
+ "./skills/trailhead-manage"
21
26
  ],
22
27
  "hooks": "./hooks/hooks.json"
23
28
  }
@@ -3,4 +3,4 @@ description: "Adopt an existing project: map the codebase once, then go lean"
3
3
  argument-hint: ""
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **adopt** action: call the Skill tool with skill name `trailhead` and arguments `adopt $ARGUMENTS`, then carry out the skill's instructions for that verb. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **adopt** action: call the Skill tool with skill name `trailhead-chart` and arguments `adopt $ARGUMENTS`, then carry out the skill's instructions for that verb. Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: Capture a bug ticket (use --of <ticket> for a regression)
3
3
  argument-hint: "[--of <ticket>] <text>"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **bug** action: call the Skill tool with skill name `trailhead` and arguments `bug $ARGUMENTS`, then **read the skill's `references/capture.md` and follow it**: that file holds the full protocol for the capture verbs. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **bug** action: call the Skill tool with skill name `trailhead-capture` and arguments `bug $ARGUMENTS`, then **read the skill's `references/capture.md` and follow it**: that file holds the full protocol for the capture verbs. Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: Guided menu setup, or get/set trailhead config
3
3
  argument-hint: "[get|set …]"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **config** action: call the Skill tool with skill name `trailhead` and arguments `config $ARGUMENTS`, then **read the skill's `references/configuration.md` and follow it**: that file holds the keys, their semantics, and the guided setup. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **config** action: call the Skill tool with skill name `trailhead-manage` and arguments `config $ARGUMENTS`, then **read the skill's `references/configuration.md` and follow it**: that file holds the keys, their semantics, and the guided setup. Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: "Show the repo dashboard: the pinned index of every open map, the w
3
3
  argument-hint: ""
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **dashboard** action: call the Skill tool with skill name `trailhead` and arguments `dashboard $ARGUMENTS`, then carry out the skill's instructions for that verb (regenerate the pinned `trailhead:dashboard` index issue from the live tracker, creating and pinning it if missing, and show it). Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **dashboard** action: call the Skill tool with skill name `trailhead-view` and arguments `dashboard $ARGUMENTS`, then carry out the skill's instructions for that verb (regenerate the pinned `trailhead:dashboard` index issue from the live tracker, creating and pinning it if missing, and show it). Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: Run a standalone grilling session on a decision or topic
3
3
  argument-hint: "[topic|ticket]"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **grill** action: call the Skill tool with skill name `trailhead` and arguments `grill $ARGUMENTS`, then **read the skill's `references/techniques/grilling.md` (and `references/techniques/domain-vocabulary.md`) and follow them**: they hold the full protocol for this action. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **grill** action: call the Skill tool with skill name `trailhead-chart` and arguments `grill $ARGUMENTS`, then carry out the skill's instructions for that verb (they run the Grilling and Domain vocabulary techniques). Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: Capture an idea into the fog (Not yet specified)
3
3
  argument-hint: "<text>"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **idea** action: call the Skill tool with skill name `trailhead` and arguments `idea $ARGUMENTS`, then **read the skill's `references/capture.md` and follow it**: that file holds the full protocol for the capture verbs. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **idea** action: call the Skill tool with skill name `trailhead-capture` and arguments `idea $ARGUMENTS`, then **read the skill's `references/capture.md` and follow it**: that file holds the full protocol for the capture verbs. Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: Triage issues opened by others and integrate the worthwhile ones in
3
3
  argument-hint: "[issue]"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **inbox** action: call the Skill tool with skill name `trailhead` and arguments `inbox $ARGUMENTS`, then **read the skill's `references/inbox.md` and follow it**: that file holds the full protocol for this action. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **inbox** action: call the Skill tool with skill name `trailhead-chart` and arguments `inbox $ARGUMENTS`, then **read the skill's `references/inbox.md` and follow it**: that file holds the full protocol for this action. Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: Show the low-res map (destination, decisions, frontier, fog)
3
3
  argument-hint: ""
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **map** action: call the Skill tool with skill name `trailhead` and arguments `map $ARGUMENTS`, then carry out the skill's instructions for that verb. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **map** action: call the Skill tool with skill name `trailhead-view` and arguments `map $ARGUMENTS`, then carry out the skill's instructions for that verb. Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: Chart a new trailhead map from a loose idea
3
3
  argument-hint: "[idea]"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **new** action: call the Skill tool with skill name `trailhead` and arguments `new $ARGUMENTS`, then carry out the skill's instructions for that verb. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **new** action: call the Skill tool with skill name `trailhead-chart` and arguments `new $ARGUMENTS`, then carry out the skill's instructions for that verb. Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: Capture a verbatim note
3
3
  argument-hint: "<text>"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **note** action: call the Skill tool with skill name `trailhead` and arguments `note $ARGUMENTS`, then **read the skill's `references/capture.md` and follow it**: that file holds the full protocol for the capture verbs. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **note** action: call the Skill tool with skill name `trailhead-capture` and arguments `note $ARGUMENTS`, then **read the skill's `references/capture.md` and follow it**: that file holds the full protocol for the capture verbs. Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: Checkpoint the ticket in play so it can be resumed later
3
3
  argument-hint: "[note]"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **pause** action: call the Skill tool with skill name `trailhead` and arguments `pause $ARGUMENTS`, then **read the skill's `references/teamwork.md` and follow it**: that file holds the pausing & resuming protocol (including, under `isolation: worktree`/`clone`, committing the work-in-progress in the ticket's isolated workspace and recording its path in the checkpoint). Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **pause** action: call the Skill tool with skill name `trailhead-work` and arguments `pause $ARGUMENTS`, then **read the teamwork reference the skill points to (`../_shared/teamwork.md`) and follow it**: that file holds the pausing & resuming protocol (including, under `isolation: worktree`/`clone`, committing the work-in-progress in the ticket's isolated workspace and recording its path in the checkpoint). Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,6 @@ description: Work one ticket whole, off the map (whiteboard); grills only if nee
3
3
  argument-hint: "[ticket | \"text\"]"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **quick** action: call the Skill tool with skill name `trailhead` and arguments `quick $ARGUMENTS`, then carry out the skill's instructions for that verb (open a `trailhead:whiteboard` ticket from the `"text"`, or take the existing ticket you name, then run the full engine on it without splitting and without map book-keeping). Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **quick** action: call the Skill tool with skill name `trailhead-work` and arguments `quick $ARGUMENTS`, then carry out the skill's instructions for that verb (open a `trailhead:whiteboard` ticket from the `"text"`, or take the existing ticket you name, then run the full engine on it without splitting and without map book-keeping). Do not re-implement the behaviour here. The skill is the single source of truth.
7
+
8
+ **When `quick "<text>"` opens a NEW whiteboard ticket, refresh the pinned `trailhead:dashboard` right after the ticket exists** (a structural event: the whiteboard has no native progress bar, so the dashboard is the only place the ticket shows). This is a required step of the create path, not optional; do not skip it. `quick <n>` on an existing *map* ticket does not refresh. See the skill's *quick* section and the dashboard freshness rule.
@@ -3,4 +3,4 @@ description: Resume a paused ticket from its checkpoint
3
3
  argument-hint: "[ticket]"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **resume** action: call the Skill tool with skill name `trailhead` and arguments `resume $ARGUMENTS`, then **read the skill's `references/teamwork.md` and follow it**: that file holds the pausing & resuming protocol (including, under `isolation: worktree`/`clone`, re-entering the ticket's isolated workspace recorded in its checkpoint). Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **resume** action: call the Skill tool with skill name `trailhead-work` and arguments `resume $ARGUMENTS`, then **read the teamwork reference the skill points to (`../_shared/teamwork.md`) and follow it**: that file holds the pausing & resuming protocol (including, under `isolation: worktree`/`clone`, re-entering the ticket's isolated workspace recorded in its checkpoint). Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: "Capture a seed: work gated on a future trigger"
3
3
  argument-hint: "<text>"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **seed** action: call the Skill tool with skill name `trailhead` and arguments `seed $ARGUMENTS`, then **read the skill's `references/capture.md` and follow it**: that file holds the full protocol for the capture verbs. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **seed** action: call the Skill tool with skill name `trailhead-capture` and arguments `seed $ARGUMENTS`, then **read the skill's `references/capture.md` and follow it**: that file holds the full protocol for the capture verbs. Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: Split an oversized ticket into children and supersede it
3
3
  argument-hint: "[ticket]"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **split** action: call the Skill tool with skill name `trailhead` and arguments `split $ARGUMENTS`, then **read the skill's `references/teamwork.md` and follow it**: that file holds the splitting protocol. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **split** action: call the Skill tool with skill name `trailhead-work` and arguments `split $ARGUMENTS`, then **read the teamwork reference the skill points to (`../_shared/teamwork.md`) and follow it**: that file holds the splitting protocol. Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: Open a ticket of any type on the fly (diverges briefly first)
3
3
  argument-hint: "<type> <title>"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **ticket** action: call the Skill tool with skill name `trailhead` and arguments `ticket $ARGUMENTS`, then carry out the skill's instructions for that verb. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **ticket** action: call the Skill tool with skill name `trailhead-chart` and arguments `ticket $ARGUMENTS`, then carry out the skill's instructions for that verb. Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: "Capture a small build ticket: defined work you will do now"
3
3
  argument-hint: "<text>"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **todo** action: call the Skill tool with skill name `trailhead` and arguments `todo $ARGUMENTS`, then **read the skill's `references/capture.md` and follow it**: that file holds the full protocol for the capture verbs. Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **todo** action: call the Skill tool with skill name `trailhead-capture` and arguments `todo $ARGUMENTS`, then **read the skill's `references/capture.md` and follow it**: that file holds the full protocol for the capture verbs. Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -2,4 +2,4 @@
2
2
  description: Check for a newer trailhead and install it
3
3
  ---
4
4
 
5
- Invoke the `trailhead` skill for the **update** action: call the Skill tool with skill name `trailhead` and arguments `update $ARGUMENTS`, then **read the skill's `references/updating.md` and follow it**: that file holds the update protocol (detect the install channel, compare versions, install the newer one where it is safe). Do not re-implement the behaviour here. The skill is the single source of truth.
5
+ Invoke the `trailhead` skill for the **update** action: call the Skill tool with skill name `trailhead-manage` and arguments `update $ARGUMENTS`, then **read the skill's `references/updating.md` and follow it**: that file holds the update protocol (detect the install channel, compare versions, install the newer one where it is safe). Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: "Show the whiteboard: loose (map-less) tickets, their frontier and
3
3
  argument-hint: ""
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **whiteboard** action: call the Skill tool with skill name `trailhead` and arguments `whiteboard $ARGUMENTS`, then carry out the skill's instructions for that verb (render the read-only whiteboard dashboard: the frontier, in-progress, and blocked `trailhead:whiteboard` tickets). Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **whiteboard** action: call the Skill tool with skill name `trailhead-view` and arguments `whiteboard $ARGUMENTS`, then carry out the skill's instructions for that verb (render the read-only whiteboard dashboard: the frontier, in-progress, and blocked `trailhead:whiteboard` tickets). Do not re-implement the behaviour here. The skill is the single source of truth.
@@ -3,4 +3,4 @@ description: Work the next frontier ticket, or the one you name
3
3
  argument-hint: "[ticket]"
4
4
  ---
5
5
 
6
- Invoke the `trailhead` skill for the **work** action: call the Skill tool with skill name `trailhead` and arguments `work $ARGUMENTS`, then carry out the skill's instructions for that verb (under the repo's `trailhead:conventions` `isolation: worktree` or `clone`, work the ticket in its own isolated workspace on a `trailhead/t<n>` branch, set up at work-start, per `references/teamwork.md`). Do not re-implement the behaviour here. The skill is the single source of truth.
6
+ Invoke the `trailhead` skill for the **work** action: call the Skill tool with skill name `trailhead-work` and arguments `work $ARGUMENTS`, then carry out the skill's instructions for that verb (under the repo's `trailhead:conventions` `isolation: worktree` or `clone`, work the ticket in its own isolated workspace on a `trailhead/t<n>` branch, set up at work-start, per the skill's teamwork reference). Do not re-implement the behaviour here. The skill is the single source of truth.