@marcomigozzi/trailhead 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.it.md +52 -14
  2. package/README.md +53 -15
  3. package/bin/auto-intro-docs.test.js +72 -0
  4. package/bin/auto-merge-docs.test.js +33 -0
  5. package/bin/auto-merge-ticket-docs.test.js +32 -0
  6. package/bin/auto-review-rounds-docs.test.js +45 -0
  7. package/bin/auto-run-docs.test.js +60 -0
  8. package/bin/auto-scope-docs.test.js +33 -0
  9. package/bin/auto-slots-docs.test.js +45 -0
  10. package/bin/auto-uat-docs.test.js +47 -0
  11. package/bin/config-dir-templates-docs.test.js +52 -0
  12. package/bin/decision-builds-docs.test.js +75 -0
  13. package/bin/fog-vs-ticket-docs.test.js +56 -0
  14. package/bin/grilling-holistic-review-docs.test.js +90 -0
  15. package/bin/lib/codex-projection.js +13 -6
  16. package/bin/lib/codex-projection.test.js +25 -2
  17. package/bin/lib/host-descriptor.js +14 -1
  18. package/bin/lib/host-descriptor.test.js +18 -0
  19. package/bin/lib/runtime-command-literals.test.js +184 -0
  20. package/bin/needs-you-docs.test.js +41 -0
  21. package/bin/pick-stress-test-docs.test.js +105 -0
  22. package/bin/report-status-docs.test.js +1 -1
  23. package/bin/trailhead.js +3 -3
  24. package/bin/trailhead.test.js +17 -18
  25. package/package.json +1 -1
  26. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  27. package/plugins/trailhead/agents/trailhead-code-review.md +2 -2
  28. package/plugins/trailhead/agents/trailhead-verify.md +2 -2
  29. package/plugins/trailhead/commands/auto.md +2 -2
  30. package/plugins/trailhead/hooks/lib/auto-merge.test.js +467 -0
  31. package/plugins/trailhead/hooks/lib/auto-queue.test.js +245 -0
  32. package/plugins/trailhead/hooks/lib/auto-run.test.js +318 -0
  33. package/plugins/trailhead/hooks/lib/auto-slot.test.js +278 -0
  34. package/plugins/trailhead/hooks/lib/checkout-in.test.js +173 -0
  35. package/plugins/trailhead/hooks/lib/dashboard.test.js +66 -3
  36. package/plugins/trailhead/hooks/lib/merge-ticket.test.js +394 -0
  37. package/plugins/trailhead/hooks/lib/model-defaults-review.js +50 -21
  38. package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +50 -0
  39. package/plugins/trailhead/hooks/lib/needs-you.test.js +177 -0
  40. package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +319 -0
  41. package/plugins/trailhead/hooks/lib/report-status.test.js +68 -1
  42. package/plugins/trailhead/hooks/lib/shell-scan.js +25 -0
  43. package/plugins/trailhead/hooks/lib/statusline-context.test.js +161 -0
  44. package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
  45. package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
  46. package/plugins/trailhead/hooks/lib/unmodelled.js +6 -1
  47. package/plugins/trailhead/hooks/mod/README.md +185 -39
  48. package/plugins/trailhead/hooks/mod/auto-run-view.ts +93 -0
  49. package/plugins/trailhead/hooks/mod/auto-run.ts +165 -0
  50. package/plugins/trailhead/hooks/mod/map-pane-view.ts +150 -25
  51. package/plugins/trailhead/hooks/mod/map-pane.tsx +557 -99
  52. package/plugins/trailhead/hooks/mod/register.tsx +298 -39
  53. package/plugins/trailhead/hooks/mod/repo-pin.ts +167 -0
  54. package/plugins/trailhead/hooks/mod/session-registry.ts +65 -0
  55. package/plugins/trailhead/hooks/mod/turn-state.ts +14 -0
  56. package/plugins/trailhead/hooks/mod/whiteboard-follow.ts +98 -0
  57. package/plugins/trailhead/hooks/mod/work-state-locate.ts +191 -0
  58. package/plugins/trailhead/hooks/mod/work-state-prompt.ts +13 -15
  59. package/plugins/trailhead/hooks/mod/work-state-view.ts +21 -5
  60. package/plugins/trailhead/hooks/mod/worktree-porcelain.ts +17 -0
  61. package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
  62. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +22 -16
  63. package/plugins/trailhead/hooks/trailhead-labels-guard.js +231 -0
  64. package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
  65. package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +336 -23
  66. package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +321 -0
  67. package/plugins/trailhead/skills/_shared/choices.md +7 -2
  68. package/plugins/trailhead/skills/_shared/configuration-reference.md +5 -5
  69. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  70. package/plugins/trailhead/skills/_shared/substrate-commands.md +26 -4
  71. package/plugins/trailhead/skills/_shared/substrate.md +4 -1
  72. package/plugins/trailhead/skills/_shared/teamwork.md +5 -5
  73. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +2 -2
  74. package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
  75. package/plugins/trailhead/skills/_shared/techniques/grilling.md +4 -2
  76. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +3 -1
  77. package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
  78. package/plugins/trailhead/skills/_shared/techniques.md +2 -2
  79. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
  80. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
  81. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
  82. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
  83. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +4 -3
  84. package/plugins/trailhead/skills/trailhead-view/SKILL.md +1 -0
  85. package/plugins/trailhead/skills/trailhead-work/SKILL.md +23 -12
  86. package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
  87. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +7 -6
  88. package/plugins/trailhead/templates/statusline-trailhead.sh +80 -15
  89. package/plugins/trailhead/templates/trailhead-auto-merge.js +318 -0
  90. package/plugins/trailhead/templates/trailhead-auto-queue.js +186 -0
  91. package/plugins/trailhead/templates/trailhead-auto-run.js +294 -0
  92. package/plugins/trailhead/templates/trailhead-auto-slot.js +239 -0
  93. package/plugins/trailhead/templates/trailhead-checkout-in.js +88 -0
  94. package/plugins/trailhead/templates/trailhead-dashboard.js +32 -12
  95. package/plugins/trailhead/templates/trailhead-merge-ticket.js +366 -0
  96. package/plugins/trailhead/templates/trailhead-needs-you.js +193 -0
  97. package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +289 -0
  98. package/plugins/trailhead/templates/trailhead-report-status.js +67 -3
  99. package/plugins/trailhead/templates/trailhead-uat-followup.js +128 -0
  100. package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
  101. package/plugins/trailhead/tests/auto-run-ui.test.ts +444 -0
  102. package/plugins/trailhead/tests/auto-run-view.test.ts +111 -0
  103. package/plugins/trailhead/tests/map-pane-ui.test.ts +1839 -72
  104. package/plugins/trailhead/tests/map-pane-view.test.ts +308 -14
  105. package/plugins/trailhead/tests/map-pane-write.test.ts +51 -0
  106. package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +77 -0
  107. package/plugins/trailhead/tests/repo-pin.test.ts +123 -0
  108. package/plugins/trailhead/tests/session-registry.test.ts +50 -0
  109. package/plugins/trailhead/tests/whiteboard-follow.test.ts +90 -0
  110. package/plugins/trailhead/tests/work-state-locate.test.ts +33 -0
  111. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +276 -2
  112. package/plugins/trailhead/tests/work-state-prompt.test.ts +29 -35
  113. package/plugins/trailhead/tests/work-state-ui.test.ts +73 -1
  114. package/plugins/trailhead/tests/work-state-view.test.ts +19 -0
  115. package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
  116. package/plugins/trailhead/types/index.d.ts +26 -1
@@ -173,8 +173,8 @@ const entries = codexHookEntries('/h');
173
173
  ok('codexHookEntries returns 5 entries', Array.isArray(entries) && entries.length === 5);
174
174
  const bashEntries = entries.filter((e) => e.event === 'PreToolUse' && e.matcher === 'Bash');
175
175
  ok('codexHookEntries: exactly one PreToolUse/Bash registration', bashEntries.length === 1);
176
- ok('codexHookEntries: the Bash entry is the merged hook with exactly --guards=commit,secret,install,body',
177
- bashEntries[0].command === 'node "/h/trailhead-bash-guards.js" --guards=commit,secret,install,body');
176
+ ok('codexHookEntries: the Bash entry is the merged hook with exactly --guards=commit,secret,install,body,labels',
177
+ bashEntries[0].command === 'node "/h/trailhead-bash-guards.js" --guards=commit,secret,install,body,labels');
178
178
  ok('codexHookEntries: no per-guard script is registered on its own', entries.every((e) =>
179
179
  !/trailhead-(commit|secret|install|search|body)-guard\.js/.test(e.command)));
180
180
  ok('codexHookEntries: secret-read-guard is PreToolUse/Read|Bash', entries.some((e) =>
@@ -441,6 +441,29 @@ ok('convertToCodex keeps CLAUDE_CODE_SESSION_ID intact',
441
441
  autoRef.includes('--auto true --auto-done <n>'));
442
442
  }
443
443
 
444
+ // #319: l'espansione CLAUDE_CONFIG_DIR diventa CODEX_HOME, mai un ibrido che punta alla config di Claude.
445
+ {
446
+ const a = convertToCodex('${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/x');
447
+ const b = convertToCodex('${CLAUDE_CONFIG_DIR:-$HOME/.claude}/hooks/y');
448
+ ok('convertToCodex maps the ~ config-dir expansion to CODEX_HOME', a === '${CODEX_HOME:-~/.codex}/trailhead/templates/x');
449
+ ok('convertToCodex maps the $HOME config-dir expansion to CODEX_HOME', b === '${CODEX_HOME:-$HOME/.codex}/hooks/y');
450
+ ok('convertToCodex leaves no CLAUDE_CONFIG_DIR behind', !/CLAUDE_CONFIG_DIR/.test(a + b));
451
+ const c = convertToCodex('config dir = `$CLAUDE_CONFIG_DIR` or `~/.claude`; `${CLAUDE_CONFIG_DIR}/x`');
452
+ ok('convertToCodex maps the bare config-dir forms to CODEX_HOME',
453
+ c === 'config dir = `$CODEX_HOME` or `~/.codex`; `${CODEX_HOME}/x`');
454
+ ok('convertToCodex leaves a longer variable name untouched',
455
+ convertToCodex('$CLAUDE_CONFIG_DIRX') === '$CLAUDE_CONFIG_DIRX');
456
+
457
+ // Ogni .md reale proiettato non deve contenere CLAUDE_CONFIG_DIR.
458
+ const root = path.join(__dirname, '../../plugins/trailhead');
459
+ const walk = (d) => fs.readdirSync(d, { withFileTypes: true }).flatMap((e) =>
460
+ e.isDirectory() ? walk(path.join(d, e.name)) : e.name.endsWith('.md') ? [path.join(d, e.name)] : []);
461
+ const files = ['skills', 'agents', 'commands'].flatMap((n) => walk(path.join(root, n)));
462
+ const leaks = files.filter((f) => /CLAUDE_CONFIG_DIR/.test(convertToCodex(fs.readFileSync(f, 'utf8'))));
463
+ ok('real plugin .md files were found', files.length > 0);
464
+ ok('no projected plugin .md keeps CLAUDE_CONFIG_DIR: ' + leaks.join(', '), leaks.length === 0);
465
+ }
466
+
444
467
  // #248: the shared boundary blocks survive the Codex projection of every real agent.
445
468
  {
446
469
  const { parseSource } = require('../../scripts/sync-agent-blocks.js');
@@ -65,6 +65,8 @@ const HOSTS = Object.freeze({
65
65
  label: 'Claude Code',
66
66
  configDirEnv: 'CLAUDE_CONFIG_DIR',
67
67
  configDirDefault: '.claude',
68
+ // Prefisso con cui l'host invoca un verbo trailhead (vedi commandForm).
69
+ commandPrefix: '/trailhead:',
68
70
  axes: Object.freeze({
69
71
  commandSurface: 'slash-file',
70
72
  subagentToolkit: 'full',
@@ -93,6 +95,7 @@ const HOSTS = Object.freeze({
93
95
  label: 'Codex',
94
96
  configDirEnv: 'CODEX_HOME',
95
97
  configDirDefault: '.codex',
98
+ commandPrefix: '$trailhead ',
96
99
  axes: Object.freeze({
97
100
  commandSurface: 'skill',
98
101
  subagentToolkit: 'full',
@@ -168,6 +171,15 @@ function hyphenateCommand(name) {
168
171
  return String(name).trim().replace(/^\//, '').replace(/:/g, '-');
169
172
  }
170
173
 
174
+ // --- Helper: commandForm ------------------------------------------------------
175
+ // Forma di invocazione di un verbo per l'host: la scrittura da usare in ogni
176
+ // testo runtime, mai un literal `/trailhead:<verb>` hardcoded.
177
+ // commandForm('claude', 'update') -> '/trailhead:update'
178
+ // commandForm('codex', 'update') -> '$trailhead update'
179
+ function commandForm(host, verb) {
180
+ return `${resolveHost(host).commandPrefix}${verb}`;
181
+ }
182
+
171
183
  // --- Helper: emitsAgentToml ---------------------------------------------------
172
184
  function emitsAgentToml(host) {
173
185
  return resolveHost(host).emitsAgentToml;
@@ -275,7 +287,7 @@ function updateNotice(host, cache) {
275
287
  const installed = String(cache.installed || '').trim();
276
288
  const ver = latest ? ` ${latest}` : '';
277
289
  const from = installed ? ` (you have ${installed})` : '';
278
- return `Heads up: trailhead${ver} is available${from}. Run /trailhead:update to install it.`;
290
+ return `Heads up: trailhead${ver} is available${from}. Run ${commandForm(host, 'update')} to install it.`;
279
291
  }
280
292
 
281
293
  // --- Codex version floor gate (decision #27) --------------------------------
@@ -390,6 +402,7 @@ module.exports = {
390
402
  getHost,
391
403
  configDirFor,
392
404
  hyphenateCommand,
405
+ commandForm,
393
406
  emitsAgentToml,
394
407
  degradations,
395
408
  modelsCollapseNotice,
@@ -8,6 +8,7 @@ const {
8
8
  getHost,
9
9
  configDirFor,
10
10
  hyphenateCommand,
11
+ commandForm,
11
12
  emitsAgentToml,
12
13
  degradations,
13
14
  modelsCollapseNotice,
@@ -127,6 +128,23 @@ ok('modelsCollapseNotice: codex namespace with only empty/nullish values does no
127
128
  return typeof msg === 'string' && msg.includes('plan') && !msg.includes('codex)');
128
129
  })());
129
130
 
131
+ // --- commandForm ---
132
+ ok('commandForm(claude, update) is the slash form', commandForm('claude', 'update') === '/trailhead:update');
133
+ ok('commandForm(codex, update) is the dollar form', commandForm('codex', 'update') === '$trailhead update');
134
+ ok('commandForm throws on an unknown host', (() => { try { commandForm('nope', 'x'); return false; } catch { return true; } })());
135
+
136
+ // Drift guard: la lib degli hook ha la propria copia di commandForm (non puo'
137
+ // richiedere bin/ una volta installata); le due devono restare identiche.
138
+ {
139
+ const lib = require('../../plugins/trailhead/hooks/lib/model-defaults-review.js');
140
+ for (const host of ['claude', 'codex']) {
141
+ for (const verb of ['config', 'update']) {
142
+ ok(`commandForm drift: hook lib matches host-descriptor (${host}, ${verb})`,
143
+ lib.commandForm(host, verb) === commandForm(host, verb));
144
+ }
145
+ }
146
+ }
147
+
130
148
  // --- updateNotice ---
131
149
  // Both hosts now have a hook bus (hookBus !== 'none'), so updateNotice never
132
150
  // fires for either: the SessionStart hook + (on Codex) its stdout
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+ // Test statico: nessun literal di comando Claude (`/trailhead:<verb>` o `/trailhead`
3
+ // nudo) dentro stringhe/template del codice runtime. Gli hook lib e i template
4
+ // vengono copiati byte-per-byte su Codex e convertToCodex riscrive solo il
5
+ // markdown, quindi un literal qui arriverebbe cosi' com'e' a un utente Codex.
6
+ // Usare commandForm(host, verb) (host-descriptor o helper locale della lib).
7
+ // Run: node runtime-command-literals.test.js. Nessun framework: assert semplici.
8
+ const assert = require('assert');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const ROOT = path.resolve(__dirname, '..', '..');
13
+ const SCAN_DIRS = [
14
+ { dir: 'plugins/trailhead/hooks', skip: ['plugins/trailhead/hooks/mod'] },
15
+ { dir: 'plugins/trailhead/templates', skip: [] },
16
+ { dir: 'bin', skip: [] },
17
+ ];
18
+
19
+ // Eccezioni motivate: file (relativo alla root) + sottostringa della riga.
20
+ // Niente numeri di riga, cosi' le modifiche attorno non rompono il test.
21
+ const ALLOWLIST = [
22
+ {
23
+ file: 'plugins/trailhead/templates/trailhead-dashboard.js',
24
+ contains: 'Regenerate any time with',
25
+ reason: 'corpo del dashboard: artefatto condiviso host-neutral scritto nell\'issue, non un messaggio runtime per host',
26
+ },
27
+ {
28
+ file: 'bin/trailhead.js',
29
+ contains: '(/trailhead:*)',
30
+ reason: 'banner dell\'installer Claude-only (il ramo Codex stampa il proprio banner)',
31
+ },
32
+ {
33
+ file: 'bin/trailhead.js',
34
+ contains: 'then run /trailhead to start',
35
+ reason: 'banner dell\'installer Claude-only',
36
+ },
37
+ {
38
+ file: 'bin/lib/codex-projection.js',
39
+ contains: 'Never \\`/trailhead:<verb>\\`',
40
+ reason: 'testo del SKILL.md Codex: nomina la forma Claude proprio per vietarla all\'agente',
41
+ },
42
+ {
43
+ file: 'bin/lib/host-descriptor.js',
44
+ contains: "commandPrefix: '/trailhead:'",
45
+ reason: 'dato del descriptor Claude da cui commandForm produce la forma Claude',
46
+ },
47
+ {
48
+ file: 'plugins/trailhead/hooks/lib/model-defaults-review.js',
49
+ contains: '`/trailhead:${verb}`',
50
+ reason: 'helper commandForm della lib: l\'unico punto che produce la forma Claude',
51
+ },
52
+ ];
53
+
54
+ // `/trailhead:` (anche seguito da `${`, `*`, `<verb>` o concatenato) oppure
55
+ // `/trailhead` nudo. Non conta dentro un path: non preceduto da parola, `.`,
56
+ // `~`, `\`, `/`, `-` e non seguito da parola, `:`, `/`, `\`, `-`.
57
+ const COMMAND_RE = /(?<![\w.~\\/-])\/trailhead(?::|(?![\w:/\\-]))/g;
58
+
59
+ function listJs(dir, skip) {
60
+ const out = [];
61
+ for (const e of fs.readdirSync(path.join(ROOT, dir), { withFileTypes: true })) {
62
+ const rel = `${dir}/${e.name}`;
63
+ if (e.isDirectory()) {
64
+ if (e.name === 'node_modules' || skip.includes(rel)) continue;
65
+ out.push(...listJs(rel, skip));
66
+ } else if (e.name.endsWith('.js') && !e.name.endsWith('.test.js')) {
67
+ out.push(rel);
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+
73
+ // Scanner minimale a stati sull'intero file: marca i caratteri che stanno
74
+ // dentro stringhe ('...', "...") e template (`...`, anche su piu' righe, con i
75
+ // loro ${...}); commenti e literal regex sono saltati. Non e' un parser JS
76
+ // completo: una regex e' riconosciuta dal carattere significativo precedente.
77
+ const REGEX_PREV = new Set(['', '(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '+', '-', '*', '%', '<', '>', '~', '^']);
78
+ function literalMask(text) {
79
+ const mask = new Uint8Array(text.length);
80
+ let i = 0;
81
+ let prev = '';
82
+ while (i < text.length) {
83
+ const c = text[i];
84
+ const n = text[i + 1];
85
+ if (c === '/' && n === '/') {
86
+ while (i < text.length && text[i] !== '\n') i++;
87
+ } else if (c === '/' && n === '*') {
88
+ const end = text.indexOf('*/', i + 2);
89
+ i = end === -1 ? text.length : end + 2;
90
+ } else if (c === '"' || c === "'" || c === '`') {
91
+ mask[i] = 1;
92
+ i++;
93
+ while (i < text.length && text[i] !== c) {
94
+ if (text[i] === '\\') { mask[i] = 1; i++; }
95
+ if (c !== '`' && text[i] === '\n') break;
96
+ mask[i] = 1;
97
+ i++;
98
+ }
99
+ if (i < text.length) mask[i] = 1;
100
+ i++;
101
+ prev = c;
102
+ } else if (c === '/' && (REGEX_PREV.has(prev) || prev === 'return')) {
103
+ i++;
104
+ let inClass = false;
105
+ while (i < text.length && text[i] !== '\n' && (inClass || text[i] !== '/')) {
106
+ if (text[i] === '\\') i++;
107
+ else if (text[i] === '[') inClass = true;
108
+ else if (text[i] === ']') inClass = false;
109
+ i++;
110
+ }
111
+ i++;
112
+ prev = ')';
113
+ } else {
114
+ if (!/\s/.test(c)) {
115
+ const word = /[A-Za-z_$]/.test(c) ? text.slice(i).match(/^[\w$]+/)[0] : c;
116
+ prev = word === 'return' ? 'return' : word.length > 1 ? 'a' : c;
117
+ i += word.length;
118
+ continue;
119
+ }
120
+ i++;
121
+ }
122
+ }
123
+ return mask;
124
+ }
125
+
126
+ const used = new Set();
127
+
128
+ function findViolations(rel, text) {
129
+ const out = [];
130
+ const mask = literalMask(text);
131
+ const lines = text.split('\n');
132
+ const starts = [];
133
+ let off = 0;
134
+ for (const l of lines) { starts.push(off); off += l.length + 1; }
135
+ for (const m of text.matchAll(COMMAND_RE)) {
136
+ if (!mask[m.index]) continue;
137
+ let ln = starts.findIndex((st, k) => k === starts.length - 1 || starts[k + 1] > m.index);
138
+ const line = lines[ln];
139
+ const idx = ALLOWLIST.findIndex((a) => a.file === rel && line.includes(a.contains));
140
+ if (idx >= 0) { used.add(idx); continue; }
141
+ out.push(`${rel}:${ln + 1}: ${line.trim()}`);
142
+ }
143
+ return out;
144
+ }
145
+
146
+ let passed = 0;
147
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
148
+
149
+ // Sanity del rilevatore: deve fermare il literal originale e ignorare i path.
150
+ ok('detector flags the original review-offer literal',
151
+ findViolations('x.js', " '3) open /trailhead:config to pick per key.',").length === 1);
152
+ ok('detector flags a bare /trailhead in a literal',
153
+ findViolations('x.js', "console.log('run /trailhead to start');").length === 1);
154
+ ok('detector ignores comment lines', findViolations('x.js', '// run /trailhead:update').length === 0);
155
+ ok('detector ignores path segments',
156
+ findViolations('x.js', "path.join('skills/trailhead/hooks', 'refs/heads/trailhead/t1')").length === 0);
157
+ ok('detector flags a concatenated literal', findViolations('x.js', "const c = '/trailhead:' + v;").length === 1);
158
+ ok('detector flags a template literal with interpolation', findViolations('x.js', 'const c = `/trailhead:${v}`;').length === 1);
159
+ ok('detector flags a literal on a later line of a multi-line template',
160
+ findViolations('x.js', 'const t = `first line\nrun /trailhead:config now`;').length === 1);
161
+ ok('detector flags the glob and placeholder forms',
162
+ findViolations('x.js', "a('/trailhead:*'); b('/trailhead:<verb>');").length === 2);
163
+ ok('detector ignores a regex literal with an escaped slash',
164
+ findViolations('x.js', "const R = /^refs\\/heads\\/trailhead\\/t(\\d+)$/; const q = /['\"]/;").length === 0);
165
+ ok('detector ignores the host-neutral helper form',
166
+ findViolations('x.js', 'const c = commandForm(host, "config");').length === 0);
167
+
168
+ // Le eccezioni in allowlist devono ancora corrispondere a qualcosa.
169
+ for (const a of ALLOWLIST) {
170
+ ok(`allowlist file exists: ${a.file}`, fs.existsSync(path.join(ROOT, a.file)));
171
+ ok(`allowlist reason given: ${a.file}`, a.reason.length > 0);
172
+ }
173
+
174
+ const files = SCAN_DIRS.flatMap(({ dir, skip }) => listJs(dir, skip));
175
+ ok('scan covers hook libs, templates and bin', files.length > 20);
176
+ const violations = files.flatMap((rel) => findViolations(rel, fs.readFileSync(path.join(ROOT, rel), 'utf8')));
177
+ assert.deepStrictEqual(violations, [],
178
+ `literal di comando Claude in codice runtime (usa commandForm):\n${violations.join('\n')}`);
179
+ passed++;
180
+ const unused = ALLOWLIST.filter((_, i) => !used.has(i)).map((a) => `${a.file}: ${a.contains}`);
181
+ assert.deepStrictEqual(unused, [], `voci di allowlist mai usate (da rimuovere):\n${unused.join('\n')}`);
182
+ passed++;
183
+
184
+ console.log(`runtime-command-literals.test.js: ${passed} assertions passed`);
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #282: the needs-you label (an autonomous run parks a ticket for the
3
+ // human) is documented where the run, the claim and the substrate depend on it.
4
+ // Plain asserts, no framework. Run: node bin/needs-you-docs.test.js
5
+ const assert = require('assert');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const plugin = path.resolve(__dirname, '..', 'plugins', 'trailhead');
10
+ const read = (rel) => fs.readFileSync(path.join(plugin, rel), 'utf8');
11
+ let passed = 0;
12
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
13
+
14
+ const auto = read('skills/trailhead-work/references/auto.md');
15
+ const work = read('skills/trailhead-work/SKILL.md');
16
+ const substrate = read('skills/_shared/substrate.md');
17
+ const commands = read('skills/_shared/substrate-commands.md');
18
+
19
+ for (const needle of ['trailhead:needs-you', 'trailhead-needs-you.js park', '`NEEDS YOU`', 'needs-you ticket', 'queue skips it', 'does not reconsider it', 'surviving blocking review concern', 'stale-base stop', 'a failing UAT setup'.replace('a failing', 'failing')]) {
20
+ ok(`auto.md names: ${needle}`, auto.includes(needle));
21
+ }
22
+ ok('auto.md no longer says "set aside" / "set-aside"', !/set[- ]aside|sets? (?:it|that|such a|the) (?:ticket )?aside|sets? .{0,20} aside/i.test(auto));
23
+
24
+ const claimStep = (work.match(/^2\. Choose the ticket\..*$/m) || [''])[0];
25
+ const quickBullet = (work.match(/^- \*\*Same isolation setup as `work`\.\*\*.*$/m) || [''])[0];
26
+ const CLEAR = 'node <templates>/trailhead-needs-you.js clear --issue <n> --repo <owner/name>';
27
+ ok('work claim step clears needs-you with the helper', claimStep.includes(CLEAR));
28
+ ok('quick isolation bullet clears needs-you with the helper', quickBullet.includes(CLEAR));
29
+ ok('work claim step has the by-hand fallback', claimStep.includes('gh issue edit <n> --remove-label trailhead:needs-you'));
30
+ ok('a failed clear is reported, never blocking the claim (work)', /failed `clear` is reported to the user in one line but never blocks the claim/.test(claimStep));
31
+ ok('a failed clear is reported, never blocking the claim (quick)', /failed `clear` is reported to the user in one line but never blocks the claim/.test(quickBullet));
32
+
33
+ ok('substrate.md documents the label, off every run, on the human frontier', /\*\*`trailhead:needs-you`\*\*[\s\S]{0,900}not a frontier exclusion for humans/.test(substrate) && substrate.includes('twenty-one labels'));
34
+ ok('substrate-commands.md ensures the label idempotently', commands.includes('gh label create trailhead:needs-you --color D93F0B --description "Needs you: an autonomous run parked it for the human" --force'));
35
+ ok('substrate-commands.md documents park and clear', commands.includes('trailhead-needs-you.js park --issue <n> --reason <text> --repo <owner/name>') && commands.includes('trailhead-needs-you.js clear --issue <n> --repo <owner/name>'));
36
+ ok('substrate-commands.md first-use list has twenty-one labels', /all twenty-one:[^\n]*`trailhead:needs-you`/.test(commands));
37
+
38
+ for (const [name, text] of [['auto.md', auto], ['SKILL.md', work], ['substrate.md', substrate], ['substrate-commands.md', commands]]) {
39
+ ok(`${name}: no em-dash`, !text.includes(String.fromCharCode(0x2014)));
40
+ }
41
+ console.log(`needs-you-docs.test.js: ${passed} checks passed`);
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ // Docs test: the agent never offers or acts on its own pick unchecked. The stress-test rule is
3
+ // stated once (choices.md) and pointed at by grilling, the delegate option, auto and the Plan;
4
+ // the Plan carries a Done-when coverage check, decision Diverge re-checks a framed option set,
5
+ // and inbox never closes or answers someone else's issue without the user's yes.
6
+ // Run: node pick-stress-test-docs.test.js
7
+ const assert = require('assert');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const skills = path.join(__dirname, '..', 'plugins', 'trailhead', 'skills');
12
+ const read = (rel) => fs.readFileSync(path.join(skills, rel), 'utf8');
13
+
14
+ let passed = 0;
15
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
16
+
17
+ const ANCHOR = 'Stress-test the pick before offering it';
18
+ const DEFINITION = `**${ANCHOR}.**`;
19
+
20
+ const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
21
+ e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]);
22
+ const lineOf = (text, pred) => text.split('\n').find(pred);
23
+
24
+ // 1. la definizione esiste una sola volta, in choices.md
25
+ const defs = walk(skills).filter((f) => fs.readFileSync(f, 'utf8').includes(DEFINITION));
26
+ ok('definition appears in exactly one file', defs.length === 1);
27
+ ok('definition lives in choices.md', defs[0] === path.join(skills, '_shared', 'choices.md'));
28
+ const choices = read('_shared/choices.md');
29
+ ok('definition appears once', choices.split(DEFINITION).length === 2);
30
+
31
+ // 2. la definizione porta i frammenti normativi
32
+ const rule = lineOf(choices, (l) => l.includes(DEFINITION));
33
+ for (const frag of [
34
+ 'a recommended answer in grilling',
35
+ '`(Recommended)`',
36
+ 'the delegate option',
37
+ 'an option `auto` takes on its own',
38
+ 'the edge case or failure mode where it breaks',
39
+ 'checked against the code rather than assumed',
40
+ 'the choices already settled',
41
+ 'real holes, not manufactured doubt',
42
+ 'One pass, no loop',
43
+ 'present only that',
44
+ 'Weak point:',
45
+ 'still recommended because',
46
+ 'conversation language',
47
+ 'where no human sees it live',
48
+ 'trivial or already-decided choice',
49
+ ]) ok(`rule carries: ${frag}`, rule.includes(frag));
50
+
51
+ // 3. i call-site puntano alla regola senza ripeterla
52
+ const pointer = (name, line) => {
53
+ ok(`${name} found`, !!line);
54
+ ok(`${name} references the anchor`, line.includes(ANCHOR));
55
+ ok(`${name} does not restate the rule`, !line.includes('manufactured doubt'));
56
+ };
57
+ const grilling = read('_shared/techniques/grilling.md');
58
+ const gl = grilling.split('\n');
59
+ const pickIdx = gl.findIndex((l) => l.startsWith('- **One question at a time, with your pick.**'));
60
+ ok('grilling pick bullet found', pickIdx >= 0);
61
+ pointer('grilling self-check bullet', gl[pickIdx + 1]);
62
+ ok('grilling bullet keeps the per-question check', gl[pickIdx + 1].includes('by question number'));
63
+ ok('grilling pick bullet points at the self-check', gl[pickIdx].includes('stress-tested per the next bullet'));
64
+ pointer('delegate step 2', lineOf(choices, (l) => l.startsWith('2. The agent states **which option it would choose')));
65
+ const auto = read('trailhead-work/references/auto.md');
66
+ const autoLine = lineOf(auto, (l) => l.startsWith('- **Advisory / process choice**'));
67
+ pointer('auto advisory choice', autoLine);
68
+ ok('auto records the weak point', autoLine.includes('`Weak point:` line'));
69
+ const engines = read('trailhead-work/references/ticket-engines.md');
70
+ const plan = lineOf(engines, (l) => l.startsWith('2. **Plan**'));
71
+ pointer('build Plan', plan);
72
+
73
+ // 4. Plan: copertura del Done when prima di ready
74
+ for (const frag of [
75
+ '**Coverage check before `ready`.**',
76
+ 'every `Done when` criterion',
77
+ 'a criterion no step delivers is a gap',
78
+ 'return `needs_input`',
79
+ 'the short inline plan too',
80
+ ]) ok(`plan carries: ${frag}`, plan.includes(frag));
81
+
82
+ // 5. Diverge: un set di opzioni gia' inquadrato viene ricontrollato
83
+ const diverge = lineOf(engines, (l) => l.startsWith('1. **Diverge'));
84
+ ok('diverge found', !!diverge);
85
+ ok('diverge no longer skips blindly', !diverge.includes('skip straight to grilling when it did.'));
86
+ for (const frag of ['still current and complete', 'Decisions so far', 'status quo', '`OPTIONS` comment'])
87
+ ok(`diverge carries: ${frag}`, diverge.includes(frag));
88
+
89
+ // 6. Inbox: nessuna chiusura o commento pubblico senza un si'
90
+ const inbox = read('trailhead-chart/references/inbox.md');
91
+ ok('inbox drops "on your own for clear ones"', !inbox.includes('on your own for clear ones'));
92
+ const intro = lineOf(inbox, (l) => l.startsWith('For each issue in the first two sections'));
93
+ for (const frag of ['propose a disposition', 'one list', 'explicit yes', 'never close', 'comment on', 'out-of-scope ruling'])
94
+ ok(`inbox intro carries: ${frag}`, intro.includes(frag));
95
+ const dup = lineOf(inbox, (l) => l.startsWith('- **Duplicate.**'));
96
+ for (const frag of ['what each one covers', 'partial overlap', 'not a duplicate'])
97
+ ok(`inbox duplicate carries: ${frag}`, dup.includes(frag));
98
+
99
+ // 7. niente em-dash
100
+ for (const rel of ['_shared/choices.md', '_shared/techniques/grilling.md', 'trailhead-work/references/auto.md',
101
+ 'trailhead-work/references/ticket-engines.md', 'trailhead-chart/references/inbox.md'])
102
+ ok(`${rel} has no em-dash`, !read(rel).includes(String.fromCharCode(8212)));
103
+ ok('this test has no em-dash', !fs.readFileSync(__filename, 'utf8').includes(String.fromCharCode(8212)));
104
+
105
+ console.log(`pick-stress-test-docs.test.js: ${passed} checks passed`);
@@ -48,7 +48,7 @@ ok('the checker command with its technique flag and a report file',
48
48
  ok('the three templates dirs, in order',
49
49
  (() => {
50
50
  const a = para.indexOf('${CLAUDE_PLUGIN_ROOT}/templates/');
51
- const b = para.indexOf('~/.claude/trailhead/templates/');
51
+ const b = para.indexOf('${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/');
52
52
  const c = para.indexOf('~/.codex/skills/trailhead/templates/');
53
53
  return a !== -1 && b > a && c > b;
54
54
  })());
package/bin/trailhead.js CHANGED
@@ -243,10 +243,10 @@ function claudePaths(configDir) {
243
243
  settings: path.join(configDir, 'settings.json'),
244
244
  };
245
245
  }
246
- // The five Bash guard modules. They are shipped, but only require()d by the
246
+ // The six Bash guard modules. They are shipped, but only require()d by the
247
247
  // merged trailhead-bash-guards.js (one process per Bash call), never registered
248
248
  // on their own. Legacy installs registered each one; the installers strip those.
249
- const BASH_GUARD_MODULES = ['trailhead-commit-guard.js', 'trailhead-secret-guard.js', 'trailhead-install-guard.js', 'trailhead-search-guard.js', 'trailhead-body-guard.js'];
249
+ const BASH_GUARD_MODULES = ['trailhead-commit-guard.js', 'trailhead-secret-guard.js', 'trailhead-install-guard.js', 'trailhead-search-guard.js', 'trailhead-body-guard.js', 'trailhead-labels-guard.js'];
250
250
  // Every hook script copied into the hooks dir (shipped runtime files).
251
251
  const HOOK_FILES = [...BASH_GUARD_MODULES, 'trailhead-bash-guards.js', 'trailhead-issue-injection-scanner.js', 'trailhead-secret-read-guard.js', 'trailhead-mockup-link-guard.js', 'trailhead-mockup-link-stop.js', 'trailhead-check-update.js'];
252
252
  // The registered entrypoints: the shipped files that must appear in
@@ -436,7 +436,7 @@ function installClaude(configDir, { useSymlink }) {
436
436
 
437
437
  const s = readJSON(P.settings);
438
438
  // Migration: an earlier install registered each Bash guard on its own; the
439
- // merged entrypoint below replaces all five.
439
+ // merged entrypoint below replaces all six.
440
440
  stripLegacyBashGuards(s);
441
441
  const added = [
442
442
  addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-bash-guards.js')),
@@ -757,12 +757,12 @@ ok('source: both Pending-UAT guard lines reference the drive-mode ask', pendingU
757
757
  // auto.md mentions the ask, AND (plan-review addition) a single localized span
758
758
  // states its outcomes TOGETHER: takes it without the confirm gate, picks
759
759
  // AI-driven when any step is agent-performable, never fakes a human-only step,
760
- // and sets the ticket aside listing them in the run-end summary.
760
+ // and routes the human-only steps to a follow-up opened before the parent closes (#281).
761
761
  const autoSourcePath = path.join(sourceSkillsDir, 'trailhead-work', 'references', 'auto.md');
762
762
  const autoSource = fs.readFileSync(autoSourcePath, 'utf8');
763
763
  ok('source: auto.md mentions the drive-mode ask', autoSource.includes('drive-mode ask'));
764
764
  ok('source: auto.md states the drive-mode-ask outcomes together (grouped, not scattered)',
765
- /drive-mode ask[\s\S]{0,120}without the confirm gate[\s\S]{0,300}AI-driven[\s\S]{0,200}agent-performable[\s\S]{0,300}never fake[\s\S]{0,300}run-end summary/.test(autoSource));
765
+ /drive-mode ask[\s\S]{0,120}without the confirm gate[\s\S]{0,300}AI-driven[\s\S]{0,200}agent-performable[\s\S]{0,300}never fakes[\s\S]{0,1500}follow-up/.test(autoSource));
766
766
 
767
767
  // choices.md carries the call-site row for the drive-mode ask.
768
768
  const choicesSourcePath = path.join(sourceSkillsDir, '_shared', 'choices.md');
@@ -816,13 +816,12 @@ ok('source: configuration-reference.md acceptance.browser clause does NOT make t
816
816
  ok('source: configuration-reference.md acceptance.browser clause states the drive-mode ask always comes first',
817
817
  /\*\*acceptance\.browser\.\*\*[\s\S]{0,200}drive-mode ask[\s\S]{0,120}always comes first/.test(configReferenceSource));
818
818
 
819
- // The run-end summary's enumerated set-aside category list is missing the new
820
- // human-only-UAT-steps class (added by #178); it must be listed there too,
821
- // matching the wording already used for it at line 29 ("human-only steps").
819
+ // Human-only UAT steps are no longer a set-aside reason (#281): the run-end summary
820
+ // lists the UAT follow-ups opened this run instead.
822
821
  const runEndSummarySection = extractSection(autoSource, '## Run-end summary');
823
822
  ok('source: auto.md Run-end summary section exists', runEndSummarySection.length > 0);
824
- ok('source: auto.md Run-end summary "what remains" list includes human-only UAT steps',
825
- /what remains[\s\S]{0,400}human-only UAT steps/.test(runEndSummarySection));
823
+ ok('source: auto.md Run-end summary "what remains" lists the UAT follow-ups opened this run',
824
+ /what remains[\s\S]{0,900}UAT follow-ups opened this run/.test(runEndSummarySection));
826
825
 
827
826
  // --- #254: skip option at the drive-mode ask (a waiver, not a deferral) -----
828
827
  // The waiver's operational contract is single-sourced in the Drive-mode bullet
@@ -892,7 +891,7 @@ ok('README.md no-performable-step case asks human-driven or skip', /human-driven
892
891
  ok('README.md point 6 allows an explicit waiver', /never closes on a UAT nobody ran unless you explicitly waive it/.test(readmeSource));
893
892
  const siteAcceptanceSource = fs.readFileSync(path.join(repoRoot, 'site', 'src', 'content', 'docs', 'docs', 'acceptance-testing.md'), 'utf8');
894
893
  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));
894
+ /the parent ticket resolves; `\/trailhead:auto` never picks skip/.test(readmeSource) && /the parent ticket resolves; `\/trailhead:auto` never picks skip/.test(siteAcceptanceSource) && /il ticket genitore si risolve; `\/trailhead:auto` non sceglie mai skip/.test(readmeItSource));
896
895
  ok('README.md build row and acceptance.browser row name skip',
897
896
  /🔨 `build`[\s\S]{0,500}or skip/.test(readmeSource) && /`acceptance\.browser`[\s\S]{0,300}or skip/.test(readmeSource));
898
897
  ok('README.it.md acceptance testing section names the waiver (skip) recorded as unconfirmed',
@@ -913,10 +912,10 @@ ok('codex: hooks.json exists', fs.existsSync(codexHooksJsonPath));
913
912
  const codexHooksJson = JSON.parse(fs.readFileSync(codexHooksJsonPath, 'utf8'));
914
913
  const codexHooksJsonStr = JSON.stringify(codexHooksJson);
915
914
  ok('codex: hooks.json registers the merged Bash guards under PreToolUse', (codexHooksJson.hooks.PreToolUse || []).some((g) => g.matcher === 'Bash' &&
916
- (g.hooks || []).some((h) => h.command.includes('trailhead-bash-guards.js') && h.command.endsWith(' --guards=commit,secret,install,body'))));
917
- ok('codex: hooks.json registers none of the per-guard scripts on their own', !/trailhead-(commit|secret|install|search|body)-guard\.js/.test(codexHooksJsonStr));
918
- ok('codex: the five guard modules the merged hook requires are still shipped (no search)',
919
- ['commit', 'secret', 'install', 'body'].every((n) => fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', `trailhead-${n}-guard.js`))) &&
915
+ (g.hooks || []).some((h) => h.command.includes('trailhead-bash-guards.js') && h.command.endsWith(' --guards=commit,secret,install,body,labels'))));
916
+ 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
+ ok('codex: the six guard modules the merged hook requires are still shipped (no search)',
918
+ ['commit', 'secret', 'install', 'body', 'labels'].every((n) => fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', `trailhead-${n}-guard.js`))) &&
920
919
  fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'trailhead-bash-guards.js')));
921
920
  ok('codex: hooks.json registers injection-scanner under PostToolUse', (codexHooksJson.hooks.PostToolUse || []).some((g) => (g.hooks || []).some((h) => h.command.includes('trailhead-issue-injection-scanner.js'))));
922
921
  ok('codex: hooks.json registers check-update under SessionStart', (codexHooksJson.hooks.SessionStart || []).some((g) => (g.hooks || []).some((h) => h.command.includes('trailhead-check-update.js'))));
@@ -1089,7 +1088,7 @@ ok('codex uninstall: cluster trailhead-view gone', !fs.existsSync(path.join(code
1089
1088
  ok('codex uninstall: hooks.json no longer contains trailhead commands', (() => {
1090
1089
  const h = JSON.parse(fs.readFileSync(codexHooksJsonPath, 'utf8'));
1091
1090
  const str = JSON.stringify(h);
1092
- return !str.includes('trailhead-bash-guards.js') && !/trailhead-(commit|secret|install|search|body)-guard\.js/.test(str) &&
1091
+ return !str.includes('trailhead-bash-guards.js') && !/trailhead-(commit|secret|install|search|body|labels)-guard\.js/.test(str) &&
1093
1092
  !str.includes('trailhead-issue-injection-scanner.js') && !str.includes('trailhead-check-update.js');
1094
1093
  })());
1095
1094
 
@@ -1305,12 +1304,12 @@ ok('claude: settings.json exists', fs.existsSync(settingsPath));
1305
1304
  const settingsContent = fs.readFileSync(settingsPath, 'utf8');
1306
1305
  ok('claude: settings.json registers the merged Bash guards', settingsContent.includes('trailhead-bash-guards.js'));
1307
1306
  ok('claude: settings.json registers none of the per-guard scripts on their own',
1308
- !/trailhead-(commit|secret|install|search|body)-guard\.js/.test(settingsContent));
1307
+ !/trailhead-(commit|secret|install|search|body|labels)-guard\.js/.test(settingsContent));
1309
1308
  ok('claude: exactly one PreToolUse/Bash trailhead-bash-guards registration', (() => {
1310
1309
  const s = JSON.parse(settingsContent);
1311
1310
  return (s.hooks.PreToolUse || []).flatMap((g) => g.hooks || []).filter((h) => h.command.includes('trailhead-bash-guards.js')).length === 1;
1312
1311
  })());
1313
- ok('claude: hooks/trailhead-bash-guards.js and its five modules exist', ['bash-guards', 'commit-guard', 'secret-guard', 'install-guard', 'search-guard', 'body-guard']
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']
1314
1313
  .every((n) => fs.existsSync(path.join(claudeDir, 'hooks', `trailhead-${n}.js`))));
1315
1314
  // mockup-link-guard: Claude-only PreToolUse(AskUserQuestion) backstop for the
1316
1315
  // Prototype "link in the same message as the approval ask" rule.
@@ -1691,7 +1690,7 @@ const bashGuardCommands = (registry) => (registry.hooks.PreToolUse || [])
1691
1690
  ok('(#207) codex migration: exit 0', out.status === 0);
1692
1691
  const cmds = bashGuardCommands(JSON.parse(fs.readFileSync(hooksJsonFile, 'utf8')));
1693
1692
  ok('(#207) codex migration: only the merged entry remains', cmds.length === 1 &&
1694
- cmds[0].endsWith(' --guards=commit,secret,install,body'));
1693
+ cmds[0].endsWith(' --guards=commit,secret,install,body,labels'));
1695
1694
  const once = fs.readFileSync(hooksJsonFile, 'utf8');
1696
1695
  runInstaller([`--codex`, `--dir=${mDir}`]);
1697
1696
  ok('(#207) codex install twice leaves the same hooks.json', fs.readFileSync(hooksJsonFile, 'utf8') === once);
@@ -2068,8 +2067,8 @@ ok('source: hooks.json keeps its command hooks next to the module (#202)',
2068
2067
 
2069
2068
  // --- #259: `trailhead:paused` label marks a paused ticket (pause adds, resume/claim drop) ---
2070
2069
  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`'));
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`'));
2073
2072
  ok('source: no stale "eighteen" or "nineteen" label count remains in the shared skills',
2074
2073
  !/eighteen|nineteen/.test(substrateCommandsSource) && !/eighteen|nineteen/.test(substrateSource));
2075
2074
  ok('source: substrate-commands.md ensures trailhead:paused idempotently',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcomigozzi/trailhead",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
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.15.0",
3
+ "version": "0.16.1",
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",
@@ -19,7 +19,7 @@ Never `cd` (or `pushd`/`env -C`) into a directory first; the full rule is in `_s
19
19
  <!-- trailhead:agent-block reviewing -->
20
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
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>`.
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
+ - 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
25
  <!-- /trailhead:agent-block reviewing -->