@marcomigozzi/trailhead 0.16.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 +10 -8
- package/README.md +10 -8
- 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/lib/codex-projection.js +8 -1
- package/bin/lib/codex-projection.test.js +23 -0
- 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/report-status-docs.test.js +1 -1
- 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/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/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 +22 -1
- package/plugins/trailhead/hooks/lib/unmodelled.js +6 -1
- package/plugins/trailhead/hooks/mod/README.md +8 -3
- package/plugins/trailhead/hooks/mod/map-pane.tsx +52 -28
- 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/configuration-reference.md +2 -2
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +3 -3
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -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-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +4 -3
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +3 -3
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +1 -0
- package/plugins/trailhead/templates/statusline-trailhead.sh +3 -3
- package/plugins/trailhead/templates/trailhead-report-status.js +67 -3
- package/plugins/trailhead/tests/map-pane-ui.test.ts +179 -2
|
@@ -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`);
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marcomigozzi/trailhead",
|
|
3
|
-
"version": "0.16.
|
|
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.16.
|
|
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 -->
|
|
@@ -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 -->
|
|
@@ -17,6 +17,25 @@ const DATA_FILE = path.join(__dirname, 'model-defaults.json');
|
|
|
17
17
|
const HOSTS = ['claude', 'codex'];
|
|
18
18
|
const TIERS = ['strong', 'standard', 'fast'];
|
|
19
19
|
|
|
20
|
+
// Host: la lib viene copiata byte-per-byte su Codex (sotto
|
|
21
|
+
// <codexHome>/skills/trailhead/hooks/lib), quindi l'host si deduce dal path
|
|
22
|
+
// installato, senza env (stessa idea di trailhead-check-update.js). Un
|
|
23
|
+
// argomento `host` esplicito sovrascrive la rilevazione (test, host futuri).
|
|
24
|
+
const CODEX_LIB_RE = /[\\/]skills[\\/]trailhead[\\/]hooks[\\/]lib$/;
|
|
25
|
+
|
|
26
|
+
function detectHost(dir = __dirname) {
|
|
27
|
+
return CODEX_LIB_RE.test(dir) ? 'codex' : 'claude';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeHost(host) {
|
|
31
|
+
return HOSTS.includes(host) ? host : detectHost();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Forma del comando per host: `/trailhead:<verb>` su Claude, `$trailhead <verb>` su Codex.
|
|
35
|
+
function commandForm(host, verb) {
|
|
36
|
+
return normalizeHost(host) === 'codex' ? `$trailhead ${verb}` : `/trailhead:${verb}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
20
39
|
function isPlainObject(v) {
|
|
21
40
|
return !!v && typeof v === 'object' && !Array.isArray(v);
|
|
22
41
|
}
|
|
@@ -187,7 +206,7 @@ function effectiveModels(layers) {
|
|
|
187
206
|
return { config: { models: outModels }, sources };
|
|
188
207
|
}
|
|
189
208
|
|
|
190
|
-
function reviewOffer({ config, data, ack, sources }) {
|
|
209
|
+
function reviewOffer({ config, data, ack, sources, host }) {
|
|
191
210
|
const current = currentEntry(data);
|
|
192
211
|
if (!current) return null;
|
|
193
212
|
const stale = stalePins(config, data).map((s) => {
|
|
@@ -196,7 +215,7 @@ function reviewOffer({ config, data, ack, sources }) {
|
|
|
196
215
|
});
|
|
197
216
|
if (!stale.length) return null;
|
|
198
217
|
if (ack && !semverLt(ack, current.since)) return null;
|
|
199
|
-
return { since: current.since, stale, notice: reviewNotice(stale, current.since) };
|
|
218
|
+
return { since: current.since, stale, notice: reviewNotice(stale, current.since, host) };
|
|
200
219
|
}
|
|
201
220
|
|
|
202
221
|
function sourceSuffix(source) {
|
|
@@ -205,7 +224,7 @@ function sourceSuffix(source) {
|
|
|
205
224
|
return ` (new default; from ${layerName}, ${source.file})`;
|
|
206
225
|
}
|
|
207
226
|
|
|
208
|
-
function reviewNotice(stale, since) {
|
|
227
|
+
function reviewNotice(stale, since, host) {
|
|
209
228
|
const lines = stale.map((s) => `- ${s.key}: ${s.from} -> ${s.to}${sourceSuffix(s.source)}`);
|
|
210
229
|
return [
|
|
211
230
|
`trailhead's default models changed in ${since}. The following pinned model(s) are older than the new default:`,
|
|
@@ -214,7 +233,7 @@ function reviewNotice(stale, since) {
|
|
|
214
233
|
'Offer the user, once, without blocking their current request, three choices:',
|
|
215
234
|
'1) update those keys to the new defaults (edit each key in the config file it comes from, with exactly the values above; keep any codex effort unchanged),',
|
|
216
235
|
'2) keep them as they are (nothing to do; trailhead will not ask again for these defaults once acknowledged),',
|
|
217
|
-
|
|
236
|
+
`3) open ${commandForm(host, 'config')} to pick per key.`,
|
|
218
237
|
'This offer reappears every time it is checked until acknowledged: run the ack instruction below only after the user has picked one of the three choices above.',
|
|
219
238
|
].join('\n');
|
|
220
239
|
}
|
|
@@ -231,25 +250,33 @@ function ackInstruction(root) {
|
|
|
231
250
|
const ACK_FILE = path.join('.trailhead', 'model-defaults-ack');
|
|
232
251
|
const CONFIG_FILE = path.join('.trailhead', 'config.json');
|
|
233
252
|
|
|
234
|
-
// Dir base della config globale:
|
|
235
|
-
|
|
253
|
+
// Dir base della config globale. Claude: CLAUDE_CONFIG_DIR o ~/.claude (come
|
|
254
|
+
// trailhead-check-update.js).
|
|
255
|
+
// Su Codex e' <codexHome>, ricavato dal path installato della lib (4 livelli
|
|
256
|
+
// sopra __dirname: <codexHome>/skills/trailhead/hooks/lib); con host forzato
|
|
257
|
+
// fuori da un'installazione Codex ripiega su CODEX_HOME o ~/.codex.
|
|
258
|
+
function globalConfigDir(host) {
|
|
259
|
+
if (normalizeHost(host) === 'codex') {
|
|
260
|
+
if (detectHost() === 'codex') return path.resolve(__dirname, '..', '..', '..', '..');
|
|
261
|
+
return process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
262
|
+
}
|
|
236
263
|
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
237
264
|
}
|
|
238
265
|
|
|
239
|
-
function globalConfigFile() {
|
|
240
|
-
return path.join(globalConfigDir(), 'trailhead', 'config.json');
|
|
266
|
+
function globalConfigFile(host) {
|
|
267
|
+
return path.join(globalConfigDir(host), 'trailhead', 'config.json');
|
|
241
268
|
}
|
|
242
269
|
|
|
243
270
|
// L'ack globale vive accanto al config.json globale, senza nidificare
|
|
244
271
|
// ".trailhead" (che e' una convenzione solo di progetto).
|
|
245
|
-
function globalAckFile() {
|
|
246
|
-
return path.join(globalConfigDir(), 'trailhead', 'model-defaults-ack');
|
|
272
|
+
function globalAckFile(host) {
|
|
273
|
+
return path.join(globalConfigDir(host), 'trailhead', 'model-defaults-ack');
|
|
247
274
|
}
|
|
248
275
|
|
|
249
276
|
// Mancante o malformato -> {}: non deve mai bloccare la review di progetto.
|
|
250
|
-
function loadGlobalConfig() {
|
|
277
|
+
function loadGlobalConfig(host) {
|
|
251
278
|
try {
|
|
252
|
-
const raw = JSON.parse(fs.readFileSync(globalConfigFile(), 'utf8'));
|
|
279
|
+
const raw = JSON.parse(fs.readFileSync(globalConfigFile(host), 'utf8'));
|
|
253
280
|
return isPlainObject(raw) ? raw : {};
|
|
254
281
|
} catch {
|
|
255
282
|
return {};
|
|
@@ -296,13 +323,13 @@ function writeAck(root, since) {
|
|
|
296
323
|
// non arriva un ack esplicito via ack(cwd). Valuta i pin EFFETTIVI: la
|
|
297
324
|
// config globale unita sotto quella di progetto (progetto vince chiave per
|
|
298
325
|
// chiave); senza un progetto sopra cwd, valuta la sola config globale.
|
|
299
|
-
function check(cwd) {
|
|
326
|
+
function check(cwd, { host } = {}) {
|
|
300
327
|
try {
|
|
301
328
|
const resolvedCwd = path.resolve(cwd || '.');
|
|
302
329
|
const root = findProjectRoot(cwd);
|
|
303
330
|
const data = loadData();
|
|
304
|
-
const globalFile = globalConfigFile();
|
|
305
|
-
const globalLayer = { layer: 'global', file: globalFile, config: loadGlobalConfig() };
|
|
331
|
+
const globalFile = globalConfigFile(host);
|
|
332
|
+
const globalLayer = { layer: 'global', file: globalFile, config: loadGlobalConfig(host) };
|
|
306
333
|
|
|
307
334
|
if (root) {
|
|
308
335
|
let projectConfig;
|
|
@@ -318,7 +345,7 @@ function check(cwd) {
|
|
|
318
345
|
};
|
|
319
346
|
const { config, sources } = effectiveModels([globalLayer, projectLayer]);
|
|
320
347
|
const ack = readAck(root);
|
|
321
|
-
const offer = reviewOffer({ config, data, ack, sources });
|
|
348
|
+
const offer = reviewOffer({ config, data, ack, sources, host });
|
|
322
349
|
if (!offer) return null;
|
|
323
350
|
return `${offer.notice}\n${ackInstruction(root)}`;
|
|
324
351
|
}
|
|
@@ -326,8 +353,8 @@ function check(cwd) {
|
|
|
326
353
|
// Nessun progetto sopra cwd: valuta la sola config globale, se esiste.
|
|
327
354
|
if (!fs.existsSync(globalFile)) return null;
|
|
328
355
|
const { config, sources } = effectiveModels([globalLayer]);
|
|
329
|
-
const ack = readAckFile(globalAckFile());
|
|
330
|
-
const offer = reviewOffer({ config, data, ack, sources });
|
|
356
|
+
const ack = readAckFile(globalAckFile(host));
|
|
357
|
+
const offer = reviewOffer({ config, data, ack, sources, host });
|
|
331
358
|
if (!offer) return null;
|
|
332
359
|
return `${offer.notice}\n${ackInstruction(resolvedCwd)}`;
|
|
333
360
|
} catch {
|
|
@@ -341,7 +368,7 @@ function check(cwd) {
|
|
|
341
368
|
// globale -> ack globale. True se un target e' stato trovato e la scrittura
|
|
342
369
|
// tentata (un fallimento silenzioso non e' riportato, comportamento
|
|
343
370
|
// invariato); false se nessun target esiste. Non lancia mai eccezioni.
|
|
344
|
-
function ack(cwd) {
|
|
371
|
+
function ack(cwd, { host } = {}) {
|
|
345
372
|
try {
|
|
346
373
|
const root = findProjectRoot(cwd);
|
|
347
374
|
const data = loadData();
|
|
@@ -351,8 +378,8 @@ function ack(cwd) {
|
|
|
351
378
|
writeAck(root, current.since);
|
|
352
379
|
return true;
|
|
353
380
|
}
|
|
354
|
-
if (fs.existsSync(globalConfigFile())) {
|
|
355
|
-
writeAckFile(globalAckFile(), current.since);
|
|
381
|
+
if (fs.existsSync(globalConfigFile(host))) {
|
|
382
|
+
writeAckFile(globalAckFile(host), current.since);
|
|
356
383
|
return true;
|
|
357
384
|
}
|
|
358
385
|
return false;
|
|
@@ -370,6 +397,8 @@ module.exports = {
|
|
|
370
397
|
effectiveModels,
|
|
371
398
|
reviewOffer,
|
|
372
399
|
reviewNotice,
|
|
400
|
+
detectHost,
|
|
401
|
+
commandForm,
|
|
373
402
|
findProjectRoot,
|
|
374
403
|
globalConfigDir,
|
|
375
404
|
globalConfigFile,
|
|
@@ -480,6 +480,56 @@ ok('stalePins: a non-object models.codex yields no codex pins (not an exception)
|
|
|
480
480
|
ackOut === '' && fs.existsSync(path.join(globalDir, 'trailhead', 'model-defaults-ack')));
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
// --- host-aware command form e global config (Codex) --------------------
|
|
484
|
+
{
|
|
485
|
+
const stale = [{ key: 'models.plan', from: 'claude-opus-4-8', to: 'claude-opus-5-5' }];
|
|
486
|
+
const codexNotice = reviewNotice(stale, cur.since, 'codex');
|
|
487
|
+
const claudeNotice = reviewNotice(stale, cur.since, 'claude');
|
|
488
|
+
ok('reviewNotice(codex) names $trailhead config', codexNotice.includes('open $trailhead config'));
|
|
489
|
+
ok('reviewNotice(codex) never names the Claude slash form', !codexNotice.includes('/trailhead:'));
|
|
490
|
+
ok('reviewNotice(claude) names /trailhead:config', claudeNotice.includes('open /trailhead:config'));
|
|
491
|
+
ok('reviewNotice default host is claude outside an installed Codex path',
|
|
492
|
+
reviewNotice(stale, cur.since).includes('/trailhead:config'));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
{
|
|
496
|
+
// Lib copiata sotto un finto <codexHome>/skills/trailhead/hooks/lib/: l'host
|
|
497
|
+
// si deduce dal path, senza env, e la config globale e' <codexHome>/trailhead/.
|
|
498
|
+
const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), 'trailhead-mdr-codexhome-'));
|
|
499
|
+
const libDir = path.join(codexHome, 'skills', 'trailhead', 'hooks', 'lib');
|
|
500
|
+
fs.mkdirSync(libDir, { recursive: true });
|
|
501
|
+
for (const f of ['model-defaults-review.js', 'model-defaults.json']) {
|
|
502
|
+
fs.copyFileSync(path.join(__dirname, f), path.join(libDir, f));
|
|
503
|
+
}
|
|
504
|
+
fs.mkdirSync(path.join(codexHome, 'trailhead'), { recursive: true });
|
|
505
|
+
fs.writeFileSync(path.join(codexHome, 'trailhead', 'config.json'), JSON.stringify({ models: { plan: 'claude-opus-4-8' } }));
|
|
506
|
+
const noProjDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trailhead-mdr-codex-noproj-'));
|
|
507
|
+
const copied = path.join(libDir, 'model-defaults-review.js');
|
|
508
|
+
|
|
509
|
+
const out = execFileSync('node', [copied, 'check', noProjDir], {
|
|
510
|
+
encoding: 'utf8',
|
|
511
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: BASE_CONFIG_DIR, CODEX_HOME: '' },
|
|
512
|
+
});
|
|
513
|
+
ok('Codex path: check() honours <codexHome>/trailhead/config.json', out.includes('models.plan: claude-opus-4-8 -> claude-opus-5-5'));
|
|
514
|
+
ok('Codex path: the notice offers $trailhead config', out.includes('open $trailhead config'));
|
|
515
|
+
ok('Codex path: the notice never names /trailhead:', !out.includes('/trailhead:'));
|
|
516
|
+
ok('Codex path: the notice tags the Codex global config path',
|
|
517
|
+
out.includes(path.join(codexHome, 'trailhead', 'config.json')));
|
|
518
|
+
|
|
519
|
+
execFileSync('node', [copied, 'ack', noProjDir], {
|
|
520
|
+
encoding: 'utf8',
|
|
521
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: BASE_CONFIG_DIR, CODEX_HOME: '' },
|
|
522
|
+
});
|
|
523
|
+
ok('Codex path: the global ack lands under <codexHome>/trailhead/',
|
|
524
|
+
fs.existsSync(path.join(codexHome, 'trailhead', 'model-defaults-ack')));
|
|
525
|
+
|
|
526
|
+
const mod = require(copied);
|
|
527
|
+
ok('Codex path: globalConfigDir resolves to the codexHome', mod.globalConfigDir() === codexHome);
|
|
528
|
+
ok('Codex path: check() is silenced after the Codex global ack', mod.check(noProjDir) === null);
|
|
529
|
+
ok('Claude behaviour unchanged: globalConfigDir still follows CLAUDE_CONFIG_DIR',
|
|
530
|
+
globalConfigDir() === BASE_CONFIG_DIR);
|
|
531
|
+
}
|
|
532
|
+
|
|
483
533
|
console.log(`model-defaults-review.test.js: ${passed} assertions passed`);
|
|
484
534
|
|
|
485
535
|
if (PRIOR_CLAUDE_CONFIG_DIR === undefined) {
|
|
@@ -16,13 +16,15 @@ const fs = require('fs');
|
|
|
16
16
|
const os = require('os');
|
|
17
17
|
|
|
18
18
|
const scriptPath = path.resolve(__dirname, '..', '..', 'templates', 'trailhead-report-status.js');
|
|
19
|
-
const { checkReport, TOKENS, CONTRACTS, FAMILY_OF } = require(scriptPath);
|
|
19
|
+
const { checkReport, TOKENS, CONTRACTS, FAMILY_OF, VERIFY_CLASSES } = require(scriptPath);
|
|
20
20
|
const techDir = path.resolve(__dirname, '..', '..', 'skills', '_shared', 'techniques');
|
|
21
21
|
|
|
22
22
|
let passed = 0;
|
|
23
23
|
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
24
24
|
|
|
25
25
|
const K = 'critical: 0 · warning: 1 · info: 2'; // the counts line
|
|
26
|
+
const K1 = 'critical: 1 · warning: 0 · info: 0';
|
|
27
|
+
const K2 = 'critical: 2 · warning: 1 · info: 0';
|
|
26
28
|
const V = 'verify';
|
|
27
29
|
const C = 'code-review';
|
|
28
30
|
const KEYS = ['action', 'verdict', 'problem', 'report', 'fields', 'body'];
|
|
@@ -119,6 +121,60 @@ const rows = [
|
|
|
119
121
|
['two counts lines', C, `status: clean\n${K}\nbody\n${K}\n`, ['reask', null, 'counts']],
|
|
120
122
|
['two different counts lines', C, `status: clean\n${K}\ncritical: 1 · warning: 1 · info: 2\n`, ['reask', null, 'counts']],
|
|
121
123
|
['counts only inside a code fence', C, `status: clean\n\`\`\`\n${K}\n\`\`\`\n`, ['reask', null, 'counts']],
|
|
124
|
+
|
|
125
|
+
// --- verdict vs content: code-review fails closed on an unclosed Critical ---
|
|
126
|
+
['clean with critical 2 and no ledger (ticket repro)', C, `status: clean\n${K2}\n\n- critical: SQL injection\n`,
|
|
127
|
+
['normalise', 'issues_found', null, `status: issues_found\n${K2}\n\n- critical: SQL injection\n`]],
|
|
128
|
+
['clean, critical 1, C1 fixed', C, `status: clean\n${K1}\nC1 fixed (abc123)\n`, ['accept', 'clean', null]],
|
|
129
|
+
['clean, critical 1, C1 deferred', C, `status: clean\n${K1}\n- C1: deferred → #12\n`, ['accept', 'clean', null]],
|
|
130
|
+
['clean, critical 1, C1 accepted', C, `status: clean\n${K1}\nC1 accepted: false positive\n`, ['accept', 'clean', null]],
|
|
131
|
+
['clean, critical 2, only C1 closed', C, `status: clean\n${K2}\nC1 fixed (a)\n`,
|
|
132
|
+
['normalise', 'issues_found', null, `status: issues_found\n${K2}\nC1 fixed (a)\n`]],
|
|
133
|
+
['clean, critical 1, C1 open', C, `status: clean\n${K1}\nC1 open\n`,
|
|
134
|
+
['normalise', 'issues_found', null, `status: issues_found\n${K1}\nC1 open\n`]],
|
|
135
|
+
['clean, critical 1, C1 fixed and C2 open', C, `status: clean\n${K1}\nC1 fixed (a)\nC2 open\n`,
|
|
136
|
+
['normalise', 'issues_found', null, `status: issues_found\n${K1}\nC1 fixed (a)\nC2 open\n`]],
|
|
137
|
+
['clean, critical 1, only a reclassified pointer', C, `status: clean\n${K1}\nC1 reclassified as W3\n`,
|
|
138
|
+
['normalise', 'issues_found', null, `status: issues_found\n${K1}\nC1 reclassified as W3\n`]],
|
|
139
|
+
['clean, critical 1, ledger line only inside a fence', C, `status: clean\n${K1}\n\`\`\`\nC1 fixed (a)\n\`\`\`\n`,
|
|
140
|
+
['normalise', 'issues_found', null, `status: issues_found\n${K1}\n\`\`\`\nC1 fixed (a)\n\`\`\`\n`]],
|
|
141
|
+
['clean, critical 2, a repeated C1 line counts once', C, `status: clean\n${K2}\nC1 fixed (a)\nc1 fixed (b)\n`,
|
|
142
|
+
['normalise', 'issues_found', null, `status: issues_found\n${K2}\nC1 fixed (a)\nc1 fixed (b)\n`]],
|
|
143
|
+
['finding prose with open after a closed ledger line stays clean', C, `status: clean\n${K1}\nC1: fixed (abc)\nC1 file.js:3 handler leaves the socket open\n`, ['accept', 'clean', null]],
|
|
144
|
+
['open redirect prose with a fixed ledger stays clean', C, `status: clean\n${K1}\nC1 fixed (abc)\nC1 open redirect in login.js:9\n`, ['accept', 'clean', null]],
|
|
145
|
+
['a lone fixed-size prose line does not close the Critical', C, `status: clean\n${K1}\nC1 fixed-size buffer overflow in x.js:3: open\n`,
|
|
146
|
+
['normalise', 'issues_found', null, `status: issues_found\n${K1}\nC1 fixed-size buffer overflow in x.js:3: open\n`]],
|
|
147
|
+
['bold id and en-dash separator ledger line', C, `status: clean\n${K1}\n- **C1** – fixed (abc)\n`, ['accept', 'clean', null]],
|
|
148
|
+
['verify subclass is not a class line', V, 'status: met\n- a, subclass: unmet\n', ['accept', 'met', null]],
|
|
149
|
+
['an unparsed open line for an uncounted Critical fails closed', C, `status: clean\n${K2}\nC1 fixed (a)\nC2 fixed (b)\nC3 open because x\n`,
|
|
150
|
+
['normalise', 'issues_found', null, `status: issues_found\n${K2}\nC1 fixed (a)\nC2 fixed (b)\nC3 open because x\n`]],
|
|
151
|
+
['em dash separator closes the Critical', C, `status: clean\n${K1}\nC1 ${String.fromCharCode(0x2014)} fixed (abc)\n`, ['accept', 'clean', null]],
|
|
152
|
+
['still open fails closed', C, `status: clean\n${K1}\nC1: still open\nC2 fixed (a)\n`,
|
|
153
|
+
['normalise', 'issues_found', null, `status: issues_found\n${K1}\nC1: still open\nC2 fixed (a)\n`]],
|
|
154
|
+
['bracketed open fails closed', C, `status: clean\n${K1}\nC1 [open]\nC2 fixed (a)\n`,
|
|
155
|
+
['normalise', 'issues_found', null, `status: issues_found\n${K1}\nC1 [open]\nC2 fixed (a)\n`]],
|
|
156
|
+
['a reclassified Critical does not gate the rest', C, `status: clean\n${K1}\nC1 reclassified as W3\nC2 fixed (a)\n`, ['accept', 'clean', null]],
|
|
157
|
+
['clean, critical 0, warnings open never gate', C, `status: clean\n${K}\nW1 open\n`, ['accept', 'clean', null]],
|
|
158
|
+
['issues_found is never changed', C, `status: issues_found\n${K1}\nC1 fixed (a)\n`, ['accept', 'issues_found', null]],
|
|
159
|
+
|
|
160
|
+
// --- verdict vs content: verify raises the tag to the highest claim class ---
|
|
161
|
+
['verify met with a met claim', V, 'status: met\n- claim 1, class: met\n', ['accept', 'met', null]],
|
|
162
|
+
['verify met with an unmet claim', V, 'status: met\n- Claim 2: login wired, class: unmet\n',
|
|
163
|
+
['normalise', 'gaps_found', null, 'status: gaps_found\n- Claim 2: login wired, class: unmet\n']],
|
|
164
|
+
['verify met with a partial claim', V, 'status: met\n- claim 1, class: partial\n',
|
|
165
|
+
['normalise', 'gaps_found', null, 'status: gaps_found\n- claim 1, class: partial\n']],
|
|
166
|
+
['verify met with an inconclusive claim', V, 'status: met\n- claim 1, class: inconclusive\n reason: unrunnable-check\n',
|
|
167
|
+
['normalise', 'inconclusive', null, 'status: inconclusive\n- claim 1, class: inconclusive\n reason: unrunnable-check\n']],
|
|
168
|
+
['verify met with inconclusive and unmet', V, 'status: met\n- a, class: inconclusive\n- b, class: unmet\n',
|
|
169
|
+
['normalise', 'gaps_found', null, 'status: gaps_found\n- a, class: inconclusive\n- b, class: unmet\n']],
|
|
170
|
+
['verify inconclusive with an unmet claim', V, 'status: inconclusive\n- b, class: unmet\n',
|
|
171
|
+
['normalise', 'gaps_found', null, 'status: gaps_found\n- b, class: unmet\n']],
|
|
172
|
+
['verify gaps_found with only met claims stays', V, 'status: gaps_found\n- a, class: met\n', ['accept', 'gaps_found', null]],
|
|
173
|
+
['verify inconclusive with only met claims stays', V, 'status: inconclusive\n- a, class: met\n', ['accept', 'inconclusive', null]],
|
|
174
|
+
['verify class line inside a fence is ignored', V, 'status: met\n```\n- a, class: unmet\n```\n', ['accept', 'met', null]],
|
|
175
|
+
['verify class line inside a blockquote is ignored', V, 'status: met\n> - a, class: unmet\n', ['accept', 'met', null]],
|
|
176
|
+
['verify upper-case class counts', V, 'status: met\n- a, Class: UNMET\n',
|
|
177
|
+
['normalise', 'gaps_found', null, 'status: gaps_found\n- a, Class: UNMET\n']],
|
|
122
178
|
];
|
|
123
179
|
|
|
124
180
|
for (const [name, technique, input, expected] of rows) {
|
|
@@ -274,6 +330,17 @@ ok('vocabulary: code-review tokens equal the ones code-review.md defines',
|
|
|
274
330
|
ok(`contracts: technique ${technique} maps to the family of trailhead-${technique} in the sync script`, FAMILIES[`trailhead-${technique}`] === family);
|
|
275
331
|
ok(`contracts: TOKENS.${technique} are the ${family} tokens`, JSON.stringify(TOKENS[technique]) === JSON.stringify(CONTRACTS[family].tokens));
|
|
276
332
|
}
|
|
333
|
+
// the reviewing block names the verify class vocabulary and the closing dispositions
|
|
334
|
+
const bullet = (block, prefix) => block.split('\n').find((l) => l.startsWith(`- ${prefix}:`)) || '';
|
|
335
|
+
const classTokens = (text) => [...text.matchAll(/class: <([a-z|]+)>/g)].flatMap((m) => m[1].split('|'));
|
|
336
|
+
ok('contracts: reviewing verify bullet names the checker class vocabulary',
|
|
337
|
+
JSON.stringify(classTokens(bullet(blocks.reviewing, 'trailhead-verify'))) === JSON.stringify(VERIFY_CLASSES));
|
|
338
|
+
const crBullet = bullet(blocks.reviewing, 'trailhead-code-review');
|
|
339
|
+
ok('contracts: reviewing code-review bullet names the closing dispositions',
|
|
340
|
+
['fixed', 'deferred', 'accepted'].every((d) => new RegExp(`\\x60${d}\\x60`).test(crBullet)));
|
|
341
|
+
const verifyDoc = fs.readFileSync(path.join(techDir, 'verify.md'), 'utf8');
|
|
342
|
+
ok('contracts: verify.md names the same class vocabulary',
|
|
343
|
+
classTokens(verifyDoc.replace(/class: ([a-z]+(?: \| [a-z]+)+)/g, (m, g) => `class: <${g.replace(/ \| /g, '|')}>`)).join() === VERIFY_CLASSES.join());
|
|
277
344
|
ok('contracts: every non-review agent has a technique here',
|
|
278
345
|
Object.entries(FAMILIES).filter(([, f]) => f !== 'reviewing').every(([a]) => FAMILY_OF[a.replace(/^trailhead-/, '')] !== undefined));
|
|
279
346
|
}
|
|
@@ -633,6 +633,23 @@ const SEARCH_VERB_CONFIG = {
|
|
|
633
633
|
awk: { patternFirst: true, valueFlags: new Set(['-f', '-v']), patternFlags: new Set() },
|
|
634
634
|
};
|
|
635
635
|
|
|
636
|
+
// #316: the -f / --file operand of a flag token, or null when the token is not one.
|
|
637
|
+
// Forms: `-f V`, `-fV`, a short cluster whose FIRST value-taking letter is `f`
|
|
638
|
+
// (`-if.env`; after another value letter the rest is that flag's value, so
|
|
639
|
+
// `-ef.env` is a pattern), `--file V`, `--file=V`. `consumed` = extra tokens used.
|
|
640
|
+
function fileFlagOperand(t, next, config) {
|
|
641
|
+
if (t === '--file') return { value: next, consumed: 1 };
|
|
642
|
+
if (t.startsWith('--file=')) return { value: t.slice('--file='.length), consumed: 0 };
|
|
643
|
+
if (t.startsWith('--')) return null;
|
|
644
|
+
for (let i = 1; i < t.length; i++) {
|
|
645
|
+
if (!config.valueFlags.has(`-${t[i]}`)) continue;
|
|
646
|
+
if (t[i] !== 'f') return null;
|
|
647
|
+
const rest = t.slice(i + 1);
|
|
648
|
+
return rest ? { value: rest, consumed: 0 } : { value: next, consumed: 1 };
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
|
|
636
653
|
// Walk a verb's argument tokens (verb itself already stripped), skipping
|
|
637
654
|
// flags (and their separate-token values), an explicit `--` end-of-options
|
|
638
655
|
// marker (which must not itself swallow the next real positional), and the
|
|
@@ -647,6 +664,14 @@ function scanArgsSkippingPattern(args, config, isMatch) {
|
|
|
647
664
|
const t = args[j];
|
|
648
665
|
if (!afterDashDash && t === '--') { afterDashDash = true; continue; }
|
|
649
666
|
if (!afterDashDash && t.startsWith('-') && t !== '-') {
|
|
667
|
+
// #316: the value of -f / --file is a FILE the verb reads, so it is tested.
|
|
668
|
+
const file = fileFlagOperand(t, args[j + 1], config);
|
|
669
|
+
if (file) {
|
|
670
|
+
if (file.value !== undefined && file.value !== '' && isMatch(file.value)) return file.value;
|
|
671
|
+
patternConsumed = true; // the pattern/script comes from that file, so positionals are FILEs
|
|
672
|
+
j += file.consumed;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
650
675
|
if (config.valueFlags.has(t)) {
|
|
651
676
|
j++; // consume its separate-token value
|
|
652
677
|
if (config.patternFlags && config.patternFlags.has(t)) patternConsumed = true;
|
|
@@ -136,5 +136,26 @@ eq('edge threshold', pct(1000, edge), 100);
|
|
|
136
136
|
// finestra minuscola: soglia mai sotto 1
|
|
137
137
|
eq('tiny window guard', pct(5, { env: { CLAUDE_CODE_AUTO_COMPACT_WINDOW: '10' } }), 100);
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
// --- credenziali: il blocco usage legge .credentials.json da CLAUDE_CONFIG_DIR (#320) ---
|
|
140
|
+
// Segnale osservabile: la dir cache viene creata solo se il guard sulle credenziali passa.
|
|
141
|
+
function usageReached({ withCreds }) {
|
|
142
|
+
const credCfg = path.join(root, withCreds ? 'cfg-creds' : 'cfg-nocreds');
|
|
143
|
+
const cache = path.join(root, withCreds ? 'cache-creds' : 'cache-nocreds');
|
|
144
|
+
fs.mkdirSync(credCfg, { recursive: true });
|
|
145
|
+
fs.mkdirSync(cache, { recursive: true });
|
|
146
|
+
if (withCreds) {
|
|
147
|
+
fs.writeFileSync(path.join(credCfg, '.credentials.json'), JSON.stringify({ claudeAiOauth: { accessToken: 'dummy' } }));
|
|
148
|
+
}
|
|
149
|
+
const env = { ...process.env, NO_COLOR: '1', HOME: home, CLAUDE_CONFIG_DIR: credCfg, XDG_CACHE_HOME: cache,
|
|
150
|
+
USAGE_API_TIMEOUT: '1', HTTPS_PROXY: 'http://127.0.0.1:9', https_proxy: 'http://127.0.0.1:9' };
|
|
151
|
+
delete env.TRAILHEAD_STATUSLINE_USAGE;
|
|
152
|
+
const input = { model: { id: 'claude-opus-5-5', display_name: 'Test' }, workspace: { project_dir: proj, current_dir: proj } };
|
|
153
|
+
spawnSync('bash', [script, '--context-only'], { input: JSON.stringify(input), env, encoding: 'utf8' });
|
|
154
|
+
return fs.existsSync(path.join(cache, 'trailhead-statusline'));
|
|
155
|
+
}
|
|
156
|
+
ok('usage block reached with creds under CLAUDE_CONFIG_DIR', usageReached({ withCreds: true }));
|
|
157
|
+
ok('usage block skipped without creds', !usageReached({ withCreds: false }));
|
|
158
|
+
|
|
159
|
+
// retry: il python in background puo' ancora scrivere nella cache mentre si pulisce
|
|
160
|
+
fs.rmSync(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
|
|
140
161
|
console.log(`statusline-context.test.js: ${passed} assertions passed`);
|
|
@@ -62,6 +62,11 @@ const WRAPPER_VALUE_FLAGS = {
|
|
|
62
62
|
nohup: [],
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
// Is `word` (as written, quotes allowed) a command wrapper from the list above? #316
|
|
66
|
+
function isWrapperWord(word) {
|
|
67
|
+
return Object.prototype.hasOwnProperty.call(WRAPPER_VALUE_FLAGS, dequote(word));
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
// Index just past a wrapper's own flags (and assignments), from the wrapper at toks[k].
|
|
66
71
|
function skipWrapper(toks, k) {
|
|
67
72
|
const w = dequote(toks[k]);
|
|
@@ -342,4 +347,4 @@ function unmodelledReason(guard, what, hit, redact) {
|
|
|
342
347
|
`interpret with certainty, so it cannot verify ${what}. Rewrite it in a simple form: ${hit.hint}.`;
|
|
343
348
|
}
|
|
344
349
|
|
|
345
|
-
module.exports = { UNMODELLED, findUnmodelled, firstRelevant, unmodelledReason };
|
|
350
|
+
module.exports = { UNMODELLED, findUnmodelled, firstRelevant, unmodelledReason, isWrapperWord };
|