@marcomigozzi/trailhead 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.it.md +52 -14
  2. package/README.md +53 -15
  3. package/bin/auto-intro-docs.test.js +72 -0
  4. package/bin/auto-merge-docs.test.js +33 -0
  5. package/bin/auto-merge-ticket-docs.test.js +32 -0
  6. package/bin/auto-review-rounds-docs.test.js +45 -0
  7. package/bin/auto-run-docs.test.js +60 -0
  8. package/bin/auto-scope-docs.test.js +33 -0
  9. package/bin/auto-slots-docs.test.js +45 -0
  10. package/bin/auto-uat-docs.test.js +47 -0
  11. package/bin/config-dir-templates-docs.test.js +52 -0
  12. package/bin/decision-builds-docs.test.js +75 -0
  13. package/bin/fog-vs-ticket-docs.test.js +56 -0
  14. package/bin/grilling-holistic-review-docs.test.js +90 -0
  15. package/bin/lib/codex-projection.js +13 -6
  16. package/bin/lib/codex-projection.test.js +25 -2
  17. package/bin/lib/host-descriptor.js +14 -1
  18. package/bin/lib/host-descriptor.test.js +18 -0
  19. package/bin/lib/runtime-command-literals.test.js +184 -0
  20. package/bin/needs-you-docs.test.js +41 -0
  21. package/bin/pick-stress-test-docs.test.js +105 -0
  22. package/bin/report-status-docs.test.js +1 -1
  23. package/bin/trailhead.js +3 -3
  24. package/bin/trailhead.test.js +17 -18
  25. package/package.json +1 -1
  26. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  27. package/plugins/trailhead/agents/trailhead-code-review.md +2 -2
  28. package/plugins/trailhead/agents/trailhead-verify.md +2 -2
  29. package/plugins/trailhead/commands/auto.md +2 -2
  30. package/plugins/trailhead/hooks/lib/auto-merge.test.js +467 -0
  31. package/plugins/trailhead/hooks/lib/auto-queue.test.js +245 -0
  32. package/plugins/trailhead/hooks/lib/auto-run.test.js +318 -0
  33. package/plugins/trailhead/hooks/lib/auto-slot.test.js +278 -0
  34. package/plugins/trailhead/hooks/lib/checkout-in.test.js +173 -0
  35. package/plugins/trailhead/hooks/lib/dashboard.test.js +66 -3
  36. package/plugins/trailhead/hooks/lib/merge-ticket.test.js +394 -0
  37. package/plugins/trailhead/hooks/lib/model-defaults-review.js +50 -21
  38. package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +50 -0
  39. package/plugins/trailhead/hooks/lib/needs-you.test.js +177 -0
  40. package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +319 -0
  41. package/plugins/trailhead/hooks/lib/report-status.test.js +68 -1
  42. package/plugins/trailhead/hooks/lib/shell-scan.js +25 -0
  43. package/plugins/trailhead/hooks/lib/statusline-context.test.js +161 -0
  44. package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
  45. package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
  46. package/plugins/trailhead/hooks/lib/unmodelled.js +6 -1
  47. package/plugins/trailhead/hooks/mod/README.md +185 -39
  48. package/plugins/trailhead/hooks/mod/auto-run-view.ts +93 -0
  49. package/plugins/trailhead/hooks/mod/auto-run.ts +165 -0
  50. package/plugins/trailhead/hooks/mod/map-pane-view.ts +150 -25
  51. package/plugins/trailhead/hooks/mod/map-pane.tsx +557 -99
  52. package/plugins/trailhead/hooks/mod/register.tsx +298 -39
  53. package/plugins/trailhead/hooks/mod/repo-pin.ts +167 -0
  54. package/plugins/trailhead/hooks/mod/session-registry.ts +65 -0
  55. package/plugins/trailhead/hooks/mod/turn-state.ts +14 -0
  56. package/plugins/trailhead/hooks/mod/whiteboard-follow.ts +98 -0
  57. package/plugins/trailhead/hooks/mod/work-state-locate.ts +191 -0
  58. package/plugins/trailhead/hooks/mod/work-state-prompt.ts +13 -15
  59. package/plugins/trailhead/hooks/mod/work-state-view.ts +21 -5
  60. package/plugins/trailhead/hooks/mod/worktree-porcelain.ts +17 -0
  61. package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
  62. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +22 -16
  63. package/plugins/trailhead/hooks/trailhead-labels-guard.js +231 -0
  64. package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
  65. package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +336 -23
  66. package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +321 -0
  67. package/plugins/trailhead/skills/_shared/choices.md +7 -2
  68. package/plugins/trailhead/skills/_shared/configuration-reference.md +5 -5
  69. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  70. package/plugins/trailhead/skills/_shared/substrate-commands.md +26 -4
  71. package/plugins/trailhead/skills/_shared/substrate.md +4 -1
  72. package/plugins/trailhead/skills/_shared/teamwork.md +5 -5
  73. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +2 -2
  74. package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
  75. package/plugins/trailhead/skills/_shared/techniques/grilling.md +4 -2
  76. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +3 -1
  77. package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
  78. package/plugins/trailhead/skills/_shared/techniques.md +2 -2
  79. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
  80. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
  81. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
  82. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
  83. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +4 -3
  84. package/plugins/trailhead/skills/trailhead-view/SKILL.md +1 -0
  85. package/plugins/trailhead/skills/trailhead-work/SKILL.md +23 -12
  86. package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
  87. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +7 -6
  88. package/plugins/trailhead/templates/statusline-trailhead.sh +80 -15
  89. package/plugins/trailhead/templates/trailhead-auto-merge.js +318 -0
  90. package/plugins/trailhead/templates/trailhead-auto-queue.js +186 -0
  91. package/plugins/trailhead/templates/trailhead-auto-run.js +294 -0
  92. package/plugins/trailhead/templates/trailhead-auto-slot.js +239 -0
  93. package/plugins/trailhead/templates/trailhead-checkout-in.js +88 -0
  94. package/plugins/trailhead/templates/trailhead-dashboard.js +32 -12
  95. package/plugins/trailhead/templates/trailhead-merge-ticket.js +366 -0
  96. package/plugins/trailhead/templates/trailhead-needs-you.js +193 -0
  97. package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +289 -0
  98. package/plugins/trailhead/templates/trailhead-report-status.js +67 -3
  99. package/plugins/trailhead/templates/trailhead-uat-followup.js +128 -0
  100. package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
  101. package/plugins/trailhead/tests/auto-run-ui.test.ts +444 -0
  102. package/plugins/trailhead/tests/auto-run-view.test.ts +111 -0
  103. package/plugins/trailhead/tests/map-pane-ui.test.ts +1839 -72
  104. package/plugins/trailhead/tests/map-pane-view.test.ts +308 -14
  105. package/plugins/trailhead/tests/map-pane-write.test.ts +51 -0
  106. package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +77 -0
  107. package/plugins/trailhead/tests/repo-pin.test.ts +123 -0
  108. package/plugins/trailhead/tests/session-registry.test.ts +50 -0
  109. package/plugins/trailhead/tests/whiteboard-follow.test.ts +90 -0
  110. package/plugins/trailhead/tests/work-state-locate.test.ts +33 -0
  111. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +276 -2
  112. package/plugins/trailhead/tests/work-state-prompt.test.ts +29 -35
  113. package/plugins/trailhead/tests/work-state-ui.test.ts +73 -1
  114. package/plugins/trailhead/tests/work-state-view.test.ts +19 -0
  115. package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
  116. package/plugins/trailhead/types/index.d.ts +26 -1
@@ -339,6 +339,154 @@ ok('hook allows Bash(cp .env.example .env) end-to-end (exit 0, no output)', a7.c
339
339
  ok('the message never echoes a token', !leak.includes(T) && !leak.includes('ghp_'));
340
340
  }
341
341
 
342
+ // --- #273: a git config value that names a command (git -c / --config-env) ---
343
+ {
344
+ const blocked = (c) => !!detectSecretRead('Bash', { command: c });
345
+ const unmodelled = (c) => { const h = detectSecretRead('Bash', { command: c }); return !!h && !!h.unmodelled; };
346
+ const exit2 = (c) => { const r = runHook('Bash', { command: c }); return r.code === 2 && JSON.parse(r.out).code === 'SECRET_READ_BLOCKED'; };
347
+ for (const c of [
348
+ 'git -c alias.x="!cat .env" x',
349
+ 'git -c core.pager="cat .env" log',
350
+ 'git -c core.sshCommand="cat .env" fetch',
351
+ ]) ok(`end to end: ${c} exits 2 SECRET_READ_BLOCKED`, exit2(c));
352
+ for (const c of [
353
+ 'git -c alias.x="!cat .env" x',
354
+ "git -c alias.x='!cat .env' x",
355
+ 'git -c pager.log="cat .env" log',
356
+ 'git -c core.editor="cat .env" commit',
357
+ 'git -c sequence.editor="cat .env" rebase -i HEAD~2',
358
+ 'git -c core.fsmonitor="cat .env" status',
359
+ 'git -c diff.external="cat .env" diff',
360
+ 'git -c diff.foo.textconv="cat .env" diff',
361
+ 'git -c filter.foo.clean="cat .env" add x',
362
+ 'git -c filter.foo.smudge="cat .env" checkout x',
363
+ 'git -c filter.foo.process="cat .env" checkout x',
364
+ 'git -c credential.helper="!cat .env" fetch',
365
+ 'git -c gpg.program="cat .env" commit -S',
366
+ 'git -c CORE.PAGER="cat .env" log',
367
+ 'git -c Core.SshCommand="cat .env" fetch',
368
+ 'git -ccore.pager="cat .env" log',
369
+ "git -calias.x='!cat .env' x",
370
+ 'git -C /tmp -c core.pager="cat .env" log',
371
+ 'git --no-pager -c core.pager="cat .env" -c user.name=a log',
372
+ '/usr/bin/git -c core.pager="cat .env" log',
373
+ 'FOO=1 git -c core.pager="cat .env" log',
374
+ 'echo hi && git -c core.pager="cat .env" log',
375
+ 'git -c core.pager="head $(cat .env)" log',
376
+ 'git -c alias.x="!f() { cat .env; }; f" x',
377
+ 'git -c core.pager=".env" log',
378
+ 'P="cat .env" git --config-env=core.pager=P log',
379
+ 'P="cat .env" git --config-env core.pager=P log',
380
+ 'P="!cat .env" git --config-env=alias.x=P x',
381
+ "P='cat .env' /usr/bin/git -C /tmp --config-env=core.sshCommand=P fetch",
382
+ ]) ok(`denies a command-valued git config: ${c}`, blocked(c));
383
+ for (const c of [
384
+ 'git -c core.pager=less log',
385
+ 'git -c user.name=.env commit',
386
+ 'git -c credential.helper=store fetch',
387
+ 'git -c alias.x="cat .env" x',
388
+ 'git -c alias.x=".env" x',
389
+ 'git -c credential.helper="cat .env" fetch',
390
+ 'git -c user.name=.env -c core.pager=less log',
391
+ 'git -c core.pager="less" -C /tmp log',
392
+ 'git -C /tmp log',
393
+ 'git -c core.autocrlf log',
394
+ 'git -c alias.x log',
395
+ 'git --config-env=user.name=NAME log',
396
+ 'git --config-env user.name=NAME log',
397
+ 'git --config-env=core.autocrlf=UNSET_VAR log',
398
+ 'P=less git --config-env=core.pager=P log',
399
+ 'P=less git --config-env core.pager=P log',
400
+ 'P="!less" git --config-env=alias.x=P x',
401
+ 'P=store git --config-env=credential.helper=P fetch',
402
+ 'git log -c core.pager="cat .env"',
403
+ 'echo -c core.pager="cat .env"',
404
+ ]) ok(`allows a harmless git config: ${c}`, !blocked(c));
405
+ for (const c of [
406
+ 'git --config-env=core.pager=P log',
407
+ 'git --config-env core.pager=P log',
408
+ 'Q=less git --config-env=core.pager=P log',
409
+ 'git --config-env=alias.x=P x',
410
+ 'git -C /tmp --config-env=core.sshCommand=P fetch',
411
+ ]) {
412
+ ok(`blocks an unresolved --config-env on a command key as unmodelled: ${c}`, unmodelled(c));
413
+ const r = runHook('Bash', { command: c });
414
+ ok(`end to end: ${c} exits 2 SECRET_READ_UNMODELLED_CONSTRUCT`, r.code === 2 && JSON.parse(r.out).code === 'SECRET_READ_UNMODELLED_CONSTRUCT');
415
+ }
416
+ ok('a resolved secret --config-env stays SECRET_READ_BLOCKED, not unmodelled', !unmodelled('P="cat .env" git --config-env=core.pager=P log'));
417
+ }
418
+
419
+ // --- #273 review C1/I3: a git global option the walk does not list must not hide a later -c ---
420
+ {
421
+ const blocked = (c) => !!detectSecretRead('Bash', { command: c });
422
+ const unmodelled = (c) => { const h = detectSecretRead('Bash', { command: c }); return !!h && !!h.unmodelled; };
423
+ for (const c of [
424
+ 'git --literal-pathspecs -c core.pager="cat .env" log',
425
+ 'git --glob-pathspecs -c core.pager="cat .env" log',
426
+ 'git --noglob-pathspecs -c core.pager="cat .env" log',
427
+ 'git --icase-pathspecs -c core.pager="cat .env" log',
428
+ 'git --exec-path=/x -c core.pager="cat .env" log',
429
+ 'git --exec-path -c core.pager="cat .env" log',
430
+ 'git --attr-source=x -c core.pager="cat .env" log',
431
+ 'git --list-cmds=main -c core.pager="cat .env" log',
432
+ 'git --some-future-flag -c core.pager="cat .env" log',
433
+ ]) ok(`denies a config command behind a global option: ${c}`, blocked(c));
434
+ ok('an unresolved --config-env behind a global option is unmodelled',
435
+ unmodelled('git --exec-path=/x --config-env=core.pager=P log'));
436
+ ok('a harmless -c behind a global option is allowed', !blocked('git --literal-pathspecs -c core.pager=less log'));
437
+ ok('git show behind --attr-source stays a subcommand walk', blocked('git --attr-source=x show HEAD:.env'));
438
+ ok('git --attr-source x ls-files .env stays name-only', !blocked('git --attr-source x ls-files .env'));
439
+ }
440
+
441
+ // --- #273 review W2: backslash escapes in a quoted config value ---
442
+ {
443
+ const blocked = (c) => !!detectSecretRead('Bash', { command: c });
444
+ for (const c of [
445
+ 'git -c core.pager="sh -c \\"cat .env\\"" log',
446
+ 'git -c core.pager="bash -c \'bash -c \\"cat .env\\"\'" log',
447
+ 'git -c core.pager="git -c core.pager=\\"cat .env\\" log" log',
448
+ 'git -c core.pager=cat\\ .env log',
449
+ ]) ok(`denies an escaped config command: ${c}`, blocked(c));
450
+ ok('single quotes keep a backslash literal', !blocked("git -c core.pager='less \\' log"));
451
+ ok('an escaped dollar stays literal text, not a path', !blocked('git -c core.pager="less \\$x" log'));
452
+ }
453
+
454
+ // --- #273 review W1: config and commands injected through leading env assignments ---
455
+ {
456
+ const blocked = (c) => !!detectSecretRead('Bash', { command: c });
457
+ const unmodelled = (c) => { const h = detectSecretRead('Bash', { command: c }); return !!h && !!h.unmodelled; };
458
+ for (const c of [
459
+ 'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.pager GIT_CONFIG_VALUE_0="cat .env" git log',
460
+ 'GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0=user.name GIT_CONFIG_VALUE_0=a GIT_CONFIG_KEY_1=alias.x GIT_CONFIG_VALUE_1="!cat .env" git x',
461
+ 'GIT_CONFIG_KEY_0=core.pager GIT_CONFIG_VALUE_0="cat .env" git log',
462
+ "GIT_CONFIG_PARAMETERS=\"'core.pager=cat .env'\" git log",
463
+ "GIT_CONFIG_PARAMETERS=\"'user.name'='a' 'core.pager'='cat .env'\" git log",
464
+ "GIT_CONFIG_PARAMETERS=\"'alias.x=!cat .env'\" git x",
465
+ 'GIT_PAGER="cat .env" git log',
466
+ 'PAGER="cat .env" git log',
467
+ 'GIT_SSH_COMMAND="cat .env" git fetch',
468
+ 'GIT_SSH="cat .env" git fetch',
469
+ 'GIT_EXTERNAL_DIFF="cat .env" git diff',
470
+ 'GIT_EDITOR="cat .env" git commit',
471
+ 'GIT_SEQUENCE_EDITOR="cat .env" git rebase -i HEAD~2',
472
+ 'EDITOR="cat .env" git commit',
473
+ 'VISUAL="cat .env" git commit',
474
+ 'GIT_ASKPASS="cat .env" git fetch',
475
+ 'SSH_ASKPASS="cat .env" git fetch',
476
+ ]) ok(`denies an env-injected git command: ${c}`, blocked(c));
477
+ for (const c of [
478
+ 'GIT_PAGER=less git log',
479
+ 'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=user.name GIT_CONFIG_VALUE_0=.env git commit',
480
+ 'GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=alias.x GIT_CONFIG_VALUE_0="cat .env" git x',
481
+ "GIT_CONFIG_PARAMETERS=\"'user.name=.env'\" git commit",
482
+ 'GIT_SSH_COMMAND="ssh -i ~/.ssh/id" git fetch',
483
+ 'PAGER="cat .env" echo hi',
484
+ 'GIT_PAGER="cat .env"',
485
+ ]) ok(`allows a harmless env-injected value: ${c}`, !blocked(c));
486
+ ok('an unparsable GIT_CONFIG_PARAMETERS on a command key is unmodelled',
487
+ unmodelled("GIT_CONFIG_PARAMETERS=\"'core.pager=less\" git log"));
488
+ }
489
+
342
490
  // --- crash safety ---
343
491
  const crash1 = runHook('Read', undefined);
344
492
  ok('missing tool_input never crashes (exit 0)', crash1.code === 0);
@@ -359,4 +507,177 @@ ok('missing tool_input never crashes (exit 0)', crash1.code === 0);
359
507
  fs.rmSync(dir, { recursive: true });
360
508
  }
361
509
 
510
+
511
+ // --- #275: more command-valued git config keys, and `git config` writing one ---
512
+ {
513
+ const blocked = (c) => !!detectSecretRead('Bash', { command: c });
514
+ const keys = [
515
+ 'core.askPass', 'core.gitProxy', 'mergetool.x.cmd', 'difftool.x.cmd', 'merge.x.driver',
516
+ 'browser.x.cmd', 'man.x.cmd', 'uploadpack.packObjectsHook', 'diff.x.command',
517
+ 'core.askpass', 'uploadpack.packobjectshook', 'MERGETOOL.X.CMD',
518
+ ];
519
+ for (const k of keys) {
520
+ ok(`denies git -c ${k}="cat .env"`, blocked(`git -c ${k}="cat .env" status`));
521
+ ok(`denies git -c${k}="cat .env" (glued)`, blocked(`git -c${k}="cat .env" status`));
522
+ ok(`allows git -c ${k}=less`, !blocked(`git -c ${k}=less status`));
523
+ ok(`allows git -c ${k}=true`, !blocked(`git -c ${k}=true status`));
524
+ }
525
+ ok('denies --config-env on a new key with an assigned var', blocked('P="cat .env" git --config-env=core.askPass=P status'));
526
+ ok('allows --config-env on a new key with a harmless var', !blocked('P=less git --config-env=core.askPass=P status'));
527
+ ok('denies GIT_CONFIG_KEY_0/VALUE_0 on a new key',
528
+ blocked('GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=uploadpack.packObjectsHook GIT_CONFIG_VALUE_0="cat .env" git status'));
529
+ ok('denies GIT_CONFIG_PARAMETERS on a new key', blocked(`GIT_CONFIG_PARAMETERS="'merge.x.driver=cat .env'" git status`));
530
+ ok('allows GIT_CONFIG_PARAMETERS with a harmless value', !blocked(`GIT_CONFIG_PARAMETERS="'merge.x.driver=true'" git status`));
531
+
532
+ for (const c of [
533
+ 'git config core.pager "cat .env"',
534
+ 'git config alias.x "!cat .env"',
535
+ 'git config --global core.pager "cat .env"',
536
+ 'git config set core.pager "cat .env"',
537
+ 'git config -f cfg core.askPass "cat .env"',
538
+ 'git config --file=cfg core.pager "cat .env"',
539
+ 'git config --file cfg core.pager "cat .env"',
540
+ 'git config --add alias.y "!cat .env"',
541
+ 'git -C /tmp config core.pager "cat .env"',
542
+ 'git config --local --type=path core.editor "cat .env"',
543
+ 'git config mergetool.x.cmd "cat .env"',
544
+ 'git config --some-future-flag core.pager "cat .env"',
545
+ ]) ok(`denies a git config write of a command: ${c}`, blocked(c));
546
+ for (const c of [
547
+ 'git config core.pager less',
548
+ 'git config alias.x "!echo hi"',
549
+ 'git config alias.x "cat .env"',
550
+ 'git config user.name "cat .env"',
551
+ 'git config --get core.pager',
552
+ 'git config core.pager=less',
553
+ 'git config --global core.pager less',
554
+ 'git config set core.pager less',
555
+ 'git config --list',
556
+ 'git config get core.pager',
557
+ 'git config --unset core.pager',
558
+ 'git config -f cfg core.pager less',
559
+ ]) ok(`allows a harmless git config: ${c}`, !blocked(c));
560
+ }
561
+
562
+ // #275: `--` ends git config options, so a dash-leading value is still the value.
563
+ for (const command of [
564
+ `git config core.pager -- '-n; cat .env'`,
565
+ `git config -- core.pager '-n; cat .env'`,
566
+ ]) {
567
+ ok(`#275 deny: ${command}`, !!detectSecretRead('Bash', { command }));
568
+ }
569
+ ok('#275 allow: git config -- core.pager less', !detectSecretRead('Bash', { command: 'git config -- core.pager less' }));
570
+
571
+ // #276: a backslash outside quotes escapes the next char, so `\.env` is `.env` to the shell.
572
+ for (const command of [
573
+ 'cat \\.env',
574
+ 'cat .\\env',
575
+ 'bash -c "cat \\.env"',
576
+ 'git -c core.pager="cat \\.env" log',
577
+ '\\bash <<EOF\ncat .env\nEOF',
578
+ 'e\\nv bash <<EOF\ncat .env\nEOF',
579
+ ]) {
580
+ ok(`#276 deny: ${command}`, !!detectSecretRead('Bash', { command }));
581
+ }
582
+ for (const command of [
583
+ "cat '\\.env'",
584
+ 'cat "\\.env"',
585
+ 'cat README\\.md',
586
+ 'echo "a\\.env b"',
587
+ ]) {
588
+ ok(`#276 allow: ${command}`, !detectSecretRead('Bash', { command }));
589
+ }
590
+ // #276 review C1: dentro apici doppi `\\` fa da escape, la shell interna riceve `\.env`.
591
+ for (const command of [
592
+ 'bash -c "cat \\\\.env"',
593
+ 'sh -c "echo; cat \\\\.env"',
594
+ 'bash -c "cat .e\\\\nv"',
595
+ 'eval "cat \\\\.env"',
596
+ ]) {
597
+ ok(`#276 deny (nested escaped backslash): ${command}`, !!detectSecretRead('Bash', { command }));
598
+ }
599
+ // già verdi prima del fix C1
600
+ for (const command of [
601
+ 'git show HEAD:\\.env',
602
+ 'cat < \\.env',
603
+ 'cat \\.env.local',
604
+ "cat '.'\\env",
605
+ ]) {
606
+ ok(`#276 deny: ${command}`, !!detectSecretRead('Bash', { command }));
607
+ }
608
+ ok('#276 allow: cat \\\\.env (literal backslash)', !detectSecretRead('Bash', { command: 'cat \\\\.env' }));
609
+ ok("#276 allow: bash -c 'cat \\\\.env' (single quotes keep both)", !detectSecretRead('Bash', { command: "bash -c 'cat \\\\.env'" }));
610
+ ok('#276 hook exits 2 on bash -c "cat \\.env"', runHook('Bash', { command: 'bash -c "cat \\.env"' }).code === 2);
611
+ ok('#276 hook exits 2 on bash -c "cat \\\\.env"', runHook('Bash', { command: 'bash -c "cat \\\\.env"' }).code === 2);
612
+
613
+ ok('#276 hook exits 2 on cat \\.env', runHook('Bash', { command: 'cat \\.env' }).code === 2);
614
+
615
+ // #316: a wrapper in front of a nested shell must not hide the inner command, and the
616
+ // value of grep/sed/awk -f / --file is a FILE operand (a pattern/script file), not a skipped value.
617
+ for (const command of [
618
+ "nice bash -c 'cat .env'",
619
+ "timeout 5 bash -c 'cat .env'",
620
+ "nice -n 5 bash -c 'cat .env'",
621
+ "env -i bash -c 'cat .env'",
622
+ "nohup bash -c 'cat .env'",
623
+ "sudo bash -c 'cat .env'",
624
+ "sudo -u root bash -c 'cat .env'",
625
+ ]) {
626
+ const r = detectSecretRead('Bash', { command });
627
+ ok(`#316 deny (wrapper + nested shell, unmodelled): ${command}`, !!r && !!r.unmodelled);
628
+ ok(`#316 hook exits 2 UNMODELLED: ${command}`, (() => { const h = runHook('Bash', { command }); return h.code === 2 && JSON.parse(h.out).code === 'SECRET_READ_UNMODELLED_CONSTRUCT'; })());
629
+ }
630
+ for (const command of [
631
+ 'grep -f .env pat f',
632
+ 'grep -f.env pat f',
633
+ 'grep -if.env pat f',
634
+ 'grep --file=.env pat f',
635
+ 'grep --file .env pat f',
636
+ 'sed -f .env x',
637
+ 'awk -f .env x',
638
+ ]) {
639
+ ok(`#316 deny (-f / --file value is a file): ${command}`, !!detectSecretRead('Bash', { command }));
640
+ }
641
+ // #316: the rest of the ticket's wrapper list, which lib/unmodelled.js does not skip.
642
+ for (const command of [
643
+ "stdbuf -oL bash -c 'cat .env'",
644
+ "time bash -c 'cat .env'",
645
+ "setsid bash -c 'cat .env'",
646
+ "doas bash -c 'cat .env'",
647
+ ]) {
648
+ const r = detectSecretRead('Bash', { command });
649
+ ok(`#316 deny (wrapper + nested shell, unmodelled): ${command}`, !!r && !!r.unmodelled);
650
+ }
651
+ // #316: with -f the pattern/script comes from a file, so the first positional is a FILE.
652
+ for (const command of [
653
+ 'grep -f pats .env',
654
+ 'grep --file=pats .env',
655
+ 'sed -f script .env',
656
+ 'awk -f prog .env',
657
+ ]) {
658
+ ok(`#316 deny (positional after -f is a file): ${command}`, !!detectSecretRead('Bash', { command }));
659
+ }
660
+ for (const command of [
661
+ "timeout 5 bash -c 'echo hi'",
662
+ 'nice -n 5 ls',
663
+ 'grep -f patterns.txt x f',
664
+ 'grep -e foo f',
665
+ 'grep -ef.env x',
666
+ ]) {
667
+ ok(`#316 allow: ${command}`, !detectSecretRead('Bash', { command }));
668
+ }
669
+ for (const command of [
670
+ 'cat .env',
671
+ "bash -c 'cat .env'",
672
+ "sh -c 'cat .env'",
673
+ "eval 'cat .env'",
674
+ 'grep x .env',
675
+ 'stdbuf -oL cat .env',
676
+ 'exec cat .env',
677
+ 'sudo -u root cat .env',
678
+ "nice eval 'cat .env'",
679
+ ]) {
680
+ ok(`#316 deny (unchanged): ${command}`, !!detectSecretRead('Bash', { command }));
681
+ }
682
+
362
683
  console.log(`✓ secret-read-guard: ${passed} assertions passed`);
@@ -19,12 +19,16 @@ Positive (delegate offered): config values and the `/trailhead config` menus, mo
19
19
 
20
20
  Negative (delegate NOT offered): `decision`-ticket convergence, a scope or destination change, an out-of-scope ruling, routing a deferred line to seed/idea/todo, whether to close an exhausted map, a claim collision. These are the human's to make (or already are a routing the human owns).
21
21
 
22
+ ## Checking the agent's own pick
23
+
24
+ **Stress-test the pick before offering it.** Whenever the agent puts forward a pick of its own (a recommended answer in grilling, an option marked `(Recommended)`, the choice it states under the delegate option, an option `auto` takes on its own), it first attacks that pick once, as the human would if they asked it to think it over again: (a) the edge case or failure mode where it breaks; (b) every premise it rests on about the code or the environment, checked against the code rather than assumed; (c) its fit with the choices already settled (earlier grilled questions, the ticket's recorded choices, the map's `Decisions so far`). Look for real holes, not manufactured doubt: a pick that survives stays a firm recommendation. One pass, no loop: when the pass breaks the pick, replace it with the better one and present only that, never "A, but maybe B". Show the outcome with the pick in one line, `Weak point: <X>; still recommended because <Y>` (rendered in the user's conversation language), so the check is visible and can be judged; where no human sees it live (`auto`), the line goes into the record the choice lands in. A trivial or already-decided choice needs no pass.
25
+
22
26
  ## Option 1: "Let the agent pick (with rationale, then confirm)"
23
27
 
24
28
  The delegate option. It does not let the agent silently decide: it is a **two-step, confirm-gated** interaction.
25
29
 
26
30
  1. The human selects the delegate option.
27
- 2. The agent states **which option it would choose and why it beats the others** (concise, honest, the real trade-off), then asks a plain **yes/no confirmation**.
31
+ 2. The agent states **which option it would choose and why it beats the others** (concise, honest, the real trade-off), stress-tested first per **Stress-test the pick before offering it** above (its `Weak point:` line included), then asks a plain **yes/no confirmation**.
28
32
  - **Yes** → act on the chosen option.
29
33
  - **No, a counter-proposal, a "let's chat", or silence** → the choice **re-opens** (back to the menu / discussion). Never proceed on anything but an explicit yes. The human may counter-propose (the agent adopts it and re-confirms) or defer at this point.
30
34
 
@@ -58,7 +62,7 @@ Worked examples: 2 real options + delegate + defer = 4 -> all in the menu; 3 rea
58
62
 
59
63
  **Menu call-sites (an `AskUserQuestion` trailhead authors): links live in the prose.** A picker renders its question and options as plain text, so nothing in it is clickable. When a question is about something the user has to open first (a mockup to approve or pick from, a published page, a PR, a ticket comment), write its clickable link(s) in the reply text **immediately before** the menu, in that same message, never only earlier in the conversation, never as a name in place of the URL, and never inside the picker. If there is no link to show yet, do not ask. For mockups the full rule is "Surface the mockup" in `techniques/prototype.md`, enforced by the mockup-link hooks (Claude Code and Codex); the Claude Code guard also accepts a link carried by the approval question itself, but the prose stays the preferred form.
60
64
 
61
- **Prose / conversational call-sites (grilling, or any choice put in words rather than a menu).** There is no cap: **state the applicable affordance(s) in words** in the same message that puts the choice (for example append "Oppure: scegli tu, propongo io / scelgo dopo se dipende da un prerequisito."). Grilling stays human-decides, so delegate is not offered there; **defer is offered in prose when the decision is genuinely blocked on a prerequisite.**
65
+ **Prose / conversational call-sites (grilling, or any choice put in words rather than a menu).** There is no cap: **state the applicable affordance(s) in words** in the same message that puts the choice (for example append "Oppure: scegli tu, propongo io / scelgo dopo se dipende da un prerequisito."). Grilling stays human-decides, so delegate is not offered there; **defer is offered in prose when the decision is genuinely blocked on a prerequisite.** The one grilling step put as a menu is the accept / amend / reject ask of the **Holistic review before the final confirmation** (Grilling technique), without delegate and without defer.
62
66
 
63
67
  `(Recommended)` on a real option is a separate, static hint that may coexist with the affordances; it is not the delegate option.
64
68
 
@@ -67,6 +71,7 @@ Worked examples: 2 real options + delegate + defer = 4 -> all in the menu; 3 rea
67
71
  | Call-site | Delegate | Defer |
68
72
  |---|---|---|
69
73
  | `decision` ticket converge (grilling) | No (human decides) | Yes, if blocked on a prerequisite |
74
+ | Grilling holistic review (accept / amend / reject) | No (the set is the human's) | No |
70
75
  | `build`/`quick` Discuss "stop and ask" | Yes | Yes |
71
76
  | Effort skip offer (consolidated) | Yes | No (now-or-never process choice) |
72
77
  | `/trailhead config` menus | Yes | No (no substrate) |
@@ -15,7 +15,7 @@ Effective config = built-in defaults ← global `~/.claude/trailhead/config.json
15
15
  | `acceptance.browser` | **`auto`** \| `on` \| `off` | how **Acceptance testing** runs |
16
16
  | `testing.webapp` / `testing.url` | bool / url | is it browser-drivable, and where |
17
17
  | `plan_review` | **`off`** \| `on` \| CLI list (`gemini,codex`) | send `build` PLANs to external AI CLIs for review (**Cross-AI plan review**) |
18
- | `plan_review.rounds` | integer (**`2`**) | max converge-and-re-review rounds |
18
+ | `plan_review.rounds` | integer (**`2`**) | max converge-and-re-review rounds; an autonomous run asks at start and its answer overrides the key for that run |
19
19
  | `code_review.rounds` | integer (**`2`**) | max fix-and-re-review rounds in **Code review** before the boundary HITL checkpoint |
20
20
  | `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) |
21
21
  | `isolation.copy` | array of repo-relative paths (**unset**) | gitignored files and directories copied from the main checkout into a new `worktree`/`clone` workspace, see **Isolation copy** |
@@ -48,7 +48,7 @@ Effective config = built-in defaults ← global `~/.claude/trailhead/config.json
48
48
 
49
49
  **Inline update check (hookless hosts).** On a host without a hook bus (the descriptor's `hookBus == 'none'`) the `trailhead-check-update.js` SessionStart hook and the statusline flag are both absent, so the engine does the check **inline at session start**, co-located with the models-collapse notice above. It is lightweight and degrades silently: read the shared cache (`$XDG_CACHE_HOME/trailhead/update-check.json`, the same file the hook writes on Claude Code) and, if it reports an update, surface a one-line notice pointing at `/trailhead:update` (the pure `updateNotice(host, cache)` helper in `bin/lib/host-descriptor.js` is the canonical wording, and returns null on a host that already notifies via the hook, so it never double-fires); if the cache is absent or stale (older than a few hours), do a minimal throttled compare of the installed `VERSION` against the latest (npm registry or the GitHub release, per install channel) and rewrite the cache **atomically** (write a temp file in the same directory as the target, then rename it over the target, exactly as the `trailhead-check-update.js` SessionStart hook does with `out + '.tmp'` then `renameSync`), so a crash mid-write cannot leave a truncated or corrupt cache. Never block or fail the session on a network or parse error. `/trailhead:update` stays the on-demand installer.
50
50
 
51
- **Pinned model review.** After an update that changed the tier defaults, the engine offers to review pinned `models.*` / `models.codex.*` ids that are older than their tier's new default (listed `key: old -> new`), with three choices: update (write the new ids, codex effort unchanged), keep (nothing to do, not asked again for these defaults), or open `/trailhead:config` to pick per key. The review evaluates the **effective** pins: the global `~/.claude/trailhead/config.json` merged under the project's `.trailhead/config.json`, project winning key by key (a project's own key, even `null`, `''`, or `inherit session`, always wins over the global value; `models.codex` follows the same per-key rule, so a stale pin inherited from the global config is flagged too, even for a project that pins nothing itself). Each stale line names the config file it actually came from, so the update choice edits the right one. The check (`model-defaults-review.js`'s `check(cwd)`, CLI subcommand `check`) is **read-only**: it never writes an ack by itself, so it keeps reoffering until the user actually answers. The ack (CLI subcommand `ack`, writing the acknowledged defaults version) is written only after the user picks one of the three choices, never just for the notice having been shown; it lives in the gitignored `.trailhead/model-defaults-ack` file when a project is found above `cwd`, whether the stale pin itself is project or global. With no project found above `cwd`, the review runs on the global config alone (if one exists) and its ack lives next to it, at `<config dir>/trailhead/model-defaults-ack` (config dir = `$CLAUDE_CONFIG_DIR` or `~/.claude`). The check runs both in the `trailhead-check-update.js` SessionStart hook and at skill config load (`../_shared/load-first.md`), once per session either way. Unset keys already follow the new defaults, so they are never flagged; unknown ids (custom or unrecognised) are never flagged either. The tier history lives in `hooks/lib/model-defaults.json`. Hookless hosts run the same `node <hooks>/lib/model-defaults-review.js check <dir>` / `... ack <dir>` pair inline at session start.
51
+ **Pinned model review.** After an update that changed the tier defaults, the engine offers to review pinned `models.*` / `models.codex.*` ids that are older than their tier's new default (listed `key: old -> new`), with three choices: update (write the new ids, codex effort unchanged), keep (nothing to do, not asked again for these defaults), or open `/trailhead:config` to pick per key. The review evaluates the **effective** pins: the global config (`~/.claude/trailhead/config.json` on Claude Code, `<codexHome>/trailhead/config.json` on Codex) merged under the project's `.trailhead/config.json`, project winning key by key (a project's own key, even `null`, `''`, or `inherit session`, always wins over the global value; `models.codex` follows the same per-key rule, so a stale pin inherited from the global config is flagged too, even for a project that pins nothing itself). Each stale line names the config file it actually came from, so the update choice edits the right one. The check (`model-defaults-review.js`'s `check(cwd)`, CLI subcommand `check`) is **read-only**: it never writes an ack by itself, so it keeps reoffering until the user actually answers. The ack (CLI subcommand `ack`, writing the acknowledged defaults version) is written only after the user picks one of the three choices, never just for the notice having been shown; it lives in the gitignored `.trailhead/model-defaults-ack` file when a project is found above `cwd`, whether the stale pin itself is project or global. With no project found above `cwd`, the review runs on the global config alone (if one exists) and its ack lives next to it, at `<config dir>/trailhead/model-defaults-ack` (config dir = `$CLAUDE_CONFIG_DIR` or `~/.claude` on Claude Code; on Codex it is `<codexHome>`, derived from the installed lib path `<codexHome>/skills/trailhead/hooks/lib`, falling back to `CODEX_HOME` or `~/.codex` only when the lib is not at that path). The check runs both in the `trailhead-check-update.js` SessionStart hook and at skill config load (`../_shared/load-first.md`), once per session either way. Unset keys already follow the new defaults, so they are never flagged; unknown ids (custom or unrecognised) are never flagged either. The tier history lives in `hooks/lib/model-defaults.json`. Hookless hosts run the same `node <hooks>/lib/model-defaults-review.js check <dir>` / `... ack <dir>` pair inline at session start.
52
52
 
53
53
  If a value is unset, it resolves to its **Balanced**-profile default for that activity (the default profile: a project with no config still gets the Balanced tier per key, resolved to a concrete id at load time, strong → `claude-opus-5-5`, standard → `claude-sonnet-5-5`, fast → `claude-haiku-5-5`). To use the session model instead of the Balanced default, set the key explicitly to `inherit session`. Never hard-fail on an unknown id: warn and inherit the session model.
54
54
 
@@ -90,7 +90,7 @@ On **trivial/small** the engine surfaces **one consolidated skip offer** and act
90
90
 
91
91
  **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.
92
92
 
93
- **`auto` has no config surface.** The autonomous run (`/trailhead:auto`, `../trailhead-work/references/auto.md`) takes no key here, by decision: it changes neither `effort` nor `models.*` (orthogonal to both). Its safety rail is hardcoded always-on, never a toggle, and its stop/go boundary is the same `choices.md` one-line test every other engine step uses (delegate options run without the confirm gate; a human-necessary choice is always set aside), so there is nothing to configure separately.
93
+ **`auto` has no config surface.** The autonomous run (`/trailhead:auto`, `../trailhead-work/references/auto.md`) takes no key here, by decision: it changes neither `effort` nor `models.*` (orthogonal to both). Its safety rail is hardcoded always-on, never a toggle, and its stop/go boundary is the same `choices.md` one-line test every other engine step uses (delegate options run without the confirm gate; a human-necessary choice always marks the ticket needs-you), so there is nothing to configure separately.
94
94
 
95
95
  Example `config.json` (same shape for the project `.trailhead/config.json` and the global `~/.claude/trailhead/config.json`):
96
96
  ```json
@@ -108,7 +108,7 @@ Example `config.json` (same shape for the project `.trailhead/config.json` and t
108
108
  - **🔀 Git**: 🌱 `main` (commit straight to the trunk) · 🌿 `pr` (feature branch + PR).
109
109
  - **🚀 Release**: 🎯 `command` (release only on an explicit command) · ♻️ `auto` (release automatically per the project's flow).
110
110
  - **🧬 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).
111
- 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`; default path `node <templates>/trailhead-issue-body.js replace --issue <conventions> --section :preamble --file <new header>` (the fenced header block, fences included), `<templates>` being the same three-dir lookup as `trailhead-work-state.js`, see **Work state file** in `teamwork.md`; by hand when the helper is missing), 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).
111
+ Write any changed value back into the `trailhead:conventions` issue header (found per the singleton fail-closed rule, see **Singletons** in `substrate.md`; a body read-modify-write: obey the **Fail-safe body rewrites** rule in `substrate.md`; default path `node <templates>/trailhead-issue-body.js replace --issue <conventions> --section :preamble --file <new header>` (the fenced header block, fences included), `<templates>` being the same three-dir lookup as `trailhead-work-state.js`, see **Work state file** in `teamwork.md`; by hand when the helper is missing), 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).
112
112
  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.
113
113
  4. **🧠 Models**: a **profile** (Balanced by default) applied whole, or **Manual** for the per-key questions.
114
114
  - **🎚️ 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`.
@@ -128,7 +128,7 @@ Example `config.json` (same shape for the project `.trailhead/config.json` and t
128
128
 
129
129
  **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.
130
130
  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**.
131
- - 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.
131
+ - On **Enable**, install it: copy the shipped script (`${CLAUDE_PLUGIN_ROOT}/templates/statusline-trailhead.sh` for a plugin install, or `${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/statusline-trailhead.sh` for an npm install, whichever exists) to `${CLAUDE_CONFIG_DIR:-~/.claude}/statusline-trailhead.sh` (executable), then set `statusLine` in `${CLAUDE_CONFIG_DIR:-~/.claude}/settings.json` to `{ "type": "command", "command": "bash \"<config dir>/statusline-trailhead.sh\"" }`, where `<config dir>` is the config directory resolved to an absolute path at install time (so the command works whatever environment Claude Code runs it with). **This is a global Claude Code setting, not per-project**, so it always lands in the config dir's `settings.json` regardless of the scope chosen in step 1.
132
132
  - **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.
133
133
 
134
134
  Keep the icons stable so the menus read the same each run. `config get` shows the same fields as a compact icon summary, read-only.
@@ -14,7 +14,7 @@ Resolving a ticket **ends the session**: one ticket per session is a hard rule (
14
14
 
15
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 lighter alternative, never in its place and never rendered twice on one line. **Characterize `quick` by its purpose, the lighter, faster path** (always size-triaged: on a small change it offers to skip the heavy steps on an explicit yes; grills only if needed; no map book-keeping), **never as merely "without splitting"**: not splitting is a side effect, not what `quick` is for. **`/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 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 lighter alternative (the same ticket, worked whole via the lighter 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 lighter alternative (the same ticket, worked whole via the lighter path), not the map. (The example is Italian; match whatever language you're conversing in.)
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. Before pointing anywhere, run the **Ticket or fog** test (charting, `../trailhead-chart/references/charting.md`, Mode 1 step 4) on every fog entry and, when some pass, offer to graduate them as **one batch** (title, type, blockers per ticket, created on a yes) so the next sessions have a frontier to work in a row instead of one fog entry at a time. If it genuinely is empty (no entry passes the test, 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 lighter alternative (the same ticket, worked whole via the lighter 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 lighter alternative (the same ticket, worked whole via the lighter 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
 
@@ -8,6 +8,10 @@ The single command reference for every `gh` mechanic the GitHub Issues substrate
8
8
  gh issue create --label "trailhead:map" --title "<destination>" --body-file <body>
9
9
  # create a child ticket (add trailhead:blocked too if it has an open blocker; add trailhead:map-<map> to scope it to its map)
10
10
  gh issue create --label "trailhead:ticket,trailhead:build,trailhead:map-<map>" --title "<question/goal>" --body-file <body>
11
+ # create a whiteboard ticket (no map): the destination label is trailhead:whiteboard
12
+ gh issue create --label "trailhead:ticket,trailhead:bug,trailhead:whiteboard" --title "<question/goal>" --body-file <body>
13
+ # the full label set (trailhead:ticket + exactly one type + exactly one destination, a map OR the whiteboard) rides in the create call itself, never a later --add-label; the labels-guard hook blocks an incomplete set. On a host without a hook bus (Codex) read the labels back right after creating and fix in place with --add-label / --remove-label if anything is missing or doubled:
14
+ gh issue view <n> --json labels
11
15
  # link the ticket to its map: native sub-issue (structure/UI) + the map label above (the query)
12
16
  gh api --method POST repos/{owner}/{repo}/issues/<map>/sub_issues -F sub_issue_id="$(gh api repos/{owner}/{repo}/issues/<ticket> --jq .id)"
13
17
  # wire a blocker: native dependency (visual frontier in the UI) + the trailhead:blocked label (the query)
@@ -142,10 +146,21 @@ gh label create trailhead:uat --color BFD4F2 --description "Pending UAT: an acce
142
146
  ```
143
147
  Tickets opened before the label existed are tagged by the idempotent backfill helper, `node <templates>/trailhead-uat-backfill.js [--repo <owner/name>] [--marker <path>]`: it ensures the label, tags every open ticket with a line starting `UAT of:`, and prints one JSON line `{"status","tagged":[...],"failed":[...]}` (a failed edit is reported, never fatal to the rest). With `--marker` it runs once per clone and exits `skipped` afterwards.
144
148
 
145
- **Find the dashboard issue:**
149
+ The needs-you label needs the same idempotent ensure, since an autonomous run can park a ticket before any first-use setup on an older repo (the helper below also runs it):
146
150
  ```bash
147
- gh issue list --label trailhead:dashboard --state open --json number --jq '.[0].number'
151
+ gh label create trailhead:needs-you --color D93F0B --description "Needs you: an autonomous run parked it for the human" --force
148
152
  ```
153
+ An autonomous run parks a ticket for the human with `node <templates>/trailhead-needs-you.js park --issue <n> --reason <text> --repo <owner/name>` (all three required). It secret-scans the reason (fail closed: nothing written when the guard is missing or hits), ensures the label, then in this order posts a comment whose first line is exactly `NEEDS YOU`, a blank line, then the reason (skipped when the latest `NEEDS YOU` comment carries the same reason, whitespace-normalised; it only sees the comments `gh` returns, so on a very long thread the worst case is one duplicate comment, never a missing one), unassigns `@me`, and adds the label last, so a partial failure leaves the ticket visible. It prints `{"status":"ok","issue","commented","unassigned","labelled"}`. A `work`/`quick` claim clears the mark with `node <templates>/trailhead-needs-you.js clear --issue <n> --repo <owner/name>`, which removes the label when present and otherwise does nothing, printing `{"status":"ok","issue","cleared":bool}`. Any failed or unparseable `gh` step prints `{"status":"error","reason"}` and exits 1 (a usage error exits 2): never read an error as a success.
154
+
155
+ Before an autonomous run opens a UAT follow-up, it looks for an existing one: `node <templates>/trailhead-uat-followup.js find --parent <n> --repo <owner/name>` (`--repo` required). It reads every `trailhead:uat` issue, open and closed, through a paginated GraphQL `repository.issues` query (`gh api graphql`), not `gh issue list --label`, which goes through the lagging search index and would miss a follow-up opened seconds earlier, keeps those with a line starting `UAT of:` that points at the parent (a link to that repo's issue `<n>`, or a bare `#<n>`), and prints one JSON line `{"status":"ok","parent":<n>,"open":[{"number","title"}],"closed":[...],"reuse":<lowest open number>|null}`. Only an open follow-up is reusable; closed ones are reported, never reused. A failed, unparseable or incomplete read (GraphQL errors, unexpected shape, page cap exceeded: more than 10,000 `trailhead:uat` issues) prints `{"status":"error","reason"}` and exits 1: treat it as unknown, never as none found.
156
+
157
+ When an autonomous run leaves a slot branch unmerged, it records the branch with `node <templates>/trailhead-merge-ticket.js add --scope <map-<n>|whiteboard> --ticket <n> --file <path> [--file <path> ...] --workspace <path> --repo <owner/name>` (all required, `--file` repeatable, one per conflicting path; the branch is `trailhead/t<n>`). It finds the scope's merge ticket through a paginated GraphQL `repository.issues` query (`gh api graphql`) over the OPEN issues labelled with the scope (`trailhead:map-<n>` or `trailhead:whiteboard`), keeping those whose body carries the `<!-- trailhead:merge-ticket v1 <base64 JSON entries> -->` marker line, and reuses the lowest-numbered one. With none open it ensures the `trailhead:needs-you` label, secret-scans the body (fail closed), and opens a `trailhead:ticket` `trailhead:task` `trailhead:needs-you` ticket carrying the scope label (for a map, wired as a native sub-issue; a failed edge is a warning). With one open it upserts this ticket's entry through the guarded body write of `trailhead-issue-body.js` and re-renders the whole list from the entries (oldest ticket first, as a suggestion); the same ticket with the same files and workspace is `unchanged` and writes nothing. It prints `{"status":"ok","action":"created|updated|unchanged","issue","url","scope","entries","warnings"}`. A failed, unparseable or incomplete read, an unknown or undecodable marker, a secret hit or a write conflict prints `{"status":"error","reason"}` and exits 1 (a usage error exits 2): treat the merge ticket state as unknown, never as none found.
158
+
159
+ **Find a singleton issue** (the dashboard, the codebase anchor, the conventions anchor). Fetch several results, never just the first:
160
+ ```bash
161
+ gh issue list --label trailhead:dashboard --state open --json number,title --limit 10
162
+ ```
163
+ Swap the label for `trailhead:codebase` or `trailhead:conventions`. Fail-closed rule for all three singleton labels: exactly one result, use it; none, take the create or self-heal path; more than one, do not pick one: surface the duplicates by title and number, and ask the user which to keep (the other is closed as a duplicate) before reading the conventions, patching the codebase issue or rewriting the dashboard. `trailhead-dashboard.js` applies the rule itself and aborts naming every duplicate.
149
164
 
150
165
  **Read the repo's pinned issues**, to know whether the dashboard is currently among them:
151
166
  ```bash
@@ -200,7 +215,14 @@ gh issue list --state open --json number,labels \
200
215
  # whiteboard frontier size: the whiteboard frontier query above, counted
201
216
  gh issue list --label "trailhead:ticket" --label "trailhead:whiteboard" --state open --search "no:assignee -label:trailhead:blocked -label:trailhead:unverified -label:trailhead:paused" --json number --jq 'length'
202
217
  # per-map exhaustion pre-filter: open issues scoped to map <n>; count == 0 makes it a candidate (not yet a verdict)
203
- gh issue list --label "trailhead:map-<n>" --state open --json number --jq 'length'
218
+ gh issue list --label "trailhead:map-<n>" --state open --limit 1000 --json number --jq 'length'
219
+ # open tickets across the repo (the `map` render's "Open across the repo" line): the open maps, then one count per map plus the whiteboard, summed.
220
+ # The maps list is the dashboard's `maps` limit (200); a list that reaches it makes the line `at least N`.
221
+ gh issue list --label trailhead:map --state open --limit 200 --json number --jq '.[].number'
222
+ # per open map <n> (the dashboard helper's `scoped` limit, 1000: a count reaching it makes the line `at least N`)
223
+ gh issue list --label "trailhead:map-<n>" --state open --limit 1000 --json number --jq length
224
+ # the whiteboard (the helper's `whiteboard` limit, 200: a count reaching it makes the line `at least N`)
225
+ gh issue list --label "trailhead:whiteboard" --state open --limit 200 --json number --jq length
204
226
  # confirm no fog before flagging a candidate closeable: its body's `## Not yet specified` must be empty
205
227
  gh issue view <map> --json body --jq .body
206
228
  # per-map sub-issue cap: total native sub-issues (>=90 near cap, >=100 at cap)
@@ -210,6 +232,6 @@ gh api repos/{owner}/{repo}/issues/<map> --jq '.sub_issues_summary.total'
210
232
  **When to refresh the body.** On structural events: a map is charted or exhausted (a map appears or disappears), a whiteboard ticket is born or resolved. And on demand via `/trailhead:dashboard`. NOT on every map-ticket resolve: that churns a pinned issue's notifications, and the native progress bar already tracks map-ticket progress on its own. The whiteboard has no native progress bar, so its ticket birth or close refreshes the dashboard, except a `quick`-born ticket (born and resolved in one session) refreshes at the handoff (Resolve, or pause), not at creation.
211
233
 
212
234
  ## First-use repo setup (do ALL THREE, every chart or adopt, never skip any)
213
- 1. Create any missing labels with `gh label create` (all twenty: `trailhead:map`, `trailhead:codebase`, `trailhead:conventions`, `trailhead:dashboard`, `trailhead:ticket`, the six type labels, `trailhead:blocked`, `trailhead:seed`, `trailhead:out-of-scope`, `trailhead:superseded`, `trailhead:unverified`, `trailhead:fog`, `trailhead:whiteboard`, `trailhead:paused`, `trailhead:uat`), then run the UAT backfill helper once (`node <templates>/trailhead-uat-backfill.js`) so a pending UAT follow-up opened before the label existed is tagged too; stay silent when it tagged nothing.
235
+ 1. Create any missing labels with `gh label create` (all twenty-one: `trailhead:map`, `trailhead:codebase`, `trailhead:conventions`, `trailhead:dashboard`, `trailhead:ticket`, the six type labels, `trailhead:blocked`, `trailhead:seed`, `trailhead:out-of-scope`, `trailhead:superseded`, `trailhead:unverified`, `trailhead:fog`, `trailhead:whiteboard`, `trailhead:paused`, `trailhead:uat`, `trailhead:needs-you`), then run the UAT backfill helper once (`node <templates>/trailhead-uat-backfill.js`) so a pending UAT follow-up opened before the label existed is tagged too; stay silent when it tagged nothing.
214
236
  2. **Check the label guard is installed and current**: `gh api repos/{owner}/{repo}/contents/.github/workflows/trailhead-label-guard.yml`; if it's absent (404), install it; if it's present, run the label guard sync script (`trailhead-label-guard-sync.js`, check-only) on the working root and, when it reports `outdated` (a pre-0.12.1 guard with no job-level `if:`, which starts a billed runner on every `issues: labeled` event), offer the upgrade: **read `teamwork.md` (Trust & provenance → Repo-side enforcement) for the exact steps**. This is part of standing up trailhead in a repo, not an optional extra: *check every time*, so a repo can never end up with the labels but no guard, or with a guard that burns Actions minutes.
215
237
  3. **Sync the commit-msg git hook**: run the commit-msg hook sync script (`trailhead-commit-msg-sync.js`; installs the `sh` wrapper plus its `trailhead-commit-msg.cjs` logic if absent, upgrades an outdated trailhead hook (including the legacy single-file one) in place, never touches a foreign one; a hook that cannot run, node off PATH or the `.cjs` missing, says so on stderr at each commit instead of failing silently) so trailhead's commit discipline (Conventional Commits subject, no `Co-Authored-By`, and a `Refs: #<n>` trailer while a `.trailhead/session-ticket` marker is active) runs on **every** `git commit` regardless of host (the Claude-Code `PreToolUse` guard stays too, defence in depth): **read `teamwork.md` (Trust & provenance → Repo-side enforcement) for the exact steps**. If the script is missing or node errors (missing means none of the three template dirs listed in `teamwork.md` has it: try them all first), fall back to inspecting the hooks dir (`git rev-parse --git-path hooks`) per `teamwork.md`, never a blind "not enforced". Like the label guard, *check every time*; unlike it, `.git/hooks/` is per-clone local state (untracked), so it is installed, never committed.
@@ -19,6 +19,8 @@ Conventions:
19
19
  | Claim | assign the ticket to yourself: `gh issue edit <n> --add-assignee @me`: the assignee *is* the claim; the claimer owns it end to end and closes it (no approver). Re-check before starting; on a collision, stop and ask the user. See [Working as a team](teamwork.md#working-as-a-team). |
20
20
  | Resolution | comment with the answer → `gh issue close` → update `Decisions so far` on the map |
21
21
 
22
+ **Singletons.** Dashboard, Codebase and Conventions are each one issue per repo. Look them up with the fail-closed rule in `substrate-commands.md` (**Find a singleton issue**): more than one open issue means do not pick one, surface the duplicates by title and number, and ask which to keep.
23
+
22
24
  **State labels** (orthogonal to the type label, on top of `trailhead:ticket`):
23
25
  - **`trailhead:blocked`**: has an open blocker; excluded from the frontier. Removed when unblocked.
24
26
  - **`trailhead:seed`**: a forward-looking capture parked until a trigger fires (kept blocked); makes seeds auditable.
@@ -28,6 +30,7 @@ Conventions:
28
30
  - **`trailhead:fog`**: an in-scope-but-not-yet-sharp issue (usually an inbound suggestion) **kept open as a clarification space** (visible and tracked, off the frontier and out of the inbox) where the reporter and others discuss until it's sharp enough to graduate into a ticket. See [Inbox](../trailhead-chart/references/inbox.md).
29
31
  - **`trailhead:whiteboard`**: a **map-less** ticket, parked on the **whiteboard** (the container for work not tied to any map). **Mutually exclusive with a `trailhead:map-<n>` label**: a ticket is on a map or on the whiteboard, never both, and a whiteboard ticket has no `Parent:` line and no native sub-issue edge. It carries its own frontier (below), off every map's. Captured there when a capture is routed to the whiteboard, or born there by `quick`; see [The whiteboard](../trailhead-view/SKILL.md#render-the-whiteboard-whiteboard).
30
32
  - **`trailhead:paused`**: a ticket whose work is paused: set by pause (after the `PAUSED` checkpoint comment), cleared by resume or by a `work`/`quick` claim of it (working it is resuming it). It is **excluded from the frontier** (a paused ticket is taken back with `/trailhead:resume`; an explicit `work <n>` / `quick <n>` claim still works it), and it tells every surface (the panel, the map and whiteboard renders, the dashboard) to show the ticket once, under its `Paused` group.
33
+ - **`trailhead:needs-you`**: a ticket an autonomous run parked because its next step needs the human (a decision, a human-only checklist, a rail action, a surviving blocking review concern): set by the run on parking, together with a `NEEDS YOU` comment naming what the human must do, written at once. It is **not a frontier exclusion for humans**: the ticket stays on the frontier and the map, and `/trailhead:work` still offers it. Only the autonomous run's own scan skips it, so later runs never reconsider it. A `work`/`quick` claim of it clears the label (taking it back is working it), and a human can remove it by hand.
31
34
  - **`trailhead:uat`**: a **marker** label, not a state: it tags a pending UAT follow-up (a ticket whose body has a `UAT of: [<parent name>](<parent url>)` line), set when the follow-up is opened and found by one label query. It is **not a state** and **never a frontier filter**: a UAT `task` stays workable from the frontier and a UAT `seed` stays blocked, exactly as without the label. The panel lists such a ticket once, under its `UAT` group (a paused one stays under `Paused`).
32
35
 
33
36
  **Frontier** = open, unassigned tickets that are **not** `trailhead:blocked`, **not** `trailhead:unverified` and **not** `trailhead:paused`. That's the whole query: the labels are the pre-computed answers to "are all blockers closed?", "is this a trusted trailhead ticket?" and "is this ticket set aside on a checkpoint?", so no per-ticket body parsing. **With more than one live map on the repo**, add the active map's `trailhead:map-<n>` label to scope the frontier to that map (still one query); with a single map the repo-wide query below is the frontier. See [Multiple maps on one repo](multi-map.md#multiple-maps-on-one-repo-parking--concurrency).
@@ -38,4 +41,4 @@ Conventions:
38
41
 
39
42
  **Blocked-by reconciliation (drift check).** The `## Blocked by` prose and the native dependency are two copies of one fact, so they can drift (it has happened: a body naming the split-*origin* while the native edge pointed at the sibling). When rendering `/trailhead:map` and before working a ticket, **reconcile them deterministically**: parse the issue numbers in the ticket's `## Blocked by`, read its native `blocked_by` (`gh api repos/{owner}/{repo}/issues/<n>/dependencies/blocked_by`), and if the two sets differ, **surface an advisory** naming both sides (the native edge is the structured reference); never auto-fix, never block. If the body has no parseable `## Blocked by`, report it **uncheckable**, never assume they agree. This is the one deterministic, false-positive-free consistency axis; higher-level "does a ticket contradict the map's decisions" drift is left to human judgement.
40
43
 
41
- **Base commands & first-use repo setup → `substrate-commands.md`.** The `gh` cookbook: create map/ticket, the child→map three-view wiring and the blocker three-move wiring, the frontier and whiteboard-frontier queries, claim, resolve, the dashboard commands (find, pin/re-pin/unpin, self-heal, body generation with count queries), and the first-use setup that runs at every chart/adopt (create the twenty labels + check/install the label guard + install the commit-msg hook). Read it at work time and at chart/adopt.
44
+ **Base commands & first-use repo setup → `substrate-commands.md`.** The `gh` cookbook: create map/ticket, the child→map three-view wiring and the blocker three-move wiring, the frontier and whiteboard-frontier queries, claim, resolve, the dashboard commands (find, pin/re-pin/unpin, self-heal, body generation with count queries), and the first-use setup that runs at every chart/adopt (create the twenty-one labels + check/install the label guard + install the commit-msg hook). Read it at work time and at chart/adopt.