@marcomigozzi/trailhead 0.15.0 → 0.16.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 +45 -9
- package/README.md +46 -10
- package/bin/auto-intro-docs.test.js +72 -0
- package/bin/auto-merge-docs.test.js +33 -0
- package/bin/auto-merge-ticket-docs.test.js +32 -0
- package/bin/auto-review-rounds-docs.test.js +45 -0
- package/bin/auto-run-docs.test.js +60 -0
- package/bin/auto-scope-docs.test.js +33 -0
- package/bin/auto-slots-docs.test.js +45 -0
- package/bin/auto-uat-docs.test.js +47 -0
- package/bin/grilling-holistic-review-docs.test.js +90 -0
- package/bin/lib/codex-projection.js +5 -5
- package/bin/lib/codex-projection.test.js +2 -2
- package/bin/needs-you-docs.test.js +41 -0
- package/bin/pick-stress-test-docs.test.js +105 -0
- package/bin/trailhead.js +3 -3
- package/bin/trailhead.test.js +17 -18
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/commands/auto.md +2 -2
- package/plugins/trailhead/hooks/lib/auto-merge.test.js +467 -0
- package/plugins/trailhead/hooks/lib/auto-queue.test.js +245 -0
- package/plugins/trailhead/hooks/lib/auto-run.test.js +318 -0
- package/plugins/trailhead/hooks/lib/auto-slot.test.js +278 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +173 -0
- package/plugins/trailhead/hooks/lib/dashboard.test.js +66 -3
- package/plugins/trailhead/hooks/lib/merge-ticket.test.js +394 -0
- package/plugins/trailhead/hooks/lib/needs-you.test.js +177 -0
- package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +319 -0
- package/plugins/trailhead/hooks/lib/statusline-context.test.js +140 -0
- package/plugins/trailhead/hooks/lib/uat-followup.test.js +169 -0
- package/plugins/trailhead/hooks/lib/unblock.test.js +207 -0
- package/plugins/trailhead/hooks/mod/README.md +180 -39
- package/plugins/trailhead/hooks/mod/auto-run-view.ts +93 -0
- package/plugins/trailhead/hooks/mod/auto-run.ts +165 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +150 -25
- package/plugins/trailhead/hooks/mod/map-pane.tsx +526 -92
- package/plugins/trailhead/hooks/mod/register.tsx +298 -39
- package/plugins/trailhead/hooks/mod/repo-pin.ts +167 -0
- package/plugins/trailhead/hooks/mod/session-registry.ts +65 -0
- package/plugins/trailhead/hooks/mod/turn-state.ts +14 -0
- package/plugins/trailhead/hooks/mod/whiteboard-follow.ts +98 -0
- package/plugins/trailhead/hooks/mod/work-state-locate.ts +191 -0
- package/plugins/trailhead/hooks/mod/work-state-prompt.ts +13 -15
- package/plugins/trailhead/hooks/mod/work-state-view.ts +21 -5
- package/plugins/trailhead/hooks/mod/worktree-porcelain.ts +17 -0
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +22 -16
- package/plugins/trailhead/hooks/trailhead-labels-guard.js +231 -0
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +212 -0
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +296 -22
- package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +253 -0
- package/plugins/trailhead/skills/_shared/choices.md +7 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +3 -3
- package/plugins/trailhead/skills/_shared/substrate-commands.md +26 -4
- package/plugins/trailhead/skills/_shared/substrate.md +4 -1
- package/plugins/trailhead/skills/_shared/teamwork.md +2 -2
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +2 -2
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +3 -1
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +4 -4
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +2 -2
- package/plugins/trailhead/skills/trailhead-view/SKILL.md +1 -0
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +21 -10
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +70 -22
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +6 -6
- package/plugins/trailhead/templates/statusline-trailhead.sh +77 -12
- package/plugins/trailhead/templates/trailhead-auto-merge.js +318 -0
- package/plugins/trailhead/templates/trailhead-auto-queue.js +186 -0
- package/plugins/trailhead/templates/trailhead-auto-run.js +294 -0
- package/plugins/trailhead/templates/trailhead-auto-slot.js +239 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +88 -0
- package/plugins/trailhead/templates/trailhead-dashboard.js +32 -12
- package/plugins/trailhead/templates/trailhead-merge-ticket.js +366 -0
- package/plugins/trailhead/templates/trailhead-needs-you.js +193 -0
- package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +289 -0
- package/plugins/trailhead/templates/trailhead-uat-followup.js +128 -0
- package/plugins/trailhead/templates/trailhead-unblock.js +111 -0
- package/plugins/trailhead/tests/auto-run-ui.test.ts +444 -0
- package/plugins/trailhead/tests/auto-run-view.test.ts +111 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +1732 -142
- package/plugins/trailhead/tests/map-pane-view.test.ts +308 -14
- package/plugins/trailhead/tests/map-pane-write.test.ts +51 -0
- package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +77 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +123 -0
- package/plugins/trailhead/tests/session-registry.test.ts +50 -0
- package/plugins/trailhead/tests/whiteboard-follow.test.ts +90 -0
- package/plugins/trailhead/tests/work-state-locate.test.ts +33 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +276 -2
- package/plugins/trailhead/tests/work-state-prompt.test.ts +29 -35
- package/plugins/trailhead/tests/work-state-ui.test.ts +73 -1
- package/plugins/trailhead/tests/work-state-view.test.ts +19 -0
- package/plugins/trailhead/tests/work-state-worktree.test.ts +310 -0
- package/plugins/trailhead/types/index.d.ts +26 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test for #295: the autonomous run persists its start-up choices and lists in
|
|
3
|
+
// .trailhead/auto-run.json so it survives a compaction. Plain asserts, no framework.
|
|
4
|
+
// Run: node bin/auto-run-docs.test.js
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const plugin = path.resolve(__dirname, '..', 'plugins', 'trailhead');
|
|
10
|
+
const auto = fs.readFileSync(path.join(plugin, 'skills/trailhead-work/references/auto.md'), 'utf8');
|
|
11
|
+
const gitignore = fs.readFileSync(path.resolve(__dirname, '..', '.gitignore'), 'utf8');
|
|
12
|
+
let passed = 0;
|
|
13
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
14
|
+
|
|
15
|
+
ok('auto.md: read runs at engagement, before the asks', /trailhead-auto-run\.js read --repo <main-checkout>[^\n]*before (any|the) ask/i.test(auto));
|
|
16
|
+
ok('auto.md: found with sameSession true is reused without asking and announced', /`sameSession: true`[^\n]*reuse it without asking/.test(auto) && /announced in one chat line naming the reused scope, slots and review/.test(auto));
|
|
17
|
+
ok('auto.md: another session\'s record asks once to resume or start fresh', /`sameSession: false`[^\n]*run still active elsewhere[^\n]*ask once/.test(auto) && /no delegate option/.test(auto) && /replaces the three asks/.test(auto));
|
|
18
|
+
ok('auto.md: resume reloads the skip and unmerged lists', /reload the record's skip and unmerged lists/.test(auto));
|
|
19
|
+
ok('auto.md: an explicit <n> that differs starts fresh with start --replace', /explicit `\/trailhead:auto <n>`[^\n]*differs[^\n]*starts a fresh run[^\n]*start --replace/.test(auto));
|
|
20
|
+
ok('auto.md: corrupt is reported then start --replace', /`corrupt`[^\n]*start --replace[^\n]*plain `start` would answer `exists`/.test(auto));
|
|
21
|
+
ok('auto.md: start runs after the last ask and before anything is claimed', /trailhead-auto-run\.js start --repo <main-checkout>[^\n]*--scope[^\n]*--slots[^\n]*--review/.test(auto) && /right after the last start-up ask and before anything is claimed/.test(auto));
|
|
22
|
+
ok('auto.md: the record is gitignored', auto.includes('.trailhead/auto-run.json') && /\.gitignore/.test(auto) && /^\.trailhead\/auto-run\.json$/m.test(gitignore));
|
|
23
|
+
ok('auto.md: every skipped outcome is reported and never blocks', /Any `skipped` result[^\n]*one chat line[^\n]*the run continues/.test(auto) && /skipped `read` is treated as `absent`/.test(auto) && /safety net, not a gate/.test(auto));
|
|
24
|
+
ok('auto.md: skip runs on each skip-list add', /trailhead-auto-run\.js skip --repo <main-checkout> --ticket <n>/.test(auto) && /every time a ticket joins the run's skip list/i.test(auto));
|
|
25
|
+
ok('auto.md: unmerged is recorded right after the abort, before the tracker call', /trailhead-auto-run\.js unmerged --repo <main-checkout> --ticket <n> --branch trailhead\/t<n>[^\n]*--file/.test(auto) && /before any tracker call/.test(auto));
|
|
26
|
+
ok('auto.md: unmerged again with --merge-ticket after the add succeeds', /--merge-ticket <n>/.test(auto) && /after the merge-ticket `add` succeeds/.test(auto));
|
|
27
|
+
ok('auto.md: a crash or failed add still leaves the branch on the persisted list', /still leaves the branch on the persisted unmerged list/.test(auto));
|
|
28
|
+
const parallel = auto.slice(auto.indexOf('## Parallel slots'), auto.indexOf('## Parking a ticket'));
|
|
29
|
+
const outside = auto.replace(parallel, '');
|
|
30
|
+
ok('auto.md: a general compaction rule sits outside Parallel slots and holds whatever the slot count and sameSession', /After a context compaction mid-run[^\n]*whatever the slot count[^\n]*trailhead-auto-run\.js read --repo <main-checkout>[^\n]*whatever `sameSession` says[^\n]*without asking/.test(outside) && /scope, slots, review choice, skip and unmerged lists/.test(outside));
|
|
31
|
+
ok('auto.md: the orchestrator knows it is mid-run from its own context', /mid-run from its own context/.test(outside));
|
|
32
|
+
ok('auto.md: the sameSession ask applies only at engagement', /`sameSession` ask applies only at engagement/.test(auto) && /fresh `\/trailhead:auto` invocation/.test(auto));
|
|
33
|
+
ok('auto.md: no session id host note (Codex)', /`sameSession` is always false on a host with no session id \(Codex\)/.test(auto) && /re-invocation after a crash always gets the resume-or-fresh ask/.test(auto) && /mid-run compaction never does/.test(auto));
|
|
34
|
+
ok('auto.md: the slot table bullet points at the general rule', /slot table from `trailhead-auto-slot\.js list`[^\n]*general compaction rule/.test(parallel) && !/trailhead-auto-run\.js read/.test(parallel));
|
|
35
|
+
ok('auto.md: run end unites the in-memory lists with the record', /in-memory skip and unmerged lists[^\n]*united with the record's/.test(auto));
|
|
36
|
+
ok('auto.md: run end clears the record, then resets the work states', /trailhead-auto-run\.js clear --repo <main-checkout>[^\n]*then[^\n]*--auto false/.test(auto) && /Clearing first means a crash between the two/.test(auto));
|
|
37
|
+
ok('auto.md: a host interrupt writes nothing, so Esc leaves the crash signature', !/graceful interrupt clears it too/.test(auto) && !/a graceful interrupt clears the record/.test(auto) && /same on disk as a crash/.test(auto) && /signature of both an Esc and a crash/.test(auto));
|
|
38
|
+
ok('auto.md: after Esc the same session reuses the record, a new session is asked again', /Esc[^\n]*reuses the record/.test(auto) && /new session[^\n]*(asked again|resume-or-fresh)/.test(auto));
|
|
39
|
+
ok('auto.md: step 2 writes the missing PAUSED checkpoint and label before resuming a crash-signature ticket', /Reconstruct the missing checkpoint/.test(auto) && /reconstructed at resume/.test(auto) && /adds `trailhead:paused` \(after the comment, so a failed comment never leaves a label without a checkpoint\)/.test(auto) && /resumes the ticket through the `resume` engine, which drops the label/.test(auto) && /no second checkpoint/.test(auto));
|
|
40
|
+
ok('auto.md: no run-end summary at a host interrupt, the resumed run covers it', /not emitted at the interrupt/.test(auto) && /resumed run's own (run-end )?summary/.test(auto));
|
|
41
|
+
ok('auto.md: crash signature and idempotence are stated on the latest checkpoint', /whose latest checkpoint is not a `PAUSED` comment/.test(auto) && /skipped only when the latest checkpoint is already a `PAUSED` comment or the label is present/.test(auto));
|
|
42
|
+
ok('auto.md: a slot ticket at the resolve step is exempt from reconstruction', /work state step is `resolve`[^\n]*exempt from (the )?reconstruction/.test(auto));
|
|
43
|
+
ok('auto.md: reconstruction happens when the ticket is resumed, not for a waiting extra', /Reconstruction happens when the ticket is actually resumed[^\n]*untouched/.test(auto));
|
|
44
|
+
const docs = {
|
|
45
|
+
'README.md': 'README.md',
|
|
46
|
+
'README.it.md': 'README.it.md',
|
|
47
|
+
'workflow.md': 'site/src/content/docs/docs/workflow.md',
|
|
48
|
+
'commands/work.md': 'site/src/content/docs/docs/commands/work.md',
|
|
49
|
+
};
|
|
50
|
+
const root = path.resolve(__dirname, '..');
|
|
51
|
+
for (const [name, rel] of Object.entries(docs)) {
|
|
52
|
+
const t = fs.readFileSync(path.join(root, rel), 'utf8');
|
|
53
|
+
const it = name === 'README.it.md';
|
|
54
|
+
ok(`${name}: no interrupt-time PAUSED checkpoint promise`, !/(Stop\/Esc|Esc or Stop|Esc)[^\n]*(keeps its claim and gets|keeps its claim with|mantiene il claim e riceve)[^\n]*`PAUSED`/.test(t) && !/gets a `PAUSED` checkpoint naming/.test(t));
|
|
55
|
+
ok(`${name}: Esc writes nothing and the next auto writes the missing PAUSED checkpoint`, it
|
|
56
|
+
? /Esc[^\n]*non scrive nulla[^\n]*stessa traccia di un crash[^\n]*`\/trailhead:auto`[^\n]*checkpoint `PAUSED` mancante[^\n]*workspace e branch/.test(t) && /stessa sessione[^\n]*riusa[^\n]*nuova sessione[^\n]*chiede di nuovo/.test(t)
|
|
57
|
+
: /Esc[^\n]*writes nothing[^\n]*same trace as a crash[^\n]*`\/trailhead:auto`[^\n]*missing `PAUSED` checkpoint[^\n]*workspace and branch/.test(t) && /same session[^\n]*reuses[^\n]*new session[^\n]*asks again/.test(t));
|
|
58
|
+
}
|
|
59
|
+
ok('auto.md: no em-dash', !auto.includes(String.fromCharCode(0x2014)));
|
|
60
|
+
console.log(`auto-run-docs.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test for #280: auto.md documents the run scope (asked at start, `<n>` skips),
|
|
3
|
+
// the queue order rule, the helper, the helper-error stop, and the report split.
|
|
4
|
+
// Run: node auto-scope-docs.test.js
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const plugin = path.join(__dirname, '..', 'plugins', 'trailhead');
|
|
10
|
+
const read = (rel) => fs.readFileSync(path.join(plugin, rel), 'utf8');
|
|
11
|
+
|
|
12
|
+
let passed = 0;
|
|
13
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
14
|
+
|
|
15
|
+
const auto = read('skills/trailhead-work/references/auto.md');
|
|
16
|
+
for (const needle of [
|
|
17
|
+
'**Pick the run scope.**', 'trailhead-auto-queue.js', "trailhead-auto-queue.js scopes", '`AskUserQuestion`', 'one map, the whiteboard, or everything',
|
|
18
|
+
'`/trailhead:auto <n>`', 'without asking', 'no delegate option', 'before anything is claimed',
|
|
19
|
+
'trailhead-auto-queue.js queue --scope', '--skip', '`next`', 'whiteboard last', 'ascending map number', 'oldest workable ticket first',
|
|
20
|
+
'A helper error stops the run', '`drift`', '`resume`', '--map none',
|
|
21
|
+
'each touched map gets a comment with **only its own part**', 'the whiteboard part stays in chat only', 'a map the run never touched gets no comment',
|
|
22
|
+
]) ok(`auto.md names: ${needle}`, auto.includes(needle));
|
|
23
|
+
|
|
24
|
+
for (const rel of ['skills/trailhead-work/references/auto.md', 'skills/trailhead-work/SKILL.md', 'commands/auto.md', 'templates/trailhead-auto-queue.js']) {
|
|
25
|
+
ok(`${rel} has no em-dash`, !read(rel).includes('—'));
|
|
26
|
+
}
|
|
27
|
+
for (const rel of ['skills/trailhead-work/SKILL.md', 'commands/auto.md']) {
|
|
28
|
+
ok(`${rel} describes a chosen scope`, /run scope/.test(read(rel)));
|
|
29
|
+
ok(`${rel} no longer says "the map's frontier"`, !read(rel).includes("the map's frontier"));
|
|
30
|
+
}
|
|
31
|
+
ok('commands/auto.md keeps the [map] hint', read('commands/auto.md').includes('argument-hint: "[map]"'));
|
|
32
|
+
|
|
33
|
+
console.log(`auto-scope-docs.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test for #283: the autonomous run asks for parallel slots and works each slot
|
|
3
|
+
// ticket in its own worktree. Plain asserts, no framework. Run: node bin/auto-slots-docs.test.js
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const plugin = path.resolve(__dirname, '..', 'plugins', 'trailhead');
|
|
9
|
+
const read = (rel) => fs.readFileSync(path.join(plugin, rel), 'utf8');
|
|
10
|
+
let passed = 0;
|
|
11
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
12
|
+
|
|
13
|
+
const auto = read('skills/trailhead-work/references/auto.md');
|
|
14
|
+
const work = read('skills/trailhead-work/SKILL.md');
|
|
15
|
+
const team = read('skills/_shared/teamwork.md');
|
|
16
|
+
|
|
17
|
+
ok('auto.md asks the slot count, 1 to 4, proposed 2', /Parallel slots: 1, 2, 3 or 4/.test(auto) && /\*\*2 proposed\*\*/.test(auto) && auto.includes('AskUserQuestion'));
|
|
18
|
+
ok('auto.md asks the slot count even with <n>', /`\/trailhead:auto <n>` skips only the scope ask: it \*\*still asks the slot count\*\*/.test(auto));
|
|
19
|
+
ok('auto.md slot ask is the human\'s, no delegate', /there is no delegate option, and it is asked before anything is claimed/.test(auto));
|
|
20
|
+
ok('auto.md names the open helper call with --base', auto.includes('trailhead-auto-slot.js open --repo <main-checkout> --ticket <n> --base <sha>'));
|
|
21
|
+
ok('auto.md names the list helper call', auto.includes('trailhead-auto-slot.js list --repo <main-checkout>'));
|
|
22
|
+
ok('auto.md handles clone-required', auto.includes('clone-required') && auto.includes('checkout -b trailhead/t<n> <base>'));
|
|
23
|
+
ok('auto.md writes sessionless slot work state', auto.includes('env -u CLAUDE_CODE_SESSION_ID node <templates>/trailhead-work-state.js set --repo <workspace>'));
|
|
24
|
+
ok('auto.md resumes a ticket in its own workspace', /resumed \*\*in that workspace\*\*, whatever the slot count/.test(auto) && auto.includes('left alone'));
|
|
25
|
+
ok('auto.md: a slot never writes the active-map marker', /A slot never writes the `\.trailhead\/active-map` marker/.test(auto) && /every map-scoped call[^.]*names the map explicitly/.test(auto));
|
|
26
|
+
ok('auto.md: isolation override above 1 slot, none and auto included', /Above 1 slot[^.]*a worktree whatever `isolation:` says\*\* \(`none` and `auto` included\)/.test(auto));
|
|
27
|
+
ok('auto.md: background dispatch, nothing inline', auto.includes('background subagent') && auto.includes('nothing runs inline'));
|
|
28
|
+
ok('auto.md: in-flight tickets join --skip when filling a slot', /in-flight ticket/.test(auto) && /--skip <csv>` with the run's skip list \*\*plus every in-flight ticket\*\*/.test(auto));
|
|
29
|
+
ok('auto.md: integration held under git: pr and for a clone slot, branch and workspace kept', /Integration stays held under `git: pr`, and for a clone slot/.test(auto) && /parked needs-you for integration/.test(auto) && /\*\*kept\*\*/.test(auto));
|
|
30
|
+
ok('auto.md: no interrupt-time PAUSED for slot tickets, resume reconstructs it per workspace, run end per workspace', !/every in-flight slot ticket\*\* gets its `PAUSED` checkpoint/.test(auto) && /naming the slot workspace path and branch/.test(auto) && auto.includes('each remaining workspace'));
|
|
31
|
+
|
|
32
|
+
ok('auto.md: every set on a slot workspace is sessionless', /every `set` on a slot workspace[^.]*`env -u CLAUDE_CODE_SESSION_ID`/.test(auto));
|
|
33
|
+
ok('auto.md: the run-end reset on a workspace is sessionless too', /env -u CLAUDE_CODE_SESSION_ID node <templates>\/trailhead-work-state\.js set --repo <workspace> --auto false/.test(auto));
|
|
34
|
+
ok('auto.md: open statuses explained (reentered clone as is, stale worktree parks needs-you)', /`reentered` clone is used as is/.test(auto) && /stale worktree[\s\S]{0,400}park it needs-you/.test(auto) && auto.includes('git worktree prune'));
|
|
35
|
+
|
|
36
|
+
for (const [name, text] of [['teamwork.md', team], ['SKILL.md', work]]) {
|
|
37
|
+
ok(`${name}: no longer says the run is always the current checkout`, !/sequential work, no concurrency|works tickets sequentially/.test(text));
|
|
38
|
+
ok(`${name}: names the worktree per ticket above one slot`, /current checkout with one parallel slot and to a worktree per ticket above one/.test(text));
|
|
39
|
+
}
|
|
40
|
+
ok('teamwork.md worktree bullet names the slot helper', team.includes('through the `trailhead-auto-slot.js` helper'));
|
|
41
|
+
|
|
42
|
+
for (const [name, text] of [['auto.md', auto], ['SKILL.md', work], ['teamwork.md', team]]) {
|
|
43
|
+
ok(`${name}: no em-dash`, !text.includes(String.fromCharCode(0x2014)));
|
|
44
|
+
}
|
|
45
|
+
console.log(`auto-slots-docs.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test for #281: an autonomous run resolves a ticket through a UAT follow-up
|
|
3
|
+
// (found first, reused on resume), writes every record when it arises, and documents
|
|
4
|
+
// the hard-stop guarantee. Run: node auto-uat-docs.test.js
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const plugin = path.join(__dirname, '..', 'plugins', 'trailhead');
|
|
10
|
+
const read = (rel) => fs.readFileSync(path.join(plugin, rel), 'utf8');
|
|
11
|
+
|
|
12
|
+
const DASH = String.fromCharCode(0x2014);
|
|
13
|
+
let passed = 0;
|
|
14
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
15
|
+
|
|
16
|
+
const AUTO = 'skills/trailhead-work/references/auto.md';
|
|
17
|
+
const ACCEPT = 'skills/_shared/techniques/acceptance-testing.md';
|
|
18
|
+
const ENGINES = 'skills/trailhead-work/references/ticket-engines.md';
|
|
19
|
+
const SUBSTRATE = 'skills/_shared/substrate-commands.md';
|
|
20
|
+
|
|
21
|
+
const auto = read(AUTO);
|
|
22
|
+
for (const needle of [
|
|
23
|
+
'trailhead-uat-followup.js find', '## Records as it goes', 'never the only record', 'UAT of:', 'trailhead:uat',
|
|
24
|
+
'A helper error is not a "none found"', 'Only an open follow-up is reused', 'closed ones never count',
|
|
25
|
+
'a crash in the instant between the claim and the work-state write', 'leftover work state', 'whose work state has `auto: true`', 'already names the same action', 'its own checklist is already the record', 'trailhead-work-state.js',
|
|
26
|
+
'loses nothing that has arisen', 'UAT follow-ups opened this run',
|
|
27
|
+
'each touched map gets a comment with **only its own part**', 'the whiteboard part stays in chat only', 'a map the run never touched gets no comment',
|
|
28
|
+
]) ok(`auto.md names: ${needle}`, auto.includes(needle));
|
|
29
|
+
ok('auto.md no longer sets a ticket aside for a human-only UAT step', !auto.includes('leaves that step incomplete and sets the ticket aside'));
|
|
30
|
+
ok('auto.md names the needs-you label (#282)', auto.includes('trailhead:needs-you'));
|
|
31
|
+
|
|
32
|
+
const accept = read(ACCEPT);
|
|
33
|
+
for (const needle of ['trailhead-uat-followup.js find', 'autonomous run', 'auto.md', 'opens no duplicate'])
|
|
34
|
+
ok(`acceptance-testing.md names: ${needle}`, accept.includes(needle));
|
|
35
|
+
|
|
36
|
+
const engines = read(ENGINES);
|
|
37
|
+
ok('ticket-engines.md names the autonomous exception in build and bug Resolve', engines.split('Under an autonomous run the guard takes option (b) itself').length - 1 >= 2);
|
|
38
|
+
|
|
39
|
+
const substrate = read(SUBSTRATE);
|
|
40
|
+
for (const needle of ['trailhead-uat-followup.js find --parent <n> --repo <owner/name>', '"reuse"', 'paginated GraphQL `repository.issues` query', 'lagging search index'])
|
|
41
|
+
ok(`substrate-commands.md names: ${needle}`, substrate.includes(needle));
|
|
42
|
+
|
|
43
|
+
for (const rel of [AUTO, ACCEPT, ENGINES, SUBSTRATE, 'templates/trailhead-uat-followup.js', 'hooks/lib/uat-followup.test.js'])
|
|
44
|
+
ok(`${rel} has no em-dash`, !read(rel).includes(DASH));
|
|
45
|
+
ok('this test file has no em-dash', !fs.readFileSync(__filename, 'utf8').includes(DASH));
|
|
46
|
+
|
|
47
|
+
console.log(`auto-uat-docs.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test for #301: every grilling close runs a holistic review pass before the final
|
|
3
|
+
// confirmation. The rule is stated once (Grilling technique); call-sites only point at it.
|
|
4
|
+
// Run: node grilling-holistic-review-docs.test.js
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const skills = path.join(__dirname, '..', 'plugins', 'trailhead', 'skills');
|
|
10
|
+
const read = (rel) => fs.readFileSync(path.join(skills, rel), 'utf8');
|
|
11
|
+
|
|
12
|
+
let passed = 0;
|
|
13
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
14
|
+
|
|
15
|
+
const ANCHOR = 'Holistic review before the final confirmation';
|
|
16
|
+
const DEFINITION = `**${ANCHOR}.**`;
|
|
17
|
+
|
|
18
|
+
const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
|
|
19
|
+
e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]);
|
|
20
|
+
|
|
21
|
+
// 1. the bold definition exists exactly once, in the Grilling technique
|
|
22
|
+
const defs = walk(skills).filter((f) => fs.readFileSync(f, 'utf8').includes(DEFINITION));
|
|
23
|
+
ok('definition appears in exactly one file', defs.length === 1);
|
|
24
|
+
ok('definition lives in grilling.md', defs[0] === path.join(skills, '_shared', 'techniques', 'grilling.md'));
|
|
25
|
+
const grilling = read('_shared/techniques/grilling.md');
|
|
26
|
+
ok('definition appears once', grilling.split(DEFINITION).length === 2);
|
|
27
|
+
|
|
28
|
+
// 2. the definition bullet carries the normative fragments
|
|
29
|
+
const lines = grilling.split('\n');
|
|
30
|
+
const rule = lines.find((l) => l.includes(DEFINITION));
|
|
31
|
+
for (const frag of [
|
|
32
|
+
'Once the frontier is empty and before the final confirmation',
|
|
33
|
+
'as a whole',
|
|
34
|
+
'against each other and against the destination',
|
|
35
|
+
'proposed amendments',
|
|
36
|
+
'non-working combinations',
|
|
37
|
+
'contradictions, gaps, choices that break each other or the destination',
|
|
38
|
+
'by question number',
|
|
39
|
+
'say so in one line and go straight to the confirmation',
|
|
40
|
+
'accept, amend or reject',
|
|
41
|
+
'reject keeps the choices as they were',
|
|
42
|
+
'consciously accepted risk',
|
|
43
|
+
'reopens that branch',
|
|
44
|
+
'the review runs again on the new set',
|
|
45
|
+
're-confirmed in one play-back',
|
|
46
|
+
'at most once more',
|
|
47
|
+
'Only then is the final set played back, confirmed and recorded',
|
|
48
|
+
'`AskUserQuestion` menu',
|
|
49
|
+
'without the delegate option',
|
|
50
|
+
]) ok(`rule carries: ${frag}`, rule.includes(frag));
|
|
51
|
+
|
|
52
|
+
// 3. the Stop bullet is followed by the review
|
|
53
|
+
const stop = lines.find((l) => l.startsWith('- **Stop only when the frontier is empty'));
|
|
54
|
+
ok('stop bullet found', !!stop);
|
|
55
|
+
ok('stop bullet references the anchor', stop.includes(ANCHOR));
|
|
56
|
+
ok('stop bullet scopes the hold to the final set', stop.includes('the final set is not confirmed or recorded before it has run'));
|
|
57
|
+
|
|
58
|
+
// 4. call-sites only point at the rule
|
|
59
|
+
const pointer = (name, text, startsWith) => {
|
|
60
|
+
const l = text.split('\n').find((x) => x.startsWith(startsWith));
|
|
61
|
+
ok(`${name} found`, !!l);
|
|
62
|
+
ok(`${name} references the anchor`, l.includes(ANCHOR));
|
|
63
|
+
ok(`${name} does not restate the rule`, !l.includes('accepted risk') && !l.includes('reject keeps'));
|
|
64
|
+
};
|
|
65
|
+
pointer('chart SKILL standalone grill', read('trailhead-chart/SKILL.md'), 'Run a standalone **Grilling**');
|
|
66
|
+
const engines = read('trailhead-work/references/ticket-engines.md');
|
|
67
|
+
pointer('decision Converge', engines, '2. **Converge.**');
|
|
68
|
+
pointer('build Discuss', engines, '1. **Discuss**');
|
|
69
|
+
ok('build Discuss records the set after the review', engines.split('\n').find((l) => l.startsWith('1. **Discuss**')).includes('after the review'));
|
|
70
|
+
const charting = read('trailhead-chart/references/charting.md');
|
|
71
|
+
pointer('charting Mode 1 step 3', charting, '3. **Create the map**');
|
|
72
|
+
pointer('charting Mode 1-bis step 2', charting, '2. **Name the destination of the remaining stretch.**');
|
|
73
|
+
pointer('charting Mode 1-bis step 4', charting, '4. **Map the frontier of the remainder**');
|
|
74
|
+
ok('charting Mode 1 step 3 covers the destination too', charting.split('\n').find((l) => l.startsWith('3. **Create the map**')).includes('step 1 destination'));
|
|
75
|
+
const choices = read('_shared/choices.md');
|
|
76
|
+
const row = choices.split('\n').find((l) => l.startsWith('| Grilling holistic review'));
|
|
77
|
+
ok('choices row found', !!row);
|
|
78
|
+
const cells = row.split('|').map((c) => c.trim());
|
|
79
|
+
ok('choices row delegate No', cells[2].startsWith('No'));
|
|
80
|
+
ok('choices row defer No', cells[3] === 'No');
|
|
81
|
+
const prose = choices.split('\n').find((l) => l.startsWith('**Prose / conversational call-sites'));
|
|
82
|
+
ok('prose paragraph references the anchor', prose.includes(ANCHOR));
|
|
83
|
+
|
|
84
|
+
// 5. no em-dash in the touched files
|
|
85
|
+
for (const rel of ['_shared/techniques/grilling.md', '_shared/choices.md', 'trailhead-chart/SKILL.md',
|
|
86
|
+
'trailhead-work/references/ticket-engines.md', 'trailhead-chart/references/charting.md'])
|
|
87
|
+
ok(`${rel} has no em-dash`, !read(rel).includes(String.fromCharCode(8212)));
|
|
88
|
+
ok('this test has no em-dash', !fs.readFileSync(__filename, 'utf8').includes(String.fromCharCode(8212)));
|
|
89
|
+
|
|
90
|
+
console.log(`grilling-holistic-review-docs.test.js: ${passed} checks passed`);
|
|
@@ -113,7 +113,7 @@ Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_inp
|
|
|
113
113
|
The engine's \`/clear\` is Codex's \`/new\` (start a fresh session). Command references in a handoff use the skill form (\`$trailhead work <n>\`).
|
|
114
114
|
|
|
115
115
|
## F. Lifecycle hooks (native Codex hooks)
|
|
116
|
-
Codex has a hook bus, so trailhead's guardrails run as **real Codex hooks**, not degraded prose. The installer registers them in \`~/.codex/hooks.json\`: commit-guard, secret-guard, install-guard and
|
|
116
|
+
Codex has a hook bus, so trailhead's guardrails run as **real Codex hooks**, not degraded prose. The installer registers them in \`~/.codex/hooks.json\`: commit-guard, secret-guard, install-guard, body-guard and labels-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs (the five run in one process, a single \`trailhead-bash-guards.js --guards=commit,secret,install,body,labels\` registration, so a Bash call pays one start-up instead of five), secret-read-guard as a \`PreToolUse\` (matcher \`Read|Bash\`) hook that denies reads of \`.env\`/\`.env.*\`/\`.secrets\`, the injection-scanner as a \`PostToolUse\` (matcher \`Bash\`) advisory, mockup-link-stop as a \`Stop\` hook (when your final reply asks to approve a mockup without carrying its link, it continues the turn so you rewrite the ask with the URL or file path in the prose), and check-update as a \`SessionStart\` hook. On Codex, file reads happen through shell commands rather than a native Read-tool event, so the secret-read guard's enforcement here runs via its **Bash** arm (protection is not lost); the \`Read\` half of its matcher is a Claude-Code-native path, registered on Codex too only so it fires without changes if a future Codex version adds an equivalent event. search-guard is Claude-Code-specific (it only prevents arming Claude's \`Read()\` deny rule under bypass permissions, a mechanism Codex lacks), so it is not projected to Codex at all. They use the same wire format they use on Claude Code (JSON on stdin, a \`decision\`/\`permissionDecision\` verdict on stdout), which Codex accepts. The host-independent git \`commit-msg\` hook is still installed at repo first-use as well (defence in depth), exactly as on Claude. Codex gates hooks behind \`features.hooks = true\` and reviews them for trust on first start: the installer enables the flag, and Codex will ask you to trust trailhead's hooks the next time it starts.
|
|
117
117
|
|
|
118
118
|
## G. Design / UI mockups: artifacts -> stitch offer, else local disk
|
|
119
119
|
The \`artifacts\` mockup mode (a Design canvas Artifact built with Claude Code's \`Artifact\` tool; the legacy \`claude.ai/design\` value means the same) does not exist on Codex: there is no \`Artifact\` tool here. When \`config.design\` names it, follow the Prototype technique's fallback: **offer once to switch this project to \`design: stitch\`** (the Google Stitch MCP runs on Codex too), and only on decline produce a throwaway static HTML mockup next to the code (\`disk\`), link it from the ticket, and say why. Never try to create or edit an Artifact on Codex. \`config.design.approval\` still applies unchanged: \`explicit\` waits for the user's go-ahead before any UI code, \`auto\` surfaces the mockup and proceeds.
|
|
@@ -366,9 +366,9 @@ function codexVerbSkillPlan(codexHome, verbs) {
|
|
|
366
366
|
// --- codexHookEntries -----------------------------------------------------
|
|
367
367
|
// The guardrail hooks trailhead registers on Codex, as {event, matcher,
|
|
368
368
|
// command} records. Commands run the copied hook scripts under hooksScriptsDir
|
|
369
|
-
// via node, matching Claude's `node "<path>"` form. The
|
|
370
|
-
// (commit, secret, install, body) are ONE registration, the merged
|
|
371
|
-
// trailhead-bash-guards.js with `--guards=commit,secret,install,body` (a single
|
|
369
|
+
// via node, matching Claude's `node "<path>"` form. The five Bash guards
|
|
370
|
+
// (commit, secret, install, body, labels) are ONE registration, the merged
|
|
371
|
+
// trailhead-bash-guards.js with `--guards=commit,secret,install,body,labels` (a single
|
|
372
372
|
// node process per Bash call); secret-read is a separate PreToolUse hook. All
|
|
373
373
|
// PreToolUse guards can veto; the injection-scanner
|
|
374
374
|
// is a PostToolUse advisory; check-update runs at SessionStart. Reused by the
|
|
@@ -381,7 +381,7 @@ function codexVerbSkillPlan(codexHome, verbs) {
|
|
|
381
381
|
function codexHookEntries(hooksScriptsDir) {
|
|
382
382
|
const cmd = (name) => `node "${path.join(hooksScriptsDir, name)}"`;
|
|
383
383
|
return [
|
|
384
|
-
{ event: 'PreToolUse', matcher: 'Bash', command: `${cmd('trailhead-bash-guards.js')} --guards=commit,secret,install,body` },
|
|
384
|
+
{ event: 'PreToolUse', matcher: 'Bash', command: `${cmd('trailhead-bash-guards.js')} --guards=commit,secret,install,body,labels` },
|
|
385
385
|
{ event: 'PreToolUse', matcher: 'Read|Bash', command: cmd('trailhead-secret-read-guard.js') },
|
|
386
386
|
{ event: 'PostToolUse', matcher: 'Bash', command: cmd('trailhead-issue-injection-scanner.js') },
|
|
387
387
|
{ event: 'Stop', matcher: '', command: cmd('trailhead-mockup-link-stop.js') },
|
|
@@ -173,8 +173,8 @@ const entries = codexHookEntries('/h');
|
|
|
173
173
|
ok('codexHookEntries returns 5 entries', Array.isArray(entries) && entries.length === 5);
|
|
174
174
|
const bashEntries = entries.filter((e) => e.event === 'PreToolUse' && e.matcher === 'Bash');
|
|
175
175
|
ok('codexHookEntries: exactly one PreToolUse/Bash registration', bashEntries.length === 1);
|
|
176
|
-
ok('codexHookEntries: the Bash entry is the merged hook with exactly --guards=commit,secret,install,body',
|
|
177
|
-
bashEntries[0].command === 'node "/h/trailhead-bash-guards.js" --guards=commit,secret,install,body');
|
|
176
|
+
ok('codexHookEntries: the Bash entry is the merged hook with exactly --guards=commit,secret,install,body,labels',
|
|
177
|
+
bashEntries[0].command === 'node "/h/trailhead-bash-guards.js" --guards=commit,secret,install,body,labels');
|
|
178
178
|
ok('codexHookEntries: no per-guard script is registered on its own', entries.every((e) =>
|
|
179
179
|
!/trailhead-(commit|secret|install|search|body)-guard\.js/.test(e.command)));
|
|
180
180
|
ok('codexHookEntries: secret-read-guard is PreToolUse/Read|Bash', entries.some((e) =>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test for #282: the needs-you label (an autonomous run parks a ticket for the
|
|
3
|
+
// human) is documented where the run, the claim and the substrate depend on it.
|
|
4
|
+
// Plain asserts, no framework. Run: node bin/needs-you-docs.test.js
|
|
5
|
+
const assert = require('assert');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const plugin = path.resolve(__dirname, '..', 'plugins', 'trailhead');
|
|
10
|
+
const read = (rel) => fs.readFileSync(path.join(plugin, rel), 'utf8');
|
|
11
|
+
let passed = 0;
|
|
12
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
13
|
+
|
|
14
|
+
const auto = read('skills/trailhead-work/references/auto.md');
|
|
15
|
+
const work = read('skills/trailhead-work/SKILL.md');
|
|
16
|
+
const substrate = read('skills/_shared/substrate.md');
|
|
17
|
+
const commands = read('skills/_shared/substrate-commands.md');
|
|
18
|
+
|
|
19
|
+
for (const needle of ['trailhead:needs-you', 'trailhead-needs-you.js park', '`NEEDS YOU`', 'needs-you ticket', 'queue skips it', 'does not reconsider it', 'surviving blocking review concern', 'stale-base stop', 'a failing UAT setup'.replace('a failing', 'failing')]) {
|
|
20
|
+
ok(`auto.md names: ${needle}`, auto.includes(needle));
|
|
21
|
+
}
|
|
22
|
+
ok('auto.md no longer says "set aside" / "set-aside"', !/set[- ]aside|sets? (?:it|that|such a|the) (?:ticket )?aside|sets? .{0,20} aside/i.test(auto));
|
|
23
|
+
|
|
24
|
+
const claimStep = (work.match(/^2\. Choose the ticket\..*$/m) || [''])[0];
|
|
25
|
+
const quickBullet = (work.match(/^- \*\*Same isolation setup as `work`\.\*\*.*$/m) || [''])[0];
|
|
26
|
+
const CLEAR = 'node <templates>/trailhead-needs-you.js clear --issue <n> --repo <owner/name>';
|
|
27
|
+
ok('work claim step clears needs-you with the helper', claimStep.includes(CLEAR));
|
|
28
|
+
ok('quick isolation bullet clears needs-you with the helper', quickBullet.includes(CLEAR));
|
|
29
|
+
ok('work claim step has the by-hand fallback', claimStep.includes('gh issue edit <n> --remove-label trailhead:needs-you'));
|
|
30
|
+
ok('a failed clear is reported, never blocking the claim (work)', /failed `clear` is reported to the user in one line but never blocks the claim/.test(claimStep));
|
|
31
|
+
ok('a failed clear is reported, never blocking the claim (quick)', /failed `clear` is reported to the user in one line but never blocks the claim/.test(quickBullet));
|
|
32
|
+
|
|
33
|
+
ok('substrate.md documents the label, off every run, on the human frontier', /\*\*`trailhead:needs-you`\*\*[\s\S]{0,900}not a frontier exclusion for humans/.test(substrate) && substrate.includes('twenty-one labels'));
|
|
34
|
+
ok('substrate-commands.md ensures the label idempotently', commands.includes('gh label create trailhead:needs-you --color D93F0B --description "Needs you: an autonomous run parked it for the human" --force'));
|
|
35
|
+
ok('substrate-commands.md documents park and clear', commands.includes('trailhead-needs-you.js park --issue <n> --reason <text> --repo <owner/name>') && commands.includes('trailhead-needs-you.js clear --issue <n> --repo <owner/name>'));
|
|
36
|
+
ok('substrate-commands.md first-use list has twenty-one labels', /all twenty-one:[^\n]*`trailhead:needs-you`/.test(commands));
|
|
37
|
+
|
|
38
|
+
for (const [name, text] of [['auto.md', auto], ['SKILL.md', work], ['substrate.md', substrate], ['substrate-commands.md', commands]]) {
|
|
39
|
+
ok(`${name}: no em-dash`, !text.includes(String.fromCharCode(0x2014)));
|
|
40
|
+
}
|
|
41
|
+
console.log(`needs-you-docs.test.js: ${passed} checks passed`);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Docs test: the agent never offers or acts on its own pick unchecked. The stress-test rule is
|
|
3
|
+
// stated once (choices.md) and pointed at by grilling, the delegate option, auto and the Plan;
|
|
4
|
+
// the Plan carries a Done-when coverage check, decision Diverge re-checks a framed option set,
|
|
5
|
+
// and inbox never closes or answers someone else's issue without the user's yes.
|
|
6
|
+
// Run: node pick-stress-test-docs.test.js
|
|
7
|
+
const assert = require('assert');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const skills = path.join(__dirname, '..', 'plugins', 'trailhead', 'skills');
|
|
12
|
+
const read = (rel) => fs.readFileSync(path.join(skills, rel), 'utf8');
|
|
13
|
+
|
|
14
|
+
let passed = 0;
|
|
15
|
+
const ok = (name, cond) => { assert.ok(cond, name); passed++; };
|
|
16
|
+
|
|
17
|
+
const ANCHOR = 'Stress-test the pick before offering it';
|
|
18
|
+
const DEFINITION = `**${ANCHOR}.**`;
|
|
19
|
+
|
|
20
|
+
const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
|
|
21
|
+
e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]);
|
|
22
|
+
const lineOf = (text, pred) => text.split('\n').find(pred);
|
|
23
|
+
|
|
24
|
+
// 1. la definizione esiste una sola volta, in choices.md
|
|
25
|
+
const defs = walk(skills).filter((f) => fs.readFileSync(f, 'utf8').includes(DEFINITION));
|
|
26
|
+
ok('definition appears in exactly one file', defs.length === 1);
|
|
27
|
+
ok('definition lives in choices.md', defs[0] === path.join(skills, '_shared', 'choices.md'));
|
|
28
|
+
const choices = read('_shared/choices.md');
|
|
29
|
+
ok('definition appears once', choices.split(DEFINITION).length === 2);
|
|
30
|
+
|
|
31
|
+
// 2. la definizione porta i frammenti normativi
|
|
32
|
+
const rule = lineOf(choices, (l) => l.includes(DEFINITION));
|
|
33
|
+
for (const frag of [
|
|
34
|
+
'a recommended answer in grilling',
|
|
35
|
+
'`(Recommended)`',
|
|
36
|
+
'the delegate option',
|
|
37
|
+
'an option `auto` takes on its own',
|
|
38
|
+
'the edge case or failure mode where it breaks',
|
|
39
|
+
'checked against the code rather than assumed',
|
|
40
|
+
'the choices already settled',
|
|
41
|
+
'real holes, not manufactured doubt',
|
|
42
|
+
'One pass, no loop',
|
|
43
|
+
'present only that',
|
|
44
|
+
'Weak point:',
|
|
45
|
+
'still recommended because',
|
|
46
|
+
'conversation language',
|
|
47
|
+
'where no human sees it live',
|
|
48
|
+
'trivial or already-decided choice',
|
|
49
|
+
]) ok(`rule carries: ${frag}`, rule.includes(frag));
|
|
50
|
+
|
|
51
|
+
// 3. i call-site puntano alla regola senza ripeterla
|
|
52
|
+
const pointer = (name, line) => {
|
|
53
|
+
ok(`${name} found`, !!line);
|
|
54
|
+
ok(`${name} references the anchor`, line.includes(ANCHOR));
|
|
55
|
+
ok(`${name} does not restate the rule`, !line.includes('manufactured doubt'));
|
|
56
|
+
};
|
|
57
|
+
const grilling = read('_shared/techniques/grilling.md');
|
|
58
|
+
const gl = grilling.split('\n');
|
|
59
|
+
const pickIdx = gl.findIndex((l) => l.startsWith('- **One question at a time, with your pick.**'));
|
|
60
|
+
ok('grilling pick bullet found', pickIdx >= 0);
|
|
61
|
+
pointer('grilling self-check bullet', gl[pickIdx + 1]);
|
|
62
|
+
ok('grilling bullet keeps the per-question check', gl[pickIdx + 1].includes('by question number'));
|
|
63
|
+
ok('grilling pick bullet points at the self-check', gl[pickIdx].includes('stress-tested per the next bullet'));
|
|
64
|
+
pointer('delegate step 2', lineOf(choices, (l) => l.startsWith('2. The agent states **which option it would choose')));
|
|
65
|
+
const auto = read('trailhead-work/references/auto.md');
|
|
66
|
+
const autoLine = lineOf(auto, (l) => l.startsWith('- **Advisory / process choice**'));
|
|
67
|
+
pointer('auto advisory choice', autoLine);
|
|
68
|
+
ok('auto records the weak point', autoLine.includes('`Weak point:` line'));
|
|
69
|
+
const engines = read('trailhead-work/references/ticket-engines.md');
|
|
70
|
+
const plan = lineOf(engines, (l) => l.startsWith('2. **Plan**'));
|
|
71
|
+
pointer('build Plan', plan);
|
|
72
|
+
|
|
73
|
+
// 4. Plan: copertura del Done when prima di ready
|
|
74
|
+
for (const frag of [
|
|
75
|
+
'**Coverage check before `ready`.**',
|
|
76
|
+
'every `Done when` criterion',
|
|
77
|
+
'a criterion no step delivers is a gap',
|
|
78
|
+
'return `needs_input`',
|
|
79
|
+
'the short inline plan too',
|
|
80
|
+
]) ok(`plan carries: ${frag}`, plan.includes(frag));
|
|
81
|
+
|
|
82
|
+
// 5. Diverge: un set di opzioni gia' inquadrato viene ricontrollato
|
|
83
|
+
const diverge = lineOf(engines, (l) => l.startsWith('1. **Diverge'));
|
|
84
|
+
ok('diverge found', !!diverge);
|
|
85
|
+
ok('diverge no longer skips blindly', !diverge.includes('skip straight to grilling when it did.'));
|
|
86
|
+
for (const frag of ['still current and complete', 'Decisions so far', 'status quo', '`OPTIONS` comment'])
|
|
87
|
+
ok(`diverge carries: ${frag}`, diverge.includes(frag));
|
|
88
|
+
|
|
89
|
+
// 6. Inbox: nessuna chiusura o commento pubblico senza un si'
|
|
90
|
+
const inbox = read('trailhead-chart/references/inbox.md');
|
|
91
|
+
ok('inbox drops "on your own for clear ones"', !inbox.includes('on your own for clear ones'));
|
|
92
|
+
const intro = lineOf(inbox, (l) => l.startsWith('For each issue in the first two sections'));
|
|
93
|
+
for (const frag of ['propose a disposition', 'one list', 'explicit yes', 'never close', 'comment on', 'out-of-scope ruling'])
|
|
94
|
+
ok(`inbox intro carries: ${frag}`, intro.includes(frag));
|
|
95
|
+
const dup = lineOf(inbox, (l) => l.startsWith('- **Duplicate.**'));
|
|
96
|
+
for (const frag of ['what each one covers', 'partial overlap', 'not a duplicate'])
|
|
97
|
+
ok(`inbox duplicate carries: ${frag}`, dup.includes(frag));
|
|
98
|
+
|
|
99
|
+
// 7. niente em-dash
|
|
100
|
+
for (const rel of ['_shared/choices.md', '_shared/techniques/grilling.md', 'trailhead-work/references/auto.md',
|
|
101
|
+
'trailhead-work/references/ticket-engines.md', 'trailhead-chart/references/inbox.md'])
|
|
102
|
+
ok(`${rel} has no em-dash`, !read(rel).includes(String.fromCharCode(8212)));
|
|
103
|
+
ok('this test has no em-dash', !fs.readFileSync(__filename, 'utf8').includes(String.fromCharCode(8212)));
|
|
104
|
+
|
|
105
|
+
console.log(`pick-stress-test-docs.test.js: ${passed} checks passed`);
|
package/bin/trailhead.js
CHANGED
|
@@ -243,10 +243,10 @@ function claudePaths(configDir) {
|
|
|
243
243
|
settings: path.join(configDir, 'settings.json'),
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
|
-
// The
|
|
246
|
+
// The six Bash guard modules. They are shipped, but only require()d by the
|
|
247
247
|
// merged trailhead-bash-guards.js (one process per Bash call), never registered
|
|
248
248
|
// on their own. Legacy installs registered each one; the installers strip those.
|
|
249
|
-
const BASH_GUARD_MODULES = ['trailhead-commit-guard.js', 'trailhead-secret-guard.js', 'trailhead-install-guard.js', 'trailhead-search-guard.js', 'trailhead-body-guard.js'];
|
|
249
|
+
const BASH_GUARD_MODULES = ['trailhead-commit-guard.js', 'trailhead-secret-guard.js', 'trailhead-install-guard.js', 'trailhead-search-guard.js', 'trailhead-body-guard.js', 'trailhead-labels-guard.js'];
|
|
250
250
|
// Every hook script copied into the hooks dir (shipped runtime files).
|
|
251
251
|
const HOOK_FILES = [...BASH_GUARD_MODULES, 'trailhead-bash-guards.js', 'trailhead-issue-injection-scanner.js', 'trailhead-secret-read-guard.js', 'trailhead-mockup-link-guard.js', 'trailhead-mockup-link-stop.js', 'trailhead-check-update.js'];
|
|
252
252
|
// The registered entrypoints: the shipped files that must appear in
|
|
@@ -436,7 +436,7 @@ function installClaude(configDir, { useSymlink }) {
|
|
|
436
436
|
|
|
437
437
|
const s = readJSON(P.settings);
|
|
438
438
|
// Migration: an earlier install registered each Bash guard on its own; the
|
|
439
|
-
// merged entrypoint below replaces all
|
|
439
|
+
// merged entrypoint below replaces all six.
|
|
440
440
|
stripLegacyBashGuards(s);
|
|
441
441
|
const added = [
|
|
442
442
|
addHook(s, 'PreToolUse', 'Bash', hookCmd('trailhead-bash-guards.js')),
|