@marcomigozzi/trailhead 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.it.md +67 -18
- package/README.md +67 -18
- package/bin/capture-action.test.js +167 -0
- package/bin/capture-docs.test.js +32 -0
- package/bin/config-dir-templates-docs.test.js +52 -0
- package/bin/decision-builds-docs.test.js +75 -0
- package/bin/engine-route-docs.test.js +148 -0
- package/bin/fog-vs-ticket-docs.test.js +56 -0
- package/bin/grill-model-docs.test.js +155 -0
- package/bin/grilling-holistic-review-docs.test.js +7 -3
- package/bin/lib/codex-projection.js +30 -14
- package/bin/lib/codex-projection.test.js +81 -17
- package/bin/lib/host-descriptor.js +15 -2
- package/bin/lib/host-descriptor.test.js +24 -0
- package/bin/lib/runtime-command-literals.test.js +184 -0
- package/bin/report-status-docs.test.js +11 -3
- package/bin/session-trust-docs.test.js +37 -0
- package/bin/surfaced-scope-docs.test.js +57 -0
- package/bin/trailhead.js +7 -5
- package/bin/trailhead.test.js +53 -11
- package/bin/unblock-docs.test.js +56 -0
- 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-codebase-map.md +1 -1
- package/plugins/trailhead/agents/trailhead-grill.md +41 -0
- package/plugins/trailhead/agents/trailhead-verify.md +2 -2
- package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
- package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
- package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
- package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +84 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +72 -22
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +113 -0
- package/plugins/trailhead/hooks/lib/model-route.js +207 -0
- package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +124 -2
- package/plugins/trailhead/hooks/lib/shell-scan.js +25 -0
- package/plugins/trailhead/hooks/lib/statusline-context.test.js +22 -1
- package/plugins/trailhead/hooks/lib/unmodelled.js +6 -1
- package/plugins/trailhead/hooks/mod/README.md +24 -10
- package/plugins/trailhead/hooks/mod/map-pane.tsx +124 -44
- package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
- package/plugins/trailhead/hooks/mod/repo-pin.ts +8 -5
- package/plugins/trailhead/hooks/trailhead-check-update.js +17 -1
- package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +40 -1
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +68 -0
- package/plugins/trailhead/skills/_shared/capture-core.md +118 -0
- package/plugins/trailhead/skills/_shared/choices.md +2 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +20 -18
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +2 -2
- package/plugins/trailhead/skills/_shared/substrate-commands.md +8 -4
- package/plugins/trailhead/skills/_shared/substrate.md +1 -1
- package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
- package/plugins/trailhead/skills/_shared/teamwork.md +9 -5
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +5 -2
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/debug.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +26 -1
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/verify.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques.md +7 -5
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
- 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 +3 -3
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +5 -4
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +13 -11
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +11 -10
- package/plugins/trailhead/templates/statusline-trailhead.sh +3 -3
- package/plugins/trailhead/templates/trailhead-capture.js +418 -0
- package/plugins/trailhead/templates/trailhead-checkout-find.js +256 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +76 -20
- package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
- package/plugins/trailhead/templates/trailhead-issue-body.js +69 -6
- package/plugins/trailhead/templates/trailhead-report-status.js +123 -11
- package/plugins/trailhead/tests/map-pane-ui.test.ts +299 -37
- package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
- package/plugins/trailhead/types/index.d.ts +7 -0
|
@@ -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`);
|
|
@@ -35,23 +35,31 @@ ok('the paragraph sits next to "Reviews never block the session"',
|
|
|
35
35
|
start !== -1 && neverBlock !== -1 && Math.abs(start - neverBlock) < 2500 && start > neverBlock);
|
|
36
36
|
const para = start === -1 ? '' : techniques.slice(start, techniques.indexOf('\n| Technique |', start));
|
|
37
37
|
const lc = para.toLowerCase();
|
|
38
|
+
ok('an inline grill round writes no report, so it is not checked',
|
|
39
|
+
para.includes('An inline `grill` round writes no report') && lc.includes('only a dispatched `trailhead-grill` report is checked'));
|
|
38
40
|
ok('the paragraph section was found and is bounded', para.length > 1000 && !para.includes('| **Grilling**'));
|
|
39
41
|
|
|
40
42
|
ok('it MUST run on every engine-agent report, the two reviews included', /MUST run on every/.test(para)
|
|
41
43
|
&& para.includes('Code review') && para.includes('Goal-backward verification'));
|
|
42
44
|
ok('it names every technique it covers',
|
|
43
|
-
['plan', 'executor', 'fix', 'debug', 'research', 'codebase-map'].every((t) => para.includes(t)) && lc.includes('every engine-agent report'));
|
|
45
|
+
['plan', 'executor', 'fix', 'debug', 'research', 'codebase-map', 'grill'].every((t) => para.includes(t)) && lc.includes('every engine-agent report'));
|
|
44
46
|
ok('the checker line carries fields and body', para.includes('{"action","verdict","problem","report","fields","body"}'));
|
|
45
47
|
ok('it covers subagent, inline and the lean inline pass', lc.includes('subagent') && lc.includes('inline') && lc.includes('lean inline pass'));
|
|
46
48
|
ok('the checker command with its technique flag and a report file',
|
|
47
|
-
para.includes('node <templates>/trailhead-report-status.js --technique <verify|code-review|plan|executor|fix|debug|research|codebase-map> --file <that dir>/report.md'));
|
|
49
|
+
para.includes('node <templates>/trailhead-report-status.js --technique <verify|code-review|plan|executor|fix|debug|research|codebase-map|grill> --file <that dir>/report.md'));
|
|
48
50
|
ok('the three templates dirs, in order',
|
|
49
51
|
(() => {
|
|
50
52
|
const a = para.indexOf('${CLAUDE_PLUGIN_ROOT}/templates/');
|
|
51
|
-
const b = para.indexOf('
|
|
53
|
+
const b = para.indexOf('${CLAUDE_CONFIG_DIR:-~/.claude}/trailhead/templates/');
|
|
52
54
|
const c = para.indexOf('~/.codex/skills/trailhead/templates/');
|
|
53
55
|
return a !== -1 && b > a && c > b;
|
|
54
56
|
})());
|
|
57
|
+
ok('a background agent is checked straight from its task output file, no temp dir',
|
|
58
|
+
para.includes('--task-output <that output file>') && lc.includes('once the agent has finished') && lc.includes('no temp dir, no copy and no cleanup'));
|
|
59
|
+
ok('the temp-dir route is for any other report', lc.includes('any other report (inline, foreground, or a host without that file)'));
|
|
60
|
+
ok('each step is its own Bash call, never bash -c, never chained',
|
|
61
|
+
lc.includes('its own bash call') && para.includes('never wrapped in `bash -c` / `sh -c`') && lc.includes('never chained')
|
|
62
|
+
&& lc.includes('cannot statically check'));
|
|
55
63
|
ok('the plugin root dir is skipped when unset', lc.includes('skipped when unset'));
|
|
56
64
|
ok('a private temp dir from mktemp -d, once per report', para.includes('`mktemp -d`') && lc.includes('once') && lc.includes('0700'));
|
|
57
65
|
ok('the raw report is written to report.md at the literal printed path with the Write tool',
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test for #333: the in-session trust rule accepts the same labellers as the label guard
|
|
3
|
+
// (github-actions[bot], TRAILHEAD_LABEL_ALLOWLIST logins, write+ collaborators). Plain asserts.
|
|
4
|
+
// Run: node bin/session-trust-docs.test.js
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(__dirname, '..');
|
|
10
|
+
const skills = path.join(root, 'plugins', 'trailhead', 'skills');
|
|
11
|
+
const tw = fs.readFileSync(path.join(skills, '_shared', 'teamwork.md'), 'utf8');
|
|
12
|
+
const inbox = fs.readFileSync(path.join(skills, 'trailhead-chart', 'references', 'inbox.md'), 'utf8');
|
|
13
|
+
const s = tw.indexOf('## Trust & provenance');
|
|
14
|
+
const e = tw.indexOf('**Repo-side enforcement');
|
|
15
|
+
const trust = s < 0 || e < s ? '' : tw.slice(s, e);
|
|
16
|
+
let passed = 0;
|
|
17
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
18
|
+
|
|
19
|
+
ok('teamwork.md has the trust section', trust !== '');
|
|
20
|
+
ok('bot is a trusted labeller', /`github-actions\[bot\]`[^]*trusted/i.test(trust));
|
|
21
|
+
ok('bot why: GITHUB_TOKEN events trigger no workflow', /GITHUB_TOKEN[^]*(trigger|run)s? no workflow[^]*guard/i.test(trust));
|
|
22
|
+
ok('allowlist read command', trust.includes('gh variable get TRAILHEAD_LABEL_ALLOWLIST'));
|
|
23
|
+
ok('allowlist: not found means empty', /not found[^]*(means|->|is) empty/i.test(trust));
|
|
24
|
+
ok('allowlist: other failure means unknown, not empty', /any other failure[^]*unknown/i.test(trust) && /unknown, not empty/i.test(trust));
|
|
25
|
+
ok('allowlist compare: comma-separated, trimmed, case-insensitive', /comma-separated[^]*trimmed[^]*case-insensitive/i.test(trust));
|
|
26
|
+
ok('unknown allowlist: held off frontier, provenance not checkable', /unknown[^]*held? off the frontier[^]*provenance not checkable/i.test(trust));
|
|
27
|
+
ok('never labelled unverified on a guess', /never[^.]*`trailhead:unverified`[^.]*guess/i.test(trust));
|
|
28
|
+
ok('failed permission lookup is unresolved too', /permission lookup (failed|fails)/i.test(trust) && /`404`[^]*real answer/i.test(trust));
|
|
29
|
+
ok('per-label provenance: latest labeled event of every label', /every `trailhead:\*` label[^]*latest `labeled` event[^]*all[^.]*trusted/i.test(trust));
|
|
30
|
+
ok('one trusted label does not vouch for another', /does not vouch for another/i.test(trust));
|
|
31
|
+
ok('still quarantines other non-write labellers', /`read`\/`none`[^]*not trusted/i.test(trust) && trust.includes('`trailhead:unverified`'));
|
|
32
|
+
ok('session and guard agree', /never strips/i.test(trust));
|
|
33
|
+
ok('inbox: session applies unverified, guard strips', /`trailhead:unverified`[^.]*applied by a session[^]*guard Action strips/i.test(inbox));
|
|
34
|
+
ok('inbox: rogue means outside the trusted labellers', /trusted labellers[^]*Trust rule|Trust rule[^]*trusted labellers/i.test(inbox) && /teamwork\.md#trust--provenance/.test(inbox));
|
|
35
|
+
ok('inbox: jq includes quarantined issues', /\| any\(startswith\("trailhead:"\)\) \| not\) or \(\[\.labels\[\]\.name\] \| any\(\. == "trailhead:unverified"\)\)/.test(inbox));
|
|
36
|
+
ok('no em-dash', !tw.includes(String.fromCharCode(0x2014)) && !inbox.includes(String.fromCharCode(0x2014)));
|
|
37
|
+
console.log(`session-trust-docs.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test: un ticket aperto a metà lavoro va sulla mappa corrente (o su quella giusta)
|
|
3
|
+
// se il tema è suo; altrimenti si chiede fra mappe aperte e whiteboard, mai whiteboard
|
|
4
|
+
// in silenzio; dopo la creazione si dice dove è stato aperto. I rimandi puntano al file.
|
|
5
|
+
// Plain asserts, no framework. Run: node bin/surfaced-scope-docs.test.js
|
|
6
|
+
const assert = require('assert');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const root = path.resolve(__dirname, '..');
|
|
11
|
+
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
|
12
|
+
let passed = 0;
|
|
13
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
14
|
+
const EM = String.fromCharCode(0x2014);
|
|
15
|
+
|
|
16
|
+
const SK = 'plugins/trailhead/skills';
|
|
17
|
+
const REF = 'Scope that surfaces while working a ticket';
|
|
18
|
+
|
|
19
|
+
// La regola condivisa
|
|
20
|
+
const rule = read(`${SK}/_shared/surfaced-scope.md`);
|
|
21
|
+
ok('rule: titled after the referenced name', rule.startsWith(`# ${REF}`));
|
|
22
|
+
ok('rule: do it, split it, or capture it', /\*\*`split`\*\* X/.test(rule) && /capture and continue/.test(rule));
|
|
23
|
+
ok('rule: Surfaced from lineage line', /`Surfaced from: /.test(rule));
|
|
24
|
+
ok('rule: current map first when the topic is its own', /1\. \*\*The current map[^\n]*no ask/.test(rule));
|
|
25
|
+
ok('rule: another open map when the topic clearly is its', /2\. \*\*Another open map[^\n]*no ask/.test(rule));
|
|
26
|
+
ok('rule: otherwise ask, listing every open map and the whiteboard', /3\. \*\*Otherwise, ask\.\*\*[^\n]*AskUserQuestion[^\n]*every open map[^\n]*whiteboard/.test(rule));
|
|
27
|
+
ok('rule: never the whiteboard silently while a map is open', /Never route to the whiteboard without this ask while any map is open/.test(rule));
|
|
28
|
+
ok('rule: no map open falls to the whiteboard', /4\. \*\*No map open → whiteboard, no ask\*\*/.test(rule));
|
|
29
|
+
ok('rule: auto defers an ask case instead of guessing', /autonomous run[^\n]*rule 3[^\n]*Pending follow-up guard/.test(rule));
|
|
30
|
+
ok('rule: announce the ticket and where it landed', /## Say what you opened, and where/.test(rule) && /\*\*where it landed\*\*/.test(rule));
|
|
31
|
+
ok('rule: no em-dash', !rule.includes(EM));
|
|
32
|
+
|
|
33
|
+
// Ogni rimando al nome porta al file (nessun rimando orfano)
|
|
34
|
+
const referrers = [
|
|
35
|
+
`${SK}/_shared/session-handoff.md`,
|
|
36
|
+
`${SK}/_shared/techniques/code-review.md`,
|
|
37
|
+
`${SK}/trailhead-work/SKILL.md`,
|
|
38
|
+
`${SK}/trailhead-work/references/ticket-engines.md`,
|
|
39
|
+
];
|
|
40
|
+
for (const rel of referrers) {
|
|
41
|
+
const text = read(rel);
|
|
42
|
+
const lines = text.split('\n').filter((l) => l.includes(REF));
|
|
43
|
+
ok(`${rel}: mentions the rule`, lines.length > 0);
|
|
44
|
+
ok(`${rel}: every mention links surfaced-scope.md`, lines.every((l) => /surfaced-scope\.md/.test(l)));
|
|
45
|
+
ok(`${rel}: no "served by the capture cluster" dangling pointer`, lines.every((l) => !/served by the capture cluster/.test(l)));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Capture e UAT seguono la stessa regola
|
|
49
|
+
const capture = read(`${SK}/trailhead-capture/references/capture.md`);
|
|
50
|
+
// #330: capture.md no longer links the full core; it states the routing inline and
|
|
51
|
+
// capture-core.md carries drift markers on the surfaced-scope.md rules it restates.
|
|
52
|
+
ok('capture: a mid-ticket capture states the surfaced-scope routing inline', /\*\*A capture fired mid-ticket\*\*[^\n]*the current map[^\n]*Surfaced from:/.test(capture));
|
|
53
|
+
ok('capture-core: surfaced-scope.md rules are drift-stamped', /<!-- core: surfaced-scope\.md \|/.test(read(`${SK}/_shared/capture-core.md`)));
|
|
54
|
+
const uat = read(`${SK}/_shared/techniques/acceptance-testing.md`);
|
|
55
|
+
ok('uat: the failing-step follow-up lands per surfaced-scope.md', /On a failing step[^\n]*surfaced-scope\.md/.test(uat));
|
|
56
|
+
|
|
57
|
+
console.log(`surfaced-scope-docs.test.js: ${passed} checks passed`);
|
package/bin/trailhead.js
CHANGED
|
@@ -160,7 +160,9 @@ function readCodexModels(codexHome) {
|
|
|
160
160
|
const globalCfg = readJSON(path.join(codexHome, 'trailhead', 'config.json')) || {};
|
|
161
161
|
const globalCodex = globalCfg.models && typeof globalCfg.models === 'object' && typeof globalCfg.models.codex === 'object' && globalCfg.models.codex ? globalCfg.models.codex : {};
|
|
162
162
|
const projectCodex = projectCfg.models && typeof projectCfg.models === 'object' && typeof projectCfg.models.codex === 'object' && projectCfg.models.codex ? projectCfg.models.codex : {};
|
|
163
|
-
|
|
163
|
+
// #323: una layer con plan ma senza grill implica grill = plan, prima del merge.
|
|
164
|
+
const { withImplicitGrill } = require(path.join(SRC, 'hooks', 'lib', 'model-defaults-review.js'));
|
|
165
|
+
return { ...withImplicitGrill(globalCodex), ...withImplicitGrill(projectCodex) };
|
|
164
166
|
} catch {
|
|
165
167
|
return {};
|
|
166
168
|
}
|
|
@@ -263,7 +265,7 @@ const ENTRYPOINT_FILES = HOOK_FILES.filter((f) => !BASH_GUARD_MODULES.includes(f
|
|
|
263
265
|
// require('./lib/mockup-link.js')). Copied by name, never a recursive
|
|
264
266
|
// sweep: the Claude hooks/lib dir is shared with other plugins, so we must
|
|
265
267
|
// not clobber a co-tenant's lib nor ship our own *.test.js files.
|
|
266
|
-
const HOOK_LIB_FILES = ['commit-message-check.js', 'shell-scan.js', 'gh-subcommand.js', 'body-sources.js', 'unmodelled.js', 'model-defaults-review.js', 'model-defaults.json', 'mockup-link.js'];
|
|
268
|
+
const HOOK_LIB_FILES = ['commit-message-check.js', 'shell-scan.js', 'gh-subcommand.js', 'body-sources.js', 'unmodelled.js', 'model-defaults-review.js', 'model-defaults.json', 'inherit-session.js', 'model-route.js', 'mockup-link.js', 'gh-auth-preflight.js'];
|
|
267
269
|
// search-guard is Claude-Code-specific (#169), and so is mockup-link-guard
|
|
268
270
|
// (it gates the AskUserQuestion tool, which Codex lacks): excluded from both
|
|
269
271
|
// the Codex hook copy and the Codex verify spec's expected hookFiles, so a
|
|
@@ -686,12 +688,12 @@ function installCodex(configDir, { useSymlink }) {
|
|
|
686
688
|
featureManual = true;
|
|
687
689
|
}
|
|
688
690
|
|
|
689
|
-
// Per-technique agent registry: project ALL
|
|
691
|
+
// Per-technique agent registry: project ALL 9 committed agents
|
|
690
692
|
// (readAgentDefs, sourced from plugins/trailhead/agents/*.md) as
|
|
691
693
|
// trailhead-<technique>.toml under the real ~/.codex/agents/ dir, uniformly.
|
|
692
|
-
// A keyed technique (plan/execute/research/review/debug/verify/codebase-map)
|
|
694
|
+
// A keyed technique (plan/execute/research/review/debug/verify/codebase-map/grill)
|
|
693
695
|
// carries its models.codex.<key> pin (merged project + global config) when
|
|
694
|
-
// set, else is pin-less;
|
|
696
|
+
// set, else is pin-less; fix tracks the models.codex.execute pin.
|
|
695
697
|
// Sweep only trailhead's own files first, so a stale pin from a prior
|
|
696
698
|
// install (or a removed technique) never lingers, without touching the
|
|
697
699
|
// user's other custom-agent TOMLs.
|
package/bin/trailhead.test.js
CHANGED
|
@@ -60,6 +60,11 @@ const MODEL_DEFAULTS_LIB_CANDIDATES = [
|
|
|
60
60
|
'../trailhead/hooks/lib/model-defaults-review.js',
|
|
61
61
|
];
|
|
62
62
|
function assertModelDefaultsLibLayout(label, sharedDir) {
|
|
63
|
+
// #323: model-route.js is resolved with the same two-path lookup (grilling.md).
|
|
64
|
+
const routeResolved = MODEL_DEFAULTS_LIB_CANDIDATES
|
|
65
|
+
.map((c) => path.join(sharedDir, c.replace('model-defaults-review.js', 'model-route.js')))
|
|
66
|
+
.find((p) => fs.existsSync(p));
|
|
67
|
+
ok(`${label}: documented model-route.js lib path resolves`, !!routeResolved);
|
|
63
68
|
const resolved = MODEL_DEFAULTS_LIB_CANDIDATES
|
|
64
69
|
.map((c) => path.join(sharedDir, c))
|
|
65
70
|
.find((p) => fs.existsSync(p));
|
|
@@ -234,7 +239,9 @@ ok('codex: trailhead-work/../_shared/substrate.md resolves', fs.existsSync(path.
|
|
|
234
239
|
ok('codex: _shared/load-first.md projected', fs.existsSync(path.join(codexDir, 'skills', '_shared', 'load-first.md')));
|
|
235
240
|
for (const cl of ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage']) {
|
|
236
241
|
ok(`codex: ${cl}/../_shared/load-first.md resolves as a sibling`, fs.existsSync(path.join(codexDir, 'skills', cl, '..', '_shared', 'load-first.md')));
|
|
237
|
-
|
|
242
|
+
// #330: the capture cluster loads the compact capture-core.md instead.
|
|
243
|
+
const want = cl === 'trailhead-capture' ? '../_shared/capture-core.md' : '../_shared/load-first.md';
|
|
244
|
+
ok(`codex: ${cl} SKILL.md references ${want}`, fs.readFileSync(path.join(codexDir, 'skills', cl, 'SKILL.md'), 'utf8').includes(want));
|
|
238
245
|
}
|
|
239
246
|
// Source-level anti-drift: the shared core-list signature line lives ONLY in
|
|
240
247
|
// _shared/load-first.md, never duplicated back into a cluster SKILL.md.
|
|
@@ -293,7 +300,8 @@ ok('source: load-first.md references ../_shared/ticket-language.md', loadFirstSo
|
|
|
293
300
|
const ticketLangAutoSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-work', 'references', 'auto.md'), 'utf8');
|
|
294
301
|
const ticketLangCaptureSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-capture', 'references', 'capture.md'), 'utf8');
|
|
295
302
|
ok('source: auto.md references ticket-language.md', ticketLangAutoSource.includes('ticket-language.md'));
|
|
296
|
-
|
|
303
|
+
// #330: capture.md points at the compact capture-core.md, which is drift-stamped against ticket-language.md.
|
|
304
|
+
ok('source: capture.md references capture-core.md for the ticket language', ticketLangCaptureSource.includes('ticket language rule in `capture-core.md`') && /<!-- core: ticket-language\.md \|/.test(fs.readFileSync(path.join(sourceSkillsDir, '_shared', 'capture-core.md'), 'utf8')));
|
|
297
305
|
// Source-level anti-drift: the substantive decoupling clause lives in EXACTLY
|
|
298
306
|
// one file across the whole skills tree, _shared/ticket-language.md. A global
|
|
299
307
|
// single-occurrence scan (not an enumerated per-file list) so any future doc
|
|
@@ -941,6 +949,10 @@ ok('codex: skills/trailhead/hooks/lib/body-sources.js exists (secret-guard requi
|
|
|
941
949
|
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'body-sources.js')));
|
|
942
950
|
ok('codex: skills/trailhead/hooks/lib/unmodelled.js exists (guards require target, #227)',
|
|
943
951
|
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'unmodelled.js')));
|
|
952
|
+
ok('codex: skills/trailhead/hooks/lib/inherit-session.js exists (required by model-route and model-defaults-review, #323)',
|
|
953
|
+
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'inherit-session.js')));
|
|
954
|
+
ok('codex: skills/trailhead/hooks/lib/model-route.js exists (grilling routing helper, #323)',
|
|
955
|
+
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'model-route.js')));
|
|
944
956
|
ok('codex: skills/trailhead/hooks/lib/model-defaults-review.js exists (check-update require target, #186)',
|
|
945
957
|
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'model-defaults-review.js')));
|
|
946
958
|
ok('codex: skills/trailhead/hooks/lib/model-defaults.json exists (#186)',
|
|
@@ -960,6 +972,8 @@ ok('codex: skills/trailhead/hooks/trailhead-mockup-link-stop.js exists',
|
|
|
960
972
|
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'trailhead-mockup-link-stop.js')));
|
|
961
973
|
ok('codex: skills/trailhead/hooks/lib/mockup-link.js exists (mockup-link-stop require target)',
|
|
962
974
|
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'mockup-link.js')));
|
|
975
|
+
ok('codex: skills/trailhead/hooks/lib/gh-auth-preflight.js exists (check-update require target)',
|
|
976
|
+
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'gh-auth-preflight.js')));
|
|
963
977
|
ok('codex: skills/trailhead/hooks/trailhead-mockup-link-guard.js does NOT exist',
|
|
964
978
|
!fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'trailhead-mockup-link-guard.js')));
|
|
965
979
|
ok('codex: skills/trailhead/hooks/lib/shell-scan.js still exists (secret-read-guard require target)',
|
|
@@ -979,8 +993,15 @@ const noPinFixTomlPath = path.join(codexAgentsDirPath, 'trailhead-fix.toml');
|
|
|
979
993
|
const noPinMapTomlPath = path.join(codexAgentsDirPath, 'trailhead-codebase-map.toml');
|
|
980
994
|
ok('codex: trailhead-fix.toml is projected with no models.codex.* set', fs.existsSync(noPinFixTomlPath));
|
|
981
995
|
ok('codex: trailhead-codebase-map.toml is projected with no models.codex.* set', fs.existsSync(noPinMapTomlPath));
|
|
982
|
-
|
|
983
|
-
|
|
996
|
+
// #325: fix non ha chiave propria, porta lo stesso pin di execute (la config del
|
|
997
|
+
// repo puo' impostarlo, quindi si confrontano le due righe model, qualunque sia).
|
|
998
|
+
const modelLine325 = (f) => (fs.readFileSync(f, 'utf8').match(/^model = .*$/m) || [''])[0];
|
|
999
|
+
const effortLine325 = (f) => (fs.readFileSync(f, 'utf8').match(/^model_reasoning_effort = .*$/m) || [''])[0];
|
|
1000
|
+
const executeTomlPath325 = path.join(codexAgentsDirPath, 'trailhead-execute.toml');
|
|
1001
|
+
ok('codex: trailhead-fix.toml carries the same model pin as trailhead-execute.toml (fix tracks execute, #325)',
|
|
1002
|
+
fs.existsSync(noPinFixTomlPath) && fs.existsSync(executeTomlPath325) &&
|
|
1003
|
+
modelLine325(noPinFixTomlPath) === modelLine325(executeTomlPath325) &&
|
|
1004
|
+
effortLine325(noPinFixTomlPath) === effortLine325(executeTomlPath325));
|
|
984
1005
|
|
|
985
1006
|
assertModelDefaultsLibLayout('codex copy', path.join(codexDir, 'skills', '_shared'));
|
|
986
1007
|
|
|
@@ -1011,7 +1032,7 @@ ok('codex copy: templates is a regular dir (not a symlink)',
|
|
|
1011
1032
|
assertModelDefaultsLibLayout('codex symlink', path.join(codexSymDir, 'skills', '_shared'));
|
|
1012
1033
|
|
|
1013
1034
|
// --- codex agent TOML projection (#38, #88) -------------------------------------
|
|
1014
|
-
// With NO models.codex.* set, a codex install still projects all
|
|
1035
|
+
// With NO models.codex.* set, a codex install still projects all 9 agents
|
|
1015
1036
|
// uniformly, pin-less (uniform projection, #88), never zero. Install from an
|
|
1016
1037
|
// isolated cwd whose .trailhead/config.json sets no models.codex.*, not
|
|
1017
1038
|
// repoRoot (whose own config now pins models.codex.*, so it would project
|
|
@@ -1024,11 +1045,11 @@ fs.writeFileSync(
|
|
|
1024
1045
|
);
|
|
1025
1046
|
const unsetCodexHomeDir = mktmp();
|
|
1026
1047
|
runInstaller([`--codex`, `--dir=${unsetCodexHomeDir}`], { cwd: unsetProjectDir });
|
|
1027
|
-
ok('codex: all
|
|
1048
|
+
ok('codex: all 9 trailhead-*.toml exist under agents/ even with models.codex.* unset', (() => {
|
|
1028
1049
|
const dir = path.join(unsetCodexHomeDir, 'agents');
|
|
1029
1050
|
if (!fs.existsSync(dir)) return false;
|
|
1030
1051
|
const tomls = fs.readdirSync(dir).filter((f) => /^trailhead-.*\.toml$/.test(f));
|
|
1031
|
-
return tomls.length ===
|
|
1052
|
+
return tomls.length === 9;
|
|
1032
1053
|
})());
|
|
1033
1054
|
ok('codex: every trailhead-*.toml is pin-less when models.codex.* is unset', (() => {
|
|
1034
1055
|
const dir = path.join(unsetCodexHomeDir, 'agents');
|
|
@@ -1037,7 +1058,7 @@ ok('codex: every trailhead-*.toml is pin-less when models.codex.* is unset', (()
|
|
|
1037
1058
|
})());
|
|
1038
1059
|
|
|
1039
1060
|
// #250: the reviewing block (single source scripts/agent-blocks.md) is projected
|
|
1040
|
-
// verbatim into the two reviewing agents' TOMLs and into none of the other
|
|
1061
|
+
// verbatim into the two reviewing agents' TOMLs and into none of the other seven.
|
|
1041
1062
|
{
|
|
1042
1063
|
const sync250 = require(path.join(repoRoot, 'scripts', 'sync-agent-blocks.js'));
|
|
1043
1064
|
const reviewingBlock250 = sync250.parseSource(fs.readFileSync(path.join(repoRoot, 'scripts', 'agent-blocks.md'), 'utf8')).reviewing;
|
|
@@ -1045,7 +1066,7 @@ ok('codex: every trailhead-*.toml is pin-less when models.codex.* is unset', (()
|
|
|
1045
1066
|
const agentsDir250 = path.join(unsetCodexHomeDir, 'agents');
|
|
1046
1067
|
const reviewingNames250 = new Set(['trailhead-verify', 'trailhead-review']); // the code-review agent projects as trailhead-review.toml
|
|
1047
1068
|
const tomls250 = fs.readdirSync(agentsDir250).filter((f) => /^trailhead-.*\.toml$/.test(f));
|
|
1048
|
-
ok('#250: all
|
|
1069
|
+
ok('#250: all 9 TOMLs are checked', tomls250.length === 9);
|
|
1049
1070
|
for (const f of tomls250) {
|
|
1050
1071
|
const name = f.replace(/\.toml$/, '');
|
|
1051
1072
|
const text = fs.readFileSync(path.join(agentsDir250, f), 'utf8');
|
|
@@ -1108,7 +1129,8 @@ ok('claude: cluster ../_shared/ reference resolves', fs.existsSync(path.join(cla
|
|
|
1108
1129
|
ok('claude: skills/_shared/load-first.md exists', fs.existsSync(path.join(claudeDir, 'skills', '_shared', 'load-first.md')));
|
|
1109
1130
|
ok('claude: trailhead-work/../_shared/load-first.md resolves', fs.existsSync(path.join(claudeDir, 'skills', 'trailhead-work', '..', '_shared', 'load-first.md')));
|
|
1110
1131
|
for (const cl of ['trailhead', 'trailhead-chart', 'trailhead-work', 'trailhead-view', 'trailhead-capture', 'trailhead-manage']) {
|
|
1111
|
-
|
|
1132
|
+
const want = cl === 'trailhead-capture' ? '../_shared/capture-core.md' : '../_shared/load-first.md';
|
|
1133
|
+
ok(`claude: ${cl} SKILL.md references ${want}`, fs.readFileSync(path.join(claudeDir, 'skills', cl, 'SKILL.md'), 'utf8').includes(want));
|
|
1112
1134
|
}
|
|
1113
1135
|
|
|
1114
1136
|
// #146: single-sourced ticket-language directive, projected on Claude too.
|
|
@@ -1128,6 +1150,10 @@ ok('claude: hooks/lib/body-sources.js exists (secret-guard require target, #210)
|
|
|
1128
1150
|
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'body-sources.js')));
|
|
1129
1151
|
ok('claude: hooks/lib/unmodelled.js exists (guards require target, #227)',
|
|
1130
1152
|
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'unmodelled.js')));
|
|
1153
|
+
ok('claude: hooks/lib/inherit-session.js exists (required by model-route and model-defaults-review, #323)',
|
|
1154
|
+
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'inherit-session.js')));
|
|
1155
|
+
ok('claude: hooks/lib/model-route.js exists (grilling routing helper, #323)',
|
|
1156
|
+
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'model-route.js')));
|
|
1131
1157
|
ok('claude: hooks/lib/model-defaults-review.js exists (check-update require target, #186)',
|
|
1132
1158
|
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'model-defaults-review.js')));
|
|
1133
1159
|
ok('claude: hooks/lib/model-defaults.json exists (#186)',
|
|
@@ -1185,7 +1211,7 @@ ok('claude: SKILL.md does not start with the codex adapter header', !fs.readFile
|
|
|
1185
1211
|
// falls back to running inline on the session model, so the per-technique
|
|
1186
1212
|
// config.models.* split silently never applies.
|
|
1187
1213
|
const claudeAgentsDir = path.join(claudeDir, 'agents');
|
|
1188
|
-
const expectedAgents = ['trailhead-plan', 'trailhead-executor', 'trailhead-research', 'trailhead-code-review', 'trailhead-debug', 'trailhead-fix', 'trailhead-codebase-map'];
|
|
1214
|
+
const expectedAgents = ['trailhead-plan', 'trailhead-executor', 'trailhead-research', 'trailhead-code-review', 'trailhead-debug', 'trailhead-fix', 'trailhead-codebase-map', 'trailhead-grill'];
|
|
1189
1215
|
for (const a of expectedAgents) {
|
|
1190
1216
|
ok(`claude: agents/${a}.md placed`, fs.existsSync(path.join(claudeAgentsDir, `${a}.md`)));
|
|
1191
1217
|
}
|
|
@@ -1315,6 +1341,7 @@ ok('claude: hooks/trailhead-bash-guards.js and its six modules exist', ['bash-gu
|
|
|
1315
1341
|
// Prototype "link in the same message as the approval ask" rule.
|
|
1316
1342
|
ok('claude: hooks/trailhead-mockup-link-guard.js exists', fs.existsSync(path.join(claudeDir, 'hooks', 'trailhead-mockup-link-guard.js')));
|
|
1317
1343
|
ok('claude: hooks/lib/mockup-link.js exists (mockup-link-guard require target)', fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'mockup-link.js')));
|
|
1344
|
+
ok('claude: hooks/lib/gh-auth-preflight.js exists (check-update require target)', fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'gh-auth-preflight.js')));
|
|
1318
1345
|
ok('claude: hooks/trailhead-mockup-link-stop.js does NOT exist (Codex-only twin)', !fs.existsSync(path.join(claudeDir, 'hooks', 'trailhead-mockup-link-stop.js')));
|
|
1319
1346
|
ok('claude: settings.json does NOT register mockup-link-stop', !settingsContent.includes('trailhead-mockup-link-stop.js'));
|
|
1320
1347
|
ok('claude: settings.json registers mockup-link-guard under PreToolUse/AskUserQuestion',
|
|
@@ -1390,6 +1417,21 @@ const pinConfigTomlPath = path.join(pinCodexHomeDir, 'config.toml');
|
|
|
1390
1417
|
ok('codex: config.toml enables multi_agent_v2 when a models.codex.* pin is projected',
|
|
1391
1418
|
fs.existsSync(pinConfigTomlPath) && fs.readFileSync(pinConfigTomlPath, 'utf8').includes('multi_agent_v2 = true'));
|
|
1392
1419
|
|
|
1420
|
+
// #323: a project layer that sets plan but not grill grills on that plan, even when
|
|
1421
|
+
// the global layer pins grill (the implicit-grill rule applies per layer, before the merge).
|
|
1422
|
+
{
|
|
1423
|
+
const pd = mktmp();
|
|
1424
|
+
fs.mkdirSync(path.join(pd, '.trailhead'), { recursive: true });
|
|
1425
|
+
fs.writeFileSync(path.join(pd, '.trailhead', 'config.json'), JSON.stringify({ models: { codex: { plan: 'gpt-5.6-sol' } } }));
|
|
1426
|
+
const ch = mktmp();
|
|
1427
|
+
fs.mkdirSync(path.join(ch, 'trailhead'), { recursive: true });
|
|
1428
|
+
fs.writeFileSync(path.join(ch, 'trailhead', 'config.json'), JSON.stringify({ models: { codex: { grill: 'gpt-5.6-luna' } } }));
|
|
1429
|
+
runInstaller([`--codex`, `--dir=${ch}`], { cwd: pd });
|
|
1430
|
+
const grillToml = fs.readFileSync(path.join(ch, 'agents', 'trailhead-grill.toml'), 'utf8');
|
|
1431
|
+
ok('codex: project plan with no project grill beats a global grill pin in trailhead-grill.toml',
|
|
1432
|
+
grillToml.includes('model = "gpt-5.6-sol"') && !grillToml.includes('gpt-5.6-luna'));
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1393
1435
|
// Uninstall must sweep the projected TOML too, without a --dir= cwd dependency.
|
|
1394
1436
|
runInstaller([`--codex`, `--dir=${pinCodexHomeDir}`, '--uninstall']);
|
|
1395
1437
|
ok('codex uninstall: trailhead-execute.toml is gone', !fs.existsSync(pinnedTomlPath));
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test for #322: resolving any ticket (map or whiteboard, via work or quick) unblocks
|
|
3
|
+
// its dependents, as a step independent of the map-only `Decisions so far` book-keeping.
|
|
4
|
+
// Plain asserts, no framework. Run: node bin/unblock-docs.test.js
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const skills = path.resolve(__dirname, '..', 'plugins', 'trailhead', 'skills');
|
|
10
|
+
const read = (p) => fs.readFileSync(path.join(skills, p), 'utf8');
|
|
11
|
+
const engines = read('trailhead-work/references/ticket-engines.md');
|
|
12
|
+
const work = read('trailhead-work/SKILL.md');
|
|
13
|
+
const commands = read('_shared/substrate-commands.md');
|
|
14
|
+
const HELPER = 'trailhead-unblock.js --issue <n> --repo <owner/name>';
|
|
15
|
+
let passed = 0;
|
|
16
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
17
|
+
|
|
18
|
+
const resolveLine = (section) => {
|
|
19
|
+
const start = engines.indexOf(section);
|
|
20
|
+
const m = /^5\. \*\*Resolve\*\*.*$/m.exec(engines.slice(start));
|
|
21
|
+
return m ? m[0] : '';
|
|
22
|
+
};
|
|
23
|
+
const unblockClause = (line) => {
|
|
24
|
+
const i = line.indexOf('**Unblock dependents');
|
|
25
|
+
if (i < 0) return '';
|
|
26
|
+
// the clause ends at the next bold step (the handoff), not at a fixed window
|
|
27
|
+
const end = line.indexOf('**Session handoff**', i);
|
|
28
|
+
return line.slice(i, end < 0 ? undefined : end);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
for (const section of ['## `build`', '## `bug`']) {
|
|
32
|
+
const line = resolveLine(section);
|
|
33
|
+
const clause = unblockClause(line);
|
|
34
|
+
ok(`engines ${section}: Resolve has an Unblock dependents clause`, clause !== '');
|
|
35
|
+
ok(`engines ${section}: names the helper`, clause.includes(HELPER));
|
|
36
|
+
ok(`engines ${section}: applies to map and whiteboard tickets alike`, /whiteboard/.test(clause) && /map/.test(clause));
|
|
37
|
+
ok(`engines ${section}: keeps the by-hand fallback`, clause.includes('--remove-label "trailhead:blocked"'));
|
|
38
|
+
ok(`engines ${section}: independent of the map-only Decisions so far update`, /independent of the map-only `Decisions so far` update/.test(clause));
|
|
39
|
+
ok(`engines ${section}: runs right after the close`, /^\*\*Unblock dependents[^*]*\*\*\s*Right after the close/.test(clause) && line.indexOf('gh issue close') < line.indexOf('**Unblock dependents'));
|
|
40
|
+
ok(`engines ${section}: fallback names the blocked_by endpoint`, clause.includes('dependencies/blocked_by'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const step5 = (/^5\. Record the resolution.*$/m.exec(work) || [''])[0];
|
|
44
|
+
ok('SKILL.md step 5: unblock is its own sentence naming the helper', /\.\s+Unblocking is a separate step[^]*\*\*unblock dependents\*\*/.test(step5) && step5.includes('trailhead-unblock.js'));
|
|
45
|
+
ok('SKILL.md step 5: holds for whiteboard tickets too', /whiteboard/.test(step5));
|
|
46
|
+
|
|
47
|
+
const bullet = (/^- \*\*No map narrative book-keeping\.\*\*.*$/m.exec(work) || [''])[0];
|
|
48
|
+
ok('SKILL.md quick bullet exists', bullet !== '');
|
|
49
|
+
ok('SKILL.md quick bullet: unblocking is among the steps that still hold', /Everything else holds:[^.]*unblocking dependents/.test(bullet) && bullet.includes('trailhead-unblock.js'));
|
|
50
|
+
ok('SKILL.md quick bullet: no map-only exception', !bullet.includes('Exception, `quick <n>` on a *map* ticket'));
|
|
51
|
+
|
|
52
|
+
ok('substrate-commands.md: unblock block names the helper as default', /Unblock:[\s\S]{0,400}trailhead-unblock\.js --issue <n> --repo <owner\/name>/.test(commands));
|
|
53
|
+
ok('substrate-commands.md: keeps the raw gh command as fallback', /Unblock:[\s\S]{0,700}gh issue edit <(?:n|dep)> --remove-label "trailhead:blocked"/.test(commands));
|
|
54
|
+
ok('substrate-commands.md: cookbook comment names the helper', /# unblock[^\n]*trailhead-unblock\.js/.test(commands));
|
|
55
|
+
ok('no em-dash', ![engines, work, commands].some((s) => s.includes(String.fromCharCode(0x2014))));
|
|
56
|
+
console.log(`unblock-docs.test.js: ${passed} checks passed`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marcomigozzi/trailhead",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Start and drive large projects as a map of tickets on GitHub Issues. Map-based onboarding + a discuss→plan→execute→verify engine, all on the Issues. Installs the trailhead skill, commands, and hooks into your agent's config dir.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"trailhead": "bin/trailhead.js"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trailhead",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Start and drive large projects as a map of tickets on GitHub Issues, resolving one at a time. Map-based onboarding + a discuss→plan→execute→verify engine with atomic commits, all on the Issues. Ships as a dispatcher plus cohesion-clustered skills over a shared core; depends on nothing beyond an authenticated gh CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Marco Migozzi",
|
|
@@ -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 -->
|