@marcomigozzi/trailhead 0.10.0 → 0.11.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 +2 -2
- package/README.md +2 -2
- package/bin/lib/install-verify.js +257 -0
- package/bin/lib/install-verify.test.js +379 -0
- package/bin/trailhead.js +86 -5
- package/bin/trailhead.test.js +500 -4
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.js +58 -2
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +103 -13
- package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +375 -0
- package/plugins/trailhead/hooks/lib/fixtures/commit-msg-a319156 +57 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +399 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +437 -0
- package/plugins/trailhead/hooks/lib/model-defaults.json +12 -0
- package/plugins/trailhead/hooks/trailhead-check-update.js +69 -14
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +32 -3
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +41 -0
- package/plugins/trailhead/skills/_shared/configuration-reference.md +9 -6
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/principles.md +1 -1
- package/plugins/trailhead/skills/_shared/session-handoff.md +4 -4
- package/plugins/trailhead/skills/_shared/substrate-commands.md +1 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +15 -3
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/prototype.md +5 -1
- package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques.md +1 -1
- package/plugins/trailhead/skills/trailhead/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +3 -2
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +6 -6
- package/plugins/trailhead/templates/trailhead-commit-msg +49 -2
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +171 -0
package/bin/trailhead.test.js
CHANGED
|
@@ -52,9 +52,42 @@ function fakeBinDir(names) {
|
|
|
52
52
|
return dir;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// #187: the pinned-model review's documented lib path (see load-first.md) is
|
|
56
|
+
// relative to a skills/_shared/ dir; assert it actually resolves on every
|
|
57
|
+
// install layout, survives realpathSync, and its check/ack CLI round-trips.
|
|
58
|
+
const MODEL_DEFAULTS_LIB_CANDIDATES = [
|
|
59
|
+
'../../hooks/lib/model-defaults-review.js',
|
|
60
|
+
'../trailhead/hooks/lib/model-defaults-review.js',
|
|
61
|
+
];
|
|
62
|
+
function assertModelDefaultsLibLayout(label, sharedDir) {
|
|
63
|
+
const resolved = MODEL_DEFAULTS_LIB_CANDIDATES
|
|
64
|
+
.map((c) => path.join(sharedDir, c))
|
|
65
|
+
.find((p) => fs.existsSync(p));
|
|
66
|
+
ok(`${label}: documented model-defaults-review.js lib path resolves`, !!resolved);
|
|
67
|
+
if (!resolved) return;
|
|
68
|
+
ok(`${label}: resolved lib path still resolves after realpathSync`,
|
|
69
|
+
fs.existsSync(fs.realpathSync(resolved)));
|
|
70
|
+
|
|
71
|
+
const tmpProject = mktmp();
|
|
72
|
+
fs.mkdirSync(path.join(tmpProject, '.trailhead'), { recursive: true });
|
|
73
|
+
fs.writeFileSync(path.join(tmpProject, '.trailhead', 'config.json'),
|
|
74
|
+
JSON.stringify({ models: { plan: 'claude-opus-4-8' } }));
|
|
75
|
+
|
|
76
|
+
const noticeOut = String(execFileSync(process.execPath, [resolved, 'check', tmpProject], { encoding: 'utf8' }));
|
|
77
|
+
ok(`${label}: CLI check prints a notice`, noticeOut.trim().length > 0 && noticeOut.includes('models.plan'));
|
|
78
|
+
|
|
79
|
+
execFileSync(process.execPath, [resolved, 'ack', tmpProject], { encoding: 'utf8' });
|
|
80
|
+
const afterAckOut = String(execFileSync(process.execPath, [resolved, 'check', tmpProject], { encoding: 'utf8' }));
|
|
81
|
+
ok(`${label}: CLI check prints nothing after CLI ack`, afterAckOut === '');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Source/plugin layout itself (not an installed copy), so drift is caught
|
|
85
|
+
// even before an install is exercised.
|
|
86
|
+
assertModelDefaultsLibLayout('source', path.join(repoRoot, 'plugins', 'trailhead', 'skills', '_shared'));
|
|
87
|
+
|
|
55
88
|
// --- codex install -----------------------------------------------------------
|
|
56
89
|
const codexDir = mktmp();
|
|
57
|
-
runInstaller([`--codex`, `--dir=${codexDir}`]);
|
|
90
|
+
const codexInstallOut = String(runInstaller([`--codex`, `--dir=${codexDir}`]));
|
|
58
91
|
|
|
59
92
|
const skillMainPath = path.join(codexDir, 'skills', 'trailhead', 'SKILL.md');
|
|
60
93
|
ok('codex: SKILL.md exists', fs.existsSync(skillMainPath));
|
|
@@ -85,6 +118,12 @@ ok('codex: commit-msg template projected', fs.existsSync(codexTemplate));
|
|
|
85
118
|
ok('codex: commit-msg template is verbatim (not converted)',
|
|
86
119
|
fs.readFileSync(codexTemplate, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg'), 'utf8'));
|
|
87
120
|
|
|
121
|
+
// #185: the commit-msg hook sync script ships wholesale with templates/ too.
|
|
122
|
+
const codexCommitMsgSync = path.join(codexDir, 'skills', 'trailhead', 'templates', 'trailhead-commit-msg-sync.js');
|
|
123
|
+
ok('codex: commit-msg-sync script projected', fs.existsSync(codexCommitMsgSync));
|
|
124
|
+
ok('codex: commit-msg-sync script is byte-identical to source',
|
|
125
|
+
fs.readFileSync(codexCommitMsgSync, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg-sync.js'), 'utf8'));
|
|
126
|
+
|
|
88
127
|
ok('codex: no commands dir', !fs.existsSync(path.join(codexDir, 'commands')));
|
|
89
128
|
ok('codex: no settings.json file', !fs.existsSync(path.join(codexDir, 'settings.json')));
|
|
90
129
|
// #46: per-verb discoverability now projects one Codex SKILL per verb
|
|
@@ -206,8 +245,8 @@ ok('source: trailhead-chart SKILL.md config-offer points at the next-step block'
|
|
|
206
245
|
// (session-handoff.md legitimately offers /trailhead:quick <n> for the next loose ticket).
|
|
207
246
|
const captureSource = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-capture', 'references', 'capture.md'), 'utf8');
|
|
208
247
|
ok('source: capture.md confirmation anchors the next step to /trailhead:work', /[Aa]nchor the next step to `\/trailhead:work <n>`, always/.test(captureSource));
|
|
209
|
-
ok('source: capture.md
|
|
210
|
-
/[Aa]nchor the next step to `\/trailhead:work <n>`[\s\S]{0,400}`\/trailhead:quick <n>` is
|
|
248
|
+
ok('source: capture.md offers /trailhead:quick as the lighter alternative, after the work anchor',
|
|
249
|
+
/[Aa]nchor the next step to `\/trailhead:work <n>`[\s\S]{0,400}`\/trailhead:quick <n>` is the \*\*lighter alternative\*\*/.test(captureSource));
|
|
211
250
|
ok('source: capture.md says quick is offered alongside work, never in its place', captureSource.includes('never in its place'));
|
|
212
251
|
ok('source: capture.md keeps /trailhead:work the anchor even for a whiteboard capture', /whiteboard[\s\S]{0,200}the anchor stays `\/trailhead:work <n>`/.test(captureSource));
|
|
213
252
|
|
|
@@ -238,6 +277,180 @@ ok('source: capture.md says a map ticket stays on its map', /a map ticket stays
|
|
|
238
277
|
const ticketEnginesSourceFor179 = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-work', 'references', 'ticket-engines.md'), 'utf8');
|
|
239
278
|
ok('source: ticket-engines.md never labels quick generically as off-map', !/`quick`[^.\n]{0,40}off-map/.test(ticketEnginesSourceFor179));
|
|
240
279
|
|
|
280
|
+
// --- quick is the lighter path, never "just without splitting" --------------
|
|
281
|
+
// Source-level invariant: wherever a next-step suggestion characterizes quick,
|
|
282
|
+
// it names what quick is for (the lighter path: always size-triaged, offers to
|
|
283
|
+
// skip the heavy steps on a small change, no map book-keeping), with no-split
|
|
284
|
+
// as a side note. The old "only the no-split alternative" / "same work as
|
|
285
|
+
// work, without split" framing made agents tell users quick merely avoids
|
|
286
|
+
// splitting.
|
|
287
|
+
const dispatcherSourceForQuick = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead', 'SKILL.md'), 'utf8');
|
|
288
|
+
ok('source: capture.md drops the "only the no-split alternative" framing', !/only\*\* the no-split alternative/.test(captureSource));
|
|
289
|
+
ok('source: session-handoff.md drops the "as the no-split alternative" framing', !sessionHandoffSource.includes('as the no-split alternative'));
|
|
290
|
+
ok('source: dispatcher SKILL.md drops the "as the no-split alternative" framing', !dispatcherSourceForQuick.includes('as the no-split alternative'));
|
|
291
|
+
ok('source: session-handoff.md example drops "lo stesso lavoro di `work`, senza split"', !sessionHandoffSource.includes('lo stesso lavoro di `work`, senza split'));
|
|
292
|
+
ok('source: capture.md says quick offers to skip the heavy steps on a small change', /quick <n>`[\s\S]{0,400}skip the heavy steps/.test(captureSource));
|
|
293
|
+
ok('source: capture.md forbids describing quick as merely avoiding a split', /never as merely "without splitting"/.test(captureSource));
|
|
294
|
+
ok('source: session-handoff.md forbids describing quick as merely avoiding a split', /never as merely "without splitting"/.test(sessionHandoffSource));
|
|
295
|
+
ok('source: session-handoff.md example renders quick as the lighter path', /> - oppure `\/trailhead:quick[^\n]*percorso leggero/.test(sessionHandoffSource));
|
|
296
|
+
|
|
297
|
+
// --- #181: scope code review and verify to the ticket's own commits ---------
|
|
298
|
+
// Source-level invariant: Code review and Goal-backward verification scope to
|
|
299
|
+
// the commits carrying this ticket's `Refs: #<n>` trailer (the "Ticket diff"
|
|
300
|
+
// rule), never a `<base>...HEAD` range, which under `git: main` sweeps in
|
|
301
|
+
// other tickets' interleaved commits. code-review.md is canonical; verify.md
|
|
302
|
+
// restates the same pinned command inline. Also a behavioral pin: run the
|
|
303
|
+
// extracted command against a scratch repo with decoys, an interleaved
|
|
304
|
+
// foreign commit, and a conflicting trunk-sync merge.
|
|
305
|
+
const codeReviewSourcePath181 = path.join(sourceSkillsDir, '_shared', 'techniques', 'code-review.md');
|
|
306
|
+
const codeReviewSource181 = fs.readFileSync(codeReviewSourcePath181, 'utf8');
|
|
307
|
+
const verifySourcePath181 = path.join(sourceSkillsDir, '_shared', 'techniques', 'verify.md');
|
|
308
|
+
const verifySource181 = fs.readFileSync(verifySourcePath181, 'utf8');
|
|
309
|
+
const techniquesSourcePath181 = path.join(sourceSkillsDir, '_shared', 'techniques.md');
|
|
310
|
+
const techniquesSource181 = fs.readFileSync(techniquesSourcePath181, 'utf8');
|
|
311
|
+
const ticketEnginesSource181 = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-work', 'references', 'ticket-engines.md'), 'utf8');
|
|
312
|
+
|
|
313
|
+
ok("code-review.md states the Ticket diff heading sentence",
|
|
314
|
+
codeReviewSource181.includes("**Ticket diff: review the ticket's own commits, never a range.**"));
|
|
315
|
+
ok('code-review.md contains the exact per-commit read form',
|
|
316
|
+
codeReviewSource181.includes('git -C <repo> show --remerge-diff <sha>'));
|
|
317
|
+
ok('code-review.md does NOT contain the old range-scoped diff', !codeReviewSource181.includes('git diff <base>...HEAD'));
|
|
318
|
+
ok('code-review.md does NOT contain the old "pin a fixed point" phrasing', !codeReviewSource181.includes('pin a fixed point'));
|
|
319
|
+
ok('code-review.md states an empty set is an error', /\*\*An empty set is an error\*\*/.test(codeReviewSource181));
|
|
320
|
+
ok('code-review.md says the set is recomputed every round and every verify run',
|
|
321
|
+
codeReviewSource181.includes('Recompute the set on every review round and every verify run'));
|
|
322
|
+
ok('code-review.md says the set is never collapsed into one span',
|
|
323
|
+
codeReviewSource181.includes('Never collapse the set into one'));
|
|
324
|
+
ok('code-review.md mentions git: pr', codeReviewSource181.includes('git: pr'));
|
|
325
|
+
ok('code-review.md mentions trailhead/t<n>', codeReviewSource181.includes('trailhead/t<n>'));
|
|
326
|
+
|
|
327
|
+
const ticketDiffLogCmdRegex = /git -C <repo> log [^\n`]*HEAD/;
|
|
328
|
+
const codeReviewCmdMatch181 = codeReviewSource181.match(ticketDiffLogCmdRegex);
|
|
329
|
+
ok('code-review.md contains the pinned Ticket diff log command', !!codeReviewCmdMatch181);
|
|
330
|
+
const codeReviewLogCmd181 = codeReviewCmdMatch181 ? codeReviewCmdMatch181[0] : '';
|
|
331
|
+
|
|
332
|
+
ok('verify.md contains the same per-commit read form as code-review.md',
|
|
333
|
+
verifySource181.includes('git -C <repo> show --remerge-diff <sha>'));
|
|
334
|
+
ok('verify.md references the Ticket diff', verifySource181.includes('**Ticket diff**'));
|
|
335
|
+
ok('verify.md references code-review.md as canonical', verifySource181.includes('code-review.md'));
|
|
336
|
+
ok('verify.md does NOT contain the old range-scoped diff', !verifySource181.includes('git diff <base>...HEAD'));
|
|
337
|
+
|
|
338
|
+
const verifyCmdMatch181 = verifySource181.match(ticketDiffLogCmdRegex);
|
|
339
|
+
ok('verify.md contains the pinned Ticket diff log command', !!verifyCmdMatch181);
|
|
340
|
+
const verifyLogCmd181 = verifyCmdMatch181 ? verifyCmdMatch181[0] : '';
|
|
341
|
+
ok('code-review.md and verify.md state the exact same log command',
|
|
342
|
+
codeReviewLogCmd181 !== '' && codeReviewLogCmd181 === verifyLogCmd181);
|
|
343
|
+
|
|
344
|
+
const verifyStepLines181 = ticketEnginesSource181.split('\n').filter((l) => l.trim().startsWith('4. **Verify**'));
|
|
345
|
+
ok('ticket-engines.md has exactly two Verify (step 4) lines', verifyStepLines181.length === 2);
|
|
346
|
+
ok('both Verify step lines reference the Ticket diff', verifyStepLines181.every((l) => l.includes('**Ticket diff**')));
|
|
347
|
+
ok('ticket-engines.md does NOT contain the old range-scoped diff', !ticketEnginesSource181.includes('git diff <base>...HEAD'));
|
|
348
|
+
|
|
349
|
+
ok("techniques.md index row updates Code review to the ticket's own commits",
|
|
350
|
+
techniquesSource181.includes("review the ticket's own commits (its `Refs: #<n>` set) on 4 axes"));
|
|
351
|
+
|
|
352
|
+
for (const [label, src] of [
|
|
353
|
+
['code-review.md', codeReviewSource181],
|
|
354
|
+
['verify.md', verifySource181],
|
|
355
|
+
['ticket-engines.md', ticketEnginesSource181],
|
|
356
|
+
['techniques.md', techniquesSource181],
|
|
357
|
+
]) {
|
|
358
|
+
ok(`${label} has no em-dash`, !src.includes('\u2014'));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// --- #181 behavioral pin: build a scratch repo and run the extracted command -
|
|
362
|
+
function gitEnv181(extra = {}) {
|
|
363
|
+
return {
|
|
364
|
+
...process.env,
|
|
365
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
366
|
+
GIT_CONFIG_NOSYSTEM: '1',
|
|
367
|
+
GIT_AUTHOR_NAME: 'Trailhead Test',
|
|
368
|
+
GIT_AUTHOR_EMAIL: 'trailhead-test@example.com',
|
|
369
|
+
GIT_COMMITTER_NAME: 'Trailhead Test',
|
|
370
|
+
GIT_COMMITTER_EMAIL: 'trailhead-test@example.com',
|
|
371
|
+
LC_ALL: 'C',
|
|
372
|
+
...extra,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function git181(dir, args, extraEnv = {}) {
|
|
376
|
+
return execFileSync('git', ['-C', dir, ...args], { env: gitEnv181(extraEnv), stdio: ['ignore', 'pipe', 'pipe'] }).toString();
|
|
377
|
+
}
|
|
378
|
+
function commit181(dir, message, dateStr) {
|
|
379
|
+
git181(dir, ['add', '-A']);
|
|
380
|
+
git181(dir, ['commit', '-m', message], { GIT_AUTHOR_DATE: dateStr, GIT_COMMITTER_DATE: dateStr });
|
|
381
|
+
return git181(dir, ['rev-parse', 'HEAD']).trim();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const t181Repo = mktmp();
|
|
385
|
+
git181(t181Repo, ['init', '-b', 'main']);
|
|
386
|
+
const fFile181 = path.join(t181Repo, 'f.txt');
|
|
387
|
+
const D181 = (n) => `2026-01-01T00:00:${String(n).padStart(2, '0')}Z`;
|
|
388
|
+
|
|
389
|
+
fs.writeFileSync(fFile181, 'a\n');
|
|
390
|
+
const shaA181 = commit181(t181Repo, 'feat: a\n\nRefs: #18', D181(1));
|
|
391
|
+
|
|
392
|
+
fs.writeFileSync(path.join(t181Repo, 'decoy-b.txt'), 'b\n');
|
|
393
|
+
commit181(t181Repo, 'feat: b\n\nRefs: #181', D181(2));
|
|
394
|
+
|
|
395
|
+
fs.writeFileSync(path.join(t181Repo, 'decoy-c.txt'), 'c\n');
|
|
396
|
+
commit181(t181Repo, 'feat: c\n\nRefs: #1800', D181(3));
|
|
397
|
+
|
|
398
|
+
fs.writeFileSync(path.join(t181Repo, 'decoy-d.txt'), 'd\n');
|
|
399
|
+
commit181(t181Repo, 'feat: d\n\nmentions Refs: #18 inline\nRefs: #99', D181(4));
|
|
400
|
+
|
|
401
|
+
fs.writeFileSync(fFile181, 'foreign\n');
|
|
402
|
+
commit181(t181Repo, 'feat: foreign\n\nRefs: #99', D181(5));
|
|
403
|
+
|
|
404
|
+
git181(t181Repo, ['checkout', '-b', 'side']);
|
|
405
|
+
fs.writeFileSync(fFile181, 'side-edit\n');
|
|
406
|
+
fs.writeFileSync(path.join(t181Repo, 'foreign.txt'), 'foreign-file\n');
|
|
407
|
+
commit181(t181Repo, 'chore: side edit', D181(6));
|
|
408
|
+
|
|
409
|
+
git181(t181Repo, ['checkout', 'main']);
|
|
410
|
+
fs.writeFileSync(fFile181, 'main-edit\n');
|
|
411
|
+
const shaF181 = commit181(t181Repo, 'fix: e\n\nRefs: #18', D181(7));
|
|
412
|
+
|
|
413
|
+
let mergeConflicted181 = false;
|
|
414
|
+
try {
|
|
415
|
+
git181(t181Repo, ['merge', 'side', '--no-commit', '--no-ff']);
|
|
416
|
+
} catch {
|
|
417
|
+
mergeConflicted181 = true;
|
|
418
|
+
}
|
|
419
|
+
ok('#181 scratch repo: the trunk-sync merge actually conflicts on f.txt', mergeConflicted181);
|
|
420
|
+
fs.writeFileSync(fFile181, 'RESOLVED\n');
|
|
421
|
+
git181(t181Repo, ['add', 'f.txt', 'foreign.txt']);
|
|
422
|
+
const shaMerge181 = commit181(t181Repo, 'merge: sync\n\nRefs: #18', D181(8));
|
|
423
|
+
|
|
424
|
+
ok('#181 scratch repo: merge sha differs from the fix sha', shaMerge181 !== shaF181);
|
|
425
|
+
|
|
426
|
+
function runShellGit181(cmd, extraEnv = {}) {
|
|
427
|
+
return execFileSync('sh', ['-c', cmd], { env: gitEnv181(extraEnv), stdio: ['ignore', 'pipe', 'pipe'] }).toString();
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const substituted181 = (cmd, n) => cmd.replace(/<repo>/g, JSON.stringify(t181Repo)).replace(/<n>/g, String(n));
|
|
431
|
+
|
|
432
|
+
const fixedPatternTypeEnv181 = {
|
|
433
|
+
GIT_CONFIG_COUNT: '1',
|
|
434
|
+
GIT_CONFIG_KEY_0: 'grep.patternType',
|
|
435
|
+
GIT_CONFIG_VALUE_0: 'fixed',
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const out18_181 = runShellGit181(substituted181(codeReviewLogCmd181, 18), fixedPatternTypeEnv181);
|
|
439
|
+
const lines18_181 = out18_181.split('\n').filter((l) => l.length > 0);
|
|
440
|
+
ok('#181 log command returns exactly [a, f, merge] oldest-first for #18, even under grep.patternType=fixed',
|
|
441
|
+
lines18_181.length === 3 && lines18_181[0] === shaA181 && lines18_181[1] === shaF181 && lines18_181[2] === shaMerge181);
|
|
442
|
+
|
|
443
|
+
const out7_181 = runShellGit181(substituted181(codeReviewLogCmd181, 7), fixedPatternTypeEnv181);
|
|
444
|
+
ok('#181 log command returns empty output for a ticket with no commits', out7_181.trim() === '');
|
|
445
|
+
|
|
446
|
+
const remergeCmd181 = 'git -C <repo> show --remerge-diff <sha>'
|
|
447
|
+
.replace('<repo>', JSON.stringify(t181Repo))
|
|
448
|
+
.replace('<sha>', shaMerge181);
|
|
449
|
+
const remergeOut181 = runShellGit181(remergeCmd181);
|
|
450
|
+
ok('#181 --remerge-diff on the merge shows the resolution hunk', remergeOut181.includes('RESOLVED'));
|
|
451
|
+
ok('#181 --remerge-diff on the merge does NOT show the foreign file the merge pulled in cleanly',
|
|
452
|
+
!remergeOut181.includes('foreign.txt'));
|
|
453
|
+
|
|
241
454
|
// --- #151: eager edge drop at close (superseded / out-of-scope only) ---------
|
|
242
455
|
// Source-level invariant: closing a ticket as trailhead:superseded or
|
|
243
456
|
// trailhead:out-of-scope drops its native sub-issue edge at close (reusing the
|
|
@@ -459,6 +672,10 @@ ok('codex: skills/trailhead/hooks/lib/commit-message-check.js exists (commit-gua
|
|
|
459
672
|
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'commit-message-check.js')));
|
|
460
673
|
ok('codex: skills/trailhead/hooks/lib/gh-subcommand.js exists (body-guard/secret-guard require target, #153)',
|
|
461
674
|
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'gh-subcommand.js')));
|
|
675
|
+
ok('codex: skills/trailhead/hooks/lib/model-defaults-review.js exists (check-update require target, #186)',
|
|
676
|
+
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'model-defaults-review.js')));
|
|
677
|
+
ok('codex: skills/trailhead/hooks/lib/model-defaults.json exists (#186)',
|
|
678
|
+
fs.existsSync(path.join(codexDir, 'skills', 'trailhead', 'hooks', 'lib', 'model-defaults.json')));
|
|
462
679
|
// #169: search-guard is Claude-Code-specific (it only prevents arming
|
|
463
680
|
// Claude's Read() deny rule under bypass permissions, a mechanism Codex
|
|
464
681
|
// lacks), so it must neither be registered nor copied on a Codex install.
|
|
@@ -485,6 +702,8 @@ ok('codex: trailhead-codebase-map.toml is projected with no models.codex.* set',
|
|
|
485
702
|
ok('codex: trailhead-fix.toml is pin-less (no model = line) with no models.codex.* set',
|
|
486
703
|
fs.existsSync(noPinFixTomlPath) && !/^model = /m.test(fs.readFileSync(noPinFixTomlPath, 'utf8')));
|
|
487
704
|
|
|
705
|
+
assertModelDefaultsLibLayout('codex copy', path.join(codexDir, 'skills', '_shared'));
|
|
706
|
+
|
|
488
707
|
// --- codex --symlink: link verbatim artifacts (hooks + templates), keep skills projected ---
|
|
489
708
|
const codexSymDir = mktmp();
|
|
490
709
|
runInstaller([`--codex`, `--symlink`, `--dir=${codexSymDir}`]);
|
|
@@ -509,6 +728,8 @@ ok('codex copy: hooks are regular files (not symlinks)',
|
|
|
509
728
|
ok('codex copy: templates is a regular dir (not a symlink)',
|
|
510
729
|
!fs.lstatSync(path.join(codexDir, 'skills', 'trailhead', 'templates')).isSymbolicLink());
|
|
511
730
|
|
|
731
|
+
assertModelDefaultsLibLayout('codex symlink', path.join(codexSymDir, 'skills', '_shared'));
|
|
732
|
+
|
|
512
733
|
// --- codex agent TOML projection (#38, #88) -------------------------------------
|
|
513
734
|
// With NO models.codex.* set, a codex install still projects all 8 agents
|
|
514
735
|
// uniformly, pin-less (uniform projection, #88), never zero. Install from an
|
|
@@ -574,7 +795,7 @@ ok('codex uninstall: hooks.json no longer contains trailhead commands', (() => {
|
|
|
574
795
|
|
|
575
796
|
// --- claude regression --------------------------------------------------------
|
|
576
797
|
const claudeDir = mktmp();
|
|
577
|
-
runInstaller([`--claude`, `--dir=${claudeDir}`]);
|
|
798
|
+
const claudeInstallOut = String(runInstaller([`--claude`, `--dir=${claudeDir}`]));
|
|
578
799
|
|
|
579
800
|
const claudeSkillPath = path.join(claudeDir, 'skills', 'trailhead', 'SKILL.md');
|
|
580
801
|
ok('claude: skills/trailhead/SKILL.md exists', fs.existsSync(claudeSkillPath));
|
|
@@ -602,6 +823,16 @@ ok('claude: hooks/lib/commit-message-check.js exists (commit-guard require targe
|
|
|
602
823
|
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'commit-message-check.js')));
|
|
603
824
|
ok('claude: hooks/lib/gh-subcommand.js exists (body-guard/secret-guard require target, #153)',
|
|
604
825
|
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'gh-subcommand.js')));
|
|
826
|
+
ok('claude: hooks/lib/model-defaults-review.js exists (check-update require target, #186)',
|
|
827
|
+
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'model-defaults-review.js')));
|
|
828
|
+
ok('claude: hooks/lib/model-defaults.json exists (#186)',
|
|
829
|
+
fs.existsSync(path.join(claudeDir, 'hooks', 'lib', 'model-defaults.json')));
|
|
830
|
+
|
|
831
|
+
// #185: the commit-msg hook sync script ships wholesale with templates/ too.
|
|
832
|
+
const claudeCommitMsgSync = path.join(claudeDir, 'trailhead', 'templates', 'trailhead-commit-msg-sync.js');
|
|
833
|
+
ok('claude: commit-msg-sync script projected', fs.existsSync(claudeCommitMsgSync));
|
|
834
|
+
ok('claude: commit-msg-sync script is byte-identical to source',
|
|
835
|
+
fs.readFileSync(claudeCommitMsgSync, 'utf8') === fs.readFileSync(path.join(repoRoot, 'plugins', 'trailhead', 'templates', 'trailhead-commit-msg-sync.js'), 'utf8'));
|
|
605
836
|
// Regression: the commit-guard does require('./lib/commit-message-check.js'), so
|
|
606
837
|
// it only loads if the lib was copied alongside it. Run it with a benign Bash
|
|
607
838
|
// payload and assert it does not crash with a missing-module error.
|
|
@@ -631,6 +862,8 @@ ok('claude: agent file registers its subagent name in frontmatter',
|
|
|
631
862
|
ok('claude: agents are real files on a copy install (not symlinks)',
|
|
632
863
|
!fs.lstatSync(path.join(claudeAgentsDir, 'trailhead-plan.md')).isSymbolicLink());
|
|
633
864
|
|
|
865
|
+
assertModelDefaultsLibLayout('claude copy', path.join(claudeDir, 'skills', '_shared'));
|
|
866
|
+
|
|
634
867
|
// --- claude --symlink: dev install links hooks live too (not just skills/commands) ---
|
|
635
868
|
const symDir = mktmp();
|
|
636
869
|
runInstaller([`--claude`, `--symlink`, `--dir=${symDir}`]);
|
|
@@ -664,6 +897,8 @@ ok('claude symlink: agent symlink resolves into the package source',
|
|
|
664
897
|
ok('claude copy: hooks/trailhead-secret-guard.js is a regular file (not a symlink)',
|
|
665
898
|
!fs.lstatSync(path.join(claudeDir, 'hooks', 'trailhead-secret-guard.js')).isSymbolicLink());
|
|
666
899
|
|
|
900
|
+
assertModelDefaultsLibLayout('claude symlink', path.join(symDir, 'skills', '_shared'));
|
|
901
|
+
|
|
667
902
|
// --- claude migration: reinstall over an old layout sweeps stale skill dirs ---
|
|
668
903
|
// An old install may carry a pre-split monolith (skills/trailhead-monolith) or a
|
|
669
904
|
// removed cluster (skills/trailhead-legacy) the current install no longer ships.
|
|
@@ -859,6 +1094,267 @@ try {
|
|
|
859
1094
|
ok('unknown flag after a valid flag: still rejected', unknownAfterValidRejected);
|
|
860
1095
|
ok('unknown flag after a valid flag: installs nothing', nothingInstalledUnder(unknownAfterValidDir));
|
|
861
1096
|
|
|
1097
|
+
// --- #185: teamwork.md/substrate-commands.md/trailhead-work SKILL.md point at
|
|
1098
|
+
// the commit-msg-sync script instead of the old manual copy/chmod steps -------
|
|
1099
|
+
ok('source: teamwork.md no longer says an existing hook is never replaced',
|
|
1100
|
+
!teamworkSource.includes('an existing hook is never replaced'));
|
|
1101
|
+
ok('source: teamwork.md no longer says reinstalled by hand',
|
|
1102
|
+
!teamworkSource.includes("until it's reinstalled by hand"));
|
|
1103
|
+
for (const [label, src] of [
|
|
1104
|
+
['teamwork.md', teamworkSource],
|
|
1105
|
+
['substrate-commands.md', substrateCommandsSource],
|
|
1106
|
+
['trailhead-work/SKILL.md', workSkillSource],
|
|
1107
|
+
]) {
|
|
1108
|
+
ok(`source: ${label} references trailhead-commit-msg-sync.js`, src.includes('trailhead-commit-msg-sync.js'));
|
|
1109
|
+
ok(`source: ${label} has no em-dash`, !src.includes('—'));
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// --- #189: script mancante / errore node -> fallback che ispeziona la hooks dir
|
|
1113
|
+
// invece di dichiarare alla cieca "git non applica le regole" ------------------
|
|
1114
|
+
ok('source: teamwork.md fallback resolves the hooks dir via rev-parse --git-path hooks',
|
|
1115
|
+
teamworkSource.includes('git -C <working-root> rev-parse --git-path hooks'));
|
|
1116
|
+
ok('source: teamwork.md fallback classifies by the trailhead header line',
|
|
1117
|
+
teamworkSource.includes('// trailhead commit-msg hook (git).'));
|
|
1118
|
+
ok('source: teamwork.md fallback checks the executable bit (test -x)',
|
|
1119
|
+
teamworkSource.includes('test -x'));
|
|
1120
|
+
ok('source: teamwork.md fallback reads only the first 5 lines',
|
|
1121
|
+
teamworkSource.includes('first 5 lines'));
|
|
1122
|
+
ok('source: teamwork.md fallback treats a symlinked commit-msg as foreign',
|
|
1123
|
+
teamworkSource.includes('symlinked `commit-msg`'));
|
|
1124
|
+
for (const [label, src] of [
|
|
1125
|
+
['trailhead-work/SKILL.md', workSkillSource],
|
|
1126
|
+
['substrate-commands.md', substrateCommandsSource],
|
|
1127
|
+
]) {
|
|
1128
|
+
ok(`source: ${label} mentions the missing-script hooks-dir fallback`,
|
|
1129
|
+
src.includes('rev-parse --git-path hooks') && /missing script or a node error|script is missing or node errors/.test(src));
|
|
1130
|
+
ok(`source: ${label} has no em-dash (#189)`, !src.includes('—'));
|
|
1131
|
+
}
|
|
1132
|
+
ok('source: teamwork.md has no em-dash (#189)', !teamworkSource.includes('—'));
|
|
1133
|
+
|
|
1134
|
+
// --- #182 ---------------------------------------------------------------------
|
|
1135
|
+
// A broken install fails loudly (verifyInstall names the gap and the run exits
|
|
1136
|
+
// non-zero, printing no "✓ trailhead installed"); a clean one still prints ✓.
|
|
1137
|
+
// brokenPkg copies bin/ + plugins/ into a fresh tmp root (so __dirname-relative
|
|
1138
|
+
// PKG/SRC resolution in trailhead.js still lines up) and applies a mutation
|
|
1139
|
+
// that breaks one source artifact; runPkg drives that copy (or the real repo)
|
|
1140
|
+
// through execFileSync without throwing on a non-zero exit.
|
|
1141
|
+
function brokenPkg(mutate) {
|
|
1142
|
+
const pkgDir = mktmp();
|
|
1143
|
+
fs.cpSync(path.join(repoRoot, 'bin'), path.join(pkgDir, 'bin'), { recursive: true });
|
|
1144
|
+
fs.cpSync(path.join(repoRoot, 'plugins'), path.join(pkgDir, 'plugins'), { recursive: true });
|
|
1145
|
+
if (mutate) mutate(pkgDir);
|
|
1146
|
+
return pkgDir;
|
|
1147
|
+
}
|
|
1148
|
+
function runPkg(pkgDir, args, env) {
|
|
1149
|
+
try {
|
|
1150
|
+
const stdout = execFileSync(process.execPath, [path.join(pkgDir, 'bin', 'trailhead.js'), ...args], {
|
|
1151
|
+
cwd: pkgDir, stdio: 'pipe', env: env || process.env,
|
|
1152
|
+
});
|
|
1153
|
+
return { status: 0, stdout: String(stdout), stderr: '' };
|
|
1154
|
+
} catch (e) {
|
|
1155
|
+
return { status: e.status, stdout: String(e.stdout || ''), stderr: String(e.stderr || '') };
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// (a) no engine agents at all: both hosts refuse and print no success line.
|
|
1160
|
+
{
|
|
1161
|
+
const noAgentsPkg = brokenPkg((p) => fs.rmSync(path.join(p, 'plugins', 'trailhead', 'agents'), { recursive: true, force: true }));
|
|
1162
|
+
const claudeOut = runPkg(noAgentsPkg, ['--claude', `--dir=${mktmp()}`]);
|
|
1163
|
+
ok('(a) claude, no agents: non-zero exit', claudeOut.status !== 0);
|
|
1164
|
+
ok('(a) claude, no agents: stderr names agents', claudeOut.stderr.includes('agents'));
|
|
1165
|
+
ok('(a) claude, no agents: stdout has no ✓ trailhead installed', !claudeOut.stdout.includes('✓ trailhead installed'));
|
|
1166
|
+
const codexOut = runPkg(noAgentsPkg, ['--codex', `--dir=${mktmp()}`]);
|
|
1167
|
+
ok('(a) codex, no agents: non-zero exit', codexOut.status !== 0);
|
|
1168
|
+
ok('(a) codex, no agents: stderr names agents', codexOut.stderr.includes('agents'));
|
|
1169
|
+
ok('(a) codex, no agents: stdout has no ✓ trailhead installed', !codexOut.stdout.includes('✓ trailhead installed'));
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// (b) a cluster's SKILL.md is missing from the source.
|
|
1173
|
+
{
|
|
1174
|
+
const pkg = brokenPkg((p) => fs.rmSync(path.join(p, 'plugins', 'trailhead', 'skills', 'trailhead-view', 'SKILL.md'), { force: true }));
|
|
1175
|
+
const out = runPkg(pkg, ['--claude', `--dir=${mktmp()}`]);
|
|
1176
|
+
ok('(b) missing cluster SKILL.md: non-zero exit', out.status !== 0);
|
|
1177
|
+
ok('(b) missing cluster SKILL.md: stderr names trailhead-view/SKILL.md', out.stderr.includes('trailhead-view/SKILL.md'));
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// (c) the shared core is missing a file.
|
|
1181
|
+
{
|
|
1182
|
+
const pkg = brokenPkg((p) => fs.rmSync(path.join(p, 'plugins', 'trailhead', 'skills', '_shared', 'substrate.md'), { force: true }));
|
|
1183
|
+
const out = runPkg(pkg, ['--claude', `--dir=${mktmp()}`]);
|
|
1184
|
+
ok('(c) missing _shared/substrate.md: non-zero exit', out.status !== 0);
|
|
1185
|
+
ok('(c) missing _shared/substrate.md: stderr names _shared/substrate.md', out.stderr.includes('_shared/substrate.md'));
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// (d) a hook script source is missing, fresh dir: named cleanly, no ENOENT stack.
|
|
1189
|
+
const missingBodyGuardPkg = brokenPkg((p) => fs.rmSync(path.join(p, 'plugins', 'trailhead', 'hooks', 'trailhead-body-guard.js'), { force: true }));
|
|
1190
|
+
{
|
|
1191
|
+
const out = runPkg(missingBodyGuardPkg, ['--claude', `--dir=${mktmp()}`]);
|
|
1192
|
+
ok('(d) missing hook source, fresh dir: non-zero exit', out.status !== 0);
|
|
1193
|
+
ok('(d) missing hook source, fresh dir: stderr names trailhead-body-guard.js', out.stderr.includes('trailhead-body-guard.js'));
|
|
1194
|
+
ok('(d) missing hook source, fresh dir: no ENOENT stack', !out.stderr.includes('ENOENT'));
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// (e) reinstalling that same broken package over a prior clean install removes
|
|
1198
|
+
// the stale hook file rather than leaving it behind, on both hosts.
|
|
1199
|
+
{
|
|
1200
|
+
const xDir = mktmp();
|
|
1201
|
+
runInstaller([`--claude`, `--dir=${xDir}`]);
|
|
1202
|
+
const out = runPkg(missingBodyGuardPkg, ['--claude', `--dir=${xDir}`]);
|
|
1203
|
+
ok('(e) claude reinstall over an old install: non-zero exit', out.status !== 0);
|
|
1204
|
+
ok('(e) claude reinstall: stderr names trailhead-body-guard.js', out.stderr.includes('trailhead-body-guard.js'));
|
|
1205
|
+
ok('(e) claude reinstall: the stale hook file is gone', !fs.existsSync(path.join(xDir, 'hooks', 'trailhead-body-guard.js')));
|
|
1206
|
+
|
|
1207
|
+
const wDir = mktmp();
|
|
1208
|
+
runInstaller([`--codex`, `--dir=${wDir}`]);
|
|
1209
|
+
const outCodex = runPkg(missingBodyGuardPkg, ['--codex', `--dir=${wDir}`]);
|
|
1210
|
+
ok('(e) codex reinstall over an old install: non-zero exit', outCodex.status !== 0);
|
|
1211
|
+
ok('(e) codex reinstall: stderr names trailhead-body-guard.js', outCodex.stderr.includes('trailhead-body-guard.js'));
|
|
1212
|
+
ok('(e) codex reinstall: the stale hook file is gone', !fs.existsSync(path.join(wDir, 'skills', 'trailhead', 'hooks', 'trailhead-body-guard.js')));
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// (f) a stale Claude registration in settings.json (target gone).
|
|
1216
|
+
{
|
|
1217
|
+
const yDir = mktmp();
|
|
1218
|
+
runInstaller([`--claude`, `--dir=${yDir}`]);
|
|
1219
|
+
const settingsPath = path.join(yDir, 'settings.json');
|
|
1220
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
1221
|
+
settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [
|
|
1222
|
+
{ type: 'command', command: `node "${path.join(yDir, 'hooks', 'trailhead-gone-guard.js')}"` },
|
|
1223
|
+
] });
|
|
1224
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
1225
|
+
const out = runPkg(repoRoot, [`--claude`, `--dir=${yDir}`]);
|
|
1226
|
+
ok('(f) stale Claude registration: non-zero exit', out.status !== 0);
|
|
1227
|
+
ok('(f) stale Claude registration: stderr names the stale target', out.stderr.includes('trailhead-gone-guard.js'));
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// (g) a stale generic Codex registration in hooks.json (target gone).
|
|
1231
|
+
{
|
|
1232
|
+
const wgDir = mktmp();
|
|
1233
|
+
runInstaller([`--codex`, `--dir=${wgDir}`]);
|
|
1234
|
+
const hooksJsonPath = path.join(wgDir, 'hooks.json');
|
|
1235
|
+
const hooksJson = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
|
|
1236
|
+
hooksJson.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [
|
|
1237
|
+
{ type: 'command', command: `node "${path.join(wgDir, 'skills', 'trailhead', 'hooks', 'trailhead-gone-guard.js')}"` },
|
|
1238
|
+
] });
|
|
1239
|
+
fs.writeFileSync(hooksJsonPath, JSON.stringify(hooksJson, null, 2));
|
|
1240
|
+
const out = runPkg(repoRoot, [`--codex`, `--dir=${wgDir}`]);
|
|
1241
|
+
ok('(g) stale Codex registration: non-zero exit', out.status !== 0);
|
|
1242
|
+
ok('(g) stale Codex registration: stderr names the stale target', out.stderr.includes('trailhead-gone-guard.js'));
|
|
1243
|
+
ok('(g) stale Codex registration: stderr names hooks.json', out.stderr.includes('hooks.json'));
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// (h) the pre-#169 search-guard registration migrates away silently on reinstall.
|
|
1247
|
+
// May already pass before the installer wires verifyInstall in (no check exists
|
|
1248
|
+
// yet to trip on it); it stays here to pin the migration's intent regardless.
|
|
1249
|
+
{
|
|
1250
|
+
const zDir = mktmp();
|
|
1251
|
+
runInstaller([`--codex`, `--dir=${zDir}`]);
|
|
1252
|
+
const hooksJsonPath = path.join(zDir, 'hooks.json');
|
|
1253
|
+
const hooksJson = JSON.parse(fs.readFileSync(hooksJsonPath, 'utf8'));
|
|
1254
|
+
hooksJson.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [
|
|
1255
|
+
{ type: 'command', command: `node "${path.join(zDir, 'skills', 'trailhead', 'hooks', 'trailhead-search-guard.js')}"` },
|
|
1256
|
+
] });
|
|
1257
|
+
fs.writeFileSync(hooksJsonPath, JSON.stringify(hooksJson, null, 2));
|
|
1258
|
+
const out = runPkg(repoRoot, [`--codex`, `--dir=${zDir}`]);
|
|
1259
|
+
ok('(h) pre-#169 search-guard migration: exit 0', out.status === 0);
|
|
1260
|
+
ok('(h) pre-#169 search-guard migration: prints ✓', out.stdout.includes('✓'));
|
|
1261
|
+
const afterHooksJson = fs.readFileSync(hooksJsonPath, 'utf8');
|
|
1262
|
+
ok('(h) pre-#169 search-guard migration: the stale entry is gone', !afterHooksJson.includes('trailhead-search-guard.js'));
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// (i) an unresolved env var on a registered target warns instead of failing.
|
|
1266
|
+
{
|
|
1267
|
+
const vDir = mktmp();
|
|
1268
|
+
runInstaller([`--claude`, `--dir=${vDir}`]);
|
|
1269
|
+
const settingsPath = path.join(vDir, 'settings.json');
|
|
1270
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
1271
|
+
settings.hooks.PreToolUse.push({ matcher: 'Bash', hooks: [
|
|
1272
|
+
{ type: 'command', command: 'node "${TRAILHEAD_T182_UNSET}/hooks/trailhead-x-guard.js"' },
|
|
1273
|
+
] });
|
|
1274
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
1275
|
+
// Delete the key outright (never set it to undefined), so the child process
|
|
1276
|
+
// never receives a literal "undefined" string for it.
|
|
1277
|
+
const envWithoutVar = { ...process.env };
|
|
1278
|
+
delete envWithoutVar.TRAILHEAD_T182_UNSET;
|
|
1279
|
+
const out = runPkg(repoRoot, [`--claude`, `--dir=${vDir}`], envWithoutVar);
|
|
1280
|
+
ok('(i) unresolved env var: exit 0', out.status === 0);
|
|
1281
|
+
ok('(i) unresolved env var: prints ✓', out.stdout.includes('✓'));
|
|
1282
|
+
ok('(i) unresolved env var: stdout warns ⚠ unverified', out.stdout.includes('⚠ unverified'));
|
|
1283
|
+
ok('(i) unresolved env var: stdout names the unresolved variable', out.stdout.includes('TRAILHEAD_T182_UNSET'));
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// (j) regression: the existing clean installs at the top of this file still
|
|
1287
|
+
// print ✓ with no ⚠ unverified.
|
|
1288
|
+
ok('(j) claude clean install: prints ✓ trailhead installed', claudeInstallOut.includes('✓ trailhead installed'));
|
|
1289
|
+
ok('(j) claude clean install: no ⚠ unverified', !claudeInstallOut.includes('⚠ unverified'));
|
|
1290
|
+
ok('(j) codex clean install: prints ✓ trailhead installed', codexInstallOut.includes('✓ trailhead installed'));
|
|
1291
|
+
ok('(j) codex clean install: no ⚠ unverified', !codexInstallOut.includes('⚠ unverified'));
|
|
1292
|
+
|
|
1293
|
+
// --- #183: severity counts and per-finding disposition gate on Resolve -------
|
|
1294
|
+
// Code review classifies findings as Critical/Warning/Info, but only Criticals
|
|
1295
|
+
// gated resolution and Warnings/Info were never tracked to a decision. Pin the
|
|
1296
|
+
// counts line under `status:`, the disposition vocabulary, and the Resolve gate
|
|
1297
|
+
// that requires every counted finding to carry one before a ticket can close.
|
|
1298
|
+
const codeReviewSourcePath183 = path.join(sourceSkillsDir, '_shared', 'techniques', 'code-review.md');
|
|
1299
|
+
const codeReviewSource183 = fs.readFileSync(codeReviewSourcePath183, 'utf8');
|
|
1300
|
+
const ticketEnginesSource183 = fs.readFileSync(path.join(sourceSkillsDir, 'trailhead-work', 'references', 'ticket-engines.md'), 'utf8');
|
|
1301
|
+
|
|
1302
|
+
ok('code-review.md states the counts format right under the status tag',
|
|
1303
|
+
codeReviewSource183.includes('critical: <n> · warning: <n> · info: <n>'));
|
|
1304
|
+
ok('code-review.md says the counts line sits right under the status tag',
|
|
1305
|
+
/right under[^.]*`status:`/.test(codeReviewSource183) || /`status:`[^.]*right under/.test(codeReviewSource183));
|
|
1306
|
+
|
|
1307
|
+
ok('code-review.md names the fixed disposition', codeReviewSource183.includes('`fixed`'));
|
|
1308
|
+
ok('code-review.md names the deferred disposition', codeReviewSource183.includes('`deferred → <ticket>`'));
|
|
1309
|
+
ok('code-review.md names the accepted disposition', codeReviewSource183.includes('`accepted: <one-line reason>`'));
|
|
1310
|
+
ok('code-review.md names the transient open disposition', codeReviewSource183.includes('`open`'));
|
|
1311
|
+
|
|
1312
|
+
ok('code-review.md says finding IDs are stable across rounds',
|
|
1313
|
+
/stable/i.test(codeReviewSource183) && /across (every|fix-and-re-review) round/i.test(codeReviewSource183));
|
|
1314
|
+
ok('code-review.md says the counts are cumulative', codeReviewSource183.includes('cumulative'));
|
|
1315
|
+
ok('code-review.md says no finding reaches Resolve with open or without a disposition',
|
|
1316
|
+
/no finding reaches Resolve with[^.]*`open`[^.]*without a disposition/.test(codeReviewSource183));
|
|
1317
|
+
|
|
1318
|
+
ok('code-review.md no longer defers Warnings/Info as "noted for the caller to weigh"',
|
|
1319
|
+
!codeReviewSource183.includes('noted for the caller to weigh'));
|
|
1320
|
+
|
|
1321
|
+
const verifyStepLines183 = ticketEnginesSource183.split('\n').filter((l) => l.trim().startsWith('4. **Verify**'));
|
|
1322
|
+
ok('ticket-engines.md has exactly two Verify (step 4) lines', verifyStepLines183.length === 2);
|
|
1323
|
+
ok('both Verify step lines mention the counts line',
|
|
1324
|
+
verifyStepLines183.every((l) => l.includes('counts line')));
|
|
1325
|
+
|
|
1326
|
+
const resolveStepLines183 = ticketEnginesSource183.split('\n').filter((l) => l.trim().startsWith('5. **Resolve**'));
|
|
1327
|
+
ok('ticket-engines.md has exactly two Resolve (step 5) lines', resolveStepLines183.length === 2);
|
|
1328
|
+
ok('both Resolve step lines carry the Disposition gate',
|
|
1329
|
+
resolveStepLines183.every((l) => l.includes('**Disposition gate (before the close)**')));
|
|
1330
|
+
|
|
1331
|
+
for (const [label, src] of [
|
|
1332
|
+
['code-review.md', codeReviewSource183],
|
|
1333
|
+
['ticket-engines.md', ticketEnginesSource183],
|
|
1334
|
+
]) {
|
|
1335
|
+
ok(`${label} has no em-dash (#183)`, !src.includes('\u2014'));
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// --- plan-review: "run until it converges" option at the rounds-exhausted ask ---
|
|
1339
|
+
const planReviewSourceConv = fs.readFileSync(path.join(sourceSkillsDir, '_shared', 'techniques', 'plan-review.md'), 'utf8');
|
|
1340
|
+
const planReviewStep4 = planReviewSourceConv.split('\n').find((l) => l.startsWith('4. **Record and decide.**')) || '';
|
|
1341
|
+
ok('plan-review.md step 4 still offers another bounded review cycle', planReviewStep4.includes('run another review cycle'));
|
|
1342
|
+
ok('plan-review.md step 4 offers running until the plan converges', /\(d\) \*\*run until it converges\*\*/.test(planReviewStep4));
|
|
1343
|
+
ok('plan-review.md converge option stops when no blocking concern remains', /run until it converges[\s\S]{0,400}no blocking concern remains/.test(planReviewStep4));
|
|
1344
|
+
ok('plan-review.md converge option has a no-progress guard that returns to this checkpoint',
|
|
1345
|
+
/run until it converges[\s\S]{0,600}no progress[\s\S]{0,300}back to this checkpoint/.test(planReviewStep4));
|
|
1346
|
+
ok('plan-review.md step 4 has no em-dash', !planReviewStep4.includes('\u2014'));
|
|
1347
|
+
|
|
1348
|
+
// --- code-review: "run until it converges" option at the rounds-exhausted ask ---
|
|
1349
|
+
const codeReviewSourceConv = fs.readFileSync(path.join(sourceSkillsDir, '_shared', 'techniques', 'code-review.md'), 'utf8');
|
|
1350
|
+
const codeReviewBoundary = codeReviewSourceConv.split('\n').find((l) => l.startsWith('**Converge on the blockers')) || '';
|
|
1351
|
+
ok('code-review.md boundary ask still offers another 2 review rounds', codeReviewBoundary.includes('run another 2 review rounds'));
|
|
1352
|
+
ok('code-review.md boundary ask offers running until it converges', /\(c\) \*\*run until it converges\*\*/.test(codeReviewBoundary));
|
|
1353
|
+
ok('code-review.md converge option stops when no Critical remains', /run until it converges[\s\S]{0,400}no Critical remains/.test(codeReviewBoundary));
|
|
1354
|
+
ok('code-review.md converge option has a no-progress guard that returns to this checkpoint',
|
|
1355
|
+
/run until it converges[\s\S]{0,600}no progress[\s\S]{0,300}back to this checkpoint/.test(codeReviewBoundary));
|
|
1356
|
+
ok('code-review.md boundary ask has no em-dash', !codeReviewBoundary.includes('\u2014'));
|
|
1357
|
+
|
|
862
1358
|
// --- cleanup -------------------------------------------------------------------
|
|
863
1359
|
for (const d of tmpDirs) {
|
|
864
1360
|
fs.rmSync(d, { recursive: true, force: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marcomigozzi/trailhead",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Start and drive large projects as a map of tickets on GitHub Issues. Map-based onboarding + a discuss→plan→execute→verify engine, all on the Issues. Installs the trailhead skill, commands, and hooks into your agent's config dir.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"trailhead": "bin/trailhead.js"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trailhead",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|