@marcomigozzi/trailhead 0.3.0 → 0.4.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 +354 -0
- package/README.md +18 -10
- package/bin/lib/codex-projection.js +400 -0
- package/bin/lib/codex-projection.test.js +252 -0
- package/bin/lib/host-descriptor.js +404 -0
- package/bin/lib/host-descriptor.test.js +172 -0
- package/bin/trailhead.js +454 -66
- package/bin/trailhead.test.js +274 -0
- package/package.json +1 -1
- package/plugins/trailhead/.claude-plugin/plugin.json +1 -1
- package/plugins/trailhead/hooks/lib/commit-message-check.js +74 -0
- package/plugins/trailhead/hooks/lib/commit-message-check.test.js +83 -0
- package/plugins/trailhead/hooks/trailhead-check-update.js +54 -24
- package/plugins/trailhead/hooks/trailhead-commit-guard.js +7 -10
- package/plugins/trailhead/skills/trailhead/SKILL.md +13 -11
- package/plugins/trailhead/skills/trailhead/references/capture.md +3 -1
- package/plugins/trailhead/skills/trailhead/references/configuration.md +7 -1
- package/plugins/trailhead/skills/trailhead/references/teamwork.md +3 -1
- package/plugins/trailhead/skills/trailhead/references/techniques/plan-review.md +1 -1
- package/plugins/trailhead/skills/trailhead/references/techniques/prototype.md +1 -1
- package/plugins/trailhead/templates/statusline-trailhead.sh +40 -8
- package/plugins/trailhead/templates/trailhead-commit-msg +57 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// codex-projection.js: pure host-native artifact projection for Codex CLI.
|
|
3
|
+
//
|
|
4
|
+
// trailhead ships one engine, written for Claude Code. This module is the
|
|
5
|
+
// single place that knows how to turn that Claude-native prose into
|
|
6
|
+
// Codex-native artifacts: the native-skill layout Codex expects things
|
|
7
|
+
// placed at (skills/trailhead/), the deterministic text rewrites that swap
|
|
8
|
+
// Claude's command surface/paths for Codex's, the adapter header prepended
|
|
9
|
+
// to the projected SKILL.md, and the agents/openai.yaml UI metadata that
|
|
10
|
+
// registers the skill for explicit-only invocation (`$trailhead <verb>`).
|
|
11
|
+
//
|
|
12
|
+
// Pure module: no fs/process access here. The installer (bin/trailhead.js)
|
|
13
|
+
// does all the I/O and calls into these functions.
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
// --- codexLayout --------------------------------------------------------
|
|
18
|
+
// Absolute paths for every artifact this module projects, rooted at a given
|
|
19
|
+
// Codex config dir (normally $CODEX_HOME or ~/.codex, resolved upstream by
|
|
20
|
+
// host-descriptor's configDirFor).
|
|
21
|
+
function codexLayout(codexHome) {
|
|
22
|
+
const skillsRoot = path.join(codexHome, 'skills');
|
|
23
|
+
const skillDir = path.join(skillsRoot, 'trailhead');
|
|
24
|
+
return {
|
|
25
|
+
skillsRoot,
|
|
26
|
+
skillDir,
|
|
27
|
+
skillMain: path.join(skillDir, 'SKILL.md'),
|
|
28
|
+
referencesDir: path.join(skillDir, 'references'),
|
|
29
|
+
agentsDir: path.join(skillDir, 'agents'),
|
|
30
|
+
agentsYaml: path.join(skillDir, 'agents', 'openai.yaml'),
|
|
31
|
+
// The REAL Codex custom-agent dir, rooted at the codex HOME (distinct
|
|
32
|
+
// from the skill-local agentsDir/agentsYaml above, which hold the
|
|
33
|
+
// openai.yaml UI metadata). This is where trailhead-<technique>.toml
|
|
34
|
+
// model pins land so Codex's native multi_agent_v2 registry finds them.
|
|
35
|
+
codexAgentsDir: path.join(codexHome, 'agents'),
|
|
36
|
+
templatesDir: path.join(skillDir, 'templates'),
|
|
37
|
+
versionFile: path.join(skillDir, 'VERSION'),
|
|
38
|
+
hooksScriptsDir: path.join(skillDir, 'hooks'),
|
|
39
|
+
hooksJson: path.join(codexHome, 'hooks.json'),
|
|
40
|
+
configToml: path.join(codexHome, 'config.toml'),
|
|
41
|
+
// Active Codex prompts dir: holds the #41 discoverability shims
|
|
42
|
+
// (trailhead.md smart entry + one trailhead-<verb>.md per verb). The
|
|
43
|
+
// install/uninstall sweep also migrates the pre-#25 layout out of it.
|
|
44
|
+
promptsDir: path.join(codexHome, 'prompts'),
|
|
45
|
+
legacyEngineDir: path.join(codexHome, 'trailhead'),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- convertToCodex ------------------------------------------------------
|
|
50
|
+
// Deterministic text rewrites, Claude-native markdown in, Codex-native
|
|
51
|
+
// markdown out. Order matters: the anchored path forms must run before the
|
|
52
|
+
// bare form (`.claude/`), and the command-surface rewrite is slash-anchored
|
|
53
|
+
// so bare GitHub labels like `trailhead:build` (no leading slash) never get
|
|
54
|
+
// touched, only the `/trailhead:<verb>` / `/trailhead` command surface does.
|
|
55
|
+
function convertToCodex(md) {
|
|
56
|
+
let out = md;
|
|
57
|
+
|
|
58
|
+
// 1. Command surface: /trailhead:<verb> -> $trailhead <verb>; bare
|
|
59
|
+
// /trailhead -> $trailhead. Slash-anchored so bare labels like
|
|
60
|
+
// `trailhead:build` (no leading slash) survive. The colon rule MUST run
|
|
61
|
+
// before the bare rule. The bare rule uses a negative lookahead, NOT \b:
|
|
62
|
+
// \b treats the boundary before `/` and `-` as a word edge, so `\b` would
|
|
63
|
+
// wrongly eat `/trailhead` inside PATH segments (`skills/trailhead/...`,
|
|
64
|
+
// `templates/trailhead-commit-msg`). Excluding `/`, `-`, `:` and word
|
|
65
|
+
// chars converts only the standalone `/trailhead` command, never a path.
|
|
66
|
+
out = out.replace(/\/trailhead:/g, '$trailhead ');
|
|
67
|
+
out = out.replace(/\/trailhead(?![\w:/-])/g, '$trailhead');
|
|
68
|
+
|
|
69
|
+
// 2. Claude's /clear -> Codex's /new.
|
|
70
|
+
out = out.replace(/\/clear\b/g, '/new');
|
|
71
|
+
|
|
72
|
+
// 3. Paths: anchored forms first, then the bare form.
|
|
73
|
+
out = out.replace(/\$HOME\/\.claude\b/g, '$HOME/.codex');
|
|
74
|
+
out = out.replace(/~\/\.claude\b/g, '~/.codex');
|
|
75
|
+
out = out.replace(/(?<![A-Za-z0-9_\-.\/~$])\.claude\//g, '.codex/');
|
|
76
|
+
|
|
77
|
+
// 4. Plugin root -> the installed skill dir.
|
|
78
|
+
out = out.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, '~/.codex/skills/trailhead');
|
|
79
|
+
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- codexSkillAdapterHeader ---------------------------------------------
|
|
84
|
+
// Prepended (plus a blank line) to the converted SKILL.md so a Codex session
|
|
85
|
+
// reading the engine gets the host-mapping rules before the engine body.
|
|
86
|
+
function codexSkillAdapterHeader() {
|
|
87
|
+
return `<codex_skill_adapter>
|
|
88
|
+
This trailhead engine is running on **Codex CLI**, not Claude Code. The installer projected it here; apply these host mappings as you follow the engine below.
|
|
89
|
+
|
|
90
|
+
## A. Commands
|
|
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
|
+
|
|
93
|
+
## B. This IS the skill
|
|
94
|
+
Codex loaded this SKILL.md because you invoked \`$trailhead\`; there is no separate Skill tool to call. When the engine says to load a \`references/*.md\` file, Read it from this skill directory (\`~/.codex/skills/trailhead/\`).
|
|
95
|
+
|
|
96
|
+
## C. AskUserQuestion -> request_user_input
|
|
97
|
+
Where the engine uses \`AskUserQuestion\`, use Codex's \`request_user_input\`: map \`header\`->\`header\`, \`question\`->\`question\`, each option \`{label, description}\`; generate an \`id\` from the header (lowercase, spaces->underscores). Codex has no \`multiSelect\`: present sequential single-selects, or a numbered freeform list and ask for comma-separated numbers. If \`request_user_input\` is unavailable, present the choices as a plain-text numbered list and STOP for the user's reply; do not silently pick a default and proceed.
|
|
98
|
+
|
|
99
|
+
## D. Subagents (native multi_agent)
|
|
100
|
+
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. A subagent runs as a FRESH session that receives a task prompt, not this conversation; run independent ones in parallel (e.g. the codebase-map readers) and \`wait_agent\` on them.
|
|
101
|
+
|
|
102
|
+
**Per-technique model pins (\`agent_type\` dispatch).** When \`models.codex.*\` is set, the installer projects one \`~/.codex/agents/trailhead-<technique>.toml\` per key (that technique's OpenAI model, plus its reasoning effort) and enables \`features.multi_agent_v2\`, so Codex exposes a per-technique agent registry. Detect that 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, \`multi_agent_v2\` is live, so spawn each delegated activity as \`spawn_agent(agent_type="trailhead-<technique>", reasoning_effort=…)\` (\`trailhead-plan\` for the Plan step, \`trailhead-execute\` for Execute, likewise research/review/debug), which routes it onto its pinned model. If the \`spawn_agent\` schema exposes NO \`agent_type\` field (only base multi_agent v1 is present, or no \`models.codex.*\` pin was projected), spawn with base \`spawn_agent\` and the subagents inherit the one session model. Trust the tool surface you actually see and degrade gracefully: \`multi_agent_v2\` is still under development upstream, so v1 stays a supported runtime.
|
|
103
|
+
|
|
104
|
+
\`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.
|
|
105
|
+
|
|
106
|
+
## E. Handoff
|
|
107
|
+
The engine's \`/clear\` is Codex's \`/new\` (start a fresh session). Command references in a handoff use the skill form (\`$trailhead work <n>\`).
|
|
108
|
+
|
|
109
|
+
## F. Lifecycle hooks (native Codex hooks)
|
|
110
|
+
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 and secret-guard as \`PreToolUse\` (matcher \`Bash\`) hooks that can veto the command before it runs, the injection-scanner as a \`PostToolUse\` (matcher \`Bash\`) advisory, and check-update as a \`SessionStart\` hook. 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.
|
|
111
|
+
|
|
112
|
+
## G. Design / UI mockups -> local disk
|
|
113
|
+
The \`claude.ai/design\` mockup mode (DesignSync: the \`claude_design\` MCP + the \`/design-sync\` skill) does not exist on Codex, so it **collapses to local-disk mockups** here: whatever \`config.design\` says, the Prototype technique's \`disk\` path is the only one available. Produce a throwaway static HTML mockup next to the code, link it from the ticket, and never attempt DesignSync / claude.ai/design on Codex (there is no \`claude_design\` MCP or \`/design-sync\` skill to call). \`config.design.approval\` still applies unchanged: \`explicit\` waits for the user's go-ahead before any UI code, \`auto\` surfaces the mockup and proceeds.
|
|
114
|
+
</codex_skill_adapter>`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- injectCodexAdapterHeader --------------------------------------------
|
|
118
|
+
// Insert the adapter header into an already-converted SKILL.md so it sits
|
|
119
|
+
// AFTER the YAML frontmatter, never above it. Codex registers a skill from the
|
|
120
|
+
// frontmatter (`name:` / `description:`) only when it is the very first bytes
|
|
121
|
+
// of SKILL.md; a header prepended above `---` pushes the frontmatter off the
|
|
122
|
+
// top and the skill never registers (#40). Splits the leading `---...---`
|
|
123
|
+
// block off and reassembles frontmatter + header + body. With no frontmatter
|
|
124
|
+
// present, falls back to a plain prepend (nothing to protect).
|
|
125
|
+
function injectCodexAdapterHeader(converted) {
|
|
126
|
+
const header = codexSkillAdapterHeader();
|
|
127
|
+
const m = /^---\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n?/.exec(converted);
|
|
128
|
+
if (!m) return header + '\n\n' + converted;
|
|
129
|
+
const frontmatter = m[0];
|
|
130
|
+
const body = converted.slice(frontmatter.length);
|
|
131
|
+
return frontmatter.replace(/\s+$/, '') + '\n\n' + header + '\n\n' + body.replace(/^\s+/, '');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- codexAgentsYaml -------------------------------------------------------
|
|
135
|
+
// UI metadata for the native-skill surface: display name, short description,
|
|
136
|
+
// a default prompt shown before invocation, and explicit-only invocation
|
|
137
|
+
// (no auto-trigger on unrelated turns).
|
|
138
|
+
function codexAgentsYaml() {
|
|
139
|
+
return `interface:
|
|
140
|
+
display_name: "Trailhead"
|
|
141
|
+
short_description: "Chart & work a project as decision tickets"
|
|
142
|
+
default_prompt: "Use $trailhead to chart a new map, or $trailhead work to take the next ticket."
|
|
143
|
+
policy:
|
|
144
|
+
allow_implicit_invocation: false
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- CODEX_AGENT_TECHNIQUES ------------------------------------------------
|
|
149
|
+
// Metadata for the 5 trailhead activities that can get a per-technique Codex
|
|
150
|
+
// custom-agent TOML: a short UI description and the developer_instructions
|
|
151
|
+
// that steer that subagent on Codex, pointing back at the projected skill.
|
|
152
|
+
const CODEX_AGENT_TECHNIQUES = Object.freeze({
|
|
153
|
+
plan: Object.freeze({
|
|
154
|
+
description: "trailhead planning subagent: produces a build ticket's PLAN (steps, seams, files, verification)",
|
|
155
|
+
developerInstructions: "You are trailhead's planning subagent on Codex. Produce the PLAN for a trailhead build ticket: the steps, the seams, the files touched, and the verification criteria, applying TDD per the ticket's config. Follow the Plan step and the referenced technique in the trailhead skill at ~/.codex/skills/trailhead. Return the plan; do not implement.",
|
|
156
|
+
}),
|
|
157
|
+
execute: Object.freeze({
|
|
158
|
+
description: 'trailhead execute subagent: implements a PLAN with atomic commits',
|
|
159
|
+
developerInstructions: "You are trailhead's execute subagent on Codex. Implement the approved PLAN for a trailhead build ticket with atomic conventional commits, each carrying a Refs: #<n> trailer for the ticket, following the repo's conventions. Follow the Execute step in the trailhead skill at ~/.codex/skills/trailhead.",
|
|
160
|
+
}),
|
|
161
|
+
research: Object.freeze({
|
|
162
|
+
description: 'trailhead research subagent: gathers a decision-ready fact from primary sources',
|
|
163
|
+
developerInstructions: "You are trailhead's research subagent on Codex. Gather the decision-ready fact a ticket waits on from primary sources, per the Research technique in the trailhead skill at ~/.codex/skills/trailhead. Record findings with citations; change no product code beyond the research artifact.",
|
|
164
|
+
}),
|
|
165
|
+
review: Object.freeze({
|
|
166
|
+
description: "trailhead code-review subagent: adversarially reviews a ticket's diff",
|
|
167
|
+
developerInstructions: "You are trailhead's code-review subagent on Codex. Review the ticket's diff adversarially on the trailhead Code review technique's axes, verifying each finding before reporting, per the trailhead skill at ~/.codex/skills/trailhead. Do not defer to a convention or a settled-decision memory.",
|
|
168
|
+
}),
|
|
169
|
+
debug: Object.freeze({
|
|
170
|
+
description: "trailhead debug subagent: finds a defect's root cause by the scientific method",
|
|
171
|
+
developerInstructions: "You are trailhead's debug subagent on Codex. Find the root cause of the ticket's defect by the scientific method (reproduce, localise, falsifiable hypotheses, confirm, verify), per the Debug technique in the trailhead skill at ~/.codex/skills/trailhead.",
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// --- codexAgentToml ---------------------------------------------------------
|
|
176
|
+
// Render one Codex custom-agent TOML. model_reasoning_effort is emitted only
|
|
177
|
+
// when effort is a non-empty string. developer_instructions is a TOML
|
|
178
|
+
// multi-line literal block ('''...'''); a stray ''' inside the text (none of
|
|
179
|
+
// ours has one, but be safe) is neutralised so it can't close the block early.
|
|
180
|
+
function codexAgentToml({ name, description, developerInstructions, model, effort }) {
|
|
181
|
+
const safeInstructions = String(developerInstructions).split("'''").join("''");
|
|
182
|
+
const lines = [
|
|
183
|
+
`name = "${name}"`,
|
|
184
|
+
`description = "${description}"`,
|
|
185
|
+
`model = "${model}"`,
|
|
186
|
+
];
|
|
187
|
+
if (typeof effort === 'string' && effort.trim() !== '') {
|
|
188
|
+
lines.push(`model_reasoning_effort = "${effort}"`);
|
|
189
|
+
}
|
|
190
|
+
lines.push(`developer_instructions = '''\n${safeInstructions}\n'''`);
|
|
191
|
+
return lines.join('\n') + '\n';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// --- codexAgentTomlPlan ------------------------------------------------------
|
|
195
|
+
// Given the resolved models.codex config (a plain object like
|
|
196
|
+
// { plan: "gpt-5.6-terra", execute: { model: "gpt-5.6-sol", effort: "high" } },
|
|
197
|
+
// or null/undefined/{}), return the set of trailhead-<technique>.toml writes
|
|
198
|
+
// the installer should make. Skips unknown keys and empty values. Iterates
|
|
199
|
+
// CODEX_AGENT_TECHNIQUES in declaration order (not the config's key order) so
|
|
200
|
+
// the write order is deterministic regardless of how the config was authored.
|
|
201
|
+
function codexAgentTomlPlan(codexHome, codexModels) {
|
|
202
|
+
const models = codexModels && typeof codexModels === 'object' ? codexModels : {};
|
|
203
|
+
const dir = codexLayout(codexHome).codexAgentsDir;
|
|
204
|
+
const writes = [];
|
|
205
|
+
|
|
206
|
+
for (const technique of Object.keys(CODEX_AGENT_TECHNIQUES)) {
|
|
207
|
+
const raw = models[technique];
|
|
208
|
+
if (raw == null) continue;
|
|
209
|
+
|
|
210
|
+
let model = null;
|
|
211
|
+
let effort = null;
|
|
212
|
+
if (typeof raw === 'string') {
|
|
213
|
+
if (raw.trim() !== '') model = raw;
|
|
214
|
+
} else if (typeof raw === 'object') {
|
|
215
|
+
const m = typeof raw.model === 'string' ? raw.model : '';
|
|
216
|
+
if (m.trim() !== '') {
|
|
217
|
+
model = m;
|
|
218
|
+
effort = typeof raw.effort === 'string' ? raw.effort : null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (!model) continue;
|
|
222
|
+
|
|
223
|
+
const meta = CODEX_AGENT_TECHNIQUES[technique];
|
|
224
|
+
const name = `trailhead-${technique}`;
|
|
225
|
+
const filePath = path.join(dir, `${name}.toml`);
|
|
226
|
+
const content = codexAgentToml({
|
|
227
|
+
name,
|
|
228
|
+
description: meta.description,
|
|
229
|
+
developerInstructions: meta.developerInstructions,
|
|
230
|
+
model,
|
|
231
|
+
effort,
|
|
232
|
+
});
|
|
233
|
+
writes.push({ technique, name, path: filePath, content });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { writes };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// --- CODEX_SMART_ENTRY -----------------------------------------------------
|
|
240
|
+
// Discoverability metadata for the bare `/trailhead` smart-entry prompt (the
|
|
241
|
+
// one shim with no corresponding commands/*.md file).
|
|
242
|
+
// --- yamlQuote -------------------------------------------------------------
|
|
243
|
+
// Double-quote a scalar for a YAML frontmatter value, escaping the two chars
|
|
244
|
+
// that matter inside a double-quoted YAML string (backslash and double-quote).
|
|
245
|
+
function yamlQuote(s) {
|
|
246
|
+
return `"${String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// --- codexVerbSkillContent -------------------------------------------------
|
|
250
|
+
// One thin per-verb Codex SKILL.md: frontmatter (name + description, the two
|
|
251
|
+
// fields Codex registers a skill from, so they sit at byte 0) over a single
|
|
252
|
+
// delegating instruction to `$trailhead <verb>`. This is the GSD approach
|
|
253
|
+
// (each command projected as its own skill), because ~/.codex/prompts/ shims
|
|
254
|
+
// do not surface as slash commands on Codex (#46). No engine logic lives here;
|
|
255
|
+
// the canonical `$trailhead` skill stays the single source of truth.
|
|
256
|
+
function codexVerbSkillContent({ verb, description } = {}) {
|
|
257
|
+
const v = String(verb == null ? '' : verb).trim();
|
|
258
|
+
const desc = (description && String(description).trim())
|
|
259
|
+
|| `trailhead ${v}`;
|
|
260
|
+
const fm = `---\nname: trailhead-${v}\ndescription: ${yamlQuote(desc)}\n---\n\n`;
|
|
261
|
+
const body = `This is a thin discoverability entry for trailhead's **${v}** action on Codex. Run \`$trailhead ${v}\`, passing along whatever arguments you were given, and follow the \`$trailhead\` skill's instructions. The \`$trailhead\` skill is the single source of truth; do not re-implement its behaviour here.\n`;
|
|
262
|
+
return fm + body;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- codexVerbSkillAgentsYaml ----------------------------------------------
|
|
266
|
+
// The per-verb skill's openai.yaml: UI metadata plus explicit-only invocation,
|
|
267
|
+
// so the 20-odd verb skills are invocable as `$trailhead-<verb>` but never
|
|
268
|
+
// auto-loaded into an unrelated turn's context (which would bloat it).
|
|
269
|
+
function codexVerbSkillAgentsYaml({ verb, description } = {}) {
|
|
270
|
+
const v = String(verb == null ? '' : verb).trim();
|
|
271
|
+
const desc = (description && String(description).trim()) || `trailhead ${v}`;
|
|
272
|
+
const short = desc.length > 180 ? `${desc.slice(0, 177)}...` : desc;
|
|
273
|
+
return `interface:
|
|
274
|
+
display_name: ${yamlQuote(`Trailhead: ${v}`)}
|
|
275
|
+
short_description: ${yamlQuote(short)}
|
|
276
|
+
default_prompt: ${yamlQuote(`Run $trailhead-${v}`)}
|
|
277
|
+
policy:
|
|
278
|
+
allow_implicit_invocation: false
|
|
279
|
+
`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// --- codexVerbSkillPlan ----------------------------------------------------
|
|
283
|
+
// Given the resolved verb list (each { verb, description? }, or a bare string
|
|
284
|
+
// verb; normally read from plugins/trailhead/commands/*.md by the installer),
|
|
285
|
+
// return the per-verb skill writes: for each verb a skills/trailhead-<verb>/
|
|
286
|
+
// dir with SKILL.md + agents/openai.yaml. No bare-smart-entry skill is emitted:
|
|
287
|
+
// the canonical `$trailhead` skill already IS smart entry. Skips empty/invalid
|
|
288
|
+
// verbs. Mirrors codexAgentTomlPlan's shape. `dirs` lists every skill dir it
|
|
289
|
+
// writes into, for the installer's ensure()/sweep.
|
|
290
|
+
function codexVerbSkillPlan(codexHome, verbs) {
|
|
291
|
+
const skillsRoot = codexLayout(codexHome).skillsRoot;
|
|
292
|
+
const list = Array.isArray(verbs) ? verbs : [];
|
|
293
|
+
const writes = [];
|
|
294
|
+
const dirs = [];
|
|
295
|
+
for (const entry of list) {
|
|
296
|
+
const v = entry && typeof entry === 'object' ? entry.verb : entry;
|
|
297
|
+
if (v == null || String(v).trim() === '') continue;
|
|
298
|
+
const verb = String(v).trim();
|
|
299
|
+
const description = entry && typeof entry === 'object' ? entry.description : undefined;
|
|
300
|
+
const skillDir = path.join(skillsRoot, `trailhead-${verb}`);
|
|
301
|
+
dirs.push(skillDir);
|
|
302
|
+
writes.push({
|
|
303
|
+
verb,
|
|
304
|
+
path: path.join(skillDir, 'SKILL.md'),
|
|
305
|
+
content: codexVerbSkillContent({ verb, description }),
|
|
306
|
+
});
|
|
307
|
+
writes.push({
|
|
308
|
+
verb,
|
|
309
|
+
path: path.join(skillDir, 'agents', 'openai.yaml'),
|
|
310
|
+
content: codexVerbSkillAgentsYaml({ verb, description }),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return { writes, dirs };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// --- codexHookEntries -----------------------------------------------------
|
|
317
|
+
// The four guardrail hooks trailhead registers on Codex, as {event, matcher,
|
|
318
|
+
// command} records. Commands run the copied guard scripts under hooksScriptsDir
|
|
319
|
+
// via node, matching Claude's `node "<path>"` form. PreToolUse guards (commit +
|
|
320
|
+
// secret) can veto; the injection-scanner is a PostToolUse advisory; check-update
|
|
321
|
+
// runs at SessionStart. Reused by the installer, which merges them into
|
|
322
|
+
// ~/.codex/hooks.json (same shape as Claude's settings.json hooks block).
|
|
323
|
+
function codexHookEntries(hooksScriptsDir) {
|
|
324
|
+
const cmd = (name) => `node "${path.join(hooksScriptsDir, name)}"`;
|
|
325
|
+
return [
|
|
326
|
+
{ event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-commit-guard.js') },
|
|
327
|
+
{ event: 'PreToolUse', matcher: 'Bash', command: cmd('trailhead-secret-guard.js') },
|
|
328
|
+
{ event: 'PostToolUse', matcher: 'Bash', command: cmd('trailhead-issue-injection-scanner.js') },
|
|
329
|
+
{ event: 'SessionStart', matcher: '', command: cmd('trailhead-check-update.js') },
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// --- enableCodexFeatureFlag -------------------------------------------------
|
|
334
|
+
// Codex gates a lifecycle feature behind `features.<flag> = true` in
|
|
335
|
+
// config.toml. Given the current config.toml text (or '' when absent) and the
|
|
336
|
+
// flag name, return the updated text that enables it, or `null` when it is
|
|
337
|
+
// already enabled, or `{ unsafe: true }` when the file can't be edited safely
|
|
338
|
+
// (the caller then prints a manual instruction rather than risk corrupting the
|
|
339
|
+
// user's config). Conservative and idempotent: it only appends a fresh table,
|
|
340
|
+
// inserts the single key into an existing [features] table, or flips an
|
|
341
|
+
// explicit `<flag> = false`; anything ambiguous is left alone.
|
|
342
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
343
|
+
|
|
344
|
+
function enableCodexFeatureFlag(tomlText, flag) {
|
|
345
|
+
const text = typeof tomlText === 'string' ? tomlText : '';
|
|
346
|
+
const flagRe = esc(flag);
|
|
347
|
+
// Dotted top-level form: features.<flag> = <x>
|
|
348
|
+
if (new RegExp(`^\\s*features\\.${flagRe}\\s*=\\s*true\\s*(#.*)?$`, 'm').test(text)) return null;
|
|
349
|
+
if (new RegExp(`^\\s*features\\.${flagRe}\\s*=`, 'm').test(text)) return { unsafe: true }; // set to something we won't touch
|
|
350
|
+
const featHeader = /^[ \t]*\[features\][ \t]*(#.*)?$/m;
|
|
351
|
+
const m = featHeader.exec(text);
|
|
352
|
+
if (m) {
|
|
353
|
+
const bodyStart = m.index + m[0].length;
|
|
354
|
+
const rest = text.slice(bodyStart);
|
|
355
|
+
const nextRel = rest.search(/^[ \t]*\[[^\]]+\][ \t]*(#.*)?$/m);
|
|
356
|
+
const bodyEnd = nextRel === -1 ? text.length : bodyStart + nextRel;
|
|
357
|
+
const body = text.slice(bodyStart, bodyEnd);
|
|
358
|
+
if (new RegExp(`^\\s*${flagRe}\\s*=\\s*true\\s*(#.*)?$`, 'm').test(body)) return null;
|
|
359
|
+
if (new RegExp(`^\\s*${flagRe}\\s*=\\s*false\\s*(#.*)?$`, 'm').test(body)) {
|
|
360
|
+
const newBody = body.replace(new RegExp(`^([ \\t]*${flagRe}[ \\t]*=[ \\t]*)false([ \\t]*(#.*)?)$`, 'm'), '$1true$2');
|
|
361
|
+
return text.slice(0, bodyStart) + newBody + text.slice(bodyEnd);
|
|
362
|
+
}
|
|
363
|
+
if (new RegExp(`^\\s*${flagRe}\\s*=`, 'm').test(body)) return { unsafe: true };
|
|
364
|
+
return text.slice(0, bodyStart) + `\n${flag} = true` + text.slice(bodyStart);
|
|
365
|
+
}
|
|
366
|
+
// No [features] table at all: append one.
|
|
367
|
+
const prefix = text.length === 0 ? '' : (text.endsWith('\n') ? '' : '\n');
|
|
368
|
+
const gap = text.length === 0 ? '' : '\n';
|
|
369
|
+
return text + prefix + gap + `[features]\n${flag} = true\n`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// --- enableCodexHooksFeature ----------------------------------------------
|
|
373
|
+
// Codex gates the hook bus behind `features.hooks = true` in config.toml.
|
|
374
|
+
function enableCodexHooksFeature(tomlText) {
|
|
375
|
+
return enableCodexFeatureFlag(tomlText, 'hooks');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// --- enableCodexMultiAgentV2Feature -----------------------------------------
|
|
379
|
+
// Codex gates per-subagent model pinning (the agents/*.toml registry) behind
|
|
380
|
+
// `features.multi_agent_v2 = true` in config.toml.
|
|
381
|
+
function enableCodexMultiAgentV2Feature(tomlText) {
|
|
382
|
+
return enableCodexFeatureFlag(tomlText, 'multi_agent_v2');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
module.exports = {
|
|
386
|
+
codexLayout,
|
|
387
|
+
convertToCodex,
|
|
388
|
+
codexSkillAdapterHeader,
|
|
389
|
+
injectCodexAdapterHeader,
|
|
390
|
+
codexAgentsYaml,
|
|
391
|
+
codexAgentToml,
|
|
392
|
+
codexAgentTomlPlan,
|
|
393
|
+
codexVerbSkillContent,
|
|
394
|
+
codexVerbSkillAgentsYaml,
|
|
395
|
+
codexVerbSkillPlan,
|
|
396
|
+
codexHookEntries,
|
|
397
|
+
enableCodexFeatureFlag,
|
|
398
|
+
enableCodexHooksFeature,
|
|
399
|
+
enableCodexMultiAgentV2Feature,
|
|
400
|
+
};
|