@marcomigozzi/trailhead 0.4.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.
- package/README.it.md +19 -14
- package/README.md +20 -15
- package/bin/lib/codex-projection.js +23 -2
- package/bin/lib/codex-projection.test.js +14 -0
- package/bin/trailhead.js +99 -36
- package/bin/trailhead.test.js +130 -5
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +7 -2
- 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/trailhead-commit-guard.js +1 -1
- 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} +9 -3
- 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/{trailhead/references → _shared}/techniques/code-review.md +5 -1
- package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/plan-review.md +4 -7
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +36 -0
- 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 +25 -452
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +28 -0
- package/plugins/trailhead/skills/{trailhead → trailhead-capture}/references/capture.md +4 -4
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +51 -0
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +24 -0
- package/plugins/trailhead/skills/{trailhead → trailhead-chart}/references/inbox.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/SKILL.md +29 -0
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +112 -0
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +65 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +52 -0
- package/plugins/trailhead/skills/trailhead/references/techniques/prototype.md +0 -18
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/codebase-map.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/debug.md +0 -0
- /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/research.md +0 -0
- /package/plugins/trailhead/skills/{trailhead/references → _shared}/techniques/tdd.md +0 -0
- /package/plugins/trailhead/skills/{trailhead → trailhead-manage}/references/updating.md +0 -0
package/bin/trailhead.test.js
CHANGED
|
@@ -74,7 +74,7 @@ ok('codex: SKILL.md still carries the adapter header', skillMainContent.includes
|
|
|
74
74
|
const engineBody = skillMainContent.slice(skillMainContent.indexOf('</codex_skill_adapter>'));
|
|
75
75
|
ok('codex: SKILL.md engine body has no /trailhead: substring', !engineBody.includes('/trailhead:'));
|
|
76
76
|
|
|
77
|
-
ok('codex:
|
|
77
|
+
ok('codex: _shared/techniques/grilling.md exists', fs.existsSync(path.join(codexDir, 'skills', '_shared', 'techniques', 'grilling.md')));
|
|
78
78
|
|
|
79
79
|
const agentsYamlPath = path.join(codexDir, 'skills', 'trailhead', 'agents', 'openai.yaml');
|
|
80
80
|
ok('codex: agents/openai.yaml exists', fs.existsSync(agentsYamlPath));
|
|
@@ -90,13 +90,29 @@ ok('codex: no settings.json file', !fs.existsSync(path.join(codexDir, 'settings.
|
|
|
90
90
|
// #46: per-verb discoverability now projects one Codex SKILL per verb
|
|
91
91
|
// (skills/trailhead-<verb>/), invocable as $trailhead-<verb>. The old
|
|
92
92
|
// ~/.codex/prompts/ shims never surfaced as slash commands on Codex.
|
|
93
|
-
|
|
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.
|
|
94
95
|
ok('codex: per-verb skills/trailhead-bug/SKILL.md exists', fs.existsSync(path.join(codexDir, 'skills', 'trailhead-bug', 'SKILL.md')));
|
|
95
|
-
ok('codex: trailhead-
|
|
96
|
-
ok('codex: trailhead-
|
|
97
|
-
ok('codex: per-verb skill is explicit-only', fs.readFileSync(path.join(codexDir, 'skills', 'trailhead-
|
|
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'));
|
|
98
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')));
|
|
99
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
|
+
|
|
100
116
|
// --- codex hooks (#29) --------------------------------------------------------
|
|
101
117
|
const codexHooksJsonPath = path.join(codexDir, 'hooks.json');
|
|
102
118
|
ok('codex: hooks.json exists', fs.existsSync(codexHooksJsonPath));
|
|
@@ -121,6 +137,30 @@ ok('codex: config.toml enables hooks feature', fs.readFileSync(codexConfigTomlPa
|
|
|
121
137
|
ok('codex: config.toml does NOT enable multi_agent_v2 when no models.codex.* pins are projected',
|
|
122
138
|
!fs.readFileSync(codexConfigTomlPath, 'utf8').includes('multi_agent_v2 = true'));
|
|
123
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
|
+
|
|
124
164
|
// --- codex agent TOML projection (#38) -----------------------------------------
|
|
125
165
|
// repoRoot's own .trailhead/config.json has no models.codex.*, so a plain
|
|
126
166
|
// install (cwd: repoRoot, no models.codex anywhere) projects no agent TOMLs.
|
|
@@ -138,12 +178,26 @@ ok('codex migration: stale prompts/trailhead.md swept on install', !fs.existsSyn
|
|
|
138
178
|
ok('codex migration: stale prompts/trailhead-work.md swept on install', !fs.existsSync(path.join(migCodexDir, 'prompts', 'trailhead-work.md')));
|
|
139
179
|
ok('codex migration: per-verb skill projected in its place', fs.existsSync(path.join(migCodexDir, 'skills', 'trailhead-work', 'SKILL.md')));
|
|
140
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
|
+
|
|
141
193
|
// --- codex uninstall (same tmp) ----------------------------------------------
|
|
142
194
|
runInstaller([`--codex`, `--dir=${codexDir}`, '--uninstall']);
|
|
143
195
|
ok('codex uninstall: skills/trailhead gone', !fs.existsSync(path.join(codexDir, 'skills', 'trailhead')));
|
|
144
196
|
ok('codex uninstall: legacy trailhead engine dir gone', !fs.existsSync(path.join(codexDir, 'trailhead')));
|
|
145
197
|
ok('codex uninstall: per-verb skills/trailhead-work gone', !fs.existsSync(path.join(codexDir, 'skills', 'trailhead-work')));
|
|
146
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')));
|
|
147
201
|
ok('codex uninstall: hooks.json no longer contains trailhead commands', (() => {
|
|
148
202
|
const h = JSON.parse(fs.readFileSync(codexHooksJsonPath, 'utf8'));
|
|
149
203
|
const str = JSON.stringify(h);
|
|
@@ -157,6 +211,12 @@ runInstaller([`--claude`, `--dir=${claudeDir}`]);
|
|
|
157
211
|
|
|
158
212
|
const claudeSkillPath = path.join(claudeDir, 'skills', 'trailhead', 'SKILL.md');
|
|
159
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')));
|
|
160
220
|
ok('claude: commands/trailhead/work.md exists', fs.existsSync(path.join(claudeDir, 'commands', 'trailhead', 'work.md')));
|
|
161
221
|
ok('claude: hooks/trailhead-commit-guard.js exists', fs.existsSync(path.join(claudeDir, 'hooks', 'trailhead-commit-guard.js')));
|
|
162
222
|
ok('claude: hooks/lib/commit-message-check.js exists (commit-guard require target)',
|
|
@@ -176,6 +236,63 @@ ok('claude: trailhead-commit-guard.js loads without MODULE_NOT_FOUND', (() => {
|
|
|
176
236
|
})());
|
|
177
237
|
ok('claude: SKILL.md does not start with the codex adapter header', !fs.readFileSync(claudeSkillPath, 'utf8').startsWith('<codex_skill_adapter>'));
|
|
178
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
|
+
|
|
179
296
|
// --- claude uninstall: remove trailhead's lib, keep a co-tenant's -------------
|
|
180
297
|
// The Claude hooks/lib dir is shared with other plugins. Drop a fake co-tenant
|
|
181
298
|
// lib next to trailhead's, uninstall, and assert only trailhead's own lib file
|
|
@@ -188,6 +305,14 @@ runInstaller([`--claude`, `--dir=${coTenantDir}`, '--uninstall']);
|
|
|
188
305
|
ok('claude uninstall: trailhead lib removed', !fs.existsSync(path.join(coTenantDir, 'hooks', 'lib', 'commit-message-check.js')));
|
|
189
306
|
ok('claude uninstall: co-tenant lib preserved (no recursive wipe)', fs.existsSync(coTenantLib));
|
|
190
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
|
+
|
|
191
316
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
192
317
|
ok('claude: settings.json exists', fs.existsSync(settingsPath));
|
|
193
318
|
const settingsContent = fs.readFileSync(settingsPath, 'utf8');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marcomigozzi/trailhead",
|
|
3
|
-
"version": "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
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
@@ -56,7 +56,7 @@ process.stdin.on('end', () => {
|
|
|
56
56
|
|
|
57
57
|
const msg = extractMessage(cmd);
|
|
58
58
|
if (msg) {
|
|
59
|
-
// stripComments:false
|
|
59
|
+
// stripComments:false, this is the literal `-m` value, where git keeps
|
|
60
60
|
// `#` lines as message text (cleanup=whitespace), so a `#…` first line is
|
|
61
61
|
// a real subject to validate, not a comment to skip.
|
|
62
62
|
const verdict = checkCommitMessage(msg, { stripComments: false });
|
|
@@ -43,8 +43,17 @@ function ghIssueWrite(cmd) {
|
|
|
43
43
|
let i = 0;
|
|
44
44
|
while (i < toks.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(toks[i])) i++; // env prefix
|
|
45
45
|
if (i >= toks.length || !/(^|\/)gh$/.test(toks[i])) return null; // the gh binary
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
i++;
|
|
47
|
+
// Skip gh global flags before the subcommand, notably `-R`/`--repo <value>`
|
|
48
|
+
// (its separate-token value must be consumed too), so `gh -R o/r issue
|
|
49
|
+
// comment …` is still recognised as a write and gets scanned. Without this a
|
|
50
|
+
// secret could slip past simply by naming the repo up front.
|
|
51
|
+
while (i < toks.length && toks[i].startsWith('-')) {
|
|
52
|
+
if (/^(-R|--repo)$/.test(toks[i]) && !toks[i].includes('=')) i++;
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
const sub = toks[i];
|
|
56
|
+
const verb = toks[i + 1];
|
|
48
57
|
if ((sub === 'issue' || sub === 'pr') && ['create', 'comment', 'edit', 'review'].includes(verb)) {
|
|
49
58
|
return sub;
|
|
50
59
|
}
|
|
@@ -53,10 +62,16 @@ function ghIssueWrite(cmd) {
|
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
// For gh issue/pr, the body may live in a --body-file <path>; read it too.
|
|
65
|
+
// The path is quoted ("..."/'...') or bare. A bare path stops at whitespace AND
|
|
66
|
+
// at shell metacharacters (; | & ( ) < > `): in a compound command like
|
|
67
|
+
// `gh ... --body-file /tmp/x.md; echo done` the `;` abuts the path with no
|
|
68
|
+
// space, so a naive [^\s]+ would swallow it into the filename and the read would
|
|
69
|
+
// ENOENT, turning a clean write into a spurious could-not-scan (and, worse, a
|
|
70
|
+
// real body left unscanned). Quoted paths keep those characters verbatim.
|
|
56
71
|
function bodyFileText(cmd) {
|
|
57
|
-
const m = cmd.match(/(?:--body-file|(?<![-\w])-F)(?:=|\s+)(["']
|
|
72
|
+
const m = cmd.match(/(?:--body-file|(?<![-\w])-F)(?:=|\s+)(?:"([^"]+)"|'([^']+)'|([^\s"';|&()<>`]+))/);
|
|
58
73
|
if (!m) return '';
|
|
59
|
-
const path = m[2];
|
|
74
|
+
const path = m[1] || m[2] || m[3];
|
|
60
75
|
if (path === '-' || path.startsWith('<')) return ''; // stdin/heredoc already in the command string
|
|
61
76
|
return fs.readFileSync(path, 'utf8').slice(0, 1_000_000);
|
|
62
77
|
}
|
|
@@ -8,7 +8,7 @@ const fs = require('fs');
|
|
|
8
8
|
const os = require('os');
|
|
9
9
|
|
|
10
10
|
const HOOK = path.join(__dirname, 'trailhead-secret-guard.js');
|
|
11
|
-
const { scan, ghIssueWrite } = require('./trailhead-secret-guard.js');
|
|
11
|
+
const { scan, ghIssueWrite, bodyFileText } = require('./trailhead-secret-guard.js');
|
|
12
12
|
|
|
13
13
|
let passed = 0;
|
|
14
14
|
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
@@ -36,7 +36,13 @@ ok('scan passes clean prose', scan('tutto ok, 368 test verdi') === null);
|
|
|
36
36
|
ok('detects gh issue comment', ghIssueWrite('gh issue comment 5 --body "x"') === 'issue');
|
|
37
37
|
ok('detects gh pr create', ghIssueWrite('gh pr create --body "x"') === 'pr');
|
|
38
38
|
ok('detects gh api body=', ghIssueWrite('gh api repos/o/r/issues/5/comments -f body=x') === 'api');
|
|
39
|
+
ok('detects gh -R o/r issue comment (global flag before subcommand)',
|
|
40
|
+
ghIssueWrite('gh -R owner/repo issue comment 5 --body x') === 'issue');
|
|
41
|
+
ok('detects gh --repo=o/r pr create (attached global flag)',
|
|
42
|
+
ghIssueWrite('gh --repo=owner/repo pr create --body x') === 'pr');
|
|
39
43
|
ok('ignores gh issue view (read)', ghIssueWrite('gh issue view 5 --json body') === null);
|
|
44
|
+
ok('ignores gh -R o/r issue view (read, even with global flag)',
|
|
45
|
+
ghIssueWrite('gh -R owner/repo issue view 5 --json body') === null);
|
|
40
46
|
ok('ignores non-gh commands', ghIssueWrite('echo ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789xx') === null);
|
|
41
47
|
|
|
42
48
|
// --- end-to-end: block ---
|
|
@@ -53,6 +59,40 @@ const b3 = runHook(`gh issue comment 5 --body-file ${leaky}`);
|
|
|
53
59
|
ok('blocks a secret inside --body-file (exit 2)', b3.code === 2 && /AWS access key/.test(b3.out));
|
|
54
60
|
fs.unlinkSync(leaky);
|
|
55
61
|
|
|
62
|
+
// --- unit: bodyFileText() path extraction ---
|
|
63
|
+
// A bare path abutting a shell metacharacter in a compound command must NOT
|
|
64
|
+
// swallow the metacharacter into the filename (regression: `.md;` -> ENOENT).
|
|
65
|
+
{
|
|
66
|
+
const leak = path.join(os.tmpdir(), `sg-meta-${process.pid}.md`);
|
|
67
|
+
fs.writeFileSync(leak, 'Deploy: AKIAIOSFODNN7EXAMPLE\n');
|
|
68
|
+
ok('bare path stops at a trailing ; (compound command)',
|
|
69
|
+
/AKIAIOSFODNN7EXAMPLE/.test(bodyFileText(`gh issue edit 5 --body-file ${leak}; echo done`)));
|
|
70
|
+
ok('bare path stops at a trailing ) ',
|
|
71
|
+
/AKIAIOSFODNN7EXAMPLE/.test(bodyFileText(`(gh issue edit 5 --body-file ${leak})`)));
|
|
72
|
+
ok('bare path still works with a following && and space',
|
|
73
|
+
/AKIAIOSFODNN7EXAMPLE/.test(bodyFileText(`gh issue edit 5 --body-file ${leak} && echo ok`)));
|
|
74
|
+
// A quoted path keeps metacharacters verbatim (only the closing quote delimits).
|
|
75
|
+
const quoted = path.join(os.tmpdir(), `sg;meta-${process.pid}.md`);
|
|
76
|
+
fs.writeFileSync(quoted, 'Deploy: AKIAIOSFODNN7EXAMPLE\n');
|
|
77
|
+
ok('quoted path keeps an inner ; in the filename',
|
|
78
|
+
/AKIAIOSFODNN7EXAMPLE/.test(bodyFileText(`gh issue edit 5 --body-file "${quoted}"`)));
|
|
79
|
+
fs.unlinkSync(leak);
|
|
80
|
+
fs.unlinkSync(quoted);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// End-to-end: the compound-command shape that produced the spurious advisory
|
|
84
|
+
// must now scan and BLOCK the real secret in the body.
|
|
85
|
+
const leaky2 = path.join(os.tmpdir(), `sg-leaky2-${process.pid}.md`);
|
|
86
|
+
fs.writeFileSync(leaky2, 'Deploy: AKIAIOSFODNN7EXAMPLE\n');
|
|
87
|
+
const b4 = runHook(`gh issue edit 5 --body-file ${leaky2}; echo done`);
|
|
88
|
+
ok('blocks a secret in --body-file even in a compound command (exit 2)',
|
|
89
|
+
b4.code === 2 && /AWS access key/.test(b4.out));
|
|
90
|
+
fs.unlinkSync(leaky2);
|
|
91
|
+
|
|
92
|
+
// A global flag before the subcommand must not let a secret slip past unscanned.
|
|
93
|
+
const b5 = runHook('gh -R owner/repo issue comment 5 --body "t ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789xx"');
|
|
94
|
+
ok('blocks an inline secret behind gh -R (exit 2)', b5.code === 2 && /"decision":"block"/.test(b5.out));
|
|
95
|
+
|
|
56
96
|
// --- end-to-end: allow ---
|
|
57
97
|
const a1 = runHook('gh issue comment 5 --body "tutto ok, 368 test verdi"');
|
|
58
98
|
ok('allows a clean write (exit 0, no output)', a1.code === 0 && a1.out.trim() === '');
|
|
@@ -7,13 +7,15 @@ Effective config = built-in defaults ← global `~/.claude/trailhead/config.json
|
|
|
7
7
|
| `models.{plan,execute,research,review,debug}` | a model id (**inherit session**) | model for that activity, see note |
|
|
8
8
|
| `models.codex.{plan,execute,research,review,debug}` | an OpenAI model id (**inherit session**) | per-activity subagent model on **Codex** (OpenAI ids), the honoured namespace there; independent of `models.*` (Claude ids); projected into `~/.codex/agents/trailhead-*.toml` and dispatched by `agent_type`. See note |
|
|
9
9
|
| `ticket.language` | an ISO 639-1 code (**`en`**) | the language trailhead **writes** its GitHub prose in (ticket titles/bodies, engine comments, map body & commit descriptions), see note. Independent of the language the agent converses in |
|
|
10
|
-
| `design` | **`disk`** \| `claude.ai/design` | mode for UI mockups (**Prototype**). `disk` = throwaway local HTML; `claude.ai/design` =
|
|
10
|
+
| `design` | **`disk`** \| `claude.ai/design` \| `stitch` | mode for UI mockups (**Prototype**). `disk` = throwaway local HTML (any host); `claude.ai/design` = create/edit the mockup on claude.ai/design via Anthropic's **official Claude Design MCP** (`claude mcp add --scope user --transport http claude-design https://api.anthropic.com/v1/design/mcp`, auth `/design-login`), **Claude only**; `stitch` = create/edit the mockup via the **Google Stitch MCP** (`claude mcp add --scope user --transport http stitch https://stitch.googleapis.com/mcp`), the **cross-host** hosted option (Claude Code **and** Codex). A hosted mode's projectId is created/reused at execution and cached in `design.project`; which surface it uses is `design.surface`. If the configured mode's MCP isn't connected, Prototype offers a one-time install before falling back (see Prototype). **On Codex** `claude.ai/design` isn't available, so Prototype **offers to switch to `stitch`** (cross-host) before degrading to `disk`; `stitch` and `disk` run on Codex unchanged |
|
|
11
|
+
| `design.surface` | **`canvas`** \| `design-system` | under a hosted `design` mode (`claude.ai/design` or `stitch`), which surface the mockup lives on. `canvas` = a single surface of screens to react to (under `claude.ai/design`: a `/design` **Artifact** or a hand-made web-app project, **not** MCP-created; under `stitch`: a Stitch project), editable visually and exportable as a live prototype. `design-system` = push components into a shared design-system project (a `PROJECT_TYPE_DESIGN_SYSTEM` synced via DesignSync `/design-sync`, or a Stitch design system), for a shared component library. **Back-compat:** unset, a cached `design.project` infers `design-system` (that's what the key meant before this key existed); with no cached project, `canvas`. See Prototype |
|
|
11
12
|
| `design.approval` | **`explicit`** \| `auto` | must a mockup be explicitly approved before the build proceeds to UI code? |
|
|
12
13
|
| `tdd` | **`seams`** \| `on` \| `off` | how the `build` engine tests |
|
|
13
14
|
| `acceptance.browser` | **`auto`** \| `on` \| `off` | how **Acceptance testing** runs |
|
|
14
15
|
| `testing.webapp` / `testing.url` | bool / url | is it browser-drivable, and where |
|
|
15
16
|
| `plan_review` | **`off`** \| `on` \| CLI list (`gemini,codex`) | send `build` PLANs to external AI CLIs for review (**Cross-AI plan review**) |
|
|
16
17
|
| `plan_review.rounds` | integer (**`2`**) | max converge-and-re-review rounds |
|
|
18
|
+
| `code_review.rounds` | integer (**`2`**) | max fix-and-re-review rounds in **Code review** before the boundary HITL checkpoint |
|
|
17
19
|
|
|
18
20
|
**Models.** Each key targets a specific activity, so you can send the heavy reasoning to a strong model and the cheap legwork to a fast one:
|
|
19
21
|
- `plan` / `execute`: the `build`/`bug` engine's **Plan** and **Execute** steps (see below).
|
|
@@ -38,12 +40,16 @@ If a value is unset, inherit the session model (never hard-fail on an unknown id
|
|
|
38
40
|
|
|
39
41
|
**tdd.** `seams` (default) = TDD at critical seams only; `on` = TDD for all behaviour with a definable input→output; `off` = tests after, or none for throwaway. The `build` engine's Plan/Execute honour this.
|
|
40
42
|
|
|
41
|
-
**acceptance.browser.** `auto` (default) = drive the browser when `testing.webapp` + `testing.url` are set,
|
|
43
|
+
**acceptance.browser.** `auto` (default) = drive the browser when `testing.webapp` + `testing.url` are set; when `testing.webapp` is set but `testing.url` is unset, first try to auto-detect the target URL (running dev server, package scripts, dev-server/framework config, project config) and propose it, driving against the accepted URL, and fall back to a guided UAT checklist only when none can be determined; `on` = always attempt browser-driving, auto-detecting the URL the same way when `testing.url` is unset; `off` = always hand the human a UAT checklist. **`off` disables *browser-driving*, not acceptance**: a user-facing change still gets a guided UAT checklist (see the Acceptance testing technique). The Verify step honours this.
|
|
44
|
+
|
|
45
|
+
**design.surface.** Under `design: claude.ai/design`, which Claude Design surface the mockup lives on. `canvas` (default) = a one-off screen to react to, edited visually and exportable as a live prototype; under `claude.ai/design` it is **not** MCP-driven (no MCP creates a regular canvas project): it's the built-in `/design` skill published as an **Artifact**, or a claude.ai/design project made by hand in the web app whose `/p/<id>` URL you paste. `design-system` = a `PROJECT_TYPE_DESIGN_SYSTEM` project driven by DesignSync (the `/design-sync` slice of the official Claude Design MCP), for a shared component library kept in sync. Unset, it infers from `design.project`: a cached project (which, before this key, was always a design system) means `design-system`; no cached project means `canvas`. The Prototype technique honours this; when the **`design-system`** surface's MCP isn't connected it **offers a one-time install** (`claude mcp add ... claude-design`, then `/design-login`) before falling to disk (the `canvas` surface needs no MCP, so no install offer). Under `design: stitch` the same key applies: `canvas` = a Stitch project with generated screens, `design-system` = a Stitch design system; the install offer and disk fallback work the same way (no intra-provider chain, the Stitch MCP either connects or falls to disk). Ignored under `design: disk`.
|
|
42
46
|
|
|
43
47
|
**design.approval.** `explicit` (default) = a UI mockup must be **explicitly approved by the user** before the `build` engine writes any real UI code; surface the mockup and wait for a clear go-ahead. `auto` = surface the mockup and proceed without blocking on a confirmation (the user can still object). The Prototype technique honours this.
|
|
44
48
|
|
|
45
49
|
**plan_review.** `off` (default) = build PLANs are not externally reviewed; `on` = review with every external AI CLI detected on the PATH; a comma list (`gemini,codex`) = only those. Needs the named CLIs installed; if none are available it's skipped, never failed. The `build` engine's Plan step runs **Cross-AI plan review** when this is on.
|
|
46
50
|
|
|
51
|
+
**code_review.rounds.** Integer, default 2. The max fix-and-re-review rounds the **Code review** technique spends chasing **Critical** (blocking) findings during the `build`/`bug` Verify step. When the budget is exhausted with Critical findings still open, the technique never resolves or continues silently: it surfaces an explicit HITL checkpoint (run another 2 rounds vs proceed with the blockers recorded). Mirrors `plan_review.rounds`.
|
|
52
|
+
|
|
47
53
|
Example `config.json` (same shape for the project `.trailhead/config.json` and the global `~/.claude/trailhead/config.json`):
|
|
48
54
|
```json
|
|
49
55
|
{ "models": { "plan": "claude-opus-4-8", "execute": "claude-sonnet-5" }, "tdd": "seams", "acceptance": { "browser": "auto" } }
|
|
@@ -60,7 +66,7 @@ Example `config.json` (same shape for the project `.trailhead/config.json` and t
|
|
|
60
66
|
3. **🧠 Models**: **always two separate questions, one for `plan` and one for `execute`; never merge them into a single model pick.** Ask each as its own `AskUserQuestion` menu (current value pre-selected), and offer the **same versioned lineup** to both. Each option is a **full model id with its version**; never a bare family alias (`opus`/`sonnet`/`haiku`), which can't say *which* live version. Build the list from the models actually reachable **at setup time** (verify, don't trust this file verbatim), offering **at least the two most recent versions per family**, e.g. `claude-fable-5`, `claude-opus-5` / `claude-opus-4-8`, `claude-sonnet-5` / `claude-sonnet-4-8`, `claude-haiku-4-5`, plus **inherit session** (the default). If that lineup exceeds the menu's option cap, drill **family → version** or include a free-id "Other" so every recent version stays reachable. The rarer `research`/`review`/`debug` sit behind an "Advanced" option, each its own separate choice, same versioned rule.
|
|
61
67
|
- **Anti-pattern, never do this:** a single screen whose options are *combined* plan+execute presets, e.g. `🧠 Plan 4.8 / Exec Sonnet 5`, `🚀 Tutto Opus 5`, `Plan Opus 5 / Exec Sonnet 5`. That bundles the two decisions into one pick. Forbidden. Plan is asked, answered, and only then is Execute asked, as a second menu. Two questions, two answers, always.
|
|
62
68
|
- **On Codex (host-detected), ask `models.codex.*` instead of the Claude lineup.** Same discipline: `plan` and `execute` as two separate `AskUserQuestion` menus, never merged; the rarer `research`/`review`/`debug` behind Advanced, each its own choice. Offer OpenAI model ids verified reachable at setup time (do not trust this list verbatim): `gpt-5.6-sol` (flagship) / `gpt-5.6-terra` (balanced) / `gpt-5.6-luna` (fast), plus **inherit session** (default). Avoid the retiring `gpt-5.4` / `gpt-5.4-mini` and the deprecated `gpt-5.3-codex`. Write the answers to `models.codex.*`; the Claude `models.*` lineup is not asked on Codex (those ids cannot run there). After writing, tell the user to **re-run the installer** (the same `--host=codex` install command) so the pins are re-projected into `~/.codex/agents/trailhead-*.toml`: the menu updates `.trailhead/config.json`, the installer is what regenerates the TOMLs and enables `features.multi_agent_v2`.
|
|
63
|
-
4. **🎨 Design**: 💾 Local disk · 🖼️ claude.ai/design (
|
|
69
|
+
4. **🎨 Design**: 💾 Local disk · 🖼️ claude.ai/design (Anthropic's official Claude Design MCP, Claude only) · 🧵 Stitch (Google Stitch MCP, cross-host: Claude Code + Codex). *Mode only, don't ask for a URL here; the concrete project is created or reused at execution time and its projectId cached in `design.project` (see Prototype).* If claude.ai/design or Stitch is picked, ask the **🖼️ Design surface**: 🖌️ Canvas (a single project of screens to react to; default; **who makes it:** on claude.ai/design you do, the `/design` Artifact or a hand-made web-app project, not MCP-driven; on Stitch the MCP generates the screens) · 🧩 Design system (push components into a shared design-system project, for a shared library; **the MCP creates and syncs it**) → `design.surface`. Then **✅ Mockup approval**: 🖐️ Explicit (wait for a go-ahead before UI code) · ⏩ Auto (proceed without blocking).
|
|
64
70
|
5. **🧪 TDD**: 🎯 Seams only · ✅ On (all definable behaviour) · ⛔ Off.
|
|
65
71
|
6. **🖥️ Acceptance testing**: 🤖 Auto · ▶️ Always browser · 🙋 Guided UAT only. If browser is enabled, ask **🔗 base URL** and confirm it's a web app.
|
|
66
72
|
7. **🧑⚖️ Plan review**: **before presenting this menu, first probe the PATH for known external AI CLIs** (e.g. `gemini`, `codex`, and any others the Cross-AI plan review technique recognises): run the detection, then build the menu from what was actually found. Never present this step with a hard-coded two-option list; the options are computed from the probe:
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## Configuration
|
|
2
|
+
|
|
3
|
+
Three layers, **nearest wins**, key by key; a key unset at one layer inherits the next:
|
|
4
|
+
- **Project**: a `.trailhead/config.json` file at the repo root: overrides for this project, committed so a team shares one config. **Config never lives in the map issue**: the map is an index of decisions, while config is the user's to change at any time, so it stays in a plain file (and never asserted into the map by the agent).
|
|
5
|
+
- **Global**: `~/.claude/trailhead/config.json` (honour `$CLAUDE_CONFIG_DIR`): your standing defaults across every project.
|
|
6
|
+
- **Defaults**: the built-in values documented in `configuration-reference.md`.
|
|
7
|
+
|
|
8
|
+
Load the effective config at the start of a work session, **from the map's project root** (`.trailhead/config.json` at the superproject repo root) plus the global file, **before** any isolation workspace is set up. **Config is a property of the project/map, not the working directory**: an `isolation: worktree`/`clone` workspace **inherits** the config loaded here and must **never re-resolve it from the isolated copy's own root**, because a per-ticket clone (a submodule clone especially) has no `.trailhead/` of its own and would silently fall back to defaults (this is exactly how a `design: claude.ai/design` project ends up wrongly using `disk` in a clone). Config **writes** too (e.g. caching `design.project`) target the project's `.trailhead/config.json` in the original checkout, never the clone. **The main session orchestrates and holds the HITL moments** (the `build`/`bug` Discuss's "stop and ask", Verify's acceptance/UAT); **every model key runs as a subagent** (`plan`, `execute` (an *executor subagent* implementing with atomic commits on `models.execute`), `research`, `review`, `debug`) so the whole per-activity model split applies in one session whatever model it runs on. Run Execute inline only when `models.execute` is unset or equals the session model. **On a subagent-less host** (`subagentToolkit == none`) there is nothing to fan out into: every activity runs inline. **Separately, on a host that can't honour the model keys** (`honorsModelKeys == false`, e.g. Codex, whose subagents run on its own OpenAI models) every `models.*` key collapses to the single session model even though the activities still fan out; when any `models.*` is set there the engine surfaces a **one-time** session-start notice that the split is not taking effect (the keys stay in `.trailhead/config.json` for when the project next runs on Claude Code). On Codex the per-activity split is available through a parallel `models.codex.*` namespace (OpenAI model ids), the honoured namespace there (projected into per-technique `~/.codex/agents/*.toml` and dispatched at runtime by `agent_type`), while `models.*` (Claude ids) still collapse to the single session model; the collapse notice points at `models.codex.*` and goes silent once it is set. A host without a hook bus (`hookBus == none`) also runs a lightweight inline update check at session start (no SessionStart hook, no statusline). See `configuration-reference.md`. The **keys, their values, the model/tdd/acceptance semantics, and the guided menu setup live in `configuration-reference.md`**: read it when running `/trailhead:config` or when you need a key's exact meaning. `/trailhead:config` with no args runs the guided setup; `config get` prints the merged config; `config set <key> <value>` writes one key.
|