@marcomigozzi/trailhead 0.17.0 → 0.18.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.
Files changed (116) hide show
  1. package/README.it.md +28 -18
  2. package/README.md +28 -18
  3. package/bin/base-preflight-docs.test.js +4 -1
  4. package/bin/bulk-cross-reference-docs.test.js +36 -0
  5. package/bin/engine-route-docs.test.js +7 -0
  6. package/bin/facet-stamp-docs.test.js +79 -0
  7. package/bin/forced-mutation-docs.test.js +38 -0
  8. package/bin/guard-axis-docs.test.js +69 -0
  9. package/bin/inbox-needs-info-docs.test.js +68 -0
  10. package/bin/lib/codex-projection.js +8 -8
  11. package/bin/lib/codex-projection.test.js +19 -3
  12. package/bin/needs-you-docs.test.js +9 -2
  13. package/bin/options-in-chat-docs.test.js +65 -0
  14. package/bin/pick-stress-test-docs.test.js +1 -1
  15. package/bin/pipeline-review-docs.test.js +118 -0
  16. package/bin/plan-review-budget-docs.test.js +63 -0
  17. package/bin/repo-target-docs.test.js +41 -0
  18. package/bin/seam-coverage-docs.test.js +46 -0
  19. package/bin/standards-brief-docs.test.js +112 -0
  20. package/bin/test-once-docs.test.js +101 -0
  21. package/bin/test-run-docs.test.js +47 -0
  22. package/bin/trailhead.js +9 -4
  23. package/bin/trailhead.test.js +18 -6
  24. package/bin/yes-accepts-recommended-docs.test.js +54 -0
  25. package/package.json +1 -1
  26. package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
  27. package/plugins/trailhead/agents/trailhead-code-review.md +2 -1
  28. package/plugins/trailhead/agents/trailhead-grill.md +2 -2
  29. package/plugins/trailhead/agents/trailhead-plan.md +1 -1
  30. package/plugins/trailhead/agents/trailhead-verify.md +1 -0
  31. package/plugins/trailhead/hooks/lib/audit-walk.test.js +62 -0
  32. package/plugins/trailhead/hooks/lib/git-env-mod.test.js +55 -0
  33. package/plugins/trailhead/hooks/lib/git-env-spawn.test.js +73 -0
  34. package/plugins/trailhead/hooks/lib/git-env.test.js +51 -0
  35. package/plugins/trailhead/hooks/lib/global-config-dir.test.js +48 -0
  36. package/plugins/trailhead/hooks/lib/heredoc-mod.test.js +85 -0
  37. package/plugins/trailhead/hooks/lib/inbox-needs-info.test.js +179 -0
  38. package/plugins/trailhead/hooks/lib/issue-body.test.js +81 -0
  39. package/plugins/trailhead/hooks/lib/needs-you.test.js +45 -1
  40. package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +272 -0
  41. package/plugins/trailhead/hooks/lib/repo-target.test.js +256 -0
  42. package/plugins/trailhead/hooks/lib/statusline-git-env.test.js +47 -0
  43. package/plugins/trailhead/hooks/lib/test-run.test.js +757 -0
  44. package/plugins/trailhead/hooks/lib/trailhead-git-env.js +40 -0
  45. package/plugins/trailhead/hooks/mod/README.md +30 -14
  46. package/plugins/trailhead/hooks/mod/auto-run.ts +2 -1
  47. package/plugins/trailhead/hooks/mod/git-env.ts +31 -0
  48. package/plugins/trailhead/hooks/mod/heredoc.ts +79 -0
  49. package/plugins/trailhead/hooks/mod/map-pane-view.ts +3 -1
  50. package/plugins/trailhead/hooks/mod/map-pane.tsx +144 -45
  51. package/plugins/trailhead/hooks/mod/repo-pin.ts +105 -4
  52. package/plugins/trailhead/hooks/mod/work-state-locate.ts +2 -1
  53. package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
  54. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +28 -14
  55. package/plugins/trailhead/hooks/trailhead-check-update.js +3 -1
  56. package/plugins/trailhead/hooks/trailhead-labels-guard.js +2 -2
  57. package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +1 -1
  58. package/plugins/trailhead/hooks/trailhead-test-guard.js +232 -0
  59. package/plugins/trailhead/hooks/trailhead-test-guard.test.js +172 -0
  60. package/plugins/trailhead/skills/_shared/capture-core.md +5 -0
  61. package/plugins/trailhead/skills/_shared/configuration-reference.md +12 -0
  62. package/plugins/trailhead/skills/_shared/load-first.md +2 -0
  63. package/plugins/trailhead/skills/_shared/substrate-commands.md +11 -2
  64. package/plugins/trailhead/skills/_shared/substrate.md +3 -2
  65. package/plugins/trailhead/skills/_shared/teamwork.md +5 -3
  66. package/plugins/trailhead/skills/_shared/techniques/code-review-pipeline.md +24 -0
  67. package/plugins/trailhead/skills/_shared/techniques/code-review.md +18 -5
  68. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +2 -2
  69. package/plugins/trailhead/skills/_shared/techniques/debug.md +1 -1
  70. package/plugins/trailhead/skills/_shared/techniques/grilling.md +3 -3
  71. package/plugins/trailhead/skills/_shared/techniques/plan-review.md +16 -4
  72. package/plugins/trailhead/skills/_shared/techniques/tdd.md +4 -2
  73. package/plugins/trailhead/skills/_shared/techniques/verify.md +2 -0
  74. package/plugins/trailhead/skills/_shared/techniques.md +3 -3
  75. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
  76. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
  77. package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +8 -3
  78. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +17 -2
  79. package/plugins/trailhead/skills/trailhead-work/SKILL.md +1 -0
  80. package/plugins/trailhead/skills/trailhead-work/references/auto.md +2 -0
  81. package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +2 -0
  82. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +7 -7
  83. package/plugins/trailhead/templates/statusline-trailhead.sh +10 -3
  84. package/plugins/trailhead/templates/trailhead-audit-walk.js +56 -4
  85. package/plugins/trailhead/templates/trailhead-auto-merge.js +2 -1
  86. package/plugins/trailhead/templates/trailhead-auto-queue.js +2 -1
  87. package/plugins/trailhead/templates/trailhead-auto-slot.js +2 -1
  88. package/plugins/trailhead/templates/trailhead-base-preflight.js +10 -5
  89. package/plugins/trailhead/templates/trailhead-capture.js +2 -1
  90. package/plugins/trailhead/templates/trailhead-checkout-find.js +3 -2
  91. package/plugins/trailhead/templates/trailhead-checkout-in.js +2 -1
  92. package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +2 -1
  93. package/plugins/trailhead/templates/trailhead-git-env.js +40 -0
  94. package/plugins/trailhead/templates/trailhead-inbox-needs-info.js +161 -0
  95. package/plugins/trailhead/templates/trailhead-isolation-answer.js +2 -1
  96. package/plugins/trailhead/templates/trailhead-isolation-copy.js +9 -4
  97. package/plugins/trailhead/templates/trailhead-issue-body.js +92 -6
  98. package/plugins/trailhead/templates/trailhead-merge-ticket.js +2 -1
  99. package/plugins/trailhead/templates/trailhead-needs-you.js +38 -4
  100. package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +261 -7
  101. package/plugins/trailhead/templates/trailhead-repo-target.js +302 -0
  102. package/plugins/trailhead/templates/trailhead-test-run.js +752 -0
  103. package/plugins/trailhead/templates/trailhead-uat-backfill.js +2 -1
  104. package/plugins/trailhead/templates/trailhead-uat-followup.js +2 -1
  105. package/plugins/trailhead/templates/trailhead-uat-setup.js +9 -4
  106. package/plugins/trailhead/templates/trailhead-unblock.js +2 -1
  107. package/plugins/trailhead/tests/auto-run-ui.test.ts +2 -1
  108. package/plugins/trailhead/tests/git-env.test.ts +20 -0
  109. package/plugins/trailhead/tests/map-pane-ui.test.ts +268 -17
  110. package/plugins/trailhead/tests/map-pane-view.test.ts +6 -0
  111. package/plugins/trailhead/tests/map-pane-write.test.ts +57 -1
  112. package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +3 -2
  113. package/plugins/trailhead/tests/repo-pin-relocate.test.ts +175 -0
  114. package/plugins/trailhead/tests/unscrub.ts +8 -0
  115. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +3 -2
  116. package/plugins/trailhead/tests/work-state-worktree.test.ts +2 -1
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #357: a red obtained by forcing a change (mutating code or a
3
+ // fixture) counts only after the change is shown to have landed, stated by the
4
+ // RED step of TDD and by the "watch it fail" step of Debug.
5
+ // Run: node forced-mutation-docs.test.js
6
+ const assert = require('assert');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const techniques = path.join(__dirname, '..', 'plugins', 'trailhead', 'skills', '_shared', 'techniques');
11
+ const read = (...p) => fs.readFileSync(path.join(...p), 'utf8');
12
+
13
+ let passed = 0;
14
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
15
+
16
+ const RULE = 'prove the change landed before trusting the run: diff the mutated file against a clean copy, or re-read it, and see the mutation there';
17
+ const WHY = 'A mutation that silently failed to apply leaves the code as it was, so the run proves nothing';
18
+
19
+ const tdd = read(techniques, 'tdd.md');
20
+ const debug = read(techniques, 'debug.md');
21
+
22
+ const red = tdd.split('\n').find((l) => l.startsWith('- **RED**:')) || '';
23
+ ok('tdd: the RED step carries the rule', red.includes(RULE));
24
+ ok('tdd: the RED step says why', red.includes(WHY));
25
+
26
+ const verifyFix = debug.split('\n').find((l) => l.startsWith('9. **Verify the fix.**')) || '';
27
+ ok('debug: the rule follows "watch it fail"', verifyFix.indexOf(RULE) > verifyFix.indexOf('Watch it fail') && verifyFix.includes('Watch it fail'));
28
+ ok('debug: the step says why', verifyFix.includes(WHY));
29
+
30
+ for (const [name, text] of Object.entries({
31
+ 'tdd.md': tdd,
32
+ 'debug.md': debug,
33
+ 'forced-mutation-docs.test.js': read(__dirname, 'forced-mutation-docs.test.js'),
34
+ })) {
35
+ ok(`${name} has no em-dash`, !text.includes(String.fromCharCode(0x2014)));
36
+ }
37
+
38
+ console.log(`forced-mutation-docs.test.js: ${passed} checks passed`);
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #352: code review has a fifth axis, Impossible-state guards,
3
+ // with an evidence rule, a Warning severity cap and explicit non-findings, and
4
+ // the reviewing agent's brief reaches it.
5
+ // Run: node guard-axis-docs.test.js
6
+ const assert = require('assert');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const root = path.join(__dirname, '..');
11
+ const skills = path.join(root, 'plugins', 'trailhead', 'skills');
12
+ const agents = path.join(root, 'plugins', 'trailhead', 'agents');
13
+ const read = (...p) => fs.readFileSync(path.join(...p), 'utf8');
14
+
15
+ let passed = 0;
16
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
17
+
18
+ const review = read(skills, '_shared', 'techniques', 'code-review.md');
19
+ const techniques = read(skills, '_shared', 'techniques.md');
20
+ const engines = read(skills, 'trailhead-work', 'references', 'ticket-engines.md');
21
+ const agent = read(agents, 'trailhead-code-review.md');
22
+
23
+ const axis = (review.split('\n').find((l) => l.startsWith('- **Impossible-state guards**')) || '');
24
+ const classify = (review.split('\n').find((l) => l.startsWith('Classify each finding')) || '');
25
+ const reuse = (review.split('\n').find((l) => l.startsWith('- **Reuse & simplification**')) || '');
26
+
27
+ ok('code-review.md: review on five axes', review.includes('Review on five axes:'));
28
+ ok('code-review.md: no four-axes leftover', !review.includes('Review on four axes'));
29
+
30
+ ok('axis bullet exists', axis.length > 0);
31
+ for (const sub of ['a guard on an impossible state', 'defence already enforced elsewhere', 'a pattern copied without its premises', 'a stale or phantom requirement', 'an abstraction with one instance']) {
32
+ ok(`axis names subtype: ${sub}`, axis.includes(sub));
33
+ }
34
+ ok('axis extends the speculative generality smell', axis.includes('extends the speculative generality smell'));
35
+
36
+ ok('axis: the guard code is reachable, the state it guards must be shown impossible',
37
+ axis.includes("The guard's own code must be reachable (live), while the state it guards against must be shown impossible"));
38
+ ok('classify: this axis is the exception, what is verified is the impossibility',
39
+ classify.includes('except for Impossible-state guards, where what you verify is that the state cannot occur, not a reachable failing path'));
40
+
41
+ ok('axis: every finding cites concrete evidence the state cannot occur',
42
+ axis.includes('Every finding cites concrete evidence (file:line or a doc) that the state cannot occur'));
43
+ ok('axis: admissible evidence kinds',
44
+ axis.includes('a closed type or enum, an API or framework invariant, fixed configuration, validation already done upstream, or a guarantee of the stack itself'));
45
+ ok('axis: no evidence, no finding, never the reviewer\'s say-so',
46
+ axis.includes("Without such evidence there is no finding, never the reviewer's say-so"));
47
+
48
+ ok('axis: severity cap', axis.includes('at most Warning, never Critical, so it never enters the Critical-only fix-and-re-review loop'));
49
+ ok('classify: severity cap', classify.includes('Impossible-state guards findings are at most Warning, never Critical, so they never enter the Critical-only fix-and-re-review loop'));
50
+
51
+ ok('axis: explicit non-findings',
52
+ axis.includes('Not findings: trust boundaries (user input, network, files, env), deliberate defence-in-depth, test and DI seams, and dead code (a separate smell, not this axis)'));
53
+ ok('axis: exclusions win over the evidence',
54
+ axis.includes('The exclusions win over the evidence: no evidence, upstream validation included, turns a check at a trust boundary into a finding.'));
55
+
56
+ ok('reuse smell list points to the new axis', /speculative generality \(delete; see \*\*Impossible-state guards\*\*\)/.test(reuse));
57
+
58
+ ok('techniques.md: protocol carried in the spawn prompt', techniques.includes('carries its full protocol in the prompt'));
59
+ ok('techniques.md: Code review row on 5 axes', techniques.includes("review the ticket's own commits (its `Refs: #<n>` set) on 5 axes"));
60
+ const verifyLines = engines.split('\n').filter((l) => l.startsWith('4. **Verify**'));
61
+ ok('ticket-engines.md: build and bug Verify found', verifyLines.length === 2);
62
+ ok('ticket-engines.md: both Verify lines reference techniques/code-review.md', verifyLines.every((l) => l.includes('techniques/code-review.md')));
63
+ ok('agent: names the injected single source', agent.includes('injected at spawn from the trailhead skill\'s single source (`_shared/techniques/code-review.md`)'));
64
+
65
+ for (const [name, text] of Object.entries({ 'code-review.md': review, 'techniques.md': techniques, 'guard-axis-docs.test.js': read(__dirname, 'guard-axis-docs.test.js') })) {
66
+ ok(`${name} has no em-dash`, !text.includes(String.fromCharCode(0x2014)));
67
+ }
68
+
69
+ console.log(`guard-axis-docs.test.js: ${passed} checks passed`);
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #354: an inbox issue waiting on its reporter is parked with
3
+ // `trailhead:needs-info`, resurfaces only on the reporter's answer, and is offered
4
+ // for closing (never auto-closed) after inbox.needs_info_days.
5
+ // Plain asserts, no framework. Run: node bin/inbox-needs-info-docs.test.js
6
+ const assert = require('assert');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const root = path.resolve(__dirname, '..');
11
+ const plugin = path.join(root, 'plugins', 'trailhead');
12
+ const read = (rel) => fs.readFileSync(path.join(plugin, rel), 'utf8');
13
+ let passed = 0;
14
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
15
+
16
+ const inbox = read('skills/trailhead-chart/references/inbox.md');
17
+ const commands = read('skills/_shared/substrate-commands.md');
18
+ const substrate = read('skills/_shared/substrate.md');
19
+ const reference = read('skills/_shared/configuration-reference.md');
20
+ const lineOf = (text, re) => (text.split('\n').find((l) => re.test(l)) || '');
21
+
22
+ // Four sections, by descriptive name.
23
+ ok('inbox.md presents four sections', inbox.includes('presents **four sections**') && !inbox.includes('presents **three sections**'));
24
+ ok('inbox.md has the Waiting on the reporter section', inbox.includes('**Waiting on the reporter.**'));
25
+ ok('the waiting section reads through the helper with the templates lookup', /trailhead-inbox-needs-info\.js --repo <owner\/name> --config-root <project root>/.test(inbox) && inbox.includes('same three-dir lookup as `trailhead-work-state.js`'));
26
+ ok('a failed helper never guesses answered or timed out', /never guess which are answered or timed out/.test(inbox));
27
+ ok('the proposal list covers the waiting section', /^For each issue in the first three sections \(incoming issues, waiting on the reporter, parked fog/m.test(inbox));
28
+
29
+ // The Needs info move.
30
+ const needsInfo = inbox.slice(inbox.indexOf('- **Needs info.**'));
31
+ ok('needs info: question posted first, then the label', /post the question \*\*first\*\*, then add `trailhead:needs-info`/.test(needsInfo));
32
+ ok('needs info: ensures the label idempotently', needsInfo.includes('gh label create trailhead:needs-info --color FBCA04') && needsInfo.includes('2>/dev/null || true'));
33
+ ok('resurfacing only on a reporter comment after the label', /only when the reporter comments after the label/.test(needsInfo));
34
+ ok('every move on an answered issue drops the label first', /dropping `trailhead:needs-info` first/.test(needsInfo));
35
+ ok('Ask again removes and re-adds the label', /\*\*Ask again\*\*[^\n]*removes and re-adds the label/.test(needsInfo));
36
+ ok('Keep waiting removes and re-adds the label, stating what a failure leaves', /\*\*Keep waiting\*\* removes and re-adds the label/.test(needsInfo) && /A failed removal keeps the old clock, so the offer returns next run; a failed re-add leaves the issue in the incoming queue/.test(needsInfo));
37
+ ok('a timeout is only an offer, never an automatic close', /only \*\*offered\*\* for closing/.test(needsInfo) && /never closed automatically/.test(needsInfo));
38
+ ok('the label comes off before the close', /remove the label, \*\*then\*\* `gh issue close <n> --reason "not planned"/.test(needsInfo));
39
+ ok('the close message follows ticket.language and asks for a new issue', needsInfo.includes('`config.ticket.language`') && needsInfo.includes('asks for a new issue linking this one'));
40
+ ok('failed steps are reported with the state left behind', /Any failed step of a move is reported naming the issue and the state it was left in/.test(needsInfo));
41
+ ok('the direct /trailhead:inbox <n> path triages whatever the state', /`\/trailhead:inbox <n>` on a needs-info issue triages it directly, whatever its state/.test(needsInfo));
42
+ ok('inbox.md has no em-dash', !inbox.includes(String.fromCharCode(8212)));
43
+
44
+ // The label set.
45
+ const firstUse = lineOf(commands, /^1\. Create any missing labels/);
46
+ ok('first-use setup lists the label among twenty-two', firstUse.includes('all twenty-two:') && firstUse.includes('`trailhead:needs-info`'));
47
+ ok('substrate-commands.md has the idempotent ensure line', commands.includes('gh label create trailhead:needs-info --color FBCA04 --description "Waiting on the reporter: back in the inbox when they answer" 2>/dev/null || true'));
48
+ ok('substrate.md documents the state label and the count', substrate.includes('**`trailhead:needs-info`**') && substrate.includes('twenty-two labels'));
49
+ ok('the labels guard exempts it (not a ticket)', /EXEMPT = \[[^\]]*'trailhead:needs-info'/.test(fs.readFileSync(path.join(plugin, 'hooks', 'trailhead-labels-guard.js'), 'utf8')));
50
+ ok('the label-guard Action strips any trailhead:* by prefix, so it needs no change', /label\.startsWith\('trailhead:'\)/.test(fs.readFileSync(path.join(root, '.github', 'workflows', 'trailhead-label-guard.yml'), 'utf8')));
51
+
52
+ // Config.
53
+ ok('configuration-reference documents inbox.needs_info_days, default 14', /\| `inbox\.needs_info_days` \| positive integer \(\*\*`14`\*\*\)/.test(reference) && reference.includes('**inbox.needs_info_days.**'));
54
+
55
+ // README mirror and site pages.
56
+ for (const f of ['README.md', 'README.it.md', 'site/src/content/docs/docs/configuration.mdx']) {
57
+ ok(`${f} documents inbox.needs_info_days`, fs.readFileSync(path.join(root, f), 'utf8').includes('`inbox.needs_info_days`'));
58
+ }
59
+ for (const f of ['README.md', 'README.it.md', 'site/src/content/docs/docs/concepts.md']) {
60
+ ok(`${f} lists the needs-info state label`, fs.readFileSync(path.join(root, f), 'utf8').includes('`needs-info` ('));
61
+ }
62
+ ok('chart.md shows four sections', fs.readFileSync(path.join(root, 'site/src/content/docs/docs/commands/chart.md'), 'utf8').includes('Shows four sections by name'));
63
+ const chartPage = fs.readFileSync(path.join(root, 'site/src/content/docs/docs/commands/chart.md'), 'utf8');
64
+ ok('chart.md no longer says the inbox decides alone', !chartPage.includes('alone on clear ones'));
65
+ ok('chart.md states the explicit yes', chartPage.includes('Nothing is written without your explicit yes'));
66
+ ok('this test has no em-dash', !fs.readFileSync(__filename, 'utf8').includes(String.fromCharCode(8212)));
67
+
68
+ console.log(`inbox-needs-info-docs.test.js: ${passed} checks passed`);
@@ -108,11 +108,11 @@ Where the engine uses \`AskUserQuestion\`, pose the choice as a **plain-text num
108
108
  After presenting the numbered list, STOP and wait: the user's **next message** is the answer to this pending question, not a fresh unrelated turn. Resolve it by matching either the **number typed** (e.g. "2") or the option's **label text** back onto that option, then continue the engine exactly as it would from a native \`AskUserQuestion\` response (e.g. write the chosen config value, or proceed down the chosen branch). If the reply matches no option, ask again rather than guessing. Never silently pick a default and proceed.
109
109
 
110
110
  ## D. Subagents (native multi_agent)
111
- Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_input\`, \`wait_agent\`, \`resume_agent\`, \`close_agent\`), stable and on by default on supported Codex (the installer gates on the floor that guarantees them). Where the engine delegates to a subagent (research, codebase-map, review, plan, execute, debug, grill), spawn a real Codex subagent with these tools and the technique's own protocol (each technique carries its full protocol in-prompt, so no specialised agent is needed; pass the ticket's \`Scope:\` when relevant). The Claude-only Explore-vs-general-purpose split does not apply: Codex has one subagent kind. A subagent runs as a FRESH session that receives a task prompt, not this conversation; run independent ones in parallel (e.g. the codebase-map readers) and \`wait_agent\` on them.
111
+ Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_input\`, \`wait_agent\`, \`resume_agent\`, \`close_agent\`), stable and on by default on supported Codex (the installer gates on the floor that guarantees them). Where the engine delegates to a subagent (research, codebase-map, review, plan, execute, debug, grill), spawn a real Codex subagent with these tools and the technique's own protocol (each technique carries its full protocol in-prompt, so no specialised agent is needed; pass the ticket's \`Scope:\` when relevant). The Claude-only Explore-vs-general-purpose split does not apply: Codex has one subagent kind. Every engine subagent must start FRESH (the task prompt only, not this conversation): the spawn turns forking off, because without that \`multi_agent_v2\` defaults \`fork_turns\` to \`all\`, a full-history fork that inherits this conversation and ignores the model/effort override. So **every** \`spawn_agent\` (engine agents, the base v1 fallback, and generic spawns such as research or the codebase-map readers) passes \`fork_turns="none"\` when the visible schema exposes \`fork_turns\` (v2), or \`fork_context=false\` when it exposes \`fork_context\` (v1), and neither when the schema exposes neither (no fork option, so pass neither); key it on the context parameter the schema shows, not on whether \`agent_type\` is present. Run independent ones in parallel (e.g. the codebase-map readers) and \`wait_agent\` on them.
112
112
 
113
113
  **Never surrender the session to one open-ended \`wait_agent\`; poll instead.** \`wait_agent\` blocks until the subagent returns, so a single unconditional \`wait_agent\` on a long or stalling subagent freezes the whole session with nothing to check: this is the review / plan-review hang. Collect every delegated subagent through a **bounded wait in a poll loop**, not one open-ended blocking call. Give \`wait_agent\` a short timeout (30 to 60s); on each timeout, report a one-line "still running" progress note and loop again, so the session stays responsive and can be checked or interrupted between polls. The two **review** steps, **Code review** and **Cross-AI plan review**, always run this way: backgrounding and polling them (never a blind \`wait_agent\`) is the engine's hard cross-host default, not merely the case where it matters most. They are also the longest quiet activities in the cycle (a full-diff adversarial review, or an external CLI) and the ones most often seen to hang. If your Codex build's \`wait_agent\` exposes no timeout, poll the agent's status/output at intervals instead of blocking, or run the review as a **background job** and check back on it at intervals; the rule is the same either way. When several subagents run in parallel, poll them as a set, a bounded wait cycling across the outstanding handles, reporting which are still running.
114
114
 
115
- **Per-technique agent registry (\`agent_type\` dispatch).** The installer always projects all 9 committed trailhead agents as \`~/.codex/agents/trailhead-<technique>.toml\` (one per technique) and enables \`features.multi_agent_v2\`, so Codex always exposes a per-technique agent registry. A TOML whose technique has a \`models.codex.<key>\` pin set carries that OpenAI model (plus its reasoning effort); every other TOML (an unpinned keyed technique, or fix with execute unset) is pin-less and its subagent inherits the session model. Detect the registry at runtime by introspecting the visible \`spawn_agent\` tool schema (GSD-style, never a config read): if its parameters expose an \`agent_type\` field, spawn each delegated activity as \`spawn_agent(agent_type="trailhead-<technique>", reasoning_effort=…)\` (\`trailhead-plan\` for the Plan step, \`trailhead-execute\` for Execute, likewise research/review/debug/verify \`trailhead-codebase-map\` for the Codebase-map step and \`trailhead-grill\` for a Grilling round, plus \`trailhead-fix\` for the Fix step, which tracks the Execute pin), which routes it onto that technique's pinned model when one is set, or the session model otherwise. An unpinned or session-equal grill runs inline in the main session (no spawn): the main session stays the HITL orchestrator and asks the user itself. If the \`spawn_agent\` schema exposes NO \`agent_type\` field at all (a Codex build with only base multi_agent v1), spawn with base \`spawn_agent\` and the subagents inherit the one session model. Trust the tool surface you actually see and degrade gracefully: \`multi_agent_v2\` is still under development upstream, so v1 stays a supported runtime.
115
+ **Per-technique agent registry (\`agent_type\` dispatch).** The installer always projects all 9 committed trailhead agents as \`~/.codex/agents/trailhead-<technique>.toml\` (one per technique) and enables \`features.multi_agent_v2\`, so Codex always exposes a per-technique agent registry. A TOML whose technique has a \`models.codex.<key>\` pin set carries that OpenAI model (plus its reasoning effort); every other TOML (an unpinned keyed technique, or fix with execute unset) is pin-less and its subagent inherits the session model. Detect the registry at runtime by introspecting the visible \`spawn_agent\` tool schema (GSD-style, never a config read): if its parameters expose an \`agent_type\` field, spawn each delegated activity as \`spawn_agent(agent_type="trailhead-<technique>", fork_turns="none", reasoning_effort=…)\` (the v2 shape: a schema exposing only \`fork_context\` takes \`fork_context=false\` instead of \`fork_turns="none"\`) (\`trailhead-plan\` for the Plan step, \`trailhead-execute\` for Execute, likewise research/review/debug/verify \`trailhead-codebase-map\` for the Codebase-map step and \`trailhead-grill\` for a Grilling round, plus \`trailhead-fix\` for the Fix step, which tracks the Execute pin), which routes it onto that technique's pinned model when one is set, or the session model otherwise. An unpinned or session-equal grill runs inline in the main session (no spawn): the main session stays the HITL orchestrator and asks the user itself. If the \`spawn_agent\` schema exposes NO \`agent_type\` field at all (a Codex build with only base multi_agent v1), spawn with base \`spawn_agent\` (still with \`fork_context=false\`, or \`fork_turns="none"\` if that is what the schema exposes) and the subagents inherit the one session model. Trust the tool surface you actually see and degrade gracefully: \`multi_agent_v2\` is still under development upstream, so v1 stays a supported runtime.
116
116
 
117
117
  \`config.models.*\` (Claude model ids) still cannot take effect here regardless: Codex's subagent model registry is OpenAI-only, so a Claude id cannot run and those keys collapse to the session model. The honoured namespace on Codex is \`models.codex.*\` (above); the one-time models-collapse notice still applies while a stray \`models.*\` is set and \`models.codex.*\` is empty.
118
118
 
@@ -120,13 +120,13 @@ Codex has a subagent toolkit: the multi_agent tools (\`spawn_agent\`, \`send_inp
120
120
  The engine's \`/clear\` is Codex's \`/new\` (start a fresh session). Command references in a handoff use the skill form (\`$trailhead work <n>\`).
121
121
 
122
122
  ## F. Lifecycle hooks (native Codex hooks)
123
- 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.
123
+ 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, labels-guard and test-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs (the six run in one process, a single \`trailhead-bash-guards.js --guards=commit,secret,install,body,labels,test\` registration, so a Bash call pays one start-up instead of six), 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.
124
124
 
125
125
  ## G. Design / UI mockups: artifacts -> stitch offer, else local disk
126
126
  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.
127
127
 
128
128
  ## H. Shell timeouts & background jobs (plan-review's external CLIs)
129
- The **Cross-AI plan review** technique invokes each external reviewer CLI (\`codex exec\`, \`gemini -p\`, \`qwen -p\`, …) through Claude Code's **Bash tool**, and tells the engine to beat that tool's ~2-minute default cut by setting its \`timeout\` parameter to the 10-minute max, or launching the call with \`run_in_background: true\` and collecting it later. Those are Claude Bash-tool knobs with no Codex equivalent, so map the *intent* onto Codex's own shell/exec tool. Reviews never block the session (the engine's hard cross-host default), so **the default is to background the reviewers** and collect them by polling, never block on them: launch each with a trailing \`&\` (or your shell tool's background form), **redirect its stdin from \`/dev/null\` and its output to a file** (\`codex exec … "<prompt>" < /dev/null > out.txt 2>&1 &\`), and poll/collect when it finishes, so several reviewers run in parallel instead of serially eating the wall-clock; or wrap each reviewer invocation in a §D subagent so the long-running call sits behind the multi_agent poll loop. Where a reviewer must run synchronously instead, give that call its **own long timeout** (the \`timeout_ms\` / equivalent on Codex's shell/exec tool, set to several minutes, never the short default), so a reviewer that legitimately runs for minutes is not cut mid-review. Either way plan-review's own rule is unchanged: never rely on a short default timeout, retry once at the wide margin, then drop a still-slow reviewer rather than let it wedge the build. **Close the reviewer's stdin when backgrounding it** (\`< /dev/null\`): \`codex exec\` reads additional prompt input from stdin and blocks on EOF, so a backgrounded reviewer that inherits an open or attached stdin sits idle at "Reading additional input from stdin..." (0% CPU) forever instead of running (passing the prompt as an argument does not exempt it). Guard the poll loop too: a reviewer pinned at 0% CPU with no output past a threshold is stuck, so surface it and drop it after the one retry rather than reporting "still running" indefinitely. This is the external-CLI **shell** path; §D covers the separate **subagent** (\`wait_agent\`) path, and the two are distinct (an external reviewer CLI is a plain shell call, not a spawned Codex subagent).
129
+ The **Cross-AI plan review** technique invokes each external reviewer CLI (\`codex exec\`, \`gemini -p\`, \`qwen -p\`, …) through Claude Code's **Bash tool**, and tells the engine to beat that tool's ~2-minute default cut by setting its \`timeout\` parameter to the 10-minute max, or launching the call with \`run_in_background: true\` and collecting it later. Those are Claude Bash-tool knobs with no Codex equivalent, so map the *intent* onto Codex's own shell/exec tool. Reviews never block the session (the engine's hard cross-host default), so **the default is to background the reviewers** and collect them by polling, never block on them: launch each through the plan-review **\`run\` subcommand** (\`node <templates>/trailhead-plan-review-rounds.js run --output <dir>/<cli>.out -- <the CLI's headless command>\`), backgrounded with a trailing \`&\` (or your shell tool's background form) and the helper's one JSON line sent to a file (\`… run --output out.txt -- codex exec … "<prompt>" > run.json &\`), then poll/collect when it finishes, so several reviewers run in parallel instead of serially eating the wall-clock; or wrap each \`run\` invocation in a §D subagent so the long-running call sits behind the multi_agent poll loop. The helper is what bounds the call (a 600-second wall-clock deadline, SIGTERM then SIGKILL to the reviewer's process group); nothing else on Codex limits a backgrounded call. Where a reviewer must run synchronously instead, it still goes through \`run\`, and the call gets its **own long timeout** (the \`timeout_ms\` / equivalent on Codex's shell/exec tool, set above the helper's deadline, never the short default), so the helper rather than the tool ends it. Either way plan-review's own rule is unchanged: branch on the JSON \`status\` (a \`timeout\` skips the \`overflow\` check), retry once, then drop a still-slow reviewer, recorded as skipped, rather than let it wedge the build; a reviewer is never reported as "still running" past the bound. **Stdin must be closed on every reviewer call**: \`codex exec\` also reads prompt input from stdin, so a call that inherits an open or attached stdin may wait for EOF (passing the prompt as an argument does not exempt it). The \`run\` helper closes it; the raw fallback (\`timeout -k 10 600 <cmd> < /dev/null > out.txt 2>&1\`, used only when the helper is missing, exits non-zero with no JSON line, or prints non-JSON output; a \`status: error\` JSON line is handled by the \`failed\`/\`error\` branch instead) appends \`< /dev/null\` itself. The line "Reading additional input from stdin..." is not evidence of a stdin hang (codex-cli prints it with stdin already closed), so never diagnose a stall from it. This is the external-CLI **shell** path; §D covers the separate **subagent** (\`wait_agent\`) path, and the two are distinct (an external reviewer CLI is a plain shell call, not a spawned Codex subagent).
130
130
  </codex_skill_adapter>`;
131
131
  }
132
132
 
@@ -381,9 +381,9 @@ function codexVerbSkillPlan(codexHome, verbs) {
381
381
  // --- codexHookEntries -----------------------------------------------------
382
382
  // The guardrail hooks trailhead registers on Codex, as {event, matcher,
383
383
  // command} records. Commands run the copied hook scripts under hooksScriptsDir
384
- // via node, matching Claude's `node "<path>"` form. The five Bash guards
385
- // (commit, secret, install, body, labels) are ONE registration, the merged
386
- // trailhead-bash-guards.js with `--guards=commit,secret,install,body,labels` (a single
384
+ // via node, matching Claude's `node "<path>"` form. The six Bash guards
385
+ // (commit, secret, install, body, labels, test) are ONE registration, the merged
386
+ // trailhead-bash-guards.js with `--guards=commit,secret,install,body,labels,test` (a single
387
387
  // node process per Bash call); secret-read is a separate PreToolUse hook. All
388
388
  // PreToolUse guards can veto; the injection-scanner
389
389
  // is a PostToolUse advisory; check-update runs at SessionStart. Reused by the
@@ -396,7 +396,7 @@ function codexVerbSkillPlan(codexHome, verbs) {
396
396
  function codexHookEntries(hooksScriptsDir) {
397
397
  const cmd = (name) => `node "${path.join(hooksScriptsDir, name)}"`;
398
398
  return [
399
- { event: 'PreToolUse', matcher: 'Bash', command: `${cmd('trailhead-bash-guards.js')} --guards=commit,secret,install,body,labels` },
399
+ { event: 'PreToolUse', matcher: 'Bash', command: `${cmd('trailhead-bash-guards.js')} --guards=commit,secret,install,body,labels,test` },
400
400
  { event: 'PreToolUse', matcher: 'Read|Bash', command: cmd('trailhead-secret-read-guard.js') },
401
401
  { event: 'PostToolUse', matcher: 'Bash', command: cmd('trailhead-issue-injection-scanner.js') },
402
402
  { event: 'Stop', matcher: '', command: cmd('trailhead-mockup-link-stop.js') },
@@ -94,6 +94,18 @@ ok('codexSkillAdapterHeader §D no longer calls per-subagent model pinning defer
94
94
  !header.includes('deferred'));
95
95
  ok('codexSkillAdapterHeader §D pins the exact agent_type dispatch call shape',
96
96
  header.includes('spawn_agent(agent_type="trailhead-<technique>"'));
97
+ ok('codexSkillAdapterHeader §D passes fork_turns="none" (v2) and fork_context=false (v1) on every spawn',
98
+ header.includes('fork_turns="none"') && header.includes('fork_context=false'));
99
+ ok('codexSkillAdapterHeader §D pins the full v2 call shape with fork_turns="none"',
100
+ header.includes('spawn_agent(agent_type="trailhead-<technique>", fork_turns="none"'));
101
+ ok('codexSkillAdapterHeader §D keys the no-fork rule on the context parameter, not on agent_type',
102
+ header.includes('key it on the context parameter the schema shows, not on whether `agent_type` is present'));
103
+ ok('codexSkillAdapterHeader §D passes neither fork parameter when the schema exposes neither',
104
+ header.includes('exposes neither') && header.includes('pass neither'));
105
+ ok('codexSkillAdapterHeader §D says a fork_context-only schema takes fork_context=false in the agent_type call',
106
+ header.includes('v2 shape') && header.includes('takes `fork_context=false` instead'));
107
+ ok('codexSkillAdapterHeader §D no longer claims a subagent is unconditionally FRESH',
108
+ !header.includes('A subagent runs as a FRESH session'));
97
109
  ok('codexSkillAdapterHeader §D keeps the base spawn_agent v1 fallback path',
98
110
  header.includes('base multi_agent v1') && header.includes('inherit the one session model'));
99
111
  ok('codexSkillAdapterHeader §D says all 9 committed agents, names trailhead-grill, and runs an unpinned grill inline',
@@ -119,6 +131,10 @@ ok('codexSkillAdapterHeader §H makes backgrounding reviewers the default',
119
131
  header.includes('the default is to background the reviewers'));
120
132
  ok('codexSkillAdapterHeader §H closes reviewer stdin to avoid the EOF hang',
121
133
  header.includes('/dev/null') && header.toLowerCase().includes('stdin'));
134
+ ok('codexSkillAdapterHeader §H bounds every reviewer call through the run subcommand (#368)',
135
+ header.includes('trailhead-plan-review-rounds.js run --output') && header.includes('timeout_ms') && header.includes('above the helper') &&
136
+ header.includes('a `timeout` skips the `overflow` check') && header.includes('timeout -k 10 600') &&
137
+ header.includes('not evidence of a stdin hang') && !header.includes('0% CPU'));
122
138
 
123
139
  // --- codexVerbSkillContent (#46) ---
124
140
  ok('codexVerbSkillContent: SKILL.md frontmatter is at byte 0 with name+description', (() => {
@@ -175,10 +191,10 @@ const entries = codexHookEntries('/h');
175
191
  ok('codexHookEntries returns 5 entries', Array.isArray(entries) && entries.length === 5);
176
192
  const bashEntries = entries.filter((e) => e.event === 'PreToolUse' && e.matcher === 'Bash');
177
193
  ok('codexHookEntries: exactly one PreToolUse/Bash registration', bashEntries.length === 1);
178
- ok('codexHookEntries: the Bash entry is the merged hook with exactly --guards=commit,secret,install,body,labels',
179
- bashEntries[0].command === 'node "/h/trailhead-bash-guards.js" --guards=commit,secret,install,body,labels');
194
+ ok('codexHookEntries: the Bash entry is the merged hook with exactly --guards=commit,secret,install,body,labels,test',
195
+ bashEntries[0].command === 'node "/h/trailhead-bash-guards.js" --guards=commit,secret,install,body,labels,test');
180
196
  ok('codexHookEntries: no per-guard script is registered on its own', entries.every((e) =>
181
- !/trailhead-(commit|secret|install|search|body)-guard\.js/.test(e.command)));
197
+ !/trailhead-(commit|secret|install|search|body|test)-guard\.js/.test(e.command)));
182
198
  ok('codexHookEntries: secret-read-guard is PreToolUse/Read|Bash', entries.some((e) =>
183
199
  e.event === 'PreToolUse' && e.matcher === 'Read|Bash' && e.command.includes('trailhead-secret-read-guard.js')));
184
200
  ok('codexHookEntries: injection-scanner is PostToolUse/Bash', entries.some((e) =>
@@ -30,10 +30,17 @@ ok('work claim step has the by-hand fallback', claimStep.includes('gh issue edit
30
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
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
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'));
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-two labels'));
34
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
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));
36
+ ok('substrate-commands.md first-use list has twenty-two labels', /all twenty-two:[^\n]*`trailhead:needs-you`/.test(commands));
37
+
38
+ // Un ticket chiuso non tiene mai il mark, comunque sia stato chiuso.
39
+ const SWEEP = 'node <templates>/trailhead-needs-you.js sweep --repo <owner/name>';
40
+ ok('substrate-commands.md: Resolve clears needs-you right after the close', /gh issue close <n>\n[^\n]*\nnode <templates>\/trailhead-needs-you\.js clear --issue <n> --repo <owner\/name>/.test(commands));
41
+ ok('substrate-commands.md documents sweep', commands.includes(SWEEP) && /however it was closed/.test(commands));
42
+ ok('work SKILL.md sweeps at every work-start', work.includes(SWEEP) && /Sweep stale needs-you marks/.test(work));
43
+ ok('auto.md sweeps at run start, before the queue', auto.includes(SWEEP) && /before the queue is read/.test(auto));
37
44
 
38
45
  for (const [name, text] of [['auto.md', auto], ['SKILL.md', work], ['substrate.md', substrate], ['substrate-commands.md', commands]]) {
39
46
  ok(`${name}: no em-dash`, !text.includes(String.fromCharCode(0x2014)));
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #365: options posted on the ticket are the record, never the ask surface.
3
+ // The rule is stated once (Grilling technique); call-sites only point at it.
4
+ // Run: node options-in-chat-docs.test.js
5
+ const assert = require('assert');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const root = path.join(__dirname, '..', 'plugins', 'trailhead');
10
+ const skills = path.join(root, 'skills');
11
+ const read = (rel) => fs.readFileSync(path.join(skills, rel), 'utf8');
12
+
13
+ let passed = 0;
14
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
15
+
16
+ const ANCHOR = 'The ask is answerable from the chat alone';
17
+ const DEFINITION = `**${ANCHOR}.**`;
18
+
19
+ const walk = (dir) => fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
20
+ e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)]);
21
+
22
+ // 1. defined exactly once, in the Grilling technique
23
+ const defs = walk(root).filter((f) => fs.readFileSync(f, 'utf8').includes(DEFINITION));
24
+ ok('definition appears in exactly one file', defs.length === 1);
25
+ ok('definition lives in grilling.md', defs[0] === path.join(skills, '_shared', 'techniques', 'grilling.md'));
26
+ const grilling = read('_shared/techniques/grilling.md');
27
+ ok('definition appears once', grilling.split(DEFINITION).length === 2);
28
+
29
+ // 2. the definition carries the normative fragments
30
+ const rule = grilling.split('\n').find((l) => l.includes(DEFINITION));
31
+ for (const frag of [
32
+ 'an `OPTIONS` comment, or any comment',
33
+ 'the record, never the ask surface',
34
+ 'renders every option asked about in chat',
35
+ 'a one-line summary',
36
+ 'its trade-off',
37
+ 'the recommendation',
38
+ 'structured question tool',
39
+ 'with no option text',
40
+ ]) ok(`rule carries: ${frag}`, rule.includes(frag));
41
+
42
+ // 3. pointers name the rule without restating it
43
+ const pointer = (name, line) => {
44
+ ok(`${name} found`, !!line);
45
+ ok(`${name} references the anchor`, line.includes(ANCHOR));
46
+ ok(`${name} does not restate the rule`, !line.includes('never the ask surface'));
47
+ };
48
+ const engines = read('trailhead-work/references/ticket-engines.md');
49
+ pointer('decision Diverge', engines.split('\n').find((l) => l.startsWith('1. **Diverge')));
50
+ pointer('decision Converge', engines.split('\n').find((l) => l.startsWith('2. **Converge.**')));
51
+ const relay = grilling.split('\n').find((l) => l.startsWith(' - `question`: relay'));
52
+ pointer('grilling relay of question', relay);
53
+ ok('relay covers a question that only names the options', relay.includes('re-render'));
54
+ pointer('chart standalone grill', read('trailhead-chart/SKILL.md').split('\n').find((l) => l.startsWith('Run a standalone **Grilling**')));
55
+ const agent = fs.readFileSync(path.join(root, 'agents', 'trailhead-grill.md'), 'utf8');
56
+ const q = agent.split('\n').find((l) => l.startsWith('- `question:`'));
57
+ pointer('trailhead-grill question field', q);
58
+ ok('question field asks label, summary, trade-off', q.includes('label') && q.includes('summary') && q.includes('trade-off'));
59
+
60
+ // 4. no em-dash
61
+ for (const f of [path.join(skills, '_shared/techniques/grilling.md'), path.join(skills, 'trailhead-work/references/ticket-engines.md'),
62
+ path.join(skills, 'trailhead-chart/SKILL.md'), path.join(root, 'agents', 'trailhead-grill.md'), __filename])
63
+ ok(`${path.basename(f)} has no em-dash`, !fs.readFileSync(f, 'utf8').includes(String.fromCharCode(8212)));
64
+
65
+ console.log(`options-in-chat-docs.test.js: ${passed} checks passed`);
@@ -89,7 +89,7 @@ for (const frag of ['still current and complete', 'Decisions so far', 'status qu
89
89
  // 6. Inbox: nessuna chiusura o commento pubblico senza un si'
90
90
  const inbox = read('trailhead-chart/references/inbox.md');
91
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'));
92
+ const intro = lineOf(inbox, (l) => l.startsWith('For each issue in the first three sections'));
93
93
  for (const frag of ['propose a disposition', 'one list', 'explicit yes', 'never close', 'comment on', 'out-of-scope ruling'])
94
94
  ok(`inbox intro carries: ${frag}`, intro.includes(frag));
95
95
  const dup = lineOf(inbox, (l) => l.startsWith('- **Duplicate.**'));
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #355: code review loads a CI/CD pipeline reliability reference
3
+ // on a pipeline-touching diff only, every finding capped at Warning, and the
4
+ // review brief, the techniques table and the agent all point at it.
5
+ // Run: node pipeline-review-docs.test.js
6
+ const assert = require('assert');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const root = path.join(__dirname, '..');
11
+ const skills = path.join(root, 'plugins', 'trailhead', 'skills');
12
+ const agents = path.join(root, 'plugins', 'trailhead', 'agents');
13
+ const read = (...p) => fs.readFileSync(path.join(...p), 'utf8');
14
+
15
+ let passed = 0;
16
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
17
+
18
+ const refPath = path.join(skills, '_shared', 'techniques', 'code-review-pipeline.md');
19
+ ok('reference file exists', fs.existsSync(refPath));
20
+ const ref = read(refPath);
21
+ const review = read(skills, '_shared', 'techniques', 'code-review.md');
22
+ const techniques = read(skills, '_shared', 'techniques.md');
23
+ const agent = read(agents, 'trailhead-code-review.md');
24
+
25
+ const route = review.split('\n').find((l) => l.startsWith('**First, route it.**')) || '';
26
+ const para = review.split('\n').find((l) => l.startsWith('**Pipeline reliability (only on a pipeline-touching diff)**')) || '';
27
+ const lines = review.split('\n');
28
+ const classifyIdx = lines.findIndex((l) => l.startsWith('Classify each finding'));
29
+ const paraIdx = lines.findIndex((l) => l.startsWith('**Pipeline reliability (only on a pipeline-touching diff)**'));
30
+ const guardIdx = lines.findIndex((l) => l.startsWith('- **Impossible-state guards**'));
31
+
32
+ // The reference: the eight checks.
33
+ for (const sub of [
34
+ '`npm install` where a lockfile exists (use `npm ci` instead)',
35
+ 'a job without `timeout-minutes`',
36
+ 'a deploy without a `concurrency` group',
37
+ 'a rebuild per environment instead of build once, promote',
38
+ 'applies only when the workflow targets more than one environment',
39
+ 'a deploy with neither a health check nor a rollback',
40
+ '`continue-on-error` on a step that later steps depend on',
41
+ 'static cloud keys where the provider supports OIDC',
42
+ 'a third-party action on `@main` or `@latest`',
43
+ ]) {
44
+ ok(`reference check: ${sub}`, ref.includes(sub));
45
+ }
46
+
47
+ ok('reference: every finding is a Warning, never Critical',
48
+ ref.includes('Every finding is a Warning, never Critical, so it never enters the Critical-only fix-and-re-review loop'));
49
+
50
+ for (const sub of [
51
+ 'global tool installs (`npm install -g`)',
52
+ 'reusable workflows (a job calling one takes neither `timeout-minutes` nor steps of its own)',
53
+ 'actions already pinned to a tag',
54
+ 'test-only workflows (a workflow that runs tests and publishes or deploys nothing: none of the checks apply)',
55
+ ]) {
56
+ ok(`reference non-finding: ${sub}`, ref.includes(sub));
57
+ }
58
+
59
+ ok('reference: hardcoded secrets stay Critical under Security',
60
+ ref.includes('Hardcoded secrets are not in this reference: they stay Critical under the Security axis'));
61
+ ok('reference: a deploy finding names what makes it a deploy one',
62
+ ref.includes('A deploy finding names what makes the workflow a deploy one (a quoted command, an `environment:`, a target)'));
63
+ ok('reference: scope is the ticket diff',
64
+ ref.includes("only jobs and steps the ticket's commits add or change, per the Ticket diff rule"));
65
+ ok('reference: not a sixth axis', ref.includes('not a sixth axis'));
66
+
67
+ // The brief: route paragraph.
68
+ ok('route: first-line route structure kept', route.includes('**First, route it.** Run `node <hooks>/lib/model-route.js review'));
69
+ ok('route: spawn prompt carries the pipeline reference line', route.includes('`pipeline reference: <absolute path>`'));
70
+ ok('route: path is code-review-pipeline.md beside the code-review.md just read',
71
+ route.includes('`code-review-pipeline.md` in the same directory as the `code-review.md` you just read by absolute path'));
72
+ ok('route: path only, never the contents', route.includes('never the reference\'s contents'));
73
+
74
+ // The brief: the new paragraph.
75
+ ok('five axes wording kept', review.includes('Review on five axes:'));
76
+ ok('paragraph exists', para.length > 0);
77
+ ok('paragraph sits after the axes and before Classify', guardIdx >= 0 && paraIdx > guardIdx && paraIdx < classifyIdx);
78
+ for (const sub of [
79
+ 'git -C <repo> show --format= --name-only <sha>',
80
+ '`.github/workflows/*`',
81
+ '`Dockerfile*`',
82
+ '`docker-compose*`',
83
+ '(basename, at any depth)',
84
+ 'a script with `deploy` in its file name',
85
+ 'a release script under another name is not matched',
86
+ 'on every pass including the lean inline one',
87
+ 'otherwise do not read it',
88
+ 'Inline, it sits beside this file',
89
+ 'the review is incomplete and fails closed',
90
+ 'a Critical finding `pipeline checks did not run` naming the matched path',
91
+ 'status is `issues_found` and Resolve stays blocked',
92
+ 'discards that incomplete report',
93
+ 'never enters the ledger',
94
+ 're-spawns with the path',
95
+ 'HITL checkpoint naming the path',
96
+ 'It is not a sixth axis',
97
+ ]) {
98
+ ok(`paragraph: ${sub}`, para.includes(sub));
99
+ }
100
+
101
+ // Pointers.
102
+ ok('techniques.md: Code review row on 5 axes kept', techniques.includes("review the ticket's own commits (its `Refs: #<n>` set) on 5 axes"));
103
+ ok('techniques.md: row names the conditional reference', techniques.includes('code-review-pipeline.md'));
104
+ ok('agent: names the reference at the path the spawn names',
105
+ agent.includes('`code-review-pipeline.md`') && agent.includes('the path the spawn names'));
106
+ ok('agent: injected single source kept', agent.includes('injected at spawn from the trailhead skill\'s single source (`_shared/techniques/code-review.md`)'));
107
+
108
+ for (const [name, text] of Object.entries({
109
+ 'code-review-pipeline.md': ref,
110
+ 'code-review.md': review,
111
+ 'techniques.md': techniques,
112
+ 'trailhead-code-review.md': agent,
113
+ 'pipeline-review-docs.test.js': read(__dirname, 'pipeline-review-docs.test.js'),
114
+ })) {
115
+ ok(`${name} has no em-dash`, !text.includes(String.fromCharCode(0x2014)));
116
+ }
117
+
118
+ console.log(`pipeline-review-docs.test.js: ${passed} checks passed`);
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #353: plan review skips a reviewer whose prompt exceeds its context.
3
+ // Plain asserts, no framework. Run: node bin/plan-review-budget-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 review = read('skills/_shared/techniques/plan-review.md');
14
+ const config = read('skills/_shared/configuration-reference.md');
15
+
16
+ ok('plan-review.md: runs the budget call before each reviewer call', /trailhead-plan-review-rounds\.js budget --repo[^\n]*--reviewer[^\n]*--prompt/.test(review));
17
+ ok('plan-review.md: names the three decisions arg, file, skip', /`arg`:[\s\S]*`file`:[\s\S]*`skip`:/.test(review));
18
+ ok('plan-review.md: file passes the pointer, never the brief as one argument', /`file`:[^\n]*`pointer`[^\n]*never the brief itself as one argument/.test(review));
19
+ ok('plan-review.md: notes the E2BIG limit of a single argument', /128 KiB[^\n]*E2BIG/.test(review));
20
+ ok('plan-review.md: skip does not invoke the reviewer and records the reason', /`skip`:\s*do not invoke[^\n]*`reason`/.test(review));
21
+ ok('plan-review.md: captures stdout and stderr together into one output file', /> <dir>\/<cli>\.out 2>&1/.test(review));
22
+ ok('plan-review.md: a shell-tool launch failure is written into the same file', /shell tool itself fails to launch[^\n]*same file/.test(review));
23
+ ok('plan-review.md: runs the overflow call on a failed call', /trailhead-plan-review-rounds\.js overflow --output/.test(review));
24
+ ok('plan-review.md: overflow true means no retry, recorded as skipped with its reason', /`overflow: true`[^\n]*do not retry[^\n]*skipped[^\n]*`reason`/.test(review));
25
+ ok('plan-review.md: the one-retry rule never applies after an overflow', /One retry \(never after a context overflow\)/.test(review));
26
+ ok('plan-review.md: PLAN-REVIEW lists every skipped or dropped reviewer with its reason, never silent', /`PLAN-REVIEW`[^\n]*every reviewer skipped or dropped, with its reason[^\n]*budget skip[^\n]*context overflow[^\n]*second timeout[^\n]*hard error[^\n]*never a silent drop/.test(review));
27
+ ok('plan-review.md: all reviewers skipped falls back to the no-reviewer rule', /every reviewer ends up skipped[^\n]*no-reviewer-left rule/.test(review));
28
+
29
+ ok('configuration-reference.md: has the plan_review.context row', /^\| `plan_review\.context` \|/m.test(config));
30
+ ok('configuration-reference.md: explains the 65% rule, the estimate and the post-check', /\*\*plan_review\.context\.\*\*[^\n]*ceil\(bytes \/ 3\)[^\n]*65%[^\n]*not a guarantee[^\n]*not retried/.test(config));
31
+ ok('configuration-reference.md: documents the per-reviewer merge', /\*\*plan_review\.context\.\*\*[^\n]*merge per reviewer[^\n]*project wins/.test(config));
32
+
33
+ ok('plan-review.md: the overflow call passes the brief so echoed lines are ignored', /overflow --output <dir>\/<cli>\.out --prompt <brief file>/.test(review));
34
+ ok('plan-review.md: a failing budget or overflow helper has a stated fallback', /When `budget` or `overflow` itself fails[^\n]*never guess[^\n]*at most 100 KiB/.test(review));
35
+
36
+ // #368: every reviewer call is bounded by the run helper.
37
+ ok('plan-review.md: every reviewer call goes through the run subcommand, whatever the launch mode', /Every reviewer call goes through the `run` subcommand, whatever the launch mode[^\n]*trailhead-plan-review-rounds\.js run --output <dir>\/<cli>\.out -- /.test(review));
38
+ ok('plan-review.md: the helper bounds the call because the Bash timeout does not apply to a background call', /helper is what bounds[^\n]*`timeout` parameter does not apply to a background call/.test(review));
39
+ ok('plan-review.md: a timeout skips overflow and takes the one-retry rule', /- `timeout`:[^\n]*\*\*Do not run `overflow`\*\*[^\n]*one-retry rule/.test(review));
40
+ ok('plan-review.md: a second timeout drops the reviewer, recorded with the seconds', /A second timeout \*\*drops\*\*[^\n]*`second timeout`[^\n]*seconds/.test(review));
41
+ ok('plan-review.md: failed or error runs overflow then the one-retry rule', /- `failed` or `error`: run `node <templates>\/trailhead-plan-review-rounds\.js overflow/.test(review));
42
+ ok('plan-review.md: the helper-fails fallback names no JSON line and excludes a status error line', /helper is missing, exits non-zero with no JSON line \(a helper killed by a signal prints none\), or prints non-JSON output\*\*[^\n]*`status: error` JSON line is not this case[^\n]*`failed`\/`error` branch/.test(review));
43
+ ok('plan-review.md: no human prompt on this path, never reported as still running past the bound', /No human prompt anywhere on this path[^\n]*never reported as "still running" past the bound/.test(review));
44
+ ok('plan-review.md: a missing run helper falls back to timeout -k 10 600, 124 or 137 meaning timeout', /helper is missing, exits non-zero[^\n]*`timeout -k 10 600 <cmd> < \/dev\/null > <dir>\/<cli>\.out 2>&1`[^\n]*`gtimeout`[^\n]*124 or 137/.test(review));
45
+ ok('plan-review.md: with no bound available the reviewer is skipped, recorded', /neither available, skip the reviewer, recording that no bound was available/.test(review));
46
+ ok('plan-review.md: the stdin line is not evidence of a stdin hang', /`Reading additional input from stdin\.\.\.` is \*\*not evidence of a stdin hang\*\*/.test(review));
47
+ ok('plan-review.md: the synchronous case still goes through run with the tool timeout above the helper deadline', /still goes through `run`[^\n]*`600000`[^\n]*above the helper's deadline/.test(review));
48
+ ok('plan-review.md: the raw-shell 0% CPU stuck check is gone', !/0% CPU/.test(review));
49
+ const techniques = read('skills/_shared/techniques.md');
50
+ ok('techniques.md: the outcome is awaited up to the bound, a stalled plan reviewer ends as a recorded drop', /the outcome always awaited \(up to its bound[^\n]*stalled reviewer ends as a dropped reviewer, recorded in `PLAN-REVIEW`, never an open wait/.test(techniques));
51
+
52
+ const root = path.resolve(__dirname, '..');
53
+ const readme = fs.readFileSync(path.join(root, 'README.md'), 'utf8');
54
+ const readmeIt = fs.readFileSync(path.join(root, 'README.it.md'), 'utf8');
55
+ const site = fs.readFileSync(path.join(root, 'site', 'src', 'content', 'docs', 'docs', 'configuration.mdx'), 'utf8');
56
+ for (const [name, text] of [['README.md', readme], ['README.it.md', readmeIt], ['configuration.mdx', site]]) {
57
+ ok(`${name}: has the plan_review.context row`, /^\| `plan_review\.context` \|/m.test(text));
58
+ }
59
+
60
+ for (const [name, text] of [['plan-review.md', review], ['configuration-reference.md', config], ['techniques.md', techniques]]) {
61
+ ok(`${name}: no em-dash`, !text.includes(String.fromCharCode(0x2014)));
62
+ }
63
+ console.log(`plan-review-budget-docs.test.js: ${passed} checks passed`);