@marcomigozzi/trailhead 0.13.0 → 0.15.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 (120) hide show
  1. package/README.it.md +142 -29
  2. package/README.md +141 -28
  3. package/bin/base-preflight-docs.test.js +65 -0
  4. package/bin/execute-split-docs.test.js +146 -0
  5. package/bin/isolation-answer-docs.test.js +41 -0
  6. package/bin/isolation-copy-docs.test.js +28 -0
  7. package/bin/lib/codex-projection.js +17 -10
  8. package/bin/lib/codex-projection.test.js +80 -11
  9. package/bin/lib/install-verify.js +10 -5
  10. package/bin/lib/install-verify.test.js +51 -0
  11. package/bin/report-status-docs.test.js +195 -0
  12. package/bin/resolve-helpers-docs.test.js +36 -0
  13. package/bin/scope-confirm-skip-docs.test.js +70 -0
  14. package/bin/trailhead.js +40 -28
  15. package/bin/trailhead.test.js +503 -6
  16. package/bin/uat-setup-docs.test.js +65 -0
  17. package/package.json +4 -1
  18. package/plugins/trailhead/.claude-plugin/plugin.json +3 -2
  19. package/plugins/trailhead/agents/trailhead-code-review.md +15 -2
  20. package/plugins/trailhead/agents/trailhead-codebase-map.md +20 -1
  21. package/plugins/trailhead/agents/trailhead-debug.md +20 -1
  22. package/plugins/trailhead/agents/trailhead-executor.md +29 -2
  23. package/plugins/trailhead/agents/trailhead-fix.md +29 -2
  24. package/plugins/trailhead/agents/trailhead-plan.md +20 -1
  25. package/plugins/trailhead/agents/trailhead-research.md +22 -3
  26. package/plugins/trailhead/agents/trailhead-verify.md +15 -2
  27. package/plugins/trailhead/commands/panel.md +10 -0
  28. package/plugins/trailhead/hooks/hooks.json +3 -18
  29. package/plugins/trailhead/hooks/lib/base-preflight.test.js +513 -0
  30. package/plugins/trailhead/hooks/lib/body-sources.js +795 -0
  31. package/plugins/trailhead/hooks/lib/commit-message-check.js +1 -1
  32. package/plugins/trailhead/hooks/lib/commit-message-check.test.js +3 -2
  33. package/plugins/trailhead/hooks/lib/commit-msg-module-repo.test.js +116 -0
  34. package/plugins/trailhead/hooks/lib/commit-msg-sync.test.js +100 -9
  35. package/plugins/trailhead/hooks/lib/dashboard.test.js +409 -0
  36. package/plugins/trailhead/hooks/lib/fixtures/commit-msg-legacy-js +104 -0
  37. package/plugins/trailhead/hooks/lib/gh-subcommand.js +92 -10
  38. package/plugins/trailhead/hooks/lib/gh-subcommand.test.js +55 -0
  39. package/plugins/trailhead/hooks/lib/isolation-answer.test.js +258 -0
  40. package/plugins/trailhead/hooks/lib/isolation-copy.test.js +298 -0
  41. package/plugins/trailhead/hooks/lib/issue-body.test.js +470 -0
  42. package/plugins/trailhead/hooks/lib/mockup-link.js +21 -5
  43. package/plugins/trailhead/hooks/lib/report-status.test.js +366 -0
  44. package/plugins/trailhead/hooks/lib/shell-scan.js +532 -65
  45. package/plugins/trailhead/hooks/lib/shell-scan.test.js +434 -0
  46. package/plugins/trailhead/hooks/lib/uat-backfill.test.js +150 -0
  47. package/plugins/trailhead/hooks/lib/uat-setup.test.js +340 -0
  48. package/plugins/trailhead/hooks/lib/unmodelled.js +345 -0
  49. package/plugins/trailhead/hooks/lib/unmodelled.test.js +173 -0
  50. package/plugins/trailhead/hooks/lib/work-state.test.js +297 -0
  51. package/plugins/trailhead/hooks/mod/README.md +219 -0
  52. package/plugins/trailhead/hooks/mod/map-pane-view.ts +606 -0
  53. package/plugins/trailhead/hooks/mod/map-pane.tsx +832 -0
  54. package/plugins/trailhead/hooks/mod/register.tsx +293 -0
  55. package/plugins/trailhead/hooks/mod/work-state-prompt.ts +69 -0
  56. package/plugins/trailhead/hooks/mod/work-state-view.ts +110 -0
  57. package/plugins/trailhead/hooks/trailhead-bash-guards.js +140 -0
  58. package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +233 -0
  59. package/plugins/trailhead/hooks/trailhead-body-guard.js +145 -61
  60. package/plugins/trailhead/hooks/trailhead-body-guard.test.js +224 -3
  61. package/plugins/trailhead/hooks/trailhead-commit-guard.js +41 -14
  62. package/plugins/trailhead/hooks/trailhead-commit-guard.test.js +2 -1
  63. package/plugins/trailhead/hooks/trailhead-install-guard.js +187 -42
  64. package/plugins/trailhead/hooks/trailhead-install-guard.test.js +182 -2
  65. package/plugins/trailhead/hooks/trailhead-mockup-link-guard.js +48 -11
  66. package/plugins/trailhead/hooks/trailhead-mockup-link-guard.test.js +75 -3
  67. package/plugins/trailhead/hooks/trailhead-search-guard.js +59 -13
  68. package/plugins/trailhead/hooks/trailhead-search-guard.test.js +68 -3
  69. package/plugins/trailhead/hooks/trailhead-secret-guard.js +541 -57
  70. package/plugins/trailhead/hooks/trailhead-secret-guard.test.js +1000 -4
  71. package/plugins/trailhead/hooks/trailhead-secret-read-guard.js +171 -6
  72. package/plugins/trailhead/hooks/trailhead-secret-read-guard.test.js +170 -0
  73. package/plugins/trailhead/skills/_shared/choices.md +7 -3
  74. package/plugins/trailhead/skills/_shared/configuration-reference.md +15 -3
  75. package/plugins/trailhead/skills/_shared/load-first.md +3 -3
  76. package/plugins/trailhead/skills/_shared/multi-map.md +3 -3
  77. package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
  78. package/plugins/trailhead/skills/_shared/substrate-commands.md +22 -11
  79. package/plugins/trailhead/skills/_shared/substrate.md +6 -4
  80. package/plugins/trailhead/skills/_shared/teamwork.md +17 -6
  81. package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +17 -9
  82. package/plugins/trailhead/skills/_shared/techniques/code-review.md +1 -1
  83. package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +1 -1
  84. package/plugins/trailhead/skills/_shared/techniques/debug.md +2 -0
  85. package/plugins/trailhead/skills/_shared/techniques/prototype.md +1 -1
  86. package/plugins/trailhead/skills/_shared/techniques/research.md +4 -2
  87. package/plugins/trailhead/skills/_shared/techniques/verify.md +1 -1
  88. package/plugins/trailhead/skills/_shared/techniques.md +12 -2
  89. package/plugins/trailhead/skills/trailhead/SKILL.md +3 -3
  90. package/plugins/trailhead/skills/trailhead-capture/SKILL.md +1 -1
  91. package/plugins/trailhead/skills/trailhead-capture/references/capture.md +2 -2
  92. package/plugins/trailhead/skills/trailhead-chart/SKILL.md +1 -1
  93. package/plugins/trailhead/skills/trailhead-chart/references/charting.md +1 -1
  94. package/plugins/trailhead/skills/trailhead-manage/SKILL.md +1 -1
  95. package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +1 -1
  96. package/plugins/trailhead/skills/trailhead-view/SKILL.md +8 -6
  97. package/plugins/trailhead/skills/trailhead-work/SKILL.md +16 -13
  98. package/plugins/trailhead/skills/trailhead-work/references/auto.md +6 -4
  99. package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +29 -0
  100. package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +28 -10
  101. package/plugins/trailhead/templates/trailhead-base-preflight.js +305 -0
  102. package/plugins/trailhead/templates/trailhead-commit-msg +38 -102
  103. package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +58 -27
  104. package/plugins/trailhead/templates/trailhead-commit-msg.cjs +114 -0
  105. package/plugins/trailhead/templates/trailhead-dashboard.js +340 -0
  106. package/plugins/trailhead/templates/trailhead-isolation-answer.js +227 -0
  107. package/plugins/trailhead/templates/trailhead-isolation-copy.js +239 -0
  108. package/plugins/trailhead/templates/trailhead-issue-body.js +459 -0
  109. package/plugins/trailhead/templates/trailhead-report-status.js +274 -0
  110. package/plugins/trailhead/templates/trailhead-uat-backfill.js +114 -0
  111. package/plugins/trailhead/templates/trailhead-uat-setup.js +385 -0
  112. package/plugins/trailhead/templates/trailhead-work-state.js +217 -0
  113. package/plugins/trailhead/tests/map-pane-ui.test.ts +1921 -0
  114. package/plugins/trailhead/tests/map-pane-view.test.ts +853 -0
  115. package/plugins/trailhead/tests/mod.test.ts +14 -0
  116. package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +207 -0
  117. package/plugins/trailhead/tests/work-state-prompt.test.ts +109 -0
  118. package/plugins/trailhead/tests/work-state-ui.test.ts +365 -0
  119. package/plugins/trailhead/tests/work-state-view.test.ts +110 -0
  120. package/plugins/trailhead/types/index.d.ts +103 -0
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #245: the skill prose names the isolation-answer helper's check/record
3
+ // reuse for the `auto` work-start ask, its fingerprint contract and the 8-hour session.
4
+ // Run: node isolation-answer-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, '..');
10
+ const skills = path.join(root, 'plugins', 'trailhead', 'skills');
11
+
12
+ let passed = 0;
13
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
14
+
15
+ const teamwork = fs.readFileSync(path.join(skills, '_shared', 'teamwork.md'), 'utf8');
16
+ const workSkill = fs.readFileSync(path.join(skills, 'trailhead-work', 'SKILL.md'), 'utf8');
17
+ const choices = fs.readFileSync(path.join(skills, '_shared', 'choices.md'), 'utf8');
18
+ const gitignore = fs.readFileSync(path.join(root, '.gitignore'), 'utf8');
19
+
20
+ for (const [name, text] of [['teamwork.md', teamwork], ['trailhead-work/SKILL.md', workSkill]]) {
21
+ ok(`${name} names check`, text.includes('trailhead-isolation-answer.js check'));
22
+ ok(`${name} names record`, text.includes('trailhead-isolation-answer.js record'));
23
+ ok(`${name} has no em-dash`, !text.includes('—'));
24
+ }
25
+ ok('choices.md has no em-dash', !choices.includes('—'));
26
+
27
+ for (const needle of [
28
+ '--others <csv|none|unavailable>',
29
+ 'unavailable',
30
+ '8 hours',
31
+ '`/trailhead:auto` and an explicit `none`/`worktree`/`clone` never read or write the record',
32
+ "Isolation: reusing this session's answer",
33
+ "(one ticket at a time)",
34
+ "add it to that repo's `.gitignore` if absent",
35
+ '.trailhead/isolation-answer.json',
36
+ 'three-dir lookup',
37
+ ]) ok(`teamwork.md carries: ${needle}`, teamwork.includes(needle));
38
+
39
+ ok('.gitignore carries the record', gitignore.includes('.trailhead/isolation-answer.json'));
40
+
41
+ console.log(`isolation-answer-docs.test.js: ${passed} checks passed`);
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ // Docs test for #242: the skill prose names the gitignored-file copy helper for new
3
+ // isolation workspaces and documents the isolation.copy config key.
4
+ // Run: node isolation-copy-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
+ // file -> strings it must carry
16
+ const EXPECT = {
17
+ 'trailhead-work/SKILL.md': ['trailhead-isolation-copy.js --from <main-checkout> --to <working-root>', 'Gitignored files in a new workspace'],
18
+ '_shared/teamwork.md': ['trailhead-isolation-copy.js --from <main-checkout> --to <working-root>', 'Gitignored files in a new workspace', 'isolation.copy', 'before any install', '`rejected`', '`warnings`', '`status: skipped`'],
19
+ '_shared/configuration-reference.md': ['`isolation.copy`', 'Isolation copy', '.example', '.sample'],
20
+ };
21
+
22
+ for (const [rel, needles] of Object.entries(EXPECT)) {
23
+ const text = read(rel);
24
+ for (const needle of needles) ok(`${rel} names: ${needle}`, text.includes(needle));
25
+ ok(`${rel} has no em-dash`, !text.includes('—'));
26
+ }
27
+
28
+ console.log(`isolation-copy-docs.test.js: ${passed} checks passed`);
@@ -91,7 +91,7 @@ This trailhead engine is running on **Codex CLI**, not Claude Code. The installe
91
91
  trailhead is a native Codex skill. Invoke it as \`$trailhead <verb>\` (e.g. \`$trailhead work\`, \`$trailhead new "idea"\`); bare \`$trailhead\` is smart entry. Never \`/trailhead:<verb>\` (that is the Claude Code surface). Whatever you type after the verb is the arguments. For discoverability the installer also projects one thin skill per verb (\`$trailhead-work\`, \`$trailhead-bug\`, …) that just delegates to \`$trailhead <verb>\`, so the verbs surface in the \`$\`-menu; \`$trailhead <verb>\` stays the canonical form and this SKILL.md the single source of truth. (Codex custom prompts under \`~/.codex/prompts/\` are deliberately NOT used: they do not surface as slash commands.)
92
92
 
93
93
  ## B. This IS the skill; the engine is split across sibling skill dirs
94
- Codex loaded this SKILL.md because you invoked \`$trailhead\` (or a \`$trailhead-<verb>\` / \`$trailhead-<cluster>\` entry); there is no separate Skill tool to call. The engine is split across sibling skill dirs under \`~/.codex/skills/\`: the dispatcher (\`trailhead/\`), five cluster skills (\`trailhead-chart/\`, \`trailhead-work/\`, \`trailhead-view/\`, \`trailhead-capture/\`, \`trailhead-manage/\`), and the shared core (\`_shared/\`, no SKILL.md). Read every referenced file **relative to the SKILL.md you are currently in**: a \`references/*.md\` from this skill's own dir, and the shared core (\`../_shared/*.md\`, or \`../../_shared/*.md\` from a \`references/\` file) from the sibling \`_shared/\` dir. Where the dispatcher's routing says "call the Skill tool with skill name \`trailhead-<cluster>\` and arguments \`<verb> <rest>\`", there is no Skill tool on Codex: **Read \`../trailhead-<cluster>/SKILL.md\` (the sibling dir) and follow it**, treating \`<verb> <rest>\` as its arguments.
94
+ Codex loaded this SKILL.md because you invoked \`$trailhead\` (or a \`$trailhead-<verb>\` / \`$trailhead-<cluster>\` entry); there is no separate Skill tool to call. The engine is split across sibling skill dirs under \`~/.codex/skills/\`: the dispatcher (\`trailhead/\`), five cluster skills (\`trailhead-chart/\`, \`trailhead-work/\`, \`trailhead-view/\`, \`trailhead-capture/\`, \`trailhead-manage/\`), and the shared core (\`_shared/\`, no SKILL.md). Resolve every referenced file **relative to the SKILL.md you are currently in** into an absolute path, then read it with a single \`cat <absolute path>\` or the file-read tool (never \`cd\` into a skills dir and then read a relative path): a \`references/*.md\` from this skill's own dir, and the shared core (\`../_shared/*.md\`, or \`../../_shared/*.md\` from a \`references/\` file) from the sibling \`_shared/\` dir. Where the dispatcher's routing says "call the Skill tool with skill name \`trailhead-<cluster>\` and arguments \`<verb> <rest>\`", there is no Skill tool on Codex: **Read \`../trailhead-<cluster>/SKILL.md\` (the sibling dir) and follow it**, treating \`<verb> <rest>\` as its arguments.
95
95
 
96
96
  ## C. AskUserQuestion -> plain-text numbered list (the guaranteed path on Codex)
97
97
  Codex's \`request_user_input\` tool only works in **Plan Mode** (Codex's collaboration mode with a plan-review step). trailhead skills run in Codex's **Default mode**: the installer never enables \`collaboration_modes\` / Plan Mode, so \`request_user_input\` is unavailable here. This is a **permanent, known constraint** of running trailhead on Codex, not a conditional "if unavailable" case.
@@ -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 body-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs, 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.
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 body-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs (the four run in one process, a single \`trailhead-bash-guards.js --guards=commit,secret,install,body\` registration, so a Bash call pays one start-up instead of four), 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.
@@ -323,6 +323,11 @@ policy:
323
323
  `;
324
324
  }
325
325
 
326
+ // --- CODEX_MOD_ONLY_VERBS --------------------------------------------------
327
+ // Verbs only the Claude Code mod answers (commands/<verb>.md exists for the
328
+ // namespaced command, but nothing on Codex can draw it): never projected.
329
+ const CODEX_MOD_ONLY_VERBS = ['panel'];
330
+
326
331
  // --- codexVerbSkillPlan ----------------------------------------------------
327
332
  // Given the resolved verb list (each { verb, description? }, or a bare string
328
333
  // verb; normally read from plugins/trailhead/commands/*.md by the installer),
@@ -340,6 +345,7 @@ function codexVerbSkillPlan(codexHome, verbs) {
340
345
  const v = entry && typeof entry === 'object' ? entry.verb : entry;
341
346
  if (v == null || String(v).trim() === '') continue;
342
347
  const verb = String(v).trim();
348
+ if (CODEX_MOD_ONLY_VERBS.includes(verb)) continue;
343
349
  const description = entry && typeof entry === 'object' ? entry.description : undefined;
344
350
  const skillDir = path.join(skillsRoot, `trailhead-${verb}`);
345
351
  dirs.push(skillDir);
@@ -358,10 +364,13 @@ function codexVerbSkillPlan(codexHome, verbs) {
358
364
  }
359
365
 
360
366
  // --- codexHookEntries -----------------------------------------------------
361
- // The six guardrail hooks trailhead registers on Codex, as {event, matcher,
362
- // command} records. Commands run the copied guard scripts under hooksScriptsDir
363
- // via node, matching Claude's `node "<path>"` form. PreToolUse guards (commit,
364
- // secret, install, body, secret-read) can veto; the injection-scanner
367
+ // The guardrail hooks trailhead registers on Codex, as {event, matcher,
368
+ // command} records. Commands run the copied hook scripts under hooksScriptsDir
369
+ // via node, matching Claude's `node "<path>"` form. The four Bash guards
370
+ // (commit, secret, install, body) are ONE registration, the merged
371
+ // trailhead-bash-guards.js with `--guards=commit,secret,install,body` (a single
372
+ // node process per Bash call); secret-read is a separate PreToolUse hook. All
373
+ // PreToolUse guards can veto; the injection-scanner
365
374
  // is a PostToolUse advisory; check-update runs at SessionStart. Reused by the
366
375
  // installer, which merges them into ~/.codex/hooks.json (same shape as
367
376
  // Claude's settings.json hooks block).
@@ -372,10 +381,7 @@ function codexVerbSkillPlan(codexHome, verbs) {
372
381
  function codexHookEntries(hooksScriptsDir) {
373
382
  const cmd = (name) => `node "${path.join(hooksScriptsDir, name)}"`;
374
383
  return [
375
- { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-commit-guard.js') },
376
- { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-secret-guard.js') },
377
- { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-install-guard.js') },
378
- { event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-body-guard.js') },
384
+ { event: 'PreToolUse', matcher: 'Bash', command: `${cmd('trailhead-bash-guards.js')} --guards=commit,secret,install,body` },
379
385
  { event: 'PreToolUse', matcher: 'Read|Bash', command: cmd('trailhead-secret-read-guard.js') },
380
386
  { event: 'PostToolUse', matcher: 'Bash', command: cmd('trailhead-issue-injection-scanner.js') },
381
387
  { event: 'Stop', matcher: '', command: cmd('trailhead-mockup-link-stop.js') },
@@ -447,6 +453,7 @@ module.exports = {
447
453
  codexVerbSkillContent,
448
454
  codexVerbSkillAgentsYaml,
449
455
  codexVerbSkillPlan,
456
+ CODEX_MOD_ONLY_VERBS,
450
457
  codexHookEntries,
451
458
  enableCodexFeatureFlag,
452
459
  enableCodexHooksFeature,
@@ -15,6 +15,7 @@ const {
15
15
  codexVerbSkillContent,
16
16
  codexVerbSkillAgentsYaml,
17
17
  codexVerbSkillPlan,
18
+ CODEX_MOD_ONLY_VERBS,
18
19
  codexHookEntries,
19
20
  enableCodexHooksFeature,
20
21
  enableCodexMultiAgentV2Feature,
@@ -160,19 +161,22 @@ ok('codexVerbSkillPlan: accepts a bare-string verb and skips empty/invalid', (()
160
161
  })());
161
162
  ok('codexVerbSkillPlan: null verbs yields nothing', codexVerbSkillPlan('/c', null).dirs.length === 0 && codexVerbSkillPlan('/c').dirs.length === 0);
162
163
 
164
+ // #208: a verb only the Claude Code mod answers (the map panel) has no Codex projection.
165
+ ok('CODEX_MOD_ONLY_VERBS names exactly the panel verb', Array.isArray(CODEX_MOD_ONLY_VERBS) && CODEX_MOD_ONLY_VERBS.length === 1 && CODEX_MOD_ONLY_VERBS[0] === 'panel');
166
+ ok('codexVerbSkillPlan: never projects a mod-only verb, object or bare string', (() => {
167
+ const plan = codexVerbSkillPlan('/c', [{ verb: 'panel', description: 'x' }, 'panel', { verb: 'map' }]);
168
+ return plan.dirs.length === 1 && plan.dirs[0] === '/c/skills/trailhead-map' && !plan.writes.some((w) => /panel/.test(w.path));
169
+ })());
170
+
163
171
  // --- codexHookEntries ---
164
172
  const entries = codexHookEntries('/h');
165
- ok('codexHookEntries returns 8 entries', Array.isArray(entries) && entries.length === 8);
166
- ok('codexHookEntries: commit-guard is PreToolUse/Bash', entries.some((e) =>
167
- e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-commit-guard.js') && e.command.includes('/h')));
168
- ok('codexHookEntries: secret-guard is PreToolUse/Bash', entries.some((e) =>
169
- e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-secret-guard.js')));
170
- ok('codexHookEntries: install-guard is PreToolUse/Bash', entries.some((e) =>
171
- e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-install-guard.js')));
172
- ok('codexHookEntries: search-guard is absent (Claude-Code-only, not projected to Codex)',
173
- entries.every((e) => !e.command.includes('trailhead-search-guard.js')));
174
- ok('codexHookEntries: body-guard is PreToolUse/Bash', entries.some((e) =>
175
- e.event === 'PreToolUse' && e.matcher === 'Bash' && e.command.includes('trailhead-body-guard.js')));
173
+ ok('codexHookEntries returns 5 entries', Array.isArray(entries) && entries.length === 5);
174
+ const bashEntries = entries.filter((e) => e.event === 'PreToolUse' && e.matcher === 'Bash');
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');
178
+ ok('codexHookEntries: no per-guard script is registered on its own', entries.every((e) =>
179
+ !/trailhead-(commit|secret|install|search|body)-guard\.js/.test(e.command)));
176
180
  ok('codexHookEntries: secret-read-guard is PreToolUse/Read|Bash', entries.some((e) =>
177
181
  e.event === 'PreToolUse' && e.matcher === 'Read|Bash' && e.command.includes('trailhead-secret-read-guard.js')));
178
182
  ok('codexHookEntries: injection-scanner is PostToolUse/Bash', entries.some((e) =>
@@ -402,4 +406,69 @@ ok('convertToCodex projects the anchor positives to Codex forms', (() => {
402
406
  !out.includes('/trailhead:work') && !out.includes('/clear');
403
407
  })());
404
408
 
409
+ // #205: the work state helper and its session env var survive the projection;
410
+ // the projected work skill and handoff carry the set/clear calls.
411
+ ok('convertToCodex keeps the trailhead-work-state.js filename',
412
+ convertToCodex('node <templates>/trailhead-work-state.js set').includes('<templates>/trailhead-work-state.js set'));
413
+ // #253: the Resolve bookkeeping helper filenames survive the projection.
414
+ ok('convertToCodex keeps the trailhead-issue-body.js filename',
415
+ convertToCodex('node <templates>/trailhead-issue-body.js append --issue 1').includes('<templates>/trailhead-issue-body.js append'));
416
+ ok('convertToCodex keeps the trailhead-dashboard.js filename',
417
+ convertToCodex('node <templates>/trailhead-dashboard.js --dry-run').includes('<templates>/trailhead-dashboard.js --dry-run'));
418
+ ok('convertToCodex keeps CLAUDE_CODE_SESSION_ID intact',
419
+ convertToCodex('session from CLAUDE_CODE_SESSION_ID when set').includes('CLAUDE_CODE_SESSION_ID'));
420
+ {
421
+ const skillsDir = path.join(__dirname, '../../plugins/trailhead/skills');
422
+ const workSkill = convertToCodex(fs.readFileSync(path.join(skillsDir, 'trailhead-work', 'SKILL.md'), 'utf8'));
423
+ ok('projected work skill carries the work-start set call',
424
+ workSkill.includes('trailhead-work-state.js set --repo <working-root> --map <n> --ticket <n> --title "<title>" --step discuss'));
425
+ // #234: the scope confirm menu call-site and the adapter's numbered-list rule both reach Codex.
426
+ const workInjected = injectCodexAdapterHeader(workSkill);
427
+ ok('projected work skill carries the step 3 scope-confirm menu call-site',
428
+ /AskUserQuestion[\s\S]{0,300}\*\*Proceed\*\*/.test(workInjected));
429
+ ok('projected work skill carries the adapter AskUserQuestion -> numbered list rule',
430
+ workInjected.includes('## C. AskUserQuestion -> plain-text numbered list'));
431
+ const handoff = convertToCodex(fs.readFileSync(path.join(skillsDir, '_shared', 'session-handoff.md'), 'utf8'));
432
+ ok('projected session handoff carries the work state clear call',
433
+ handoff.includes('trailhead-work-state.js clear --repo <working-root>'));
434
+ const teamwork = convertToCodex(fs.readFileSync(path.join(skillsDir, '_shared', 'teamwork.md'), 'utf8'));
435
+ ok('projected teamwork carries the step change call',
436
+ teamwork.includes('trailhead-work-state.js set --repo <working-root> --step <step>'));
437
+ ok('projected teamwork carries the autoDone counter flag',
438
+ teamwork.includes('--auto true --auto-done <n>'));
439
+ const autoRef = convertToCodex(fs.readFileSync(path.join(skillsDir, 'trailhead-work', 'references', 'auto.md'), 'utf8'));
440
+ ok('projected auto reference carries the autoDone counter flag',
441
+ autoRef.includes('--auto true --auto-done <n>'));
442
+ }
443
+
444
+ // #248: the shared boundary blocks survive the Codex projection of every real agent.
445
+ {
446
+ const { parseSource } = require('../../scripts/sync-agent-blocks.js');
447
+ const shared = parseSource(fs.readFileSync(path.join(__dirname, '../../scripts/agent-blocks.md'), 'utf8'));
448
+ const agentsDir = path.join(__dirname, '../../plugins/trailhead/agents');
449
+ const files = fs.readdirSync(agentsDir).filter((f) => /^trailhead-.*\.md$/.test(f));
450
+ ok('projection boundaries: found the eight real agents', files.length === 8);
451
+ ok('projection boundaries: source has common and implementing blocks', !!shared.common && !!shared.implementing);
452
+ // #249: every agent's projected TOML carries exactly its family block, whole.
453
+ const familyOf = {
454
+ 'trailhead-verify': 'reviewing', 'trailhead-code-review': 'reviewing',
455
+ 'trailhead-executor': 'implementing', 'trailhead-fix': 'implementing',
456
+ 'trailhead-debug': 'investigating', 'trailhead-research': 'investigating', 'trailhead-codebase-map': 'investigating',
457
+ 'trailhead-plan': 'plan',
458
+ };
459
+ const families = ['reviewing', 'implementing', 'investigating', 'plan'];
460
+ ok('projection boundaries: source has every family block', families.every((f) => !!shared[f]));
461
+ for (const f of files) {
462
+ const name = f.slice(0, -3);
463
+ const body = fs.readFileSync(path.join(agentsDir, f), 'utf8').replace(/^---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n?/, '');
464
+ const out = codexAgentToml({ name, description: 'd', developerInstructions: body, model: null, effort: null });
465
+ ok(`${name}: projection carries the whole common block`, !!shared.common && out.includes(shared.common));
466
+ ok(`${name}: has a family`, families.includes(familyOf[name]));
467
+ for (const fam of families) {
468
+ ok(`${name}: projection ${familyOf[name] === fam ? 'carries the whole' : 'omits the'} ${fam} block`,
469
+ !!shared[fam] && out.includes(shared[fam]) === (familyOf[name] === fam));
470
+ }
471
+ }
472
+ }
473
+
405
474
  console.log(`✓ codex-projection: ${passed} assertions passed`);
@@ -135,6 +135,9 @@ function readRegistry(registryFile) {
135
135
  // token expands to exactly hooksDir/<f>, OR an unresolved token's basename is
136
136
  // <f> (stays warning-only, never also missing).
137
137
  function checkRegisteredTargets(spec, missing, unverified) {
138
+ // Shipped files (hookFiles) may include modules that are only require()d;
139
+ // the files that must be registered are the entrypoints (default: all shipped).
140
+ const entrypoints = spec.entrypointFiles || spec.hookFiles;
138
141
  const registry = readRegistry(spec.registryFile);
139
142
  const registryName = path.basename(spec.registryFile);
140
143
  const events = (registry && typeof registry.hooks === 'object' && registry.hooks) ? registry.hooks : {};
@@ -168,7 +171,7 @@ function checkRegisteredTargets(spec, missing, unverified) {
168
171
  }
169
172
  }
170
173
 
171
- for (const f of spec.hookFiles) {
174
+ for (const f of entrypoints) {
172
175
  if (!registeredExact.has(f) && !registeredViaUnresolved.has(f)) {
173
176
  missing.push(`hook registration ${f} (${registryName})`);
174
177
  }
@@ -185,8 +188,8 @@ function checkAgents(spec, missing) {
185
188
  }
186
189
 
187
190
  // --- verifyInstall --------------------------------------------------------------
188
- // spec = { skillsRoot, skillDirs, sharedFiles, hooksDir, hookFiles,
189
- // hookLibFiles, registryFile, agentsDir, agentPattern, env, home }
191
+ // spec = { skillsRoot, skillDirs, sharedFiles, hooksDir, hookFiles (shipped),
192
+ // entrypointFiles (registered; defaults to hookFiles), hookLibFiles, registryFile, agentsDir, agentPattern, env, home }
190
193
  function verifyInstall(spec) {
191
194
  const missing = [];
192
195
  const unverified = [];
@@ -199,13 +202,14 @@ function verifyInstall(spec) {
199
202
  }
200
203
 
201
204
  // --- spec builders ---------------------------------------------------------
202
- function claudeVerifySpec(configDir, { skillDirs, sharedFiles, hookFiles, hookLibFiles, env, home }) {
205
+ function claudeVerifySpec(configDir, { skillDirs, sharedFiles, hookFiles, entrypointFiles, hookLibFiles, env, home }) {
203
206
  return {
204
207
  skillsRoot: path.join(configDir, 'skills'),
205
208
  skillDirs,
206
209
  sharedFiles,
207
210
  hooksDir: path.join(configDir, 'hooks'),
208
211
  hookFiles,
212
+ entrypointFiles,
209
213
  hookLibFiles,
210
214
  registryFile: path.join(configDir, 'settings.json'),
211
215
  agentsDir: path.join(configDir, 'agents'),
@@ -215,7 +219,7 @@ function claudeVerifySpec(configDir, { skillDirs, sharedFiles, hookFiles, hookLi
215
219
  };
216
220
  }
217
221
 
218
- function codexVerifySpec(configDir, { skillDirs, sharedFiles, hookFiles, hookLibFiles, env, home }) {
222
+ function codexVerifySpec(configDir, { skillDirs, sharedFiles, hookFiles, entrypointFiles, hookLibFiles, env, home }) {
219
223
  const L = codexLayout(configDir);
220
224
  return {
221
225
  skillsRoot: L.skillsRoot,
@@ -223,6 +227,7 @@ function codexVerifySpec(configDir, { skillDirs, sharedFiles, hookFiles, hookLib
223
227
  sharedFiles,
224
228
  hooksDir: L.hooksScriptsDir,
225
229
  hookFiles,
230
+ entrypointFiles,
226
231
  hookLibFiles,
227
232
  registryFile: L.hooksJson,
228
233
  agentsDir: L.codexAgentsDir,
@@ -359,6 +359,57 @@ function buildCodexFixture() {
359
359
  r.unverified.some((u) => u.includes('CLAUDE_PLUGIN_ROOT undefined')));
360
360
  }
361
361
 
362
+ // --- shipped files vs registered entrypoints (#207) ---------------------------
363
+ // The guard modules are shipped (they must exist) but only require()d by the
364
+ // merged hook, so they are never required to be registered.
365
+ function buildMergedFixture(host) {
366
+ const configDir = mktmp();
367
+ const skillDirs = ['trailhead', '_shared'];
368
+ const sharedFiles = ['load-first.md'];
369
+ const modules = ['trailhead-commit-guard.js', 'trailhead-body-guard.js'];
370
+ const entrypointFiles = ['trailhead-bash-guards.js'];
371
+ const hookFiles = [...modules, ...entrypointFiles];
372
+ const hooksDir = host === 'codex' ? path.join(configDir, 'skills', 'trailhead', 'hooks') : path.join(configDir, 'hooks');
373
+ write(path.join(configDir, 'skills', 'trailhead', 'SKILL.md'), 'x');
374
+ write(path.join(configDir, 'skills', '_shared', 'load-first.md'), 'x');
375
+ for (const f of hookFiles) write(path.join(hooksDir, f), 'x');
376
+ write(path.join(configDir, 'agents', host === 'codex' ? 'trailhead-execute.toml' : 'trailhead-execute.md'), 'x');
377
+ const registry = { hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [
378
+ { type: 'command', command: `node "${path.join(hooksDir, 'trailhead-bash-guards.js')}"${host === 'codex' ? ' --guards=commit,body' : ''}` },
379
+ ] }] } };
380
+ write(path.join(configDir, host === 'codex' ? 'hooks.json' : 'settings.json'), JSON.stringify(registry, null, 2));
381
+ const build = host === 'codex' ? codexVerifySpec : claudeVerifySpec;
382
+ const spec = build(configDir, { skillDirs, sharedFiles, hookFiles, entrypointFiles, hookLibFiles: [], env: {}, home: H });
383
+ return { configDir, spec, hooksDir };
384
+ }
385
+ for (const host of ['claude', 'codex']) {
386
+ {
387
+ const { spec } = buildMergedFixture(host);
388
+ const r = verifyInstall(spec);
389
+ ok(`${host}: passes with the guard modules shipped but unregistered`, r.missing.length === 0 && r.unverified.length === 0);
390
+ }
391
+ {
392
+ const { spec, hooksDir } = buildMergedFixture(host);
393
+ fs.rmSync(path.join(hooksDir, 'trailhead-commit-guard.js'));
394
+ const r = verifyInstall(spec);
395
+ ok(`${host}: a shipped guard module that is missing is still reported`, r.missing.includes('hook script trailhead-commit-guard.js'));
396
+ }
397
+ {
398
+ const { configDir, spec } = buildMergedFixture(host);
399
+ write(path.join(configDir, host === 'codex' ? 'hooks.json' : 'settings.json'), JSON.stringify({ hooks: {} }));
400
+ const r = verifyInstall(spec);
401
+ ok(`${host}: fails when the registered entrypoint is not registered`,
402
+ r.missing.some((m) => m.startsWith('hook registration trailhead-bash-guards.js')));
403
+ }
404
+ {
405
+ const { spec, hooksDir } = buildMergedFixture(host);
406
+ fs.rmSync(path.join(hooksDir, 'trailhead-bash-guards.js'));
407
+ const r = verifyInstall(spec);
408
+ ok(`${host}: fails when the registered entrypoint file is missing`,
409
+ r.missing.includes('hook script trailhead-bash-guards.js') && r.missing.some((m) => m.startsWith('registered hook target')));
410
+ }
411
+ }
412
+
362
413
  // --- formatters --------------------------------------------------------------
363
414
  {
364
415
  const out = formatVerifyFailure('Claude Code', '/c/dir', { missing: ['skill trailhead-work/SKILL.md'], unverified: ['/x/trailhead-y.js (PreToolUse, settings.json: $Y undefined)'] });
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ // Pins the report status protocol in the shipped skills (#250, #249): the
3
+ // canonical "Malformed review report" paragraph (every engine-agent report) and
4
+ // VERIFY comment layout in _shared/techniques.md, the clause on both Verify lines of
5
+ // ticket-engines.md, the return-not-post wording in the verify and code-review
6
+ // techniques, and how Plan, Execute, Fix, Diagnose, research and the codebase map
7
+ // read the fields of their agent's report.
8
+ // Run: node report-status-docs.test.js. No framework: plain asserts.
9
+ const assert = require('assert');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ let passed = 0;
14
+ const ok = (name, cond) => { assert.ok(cond, name); passed++; };
15
+
16
+ const skills = path.join(__dirname, '..', 'plugins', 'trailhead', 'skills');
17
+ const read = (...p) => fs.readFileSync(path.join(skills, ...p), 'utf8');
18
+ const techniques = read('_shared', 'techniques.md');
19
+ const verify = read('_shared', 'techniques', 'verify.md');
20
+ const codeReview = read('_shared', 'techniques', 'code-review.md');
21
+ const acceptance = read('_shared', 'techniques', 'acceptance-testing.md');
22
+ const engines = read('trailhead-work', 'references', 'ticket-engines.md');
23
+ const brief = read('trailhead-work', 'references', 'executor-brief.md');
24
+ const research = read('_shared', 'techniques', 'research.md');
25
+ const codebaseMap = read('_shared', 'techniques', 'codebase-map.md');
26
+ const debug = read('_shared', 'techniques', 'debug.md');
27
+ const charting = read('trailhead-chart', 'references', 'charting.md');
28
+ const researchAgent = fs.readFileSync(path.join(__dirname, '..', 'plugins', 'trailhead', 'agents', 'trailhead-research.md'), 'utf8');
29
+
30
+ // --- the canonical paragraph -------------------------------------------------------
31
+ const start = techniques.indexOf('**Malformed review report');
32
+ ok('techniques.md has the Malformed review report paragraph', start !== -1);
33
+ const neverBlock = techniques.indexOf('**Reviews never block the session');
34
+ ok('the paragraph sits next to "Reviews never block the session"',
35
+ start !== -1 && neverBlock !== -1 && Math.abs(start - neverBlock) < 2500 && start > neverBlock);
36
+ const para = start === -1 ? '' : techniques.slice(start, techniques.indexOf('\n| Technique |', start));
37
+ const lc = para.toLowerCase();
38
+ ok('the paragraph section was found and is bounded', para.length > 1000 && !para.includes('| **Grilling**'));
39
+
40
+ ok('it MUST run on every engine-agent report, the two reviews included', /MUST run on every/.test(para)
41
+ && para.includes('Code review') && para.includes('Goal-backward verification'));
42
+ ok('it names every technique it covers',
43
+ ['plan', 'executor', 'fix', 'debug', 'research', 'codebase-map'].every((t) => para.includes(t)) && lc.includes('every engine-agent report'));
44
+ ok('the checker line carries fields and body', para.includes('{"action","verdict","problem","report","fields","body"}'));
45
+ ok('it covers subagent, inline and the lean inline pass', lc.includes('subagent') && lc.includes('inline') && lc.includes('lean inline pass'));
46
+ ok('the checker command with its technique flag and a report file',
47
+ para.includes('node <templates>/trailhead-report-status.js --technique <verify|code-review|plan|executor|fix|debug|research|codebase-map> --file <that dir>/report.md'));
48
+ ok('the three templates dirs, in order',
49
+ (() => {
50
+ const a = para.indexOf('${CLAUDE_PLUGIN_ROOT}/templates/');
51
+ const b = para.indexOf('~/.claude/trailhead/templates/');
52
+ const c = para.indexOf('~/.codex/skills/trailhead/templates/');
53
+ return a !== -1 && b > a && c > b;
54
+ })());
55
+ ok('the plugin root dir is skipped when unset', lc.includes('skipped when unset'));
56
+ ok('a private temp dir from mktemp -d, once per report', para.includes('`mktemp -d`') && lc.includes('once') && lc.includes('0700'));
57
+ ok('the raw report is written to report.md at the literal printed path with the Write tool',
58
+ para.includes('<that dir>/report.md') && para.includes('Write tool') && lc.includes('literal printed path'));
59
+ ok('the dir is removed with rm -rf at the literal absolute path', para.includes('rm -rf <that dir>') && lc.includes('literal absolute path'));
60
+ ok('cleanup runs after every path',
61
+ lc.includes('cleanup runs after every path') && ['accept', 'normalise', 're-ask check', 'second failure', 'checker failure'].every((w) => lc.includes(w)));
62
+ ok('never a fixed or predictable path, never reused', lc.includes('never a fixed or predictable path') && lc.includes('never reused'));
63
+ ok('stdin stays a CLI input but the protocol does not use it', lc.includes('stdin') && lc.includes('does not use'));
64
+ ok('the checker-returned report replaces the raw report', lc.includes('checker-returned `report` replaces the raw report'));
65
+ ok('the engine branches only on the returned verdict', para.includes('branches only on the returned `verdict`'));
66
+ ok('the engine reads the returned fields and body, never re-derives them from prose',
67
+ para.includes('reads the returned `fields`') && para.includes('`body`') && lc.includes('instead of re-deriving them from the prose'));
68
+ ok('reask: ask once, continue the same agent where the host allows, else re-dispatch',
69
+ lc.includes('ask once') && lc.includes('continue the same agent') && lc.includes('re-dispatch the same `trailhead-<technique>` agent')
70
+ && lc.includes('same model override') && lc.includes('malformed report quoted'));
71
+ const prompt = 'Your report could not be accepted: `<problem>` (missing: no `status:` line; unknown-token: `<found>` is not an allowed token; '
72
+ + 'conflicting: more than one `status:` line, so quote or reword any status text in the body, which must not start a line with `status:` outside a code fence or blockquote; counts: the counts line is missing or repeated; fields: a required field is missing or an invariant is broken). '
73
+ + 'Return the complete report again with its first line exactly `status: <token>`, where `<token>` is one of `<allowed tokens>`, '
74
+ + 'and no other `status:` line<for code-review: , and its second line exactly `critical: <n> · warning: <n> · info: <n>`>'
75
+ + '<for fields: , with each of `<required fields>` as a top-level `<label>:` line and `<the broken invariant>` holding, with nothing after the last field>. '
76
+ + 'Do not post it anywhere.';
77
+ ok('the corrective prompt, verbatim', para.includes(prompt));
78
+ ok('for code-review the prompt always restates both required lines', lc.includes('always restates both required lines'));
79
+ ok('for fields the prompt names the missing fields and the broken invariant', lc.includes('names the missing or empty fields and the invariant that broke'));
80
+ ok('the reply is re-checked', lc.includes('re-check'));
81
+ ok('a second failure: no verdict, nothing posted, no third ask, no inference from the body',
82
+ lc.includes('no verdict') && lc.includes('nothing posted') && lc.includes('no third ask') && lc.includes('no inference from the body'));
83
+ ok('a second failure: HITL checkpoint naming the technique and the problem, Resolve stays blocked',
84
+ para.includes('HITL checkpoint') && lc.includes('naming the technique and the problem') && lc.includes('resolve stays blocked'));
85
+ ok('the previous VERIFY comment stays latest', lc.includes('previous `verify` comment stays latest'));
86
+ ok('a step that needed the report stays blocked too', lc.includes('so does any step that needed the report'));
87
+ ok('the layout part is for the reviewing pair only', para.includes('**VERIFY comment layout (the reviewing pair only).**'));
88
+ ok('checker missing, exit 2 or non-JSON: apply the same rules by hand, never guess',
89
+ lc.includes('checker is missing') && lc.includes('exit 2') && lc.includes('non-json') && lc.includes('by hand') && lc.includes('never guess a verdict'));
90
+
91
+ // --- the VERIFY comment layout ------------------------------------------------------
92
+ ok('layout: one VERIFY comment per Verify round', /one `VERIFY` comment per Verify round/.test(para));
93
+ ok('layout part 1: the code-review report verbatim, status line 1, counts line 2, ledger, findings',
94
+ lc.includes('code-review report verbatim') && lc.includes('counts line 2') && lc.includes('ledger'));
95
+ ok('layout part 2: a "## Goal-backward verification" heading and the verify report verbatim',
96
+ para.includes('`## Goal-backward verification` heading') && lc.includes('verify report verbatim'));
97
+ ok('layout part 3: test or criterion outcome, then acceptance testing results',
98
+ lc.includes('test/criterion outcome') && lc.includes('acceptance-testing results'));
99
+ ok('layout: an issues_found round posts parts 1 and 3 only', lc.includes('`issues_found` posts parts 1 and 3 only'));
100
+ ok('layout: a verify-only round repeats the latest code-review report verbatim as part 1',
101
+ lc.includes('repeats the latest checker-returned code-review report verbatim as part 1'));
102
+ ok('layout: the latest VERIFY comment holds the counts line and ledger', lc.includes('latest `verify` comment always holds the authoritative counts line and ledger'));
103
+ ok('layout: the engine never reads the comment back to branch', lc.includes('never by reading the comment back'));
104
+
105
+ // --- both Verify lines -----------------------------------------------------------------
106
+ const verifyLines = engines.split('\n').filter((l) => l.trim().startsWith('4. **Verify**'));
107
+ ok('ticket-engines.md has exactly two Verify (step 4) lines', verifyLines.length === 2);
108
+ for (const [i, l] of verifyLines.entries()) {
109
+ ok(`Verify line ${i + 1}: run the report-status check before branching on status`, l.includes('run the report-status check before branching on status'));
110
+ ok(`Verify line ${i + 1}: points to the paragraph by name`, l.includes('*Malformed review report*') && l.includes('../../_shared/techniques.md'));
111
+ ok(`Verify line ${i + 1}: points to the VERIFY comment layout`, l.includes('layout'));
112
+ ok(`Verify line ${i + 1}: keeps the Ticket diff reference (#181)`, l.includes('**Ticket diff**'));
113
+ }
114
+
115
+ // --- the techniques return, the orchestrator posts --------------------------------------
116
+ ok('verify.md: "Return the verdict" with a leading status', verify.includes('Return the verdict with a leading `status:`'));
117
+ ok('verify.md: the orchestrator posts it under the Goal-backward verification heading of the round\'s VERIFY comment',
118
+ verify.includes('the orchestrator posts it') && verify.includes('`## Goal-backward verification` heading') && verify.includes("round's `VERIFY` comment"));
119
+ ok('verify.md: no "Post the result"', !verify.includes('Post the result'));
120
+ ok('verify.md keeps the three tokens', ['met', 'gaps_found', 'inconclusive'].every((t) => verify.includes(`\`status: ${t}\``)));
121
+ ok('code-review.md: returns the report and the orchestrator posts it as the VERIFY comment',
122
+ /Return the report/.test(codeReview) && codeReview.includes('the orchestrator posts it as the `VERIFY` comment'));
123
+ ok('code-review.md: no "Report in the ticket\'s VERIFY comment"', !codeReview.includes("Report in the ticket's `VERIFY` comment"));
124
+ ok('code-review.md: status tag still on its own first line', codeReview.includes('machine-checkable status tag on its own first line'));
125
+ ok('code-review.md: counts line phrasing kept', codeReview.includes('critical: <n> · warning: <n> · info: <n>')
126
+ && codeReview.includes('**Counts line, right under `status:`.**'));
127
+ ok('code-review.md: dispositions and the latest VERIFY comment kept',
128
+ codeReview.includes('`fixed (<commit>)`') && codeReview.includes('latest `VERIFY` comment'));
129
+ ok('code-review.md keeps the #181 Ticket diff rule', codeReview.includes("**Ticket diff: review the ticket's own commits, never a range.**"));
130
+
131
+ // --- acceptance results are part 3 of the round's single VERIFY comment ---------------
132
+ ok('acceptance-testing.md: results are gathered as it goes and land in part 3 of the round\'s VERIFY comment',
133
+ acceptance.includes('as it goes') && acceptance.includes("part 3 of the round's single `VERIFY` comment"));
134
+ ok('acceptance-testing.md: the orchestrator posts once the round\'s checks, including acceptance, are done',
135
+ acceptance.includes('the orchestrator posts once the round\'s checks, including this one, are done'));
136
+ ok('acceptance-testing.md: no longer records "in the VERIFY comment as it goes"', !acceptance.includes('in the `VERIFY` comment as it goes'));
137
+ ok('acceptance-testing.md: the screenshot and the pending checklist go to part 3, not straight into a comment',
138
+ !acceptance.includes('screenshot into the `VERIFY` comment') && !acceptance.includes('checklist in the `VERIFY` comment'));
139
+ ok('acceptance-testing.md: the two start paths outside a Verify round post their own VERIFY comment',
140
+ acceptance.includes("the Pending-UAT guard's option (a) and a UAT follow-up, there is no round: the orchestrator posts the results as a `VERIFY` comment of their own"));
141
+ ok('acceptance-testing.md: no em-dash', !acceptance.includes('\u2014'));
142
+
143
+ // --- every engine step reads the fields of its agent's report (#249) --------------------
144
+ const stepOf = (src, startPrefix, endPrefix) => {
145
+ const ls = src.split('\n');
146
+ const a = ls.findIndex((l) => l.startsWith(startPrefix));
147
+ const b = ls.findIndex((l, i) => i > a && l.startsWith(endPrefix));
148
+ return a === -1 ? '' : ls.slice(a, b === -1 ? undefined : b).join('\n');
149
+ };
150
+ const has = (src, ...needles) => needles.every((n) => src.includes(n));
151
+ const plan = stepOf(engines, '2. **Plan**', '3. **Execute**');
152
+ const execute = stepOf(engines, '3. **Execute**', '4. **Verify**');
153
+ const fixStep = stepOf(engines, '3. **Fix**', '4. **Verify**');
154
+ const diagnose = stepOf(engines, '2. **Diagnose**', '3. **Fix**');
155
+ ok('the steps were found', [plan, execute, fixStep, diagnose].every((x) => x.length > 300));
156
+ ok('Plan: checks the planner report with --technique plan', has(plan, '--technique plan', '*Malformed review report*'));
157
+ ok('Plan: ready posts the checker body verbatim as the PLAN comment', has(plan, '`status: ready`', "checker's `body` verbatim", '`PLAN` comment'));
158
+ ok('Plan: needs_input asks the questions, re-spawns with the answers, never guesses',
159
+ has(plan, '`status: needs_input`', '`questions:`', 're-spawn the planner with the answers', 'never answer them yourself or guess'));
160
+ for (const [label, step, tech] of [['Execute', execute, 'executor'], ['Fix', fixStep, 'fix']]) {
161
+ ok(`${label}: checks the report with --technique ${tech}`, has(step, `--technique ${tech}`, '*Malformed review report*'));
162
+ ok(`${label}: branches on the verdict and reads the fields, never reconstructs from prose`,
163
+ has(step, "checker's `verdict`", '`fields`', 'never reconstruct a result from the prose'));
164
+ ok(`${label}: done goes to Verify`, has(step, '`status: done` -> Verify'));
165
+ ok(`${label}: partial or blocked acts on each blockers entry, then re-spawns`,
166
+ has(step, '`status: partial` or `blocked`', 'each `blockers:` entry', 'foreign change', 're-spawn'));
167
+ ok(`${label}: posts fidelity as the fidelity comment and routes surfaced scope`, has(step, 'Post `fidelity:` as the fidelity comment', '`surfaced scope:`'));
168
+ ok(`${label}: commits must equal the Refs set since the base, both directions, every SHA resolves, a mismatch is surfaced`,
169
+ has(step, '`commits:`', 'must equal the `Refs: #<n>` commit set since the base SHA', 'both directions', 'every SHA must resolve', 'surfaced to the user, never patched over'));
170
+ }
171
+ ok('Diagnose: checks the debug report with --technique debug', has(diagnose, '--technique debug', '*Malformed review report*'));
172
+ ok('Diagnose: found feeds the DIAGNOSIS from answer and evidence', has(diagnose, '`status: found`', '`answer:`', '`evidence:`', '`DIAGNOSIS`'));
173
+ ok('Diagnose: inconclusive stays in diagnosis, needs_input asks the open questions',
174
+ has(diagnose, '`status: inconclusive`', 'stays in diagnosis', '`status: needs_input`', '`open questions:`'));
175
+ ok('research: no research/<name> branch anywhere', ![research, charting, researchAgent].some((t) => /research\/(<name>|&lt;name)/.test(t)));
176
+ ok('research: the report is the artifact, posted as a RESEARCH comment with every claim and its URL',
177
+ has(research, 'the report is the artifact', '`RESEARCH` comment', '`evidence:`', 'URL'));
178
+ ok('research: branches on found, inconclusive and needs_input',
179
+ has(research, '`status: found`', 'resolve the ticket', '`status: inconclusive`', 'accept the answer at that confidence, narrow the question, or run another round',
180
+ '`status: needs_input`', '`open questions:`', 're-spawn'));
181
+ ok('research: checked with --technique research', research.includes('--technique research'));
182
+ ok('charting: the research fan-out posts the report, no branch', has(charting, '`RESEARCH` comment', 'no branch'));
183
+ ok('research agent: identity returns findings in the report', researchAgent.includes('returning findings with their citations in your report') && researchAgent.includes('commit nothing'));
184
+ ok('codebase-map: each reader returns an investigating report checked with --technique codebase-map',
185
+ has(codebaseMap, 'investigating report', '--technique codebase-map'));
186
+ ok('codebase-map: distils the answer fields; inconclusive notes the gap; needs_input asked once then re-spawned or unmapped',
187
+ has(codebaseMap, 'distil** the `answer:` fields', '`status: inconclusive`', 'note the gap', '`status: needs_input`', 'once', 're-spawn that facet', 'unmapped'));
188
+ ok('debug: a Report line points at the investigating contract', has(debug, '**Report.**', 'investigating report', 'never commits or creates a branch', '`DIAGNOSIS`'));
189
+ ok('brief: the report names the implementing Return contract', has(brief, 'implementing `Return contract`', '`blockers:`'));
190
+
191
+ for (const [label, src] of [['techniques.md', techniques], ['verify.md', verify], ['code-review.md', codeReview], ['ticket-engines.md', engines], ['executor-brief.md', brief], ['research.md', research], ['codebase-map.md', codebaseMap], ['debug.md', debug], ['charting.md', charting]]) {
192
+ ok(`${label}: no em-dash`, !src.includes('—'));
193
+ }
194
+
195
+ console.log(`report-status-docs.test.js: ${passed} checks passed`);