feat(#774): emit service_tier/model_verbosity in Codex agent TOML + agents/openai.yaml skill chip (#828)
* feat(#774): emit service_tier/model_verbosity in Codex agent TOML + agents/openai.yaml skill chip - Add service_tier = "flex" and model_verbosity = "low" to the Codex ConfigProfile TOML for light-tier agents (gsd-research-synthesizer, gsd-codebase-mapper, gsd-plan-checker, and 8 others identified via AGENT_DEFAULT_TIERS). Field names/values verified against Codex schema (profile_toml.rs / config_types.rs Verbosity enum). Non-light agents are unaffected. - Add generateCodexSkillMetadataYaml() and writeCodexSkillMetadataFiles(): after installRuntimeArtifacts, iterate every gsd-* skill directory, read the short-description already emitted in the SKILL.md frontmatter by convertClaudeCommandToCodexSkill, and write agents/openai.yaml with interface.display_name and interface.short_description for the Codex TUI skill picker chip. - yamlQuote (JSON.stringify) handles all YAML-unsafe chars. - User-owned gsd-dev-preferences dir is never overwritten. - Errors per-skill are swallowed so a bad SKILL.md can't abort install. - agents/openai.yaml is covered by the snapshot/rollback system and manifest hash (writeManifest hashes skill dirs recursively). - Uninstall symmetry: _removeGsdEntries removes whole gsd-* dirs. - 21 new tests in codex-config.test.cjs covering service_tier/verbosity TOML emission, YAML generation (round-trip via js-yaml), and writeCodexSkillMetadataFiles including an e2e integration test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#774): correct docs-lint coverage — proper changeset format + USER-GUIDE entry Rewrite the changeset fragment from old @opengsd/gsd-core:patch format to the required type:/pr: schema so the docs-lint parser can consume it. Add a new "Codex skill picker and agent scheduling (#774)" section to docs/USER-GUIDE.md describing the flex-tier scheduling and /skills TUI chip enrichments — both are user-visible and belong in docs rather than behind a docs-exempt marker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
135
bin/install.js
135
bin/install.js
@@ -2952,6 +2952,20 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
|
||||
const _renderedEffortCodex = _getGsdEffortCatalog().renderEffortForRuntime('codex', _universalEffortCodex).value;
|
||||
lines.push(`model_reasoning_effort = ${JSON.stringify(_renderedEffortCodex)}`);
|
||||
|
||||
// #774 — Emit service_tier and model_verbosity for light-tier agents.
|
||||
// Light-tier agents (routingTier: "light" in model-catalog.json) are haiku-equivalent
|
||||
// and benefit from Codex's "flex" service tier (lower cost, background processing)
|
||||
// and "low" verbosity (reduced token output). Both fields are validated against the
|
||||
// Codex ConfigProfile schema (codex-rs/config/src/profile_toml.rs):
|
||||
// service_tier: Option<String> — "flex" | "fast" (legacy)
|
||||
// model_verbosity: Option<Verbosity> — "low" | "medium" | "high"
|
||||
const { AGENT_DEFAULT_TIERS: _agentTiers } = _getGsdEffortCatalog();
|
||||
const _agentRoutingTier = _agentTiers?.[resolvedName] || _agentTiers?.[agentName];
|
||||
if (_agentRoutingTier === 'light') {
|
||||
lines.push(`service_tier = "flex"`);
|
||||
lines.push(`model_verbosity = "low"`);
|
||||
}
|
||||
|
||||
// Agent prompts contain raw backslashes in regexes and shell snippets.
|
||||
// TOML literal multiline strings preserve them without escape parsing.
|
||||
lines.push(`developer_instructions = '''`);
|
||||
@@ -2961,6 +2975,115 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the agents/openai.yaml TUI chip metadata content for a Codex skill.
|
||||
*
|
||||
* This file is written alongside SKILL.md as <skill-dir>/agents/openai.yaml.
|
||||
* Codex loads it as a SkillMetadataFile (codex-rs/core-skills/src/loader.rs),
|
||||
* making the skill discoverable in the /skills TUI popup with a display name
|
||||
* and short description. If the file is absent, Codex silently skips it (fails open).
|
||||
*
|
||||
* Schema (interface section):
|
||||
* display_name: short human-readable skill name (strip gsd- prefix)
|
||||
* short_description: 1-2 sentence description for TUI chip, ≤180 chars
|
||||
*
|
||||
* @param {string} skillName - Full skill name e.g. "gsd-plan-phase"
|
||||
* @param {string} shortDescription - Description text (already truncated by caller)
|
||||
* @returns {string} YAML content for agents/openai.yaml
|
||||
*/
|
||||
function generateCodexSkillMetadataYaml(skillName, shortDescription) {
|
||||
// Display name: strip "gsd-" prefix and convert hyphens to spaces for readability.
|
||||
const displayName = skillName.replace(/^gsd-/, '').replace(/-/g, ' ');
|
||||
// yamlQuote (= JSON.stringify) handles all YAML-unsafe chars: backslashes,
|
||||
// quotes, newlines, control characters, and Unicode escapes.
|
||||
return [
|
||||
'interface:',
|
||||
` display_name: ${yamlQuote(displayName)}`,
|
||||
` short_description: ${yamlQuote(shortDescription)}`,
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Write agents/openai.yaml TUI chip metadata for each gsd-* skill directory.
|
||||
*
|
||||
* Called after layout-driven skill install for Codex. Iterates every gsd-*
|
||||
* skill directory in skillsDir, reads the SKILL.md frontmatter to extract the
|
||||
* short-description already emitted by convertClaudeCommandToCodexSkill, then
|
||||
* writes <skill-dir>/agents/openai.yaml using generateCodexSkillMetadataYaml.
|
||||
*
|
||||
* Fails open: individual skill directories that cannot be processed are silently
|
||||
* skipped so a single malformed SKILL.md cannot block the whole install.
|
||||
*
|
||||
* User-owned skill directories (e.g. gsd-dev-preferences) are explicitly
|
||||
* skipped so existing user-authored agents/openai.yaml files are never
|
||||
* overwritten. These dirs are listed in the same USER_OWNED_SKILL_DIRS
|
||||
* constant used by installOpencodeFamilySkills.
|
||||
*
|
||||
* The YAML-quoted description value is unescaped before embedding so that
|
||||
* YAML escape sequences (e.g. \" in a double-quoted scalar) become the
|
||||
* literal characters they represent rather than being double-escaped in the
|
||||
* output.
|
||||
*
|
||||
* @param {string} skillsDir - Path to the skills/ directory (e.g. ~/.codex/skills)
|
||||
*/
|
||||
function writeCodexSkillMetadataFiles(skillsDir) {
|
||||
if (!fs.existsSync(skillsDir)) return;
|
||||
// Mirror the user-owned list from installOpencodeFamilySkills (#2973).
|
||||
// We MUST skip these dirs — their contents are user-generated and must
|
||||
// never be overwritten by GSD's install path.
|
||||
const _userOwnedSkillDirs = new Set(['gsd-dev-preferences']);
|
||||
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory() || !entry.name.startsWith('gsd-')) continue;
|
||||
if (_userOwnedSkillDirs.has(entry.name)) continue; // preserve user content
|
||||
const skillDir = path.join(skillsDir, entry.name);
|
||||
const skillMdPath = path.join(skillDir, 'SKILL.md');
|
||||
try {
|
||||
const content = fs.readFileSync(skillMdPath, 'utf8');
|
||||
const { frontmatter } = extractFrontmatterAndBody(content);
|
||||
// Prefer the short-description field emitted by convertClaudeCommandToCodexSkill;
|
||||
// fall back to description, then a synthetic label from the skill name.
|
||||
let shortDesc = '';
|
||||
if (frontmatter) {
|
||||
// SKILL.md uses YAML frontmatter with a nested metadata.short-description key.
|
||||
// extractFrontmatterField handles only top-level keys; parse the metadata block
|
||||
// by looking for " short-description:" directly.
|
||||
const metaMatch = frontmatter.match(/^[ \t]*metadata\s*:\s*\n((?:[ \t]+.*\n?)*)/m);
|
||||
if (metaMatch) {
|
||||
const metaBlock = metaMatch[1];
|
||||
const sdMatch = metaBlock.match(/^[ \t]+short-description\s*:\s*(.+)$/m);
|
||||
if (sdMatch) {
|
||||
// Unescape YAML double-quoted scalar escapes before embedding.
|
||||
// convertClaudeCommandToCodexSkill always emits a double-quoted
|
||||
// value (via yamlQuote) so only double-quote unescaping is needed.
|
||||
let raw = sdMatch[1].trim();
|
||||
if (raw.startsWith('"') && raw.endsWith('"')) {
|
||||
// Strip outer double-quotes and decode \" → " and \\ → \
|
||||
raw = raw.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
||||
} else {
|
||||
// Single-quoted or unquoted: strip surrounding quotes/whitespace
|
||||
raw = raw.replace(/^["']|["']$/g, '');
|
||||
}
|
||||
shortDesc = raw;
|
||||
}
|
||||
}
|
||||
if (!shortDesc) {
|
||||
shortDesc = extractFrontmatterField(frontmatter, 'description') || '';
|
||||
}
|
||||
}
|
||||
if (!shortDesc) {
|
||||
shortDesc = `Run GSD workflow ${entry.name}.`;
|
||||
}
|
||||
const yamlContent = generateCodexSkillMetadataYaml(entry.name, shortDesc);
|
||||
const agentsSubdir = path.join(skillDir, 'agents');
|
||||
fs.mkdirSync(agentsSubdir, { recursive: true });
|
||||
fs.writeFileSync(path.join(agentsSubdir, 'openai.yaml'), yamlContent);
|
||||
} catch (_err) {
|
||||
// Fail open — missing or unreadable SKILL.md must not block the install.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the GSD config block for Codex config.toml.
|
||||
* @param {Array<{name: string, description: string}>} agents
|
||||
@@ -9227,6 +9350,16 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
const scope = isGlobal ? 'global' : 'local';
|
||||
installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile);
|
||||
|
||||
// #774 — Codex only: write agents/openai.yaml TUI chip metadata alongside each
|
||||
// installed skill so the /skills popup shows name + description for each gsd-* skill.
|
||||
// The SkillMetadataFile is loaded by codex-rs/core-skills/src/loader.rs from
|
||||
// <skill-dir>/agents/openai.yaml; absence is silently tolerated (fails open).
|
||||
// We parse the SKILL.md frontmatter to extract short-description already emitted
|
||||
// by convertClaudeCommandToCodexSkill and use it as the TUI chip description.
|
||||
if (isCodex) {
|
||||
writeCodexSkillMetadataFiles(path.join(targetDir, 'skills'));
|
||||
}
|
||||
|
||||
// Hermes only: write DESCRIPTION.md for the gsd/ category after layout install
|
||||
if (isHermes) {
|
||||
writeHermesCategoryDescription(path.join(targetDir, 'skills', 'gsd'));
|
||||
@@ -11649,6 +11782,8 @@ module.exports = {
|
||||
convertClaudeToGeminiAgent,
|
||||
convertClaudeAgentToCodexAgent,
|
||||
generateCodexAgentToml,
|
||||
generateCodexSkillMetadataYaml,
|
||||
writeCodexSkillMetadataFiles,
|
||||
generateCodexConfigBlock,
|
||||
stripGsdFromCodexConfig,
|
||||
migrateCodexHooksMapFormat,
|
||||
|
||||
Reference in New Issue
Block a user