@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.
- package/README.it.md +52 -14
- package/README.md +53 -15
- package/bin/auto-intro-docs.test.js +72 -0
- package/bin/auto-merge-docs.test.js +33 -0
- package/bin/auto-merge-ticket-docs.test.js +32 -0
- package/bin/auto-review-rounds-docs.test.js +45 -0
- package/bin/auto-run-docs.test.js +60 -0
- package/bin/auto-scope-docs.test.js +33 -0
- package/bin/auto-slots-docs.test.js +45 -0
- package/bin/auto-uat-docs.test.js +47 -0
- package/bin/config-dir-templates-docs.test.js +52 -0
- package/bin/decision-builds-docs.test.js +75 -0
- package/bin/fog-vs-ticket-docs.test.js +56 -0
- package/bin/grilling-holistic-review-docs.test.js +90 -0
- package/bin/lib/codex-projection.js +13 -6
- package/bin/lib/codex-projection.test.js +25 -2
- package/bin/lib/host-descriptor.js +14 -1
- package/bin/lib/host-descriptor.test.js +18 -0
- package/bin/lib/runtime-command-literals.test.js +184 -0
- package/bin/needs-you-docs.test.js +41 -0
- package/bin/pick-stress-test-docs.test.js +105 -0
- package/bin/report-status-docs.test.js +1 -1
- package/bin/trailhead.js +3 -3
- package/bin/trailhead.test.js +17 -18
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +2 -2
- package/plugins/trailhead/agents/trailhead-verify.md +2 -2
- package/plugins/trailhead/commands/auto.md +2 -2
- package/plugins/trailhead/hooks/lib/auto-merge.test.js +467 -0
- package/plugins/trailhead/hooks/lib/auto-queue.test.js +245 -0
- package/plugins/trailhead/hooks/lib/auto-run.test.js +318 -0
- package/plugins/trailhead/hooks/lib/auto-slot.test.js +278 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +173 -0
- package/plugins/trailhead/hooks/lib/dashboard.test.js +66 -3
- package/plugins/trailhead/hooks/lib/merge-ticket.test.js +394 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +50 -21
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +50 -0
- package/plugins/trailhead/hooks/lib/needs-you.test.js +177 -0
- package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +319 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +68 -1
- package/plugins/trailhead/hooks/lib/shell-scan.js +25 -0
- package/plugins/trailhead/hooks/lib/statusline-context.test.js +161 -0
- package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
- package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
- package/plugins/trailhead/hooks/lib/unmodelled.js +6 -1
- package/plugins/trailhead/hooks/mod/README.md +185 -39
- package/plugins/trailhead/hooks/mod/auto-run-view.ts +93 -0
- package/plugins/trailhead/hooks/mod/auto-run.ts +165 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +150 -25
- package/plugins/trailhead/hooks/mod/map-pane.tsx +557 -99
- package/plugins/trailhead/hooks/mod/register.tsx +298 -39
- package/plugins/trailhead/hooks/mod/repo-pin.ts +167 -0
- package/plugins/trailhead/hooks/mod/session-registry.ts +65 -0
- package/plugins/trailhead/hooks/mod/turn-state.ts +14 -0
- package/plugins/trailhead/hooks/mod/whiteboard-follow.ts +98 -0
- package/plugins/trailhead/hooks/mod/work-state-locate.ts +191 -0
- package/plugins/trailhead/hooks/mod/work-state-prompt.ts +13 -15
- package/plugins/trailhead/hooks/mod/work-state-view.ts +21 -5
- package/plugins/trailhead/hooks/mod/worktree-porcelain.ts +17 -0
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +22 -16
- package/plugins/trailhead/hooks/trailhead-labels-guard.js +231 -0
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +336 -23
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +321 -0
- package/plugins/trailhead/skills/_shared/choices.md +7 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +5 -5
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +26 -4
- package/plugins/trailhead/skills/_shared/substrate.md +4 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +5 -5
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +2 -2
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +3 -1
- package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques.md +2 -2
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +4 -3
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +1 -0
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +23 -12
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +7 -6
- package/plugins/trailhead/templates/statusline-trailhead.sh +80 -15
- package/plugins/trailhead/templates/trailhead-auto-merge.js +318 -0
- package/plugins/trailhead/templates/trailhead-auto-queue.js +186 -0
- package/plugins/trailhead/templates/trailhead-auto-run.js +294 -0
- package/plugins/trailhead/templates/trailhead-auto-slot.js +239 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +88 -0
- package/plugins/trailhead/templates/trailhead-dashboard.js +32 -12
- package/plugins/trailhead/templates/trailhead-merge-ticket.js +366 -0
- package/plugins/trailhead/templates/trailhead-needs-you.js +193 -0
- package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +289 -0
- package/plugins/trailhead/templates/trailhead-report-status.js +67 -3
- package/plugins/trailhead/templates/trailhead-uat-followup.js +128 -0
- package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
- package/plugins/trailhead/tests/auto-run-ui.test.ts +444 -0
- package/plugins/trailhead/tests/auto-run-view.test.ts +111 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +1839 -72
- package/plugins/trailhead/tests/map-pane-view.test.ts +308 -14
- package/plugins/trailhead/tests/map-pane-write.test.ts +51 -0
- package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +77 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +123 -0
- package/plugins/trailhead/tests/session-registry.test.ts +50 -0
- package/plugins/trailhead/tests/whiteboard-follow.test.ts +90 -0
- package/plugins/trailhead/tests/work-state-locate.test.ts +33 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +276 -2
- package/plugins/trailhead/tests/work-state-prompt.test.ts +29 -35
- package/plugins/trailhead/tests/work-state-ui.test.ts +73 -1
- package/plugins/trailhead/tests/work-state-view.test.ts +19 -0
- package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
- 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
|
|
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('
|
|
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
|
|
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
|
|
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')),
|
package/bin/trailhead.test.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
//
|
|
820
|
-
//
|
|
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"
|
|
825
|
-
/what remains[\s\S]{0,
|
|
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
|
-
/
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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 -->
|