fix(#3533): effort inherit — expressible, omitted at writers, never re-added (#3541)

This commit is contained in:
Tom Boucher
2026-08-15 07:00:44 -04:00
committed by GitHub
parent 500df8e37f
commit 50d5368add
10 changed files with 284 additions and 8 deletions

View File

@@ -106,7 +106,8 @@ interface CommitToSubrepoRepoResult {
interface EffortSyncChange {
agent: string;
from: string | null;
to: string;
// #3533 (10d): to === null is the typed IR for omission (inherit strips the key).
to: string | null;
}
// ─── Phase Status ─────────────────────────────────────────────────────────────
@@ -744,6 +745,28 @@ function setEffortFrontmatter(content: string, effortValue: string): string {
return content.slice(0, closingStart) + `effort: ${effortValue}${eol}` + content.slice(closingStart);
}
/**
* #3533 (10d) — remove exactly the frontmatter `effort:` line (and its line
* ending) so an agent configured for `inherit` carries NO key. Mirrors the
* codex-agent-toml strip discipline: targeted line removal, EOL-aware, every
* other byte (comments, sibling keys, the body) untouched.
*/
function removeEffortFrontmatter(content: string): string {
// Scoped to the FIRST frontmatter block (not a whole-file /m match): a
// preamble or body line starting with `effort:` (a fenced config example,
// a thematic-break flanked fragment) must never be the line removed.
const fmRe = /^---\r?\n([\s\S]*?)^---\r?$/m;
const match = fmRe.exec(content);
if (!match) return content;
const fmBody = match[1];
const lineRe = /^effort:[ \t]*.*\r?\n?/m;
if (!lineRe.test(fmBody)) return content;
const strippedFm = fmBody.replace(lineRe, '');
const openLen = 3 + (/^---\r\n/.test(content) ? 2 : 1);
const closingStart = match.index + openLen + fmBody.length;
return content.slice(0, match.index + openLen) + strippedFm + content.slice(closingStart);
}
/**
* #488 — Re-sync effort: frontmatter in all installed gsd-*.md agent files to
* match the current effort config, without requiring a full reinstall.
@@ -811,6 +834,25 @@ function cmdEffortSync(cwd: string, raw: boolean, opts?: { dryRun?: boolean; con
// Resolve using install-time logic: home defaults merged with project config.
const universalEffort = resolveInstallTimeEffort(effortCfg, agentName);
// #3533 (10d): 'inherit' means the key must NOT exist. An absent key is
// the CORRECT state (in sync, skipped) — before #3533 absence read as null
// drift and the sync re-added a hand-stripped key on every apply. A
// present key under inherit is stripped, reported as {from, to: null}.
if (universalEffort === 'inherit') {
// eslint-disable-next-line local/no-unbounded-quantifier -- same lazy `*?` bounded by the `^---$/m` closing anchor as the concrete-path fmMatch below; duplicated here so the inherit branch validates against the same frontmatter span the strip targets
const fmMatchInherit = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(content);
if (!fmMatchInherit) { skipped++; continue; }
const effortMatchInherit = /^effort:[ \t]*(.+?)[ \t]*$/m.exec(fmMatchInherit[1]);
if (!effortMatchInherit) { skipped++; continue; }
changes.push({ agent: agentName, from: effortMatchInherit[1], to: null });
synced++;
if (!dryRun) {
fs.writeFileSync(filePath, removeEffortFrontmatter(content));
}
continue;
}
const rendered = renderEffortForRuntime(runtime, universalEffort);
const newEffortValue = rendered.value;

View File

@@ -325,6 +325,13 @@ export function renderEffortArgv(
* Render a universal effort string for a specific runtime.
*/
export function renderEffortForRuntime(runtime: string, universalEffort: string): RenderedEffort {
// #3533 (10d): 'inherit' is not a wire level on ANY runtime — it means
// "omit the key / pass no argument and follow the session/host default".
// Renderers must never emit it as a literal; null param/channel tells
// resolve-execution consumers there is no propagation.
if (universalEffort === 'inherit') {
return { value: 'inherit', param: null, channel: null };
}
const spec = EFFORT_RENDERING[runtime];
if (!spec) {
return { value: universalEffort, param: null, channel: null };

View File

@@ -744,7 +744,12 @@ function resolveProviderEscalation(
// ─── #443 — Unified effort + fast_mode resolvers ─────────────────────────────
const VALID_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
const EFFORT_SET = new Set(VALID_EFFORTS);
// #3533 (10d): the VOCABULARY carries one more member than the LADDER —
// 'inherit' is a declarable effort choice ("follow the session", expressed by
// OMITTING the effort key at the writer) but not a level nextEffort may step
// into. Keeping it out of VALID_EFFORTS means escalation (resolveEffortForTier)
// never walks past an explicit inherit: nextEffort('inherit') is null.
const EFFORT_SET = new Set([...VALID_EFFORTS, 'inherit']);
/**
* Walk one step up the effort ladder from `e`.