@marcomigozzi/trailhead 0.16.1 → 0.18.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 (154) hide show
  1. package/README.it.md +82 -25
  2. package/README.md +82 -25
  3. package/bin/base-preflight-docs.test.js +4 -1
  4. package/bin/bulk-cross-reference-docs.test.js +36 -0
  5. package/bin/capture-action.test.js +167 -0
  6. package/bin/capture-docs.test.js +32 -0
  7. package/bin/engine-route-docs.test.js +155 -0
  8. package/bin/facet-stamp-docs.test.js +79 -0
  9. package/bin/forced-mutation-docs.test.js +38 -0
  10. package/bin/grill-model-docs.test.js +155 -0
  11. package/bin/grilling-holistic-review-docs.test.js +7 -3
  12. package/bin/guard-axis-docs.test.js +69 -0
  13. package/bin/inbox-needs-info-docs.test.js +68 -0
  14. package/bin/lib/codex-projection.js +28 -19
  15. package/bin/lib/codex-projection.test.js +77 -20
  16. package/bin/lib/host-descriptor.js +1 -1
  17. package/bin/lib/host-descriptor.test.js +6 -0
  18. package/bin/needs-you-docs.test.js +9 -2
  19. package/bin/options-in-chat-docs.test.js +65 -0
  20. package/bin/pick-stress-test-docs.test.js +1 -1
  21. package/bin/pipeline-review-docs.test.js +118 -0
  22. package/bin/plan-review-budget-docs.test.js +63 -0
  23. package/bin/repo-target-docs.test.js +41 -0
  24. package/bin/report-status-docs.test.js +10 -2
  25. package/bin/seam-coverage-docs.test.js +46 -0
  26. package/bin/session-trust-docs.test.js +37 -0
  27. package/bin/standards-brief-docs.test.js +112 -0
  28. package/bin/surfaced-scope-docs.test.js +57 -0
  29. package/bin/test-once-docs.test.js +101 -0
  30. package/bin/test-run-docs.test.js +47 -0
  31. package/bin/trailhead.js +15 -8
  32. package/bin/trailhead.test.js +71 -17
  33. package/bin/unblock-docs.test.js +56 -0
  34. package/bin/yes-accepts-recommended-docs.test.js +54 -0
  35. package/package.json +1 -1
  36. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  37. package/plugins/trailhead/agents/trailhead-code-review.md +2 -1
  38. package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
  39. package/plugins/trailhead/agents/trailhead-grill.md +41 -0
  40. package/plugins/trailhead/agents/trailhead-plan.md +1 -1
  41. package/plugins/trailhead/agents/trailhead-verify.md +1 -0
  42. package/plugins/trailhead/hooks/lib/audit-walk.test.js +62 -0
  43. package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
  44. package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
  45. package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
  46. package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
  47. package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
  48. package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
  49. package/plugins/trailhead/hooks/lib/git-env-mod.test.js +55 -0
  50. package/plugins/trailhead/hooks/lib/git-env-spawn.test.js +73 -0
  51. package/plugins/trailhead/hooks/lib/git-env.test.js +51 -0
  52. package/plugins/trailhead/hooks/lib/global-config-dir.test.js +48 -0
  53. package/plugins/trailhead/hooks/lib/heredoc-mod.test.js +85 -0
  54. package/plugins/trailhead/hooks/lib/inbox-needs-info.test.js +179 -0
  55. package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
  56. package/plugins/trailhead/hooks/lib/issue-body.test.js +165 -0
  57. package/plugins/trailhead/hooks/lib/model-defaults-review.js +22 -1
  58. package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +64 -1
  59. package/plugins/trailhead/hooks/lib/model-route.js +207 -0
  60. package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
  61. package/plugins/trailhead/hooks/lib/needs-you.test.js +45 -1
  62. package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +272 -0
  63. package/plugins/trailhead/hooks/lib/repo-target.test.js +256 -0
  64. package/plugins/trailhead/hooks/lib/report-status.test.js +56 -1
  65. package/plugins/trailhead/hooks/lib/statusline-git-env.test.js +47 -0
  66. package/plugins/trailhead/hooks/lib/test-run.test.js +757 -0
  67. package/plugins/trailhead/hooks/lib/trailhead-git-env.js +40 -0
  68. package/plugins/trailhead/hooks/mod/README.md +42 -17
  69. package/plugins/trailhead/hooks/mod/auto-run.ts +2 -1
  70. package/plugins/trailhead/hooks/mod/git-env.ts +31 -0
  71. package/plugins/trailhead/hooks/mod/heredoc.ts +79 -0
  72. package/plugins/trailhead/hooks/mod/map-pane-view.ts +3 -1
  73. package/plugins/trailhead/hooks/mod/map-pane.tsx +204 -49
  74. package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
  75. package/plugins/trailhead/hooks/mod/repo-pin.ts +113 -9
  76. package/plugins/trailhead/hooks/mod/work-state-locate.ts +2 -1
  77. package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
  78. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +28 -14
  79. package/plugins/trailhead/hooks/trailhead-check-update.js +20 -2
  80. package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
  81. package/plugins/trailhead/hooks/trailhead-labels-guard.js +2 -2
  82. package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +1 -1
  83. package/plugins/trailhead/hooks/trailhead-test-guard.js +232 -0
  84. package/plugins/trailhead/hooks/trailhead-test-guard.test.js +172 -0
  85. package/plugins/trailhead/skills/_shared/capture-core.md +123 -0
  86. package/plugins/trailhead/skills/_shared/choices.md +2 -2
  87. package/plugins/trailhead/skills/_shared/configuration-reference.md +30 -16
  88. package/plugins/trailhead/skills/_shared/configuration.md +1 -1
  89. package/plugins/trailhead/skills/_shared/load-first.md +2 -0
  90. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  91. package/plugins/trailhead/skills/_shared/substrate-commands.md +19 -6
  92. package/plugins/trailhead/skills/_shared/substrate.md +4 -3
  93. package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
  94. package/plugins/trailhead/skills/_shared/teamwork.md +11 -5
  95. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
  96. package/plugins/trailhead/skills/_shared/techniques/code-review-pipeline.md +24 -0
  97. package/plugins/trailhead/skills/_shared/techniques/code-review.md +22 -6
  98. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +6 -3
  99. package/plugins/trailhead/skills/_shared/techniques/debug.md +5 -2
  100. package/plugins/trailhead/skills/_shared/techniques/grilling.md +28 -3
  101. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +16 -4
  102. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
  103. package/plugins/trailhead/skills/_shared/techniques/tdd.md +4 -2
  104. package/plugins/trailhead/skills/_shared/techniques/verify.md +5 -1
  105. package/plugins/trailhead/skills/_shared/techniques.md +8 -6
  106. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
  107. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
  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 +11 -6
  111. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +18 -3
  112. package/plugins/trailhead/skills/trailhead-work/SKILL.md +12 -9
  113. package/plugins/trailhead/skills/trailhead-work/references/auto.md +3 -1
  114. package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +2 -0
  115. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +13 -13
  116. package/plugins/trailhead/templates/statusline-trailhead.sh +10 -3
  117. package/plugins/trailhead/templates/trailhead-audit-walk.js +56 -4
  118. package/plugins/trailhead/templates/trailhead-auto-merge.js +2 -1
  119. package/plugins/trailhead/templates/trailhead-auto-queue.js +2 -1
  120. package/plugins/trailhead/templates/trailhead-auto-slot.js +2 -1
  121. package/plugins/trailhead/templates/trailhead-base-preflight.js +10 -5
  122. package/plugins/trailhead/templates/trailhead-capture.js +419 -0
  123. package/plugins/trailhead/templates/trailhead-checkout-find.js +257 -0
  124. package/plugins/trailhead/templates/trailhead-checkout-in.js +78 -21
  125. package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +2 -1
  126. package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
  127. package/plugins/trailhead/templates/trailhead-git-env.js +40 -0
  128. package/plugins/trailhead/templates/trailhead-inbox-needs-info.js +161 -0
  129. package/plugins/trailhead/templates/trailhead-isolation-answer.js +2 -1
  130. package/plugins/trailhead/templates/trailhead-isolation-copy.js +9 -4
  131. package/plugins/trailhead/templates/trailhead-issue-body.js +161 -12
  132. package/plugins/trailhead/templates/trailhead-merge-ticket.js +2 -1
  133. package/plugins/trailhead/templates/trailhead-needs-you.js +38 -4
  134. package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +261 -7
  135. package/plugins/trailhead/templates/trailhead-repo-target.js +302 -0
  136. package/plugins/trailhead/templates/trailhead-report-status.js +57 -9
  137. package/plugins/trailhead/templates/trailhead-test-run.js +752 -0
  138. package/plugins/trailhead/templates/trailhead-uat-backfill.js +2 -1
  139. package/plugins/trailhead/templates/trailhead-uat-followup.js +2 -1
  140. package/plugins/trailhead/templates/trailhead-uat-setup.js +9 -4
  141. package/plugins/trailhead/templates/trailhead-unblock.js +2 -1
  142. package/plugins/trailhead/tests/auto-run-ui.test.ts +2 -1
  143. package/plugins/trailhead/tests/git-env.test.ts +20 -0
  144. package/plugins/trailhead/tests/map-pane-ui.test.ts +386 -50
  145. package/plugins/trailhead/tests/map-pane-view.test.ts +6 -0
  146. package/plugins/trailhead/tests/map-pane-write.test.ts +57 -1
  147. package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
  148. package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +3 -2
  149. package/plugins/trailhead/tests/repo-pin-relocate.test.ts +175 -0
  150. package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
  151. package/plugins/trailhead/tests/unscrub.ts +8 -0
  152. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +3 -2
  153. package/plugins/trailhead/tests/work-state-worktree.test.ts +2 -1
  154. package/plugins/trailhead/types/index.d.ts +7 -0
@@ -60,6 +60,11 @@ const MODEL_DEFAULTS_LIB_CANDIDATES = [
60
60
  '../trailhead/hooks/lib/model-defaults-review.js',
61
61
  ];
62
62
  function assertModelDefaultsLibLayout(label, sharedDir) {
63
+ // #323: model-route.js is resolved with the same two-path lookup (grilling.md).
64
+ const routeResolved = MODEL_DEFAULTS_LIB_CANDIDATES
65
+ .map((c) => path.join(sharedDir, c.replace('model-defaults-review.js', 'model-route.js')))
66
+ .find((p) => fs.existsSync(p));
67
+ ok(`${label}: documented model-route.js lib path resolves`, !!routeResolved);
63
68
  const resolved = MODEL_DEFAULTS_LIB_CANDIDATES
64
69
  .map((c) => path.join(sharedDir, c))
65
70
  .find((p) => fs.existsSync(p));
@@ -234,7 +239,9 @@ ok('codex: trailhead-work/../_shared/substrate.md resolves', fs.existsSync(path.
234
239
  ok('codex: _shared/load-first.md projected', fs.existsSync(path.join(codexDir, 'skills', '_shared', 'load-first.md')));
235
240
  for (const cl of ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage']) {
236
241
  ok(`codex: ${cl}/../_shared/load-first.md resolves as a sibling`, fs.existsSync(path.join(codexDir, 'skills', cl, '..', '_shared', 'load-first.md')));
237
- ok(`codex: ${cl} SKILL.md references ../_shared/load-first.md`, fs.readFileSync(path.join(codexDir, 'skills', cl, 'SKILL.md'), 'utf8').includes('../_shared/load-first.md'));
242
+ // #330: the capture cluster loads the compact capture-core.md instead.
243
+ const want = cl === 'trailhead-capture' ? '../_shared/capture-core.md' : '../_shared/load-first.md';
244
+ ok(`codex: ${cl} SKILL.md references ${want}`, fs.readFileSync(path.join(codexDir, 'skills', cl, 'SKILL.md'), 'utf8').includes(want));
238
245
  }
239
246
  // Source-level anti-drift: the shared core-list signature line lives ONLY in
240
247
  // _shared/load-first.md, never duplicated back into a cluster SKILL.md.
@@ -293,7 +300,8 @@ ok('source: load-first.md references ../_shared/ticket-language.md', loadFirstSo
293
300
  const ticketLangAutoSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-work', 'references', 'auto.md'), 'utf8');
294
301
  const ticketLangCaptureSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-capture', 'references', 'capture.md'), 'utf8');
295
302
  ok('source: auto.md references ticket-language.md', ticketLangAutoSource.includes('ticket-language.md'));
296
- ok('source: capture.md references ticket-language.md', ticketLangCaptureSource.includes('ticket-language.md'));
303
+ // #330: capture.md points at the compact capture-core.md, which is drift-stamped against ticket-language.md.
304
+ ok('source: capture.md references capture-core.md for the ticket language', ticketLangCaptureSource.includes('ticket language rule in `capture-core.md`') && /<!-- core: ticket-language\.md \|/.test(fs.readFileSync(path.join(sourceSkillsDir, '_shared', 'capture-core.md'), 'utf8')));
297
305
  // Source-level anti-drift: the substantive decoupling clause lives in EXACTLY
298
306
  // one file across the whole skills tree, _shared/ticket-language.md. A global
299
307
  // single-occurrence scan (not an enumerated per-file list) so any future doc
@@ -463,7 +471,7 @@ ok('both Verify step lines reference the Ticket diff', verifyStepLines181.every(
463
471
  ok('ticket-engines.md does NOT contain the old range-scoped diff', !ticketEnginesSource181.includes('git diff <base>...HEAD'));
464
472
 
465
473
  ok("techniques.md index row updates Code review to the ticket's own commits",
466
- techniquesSource181.includes("review the ticket's own commits (its `Refs: #<n>` set) on 4 axes"));
474
+ techniquesSource181.includes("review the ticket's own commits (its `Refs: #<n>` set) on 5 axes"));
467
475
 
468
476
  for (const [label, src] of [
469
477
  ['code-review.md', codeReviewSource181],
@@ -912,7 +920,7 @@ ok('codex: hooks.json exists', fs.existsSync(codexHooksJsonPath));
912
920
  const codexHooksJson = JSON.parse(fs.readFileSync(codexHooksJsonPath, 'utf8'));
913
921
  const codexHooksJsonStr = JSON.stringify(codexHooksJson);
914
922
  ok('codex: hooks.json registers the merged Bash guards under PreToolUse', (codexHooksJson.hooks.PreToolUse || []).some((g) => g.matcher === 'Bash' &&
915
- (g.hooks || []).some((h) => h.command.includes('trailhead-bash-guards.js') && h.command.endsWith(' --guards=commit,secret,install,body,labels'))));
923
+ (g.hooks || []).some((h) => h.command.includes('trailhead-bash-guards.js') && h.command.endsWith(' --guards=commit,secret,install,body,labels,test'))));
916
924
  ok('codex: hooks.json registers none of the per-guard scripts on their own', !/trailhead-(commit|secret|install|search|body|labels)-guard\.js/.test(codexHooksJsonStr));
917
925
  ok('codex: the six guard modules the merged hook requires are still shipped (no search)',
918
926
  ['commit', 'secret', 'install', 'body', 'labels'].every((n) => fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', `trailhead-${n}-guard.js`))) &&
@@ -941,6 +949,10 @@ ok('codex: skills/trailhead/hooks/lib/body-sources.js exists (secret-guard requi
941
949
  fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'body-sources.js')));
942
950
  ok('codex: skills/trailhead/hooks/lib/unmodelled.js exists (guards require target, #227)',
943
951
  fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'unmodelled.js')));
952
+ ok('codex: skills/trailhead/hooks/lib/inherit-session.js exists (required by model-route and model-defaults-review, #323)',
953
+ fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'inherit-session.js')));
954
+ ok('codex: skills/trailhead/hooks/lib/model-route.js exists (grilling routing helper, #323)',
955
+ fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'model-route.js')));
944
956
  ok('codex: skills/trailhead/hooks/lib/model-defaults-review.js exists (check-update require target, #186)',
945
957
  fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'model-defaults-review.js')));
946
958
  ok('codex: skills/trailhead/hooks/lib/model-defaults.json exists (#186)',
@@ -960,6 +972,10 @@ ok('codex: skills/trailhead/hooks/trailhead-mockup-link-stop.js exists',
960
972
  fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'trailhead-mockup-link-stop.js')));
961
973
  ok('codex: skills/trailhead/hooks/lib/mockup-link.js exists (mockup-link-stop require target)',
962
974
  fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'mockup-link.js')));
975
+ ok('codex: skills/trailhead/hooks/lib/gh-auth-preflight.js exists (check-update require target)',
976
+ fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'gh-auth-preflight.js')));
977
+ ok('codex: skills/trailhead/hooks/lib/trailhead-git-env.js exists (check-update require target)',
978
+ fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'trailhead-git-env.js')));
963
979
  ok('codex: skills/trailhead/hooks/trailhead-mockup-link-guard.js does NOT exist',
964
980
  !fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'trailhead-mockup-link-guard.js')));
965
981
  ok('codex: skills/trailhead/hooks/lib/shell-scan.js still exists (secret-read-guard require target)',
@@ -979,8 +995,15 @@ const noPinFixTomlPath = path.join(codexAgentsDirPath, 'trailhead-fix.toml');
979
995
  const noPinMapTomlPath = path.join(codexAgentsDirPath, 'trailhead-codebase-map.toml');
980
996
  ok('codex: trailhead-fix.toml is projected with no models.codex.* set', fs.existsSync(noPinFixTomlPath));
981
997
  ok('codex: trailhead-codebase-map.toml is projected with no models.codex.* set', fs.existsSync(noPinMapTomlPath));
982
- ok('codex: trailhead-fix.toml is pin-less (no model = line) with no models.codex.* set',
983
- fs.existsSync(noPinFixTomlPath) && !/^model = /m.test(fs.readFileSync(noPinFixTomlPath, 'utf8')));
998
+ // #325: fix non ha chiave propria, porta lo stesso pin di execute (la config del
999
+ // repo puo' impostarlo, quindi si confrontano le due righe model, qualunque sia).
1000
+ const modelLine325 = (f) => (fs.readFileSync(f, 'utf8').match(/^model = .*$/m) || [''])[0];
1001
+ const effortLine325 = (f) => (fs.readFileSync(f, 'utf8').match(/^model_reasoning_effort = .*$/m) || [''])[0];
1002
+ const executeTomlPath325 = path.join(codexAgentsDirPath, 'trailhead-execute.toml');
1003
+ ok('codex: trailhead-fix.toml carries the same model pin as trailhead-execute.toml (fix tracks execute, #325)',
1004
+ fs.existsSync(noPinFixTomlPath) && fs.existsSync(executeTomlPath325) &&
1005
+ modelLine325(noPinFixTomlPath) === modelLine325(executeTomlPath325) &&
1006
+ effortLine325(noPinFixTomlPath) === effortLine325(executeTomlPath325));
984
1007
 
985
1008
  assertModelDefaultsLibLayout('codex copy', path.join(codexDir, 'skills', '_shared'));
986
1009
 
@@ -1011,7 +1034,7 @@ ok('codex copy: templates is a regular dir (not a symlink)',
1011
1034
  assertModelDefaultsLibLayout('codex symlink', path.join(codexSymDir, 'skills', '_shared'));
1012
1035
 
1013
1036
  // --- codex agent TOML projection (#38, #88) -------------------------------------
1014
- // With NO models.codex.* set, a codex install still projects all 8 agents
1037
+ // With NO models.codex.* set, a codex install still projects all 9 agents
1015
1038
  // uniformly, pin-less (uniform projection, #88), never zero. Install from an
1016
1039
  // isolated cwd whose .trailhead/config.json sets no models.codex.*, not
1017
1040
  // repoRoot (whose own config now pins models.codex.*, so it would project
@@ -1024,11 +1047,11 @@ fs.writeFileSync(
1024
1047
  );
1025
1048
  const unsetCodexHomeDir = mktmp();
1026
1049
  runInstaller([`--codex`, `--dir=${unsetCodexHomeDir}`], { cwd: unsetProjectDir });
1027
- ok('codex: all 8 trailhead-*.toml exist under agents/ even with models.codex.* unset', (() => {
1050
+ ok('codex: all 9 trailhead-*.toml exist under agents/ even with models.codex.* unset', (() => {
1028
1051
  const dir = path.join(unsetCodexHomeDir, 'agents');
1029
1052
  if (!fs.existsSync(dir)) return false;
1030
1053
  const tomls = fs.readdirSync(dir).filter((f) => /^trailhead-.*\.toml$/.test(f));
1031
- return tomls.length === 8;
1054
+ return tomls.length === 9;
1032
1055
  })());
1033
1056
  ok('codex: every trailhead-*.toml is pin-less when models.codex.* is unset', (() => {
1034
1057
  const dir = path.join(unsetCodexHomeDir, 'agents');
@@ -1037,7 +1060,7 @@ ok('codex: every trailhead-*.toml is pin-less when models.codex.* is unset', (()
1037
1060
  })());
1038
1061
 
1039
1062
  // #250: the reviewing block (single source scripts/agent-blocks.md) is projected
1040
- // verbatim into the two reviewing agents' TOMLs and into none of the other six.
1063
+ // verbatim into the two reviewing agents' TOMLs and into none of the other seven.
1041
1064
  {
1042
1065
  const sync250 = require(path.join(repoRoot, 'scripts', 'sync-agent-blocks.js'));
1043
1066
  const reviewingBlock250 = sync250.parseSource(fs.readFileSync(path.join(repoRoot, 'scripts', 'agent-blocks.md'), 'utf8')).reviewing;
@@ -1045,7 +1068,7 @@ ok('codex: every trailhead-*.toml is pin-less when models.codex.* is unset', (()
1045
1068
  const agentsDir250 = path.join(unsetCodexHomeDir, 'agents');
1046
1069
  const reviewingNames250 = new Set(['trailhead-verify', 'trailhead-review']); // the code-review agent projects as trailhead-review.toml
1047
1070
  const tomls250 = fs.readdirSync(agentsDir250).filter((f) => /^trailhead-.*\.toml$/.test(f));
1048
- ok('#250: all 8 TOMLs are checked', tomls250.length === 8);
1071
+ ok('#250: all 9 TOMLs are checked', tomls250.length === 9);
1049
1072
  for (const f of tomls250) {
1050
1073
  const name = f.replace(/\.toml$/, '');
1051
1074
  const text = fs.readFileSync(path.join(agentsDir250, f), 'utf8');
@@ -1065,6 +1088,15 @@ ok('codex migration: stale prompts/trailhead.md swept on install', !fs.existsSyn
1065
1088
  ok('codex migration: stale prompts/trailhead-work.md swept on install', !fs.existsSync(path.join(migCodexDir, 'prompts', 'trailhead-work.md')));
1066
1089
  ok('codex migration: per-verb skill projected in its place', fs.existsSync(path.join(migCodexDir, 'skills', 'trailhead-work', 'SKILL.md')));
1067
1090
 
1091
+ // La config globale di Codex sta nella cartella del vecchio motore: un aggiornamento non deve cancellarla.
1092
+ const keepCodexDir = mktmp();
1093
+ fs.mkdirSync(path.join(keepCodexDir, 'trailhead', 'references'), { recursive: true });
1094
+ fs.writeFileSync(path.join(keepCodexDir, 'trailhead', 'references', 'old.md'), 'stale engine\n');
1095
+ fs.writeFileSync(path.join(keepCodexDir, 'trailhead', 'config.json'), '{"testing":{"full":"npm test"}}\n');
1096
+ runInstaller([`--codex`, `--dir=${keepCodexDir}`]);
1097
+ ok('codex install: global config survives', fs.existsSync(path.join(keepCodexDir, 'trailhead', 'config.json')));
1098
+ ok('codex install: old engine files still swept', !fs.existsSync(path.join(keepCodexDir, 'trailhead', 'references')));
1099
+
1068
1100
  // codex migration: a stale trailhead-owned skill dir (pre-split monolith /
1069
1101
  // removed cluster) is swept on reinstall; a co-tenant skill is preserved.
1070
1102
  const migCodexSkillDir = mktmp();
@@ -1108,7 +1140,8 @@ ok('claude: cluster ../_shared/ reference resolves', fs.existsSync(path.join(cla
1108
1140
  ok('claude: skills/_shared/load-first.md exists', fs.existsSync(path.join(claudeDir, 'skills', '_shared', 'load-first.md')));
1109
1141
  ok('claude: trailhead-work/../_shared/load-first.md resolves', fs.existsSync(path.join(claudeDir, 'skills', 'trailhead-work', '..', '_shared', 'load-first.md')));
1110
1142
  for (const cl of ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage']) {
1111
- ok(`claude: ${cl} SKILL.md references ../_shared/load-first.md`, fs.readFileSync(path.join(claudeDir, 'skills', cl, 'SKILL.md'), 'utf8').includes('../_shared/load-first.md'));
1143
+ const want = cl === 'trailhead-capture' ? '../_shared/capture-core.md' : '../_shared/load-first.md';
1144
+ ok(`claude: ${cl} SKILL.md references ${want}`, fs.readFileSync(path.join(claudeDir, 'skills', cl, 'SKILL.md'), 'utf8').includes(want));
1112
1145
  }
1113
1146
 
1114
1147
  // #146: single-sourced ticket-language directive, projected on Claude too.
@@ -1128,6 +1161,10 @@ ok('claude: hooks/lib/body-sources.js exists (secret-guard require target, #210)
1128
1161
  fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'body-sources.js')));
1129
1162
  ok('claude: hooks/lib/unmodelled.js exists (guards require target, #227)',
1130
1163
  fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'unmodelled.js')));
1164
+ ok('claude: hooks/lib/inherit-session.js exists (required by model-route and model-defaults-review, #323)',
1165
+ fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'inherit-session.js')));
1166
+ ok('claude: hooks/lib/model-route.js exists (grilling routing helper, #323)',
1167
+ fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'model-route.js')));
1131
1168
  ok('claude: hooks/lib/model-defaults-review.js exists (check-update require target, #186)',
1132
1169
  fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'model-defaults-review.js')));
1133
1170
  ok('claude: hooks/lib/model-defaults.json exists (#186)',
@@ -1185,7 +1222,7 @@ ok('claude: SKILL.md does not start with the codex adapter header', !fs.readFile
1185
1222
  // falls back to running inline on the session model, so the per-technique
1186
1223
  // config.models.* split silently never applies.
1187
1224
  const claudeAgentsDir = path.join(claudeDir, 'agents');
1188
- const expectedAgents = ['trailhead-plan', 'trailhead-executor', 'trailhead-research', 'trailhead-code-review', 'trailhead-debug', 'trailhead-fix', 'trailhead-codebase-map'];
1225
+ const expectedAgents = ['trailhead-plan', 'trailhead-executor', 'trailhead-research', 'trailhead-code-review', 'trailhead-debug', 'trailhead-fix', 'trailhead-codebase-map', 'trailhead-grill'];
1189
1226
  for (const a of expectedAgents) {
1190
1227
  ok(`claude: agents/${a}.md placed`, fs.existsSync(path.join(claudeAgentsDir, `${a}.md`)));
1191
1228
  }
@@ -1309,12 +1346,14 @@ ok('claude: exactly one PreToolUse/Bash trailhead-bash-guards registration', (()
1309
1346
  const s = JSON.parse(settingsContent);
1310
1347
  return (s.hooks.PreToolUse || []).flatMap((g) => g.hooks || []).filter((h) => h.command.includes('trailhead-bash-guards.js')).length === 1;
1311
1348
  })());
1312
- ok('claude: hooks/trailhead-bash-guards.js and its six modules exist', ['bash-guards', 'commit-guard', 'secret-guard', 'install-guard', 'search-guard', 'body-guard', 'labels-guard']
1349
+ ok('claude: hooks/trailhead-bash-guards.js and its seven modules exist', ['bash-guards', 'commit-guard', 'secret-guard', 'install-guard', 'search-guard', 'body-guard', 'labels-guard', 'test-guard']
1313
1350
  .every((n) => fs.existsSync(path.join(claudeDir, 'hooks', `trailhead-${n}.js`))));
1314
1351
  // mockup-link-guard: Claude-only PreToolUse(AskUserQuestion) backstop for the
1315
1352
  // Prototype "link in the same message as the approval ask" rule.
1316
1353
  ok('claude: hooks/trailhead-mockup-link-guard.js exists', fs.existsSync(path.join(claudeDir, 'hooks', 'trailhead-mockup-link-guard.js')));
1317
1354
  ok('claude: hooks/lib/mockup-link.js exists (mockup-link-guard require target)', fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'mockup-link.js')));
1355
+ ok('claude: hooks/lib/gh-auth-preflight.js exists (check-update require target)', fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'gh-auth-preflight.js')));
1356
+ ok('claude: hooks/lib/trailhead-git-env.js exists (check-update require target)', fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'trailhead-git-env.js')));
1318
1357
  ok('claude: hooks/trailhead-mockup-link-stop.js does NOT exist (Codex-only twin)', !fs.existsSync(path.join(claudeDir, 'hooks', 'trailhead-mockup-link-stop.js')));
1319
1358
  ok('claude: settings.json does NOT register mockup-link-stop', !settingsContent.includes('trailhead-mockup-link-stop.js'));
1320
1359
  ok('claude: settings.json registers mockup-link-guard under PreToolUse/AskUserQuestion',
@@ -1390,6 +1429,21 @@ const pinConfigTomlPath = path.join(pinCodexHomeDir, 'config.toml');
1390
1429
  ok('codex: config.toml enables multi_agent_v2 when a models.codex.* pin is projected',
1391
1430
  fs.existsSync(pinConfigTomlPath) && fs.readFileSync(pinConfigTomlPath, 'utf8').includes('multi_agent_v2 = true'));
1392
1431
 
1432
+ // #323: a project layer that sets plan but not grill grills on that plan, even when
1433
+ // the global layer pins grill (the implicit-grill rule applies per layer, before the merge).
1434
+ {
1435
+ const pd = mktmp();
1436
+ fs.mkdirSync(path.join(pd, '.trailhead'), { recursive: true });
1437
+ fs.writeFileSync(path.join(pd, '.trailhead', 'config.json'), JSON.stringify({ models: { codex: { plan: 'gpt-5.6-sol' } } }));
1438
+ const ch = mktmp();
1439
+ fs.mkdirSync(path.join(ch, 'trailhead'), { recursive: true });
1440
+ fs.writeFileSync(path.join(ch, 'trailhead', 'config.json'), JSON.stringify({ models: { codex: { grill: 'gpt-5.6-luna' } } }));
1441
+ runInstaller([`--codex`, `--dir=${ch}`], { cwd: pd });
1442
+ const grillToml = fs.readFileSync(path.join(ch, 'agents', 'trailhead-grill.toml'), 'utf8');
1443
+ ok('codex: project plan with no project grill beats a global grill pin in trailhead-grill.toml',
1444
+ grillToml.includes('model = "gpt-5.6-sol"') && !grillToml.includes('gpt-5.6-luna'));
1445
+ }
1446
+
1393
1447
  // Uninstall must sweep the projected TOML too, without a --dir= cwd dependency.
1394
1448
  runInstaller([`--codex`, `--dir=${pinCodexHomeDir}`, '--uninstall']);
1395
1449
  ok('codex uninstall: trailhead-execute.toml is gone', !fs.existsSync(pinnedTomlPath));
@@ -1690,7 +1744,7 @@ const bashGuardCommands = (registry) => (registry.hooks.PreToolUse || [])
1690
1744
  ok('(#207) codex migration: exit 0', out.status === 0);
1691
1745
  const cmds = bashGuardCommands(JSON.parse(fs.readFileSync(hooksJsonFile, 'utf8')));
1692
1746
  ok('(#207) codex migration: only the merged entry remains', cmds.length === 1 &&
1693
- cmds[0].endsWith(' --guards=commit,secret,install,body,labels'));
1747
+ cmds[0].endsWith(' --guards=commit,secret,install,body,labels,test'));
1694
1748
  const once = fs.readFileSync(hooksJsonFile, 'utf8');
1695
1749
  runInstaller([`--codex`, `--dir=${mDir}`]);
1696
1750
  ok('(#207) codex install twice leaves the same hooks.json', fs.readFileSync(hooksJsonFile, 'utf8') === once);
@@ -2067,8 +2121,8 @@ ok('source: hooks.json keeps its command hooks next to the module (#202)',
2067
2121
 
2068
2122
  // --- #259: `trailhead:paused` label marks a paused ticket (pause adds, resume/claim drop) ---
2069
2123
  const firstUseLine = (substrateCommandsSource.match(/^1\. Create any missing labels.*$/m) || [''])[0];
2070
- ok('source: first-use setup lists twenty-one labels including trailhead:paused, trailhead:uat and trailhead:needs-you',
2071
- /all twenty-one:/.test(firstUseLine) && firstUseLine.includes('`trailhead:paused`') && firstUseLine.includes('`trailhead:uat`') && firstUseLine.includes('`trailhead:needs-you`'));
2124
+ ok('source: first-use setup lists twenty-two labels including trailhead:paused, trailhead:uat, trailhead:needs-you and trailhead:needs-info',
2125
+ /all twenty-two:/.test(firstUseLine) && firstUseLine.includes('`trailhead:paused`') && firstUseLine.includes('`trailhead:uat`') && firstUseLine.includes('`trailhead:needs-you`') && firstUseLine.includes('`trailhead:needs-info`'));
2072
2126
  ok('source: no stale "eighteen" or "nineteen" label count remains in the shared skills',
2073
2127
  !/eighteen|nineteen/.test(substrateCommandsSource) && !/eighteen|nineteen/.test(substrateSource));
2074
2128
  ok('source: substrate-commands.md ensures trailhead:paused idempotently',
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #322: resolving any ticket (map or whiteboard, via work or quick) unblocks
3
+ // its dependents, as a step independent of the map-only `Decisions so far` book-keeping.
4
+ // Plain asserts, no framework. Run: node bin/unblock-docs.test.js
5
+ const assert = require('assert');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const skills = path.resolve(__dirname, '..', 'plugins', 'trailhead', 'skills');
10
+ const read = (p) => fs.readFileSync(path.join(skills, p), 'utf8');
11
+ const engines = read('trailhead-work/references/ticket-engines.md');
12
+ const work = read('trailhead-work/SKILL.md');
13
+ const commands = read('_shared/substrate-commands.md');
14
+ const HELPER = 'trailhead-unblock.js --issue <n> --repo <owner/name>';
15
+ let passed = 0;
16
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
17
+
18
+ const resolveLine = (section) => {
19
+ const start = engines.indexOf(section);
20
+ const m = /^5\. \*\*Resolve\*\*.*$/m.exec(engines.slice(start));
21
+ return m ? m[0] : '';
22
+ };
23
+ const unblockClause = (line) => {
24
+ const i = line.indexOf('**Unblock dependents');
25
+ if (i < 0) return '';
26
+ // the clause ends at the next bold step (the handoff), not at a fixed window
27
+ const end = line.indexOf('**Session handoff**', i);
28
+ return line.slice(i, end < 0 ? undefined : end);
29
+ };
30
+
31
+ for (const section of ['## `build`', '## `bug`']) {
32
+ const line = resolveLine(section);
33
+ const clause = unblockClause(line);
34
+ ok(`engines ${section}: Resolve has an Unblock dependents clause`, clause !== '');
35
+ ok(`engines ${section}: names the helper`, clause.includes(HELPER));
36
+ ok(`engines ${section}: applies to map and whiteboard tickets alike`, /whiteboard/.test(clause) && /map/.test(clause));
37
+ ok(`engines ${section}: keeps the by-hand fallback`, clause.includes('--remove-label "trailhead:blocked"'));
38
+ ok(`engines ${section}: independent of the map-only Decisions so far update`, /independent of the map-only `Decisions so far` update/.test(clause));
39
+ ok(`engines ${section}: runs right after the close`, /^\*\*Unblock dependents[^*]*\*\*\s*Right after the close/.test(clause) && line.indexOf('gh issue close') < line.indexOf('**Unblock dependents'));
40
+ ok(`engines ${section}: fallback names the blocked_by endpoint`, clause.includes('dependencies/blocked_by'));
41
+ }
42
+
43
+ const step5 = (/^5\. Record the resolution.*$/m.exec(work) || [''])[0];
44
+ ok('SKILL.md step 5: unblock is its own sentence naming the helper', /\.\s+Unblocking is a separate step[^]*\*\*unblock dependents\*\*/.test(step5) && step5.includes('trailhead-unblock.js'));
45
+ ok('SKILL.md step 5: holds for whiteboard tickets too', /whiteboard/.test(step5));
46
+
47
+ const bullet = (/^- \*\*No map narrative book-keeping\.\*\*.*$/m.exec(work) || [''])[0];
48
+ ok('SKILL.md quick bullet exists', bullet !== '');
49
+ ok('SKILL.md quick bullet: unblocking is among the steps that still hold', /Everything else holds:[^.]*unblocking dependents/.test(bullet) && bullet.includes('trailhead-unblock.js'));
50
+ ok('SKILL.md quick bullet: no map-only exception', !bullet.includes('Exception, `quick <n>` on a *map* ticket'));
51
+
52
+ ok('substrate-commands.md: unblock block names the helper as default', /Unblock:[\s\S]{0,400}trailhead-unblock\.js --issue <n> --repo <owner\/name>/.test(commands));
53
+ ok('substrate-commands.md: keeps the raw gh command as fallback', /Unblock:[\s\S]{0,700}gh issue edit <(?:n|dep)> --remove-label "trailhead:blocked"/.test(commands));
54
+ ok('substrate-commands.md: cookbook comment names the helper', /# unblock[^\n]*trailhead-unblock\.js/.test(commands));
55
+ ok('no em-dash', ![engines, work, commands].some((s) => s.includes(String.fromCharCode(0x2014))));
56
+ console.log(`unblock-docs.test.js: ${passed} checks passed`);
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #358: in grilling a plain "yes" always accepts the recommended answer.
3
+ // The rule is defined once (Grilling technique); the grill return contract states it for the agent.
4
+ // Run: node yes-accepts-recommended-docs.test.js
5
+ const assert = require('assert');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const repo = path.join(__dirname, '..');
10
+ const plugin = path.join(repo, 'plugins', 'trailhead');
11
+ const read = (...rel) => fs.readFileSync(path.join(...rel), 'utf8');
12
+ const line = (text, start) => text.split('\n').find((l) => l.startsWith(start));
13
+
14
+ let passed = 0;
15
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
16
+
17
+ const DEFINITION = '**A yes accepts the recommendation.**';
18
+ const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
19
+ e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]);
20
+
21
+ // 1. defined exactly once, in the Grilling technique
22
+ const techniquePath = path.join(plugin, 'skills', '_shared', 'techniques', 'grilling.md');
23
+ const defs = walk(plugin).filter((f) => read(f).includes(DEFINITION));
24
+ ok('definition appears in exactly one file', defs.length === 1);
25
+ ok('definition lives in grilling.md', defs[0] === techniquePath);
26
+
27
+ // 2. the definition carries the normative fragments, on the one-question bullet
28
+ const technique = read(techniquePath);
29
+ const rule = line(technique, '- **One question at a time, with your pick.**');
30
+ ok('rule sits on the one-question bullet', rule.includes(DEFINITION));
31
+ for (const frag of [
32
+ 'a plain "yes" accepts your pick',
33
+ 'never an open "should we A or B?" followed by a recommendation',
34
+ 'exactly one meaning',
35
+ 'A play-back',
36
+ ]) ok(`rule carries: ${frag}`, rule.includes(frag));
37
+
38
+ // 3. the relay never rewords the question out of the rule
39
+ const relay = line(technique, ' - `question`: relay');
40
+ ok('relay names the rule', relay.includes('**A yes accepts the recommendation**'));
41
+
42
+ // 4. the return contract states it, in the source block and in the synced agent
43
+ for (const [name, text] of [
44
+ ['agent-blocks.md', read(repo, 'scripts', 'agent-blocks.md')],
45
+ ['trailhead-grill.md', read(plugin, 'agents', 'trailhead-grill.md')],
46
+ ]) {
47
+ const q = line(text, '- `question:` the one numbered question');
48
+ const r = line(text, '- `recommended:` your pick');
49
+ ok(`${name}: question is worded so a yes accepts the pick`, q.includes('a plain "yes" accepts `recommended:`'));
50
+ ok(`${name}: question bans the open form`, q.includes('never an open "should we A or B?"'));
51
+ ok(`${name}: recommended is what a yes accepts`, r.includes('what a plain "yes" accepts'));
52
+ }
53
+
54
+ console.log(`yes-accepts-recommended-docs: ${passed} assertions passed`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcomigozzi/trailhead",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "description": "Start and drive large projects as a map of tickets on GitHub Issues. Map-based onboarding + a discuss→plan→execute→verify engine, all on the Issues. Installs the trailhead skill, commands, and hooks into your agent's config dir.",
5
5
  "bin": {
6
6
  "trailhead": "bin/trailhead.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trailhead",
3
- "version": "0.16.1",
3
+ "version": "0.18.0",
4
4
  "description": "Start and drive large projects as a map of tickets on GitHub Issues, resolving one at a time. Map-based onboarding + a discuss→plan→execute→verify engine with atomic commits, all on the Issues. Ships as a dispatcher plus cohesion-clustered skills over a shared core; depends on nothing beyond an authenticated gh CLI.",
5
5
  "author": {
6
6
  "name": "Marco Migozzi",
@@ -7,7 +7,7 @@ tools: Read, Grep, Glob, Bash, WebFetch, WebSearch, TodoWrite
7
7
 
8
8
  trailhead's code-review subagent. Review the ticket's diff adversarially on the Code review technique's axes, verifying each finding before reporting. Do not defer to a convention or a settled-decision memory.
9
9
 
10
- The full protocol is injected at spawn from the trailhead skill's single source (`_shared/techniques/code-review.md`); follow that, not any embedded copy. Read-only by policy: it reviews and returns the report (the orchestrator posts it as the `VERIFY` comment), it must never Edit/Write or commit. Model-agnostic by design: the engine passes the `config.models.review` model as the spawn-time override, so config stays authoritative.
10
+ The full protocol is injected at spawn from the trailhead skill's single source (`_shared/techniques/code-review.md`); follow that, not any embedded copy. Read-only by policy: it reviews and returns the report (the orchestrator posts it as the `VERIFY` comment), it must never Edit/Write or commit. Model-agnostic by design: the engine passes the `config.models.review` model as the spawn-time override, so config stays authoritative. A pipeline-touching diff also loads `code-review-pipeline.md`, at the path the spawn names. Read the repo's standards files, at the paths the spawn names, for the Standards & spec axis, and cite the file for each standards finding.
11
11
 
12
12
  <!-- trailhead:agent-block common -->
13
13
  **Search-command hygiene.** Prefer the Grep and Read tools; use absolute paths and `git -C <abs>`.
@@ -22,4 +22,5 @@ Never `cd` (or `pushd`/`env -C`) into a directory first; the full rule is in `_s
22
22
  - trailhead-verify: `<token>` is one of `met` | `gaps_found` | `inconclusive`; each claim line carries `class: <met|unmet|partial|inconclusive>`, and `met` lists no `unmet`, `partial` or `inconclusive` claim.
23
23
  - trailhead-code-review: `<token>` is one of `clean` | `issues_found`, and line 2 is the counts line `critical: <n> · warning: <n> · info: <n>`; `clean` with a non-zero `critical` count requires a ledger line closing every Critical (`fixed`, `deferred` or `accepted`), none `open`.
24
24
  - Read-only: never Edit/Write, never commit.
25
+ - When the orchestrator passes a test record (`test record: ...`, the full-suite outcome of Verify), read it and never re-run the full suite (a focused test to probe one finding is fine); with no record passed, test as today (without `testing.full` no test record is passed).
25
26
  <!-- /trailhead:agent-block reviewing -->
@@ -7,7 +7,7 @@ tools: Read, Grep, Glob, Bash, WebFetch, WebSearch, TodoWrite
7
7
 
8
8
  trailhead's codebase-map reader subagent. Map ONE non-overlapping facet of the repo for the distilled `trailhead:codebase` issue; do not read another reader's territory.
9
9
 
10
- The full protocol is injected at spawn from the trailhead skill's single source (`_shared/techniques/codebase-map.md`); follow that, not any embedded copy. Read-only by policy: read-only by construction. Model-agnostic by design: the engine passes the session model at spawn (no dedicated model key), so config stays authoritative.
10
+ The full protocol is injected at spawn from the trailhead skill's single source (`_shared/techniques/codebase-map.md`); follow that, not any embedded copy. Read-only by policy: read-only by construction. Model-agnostic by design: the engine passes the `config.models.codebase-map` model as the spawn-time override, so config stays authoritative.
11
11
 
12
12
  <!-- trailhead:agent-block common -->
13
13
  **Search-command hygiene.** Prefer the Grep and Read tools; use absolute paths and `git -C <abs>`.
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: trailhead-grill
3
+ color: yellow
4
+ description: "trailhead grill subagent: one read-only Grilling round, returned as a question or a holistic review."
5
+ tools: Read, Grep, Glob, WebFetch, WebSearch, TodoWrite
6
+ ---
7
+
8
+ trailhead's grill subagent. Run one round of Grilling for the orchestrator: read the code facts the question needs, then return either the next numbered question with your recommended answer, or the holistic review of the settled choices. You are read-only: you never talk to the user and never answer on their behalf; the orchestrator relays your question and re-dispatches you with the answer.
9
+
10
+ The full protocol is injected at spawn from the trailhead skill's single source (`_shared/techniques/grilling.md`); follow its round bullets and the `Return contract` below, not any embedded copy. The orchestrator-facing section of that file is not yours. The orchestrator passes the ticket body, the relevant decisions, the tickets the subject names and the Q&A so far; a fact you cannot reach goes under `facts` as `not looked up:`. Model-agnostic by design: the engine passes the `config.models.grill` model as the spawn-time override, so config stays authoritative.
11
+
12
+ <!-- trailhead:agent-block common -->
13
+ **Search-command hygiene.** Prefer the Grep and Read tools; use absolute paths and `git -C <abs>`.
14
+ Never `cd` (or `pushd`/`env -C`) into a directory first; the full rule is in `_shared/techniques.md`.
15
+
16
+ **Boundaries.** Never `git push`. Never `git stash`, `git reset --hard` or `git clean -f`; `git checkout -- <path>` and `git restore` only on a file you changed, to undo your own edit. Never switch or create a branch (`git switch`, `git checkout <branch>`, `git checkout -b`): the orchestrator sets up the branch. Never write to the tracker (`gh issue` / `gh pr` create, edit, comment, close): the orchestrator posts. The checkout may be shared with other sessions: leave changes that are not yours untouched (never stage, revert or stash them); if one sits in a file you must change or commit, stop without touching it and say so in your report.
17
+ <!-- /trailhead:agent-block common -->
18
+
19
+ <!-- trailhead:agent-block grilling -->
20
+ **Return contract.** Return the complete report as your final message. Do not post it anywhere (no issue comment, no PR comment): the orchestrator posts what it records. Never commit. Read-only: never Edit/Write.
21
+ - The first line of the report is `status: <token>`, verbatim, with nothing before it. It is the only line starting with `status:`: status text anywhere in the body is quoted in a code fence or a blockquote, or reworded.
22
+ - statuses: `question` | `review`.
23
+ - required fields: `settled`, `frontier`, `amendments`, `conflicts`.
24
+ - optional fields: `question`, `recommended`, `areas`, `facts`.
25
+ - invariant: `question` => `question` non-empty.
26
+ - invariant: `question` => `recommended` non-empty.
27
+ - invariant: `question` => `amendments` none.
28
+ - invariant: `question` => `conflicts` none.
29
+ - invariant: `review` => `frontier` none.
30
+ - invariant: `review` => `question` none.
31
+ - A field is a top-level line `<label>:` (outside code fences and blockquotes); its value is the rest of that line plus every following line up to the next field label. An empty value or `none` means none.
32
+ - Fields are the end of the report: nothing after the last field.
33
+ - You never talk to the user and never answer on their behalf: the orchestrator relays the question and collects the answer. The recommended answer is only your pick.
34
+ - `settled:` every settled choice so far, one per line by question number, with its rationale; `none` before the first answer.
35
+ - `frontier:` the open questions whose prerequisites are settled; `none` once it is empty.
36
+ - `question:` the one numbered question to ask now, worded around your pick so a plain "yes" accepts `recommended:` and has no other meaning; never an open "should we A or B?" followed by a recommendation. Illustrate each option (its label, a one-line summary, its trade-off) so the relay can render them per **The ask is answerable from the chat alone**; never a reference to the ticket in place of the options. Offer defer in prose when the question is blocked on a prerequisite. Play back the user's understanding when it closes a branch.
37
+ - `recommended:` your pick, what a plain "yes" accepts, stress-tested, ending with a `Weak point:` line (not a field label, so a review may carry one per fix). Never the delegate option.
38
+ - `areas:` for a multi-part space, every functional area, each marked visited or open.
39
+ - `facts:` what you looked up, with `file:line` or a URL. A fact you could not reach is written as `not looked up: <what>`; the orchestrator fetches it for the next round.
40
+ - `amendments:` / `conflicts:` the holistic review. Each item names its question numbers; each conflict carries its fix, or one line saying where it goes. Both are `none` when nothing is found, and always `none` on a `question` round.
41
+ <!-- /trailhead:agent-block grilling -->
@@ -26,6 +26,6 @@ Never `cd` (or `pushd`/`env -C`) into a directory first; the full rule is in `_s
26
26
  - invariant: `needs_input` => `questions` non-empty.
27
27
  - A field is a top-level line `<label>:` (outside code fences and blockquotes); its value is the rest of that line plus every following line up to the next field label. An empty value or `none` means none.
28
28
  - With `needs_input`, fields are the end of the report: nothing after the last field.
29
- - `ready`: the rest of the report is the PLAN (the `body`): the steps, the seams, the files touched and the verification criteria. It is posted verbatim, so write nothing in it that is not the PLAN.
29
+ - `ready`: the rest of the report is the PLAN (the `body`): the steps, the seams (one line each, `<seam>: catches <what a test here fails on>; misses <what gets through it>`), the files touched and the verification criteria. It is posted verbatim, so write nothing in it that is not the PLAN.
30
30
  - `needs_input`: `questions:` holds the blocking questions only; no PLAN.
31
31
  <!-- /trailhead:agent-block plan -->
@@ -22,4 +22,5 @@ Never `cd` (or `pushd`/`env -C`) into a directory first; the full rule is in `_s
22
22
  - trailhead-verify: `<token>` is one of `met` | `gaps_found` | `inconclusive`; each claim line carries `class: <met|unmet|partial|inconclusive>`, and `met` lists no `unmet`, `partial` or `inconclusive` claim.
23
23
  - trailhead-code-review: `<token>` is one of `clean` | `issues_found`, and line 2 is the counts line `critical: <n> · warning: <n> · info: <n>`; `clean` with a non-zero `critical` count requires a ledger line closing every Critical (`fixed`, `deferred` or `accepted`), none `open`.
24
24
  - Read-only: never Edit/Write, never commit.
25
+ - When the orchestrator passes a test record (`test record: ...`, the full-suite outcome of Verify), read it and never re-run the full suite (a focused test to probe one finding is fine); with no record passed, test as today (without `testing.full` no test record is passed).
25
26
  <!-- /trailhead:agent-block reviewing -->
@@ -798,5 +798,67 @@ function cli(args) {
798
798
  ok('attribute merge replacing a tracked line: merge is not a producer', !shas(c).includes(m));
799
799
  }
800
800
 
801
+ // --- facets: freshness of the codebase issue's facets -------------------------
802
+ {
803
+ const facetsRun = (dir, body) => {
804
+ const file = path.join(dir, '.git', 'facets-body.md');
805
+ fs.writeFileSync(file, body);
806
+ let text = '';
807
+ const code = walk.main(['facets', '--repo', dir, '--body-file', file], { stdout: { write: (t) => { text += t; } } });
808
+ return { code, out: JSON.parse(text) };
809
+ };
810
+ const facet = (name, sha, ...cited) => `## ${name}\n\n${sha ? `_as of ${sha}_\n\n` : ''}${cited.map((c) => `- \`${c}\` is here`).join('\n')}\n\n`;
811
+ const byName = (r, name) => r.out.facets.find((f) => f.name === name);
812
+
813
+ const dir = mkrepo();
814
+ const c1 = commit(dir, {
815
+ 'src/a.js': 'a\n', 'src/b.js': 'b\n', 'lib/old.js': 'o\n', 'lib/mv.js': 'm\n',
816
+ 'docs/x.md': 'x\n', 'star/[ab].txt': 's\n', 'star/a.txt': 'plain\n',
817
+ }, 'feat: base');
818
+ const s1 = c1.slice(0, 7);
819
+ const body = '# Codebase\n\n' + [
820
+ facet('Fresh', s1, 'docs/x.md'),
821
+ facet('Edited', s1, 'src/a.js:3-9', 'docs/x.md'),
822
+ facet('Gone', s1, 'lib/old.js'),
823
+ facet('Moved', s1, 'lib/mv.js'),
824
+ facet('Unstamped', null, 'src/b.js'),
825
+ facet('Rewritten', 'deadbeef', 'src/b.js'),
826
+ facet('Nothing', s1, 'no/such.js', '/etc/passwd', '../x', '-rf', ':x', 'a b', 'plain words'),
827
+ facet('Glob', s1, 'star/[ab].txt'),
828
+ facet('Dir', s1, 'src/'),
829
+ ].join('') + '## Fenced\n\n```\n## Not a facet\n```\n';
830
+ commit(dir, { 'src/a.js': 'a2\n', 'lib/old.js': null, 'star/[ab].txt': 's2\n' }, 'fix: edits');
831
+ git(dir, ['mv', 'lib/mv.js', 'lib/moved.js']);
832
+ const c2 = finishCommit(dir, ['commit', '-q'], 'refactor: rename', {});
833
+ const r = facetsRun(dir, body);
834
+ eq('facets: exit 0 and head', [r.code, r.out.head], [0, c2]);
835
+ eq('facets: fresh facet is current', [byName(r, 'Fresh').status, byName(r, 'Fresh').changed], ['current', []]);
836
+ eq('facets: edited cited file (with :line suffix) is stale', [byName(r, 'Edited').status, byName(r, 'Edited').changed, byName(r, 'Edited').paths], ['stale', ['src/a.js'], ['src/a.js', 'docs/x.md']]);
837
+ eq('facets: deleted cited file is stale', [byName(r, 'Gone').status, byName(r, 'Gone').changed], ['stale', ['lib/old.js']]);
838
+ eq('facets: renamed cited file is stale', byName(r, 'Moved').status, 'stale');
839
+ eq('facets: no stamp is unstamped', [byName(r, 'Unstamped').status, byName(r, 'Unstamped').sha], ['unstamped', null]);
840
+ eq('facets: unknown stamp is unresolved', byName(r, 'Rewritten').status, 'unresolved');
841
+ eq('facets: unsafe and missing tokens are ignored, nothing resolved is no-paths', [byName(r, 'Nothing').status, byName(r, 'Nothing').paths], ['no-paths', []]);
842
+ eq('facets: glob characters match literally and a real change reads stale', [byName(r, 'Glob').status, byName(r, 'Glob').changed], ['stale', ['star/[ab].txt']]);
843
+ eq('facets: trailing slash is stripped', [byName(r, 'Dir').status, byName(r, 'Dir').paths], ['stale', ['src']]);
844
+ ok('facets: a fenced ## is not a facet', !r.out.facets.some((f) => f.name === 'Not a facet'));
845
+ eq('facets: fenced section is listed as unstamped', byName(r, 'Fenced').status, 'unstamped');
846
+
847
+ // A stamp that is a real commit but not an ancestor of HEAD is unresolved.
848
+ git(dir, ['checkout', '-q', '-b', 'side', c1]);
849
+ const side = commit(dir, { 'side.txt': 's\n' }, 'feat: side');
850
+ git(dir, ['checkout', '-q', 'main']);
851
+ const o = facetsRun(dir, facet('Side', side.slice(0, 7), 'src/b.js'));
852
+ eq('facets: a stamp off HEAD history is unresolved', byName(o, 'Side').status, 'unresolved');
853
+
854
+ // Usage errors exit 2 with {"error":...}.
855
+ let text = '';
856
+ const bad = walk.main(['facets', '--repo', dir], { stdout: { write: (t) => { text += t; } } });
857
+ ok('facets: --body-file is required', bad === 2 && JSON.parse(text).error);
858
+ text = '';
859
+ const missing = walk.main(['facets', '--repo', dir, '--body-file', '/nonexistent/body.md'], { stdout: { write: (t) => { text += t; } } });
860
+ ok('facets: unreadable body file is a fatal error', missing === 2 && JSON.parse(text).error);
861
+ }
862
+
801
863
  cleanup();
802
864
  console.log(`✓ audit-walk: ${passed} assertions passed`);