@marcomigozzi/trailhead 0.16.1 → 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.
- package/README.it.md +82 -25
- package/README.md +82 -25
- package/bin/base-preflight-docs.test.js +4 -1
- package/bin/bulk-cross-reference-docs.test.js +36 -0
- package/bin/capture-action.test.js +167 -0
- package/bin/capture-docs.test.js +32 -0
- package/bin/engine-route-docs.test.js +155 -0
- package/bin/facet-stamp-docs.test.js +79 -0
- package/bin/forced-mutation-docs.test.js +38 -0
- package/bin/grill-model-docs.test.js +155 -0
- package/bin/grilling-holistic-review-docs.test.js +7 -3
- package/bin/guard-axis-docs.test.js +69 -0
- package/bin/inbox-needs-info-docs.test.js +68 -0
- package/bin/lib/codex-projection.js +28 -19
- package/bin/lib/codex-projection.test.js +77 -20
- package/bin/lib/host-descriptor.js +1 -1
- package/bin/lib/host-descriptor.test.js +6 -0
- package/bin/needs-you-docs.test.js +9 -2
- package/bin/options-in-chat-docs.test.js +65 -0
- package/bin/pick-stress-test-docs.test.js +1 -1
- package/bin/pipeline-review-docs.test.js +118 -0
- package/bin/plan-review-budget-docs.test.js +63 -0
- package/bin/repo-target-docs.test.js +41 -0
- package/bin/report-status-docs.test.js +10 -2
- package/bin/seam-coverage-docs.test.js +46 -0
- package/bin/session-trust-docs.test.js +37 -0
- package/bin/standards-brief-docs.test.js +112 -0
- package/bin/surfaced-scope-docs.test.js +57 -0
- package/bin/test-once-docs.test.js +101 -0
- package/bin/test-run-docs.test.js +47 -0
- package/bin/trailhead.js +15 -8
- package/bin/trailhead.test.js +71 -17
- package/bin/unblock-docs.test.js +56 -0
- package/bin/yes-accepts-recommended-docs.test.js +54 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/agents/trailhead-code-review.md +2 -1
- package/plugins/trailhead/agents/trailhead-codebase-map.md +1 -1
- package/plugins/trailhead/agents/trailhead-grill.md +41 -0
- package/plugins/trailhead/agents/trailhead-plan.md +1 -1
- package/plugins/trailhead/agents/trailhead-verify.md +1 -0
- package/plugins/trailhead/hooks/lib/audit-walk.test.js +62 -0
- package/plugins/trailhead/hooks/lib/capture.test.js +439 -0
- package/plugins/trailhead/hooks/lib/checkout-find.test.js +195 -0
- package/plugins/trailhead/hooks/lib/checkout-in.test.js +83 -7
- package/plugins/trailhead/hooks/lib/dashboard.test.js +19 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.js +89 -0
- package/plugins/trailhead/hooks/lib/gh-auth-preflight.test.js +132 -0
- package/plugins/trailhead/hooks/lib/git-env-mod.test.js +55 -0
- package/plugins/trailhead/hooks/lib/git-env-spawn.test.js +73 -0
- package/plugins/trailhead/hooks/lib/git-env.test.js +51 -0
- package/plugins/trailhead/hooks/lib/global-config-dir.test.js +48 -0
- package/plugins/trailhead/hooks/lib/heredoc-mod.test.js +85 -0
- package/plugins/trailhead/hooks/lib/inbox-needs-info.test.js +179 -0
- package/plugins/trailhead/hooks/lib/inherit-session.js +17 -0
- package/plugins/trailhead/hooks/lib/issue-body.test.js +165 -0
- package/plugins/trailhead/hooks/lib/model-defaults-review.js +22 -1
- package/plugins/trailhead/hooks/lib/model-defaults-review.test.js +64 -1
- package/plugins/trailhead/hooks/lib/model-route.js +207 -0
- package/plugins/trailhead/hooks/lib/model-route.test.js +231 -0
- package/plugins/trailhead/hooks/lib/needs-you.test.js +45 -1
- package/plugins/trailhead/hooks/lib/plan-review-rounds.test.js +272 -0
- package/plugins/trailhead/hooks/lib/repo-target.test.js +256 -0
- package/plugins/trailhead/hooks/lib/report-status.test.js +56 -1
- package/plugins/trailhead/hooks/lib/statusline-git-env.test.js +47 -0
- package/plugins/trailhead/hooks/lib/test-run.test.js +757 -0
- package/plugins/trailhead/hooks/lib/trailhead-git-env.js +40 -0
- package/plugins/trailhead/hooks/mod/README.md +42 -17
- package/plugins/trailhead/hooks/mod/auto-run.ts +2 -1
- package/plugins/trailhead/hooks/mod/git-env.ts +31 -0
- package/plugins/trailhead/hooks/mod/heredoc.ts +79 -0
- package/plugins/trailhead/hooks/mod/map-pane-view.ts +3 -1
- package/plugins/trailhead/hooks/mod/map-pane.tsx +204 -49
- package/plugins/trailhead/hooks/mod/my-tickets.ts +76 -0
- package/plugins/trailhead/hooks/mod/repo-pin.ts +113 -9
- package/plugins/trailhead/hooks/mod/work-state-locate.ts +2 -1
- package/plugins/trailhead/hooks/trailhead-bash-guards.js +8 -8
- package/plugins/trailhead/hooks/trailhead-bash-guards.test.js +28 -14
- package/plugins/trailhead/hooks/trailhead-check-update.js +20 -2
- package/plugins/trailhead/hooks/trailhead-check-update.test.js +60 -0
- package/plugins/trailhead/hooks/trailhead-labels-guard.js +2 -2
- package/plugins/trailhead/hooks/trailhead-labels-guard.test.js +1 -1
- package/plugins/trailhead/hooks/trailhead-test-guard.js +232 -0
- package/plugins/trailhead/hooks/trailhead-test-guard.test.js +172 -0
- package/plugins/trailhead/skills/_shared/capture-core.md +123 -0
- package/plugins/trailhead/skills/_shared/choices.md +2 -2
- package/plugins/trailhead/skills/_shared/configuration-reference.md +30 -16
- package/plugins/trailhead/skills/_shared/configuration.md +1 -1
- package/plugins/trailhead/skills/_shared/load-first.md +2 -0
- package/plugins/trailhead/skills/_shared/session-handoff.md +1 -1
- package/plugins/trailhead/skills/_shared/substrate-commands.md +19 -6
- package/plugins/trailhead/skills/_shared/substrate.md +4 -3
- package/plugins/trailhead/skills/_shared/surfaced-scope.md +23 -0
- package/plugins/trailhead/skills/_shared/teamwork.md +11 -5
- package/plugins/trailhead/skills/_shared/techniques/acceptance-testing.md +1 -1
- package/plugins/trailhead/skills/_shared/techniques/code-review-pipeline.md +24 -0
- package/plugins/trailhead/skills/_shared/techniques/code-review.md +22 -6
- package/plugins/trailhead/skills/_shared/techniques/codebase-map.md +6 -3
- package/plugins/trailhead/skills/_shared/techniques/debug.md +5 -2
- package/plugins/trailhead/skills/_shared/techniques/grilling.md +28 -3
- package/plugins/trailhead/skills/_shared/techniques/plan-review.md +16 -4
- package/plugins/trailhead/skills/_shared/techniques/research.md +4 -1
- package/plugins/trailhead/skills/_shared/techniques/tdd.md +4 -2
- package/plugins/trailhead/skills/_shared/techniques/verify.md +5 -1
- package/plugins/trailhead/skills/_shared/techniques.md +8 -6
- package/plugins/trailhead/skills/trailhead-capture/SKILL.md +2 -4
- package/plugins/trailhead/skills/trailhead-capture/references/capture.md +29 -4
- package/plugins/trailhead/skills/trailhead-chart/SKILL.md +2 -2
- package/plugins/trailhead/skills/trailhead-chart/references/charting.md +5 -5
- package/plugins/trailhead/skills/trailhead-chart/references/inbox.md +11 -6
- package/plugins/trailhead/skills/trailhead-manage/references/auditing.md +18 -3
- package/plugins/trailhead/skills/trailhead-work/SKILL.md +12 -9
- package/plugins/trailhead/skills/trailhead-work/references/auto.md +3 -1
- package/plugins/trailhead/skills/trailhead-work/references/executor-brief.md +2 -0
- package/plugins/trailhead/skills/trailhead-work/references/ticket-engines.md +13 -13
- package/plugins/trailhead/templates/statusline-trailhead.sh +10 -3
- package/plugins/trailhead/templates/trailhead-audit-walk.js +56 -4
- package/plugins/trailhead/templates/trailhead-auto-merge.js +2 -1
- package/plugins/trailhead/templates/trailhead-auto-queue.js +2 -1
- package/plugins/trailhead/templates/trailhead-auto-slot.js +2 -1
- package/plugins/trailhead/templates/trailhead-base-preflight.js +10 -5
- package/plugins/trailhead/templates/trailhead-capture.js +419 -0
- package/plugins/trailhead/templates/trailhead-checkout-find.js +257 -0
- package/plugins/trailhead/templates/trailhead-checkout-in.js +78 -21
- package/plugins/trailhead/templates/trailhead-commit-msg-sync.js +2 -1
- package/plugins/trailhead/templates/trailhead-dashboard.js +15 -2
- package/plugins/trailhead/templates/trailhead-git-env.js +40 -0
- package/plugins/trailhead/templates/trailhead-inbox-needs-info.js +161 -0
- package/plugins/trailhead/templates/trailhead-isolation-answer.js +2 -1
- package/plugins/trailhead/templates/trailhead-isolation-copy.js +9 -4
- package/plugins/trailhead/templates/trailhead-issue-body.js +161 -12
- package/plugins/trailhead/templates/trailhead-merge-ticket.js +2 -1
- package/plugins/trailhead/templates/trailhead-needs-you.js +38 -4
- package/plugins/trailhead/templates/trailhead-plan-review-rounds.js +261 -7
- package/plugins/trailhead/templates/trailhead-repo-target.js +302 -0
- package/plugins/trailhead/templates/trailhead-report-status.js +57 -9
- package/plugins/trailhead/templates/trailhead-test-run.js +752 -0
- package/plugins/trailhead/templates/trailhead-uat-backfill.js +2 -1
- package/plugins/trailhead/templates/trailhead-uat-followup.js +2 -1
- package/plugins/trailhead/templates/trailhead-uat-setup.js +9 -4
- package/plugins/trailhead/templates/trailhead-unblock.js +2 -1
- package/plugins/trailhead/tests/auto-run-ui.test.ts +2 -1
- package/plugins/trailhead/tests/git-env.test.ts +20 -0
- package/plugins/trailhead/tests/map-pane-ui.test.ts +386 -50
- package/plugins/trailhead/tests/map-pane-view.test.ts +6 -0
- package/plugins/trailhead/tests/map-pane-write.test.ts +57 -1
- package/plugins/trailhead/tests/my-tickets.test.ts +66 -0
- package/plugins/trailhead/tests/repo-pin-late-repo.test.ts +3 -2
- package/plugins/trailhead/tests/repo-pin-relocate.test.ts +175 -0
- package/plugins/trailhead/tests/repo-pin.test.ts +20 -5
- package/plugins/trailhead/tests/unscrub.ts +8 -0
- package/plugins/trailhead/tests/work-state-prompt-hook.test.ts +3 -2
- package/plugins/trailhead/tests/work-state-worktree.test.ts +2 -1
- package/plugins/trailhead/types/index.d.ts +7 -0
|
@@ -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), 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.
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
|
@@ -191,10 +191,11 @@ function codexTechniqueKey(name) {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
// --- MODEL_KEYS --------------------------------------------------------------
|
|
194
|
-
// The
|
|
195
|
-
// other committed agent (fix) has no pin key
|
|
196
|
-
//
|
|
197
|
-
const
|
|
194
|
+
// The 8 pinnable technique keys, mirroring the models.codex.* namespace. The
|
|
195
|
+
// other committed agent (fix) has no pin key of its own: it tracks execute.
|
|
196
|
+
// Riconoscimento condiviso di "inherit session" (stesso del router, hooks/lib); modulo senza dipendenze, la proiezione resta pura.
|
|
197
|
+
const { isInheritSession } = require('../../plugins/trailhead/hooks/lib/inherit-session.js');
|
|
198
|
+
const MODEL_KEYS = new Set(['plan', 'execute', 'research', 'review', 'debug', 'verify', 'codebase-map', 'grill']);
|
|
198
199
|
|
|
199
200
|
// --- codexAgentToml ---------------------------------------------------------
|
|
200
201
|
// Render one Codex custom-agent TOML. When `model` is empty/absent, the TOML
|
|
@@ -238,9 +239,10 @@ function codexAgentToml({ name, description, developerInstructions, model, effor
|
|
|
238
239
|
// readAgentDefs(): every committed plugins/trailhead/agents/trailhead-*.md,
|
|
239
240
|
// each { name, description, tools, body }), return the set of
|
|
240
241
|
// trailhead-<technique>.toml writes the installer should make: ONE per agent,
|
|
241
|
-
// uniformly (all
|
|
242
|
-
// review/debug/verify/codebase-map) carries its models.codex.<key> pin when set, else is
|
|
243
|
-
// pin-less;
|
|
242
|
+
// uniformly (all 9 project, always). A keyed agent (plan/execute/research/
|
|
243
|
+
// review/debug/verify/codebase-map/grill) carries its models.codex.<key> pin when set, else is
|
|
244
|
+
// pin-less; fix has no key and carries the models.codex.execute pin (same
|
|
245
|
+
// string/object/effort/inherit-session handling), so no agent is keyless.
|
|
244
246
|
// Iterates agentDefs in the given order (readAgentDefs sorts by name) so the
|
|
245
247
|
// write order is deterministic.
|
|
246
248
|
function codexAgentTomlPlan(codexHome, codexModels, agentDefs) {
|
|
@@ -256,13 +258,19 @@ function codexAgentTomlPlan(codexHome, codexModels, agentDefs) {
|
|
|
256
258
|
|
|
257
259
|
let model = null;
|
|
258
260
|
let effort = null;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
+
// grill non impostato segue models.codex.plan (come model-route.js).
|
|
262
|
+
// fix non ha chiave propria: segue models.codex.execute (come model-route.js).
|
|
263
|
+
let pinKey = key === 'grill' && (models.grill == null || (typeof models.grill === 'string' && models.grill.trim() === '')) ? 'plan' : key;
|
|
264
|
+
if (key === 'fix') pinKey = 'execute';
|
|
265
|
+
if ((MODEL_KEYS.has(key) || key === 'fix') && models[pinKey] != null) {
|
|
266
|
+
const raw = models[pinKey];
|
|
267
|
+
// "inherit session" e' il default Codex documentato: nessun pin.
|
|
268
|
+
const isInherit = isInheritSession;
|
|
261
269
|
if (typeof raw === 'string') {
|
|
262
|
-
if (raw.trim() !== '') model = raw;
|
|
270
|
+
if (raw.trim() !== '' && !isInherit(raw)) model = raw;
|
|
263
271
|
} else if (typeof raw === 'object') {
|
|
264
272
|
const m = typeof raw.model === 'string' ? raw.model : '';
|
|
265
|
-
if (m.trim() !== '') {
|
|
273
|
+
if (m.trim() !== '' && !isInherit(m)) {
|
|
266
274
|
model = m;
|
|
267
275
|
effort = typeof raw.effort === 'string' ? raw.effort : null;
|
|
268
276
|
}
|
|
@@ -373,9 +381,9 @@ function codexVerbSkillPlan(codexHome, verbs) {
|
|
|
373
381
|
// --- codexHookEntries -----------------------------------------------------
|
|
374
382
|
// The guardrail hooks trailhead registers on Codex, as {event, matcher,
|
|
375
383
|
// command} records. Commands run the copied hook scripts under hooksScriptsDir
|
|
376
|
-
// via node, matching Claude's `node "<path>"` form. The
|
|
377
|
-
// (commit, secret, install, body, labels) are ONE registration, the merged
|
|
378
|
-
// 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
|
|
379
387
|
// node process per Bash call); secret-read is a separate PreToolUse hook. All
|
|
380
388
|
// PreToolUse guards can veto; the injection-scanner
|
|
381
389
|
// is a PostToolUse advisory; check-update runs at SessionStart. Reused by the
|
|
@@ -388,7 +396,7 @@ function codexVerbSkillPlan(codexHome, verbs) {
|
|
|
388
396
|
function codexHookEntries(hooksScriptsDir) {
|
|
389
397
|
const cmd = (name) => `node "${path.join(hooksScriptsDir, name)}"`;
|
|
390
398
|
return [
|
|
391
|
-
{ 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` },
|
|
392
400
|
{ event: 'PreToolUse', matcher: 'Read|Bash', command: cmd('trailhead-secret-read-guard.js') },
|
|
393
401
|
{ event: 'PostToolUse', matcher: 'Bash', command: cmd('trailhead-issue-injection-scanner.js') },
|
|
394
402
|
{ event: 'Stop', matcher: '', command: cmd('trailhead-mockup-link-stop.js') },
|
|
@@ -457,6 +465,7 @@ module.exports = {
|
|
|
457
465
|
codexClusterAgentsYaml,
|
|
458
466
|
codexAgentToml,
|
|
459
467
|
codexAgentTomlPlan,
|
|
468
|
+
MODEL_KEYS,
|
|
460
469
|
codexVerbSkillContent,
|
|
461
470
|
codexVerbSkillAgentsYaml,
|
|
462
471
|
codexVerbSkillPlan,
|
|
@@ -94,8 +94,22 @@ 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'));
|
|
111
|
+
ok('codexSkillAdapterHeader §D says all 9 committed agents, names trailhead-grill, and runs an unpinned grill inline',
|
|
112
|
+
header.includes('all 9 committed') && header.includes('trailhead-grill') && /unpinned or session-equal grill runs inline/.test(header));
|
|
99
113
|
ok('codexSkillAdapterHeader §D warns against a single blocking wait_agent with a poll loop',
|
|
100
114
|
header.includes('bounded wait') && header.includes('poll'));
|
|
101
115
|
ok('codexSkillAdapterHeader §D offers a background-job fallback for a stalling review',
|
|
@@ -117,6 +131,10 @@ ok('codexSkillAdapterHeader §H makes backgrounding reviewers the default',
|
|
|
117
131
|
header.includes('the default is to background the reviewers'));
|
|
118
132
|
ok('codexSkillAdapterHeader §H closes reviewer stdin to avoid the EOF hang',
|
|
119
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'));
|
|
120
138
|
|
|
121
139
|
// --- codexVerbSkillContent (#46) ---
|
|
122
140
|
ok('codexVerbSkillContent: SKILL.md frontmatter is at byte 0 with name+description', (() => {
|
|
@@ -173,10 +191,10 @@ const entries = codexHookEntries('/h');
|
|
|
173
191
|
ok('codexHookEntries returns 5 entries', Array.isArray(entries) && entries.length === 5);
|
|
174
192
|
const bashEntries = entries.filter((e) => e.event === 'PreToolUse' && e.matcher === 'Bash');
|
|
175
193
|
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,labels',
|
|
177
|
-
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');
|
|
178
196
|
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)));
|
|
197
|
+
!/trailhead-(commit|secret|install|search|body|test)-guard\.js/.test(e.command)));
|
|
180
198
|
ok('codexHookEntries: secret-read-guard is PreToolUse/Read|Bash', entries.some((e) =>
|
|
181
199
|
e.event === 'PreToolUse' && e.matcher === 'Read|Bash' && e.command.includes('trailhead-secret-read-guard.js')));
|
|
182
200
|
ok('codexHookEntries: injection-scanner is PostToolUse/Bash', entries.some((e) =>
|
|
@@ -261,10 +279,10 @@ ok('codexAgentToml escapes a " and \\ in name/description so the TOML stays vali
|
|
|
261
279
|
})());
|
|
262
280
|
|
|
263
281
|
// --- codexAgentTomlPlan ---
|
|
264
|
-
//
|
|
265
|
-
// sources:
|
|
282
|
+
// 8-entry stub agentDefs mirroring the real plugins/trailhead/agents/*.md
|
|
283
|
+
// sources: 7 keyed techniques (plan/execute/research/review/debug/codebase-map/grill,
|
|
266
284
|
// whose filenames derive from trailhead-executor -> execute and
|
|
267
|
-
// trailhead-code-review -> review) +
|
|
285
|
+
// trailhead-code-review -> review) + fix, which tracks the execute pin.
|
|
268
286
|
const STUB_AGENT_DEFS = [
|
|
269
287
|
{ name: 'trailhead-plan', description: 'plan desc', tools: 'Read', body: 'Plan body.' },
|
|
270
288
|
{ name: 'trailhead-executor', description: 'execute desc', tools: 'Read, Write', body: 'Execute body.' },
|
|
@@ -273,37 +291,76 @@ const STUB_AGENT_DEFS = [
|
|
|
273
291
|
{ name: 'trailhead-debug', description: 'debug desc', tools: 'Read', body: 'Debug body.' },
|
|
274
292
|
{ name: 'trailhead-fix', description: 'fix desc', tools: 'Read, Write', body: 'Fix body.' },
|
|
275
293
|
{ name: 'trailhead-codebase-map', description: 'map desc', tools: 'Read', body: 'Map body.' },
|
|
294
|
+
{ name: 'trailhead-grill', description: 'grill desc', tools: 'Read', body: 'Grill body.' },
|
|
276
295
|
];
|
|
277
296
|
|
|
278
|
-
ok('codexAgentTomlPlan:
|
|
297
|
+
ok('codexAgentTomlPlan: 8 agentDefs -> 8 writes with only execute (and fix, which tracks it) carrying a model', (() => {
|
|
279
298
|
const plan = codexAgentTomlPlan('/c', { execute: 'gpt-5.6-terra' }, STUB_AGENT_DEFS);
|
|
280
|
-
if (plan.writes.length !==
|
|
299
|
+
if (plan.writes.length !== 8) return false;
|
|
281
300
|
const byName = Object.fromEntries(plan.writes.map((w) => [w.name, w]));
|
|
282
301
|
const execute = byName['trailhead-execute'];
|
|
283
302
|
if (!execute || execute.path !== '/c/agents/trailhead-execute.toml') return false;
|
|
284
303
|
if (!execute.content.includes('model = "gpt-5.6-terra"')) return false;
|
|
285
|
-
const
|
|
304
|
+
const fix = byName['trailhead-fix'];
|
|
305
|
+
if (!fix || !fix.content.includes('model = "gpt-5.6-terra"')) return false;
|
|
306
|
+
const others = plan.writes.filter((w) => w.name !== 'trailhead-execute' && w.name !== 'trailhead-fix');
|
|
286
307
|
return others.length === 6 && others.every((w) => !/^model = /m.test(w.content));
|
|
287
308
|
})());
|
|
288
309
|
|
|
289
|
-
ok('codexAgentTomlPlan:
|
|
290
|
-
const
|
|
310
|
+
ok('codexAgentTomlPlan: models.codex.grill pins trailhead-grill.toml; unset leaves it pin-less', (() => {
|
|
311
|
+
const pinned = codexAgentTomlPlan('/c', { grill: 'gpt-5.6-sol' }, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-grill');
|
|
312
|
+
const unset = codexAgentTomlPlan('/c', {}, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-grill');
|
|
313
|
+
return pinned && pinned.path === '/c/agents/trailhead-grill.toml' && pinned.content.includes('model = "gpt-5.6-sol"') &&
|
|
314
|
+
unset && !/^model = /m.test(unset.content);
|
|
315
|
+
})());
|
|
316
|
+
|
|
317
|
+
ok('codexAgentTomlPlan: an unset models.codex.grill follows models.codex.plan; a set grill wins', (() => {
|
|
318
|
+
const find = (m) => codexAgentTomlPlan('/c', m, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-grill');
|
|
319
|
+
const follow = find({ plan: { model: 'gpt-5.6-sol', effort: 'high' } });
|
|
320
|
+
const own = find({ plan: 'gpt-5.6-sol', grill: 'gpt-5.6-terra' });
|
|
321
|
+
const none = find({ execute: 'gpt-5.6-sol' });
|
|
322
|
+
const plan = codexAgentTomlPlan('/c', { plan: 'gpt-5.6-sol' }, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-plan');
|
|
323
|
+
return follow.content.includes('model = "gpt-5.6-sol"') && follow.content.includes('model_reasoning_effort = "high"') &&
|
|
324
|
+
own.content.includes('model = "gpt-5.6-terra"') && !/^model = /m.test(none.content) && plan.content.includes('model = "gpt-5.6-sol"');
|
|
325
|
+
})());
|
|
326
|
+
|
|
327
|
+
ok('codexAgentTomlPlan: a literal inherit session is pin-less for every key, and grill follows it', (() => {
|
|
328
|
+
const find = (m, n) => codexAgentTomlPlan('/c', m, STUB_AGENT_DEFS).writes.find((w) => w.name === n);
|
|
329
|
+
const pinned = (w) => /^model = /m.test(w.content);
|
|
330
|
+
const a = { plan: 'inherit session' };
|
|
331
|
+
const b = { plan: 'gpt-5.6-sol', grill: 'inherit session' };
|
|
332
|
+
const c = { execute: ' Inherit Session ', research: { model: 'inherit session', effort: 'high' } };
|
|
333
|
+
return !pinned(find(a, 'trailhead-plan')) && !pinned(find(a, 'trailhead-grill')) &&
|
|
334
|
+
!pinned(find(b, 'trailhead-grill')) && find(b, 'trailhead-plan').content.includes('model = "gpt-5.6-sol"') &&
|
|
335
|
+
!pinned(find(c, 'trailhead-execute')) && !pinned(find(c, 'trailhead-research')) &&
|
|
336
|
+
!find(c, 'trailhead-research').content.includes('model_reasoning_effort');
|
|
337
|
+
})());
|
|
338
|
+
|
|
339
|
+
ok('codexAgentTomlPlan: trailhead-fix carries the execute pin (not a fix key); trailhead-codebase-map pins on codebase-map', (() => {
|
|
340
|
+
const plan = codexAgentTomlPlan('/c', { execute: { model: 'gpt-5.6-sol', effort: 'high' }, fix: 'gpt-5.6-luna', 'codebase-map': 'gpt-5.6-terra' }, STUB_AGENT_DEFS);
|
|
291
341
|
const byName = Object.fromEntries(plan.writes.map((w) => [w.name, w]));
|
|
292
342
|
const fix = byName['trailhead-fix'];
|
|
293
343
|
const map = byName['trailhead-codebase-map'];
|
|
294
344
|
return fix && map &&
|
|
295
|
-
fix.path === '/c/agents/trailhead-fix.toml' &&
|
|
345
|
+
fix.path === '/c/agents/trailhead-fix.toml' && fix.content.includes('model = "gpt-5.6-sol"') &&
|
|
346
|
+
fix.content.includes('model_reasoning_effort = "high"') && !fix.content.includes('gpt-5.6-luna') &&
|
|
296
347
|
map.path === '/c/agents/trailhead-codebase-map.toml' && map.content.includes('model = "gpt-5.6-terra"');
|
|
297
348
|
})());
|
|
298
349
|
|
|
299
|
-
ok('codexAgentTomlPlan:
|
|
350
|
+
ok('codexAgentTomlPlan: trailhead-fix is pin-less when execute is unset or inherit session', (() => {
|
|
351
|
+
const find = (m) => codexAgentTomlPlan('/c', m, STUB_AGENT_DEFS).writes.find((w) => w.name === 'trailhead-fix');
|
|
352
|
+
return ![{}, { plan: 'gpt-5.6-sol' }, { execute: 'inherit session' }, { execute: { model: ' Inherit Session ', effort: 'high' } }]
|
|
353
|
+
.some((m) => /^model = /m.test(find(m).content) || find(m).content.includes('model_reasoning_effort'));
|
|
354
|
+
})());
|
|
355
|
+
|
|
356
|
+
ok('codexAgentTomlPlan: empty models object -> 8 pin-less writes (not zero)', (() => {
|
|
300
357
|
const plan = codexAgentTomlPlan('/c', {}, STUB_AGENT_DEFS);
|
|
301
|
-
return plan.writes.length ===
|
|
358
|
+
return plan.writes.length === 8 && plan.writes.every((w) => !/^model = /m.test(w.content));
|
|
302
359
|
})());
|
|
303
360
|
|
|
304
|
-
ok('codexAgentTomlPlan: null models ->
|
|
361
|
+
ok('codexAgentTomlPlan: null models -> 8 pin-less writes (not zero)', (() => {
|
|
305
362
|
const plan = codexAgentTomlPlan('/c', null, STUB_AGENT_DEFS);
|
|
306
|
-
return plan.writes.length ===
|
|
363
|
+
return plan.writes.length === 8 && plan.writes.every((w) => !/^model = /m.test(w.content));
|
|
307
364
|
})());
|
|
308
365
|
|
|
309
366
|
ok('codexAgentTomlPlan: object model value on the execute key carries model + model_reasoning_effort', (() => {
|
|
@@ -315,7 +372,7 @@ ok('codexAgentTomlPlan: object model value on the execute key carries model + mo
|
|
|
315
372
|
ok('codexAgentTomlPlan: a keyed key whose object value has no model stays pin-less', (() => {
|
|
316
373
|
const plan = codexAgentTomlPlan('/c', { execute: { effort: 'high' } }, STUB_AGENT_DEFS);
|
|
317
374
|
const execute = plan.writes.find((w) => w.name === 'trailhead-execute');
|
|
318
|
-
return plan.writes.length ===
|
|
375
|
+
return plan.writes.length === 8 && !/^model = /m.test(execute.content) && !execute.content.includes('model_reasoning_effort');
|
|
319
376
|
})());
|
|
320
377
|
|
|
321
378
|
ok('codexAgentTomlPlan: description is routed through convertToCodex (host paths rewritten)', (() => {
|
|
@@ -470,16 +527,16 @@ ok('convertToCodex keeps CLAUDE_CODE_SESSION_ID intact',
|
|
|
470
527
|
const shared = parseSource(fs.readFileSync(path.join(__dirname, '../../scripts/agent-blocks.md'), 'utf8'));
|
|
471
528
|
const agentsDir = path.join(__dirname, '../../plugins/trailhead/agents');
|
|
472
529
|
const files = fs.readdirSync(agentsDir).filter((f) => /^trailhead-.*\.md$/.test(f));
|
|
473
|
-
ok('projection boundaries: found the
|
|
530
|
+
ok('projection boundaries: found the nine real agents', files.length === 9);
|
|
474
531
|
ok('projection boundaries: source has common and implementing blocks', !!shared.common && !!shared.implementing);
|
|
475
532
|
// #249: every agent's projected TOML carries exactly its family block, whole.
|
|
476
533
|
const familyOf = {
|
|
477
534
|
'trailhead-verify': 'reviewing', 'trailhead-code-review': 'reviewing',
|
|
478
535
|
'trailhead-executor': 'implementing', 'trailhead-fix': 'implementing',
|
|
479
536
|
'trailhead-debug': 'investigating', 'trailhead-research': 'investigating', 'trailhead-codebase-map': 'investigating',
|
|
480
|
-
'trailhead-plan': 'plan',
|
|
537
|
+
'trailhead-plan': 'plan', 'trailhead-grill': 'grilling',
|
|
481
538
|
};
|
|
482
|
-
const families = ['reviewing', 'implementing', 'investigating', 'plan'];
|
|
539
|
+
const families = ['reviewing', 'implementing', 'investigating', 'plan', 'grilling'];
|
|
483
540
|
ok('projection boundaries: source has every family block', families.every((f) => !!shared[f]));
|
|
484
541
|
for (const f of files) {
|
|
485
542
|
const name = f.slice(0, -3);
|
|
@@ -196,7 +196,7 @@ function degradations(host) {
|
|
|
196
196
|
|
|
197
197
|
if (descriptor.axes.subagentToolkit === 'none') {
|
|
198
198
|
entries.push({
|
|
199
|
-
from: 'subagent fan-out (research, codebase-map, review, plan, execute)',
|
|
199
|
+
from: 'subagent fan-out (research, codebase-map, review, plan, execute, grill)',
|
|
200
200
|
to: 'inline sequential execution in the main session',
|
|
201
201
|
});
|
|
202
202
|
}
|
|
@@ -105,6 +105,12 @@ ok('modelsCollapseNotice(codex, models set) names the set keys', (() => {
|
|
|
105
105
|
const msg = modelsCollapseNotice('codex', { models: { plan: 'x', execute: 'y' } });
|
|
106
106
|
return typeof msg === 'string' && msg.includes('plan') && msg.includes('execute') && msg.includes('Codex');
|
|
107
107
|
})());
|
|
108
|
+
ok('modelsCollapseNotice(codex, models.grill set) names grill', (() => {
|
|
109
|
+
const msg = modelsCollapseNotice('codex', { models: { grill: 'claude-opus-5-5' } });
|
|
110
|
+
return typeof msg === 'string' && msg.includes('(grill)') && msg.includes('models.codex');
|
|
111
|
+
})());
|
|
112
|
+
ok('modelsCollapseNotice is null once models.codex.grill is set',
|
|
113
|
+
modelsCollapseNotice('codex', { models: { grill: 'claude-opus-5-5', codex: { grill: 'gpt-5.6-sol' } } }) === null);
|
|
108
114
|
ok('modelsCollapseNotice(codex, undefined config) is null',
|
|
109
115
|
modelsCollapseNotice('codex') === null);
|
|
110
116
|
ok('modelsCollapseNotice(claude, models set) is null (Claude honours the keys)',
|
|
@@ -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-
|
|
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-
|
|
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
|
|
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`);
|