@marcomigozzi/trailhead 0.14.0 → 0.15.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 (92) hide show
  1. package/README.it.md +118 -30
  2. package/README.md +107 -29
  3. package/bin/base-preflight-docs.test.js +65 -0
  4. package/bin/execute-split-docs.test.js +146 -0
  5. package/bin/isolation-answer-docs.test.js +41 -0
  6. package/bin/isolation-copy-docs.test.js +28 -0
  7. package/bin/lib/codex-projection.js +1 -1
  8. package/bin/lib/codex-projection.test.js +41 -0
  9. package/bin/report-status-docs.test.js +195 -0
  10. package/bin/resolve-helpers-docs.test.js +36 -0
  11. package/bin/scope-confirm-skip-docs.test.js +70 -0
  12. package/bin/trailhead.js +3 -3
  13. package/bin/trailhead.test.js +341 -1
  14. package/bin/uat-setup-docs.test.js +65 -0
  15. package/package.json +1 -1
  16. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  17. package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
  18. package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
  19. package/plugins/trailhead/agents/trailhead-debug.md +20 -1
  20. package/plugins/trailhead/agents/trailhead-executor.md +29 -2
  21. package/plugins/trailhead/agents/trailhead-fix.md +29 -2
  22. package/plugins/trailhead/agents/trailhead-plan.md +20 -1
  23. package/plugins/trailhead/agents/trailhead-research.md +22 -3
  24. package/plugins/trailhead/agents/trailhead-verify.md +15 -2
  25. package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
  26. package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
  27. package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
  28. package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
  29. package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
  30. package/plugins/trailhead/hooks/lib/dashboard.test.js +409 -0
  31. package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
  32. package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
  33. package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
  34. package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
  35. package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
  36. package/plugins/trailhead/hooks/lib/shell-scan.js +36 -1
  37. package/plugins/trailhead/hooks/lib/shell-scan.test.js +14 -0
  38. package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
  39. package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
  40. package/plugins/trailhead/hooks/lib/work-state.test.js +45 -0
  41. package/plugins/trailhead/hooks/mod/README.md +38 -6
  42. package/plugins/trailhead/hooks/mod/map-pane-view.ts +168 -15
  43. package/plugins/trailhead/hooks/mod/map-pane.tsx +275 -106
  44. package/plugins/trailhead/hooks/mod/register.tsx +15 -2
  45. package/plugins/trailhead/hooks/mod/work-state-view.ts +17 -0
  46. package/plugins/trailhead/hooks/trailhead-install-guard.js +169 -34
  47. package/plugins/trailhead/hooks/trailhead-install-guard.test.js +180 -1
  48. package/plugins/trailhead/hooks/trailhead-search-guard.js +43 -7
  49. package/plugins/trailhead/hooks/trailhead-search-guard.test.js +66 -2
  50. package/plugins/trailhead/skills/_shared/choices.md +4 -2
  51. package/plugins/trailhead/skills/_shared/configuration-reference.md +15 -3
  52. package/plugins/trailhead/skills/_shared/load-first.md +3 -3
  53. package/plugins/trailhead/skills/_shared/multi-map.md +2 -2
  54. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  55. package/plugins/trailhead/skills/_shared/substrate-commands.md +22 -11
  56. package/plugins/trailhead/skills/_shared/substrate.md +6 -4
  57. package/plugins/trailhead/skills/_shared/teamwork.md +16 -7
  58. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +17 -9
  59. package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
  60. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
  61. package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
  62. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
  63. package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
  64. package/plugins/trailhead/skills/_shared/techniques.md +12 -2
  65. package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
  66. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
  67. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +2 -2
  68. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
  69. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
  70. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
  71. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
  72. package/plugins/trailhead/skills/trailhead-view/SKILL.md +8 -6
  73. package/plugins/trailhead/skills/trailhead-work/SKILL.md +14 -12
  74. package/plugins/trailhead/skills/trailhead-work/references/auto.md +4 -4
  75. package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
  76. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +26 -10
  77. package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
  78. package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
  79. package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
  80. package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
  81. package/plugins/trailhead/templates/trailhead-dashboard.js +340 -0
  82. package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
  83. package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
  84. package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
  85. package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
  86. package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
  87. package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
  88. package/plugins/trailhead/templates/trailhead-work-state.js +25 -7
  89. package/plugins/trailhead/tests/map-pane-ui.test.ts +566 -19
  90. package/plugins/trailhead/tests/map-pane-view.test.ts +304 -16
  91. package/plugins/trailhead/tests/work-state-view.test.ts +25 -1
  92. package/plugins/trailhead/types/index.d.ts +37 -3
@@ -118,6 +118,12 @@ ok('codex: commit-msg template projected', fs.existsSync(codexTemplate));
118
118
  ok('codex: commit-msg template is verbatim (not converted)',
119
119
  fs.readFileSync(codexTemplate, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg'), 'utf8'));
120
120
 
121
+ // #257: the hook logic ships as a .cjs beside the sh wrapper.
122
+ const codexCommitMsgCjs = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-commit-msg.cjs');
123
+ ok('codex: commit-msg .cjs logic projected', fs.existsSync(codexCommitMsgCjs));
124
+ ok('codex: commit-msg .cjs logic is verbatim (not converted)',
125
+ fs.readFileSync(codexCommitMsgCjs, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg.cjs'), 'utf8'));
126
+
121
127
  // #185: the commit-msg hook sync script ships wholesale with templates/ too.
122
128
  const codexCommitMsgSync = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-commit-msg-sync.js');
123
129
  ok('codex: commit-msg-sync script projected', fs.existsSync(codexCommitMsgSync));
@@ -129,6 +135,24 @@ ok('codex: work-state helper projected', fs.existsSync(codexWorkState));
129
135
  ok('codex: work-state helper is byte-identical to source',
130
136
  fs.readFileSync(codexWorkState, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-work-state.js'), 'utf8'));
131
137
  ok('codex: work-state helper keeps its executable bit', (fs.statSync(codexWorkState).mode & 0o111) !== 0);
138
+ // #242: the isolation copy helper rides in templates/ on Codex too, executable.
139
+ const codexIsolationCopy = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-isolation-copy.js');
140
+ ok('codex: isolation-copy helper projected', fs.existsSync(codexIsolationCopy));
141
+ ok('codex: isolation-copy helper is byte-identical to source',
142
+ fs.readFileSync(codexIsolationCopy, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-isolation-copy.js'), 'utf8'));
143
+ ok('codex: isolation-copy helper keeps its executable bit', (fs.statSync(codexIsolationCopy).mode & 0o111) !== 0);
144
+ // #243: the stale-base preflight helper rides in templates/ on Codex too, executable.
145
+ const codexBasePreflight = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-base-preflight.js');
146
+ ok('codex: base-preflight helper projected', fs.existsSync(codexBasePreflight));
147
+ ok('codex: base-preflight helper is byte-identical to source',
148
+ fs.readFileSync(codexBasePreflight, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-base-preflight.js'), 'utf8'));
149
+ ok('codex: base-preflight helper keeps its executable bit', (fs.statSync(codexBasePreflight).mode & 0o111) !== 0);
150
+ // #252: the UAT setup helper rides in templates/ on Codex too, executable.
151
+ const codexUatSetup = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-uat-setup.js');
152
+ ok('codex: uat-setup helper projected', fs.existsSync(codexUatSetup));
153
+ ok('codex: uat-setup helper is byte-identical to source',
154
+ fs.readFileSync(codexUatSetup, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-uat-setup.js'), 'utf8'));
155
+ ok('codex: uat-setup helper keeps its executable bit', (fs.statSync(codexUatSetup).mode & 0o111) !== 0);
132
156
  // Il sync del label guard (migrazione all'if a livello di job) viaggia con templates/.
133
157
  const codexLabelGuardSync = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-label-guard-sync.js');
134
158
  ok('codex: label-guard-sync script is byte-identical to source', fs.existsSync(codexLabelGuardSync) &&
@@ -140,6 +164,18 @@ ok('codex: audit-walk script is byte-identical to source',
140
164
  fs.readFileSync(codexAuditWalk, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-audit-walk.js'), 'utf8'));
141
165
  ok('codex: audit-walk script keeps its executable bit', (fs.statSync(codexAuditWalk).mode & 0o111) !== 0);
142
166
 
167
+ // #253: the Resolve bookkeeping helpers ride in templates/ on Codex too, executable,
168
+ // and find the secret-guard module from their installed location.
169
+ for (const helper of ['trailhead-issue-body.js', 'trailhead-dashboard.js']) {
170
+ const projected = path.join(codexDir, 'skills', 'trailhead', 'templates', helper);
171
+ ok(`codex: ${helper} projected`, fs.existsSync(projected));
172
+ ok(`codex: ${helper} is byte-identical to source`,
173
+ fs.readFileSync(projected, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', helper), 'utf8'));
174
+ ok(`codex: ${helper} keeps its executable bit`, (fs.statSync(projected).mode & 0o111) !== 0);
175
+ }
176
+ ok('codex: the installed issue-body helper finds the secret guard',
177
+ require(path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-issue-body.js')).loadGuard() !== null);
178
+
143
179
  ok('codex: no commands dir', !fs.existsSync(path.join(codexDir, 'commands')));
144
180
  ok('codex: no settings.json file', !fs.existsSync(path.join(codexDir, 'settings.json')));
145
181
  // #46: per-verb discoverability now projects one Codex SKILL per verb
@@ -210,6 +246,28 @@ for (const cl of ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-v
210
246
  ok(`source: ${cl}/SKILL.md does NOT duplicate the core-list signature line`, !fs.readFileSync(path.join(sourceSkillsDir, cl, 'SKILL.md'), 'utf8').includes(loadFirstSignature));
211
247
  }
212
248
 
249
+ // --- #237: the shared core is loaded by ABSOLUTE path with the Read tool -----
250
+ const clusters237 = ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage'];
251
+ const loadFirst237 = fs.readFileSync(loadFirstSourcePath, 'utf8');
252
+ for (const cl of clusters237) {
253
+ const src = fs.readFileSync(path.join(sourceSkillsDir, cl, 'SKILL.md'), 'utf8');
254
+ const start = src.indexOf('## Load first');
255
+ const rest = src.slice(start + 3);
256
+ const nextH = rest.search(/\n## /);
257
+ const para = start < 0 ? '' : (nextH < 0 ? rest : rest.slice(0, nextH));
258
+ ok(`#237: ${cl} Load first names the base directory`, /base directory/i.test(para));
259
+ ok(`#237: ${cl} Load first requires an absolute path`, /absolute path/i.test(para));
260
+ ok(`#237: ${cl} Load first requires the Read tool`, /Read tool/.test(para));
261
+ ok(`#237: ${cl} Load first has no bare relative "read ../_shared/load-first.md"`, !/read `\.\.\/_shared\/load-first\.md`/i.test(para));
262
+ ok(`#237: ${cl} SKILL.md has no shell read of a relative _shared/ path`, !/\b(cat|head|tail|sed\s+-n\s+\S+)\s+(\.\.\/)*_shared\//.test(src));
263
+ ok(`#237: ${cl} SKILL.md has no cd-and-read compound`, !/\bcd\s+\S+\s*&&\s*(cat|head|tail|sed|grep)\b/.test(src));
264
+ }
265
+ ok('#237: load-first.md states the absolute-path rule with the Read tool', /absolute path/i.test(loadFirst237) && /Read tool/.test(loadFirst237));
266
+ ok('#237: load-first.md resolves <lib> and <templates> to absolute paths', loadFirst237.includes('`<lib>` (an absolute path') && loadFirst237.includes('`<templates>` (an absolute path)'));
267
+ ok('#237: load-first.md covers every other ../_shared/ or references/ path', loadFirst237.includes('references/') && /every other/i.test(loadFirst237));
268
+ ok('#237: load-first.md no longer says paths are relative to the cluster SKILL.md', !loadFirst237.includes('relative to the cluster `SKILL.md`') && !loadFirst237.includes('relative to the cluster SKILL.md that'));
269
+ ok('#237: load-first.md has no shell read of a relative _shared/ path', !/\b(cat|head|tail|sed\s+-n\s+\S+)\s+(\.\.\/)*_shared\//.test(loadFirst237) && !/\bcd\s+\S+\s*&&\s*(cat|head|tail|sed|grep)\b/.test(loadFirst237));
270
+
213
271
  // --- #146: single-sourced ticket-language directive (anti-drift) -------------
214
272
  // The standing ticket-language rule (write Issue prose + commit bodies in
215
273
  // config.ticket.language, decoupled from the chat language) lives in ONE place,
@@ -287,6 +345,27 @@ ok('source: capture.md offers /trailhead:quick as the lighter alternative, after
287
345
  ok('source: capture.md says quick is offered alongside work, never in its place', captureSource.includes('never in its place'));
288
346
  ok('source: capture.md keeps /trailhead:work the anchor even for a whiteboard capture', /whiteboard[\s\S]{0,200}the anchor stays `\/trailhead:work <n>`/.test(captureSource));
289
347
 
348
+ // --- #246: `bug --of <ticket>` inherits the origin's destination ------------
349
+ // Source-level invariant: capture.md's `bug` bullet makes `--of` inherit the origin
350
+ // ticket's map (open) or the whiteboard without the which-map ask, checks the origin
351
+ // map's state before inheriting, falls back to the Map or whiteboard rule on a closed
352
+ // map / absent or contradictory labels, and names the inherited destination and the
353
+ // origin (each by name and number) in the confirmation line. The Map or whiteboard
354
+ // paragraph cross-references the exception (single source stays in the `bug` bullet).
355
+ const captureBugBullet = (captureSource.match(/^- \*\*`bug`\*\*.*$/m) || [''])[0];
356
+ const captureMapParagraph = (captureSource.match(/^\*\*Map or whiteboard:\*\*.*$/m) || [''])[0];
357
+ ok('source: capture.md `bug --of` inherits the origin destination instead of asking', /\*\*`--of` also inherits the origin's destination instead of asking/.test(captureBugBullet));
358
+ ok('source: capture.md `bug --of` lands on an OPEN origin map with no ask, and a closed one falls back',
359
+ /\*\*open\*\* -> land on map `<n>`[\s\S]{0,120}no ask; \*\*closed\*\* -> fall back/.test(captureBugBullet));
360
+ ok('source: capture.md `bug --of` lands on the whiteboard for a whiteboard origin, no ask, with the dashboard refresh',
361
+ /carries `trailhead:whiteboard`[\s\S]{0,80}land on the whiteboard, no ask \(dashboard refresh/.test(captureBugBullet));
362
+ ok('source: capture.md `bug --of` checks the origin map state (--json state) before inheriting', /gh issue view <n> --json state/.test(captureBugBullet));
363
+ ok('source: capture.md `bug --of` falls back to the Map or whiteboard rule on closed map, absent or contradictory labels',
364
+ /[Ff]all back to the existing Map or whiteboard rule[\s\S]{0,600}closed[\s\S]{0,300}neither label[\s\S]{0,300}contradictory/.test(captureBugBullet));
365
+ ok('source: capture.md `bug --of` confirmation names the inherited destination and the origin, each by name and number',
366
+ /[Cc]onfirmation line[\s\S]{0,300}inherited[\s\S]{0,200}by name and number[\s\S]{0,200}origin[\s\S]{0,100}by name and number/.test(captureBugBullet));
367
+ ok('source: capture.md Map or whiteboard paragraph cross-references the `bug --of` ask exception', /`bug --of`[\s\S]{0,200}skip/.test(captureMapParagraph));
368
+
290
369
  // --- #179: `quick <n>` on a map ticket is never "off the map" ----------------
291
370
  // Source-level invariant: `quick <n>` on an EXISTING ticket does not leave the
292
371
  // map (it keeps its trailhead:map-<n> label and sub-issue edge, and dependent-
@@ -543,6 +622,30 @@ ok('source: trailhead-work SKILL.md work step 1 points to the named-ticket activ
543
622
  ok('source: trailhead-work SKILL.md quick isolation bullet points to the named-ticket active-map rule',
544
623
  namedTicketMapPointer.test(quickIsolationBullet));
545
624
 
625
+ // --- #234: the ticket-start scope confirm is a host menu, not free text ------
626
+ const workStep3 = unitFrom(workSkillSource, '3. **Brief the ticket and confirm scope', /\n(?:\d+\. |\s*\n)/);
627
+ const quickBriefBullet = unitFrom(workSkillSource, '- **Briefs and confirms scope first', /\n/);
628
+ const choicesScopeSource = fs.readFileSync(path.join(sourceSkillsDir, "_shared", "choices.md"), "utf8");
629
+ ok('source: work step 3 poses the scope confirm as an AskUserQuestion menu, Proceed (Recommended) first, Change something second',
630
+ /`AskUserQuestion`[\s\S]{0,300}\*\*Proceed\*\* \(Recommended\)[\s\S]{0,200}\*\*Change something\*\*/.test(workStep3));
631
+ ok('source: work step 3 offers the delegate option on the scope confirm',
632
+ /\*\*delegate\*\*[\s\S]{0,200}choices\.md/i.test(workStep3));
633
+ ok('source: work step 3 divergent change is a menu (as-is vs separate ticket) without delegate',
634
+ /diverges[\s\S]{0,500}`AskUserQuestion`[\s\S]{0,300}without the delegate option/.test(workStep3));
635
+ ok('source: work step 3 has a plain-text numbered list fallback for hosts without a question tool (Codex)',
636
+ /no structured question tool[\s\S]{0,120}plain-text numbered list/.test(workStep3));
637
+ ok('source: work step 3 keeps the auto sentence (proceeds without stopping to brief)',
638
+ /`auto` proceeds \(takes the delegate option\) without stopping to brief/.test(workStep3));
639
+ ok('source: work step 3 no longer asks the change question as free text',
640
+ !/ask whether they want to change anything/.test(workStep3));
641
+ ok('source: quick brief bullet poses the same menu and keeps a one-line note for quick "<text>"',
642
+ /`AskUserQuestion`/.test(quickBriefBullet) && /\*\*Proceed\*\* \(Recommended\)/.test(quickBriefBullet) &&
643
+ /one-line note/.test(quickBriefBullet) && !/one-line confirmation/.test(quickBriefBullet));
644
+ ok('source: choices.md call-site table has the ticket-start scope confirm row (delegate yes, defer no)',
645
+ /\| Ticket-start scope confirm \(`work`\/`quick` brief\) \| Yes[^|]*\| No[^|]*\|/.test(choicesScopeSource));
646
+ ok('source: choices.md call-site table has the divergent-change row (delegate no, defer no)',
647
+ /\| Divergent change at the brief \(as-is vs separate ticket\) \| No[^|]*\| No[^|]*\|/.test(choicesScopeSource));
648
+
546
649
  const inboxSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-chart', 'references', 'inbox.md'), 'utf8');
547
650
  ok('source: inbox.md out-of-scope close drops the native sub-issue edge',
548
651
  /`trailhead:out-of-scope`, \*\*drop its native sub-issue edge\*\*/.test(inboxSource));
@@ -580,7 +683,7 @@ for (const phrase of [
580
683
  '**wait for an explicit answer**',
581
684
  '**silence or an ambiguous reply re-asks**',
582
685
  '**One-path skip**',
583
- '**state the reason in one line**',
686
+ '**states the reason in one line**',
584
687
  'VERIFY',
585
688
  'only for the steps it cannot perform',
586
689
  'runs unchanged',
@@ -721,6 +824,89 @@ ok('source: auto.md Run-end summary section exists', runEndSummarySection.length
721
824
  ok('source: auto.md Run-end summary "what remains" list includes human-only UAT steps',
722
825
  /what remains[\s\S]{0,400}human-only UAT steps/.test(runEndSummarySection));
723
826
 
827
+ // --- #254: skip option at the drive-mode ask (a waiver, not a deferral) -----
828
+ // The waiver's operational contract is single-sourced in the Drive-mode bullet
829
+ // (acceptance-testing.md); the other call-sites carry only the routing /
830
+ // closure consequence and name the bullet.
831
+ const driveModeMarkerLine = driveModeSpan.split('\n')[0] || '';
832
+ ok('source: Drive-mode marker line names the skip option alongside AI-driven / human-driven',
833
+ driveModeMarkerLine.includes('AI-driven') && driveModeMarkerLine.includes('human-driven') && driveModeMarkerLine.includes('skip (waive acceptance)'));
834
+ const skipSubBullet = driveModeSpan.split('\n').find((l) => l.startsWith(' - **Skip (waive acceptance).**')) || '';
835
+ ok('source: Drive-mode span carries the "Skip (waive acceptance)" sub-bullet', skipSubBullet.length > 0);
836
+ for (const phrase of [
837
+ 'waived by the user (unconfirmed)',
838
+ 'never a pass',
839
+ 'the Pending-UAT guard does not fire',
840
+ 'no follow-up',
841
+ 'The delegate option never picks skip',
842
+ '`auto` never picks skip',
843
+ 'UAT of:',
844
+ 'closes as waived',
845
+ 'resolution comment',
846
+ 'all three start paths',
847
+ ]) {
848
+ ok(`source: Skip sub-bullet pins "${phrase}"`, skipSubBullet.includes(phrase));
849
+ }
850
+ const oneAskLine = driveModeSpan.split('\n').find((l) => l.startsWith(' - **One-path skip**')) || '';
851
+ ok('source: one-path skip no longer suppresses the ask outright (drops only the AI-driven option)',
852
+ oneAskLine.includes('drops only the AI-driven option') && oneAskLine.includes('human-driven or skip') && !oneAskLine.includes('skip the ask'));
853
+ ok('source: one-path skip still states the reason in one line, naming the blocking class',
854
+ oneAskLine.includes('**states the reason in one line**') && oneAskLine.includes('blocking class') && !oneAskLine.includes('prefaced by'));
855
+ ok('source: on the reduced ask (no performable step) the delegate option resolves to human-driven, never skip',
856
+ /delegate option resolves to human-driven \(never skip\)/.test(oneAskLine));
857
+ ok('source: Pending-UAT bullet distinguishes a waiver from a deferral', pendingUatLine.includes('a waiver is not a deferral'));
858
+ ok('source: Pending-UAT bullet pins the waiver exception (picked skip, recorded as unconfirmed, nothing opened)',
859
+ /The one exception is an explicit waiver[^.]*\. The human picked skip at the drive-mode ask[^.]*unconfirmed in `VERIFY`[^.]*nothing is opened/.test(pendingUatLine));
860
+ const waiverResolveRe = /resolv(e|ing) on a confirmed pass, or on an explicit waiver if the user skips at the ask/;
861
+ ok('source: Pending-UAT bullet option (a) resolves on a confirmed pass or an explicit waiver',
862
+ waiverResolveRe.test(pendingUatLine) && !pendingUatLine.includes('only on a confirmed pass'));
863
+ ok('source: both ticket-engines Resolve lines let option (a) resolve on a confirmed pass or an explicit waiver',
864
+ guardLines254Early().length === 2 && guardLines254Early().every((l) => waiverResolveRe.test(l) && !l.includes('only on a confirmed pass')));
865
+ ok('source: with-no-ticket preflight-then-ask phrase still occurs exactly twice in ticket-engines.md',
866
+ ticketEnginesSource.split('with no ticket (the stale-base preflight first, then the UAT setup, then the drive-mode ask').length - 1 === 2);
867
+ ok('source: both Verify steps let the user skip, recorded as waived',
868
+ verifyStepLines.length === 2 && verifyStepLines.every((l) => /or skip \(waive acceptance\), recorded as waived/.test(l)));
869
+ ok('source: techniques.md index row and auto.md lead-in name skip',
870
+ /ask who drives \(AI or human, or skip\)/.test(fs.readFileSync(path.join(sourceSkillsDir, '_shared', 'techniques.md'), 'utf8')) &&
871
+ autoSource.includes('drive-mode ask (AI-driven, human-driven, or skip)'));
872
+
873
+ function guardLines254Early() { return ticketEnginesSource.split('\n').filter((l) => l.includes('Pending-UAT guard (before the close)')); }
874
+ const guardLines254 = ticketEnginesSource.split('\n').filter((l) => l.includes('Pending-UAT guard (before the close)'));
875
+ ok('source: both Pending-UAT guard lines say the guard does not fire when acceptance was explicitly waived',
876
+ guardLines254.length === 2 && guardLines254.every((l) => l.includes('explicitly waived')));
877
+ ok('source: both Resolve steps state the waiver in the resolution comment',
878
+ guardLines254.length === 2 && guardLines254.every((l) => /resolution comment states the waiver/.test(l)));
879
+ ok('source: the UAT follow-ups section closes on a confirmed pass or an explicit waiver',
880
+ uatFollowupSection.includes('confirmed pass or an explicit waiver') && uatFollowupSection.includes('closes as waived'));
881
+ const driveRowLine = choicesSource.split('\n').find((l) => /^\|[^\n]*[Dd]rive-mode ask/.test(l)) || '';
882
+ ok('source: choices.md drive-mode row names skip and states delegate never picks it',
883
+ driveRowLine.includes('skip') && /delegate never picks skip/.test(driveRowLine) && driveRowLine.includes('reduced ask') && driveRowLine.includes('human-driven'));
884
+ ok('source: configuration-reference.md acceptance.browser paragraph names skip alongside AI-driven / human-driven',
885
+ /\*\*acceptance\.browser\.\*\*[\s\S]{0,200}human-driven,? or skip/.test(configReferenceSource));
886
+ ok('source: auto.md states auto never picks skip', /never picks skip[^\n]*waiving acceptance/.test(autoSource));
887
+
888
+ // Docs mirror: README, README.it, site (waiver recorded as unconfirmed; no unconditional "never closes").
889
+ ok('README.md acceptance testing section names the waiver (skip) recorded as unconfirmed',
890
+ /\*\*Skip is a waiver, not a deferral\*\*[^\n]{0,80}`waived by the user \(unconfirmed\)`, never as a pass/.test(readmeSource));
891
+ ok('README.md no-performable-step case asks human-driven or skip', /human-driven\*\* or \*\*skip\*\*/.test(readmeSource));
892
+ ok('README.md point 6 allows an explicit waiver', /never closes on a UAT nobody ran unless you explicitly waive it/.test(readmeSource));
893
+ const siteAcceptanceSource = fs.readFileSync(path.join(repoRoot, 'site', 'src', 'content', 'docs', 'docs', 'acceptance-testing.md'), 'utf8');
894
+ ok('docs: auto never-picks-skip antecedent is the auto run, in all three docs',
895
+ /listed in the run's summary; `\/trailhead:auto` never picks skip/.test(readmeSource) && /listed in the run's summary; `\/trailhead:auto` never picks skip/.test(siteAcceptanceSource) && /elencato nel riepilogo della run; `\/trailhead:auto` non sceglie mai skip/.test(readmeItSource));
896
+ ok('README.md build row and acceptance.browser row name skip',
897
+ /🔨 `build`[\s\S]{0,500}or skip/.test(readmeSource) && /`acceptance\.browser`[\s\S]{0,300}or skip/.test(readmeSource));
898
+ ok('README.it.md acceptance testing section names the waiver (skip) recorded as unconfirmed',
899
+ /\*\*Skip è una rinuncia, non un rinvio\*\*[^\n]{0,160}`waived by the user \(unconfirmed\)`, cioè non confermata/.test(readmeItSource));
900
+ ok('README.it.md no-performable-step case asks human-driven o skip', /human-driven\*\* o \*\*skip\*\*/.test(readmeItSource));
901
+ ok('README.it.md point 6 allows an explicit waiver', /non si chiude mai su una UAT che nessuno ha eseguito, salvo che tu ci rinunci esplicitamente/.test(readmeItSource));
902
+ ok('README.it.md build row and acceptance.browser row name skip',
903
+ /🔨 `build`[\s\S]{0,500}o skip/.test(readmeItSource) && /`acceptance\.browser`[\s\S]{0,300}o skip/.test(readmeItSource));
904
+ ok('site acceptance-testing.md names the waiver (skip) recorded as unconfirmed', /\*\*Skip is a waiver, not a deferral\*\*[^\n]{0,80}`waived by the user \(unconfirmed\)`, never as a pass/.test(siteAcceptanceSource));
905
+ ok('site acceptance-testing.md no-performable-step case asks human-driven or skip', /human-driven\*\* or \*\*skip\*\*/.test(siteAcceptanceSource));
906
+ ok('site acceptance-testing.md point 6 allows an explicit waiver', /never closes on a UAT nobody ran unless you explicitly waive it/.test(siteAcceptanceSource));
907
+ ok('site ticket-types.md build row and configuration.mdx acceptance.browser row name skip',
908
+ /`build`[\s\S]{0,500}or skip/.test(ticketTypesSource) && /`acceptance\.browser`[\s\S]{0,300}or skip/.test(configurationMdxSource));
909
+
724
910
  // --- codex hooks (#29) --------------------------------------------------------
725
911
  const codexHooksJsonPath = path.join(codexDir, 'hooks.json');
726
912
  ok('codex: hooks.json exists', fs.existsSync(codexHooksJsonPath));
@@ -851,6 +1037,25 @@ ok('codex: every trailhead-*.toml is pin-less when models.codex.* is unset', (()
851
1037
  return tomls.every((f) => !/^model = /m.test(fs.readFileSync(path.join(dir, f), 'utf8')));
852
1038
  })());
853
1039
 
1040
+ // #250: the reviewing block (single source scripts/agent-blocks.md) is projected
1041
+ // verbatim into the two reviewing agents' TOMLs and into none of the other six.
1042
+ {
1043
+ const sync250 = require(path.join(repoRoot, 'scripts', 'sync-agent-blocks.js'));
1044
+ const reviewingBlock250 = sync250.parseSource(fs.readFileSync(path.join(repoRoot, 'scripts', 'agent-blocks.md'), 'utf8')).reviewing;
1045
+ ok('#250: the source defines a reviewing block', typeof reviewingBlock250 === 'string' && reviewingBlock250.length > 0);
1046
+ const agentsDir250 = path.join(unsetCodexHomeDir, 'agents');
1047
+ const reviewingNames250 = new Set(['trailhead-verify', 'trailhead-review']); // the code-review agent projects as trailhead-review.toml
1048
+ const tomls250 = fs.readdirSync(agentsDir250).filter((f) => /^trailhead-.*\.toml$/.test(f));
1049
+ ok('#250: all 8 TOMLs are checked', tomls250.length === 8);
1050
+ for (const f of tomls250) {
1051
+ const name = f.replace(/\.toml$/, '');
1052
+ const text = fs.readFileSync(path.join(agentsDir250, f), 'utf8');
1053
+ const has = typeof reviewingBlock250 === 'string' && reviewingBlock250.length > 0 && text.includes(reviewingBlock250);
1054
+ ok(`#250: ${f} ${reviewingNames250.has(name) ? 'carries' : 'does not carry'} the reviewing block verbatim`,
1055
+ has === reviewingNames250.has(name));
1056
+ }
1057
+ }
1058
+
854
1059
  // --- codex migration (#46): install over old prompt shims sweeps them --------
855
1060
  const migCodexDir = mktmp();
856
1061
  fs.mkdirSync(path.join(migCodexDir, 'prompts'), { recursive: true });
@@ -932,6 +1137,12 @@ ok('claude: hooks/lib/model-defaults.json exists (#186)',
932
1137
  // #202: the mod is not delivered by the npm installer yet.
933
1138
  ok('claude: no hooks/mod/ dir in the npm install (#202)', !fs.existsSync(path.join(claudeDir, 'hooks', 'mod')));
934
1139
 
1140
+ // #257: the hook logic ships as a .cjs beside the sh wrapper.
1141
+ const claudeCommitMsgCjs = path.join(claudeDir, 'trailhead', 'templates', 'trailhead-commit-msg.cjs');
1142
+ ok('claude: commit-msg .cjs logic projected', fs.existsSync(claudeCommitMsgCjs));
1143
+ ok('claude: commit-msg .cjs logic is byte-identical to source',
1144
+ fs.readFileSync(claudeCommitMsgCjs, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg.cjs'), 'utf8'));
1145
+
935
1146
  // #185: the commit-msg hook sync script ships wholesale with templates/ too.
936
1147
  const claudeCommitMsgSync = path.join(claudeDir, 'trailhead', 'templates', 'trailhead-commit-msg-sync.js');
937
1148
  ok('claude: commit-msg-sync script projected', fs.existsSync(claudeCommitMsgSync));
@@ -944,6 +1155,17 @@ const claudeAuditWalk = path.join(claudeDir, 'trailhead', 'templates', 'trailhea
944
1155
  ok('claude: audit-walk script projected', fs.existsSync(claudeAuditWalk));
945
1156
  ok('claude: audit-walk script is byte-identical to source',
946
1157
  fs.readFileSync(claudeAuditWalk, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-audit-walk.js'), 'utf8'));
1158
+ // #253: the Resolve bookkeeping helpers are installed, and (npm layout: templates at
1159
+ // <config>/trailhead/templates, hooks at <config>/hooks) find the secret guard.
1160
+ for (const helper of ['trailhead-issue-body.js', 'trailhead-dashboard.js']) {
1161
+ const installed = path.join(claudeDir, 'trailhead', 'templates', helper);
1162
+ ok(`claude: ${helper} installed`, fs.existsSync(installed));
1163
+ ok(`claude: ${helper} is byte-identical to source`,
1164
+ fs.readFileSync(installed, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', helper), 'utf8'));
1165
+ ok(`claude: ${helper} keeps its executable bit`, (fs.statSync(installed).mode & 0o111) !== 0);
1166
+ }
1167
+ ok('claude: the installed issue-body helper finds the secret guard',
1168
+ require(path.join(claudeDir, 'trailhead', 'templates', 'trailhead-issue-body.js')).loadGuard() !== null);
947
1169
  // Regression: the commit-guard does require('./lib/commit-message-check.js'), so
948
1170
  // it only loads if the lib was copied alongside it. Run it with a benign Bash
949
1171
  // payload and assert it does not crash with a missing-module error.
@@ -1251,6 +1473,9 @@ ok('source: teamwork.md fallback resolves the hooks dir via rev-parse --git-path
1251
1473
  teamworkSource.includes('git -C <working-root> rev-parse --git-path hooks'));
1252
1474
  ok('source: teamwork.md fallback classifies by the trailhead header line',
1253
1475
  teamworkSource.includes('// trailhead commit-msg hook (git).'));
1476
+ ok('source: teamwork.md fallback knows the sh wrapper header and the .cjs logic file (#257)',
1477
+ teamworkSource.includes('# trailhead commit-msg hook (git).') && teamworkSource.includes('trailhead-commit-msg.cjs'));
1478
+ ok('source: substrate-commands.md names the .cjs logic file (#257)', substrateCommandsSource.includes('trailhead-commit-msg.cjs'));
1254
1479
  ok('source: teamwork.md fallback checks the executable bit (test -x)',
1255
1480
  teamworkSource.includes('test -x'));
1256
1481
  ok('source: teamwork.md fallback reads only the first 5 lines',
@@ -1841,6 +2066,121 @@ ok('source: the declared module file exists, relative to hooks.json (#202)',
1841
2066
  ok('source: hooks.json keeps its command hooks next to the module (#202)',
1842
2067
  ['PreToolUse', 'PostToolUse', 'SessionStart'].every((ev) => (pluginHooksJson.hooks?.[ev] || []).length > 0));
1843
2068
 
2069
+ // --- #259: `trailhead:paused` label marks a paused ticket (pause adds, resume/claim drop) ---
2070
+ const firstUseLine = (substrateCommandsSource.match(/^1\. Create any missing labels.*$/m) || [''])[0];
2071
+ ok('source: first-use setup lists twenty labels including trailhead:paused and trailhead:uat',
2072
+ /all twenty:/.test(firstUseLine) && firstUseLine.includes('`trailhead:paused`') && firstUseLine.includes('`trailhead:uat`'));
2073
+ ok('source: no stale "eighteen" or "nineteen" label count remains in the shared skills',
2074
+ !/eighteen|nineteen/.test(substrateCommandsSource) && !/eighteen|nineteen/.test(substrateSource));
2075
+ ok('source: substrate-commands.md ensures trailhead:paused idempotently',
2076
+ /gh label create trailhead:paused --color FBCA04 --description "Paused: a PAUSED checkpoint holds its next step" 2>\/dev\/null \|\| true/.test(substrateCommandsSource));
2077
+ ok('source: substrate.md documents trailhead:paused as set by pause, cleared by resume or claim, off the frontier (#263)',
2078
+ /\*\*`trailhead:paused`\*\*[\s\S]{0,500}pause[\s\S]{0,200}resume[\s\S]{0,200}claim[\s\S]{0,400}excluded from the frontier/.test(substrateSource));
2079
+ const pauseBullet = (teamworkSource.match(/^- \*\*Pause\*\*.*$/m) || [''])[0];
2080
+ const resumeBullet = (teamworkSource.match(/^- \*\*Resume\*\*.*$/m) || [''])[0];
2081
+ ok('source: teamwork.md pause adds trailhead:paused (created idempotently) after the PAUSED comment',
2082
+ /post a \*\*`PAUSED`\*\* checkpoint comment[\s\S]{0,1200}add the `trailhead:paused` label[\s\S]{0,120}creating it idempotently first[\s\S]{0,200}after the `PAUSED` comment/.test(pauseBullet));
2083
+ ok('source: teamwork.md resume removes trailhead:paused only after the claim is confirmed',
2084
+ /gh issue edit <n> --remove-label trailhead:paused/.test(resumeBullet) &&
2085
+ /only after the claim is confirmed[\s\S]{0,300}a refused resume leaves the ticket visibly paused/.test(resumeBullet));
2086
+ ok('source: work SKILL.md claim step drops trailhead:paused from a claimed paused ticket',
2087
+ /\*\*Claim\*\* it:[\s\S]{0,1200}carries `trailhead:paused`[\s\S]{0,120}drops it at the claim/.test(workSkillSource));
2088
+ ok('source: work SKILL.md quick isolation bullet drops trailhead:paused at the claim',
2089
+ /^- \*\*Same isolation setup as `work`\.\*\*.*claimed ticket that carries `trailhead:paused` drops it at the claim/m.test(workSkillSource));
2090
+
2091
+ // --- #261: `trailhead:uat` label marks a pending UAT follow-up (marker, not a state) ---
2092
+ {
2093
+ ok('source: substrate-commands.md ensures trailhead:uat idempotently',
2094
+ /gh label create trailhead:uat --color [0-9A-Fa-f]{6} --description "Pending UAT: an acceptance test still to run" 2>\/dev\/null \|\| true/.test(substrateCommandsSource));
2095
+ {
2096
+ const helperSource = fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-uat-backfill.js'), 'utf8');
2097
+ const docColor = (substrateCommandsSource.match(/gh label create trailhead:uat --color ([0-9A-Fa-f]{6})/) || [])[1];
2098
+ const helperColor = (helperSource.match(/const COLOR = '([0-9A-Fa-f]{6})'/) || [])[1];
2099
+ ok('source: the backfill helper creates trailhead:uat with the same colour as substrate-commands.md',
2100
+ docColor !== undefined && docColor === helperColor);
2101
+ }
2102
+ ok('source: substrate-commands.md has no create-then-backfill chain and invokes the backfill helper',
2103
+ /trailhead-uat-backfill\.js/.test(substrateCommandsSource) && !/trailhead:uat[^\n]*&&[^\n]*backfill/.test(substrateCommandsSource));
2104
+ ok('source: first-use setup runs the backfill helper after the labels',
2105
+ /^1\. Create any missing labels[\s\S]{0,1500}?trailhead-uat-backfill\.js/m.test(substrateCommandsSource));
2106
+ ok('source: substrate.md documents trailhead:uat as a marker label right after the state labels, not a state, not a frontier filter',
2107
+ /\*\*`trailhead:uat`\*\*[\s\S]{0,700}marker[\s\S]{0,400}not a state[\s\S]{0,400}never a frontier filter/.test(substrateSource)
2108
+ && substrateSource.indexOf('**`trailhead:uat`**') > substrateSource.indexOf('**`trailhead:paused`**')
2109
+ && substrateSource.indexOf('**`trailhead:uat`**') < substrateSource.indexOf('**Frontier**'));
2110
+ ok('source: acceptance-testing.md option (b) opens the follow-up with trailhead:uat in the gh issue create --label list, label ensured first',
2111
+ /open a follow-up ticket before that close[\s\S]{0,900}--label[^\n]{0,200}`?trailhead:uat/.test(acceptanceTestingSource)
2112
+ && /ensur\w* (the )?`?trailhead:uat`? (label )?idempotently/.test(acceptanceTestingSource));
2113
+ ok('source: ticket-engines.md UAT follow-ups section names the label',
2114
+ /UAT follow-up[\s\S]{0,1500}`trailhead:uat`/.test(ticketEnginesSource.split('\n').filter((l) => /^A ticket whose body carries/.test(l)).join('\n')));
2115
+ ok('source: work SKILL.md work-start runs the backfill helper once per clone, marker gated',
2116
+ /trailhead-uat-backfill\.js[^\n]*--marker[^\n]*\.trailhead\/uat-backfill/.test(workSkillSource));
2117
+ ok('source: work SKILL.md passes the backfill marker as an absolute path under the working root',
2118
+ /trailhead-uat-backfill\.js[^\n]*--marker <working-root>\/\.trailhead\/uat-backfill/.test(workSkillSource));
2119
+ ok('source: .gitignore ignores .trailhead/uat-backfill',
2120
+ /^\.trailhead\/uat-backfill$/m.test(fs.readFileSync(path.join(repoRoot, '.gitignore'), 'utf8')));
2121
+ ok('source: work SKILL.md mentions the uat-backfill marker as gitignored per-clone state',
2122
+ /`\.trailhead\/uat-backfill`[^\n]*(gitignored|never committed)/.test(workSkillSource));
2123
+ const producerSources = { 'acceptance-testing.md': acceptanceTestingSource, 'site acceptance-testing.md': siteAcceptanceSource };
2124
+ for (const [name, src] of Object.entries(producerSources)) {
2125
+ ok(`source: ${name} writes the pointer as UAT of: [<parent name>](<parent url>)`,
2126
+ src.includes('UAT of: [<parent name>](<parent url>)') && !/UAT of: <parent name>\(link\)|UAT of: #<n>/.test(src));
2127
+ }
2128
+ }
2129
+
2130
+ // --- #263: a paused ticket is off the frontier on every surface ---
2131
+ {
2132
+ const viewSkillSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-view', 'SKILL.md'), 'utf8');
2133
+ const siteDocs = path.join(repoRoot, 'site', 'src', 'content', 'docs', 'docs');
2134
+ const siteViewSource = fs.readFileSync(path.join(siteDocs, 'commands', 'view.md'), 'utf8');
2135
+ const siteWorkSource = fs.readFileSync(path.join(siteDocs, 'commands', 'work.md'), 'utf8');
2136
+ const changelogSource = fs.readFileSync(path.join(repoRoot, 'CHANGELOG.md'), 'utf8');
2137
+ const searchExprs = (src) => [...src.matchAll(/--search\s+"([^"]*no:assignee[^"]*)"/g)].map((m) => m[1]);
2138
+ const base = 'no:assignee -label:trailhead:blocked -label:trailhead:unverified -label:trailhead:paused';
2139
+ const wbExcl = `${base} -label:trailhead:whiteboard`;
2140
+ const tally = (exprs) => exprs.reduce((acc, e) => ({ ...acc, [e]: (acc[e] || 0) + 1 }), {});
2141
+ const sameTally = (got, want) => JSON.stringify(Object.entries(got).sort()) === JSON.stringify(Object.entries(want).sort());
2142
+
2143
+ ok('source: substrate-commands.md frontier queries each exclude paused (two repo-wide, four label-scoped or whiteboard or count)',
2144
+ sameTally(tally(searchExprs(substrateCommandsSource)), { [wbExcl]: 2, [base]: 4 }));
2145
+ ok('source: multi-map.md scoped frontier query excludes paused', sameTally(tally(searchExprs(multiMapSource)), { [base]: 1 }));
2146
+ ok('source: trailhead-view SKILL.md whiteboard frontier query excludes paused', sameTally(tally(searchExprs(viewSkillSource)), { [base]: 1 }));
2147
+ ok('source: the cookbook frontier comment says not paused',
2148
+ /# the frontier: open, unassigned, not blocked, not unverified, not paused, one query/.test(substrateCommandsSource));
2149
+
2150
+ ok('source: substrate.md Frontier definition excludes trailhead:paused',
2151
+ /\*\*Frontier\*\* = open, unassigned tickets that are \*\*not\*\* `trailhead:blocked`, \*\*not\*\* `trailhead:unverified` and \*\*not\*\* `trailhead:paused`/.test(substrateSource));
2152
+ const pausedBullet = (substrateSource.match(/^- \*\*`trailhead:paused`\*\*.*$/m) || [''])[0];
2153
+ ok('source: substrate.md paused bullet: excluded from the frontier, taken back with /trailhead:resume',
2154
+ /excluded from the frontier/.test(pausedBullet) && /\/trailhead:resume/.test(pausedBullet));
2155
+ ok('source: the old "does not change the frontier query" wording is gone',
2156
+ !/does not change the frontier query/.test(substrateSource));
2157
+
2158
+ const paused = /^- \*\*In progress\*\*[^\n]*\n- \*\*Paused\*\*[^\n]*\/trailhead:resume <n>`[^\n]*\n- \*\*Blocked\*\*/gm;
2159
+ ok('source: the map and whiteboard renders both list a Paused group between In progress and Blocked',
2160
+ (viewSkillSource.match(paused) || []).length === 2);
2161
+ ok('source: both render Frontier bullets say not paused',
2162
+ (viewSkillSource.match(/^- \*\*Frontier\*\*:[^\n]*not[^\n]*`trailhead:paused`/gm) || []).length === 2);
2163
+
2164
+ ok('source: multi-map.md park & switch resumes with /trailhead:resume only',
2165
+ /Resume later with `\/trailhead:resume`/.test(multiMapSource) && !/just `\/trailhead:work` once that map is active/.test(multiMapSource));
2166
+
2167
+ ok('source: work SKILL.md: bare work takes the paused-excluding frontier, a named work or quick still works a paused ticket',
2168
+ /Otherwise take a frontier ticket[\s\S]{0,600}never a `trailhead:paused` ticket/.test(workSkillSource) &&
2169
+ /\*\*named\*\* ticket may also be a `trailhead:paused` one, which `work <n>` \/ `quick <n>` takes back/.test(workSkillSource) &&
2170
+ /carries `trailhead:paused` drops it at the claim/.test(workSkillSource));
2171
+
2172
+ ok('source: auto.md resumes owned paused tickets in step 2 and no longer says they come back on the frontier',
2173
+ /\*\*each\*\* ticket you own[\s\S]{0,200}`trailhead:paused`/.test(autoSource) &&
2174
+ !/paused\/stuck tickets are back on the frontier/.test(autoSource) && !/paused[^.\n]{0,40}back on the frontier/.test(autoSource));
2175
+ ok('source: site work.md says a paused ticket is off the frontier until resumed',
2176
+ /off the frontier until (it is )?resumed/.test(siteWorkSource));
2177
+ ok('source: site view.md describes the Paused group off the frontier', /Paused/.test(siteViewSource) && /not paused|off the frontier/.test(siteViewSource));
2178
+ const conceptsSource = fs.readFileSync(path.join(siteDocs, 'concepts.md'), 'utf8');
2179
+ ok('source: concepts.md, README.md and README.it.md frontier definitions mention paused',
2180
+ /not paused/.test(conceptsSource) && /not paused/.test(readmeSource) && /in pausa/.test(readmeItSource));
2181
+ ok('source: CHANGELOG Unreleased > Changed has a #263 entry', /### Changed[\s\S]*?\(#263\)/.test(changelogSource.split('## 0.14.0')[0]));
2182
+ }
2183
+
1844
2184
  for (const d of tmpDirs) {
1845
2185
  fs.rmSync(d, { recursive: true, force: true });
1846
2186
  }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #252: the skill prose wires the UAT setup step into every UAT start
3
+ // (after the stale-base preflight, before the drive-mode ask) and documents the
4
+ // uat.setup / uat.accounts config keys.
5
+ // Run: node uat-setup-docs.test.js
6
+ const assert = require('assert');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const skills = path.join(__dirname, '..', 'plugins', 'trailhead', 'skills');
11
+ const read = (rel) => fs.readFileSync(path.join(skills, rel), 'utf8');
12
+
13
+ let passed = 0;
14
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
15
+
16
+ const HELPER = 'trailhead-uat-setup.js';
17
+ const EXPECT = {
18
+ '_shared/techniques/acceptance-testing.md': [
19
+ '**UAT setup comes next.**', HELPER, '`uat.setup`', '`uat.accounts`', '`unconfigured`', '`invalid`', 'retry', 'treat as a defect', 'Never a silent fallback',
20
+ 'never a value', 'step names and results only', 'declined again, it is not re-offered this session', '--config-root',
21
+ '`run --from <failedStep>`', '`run --from <failedStep + 1>`', '`--only <i>`', '`--step-timeout`', '`--ready-timeout`', 'kills only its `sh`', '> /tmp/dev.log 2>&1 &',
22
+ ],
23
+ '_shared/configuration-reference.md': [
24
+ '| `uat.setup` |', '| `uat.accounts` |', '**UAT setup.**', '`missing-env`', 'References, never values', 'the nested form wins', '`testing.webapp` / `testing.url` are unchanged',
25
+ '**🧰 UAT setup**', '**👤 Test account**', 'never the secret\'s value',
26
+ '`\\$NAME`', 'outside single quotes', 'redirect its own output', '> /tmp/dev.log 2>&1 &',
27
+ ],
28
+ 'trailhead-work/references/auto.md': ['**UAT setup in `auto`**', 'no probe, no proposal, no save offer', 'run-end summary'],
29
+ };
30
+ for (const [rel, needles] of Object.entries(EXPECT)) {
31
+ const text = read(rel);
32
+ for (const needle of needles) ok(`${rel} names: ${needle}`, text.includes(needle));
33
+ }
34
+
35
+ // In the technique: preflight bullet < setup bullet < drive-mode ask.
36
+ const tech = read('_shared/techniques/acceptance-testing.md');
37
+ const pre = tech.indexOf('- **Stale-base preflight comes first.**');
38
+ const setup = tech.indexOf('- **UAT setup comes next.**');
39
+ const ask = tech.indexOf('- **Drive mode: ask who drives the UAT.**');
40
+ ok('setup bullet sits between the preflight bullet and the drive-mode ask', pre >= 0 && pre < setup && setup < ask);
41
+ ok('setup names all three start paths', ['`build`/`bug` Verify', 'Pending-UAT guard', '`UAT of:`'].every((s) => tech.slice(setup, ask).includes(s)));
42
+
43
+ // Each engine start path names the setup after the preflight and before the drive-mode ask.
44
+ const engines = read('trailhead-work/references/ticket-engines.md');
45
+ const lines = engines.split('\n');
46
+ const step = (prefix, from = 0) => lines.findIndex((l, i) => i >= from && l.startsWith(prefix));
47
+ const buildVerify = step('4. **Verify**');
48
+ const buildResolve = step('5. **Resolve**', buildVerify + 1);
49
+ const bugVerify = step('4. **Verify**', buildResolve + 1);
50
+ for (const [name, idx] of [['build Verify', buildVerify], ['bug Verify', bugVerify]]) {
51
+ const l = lines[idx];
52
+ const s = l.indexOf('opening with the UAT setup');
53
+ ok(`${name} names the UAT setup`, s >= 0);
54
+ ok(`${name}: preflight, then setup, then the drive-mode ask`, l.indexOf('Stale-base preflight') < s && s < l.indexOf('then the drive-mode ask'));
55
+ }
56
+ ok('Pending-UAT option (a) runs preflight, setup, then the ask (build and bug)',
57
+ engines.split('with no ticket (the stale-base preflight first, then the UAT setup, then the drive-mode ask').length === 3);
58
+ ok('UAT follow-up: preflight, then setup, then the ask',
59
+ /starting with the stale-base preflight[^;]*, then the UAT setup \(`trailhead-uat-setup\.js`[^;]*, and then the drive-mode ask/.test(engines));
60
+
61
+ for (const rel of ['_shared/configuration-reference.md', '_shared/techniques/acceptance-testing.md', 'trailhead-work/references/ticket-engines.md', 'trailhead-work/references/auto.md']) {
62
+ ok(`${rel} has no em-dash`, !read(rel).includes('—'));
63
+ }
64
+
65
+ console.log(`uat-setup-docs.test.js: ${passed} checks passed`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcomigozzi/trailhead",
3
- "version": "0.14.0",
3
+ "version": "0.15.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.14.0",
3
+ "version": "0.15.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,6 +7,19 @@ 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 reports into trailhead's `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.
11
11
 
12
- **Search-command hygiene.** Prefer the Grep and Read tools over shelling out; when a Bash `grep`/read is genuinely needed, pass the path as an explicit argument (`grep -niE "<patterns>" <abs>/<file>`, `wc -l <abs>/<file>`) and run git as `git -C <abs> ...`. **Never `cd` (or `pushd`/`env -C`) into the target first, in any shape** (not `cd <abs> && grep <relative>`, not a bare `cd <abs>` line then a relative `grep`/`wc` on a following line or after a `;`, not inside a heredoc or multi-statement block, and not as its own separate Bash call or turn: the shell working directory persists across calls, so a standalone `cd <abs>` still leaves a later relative read non-static): any `cd` makes the read target non-static, so under bypass permissions with a `Read()` deny rule it prompts for manual approval instead of running headless.
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 reviewing -->
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 the `VERIFY` comment after it checks the status line.
21
+ - The first line of the report is the technique's `status:` line, verbatim: `status: <token>`, with nothing before it and no other `status:` line.
22
+ - trailhead-verify: `<token>` is one of `met` | `gaps_found` | `inconclusive`.
23
+ - trailhead-code-review: `<token>` is one of `clean` | `issues_found`, and line 2 is the counts line `critical: <n> · warning: <n> · info: <n>`.
24
+ - Read-only: never Edit/Write, never commit.
25
+ <!-- /trailhead:agent-block reviewing -->
@@ -9,4 +9,23 @@ trailhead's codebase-map reader subagent. Map ONE non-overlapping facet of the r
9
9
 
10
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.
11
11
 
12
- **Search-command hygiene.** Prefer the Grep and Read tools over shelling out; when a Bash `grep`/read is genuinely needed, pass the path as an explicit argument (`grep -niE "<patterns>" <abs>/<file>`, `wc -l <abs>/<file>`) and run git as `git -C <abs> ...`. **Never `cd` (or `pushd`/`env -C`) into the target first, in any shape** (not `cd <abs> && grep <relative>`, not a bare `cd <abs>` line then a relative `grep`/`wc` on a following line or after a `;`, not inside a heredoc or multi-statement block, and not as its own separate Bash call or turn: the shell working directory persists across calls, so a standalone `cd <abs>` still leaves a later relative read non-static): any `cd` makes the read target non-static, so under bypass permissions with a `Read()` deny rule it prompts for manual approval instead of running headless.
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 investigating -->
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 it. Never commit, and never create or switch a branch: the report is the only artifact.
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: `found` | `inconclusive` | `needs_input`.
23
+ - required fields: `answer`, `evidence`, `open questions`.
24
+ - optional fields: none.
25
+ - invariant: `needs_input` => `open questions` non-empty.
26
+ - 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.
27
+ - Fields are the end of the report: nothing after the last field.
28
+ - `answer:` the decision-ready answer to the one question you were given, with your confidence.
29
+ - `evidence:` what backs the answer, one item per line: `file:line` for code, the URL for every external claim.
30
+ - `open questions:` what you could not settle, or `none`. With `needs_input` it holds the questions only the user can answer.
31
+ <!-- /trailhead:agent-block investigating -->
@@ -9,4 +9,23 @@ trailhead's debug subagent. Find the root cause of the ticket's defect by the sc
9
9
 
10
10
  The full protocol is injected at spawn from the trailhead skill's single source (`_shared/techniques/debug.md`); follow that, not any embedded copy. Full tools by policy: it adds instrumentation and regression tests, so it needs Write. Model-agnostic by design: the engine passes the `config.models.debug` model as the spawn-time override, so config stays authoritative.
11
11
 
12
- **Search-command hygiene.** Prefer the Grep and Read tools over shelling out; when a Bash `grep`/read is genuinely needed, pass the path as an explicit argument (`grep -niE "<patterns>" <abs>/<file>`, `wc -l <abs>/<file>`) and run git as `git -C <abs> ...`. **Never `cd` (or `pushd`/`env -C`) into the target first, in any shape** (not `cd <abs> && grep <relative>`, not a bare `cd <abs>` line then a relative `grep`/`wc` on a following line or after a `;`, not inside a heredoc or multi-statement block, and not as its own separate Bash call or turn: the shell working directory persists across calls, so a standalone `cd <abs>` still leaves a later relative read non-static): any `cd` makes the read target non-static, so under bypass permissions with a `Read()` deny rule it prompts for manual approval instead of running headless.
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 investigating -->
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 it. Never commit, and never create or switch a branch: the report is the only artifact.
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: `found` | `inconclusive` | `needs_input`.
23
+ - required fields: `answer`, `evidence`, `open questions`.
24
+ - optional fields: none.
25
+ - invariant: `needs_input` => `open questions` non-empty.
26
+ - 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.
27
+ - Fields are the end of the report: nothing after the last field.
28
+ - `answer:` the decision-ready answer to the one question you were given, with your confidence.
29
+ - `evidence:` what backs the answer, one item per line: `file:line` for code, the URL for every external claim.
30
+ - `open questions:` what you could not settle, or `none`. With `needs_input` it holds the questions only the user can answer.
31
+ <!-- /trailhead:agent-block investigating -->