* test(#443): RED unified effort + fast_mode + resolve-execution All 68 tests failing as expected — no implementation yet. Covers: effort cascade (tier defaults, overrides, invalid fallthrough), fast_mode cascade (boolean-only, tier defaults), resolveEffortForTier escalation, renderEffortForRuntime clamping, resolve-execution CLI, config schema new keys, QA hostile-input matrix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(#443): unified cross-provider effort + fast_mode knobs and resolve-execution query Adds config-driven effort control (universal ladder: minimal<low<medium<high<xhigh<max) and fast_mode propagation knobs, with per-runtime rendering that clamps the unique tail values (max=Anthropic-only clamps to xhigh on Codex; minimal=Codex-only clamps to low on Claude). Key changes: - config-schema.manifest.json: add effort.default, fast_mode.enabled as validKeys; add 4 dynamicKeyPatterns for effort.routing_tier_defaults, effort.agent_overrides, fast_mode.routing_tier_defaults, fast_mode.agent_overrides; fix stale _comment - config-defaults.manifest.json: add effort and fast_mode blocks with tier defaults - model-catalog.cjs: add EFFORT_RENDERING map, renderEffortForRuntime(), RUNTIMES_WITH_FAST_MODE - model-profiles.cjs: re-export new catalog exports - core.cjs: add resolveEffortInternal, resolveFastModeInternal, resolveEffortForTier, VALID_EFFORTS, EFFORT_SET, nextEffort; pass effort/fast_mode through loadConfig - commands.cjs: replace reasoning_effort in cmdResolveModel with unified effort; add cmdResolveExecution (superset command with effort_rendered, effort_param, effort_propagation, fast_mode, fast_mode_supported) - gsd-tools.cjs: add resolve-execution case with --effort/--fast-mode/--attempt flags - tests/feat-443: 69 tests covering cascade, rendering, escalation, CLI, schema, QA matrix - tests/commands.test.cjs: convert 3 reasoning_effort assertions to unified effort - docs/CONFIGURATION.md: document effort + fast_mode + resolve-execution sections - settings-advanced.md: list new effort/fast_mode keys in confirmation table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(#443): remove dead catalog effort lane; unify codex effort through renderEffortForRuntime - Remove resolveReasoningEffortInternal (catalog-driven effort function) from core.cjs and its export; remove from commands.cjs destructure import - Convert tests/issue-2517-runtime-aware-profiles.test.cjs: all 11 effort assertions now use resolveEffortInternal + renderEffortForRuntime; Claude effort is first-class (output_config.effort); unknown runtimes assert param===null - Convert tests/feat-3023-model-phase-types.test.cjs: replace the entire resolveReasoningEffortInternal describe with unified effort assertions; effort derives from AGENT_DEFAULT_TIERS routing tier, not phase-type tier Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(#443): ADR for unified cross-provider effort + fast-mode routing Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * test(#443): architecture-level QA invariants + test-strategy doc Add 48-test integration suite (feat-443-effort-fast-mode.integration.test.cjs) covering 8 architectural invariants: cross-provider validity (never emit a value the real API would 400 on), param/channel contract stability, resolve-execution JSON contract (all 8 keys + correct types), totality across the full 33-agent registry, fast-mode honesty (claude always fast_mode_supported=false), precedence first-valid-wins matrix for both effort and fast_mode cascades, dynamic-routing composition (effort escalation independent of model tier), and config-set round-trip for all new effort/* and fast_mode/* key namespaces. Append test-strategy section with invariant rationale and E2E gap documentation to docs/TESTING-SUITES.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(#443): add failing install-wiring tests for effort per-runtime injection (RED) TDD RED: 10 failing tests covering: - Claude .md gets effort: injected per tier (planner=xhigh, mapper=low, executor=high) - Gemini .md does NOT get effort: (already passing — Gemini-safe) - Codex .toml gets model_reasoning_effort via unified resolver - Config-driven: effort.agent_overrides drives both Claude .md and Codex .toml - Source purity: agents/*.md have no effort: key (already passing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(#443): wire effort per-runtime at install (Claude .md frontmatter + Codex .toml unified) - Import AGENT_DEFAULT_TIERS and renderEffortForRuntime from model-catalog.cjs - Add readGsdEffectiveEffortConfig(targetDir): reads merged effort config from .planning/config.json (per-project wins) + ~/.gsd/defaults.json (global fallback), same probe pattern as readGsdRuntimeProfileResolver - Add resolveInstallTimeEffort(effortCfg, agentName): pure function matching resolveEffortInternal() precedence (agent_overrides > routing_tier_defaults > default > 'high') without loadConfig side-effects (no sub-repo detection, no migration writes) - Claude agent copy loop: inject `effort: <value>` into frontmatter ONLY for runtime === 'claude'; all other .md runtimes (Gemini, Qwen, Hermes, etc.) stay effort-free (Gemini-safe source contract preserved in agents/*.md) - generateCodexAgentToml: add effortCfg param; emit model_reasoning_effort from unified resolver (replaces old catalog entry.reasoning_effort); Codex clamps max → xhigh via renderEffortForRuntime('codex', ...) - installCodexConfig: pass readGsdEffectiveEffortConfig(targetDir) to generateCodexAgentToml so per-project config wins for Codex .toml too - Update failing tests to GREEN: 12/12 pass; all 17 install tests pass; 2847/2848 unit tests pass (1 pre-existing failure: policy-shell-pinning) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(#443): source install effort defaults from manifest (kill drift) + guard test Replace hardcoded _GSD_EFFORT_MANIFEST_TIER_DEFAULTS and the 'high' fallback in resolveInstallTimeEffort with values read from config-defaults.manifest.json at module init, using the same __dirname-relative path install.js already uses for all shared manifests. Add feat-443-effort-defaults-drift.test.cjs to assert equality between install.js's runtime constants and the manifest on every CI run. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#443): reconcile Codex TOML tests with unified effort design The #443 unified effort resolver makes generateCodexAgentToml always emit model_reasoning_effort (driven by resolveInstallTimeEffort, not model_profile_overrides). The test 'generated TOML omits reasoning_effort when runtime has none' had an obsolete premise — model_profile_overrides.reasoning_effort:'' no longer suppresses unified effort. Convert it to assert the new invariant: Codex TOML always carries a valid model_reasoning_effort from the agent's routing tier (xhigh for gsd-planner, a heavy-tier agent), while model_profile_overrides model override is still respected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#443): make install.js effort resolution lazy (no load-time side effects breaking launcher-parity) Replace module-load-time IIFE + hard throw (config-defaults.manifest.json read) and top-level require of model-catalog.cjs with a lazy _getGsdEffortCatalog() getter that initialises on first call from resolveInstallTimeEffort / generateCodexAgentToml / Claude .md effort injection. Requiring install.js in unrelated test contexts (e.g. runtime-launcher-parity) no longer triggers manifest IO or throws, eliminating the load-time side effect that changed subprocess exit codes / stderr on the bench. Drift-guard exports (_GSD_EFFORT_MANIFEST_TIER_DEFAULTS / _GSD_EFFORT_MANIFEST_DEFAULT) preserved as lazy getter properties on module.exports so feat-443-effort-defaults-drift still validates them without forcing eager load. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#443): isolate install-wiring test HOME to stop \$HOME/.claude pollution breaking launcher-parity runGlobalInstall() now redirects HOME to a per-call isolated tmpdir in addition to the existing runtime-specific env-var redirects (CLAUDE_CONFIG_DIR, GEMINI_CONFIG_DIR, CODEX_HOME). This ensures install.js code that uses os.homedir() directly — including the ~/.cache/gsd update-check deletion, ~/.gsd/defaults.json reads, and any HOME-relative npm subprocess writes — never touches the real \$HOME during the test. Without the HOME isolation the install test (which is new to this branch and is now picked up by Docker's raw \`tests/*.test.cjs\` glob) could write or delete files under the real \$HOME, causing runtime-launcher-parity test (D) to fail: (D) asserts a loud non-zero exit when \$RUNTIME_DIR/gsd-tools.cjs is absent and gsd-tools is not on PATH, but the launcher's \$HOME/.claude fallback arm succeeds if \$HOME/.claude/get-shit-done/bin/gsd-tools.cjs exists. Also sets GSD_SKIP_STALE_SDK_CHECK=1 to suppress the \`npm ls -g\` subprocess that the global installer spawns — irrelevant to effort-wiring assertions, slow, and potentially writes to ~/.npm cache. All 12 feat-443 install-wiring assertions preserved. Drift-guard 5/5. Unit suite 2848/2850 (pre-existing policy-shell-pinning.test.cjs failure on next). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(#443): add changeset fragment for effort + fast-mode routing Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(#443): set GSD_TEST_MODE before requiring install.js in drift-guard test to prevent HOME leak Without GSD_TEST_MODE=1, require('bin/install.js') runs the module's main install block (guarded by !GSD_TEST_MODE), performing a real global Claude install into $HOME/.claude/. On CI ubuntu where node is on standard PATH, the launcher's $HOME/.claude fallback arm then finds gsd-tools.cjs, causing runtime-launcher-parity test (D) to exit zero when it must exit non-zero. Root cause: feat-443-effort-defaults-drift.test.cjs (unit suite) runs alphabetically before runtime-launcher-parity.test.cjs in the same node --test invocation. Each runs in a separate worker process but shares the same HOME. The drift test's install leaks gsd-tools.cjs into that HOME, then the launcher test's bash subprocess finds it via the $HOME/.claude arm. Fix: add process.env.GSD_TEST_MODE = '1' at the top of the drift-guard test, before the require(installPath) call. This matches the pattern used by feat-443-effort-fast-mode.test.cjs and feat-443-effort-install-wiring .install.test.cjs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#443): deterministic resolve-execution arg parsing + validate install-time effort (Codex adversarial findings) Finding 1: resolve-execution --effort low gsd-planner misrouted 'low' as the agent. Replace find(non-dash) with a proper flag-consuming loop that collects a single positional; validate missing/extra positionals and malformed --attempt values. Finding 2: resolveInstallTimeEffort returned unvalidated effort strings (e.g. "ultra") verbatim. Each precedence layer now checks GSD_EFFORT_SET (imported once from core.cjs) before accepting a value, mirroring resolveEffortInternal exactly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#443): newline-agnostic effort frontmatter injection (Windows CRLF) + CRLF-safe assertions Extracts injectEffortFrontmatter(content, effortValue) pure helper that detects EOL (LF vs CRLF) from the opening '---' line and inserts 'effort: <value>' before the closing '---' delimiter using the same EOL as the surrounding frontmatter. Regex now uses /^---\r?\n([\s\S]*?)^---\r?$/m instead of the LF-only /^(---\n[\s\S]*?)(---)(\n|$)/ that silently skipped CRLF files on Windows (git core.autocrlf=true checkout). Also adds 7 unit tests covering LF, CRLF, idempotency, no-frontmatter, and complex frontmatter cases. Exports injectEffortFrontmatter from module.exports. Fixes 6 CI failures in tests/feat-443-effort-install-wiring.install.test.cjs on windows-latest runners (lines 138, 145, 152, 261, 345, 356). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: CI Rebase Check <ci@gsd-redux> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
278
bin/install.js
278
bin/install.js
@@ -145,8 +145,61 @@ const { MODEL_PROFILES: GSD_MODEL_PROFILES } = require(path.join(_gsdLibDir, 'mo
|
||||
const {
|
||||
RUNTIME_PROFILE_MAP: GSD_RUNTIME_PROFILE_MAP,
|
||||
resolveTierEntry: gsdResolveTierEntry,
|
||||
EFFORT_SET: GSD_EFFORT_SET,
|
||||
} = require(path.join(_gsdLibDir, 'core.cjs'));
|
||||
|
||||
// #443 — model-catalog and config-defaults.manifest.json exports needed only
|
||||
// by effort-resolution code paths (resolveInstallTimeEffort /
|
||||
// generateCodexAgentToml / Claude .md effort injection). Loaded lazily the
|
||||
// first time they are needed so that requiring install.js in test contexts that
|
||||
// never trigger an install does NOT produce module-load-time side effects (the
|
||||
// manifest read + hard throw) that could alter subprocess exit codes or stderr.
|
||||
let _gsdEffortCatalogCache = null;
|
||||
function _getGsdEffortCatalog() {
|
||||
if (_gsdEffortCatalogCache) return _gsdEffortCatalogCache;
|
||||
|
||||
const { AGENT_DEFAULT_TIERS, renderEffortForRuntime } = require(path.join(_gsdLibDir, 'model-catalog.cjs'));
|
||||
|
||||
const manifestPath = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'get-shit-done',
|
||||
'bin',
|
||||
'shared',
|
||||
'config-defaults.manifest.json'
|
||||
);
|
||||
let manifestData;
|
||||
try {
|
||||
manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
||||
} catch (_err) {
|
||||
// Fail loudly — a missing manifest is a broken install, not a soft degradation.
|
||||
throw new Error(
|
||||
`gsd install: cannot load config-defaults.manifest.json at ${manifestPath}: ${_err.message}`
|
||||
);
|
||||
}
|
||||
|
||||
const tierDefaults =
|
||||
(manifestData.effort &&
|
||||
manifestData.effort.routing_tier_defaults &&
|
||||
typeof manifestData.effort.routing_tier_defaults === 'object' &&
|
||||
!Array.isArray(manifestData.effort.routing_tier_defaults))
|
||||
? manifestData.effort.routing_tier_defaults
|
||||
: { light: 'low', standard: 'high', heavy: 'xhigh' }; // guard: unreachable if manifest is valid
|
||||
|
||||
const effortDefault =
|
||||
(manifestData.effort && typeof manifestData.effort.default === 'string')
|
||||
? manifestData.effort.default
|
||||
: 'high'; // guard: unreachable if manifest is valid
|
||||
|
||||
_gsdEffortCatalogCache = {
|
||||
AGENT_DEFAULT_TIERS,
|
||||
renderEffortForRuntime,
|
||||
EFFORT_MANIFEST_TIER_DEFAULTS: tierDefaults,
|
||||
EFFORT_MANIFEST_DEFAULT: effortDefault,
|
||||
};
|
||||
return _gsdEffortCatalogCache;
|
||||
}
|
||||
|
||||
const {
|
||||
MINIMAL_SKILL_ALLOWLIST,
|
||||
isMinimalMode,
|
||||
@@ -1381,6 +1434,185 @@ function readGsdEffectiveModelOverrides(targetDir = null) {
|
||||
return { ...(global || {}), ...(projectOverrides || {}) };
|
||||
}
|
||||
|
||||
/**
|
||||
* #443 — Read the merged `effort` config block for install-time effort resolution.
|
||||
*
|
||||
* Probes the same config sources as readGsdRuntimeProfileResolver (per-project
|
||||
* `.planning/config.json` wins over `~/.gsd/defaults.json`) but extracts the
|
||||
* `effort` object instead of the model-profile fields.
|
||||
*
|
||||
* Returns the merged `effort` object or null when neither source defines one.
|
||||
* The caller can pass this to resolveInstallTimeEffort() which is pure and
|
||||
* requires no filesystem access beyond what this helper already performs.
|
||||
*
|
||||
* @param {string|null} targetDir Runtime install root (walks up to find .planning/).
|
||||
* @returns {object|null}
|
||||
*/
|
||||
function readGsdEffectiveEffortConfig(targetDir = null) {
|
||||
const homeDefaults = _readGsdConfigFile(
|
||||
path.join(os.homedir(), '.gsd', 'defaults.json'),
|
||||
'~/.gsd/defaults.json'
|
||||
);
|
||||
|
||||
let projectConfig = null;
|
||||
if (targetDir) {
|
||||
let probeDir = path.resolve(targetDir);
|
||||
for (let depth = 0; depth < 8; depth += 1) {
|
||||
const candidate = path.join(probeDir, '.planning', 'config.json');
|
||||
if (fs.existsSync(candidate)) {
|
||||
projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
|
||||
break;
|
||||
}
|
||||
const parent = path.dirname(probeDir);
|
||||
if (parent === probeDir) break;
|
||||
probeDir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
const homeEffort = (homeDefaults && homeDefaults.effort && typeof homeDefaults.effort === 'object' && !Array.isArray(homeDefaults.effort))
|
||||
? homeDefaults.effort
|
||||
: null;
|
||||
const projectEffort = (projectConfig && projectConfig.effort && typeof projectConfig.effort === 'object' && !Array.isArray(projectConfig.effort))
|
||||
? projectConfig.effort
|
||||
: null;
|
||||
|
||||
if (!homeEffort && !projectEffort) return null;
|
||||
|
||||
// Per-project wins on conflict within each sub-field. Merge field-by-field so
|
||||
// a project config that only sets agent_overrides still inherits global
|
||||
// routing_tier_defaults and default.
|
||||
return {
|
||||
...(homeEffort || {}),
|
||||
...(projectEffort || {}),
|
||||
// Deep-merge agent_overrides (project wins per-key)
|
||||
agent_overrides: {
|
||||
...((homeEffort && homeEffort.agent_overrides) || {}),
|
||||
...((projectEffort && projectEffort.agent_overrides) || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* #443 — Resolve install-time effort for a given agent, using the same
|
||||
* precedence chain as resolveEffortInternal() in core.cjs, but operating
|
||||
* on a pre-loaded effortCfg object (no loadConfig side-effects at install).
|
||||
*
|
||||
* Precedence (mirrors resolveEffortInternal):
|
||||
* 1. effortCfg.agent_overrides[agentName]
|
||||
* 2. effortCfg.routing_tier_defaults[agentTier] (if effortCfg present)
|
||||
* — OR manifest tier defaults when effortCfg is null
|
||||
* 3. effortCfg.default
|
||||
* 4. 'high' (hardcoded fallback)
|
||||
*
|
||||
* @param {object|null} effortCfg Result of readGsdEffectiveEffortConfig().
|
||||
* @param {string} agentName e.g. 'gsd-planner'
|
||||
* @returns {string} Universal effort string (low/medium/high/xhigh/max/minimal)
|
||||
*/
|
||||
function resolveInstallTimeEffort(effortCfg, agentName) {
|
||||
// Validates each candidate against the canonical EFFORT_SET (sourced once
|
||||
// from core.cjs) before accepting it, mirroring resolveEffortInternal exactly.
|
||||
// Invalid values fall through to the next precedence layer; final fallback 'high'.
|
||||
|
||||
// Step 1: agent_overrides
|
||||
if (effortCfg) {
|
||||
const ao = effortCfg.agent_overrides;
|
||||
if (ao && typeof ao === 'object' && !Array.isArray(ao)) {
|
||||
const v = ao[agentName];
|
||||
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: routing_tier_defaults keyed by the agent's catalog tier
|
||||
const { AGENT_DEFAULT_TIERS, EFFORT_MANIFEST_TIER_DEFAULTS, EFFORT_MANIFEST_DEFAULT } = _getGsdEffortCatalog();
|
||||
const agentTier = AGENT_DEFAULT_TIERS[agentName];
|
||||
if (agentTier) {
|
||||
if (effortCfg && effortCfg.routing_tier_defaults &&
|
||||
typeof effortCfg.routing_tier_defaults === 'object' &&
|
||||
!Array.isArray(effortCfg.routing_tier_defaults)) {
|
||||
const v = effortCfg.routing_tier_defaults[agentTier];
|
||||
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
||||
} else if (!effortCfg) {
|
||||
// No effort config — use manifest tier defaults
|
||||
const v = EFFORT_MANIFEST_TIER_DEFAULTS[agentTier];
|
||||
if (typeof v === 'string' && GSD_EFFORT_SET.has(v)) return v;
|
||||
}
|
||||
// effortCfg exists but has no routing_tier_defaults — fall through
|
||||
}
|
||||
|
||||
// Step 3: effort.default
|
||||
if (effortCfg) {
|
||||
const d = effortCfg.default;
|
||||
if (typeof d === 'string' && GSD_EFFORT_SET.has(d)) return d;
|
||||
}
|
||||
|
||||
// Step 4: manifest default (sourced from config-defaults.manifest.json effort.default)
|
||||
// If even the manifest default is invalid, fall back to 'high'.
|
||||
if (typeof EFFORT_MANIFEST_DEFAULT === 'string' && GSD_EFFORT_SET.has(EFFORT_MANIFEST_DEFAULT)) {
|
||||
return EFFORT_MANIFEST_DEFAULT;
|
||||
}
|
||||
return 'high';
|
||||
}
|
||||
|
||||
/**
|
||||
* #443 — Inject `effort: <value>` into YAML frontmatter of a Claude .md agent
|
||||
* file in a newline-agnostic way (LF and CRLF source files are both handled).
|
||||
*
|
||||
* The function:
|
||||
* - Detects the file's EOL (CRLF if the first `---` line ends with \r\n,
|
||||
* otherwise LF).
|
||||
* - Skips injection if an `effort:` key already exists in the frontmatter
|
||||
* (idempotent).
|
||||
* - Inserts `effort: <value>` immediately before the closing `---` delimiter,
|
||||
* using the same EOL as the surrounding frontmatter so the output file
|
||||
* stays EOL-consistent.
|
||||
* - Returns the original content unchanged when no YAML frontmatter is found.
|
||||
*
|
||||
* @param {string} content Raw file content (may have LF or CRLF endings).
|
||||
* @param {string} effortValue Rendered effort string, e.g. "xhigh".
|
||||
* @returns {string} Updated content with `effort:` injected, or the
|
||||
* original content when no frontmatter is found.
|
||||
*/
|
||||
function injectEffortFrontmatter(content, effortValue) {
|
||||
// Detect the dominant EOL from the first line (the opening `---`).
|
||||
// If the very first `---` is followed by \r\n, treat the whole file as CRLF.
|
||||
const eol = /^---\r\n/.test(content) ? '\r\n' : '\n';
|
||||
|
||||
// Build a frontmatter-matching regex that tolerates an optional \r before
|
||||
// each \n, so we handle both LF and CRLF files without needing to normalise
|
||||
// the whole content.
|
||||
//
|
||||
// Breakdown:
|
||||
// ^---\r?\n — opening delimiter (with optional \r)
|
||||
// ([\s\S]*?) — frontmatter body (non-greedy)
|
||||
// ^---\r?$ — closing delimiter line (optional \r, $ before \n in
|
||||
// multiline mode)
|
||||
// (\r?\n|$) — newline after closing --- (or end of string)
|
||||
//
|
||||
// The `m` flag makes ^ / $ match at every line boundary.
|
||||
const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
|
||||
const match = fmRe.exec(content);
|
||||
if (!match) return content; // no YAML frontmatter — leave unchanged
|
||||
|
||||
// Idempotency guard: don't insert a second effort: line.
|
||||
const fmBody = match[1]; // content between the two `---` lines
|
||||
if (/^effort:/m.test(fmBody)) return content;
|
||||
|
||||
// Locate the exact position of the closing `---` line so we can insert
|
||||
// before it using a simple string splice (avoids re-running the regex and
|
||||
// avoids any edge-cases with $ matching \r differently per engine).
|
||||
const closeIdx = match.index + 4 + fmBody.length; // 4 = len("---\n") (opening)
|
||||
// Actually compute based on the full match start + captured group length:
|
||||
// match[0] = full frontmatter block; match.index = start of that block.
|
||||
// The closing `---` starts at: match.index + ("---" + eol).length + fmBody.length
|
||||
const openLen = 3 + eol.length; // "---" + eol
|
||||
const closingStart = match.index + openLen + fmBody.length;
|
||||
|
||||
const before = content.slice(0, closingStart);
|
||||
const after = content.slice(closingStart);
|
||||
return `${before}effort: ${effortValue}${eol}${after}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* #2517 — Read a single GSD config file (defaults.json or per-project
|
||||
* config.json) into a plain object, returning null on missing/empty files
|
||||
@@ -2691,8 +2923,14 @@ purpose: ${toSingleLine(description)}
|
||||
* Generate a per-agent .toml config file for Codex.
|
||||
* Sets required agent metadata, sandbox_mode, and developer_instructions
|
||||
* from the agent markdown content.
|
||||
*
|
||||
* @param {string} agentName
|
||||
* @param {string} agentContent
|
||||
* @param {object|null} modelOverrides
|
||||
* @param {object|null} runtimeResolver — runtime-aware tier resolver from readGsdRuntimeProfileResolver
|
||||
* @param {object|null} effortCfg — #443: merged effort config from readGsdEffectiveEffortConfig
|
||||
*/
|
||||
function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, runtimeResolver = null) {
|
||||
function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, runtimeResolver = null, effortCfg = null) {
|
||||
const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only';
|
||||
const { frontmatter, body } = extractFrontmatterAndBody(agentContent);
|
||||
const frontmatterText = frontmatter || '';
|
||||
@@ -2721,12 +2959,20 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null,
|
||||
const entry = runtimeResolver.resolve(resolvedName) || runtimeResolver.resolve(agentName);
|
||||
if (entry?.model) {
|
||||
lines.push(`model = ${JSON.stringify(entry.model)}`);
|
||||
if (entry.reasoning_effort) {
|
||||
lines.push(`model_reasoning_effort = ${JSON.stringify(entry.reasoning_effort)}`);
|
||||
}
|
||||
// model is resolved here; reasoning_effort from catalog tier is REPLACED by the
|
||||
// unified effort resolver below (#443). Do NOT emit entry.reasoning_effort here.
|
||||
}
|
||||
}
|
||||
|
||||
// #443 — Unified effort for Codex .toml. Uses the same config-driven precedence chain
|
||||
// as the Claude .md effort injection (resolveInstallTimeEffort), so both runtimes read
|
||||
// from the same effort.agent_overrides / effort.routing_tier_defaults / effort.default
|
||||
// config source. Codex does not support 'max' → clamped to 'xhigh' by
|
||||
// gsdRenderEffortForRuntime('codex', ...).
|
||||
const _universalEffortCodex = resolveInstallTimeEffort(effortCfg, resolvedName !== agentName ? resolvedName : agentName);
|
||||
const _renderedEffortCodex = _getGsdEffortCatalog().renderEffortForRuntime('codex', _universalEffortCodex).value;
|
||||
lines.push(`model_reasoning_effort = ${JSON.stringify(_renderedEffortCodex)}`);
|
||||
|
||||
// Agent prompts contain raw backslashes in regexes and shell snippets.
|
||||
// TOML literal multiline strings preserve them without escape parsing.
|
||||
lines.push(`developer_instructions = '''`);
|
||||
@@ -4992,7 +5238,10 @@ function installCodexConfig(targetDir, agentsSrc) {
|
||||
// setting runtime in the project config reaches the Codex emit path is
|
||||
// false (review finding #1).
|
||||
const runtimeResolver = readGsdRuntimeProfileResolver(targetDir);
|
||||
const tomlContent = generateCodexAgentToml(name, content, modelOverrides, runtimeResolver);
|
||||
// #443 — pass unified effort config so model_reasoning_effort in the .toml
|
||||
// follows the same config-driven precedence as the Claude .md effort key.
|
||||
const effortCfg = readGsdEffectiveEffortConfig(targetDir);
|
||||
const tomlContent = generateCodexAgentToml(name, content, modelOverrides, runtimeResolver, effortCfg);
|
||||
fs.writeFileSync(path.join(agentsTomlDir, `${name}.toml`), tomlContent);
|
||||
}
|
||||
|
||||
@@ -8686,6 +8935,20 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
|
||||
content = content.replace(/\.claude\//g, '.hermes/');
|
||||
}
|
||||
// #443 — Inject `effort:` into the Claude .md frontmatter ONLY.
|
||||
// Gemini/OpenCode/Qwen/Hermes also produce .md files but break on
|
||||
// unknown frontmatter keys (the repo bans skills:/permissionMode: for
|
||||
// the same reason — see tests/agent-frontmatter.test.cjs).
|
||||
// Claude Code reads per-subagent `effort:` frontmatter (anthropics/claude-code #31536).
|
||||
// Injection is per-runtime at install time because the canonical source
|
||||
// agents/*.md must stay Gemini-safe (no effort: key in source).
|
||||
if (runtime === 'claude') {
|
||||
const _effortCfg = readGsdEffectiveEffortConfig(targetDir);
|
||||
const _agentName = entry.name.replace(/\.md$/, '');
|
||||
const _universalEffort = resolveInstallTimeEffort(_effortCfg, _agentName);
|
||||
const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime('claude', _universalEffort).value;
|
||||
content = injectEffortFrontmatter(content, _renderedEffort);
|
||||
}
|
||||
// #3677 — normalize retired `/gsd:<cmd>` colon refs in the agent body
|
||||
// to the canonical hyphen form `/gsd-<cmd>` for hyphen-`name:`
|
||||
// runtimes (claude / qwen / hermes). Self-converting runtimes and
|
||||
@@ -11419,6 +11682,11 @@ module.exports = {
|
||||
installCodexConfig,
|
||||
readGsdRuntimeProfileResolver,
|
||||
readGsdEffectiveModelOverrides,
|
||||
readGsdEffectiveEffortConfig,
|
||||
resolveInstallTimeEffort,
|
||||
injectEffortFrontmatter,
|
||||
get _GSD_EFFORT_MANIFEST_TIER_DEFAULTS() { return _getGsdEffortCatalog().EFFORT_MANIFEST_TIER_DEFAULTS; },
|
||||
get _GSD_EFFORT_MANIFEST_DEFAULT() { return _getGsdEffortCatalog().EFFORT_MANIFEST_DEFAULT; },
|
||||
install,
|
||||
installAllRuntimes,
|
||||
uninstall,
|
||||
|
||||
Reference in New Issue
Block a user