@marcomigozzi/trailhead 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.it.md +158 -34
  2. package/README.md +147 -33
  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/base-preflight-docs.test.js +65 -0
  12. package/bin/execute-split-docs.test.js +146 -0
  13. package/bin/grilling-holistic-review-docs.test.js +90 -0
  14. package/bin/isolation-answer-docs.test.js +41 -0
  15. package/bin/isolation-copy-docs.test.js +28 -0
  16. package/bin/lib/codex-projection.js +6 -6
  17. package/bin/lib/codex-projection.test.js +43 -2
  18. package/bin/needs-you-docs.test.js +41 -0
  19. package/bin/pick-stress-test-docs.test.js +105 -0
  20. package/bin/report-status-docs.test.js +195 -0
  21. package/bin/resolve-helpers-docs.test.js +36 -0
  22. package/bin/scope-confirm-skip-docs.test.js +70 -0
  23. package/bin/trailhead.js +6 -6
  24. package/bin/trailhead.test.js +355 -16
  25. package/bin/uat-setup-docs.test.js +65 -0
  26. package/package.json +1 -1
  27. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  28. package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
  29. package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
  30. package/plugins/trailhead/agents/trailhead-debug.md +20 -1
  31. package/plugins/trailhead/agents/trailhead-executor.md +29 -2
  32. package/plugins/trailhead/agents/trailhead-fix.md +29 -2
  33. package/plugins/trailhead/agents/trailhead-plan.md +20 -1
  34. package/plugins/trailhead/agents/trailhead-research.md +22 -3
  35. package/plugins/trailhead/agents/trailhead-verify.md +15 -2
  36. package/plugins/trailhead/commands/auto.md +2 -2
  37. package/plugins/trailhead/hooks/lib/auto-merge.test.js +467 -0
  38. package/plugins/trailhead/hooks/lib/auto-queue.test.js +245 -0
  39. package/plugins/trailhead/hooks/lib/auto-run.test.js +318 -0
  40. package/plugins/trailhead/hooks/lib/auto-slot.test.js +278 -0
  41. package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
  42. package/plugins/trailhead/hooks/lib/checkout-in.test.js +173 -0
  43. package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
  44. package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
  45. package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
  46. package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
  47. package/plugins/trailhead/hooks/lib/dashboard.test.js +472 -0
  48. package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
  49. package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
  50. package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
  51. package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
  52. package/plugins/trailhead/hooks/lib/merge-ticket.test.js +394 -0
  53. package/plugins/trailhead/hooks/lib/needs-you.test.js +177 -0
  54. package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +319 -0
  55. package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
  56. package/plugins/trailhead/hooks/lib/shell-scan.js +36 -1
  57. package/plugins/trailhead/hooks/lib/shell-scan.test.js +14 -0
  58. package/plugins/trailhead/hooks/lib/statusline-context.test.js +140 -0
  59. package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
  60. package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
  61. package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
  62. package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
  63. package/plugins/trailhead/hooks/lib/work-state.test.js +45 -0
  64. package/plugins/trailhead/hooks/mod/README.md +209 -36
  65. package/plugins/trailhead/hooks/mod/auto-run-view.ts +93 -0
  66. package/plugins/trailhead/hooks/mod/auto-run.ts +165 -0
  67. package/plugins/trailhead/hooks/mod/map-pane-view.ts +304 -26
  68. package/plugins/trailhead/hooks/mod/map-pane.tsx +763 -160
  69. package/plugins/trailhead/hooks/mod/register.tsx +312 -40
  70. package/plugins/trailhead/hooks/mod/repo-pin.ts +167 -0
  71. package/plugins/trailhead/hooks/mod/session-registry.ts +65 -0
  72. package/plugins/trailhead/hooks/mod/turn-state.ts +14 -0
  73. package/plugins/trailhead/hooks/mod/whiteboard-follow.ts +98 -0
  74. package/plugins/trailhead/hooks/mod/work-state-locate.ts +191 -0
  75. package/plugins/trailhead/hooks/mod/work-state-prompt.ts +13 -15
  76. package/plugins/trailhead/hooks/mod/work-state-view.ts +38 -5
  77. package/plugins/trailhead/hooks/mod/worktree-porcelain.ts +17 -0
  78. package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
  79. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +22 -16
  80. package/plugins/trailhead/hooks/trailhead-install-guard.js +169 -34
  81. package/plugins/trailhead/hooks/trailhead-install-guard.test.js +180 -1
  82. package/plugins/trailhead/hooks/trailhead-labels-guard.js +231 -0
  83. package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
  84. package/plugins/trailhead/hooks/trailhead-search-guard.js +43 -7
  85. package/plugins/trailhead/hooks/trailhead-search-guard.test.js +66 -2
  86. package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +296 -22
  87. package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +253 -0
  88. package/plugins/trailhead/skills/_shared/choices.md +11 -4
  89. package/plugins/trailhead/skills/_shared/configuration-reference.md +17 -5
  90. package/plugins/trailhead/skills/_shared/load-first.md +3 -3
  91. package/plugins/trailhead/skills/_shared/multi-map.md +2 -2
  92. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  93. package/plugins/trailhead/skills/_shared/substrate-commands.md +47 -14
  94. package/plugins/trailhead/skills/_shared/substrate.md +9 -4
  95. package/plugins/trailhead/skills/_shared/teamwork.md +17 -8
  96. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +18 -10
  97. package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
  98. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
  99. package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
  100. package/plugins/trailhead/skills/_shared/techniques/grilling.md +4 -2
  101. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +3 -1
  102. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
  103. package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
  104. package/plugins/trailhead/skills/_shared/techniques.md +12 -2
  105. package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
  106. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
  107. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +3 -3
  108. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
  109. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
  110. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
  111. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
  112. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
  113. package/plugins/trailhead/skills/trailhead-view/SKILL.md +9 -6
  114. package/plugins/trailhead/skills/trailhead-work/SKILL.md +31 -18
  115. package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
  116. package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
  117. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +29 -13
  118. package/plugins/trailhead/templates/statusline-trailhead.sh +77 -12
  119. package/plugins/trailhead/templates/trailhead-auto-merge.js +318 -0
  120. package/plugins/trailhead/templates/trailhead-auto-queue.js +186 -0
  121. package/plugins/trailhead/templates/trailhead-auto-run.js +294 -0
  122. package/plugins/trailhead/templates/trailhead-auto-slot.js +239 -0
  123. package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
  124. package/plugins/trailhead/templates/trailhead-checkout-in.js +88 -0
  125. package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
  126. package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
  127. package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
  128. package/plugins/trailhead/templates/trailhead-dashboard.js +360 -0
  129. package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
  130. package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
  131. package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
  132. package/plugins/trailhead/templates/trailhead-merge-ticket.js +366 -0
  133. package/plugins/trailhead/templates/trailhead-needs-you.js +193 -0
  134. package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +289 -0
  135. package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
  136. package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
  137. package/plugins/trailhead/templates/trailhead-uat-followup.js +128 -0
  138. package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
  139. package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
  140. package/plugins/trailhead/templates/trailhead-work-state.js +25 -7
  141. package/plugins/trailhead/tests/auto-run-ui.test.ts +444 -0
  142. package/plugins/trailhead/tests/auto-run-view.test.ts +111 -0
  143. package/plugins/trailhead/tests/map-pane-ui.test.ts +2519 -382
  144. package/plugins/trailhead/tests/map-pane-view.test.ts +600 -18
  145. package/plugins/trailhead/tests/map-pane-write.test.ts +51 -0
  146. package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +77 -0
  147. package/plugins/trailhead/tests/repo-pin.test.ts +123 -0
  148. package/plugins/trailhead/tests/session-registry.test.ts +50 -0
  149. package/plugins/trailhead/tests/whiteboard-follow.test.ts +90 -0
  150. package/plugins/trailhead/tests/work-state-locate.test.ts +33 -0
  151. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +276 -2
  152. package/plugins/trailhead/tests/work-state-prompt.test.ts +29 -35
  153. package/plugins/trailhead/tests/work-state-ui.test.ts +73 -1
  154. package/plugins/trailhead/tests/work-state-view.test.ts +44 -1
  155. package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
  156. package/plugins/trailhead/types/index.d.ts +62 -3
@@ -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,109 @@ 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
+
362
615
  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,18 +71,21 @@ 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) |
73
78
  | Cross-AI plan-review convergence | Yes | Sometimes (a concern blocked on a prerequisite) |
74
79
  | Verify checkpoint (which fix / how to proceed) | Yes for the advisory sub-choice; accept/resolve stays the human's | No |
75
- | Acceptance testing drive-mode ask (AI-driven vs human-driven) | Yes | No (now-or-never at UAT start) |
80
+ | Acceptance testing drive-mode ask (AI-driven, human-driven, or skip) | Yes, but delegate never picks skip (waiving acceptance is the human's call; on the reduced ask with no agent-performable step it resolves to human-driven) | No (now-or-never at UAT start) |
76
81
  | Prototype "want mockups?" | Yes | No |
77
82
  | Teamwork split shape / submodule split | Yes (the shape is advisory) | No |
78
83
  | Claim collision | No (coordination stop) | No |
79
- | `auto` isolation work-start ask (concurrency) | Yes (an autonomous run takes it as the current checkout without asking) | No (now-or-never at work-start) |
84
+ | `auto` isolation work-start ask (concurrency) | Yes (an autonomous run takes it as the current checkout without asking; a remembered answer is reused without asking) | No (now-or-never at work-start) |
80
85
  | Exhaustion "close the map?" | No (record the human owns) | No |
81
86
  | Out-of-scope routing (deferred line → seed/idea/todo) | No (routing the human owns) | This IS a defer already |
87
+ | Ticket-start scope confirm (`work`/`quick` brief) | Yes (a process confirm; `auto` takes it); skipped per **Skip the confirm on a ticket this session already shaped** in `../trailhead-work/SKILL.md` | No (now-or-never at ticket start) |
88
+ | Divergent change at the brief (as-is vs separate ticket) | No (routing the human owns) | No (a deferral is parked by the Pending follow-up guard) |
82
89
 
83
90
  The one-line test above generalises to any call-site not listed: if the answer is itself a project decision (a `decision`-ticket answer, or a destination/scope change) there is no delegate; a process choice that merely gets mentioned in a later resolution comment is still advisory.
84
91
 
@@ -15,11 +15,21 @@ 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
+ | `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** |
22
+ | `preflight.checks` | array of shell commands (**unset**) | project checks run by the stale-base preflight before UAT and integration, see **Preflight checks** |
23
+ | `uat.setup` | array of steps `{name, run, check?, note?}` (**unset**) | ordered setup the agent runs before the drive-mode ask at every UAT start, see **UAT setup** |
24
+ | `uat.accounts` | object role -> `{user, secret}` (**unset**) | named test accounts for UAT; `secret` is always a `$ENV` reference, see **UAT setup** |
21
25
 
22
- **`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.
26
+ **`git`, `release`, and the isolation mode are not config.json keys** (the one exception is `isolation.copy`, which lives in config.json; the `isolation:` mode itself stays in the conventions header). 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.
27
+
28
+ **Isolation copy.** `isolation.copy` lists the gitignored files a new `worktree`/`clone` workspace needs to run (`.env`, `.npmrc`, local config), copied from the main checkout right after the workspace is created by `trailhead-isolation-copy.js --from <main-checkout> --to <working-root>`. Write it nested (`{"isolation": {"copy": [".env"]}}`) or flat (`"isolation.copy": [".env"]`); if one file carries both, the nested form wins and a warning says so. The project value replaces the global one as a whole (no list merge, and even `[]` replaces); the global list applies only when the project file has no `isolation.copy`. Entries are plain repo-relative paths (a file or a directory), no globs. An existing destination is never overwritten (reported `present`). Unsafe entries are rejected: absolute paths, `..` segments, any `.git` segment, and any path whose parent directory resolves outside its root (source or destination, so an intermediate symlink cannot redirect a read or write); a final symlink is copied as a symlink, never followed, and nested `.git` entries are skipped inside copied directories. When the key is unset, tracked `*.example` / `*.sample` files whose stripped sibling (`.env.example` -> `.env`) exists and is gitignored in the main checkout are offered as suggestions, never copied unasked. An invalid value (bad JSON, a non-array, a non-string or empty entry) copies nothing, offers nothing, and is reported.
29
+
30
+ **Preflight checks.** `preflight.checks` lists shell commands the stale-base preflight (`trailhead-base-preflight.js`, **Stale-base preflight** in `teamwork.md`) runs at the working root, after any sync, before UAT and before integration. A non-zero exit fails the check and stops the ticket there. Write it nested (`{"preflight": {"checks": ["..."]}}`) or flat (`"preflight.checks": ["..."]`); if one file carries both, the nested form wins and a warning says so. The project value replaces the global one as a whole value. Use it for rules a textual merge cannot see, for example unique migration timestamps: `"find supabase/migrations -name '*.sql' | sed 's#.*/##; s#_.*##' | sort | uniq -d | (! grep .)"` fails when two migrations share a prefix. Each command has a timeout; an invalid declaration (not an array, or an empty entry) runs nothing and is reported as `checksError`.
31
+
32
+ **UAT setup.** `uat.setup` is an ordered list of steps `{ "name", "run", "check"?, "note"? }` that `trailhead-uat-setup.js` runs at every UAT start, after the stale-base preflight and before the drive-mode ask (see **UAT setup comes next** in `techniques/acceptance-testing.md`). `run` is a shell command run at the working root; `check`, when present, is a shell command that exits 0 when the step is already up: a passing `check` skips the step, and after `run` the helper polls `check` until it passes (or times out), so a dev server backgrounded inside `run` is detected through it; with no `check` the step always runs. `note` is free text the agent honours before running that step (for example "ask before using the Pixel"), the home for project-specific rules. `"uat": { "setup": [] }` means "no setup needed": no probe and no offer. `uat.accounts` holds named test accounts, `{ "<role>": { "user": "...", "secret": "$ENV_VAR" } }`; the agent picks the account matching the UAT step and asks when ambiguous. **References, never values:** `secret` must be a bare `$NAME` or `${NAME}` reference (a literal is rejected), and every machine-specific value (device id, local URL, a personal `user`) is a `$ENV` reference too, so the committed `.trailhead/config.json` stays generic and works unchanged in isolation workspaces; a shared QA `user` may be a literal, a personal or machine-specific one must be a reference (a documented rule, not checked). A step whose referenced variable is unset or empty fails as `missing-env` naming the variable, and nothing runs. A `$NAME` / `${NAME}` outside single quotes in `run` / `check` is such a reference and must be an exported env variable; for a shell-local one (a loop variable, an `awk` field) use single quotes or escape it as `\$NAME`. A single quote inside double quotes (for example inside `"$(awk '{print $NF}')"`) does not exempt a reference; put such a command in a script file and call the script from `run`. A long-lived process backgrounded in `run` must redirect its own output (for example `> /tmp/dev.log 2>&1 &`), else it keeps writing to the helper's deleted capture file. Write it nested (`{"uat": {"setup": [...]}}`) or flat (`"uat.setup": [...]`); if one file carries both, the nested form wins and a warning says so. The project value replaces the global one key by key as a whole value. `testing.webapp` / `testing.url` are unchanged: setup brings the target up, `testing.url` says where to drive it.
23
33
 
24
34
  **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:
25
35
  - `plan` / `execute`: the `build`/`bug` engine's **Plan** and **Execute** steps (see below).
@@ -61,7 +71,7 @@ If a value is unset, it resolves to its **Balanced**-profile default for that ac
61
71
 
62
72
  **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.
63
73
 
64
- **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.
74
+ **acceptance.browser.** The drive-mode ask (AI-driven, human-driven, or skip, see the Acceptance testing technique) **always comes first, before any step runs**: no value of this key pre-answers it, and none implies a waiver. 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.
65
75
 
66
76
  **design.surface / design.system.** Under `design: stitch`, `design.surface` picks the surface: `canvas` (default) = a Stitch project with generated screens, `design-system` = a Stitch design system; the install offer and disk fallback are in Prototype (the Stitch MCP either connects or falls to disk). Under `design: artifacts` there is one surface, a **Design canvas Artifact** (`design.project` = its URL), and `design.surface` is ignored; `design.system` names the **Design System Artifact** installed on that canvas, a read-only reference: screens are never pushed into it. **Back-compat:** a `design.project` left from the standalone claude.ai/design (a bare project id or a `claude.ai/design/p/<id>` URL) is not reusable; Prototype says so once and attaches a new canvas. Ignored under `design: disk`.
67
77
 
@@ -80,7 +90,7 @@ On **trivial/small** the engine surfaces **one consolidated skip offer** and act
80
90
 
81
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.
82
92
 
83
- **`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.
84
94
 
85
95
  Example `config.json` (same shape for the project `.trailhead/config.json` and the global `~/.claude/trailhead/config.json`):
86
96
  ```json
@@ -98,7 +108,7 @@ Example `config.json` (same shape for the project `.trailhead/config.json` and t
98
108
  - **🔀 Git**: 🌱 `main` (commit straight to the trunk) · 🌿 `pr` (feature branch + PR).
99
109
  - **🚀 Release**: 🎯 `command` (release only on an explicit command) · ♻️ `auto` (release automatically per the project's flow).
100
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).
101
- 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).
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).
102
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.
103
113
  4. **🧠 Models**: a **profile** (Balanced by default) applied whole, or **Manual** for the per-key questions.
104
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`.
@@ -109,6 +119,8 @@ Example `config.json` (same shape for the project `.trailhead/config.json` and t
109
119
  6. **🧪 TDD**: 🎯 Seams only · ✅ On (all definable behaviour) · ⛔ Off.
110
120
  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.
111
121
  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.
122
+ - **🧰 UAT setup** (sub-step): ask whether the project needs setup before a UAT: ⏭️ Keep current (when `uat.setup` is already set) · 🔎 Detect now and propose (probe package scripts, Expo/adb, docker compose, Supabase functions/migrations, an already-running server; show the plan, save on yes with `$ENV` references) · 🚫 None needed (writes `uat.setup: []`) · ⏳ Decide at the first UAT (leaves it unset).
123
+ - **👤 Test account** (optional sub-step): offer to add an account to `uat.accounts`; ask the role, the user, and the **name of the env var** holding the secret, never the secret's value.
112
124
  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:
113
125
  - **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).
114
126
  - **One CLI found**: ⛔ Off · 🤝 Use `<name>`. (With a single CLI, "all" and "pick specific" collapse to the same thing, so don't show both.)
@@ -2,7 +2,7 @@
2
2
 
3
3
  The **shared-core load contract**, single-sourced here and referenced by every cluster `SKILL.md`: the dispatcher `trailhead/` and the five clusters `trailhead-chart` / `trailhead-work` / `trailhead-view` / `trailhead-capture` / `trailhead-manage`. Each cluster's own `## Load first, in order` points here in a line, then states its cluster-specific isolation posture; this file holds the part that is identical for all of them.
4
4
 
5
- The paths below are written **relative to the cluster `SKILL.md` that pointed you here** (exactly as that `SKILL.md`'s own shared-core references are), so `../_shared/` resolves to this directory from any cluster.
5
+ Every path below is a locator, spelled the way the cluster `SKILL.md` that pointed you here spells it (so `../_shared/` is this directory). Do not read it relative: resolve it against that cluster's base directory into an **absolute path** under the `_shared/` directory this file was loaded from, and read each of the six core files with the **Read tool** on that absolute path (on a host without a Read tool, a single `cat <absolute path>` with no `cd`). Never `cd` into a skills directory and then read a relative path. The same goes for `<lib>` and `<templates>` below: pass absolute paths to `node`. Every other `../_shared/...` or `references/...` path the engine names (in any cluster doc, reference, or technique) is resolved the same way, to an absolute path, before reading.
6
6
 
7
7
  Before doing anything, read the shared core, in order:
8
8
 
@@ -17,6 +17,6 @@ Before doing anything, read the shared core, in order:
17
17
 
18
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.
19
19
 
20
- **Pinned model review, once per session.** After loading the effective config, run the read-only check: `node <lib> check <project root>`, where `<lib>` is the first of these two paths (relative to this `_shared/` dir) that exists: `../../hooks/lib/model-defaults-review.js` (Claude plugin, Claude npm copy, dev-symlink install), else `../trailhead/hooks/lib/model-defaults-review.js` (Codex). If neither exists, skip silently. If it prints a notice, offer it to the user without blocking the current request; only after the user answers any of its three choices, run `node <lib> ack <project root>`. Do this at most once per session: skip it if the SessionStart hook (or an earlier config load this session) already surfaced the same offer. Repeats across sessions are expected until the user answers.
20
+ **Pinned model review, once per session.** After loading the effective config, run the read-only check: `node <lib> check <project root>`, where `<lib>` (an absolute path, resolved from this `_shared/` dir's absolute location) is the first of these two paths (relative to this `_shared/` dir) that exists: `../../hooks/lib/model-defaults-review.js` (Claude plugin, Claude npm copy, dev-symlink install), else `../trailhead/hooks/lib/model-defaults-review.js` (Codex). If neither exists, skip silently. If it prints a notice, offer it to the user without blocking the current request; only after the user answers any of its three choices, run `node <lib> ack <project root>`. Do this at most once per session: skip it if the SessionStart hook (or an earlier config load this session) already surfaced the same offer. Repeats across sessions are expected until the user answers.
21
21
 
22
- **Label guard check, once per session.** Same pattern for the repo's label guard: run `node <templates>/trailhead-label-guard-sync.js --repo <project root> --notice`, with `<templates>` resolved as for the commit-msg sync script (see `teamwork.md`, Trust & provenance → Repo-side enforcement; skip silently if none of the template dirs has the script). It prints an offer only when the committed guard is the outdated one (no job-level `if:`, so every label event bills a runner) and the user has not answered for that file yet. Offer it without blocking the current request and follow its instructions; only after the user picks either choice, run the `--ack` command it names. At most once per session: skip it if the SessionStart hook already surfaced it.
22
+ **Label guard check, once per session.** Same pattern for the repo's label guard: run `node <templates>/trailhead-label-guard-sync.js --repo <project root> --notice`, with `<templates>` (an absolute path) resolved as for the commit-msg sync script (see `teamwork.md`, Trust & provenance → Repo-side enforcement; skip silently if none of the template dirs has the script). It prints an offer only when the committed guard is the outdated one (no job-level `if:`, so every label event bills a runner) and the user has not answered for that file yet. Offer it without blocking the current request and follow its instructions; only after the user picks either choice, run the `--ack` command it names. At most once per session: skip it if the SessionStart hook already surfaced it.
@@ -10,12 +10,12 @@ Both are set at chart and on every ticket **in the same pass** (create the ticke
10
10
  **Scoped frontier** (one query, per map):
11
11
  ```bash
12
12
  gh issue list --label "trailhead:ticket" --label "trailhead:map-<n>" --state open \
13
- --search "no:assignee -label:trailhead:blocked -label:trailhead:unverified"
13
+ --search "no:assignee -label:trailhead:blocked -label:trailhead:unverified -label:trailhead:paused"
14
14
  ```
15
15
 
16
16
  **Active map.** Which map a session works is the **active map**: a `.trailhead/active-map` marker at the working root (one line `#<n>`, **gitignored**, per-session local state like `session-ticket`, never committed). `/trailhead:work` and `/trailhead:map` default to it; naming a map/ticket explicitly always overrides. A named ticket that carries a `trailhead:map-<n>` label makes that map the active map (write `.trailhead/active-map`); a whiteboard ticket leaves the active map alone. Each checkout/clone (so each teammate) has its own.
17
17
 
18
- **Park & switch** = **pause** the in-flight ticket (`/trailhead:pause`, a `PAUSED` checkpoint), then point the active map at the other's number. Nothing else to save: every map lives on the Issues, durable. Resume later with `/trailhead:resume`, or just `/trailhead:work` once that map is active again.
18
+ **Park & switch** = **pause** the in-flight ticket (`/trailhead:pause`, a `PAUSED` checkpoint), then point the active map at the other's number. Nothing else to save: every map lives on the Issues, durable. Resume later with `/trailhead:resume` (a paused ticket is off the frontier, so bare `/trailhead:work` never picks it up).
19
19
 
20
20
  **Discovery.** `gh issue list --label "trailhead:map" --state open` enumerates the live maps. `/trailhead:map` with **no active map and more than one open** lists them and asks which; `/trailhead:map <n>` shows that one and makes it active.
21
21
 
@@ -1,6 +1,6 @@
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. 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) and **clear the work state** (`node <templates>/trailhead-work-state.js clear --repo <working-root>`, see **Work state file** in `teamwork.md`), 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) and **clear the work state**: one call, `node <templates>/trailhead-work-state.js clear --repo <working-root>` (see **Work state file** in `teamwork.md`), removes both. When the helper is missing, delete `.trailhead/session-ticket` and `.trailhead/work-state.json` by hand. Then **always** sign off with, in order:
4
4
 
5
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 and number, both visible (e.g. *Add login rate limit* (#45), never the name alone nor a bare `#number`).