@marcomigozzi/trailhead 0.9.0 → 0.10.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 +9 -9
- package/README.md +9 -9
- package/bin/lib/codex-projection.js +13 -6
- package/bin/lib/codex-projection.test.js +45 -3
- package/bin/trailhead.js +12 -5
- package/bin/trailhead.test.js +198 -2
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/commands/quick.md +1 -1
- package/plugins/trailhead/skills/_shared/choices.md +3 -0
- package/plugins/trailhead/skills/_shared/configuration-reference.md +22 -15
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/session-handoff.md +8 -6
- package/plugins/trailhead/skills/_shared/teamwork.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +13 -4
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +8 -8
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +3 -1
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +12 -9
package/bin/trailhead.test.js
CHANGED
|
@@ -206,11 +206,38 @@ ok('source: trailhead-chart SKILL.md config-offer points at the next-step block'
|
|
|
206
206
|
// (session-handoff.md legitimately offers /trailhead:quick <n> for the next loose ticket).
|
|
207
207
|
const captureSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-capture', 'references', 'capture.md'), 'utf8');
|
|
208
208
|
ok('source: capture.md confirmation anchors the next step to /trailhead:work', /[Aa]nchor the next step to `\/trailhead:work <n>`, always/.test(captureSource));
|
|
209
|
-
ok('source: capture.md demotes /trailhead:quick to only the
|
|
210
|
-
/[Aa]nchor the next step to `\/trailhead:work <n>`[\s\S]{0,400}`\/trailhead:quick <n>` is \*\*only\*\* the
|
|
209
|
+
ok('source: capture.md demotes /trailhead:quick to only the no-split alternative, after the work anchor',
|
|
210
|
+
/[Aa]nchor the next step to `\/trailhead:work <n>`[\s\S]{0,400}`\/trailhead:quick <n>` is \*\*only\*\* the no-split alternative/.test(captureSource));
|
|
211
211
|
ok('source: capture.md says quick is offered alongside work, never in its place', captureSource.includes('never in its place'));
|
|
212
212
|
ok('source: capture.md keeps /trailhead:work the anchor even for a whiteboard capture', /whiteboard[\s\S]{0,200}the anchor stays `\/trailhead:work <n>`/.test(captureSource));
|
|
213
213
|
|
|
214
|
+
// --- #179: `quick <n>` on a map ticket is never "off the map" ----------------
|
|
215
|
+
// Source-level invariant: `quick <n>` on an EXISTING ticket does not leave the
|
|
216
|
+
// map (it keeps its trailhead:map-<n> label and sub-issue edge, and dependent-
|
|
217
|
+
// unblocking still runs); "off the map" describes only `quick "<text>"`, which
|
|
218
|
+
// opens a NEW whiteboard ticket. capture.md, session-handoff.md, and
|
|
219
|
+
// trailhead-work/SKILL.md must no longer carry the old "that same work done
|
|
220
|
+
// off the map" phrasing, nor describe `quick <n>` itself as off the map.
|
|
221
|
+
const sessionHandoffSource = fs.readFileSync(path.join(sourceSkillsDir, '_shared', 'session-handoff.md'), 'utf8');
|
|
222
|
+
const workSkillSourceFor179 = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-work', 'SKILL.md'), 'utf8');
|
|
223
|
+
const offMapQuickN = /`?quick <n>`?[^.\n]{0,80}off the map/i;
|
|
224
|
+
// The explicit "never describe ... as off the map" clause legitimately puts
|
|
225
|
+
// `quick <n>` near "off the map" in the same sentence, so strip it before
|
|
226
|
+
// running the negative proximity check below.
|
|
227
|
+
const neverDescribeClause = /[Nn]ever describe `quick <n>`[^\n]*?off the map\.?/;
|
|
228
|
+
const sessionHandoffMinusClause = sessionHandoffSource.replace(neverDescribeClause, '');
|
|
229
|
+
ok('source: capture.md drops the old "that same work done off the map" phrasing', !captureSource.includes('that same work done off the map'));
|
|
230
|
+
ok('source: session-handoff.md drops the old "that same work done off the map" phrasing', !sessionHandoffSource.includes('that same work done off the map'));
|
|
231
|
+
ok('source: trailhead-work SKILL.md drops the old "that same work done off the map" phrasing', !workSkillSourceFor179.includes('that same work done off the map'));
|
|
232
|
+
ok('source: capture.md never describes quick <n> as off the map', !offMapQuickN.test(captureSource));
|
|
233
|
+
ok('source: session-handoff.md never describes quick <n> as off the map (outside the explicit never-clause)', !offMapQuickN.test(sessionHandoffMinusClause));
|
|
234
|
+
ok('source: trailhead-work SKILL.md never describes quick <n> as off the map', !offMapQuickN.test(workSkillSourceFor179));
|
|
235
|
+
ok('source: session-handoff.md explicitly says never to describe quick <n> on a map ticket as off the map',
|
|
236
|
+
neverDescribeClause.test(sessionHandoffSource));
|
|
237
|
+
ok('source: capture.md says a map ticket stays on its map', /a map ticket stays on its map/.test(captureSource));
|
|
238
|
+
const ticketEnginesSourceFor179 = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-work', 'references', 'ticket-engines.md'), 'utf8');
|
|
239
|
+
ok('source: ticket-engines.md never labels quick generically as off-map', !/`quick`[^.\n]{0,40}off-map/.test(ticketEnginesSourceFor179));
|
|
240
|
+
|
|
214
241
|
// --- #151: eager edge drop at close (superseded / out-of-scope only) ---------
|
|
215
242
|
// Source-level invariant: closing a ticket as trailhead:superseded or
|
|
216
243
|
// trailhead:out-of-scope drops its native sub-issue edge at close (reusing the
|
|
@@ -255,6 +282,167 @@ const ticketEnginesSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhea
|
|
|
255
282
|
ok('source: ticket-engines.md resolved-close path does NOT drop the native sub-issue edge',
|
|
256
283
|
!/drop (its|any) native sub-issue edge/.test(ticketEnginesSource));
|
|
257
284
|
|
|
285
|
+
// --- #178: drive-mode ask (AI-driven vs human-driven) at every UAT start -----
|
|
286
|
+
// Single-sourced in techniques/acceptance-testing.md as the "Drive mode" bullet;
|
|
287
|
+
// every other call-site (ticket-engines.md, auto.md, choices.md) references it
|
|
288
|
+
// by name ("drive-mode ask") rather than restating its clauses. Extract the
|
|
289
|
+
// bullet's own span (its marker line through the end of its indented ` - `
|
|
290
|
+
// sub-bullets) and pin each clause inside that span, mirroring the #139/#146
|
|
291
|
+
// source-level anti-drift style.
|
|
292
|
+
function extractBulletSpan(source, markerLine) {
|
|
293
|
+
const lines = source.split('\n');
|
|
294
|
+
const startIdx = lines.findIndex((l) => l.includes(markerLine));
|
|
295
|
+
if (startIdx === -1) return '';
|
|
296
|
+
let endIdx = startIdx + 1;
|
|
297
|
+
while (endIdx < lines.length && lines[endIdx].startsWith(' - ')) endIdx++;
|
|
298
|
+
return lines.slice(startIdx, endIdx).join('\n');
|
|
299
|
+
}
|
|
300
|
+
const driveModeMarker = '- **Drive mode: ask who drives the UAT.**';
|
|
301
|
+
const acceptanceTestingSourcePath = path.join(sourceSkillsDir, '_shared', 'techniques', 'acceptance-testing.md');
|
|
302
|
+
const acceptanceTestingSource = fs.readFileSync(acceptanceTestingSourcePath, 'utf8');
|
|
303
|
+
const driveModeSpan = extractBulletSpan(acceptanceTestingSource, driveModeMarker);
|
|
304
|
+
ok('source: acceptance-testing.md carries the Drive-mode bullet', driveModeSpan.length > 0);
|
|
305
|
+
for (const phrase of [
|
|
306
|
+
'**before any step runs**',
|
|
307
|
+
'**wait for an explicit answer**',
|
|
308
|
+
'**silence or an ambiguous reply re-asks**',
|
|
309
|
+
'**One-path skip**',
|
|
310
|
+
'**state the reason in one line**',
|
|
311
|
+
'VERIFY',
|
|
312
|
+
'only for the steps it cannot perform',
|
|
313
|
+
'runs unchanged',
|
|
314
|
+
'**in both modes**',
|
|
315
|
+
'acceptance.browser: off',
|
|
316
|
+
'without the confirm gate',
|
|
317
|
+
]) {
|
|
318
|
+
ok(`source: acceptance-testing.md Drive-mode span pins "${phrase}"`, driveModeSpan.includes(phrase));
|
|
319
|
+
}
|
|
320
|
+
// The three start paths.
|
|
321
|
+
ok('source: Drive-mode span names build/bug Verify as a start path', driveModeSpan.includes('Verify'));
|
|
322
|
+
ok("source: Drive-mode span names the Pending-UAT guard's option (a) as a start path", driveModeSpan.includes('Pending-UAT guard'));
|
|
323
|
+
ok('source: Drive-mode span names a UAT follow-up (UAT of:) as a start path', driveModeSpan.includes('UAT of:'));
|
|
324
|
+
// The three performability classes.
|
|
325
|
+
ok('source: Drive-mode span names the physical-device/hardware performability class', driveModeSpan.includes('a physical device or hardware'));
|
|
326
|
+
ok('source: Drive-mode span names the credentials/accounts performability class', driveModeSpan.includes('credentials or accounts it does not hold'));
|
|
327
|
+
ok('source: Drive-mode span names the human sensory-judgement performability class', driveModeSpan.includes('a human sensory judgement'));
|
|
328
|
+
|
|
329
|
+
// The "Pending UAT must stay tracked" bullet references the ask for BOTH (a)
|
|
330
|
+
// run-now and (b) the follow-up (grouped: two mentions in the same bullet).
|
|
331
|
+
const pendingUatBulletMarker = '**Pending UAT must stay tracked: a closed ticket is not tracking.**';
|
|
332
|
+
const pendingUatLine = acceptanceTestingSource.split('\n').find((l) => l.includes(pendingUatBulletMarker)) || '';
|
|
333
|
+
ok('source: Pending-UAT bullet exists', pendingUatLine.length > 0);
|
|
334
|
+
ok('source: Pending-UAT bullet references the drive-mode ask for both (a) and the follow-up',
|
|
335
|
+
(pendingUatLine.match(/drive-mode ask/g) || []).length >= 2);
|
|
336
|
+
|
|
337
|
+
// Single-source anti-drift: the bullet's own signature phrase appears in
|
|
338
|
+
// exactly one file across the whole skills tree (mirrors #146).
|
|
339
|
+
const driveModeSignature = 'ask who drives the UAT';
|
|
340
|
+
const driveModeSignatureFiles = mdFilesUnder(sourceSkillsDir).filter((f) => fs.readFileSync(f, 'utf8').includes(driveModeSignature));
|
|
341
|
+
ok('source: "ask who drives the UAT" signature appears in exactly one skills-tree file', driveModeSignatureFiles.length === 1);
|
|
342
|
+
ok('source: that one file is techniques/acceptance-testing.md', driveModeSignatureFiles.length === 1 && driveModeSignatureFiles[0] === acceptanceTestingSourcePath);
|
|
343
|
+
|
|
344
|
+
// The Codex install projects acceptance-testing.md with the Drive-mode bullet.
|
|
345
|
+
ok('codex: skills/_shared/techniques/acceptance-testing.md is projected with the Drive-mode bullet',
|
|
346
|
+
fs.readFileSync(path.join(codexDir, 'skills', '_shared', 'techniques', 'acceptance-testing.md'), 'utf8').includes(driveModeMarker));
|
|
347
|
+
|
|
348
|
+
// --- #178 (2/4): route UAT follow-ups by pointer, engines through the ask ----
|
|
349
|
+
// A new type-independent section, keyed on the `UAT of:` pointer rather than
|
|
350
|
+
// the ticket's type label, must sit before the first per-type `## `decision``
|
|
351
|
+
// heading (work/quick/auto all dispatch through this file).
|
|
352
|
+
function extractSection(source, headingMarker) {
|
|
353
|
+
const start = source.indexOf(headingMarker);
|
|
354
|
+
if (start === -1) return '';
|
|
355
|
+
const rest = source.slice(start + headingMarker.length);
|
|
356
|
+
const nextHeadingIdx = rest.search(/\n## /);
|
|
357
|
+
return headingMarker + (nextHeadingIdx === -1 ? rest : rest.slice(0, nextHeadingIdx));
|
|
358
|
+
}
|
|
359
|
+
const uatFollowupHeadingMarker = '## UAT follow-ups (any type)';
|
|
360
|
+
const decisionHeadingMarker = '## `decision`';
|
|
361
|
+
ok('source: ticket-engines.md has the "UAT follow-ups (any type)" section', ticketEnginesSource.includes(uatFollowupHeadingMarker));
|
|
362
|
+
ok('source: the UAT follow-ups section precedes the first `decision` heading',
|
|
363
|
+
ticketEnginesSource.indexOf(uatFollowupHeadingMarker) !== -1 &&
|
|
364
|
+
ticketEnginesSource.indexOf(uatFollowupHeadingMarker) < ticketEnginesSource.indexOf(decisionHeadingMarker));
|
|
365
|
+
const uatFollowupSection = extractSection(ticketEnginesSource, uatFollowupHeadingMarker);
|
|
366
|
+
for (const phrase of ['UAT of:', 'whatever its type label', 'trailhead:seed', '`VERIFY` comment on the follow-up', 'acceptance-testing.md']) {
|
|
367
|
+
ok(`source: the UAT follow-ups section contains "${phrase}"`, uatFollowupSection.includes(phrase));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Both the build and bug engines' Verify step, and both Resolve steps'
|
|
371
|
+
// Pending-UAT guard, must route through the drive-mode ask by name (the
|
|
372
|
+
// single source stays acceptance-testing.md; these reference it).
|
|
373
|
+
const verifyStepLines = ticketEnginesSource.split('\n').filter((l) => l.trim().startsWith('4. **Verify**'));
|
|
374
|
+
ok('source: ticket-engines.md carries both the build and bug Verify step lines', verifyStepLines.length === 2);
|
|
375
|
+
ok('source: both Verify step lines reference the drive-mode ask', verifyStepLines.length === 2 && verifyStepLines.every((l) => l.includes('drive-mode ask')));
|
|
376
|
+
|
|
377
|
+
const pendingUatGuardLines = ticketEnginesSource.split('\n').filter((l) => l.includes('Pending-UAT guard (before the close)'));
|
|
378
|
+
ok('source: ticket-engines.md carries both build and bug Pending-UAT guard lines', pendingUatGuardLines.length === 2);
|
|
379
|
+
ok('source: both Pending-UAT guard lines reference the drive-mode ask', pendingUatGuardLines.length === 2 && pendingUatGuardLines.every((l) => l.includes('drive-mode ask')));
|
|
380
|
+
|
|
381
|
+
// auto.md mentions the ask, AND (plan-review addition) a single localized span
|
|
382
|
+
// states its outcomes TOGETHER: takes it without the confirm gate, picks
|
|
383
|
+
// AI-driven when any step is agent-performable, never fakes a human-only step,
|
|
384
|
+
// and sets the ticket aside listing them in the run-end summary.
|
|
385
|
+
const autoSourcePath = path.join(sourceSkillsDir, 'trailhead-work', 'references', 'auto.md');
|
|
386
|
+
const autoSource = fs.readFileSync(autoSourcePath, 'utf8');
|
|
387
|
+
ok('source: auto.md mentions the drive-mode ask', autoSource.includes('drive-mode ask'));
|
|
388
|
+
ok('source: auto.md states the drive-mode-ask outcomes together (grouped, not scattered)',
|
|
389
|
+
/drive-mode ask[\s\S]{0,120}without the confirm gate[\s\S]{0,300}AI-driven[\s\S]{0,200}agent-performable[\s\S]{0,300}never fake[\s\S]{0,300}run-end summary/.test(autoSource));
|
|
390
|
+
|
|
391
|
+
// choices.md carries the call-site row for the drive-mode ask.
|
|
392
|
+
const choicesSourcePath = path.join(sourceSkillsDir, '_shared', 'choices.md');
|
|
393
|
+
const choicesSource = fs.readFileSync(choicesSourcePath, 'utf8');
|
|
394
|
+
ok('source: choices.md call-site table carries the drive-mode ask row', /\|[^\n]*[Dd]rive-mode ask[^\n]*\|/.test(choicesSource));
|
|
395
|
+
|
|
396
|
+
// --- #178 (3/4): acceptance.browser off keeps AI-driven UAT via commands ----
|
|
397
|
+
const configReferencePath = path.join(sourceSkillsDir, '_shared', 'configuration-reference.md');
|
|
398
|
+
const configReferenceSource = fs.readFileSync(configReferencePath, 'utf8');
|
|
399
|
+
ok('source: configuration-reference.md acceptance.browser explanation clarifies off still runs AI-driven checks via commands',
|
|
400
|
+
/\*\*acceptance\.browser\.\*\*[\s\S]{0,1000}`off`[\s\S]{0,300}AI-driven[\s\S]{0,300}commands/.test(configReferenceSource));
|
|
401
|
+
ok('source: configuration-reference.md guided-setup menu no longer labels the off option "Guided UAT only"',
|
|
402
|
+
!configReferenceSource.includes('Guided UAT only'));
|
|
403
|
+
ok('source: configuration-reference.md guided-setup menu relabels the off option "No browser"',
|
|
404
|
+
/Acceptance testing[\s\S]{0,200}No browser/.test(configReferenceSource));
|
|
405
|
+
|
|
406
|
+
// --- #178 (4/4): mirror the UAT drive-mode ask in READMEs and /docs ---------
|
|
407
|
+
const readmeSource = fs.readFileSync(path.join(repoRoot, 'README.md'), 'utf8');
|
|
408
|
+
const readmeItSource = fs.readFileSync(path.join(repoRoot, 'README.it.md'), 'utf8');
|
|
409
|
+
const ticketTypesSource = fs.readFileSync(path.join(repoRoot, 'site', 'src', 'content', 'docs', 'docs', 'ticket-types.md'), 'utf8');
|
|
410
|
+
const workflowSource = fs.readFileSync(path.join(repoRoot, 'site', 'src', 'content', 'docs', 'docs', 'workflow.md'), 'utf8');
|
|
411
|
+
const configurationMdxSource = fs.readFileSync(path.join(repoRoot, 'site', 'src', 'content', 'docs', 'docs', 'configuration.mdx'), 'utf8');
|
|
412
|
+
|
|
413
|
+
ok('README.md build row frames acceptance testing as asking who drives', /🔨 `build`[\s\S]{0,400}who drives/.test(readmeSource));
|
|
414
|
+
ok('README.md worked example frames the login-flow acceptance as AI-driven', /asked who drives[\s\S]{0,100}AI-driven/.test(readmeSource));
|
|
415
|
+
ok('README.md acceptance.browser row says off still runs AI-driven checks', /`acceptance\.browser`[\s\S]{0,400}AI-driven/.test(readmeSource));
|
|
416
|
+
|
|
417
|
+
ok('README.it.md build row frames acceptance testing as asking who drives (chi guida)', /🔨 `build`[\s\S]{0,400}chi guida/.test(readmeItSource));
|
|
418
|
+
ok('README.it.md worked example frames the login-flow acceptance as AI-driven', /chi guida[\s\S]{0,100}AI-driven/.test(readmeItSource));
|
|
419
|
+
ok('README.it.md acceptance.browser row says off still runs AI-driven checks', /`acceptance\.browser`[\s\S]{0,400}AI-driven/.test(readmeItSource));
|
|
420
|
+
|
|
421
|
+
ok('site ticket-types.md build row frames acceptance testing as asking who drives', /`build`[\s\S]{0,400}who drives/.test(ticketTypesSource));
|
|
422
|
+
ok('site workflow.md worked example frames the login-flow acceptance as AI-driven', /asked who drives[\s\S]{0,100}AI-driven/.test(workflowSource));
|
|
423
|
+
ok('site configuration.mdx acceptance.browser row says off still runs AI-driven checks', /`acceptance\.browser`[\s\S]{0,400}AI-driven/.test(configurationMdxSource));
|
|
424
|
+
|
|
425
|
+
// --- #178 fix: keep the drive-mode ask ahead of URL resolution --------------
|
|
426
|
+
// Code review found the acceptance.browser clause made the drive-mode ask
|
|
427
|
+
// CONDITIONAL on URL resolution ("fall back to the drive-mode ask ... only
|
|
428
|
+
// when none can be determined"), contradicting the single source
|
|
429
|
+
// (acceptance-testing.md: the ask fires before any step runs, always) and
|
|
430
|
+
// conflating it with the URL-not-found fallback (ask for the URL, or guided
|
|
431
|
+
// UAT). The clause must instead state the ask always comes first and never
|
|
432
|
+
// gate it on URL detection.
|
|
433
|
+
ok('source: configuration-reference.md acceptance.browser clause does NOT make the drive-mode ask conditional on URL resolution',
|
|
434
|
+
!configReferenceSource.includes('fall back to the drive-mode ask'));
|
|
435
|
+
ok('source: configuration-reference.md acceptance.browser clause states the drive-mode ask always comes first',
|
|
436
|
+
/\*\*acceptance\.browser\.\*\*[\s\S]{0,200}drive-mode ask[\s\S]{0,120}always comes first/.test(configReferenceSource));
|
|
437
|
+
|
|
438
|
+
// The run-end summary's enumerated set-aside category list is missing the new
|
|
439
|
+
// human-only-UAT-steps class (added by #178); it must be listed there too,
|
|
440
|
+
// matching the wording already used for it at line 29 ("human-only steps").
|
|
441
|
+
const runEndSummarySection = extractSection(autoSource, '## Run-end summary');
|
|
442
|
+
ok('source: auto.md Run-end summary section exists', runEndSummarySection.length > 0);
|
|
443
|
+
ok('source: auto.md Run-end summary "what remains" list includes human-only UAT steps',
|
|
444
|
+
/what remains[\s\S]{0,400}human-only UAT steps/.test(runEndSummarySection));
|
|
445
|
+
|
|
258
446
|
// --- codex hooks (#29) --------------------------------------------------------
|
|
259
447
|
const codexHooksJsonPath = path.join(codexDir, 'hooks.json');
|
|
260
448
|
ok('codex: hooks.json exists', fs.existsSync(codexHooksJsonPath));
|
|
@@ -271,6 +459,14 @@ ok('codex: skills/trailhead/hooks/lib/commit-message-check.js exists (commit-gua
|
|
|
271
459
|
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'commit-message-check.js')));
|
|
272
460
|
ok('codex: skills/trailhead/hooks/lib/gh-subcommand.js exists (body-guard/secret-guard require target, #153)',
|
|
273
461
|
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'gh-subcommand.js')));
|
|
462
|
+
// #169: search-guard is Claude-Code-specific (it only prevents arming
|
|
463
|
+
// Claude's Read() deny rule under bypass permissions, a mechanism Codex
|
|
464
|
+
// lacks), so it must neither be registered nor copied on a Codex install.
|
|
465
|
+
ok('codex: hooks.json does NOT register search-guard under any event', !codexHooksJsonStr.includes('trailhead-search-guard.js'));
|
|
466
|
+
ok('codex: skills/trailhead/hooks/trailhead-search-guard.js does NOT exist',
|
|
467
|
+
!fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'trailhead-search-guard.js')));
|
|
468
|
+
ok('codex: skills/trailhead/hooks/lib/shell-scan.js still exists (secret-read-guard require target)',
|
|
469
|
+
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'shell-scan.js')));
|
|
274
470
|
|
|
275
471
|
const codexConfigTomlPath = path.join(codexDir, 'config.toml');
|
|
276
472
|
ok('codex: config.toml exists', fs.existsSync(codexConfigTomlPath));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marcomigozzi/trailhead",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trailhead",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Start and drive large projects as a map of tickets on GitHub Issues, resolving one at a time. Map-based onboarding + a discuss→plan→execute→verify engine with atomic commits, all on the Issues. Ships as a dispatcher plus cohesion-clustered skills over a shared core; depends on nothing beyond an authenticated gh CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Marco Migozzi",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Work one ticket whole,
|
|
2
|
+
description: Work one ticket whole, never splits: a new whiteboard ticket from text, or an existing one; grills only if needed
|
|
3
3
|
argument-hint: "[ticket | \"text\"]"
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -46,6 +46,8 @@ This is the operational recipe every applicable call-site follows. It is **manda
|
|
|
46
46
|
- Delegate -> "Let the agent pick (with rationale, then confirm)" (IT: "Lascia scegliere all'agente (con motivazione, poi conferma)", short "scegli tu").
|
|
47
47
|
- Defer -> "Defer this decision" (IT: "Rimanda questa decisione", short "scelgo dopo").
|
|
48
48
|
|
|
49
|
+
**Menu call-sites (an `AskUserQuestion` trailhead authors): the dedup pass.** A menu may batch several questions (the host tool accepts up to four), and nothing else collapses them. Before assembling the menu, dedup the batch: when two questions resolve the SAME underlying decision (one restates or subsumes the other), merge them into a single question instead of asking the same problem twice; keep genuinely distinct questions separate. The discriminator is "same decision", not merely "same topic": two questions that share a subject but settle different things stay separate. This pass runs once over the whole batch, before the per-question cap rule below.
|
|
50
|
+
|
|
49
51
|
**Menu call-sites (an `AskUserQuestion` trailhead authors): the cap rule.** The host tool accepts only 2-4 real options (plus its own auto escape), so the affordances can be crowded out. The rule is deterministic: **fill up to the cap, then fall back to prose.**
|
|
50
52
|
- If **(real options + applicable affordances) <= 4**, add each applicable affordance as an explicit extra option in the same menu.
|
|
51
53
|
- If they **would exceed 4**, keep the real options in the menu and offer the affordance(s) that did not fit in **one line of prose immediately after the menu** (or as an immediate follow-up question). The cap never silently swallows them.
|
|
@@ -66,6 +68,7 @@ Worked examples: 2 real options + delegate + defer = 4 -> all in the menu; 3 rea
|
|
|
66
68
|
| `/trailhead config` menus | Yes | No (no substrate) |
|
|
67
69
|
| Cross-AI plan-review convergence | Yes | Sometimes (a concern blocked on a prerequisite) |
|
|
68
70
|
| Verify checkpoint (which fix / how to proceed) | Yes for the advisory sub-choice; accept/resolve stays the human's | No |
|
|
71
|
+
| Acceptance testing drive-mode ask (AI-driven vs human-driven) | Yes | No (now-or-never at UAT start) |
|
|
69
72
|
| Prototype "want mockups?" | Yes | No |
|
|
70
73
|
| Teamwork split shape / submodule split | Yes (the shape is advisory) | No |
|
|
71
74
|
| Claim collision | No (coordination stop) | No |
|
|
@@ -16,7 +16,9 @@ Effective config = built-in defaults ← global `~/.claude/trailhead/config.json
|
|
|
16
16
|
| `plan_review` | **`off`** \| `on` \| CLI list (`gemini,codex`) | send `build` PLANs to external AI CLIs for review (**Cross-AI plan review**) |
|
|
17
17
|
| `plan_review.rounds` | integer (**`2`**) | max converge-and-re-review rounds |
|
|
18
18
|
| `code_review.rounds` | integer (**`2`**) | max fix-and-re-review rounds in **Code review** before the boundary HITL checkpoint |
|
|
19
|
-
| `effort` | **`standard`** \| `lean` | how hard the `build`/`bug
|
|
19
|
+
| `effort` | **`standard`** \| `lean` | how hard the `build`/`bug` engine works a change: `standard` runs every step; `lean` size-triages and offers to skip heavy steps (plan pass, TDD, cross-AI review, full code review, goal-backward verify) on small changes. **Governs `build`/`bug` only; `quick` always size-triages regardless of this key** (see effort below) |
|
|
20
|
+
|
|
21
|
+
**`git`, `release`, and `isolation` are not config.json keys.** They are **conventions-header keys**, stored in the repo's `trailhead:conventions` issue header (documented in `substrate.md`), and are reachable from the guided setup's **Way of working** step, not from this table.
|
|
20
22
|
|
|
21
23
|
**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:
|
|
22
24
|
- `plan` / `execute`: the `build`/`bug` engine's **Plan** and **Execute** steps (see below).
|
|
@@ -39,7 +41,7 @@ If a value is unset, it resolves to its **Balanced**-profile default for that ac
|
|
|
39
41
|
|
|
40
42
|
**Everything else runs in the main session on its own model, there is no key for it:** charting, grilling, naming the destination, framing and **writing the tickets**, graduating fog, and map/decision operations. That's deliberate: it's HITL/orchestration work (you reason *with* the user), so the model that writes a ticket is the one grilling you; it can't fan out to a subagent. Practical upshot: **run a charting session (`/trailhead:new`/`:adopt`/`:ticket`) on your strong model**. The `models.*` keys above govern only the fan-out activities, not the quality of the tickets themselves. **Store a full model id** (e.g. `claude-opus-5`, `claude-fable-5`), never a bare family alias like `opus`; a family now spans several live versions, so an alias can't say which one. (On Claude Code the `Agent` tool takes only a family alias, so at subagent spawn the stored id is mapped to its alias, see the dispatch rule in `techniques.md`; the full id is still what gets stored, so the exact version stays on record and drives the inline-vs-subagent decision, even though the alias-only spawn cannot convey that version to the subagent.) **`models.plan` and `models.execute` are always distinct choices**: never collapse them into one shared model pick, even when the user would pick the same id for both.
|
|
41
43
|
|
|
42
|
-
**Model-tier profiles.** Three profile seeds for the guided setup's Models step (§
|
|
44
|
+
**Model-tier profiles.** Three profile seeds for the guided setup's Models step (§4), one philosophy: *smart at every level* (research is never the strongest tier; execute and debug are never the fastest). Choosing a profile **applies all seven keys directly** (no further per-key questions); the separate **Manual** path is where `plan`/`execute`/the rest are picked one by one. **The default profile is `Balanced`** (the **(consigliato)** one): a project with no config resolves its seven model keys as Balanced. **High** and **Low** are the alternatives, and **Manual** lets you set each key yourself (including `inherit session` per key). `fix` follows `execute`, so it needs no profile entry of its own.
|
|
43
45
|
|
|
44
46
|
| profile | plan | execute | research | review | debug | verify | codebase-map |
|
|
45
47
|
|---|---|---|---|---|---|---|---|
|
|
@@ -55,7 +57,7 @@ If a value is unset, it resolves to its **Balanced**-profile default for that ac
|
|
|
55
57
|
|
|
56
58
|
**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.
|
|
57
59
|
|
|
58
|
-
**acceptance.browser.** `auto` (default) = drive
|
|
60
|
+
**acceptance.browser.** The drive-mode ask (AI-driven vs human-driven, see the Acceptance testing technique) **always comes first, before any step runs**: no value of this key pre-answers it. This key only governs whether and how an AI-driven step uses the browser once that ask is answered AI-driven: `auto` (default) = browser-drive when `testing.webapp` + `testing.url` are set, or (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; when no URL can be determined, fall back per the Acceptance testing technique (ask the human for the URL, or the guided UAT for that step); `on` = always attempt browser-driving, auto-detecting the URL the same way when `testing.url` is unset; `off` = never drive a browser. **`off` disables *browser-driving*, not acceptance, and not AI-driven checking**: AI-driven still runs its non-browser checks via commands / the test suite for whatever steps allow it; only a step that genuinely needs the human (or the whole UAT, under human-driven) gets the guided UAT walkthrough. The Verify step honours this.
|
|
59
61
|
|
|
60
62
|
**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`.
|
|
61
63
|
|
|
@@ -65,12 +67,12 @@ If a value is unset, it resolves to its **Balanced**-profile default for that ac
|
|
|
65
67
|
|
|
66
68
|
**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`.
|
|
67
69
|
|
|
68
|
-
**effort (cost-aware gating).** `standard` (default) = the full flow: every `build`/`bug
|
|
70
|
+
**effort (cost-aware gating).** `standard` (default) = the full flow for the map cycle: every `build`/`bug` step runs at full cost, no triage (today's behaviour, backward-compatible). `lean` = **cost-aware**, defined **canonically here**; the `build`/`bug` engine references this section under `lean` and adds only its own timing. **`quick` is the exception: it runs the size triage and the offer below on *every* ticket, independent of `effort`** (reaching for `quick` is itself the small-piece signal; `effort` governs the map cycle, not `quick`, see `../trailhead-work/SKILL.md`). Under `lean` (and always for `quick`) the engine runs a **size triage** in the orchestrator (main session, inline, no subagent, so the triage itself costs almost nothing), classifying the change predictively from the ticket scope and a quick glance at the target files (the real diff is unknown before Execute):
|
|
69
71
|
- **trivial**: a single file, a handful of lines, no new seam, no new dependency, no architectural change (a copy/CSS/doc/constant edit).
|
|
70
72
|
- **small**: localized, one or two files, within existing seams, no new dependency, no architectural change.
|
|
71
73
|
- **normal**: anything else. **Risk floor:** a change is **never** trivial/small, however few its lines, when it touches security or auth, a data migration, a public API or contract, money or permissions, or concurrency, or when it adds or updates a dependency; such a change is always `normal`. When in doubt, classify up.
|
|
72
74
|
|
|
73
|
-
On **trivial/small** the engine surfaces **one consolidated skip offer** and acts only on an **explicit opt-in**: it names the concrete heavy steps proposed to skip for *this* change and the safety trade-off, then goes lean **only on a clear yes**. On a no, on silence, or on any ambiguity, it keeps the **full flow** (the fail-safe is always toward more checking, never less). The heavy steps it may offer to skip are the **plan subagent** (replaced by a short inline plan, never zero plan), **TDD**, **Cross-AI plan review**, the full adversarial **Code review** subagent (replaced by a lighter inline pass), and the **Goal-backward verification** subagent (replaced by a quick inline goal check); the two inline replacements are dropped entirely only when the change is truly cosmetic. The offer lists **only steps actually in play** for this ticket and config (it will not mention Cross-AI plan review when `plan_review` is off, or TDD when `tdd: off`). On **normal
|
|
75
|
+
On **trivial/small** the engine surfaces **one consolidated skip offer** and acts only on an **explicit opt-in**: it names the concrete heavy steps proposed to skip for *this* change and the safety trade-off, then goes lean **only on a clear yes**. On a no, on silence, or on any ambiguity, it keeps the **full flow** (the fail-safe is always toward more checking, never less). The heavy steps it may offer to skip are the **plan subagent** (replaced by a short inline plan, never zero plan), **TDD**, **Cross-AI plan review**, the full adversarial **Code review** subagent (replaced by a lighter inline pass), and the **Goal-backward verification** subagent (replaced by a quick inline goal check); the two inline replacements are dropped entirely only when the change is truly cosmetic. The offer lists **only steps actually in play** for this ticket and config (it will not mention Cross-AI plan review when `plan_review` is off, or TDD when `tdd: off`). On **normal** there is no offer and the full flow runs; the `build`/`bug` engine likewise runs the full flow with no triage under `effort: standard`, whereas `quick` always triages (it just finds nothing to offer on a `normal` ticket). This offer **always includes** the "let the agent pick (with rationale, then confirm)" delegate option, per the injection recipe in `choices.md`.
|
|
74
76
|
|
|
75
77
|
**Never in the offer** (always kept, whatever the size or the answer): atomic `Refs:` commits, running the tests / the plan's verification criterion, the post-commit self-check, `bug`'s Diagnose (a confirmed root cause is mandatory before Fix), and Resolve. `effort` scales *redundant heavy* work to the change; it never drops a correctness check.
|
|
76
78
|
|
|
@@ -83,28 +85,33 @@ Example `config.json` (same shape for the project `.trailhead/config.json` and t
|
|
|
83
85
|
|
|
84
86
|
## Guided setup
|
|
85
87
|
|
|
86
|
-
`/trailhead config` (no args) runs an **interactive walkthrough**: never make the user hand-edit JSON. Present each step as an `AskUserQuestion` menu with **icon-labelled options** and the current value pre-selected; write the answers to the chosen scope at the end and show a summary. These menus **offer** the delegate option (agent picks, with rationale, then confirm), per the injection recipe in `choices.md`: as an explicit menu option where the cap allows, otherwise in a prose line right after the menu (several of these steps already list 4 or more real options, so there delegate is offered in prose).
|
|
88
|
+
`/trailhead config` (no args) runs an **interactive walkthrough**: never make the user hand-edit JSON. Present each step, the config **scope** step 1 included, as an `AskUserQuestion` menu with **icon-labelled options** and the current value pre-selected (the scope is a selectable choice too, not a free-text prose lead-in); write the answers to the chosen scope at the end and show a summary. The **Way of working** step (step 2) is the one exception to "the chosen scope": its keys (`git`/`release`/`isolation`) are written to the repo's `trailhead:conventions` issue header, never to `.trailhead/config.json`, because they are repo-shared conventions, not per-scope config. These menus **offer** the delegate option (agent picks, with rationale, then confirm), per the injection recipe in `choices.md`: as an explicit menu option where the cap allows, otherwise in a prose line right after the menu (several of these steps already list 4 or more real options, so there delegate is offered in prose).
|
|
87
89
|
|
|
88
90
|
**Never skip a step.** Walk every step in order **and** every conditional sub-step its branch activates; never omit one, and **never silently take a default for a step the flow reaches**. The pre-selected value is a convenience, not licence to not ask; a step counts as done only once the user has actually answered it. If a choice opens follow-ups (e.g. *pick specific CLIs* → which ones **and** how many rounds; *browser on* → base URL), ask **each** before moving on.
|
|
89
91
|
|
|
90
|
-
1. **Scope
|
|
91
|
-
2.
|
|
92
|
-
|
|
92
|
+
1. **Scope** (a selectable `AskUserQuestion` menu like every config-key step below, rendered as a numbered list on Codex and the native picker on Claude, **never a free-text prose question**): 📁 This project (`.trailhead/config.json` at the repo root, the in-repo default) · 🌍 Global default (`~/.claude/trailhead/config.json`). Skip the menu and write global directly only when run outside a repo (there is no project scope to offer there).
|
|
93
|
+
2. **⚙️ Way of working** (`git` · `release` · `isolation`): the conventions-header keys that govern how work is committed, released, and isolated. **These live in the repo's `trailhead:conventions` issue header, not in `.trailhead/config.json`**, so they are written there (repo-scoped, shared by every map of this repo), independent of the Scope chosen in step 1. **Skip this step entirely when run outside a repo** (there is no conventions issue to write). Present each as its own `AskUserQuestion` menu, current header value pre-selected:
|
|
94
|
+
- **🔀 Git**: 🌱 `main` (commit straight to the trunk) · 🌿 `pr` (feature branch + PR).
|
|
95
|
+
- **🚀 Release**: 🎯 `command` (release only on an explicit command) · ♻️ `auto` (release automatically per the project's flow).
|
|
96
|
+
- **🧬 Isolation**: 🤖 `auto` (default; decide per ticket at work-start) · 📍 `none` (work in the current checkout) · 🌳 `worktree` (a `git worktree` + `trailhead/t<n>` branch per ticket) · 📦 `clone` (a dedicated per-ticket clone, for path-bound apps a worktree can't build and for submodule superrepos).
|
|
97
|
+
Write any changed value back into the `trailhead:conventions` issue header (a body read-modify-write: obey the **Fail-safe body rewrites** rule in `substrate.md`), leaving the human prose below the header untouched. **If no `trailhead:conventions` issue exists yet** (a repo not charted/adopted): say conventions are normally set at `/trailhead:new`/`:adopt`, and offer to create a minimal header issue with the chosen values, or skip. Creating it here first runs the **first-use repo setup** (see `substrate-commands.md`) so the `trailhead:conventions` label exists before `gh issue create --label trailhead:conventions` (an uncharted repo has no `trailhead:*` labels yet, so the create would otherwise fail).
|
|
98
|
+
3. **🌐 Ticket language**: the language trailhead writes its GitHub prose & commit descriptions in (`ticket.language`, ISO 639-1). Offer 🇬🇧 English (`en`, default) · 🇮🇹 Italiano (`it`) · 🇪🇸 Español (`es`) · 🇫🇷 Français (`fr`) · 🇩🇪 Deutsch (`de`) · 🇵🇹 Português (`pt`), plus a free-code option for any other ISO 639-1 code. Note in the prompt that this is *not* the language the agent converses in.
|
|
99
|
+
4. **🧠 Models**: a **profile** (Balanced by default) applied whole, or **Manual** for the per-key questions.
|
|
93
100
|
- **🎚️ Profile, asked first (and often the only Models question).** Present one `AskUserQuestion` menu: 🏔️ **High** · ⚖️ **Balanced (consigliato)** · 🍃 **Low** · ✍️ **Manual (specify each model)**. **On a project with no existing config, pre-select ⚖️ Balanced** (the default profile); when a config already exists, pre-select the profile its keys match (or **Manual** when they match none). **On High / Balanced / Low this is terminal:** seed the seven model keys from the **Model-tier profiles** table above, resolving each tier-class to a concrete id **at setup time** (verify the lineup, don't trust ids verbatim), **write them, and do NOT ask the per-key questions**. Only **Manual** proceeds to the per-key questions below (where any key may be left at `inherit session`). **Host branch:** on **Claude** the profile seeds `models.*` with tier-class ids; on **Codex** it seeds `models.codex.*` as `{model, effort}` (effort is the Codex-only face), and after applying keep the **re-run the installer** instruction from the Codex per-key path below so the pins are re-projected into `~/.codex/agents/trailhead-*.toml`.
|
|
94
101
|
- **The per-key questions (Manual path only): 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 no-override option, the session model). 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`/`verify`/`codebase-map` sit behind an "Advanced" option, each its own separate choice, same versioned rule.
|
|
95
102
|
- **Anti-pattern, never do this (in the Manual path):** 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. In Manual, Plan is asked, answered, and only then is Execute asked, as a second menu: two questions, two answers, always. (A **named tier profile** applied whole is **not** this anti-pattern: it is a coherent per-key tier assignment chosen deliberately, not an ad-hoc plan+execute combo, which is why picking a profile is terminal.)
|
|
96
103
|
- **On Codex (host-detected), the Manual path asks `models.codex.*` instead of the Claude lineup.** Same discipline: `plan` and `execute` as two separate `AskUserQuestion` menus, never merged; the rarer `research`/`review`/`debug`/`verify`/`codebase-map` 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`.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
5. **🎨 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).
|
|
105
|
+
6. **🧪 TDD**: 🎯 Seams only · ✅ On (all definable behaviour) · ⛔ Off.
|
|
106
|
+
7. **⚖️ Effort**: how hard the `build`/`bug` engine works each change (`effort`). 🛠️ **Standard** (default; every step runs) · 🍃 **Lean** (size-triage small changes and offer to skip heavy steps, on an explicit yes only: the plan pass, TDD, cross-AI review, the full code review, the goal-backward verify; correctness checks always stay). Governs `build`/`bug` only: `quick` always size-triages regardless.
|
|
107
|
+
8. **🖥️ Acceptance testing**: 🤖 Auto · ▶️ Always browser · 🚫 No browser (acceptance still runs, starting with the drive-mode ask; only browser-driving is off, see `acceptance.browser` above). If browser is enabled, ask **🔗 base URL** and confirm it's a web app.
|
|
108
|
+
9. **🧑⚖️ Plan review**: **before presenting this menu, first probe the PATH for known external AI CLIs** (probe **every** CLI in the Cross-AI plan review roster, do not shorten the list: `claude`, `gemini`, `codex`, `qwen`, `kimi`, `agy`, `opencode`, `cursor-agent`, `coderabbit`, plus any OpenAI-compatible local server configured; `kimi` counts only when `kimi --help` mentions `--output-format`, per the technique): `command -v` each one, then build the menu from what was actually found. **Exclude the current host's own CLI** (never offer `codex` when running on Codex, nor `claude` when running on Claude Code): a session reviewing a plan with its own model is not cross-AI, so the host's own CLI must never appear as an option. Never present this step with a hard-coded two-option list; the options are computed from the probe:
|
|
102
109
|
- **No CLI found**: offer only ⛔ Off, and say in the prompt that no external AI CLI was detected on the PATH (so the other choices would do nothing).
|
|
103
110
|
- **One CLI found**: ⛔ Off · 🤝 Use `<name>`. (With a single CLI, "all" and "pick specific" collapse to the same thing, so don't show both.)
|
|
104
111
|
- **Two or more found**: ⛔ Off · 🤝 All detected (`<name>`, `<name>`, …) · 🎯 Pick specific. Always show all three; never drop *Pick specific*. On *Pick specific*, ask **which CLIs** as a **multi-select** listing each detected CLI by name (never a free-text field). List the actual names found by the probe, not the placeholder examples from this file.
|
|
105
112
|
|
|
106
113
|
**Anything but Off has mandatory follow-ups, never skip them:** after the CLIs are settled (whether via *All detected* or *Pick specific*), ask **🔁 rounds** (default 2). Off is the only branch with no follow-up; every other path asks rounds explicitly rather than taking the default.
|
|
107
|
-
|
|
114
|
+
10. **📊 Statusline**: offer trailhead's **Claude Code status bar**, and **say in the prompt what it shows** so the choice is informed. It renders one line: **model · project · branch · plan usage (`5h %` · `⏳ reset` · `7d %`) · a context-window bar (`362k/1000k (45%)`)**, plus a **second line with the active ticket** (`▸ #N Title`) whenever one is in play (from the `trailhead/t<n>` branch or the `.trailhead/session-ticket` marker). The plan-usage % comes from Anthropic's own usage endpoint (the same source the Claude UI uses); the context bar and ticket are computed locally. Options: ⛔ **Off** (default; leave your statusline untouched) · ✅ **Enable**.
|
|
108
115
|
- On **Enable**, install it: copy the shipped script (`${CLAUDE_PLUGIN_ROOT}/templates/statusline-trailhead.sh` for a plugin install, or `~/.claude/trailhead/templates/statusline-trailhead.sh` for an npm install, whichever exists) to `~/.claude/statusline-trailhead.sh` (executable), then set `statusLine` in `~/.claude/settings.json` to `{ "type": "command", "command": "bash \"$HOME/.claude/statusline-trailhead.sh\"" }`. **This is a global Claude Code setting, not per-project**, so it always lands in `~/.claude/settings.json` regardless of the scope chosen in step 1.
|
|
109
116
|
- **If a `statusLine` is already configured** (e.g. `ccstatusline`), **do not clobber it silently**: show what it is and ask whether to **replace** it with trailhead's, or **keep** the current one (in which case, mention that the script also supports single-segment widgets, `--ticket-only` / `--context-only` / `--usage-only`, to add into an existing statusline tool). Needs `jq` and `python3` on the PATH; if either is missing, say so and skip the enable rather than wiring a broken command.
|
|
110
117
|
|
|
@@ -13,4 +13,6 @@ Before doing anything, read the shared core, in order:
|
|
|
13
13
|
5. `../_shared/configuration.md`: the three config layers and the load contract.
|
|
14
14
|
6. `../_shared/techniques.md`: the technique index and the subagent-type rule.
|
|
15
15
|
|
|
16
|
+
**Host command forms (standing render rule).** Every trailhead command you write in prose you generate at runtime, the session handoff, a capture confirmation, any next-step suggestion, and not only where a doc example already shows one, uses **this host's command forms**: `/clear` to clear context, and the `/trailhead:<verb>` family (`/trailhead:work <n>`, `/trailhead:quick <n>`, `/trailhead:map`, `/trailhead:dashboard`). These literals are this host's surface. **Never emit a command form that belongs to a different host** (one you may have read elsewhere in this repository, such as in the projection code and its tests, or earlier in the session): the forms stated in this contract are the only ones correct for the host you are running on.
|
|
17
|
+
|
|
16
18
|
Load the **effective config** (per `../_shared/configuration.md`) at session start, from the map's project root (`.trailhead/config.json` plus the global file), before any isolation workspace is set up.
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
## Session handoff
|
|
2
2
|
|
|
3
|
-
Resolving a ticket **ends the session**: one ticket per session is a hard rule (see [Principles](principles.md)). So every resolution closes the same way. After the resolution comment + `gh issue close` + the `Decisions so far` update, **remove the session-ticket marker** (`.trailhead/session-ticket` at the working root, if you wrote one, see Mode 2), then **always** sign off with, in order:
|
|
3
|
+
Resolving a ticket **ends the session**: one ticket per session is a hard rule (see [Principles](principles.md)). So every resolution closes the same way. Render every command in this ritual in this host's command forms (the **Host command forms** rule in `load-first.md`). After the resolution comment + `gh issue close` + the `Decisions so far` update, **remove the session-ticket marker** (`.trailhead/session-ticket` at the working root, if you wrote one, see Mode 2), then **always** sign off with, in order:
|
|
4
4
|
|
|
5
|
-
1. **A short recap of what was done** (2 to 4 lines) for the resolved ticket: the change in plain language, the files or areas touched (when applicable), and the outcome (verified / released / next step). Keep it **result-oriented**: say what changed and what it means, never trailhead's internal machinery (no mode or step names, no engine-role or technique names). It **augments** the ritual (it does not replace anything below): the recap sits above the one-line confirmation and gives the user a quick account of the ticket without reconstructing it from the commits or the issue timeline. It rides with a **resolution** only; a decision-ticket resolution may touch no files, hence "when applicable", and a capture or research batch (which never reach this ritual) carry no recap.
|
|
5
|
+
1. **A short recap of what was done** (2 to 4 lines) for the resolved ticket: the change in plain language, the files or areas touched (when applicable), and the outcome (verified / released / next step). Keep it **result-oriented**: say what changed and what it means, never trailhead's internal machinery (no mode or step names, no engine-role or technique names). It **augments** the ritual (it does not replace anything below): the recap sits above the one-line confirmation and gives the user a quick account of the ticket without reconstructing it from the commits or the issue timeline. It rides with a **resolution** only; a decision-ticket resolution may touch no files, hence "when applicable", and a capture or research batch (which never reach this ritual) carry no recap. It also carries forward any **pending follow-up** the Pending follow-up guard below identified, so a follow-up surfaced while working never has to be reconstructed from scrollback.
|
|
6
6
|
2. **A one-line confirmation** the ticket is resolved, by name (not a bare `#number`).
|
|
7
7
|
3. **The next step: a scannable block whose FIRST line is `/clear`, always.** Never bury it in prose, and **never point at the next ticket without leading with `/clear`**. `/clear` is its own first bullet, phrased as the first thing to do ("fai `/clear`, poi..."): **never demote it to a trailing parenthetical** on the next command. `/trailhead:work 29 (dopo /clear)` is the bug, not the fix: the reader runs the command and only then reads the note, when the context is already dirty. State the `/clear` **every time** (not only when context is heavy): it must come *before* the next command, on its own line first, so the next ticket starts on clean context. Render it as a short labelled list in the user's conversation language, along these lines:
|
|
8
8
|
|
|
9
9
|
> **Prossimo passo:**
|
|
10
10
|
> - `/clear`: per liberare il contesto
|
|
11
|
-
> - poi `/trailhead:work <numero>` per lavorare **<nome del prossimo ticket>** (oppure `/trailhead:work` che prende **<numero>** in automatico)
|
|
11
|
+
> - poi `/trailhead:work <numero>` per lavorare **<nome del prossimo ticket>** (il ciclo completo; oppure `/trailhead:work` che prende **<numero>** in automatico)
|
|
12
12
|
> - in alternativa `/trailhead:map` per la vista completa e scegliere tu, o `/trailhead:dashboard` per l'indice di tutte le mappe e del whiteboard
|
|
13
|
-
> - oppure `/trailhead:quick
|
|
13
|
+
> - oppure `/trailhead:quick "<il pezzo>"` per lavorare un pezzo fuori mappa: lo stesso lavoro di `work`, senza split, aperto e portato a termine subito
|
|
14
14
|
|
|
15
|
-
Name the next frontier ticket so the user knows what they're taking; put its **number** in the command (never the title). **When the frontier holds several mutually independent tickets, don't crown one: present them as a set to choose from**, naming each with its number (see **Frontier order carries no priority**). Bare `/trailhead:work` still acts on one, but the block keeps the choice visible. The block is the list of things to do **after the `/clear`** (it leads with it) and stays on screen in the terminal to read from, so each command must be either self-sufficient (a number already known) or safe to type by hand. **This self-sufficiency governs *every* line after `/clear`, not only the commands: never sequence after `/clear` an action the *agent* must perform that depends on context `/clear` will erase** (an "I'll open the ticket for you" gated on a decision taken this session). `/clear` wipes that context, so the agent cannot honour it and the user is stuck: clearing loses what the step needs, while not clearing violates the `/clear`-first rule. When an agent-side follow-up is genuinely needed, resolve it one of two ways: **do the agent action *before* the handoff** (perform it now, then render the `/clear`-first block pointing at its result), or **convert the next step into a user-runnable command** (`/trailhead:work <n>` with the resulting number, `/trailhead:map` for the user to choose, or the diverge-first `/trailhead:ticket` framing); never "I'll do it after you clear." The commands a handoff block may contain are `/clear`, `/trailhead:work` (bare or `<number>`), `/trailhead:map`, `/trailhead:dashboard`, and `/trailhead:quick`; **in a whiteboard session** (the resolved ticket was `trailhead:whiteboard`), also `/trailhead:whiteboard`. **Always offer the read-only views alongside the next action**: `/trailhead:map` for the low-res map and `/trailhead:dashboard` for the repo-wide index (all maps + the whiteboard), so the user can look before choosing. **`/trailhead:quick` is the one command that may carry a free-text argument here**: `/trailhead:quick "<short seed>"` when a concrete off-map piece is actually at hand (its argument is forgiving freeform seed text, not an exact title to match, so pre-filling it is helpful, not error-prone), or **bare `/trailhead:quick`** as the generic "if you want to work a piece off the map" offer, which prompts for the piece itself; keep any seed short and quote-safe. **`/trailhead:work` always takes the number, never the title** of an existing ticket, and **never hand the user a `/trailhead:ticket <type> "<title>"`** (or any other title-bearing command): adding a map ticket needs the diverge-first framing (Mode 3), so it is never pre-filled.
|
|
15
|
+
Name the next frontier ticket so the user knows what they're taking; put its **number** in the command (never the title). **When the frontier holds several mutually independent tickets, don't crown one: present them as a set to choose from**, naming each with its number (see **Frontier order carries no priority**). Bare `/trailhead:work` still acts on one, but the block keeps the choice visible. The block is the list of things to do **after the `/clear`** (it leads with it) and stays on screen in the terminal to read from, so each command must be either self-sufficient (a number already known) or safe to type by hand. **This self-sufficiency governs *every* line after `/clear`, not only the commands: never sequence after `/clear` an action the *agent* must perform that depends on context `/clear` will erase** (an "I'll open the ticket for you" gated on a decision taken this session). `/clear` wipes that context, so the agent cannot honour it and the user is stuck: clearing loses what the step needs, while not clearing violates the `/clear`-first rule. When an agent-side follow-up is genuinely needed, resolve it one of two ways: **do the agent action *before* the handoff** (perform it now, then render the `/clear`-first block pointing at its result), or **convert the next step into a user-runnable command** (`/trailhead:work <n>` with the resulting number, `/trailhead:map` for the user to choose, or the diverge-first `/trailhead:ticket` framing); never "I'll do it after you clear." The commands a handoff block may contain are `/clear`, `/trailhead:work` (bare or `<number>`), `/trailhead:map`, `/trailhead:dashboard`, and `/trailhead:quick`; **in a whiteboard session** (the resolved ticket was `trailhead:whiteboard`), also `/trailhead:whiteboard`. **Always offer the read-only views alongside the next action**: `/trailhead:map` for the low-res map and `/trailhead:dashboard` for the repo-wide index (all maps + the whiteboard), so the user can look before choosing. **`/trailhead:quick` is the one command that may carry a free-text argument here**: `/trailhead:quick "<short seed>"` when a concrete off-map piece is actually at hand (its argument is forgiving freeform seed text, not an exact title to match, so pre-filling it is helpful, not error-prone), or **bare `/trailhead:quick`** as the generic "if you want to work a piece off the map" offer, which prompts for the piece itself; keep any seed short and quote-safe. **Describe `work` and `quick` correctly wherever the block characterizes them:** `work` is the full cycle (Discuss -> Plan -> Execute -> Verify, with TDD during Execute per `config.tdd`); `quick <n>` works that same existing ticket whole via the lighter, no-split path (always size-triaged), a map ticket stays on its map; only `quick "<text>"` opens a new ticket off the map (the whiteboard). Never describe `quick <n>` on a map ticket as off the map. Neither is "the split command" (that is the separate `split` verb), so never frame `work` as splitting. Anchor `/trailhead:work <n>` first and offer `/trailhead:quick <n>` alongside as the no-split alternative, never in its place and never rendered twice on one line. **`/trailhead:work` always takes the number, never the title** of an existing ticket, and **never hand the user a `/trailhead:ticket <type> "<title>"`** (or any other title-bearing command): adding a map ticket needs the diverge-first framing (Mode 3), so it is never pre-filled.
|
|
16
16
|
|
|
17
|
-
**If the frontier is empty**, that usually means fog that this resolution made specifiable should have been **graduated into a ticket already** (Mode 2 step 6), so the frontier isn't actually empty. If it genuinely is (nothing ticketable yet, or the user must choose which fog item to take next), point the block to **`/trailhead:map`** and offer to open the next ticket *yourself* once they say which one (named by its number in the numbered `/trailhead:map` fog list), rather than pre-filling a `/trailhead:ticket` command for them. **Fog still on the map takes priority over loose whiteboard work.** If the map still has fog (unspecified areas in *Not yet specified*), lead with **`/trailhead:map`** to dissolve it (its fog list numbers each entry, so a specific point can be named by number): that fog is the map's own remaining work, so graduating it comes before any off-map ticket. **Only when the map is genuinely complete** (frontier empty *and* no fog left) and the whiteboard still holds open loose tickets, do not sign off as if nothing remains: check for them and, if any exist, **surface them**, pointing the block at **`/trailhead:whiteboard`** (or **`/trailhead:dashboard`** for the full index) and naming the loose tickets, anchored on **`/trailhead:work <n>`** (the full
|
|
17
|
+
**If the frontier is empty**, that usually means fog that this resolution made specifiable should have been **graduated into a ticket already** (Mode 2 step 6), so the frontier isn't actually empty. If it genuinely is (nothing ticketable yet, or the user must choose which fog item to take next), point the block to **`/trailhead:map`** and offer to open the next ticket *yourself* once they say which one (named by its number in the numbered `/trailhead:map` fog list), rather than pre-filling a `/trailhead:ticket` command for them. **Fog still on the map takes priority over loose whiteboard work.** If the map still has fog (unspecified areas in *Not yet specified*), lead with **`/trailhead:map`** to dissolve it (its fog list numbers each entry, so a specific point can be named by number): that fog is the map's own remaining work, so graduating it comes before any off-map ticket. **Only when the map is genuinely complete** (frontier empty *and* no fog left) and the whiteboard still holds open loose tickets, do not sign off as if nothing remains: check for them and, if any exist, **surface them**, pointing the block at **`/trailhead:whiteboard`** (or **`/trailhead:dashboard`** for the full index) and naming the loose tickets, anchored on **`/trailhead:work <n>`** (the full cycle Discuss -> Plan -> Execute -> Verify; for a whiteboard ticket it works the ticket whole, and split stays available only if it proves oversized, but `work` is not itself the split command) with `/trailhead:quick <n>` offered alongside as the no-split alternative (the same ticket, worked whole via the lighter no-split path), never in its place. **In a whiteboard session** the fog-graduation reasoning does not apply (the whiteboard has no fog): point the block at **`/trailhead:whiteboard`**, anchored on **`/trailhead:work <n>`** for the next loose ticket (the full cycle; split stays available only if it proves oversized) with `/trailhead:quick <n>` alongside as the no-split alternative (the same ticket, worked whole via the lighter no-split path), not the map. (The example is Italian; match whatever language you're conversing in.)
|
|
18
18
|
|
|
19
19
|
**Pending codebase offer (greenfield): resolve it *before* this block, exactly once.** If this session's Work-the-map step 1 marked a greenfield codebase-issue offer **pending** (the repo has no `trailhead:codebase` issue but now holds substantial code), surface the offer and resolve it **before rendering the `/clear`-first next-step block**, never as a line inside that block. This is deliberate: its "yes" triggers an **agent action** (generate and pin the codebase issue), and the self-sufficiency rule above forbids an agent-side action sequenced after `/clear`, so the offer is settled while the agent still holds context, exactly the "do the agent action before the handoff" resolution that rule prescribes. It is **an offer the user accepts with a yes, not a command they type** (there is no `/trailhead:codebase` command), so pose it as a yes/no, along the lines of *"vuoi che generi l'issue codebase adesso? (una mappa del repo, fissata tra i pin)"* (in the user's conversation language). Ask it **exactly once**: on a yes, generate and pin it now via the **Codebase map** technique, *then* render the `/clear`-first block (which may point at the freshly pinned issue); on a no or silence, render the block without it and don't nag again this session (the pending state is re-derived from repo state each session, so an offer left unresolved, whether declined or simply unanswered, re-surfaces next session until a `trailhead:codebase` issue exists, and nothing is lost). A whiteboard handoff never carries it (even if `work <n>` ran step 1 against a background map, the offer belongs to that map, not this loose ticket).
|
|
20
20
|
|
|
21
|
+
**Pending follow-up guard: re-pose it *before* this block, exactly once.** If, while working this ticket, a **follow-up worth opening surfaced but was not opened** (a divergent or out-of-scope request raised at the [work brief gate](../trailhead-work/SKILL.md) or mid-work under *Scope that surfaces while working a ticket*, that the user deferred or left unsettled rather than capturing on the spot), **carry it into the recap and re-pose the choice at the handoff, before rendering the `/clear`-first block**, never as a line inside that block. This mirrors the **Pending-UAT guard** (the `build`/`bug` Resolve step): a surfaced-but-unopened follow-up gets the same "do not let it die silently" guarantee. Pose it as an explicit two-way choice, in the user's conversation language: **(a) open a tracked ticket for it now** (a `todo` / `bug` / `seed` via the capture cluster, or a map ticket via the chart cluster's `/trailhead:ticket`, as fits), or **(b) let it go, or do it later as `/trailhead:quick`**. Keep **both** paths on the table: `quick` alone is not the whole offer, opening a tracked ticket must stay available, and that is the difference from the plain next-step block, which only offers `quick`. Because opening a ticket is an **agent action**, settle it here while the agent still holds context (exactly the "do the agent action before the handoff" resolution the self-sufficiency rule above prescribes, since `/clear` would erase what a post-clear capture needs): on **(a)**, open the ticket now (via the capture cluster for a `todo`/`bug`/`seed`, or the chart cluster's `/trailhead:ticket` for a map ticket), *then* render the `/clear`-first block anchored on `/trailhead:work <n>` for the freshly opened ticket (with `/trailhead:quick <n>` alongside as the no-split alternative, never in its place); on **(b)**, render the block without it, and when a concrete piece is at hand offer `/trailhead:quick "<the follow-up>"` as the off-map alternative. Re-pose it **exactly once** and **wait for an explicit answer**: never open a tracked ticket without the user's explicit yes. On **silence or an ambiguous reply**, treat it as (b) (render the block, offer `quick`) and do not nag again, but never silently invent a ticket; a follow-up the user explicitly lets go is likewise not nagged again. When no such follow-up surfaced this guard does not fire.
|
|
22
|
+
|
|
21
23
|
**This block is required whenever you point the user at the next ticket**, including in a **later follow-up** (after answering their questions, not only in the instant you resolve the ticket) **and in a capture's confirmation line** when it tells the user how to work the just-captured ticket. Never write "next: `/trailhead:work …`" without the `/clear` before it. **A capture is not a resolution**, so its confirmation needn't render the full labelled block: a compact, future-conditional one-liner is right, along the lines of *"to work it later: `/clear`, then `/trailhead:work 45`"* (in the user's conversation language), but the `/clear`-before-`/trailhead:work` order is non-negotiable there too. A bare `/trailhead:work 45` with no `/clear`, or a `/trailhead:work 45 (dopo /clear)` that hides `/clear` in a trailing parenthetical instead of leading with it, is the bug: `/clear` is always the first thing, on its own, before the next command.
|
|
22
24
|
|
|
23
25
|
This ritual fires from every **Resolve** step (`build`, `bug`) and from **Mode 2**'s close (which resolves `decision` / `prototype` / `task` too). It does **not** fire after a `capture` (resolves nothing, so no recap either) or a parallel `research` batch (AFK, not session-ending).
|
|
@@ -21,7 +21,7 @@ Whoever claims a ticket **owns it end to end**: they work it and close it themse
|
|
|
21
21
|
Claiming stops two sessions from working the *same* ticket; it does nothing about two sessions working *different* tickets in the **same checkout**. The working tree and the git index are process-global: two agents editing and committing in one clone corrupt each other's state regardless of ticket type (a `bug` is no more or less exposed than a `build`). The claim is the ticket-level lock; the **worktree is the filesystem-level one**.
|
|
22
22
|
|
|
23
23
|
The repo's `trailhead:conventions` `isolation:` header decides:
|
|
24
|
-
- **`auto`** (default): resolve the mode at a ticket's **work-start, before any edit**. Ask once whether more than one ticket will be worked at once on this machine this session (this **is** the `clone` gate below, generalised to the whole mode, so a `clone` resolution never re-asks). **No** (a lone ticket): work in the current checkout, exactly like `none`. **Yes**: resolve by repo shape, a plain repo -> `worktree`, a **superrepo** (a `.gitmodules` at the checkout root that declares at least one submodule; equivalently `git submodule status` lists one) -> `clone` (the superproject clone + submodule provisioning below). Because trailhead resolves **one ticket per session**, the ask fires at most once per ticket work-start (so once per session in normal one-ticket-per-session work; a within-session ticket switch, discouraged, re-asks at the new ticket's work-start); the autonomous run (`/trailhead:auto`) **never asks** (it works tickets sequentially, so there is no concurrency) and resolves `auto` to the current checkout. `auto` is a **runtime interpretation**: the engine never writes `isolation: auto` into the conventions.
|
|
24
|
+
- **`auto`** (default): resolve the mode at a ticket's **work-start, before any edit**. First run a read-only, best-effort **assigned-ticket pre-check** to inform (never decide) the ask: `gh issue list --assignee @me --state open --json number,title,body`, then drop the current ticket's own number from the result (the claim precedes this resolution, so the current ticket is already assigned to `@me`, and must be excluded or the pre-check always sees a false "other"); on any `gh` error skip the pre-check and put the ask neutrally rather than failing the work. An assigned ticket is a **heuristic** for concurrent work, never proof (it may be a live parallel session or a parked/paused claim). **0 other assigned tickets**: put the ask neutrally, as below. **One or more other open assigned tickets**: **pre-bias** the ask, name the already-assigned ticket(s) and default the answer toward *yes* (`worktree`/`clone`), but **still ask**, with an easy neutral/no; a ticket whose `Scope:` is absent is presented as *overlap unknown*, never as evidence of safe concurrency. **Skip the ask entirely only in the one unambiguous, self-referential case**: the session is already inside **this** ticket's linked worktree, that is all of (a) `git rev-parse --git-common-dir` differs from `git rev-parse --git-dir` (a linked worktree), (b) the checked-out branch is exactly `trailhead/t<N>` with `N` equal to the current ticket number, and (c) the working-root `.trailhead/session-ticket` marker names the current ticket; then isolation is already this ticket's operating mode, so resolve to that worktree and announce it without asking. A per-ticket **clone** is deliberately **not** a skip trigger (no reliable read-only predicate separates a dedicated clone from the primary checkout switched onto a `trailhead/t<n>` branch), so it takes the pre-bias-and-ask path. This pre-check is read-only and changes no `worktree`/`clone` mechanic, and **scope overlap with another in-progress ticket is out of its remit** (the existing serialise / undeclared-coupling handling owns that). Ask once whether more than one ticket will be worked at once on this machine this session (this **is** the `clone` gate below, generalised to the whole mode, so a `clone` resolution never re-asks). **No** (a lone ticket): work in the current checkout, exactly like `none`. **Yes**: resolve by repo shape, a plain repo -> `worktree`, a **superrepo** (a `.gitmodules` at the checkout root that declares at least one submodule; equivalently `git submodule status` lists one) -> `clone` (the superproject clone + submodule provisioning below). Because trailhead resolves **one ticket per session**, the ask fires at most once per ticket work-start (so once per session in normal one-ticket-per-session work; a within-session ticket switch, discouraged, re-asks at the new ticket's work-start); the autonomous run (`/trailhead:auto`) **never asks** and never runs the pre-check (it works tickets sequentially, so there is no concurrency) and resolves `auto` to the current checkout. `auto` is a **runtime interpretation**: the engine never writes `isolation: auto` into the conventions.
|
|
25
25
|
- **`none`**: one session at a time per checkout. Simplest, and correct for a repo worked one ticket at a time. Two concurrent sessions here must use **separate clones**, or they collide.
|
|
26
26
|
- **`worktree`**: each executing ticket runs in its own `git worktree` on a `trailhead/t<n>` branch (`git worktree add ../<repo>-t<n> -b trailhead/t<n>`), so concurrent sessions never share a working tree or index. Because git refuses to check out one branch in two worktrees, **worktree isolation implies a branch per ticket even under `git: main`**: the trunk is never checked out twice. At **Resolve**, integrate the branch to the trunk per `git:` (fast-forward or merge into `main`, or open the PR when `git: pr`), then `git worktree remove` it. On **Pause**, the `PAUSED` checkpoint records the worktree path and branch so a resume (by anyone, once released) lands back in it.
|
|
27
27
|
- **`clone`**: each executing ticket works in its own **independent working copy** at `../<repo>-t<n>` on a `trailhead/t<n>` branch (for a submodule-bearing superrepo the clone is of the **superproject** with `--recurse-submodules`, never the submodule alone, so the gitlink bump has a home and each ticket gets independent submodule state; see **Git submodules force `clone`** below). That is the point: it has its own installed dependencies, so it **builds and UATs like the real thing**: the isolation for **path-bound apps a worktree can't run** (React Native/Expo, native toolchains, a local `node_modules`). **A clone is heavy (a full clone + an install), so gate it every time: before cloning, ask whether more than one ticket will be worked at once on this machine this session. If not, skip the clone and work this ticket in the current checkout** (isolation only guards concurrent workspaces; a lone ticket has nothing to collide with). If yes, set it up at work-start by **whichever is faster**:
|
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
# Acceptance testing
|
|
2
2
|
Automated tests prove the code does what the *code* intended; acceptance testing proves it does what the *user* wanted. After tests + code review pass, decide whether the change needs acceptance testing (user-facing behaviour usually does; a pure refactor or internal util usually doesn't) and pick the cheapest sufficient path:
|
|
3
3
|
|
|
4
|
-
- **Automated first, but it does NOT discharge acceptance for user-facing behaviour.** Run the ticket's test/criterion and the suite. Passing tests prove *code-intent*; they close acceptance only for a change the user never sees (refactor, internal util). For anything the user perceives (a new screen, data that now appears/updates, a changed flow) automated green is *not* "done": you still owe a browser-drive or a guided UAT.
|
|
5
|
-
- **
|
|
4
|
+
- **Automated first, but it does NOT discharge acceptance for user-facing behaviour.** Run the ticket's test/criterion and the suite. Passing tests prove *code-intent*; they close acceptance only for a change the user never sees (refactor, internal util). For anything the user perceives (a new screen, data that now appears/updates, a changed flow) automated green is *not* "done": you still owe the drive-mode ask below, then a browser-drive, AI-driven checks, or a guided UAT.
|
|
5
|
+
- **Drive mode: ask who drives the UAT.** Every UAT start puts one explicit ask to the human, **before any step runs**: AI-driven or human-driven (plus the standard delegate option, per `../choices.md`; no defer, this is now-or-never at UAT start). It fires at three start paths: `build`/`bug` Verify, when acceptance testing is owed; the Pending-UAT guard's option (a) (run the UAT now); and resolving a UAT follow-up (any ticket carrying `UAT of:` in its body). The ask must **wait for an explicit answer**: **silence or an ambiguous reply re-asks**, it never defaults to either mode. Never fires when acceptance is not owed.
|
|
6
|
+
- **One-path skip**: a step is agent-performable unless it needs something the agent cannot access this session: a physical device or hardware, credentials or accounts it does not hold, or a human sensory judgement (feel, sound, haptics, subjective look), evaluated over the checklist before asking. No step performable: skip the ask, go human-driven, and **state the reason in one line** naming the blocking class. Any step performable: ask.
|
|
7
|
+
- **AI-driven.** Per step: browser-drive when drivable (the URL resolution below), else commands, the suite, or scripted checks; record each result and its evidence in the `VERIFY` comment as it goes; ask the human only for the steps it cannot perform, one at a time, conversationally, exactly like the guided walkthrough below.
|
|
8
|
+
- **Human-driven.** The guided conversational walkthrough below runs unchanged: one step at a time, wait for the human's report, never a checklist to self-serve.
|
|
9
|
+
- Applies **in both modes**: the failing-step handling, the partial-run / pending tracking, and the mockup-fidelity check below apply the same way, whichever mode drove the run.
|
|
10
|
+
- **Config.** `acceptance.browser: off` disables browser-driving, not AI-driven checks run via commands or the test suite, and it does not pre-answer this ask: the ask still fires, and an AI-driven answer still runs its non-browser checks.
|
|
11
|
+
- **`auto` takes this ask as the delegate option without the confirm gate** (per `../choices.md`): AI-driven when any step is agent-performable; human-only steps stay incomplete and the ticket is set aside, listing them in the run-end summary (never faked); no performable step means the whole UAT is human-only and the ticket is set aside. Full autonomous-run behaviour lives in `../../trailhead-work/references/auto.md`.
|
|
12
|
+
- **AI-driven, browser-drivable web app → drive it yourself.** Governed by `config.acceptance.browser` + `config.testing`: when it's `auto`/`on` and `testing.webapp` is set with `testing.url` set or resolvable (see below), use the browser-automation tools to exercise the actual change end-to-end (navigate, fill, click, assert visible state; capture a screenshot into the `VERIFY` comment). Prefer this over asking the human when the flow is scriptable. Never trigger native/JS dialogs that would wedge the session.
|
|
6
13
|
- **Auto mode resolves the URL before falling back.** Under `acceptance.browser: auto`/`on`, when `testing.webapp` is set but `testing.url` is not, do not drop straight to asking: first try to determine the target URL from the project. Probe, cheapest first, a dev server already running (the likely local ports, or a server log / PID the project writes), package scripts (`dev`/`start`/`preview` in `package.json` or the project's task runner), dev-server and framework config (the configured host/port in e.g. Vite/Next/Astro/Expo config), and project config. When one is found, **propose it** ("acceptance will run against `http://localhost:5173`, correct?") for the user to accept or correct, then drive the browser against the accepted URL. Only when no URL can be determined fall back to asking the human, or to the guided UAT below.
|
|
7
|
-
- **
|
|
14
|
+
- **Human-driven, or an AI-driven step that isn't performable → guided UAT is MANDATORY, and it is CONVERSATIONAL.** This is the human-driven mode from the drive-mode ask above, and it is also what an AI-driven run falls back to for the specific step(s) it cannot perform: when `config.acceptance.browser` is `off`, or no target URL could be determined (the auto attempt above included), or the target isn't browser-drivable (a mobile/native app, a CLI, real hardware, real credentials), **`off` means "acceptance via a guided walkthrough," not "skip acceptance."** For any user-facing change you MUST **run the human through the UAT one step at a time, in the chat**: state step 1 (what to do, where to tap/look, the expected result), then **stop and wait** for the human to report what they actually saw; only then move to step 2. It is a dialogue *you* drive, turn by turn, not a list you hand over. **Anti-pattern, never do this:** post the full numbered checklist and point the user at the ticket to self-serve ("follow the 9 steps in the comment of #NN"). That is exactly the failure this rule kills: the checklist is the *record*, never the *interaction*. Mirror each step's outcome into the `VERIFY` comment as you go; don't close on green you didn't actually see.
|
|
8
15
|
- **On a failing step: stop, take note, open the closing ticket then and there.** A failed step is an acceptance finding, not a footnote: record expected-vs-seen, and immediately open the follow-up that will carry it to resolution: a `bug` ticket (with `Regression of:`/`Surfaced from:` as fits), or a fix on this ticket before it resolves if it's small and in-scope. Then ask the human whether to keep going through the remaining steps or stop here. Never silently log a fail and carry on to Resolve.
|
|
9
16
|
- **When only some steps are runnable now** (e.g. a few need a backend deployed first), walk the runnable ones conversationally **now**, and mark the rest **explicitly pending** with what unblocks them, tracked as below. Don't defer the whole UAT just because part of it is blocked.
|
|
10
17
|
- **When the human can only run it later** (on-device, hardware you don't have right now), record the full numbered checklist in the `VERIFY` comment as the durable artifact and mark UAT **explicitly pending**, but the walkthrough stays conversational **when they come to run it**: never shrink it to a vague one-liner ("just check it on the device"), and never let "automated passed" quietly stand in for it. Deferring *where/when* the human runs it is fine; replacing the live step-by-step with a self-serve checklist is not.
|
|
11
|
-
- **Pending UAT must stay tracked: a closed ticket is not tracking.** Don't resolve/close a user-facing ticket on unconfirmed acceptance and leave the checklist as a comment on the closed issue, where nobody will look. The default is to **keep the ticket open** until the human confirms the UAT passes (closing on green you didn't see is a premature close). **
|
|
18
|
+
- **Pending UAT must stay tracked: a closed ticket is not tracking.** Don't resolve/close a user-facing ticket on unconfirmed acceptance and leave the checklist as a comment on the closed issue, where nobody will look. The default is to **keep the ticket open** until the human confirms the UAT passes (closing on green you didn't see is a premature close). **When UAT hasn't been run at Resolve, pose the choice to the human and wait for an explicit answer, never decide it for them:** (a) **run the UAT now**, in this session, with no ticket (opens with the drive-mode ask above, then the guided conversational walkthrough or AI-driven checks, resolving only on a confirmed pass; a failing step becomes a fix or a `bug` follow-up and an incomplete/blocked step is marked pending and tracked, per the failing-step and partial-run rules above, never a silent close), or (b) **open a UAT ticket** to run it later. **Never open a UAT ticket without asking, and never let silence or an ambiguous reply default to either path.** Only when the human picks (b) do you **open a follow-up ticket before that close** that carries the pending UAT and links back to the parent with a `UAT of: <parent name>(link)` pointer in the body. Route it the way `../choices.md` routes a deferred choice: a **`trailhead:seed`** (kept blocked) when the run is gated on a future external trigger not yet true (hardware you don't have, a device out of reach), or a follow-up **`task`** on the frontier when it's just scheduled or batched work you could pick up any time. Either way the follow-up **carries the full numbered checklist and, when it comes to resolution, opens with the drive-mode ask above before running the guided conversational walkthrough**, never a self-serve check-off. **Never close a user-facing ticket on a chosen-deferred UAT with nothing opened**: the parent may close as agreed, but the owed UAT stays visible on the tracker as its own ticket, linked to the parent, until it is actually performed. The pending check must be visible on the tracker, not buried.
|
|
19
|
+
|
|
20
|
+
**Mockup-fidelity check (when the ticket had an approved mockup).** If the change was preceded by an approved mockup (the **Prototype** technique produced one and it was approved and recorded at Execute), acceptance testing MUST check the built screen **against that approved mockup**, not merely against "what the user wanted" in the abstract. Pull the mockup's current state to compare against (re-fetch it in `claude.ai/design` / `stitch` mode via the technique's re-fetch rule, or open the linked `disk` mockup); never compare from memory. Walk it **element by element and state by state**, layout, spacing, sizing, colour, typography, copy, and every state it shows (empty, loading, error, hover, active, disabled, and the like), enumerating from the approved mockup itself (the spec) and cross-checking the executor's recorded Execute-step fidelity list and any element it flagged infeasible, and confirm each is reproduced. Record the per-element and per-state result in the `VERIFY` comment. Any **unforced** deviation is an acceptance finding, never waved through as "close enough"; a genuinely infeasible part must already have been raised with the user at Execute (per the Prototype technique), not surface here as a silent drift.
|
|
12
21
|
|
|
13
22
|
Acceptance failures are findings like any other: a `bug` follow-up or a fix on this ticket before it resolves.
|
|
14
23
|
|